Squash the current repository state back into one baseline commit while preserving the README reframing and repository contents. Constraint: User explicitly requested a single squashed commit with subject "asdf" Confidence: high Scope-risk: broad Reversibility: clean Directive: This commit intentionally rewrites published history; coordinate before future force-pushes Tested: git status clean; local history rewritten to one commit; force-pushed main to origin and instructkr Not-tested: Fresh clone verification after push
142 lines
4.1 KiB
TypeScript
142 lines
4.1 KiB
TypeScript
import {
|
|
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
logEvent,
|
|
} from '../services/analytics/index.js'
|
|
import { logForDebugging } from '../utils/debug.js'
|
|
import { errorMessage } from '../utils/errors.js'
|
|
import { jsonStringify } from '../utils/slowOperations.js'
|
|
|
|
const DEBUG_MSG_LIMIT = 2000
|
|
|
|
const SECRET_FIELD_NAMES = [
|
|
'session_ingress_token',
|
|
'environment_secret',
|
|
'access_token',
|
|
'secret',
|
|
'token',
|
|
]
|
|
|
|
const SECRET_PATTERN = new RegExp(
|
|
`"(${SECRET_FIELD_NAMES.join('|')})"\\s*:\\s*"([^"]*)"`,
|
|
'g',
|
|
)
|
|
|
|
const REDACT_MIN_LENGTH = 16
|
|
|
|
export function redactSecrets(s: string): string {
|
|
return s.replace(SECRET_PATTERN, (_match, field: string, value: string) => {
|
|
if (value.length < REDACT_MIN_LENGTH) {
|
|
return `"${field}":"[REDACTED]"`
|
|
}
|
|
const redacted = `${value.slice(0, 8)}...${value.slice(-4)}`
|
|
return `"${field}":"${redacted}"`
|
|
})
|
|
}
|
|
|
|
/** Truncate a string for debug logging, collapsing newlines. */
|
|
export function debugTruncate(s: string): string {
|
|
const flat = s.replace(/\n/g, '\\n')
|
|
if (flat.length <= DEBUG_MSG_LIMIT) {
|
|
return flat
|
|
}
|
|
return flat.slice(0, DEBUG_MSG_LIMIT) + `... (${flat.length} chars)`
|
|
}
|
|
|
|
/** Truncate a JSON-serializable value for debug logging. */
|
|
export function debugBody(data: unknown): string {
|
|
const raw = typeof data === 'string' ? data : jsonStringify(data)
|
|
const s = redactSecrets(raw)
|
|
if (s.length <= DEBUG_MSG_LIMIT) {
|
|
return s
|
|
}
|
|
return s.slice(0, DEBUG_MSG_LIMIT) + `... (${s.length} chars)`
|
|
}
|
|
|
|
/**
|
|
* Extract a descriptive error message from an axios error (or any error).
|
|
* For HTTP errors, appends the server's response body message if available,
|
|
* since axios's default message only includes the status code.
|
|
*/
|
|
export function describeAxiosError(err: unknown): string {
|
|
const msg = errorMessage(err)
|
|
if (err && typeof err === 'object' && 'response' in err) {
|
|
const response = (err as { response?: { data?: unknown } }).response
|
|
if (response?.data && typeof response.data === 'object') {
|
|
const data = response.data as Record<string, unknown>
|
|
const detail =
|
|
typeof data.message === 'string'
|
|
? data.message
|
|
: typeof data.error === 'object' &&
|
|
data.error &&
|
|
'message' in data.error &&
|
|
typeof (data.error as Record<string, unknown>).message ===
|
|
'string'
|
|
? (data.error as Record<string, unknown>).message
|
|
: undefined
|
|
if (detail) {
|
|
return `${msg}: ${detail}`
|
|
}
|
|
}
|
|
}
|
|
return msg
|
|
}
|
|
|
|
/**
|
|
* Extract the HTTP status code from an axios error, if present.
|
|
* Returns undefined for non-HTTP errors (e.g. network failures).
|
|
*/
|
|
export function extractHttpStatus(err: unknown): number | undefined {
|
|
if (
|
|
err &&
|
|
typeof err === 'object' &&
|
|
'response' in err &&
|
|
(err as { response?: { status?: unknown } }).response &&
|
|
typeof (err as { response: { status?: unknown } }).response.status ===
|
|
'number'
|
|
) {
|
|
return (err as { response: { status: number } }).response.status
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
/**
|
|
* Pull a human-readable message out of an API error response body.
|
|
* Checks `data.message` first, then `data.error.message`.
|
|
*/
|
|
export function extractErrorDetail(data: unknown): string | undefined {
|
|
if (!data || typeof data !== 'object') return undefined
|
|
if ('message' in data && typeof data.message === 'string') {
|
|
return data.message
|
|
}
|
|
if (
|
|
'error' in data &&
|
|
data.error !== null &&
|
|
typeof data.error === 'object' &&
|
|
'message' in data.error &&
|
|
typeof data.error.message === 'string'
|
|
) {
|
|
return data.error.message
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
/**
|
|
* Log a bridge init skip — debug message + `tengu_bridge_repl_skipped`
|
|
* analytics event. Centralizes the event name and the AnalyticsMetadata
|
|
* cast so call sites don't each repeat the 5-line boilerplate.
|
|
*/
|
|
export function logBridgeSkip(
|
|
reason: string,
|
|
debugMsg?: string,
|
|
v2?: boolean,
|
|
): void {
|
|
if (debugMsg) {
|
|
logForDebugging(debugMsg)
|
|
}
|
|
logEvent('tengu_bridge_repl_skipped', {
|
|
reason:
|
|
reason as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
...(v2 !== undefined && { v2 }),
|
|
})
|
|
}
|