@rebasepro/server 0.20.0 → 0.21.0

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.
Files changed (68) hide show
  1. package/dist/{admin_block-0Xu0r6eZ.js → admin_block-DxKLmdiv.js} +26 -8
  2. package/dist/{admin_block-0Xu0r6eZ.js.map → admin_block-DxKLmdiv.js.map} +1 -1
  3. package/dist/api/ast-schema-editor.d.ts +15 -0
  4. package/dist/api/rest/api-generator.d.ts +28 -0
  5. package/dist/{ast-schema-editor-C6mDz0XN.js → ast-schema-editor-CslO8Oje.js} +24 -4
  6. package/dist/ast-schema-editor-CslO8Oje.js.map +1 -0
  7. package/dist/auth/jwt.d.ts +67 -0
  8. package/dist/auth/rls-scope.d.ts +14 -0
  9. package/dist/{auth-DJsLXsCR.js → auth-gzoRjnuh.js} +24 -12
  10. package/dist/auth-gzoRjnuh.js.map +1 -0
  11. package/dist/boot/env.d.ts +8 -2
  12. package/dist/boot/fetch-bundle.d.ts +19 -0
  13. package/dist/boot/role.d.ts +2 -0
  14. package/dist/contract-routes-eLxV0le1.js.map +1 -1
  15. package/dist/{cron-routes-Bfwni8Zg.js → cron-routes-BamgO0Xr.js} +2 -2
  16. package/dist/{cron-routes-Bfwni8Zg.js.map → cron-routes-BamgO0Xr.js.map} +1 -1
  17. package/dist/{cron-store-Bsiw4Q6u.js → cron-store-yXOyIEmn.js} +2 -3
  18. package/dist/{cron-store-Bsiw4Q6u.js.map → cron-store-yXOyIEmn.js.map} +1 -1
  19. package/dist/db/ambient-transaction.d.ts +48 -0
  20. package/dist/ddl-bootstrap-CfNvxMuK.js.map +1 -1
  21. package/dist/deploy/pod-contract.d.ts +1 -1
  22. package/dist/errors-DMImyqyR.js.map +1 -1
  23. package/dist/functions/index.js.map +1 -1
  24. package/dist/{history-recorder-r5_IzSHK.js → history-recorder-hBGuXcZc.js} +2 -2
  25. package/dist/{history-recorder-r5_IzSHK.js.map → history-recorder-hBGuXcZc.js.map} +1 -1
  26. package/dist/{history-store-D4RVK-uZ.js → history-store-C9EtGtoL.js} +5 -6
  27. package/dist/history-store-C9EtGtoL.js.map +1 -0
  28. package/dist/index.d.ts +2 -1
  29. package/dist/index.es.js +2266 -93
  30. package/dist/index.es.js.map +1 -1
  31. package/dist/init/callback-client.d.ts +16 -0
  32. package/dist/init/surfaces.d.ts +37 -7
  33. package/dist/jobs/types.d.ts +5 -0
  34. package/dist/{jobs-CW5lm_Ix.js → jobs-DZ8DLLcU.js} +82 -13
  35. package/dist/jobs-DZ8DLLcU.js.map +1 -0
  36. package/dist/{jwt-DATvkKB_.js → jwt-C4OW-DNq.js} +136 -4
  37. package/dist/jwt-C4OW-DNq.js.map +1 -0
  38. package/dist/logger-DO2PZc4i.js.map +1 -1
  39. package/dist/mcp/consent-page.d.ts +75 -0
  40. package/dist/mcp/mcp-routes.d.ts +52 -0
  41. package/dist/mcp/mcp-tools.d.ts +51 -0
  42. package/dist/mcp/oauth-metadata.d.ts +123 -0
  43. package/dist/mcp/oauth-routes.d.ts +52 -0
  44. package/dist/mcp/oauth-store.d.ts +157 -0
  45. package/dist/metrics/index.d.ts +16 -1
  46. package/dist/{openapi-generator-DGyLbISS.js → openapi-generator-CAFKF36E.js} +2 -8
  47. package/dist/openapi-generator-CAFKF36E.js.map +1 -0
  48. package/dist/{query-parser-BQiPZrM-.js → query-parser-uqzkZ82i.js} +2 -2
  49. package/dist/{query-parser-BQiPZrM-.js.map → query-parser-uqzkZ82i.js.map} +1 -1
  50. package/dist/{schema-editor-routes-C3TLZqAC.js → schema-editor-routes-BKOmdf4M.js} +2 -2
  51. package/dist/{schema-editor-routes-C3TLZqAC.js.map → schema-editor-routes-BKOmdf4M.js.map} +1 -1
  52. package/dist/services/webhook-service.d.ts +11 -8
  53. package/dist/sql-rows-C6GEc2oE.js +45 -0
  54. package/dist/sql-rows-C6GEc2oE.js.map +1 -0
  55. package/dist/src-Br6ARbs6.js.map +1 -1
  56. package/dist/{src-DqZ9YiGA.js → src-DW1cQ_28.js} +261 -17
  57. package/dist/src-DW1cQ_28.js.map +1 -0
  58. package/dist/utils/portable-crypto.d.ts +8 -0
  59. package/package.json +5 -5
  60. package/dist/ast-schema-editor-C6mDz0XN.js.map +0 -1
  61. package/dist/auth-DJsLXsCR.js.map +0 -1
  62. package/dist/history-store-D4RVK-uZ.js.map +0 -1
  63. package/dist/internal-tables-DYVcFFSv.js +0 -117
  64. package/dist/internal-tables-DYVcFFSv.js.map +0 -1
  65. package/dist/jobs-CW5lm_Ix.js.map +0 -1
  66. package/dist/jwt-DATvkKB_.js.map +0 -1
  67. package/dist/openapi-generator-DGyLbISS.js.map +0 -1
  68. package/dist/src-DqZ9YiGA.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"logger-DO2PZc4i.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\nfunction getMinLevel(): LogLevel {\n if (configuredLevel) return configuredLevel;\n const env = (hostEnv().LOG_LEVEL || \"info\").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 unknown 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 * 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\") {\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\") {\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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"}
