@rebasepro/server 0.25.0 → 0.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{GCSStorageController-C7ARrDAp.js → GCSStorageController-CRd9bFrc.js} +3 -3
- package/dist/{GCSStorageController-C7ARrDAp.js.map → GCSStorageController-CRd9bFrc.js.map} +1 -1
- package/dist/{S3StorageController-qevJ0pVE.js → S3StorageController-D00EFa5P.js} +3 -3
- package/dist/{S3StorageController-qevJ0pVE.js.map → S3StorageController-D00EFa5P.js.map} +1 -1
- package/dist/api/closed-database.d.ts +25 -0
- package/dist/{auth-CteGWxsL.js → auth-Dkvo2tTJ.js} +26 -18
- package/dist/auth-Dkvo2tTJ.js.map +1 -0
- package/dist/backend/boot-announcements.d.ts +13 -0
- package/dist/backend/create-backend.d.ts +4 -0
- package/dist/{backup-Dq0lyJ_D.js → backup-B9m3cb3X.js} +7 -7
- package/dist/{backup-Dq0lyJ_D.js.map → backup-B9m3cb3X.js.map} +1 -1
- package/dist/{contract-routes-DiFwiLg2.js → contract-routes-CsFXW7j4.js} +2 -2
- package/dist/{contract-routes-DiFwiLg2.js.map → contract-routes-CsFXW7j4.js.map} +1 -1
- package/dist/{cron-loader-NHqRfKF0.js → cron-loader-Dj6lA4HQ.js} +2 -2
- package/dist/{cron-loader-NHqRfKF0.js.map → cron-loader-Dj6lA4HQ.js.map} +1 -1
- package/dist/{cron-routes-Dhf82HzT.js → cron-routes-CwbLZguO.js} +5 -5
- package/dist/{cron-routes-Dhf82HzT.js.map → cron-routes-CwbLZguO.js.map} +1 -1
- package/dist/{cron-scheduler-D1DT0nw5.js → cron-scheduler-B04ACoUE.js} +2 -2
- package/dist/{cron-scheduler-D1DT0nw5.js.map → cron-scheduler-B04ACoUE.js.map} +1 -1
- package/dist/{cron-store-DEpVstUU.js → cron-store-DkyfuLOH.js} +3 -3
- package/dist/{cron-store-DEpVstUU.js.map → cron-store-DkyfuLOH.js.map} +1 -1
- package/dist/{ddl-bootstrap-BNqi2NZe.js → ddl-bootstrap-Ck20GSIX.js} +2 -2
- package/dist/{ddl-bootstrap-BNqi2NZe.js.map → ddl-bootstrap-Ck20GSIX.js.map} +1 -1
- package/dist/{errors-_ytoGi8A.js → errors-OMWkKSH-.js} +66 -5
- package/dist/errors-OMWkKSH-.js.map +1 -0
- package/dist/{function-loader-DbCSoNJj.js → function-loader-KKDqTcIr.js} +2 -2
- package/dist/{function-loader-DbCSoNJj.js.map → function-loader-KKDqTcIr.js.map} +1 -1
- package/dist/{function-routes-DNV4RUF0.js → function-routes-RxrCHNuM.js} +2 -2
- package/dist/{function-routes-DNV4RUF0.js.map → function-routes-RxrCHNuM.js.map} +1 -1
- package/dist/functions/index.js +18 -1
- package/dist/functions/index.js.map +1 -1
- package/dist/{history-recorder-RZ1Ow4YY.js → history-recorder-B2rkuhuy.js} +2 -2
- package/dist/{history-recorder-RZ1Ow4YY.js.map → history-recorder-B2rkuhuy.js.map} +1 -1
- package/dist/index.es.js +529 -113
- package/dist/index.es.js.map +1 -1
- package/dist/{jobs-8G-_agld.js → jobs-h1WRq3cp.js} +3 -3
- package/dist/{jobs-8G-_agld.js.map → jobs-h1WRq3cp.js.map} +1 -1
- package/dist/{jwt-YhciR93H.js → jwt-D3oFauZI.js} +2 -2
- package/dist/{jwt-YhciR93H.js.map → jwt-D3oFauZI.js.map} +1 -1
- package/dist/{logger-Bt32sHFw.js → logger-CW8pmTS6.js} +35 -3
- package/dist/logger-CW8pmTS6.js.map +1 -0
- package/dist/{logs-routes-CelTiS7C.js → logs-routes-CBt7mMIL.js} +3 -3
- package/dist/{logs-routes-CelTiS7C.js.map → logs-routes-CBt7mMIL.js.map} +1 -1
- package/dist/{openapi-generator-BamY6-ga.js → openapi-generator-BjHr6upl.js} +2 -2
- package/dist/{openapi-generator-BamY6-ga.js.map → openapi-generator-BjHr6upl.js.map} +1 -1
- package/dist/{proxy-BEKbsil6.js → proxy-CH8Jsovg.js} +2 -2
- package/dist/{proxy-BEKbsil6.js.map → proxy-CH8Jsovg.js.map} +1 -1
- package/dist/{query-parser-CGohjyWI.js → query-parser-otFZqJsC.js} +3 -3
- package/dist/{query-parser-CGohjyWI.js.map → query-parser-otFZqJsC.js.map} +1 -1
- package/dist/{request-timeout-CRny5KD0.js → request-timeout-59crS-9u.js} +3 -3
- package/dist/{request-timeout-CRny5KD0.js.map → request-timeout-59crS-9u.js.map} +1 -1
- package/dist/{schema-editor-routes-CnNGCayU.js → schema-editor-routes-Bsut0bCC.js} +2 -2
- package/dist/{schema-editor-routes-CnNGCayU.js.map → schema-editor-routes-Bsut0bCC.js.map} +1 -1
- package/dist/{soft-delete-params-BAgpmBvp.js → soft-delete-params-BmlVwecC.js} +2 -2
- package/dist/{soft-delete-params-BAgpmBvp.js.map → soft-delete-params-BmlVwecC.js.map} +1 -1
- package/dist/{sql-export-routes-BgTJ7K3S.js → sql-export-routes-DmB_uZ4l.js} +3 -3
- package/dist/{sql-export-routes-BgTJ7K3S.js.map → sql-export-routes-DmB_uZ4l.js.map} +1 -1
- package/dist/{upload-validation-I9cCUBIW.js → upload-validation-Bgo626uY.js} +2 -2
- package/dist/{upload-validation-I9cCUBIW.js.map → upload-validation-Bgo626uY.js.map} +1 -1
- package/dist/utils/dev-dependency-command.d.ts +15 -0
- package/dist/utils/dev-port.d.ts +8 -0
- package/dist/utils/logger.d.ts +24 -0
- package/package.json +5 -5
- package/dist/auth-CteGWxsL.js.map +0 -1
- package/dist/errors-_ytoGi8A.js.map +0 -1
- package/dist/logger-Bt32sHFw.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"errors-_ytoGi8A.js","names":[],"sources":["../src/api/errors.ts"],"sourcesContent":["import type { Context, ErrorHandler } from \"hono\";\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\";\nimport type { HonoEnv } from \"./types\";\nimport { logger } from \"../utils/logger\";\nimport { hostEnv } from \"../utils/host\";\n\n/**\n * A stale caller's schema stamp, as the cause of the error it explains.\n *\n * `createSchemaDriftDetector` puts the two stamps on the context when a request\n * carries an `x-rebase-schema` older than this backend's. It lives next door;\n * this half is here because `errors.ts` is in the graph of\n * `@rebasepro/server/functions` and may not reach `@rebasepro/types` at runtime.\n */\nfunction schemaDriftCause(drift: { client: string; server: string } | undefined): {\n code: string;\n message: string;\n clientSchema: string;\n serverSchema: string;\n} | undefined {\n if (!drift) return undefined;\n return {\n code: \"SCHEMA_DRIFT\",\n message:\n `This client was generated against schema ${drift.client}; this backend serves `\n + `${drift.server}. If the field named above was renamed or removed, regenerate the `\n + \"SDK (`rebase generate-sdk`) and rebuild.\",\n clientSchema: drift.client,\n serverSchema: drift.server\n };\n}\n\n/** Tracks whether we've already shown the doctor hint (once per process). */\nlet _schemaDriftHinted = false;\n\n/**\n * The schema-drift remedy, in the words that work on *this* database.\n *\n * On the managed development database boot applies additive changes, so\n * restarting `rebase dev` is the first fix there, and `rebase db push` — which\n * plans against a throwaway database of its own — applies what boot leaves\n * alone. Elsewhere the push or the migrations are the fix, and a deployed\n * runtime applies the schema at boot, so the remedy has to know which database\n * is under this run.\n *\n * `REBASE_DEV_DATABASE_KIND` is set by the CLI from the database it resolved,\n * the same variable `rebase schema generate`'s closing line already branches\n * on. Absent — a deployed backend, a container, anything not started by\n * `rebase dev` — the answer is the general one.\n */\nexport function schemaDriftRemedy(): { short: string; lines: string[] } {\n if (hostEnv().REBASE_DEV_DATABASE_KIND === \"managed\") {\n return {\n short: \"Restart `rebase dev` — boot applies additive schema changes to the managed database — \" +\n \"or run `rebase db push` for a change boot leaves alone.\",\n lines: [\n \" Quick fixes (managed development database):\",\n \" restart `rebase dev` boot applies additive changes\",\n \" rebase db push applies what boot leaves alone\",\n \" (a drop or a retype; it asks first)\",\n \" rebase doctor full 3-way drift report\"\n ]\n };\n }\n\n return {\n short: \"Run `rebase db push` to sync your schema, or `rebase db migrate` to apply pending migrations.\",\n lines: [\n \" Quick fixes (local dev, against DATABASE_URL):\",\n \" rebase db push sync schema to database (dev)\",\n \" rebase db migrate apply pending migrations (prod)\",\n \" rebase doctor full 3-way drift report\",\n \"\",\n \" Managed cloud: the runtime applies schema + RLS at boot\",\n \" (REBASE_MIGRATE_ON_BOOT); redeploy rather than db push,\",\n \" which cannot reach the tenant database.\"\n ]\n };\n}\n\n/** Shape of Postgres / network errors with diagnostic codes */\ninterface PgLikeError {\n code?: string;\n address?: string;\n port?: number;\n message?: string;\n table?: string;\n column?: string;\n schema?: string;\n detail?: string;\n hint?: string;\n constraint?: string;\n}\n\n/** 5-character SQLSTATE, e.g. `42501`, `23505`. */\nconst SQLSTATE_RE = /^[0-9A-Z]{5}$/;\n\n/**\n * What SQLSTATE 25006 means here, in the only terms that help the author fix it.\n *\n * Every request-scoped read runs `withTransaction(..., { accessMode: \"read\n * only\" })`, so a write attempted anywhere under it — including from a\n * `context.data` call inside an `afterRead` callback — is refused by Postgres\n * rather than by us. The callback name is in the message because that is the\n * file the reader has to open, and nothing else on a read path can raise this.\n */\nconst READ_ONLY_TRANSACTION_MESSAGE =\n \"An `afterRead` callback tried to write. Request-scoped reads run in a READ ONLY \" +\n \"transaction, so neither the callback nor anything it calls (context.data included) \" +\n \"may write. Move the write outside the read: enqueue a background job, or use \" +\n \"`rebase.dataAsAdmin` from a job or a custom function.\";\n\n/**\n * Walk the cause chain for the underlying database error, identified by a\n * 5-char SQLSTATE `code`. Drizzle wraps the pg error in `.cause`, and route\n * code sometimes wraps drizzle again, so the real error may sit several\n * levels down.\n */\nfunction extractDbError(error: unknown, depth = 0): PgLikeError | null {\n if (!error || typeof error !== \"object\" || depth > 8) return null;\n const e = error as PgLikeError & { cause?: unknown };\n if (typeof e.code === \"string\" && SQLSTATE_RE.test(e.code)) return e;\n if (e.cause && typeof e.cause === \"object\") return extractDbError(e.cause, depth + 1);\n return null;\n}\n\n/**\n * Did the database refuse a value as unreadable for its column's type —\n * SQLSTATE class 22, anywhere down the cause chain?\n *\n * For a lookup by an id the caller supplied, that is an answer rather than a\n * failure: `abc` against a UUID key names no row, exactly as a well-formed id\n * nobody has does.\n */\nexport function isDataException(error: unknown): boolean {\n return extractDbError(error)?.code?.startsWith(\"22\") === true;\n}\n\n/**\n * The two ways a request fails before it ever reaches the database, read off\n * the cause chain by pg-pool's own wording (it sets no code on either).\n *\n * - `timeout exceeded when trying to connect` — every pooled connection was\n * busy for the whole of `connectionTimeoutMillis`. pg-pool raises it only\n * from its wait queue, so it means the pool is exhausted: slow queries\n * holding connections, or `DB_POOL_MAX` too small for the load.\n * - `Connection terminated due to connection timeout` — a fresh connection\n * took longer than that to open: the database is slow or far away.\n *\n * Either is load, not a fault. Answered as a 500 they read as a crash and gave\n * a client nothing to back off on; as a 503 with `Retry-After` the SDK's\n * existing retry path takes them (`offline-connectivity.ts`).\n */\nconst POOL_FAILURES: ReadonlyArray<{ message: string; code: \"DB_POOL_EXHAUSTED\" | \"DB_CONNECT_TIMEOUT\" }> = [\n { message: \"timeout exceeded when trying to connect\", code: \"DB_POOL_EXHAUSTED\" },\n { message: \"Connection terminated due to connection timeout\", code: \"DB_CONNECT_TIMEOUT\" }\n];\n\nfunction poolFailureOf(error: unknown, depth = 0): \"DB_POOL_EXHAUSTED\" | \"DB_CONNECT_TIMEOUT\" | undefined {\n if (!error || typeof error !== \"object\" || depth > 8) return undefined;\n const message = Reflect.get(error, \"message\");\n if (typeof message === \"string\") {\n const match = POOL_FAILURES.find(failure => message === failure.message);\n if (match) return match.code;\n }\n return poolFailureOf(Reflect.get(error, \"cause\"), depth + 1);\n}\n\n/** Seconds a client is asked to wait after a pool failure — one pool timeout's worth is too long to make a person wait. */\nconst POOL_RETRY_AFTER_SECONDS = 1;\n\n/**\n * SQLSTATEs that mean the database dropped or refused the connection, not\n * that it judged the statement: class 08 (connection exception), and the\n * three shutdown codes — `57P01` a server shutting down or an administrator\n * ending the session, `57P02` a crash restart, `57P03` a server still\n * starting.\n */\nconst UNREACHABLE_SQLSTATES = new Set([\"57P01\", \"57P02\", \"57P03\"]);\n\n/**\n * What node-postgres says when the socket under a client closed, in the words\n * it sets no code on.\n */\nconst CONNECTION_LOST_MESSAGES = new Set([\n \"Connection terminated unexpectedly\",\n \"Client has encountered a connection error and is not queryable\"\n]);\n\n/** The socket failures Node raises when a host cannot be reached or drops the connection. */\nconst SOCKET_FAILURE_CODES = new Set([\n \"ECONNREFUSED\", \"ECONNRESET\", \"ENOTFOUND\", \"EAI_AGAIN\", \"EHOSTUNREACH\", \"ENETUNREACH\", \"ETIMEDOUT\", \"EPIPE\"\n]);\n\n/**\n * A Node socket failure: one of {@link SOCKET_FAILURE_CODES} on an error the\n * runtime raised for a system call, or the `AggregateError` `net` raises when\n * every address a host resolved to failed that way.\n */\nfunction isSocketFailure(error: object, depth = 0): boolean {\n const code = Reflect.get(error, \"code\");\n if (typeof code === \"string\" && SOCKET_FAILURE_CODES.has(code) && typeof Reflect.get(error, \"syscall\") === \"string\") {\n return true;\n }\n const inner = Reflect.get(error, \"errors\");\n return depth < 2 && Array.isArray(inner) && inner.length > 0 &&\n inner.every(item => typeof item === \"object\" && item !== null && isSocketFailure(item, depth + 1));\n}\n\n/**\n * Did the request fail because the database could not be reached at all — not\n * because it refused the statement, and not because some other service is down?\n *\n * Read off the error the database driver raised, wherever it sits in the cause\n * chain:\n *\n * - a SQLSTATE that means the connection was refused or dropped (class `08`,\n * `57P01`–`57P03`). The database's own word, so it settles the question\n * either way: a SQLSTATE of any other class is the database answering;\n * - node-postgres's own sentence for a socket that closed under a query;\n * - a socket failure (`ECONNREFUSED`, `ENOTFOUND`, …), but only where it can\n * only be the database's: thrown as it is, which is how node-postgres hands\n * back a pool connection it could not open, or as the cause of a failed\n * Drizzle query (the wrapper that carries the statement). Under any other\n * wrapper it belongs to another client: `fetch failed` is a `fetch()` to\n * some other host, and an S3 call that cannot connect carries the AWS SDK's\n * `$metadata`. Answering those \"the database is unreachable\" would send an\n * operator to a database that is fine.\n */\nexport function isDatabaseUnreachable(error: unknown): boolean {\n if (typeof error !== \"object\" || error === null) return false;\n\n const reported = databaseReport(error);\n if (reported !== undefined) return reported;\n\n const socketFailureOf = (candidate: unknown): boolean =>\n typeof candidate === \"object\" && candidate !== null &&\n isSocketFailure(candidate) && Reflect.get(candidate, \"$metadata\") === undefined;\n if (socketFailureOf(error)) return true;\n return typeof Reflect.get(error, \"query\") === \"string\" && socketFailureOf(Reflect.get(error, \"cause\"));\n}\n\n/**\n * What the database itself said about the connection, down the cause chain:\n * `true` for a lost or refused connection, `false` for a SQLSTATE that is the\n * database answering, `undefined` when nothing on the chain is the database's.\n */\nfunction databaseReport(error: object, depth = 0): boolean | undefined {\n const code = Reflect.get(error, \"code\");\n // `EPIPE` has a SQLSTATE's shape, and is a socket's.\n if (typeof code === \"string\" && SQLSTATE_RE.test(code) && !SOCKET_FAILURE_CODES.has(code)) {\n return code.startsWith(\"08\") || UNREACHABLE_SQLSTATES.has(code);\n }\n const message = Reflect.get(error, \"message\");\n if (typeof message === \"string\" && CONNECTION_LOST_MESSAGES.has(message)) return true;\n const cause = Reflect.get(error, \"cause\");\n return depth < 8 && typeof cause === \"object\" && cause !== null ? databaseReport(cause, depth + 1) : undefined;\n}\n\n/**\n * The operator's half of a connection that failed — to the database, or to a\n * service a request called — in a few words: the socket failure and the host\n * it names (`ECONNREFUSED at 127.0.0.1:5432`), or what the database or its\n * client said about the connection.\n */\nfunction describeConnectionFailure(error: unknown, depth = 0): string {\n if (typeof error !== \"object\" || error === null || depth > 8) return \"the connection failed\";\n const code = Reflect.get(error, \"code\");\n const message = Reflect.get(error, \"message\");\n if (typeof code === \"string\" && SOCKET_FAILURE_CODES.has(code)) {\n const address = Reflect.get(error, \"address\");\n const port = Reflect.get(error, \"port\");\n const hostname = Reflect.get(error, \"hostname\");\n if (address !== undefined) return `${code} at ${String(address)}${port !== undefined ? `:${String(port)}` : \"\"}`;\n if (typeof hostname === \"string\") return `${code} for ${hostname}`;\n }\n if (typeof message === \"string\" && (CONNECTION_LOST_MESSAGES.has(message) ||\n (typeof code === \"string\" && (code.startsWith(\"08\") || UNREACHABLE_SQLSTATES.has(code))))) {\n return typeof code === \"string\" ? `${message} (${code})` : message;\n }\n const inner = Reflect.get(error, \"errors\");\n if (Array.isArray(inner) && inner.length > 0) return describeConnectionFailure(inner[0], depth + 1);\n const cause = Reflect.get(error, \"cause\");\n if (typeof cause === \"object\" && cause !== null) return describeConnectionFailure(cause, depth + 1);\n return typeof code === \"string\" ? code : \"the connection failed\";\n}\n\n/**\n * Is there a Node socket failure under this error, anywhere down its cause\n * chain?\n *\n * Asked only once {@link isDatabaseUnreachable} has said no, so what it finds\n * belongs to another service: a `fetch()` to some other host (`fetch failed`,\n * its `cause` the socket's error), an S3 call that cannot connect (the AWS SDK\n * hands back the socket's own error with `$metadata` on it), or anything else\n * wrapping one. An error that only carries a socket code, with no system call\n * behind it, is not one.\n */\nfunction hasUpstreamSocketFailure(error: unknown, depth = 0): boolean {\n if (typeof error !== \"object\" || error === null || depth > 8) return false;\n return isSocketFailure(error) || hasUpstreamSocketFailure(Reflect.get(error, \"cause\"), depth + 1);\n}\n\n/** What a caller is told when a service the request needs cannot be reached. The host stays in the log. */\nconst UPSTREAM_UNAVAILABLE_MESSAGE =\n \"A service this request depends on could not be reached, so it was not served. Retry shortly.\";\n\n/** What a caller is told when the database cannot be reached. Retryable, and not about them. */\nexport const DATABASE_UNAVAILABLE_MESSAGE =\n \"The database could not be reached, so this request was not served. \" +\n \"Retry shortly; if it keeps happening, the server's database is down or unreachable from it.\";\n\n/** Seconds a client is asked to wait before retrying a request the database could not be reached for. */\nconst DATABASE_RETRY_AFTER_SECONDS = 5;\n\n/** The operator's line for an unreachable database, at most once a minute — see {@link warnPoolFailure}. */\nlet lastUnreachableWarningAt = 0;\n\nfunction warnDatabaseUnreachable(detail: string): void {\n const now = Date.now();\n if (now - lastUnreachableWarningAt < POOL_WARNING_INTERVAL_MS) return;\n lastUnreachableWarningAt = now;\n logger.error(\n `❌ The database is unreachable (${detail}), so requests that need it are answered 503 DATABASE_UNAVAILABLE. ` +\n \"Check that it is running and that this server can reach it; requests recover on their own once it is back. \" +\n \"Not repeated for a minute.\"\n );\n}\n\n/**\n * The operator's line, at most once a minute. A pool that is exhausted is\n * exhausted for every request at once, and a line per request buries the one\n * thing to do about it.\n */\nconst POOL_WARNING_INTERVAL_MS = 60_000;\nlet lastPoolWarningAt = 0;\n\nfunction warnPoolFailure(code: \"DB_POOL_EXHAUSTED\" | \"DB_CONNECT_TIMEOUT\"): void {\n const now = Date.now();\n if (now - lastPoolWarningAt < POOL_WARNING_INTERVAL_MS) return;\n lastPoolWarningAt = now;\n logger.warn(code === \"DB_POOL_EXHAUSTED\"\n ? \"⚠️ Database pool exhausted: every connection stayed busy for DB_POOL_CONNECT_TIMEOUT, so requests \" +\n \"are answered 503. Raise DB_POOL_MAX (default 20; DB_POOL_MAX__<KEY> for another data source), or \" +\n \"find the slow queries or long transactions holding connections. Not repeated for a minute.\"\n : \"⚠️ Opening a database connection took longer than DB_POOL_CONNECT_TIMEOUT, so requests are \" +\n \"answered 503. The database is slow to accept connections or far away. Not repeated for a minute.\");\n}\n\n/**\n * Extract the missing table or column name from a PG error.\n * PG 42P01 messages look like: 'relation \"my_table\" does not exist'\n * PG 42703 messages look like: 'column \"my_col\" does not exist' or 'column my_table.my_col does not exist'\n */\nfunction extractMissingIdentifier(pgMessage?: string): string | null {\n if (!pgMessage) return null;\n // Match quoted identifier: relation \"xxx\" / column \"xxx\"\n const quoted = pgMessage.match(/(?:relation|column|table)\\s+\"([^\"]+)\"/i);\n if (quoted) return quoted[1];\n // Match unquoted: column table.col does not exist\n const unquoted = pgMessage.match(/(?:relation|column|table)\\s+([\\w.]+)\\s+does not exist/i);\n if (unquoted) return unquoted[1];\n return null;\n}\n\n/**\n * The sentence a `INVALID_FILTER_VALUE` answer carries.\n *\n * Built from Postgres's own wording rather than passed through, because the\n * driver hands the whole failed statement over as `error.message` and this one\n * goes to the caller in production too. What is quoted back is the type name\n * and the literal the caller themselves sent — never a table, a column list or\n * a statement.\n *\n * The three shapes are the ones a filter actually produces: an unparseable\n * literal (22P02, `?id=eq.abc`), a value outside an enum's labels (22P02 with\n * different wording), and a number past the column type's range (22003). A\n * fourth SQLSTATE in class 22 lands on the general sentence, which still says\n * the useful thing: it is the value that is wrong, not the server.\n */\nfunction describeDataException(dbError?: PgLikeError): string {\n const message = dbError?.message ?? \"\";\n const column = dbError?.column ? ` for column \"${dbError.column}\"` : \"\";\n\n const syntax = message.match(/invalid input syntax for type ([\\w ]+): \"(.*)\"/);\n if (syntax) return `\"${syntax[2]}\" is not a valid ${syntax[1]}${column}.`;\n\n const enumValue = message.match(/invalid input value for enum ([\\w.\"]+): \"(.*)\"/);\n if (enumValue) return `\"${enumValue[2]}\" is not one of the values of ${enumValue[1]}${column}.`;\n\n const range = message.match(/value \"(.*)\" is out of range for type ([\\w ]+)/);\n if (range) return `\"${range[1]}\" is out of range for ${range[2]}${column}.`;\n\n return `A value in this request could not be read as the type of the column it was compared against${column}.`;\n}\n\n/**\n * Standardized API error class.\n * Throw this from any route handler — the errorHandler middleware\n * will format it into `{ error: { message, code, details? } }`.\n */\nexport class ApiError extends Error {\n public readonly statusCode: number;\n public readonly code: string;\n public readonly details?: unknown;\n /**\n * Whether this outcome is a routine part of normal operation rather than\n * something an operator should look at. Expected errors log at debug; every\n * other operational error logs at warn.\n *\n * The motivating case is `POST /auth/refresh` with no session: clients\n * refresh on page load before they know whether one exists, so every\n * anonymous page view is a 401 — correct, and not worth a warning line.\n *\n * The other class is a caller-caused 4xx that never reached the database: a\n * mistyped filter operator, sort direction or limit, a request for a\n * collection that does not exist. Nothing on this server is wrong, and the\n * response body has already told the caller what to fix — while one client\n * holding a stale name would otherwise write a warning per request, forever,\n * until the level means nothing. See `api/rest/query-parser.ts`.\n *\n * What stays at warn is anything that says something about the *server*:\n * a schema that has drifted from the code, a permission the database\n * refused, a dependency that failed. Those are 4xx too, and they are still\n * incidents.\n */\n public readonly expected: boolean;\n\n constructor(statusCode: number, code: string, message: string, details?: unknown, expected = false) {\n super(message);\n this.name = \"ApiError\";\n this.statusCode = statusCode;\n this.code = code;\n this.details = details;\n this.expected = expected;\n }\n\n // ── Factory methods ──────────────────────────────────────────────\n\n static badRequest(message: string, code = \"BAD_REQUEST\", details?: unknown): ApiError {\n return new ApiError(400, code, message, details);\n }\n\n static unauthorized(message: string, code = \"UNAUTHORIZED\"): ApiError {\n return new ApiError(401, code, message);\n }\n\n /**\n * A 401 that is a normal outcome, not an incident — logged at debug.\n * See {@link ApiError.expected}.\n */\n static unauthenticated(message: string, code = \"UNAUTHORIZED\"): ApiError {\n return new ApiError(401, code, message, undefined, true);\n }\n\n static forbidden(message: string, code = \"FORBIDDEN\"): ApiError {\n return new ApiError(403, code, message);\n }\n\n static notFound(message: string, code = \"NOT_FOUND\"): ApiError {\n return new ApiError(404, code, message);\n }\n\n /**\n * `details` because a 409 is usually a `23505`, and the one thing the\n * caller needs is *which field* collided. The column name goes in; the\n * value never does — see `pgFieldViolations`.\n */\n static conflict(message: string, code = \"CONFLICT\", details?: unknown): ApiError {\n return new ApiError(409, code, message, details);\n }\n\n static internal(message: string, code = \"INTERNAL_ERROR\"): ApiError {\n return new ApiError(500, code, message);\n }\n\n static serviceUnavailable(message: string, code = \"SERVICE_UNAVAILABLE\"): ApiError {\n return new ApiError(503, code, message);\n }\n\n /**\n * `503 DATABASE_UNAVAILABLE`: the database could not be reached. What\n * every door answers then — the data API, sign-in, the admin routes and\n * the realtime socket — so a client sees one retryable answer, sent with\n * `Retry-After`, rather than a permission failure or a crash.\n *\n * @see isDatabaseUnreachable\n */\n static databaseUnavailable(message = DATABASE_UNAVAILABLE_MESSAGE): ApiError {\n return new ApiError(503, \"DATABASE_UNAVAILABLE\", message);\n }\n}\n\n/**\n * Canonical error response shape:\n * `{ error: { message: string, code: string, details?: unknown } }`\n */\nexport interface ErrorResponse {\n error: {\n message: string;\n code: string;\n details?: unknown;\n /** Request correlation ID for tracing (echoes X-Request-ID). */\n requestId?: string;\n /**\n * Why this request was going to fail whatever it asked for.\n *\n * Only `SCHEMA_DRIFT` today: the caller's `x-rebase-schema` stamp is\n * older than this backend's, so a 400 naming an unknown field is very\n * likely a rename the client has not regenerated for. The error itself\n * is unchanged — this explains it, it does not cause it.\n */\n cause?: {\n code: string;\n message: string;\n clientSchema: string;\n serverSchema: string;\n };\n };\n}\n\n/**\n * General shape of errors that flow through the API error handler.\n * Extends Error with optional HTTP status, error code, and details.\n */\nexport interface RebaseApiError extends Error {\n statusCode?: number;\n code?: string;\n details?: unknown;\n}\n\n/**\n * The answer an error chose for itself, read the same way at every door.\n *\n * @see declaredErrorAnswer\n */\nexport interface DeclaredErrorAnswer {\n /** The HTTP status the error carries. A socket frame has no slot for it. */\n status: number;\n code: string;\n message: string;\n details?: unknown;\n /** See {@link ApiError.expected}: log it at debug, not warn. */\n expected: boolean;\n}\n\n/**\n * The status, code and message an error carries as its own answer — or\n * `undefined` for an error that carries none, which is a server fault and gets\n * masked.\n *\n * Two classes carry one. The server's `ApiError`, and `RebaseApiError` (or its\n * `RebaseClientError` subclass) from `@rebasepro/types` once it has a status.\n * The second is the browser-safe class: a `config/collections/*.ts` file is\n * bundled into the admin SPA and cannot import this package, so it is what a\n * collection callback throws, and what a callback refusal becomes —\n * `callbackRefusal` returns one, and `toCallbackError` wraps anything thrown\n * that does not already carry a status.\n *\n * One function because several doors turn an error into an answer: the REST\n * error handler, the two WebSocket servers, and the Postgres realtime\n * subscriptions. Each used to list the classes it recognised by hand. The\n * sockets listed only `ApiError`, so a `beforeDelete` veto that REST\n * answered as 400 `CALLBACK_REJECTED` with the author's message reached the\n * admin panel — which writes through the socket — as `INTERNAL_ERROR`, and in\n * production as \"An unexpected error occurred\".\n *\n * Matched by name as well as `instanceof`: a monorepo can resolve two copies of\n * a package, and `instanceof` is false across them. Name matching is also why\n * this file needs no runtime import of `@rebasepro/types`, which it may not\n * have — it is in the graph of `@rebasepro/server/functions`.\n */\nexport function declaredErrorAnswer(error: unknown): DeclaredErrorAnswer | undefined {\n if (error === null || typeof error !== \"object\") return undefined;\n const e = error as { name?: unknown; message?: unknown; code?: unknown; details?: unknown; statusCode?: unknown; status?: unknown };\n\n let status: number | undefined;\n if (error instanceof ApiError || e.name === \"ApiError\") {\n status = typeof e.statusCode === \"number\" ? e.statusCode : undefined;\n } else if (typeof e.name === \"string\" && /^Rebase(Api|Client)Error$/.test(e.name)) {\n // It spells its status `status`; `statusCode` wins when both are set.\n status = typeof e.statusCode === \"number\" ? e.statusCode\n : typeof e.status === \"number\" ? e.status\n : undefined;\n // Without a status it has not chosen an answer — `RebaseClientError`\n // is also raised for plain logic errors — so it is not one here.\n if (status === undefined) return undefined;\n } else {\n return undefined;\n }\n\n return {\n status: status || 500,\n code: typeof e.code === \"string\" && e.code ? e.code : \"INTERNAL_ERROR\",\n message: typeof e.message === \"string\" ? e.message : String(e.message ?? \"\"),\n ...(e.details !== undefined && { details: e.details }),\n expected: error instanceof ApiError && error.expected\n };\n}\n\n// `isRebaseApiError` was here. It read `return error instanceof Error`, so it\n// answered yes to every error while being named and used as though it\n// discriminated — the create and update handlers guarded a \"classify this as\n// BAD_REQUEST\" branch on it, and an unreachable database was therefore reported\n// to callers as a bad request. Deleted rather than repaired: the shape it\n// claimed to test is not decidable from an `Error`, and the layer that does\n// know — the driver, which holds the SQLSTATE — raises a real `ApiError`.\n\n/**\n * Leave the code and the message where the request log will find them.\n *\n * A failed request used to produce two lines, each holding half of it: this\n * handler had the code and the diagnosis, `requestLogger` had the user, the\n * collection, the status and the latency. Correlating them meant matching on\n * the request id — which only one of them printed reliably — and the pair cost\n * twice the volume for less than one line's worth of meaning.\n */\nfunction handOffToRequestLog(c: Context<HonoEnv>, code: string, message: string, expected = false): void {\n if (typeof c.set !== \"function\") return;\n c.set(\"errorSummary\", { code, message, expected });\n}\n\n/**\n * Is a request line coming for this request?\n *\n * `requestLogger` claims it before the handler runs, so by the time an error\n * reaches here the answer is already known. When nothing claimed it — a router\n * a project mounted onto its own Hono app, a test driving `app.fetch`\n * directly — this handler stays the only thing that would report the failure,\n * so it still writes its own line. Silence is the one outcome neither half may\n * produce.\n */\nfunction requestWillBeLogged(c: Context<HonoEnv>): boolean {\n return typeof c.get === \"function\" && c.get(\"requestLogged\") === true;\n}\n\n/**\n * Hono error-handling middleware (`app.onError`).\n * Converts any error into the canonical `{ error: { message, code } }` shape.\n */\nexport const errorHandler: ErrorHandler<HonoEnv> = (err, c) => {\n // Typecast custom error properties\n const error: RebaseApiError = err;\n const reqId = typeof c.get === \"function\" ? c.get(\"requestId\") : undefined;\n\n /* A stale SDK, named on the errors it explains.\n\n 400 and 404 only: those are what a renamed or removed field produces —\n an unknown filter field is a 400, a collection gone from under its slug\n is a 404 — and they are the two a caller can act on by regenerating.\n Attaching it to a 500 would be noise, since a server fault has nothing to\n do with how old the caller's schema is. */\n const driftFor = (status: number) =>\n (status === 400 || status === 404) && typeof c.get === \"function\"\n ? schemaDriftCause(c.get(\"schemaDrift\"))\n : undefined;\n\n // An error that chose its own answer — `ApiError`, or the browser-safe\n // `RebaseApiError` a collection callback throws. The same predicate the\n // WebSocket servers use; see `declaredErrorAnswer`.\n const answer = declaredErrorAnswer(error);\n if (answer) {\n // Operational errors — log at warn, unless the error declares itself a\n // routine outcome (see ApiError.expected), which would otherwise put a\n // warning in the log for every anonymous page view.\n handOffToRequestLog(c, answer.code, answer.message, answer.expected);\n if (answer.code === \"DATABASE_UNAVAILABLE\") c.header(\"Retry-After\", String(DATABASE_RETRY_AFTER_SECONDS));\n if (!requestWillBeLogged(c)) {\n const line = `[API] ${c.req.method} ${c.req.path} → ${answer.status} ${answer.code}: ${answer.message}` +\n (reqId ? ` [${reqId}]` : \"\");\n if (answer.expected) {\n logger.debug(line);\n } else {\n logger.warn(`⚠️ ${line}`);\n }\n }\n const apiErrorDrift = driftFor(answer.status);\n return c.json({\n error: {\n message: answer.message,\n code: answer.code,\n ...(answer.details !== undefined && { details: answer.details }),\n ...(reqId && { requestId: reqId }),\n ...(apiErrorDrift && { cause: apiErrorDrift })\n }\n } satisfies ErrorResponse, answer.status as ContentfulStatusCode);\n }\n\n // A body that is not JSON, read with `c.req.json()` — every auth and admin\n // route reads its body that way. It threw a bare `SyntaxError`, which\n // landed here as a 500 logged with a stack: a server fault for the\n // caller's typo, where the data API (which parses its own body) answered\n // 400. Mapped once, here, rather than at each of forty call sites.\n if (err instanceof SyntaxError && /JSON/i.test(err.message)) {\n handOffToRequestLog(c, \"INVALID_JSON\", \"Invalid JSON body\");\n return c.json({\n error: {\n message: \"Invalid JSON body\",\n code: \"INVALID_JSON\",\n ...(reqId && { requestId: reqId })\n }\n } satisfies ErrorResponse, 400);\n }\n\n let statusCode = error.statusCode || codeToStatus(error.code) || 500;\n let code = error.code || \"INTERNAL_ERROR\";\n\n // Handle DB connection and specific system errors for better logging\n let logMessage = error.message;\n\n // Resolve the actual cause — Node's net module wraps dual-stack failures\n // in an AggregateError whose inner errors carry the real address/port.\n let resolvedCause: PgLikeError | undefined;\n if (error.cause && typeof error.cause === \"object\" && error.cause !== null && \"code\" in error.cause) {\n const cause = error.cause as PgLikeError & { errors?: PgLikeError[] };\n if (cause.code === \"ECONNREFUSED\" && !cause.address && Array.isArray(cause.errors)) {\n // AggregateError — pick the first inner error that has address info\n resolvedCause = cause.errors.find(e => e.address) || cause;\n } else {\n resolvedCause = cause;\n }\n }\n\n // The real database error may sit several levels down the cause chain.\n // Losing it turns a precise failure (e.g. an RLS denial) into an opaque\n // \"Failed query: …\" 500 that is undiagnosable without direct DB access.\n const dbError = extractDbError(error);\n const poolFailure = dbError ? undefined : poolFailureOf(error);\n // Ahead of every SQLSTATE arm below: `57P01` is a SQLSTATE too, and it\n // says the connection is gone, not that the statement was wrong.\n const unreachable = !poolFailure && isDatabaseUnreachable(error);\n // A socket failure that is not the database's is another service's: a\n // `fetch()`, an S3 call. It answered `500` with the errno as the code\n // (`ECONNREFUSED`), or `INTERNAL_ERROR` — a server fault, for a service it\n // calls being down.\n const upstream = !poolFailure && !unreachable && !dbError && hasUpstreamSocketFailure(error);\n\n if (poolFailure) {\n code = poolFailure;\n statusCode = 503;\n logMessage = poolFailure === \"DB_POOL_EXHAUSTED\"\n ? \"Timed out waiting for a database connection: every pooled connection was busy (DB_POOL_MAX).\"\n : \"Timed out opening a database connection (DB_POOL_CONNECT_TIMEOUT).\";\n warnPoolFailure(poolFailure);\n c.header(\"Retry-After\", String(POOL_RETRY_AFTER_SECONDS));\n } else if (unreachable) {\n code = \"DATABASE_UNAVAILABLE\";\n statusCode = 503;\n const reason = describeConnectionFailure(error);\n logMessage = `The database could not be reached: ${reason}.`;\n warnDatabaseUnreachable(reason);\n c.header(\"Retry-After\", String(DATABASE_RETRY_AFTER_SECONDS));\n } else if (upstream) {\n code = \"SERVICE_UNAVAILABLE\";\n statusCode = 503;\n logMessage = `A service this request depends on could not be reached: ${describeConnectionFailure(error)}.`;\n } else if (resolvedCause && (resolvedCause.code === \"ENETUNREACH\" || resolvedCause.code === \"ECONNREFUSED\")) {\n // Some other service: a database that cannot be reached is the arm\n // above, so this one does not say \"database\".\n const cause = resolvedCause;\n logMessage = cause.code === \"ENETUNREACH\"\n ? `Network unreachable: cannot connect to ${cause.address}:${cause.port}.`\n : `Connection refused by ${cause.address}:${cause.port}.`;\n } else if (\"code\" in error && error.code === \"ENETUNREACH\") {\n const netErr = error as PgLikeError;\n logMessage = `Network unreachable. Cannot connect to service at ${netErr.address}:${netErr.port}.`;\n } else if (dbError && (dbError.code === \"42703\" || dbError.code === \"42P01\")) {\n code = \"SCHEMA_DRIFT\";\n const issue = dbError.code === \"42703\" ? \"column\" : \"table\";\n const identifier = dbError.table || dbError.column || extractMissingIdentifier(dbError.message) || \"unknown\";\n logMessage = `Schema drift: ${issue} \"${identifier}\" does not exist in the database. ${schemaDriftRemedy().short}`;\n } else if (dbError) {\n const parts = [`[PG ${dbError.code}] ${dbError.message}`];\n if (dbError.detail) parts.push(`Detail: ${dbError.detail}`);\n if (dbError.hint) parts.push(`Hint: ${dbError.hint}`);\n if (dbError.table) parts.push(`Table: ${dbError.table}`);\n if (dbError.column) parts.push(`Column: ${dbError.column}`);\n if (dbError.constraint) parts.push(`Constraint: ${dbError.constraint}`);\n if (dbError.code === \"42501\") {\n code = \"DB_PERMISSION_DENIED\";\n const ungranted = missingGrant(dbError.message);\n parts.push(ungranted\n ? `The role this request runs as was never granted ${ungranted.kind} \"${ungranted.name}\" — ` +\n \"a missing GRANT, not a row-level security policy.\"\n : \"The database rejected the statement for lack of privilege — usually a row-level \" +\n `security policy${dbError.table ? ` on \"${dbError.table}\"` : \"\"} denying this role, ` +\n \"or a stale FORCE ROW LEVEL SECURITY flag binding the owner connection.\"\n );\n }\n // 25006 read_only_sql_transaction. A request-scoped read opens its\n // transaction `READ ONLY`, so the only way to reach this is user code on\n // a read path attempting a write — which means an `afterRead` callback,\n // or something it called. Left in the generic branch it was a 500\n // \"Internal Server Error\", indistinguishable from the database being\n // down; it is the caller's own code, and it is not a server failure.\n if (dbError.code === \"25006\") {\n code = \"READ_ONLY_TRANSACTION\";\n statusCode = 409;\n parts.push(READ_ONLY_TRANSACTION_MESSAGE);\n }\n // SQLSTATE class 22 — data exception. The caller sent a value the\n // column's type cannot hold: `?id=eq.abc` on an integer key,\n // `?status=eq.nope` on an enum, a timestamp that is not one, a number\n // past the type's range. Every *other* bad query parameter already has\n // a precise 400 (`INVALID_LIMIT`, `UNKNOWN_FILTER_FIELD`,\n // `INVALID_LOGICAL_GROUP`); a bad *value* fell off the end of this\n // chain and answered 500 INTERNAL_ERROR — and in production `dbMessage`\n // is stripped, so the caller got a bare 500 naming nothing and had no\n // way to learn their own typo was the cause.\n if (dbError.code?.startsWith(\"22\")) {\n code = \"INVALID_FILTER_VALUE\";\n statusCode = 400;\n }\n logMessage = parts.join(\". \");\n }\n\n const isDbSchemaMismatch = code === \"SCHEMA_DRIFT\";\n\n // `logMessage`, not the sanitized client message: the request line is a\n // server log, and the whole point of this branch is the diagnosis it built.\n handOffToRequestLog(c, code, logMessage);\n\n if (isDbSchemaMismatch) {\n // Database schema mismatch is logged as a warning instead of a fatal error\n if (!requestWillBeLogged(c)) logger.warn(\n `⚠️ [API] ${c.req.method} ${c.req.path} → ${statusCode} ${code}: ${logMessage}` +\n (reqId ? ` [${reqId}]` : \"\")\n );\n // In dev mode, show a one-time hint to run `rebase doctor`\n if (!_schemaDriftHinted && hostEnv().NODE_ENV !== \"production\") {\n _schemaDriftHinted = true;\n // Drawn rather than hand-aligned: the remedy inside it now varies\n // with the database, and a box whose rows were padded by hand\n // stayed straight only for the text it was written around.\n const WIDTH = 62;\n const row = (text: string) => `│${text.padEnd(WIDTH).slice(0, WIDTH)}│`;\n logger.warn([\n \"\",\n `┌${\"─\".repeat(WIDTH)}┐`,\n // One space short, deliberately: the emoji occupies two columns\n // in a terminal and one in `String.length`.\n `│${\" 💡 TIP: Run `rebase doctor` for full schema diagnostics\".padEnd(WIDTH - 1)}│`,\n row(\"\"),\n ...schemaDriftRemedy().lines.map(row),\n `└${\"─\".repeat(WIDTH)}┘`,\n \"\"\n ].join(\"\\n\"));\n }\n } else if (poolFailure || unreachable) {\n // Said once a minute by `warnPoolFailure` / `warnDatabaseUnreachable`;\n // the request line carries each occurrence.\n if (!requestWillBeLogged(c)) logger.warn(\n `⚠️ [API] ${c.req.method} ${c.req.path} → ${statusCode} ${code}: ${logMessage}` +\n (reqId ? ` [${reqId}]` : \"\")\n );\n } else if (code === \"READ_ONLY_TRANSACTION\" || code === \"INVALID_FILTER_VALUE\") {\n // A 4xx: the application's own callback, refused — or a filter value\n // the caller's own request could not have worked with. Not a server\n // fault, so not an ❌ in the log either — and, like the drift arm above,\n // not a second line when the request log is already going to carry it.\n if (!requestWillBeLogged(c)) logger.warn(\n `⚠️ [API] ${c.req.method} ${c.req.path} → ${statusCode} ${code}: ${logMessage}` +\n (reqId ? ` [${reqId}]` : \"\")\n );\n } else if (!requestWillBeLogged(c)) {\n // Unexpected errors — log at error level\n logger.error(\n `❌ [API] ${c.req.method} ${c.req.path} → ${statusCode} ${code}: ${logMessage}` +\n (reqId ? ` [${reqId}]` : \"\")\n );\n }\n\n // Suppress the huge stack trace for known DB errors: it is noisy, and the\n // extracted [PG …] line above carries the signal. The SQL and the bound\n // params it used to leak are no longer this branch's problem — `logger`\n // strips Drizzle's `Failed query: … / params: …` wrapper out of every\n // message and stack it emits, so the fallbacks below (a connection dropped\n // mid-statement carries no SQLSTATE, so `dbError` is null and the stack is\n // logged) are covered too.\n const suppressStack = isDbSchemaMismatch || dbError !== null || poolFailure !== undefined || unreachable ||\n (statusCode < 500 && code === \"BAD_REQUEST\");\n if (!suppressStack) {\n // The error goes in as a value, not as `String(error.stack)`. A string\n // is a leaf to the logger: `serialiseError` — the `.cause`/\n // `AggregateError` walker the boot path relies on — never runs on one,\n // so the request path used to print the outer wrapper's stack and drop\n // the sentence that says what actually failed (`connect ECONNRESET`,\n // sitting two `.cause` links down). Structured, it walks the chain and\n // redacts each link on the way.\n logger.error(\"unhandled request error\", { error });\n }\n\n // Sanitize the message for the client to prevent leaking sensitive details\n // like SQL queries or internal IP addresses.\n let clientMessage = \"An unexpected error occurred\";\n if (code === \"READ_ONLY_TRANSACTION\") {\n // Ahead of the generic 4xx arm below, which would echo the raw driver\n // message (\"Failed query: insert into …\") back to the caller.\n clientMessage = READ_ONLY_TRANSACTION_MESSAGE;\n } else if (code === \"INVALID_FILTER_VALUE\") {\n // Also ahead of the 4xx arm: `error.message` here is the driver's\n // \"Failed query: select … / params: …\", which is both unhelpful and the\n // one thing this envelope must never carry.\n clientMessage = describeDataException(dbError || (error as PgLikeError));\n } else if (statusCode < 500 && error.message) {\n // If it's a 4xx error (e.g. from validation), it's generally safe to send the message\n clientMessage = error.message;\n } else if (error instanceof ApiError || error.name === \"ApiError\") {\n // We already handled ApiError above, but just in case\n clientMessage = error.message;\n } else if (code === \"SCHEMA_DRIFT\") {\n const pgErr = dbError || (error as PgLikeError);\n const issue = pgErr.code === \"42703\" ? \"column\" : \"table\";\n const identifier = pgErr.table || pgErr.column || extractMissingIdentifier(pgErr.message || error.message) || \"unknown\";\n clientMessage = `Schema drift: ${issue} \"${identifier}\" does not exist. ${schemaDriftRemedy().short}`;\n } else if (poolFailure) {\n clientMessage = \"The database is busy: no connection was free in time. Retry shortly.\";\n } else if (unreachable) {\n clientMessage = DATABASE_UNAVAILABLE_MESSAGE;\n } else if (upstream) {\n clientMessage = UPSTREAM_UNAVAILABLE_MESSAGE;\n } else if (code === \"DB_PERMISSION_DENIED\") {\n const ungranted = missingGrant(dbError?.message);\n clientMessage = ungranted\n ? `Permission denied by the database: the role this request runs as has no privilege on ${ungranted.kind} ` +\n `\"${ungranted.name}\". That is a missing GRANT to that role, not a row-level security policy.`\n : `Permission denied by the database${dbError?.table ? ` on \"${dbError.table}\"` : \"\"} (row-level security). Check the RLS policies for this table.`;\n } else if (code === \"INTERNAL_ERROR\") {\n clientMessage = \"Internal Server Error\";\n }\n\n // Database diagnostics for the envelope: the SQLSTATE is always safe to\n // return; message/detail/hint can reference schema internals, so only\n // outside production.\n const dbDetails = dbError ? {\n dbCode: dbError.code,\n ...(hostEnv().NODE_ENV !== \"production\" && {\n dbMessage: dbError.message,\n ...(dbError.detail && { detail: dbError.detail }),\n ...(dbError.hint && { hint: dbError.hint })\n })\n } : undefined;\n\n const drift = driftFor(statusCode);\n\n return c.json({\n error: {\n message: clientMessage,\n code,\n ...(error.details !== undefined\n ? { details: error.details }\n : dbDetails !== undefined ? { details: dbDetails } : {}),\n ...(reqId && { requestId: reqId }),\n ...(drift && { cause: drift })\n }\n } satisfies ErrorResponse, statusCode as ContentfulStatusCode);\n};\n\n/**\n * The object a `42501` names when no policy was involved.\n *\n * One SQLSTATE covers two different failures. A policy refusing a row says\n * \"… violates row-level security policy …\"; a role that was never granted an\n * object says \"permission denied for schema rebase\" (or table, function, …).\n * Answering the second with \"check the RLS policies\" sent people looking at\n * policies that were fine, for a grant that was missing.\n */\nfunction missingGrant(message: string | undefined): { kind: string; name: string } | undefined {\n const match = message?.match(/^permission denied for (schema|table|relation|view|materialized view|sequence|function|procedure|routine|type|column|database|foreign table|large object|tablespace|language) (.+)$/i);\n return match ? { kind: match[1].toLowerCase(), name: match[2].replace(/^\"|\"$/g, \"\") } : undefined;\n}\n\n/**\n * Map known error codes to HTTP status codes.\n */\nfunction codeToStatus(code?: string): number | undefined {\n if (!code) return undefined;\n const map: Record<string, number> = {\n BAD_REQUEST: 400,\n INVALID_INPUT: 400,\n WEAK_PASSWORD: 400,\n UNAUTHORIZED: 401,\n INVALID_CREDENTIALS: 401,\n INVALID_TOKEN: 401,\n FORBIDDEN: 403,\n NOT_FOUND: 404,\n CONFLICT: 409,\n EMAIL_EXISTS: 409,\n ROLE_EXISTS: 409,\n READ_ONLY_TRANSACTION: 409,\n INVALID_FILTER_VALUE: 400,\n SCHEMA_DRIFT: 500,\n DB_PERMISSION_DENIED: 500,\n INTERNAL_ERROR: 500,\n NOT_CONFIGURED: 503,\n SERVICE_UNAVAILABLE: 503\n };\n return map[code];\n}\n\n\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"logger-Bt32sHFw.js","names":[],"sources":["../src/utils/host.ts","../src/utils/logger.ts"],"sourcesContent":["/**\n * The host globals this package reads, behind functions that do not assume Node.\n *\n * `process` is not defined on workerd, on Deno Deploy without the compat flag,\n * or in a browser. That matters here for one specific reason: the portable\n * authoring surface (`@rebasepro/server/functions`) reaches the logger and the\n * error handler, and a bare `process.env.NODE_ENV` inside either of them turns\n * the first log line of a request into a `ReferenceError` on a runtime that has\n * no `process` — a failure that reads as \"the framework crashed\" rather than\n * \"this runtime has no process object\".\n *\n * Nothing here throws and nothing here is async. A runtime that cannot answer\n * gets the empty answer, because every caller in this file's blast radius is\n * choosing a log level or a format, and the safe default for both is the\n * development one.\n *\n * @module\n */\n\n/**\n * Where an adapter with no `process` can publish the environment.\n *\n * Cloudflare Workers hand the environment to the *request*, not to the module,\n * so there is no global to read at import time. An edge adapter that has\n * already seen a request can stash the bag here and every contextless reader in\n * the framework — the logger, chiefly — starts answering correctly.\n *\n * `Symbol.for` rather than a module-local for the same reason the singleton\n * uses it: more than one copy of this module can be loaded into one process,\n * and a module-local would leave every copy but the writer's blind. See\n * `../singleton.ts`.\n *\n * Request-scoped code should NOT read this. Use `getEnv(c)` from\n * `@rebasepro/server/functions`, which reads the binding attached to the\n * request it is serving — the only correct source on a runtime where two\n * concurrent requests can carry different bindings.\n */\nconst ENV_SLOT = Symbol.for(\"@rebasepro/server:host-env\");\n\ntype GlobalWithEnv = typeof globalThis & {\n [ENV_SLOT]?: Record<string, string | undefined>;\n process?: { env?: Record<string, string | undefined> };\n};\n\n/**\n * The process environment, or the closest thing this runtime has to one.\n *\n * Order: a bag published by {@link setHostEnv} first, because an adapter that\n * set one knows more than the ambient globals do; then `process.env`; then\n * nothing.\n */\nexport function hostEnv(): Record<string, string | undefined> {\n const global = globalThis as GlobalWithEnv;\n return global[ENV_SLOT] ?? global.process?.env ?? {};\n}\n\n/**\n * Read one environment variable without touching `process` directly.\n *\n * Trimmed, and blank is treated as absent — a variable declared with no value\n * is the ordinary way to write a compose file or a `.env` line, and every\n * caller in this package means \"unset\" by it. See `resolveFunctionsTimeoutMs`,\n * which learned that the hard way.\n */\nexport function hostEnvVar(name: string): string | undefined {\n const raw = hostEnv()[name];\n if (typeof raw !== \"string\") return undefined;\n const trimmed = raw.trim();\n return trimmed === \"\" ? undefined : trimmed;\n}\n\n/**\n * Publish an environment bag for contextless readers.\n *\n * Called by an adapter for a runtime whose environment is not ambient. Merges\n * rather than replaces, so two adapters (or an adapter plus a test) do not\n * silently erase each other's variables.\n */\nexport function setHostEnv(env: Record<string, string | undefined>): void {\n const global = globalThis as GlobalWithEnv;\n global[ENV_SLOT] = {\n ...(global[ENV_SLOT] ?? {}),\n ...env\n };\n}\n\n/** @internal Test seam — drops anything {@link setHostEnv} published. */\nexport function _clearHostEnv(): void {\n delete (globalThis as GlobalWithEnv)[ENV_SLOT];\n}\n\ntype GlobalWithStdio = typeof globalThis & {\n process?: {\n stdout?: { write?: (chunk: string) => unknown };\n stderr?: { write?: (chunk: string) => unknown };\n };\n};\n\n/**\n * Write one already-formatted line to the process's output.\n *\n * `process.stdout.write` is preferred where it exists because it is the only\n * one of the two that does not append its own formatting to a line that is\n * already a complete JSON document — `console.log` on Node is\n * `process.stdout.write` plus `util.format`, and `util.format` will happily\n * reinterpret a `%s` that appeared inside a user's log message.\n *\n * Where it does not exist, `console` is the runtime's log sink and is what its\n * platform collects.\n */\nexport function writeLine(stream: \"out\" | \"err\", line: string): void {\n const proc = (globalThis as GlobalWithStdio).process;\n const sink = stream === \"err\" ? proc?.stderr : proc?.stdout;\n if (typeof sink?.write === \"function\") {\n sink.write(line + \"\\n\");\n return;\n }\n if (stream === \"err\") console.error(line);\n else console.log(line);\n}\n","/**\n * Structured Logger for Rebase Backend\n *\n * Outputs JSON lines when `NODE_ENV=production`, human-readable prefixed\n * lines otherwise. Designed to work with Google Cloud Logging severity levels.\n *\n * Every line — message and data, at any depth — passes through the redaction\n * below, which strips Drizzle's `Failed query: … / params: …` wrapper and the\n * values of secret-looking keys. See the block above `serialiseError`.\n *\n * Usage:\n * import { logger } from \"./utils/logger\";\n * logger.info(\"Server started\", { port: 3001 });\n * logger.error(\"Request failed\", { path: \"/api/test\", error: err });\n *\n * Every host global goes through `./host`, and that is load-bearing rather than\n * tidy: this module is reachable from `@rebasepro/server/functions`, the\n * authoring surface that has to import cleanly on a runtime with no `process`.\n * A bare `process.env.NODE_ENV` here would make the first log line of the first\n * request on workerd a `ReferenceError`.\n */\nimport { hostEnv, writeLine } from \"./host\";\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\n/** Google Cloud Logging severity strings. */\nconst GCP_SEVERITY: Record<LogLevel, string> = {\n debug: \"DEBUG\",\n info: \"INFO\",\n warn: \"WARNING\",\n error: \"ERROR\"\n};\n\nconst LOG_PRIORITY: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3\n};\n\nexport interface LogEntry {\n severity: string;\n message: string;\n timestamp: string;\n [key: string]: unknown;\n}\n\nexport interface Logger {\n debug(message: string, data?: Record<string, unknown>): void;\n info(message: string, data?: Record<string, unknown>): void;\n warn(message: string, data?: Record<string, unknown>): void;\n error(message: string, data?: Record<string, unknown>): void;\n child(defaultFields: Record<string, unknown>): Logger;\n}\n\nfunction isProduction(): boolean {\n return hostEnv().NODE_ENV === \"production\";\n}\n\n/**\n * An explicit level from `config.logging.level`, when a project set one.\n *\n * Outranks `LOG_LEVEL` because it is the more specific statement: an\n * environment variable is the deployment's default, and this is the\n * application saying what it wants regardless of where it runs.\n *\n * There used to be a second, separate mechanism for this — `utils/logging.ts`\n * reassigned `console.debug`/`console.log`/`console.warn` to no-ops — and the\n * two disagreed in a way nobody could have guessed from either: `LOG_LEVEL=warn`\n * silenced this logger's info lines *and* every `console.log` in the process,\n * including a dependency's, including a project's own debugging. It also could\n * not be undone, because the originals were gone.\n */\nlet configuredLevel: LogLevel | undefined;\n\n/**\n * Set the level from configuration. `undefined` returns to `LOG_LEVEL`.\n *\n * Read per line rather than captured at construction, so a logger created\n * before configuration is read still honours it — which the singleton below\n * always is.\n */\nexport function setLogLevel(level?: LogLevel): void {\n configuredLevel = level;\n}\n\n/** Every line to stderr, when a command's stdout is its result. See {@link setLogOutput}. */\nlet everyLineToStderr = false;\n\n/**\n * Send every log line to stderr — `\"stderr\"` — or back to the default with\n * `undefined`, where errors (and in development, warnings) go to stderr and\n * the rest to stdout.\n *\n * For a process whose stdout is its answer: `rebase console --eval` prints a\n * value for `jq` to read, and a log line on stdout would be part of it.\n */\nexport function setLogOutput(output?: \"stderr\"): void {\n everyLineToStderr = output === \"stderr\";\n}\n\nfunction getMinLevel(): LogLevel {\n if (configuredLevel) return configuredLevel;\n const env = (hostEnv().LOG_LEVEL || \"info\").trim().toLowerCase();\n if (env in LOG_PRIORITY) return env as LogLevel;\n return \"info\";\n}\n\n// ── Redaction ───────────────────────────────────────────────────────\n//\n// Drizzle builds every query failure as\n// `Failed query: ${query}\\nparams: ${params}` (drizzle-orm/errors.js), so the\n// statement *and* every bound value ride along in `.message` and `.stack` of\n// whatever a driver rethrows — an email and a bcrypt hash reach stdout the\n// moment a registration hits a unique violation. The redaction lives here, in\n// the one function every log line passes through, rather than at the ~124\n// `{ error: … }` call sites: a per-site rule is what produced the leak (one\n// file suppressed the stack, four others did not), and the next caller would\n// reintroduce it. Nothing above this line needs to know about it.\n\nconst FAILED_QUERY_MARKER = \"Failed query:\";\n/**\n * The marker says how to lift it.\n *\n * Every DDL, RLS and CDC failure ends at this string, and the statement is the\n * whole diagnosis — three of them landed in one boot of a two-database project,\n * each a dead end. The switch existed; nothing named it, in the log or in the\n * docs, so `grep -rn REBASE_LOG_RAW_QUERIES` over the documentation, the\n * templates and the agent skills came back empty.\n */\nconst REDACTED_QUERY =\n \"Failed query: [redacted — set REBASE_LOG_RAW_QUERIES=true in development to see it]\";\nconst REDACTED_VALUE = \"[redacted]\";\n\n/**\n * Key fragments whose values are never safe to publish. Compared against the\n * key with separators and case removed, so `api_key`, `apiKey` and `API-KEY`\n * all match `apikey`.\n */\nconst SENSITIVE_KEY_FRAGMENTS = [\n \"password\",\n \"passwd\",\n \"passphrase\",\n \"secret\",\n \"token\",\n \"apikey\",\n \"authorization\",\n \"credential\",\n \"cookie\",\n \"privatekey\",\n \"sessionid\"\n];\n\n/** Longest structure the redactor will walk before giving up. */\nconst MAX_REDACT_DEPTH = 8;\n\nfunction isSensitiveKey(key: string): boolean {\n const normalised = key.toLowerCase().replace(/[^a-z0-9]/g, \"\");\n return SENSITIVE_KEY_FRAGMENTS.some(fragment => normalised.includes(fragment));\n}\n\n/**\n * Whether a SQL statement may be written out at all.\n *\n * The escape hatch for the `Failed query:` strip — the statement is the fastest\n * way to diagnose a failing query on a developer machine. Ignored in\n * production, so a runtime that inherits the variable cannot leak because of\n * it, and it never re-enables the key deny-list.\n *\n * Exported because it is the *only* answer to \"may this process print SQL\", and\n * a driver that wants to trace what it executes has to ask the same question.\n * The Postgres driver used to decide for itself, with a `console.debug` gated\n * on `NODE_ENV` alone: every statement went to stdout whatever `LOG_LEVEL`\n * said, and it went there without passing through the redaction that lives in\n * this file.\n */\nexport function rawQueryLoggingEnabled(): boolean {\n return hostEnv().NODE_ENV !== \"production\"\n && hostEnv().REBASE_LOG_RAW_QUERIES === \"true\";\n}\n\n/**\n * Strip every `Failed query: … / params: …` span out of a message or stack,\n * keeping the surrounding text (including stack frames, which carry no user\n * data). When no `params:` line follows the marker the rest of the string is\n * dropped: a statement of unknown extent is treated as sensitive rather than\n * guessed at.\n *\n * Idempotent, and it has to be: an already-redacted span still starts with the\n * marker but has no `params:` line, so a second pass over it would fall into\n * the drop-the-rest branch and eat the stack frames behind it. Redaction runs\n * more than once on the same string in practice — the cron scheduler redacts\n * before persisting and then logs the result.\n */\nexport function redactSensitiveText(text: string): string {\n if (!text.includes(FAILED_QUERY_MARKER) || rawQueryLoggingEnabled()) return text;\n\n let out = text;\n let idx = out.indexOf(FAILED_QUERY_MARKER);\n while (idx !== -1) {\n if (out.startsWith(REDACTED_QUERY, idx)) {\n idx = out.indexOf(FAILED_QUERY_MARKER, idx + REDACTED_QUERY.length);\n continue;\n }\n const paramsIdx = out.indexOf(\"\\nparams:\", idx);\n let end: number;\n if (paramsIdx === -1) {\n end = out.length;\n } else {\n const eol = out.indexOf(\"\\n\", paramsIdx + 1);\n end = eol === -1 ? out.length : eol;\n }\n out = out.slice(0, idx) + REDACTED_QUERY + out.slice(end);\n idx = out.indexOf(FAILED_QUERY_MARKER, idx + REDACTED_QUERY.length);\n }\n return out;\n}\n\n/**\n * Diagnostic own-properties worth carrying up out of an error.\n *\n * These are what a socket failure actually says: `ECONNREFUSED` with the\n * `address` and `port` it was refused on, `ENOTFOUND` with the hostname that\n * did not resolve. They live as own-properties on the Node error rather than in\n * its message, so a serialiser that copies only `message` and `stack` prints a\n * boot failure that names no host, no port and no reason.\n *\n * Deliberately a fixed list rather than \"every own-property\": `DrizzleQueryError`\n * carries `query` and `params` beside its message, and copying those would put\n * the statement and its bound values — an email, a bcrypt hash — straight back\n * on stdout, which is what the redaction above exists to prevent. Postgres's own\n * `detail` and `hint` are left out for the same reason: `23505` reports\n * `Key (email)=(a@b.c) already exists.`, which is a row's contents.\n */\nconst ERROR_DETAIL_KEYS = [\"code\", \"errno\", \"syscall\", \"address\", \"port\", \"hostname\"] as const;\n\n/** How far the cause chain is followed before the serialiser gives up. */\nconst MAX_CAUSE_DEPTH = 4;\n\n/** How many of an `AggregateError`'s children are serialised. */\nconst MAX_AGGREGATE_ERRORS = 4;\n\n/**\n * Serialise an Error into a plain object, with the query text redacted out of\n * its message and stack. `query`/`params` own-properties — which\n * `DrizzleQueryError` carries beside the message — are deliberately not copied.\n *\n * The chain matters more than the top. Everything a driver rethrows is a\n * wrapper: Drizzle's is `Failed query: SELECT 1` with a stack through drizzle\n * internals, and the sentence that says what is wrong — `connect ECONNREFUSED\n * 127.0.0.1:5432`, `password authentication failed for user \"app\"` — sits in\n * `.cause`, or inside the `AggregateError.errors` that `net` raises when every\n * resolved address is refused. Serialising only the wrapper is why a boot\n * against a stopped database used to log a redacted query and nothing else.\n *\n * Handles non-Error values gracefully.\n */\nfunction serialiseError(value: unknown, depth = 0): Record<string, unknown> {\n const isError = value instanceof Error;\n // A cause is not always an Error: drivers throw plain `{ code, address,\n // port }` bags, and stringifying one yields `[object Object]`, which is\n // worse than nothing. Only the named fields are copied out of it — the same\n // fixed list, for the same reason.\n const isDetailBag = !isError && depth > 0 && Boolean(value) && typeof value === \"object\" && !Array.isArray(value);\n if (!isError && !isDetailBag) {\n return { value: redactSensitiveText(String(value)) };\n }\n\n const own = value as Record<string, unknown>;\n const out: Record<string, unknown> = isError\n ? {\n name: (value as Error).name,\n message: redactSensitiveText((value as Error).message),\n stack: (value as Error).stack ? redactSensitiveText((value as Error).stack as string) : undefined\n }\n : {\n ...(typeof own.name === \"string\" ? { name: own.name } : {}),\n ...(typeof own.message === \"string\" ? { message: redactSensitiveText(own.message) } : {})\n };\n\n for (const key of ERROR_DETAIL_KEYS) {\n const detail = own[key];\n if (detail === undefined || detail === null) continue;\n if (typeof detail === \"object\") continue;\n out[key] = typeof detail === \"string\" ? redactSensitiveText(detail) : detail;\n }\n\n if (depth >= MAX_CAUSE_DEPTH) return out;\n\n if (own.cause !== undefined && own.cause !== null) {\n out.cause = serialiseError(own.cause, depth + 1);\n }\n const aggregated = own.errors;\n if (Array.isArray(aggregated) && aggregated.length > 0) {\n out.errors = aggregated\n .slice(0, MAX_AGGREGATE_ERRORS)\n .map(item => serialiseError(item, depth + 1));\n }\n return out;\n}\n\n/**\n * Redact one logged value: errors are serialised, strings are stripped of\n * query text, objects and arrays are walked. Cycles and over-deep structures\n * collapse to a marker rather than throwing — a logger that can fail is worse\n * than one that logs less. (An object referenced twice in one payload is\n * reported as `[circular]` the second time; bounding the walk matters more\n * than rendering a shared reference twice.)\n */\nfunction redactValue(value: unknown, depth: number, seen: WeakSet<object>): unknown {\n // `serialiseError` returns only already-redacted strings, so it is the\n // terminal step — walking its output again would redact twice.\n if (value instanceof Error) return serialiseError(value);\n if (typeof value === \"string\") return redactSensitiveText(value);\n if (value === null || typeof value !== \"object\") return value;\n if (depth >= MAX_REDACT_DEPTH) return \"[truncated]\";\n if (seen.has(value)) return \"[circular]\";\n seen.add(value);\n\n if (Array.isArray(value)) {\n return value.map(item => redactValue(item, depth + 1, seen));\n }\n if (value instanceof Date) return value;\n\n const out: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(value as Record<string, unknown>)) {\n out[key] = isSensitiveKey(key) ? REDACTED_VALUE : redactValue(val, depth + 1, seen);\n }\n return out;\n}\n\nfunction formatData(data?: Record<string, unknown>): Record<string, unknown> | undefined {\n if (!data) return undefined;\n const seen = new WeakSet<object>();\n const out: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(data)) {\n out[key] = isSensitiveKey(key) ? REDACTED_VALUE : redactValue(val, 0, seen);\n }\n return out;\n}\n\n/**\n * `JSON.stringify`, with the redaction a structured field gets.\n *\n * For a value that has to go *into the message*. Once an object is a string,\n * `emit` can only strip `Failed query:` spans out of it: a sensitive key would\n * pass, and so would the `query` and `params` own-properties `DrizzleQueryError`\n * carries beside its message, which `JSON.stringify` copies and the message\n * redaction never sees. Unlike the structured walk this honours `toJSON`, since\n * the result is read as text. Never throws: a value that cannot be stringified\n * is a marker, not a failed log line.\n */\nexport function stringifyForLog(value: unknown): string | undefined {\n try {\n return JSON.stringify(value, (key, val: unknown) => {\n if (isSensitiveKey(key)) return REDACTED_VALUE;\n if (val instanceof Error) return serialiseError(val);\n if (typeof val === \"string\") return redactSensitiveText(val);\n return val;\n });\n } catch {\n return \"[unserialisable]\";\n }\n}\n\n/**\n * Something that wants a copy of every line this logger writes.\n *\n * Receives the message and fields *after* redaction, never before: a sink is\n * another destination for the same line, and the one thing that must not vary\n * by destination is whether the query and its bound values are in it.\n */\nexport type LogSink = (\n level: LogLevel,\n message: string,\n data: Record<string, unknown>\n) => void;\n\nconst sinks = new Set<LogSink>();\n\n/**\n * Tee this logger somewhere else. Returns the unsubscribe.\n *\n * The Studio's Logs Explorer is the caller: its ring buffer used to be fed only\n * by a request middleware, so the panel showed a wall of `GET … 200` and not one\n * of the errors, warnings or diagnoses the server was writing to stdout at the\n * same moment. A log viewer that cannot show you an error is a log viewer\n * nobody opens twice.\n *\n * A sink MUST NOT log. It is called from inside `emit`, so anything that comes\n * back through `logger` recurses; the guard below stops the stack blowing, but\n * the line is dropped rather than delivered, which is its own bug.\n */\nexport function addLogSink(sink: LogSink): () => void {\n sinks.add(sink);\n return () => { sinks.delete(sink); };\n}\n\n/** Re-entrancy guard: see `addLogSink`. */\nlet inSink = false;\n\nfunction fanOut(level: LogLevel, message: string, data: Record<string, unknown>): void {\n if (sinks.size === 0 || inSink) return;\n inSink = true;\n try {\n for (const sink of sinks) {\n // One broken sink must not take down the line, nor the request that\n // was writing it.\n try { sink(level, message, data); } catch { /* a broken tee is not the caller's problem */ }\n }\n } finally {\n inSink = false;\n }\n}\n\nfunction createLogger(rawDefaultFields: Record<string, unknown> = {}): Logger {\n // Child fields go through the same pass as per-call data — they are merged\n // into every line this logger emits, so leaving them raw would be a hole\n // the moment `child()` gets its first caller.\n const defaultFields = formatData(rawDefaultFields) ?? {};\n\n function emit(level: LogLevel, message: string, data?: Record<string, unknown>): void {\n // Per line, not captured at construction: the singleton is created when\n // this module is first imported, which is long before a project's\n // `config.logging.level` has been read.\n if (LOG_PRIORITY[level] < LOG_PRIORITY[getMinLevel()]) return;\n\n // The message is redacted too, not just the data: several call sites\n // interpolate `error.message` straight into the line they log.\n const safeMessage = redactSensitiveText(message);\n const merged = { ...defaultFields,\n...formatData(data) };\n\n // Before the write, so a sink still sees the line if stdout is the\n // thing that is broken.\n fanOut(level, safeMessage, merged);\n\n if (isProduction()) {\n // Structured JSON for Cloud Logging\n const entry: LogEntry = {\n severity: GCP_SEVERITY[level],\n message: safeMessage,\n timestamp: new Date().toISOString(),\n ...merged\n };\n const line = JSON.stringify(entry);\n\n if (level === \"error\" || everyLineToStderr) {\n writeLine(\"err\", line);\n } else {\n writeLine(\"out\", line);\n }\n } else {\n // Human-readable for development\n const prefix = level === \"error\" ? \"❌\"\n : level === \"warn\" ? \"⚠️\"\n : level === \"info\" ? \"ℹ️\"\n : \"🐛\";\n const extra = Object.keys(merged).length > 0 ? ` ${JSON.stringify(merged)}` : \"\";\n const out = `${prefix} [${level.toUpperCase()}] ${safeMessage}${extra}`;\n\n if (level === \"error\" || everyLineToStderr) {\n console.error(out);\n } else if (level === \"warn\") {\n console.warn(out);\n } else {\n console.log(out);\n }\n }\n }\n\n return {\n debug: (msg, data) => emit(\"debug\", msg, data),\n info: (msg, data) => emit(\"info\", msg, data),\n warn: (msg, data) => emit(\"warn\", msg, data),\n error: (msg, data) => emit(\"error\", msg, data),\n child(fields: Record<string, unknown>): Logger {\n return createLogger({ ...defaultFields,\n...fields });\n }\n };\n}\n\n/**\n * Singleton logger instance.\n * In production: emits JSON lines with `severity`, `message`, `timestamp`.\n * In development: emits human-readable prefixed lines.\n */\nexport const logger: Logger = createLogger();\n\n/**\n * The cause chain, one readable line per link.\n *\n * `serialiseError` puts the chain in the structured payload, which is the right\n * place for a log aggregator and the wrong place for a person staring at a\n * container that will not start: the sentence they need is inside a JSON blob\n * behind an escaped stack trace. This renders the same chain as lines to print\n * beside the headline, so the first thing on screen after \"Failed to start\" is\n * `caused by: connect ECONNREFUSED 127.0.0.1:5432 (ECONNREFUSED)`.\n *\n * Redacted like everything else, and bounded by the same depth: a chain is\n * usually two links and never usefully more than four.\n */\nexport function describeCauseChain(error: unknown): string[] {\n const lines: string[] = [];\n const seen = new Set<unknown>();\n\n const walk = (value: unknown, depth: number): void => {\n if (depth > MAX_CAUSE_DEPTH || value === undefined || value === null) return;\n if (typeof value === \"object\") {\n if (seen.has(value)) return;\n seen.add(value);\n }\n if (depth > 0) {\n const described = describeOneCause(value);\n if (described) lines.push(`caused by: ${described}`);\n }\n if (typeof value !== \"object\") return;\n const own = value as Record<string, unknown>;\n walk(own.cause, depth + 1);\n const aggregated = own.errors;\n if (Array.isArray(aggregated)) {\n for (const item of aggregated.slice(0, MAX_AGGREGATE_ERRORS)) walk(item, depth + 1);\n }\n };\n\n walk(error, 0);\n return lines;\n}\n\n/** One cause rendered as `message (CODE) address:port`, or nothing to say. */\nfunction describeOneCause(value: unknown): string | undefined {\n if (value === null || typeof value !== \"object\") {\n const text = redactSensitiveText(String(value));\n return text || undefined;\n }\n const own = value as Record<string, unknown>;\n const message = typeof own.message === \"string\" && own.message\n ? redactSensitiveText(own.message)\n : undefined;\n const code = typeof own.code === \"string\" ? own.code : undefined;\n // Only when the message does not already carry it. Node writes\n // `connect ECONNREFUSED 127.0.0.1:5432` and also sets `address`/`port`, and\n // repeating the endpoint reads like two different facts.\n const endpoint = own.address !== undefined && own.port !== undefined\n ? `${String(own.address)}:${String(own.port)}`\n : undefined;\n const where = endpoint && !(message ?? \"\").includes(endpoint) ? endpoint : undefined;\n const parts = [message ?? code, code && message ? `(${code})` : undefined, where]\n .filter(Boolean);\n return parts.length > 0 ? parts.join(\" \") : undefined;\n}\n"],"mappings":"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"}
|