1
+ {"version":3,"file":"logger-DO2PZc4i.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\nfunction getMinLevel(): LogLevel {\n if (configuredLevel) return configuredLevel;\n const env = (hostEnv().LOG_LEVEL || \"info\").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 * 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\") {\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\") {\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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"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The one screen a person sees in the whole OAuth flow.
3
+ *
4
+ * Self-contained HTML with no build step, no framework and no external asset.
5
+ * It is served by the backend, which may be running with no frontend deployed
6
+ * at all — a `runtimeMode: managed` API pod has no bundle to link a stylesheet
7
+ * out of — and a consent screen that renders unstyled or half-loaded is one
8
+ * people click through without reading.
9
+ *
10
+ * What it must get right, in order of how badly it goes wrong:
11
+ *
12
+ * 1. **Name the client, and make clear it is not us.** The single most common
13
+ * OAuth phishing shape is a consent screen that looks like the provider's
14
+ * own login and mentions the third party in small print. The client's name
15
+ * is attacker-controlled — anyone may register — so it is escaped, length-
16
+ * capped, and always rendered as a quoted, untrusted string.
17
+ * 2. **Say what is being granted in words**, not scope identifiers. `mcp:read`
18
+ * means nothing to the person deciding.
19
+ * 3. **Say what is NOT being granted.** The interesting property of this
20
+ * integration is that the grant cannot exceed the user's own access, and
21
+ * stating it is what makes the decision an informed one.
22
+ *
23
+ * 4. **Promise only what the system keeps.** This screen briefly said "You can
24
+ * revoke this at any time" while nothing user-facing could revoke anything.
25
+ * It says it again now that `DELETE ${basePath}/oauth/grants/:clientId`
26
+ * exists — and says *where*, because "at any time" with no route to it is
27
+ * the sentence that makes saying yes feel safe while meaning nothing.
28
+ * 5. **Never post the password anywhere but the existing login endpoint.**
29
+ * The form below sends credentials to `${basePath}/auth/login` and nowhere
30
+ * else; this file's own endpoint receives only the resulting session token.
31
+ * 6. **Refuse to be framed.** See {@link consentPageHeaders} — a consent screen
32
+ * that can be iframed is a consent screen an attacker can have clicked.
33
+ */
34
+ export interface ConsentPageParams {
35
+ clientName: string;
36
+ scope: string;
37
+ scopeDescriptions: {
38
+ scope: string;
39
+ description: string;
40
+ }[];
41
+ /** The signed authorization request, posted back untouched. */
42
+ requestToken: string;
43
+ /** The existing auth endpoint the sign-in form posts to. */
44
+ loginUrl: string;
45
+ /** Where the decision is posted. */
46
+ decisionUrl: string;
47
+ /** The canonical resource, shown so the user can see which person this is. */
48
+ resource: string;
49
+ /**
50
+ * A per-response nonce for the inline script.
51
+ *
52
+ * The page carries its own behaviour inline — it has no build step and no
53
+ * asset host to load from — so a CSP that forbids inline script would
54
+ * forbid the page. A nonce keeps the policy strict for everything the page
55
+ * did NOT emit, which is the part an injection would need.
56
+ */
57
+ nonce: string;
58
+ }
59
+ /**
60
+ * The headers a consent screen must carry.
61
+ *
62
+ * `frame-ancestors 'none'` and `X-Frame-Options` are the ones that matter, and
63
+ * they were missing. A consent screen is the single page in an OAuth flow that
64
+ * must not be frameable: an attacker who can iframe it, make it transparent and
65
+ * float a button under the user's cursor gets "Allow" pressed for a client they
66
+ * registered — clickjacking, with the prize being the victim's data. The user
67
+ * sees nothing, because the page they consented on was never visible.
68
+ *
69
+ * The rest follows from what the page is. `no-store`, because the HTML embeds a
70
+ * signed authorization request. `no-referrer`, so the `client_id` and `state`
71
+ * in this URL do not travel to whatever the redirect target loads. `form-action
72
+ * 'self'`, so an injection cannot repoint the form at its own collector.
73
+ */
74
+ export declare function consentPageHeaders(nonce: string): Record<string, string>;
75
+ export declare function renderConsentPage(params: ConsentPageParams): string;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The MCP endpoint: an OAuth 2.1 resource server speaking JSON-RPC.
3
+ *
4
+ * The protocol subset is deliberately hand-written rather than taken from
5
+ * `@modelcontextprotocol/sdk`. The SDK's HTTP transport is built around Node's
6
+ * `req`/`res`, this server is Hono, and the adapter would be more code than the
7
+ * protocol — which, for a server that offers tools and neither prompts nor
8
+ * sampling, is `initialize`, `tools/list`, `tools/call` and `ping`. It also
9
+ * keeps a dependency out of `@rebasepro/server`, whose import graph feeds the
10
+ * portable functions bundle.
11
+ *
12
+ * Streamable HTTP, in its simplest legal form: a POST carrying one JSON-RPC
13
+ * message, answered with one JSON response. The specification permits a server
14
+ * to answer `application/json` instead of opening an SSE stream, and nothing
15
+ * here pushes server-initiated messages, so `GET /mcp` returns 405 with an
16
+ * `Allow` header rather than pretending to hold a stream open.
17
+ *
18
+ * The authorization half is the part that matters, and it runs before any of
19
+ * the above: no token is 401 with the `WWW-Authenticate` challenge that tells a
20
+ * client where the authorization server is, a token for the wrong audience is
21
+ * 401, and a token too narrow for the tool it names is 403 `insufficient_scope`.
22
+ */
23
+ import { Hono } from "hono";
24
+ import type { CollectionConfig, DataDriver } from "@rebasepro/types";
25
+ import type { HonoEnv } from "../api/types.js";
26
+ export interface McpRoutesConfig {
27
+ /** The externally reachable origin. */
28
+ publicUrl: string;
29
+ /** Where this router is mounted. */
30
+ mcpPath: string;
31
+ /** Where the OAuth endpoints live, for the AS metadata document. */
32
+ oauthBasePath: string;
33
+ /** Resolved per request, because a driver may be swapped at runtime. */
34
+ getDriver(): DataDriver | undefined;
35
+ getCollections(): CollectionConfig[];
36
+ /** The server's own name and version, for `initialize`. */
37
+ serverInfo: {
38
+ name: string;
39
+ version: string;
40
+ };
41
+ }
42
+ /**
43
+ * The `.well-known` documents.
44
+ *
45
+ * Mounted at the ROOT, not under `basePath`, because RFC 8414 and RFC 9728 both
46
+ * define the path relative to the origin — the same reason `jwks.json` sits
47
+ * there. A client fetches these before it has any token, so they are public and
48
+ * uncredentialed by design.
49
+ */
50
+ export declare function createMcpWellKnownRoutes(config: McpRoutesConfig): Hono<HonoEnv>;
51
+ /** The MCP endpoint itself. */
52
+ export declare function createMcpRoutes(config: McpRoutesConfig): Hono<HonoEnv>;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The tools an authorized MCP client may call, and the identity they run as.
3
+ *
4
+ * The whole design is one sentence: **every database call in this file goes
5
+ * through `scopeDataDriver(driver, { uid, roles })` first**, so the connection
6
+ * runs as `rebase_user` with `app.uid` set to the person who consented, and the
7
+ * rows that come back are the rows that person's policies allow. The tools do
8
+ * not filter. The database does. That is the difference between this and every
9
+ * admin-key integration: there is no path here that can read a row its caller
10
+ * could not read through the application itself.
11
+ *
12
+ * Two consequences worth stating, because they look like bugs otherwise:
13
+ *
14
+ * - A tool call can return an empty list for a collection that plainly has
15
+ * rows. That is RLS working.
16
+ * - A write can fail with a permission error the tool cannot explain in
17
+ * detail, because the policy that refused it is not visible from here.
18
+ *
19
+ * `mcp:write` gates whether the mutating tools are *offered* at all. It is a
20
+ * second lock, not the main one — a `mcp:write` token still cannot write a row
21
+ * the user could not write themselves.
22
+ */
23
+ import type { CollectionConfig, DataDriver } from "@rebasepro/types";
24
+ /** The identity a tool call runs as. Comes from the verified access token. */
25
+ export interface McpCaller {
26
+ uid: string;
27
+ roles: string[];
28
+ scope: string;
29
+ clientId: string;
30
+ }
31
+ export interface McpToolContext {
32
+ driver: DataDriver;
33
+ collections: CollectionConfig[];
34
+ caller: McpCaller;
35
+ }
36
+ export interface McpToolDefinition {
37
+ name: string;
38
+ description: string;
39
+ inputSchema: Record<string, unknown>;
40
+ /** The scope a caller must hold for this tool to be listed or callable. */
41
+ requiredScope: "mcp:read" | "mcp:write";
42
+ run(args: Record<string, unknown>, ctx: McpToolContext): Promise<unknown>;
43
+ }
44
+ /** An error whose message is safe to hand back to the model. */
45
+ export declare class McpToolError extends Error {
46
+ }
47
+ export declare const MCP_TOOLS: McpToolDefinition[];
48
+ /** The tools a caller holding `scope` may see and call. */
49
+ export declare function toolsForScope(scope: string): McpToolDefinition[];
50
+ /** Look up a tool by name, honouring scope — an unlisted tool is not callable. */
51
+ export declare function findTool(name: string, scope: string): McpToolDefinition | undefined;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * What a token may do at `/mcp`.
3
+ *
4
+ * Two, and no more. RFC 9728 asks `scopes_supported` to be the minimal set that
5
+ * makes the resource usable, and the MCP specification's scope-minimization
6
+ * guidance points the same way: a client that only needs to read should be able
7
+ * to ask for exactly that, and a consent screen listing eleven fine-grained
8
+ * permissions is one nobody reads.
9
+ *
10
+ * They are not a substitute for RLS. A `mcp:read` token still reads through the
11
+ * user's own policies — the scope decides whether the tool exists for this
12
+ * session, the database decides which rows come back.
13
+ */
14
+ export declare const MCP_SCOPES: readonly ["mcp:read", "mcp:write"];
15
+ export type McpScope = (typeof MCP_SCOPES)[number];
16
+ /** The scope a client gets when it asks for nothing in particular. */
17
+ export declare const DEFAULT_MCP_SCOPE = "mcp:read";
18
+ /**
19
+ * The canonical resource identifier for this server's MCP endpoint.
20
+ *
21
+ * RFC 8707 §2 and the MCP specification agree on the shape: an absolute URI,
22
+ * lowercase scheme and host, no fragment, and — where the spec expresses a
23
+ * preference — no trailing slash. Clients send this back as the `resource`
24
+ * parameter and the token's audience is bound to it, so a value that disagrees
25
+ * with what the client computed rejects every token. It is worth being fussy.
26
+ */
27
+ export declare function canonicalResourceUri(publicUrl: string, mcpPath?: string): string;
28
+ /** The issuer identifier: the origin, with no path. */
29
+ export declare function issuerFor(publicUrl: string): string;
30
+ /**
31
+ * Where the protected-resource metadata for a given resource lives.
32
+ *
33
+ * RFC 9728 §3.1 inserts `.well-known/oauth-protected-resource` **between** the
34
+ * host and the resource's own path — `https://h/mcp` is described at
35
+ * `https://h/.well-known/oauth-protected-resource/mcp`, not at
36
+ * `…/mcp/.well-known/…`. Getting this backwards is a 404 the client reports as
37
+ * "no authorization server", which points at everything except the path.
38
+ */
39
+ export declare function protectedResourceMetadataPath(mcpPath?: string): string;
40
+ export interface ProtectedResourceMetadata {
41
+ resource: string;
42
+ authorization_servers: string[];
43
+ scopes_supported: string[];
44
+ bearer_methods_supported: string[];
45
+ resource_documentation?: string;
46
+ }
47
+ export declare function protectedResourceMetadata(publicUrl: string, mcpPath?: string): ProtectedResourceMetadata;
48
+ export interface AuthorizationServerMetadata {
49
+ issuer: string;
50
+ authorization_endpoint: string;
51
+ token_endpoint: string;
52
+ registration_endpoint: string;
53
+ revocation_endpoint: string;
54
+ revocation_endpoint_auth_methods_supported: string[];
55
+ scopes_supported: string[];
56
+ response_types_supported: string[];
57
+ grant_types_supported: string[];
58
+ code_challenge_methods_supported: string[];
59
+ token_endpoint_auth_methods_supported: string[];
60
+ authorization_response_iss_parameter_supported: boolean;
61
+ service_documentation?: string;
62
+ }
63
+ export declare function authorizationServerMetadata(publicUrl: string, oauthBasePath: string): AuthorizationServerMetadata;
64
+ /**
65
+ * Verify a PKCE code verifier against the challenge recorded at authorize time.
66
+ *
67
+ * RFC 7636 §4.6: BASE64URL(SHA256(ASCII(verifier))) === challenge, with the
68
+ * base64url unpadded. Only S256 is accepted — `plain` compares the verifier to
69
+ * itself, which protects against nothing once the code has leaked, and OAuth
70
+ * 2.1 removes it.
71
+ */
72
+ export declare function verifyPkce(verifier: string, challenge: string, method: string): Promise<boolean>;
73
+ /** Unpadded base64url, as every one of these RFCs means it. */
74
+ export declare function base64UrlEncode(bytes: Uint8Array): string;
75
+ /**
76
+ * Is `candidate` one of the client's registered redirect URIs?
77
+ *
78
+ * Exact string comparison, which is what OAuth 2.1 §4.1.2.1 requires and what
79
+ * closes open redirection. No prefix matching, no ignoring the query, no
80
+ * "same origin is close enough" — every one of those has been the published
81
+ * root cause of a token-stealing redirect, because an attacker who controls any
82
+ * path or parameter on a permitted origin controls where the code lands.
83
+ *
84
+ * The one concession is loopback: RFC 8252 §7.3 requires the port to be ignored
85
+ * for `http://127.0.0.1` and `http://[::1]`, because a native client binds an
86
+ * ephemeral port it cannot know at registration time. `localhost` is
87
+ * deliberately NOT included — RFC 8252 §8.3 recommends against it, since it
88
+ * resolves through a name service an attacker may influence.
89
+ */
90
+ export declare function redirectUriAllowed(candidate: string, registered: string[]): boolean;
91
+ /**
92
+ * Narrow a requested scope to what this server issues.
93
+ *
94
+ * An unknown scope is dropped rather than refused. RFC 6749 §3.3 allows either,
95
+ * and dropping is what keeps a client that asks for `openid profile email` out
96
+ * of habit from failing to connect at all — it gets the MCP scopes it is
97
+ * entitled to and none of the ones it invented.
98
+ */
99
+ export declare function narrowScope(requested: string | undefined | null): string;
100
+ /** Does a granted scope string carry this permission? */
101
+ export declare function scopeAllows(scope: string, needed: McpScope): boolean;
102
+ /**
103
+ * The `WWW-Authenticate` value for an unauthenticated request.
104
+ *
105
+ * RFC 6750 §3 plus the MCP specification's requirement that the challenge point
106
+ * at the protected-resource metadata: a client with no token discovers the
107
+ * entire authorization server from this one header, so an omitted
108
+ * `resource_metadata` is what makes a server look like it has no auth at all.
109
+ */
110
+ export declare function bearerChallenge(publicUrl: string, mcpPath: string, scope?: string): string;
111
+ /** The 403 challenge for a token that is valid but too narrow. */
112
+ export declare function insufficientScopeChallenge(publicUrl: string, mcpPath: string, needed: string, description: string): string;
113
+ /**
114
+ * Does the `resource` a client asked for name this server?
115
+ *
116
+ * RFC 8707 lets a client send a resource more specific than the one the server
117
+ * advertises, so this compares on origin plus path prefix rather than equality
118
+ * — `https://h/mcp` accepts `https://h/mcp` and rejects `https://h/mcpx` and
119
+ * anything on another origin. Rejecting is the point: a token minted for
120
+ * another audience must never be usable here, and the check that stops that
121
+ * starts at the authorize request.
122
+ */
123
+ export declare function resourceMatches(requested: string, canonical: string): boolean;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The OAuth 2.1 authorization server that issues tokens for `/mcp`.
3
+ *
4
+ * Three endpoints and a decision handler:
5
+ *
6
+ * POST /register RFC 7591 dynamic client registration
7
+ * GET /authorize the user-facing consent hop
8
+ * POST /authorize/decision what the consent page posts back
9
+ * POST /token code and refresh grants
10
+ *
11
+ * The one shape worth explaining up front is `/authorize/decision`. This server
12
+ * already has a login endpoint, a password policy, rate limits and whatever MFA
13
+ * the deployment configured, and duplicating any of that here would mean a
14
+ * second credential path with its own bugs. So the consent page never sees a
15
+ * password: it posts to the EXISTING `${basePath}/auth/login`, gets an ordinary
16
+ * session token back, and hands that to `/authorize/decision` as proof of who
17
+ * is consenting. This file authenticates a person exactly once, with the same
18
+ * function `/api/data` uses, and otherwise deals only in grants.
19
+ *
20
+ * The authorization request itself is carried across that hop as a signed,
21
+ * 10-minute token rather than as form fields. Without that, `/authorize/
22
+ * decision` would mint a code for whatever `client_id` and `redirect_uri` its
23
+ * caller typed — an open code-minting endpoint that skips every check
24
+ * `/authorize` performs.
25
+ */
26
+ import { Hono } from "hono";
27
+ import type { HonoEnv } from "../api/types.js";
28
+ import type { OAuthStore } from "./oauth-store.js";
29
+ export interface OAuthRoutesConfig {
30
+ store: OAuthStore;
31
+ /** The externally reachable origin of this deployment. */
32
+ publicUrl: string;
33
+ /** Where `/mcp` is mounted, e.g. `/mcp`. */
34
+ mcpPath: string;
35
+ /** Where the auth routes live, e.g. `/api/auth` — the consent page posts there. */
36
+ authBasePath: string;
37
+ /** Whether open registration is permitted. */
38
+ allowDynamicRegistration: boolean;
39
+ }
40
+ export declare function createOAuthRoutes(config: OAuthRoutesConfig): Hono<HonoEnv>;
41
+ /**
42
+ * Why a redirect URI cannot be registered, or null if it can.
43
+ *
44
+ * Refusing at registration is cheaper than refusing at authorize time, and it
45
+ * means a stored row is never itself the hazard.
46
+ */
47
+ export declare function redirectUriProblem(uri: string): string | null;
48
+ /** Human-readable consent lines, one per granted scope. */
49
+ export declare function describeScopes(scope: string): {
50
+ scope: string;
51
+ description: string;
52
+ }[];