@objectstack/types 17.1.0 → 17.3.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.
- package/CHANGELOG.md +1026 -0
- package/dist/index.d.mts +533 -9
- package/dist/index.d.ts +533 -9
- package/dist/index.js +309 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +293 -3
- package/dist/index.mjs.map +1 -1
- package/dist/node.d.mts +90 -11
- package/dist/node.d.ts +90 -11
- package/dist/node.js +213 -10
- package/dist/node.js.map +1 -1
- package/dist/node.mjs +215 -12
- package/dist/node.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/degraded-boot.ts","../src/env.ts","../src/error-leak.ts","../src/keyset-walk.ts","../src/module-not-found.ts","../src/response-envelope.ts","../src/thrown-http-error.ts","../src/validation-failure.ts","../src/relation-sub-object.ts","../src/unique-violation.ts","../src/unbacked-conflict-target.ts","../src/unique-scope-install-gate.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nexport * from './degraded-boot.js';\nexport * from './env.js';\nexport * from './error-leak.js';\n// Seek-based pagination for batch walks — the offset alternative that neither\n// skips rows when the walk mutates as it goes, nor costs O(n²/p) (#4363).\nexport * from './keyset-walk.js';\nexport * from './module-not-found.js';\nexport * from './response-envelope.js';\n// [#8016] The one rule for \"what HTTP answer does a THROWN error declare?\",\n// plus the validation-failure recogniser it reads. Both doors of\n// `/api/v1/packages` call it: the runtime dispatcher's `errorFromThrown` and the\n// direct-mount REST registrar, which used to answer 500 INTERNAL_ERROR for a\n// coded 4xx the dispatcher mapped correctly.\nexport * from './thrown-http-error.js';\nexport * from './validation-failure.js';\n// [#6615] The one home for Postgres' `«sub-object» \"x\" of relation \"y\"` phrase,\n// whose missing-COLUMN spelling contains a legal missing-TABLE phrase as a\n// substring. Three packages had each repaired that superstring hole separately.\nexport * from './relation-sub-object.js';\n// [#6250] The one named \"is this a unique-constraint violation?\" predicate.\n// Four hand-written vocabularies used to answer it and disagreed about MySQL,\n// which is why every MySQL conflict came back 500 instead of 409.\nexport * from './unique-violation.js';\n// [#8567] The OPPOSITE question, kept deliberately separate: \"is this the\n// database refusing an ON CONFLICT target that no unique index backs?\" One\n// measured limb per dialect that can raise it (SQLite, Postgres); MySQL cannot,\n// because knex compiles the conflict target away. Never merge the two — a\n// merged predicate reports a working constraint as a missing one.\nexport * from './unbacked-conflict-target.js';\n// [ADR-0120 D5e] The `isolated`-posture install gate for `'global'` uniques —\n// the pure enumerator both the hard stop (install seam) and the advisories\n// (`os doctor` / `os migrate plan`) read, so the three cannot drift apart.\nexport * from './unique-scope-install-gate.js';\n\n// Placeholder for Kernel interface to avoid circular dependency\n// The actual Kernel implementation will satisfy this interface.\nexport interface IKernel {\n // We can add specific methods here that plugins are allowed to call\n // forcing a stricter contract than exposing the whole class.\n ql?: any; // ObjectQL instance (optional to support initialization phase)\n start(): Promise<void>;\n // ... expose other needed public methods\n [key: string]: any; \n}\n\nexport interface RuntimeContext {\n engine: IKernel;\n}\n\nexport interface RuntimePlugin {\n name: string;\n install?: (ctx: RuntimeContext) => void | Promise<void>;\n onStart?: (ctx: RuntimeContext) => void | Promise<void>;\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Degraded-boot reporting, shared by every subsystem that can be told to boot\n * without a datasource it needs.\n *\n * Two of them exist today and they opt in through the *same* operator flag\n * (`OS_ALLOW_DRIVER_CONNECT_FAILURE`, see {@link resolveAllowDriverConnectFailure}):\n *\n * - `ObjectQLEngine.init()` — a boot-registered driver whose `connect()`\n * rejected (framework#3741).\n * - `DatasourceConnectionService` — a declared datasource that objects bind to\n * explicitly, or an `external` one with `validation.onMismatch:'fail'`,\n * that could not be connected (framework#3758).\n *\n * They live in different packages but owe the operator the same thing: the\n * degraded state must be impossible to miss.\n */\n\n/**\n * Emit the degraded-boot banner on a channel the host cannot accidentally\n * silence.\n *\n * `OS_ALLOW_DRIVER_CONNECT_FAILURE` only justifies itself if the state it opts\n * into is impossible to miss — and a logger-only banner is missable, because\n * the logger answers to a level the operator sets. `Logger.write()` returns\n * before emitting anything when the record is below `config.level`, so at\n * `--log-level error`, `fatal`, or `silent` this `warn` never reaches ANY\n * stream. A production host running at `error` is exactly the deployment this\n * flag exists for, and is exactly where the banner would vanish. Writing to\n * stderr as well is the same belt-and-braces the kernel already uses for\n * plugin startup failures.\n *\n * A second reason used to be load-bearing and no longer is: `os serve` blanked\n * ALL of stdout while the kernel booted, and `Logger` routes `warn` to stdout,\n * so a boot-phase banner was swallowed at every level. That was framework#4012\n * and is fixed — the boot window buffers and replays `warn`-and-above instead\n * of discarding it. Do not re-derive this helper's necessity from the\n * boot-quiet capture; the level filter is what keeps it alive.\n *\n * Best-effort and never throws: falls back to `console.error`, then to silence\n * on runtimes that have neither (the logger still carries the structured\n * record either way).\n */\nexport function emitDegradedBootBanner(message: string): void {\n const proc = (globalThis as {\n process?: { stderr?: { write?: (chunk: string) => unknown } };\n }).process;\n try {\n if (typeof proc?.stderr?.write === 'function') {\n proc.stderr.write(`${message}\\n`);\n return;\n }\n } catch {\n /* stderr unavailable / closed — fall through to console */\n }\n try {\n (globalThis as { console?: { error?: (msg: string) => void } }).console?.error?.(message);\n } catch {\n /* no output channel at all — the logger record is the remaining trace */\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Environment-variable helpers shared across `@objectstack/*` packages.\n *\n * The framework standardises on `OS_*` prefixed env vars (see AGENTS.md\n * \"Environment Variables\" section). Some historical names predate this\n * convention — `AUTH_SECRET`, `ROOT_DOMAIN`, `OBJECTSTACK_*`, …\n *\n * To migrate without breaking user `.env` files mid-release, call\n * {@link readEnvWithDeprecation} at every legacy read site:\n *\n * const v = readEnvWithDeprecation('OS_AUTH_SECRET', 'AUTH_SECRET');\n *\n * If only the legacy name is set, the value is still returned but a\n * one-shot `console.warn` fires (per-process per-variable) telling\n * operators to rename it.\n */\n\nimport {\n normalizeTenancyPosture,\n TENANCY_POSTURES,\n type TenancyPosture,\n} from '@objectstack/spec/security';\n\nconst _warnedKeys = new Set<string>();\n\n/**\n * Read an env var, preferring the canonical `OS_*` name and falling\n * back to one or more legacy aliases.\n *\n * When only a legacy alias is set, emits a one-shot deprecation warning.\n * The warning is process-wide deduplicated: identical (preferred, legacy)\n * pairs will only warn once even if read from multiple call sites.\n *\n * Legacy aliases are checked in order; the first one with a defined\n * value wins (and triggers the warning for that specific alias).\n *\n * Safe to call from environments where `process` is unavailable (returns\n * `undefined`); the warning is suppressed when running outside Node-like\n * runtimes that lack `console.warn`.\n *\n * @param preferred Canonical OS_*-prefixed env var name.\n * @param legacy Older name (or array of older names) to fall back on.\n * @param options Optional behaviour flags. Set `silent: true` for aliases\n * that remain accepted conventions rather than true legacy\n * names — e.g. `PORT`, which PaaS platforms (Render, Railway,\n * Heroku, Fly, …) inject automatically. Warning on those\n * would nag operators about env they never set.\n * @returns The resolved value, or `undefined` if neither is set.\n */\nexport function readEnvWithDeprecation(\n preferred: string,\n legacy: string | readonly string[],\n options?: { silent?: boolean },\n): string | undefined {\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } })\n .process?.env;\n if (!env) return undefined;\n\n const preferredValue = env[preferred];\n if (preferredValue !== undefined) return preferredValue;\n\n const legacyList = typeof legacy === 'string' ? [legacy] : legacy;\n for (const legacyName of legacyList) {\n const legacyValue = env[legacyName];\n if (legacyValue !== undefined) {\n const dedupeKey = `${preferred}|${legacyName}`;\n if (!options?.silent && !_warnedKeys.has(dedupeKey)) {\n _warnedKeys.add(dedupeKey);\n const consoleRef = (globalThis as { console?: { warn?: (msg: string) => void } }).console;\n try {\n consoleRef?.warn?.(\n `[ObjectStack] Env var \\`${legacyName}\\` is deprecated; rename it to \\`${preferred}\\`. ` +\n `The legacy name still works for now but will be removed in a future major release.`,\n );\n } catch {\n /* `console.warn` unavailable (exotic runtime) — ignore */\n }\n }\n return legacyValue;\n }\n }\n\n return undefined;\n}\n\n/**\n * Read the LEGACY `OS_MULTI_ORG_ENABLED` boolean.\n *\n * ⚠️ **[ADR-0105 D1] DEMOTED — not the knob to gate on.** `OS_TENANCY_POSTURE`\n * superseded this flag and is the authoritative one;\n * {@link resolveTenancyPosture} is where the two are reconciled (posture when\n * set, else this boolean). This function only reports the legacy input, so a\n * deployment that sets ONLY the canonical `OS_TENANCY_POSTURE` reads `false`\n * here while genuinely running a walled multi-organization posture.\n *\n * **Answering \"is this deployment multi-org?\" with this function is a bug.**\n * Ask the posture instead — `postureEnforcesWall(resolveTenancyPosture())`\n * (`@objectstack/spec/security`) — or, inside a running kernel, the `tenancy`\n * service, which additionally knows whether the requested wall is actually\n * ENFORCED (ADR-0093 D4/D5). Two shipped defects came from gating on this\n * boolean after the demotion: cloud#1020 (the EE licence gate) and #5233\n * (`organization/create` 403'd on a posture-only deployment whose organization\n * wall was fully mounted — the guided \"create your workspace\" path dead-ended).\n * The sentence this paragraph replaced actively instructed both.\n *\n * Legitimate remaining callers are the ones that specifically mean *the legacy\n * input*: {@link resolveTenancyPosture}'s own back-compat fallback, and\n * back-compat/reporting surfaces that must echo what the operator typed.\n *\n * Resolution: `OS_MULTI_ORG_ENABLED`; else `false`. Any value other than a\n * case-insensitive `'false'` enables it. (The legacy `OS_MULTI_TENANT` alias was\n * removed in 11.0.)\n *\n * Reads `process.env` live on each call; memoise at the call site if the\n * result must be stable for the process lifetime.\n */\nexport function resolveMultiOrgEnabled(): boolean {\n const raw = readEnvWithDeprecation('OS_MULTI_ORG_ENABLED', []);\n return String(raw ?? 'false').toLowerCase() !== 'false';\n}\n\n/**\n * [ADR-0105 D1] Resolve the deployment's REQUESTED tenancy posture —\n * `single` | `group` | `isolated`.\n *\n * `OS_TENANCY_POSTURE` is the canonical knob and generalizes the boolean\n * `OS_MULTI_ORG_ENABLED` it supersedes:\n *\n * - set → that posture (the legacy spelling `multi` normalizes to `isolated`)\n * - unset → derived from `OS_MULTI_ORG_ENABLED`: `true` ⇒ `isolated`, else `single`\n *\n * so every existing deployment keeps its current posture with no config change.\n *\n * An unrecognized value THROWS rather than falling back. A typo'd posture that\n * quietly resolved to `single` would silently remove the organization wall —\n * the deployment-layer form of the \"declared but unenforced\" defect ADR-0049\n * forbids, and the same reasoning behind ADR-0093 D5's refusal to boot into\n * undeclared degradation.\n *\n * This resolves what the operator ASKED FOR. Whether the posture is actually\n * enforced is the `tenancy` service's answer (`isolationActive` / `degraded`).\n */\nexport function resolveTenancyPosture(): TenancyPosture {\n // Read through `globalThis` like `readEnvWithDeprecation` does — this package\n // targets non-Node runtimes too, where a bare `process` reference throws.\n const raw = (globalThis as { process?: { env?: Record<string, string | undefined> } })\n .process?.env?.OS_TENANCY_POSTURE;\n if (raw != null && String(raw).trim() !== '') {\n const posture = normalizeTenancyPosture(raw);\n if (!posture) {\n throw new Error(\n `Invalid OS_TENANCY_POSTURE=${JSON.stringify(String(raw))}. ` +\n `Expected one of: ${TENANCY_POSTURES.join(', ')} (or the legacy alias 'multi' = 'isolated'). ` +\n 'Refusing to boot rather than silently falling back to a posture with no organization wall.',\n );\n }\n return posture;\n }\n return resolveMultiOrgEnabled() ? 'isolated' : 'single';\n}\n\n/**\n * Escape hatch for the degraded-tenancy boot guard (ADR-0093 D5).\n *\n * When `OS_MULTI_ORG_ENABLED=true` but the enterprise `@objectstack/organizations`\n * package cannot provide tenant isolation, the platform refuses to boot — a\n * deployment that asked for tenant isolation must not serve traffic pretending\n * to have it (ADR-0049 at the deployment layer). Setting this to a truthy value\n * (`true`/`1`/`on`/`yes`, case-insensitive) boots anyway in an explicitly\n * *degraded* state that is branded everywhere an operator looks. Defaults OFF —\n * an unset flag means \"fail fast\".\n */\nexport function resolveAllowDegradedTenancy(): boolean {\n const raw = readEnvWithDeprecation('OS_ALLOW_DEGRADED_TENANCY', [], { silent: true });\n if (raw == null) return false;\n return ['1', 'true', 'on', 'yes'].includes(String(raw).trim().toLowerCase());\n}\n\n/**\n * Escape hatch for the driver-connect boot guard (framework#3741).\n *\n * `ObjectQLEngine.init()` connects every boot-registered driver and, by\n * default, refuses to boot when any of them fails — a server whose database is\n * unreachable must not report itself started and then 500 every request with an\n * error that reads nothing like \"the database is down\". Failing there is also\n * what gives a driver the ability to REFUSE STARTUP at all: any fatal startup\n * check a driver wants to run (licence, server version, incompatible\n * configuration, missing capability) can simply throw from `connect()`.\n *\n * Setting this to a truthy value (`true`/`1`/`on`/`yes`, case-insensitive)\n * boots anyway, in an explicitly degraded state that is logged loudly at\n * startup. Every query routed to a failed driver fails until the datasource\n * becomes reachable — the underlying clients do re-establish connections on\n * their own (framework#3759) — but the boot-time schema sync those drivers\n * missed is never re-run, so their tables may simply not exist afterwards.\n * Defaults OFF — an unset flag means \"fail fast\".\n */\nexport function resolveAllowDriverConnectFailure(): boolean {\n const raw = readEnvWithDeprecation('OS_ALLOW_DRIVER_CONNECT_FAILURE', [], { silent: true });\n if (raw == null) return false;\n return ['1', 'true', 'on', 'yes'].includes(String(raw).trim().toLowerCase());\n}\n\n/**\n * Escape hatch for plugin-dev's production boot guard (ADR-0115 D6, #3900).\n *\n * `DevPlugin.init()` refuses to run under `NODE_ENV=production`: the stack it\n * assembles is built around an auth secret published inside the npm package and\n * an in-memory driver with persistence off, neither of which a production\n * deployment should acquire by accident. Setting this to a truthy value\n * (`true`/`1`/`on`/`yes`, case-insensitive) boots anyway, in an explicitly\n * degraded state that is branded in the boot log and on the ready banner.\n * Defaults OFF — an unset flag means \"fail fast\".\n *\n * Lives here rather than as a bare `process.env[…] === '1'` inside plugin-dev so\n * that the whole `OS_ALLOW_*` family answers to one truthy vocabulary: the\n * strict `=== '1'` it replaced fails CLOSED on `OS_ALLOW_DEV_PLUGIN=true`, which\n * is safe but reads to an operator as the flag being broken.\n */\nexport function resolveAllowDevPlugin(): boolean {\n const raw = readEnvWithDeprecation('OS_ALLOW_DEV_PLUGIN', [], { silent: true });\n if (raw == null) return false;\n return ['1', 'true', 'on', 'yes'].includes(String(raw).trim().toLowerCase());\n}\n\n/**\n * SINGLE decision point for \"is the MCP HTTP surface (`/api/v1/mcp`) on?\".\n *\n * MCP is a core platform capability and defaults ON: an unset\n * `OS_MCP_SERVER_ENABLED` means the surface is served. Operators opt OUT with\n * an explicit falsy value (`false`/`0`/`off`/`no`, case-insensitive); any\n * other value — including the historical `true` — keeps it on.\n *\n * Every consumer of the flag — the runtime dispatcher's `/mcp` route gate,\n * the CLI's MCP plugin auto-load, the REST `/discovery` advertisement, and\n * the auth service's OAuth/DCR follow-defaults — MUST call this instead of\n * re-reading the env, so the served route, the advertised route, and the\n * authorization track can never disagree.\n *\n * Note the asymmetry with the MCP plugin's *stdio* auto-start\n * ({@link resolveMcpStdioAutoStart}), which stays opt-in and is gated by a\n * SEPARATE switch: attaching a long-lived stdio transport to every process is\n * a side effect no default should impose, while the HTTP surface is served\n * statelessly per-request.\n */\nexport function isMcpServerEnabled(): boolean {\n const raw = readEnvWithDeprecation('OS_MCP_SERVER_ENABLED', 'MCP_SERVER_ENABLED', {\n silent: true,\n });\n if (raw == null) return true;\n return !['0', 'false', 'off', 'no'].includes(raw.trim().toLowerCase());\n}\n\n/**\n * SINGLE decision point for \"should the MCP plugin auto-start a long-lived\n * (stdio) transport?\" — distinct from {@link isMcpServerEnabled}, which governs\n * the stateless HTTP surface.\n *\n * The stdio transport is a different, stricter posture: the plugin bridges the\n * RAW metadata service + data engine onto the long-lived server with NO\n * per-request principal (unscoped — see the `mcp-stdio-authority` conformance\n * row), so it is safe only as a single-operator LOCAL tool and MUST stay\n * opt-in. It defaults OFF.\n *\n * Canonical switch: `OS_MCP_STDIO_ENABLED` (truthy). The plugin also starts it\n * when constructed with `{ autoStart: true }` (that path is checked by the\n * caller, not here).\n *\n * DEPRECATED alias: `OS_MCP_SERVER_ENABLED=true` historically ALSO started\n * stdio — overloading the very var that gates the HTTP surface, so an operator\n * setting it to \"make sure MCP is on\" silently attached an unscoped transport.\n * That trigger still works (with a one-time warning from the caller) for one\n * release; prefer the dedicated var. Note `OS_MCP_SERVER_ENABLED=false` only\n * ever gated the HTTP surface and never started stdio, so it is unaffected.\n *\n * @returns `enabled` — whether stdio auto-start is requested by the env; and\n * `viaDeprecatedAlias` — whether it came through the legacy\n * `OS_MCP_SERVER_ENABLED=true` trigger (so the caller can warn once).\n */\nexport function resolveMcpStdioAutoStart(): { enabled: boolean; viaDeprecatedAlias: boolean } {\n const stdio = readEnvWithDeprecation('OS_MCP_STDIO_ENABLED', [], { silent: true });\n if (stdio != null && ['1', 'true', 'on', 'yes'].includes(stdio.trim().toLowerCase())) {\n return { enabled: true, viaDeprecatedAlias: false };\n }\n // Legacy trigger: only the literal `true` ever started stdio (preserved\n // exactly). `OS_MCP_SERVER_ENABLED=false`/other values never did.\n const legacy = readEnvWithDeprecation('OS_MCP_SERVER_ENABLED', 'MCP_SERVER_ENABLED', { silent: true });\n if (legacy != null && legacy.trim().toLowerCase() === 'true') {\n return { enabled: true, viaDeprecatedAlias: true };\n }\n return { enabled: false, viaDeprecatedAlias: false };\n}\n\n/**\n * Maximum number of organizations a single user may CREATE, from `OS_ORG_LIMIT`.\n * The auth plugin forwards this as better-auth's `organizationLimit` in function\n * form, counting only the caller's `role=owner` memberships — so it caps\n * self-created orgs (each of which can auto-provision a free environment on the\n * cloud control plane) without penalising a user invited into many orgs.\n *\n * Only meaningful under a posture that enforces an organization wall, i.e.\n * `postureEnforcesWall({@link resolveTenancyPosture}())` — NOT the demoted\n * `resolveMultiOrgEnabled()` boolean (ADR-0105 D1, #5233).\n * Returns `undefined` when unset or non-positive → no limit (better-auth treats\n * an absent `organizationLimit` as unlimited), preserving self-host behaviour.\n * Deployments that let users self-create orgs SHOULD set a generous cap.\n */\nexport function resolveOrgLimit(): number | undefined {\n const raw = readEnvWithDeprecation('OS_ORG_LIMIT', [], { silent: true });\n if (raw == null || String(raw).trim() === '') return undefined;\n const n = Number.parseInt(String(raw), 10);\n return Number.isFinite(n) && n > 0 ? n : undefined;\n}\n\n/**\n * SINGLE decision point for \"is pinyin search recall on?\" (#2486).\n *\n * Pinyin search is a deployment/locale-level capability, not field metadata:\n * Chinese deployments want it, pure-Japanese/English deployments don't. The\n * flag gates the whole feature end-to-end — the SchemaRegistry's compile-time\n * `__search` companion-column seam AND the `plugin-pinyin-search` populate\n * hooks — so there is no half-state where a column exists but nobody fills it\n * (ADR-0049: no declared-but-unenforced capability).\n *\n * Resolution:\n * 1. An explicit `OS_SEARCH_PINYIN_ENABLED` always wins — truthy\n * (`1`/`true`/`on`/`yes`) enables, anything else disables.\n * 2. When unset, the default derives from the deployment's configured\n * locales (`opts.locales`, e.g. the stack's `i18n.defaultLocale` +\n * `supportedLocales`): any `zh-*` locale turns it on.\n * 3. No env var and no `zh-*` locale → off. OSS / non-Chinese deployments\n * never load `pinyin-pro` and pay zero compute cost.\n *\n * Hosts that know the stack's i18n config — the CLI `serve` boot path AND the\n * standalone artifact boot (`createStandaloneStack`, which `os migrate`\n * plan/apply and embedders go through) — resolve once with locales and stamp\n * the decision back into the env via {@link stampSearchPinyinEnabled}, so\n * downstream consumers constructed without config access (per-engine\n * SchemaRegistry) read the same answer via the no-arg form (#3955).\n */\nexport function resolveSearchPinyinEnabled(opts?: { locales?: readonly string[] }): boolean {\n const raw = readEnvWithDeprecation('OS_SEARCH_PINYIN_ENABLED', [], { silent: true });\n if (raw != null && String(raw).trim() !== '') {\n return ['1', 'true', 'on', 'yes'].includes(String(raw).trim().toLowerCase());\n }\n return (opts?.locales ?? []).some((l) => /^zh([-_]|$)/i.test(String(l ?? '').trim()));\n}\n\n/**\n * The locales a stack's `i18n` config declares — `defaultLocale`,\n * `fallbackLocale`, then `supportedLocales`. Accepts the config loosely typed\n * (`unknown`) so any boot path can pass whatever its stack config or compiled\n * artifact carries without importing spec schemas; non-string entries and a\n * non-object config collapse to `[]`.\n */\nexport function collectConfiguredLocales(i18n: unknown): string[] {\n const cfg = (i18n && typeof i18n === 'object' ? i18n : {}) as {\n defaultLocale?: unknown;\n fallbackLocale?: unknown;\n supportedLocales?: unknown;\n };\n return [\n cfg.defaultLocale,\n cfg.fallbackLocale,\n ...(Array.isArray(cfg.supportedLocales) ? cfg.supportedLocales : []),\n ].filter((l): l is string => typeof l === 'string');\n}\n\n/**\n * Resolve the pinyin-search decision from a stack's `i18n` config and stamp a\n * positive result back into `OS_SEARCH_PINYIN_ENABLED` (#2486, #3955).\n *\n * Every boot path that SEES the stack config must stamp, because consumers\n * constructed later without config access (each engine's `SchemaRegistry`\n * provisioning the `__search` companion column, the `plugin-pinyin-search`\n * gate) read the decision through the no-arg\n * {@link resolveSearchPinyinEnabled}. A boot path that skips the stamp\n * computes a schema view WITHOUT the companion columns — which is how\n * `os migrate` came to flag the dev runtime's live `__search` columns as\n * destructive orphans (#3955). Call sites: the CLI `serve`/`dev` boot\n * (`objectstack.config.ts`) and `createStandaloneStack` (compiled artifact —\n * `os migrate plan`/`apply`, embedders).\n *\n * An explicit `OS_SEARCH_PINYIN_ENABLED` always wins — the resolver reads it\n * before consulting locales, so the stamp only materializes the\n * locale-derived default. Only a positive decision is written: \"unset\" and\n * \"off\" read identically through the no-arg resolver, and leaving the var\n * untouched keeps a later boot free to re-derive from ITS config.\n */\nexport function stampSearchPinyinEnabled(i18n: unknown): boolean {\n const enabled = resolveSearchPinyinEnabled({ locales: collectConfiguredLocales(i18n) });\n // Write through `globalThis` like `readEnvWithDeprecation` reads — this\n // package has no Node type dependency (edge-safe); no env object → no stamp.\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } })\n .process?.env;\n if (enabled && env) env.OS_SEARCH_PINYIN_ENABLED = 'true';\n return enabled;\n}\n\n/**\n * SINGLE decision point for a sandbox script-runner DEFAULT (ms), resolved from\n * the environment (framework#3259 / ADR-0102).\n *\n * The QuickJS sandbox meters each hook/action invocation against a per-invocation\n * budget. Two dimensions are env-tunable:\n * - the **CPU-time budget** for hooks / actions — how much *VM-active* time a\n * body may burn (built-in 250ms hooks / 5000ms actions); and\n * - the **wall-clock ceiling** — the backstop bounding a body parked forever on\n * a host call that never settles (built-in 30_000ms).\n *\n * The built-in defaults suit a warm, idle host; a heavily loaded or slow host\n * (an oversubscribed CI runner, constrained production hardware) may need a\n * higher floor. This lets an operator raise it once, deployment-wide, instead of\n * re-tuning every call site.\n *\n * Canonical vars (OS_{DOMAIN}_{NAME}, DOMAIN=SANDBOX):\n * - hook → `OS_SANDBOX_HOOK_TIMEOUT_MS`\n * - action → `OS_SANDBOX_ACTION_TIMEOUT_MS`\n * - wallCeiling → `OS_SANDBOX_WALL_CEILING_MS`\n *\n * Only a positive integer is honored; unset / empty / non-numeric / non-positive\n * falls back to `fallback`, so behaviour is byte-for-byte unchanged when the var\n * is absent. This is a FALLBACK default ONLY: an explicit constructor option\n * still wins over it, and (for the CPU budget) a body's own declared `timeoutMs`\n * still wins over the resolved default per the runner's resolution rule.\n */\nexport function resolveSandboxTimeoutMs(\n kind: 'hook' | 'action' | 'wallCeiling',\n fallback: number,\n): number {\n const name =\n kind === 'hook'\n ? 'OS_SANDBOX_HOOK_TIMEOUT_MS'\n : kind === 'action'\n ? 'OS_SANDBOX_ACTION_TIMEOUT_MS'\n : 'OS_SANDBOX_WALL_CEILING_MS';\n const raw = readEnvWithDeprecation(name, [], { silent: true });\n if (raw == null || String(raw).trim() === '') return fallback;\n const n = Number.parseInt(String(raw).trim(), 10);\n return Number.isFinite(n) && n > 0 ? n : fallback;\n}\n\n/**\n * Internal: clear the dedupe set. Test-only; exposed so suite-wide\n * deprecation warnings don't bleed between tests.\n *\n * @internal\n */\nexport function _resetEnvDeprecationWarnings(): void {\n _warnedKeys.clear();\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Shared \"does this error message leak server internals?\" heuristic (#3867).\n *\n * ObjectStack has more than one HTTP boundary. `@objectstack/rest` guards the\n * REST data routes inside `mapDataError`; the dispatcher-plugin routes\n * (`/analytics`, `/packages`, `/i18n`, `/automation`, …) exit\n * through `errorResponseBase`. Before #3867 only the first of those sanitised\n * anything, so a driver error raised under `/analytics/query` reached the\n * client verbatim — a real SQL statement in the response body:\n *\n * ```\n * {\"success\":false,\"error\":{\"message\":\"SELECT FROM \\\"sqlite_sequence\\\" - near \\\"FROM\\\": syntax error\",\"code\":500}}\n * ```\n *\n * \"Do not ship driver internals to clients\" is a property of the HTTP\n * boundary, not of one router, so the predicate lives here — the package both\n * `@objectstack/rest` and `@objectstack/runtime` already depend on — and each\n * boundary applies it in its own envelope. One heuristic, one place to widen\n * when a new dialect's phrasing shows up.\n *\n * Deliberately a *heuristic over the message*, not a driver taxonomy: these\n * errors arrive as plain `Error`s from a half-dozen dialects with no shared\n * shape. It is applied only where the outcome is already a 5xx, so a false\n * positive costs a caller nothing but detail on a response that was a server\n * fault anyway — while the full text still reaches server logs and the\n * error reporter.\n *\n * [#5811] {@link declaresServerFault} joins it here for the same reason and\n * answers the other half of the question: the heuristic asks whether a message\n * *sounds* internal, the declaration asks whether the producer *said so*.\n */\n\n/** Generic replacement text for a message that trips {@link looksLikeInternalErrorLeak}. */\nexport const INTERNAL_ERROR_MESSAGE = 'Internal server error';\n\n/**\n * [#8132, #8739] The dialect phrasings this list COVERS — the SQLite family,\n * Postgres and MySQL/MariaDB — each anchored on the driver's own errmsg\n * template rather than on its tail. Coverage, not a census of what this repo\n * runs: see \"What this list covers, and what it does not\" below, which is the\n * load-bearing half for anyone sizing a disclosure residual.\n *\n * The gap that forced these: the keyword set below caught SQLite's\n * `SQLITE_ERROR: no such table: sys_metadata` through the `sqlite_` limb, while\n * the Postgres phrasing of *the same condition* —\n * `relation \"sys_metadata\" does not exist` — matched nothing and shipped a\n * physical table name to the client from every boundary that applies the\n * predicate.\n *\n * **Why anchored, and never on the bare tail.** `does not exist` is ordinary\n * business English: \"user does not exist\", \"record does not exist\". Matching\n * that substring would replace legitimate answers with `Internal server error`,\n * so each pattern requires what the DRIVER always emits and prose usually does\n * not — a quoted identifier, or the trailing colon of SQLite's template. The\n * negative cases in `error-leak.test.ts` pin that distinction.\n *\n * **What this list covers, and what it does not.** The module note above argues\n * against growing a driver taxonomy, and that reason still holds on its own: a\n * phrasing list is unbounded *across dialects*, because every dialect spells\n * every one of these conditions its own way. So these entries are a COVERAGE\n * statement, not a census — the two spellings #8132 measured the gap on, plus\n * the three MySQL templates #8739 added — and {@link declaresServerFault}\n * remains the answer that does not depend on phrasing at all.\n *\n * **Covered as of #8739: MySQL/MariaDB.** Under the maintainer's 2026-08-15\n * ruling on #8739, MySQL is a SUPPORTED DEPLOYMENT TARGET, not merely a tested\n * dialect — the answer the published surface already implied\n * (`OS_DATABASE_DRIVER=mysql` is a documented deployment knob, `MysqlConfig` is\n * authorable datasource config, `types.mdx` specifies per-field MySQL DDL) and\n * the one CI's required live-MySQL check already behaves as if. A supported\n * target's driver text reaches these boundaries in production, so its\n * templates belong here. Three are covered, one per condition the other two\n * dialects are already covered for:\n *\n * - `Table 'app.t' doesn't exist` (ER_NO_SUCH_TABLE 1146) — the missing-object\n * condition SQLite spells `no such table:` and Postgres spells\n * `relation \"t\" does not exist`.\n * - `Unknown column 'c' in 'field list'` (ER_BAD_FIELD_ERROR 1054) — the same\n * condition for a column. The clause name varies (`field list`,\n * `where clause`, `order clause`, `on clause`) and is REQUIRED by the\n * pattern; it is what separates the driver's template from prose.\n * - `Duplicate entry 'x' for key 'i'` (ER_DUP_ENTRY 1062) — the\n * unique-violation condition the `constraint failed` / `unique constraint`\n * keyword limbs already catch for SQLite and Postgres and cannot catch here,\n * because MySQL's spelling shares no word with either. It is also the ONLY\n * one of the three whose text embeds a CALLER'S VALUE rather than an\n * identifier, which is what made the pre-#8739 gap worth closing rather than\n * documenting.\n *\n * ⛔ Adding this limb does NOT re-open #6250's decision one package over.\n * `@objectstack/rest` answers the 409 conflict question with\n * `isUniqueViolationError` (`unique-violation.ts`), ABOVE and independently of\n * this predicate, precisely so a disclosure rule never decides a status. That\n * ordering is what keeps the two unentangled now that both recognise the same\n * MySQL sentence; `rest-unique-violation-dialects.test.ts` pins it.\n *\n * ⚠️ **This list's silence is STILL NOT evidence that a dialect is\n * unreachable, and MySQL is why the warning is worded that way.** Until #8739\n * this paragraph said \"nobody here runs\" MySQL/MSSQL/Oracle, and a reviewer\n * sizing a disclosure residual read it as one. It was false for MySQL,\n * measurably, on the same tree:\n *\n * - `driver-sql` branches on `mysql`/`mysql2` — the `isMysql` getter,\n * `withUtcSession`, and the `dialect === 'mysql'` arms of\n * `textMatchPredicate` / `likePatternPredicate`.\n * - CI stands up a live `mysql:8.0` service for the job named\n * `Temporal Conformance (live PG + MySQL)`, which IS a required check, and\n * its `OS_EXPECT_LIVE_DIALECT_MATRIX` flag turns a missing MySQL URL into a\n * named red rather than a quiet skip.\n * - Live MySQL 8.0.46 measurements produced merged driver fixes (#8621,\n * #8622), and `unique-violation.ts` — one file over — names sqlite /\n * postgres / mysql as the three dialect families `sql-driver.ts` recognises.\n *\n * The rule that outlives any particular dialect: **a `false` from this\n * predicate means UNCOVERED, never \"safe\"**, and the reachability of an\n * uncovered dialect is a separate question this file cannot answer. MSSQL and\n * Oracle are uncovered today — `Invalid object name 'sys_metadata'.`,\n * `ORA-00942: table or view does not exist` both return FALSE — and that is a\n * statement about this list, not about them; `error-leak.test.ts` pins those\n * two as the standing example so the distinction keeps a live subject.\n * {@link declaresServerFault} is the phrasing-independent answer, and\n * `metadata-protocol`'s `protocol.driver-text-disclosure.test.ts` is the worked\n * demonstration that a producer which withholds by DECLARATION needs no dialect\n * list at all.\n *\n * ⛔ **What is deliberately NOT added here, and why.** MySQL's ACL family\n * (`Access denied for user 'u'@'h' to database 'd'`, ER_DBACCESS_DENIED_ERROR\n * 1044; `SELECT command denied to user … for table 't'`,\n * ER_TABLEACCESS_DENIED_ERROR 1142) is the counterpart of the Postgres\n * `permission denied for table` limb above and is NOT covered: nothing in this\n * repo has raised one off a live server, and `unique-violation.ts`' standing\n * rule for this neighbourhood is that a dialect's spelling is added when it has\n * been MEASURED off a thrown error, never on a plausible reading of the\n * dialect's manual. `Access denied` also collides with this platform's own\n * security prose (`[Security] Access denied: …`, pinned as a negative case), so\n * a guessed pattern here is the over-match direction, which suppresses\n * diagnostics an operator needs. Measure one, then add it.\n *\n * ⚠️ Related but NOT reusable: `relation-sub-object.ts` owns the same Postgres\n * sentence for two other questions (which column? / is this a sub-object?), and\n * its note warns that its two widths must never be collapsed. Neither answers\n * \"is this a leak\", and its central problem does not arise here: a message like\n * `column \"label\" of relation \"sys_team\" does not exist` contains a complete\n * missing-TABLE phrase as a substring, which is a hazard when you are deciding\n * WHICH object is missing and a non-issue when the verdict is \"leak\" either way.\n * That is why this asks its own question with its own patterns.\n */\nconst DIALECT_LEAK_PHRASINGS: readonly RegExp[] = [\n // Postgres 42P01 / 42703 (and, as a superstring, the `… of relation \"…\"`\n // sub-object family: 42704 and friends). The quotes are required because\n // Postgres always emits them here.\n /\\b(?:relation|column)\\s+[\"'`][^\"'`]+[\"'`]\\s+does not exist/i,\n // Postgres 42501. Restricted to physical object kinds: `schema`, `view`,\n // `function` and `column` are all ObjectStack AUTHORING vocabulary, so a\n // product message could legitimately use them and a miss is the cheap\n // direction (the outcome is already a 5xx).\n /\\bpermission denied for (?:table|relation|sequence|database)\\b/i,\n // SQLite/libsql, message-only form. The `sqlite_` limb below catches these\n // only when the driver prefixed its code; `better-sqlite3` and libsql both\n // raise them bare, which is the shape measured across this repo.\n /\\bno such (?:table|column):/i,\n // [#8739] MySQL/MariaDB ER_NO_SUCH_TABLE (1146): `Table 'app.t' doesn't\n // exist`. Its own template, not a spelling of the Postgres one — MySQL\n // contracts the verb and quotes `db.table` as a single identifier — so the\n // `relation|column … does not exist` limb above cannot reach it. The quotes\n // are required for the same reason they are there: the driver always emits\n // them and prose about a table usually does not.\n /\\btable\\s+[\"'`][^\"'`]+[\"'`]\\s+doesn't exist/i,\n // [#8739] MySQL/MariaDB ER_BAD_FIELD_ERROR (1054): `Unknown column 'c' in\n // 'field list'`. BOTH quoted parts are required. The second is MySQL's clause\n // name — `field list`, `where clause`, `order clause`, `on clause` — and it\n // is the half that makes this the driver's template rather than a sentence\n // that merely calls a column unknown, which an import or mapping feature has\n // every right to say.\n /\\bunknown column\\s+[\"'`][^\"'`]+[\"'`]\\s+in\\s+[\"'`][^\"'`]+[\"'`]/i,\n // [#8739] MySQL/MariaDB ER_DUP_ENTRY (1062): `Duplicate entry\n // 'acme@example.com' for key 'crm_account.email'`. `for key` + a quoted index\n // is the anchor; the VALUE half is matched loosely and lazily because it is\n // the caller's own text and MySQL does not escape a quote inside it\n // (`Duplicate entry 'O'Brien' for key 'i'` is a real shape). A bare\n // `duplicate entry` with no `for key '…'` tail is not this template and is\n // left alone.\n /\\bduplicate entry\\s+[\"'`].*?[\"'`]\\s+for key\\s+[\"'`][^\"'`]+[\"'`]/i,\n];\n\n/**\n * Whether `message` looks like a raw SQL statement or driver/engine dump that\n * must not be returned to an API client.\n *\n * Matches: dialect error codes (`SQLSTATE`, `sqlite_*`), bare statements\n * (a message that *starts* as `SELECT`/`INSERT INTO`/`UPDATE`/`DELETE FROM` —\n * drivers prefix the offending SQL to their message), constraint-violation\n * dumps, which name physical tables and columns, and the\n * {@link DIALECT_LEAK_PHRASINGS} the list covers — the SQLite family, Postgres\n * and, since #8739, MySQL/MariaDB. A dialect outside that coverage (MSSQL and\n * Oracle are the standing examples) makes this return FALSE without meaning the\n * text is safe; read {@link DIALECT_LEAK_PHRASINGS}' note before sizing\n * anything on a `false`.\n *\n * Does NOT match ordinary business or validation messages, which is why the\n * statement forms are anchored with `startsWith` and the dialect phrasings on\n * the driver's template: a legitimate message may *mention* \"update\", or say\n * \"does not exist\" about a business record, without being either.\n */\nexport function looksLikeInternalErrorLeak(message: string | undefined | null): boolean {\n if (!message) return false;\n const lower = String(message).toLowerCase();\n return (\n lower.includes('sqlite_') ||\n lower.includes('sqlstate') ||\n lower.startsWith('insert into ') ||\n lower.startsWith('update ') ||\n lower.startsWith('select ') ||\n lower.startsWith('delete from ') ||\n lower.includes('constraint failed') ||\n lower.includes('unique constraint') ||\n lower.includes('foreign key') ||\n DIALECT_LEAK_PHRASINGS.some((pattern) => pattern.test(lower))\n );\n}\n\n/**\n * Whether the thrown error **declares a server fault** in the ADR-0112 envelope:\n * `status >= 500` *and* a non-empty `code`.\n *\n * The counterpart to {@link looksLikeInternalErrorLeak}, and deliberately not a\n * message test at all. Some server faults are dangerous to echo while saying\n * nothing a phrasing heuristic can recognise — the motivating family is\n * `service-analytics`' `read-scope-sql.ts`, whose ten fail-closed RLS lowering\n * refusals name the FIELD NAMES AND COMPARANDS OF THE RLS POLICY:\n *\n * ```\n * [read-scope-sql] unsafe field identifier \"secret_policy_field\" — refusing to\n * build read scope (fail-closed).\n * ```\n *\n * That text comes from an administrator's sharing rule compiled by the security\n * service; the tenant who receives it never wrote it and must not be able to read\n * it out of an error body. Measured, all eleven of its message shapes return\n * FALSE from `looksLikeInternalErrorLeak` — they look nothing like a driver dump —\n * so a boundary that only ran the heuristic echoed every one of them verbatim\n * (#5811 measured 11/11 through `errorResponseBase`). Teaching the heuristic to\n * recognise `[read-scope-sql]` would have been *more* message sniffing, which is\n * the mechanism #5352/#5367 exist to remove. So the withhold keys on the\n * DECLARATION instead: a producer that says `status >= 500` with a `code` has\n * declared that this is the server's fault, and a server fault's detail belongs in\n * the operator's log, not in the caller's body.\n *\n * **Both halves are required, and it is deliberately NOT \"any 5xx\".** #5667 kept\n * UNDECLARED 5xx errors legible on purpose — a bare `Error` from our own code\n * (\"no strategy can handle query …\") is the operator's own bug report, carries\n * nothing tenant-sensitive, and still falls to `looksLikeInternalErrorLeak`.\n * Widening this to every 500 would delete that decision.\n *\n * **Reads `status`, not `statusCode`.** `status` is the channel ADR-0112 declares;\n * `statusCode` is an alternate spelling some boundaries tolerate when *deriving*\n * an HTTP status. Accepting it here would make the disclosure rule depend on which\n * spelling a producer happened to use — consumer-side leniency of exactly the kind\n * Prime Directive #12 removes. A producer that wants its detail withheld declares\n * the envelope.\n *\n * Costs no diagnostics: every boundary that applies this still logs the untouched\n * error and hands it to the error reporter.\n *\n * @param err - the thrown value, of any shape (a non-object is simply not a\n * declaration).\n */\nexport function declaresServerFault(err: unknown): boolean {\n if (typeof err !== 'object' || err === null) return false;\n const { status, code } = err as { status?: unknown; code?: unknown };\n return typeof status === 'number' && status >= 500 && typeof code === 'string' && code.length > 0;\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Seek-based (keyset) pagination for the batch walks that read a whole object.\n *\n * # Why this exists rather than `limit`/`offset`\n *\n * A background walk that pages with a growing `offset` — rebuild an index,\n * verify file references, backfill a projection — is wrong in two ways that a\n * seek fixes at once.\n *\n * **It can skip rows.** `LIMIT n OFFSET k` is a slice of an arrangement, and\n * the arrangement has to be the *same* one on every page for the slices to\n * partition the set. Drivers now guarantee that for a single read\n * (objectstack#4363), but not across a walk that *mutates as it goes*: a\n * backfill that updates each page, or a rebuild that deletes, changes the very\n * set the next offset counts into. Rows shift past the cursor and are never\n * visited. For a verifier that decides which files are still referenced, or an\n * index rebuild that deletes what it did not see, a skipped row is not a slow\n * page — it is a wrong answer that looks like a clean run. A seek predicate\n * carries the position *in the data* instead of counting from the start, so an\n * update cannot move a row past it and a delete cannot shift one under it.\n *\n * **It is quadratic.** The database must produce and discard every skipped row\n * to honor an offset, so walking n rows in pages of p costs O(n²/p). On a\n * 2M-row table the last pages were measured at ~1.1 s each against ~0.09 s for\n * the first. A seek starts each page at the cursor, so every page costs the\n * same: O(n) for the walk, and index-served throughout.\n *\n * # What it requires\n *\n * A column that is **unique and orderable** — `id` by default, which every\n * object this driver-managed platform creates carries. An object without one\n * (a federated table, ADR-0015) cannot be walked this way; callers that scan\n * arbitrary registry objects already skip what they cannot read, and that is\n * the correct outcome here too rather than a silent partial scan.\n *\n * # Shape\n *\n * `read` is the caller's own query — this owns the loop, the cursor and the\n * `where` merge, and nothing else. Deliberately one implementation rather than\n * the six hand-rolled copies it replaces: the cursor merge is the part that is\n * easy to get subtly wrong (an object whose own `where` already constrains the\n * key), and six copies of it drift silently.\n *\n * @example\n * const walk = keysetWalk<Row>(\n * (q) => engine.find('sys_approval_request', { ...q, fields: ['id'], context: SYSTEM_CTX }),\n * { where: { status: 'pending' }, pageSize: 500 },\n * );\n * for await (const page of walk.pages()) { … }\n * if (walk.truncated) { … }\n */\n\n/** The query a {@link keysetWalk} hands its reader: the caller's `where`, narrowed by the cursor. */\nexport interface KeysetPageQuery {\n /** The caller's `where`, AND-ed with the seek predicate once the walk has a cursor. */\n where?: unknown;\n /** Always ascending on the key column — the walk's order IS the seek order. */\n orderBy: Array<{ field: string; order: 'asc' }>;\n /** Page size. */\n limit: number;\n}\n\nexport interface KeysetWalkOptions {\n /** The caller's filter, applied to every page. */\n where?: unknown;\n /** Rows per page. */\n pageSize: number;\n /**\n * Stop after this many rows and set {@link KeysetWalk.truncated}. Omit for an\n * unbounded walk. A cap is not a failure — it is how a scan bounds its own\n * cost — but it must be reported, or a partial scan reads as a complete one.\n */\n max?: number;\n /** Unique, orderable column to seek on. Defaults to `id`. */\n key?: string;\n}\n\nexport interface KeysetWalk<T> {\n /** Pages, in key order, until the source is exhausted or `max` is reached. */\n pages(): AsyncGenerator<T[]>;\n /** Rows yielded so far. */\n readonly scanned: number;\n /** True when `max` stopped the walk before the source was exhausted. */\n readonly truncated: boolean;\n}\n\n/**\n * AND the seek predicate onto the caller's filter.\n *\n * Uses `$and` rather than spreading the key into the same object: a caller\n * whose own `where` already constrains the key column (`{ id: { $in: [...] } }`)\n * would otherwise have that constraint silently overwritten by the cursor, and\n * the walk would return rows the caller excluded. `$and` composes instead of\n * colliding, and every driver executes it.\n */\nfunction withCursor(where: unknown, key: string, cursor: unknown): unknown {\n const seek = { [key]: { $gt: cursor } };\n if (where == null) return seek;\n if (typeof where === 'object' && Object.keys(where as object).length === 0) return seek;\n return { $and: [where, seek] };\n}\n\n/**\n * Walk an object by seeking past the last key rather than counting from the\n * start. See the module comment for why every batch scan should.\n *\n * `read` receives a {@link KeysetPageQuery} and returns the page; the caller\n * owns everything else about the query (projection, context, object name).\n */\nexport function keysetWalk<T extends Record<string, unknown>>(\n read: (query: KeysetPageQuery) => Promise<T[]>,\n options: KeysetWalkOptions,\n): KeysetWalk<T> {\n const key = options.key ?? 'id';\n const pageSize = options.pageSize;\n let scanned = 0;\n let truncated = false;\n\n async function* pages(): AsyncGenerator<T[]> {\n let cursor: unknown = undefined;\n for (;;) {\n const want = options.max == null ? pageSize : Math.min(pageSize, options.max - scanned);\n if (want <= 0) {\n truncated = true;\n return;\n }\n\n // When `max` clips this page, ask for ONE more row than we will yield.\n // That extra row is the difference between \"the cap stopped us\" and \"the\n // source ended at exactly the cap\" — without it a walk that read\n // everything still reports `truncated`, and a caller acting on that goes\n // looking for rows that were never withheld.\n const clipped = options.max != null && want < pageSize;\n const page = await read({\n where: cursor === undefined ? options.where : withCursor(options.where, key, cursor),\n orderBy: [{ field: key, order: 'asc' }],\n limit: clipped ? want + 1 : want,\n });\n if (!Array.isArray(page) || page.length === 0) return;\n\n const overflow = clipped && page.length > want;\n const emit = overflow ? page.slice(0, want) : page;\n scanned += emit.length;\n yield emit;\n\n if (overflow) {\n truncated = true;\n return;\n }\n\n const last = emit[emit.length - 1]?.[key];\n // A row without the key column cannot advance the cursor, and continuing\n // would re-read the same page forever. Stop and report it as truncation\n // rather than spin: a walk that cannot seek is not a walk that finished.\n if (last === undefined || last === null) {\n truncated = true;\n return;\n }\n // The same stop for a reader that did not APPLY the seek — the cursor\n // comes back no further along than it went in, so the next page would be\n // this page again, forever. Production drivers execute the predicate;\n // a test double or a future reader that quietly drops it would otherwise\n // hang rather than fail, and a hang is the one failure nobody can read.\n if (cursor !== undefined && !(String(last) > String(cursor))) {\n truncated = true;\n return;\n }\n cursor = last;\n\n // A short page means the source is exhausted.\n if (emit.length < want) return;\n // Reaching the cap on a full, unclipped page: more rows may remain, and\n // the next iteration's `want <= 0` reports that as truncation.\n if (options.max != null && scanned >= options.max && !clipped) continue;\n if (options.max != null && scanned >= options.max) return;\n }\n }\n\n return {\n pages,\n get scanned() {\n return scanned;\n },\n get truncated() {\n return truncated;\n },\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * True when a dynamic `import()` / `require.resolve()` failed because the\n * module is simply NOT INSTALLED — as opposed to the module being present but\n * throwing while it loads (a real crash). Checking `err.code` FIRST matters:\n * ESM reports a missing package as `err.code === 'ERR_MODULE_NOT_FOUND'` with\n * the human message `Cannot find package '...'`; matching only the older\n * `Cannot find module` string mis-classifies that as a crash (framework#1595).\n *\n * Single shared owner for this classification (framework#3265): the CLI's\n * optional-plugin guards and `requires` capability resolver delegate here, and\n * cloud's `objectos-runtime` capability loader is expected to adopt it at its\n * next framework pin bump — so the parallel loaders cannot drift apart and\n * re-introduce the #1595 false-alarm class.\n */\nexport function isModuleNotFoundError(err: unknown): boolean {\n const code = (err as { code?: string } | null | undefined)?.code;\n if (code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') return true;\n const msg = err instanceof Error ? err.message : String(err);\n return msg.includes('Cannot find module') || msg.includes('Cannot find package');\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * The ONE writer for the declared REST response envelope (#3973).\n *\n * `BaseResponseSchema` (`packages/spec/src/api/contract.zod.ts`) declares one\n * envelope for every REST body the platform emits:\n *\n * { success: true, data }\n * { success: false, error: { code, message } }\n *\n * The schema declares it once. Until this file, the code that *wrote* it was\n * copied per route module — seven `sendOk` / `sendError` pairs after #3843 and\n * #3983 converted the last drifting one, so the envelope's shape lived in\n * fourteen places rather than one.\n *\n * ## Why a shared builder rather than seven agreeing copies\n *\n * `scripts/check-route-envelope.mjs` proves the copies agree today, and that is\n * exactly why this is a cleanup and not a bug fix. But a guard proves agreement;\n * it does not create it. An eighth module starts by copying the pair again —\n * which is not hypothetical, it is the observed history: `share-link-routes.ts`\n * was found by the repo-wide scan already drifting, and its drift had broken\n * `client.shareLinks.create()` / `.list()` through `unwrapResponse` (#3983).\n *\n * ## Why here\n *\n * Placement was the open question in #3973, not design. `packages/spec` is\n * schemas-only (Prime Directive #2), and the callers span `packages/rest`, four\n * `services/*` and one `plugins/*`, which rules out anything that depends on\n * them. `@objectstack/types` depends on nothing but `@objectstack/spec`, so\n * every caller can reach it, and it is where the repo already puts a helper the\n * HTTP boundaries share: {@link looksLikeInternalErrorLeak} lives one file over\n * for the same reason, and made the same argument first — \"do not ship driver\n * internals to clients\" is a property of the boundary, not of one router.\n *\n * Writing the declared envelope is the same kind of property.\n *\n * ## What this does NOT change\n *\n * Every byte on the wire. The seven pairs were already identical modulo the\n * optional `status` and `extra` parameters unioned below; this file is their\n * union, and each module's driven conformance suite still parses its real\n * bodies against the real spec schemas.\n *\n * The dispatcher surface (`packages/runtime/src/domains/*`) is deliberately not\n * a caller: those handlers RETURN `{ status, body }` for a central sender rather\n * than writing to a response, so they are already consolidated behind their own\n * `deps.success` / `deps.error` helpers and audited by the other half of\n * `check-route-envelope.mjs`.\n */\n\nimport type { ApiError, ErrorCode } from '@objectstack/spec/api';\n\n/**\n * The only thing an envelope builder needs from a response object.\n *\n * Structural on purpose, so this file depends on no HTTP contract at all:\n * `IHttpResponse` (`@objectstack/spec/contracts`) satisfies it, and so does the\n * `any`-typed `res` the three older route modules still carry. That is what lets\n * a package import the builders without also importing a server abstraction.\n */\nexport interface EnvelopeResponse {\n status(code: number): EnvelopeResponse;\n json(body: unknown): unknown;\n}\n\n/**\n * Emit a success body in the DECLARED envelope — `{ success: true, data }`.\n *\n * `data` carries the route's payload; it is not spread. A payload duplicated\n * into a stray top-level key (`{ success: true, data: link, link }`) parses\n * clean against `BaseResponseSchema` and is still drift — that shipped on\n * `/share-links` for as long as nobody looked (#4038), which is why\n * `envelopeViolations` exists beside the schema and why there is one `data`\n * slot here rather than a spread.\n *\n * `status` defaults to 200 and is set explicitly even then. Five of the seven\n * modules already did that; the two that called `res.json(...)` bare are\n * unaffected, because the default they were relying on is the value now passed.\n */\nexport function sendOk(res: EnvelopeResponse, data: unknown, status = 200): void {\n res.status(status).json({ success: true, data });\n}\n\n/**\n * Emit an error in the DECLARED envelope — `{ success: false, error: { code,\n * message } }`, with `code` a semantic STRING and `message` a field OF `error`\n * rather than a sibling of it.\n *\n * Both halves of that sentence were once wrong somewhere: `error` was a bare\n * string in `service-storage` and `admin-routes` (so `body.error.message` read\n * `undefined`), and `code` was the human message in `package-routes` (#3675 →\n * #3689 → #3843).\n *\n * ## `code` is the closed ADR-0112 vocabulary, not `string`\n *\n * All seven copies typed this parameter `string`, so an invented code was caught\n * only at runtime, by a conformance suite parsing a driven body against\n * `ApiErrorSchema` — i.e. only on the routes a test happened to drive. `ErrorCode`\n * is `StandardErrorCode ∪ ERROR_CODE_LEDGER` (`error-code-ledger.zod.ts`), the\n * same union that schema validates against, so consolidating here moves the check\n * to compile time for every call site at once. It cost no call-site churn: every\n * code the seven modules emit was already registered.\n *\n * A new code is registered in `ERROR_CODE_LEDGER` under its owning package —\n * and if the condition is generic (not found / permission / validation), the\n * standard catalog is used instead of registering a synonym for it.\n *\n * ## `extra` is `ApiError`'s own optional fields, not a `Record`\n *\n * Merged into `error`, and typed as exactly what `ApiErrorSchema` declares\n * beside `code` and `message` — `details`, `category`, `requestId`, `httpStatus`.\n * `details` is the slot for structured context: `package-routes` puts a partial\n * delete's per-item failures there, `settings-routes` the whole\n * `SettingsActionResult`.\n *\n * This started as `Record<string, unknown>`, because `settings-routes` also hung\n * `namespace` / `key` / `reason` / `fields` beside `code`, which the schema does\n * not declare. Those bodies passed every gate anyway — `ApiErrorSchema` is a\n * plain `z.object`, so unknown keys were STRIPPED rather than rejected, and\n * `envelopeViolations` inspects only the body's top level — making them\n * conformant *by stripping* rather than by declaration. #4224 moved that module's\n * four branches onto `details`, which is what lets the parameter close here.\n *\n * Closing it at the shared builder is the part that lasts: an undeclared sibling\n * is now a compile error in every module at once, rather than a key that quietly\n * evaporates at the schema boundary in whichever module reintroduces it.\n */\nexport function sendError(\n res: EnvelopeResponse,\n status: number,\n code: ErrorCode,\n message: string,\n extra?: Pick<ApiError, 'category' | 'httpStatus' | 'details' | 'requestId'>,\n): void {\n res.status(status).json({ success: false, error: { code, message, ...extra } });\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * The ONE rule for \"what HTTP answer does a THROWN error declare?\" (#8016).\n *\n * A service or protocol throw that carries its own `.status` / `.statusCode`\n * and its own semantic `.code` is a *refusal*, not a fault: the caller asked\n * for something the platform will not do, and the honest answer is that status\n * with that code. A throw carrying neither is a fault, and the honest answer is\n * the caller's fallback — 500 `INTERNAL_ERROR` at an HTTP boundary.\n *\n * ## Why this is shared rather than restated per door\n *\n * `/api/v1/packages` has **two** HTTP doors. The runtime dispatcher's\n * `HttpDispatcher.errorFromThrown` read `.status` first and answered `409\n * DESTRUCTIVE_CHANGE` for a `metadata-protocol` refusal. The direct-mount REST\n * registrar (`packages/rest/src/package-routes.ts`) had four catch-alls that\n * answered `500 INTERNAL_ERROR` regardless — and *that* registrar mounts first\n * in the production stack, so 500 was what production actually returned. One\n * throw, two answers, and the wrong one was the live one (#8016).\n *\n * The rule therefore lives in ONE function that both doors call. It could not\n * live in `packages/runtime`: `@objectstack/runtime` depends on\n * `@objectstack/rest`, so the arrow only points one way and `errorFromThrown`\n * is unreachable from the REST door by construction. `@objectstack/types`\n * depends on nothing but `@objectstack/spec`, which is exactly why the other\n * shared HTTP-boundary helpers already live here — `looksLikeInternalErrorLeak`\n * (\"do not ship driver internals to clients\") and `sendOk`/`sendError` (\"write\n * the declared envelope\"). \"What status does this throw mean?\" is the same kind\n * of property: it belongs to the boundary, not to one router.\n *\n * ## Two spellings of the code, because the two envelopes are not equally closed\n *\n * {@link ThrownHttpError.code} is narrowed to `StandardErrorCode ∪\n * ERROR_CODE_LEDGER` — the union `ApiErrorSchema` validates against — so a\n * throw whose `.code` is not a registered member does not get to name itself;\n * it falls to the code the status derives. That is the same rule\n * `metadata-protocol`'s `toRowApiError` applies to a per-row batch error, and\n * it is what lets `sendError`'s closed `ErrorCode` parameter be satisfied\n * without a cast. The direct-mount REST door needs exactly this: its bodies are\n * parsed against `BaseResponseSchema` by its own conformance suite, so an\n * unregistered code there is a failing test, not a wire answer.\n *\n * {@link ThrownHttpError.declaredCode} is the producer's own string, verbatim\n * and un-narrowed. Until #9106 it was what the dispatcher door put in\n * `error.code`; since the #9106 ruling it is what BOTH doors surface as the\n * wire's `declaredCode` when it is not a vocabulary member (see below).\n *\n * [#8087] The first ruling on that gap (maintainer, 2026-08-12) kept the\n * dispatcher's verbatim spelling and delivered a GATE — the unregistered\n * producers are measured and classified\n * (`packages/runtime/src/dispatcher-error-vocabulary.ts`,\n * `pnpm check:dispatcher-error-vocabulary`) instead of named in prose here.\n * The gate's own first derivation then measured the limb no registration can\n * close: a metadata app's action code crosses the sandbox boundary carrying\n * the app's OWN `.code` (#7867), authored by tenants at runtime.\n *\n * [#9106] That limb was ruled (maintainer, 2026-08-16): **`error.code` is a\n * closed vocabulary at every door.** The dispatcher door now takes\n * {@link ThrownHttpError.code} — the demote this resolver has always computed,\n * and the REST door's spelling since #8016 — and a producer's unregistered\n * string rides the wire's `declaredCode` (declared on `ApiErrorSchema`)\n * instead of `error.code`. #7867's capability is preserved: the author's code\n * still crosses the sandbox and still reaches the wire — in the open,\n * author-authored channel, not the closed one. Use\n * {@link demotedDeclaredCode} to read the spelling a boundary should surface\n * beside the closed `code`.\n *\n * So the doors agree on **status** and on **code** unconditionally now — both\n * answers come from ONE function, which is what keeps agreement a construction\n * rather than two suites agreeing about literals.\n *\n * ## What this deliberately does NOT decide\n *\n * - **Message disclosure.** A 5xx message may name physical tables or carry a\n * driver dump; withholding it is `looksLikeInternalErrorLeak`'s job, applied\n * by the caller (the dispatcher does; see #3867). This function returns the\n * thrown message verbatim.\n * - **Whether a declared status is *plausible*.** No 400-599 band is imposed,\n * because the dispatcher never imposed one and this function exists to make\n * the two doors agree. Narrowing the accepted band is a change to the rule,\n * and it belongs here — in one place, for both doors — if it is ever made.\n */\n\nimport { ErrorCode, standardErrorCodeForHttpStatus } from '@objectstack/spec/api';\nimport { validationFailureDetails, VALIDATION_FAILED_STATUS } from './validation-failure.js';\n\n/** The HTTP answer a thrown error declares. See {@link resolveThrownHttpError}. */\nexport interface ThrownHttpError {\n /** The producer's own `status`/`statusCode`, or the caller's fallback. */\n status: number;\n /**\n * The status the THROW ITSELF declared — `.status`, `.statusCode`, or the\n * 400 a validation-shaped throw declares by shape — and **absent** when it\n * declared none, i.e. when {@link ThrownHttpError.status} above is the\n * caller's `fallbackStatus`.\n *\n * ## Why `status` cannot answer this\n *\n * A producer that declares `500` and one that declares nothing both resolve\n * to `status: 500`, so a caller that must tell \"the producer said so\" from\n * \"I supplied the default\" cannot read it off the value. The workaround in\n * the repo was to probe this function with a fallback no producer declares\n * — `resolveThrownHttpError(e, 0).status !== 0`. That is a magic number\n * standing in for a fact this function already computed, and it fails\n * silently the day a producer declares the sentinel. So the fact is stated;\n * `packages/rest`'s publish-classification suite now reads\n * `resolveThrownHttpError(error).declaredStatus !== undefined` instead of\n * hand-spelling the workaround.\n *\n * ## Who needs the distinction\n *\n * A sink that mirrors the status onto RESPONSE DATA instead of into the\n * response's own status line — where the fallback would not be a default but\n * an invention. `metadata-protocol`'s `toRowApiError` is the measured one\n * (#8570): a batch row rides a **200**, so stamping `status` there would put\n * `httpStatus: 500` on every undeclared driver fault, an ADDITION to the\n * wire, where stamping `declaredStatus` restores only what a producer really\n * declared. Boundaries that answer with the status itself keep reading\n * `status` — the fallback is exactly what they want.\n */\n declaredStatus?: number;\n /**\n * A member of the declared ADR-0112 vocabulary — for a boundary whose\n * envelope is checked against it. Never the HTTP status.\n */\n code: ErrorCode;\n /**\n * The producer's own code, verbatim and un-narrowed, or `undefined` when it\n * declared none. Never for `error.code` — that slot takes {@link code} at\n * every door (#9106) — but for the wire's `declaredCode` channel when the\n * spelling is not a vocabulary member ({@link demotedDeclaredCode}). See the\n * module note on why there are two.\n */\n declaredCode?: string;\n /** The thrown message, UNSANITISED — see the module note on disclosure. */\n message: string;\n /**\n * The producer's user-facing refusal text, verbatim — present exactly when\n * the throw carried a non-empty string `userMessage` (#9934).\n *\n * This is the producer-side opt-in the objectui#5210 ruling asked for\n * (maintainer, 2026-08-19, option 1): an application hook's refusal has no\n * way to distinguish author-written user guidance from platform diagnostics,\n * so the console substitutes a generic string on 403 (the recorded #3821\n * fix) and every author-written remedy is suppressed with the diagnostics.\n * A producer that sets `userMessage` on the thrown error is saying, at throw\n * time, \"this exact text is addressed to the END USER\" — a consumer renders\n * it verbatim and keeps the generic substitution for everything unmarked.\n *\n * Deliberately a FIELD carrying the text, not a boolean beside `message`:\n * the mark and the marked text are one value, so a boundary that rewraps or\n * substitutes `message` (sanitisation, truncation, the sandbox debug\n * wrapper) can never accidentally promote platform prose into the marked\n * channel — the #3821 protection holds by construction. Read through\n * {@link declaredUserMessage}, never with an inline `typeof` probe.\n *\n * Status-agnostic on purpose (the ruling's second constraint): a 400, 403,\n * 409 or 503 refusal may all carry it. It never REPLACES `message` — the\n * diagnostic channel keeps its wording for logs and developers.\n */\n userMessage?: string;\n /**\n * Structured context: spec-validation `issues[]`, record-validation\n * `fields[]`. Absent rather than `{}` when the throw carried none, so an\n * empty object never reads as \"there is context here\".\n */\n details?: Record<string, unknown>;\n}\n\n/**\n * Resolve a thrown error into the status, code, message and structured context\n * an HTTP boundary should answer with.\n *\n * Precedence, in order:\n *\n * | Question | Answer |\n * |---|---|\n * | status | `.status` → `.statusCode` → 400 if it is a validation failure → `fallbackStatus` |\n * | declaredStatus | the same chain WITHOUT the fallback — absent when the throw declared none |\n * | code | `VALIDATION_FAILED` if it is one → a REGISTERED `.code` → derived from the status |\n * | declaredCode | `VALIDATION_FAILED` if it is one → any non-empty string `.code` → absent |\n * | message | `.message` when it is a string → `String(error)` |\n * | userMessage | a non-empty string `.userMessage` → absent (see {@link declaredUserMessage}) |\n *\n * Both status spellings are read because both are produced in this repo:\n * `plugin-approvals`' lifecycle hooks and `metadata-protocol` throw\n * `statusCode`, `metadata-protocol`'s conflicts throw `status`. Reading one\n * spelling is how `/api/v1/data` answered 500 for a deliberate `409\n * RECORD_LOCKED` until #7525.\n */\nexport function resolveThrownHttpError(error: unknown, fallbackStatus = 500): ThrownHttpError {\n const e = error as any;\n const validation = validationFailureDetails(e);\n\n // The validation SHAPE is a declaration too: `ValidationError` carries no\n // status because deciding it means 400 is the boundary's job, but the\n // producer did say \"this is a client's input problem\" — which is the fact\n // `declaredStatus` reports. Only the `fallbackStatus` limb below is the\n // caller's own invention, and it is the only one left out.\n const declaredStatus =\n typeof e?.status === 'number' ? e.status\n : typeof e?.statusCode === 'number' ? e.statusCode\n : validation ? VALIDATION_FAILED_STATUS\n : undefined;\n const status = declaredStatus ?? fallbackStatus;\n\n const spelled = typeof e?.code === 'string' && e.code !== '' ? e.code : undefined;\n // A `.code` the ledger does not know cannot go in a slot typed as the closed\n // vocabulary — see the module note on why there are two spellings.\n const registered = spelled !== undefined && ErrorCode.safeParse(spelled).success\n ? (spelled as ErrorCode)\n : undefined;\n const code: ErrorCode = validation\n ? validation.code\n : (registered ?? standardErrorCodeForHttpStatus(status));\n const declaredCode = validation ? validation.code : spelled;\n\n const issues = Array.isArray(e?.issues) ? e.issues : undefined;\n const details: Record<string, unknown> = {\n // A truthy NON-string `code` (a driver errno, say) is context and stays\n // context — promoting it would put a number in the field callers branch on,\n // which is the drift #3842 removed.\n ...(!validation && e?.code && typeof e.code !== 'string' ? { code: e.code } : {}),\n ...(issues ? { issues } : {}),\n ...(validation ? { fields: validation.fields } : {}),\n };\n\n const userMessage = declaredUserMessage(error);\n\n return {\n status,\n ...(declaredStatus !== undefined ? { declaredStatus } : {}),\n code,\n ...(declaredCode !== undefined ? { declaredCode } : {}),\n message: typeof e?.message === 'string' ? e.message : String(error),\n ...(userMessage !== undefined ? { userMessage } : {}),\n ...(Object.keys(details).length > 0 ? { details } : {}),\n };\n}\n\n/**\n * The user-facing refusal text a thrown error DECLARED, or `undefined` when it\n * declared none (#9934). See {@link ThrownHttpError.userMessage} for what the\n * declaration means and why it is a text-carrying field rather than a flag.\n *\n * The ONE read every boundary applies — the REST classification door, the\n * dispatcher door, and the sandbox side-channel all call this rather than\n * probing `error.userMessage` themselves, so \"what counts as marked\" cannot\n * fork per door the way the `status`/`statusCode` spelling once did (#7525).\n *\n * A non-string or blank `userMessage` is NOT a declaration: `undefined`, a\n * number, `''` and whitespace-only all answer `undefined`, so nothing invents\n * a marked message for a producer that never wrote one — absent means the\n * consumer keeps its generic substitution (#3821 preserved by construction).\n */\nexport function declaredUserMessage(error: unknown): string | undefined {\n const declared = (error as { userMessage?: unknown } | null | undefined)?.userMessage;\n return typeof declared === 'string' && declared.trim().length > 0 ? declared : undefined;\n}\n\n/**\n * The producer's spelling a boundary should surface as the wire's\n * `declaredCode` beside the closed `code` — or `undefined` when there is\n * nothing to surface (#9106).\n *\n * Present exactly when the throw spelled a code that did NOT survive into\n * {@link ThrownHttpError.code} — i.e. the demote happened. A registered code\n * is already in `code`, so emitting it again would put two spellings of one\n * fact on every refusal; a throw with no code has nothing to declare. Spelled\n * once here rather than as three `!==` comparisons at three exits, so\n * \"presence means demotion\" (`ApiErrorSchema.declaredCode`'s documented\n * semantics) has one definition.\n */\nexport function demotedDeclaredCode(thrown: ThrownHttpError): string | undefined {\n return thrown.declaredCode !== undefined && thrown.declaredCode !== thrown.code\n ? thrown.declaredCode\n : undefined;\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Recognising a record-validation failure at an HTTP boundary.\n *\n * `ValidationError` (`@objectstack/objectql`'s record/rule validators) carries\n * `.code = 'VALIDATION_FAILED'` and `.fields[]` — one entry per offending\n * field — but deliberately carries NO `.status` / `.statusCode` and no\n * `.issues`. It is a plain domain error; deciding it means \"400\" is the job of\n * whichever boundary serves it.\n *\n * `@objectstack/rest` has always done that (`mapDataError` → 400 with\n * `fields[]`). The runtime dispatcher's two error exits did not (#3918): with\n * no `.status` to read they fell back to **500**, and both read only `.issues`\n * for structured detail — which a `ValidationError` never has — so `fields[]`\n * was dropped and the caller got a generic \"internal error\" for what was\n * really a user-input mistake. That forecloses per-field error display on every\n * surface the dispatcher serves.\n *\n * Matched by duck-typing on `code` / `name` — exactly the predicate\n * `mapDataError` uses — so this module stays free of a runtime dependency on\n * `objectql`, and so hand-rolled errors of the same shape (e.g. a hook that\n * throws `{ code: 'VALIDATION_FAILED', fields }`) are served identically.\n *\n * ## Why it lives in `@objectstack/types` (#8016)\n *\n * It was `packages/runtime/src/validation-failure.ts` until the *package* door's\n * four status-blind catch-alls were converged onto the dispatcher's mapping\n * ({@link resolveThrownHttpError}, one file over). That resolver has to answer\n * \"is this throw a validation failure?\" the same way on both doors, and\n * `@objectstack/rest` cannot import `@objectstack/runtime` — runtime depends on\n * rest, so the arrow only points one way. `@objectstack/types` depends on\n * nothing but `@objectstack/spec`, which is why the shared HTTP-boundary\n * helpers (`looksLikeInternalErrorLeak`, `sendOk`/`sendError`) already live\n * here. This module moved for the same reason and is unchanged otherwise;\n * `packages/runtime/src/validation-failure.ts` re-exports it, so every runtime\n * import site still reads the name it always did.\n */\n\nimport { zodIssuesToFields } from '@objectstack/spec/api';\nimport type { FieldErrorCode } from '@objectstack/spec/api';\n\n/** The HTTP status a validation failure maps to when the error names none. */\nexport const VALIDATION_FAILED_STATUS = 400;\n\nexport interface ValidationFailureDetails {\n code: 'VALIDATION_FAILED';\n /** Per-field envelopes, passed through verbatim. `[]` when absent/malformed. */\n fields: unknown[];\n}\n\n/**\n * Structured `details` for a thrown validation failure, or `undefined` when\n * `err` is not one. Callers use the `undefined` result as the predicate and the\n * returned object as the `details` payload, so the two can never disagree.\n */\nexport function validationFailureDetails(err: any): ValidationFailureDetails | undefined {\n if (!err) return undefined;\n if (err.code !== 'VALIDATION_FAILED' && err.name !== 'ValidationError') return undefined;\n return {\n code: 'VALIDATION_FAILED',\n fields: Array.isArray(err.fields) ? err.fields : [],\n };\n}\n\n/**\n * [#3878/#3899] The CONSTRUCTOR for the shape {@link validationFailureDetails}\n * recognises — kept in the same module so the two can never drift. Thrown from\n * a domain handler, both dispatcher error exits map it to\n * `400 VALIDATION_FAILED` + `details.fields[]` (#3918) with no new error\n * channel and no runtime dependency on objectql's `ValidationError` class.\n * First built inline by the analytics domain; hoisted here when notifications\n * and automation grew the same entry gates rather than a third copy.\n */\nexport function validationFailure(message: string, fields: unknown[]): Error {\n const err = new Error(message) as Error & { code: string; fields: unknown[] };\n err.name = 'ValidationError';\n err.code = 'VALIDATION_FAILED';\n err.fields = fields;\n return err;\n}\n\n/**\n * Zod issues → the dispatcher's `fields[]` envelope entries\n * (`{ field, code, message }`). `'(body)'` names a root-level failure — a body\n * that is the wrong TYPE entirely has no path to point at.\n *\n * ## The `code` is an ADR-0114 `FieldErrorCode`, not Zod's (#8124)\n *\n * This used to assign `issue.code` verbatim, which put Zod's own vocabulary\n * (`unrecognized_keys`, `too_small`, …) on a wire position\n * `FieldErrorSchema.code` declares as a CLOSED catalog — the exact\n * pass-through ADR-0114 D3 closed on the REST transport. It now maps through\n * `zodIssuesToFields`, the one D3 implementation in the repo, which lives in\n * `@objectstack/spec` beside the catalog it is total over (this package cannot\n * import `@objectstack/rest`, where the compliant copy grew up — the\n * dependency arrow points the other way, which is what #8124 moved it for).\n *\n * Two things ride along, both additive:\n *\n * - **The optional `input`** (the value that was parsed) buys the D3\n * `invalid_type` split: with it a MISSING required property is reported as\n * `required` instead of the `invalid_type` Zod spells it as. Callers without\n * the input at hand degrade per the D3 table — every code is still a\n * catalog member.\n * - **Union expansion (#5014)**: a rejection behind a `z.union` yields the\n * union's own entry PLUS the branch entries that explain it, so entry count\n * is not issue count. Read `fields.length` as the number of field errors.\n */\nexport function fieldsFromZodIssues(\n issues: Array<{ path: Array<string | number | symbol>; code: string; message: string }>,\n ...input: [] | [unknown]\n): Array<{ field: string; code: FieldErrorCode; message: string }> {\n return zodIssuesToFields(issues, ...input).map((entry) =>\n entry.field === '' ? { ...entry, field: '(body)' } : entry,\n );\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * The one home for Postgres' `«sub-object» \"x\" of relation \"y\" …` phrasing\n * (#6615).\n *\n * ## The superstring hole, stated once\n *\n * Postgres phrases a failure about something *inside* a relation by naming the\n * relation too:\n *\n * ```\n * column \"label\" of relation \"sys_team\" does not exist (42703)\n * constraint \"uq_sys_team_name\" of relation \"sys_team\" does not exist (42704)\n * column \"environment_id\" of relation \"sys_metadata\" already exists (42701)\n * ```\n *\n * Every one of those **contains a complete, legal missing-TABLE phrase** —\n * `relation \"sys_team\" does not exist` — as a substring, while meaning the\n * opposite: the relation is right there, which is precisely why it could be\n * named. No amount of tightening a \"does this say a relation is missing?\"\n * regex can remove that match, because the phrase really is in there. The only\n * repair is to ask the more specific question FIRST. That makes the ORDER the\n * fix, not the pattern — and it is why three packages each grew their own copy\n * of this phrase (#5352, #6035/PR #6346, #6347/PR #6613) before it was given a\n * home.\n *\n * ## Two widths, on purpose — never collapse them\n *\n * The three consumers do not want the same regex, and the difference is not\n * sloppiness: it is **which direction of error is safe** at each site.\n *\n * | consumer | asks | uses | a MISS costs |\n * |:---|:---|:---|:---|\n * | `@objectstack/rest` `mapDataError` (#5352) | which column? | {@link matchMissingColumnOfRelation} | a vaguer message (`404` instead of `400 INVALID_FIELD`) |\n * | `@objectstack/service-analytics` `isMissingSourceError` / `missingSourceRelation` (#6035) | is this a missing COLUMN, so keep it hard? | {@link matchMissingColumnOfRelation} | a mistyped column degrades to a confident empty chart |\n * | `@objectstack/metadata` `MISSING_TABLE.excludes` (#6347) | is this about a sub-object, so not a missing table? | {@link isRelationSubObjectPhrase} | a corruption verdict returns (`event_seq` restarts at 1) |\n *\n * The first two **extract**, so they must be strict: over-matching there would\n * turn a genuinely missing table into a hard failure and regress #5033's\n * deliberate leniency, while under-matching merely keeps today's verdict. The\n * third **excludes**, so it is deliberately wider — any sub-object, any quoted\n * identifier, any verdict — because over-matching there only ever converts a\n * benign verdict into a loud one, and a miss restores data corruption.\n *\n * Collapsing the two into one regex would therefore be wrong for one caller\n * whichever width won. They are two exports for that reason, and the reason is\n * load-bearing rather than stylistic.\n *\n * ## Home\n *\n * `@objectstack/types`, following `isUniqueViolationError`'s move\n * (#6250 — four hand-written answers to one question) and\n * `isModuleNotFoundError`'s (framework#3265 — \"single shared owner … so the\n * parallel loaders cannot drift apart\"). This module deliberately imports\n * nothing.\n *\n * ⚠️ Unlike #6250, adopting this **does** add one dependency edge:\n * `@objectstack/service-analytics` did not depend on `@objectstack/types`\n * before #6615. It is acyclic by construction — `@objectstack/types` depends\n * only on `@objectstack/spec`, which depends on nothing in-repo, so no package\n * except `spec` itself can form a cycle by consuming it — and 25 of the repo's\n * 73 packages (5 of 16 services) already carry the same edge. Recorded here\n * rather than left for a reader to rediscover.\n */\n\n/**\n * Postgres' missing-COLUMN template, strictly. Returns the column name, or\n * `undefined` when the message is not that phrase.\n *\n * Anchored to `column \"%s\" of relation \"%s\" does not exist` — the exact errmsg\n * template Postgres emits for SQLSTATE 42703 on the write path\n * (`INSERT` / `UPDATE` / `ALTER`). Both quotes are required because Postgres\n * always emits them here, and requiring them is the safe direction of error for\n * the two consumers that call this.\n *\n * Deliberately narrow in two further ways, both preserved verbatim from the\n * open-coded copies this replaces:\n *\n * - the identifier is `[a-z0-9_]+` (case-insensitive), so a quoted identifier\n * carrying a space or punctuation is NOT matched. Postgres can quote such\n * names; the consumers accept the miss because a miss is the cheap direction.\n * - the relation is `\\S+` — quoted or bare, unparsed. This function answers\n * \"which COLUMN\", never \"which relation\".\n *\n * The read-path phrasing `column \"bogus\" does not exist` is a different\n * sentence with no relation in it, so it does not match — and it does not need\n * to: it carries no missing-table substring, which is the whole hole this\n * module exists for.\n */\nexport function matchMissingColumnOfRelation(message: string): string | undefined {\n return MISSING_COLUMN_OF_RELATION.exec(message)?.[1];\n}\n\n/**\n * The same quirk, **wider**: does this message talk about any sub-object of a\n * relation, in any verdict?\n *\n * Drops all three of {@link matchMissingColumnOfRelation}'s anchors — the\n * literal `column`, the `[a-z0-9_]+` identifier shape, and the trailing\n * `does not exist` — so it also recognises `constraint \"uq_x\" of relation \"y\"\n * does not exist` (42704), `column \"x\" of relation \"y\" already exists` (42701),\n * and every other sub-object Postgres phrases this way.\n *\n * For **exclusion** callers only. A `true` here means \"the relation is present,\n * so whatever else this error is, it is not a missing table\"; it does not mean\n * the error is benign and it names nothing. Using it to extract would be a\n * category error — there is no capture group precisely so that it cannot be.\n */\nexport function isRelationSubObjectPhrase(message: string): boolean {\n return RELATION_SUB_OBJECT.test(message);\n}\n\n/**\n * The strict extractor's pattern. Module-private: exported behaviour is the two\n * functions above, so a consumer cannot read the wrong capture group, re-flag\n * the regex, or quietly widen one width toward the other.\n */\nconst MISSING_COLUMN_OF_RELATION =\n /column\\s+[\"'`]([a-z0-9_]+)[\"'`]\\s+of relation\\s+\\S+\\s+does not exist/i;\n\n/** The wide detector's pattern. Module-private for the same reason. */\nconst RELATION_SUB_OBJECT = /[\"'`][^\"'`]+[\"'`]\\s+of relation\\s/i;\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * The one named predicate for \"is this driver error a unique-constraint\n * violation?\" (#6250).\n *\n * ## The defect this retires\n *\n * Before this module the repo carried **four** hand-written, mutually different\n * answers to that single question — no two covering the same dialects:\n *\n * | where | judged by | covered |\n * |:---|:---|:---|\n * | `service-messaging`'s `isUniqueViolation()` | 3 codes + 3 message substrings | all three |\n * | `@objectstack/rest`'s `mapDataError` | `unique constraint` / `unique violation` only | **no MySQL** |\n * | `@objectstack/rest`'s `sanitizeRowError` | three column-extracting regexes | all three |\n * | `driver-sql`'s inline regex | `unique constraint failed\\|duplicate entry\\|duplicate key value` | all three |\n *\n * The REST row is the one a user could feel. Its verdict decides whether a\n * conflict comes back as the API contract's `409 UNIQUE_VIOLATION` (a\n * registered code in `packages/spec/src/api/error-code-ledger.zod.ts`) or as a\n * generic `500 INTERNAL_ERROR`, and MySQL's phrasing —\n * `ER_DUP_ENTRY: Duplicate entry 'acme@example.com' for key 'idx_email_unique'`\n * — matches neither substring. Measured on `origin/main` before this change,\n * through the real `mapDataError`:\n *\n * ```\n * mysql, bare message => 500 INTERNAL_ERROR ← the reported defect\n * mysql, knex-prefixed SQL => 500 DATABASE_ERROR ← second spelling, same hole\n * postgres, SQLSTATE only => 500 INTERNAL_ERROR ← the code channel was unread\n * sqlite, message => 409 UNIQUE_VIOLATION\n * postgres, message => 409 UNIQUE_VIOLATION\n * ```\n *\n * So the hole was never MySQL-only: it was \"the mapping reads one channel\n * (message substrings) of the two that drivers actually use\". SQLite and\n * Postgres were invisible survivors because their prose happens to contain the\n * words the substring test looks for.\n *\n * ## Why a predicate rather than a wider heuristic\n *\n * `looksLikeInternalErrorLeak` (one file over) answers a **different**\n * question — \"would echoing this text leak server internals?\" — and the 409\n * mapping used to be nested *inside* its true-branch, so a message had to look\n * like a leak before it could be recognised as a conflict. Those two questions\n * have no reason to agree, and MySQL is the case where they don't. Widening the\n * leak heuristic to reach the conflict branch would have coupled them harder\n * and quietly reclassified unrelated driver text as safe-to-expose; naming the\n * conflict question separately unpicks them instead. Same move as #5841's\n * `isMissingTableError`, and the same reason.\n *\n * ## Home\n *\n * `@objectstack/types` because every consumer of the question already depends\n * on it, so adopting the predicate never adds an edge. This module deliberately\n * imports nothing.\n *\n * ## The second question, answered separately\n *\n * `isUniqueViolationError` answers yes/no. **Which column** conflicted is a\n * different question with a different failure mode, so it is a different export:\n * {@link uniqueViolationColumn}, added by #6544 under the maintainer's\n * 2026-08-08 ruling. Read its doc comment before touching either — the two are\n * gated on each other and the column answer is deliberately narrower than the\n * boolean.\n *\n * ## ⚠️ The INVERSE question lives next door — do not merge them\n *\n * `isUnbackedConflictTargetError` (`unbacked-conflict-target.ts`, #8567) asks\n * whether the database refused an `ON CONFLICT` target because **no unique\n * index exists** for it. This predicate asks whether one **exists and was\n * violated**. Same neighbourhood, same vocabulary, inverse verdicts:\n * answering an unbacked target with a 409 `UNIQUE_VIOLATION` tells the client\n * to change a value when nothing collided, and answering a real conflict with\n * \"add a unique index\" sends an operator after an index that is already there.\n * Neither predicate may grow a limb belonging to the other.\n *\n * ⚠️ This predicate WAS on the wrong side of that line, and #8590 moved it\n * back. The `message` limb used to read `unique constraint` as a bare word\n * pair, which matched every sentence containing those two words **including\n * the ones saying the constraint is absent**. Since #8590 the limb requires a\n * VIOLATION phrasing — `unique constraint failed` (SQLite) or\n * `violates unique constraint` (Postgres) — so a sentence that merely mentions\n * a unique constraint no longer answers yes. The reasoning, and the measured\n * sentences that forced it, are on {@link UNIQUE_VIOLATION} below;\n * `unique-violation-absence-sentences.test.ts` pins the absence sentences and\n * `unbacked-conflict-target.test.ts` pins both predicates' verdicts per dialect,\n * so neither the fix nor a fresh drift can land silently in either direction.\n */\n\n/**\n * One dialect vocabulary, in the three channels drivers actually use.\n *\n * Same shape as `@objectstack/metadata`'s `DriverErrorSignature` — deliberately,\n * because it is the shape the drivers force: Postgres puts SQLSTATE on `code`,\n * mysql2 puts a symbolic name on `code` *and* a number on `errno`, and the\n * SQLite family often gives nothing but prose.\n */\ninterface UniqueViolationSignature {\n /** `error.code` — Postgres SQLSTATE, mysql2's symbolic name, SQLite's extended result code. */\n readonly codes: ReadonlySet<string>;\n /** `error.errno` — MySQL/MariaDB's numeric equivalent of the same condition. */\n readonly errnos: ReadonlySet<number>;\n /** `error.message` — the only channel a knex-wrapped or SQLite-family error reliably carries. */\n readonly message: RegExp;\n}\n\n/**\n * The union of every unique-violation signal the four pre-existing\n * implementations encoded, plus the `errno` channel their `code`-only reads\n * missed.\n *\n * **Seeded from what real drivers emit, not invented here.** Every entry traces\n * to one of the four inventoried implementations; nothing was added on a guess:\n *\n * - `23505` — PostgreSQL SQLSTATE `unique_violation` (from `service-messaging`).\n * - `ER_DUP_ENTRY` — mysql2's symbolic name for 1062 (from `service-messaging`).\n * - `SQLITE_CONSTRAINT_UNIQUE` — better-sqlite3 / libsql extended result code\n * (from `service-messaging`).\n * - `1062` — the same MySQL condition on the channel mysql2 *also* sets. The\n * one addition, and not a new dialect: `@objectstack/metadata`'s\n * `schema-sync-errors.ts` already reads `errno` alongside `code` for exactly\n * these drivers, so a code-only read is a known gap rather than a decision.\n *\n * The message limbs are a **superset of what `mapDataError` already treated as\n * 409**, which is what makes routing REST through this predicate incapable of\n * narrowing a verdict a client relies on today:\n *\n * - `unique constraint failed` — SQLite's `UNIQUE constraint failed: t.c`.\n * - `violates unique constraint` — Postgres' `... violates unique constraint\n * \"...\"`. These two replaced a single bare `unique constraint` limb that was\n * inherited verbatim from the REST branch; see \"Why a VIOLATION phrasing\"\n * below for the sentences that forced the split. Both dialects' genuine\n * spellings are preserved exactly — that was the constraint on the fix.\n * - `unique violation` — inherited verbatim from the same limb (SQLSTATE\n * 23505's condition name, which some transports render as prose).\n * - `duplicate key` — Postgres' `duplicate key value violates ...`\n * (from `service-messaging` and `driver-sql`).\n * - `duplicate entry` — MySQL's `Duplicate entry 'x' for key 'i'`\n * (from `service-messaging` and `driver-sql`). **This is the limb whose\n * absence made every MySQL conflict a 500.**\n *\n * Deliberately NOT here: bare `constraint failed`, which SQLite emits for\n * NOT NULL and FOREIGN KEY too. A predicate that says \"unique\" too often is a\n * worse bug than the one being fixed — a not-null violation answered as\n * `409 UNIQUE_VIOLATION` tells the client to change a value that is not the\n * problem, and 409 is a status an SDK will not retry.\n *\n * ## Why a VIOLATION phrasing, not the word pair (#8590)\n *\n * The limb used to be a bare `unique constraint`, and a word pair is not a\n * condition: databases put those two words in sentences that say a unique\n * constraint is **ABSENT** just as readily as in ones that say a row broke it.\n * Both spellings below were raised on real servers — SQLite via\n * better-sqlite3, PostgreSQL 16.13 via `pg` 8.22.0, both through knex 3.3.0 —\n * and the bare limb answered `true` to every one of them:\n *\n * ```\n * # SQLITE — no unique index backs the ON CONFLICT target (#8445, #8590)\n * ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint\n *\n * # POSTGRES 42830 — a FOREIGN KEY referencing a non-unique column\n * there is no unique constraint matching given keys for referenced table \"t\"\n * ```\n *\n * The Postgres sentence is why this is an allowlist of violation phrasings and\n * not a negative lookahead on SQLite's sentence. #8590 was filed believing the\n * collision was SQLite-only and that Postgres escaped \"by luck of word order\";\n * measuring the dialects for the fix found 42830, where Postgres puts the same\n * two words adjacent in its own absence sentence. A lookahead keyed on the\n * SQLite wording answers `true` there — it is a blocklist, and it can only ever\n * enumerate the absence sentences somebody already tripped over. Requiring a\n * violation phrasing inverts the default to match this module's stated one:\n * **unrecognised is `false`**, so a sentence nobody has measured is not a\n * conflict until a limb says it is.\n *\n * ⚠️ The three supported dialect families are exactly sqlite / postgres / mysql\n * (`sql-driver.ts` recognises no others), and all three were measured on live\n * servers for #8590, in both directions, including MySQL's `Duplicate entry`\n * path. A dialect added later needs its violation spelling added HERE, measured\n * off a thrown error — not a loosened limb.\n */\nconst UNIQUE_VIOLATION: UniqueViolationSignature = {\n codes: new Set(['23505', 'ER_DUP_ENTRY', 'SQLITE_CONSTRAINT_UNIQUE']),\n errnos: new Set([1062]),\n message: /unique constraint failed|violates unique constraint|unique violation|duplicate key|duplicate entry/i,\n};\n\n/** How far to follow an `error.cause` chain — drivers wrap, but not deeply. */\nconst MAX_CAUSE_DEPTH = 4;\n\n/**\n * Whether a thrown driver error is a unique/primary-key constraint violation.\n *\n * Reads all three channels in turn — `code`, `errno`, `message` — then one step\n * down the `cause` chain, because pool and query-builder layers re-throw with\n * the original attached. A plain string is judged on the message channel, so a\n * caller that has already unwrapped `err.message` can pass it straight in.\n *\n * **Unrecognised is always `false`.** The default has to be \"not a conflict\":\n * a false positive relabels an unrelated failure as the client's fault (a 409\n * an SDK will not retry, pointing at a value that is fine), while a false\n * negative costs only the generic envelope that was the status quo.\n *\n * @param error - the thrown value, of any shape.\n *\n * @example\n * ```ts\n * catch (error) {\n * if (isUniqueViolationError(error)) return conflict(); // 409 UNIQUE_VIOLATION\n * throw error;\n * }\n * ```\n */\nexport function isUniqueViolationError(error: unknown): boolean {\n return matchesUniqueViolation(error, 0);\n}\n\nfunction matchesUniqueViolation(error: unknown, depth: number): boolean {\n if (error === null || error === undefined || depth > MAX_CAUSE_DEPTH) return false;\n\n if (typeof error === 'string') return UNIQUE_VIOLATION.message.test(error);\n if (typeof error !== 'object') return false;\n\n const err = error as { code?: unknown; errno?: unknown; message?: unknown; cause?: unknown };\n\n if (typeof err.code === 'string' && UNIQUE_VIOLATION.codes.has(err.code)) return true;\n // Postgres drivers hand SQLSTATE back as a string; a numeric `code` is\n // MySQL's errno wearing the other field's name, so it is judged as one.\n if (typeof err.code === 'number' && UNIQUE_VIOLATION.errnos.has(err.code)) return true;\n if (typeof err.errno === 'number' && UNIQUE_VIOLATION.errnos.has(err.errno)) return true;\n if (typeof err.message === 'string' && UNIQUE_VIOLATION.message.test(err.message)) return true;\n\n return matchesUniqueViolation(err.cause, depth + 1);\n}\n\n/* ------------------------------------------------------------------------- *\n * #6544 — which column conflicted\n * ------------------------------------------------------------------------- */\n\n/**\n * SQLite names the offending **columns** directly, as `table.column` pairs:\n * `UNIQUE constraint failed: sys_user.email`. Captured to end-of-line because\n * knex prefixes the failing statement, so the useful part is always the tail.\n */\nconst SQLITE_TARGETS = /unique constraint failed:\\s*([^\\n]*)/i;\n\n/**\n * Postgres names the offending **columns** only in its `DETAIL:` line —\n * `Key (email)=(acme@example.com) already exists.` — which node-postgres puts\n * on `error.detail` and knex flattens into the message. The trailing `=(`\n * is required: it is what separates this form from the constraint-name form\n * (`violates unique constraint \"sys_user_email_key\"`), which names an INDEX.\n *\n * An expression index (`Key (lower(email))=(…)`) cannot match, because the\n * capture forbids `)` — which is the correct answer: `lower(email)` is not a\n * column.\n */\nconst POSTGRES_DETAIL_TARGETS = /\\bkey \\(([^)]+)\\)=\\(/i;\n\n/** SQLite's other spelling, for a partial or expression index: `UNIQUE constraint failed: index 'x'`. */\nconst SQLITE_INDEX_FORM = /^index\\b/i;\n\n/** What a column name may look like once the table qualifier and quoting are stripped. */\nconst PLAIN_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;\n\n/** Strip quoting and any `table.` qualifier from one constraint target. */\nfunction bareIdentifier(raw: string): string {\n const stripped = raw.trim().replace(/[`\"'[\\]]/g, '');\n const dot = stripped.lastIndexOf('.');\n return dot >= 0 ? stripped.slice(dot + 1) : stripped;\n}\n\n/**\n * Reduce one dialect's list of constraint targets to THE conflicting column,\n * or `undefined` when there is not exactly one that is determinably a column.\n *\n * A composite key resolves to `undefined` on purpose: there is no single\n * offending column, and picking the first is the same class of wrong answer as\n * returning an index name — it points a form at `tenant_id` when what the user\n * typed twice was `email`.\n */\nfunction soleColumn(targets: string): string | undefined {\n const names = targets.split(',').map(bareIdentifier);\n if (names.length !== 1) return undefined;\n const [name] = names;\n return PLAIN_IDENTIFIER.test(name) ? name : undefined;\n}\n\nfunction columnFromText(text: string): string | undefined {\n const sqlite = SQLITE_TARGETS.exec(text);\n if (sqlite) {\n const targets = sqlite[1].trim();\n // `index 'idx_email_unique'` is an index name, not a column. Refuse.\n return SQLITE_INDEX_FORM.test(targets) ? undefined : soleColumn(targets);\n }\n\n const postgres = POSTGRES_DETAIL_TARGETS.exec(text);\n if (postgres) return soleColumn(postgres[1]);\n\n // MySQL deliberately has no limb here — see the doc comment on\n // `uniqueViolationColumn`. `Duplicate entry 'x' for key 'i'` names `i`,\n // which is an INDEX, and this function does not guess columns from indexes.\n return undefined;\n}\n\nfunction findUniqueViolationColumn(error: unknown, depth: number): string | undefined {\n if (error === null || error === undefined || depth > MAX_CAUSE_DEPTH) return undefined;\n\n if (typeof error === 'string') return columnFromText(error);\n if (typeof error !== 'object') return undefined;\n\n const err = error as { message?: unknown; detail?: unknown; cause?: unknown };\n\n if (typeof err.message === 'string') {\n const fromMessage = columnFromText(err.message);\n if (fromMessage !== undefined) return fromMessage;\n }\n // node-postgres keeps the `DETAIL:` line off the message and on its own\n // field, so for the driver we actually ship this is where the column is.\n if (typeof err.detail === 'string') {\n const fromDetail = columnFromText(err.detail);\n if (fromDetail !== undefined) return fromDetail;\n }\n\n return findUniqueViolationColumn(err.cause, depth + 1);\n}\n\n/**\n * Which column a unique-constraint violation was raised on — or `undefined`\n * when the dialect did not determinably name one (#6544).\n *\n * ## The contract, and why it is this narrow\n *\n * **A value comes back only when the identifier the driver printed is\n * determinably a COLUMN.** When a dialect names an *index* instead — MySQL's\n * `Duplicate entry 'a@b.com' for key 'idx_email_unique'`, Postgres'\n * `violates unique constraint \"sys_user_email_key\"`, SQLite's\n * `UNIQUE constraint failed: index 'idx_lower_email'` — the answer is\n * `undefined`, never the index name.\n *\n * That is the maintainer's 2026-08-08 ruling on #6544, and the reasoning is the\n * caller's, not this module's: **an index name mistaken for a column is worse\n * than no answer at all.**\n *\n * - `@objectstack/rest`'s import runner renders this into a form field —\n * \"A record with this `email` already exists.\" An index name there points\n * the user at a field that does not exist on the object, so they cannot act\n * on it; `undefined` degrades to generic copy, which is merely less helpful.\n * - #5495's autonumber-retry branch asks a yes/no question of the answer —\n * \"is the conflicting column the autonumber field?\" — and an index name\n * produces a *wrong retry decision*, not a vaguer one.\n *\n * ⛔ **The accepted cost: MySQL deployments usually get no column.** MySQL's\n * duplicate-entry message names the index and never the column, so there is\n * nothing here to read. That is deliberate. Do not \"improve\" this by deriving a\n * column from an index name (`idx_email_unique` → `email`, or MySQL 8's\n * `for key 'sys_user.email'` → `email`): index names are free-form, a\n * deployment's may match no column at all, and a plausible-looking wrong field\n * is exactly the failure this export exists to avoid. If MySQL must name\n * columns, the answer is a schema lookup of the index — a different, wider\n * contract — not a guess in this function.\n *\n * A **composite** key is `undefined` for the same reason: `Key (tenant_id,\n * email)=(…)` has no single offending column, and naming the first is the same\n * class of wrong answer.\n *\n * ## What it reads\n *\n * Gated on {@link isUniqueViolationError}, so a NOT NULL or FOREIGN KEY failure\n * can never reach the extraction — SQLite's `NOT NULL constraint failed: t.c`\n * shares its shape with the positive and is refused at the gate, not by the\n * patterns. Then `message`, then `detail` (node-postgres keeps its `DETAIL:`\n * line there), then one step down the `cause` chain, bounded exactly as the\n * predicate's walk is. A bare string is read as a message, so a caller holding\n * only `err.message` can pass it straight in.\n *\n * @param error - the thrown value, of any shape.\n * @returns the conflicting column, or `undefined` when none is determinable.\n *\n * @example\n * ```ts\n * const column = uniqueViolationColumn(error);\n * return column\n * ? `A record with this ${column} already exists.`\n * : 'A record with this value already exists.';\n * ```\n */\nexport function uniqueViolationColumn(error: unknown): string | undefined {\n if (!isUniqueViolationError(error)) return undefined;\n return findUniqueViolationColumn(error, 0);\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * The one named predicate for \"did the database refuse this `ON CONFLICT`\n * target because no PRIMARY KEY or UNIQUE index backs it?\" (#8567).\n *\n * ## ⚠️ This is NOT `isUniqueViolationError` — it is the OPPOSITE condition\n *\n * Read this before touching either predicate. They are one file apart and one\n * word apart in English, and they answer inverse questions:\n *\n * | predicate | the index | the row |\n * |:---|:---|:---|\n * | {@link isUniqueViolationError} | **exists** | violated it |\n * | `isUnbackedConflictTargetError` | **does not exist** | never got compared |\n *\n * Merging them — or reaching for whichever one autocomplete offers — reports a\n * *working* constraint as a missing one, which sends an operator to add an\n * index that is already there while the real duplicate goes unexplained. The\n * warning is repeated at both call sites and in `unique-violation.ts` because\n * it is the most expensive mistake available anywhere near this question.\n *\n * ⛔ **Nothing below may take a limb from that vocabulary, or give one to it.**\n * Unconditional, and permanent: the two predicates answer inverse questions, so\n * a limb that travels between them produces a confident inverted answer. This\n * prohibition was once written as holding \"while #8590 is open\", which was\n * wrong twice over — it reads as expiring, and #8590 has since closed.\n *\n * ⚠️ The separation **was** broken in the pre-existing direction, and pinning\n * it is what found that: `isUniqueViolationError` claimed SQLite's\n * unbacked-target error, because that sentence ends `…PRIMARY KEY or UNIQUE\n * constraint` and its vocabulary matched the word pair `unique constraint`\n * wherever it appeared — including inside a sentence saying the constraint is\n * ABSENT. #8567 filed that as #8590 and pinned it rather than fixing it, which\n * would have moved verdicts in six consuming packages on a card that measured a\n * different question. **#8590 has since closed it**: that predicate's message\n * limb now requires a VIOLATION phrasing — `unique constraint failed` (SQLite)\n * or `violates unique constraint` (Postgres) — so merely mentioning a unique\n * constraint no longer answers yes.\n *\n * ⚠️ Postgres was believed to escape that collision \"by luck of word order\",\n * its `unique or exclusion constraint` not being adjacent. #8590's dialect\n * sweep disproved it: PG **42830**, `there is no unique constraint matching\n * given keys for referenced table \"t\"` — a FOREIGN KEY referencing a non-unique\n * column — puts the pair adjacent in Postgres' own ABSENCE sentence. Both\n * dialects had the collision; only SQLite's instance sat on the path this file\n * measures. That is why the fix is an allowlist of violation phrasings and not\n * a negative lookahead on SQLite's sentence, which would still answer `true`\n * there.\n *\n * `unbacked-conflict-target.test.ts` records both predicates' verdicts on every\n * measured text, per dialect, and `unique-violation-absence-sentences.test.ts`\n * pins the absence sentences on both sides — so neither a fix nor a fresh drift\n * can land silently in either direction.\n *\n * ## What each dialect actually says — measured, never transcribed\n *\n * #8445 landed this recognition for SQLite alone and said so: the container\n * that implemented it had no other server, and transcribing another dialect's\n * wording from memory was ruled out as evidence. #8567 raised the condition on\n * a real Postgres 16.13 (system PG16 binaries, `initdb` + `pg_ctl`, no\n * container runtime) through the same knex + `pg` path `SqlDriver.upsert`\n * uses, and read the fields off the thrown error object:\n *\n * ```\n * # POSTGRES 16.13, knex 3.3.0 + pg 8.22.0\n * upsert('plain', { email: 'a@b.com', title: 'x' }, ['email'])\n * -> name=error (DatabaseError) code=42P10 severity=ERROR status=undefined\n * routine=infer_arbiter_indexes constraint=undefined detail=undefined\n * msg=insert into \"plain\" (\"email\", \"id\", \"title\") values ($1, $2, $3)\n * on conflict (\"email\") do update set \"title\" = excluded.\"title\"\n * - there is no unique or exclusion constraint matching the ON CONFLICT specification\n *\n * # SQLITE 3.x, knex 3.3.0 + better-sqlite3 (#8445's measurement, unchanged)\n * upsert('plain', { email: 'a@b.com', title: 'x' }, ['email'])\n * -> name=SqliteError code=SQLITE_ERROR status=undefined\n * msg=insert into `plain` (...) values (...) on conflict (`email`) do update set ...\n * - ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint\n * ```\n *\n * Two dialects, two unrelated sentences, and the same envelope from knex: the\n * STATEMENT, then ` - `, then the server's own text. That tail is what both\n * limbs below are anchored on, so a knex-prefixed message and a bare driver\n * message are recognised identically.\n *\n * ## Why the `code` channel is unused — also measured\n *\n * The obvious predicate is `code === '42P10'`, and it is wrong in both\n * directions:\n *\n * - **SQLite has no code to read.** It answers plain `SQLITE_ERROR`, the same\n * generic code a syntax error or a missing table carries. `driver-sql`'s\n * own suite pins that: an upsert against a table that was never created\n * must come back as itself, and it is a `SQLITE_ERROR` too.\n * - **Postgres' code OVER-matches.** `42P10` is `invalid_column_reference`,\n * not \"unbacked conflict target\". Measured on the same cluster, same\n * session:\n *\n * ```\n * select id from plain order by 7 -> code=42P10 \"ORDER BY position 7 is not in select list\"\n * select id from plain group by 9 -> code=42P10 \"GROUP BY position 9 is not in select list\"\n * ```\n *\n * A code-only limb would answer `VALIDATION_ERROR` \"no unique index backs\n * your conflict keys\" to a caller whose real defect is an out-of-range sort\n * position — a refusal pointing at the wrong thing entirely. So the message\n * is not a fallback for a missing code here; it is the only channel that\n * identifies the condition, on both dialects, and the code channel is\n * deliberately left unread rather than ANDed in for a narrowing it does not\n * provide.\n *\n * The Postgres limb is safe to match on prose because the sentence has exactly\n * one source: `infer_arbiter_indexes` (`plancat.c`), reached only while\n * planning an `ON CONFLICT` inference, which the measured `routine` field\n * confirms. The SQLite limb has the same property, stated at #8445.\n *\n * ## MySQL: the condition cannot arise, and that is measured too\n *\n * MySQL has no `ON CONFLICT` syntax. knex compiles the driver's exact call to\n * `ON DUPLICATE KEY UPDATE`, which takes **no conflict target** — the named\n * keys are dropped from the statement before it leaves the process, so the\n * server is never asked to find an index for them and cannot complain that\n * none exists. Compiled with knex 3.3.0 on the `mysql2` dialect, no server\n * needed (`.toSQL()`), and pinned by\n * `sql-driver-upsert-conflict-target-dialects.test.ts`:\n *\n * ```\n * knex('plain').insert({...}).onConflict(['email']).merge(['title']).toSQL()\n * mysql2 -> insert into `plain` (`email`, `id`, `title`) values (?, ?, ?)\n * on duplicate key update `title` = values(`title`) ← no `email` target\n * pg -> insert into \"plain\" (...) values ($1, $2, $3)\n * on conflict (\"email\") do update set \"title\" = excluded.\"title\"\n * ```\n *\n * So there is no MySQL limb to write, and its absence is a finding rather than\n * a gap. ⚠️ What MySQL does *instead* — merge on whichever unique key the row\n * happens to collide with, or insert a second row — is a different defect with\n * a different fix, and is NOT this predicate's business.\n *\n * ## Home\n *\n * `@objectstack/types`, beside {@link isUniqueViolationError}, for the reason\n * that module records: every consumer of the question already depends on this\n * package, so naming it here never adds an edge, and this module deliberately\n * imports nothing. The alternative — a second private regex in each driver\n * that meets the condition — is exactly the state `unique-violation.ts` was\n * written to retire, where four hand-written vocabularies disagreed about\n * MySQL and nobody could see it.\n */\n\n/**\n * One dialect vocabulary for this condition, in the channel that carries it.\n *\n * Deliberately **message-only**, unlike `UniqueViolationSignature`'s\n * three-channel table — the module head records the measurements: SQLite's\n * `code` is the generic `SQLITE_ERROR`, and Postgres' `42P10` is\n * `invalid_column_reference`, which an out-of-range `ORDER BY` position also\n * raises. Neither channel narrows anything, and a `codes` set standing empty\n * beside them would read as \"nobody has filled this in yet\" rather than as the\n * decision it is.\n */\ninterface UnbackedConflictTargetSignature {\n /**\n * `error.message` — matched on the server's own sentence, which knex leaves\n * as the tail after the statement and ` - `.\n */\n readonly message: RegExp;\n}\n\n/**\n * Every wording measured for this condition, one limb per dialect that can\n * raise it. Nothing here is inferred: each limb was read off a thrown error\n * object, and the transcript is in the module head above.\n *\n * - SQLite: `ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE\n * constraint` — stable since `ON CONFLICT` arrived in 3.24 (#8445).\n * - Postgres: `there is no unique or exclusion constraint matching the ON\n * CONFLICT specification` — `infer_arbiter_indexes`, PG 16.13 (#8567).\n *\n * Deliberately NOT here: any limb for MySQL (the condition cannot reach the\n * server — see the module head), and any bare `ON CONFLICT` fragment. A limb\n * loose enough to match `on conflict` alone would match the driver's own\n * *statement* text, which knex prefixes onto every upsert failure — including\n * a unique violation, which is the opposite condition.\n */\nconst UNBACKED_CONFLICT_TARGET: UnbackedConflictTargetSignature = {\n message:\n /ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint|there is no unique or exclusion constraint matching the ON CONFLICT specification/i,\n};\n\n/** How far to follow an `error.cause` chain — drivers wrap, but not deeply. */\nconst MAX_CAUSE_DEPTH = 4;\n\n/**\n * Whether a thrown driver error says the `ON CONFLICT` target it was given is\n * backed by no PRIMARY KEY or UNIQUE index.\n *\n * Reads the message channel, then one step at a time down the `cause` chain —\n * pool and query-builder layers re-throw with the original attached, and the\n * refusal this predicate gates keeps the raw error as its own `cause`. A plain\n * string is judged directly, so a caller that already unwrapped `err.message`\n * can pass it in.\n *\n * **Unrecognised is always `false`.** A false positive is the expensive\n * direction: it tells a caller to go add an index when the real failure was a\n * syntax error, a missing table, or — worst — a genuine unique violation on an\n * index that exists and works. A false negative costs only the raw error that\n * was the status quo before recognition existed.\n *\n * @param error - the thrown value, of any shape.\n *\n * @example\n * ```ts\n * catch (error) {\n * // ⚠️ NOT isUniqueViolationError — that is the opposite condition.\n * if (isUnbackedConflictTargetError(error)) throw refuseUnbackedConflictTarget(object, keys, error);\n * throw error;\n * }\n * ```\n */\nexport function isUnbackedConflictTargetError(error: unknown): boolean {\n return matchesUnbackedConflictTarget(error, 0);\n}\n\nfunction matchesUnbackedConflictTarget(error: unknown, depth: number): boolean {\n if (error === null || error === undefined || depth > MAX_CAUSE_DEPTH) return false;\n\n if (typeof error === 'string') return UNBACKED_CONFLICT_TARGET.message.test(error);\n if (typeof error !== 'object') return false;\n\n const err = error as { message?: unknown; cause?: unknown };\n\n if (typeof err.message === 'string' && UNBACKED_CONFLICT_TARGET.message.test(err.message)) return true;\n\n return matchesUnbackedConflictTarget(err.cause, depth + 1);\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * [ADR-0120 D5e] The `isolated`-posture install gate for `'global'` uniqueness.\n *\n * ## Why a gate exists at all\n *\n * ADR-0120's scope vocabulary is deliberately **posture-invariant**: the author\n * states a business boundary (`'organization'` = one holder per organization,\n * `'global'` = one holder across the whole installation) and the same app\n * package runs unmodified under every tenancy posture (ADR-0105 D1\n * `single | group | isolated`). No index shape reads the posture — a posture\n * flip has zero automatic schema consequences, which is exactly what makes one\n * app package serve all three.\n *\n * One residual survives that invariance, and only in one direction\n * (ADR-0120 §Posture portability, Resolved question #4):\n *\n * - Under `single` / `group`, `'global'` means \"the installation\" — which for a\n * `group` deployment IS the customer company (集团). An app business rule\n * spelled `'global'` is correct there.\n * - Under `isolated`, organizations are **separate customers**. The identical\n * declaration now crosses customers: it over-constrains (customer B cannot\n * reuse customer A's material code) and it becomes a cross-tenant existence\n * oracle — the very leak #3696 closed for field-level uniques (S10).\n *\n * `'global'` is therefore physically posture-invariant but not *safety*-invariant,\n * and the ADR's S14 row records the honest cost: \"unique across the whole\n * company\" is not expressible in metadata alone, because it means the\n * installation under `group` and one organization under `isolated`. A third,\n * posture-resolved word (`'company'`) was designed and **rejected** — it is the\n * one token that cannot be used without first understanding the posture\n * spectrum, exactly the cognitive load an AI-authored vocabulary must not carry.\n * The scenario is handled **here**, at the deployment seam, instead.\n *\n * ## Why a HARD stop and not an advisory\n *\n * Maintainer decision, 2026-08-04 (ADR-0120 Resolved #4). An advisory that\n * nobody reads leaves a cross-customer constraint enforced in production — the\n * ADR-0049/0078 class this whole ADR exists to close. So installing an app that\n * carries `'global'` uniques on non-`sys` objects into an `isolated` environment\n * **stops**, lists each index, and asks the installer (typically an AI agent) to\n * either confirm it as genuinely platform-wide or rewrite it to\n * `'organization'`. The confirmation is recorded in the install manifest\n * (ADR-0104 attestation style) so it is **never re-asked**.\n *\n * ⛔ **Never a boot-time warning** (#4884 discipline). A deployment whose apps\n * were installed before this gate existed, or whose posture changed after\n * install, is reached by the ADVISORY form in `os doctor` / `os migrate plan` —\n * the two cases a gate at the install seam structurally cannot see. Turning\n * this into a startup diagnostic would fire on every boot of every deployment\n * forever, which is the false-alarm class #4884 retired.\n *\n * ## What counts as a finding\n *\n * | Declaration | Finding? | Why |\n * |:---|:---|:---|\n * | field `unique: 'global'` | ✅ | one holder across the installation — crosses customers under `isolated` |\n * | declared index `unique: 'global'` | ✅ | same boundary, spelled on the index |\n * | declared index `unique: true` | ✅ | ADR-0120 D1: bare `true` **is** the deprecated positional spelling of `'global'`; identical physical shape, identical hazard. Excluding it would leave the gate bypassable by spelling for the whole of 17.x |\n * | field `unique: true` / `'organization'` | ❌ | per-organization — correct under every posture |\n * | declared index `unique: 'organization'` | ❌ | per-organization (D3 NULL-safe key part) |\n * | anything on a `sys_*` object | ❌ | engine idempotency / dedup keys (the ADR's S5 inventory) are platform-wide **by construction**; asking about them on every install is the false-alarm class again |\n *\n * The enumeration is a pure projection of declared metadata — no tenancy\n * inference, no database access — which is what lets the identical function\n * serve the hard gate, `os doctor` and `os migrate plan`.\n */\n\nimport { normalizeTenancyPosture, type TenancyPosture } from '@objectstack/spec/security';\n\n/** Objects owned by the platform itself never raise a finding. */\nconst SYS_OBJECT_PREFIXES = ['sys_', 'base_'] as const;\n\n/**\n * Is this object platform-owned (the ADR's \"`sys` objects\")?\n *\n * The ADR scopes the gate to **non-`sys`** objects because the platform's own\n * `'global'` uniques are the S5 inventory — `sys_job.name`,\n * `sys_notification.dedup_key`, `http_delivery (source, dedup_key)` and the rest\n * — engine idempotency keys that are platform-wide on purpose and identical\n * under every posture. Re-confirming them on every app install would be the\n * #4884 false-alarm class with extra steps.\n *\n * `base_` is included alongside `sys_`: it is the platform's other reserved\n * object prefix, carrying the same \"owned by the framework, not the app\"\n * meaning. An app object can never legitimately claim either.\n */\nexport function isPlatformOwnedObject(objectName: unknown): boolean {\n const name = typeof objectName === 'string' ? objectName.trim().toLowerCase() : '';\n if (!name) return false;\n return SYS_OBJECT_PREFIXES.some((prefix) => name.startsWith(prefix));\n}\n\n/**\n * Does a FIELD-level `unique` value ask for the installation-wide boundary?\n *\n * Only the explicit `'global'` does. Bare `true` at field level is the\n * documented, unambiguous synonym of `'organization'` (ADR-0120 D1 —\n * \"field-level bare `true` stays valid indefinitely\", Resolved #2), so it is\n * never a finding.\n */\nexport function fieldUniqueIsGlobal(unique: unknown): boolean {\n return unique === 'global';\n}\n\n/**\n * Does a DECLARED-INDEX `unique` value ask for the installation-wide boundary?\n *\n * `'global'` and bare `true` both do. Per ADR-0120 D1 the bare spelling **is**\n * `'global'` — \"today's verbatim semantics, materialized over exactly the listed\n * columns\" — deprecated (lint `unique/unscoped-declared-index` warns in 17.x,\n * protocol 18 rejects it, #5082) but physically identical while it lasts. A gate\n * that judged only the explicit word would be bypassable by writing the\n * deprecated one, which is the #4986 trap wearing the gate's own uniform.\n */\nexport function declaredIndexUniqueIsGlobal(unique: unknown): boolean {\n return unique === 'global' || unique === true;\n}\n\n/** One installation-wide unique declaration found on an app (non-`sys`) object. */\nexport interface GlobalUniqueFinding {\n /** Stable identity for the attestation record — see {@link globalUniqueFindingId}. */\n readonly id: string;\n /** Object (and therefore table) the declaration sits on. */\n readonly object: string;\n /** Which spelling carried it. */\n readonly kind: 'field' | 'index';\n /** Field name for `kind: 'field'`; the index's declared name (when it has one) otherwise. */\n readonly name?: string;\n /** The columns the constraint spans, in declaration order. */\n readonly columns: readonly string[];\n /** The exact authored value (`true` | `'global'`) — quoted back in the stop message. */\n readonly spelling: true | 'global';\n}\n\n/**\n * Stable id for one finding, used as the attestation key.\n *\n * Keyed by object + kind + **columns**, deliberately NOT by the index's optional\n * `name`: a declared index may be anonymous, and renaming an index does not\n * change which constraint the installer confirmed. Two indexes on the same\n * object spanning the same columns are the same constraint by any physical\n * reading, so collapsing them is correct rather than lossy.\n */\nexport function globalUniqueFindingId(\n objectName: string,\n kind: 'field' | 'index',\n columns: readonly string[],\n): string {\n return `${objectName}:${kind}:${columns.join('+')}`;\n}\n\n/** Field map or field array — both authoring shapes are accepted. */\nfunction fieldEntriesOf(fields: unknown): Array<{ name: string; def: any }> {\n if (!fields) return [];\n if (Array.isArray(fields)) {\n return fields\n .filter((f: any) => f && f.name != null)\n .map((f: any) => ({ name: String(f.name), def: f }));\n }\n if (typeof fields !== 'object') return [];\n return Object.entries(fields as Record<string, any>).map(([name, def]) => ({ name, def }));\n}\n\n/**\n * Enumerate every installation-wide unique declared on an app's non-`sys`\n * objects (ADR-0120 D5e).\n *\n * Pure and posture-agnostic on purpose: the CALLER decides whether the posture\n * makes these findings a hard stop (`isolated`, at install) or an advisory\n * (`os doctor` / `os migrate plan`). Deterministic order — objects as supplied,\n * fields before indexes within an object — so the stop message and the\n * attestation record are reproducible across runs.\n */\nexport function collectGlobalUniques(objects: unknown): GlobalUniqueFinding[] {\n if (!Array.isArray(objects)) return [];\n const findings: GlobalUniqueFinding[] = [];\n\n for (const obj of objects as any[]) {\n const objectName = typeof obj?.name === 'string' ? obj.name.trim() : '';\n if (!objectName) continue;\n if (isPlatformOwnedObject(objectName)) continue;\n\n for (const { name, def } of fieldEntriesOf(obj?.fields)) {\n if (!fieldUniqueIsGlobal(def?.unique)) continue;\n findings.push({\n id: globalUniqueFindingId(objectName, 'field', [name]),\n object: objectName,\n kind: 'field',\n name,\n columns: [name],\n spelling: 'global',\n });\n }\n\n const declaredIndexes = Array.isArray(obj?.indexes) ? obj.indexes : [];\n for (const idx of declaredIndexes as any[]) {\n if (!declaredIndexUniqueIsGlobal(idx?.unique)) continue;\n const columns = Array.isArray(idx?.fields)\n ? idx.fields.filter((f: unknown) => typeof f === 'string').map((f: string) => f)\n : [];\n if (columns.length === 0) continue;\n const indexName = typeof idx?.name === 'string' && idx.name.trim() ? idx.name.trim() : undefined;\n findings.push({\n id: globalUniqueFindingId(objectName, 'index', columns),\n object: objectName,\n kind: 'index',\n ...(indexName ? { name: indexName } : {}),\n columns,\n spelling: idx.unique === true ? true : 'global',\n });\n }\n }\n\n return findings;\n}\n\n/**\n * The attestation recorded in the install manifest once an installer has\n * confirmed a set of findings as genuinely platform-wide (ADR-0104 style).\n *\n * Shape follows the ADR-0104 precedent rather than inventing one: the FACT\n * observed (which constraint ids a human/agent affirmed), WHO affirmed it, WHEN,\n * and under WHICH posture the question was asked. That last field is what keeps\n * the record honest — an attestation given under `isolated` is evidence about\n * `isolated`, and nothing else.\n *\n * Never rewritten in place: confirmations ACCUMULATE. A later install of a newer\n * version that adds a new `'global'` index asks about the new one only — the\n * earlier answers stand, which is the \"之后不复问\" half of the decision.\n */\nexport interface GlobalUniqueAttestation {\n /** Posture the confirmation was given under. */\n readonly posture: TenancyPosture;\n /** Finding ids affirmed as genuinely platform-wide. */\n readonly confirmed: readonly string[];\n /** ISO timestamp of the most recent confirmation. */\n readonly attestedAt: string;\n /** Identity of the confirming installer, when the seam knows one. */\n readonly attestedBy?: string | null;\n}\n\n/**\n * Which findings still need an answer, given an existing attestation.\n *\n * Returns the findings NOT covered by `attestation.confirmed`. An empty result\n * means the install proceeds silently — this is the mechanism behind \"never\n * re-asked\".\n *\n * An attestation recorded under a DIFFERENT posture does not carry over: the\n * question \"is this genuinely platform-wide, knowing organizations here are\n * separate customers?\" was never asked. Confirmations made under `isolated` are\n * the only ones that answer it, so a `single`-posture record is treated as\n * absent rather than as consent — the conservative direction, and the only one\n * that cannot silently admit a cross-customer constraint.\n */\nexport function unconfirmedGlobalUniques(\n findings: readonly GlobalUniqueFinding[],\n attestation: GlobalUniqueAttestation | undefined | null,\n posture: TenancyPosture,\n): GlobalUniqueFinding[] {\n if (!attestation || attestation.posture !== posture) return [...findings];\n const confirmed = new Set(attestation.confirmed ?? []);\n return findings.filter((f) => !confirmed.has(f.id));\n}\n\n/**\n * Merge a new set of confirmations into an existing attestation.\n *\n * Additive by construction — see {@link GlobalUniqueAttestation}. A record from\n * another posture is replaced rather than merged: its `confirmed` ids answered a\n * different question.\n */\nexport function recordGlobalUniqueAttestation(\n previous: GlobalUniqueAttestation | undefined | null,\n confirmedIds: readonly string[],\n posture: TenancyPosture,\n attestedBy?: string | null,\n now: string = new Date().toISOString(),\n): GlobalUniqueAttestation {\n const carried = previous && previous.posture === posture ? previous.confirmed ?? [] : [];\n const merged = Array.from(new Set([...carried, ...confirmedIds])).sort();\n return {\n posture,\n confirmed: merged,\n attestedAt: now,\n ...(attestedBy !== undefined ? { attestedBy } : {}),\n };\n}\n\n/** Render one finding the way both the hard stop and the advisory quote it. */\nexport function describeGlobalUniqueFinding(finding: GlobalUniqueFinding): string {\n const spelling = finding.spelling === true ? '`unique: true`' : \"`unique: 'global'`\";\n const deprecated = finding.spelling === true ? ' [deprecated bare spelling of \\'global\\']' : '';\n if (finding.kind === 'field') {\n return `${finding.object}.${finding.name} — field-level ${spelling}`;\n }\n const label = finding.name ? ` '${finding.name}'` : '';\n return `${finding.object} — declared index${label} [${finding.columns.join(', ')}] ${spelling}${deprecated}`;\n}\n\n/**\n * The prescription every surface repeats verbatim, so the hard stop and the two\n * advisories cannot drift into three different pieces of advice.\n */\nexport const GLOBAL_UNIQUE_ISOLATED_PRESCRIPTION =\n \"Under the 'isolated' posture organizations are separate CUSTOMERS, so an installation-wide unique \" +\n 'constrains across customers and can reveal that another customer already holds a value (ADR-0120 S10/S14). ' +\n 'For each index above, either (a) confirm it is genuinely platform-wide — an infrastructure/dedup key, a DNS ' +\n 'hostname, an external provider id — or (b) rewrite it to `unique: \\'organization\\'` so it is one holder per ' +\n 'organization. See ADR-0120 §Posture portability.';\n\n/**\n * The full hard-stop message for an install into an `isolated` environment.\n *\n * Built here rather than at the install seam so the CLI, the HTTP surface and\n * the tests all quote one text.\n */\nexport function buildGlobalUniqueStopMessage(\n appLabel: string,\n findings: readonly GlobalUniqueFinding[],\n): string {\n const lines = findings.map((f) => ` • ${describeGlobalUniqueFinding(f)}`);\n return (\n `'${appLabel}' declares ${findings.length} installation-wide unique constraint(s) on its own objects, and this ` +\n \"environment runs the 'isolated' tenancy posture (ADR-0120 D5e):\\n\" +\n `${lines.join('\\n')}\\n` +\n `${GLOBAL_UNIQUE_ISOLATED_PRESCRIPTION}\\n` +\n 'Re-run the install with the confirmation to record it in the install manifest — it is asked once, ' +\n 'never again for the same constraints.'\n );\n}\n\n/** Error code the install seam returns when the gate stops an install. */\nexport const GLOBAL_UNIQUE_CONFIRMATION_REQUIRED = 'UNIQUE_SCOPE_CONFIRMATION_REQUIRED';\n\n/**\n * Does this posture make `'global'` uniques a decision point at all?\n *\n * `isolated` only. Under `single` there is one customer; under `group` the\n * installation IS the customer company, which is what `'global'` means there —\n * both are the benign direction the ADR leaves to the app's install notes.\n */\nexport function postureGatesGlobalUniques(posture: unknown): boolean {\n return normalizeTenancyPosture(posture) === 'isolated';\n}\n"],"mappings":"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","names":["import_api","MAX_CAUSE_DEPTH","import_security"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/degraded-boot.ts","../src/email-verified.ts","../src/env.ts","../src/error-leak.ts","../src/keyset-walk.ts","../src/module-not-found.ts","../src/server-fault-log.ts","../src/response-envelope.ts","../src/thrown-http-error.ts","../src/validation-failure.ts","../src/relation-sub-object.ts","../src/unique-violation.ts","../src/driver-error-classification.ts","../src/unbacked-conflict-target.ts","../src/unique-scope-install-gate.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nexport * from './degraded-boot.js';\n// [#11343/#12751] The one verified-email predicate the walled owner-elevation\n// gate (plugin-security) and the owner-verification boot diagnostic\n// (plugin-auth) both read — see the module doc for why it must be one.\nexport * from './email-verified.js';\nexport * from './env.js';\nexport * from './error-leak.js';\n// Seek-based pagination for batch walks — the offset alternative that neither\n// skips rows when the walk mutates as it goes, nor costs O(n²/p) (#4363).\nexport * from './keyset-walk.js';\nexport * from './module-not-found.js';\nexport * from './response-envelope.js';\n// [#8016] The one rule for \"what HTTP answer does a THROWN error declare?\",\n// plus the validation-failure recogniser it reads. Both doors of\n// `/api/v1/packages` call it: the runtime dispatcher's `errorFromThrown` and the\n// direct-mount REST registrar, which used to answer 500 INTERNAL_ERROR for a\n// coded 4xx the dispatcher mapped correctly.\nexport * from './thrown-http-error.js';\n// [#14310] The sibling rule to the one above, for the same two doors: \"is this\n// answer worth an operator's attention?\". `resolveThrownHttpError` decides what\n// the CLIENT is told; this decides what the LOG says — 5xx always, 4xx never.\nexport * from './server-fault-log.js';\nexport * from './validation-failure.js';\n// [#6615] The one home for Postgres' `«sub-object» \"x\" of relation \"y\"` phrase,\n// whose missing-COLUMN spelling contains a legal missing-TABLE phrase as a\n// substring. Three packages had each repaired that superstring hole separately.\nexport * from './relation-sub-object.js';\n// [#6250] The one named \"is this a unique-constraint violation?\" predicate.\n// Four hand-written vocabularies used to answer it and disagreed about MySQL,\n// which is why every MySQL conflict came back 500 instead of 409.\nexport * from './unique-violation.js';\n// [#4728/#4825, moved here by #13279] The one \"which driver failures may be\n// silenced?\" vocabulary — `isMissingTableError` (a READ failed because the\n// table was never provisioned) and `isSchemaAlreadyExistsError` (a DDL failure\n// that was just the table already being there). It was `@objectstack/metadata`'s\n// until `@objectstack/core`'s authorization resolver had to ask it, and core\n// cannot import metadata — metadata depends on core. Same Home rule as\n// `unique-violation.js` above: every consumer already depends on this package,\n// so adopting the predicate adds no edge. `@objectstack/metadata/errors` still\n// re-exports `isMissingTableError` for its published consumers.\nexport * from './driver-error-classification.js';\n// [#8567] The OPPOSITE question, kept deliberately separate: \"is this the\n// database refusing an ON CONFLICT target that no unique index backs?\" One\n// measured limb per dialect that can raise it (SQLite, Postgres); MySQL cannot,\n// because knex compiles the conflict target away. Never merge the two — a\n// merged predicate reports a working constraint as a missing one.\nexport * from './unbacked-conflict-target.js';\n// [ADR-0120 D5e] The `isolated`-posture install gate for `'global'` uniques —\n// the pure enumerator both the hard stop (install seam) and the advisories\n// (`os doctor` / `os migrate plan`) read, so the three cannot drift apart.\nexport * from './unique-scope-install-gate.js';\n\n// Placeholder for Kernel interface to avoid circular dependency\n// The actual Kernel implementation will satisfy this interface.\nexport interface IKernel {\n // We can add specific methods here that plugins are allowed to call\n // forcing a stricter contract than exposing the whole class.\n ql?: any; // ObjectQL instance (optional to support initialization phase)\n start(): Promise<void>;\n // ... expose other needed public methods\n [key: string]: any; \n}\n\nexport interface RuntimeContext {\n engine: IKernel;\n}\n\nexport interface RuntimePlugin {\n name: string;\n install?: (ctx: RuntimeContext) => void | Promise<void>;\n onStart?: (ctx: RuntimeContext) => void | Promise<void>;\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Degraded-boot reporting, shared by every subsystem that can be told to boot\n * without a datasource it needs.\n *\n * Two of them exist today and they opt in through the *same* operator flag\n * (`OS_ALLOW_DRIVER_CONNECT_FAILURE`, see {@link resolveAllowDriverConnectFailure}):\n *\n * - `ObjectQLEngine.init()` — a boot-registered driver whose `connect()`\n * rejected (framework#3741).\n * - `DatasourceConnectionService` — a declared datasource that objects bind to\n * explicitly, or an `external` one with `validation.onMismatch:'fail'`,\n * that could not be connected (framework#3758).\n *\n * They live in different packages but owe the operator the same thing: the\n * degraded state must be impossible to miss.\n */\n\n/**\n * Emit the degraded-boot banner on a channel the host cannot accidentally\n * silence.\n *\n * `OS_ALLOW_DRIVER_CONNECT_FAILURE` only justifies itself if the state it opts\n * into is impossible to miss — and a logger-only banner is missable, because\n * the logger answers to a level the operator sets. `Logger.write()` returns\n * before emitting anything when the record is below `config.level`, so at\n * `--log-level error`, `fatal`, or `silent` this `warn` never reaches ANY\n * stream. A production host running at `error` is exactly the deployment this\n * flag exists for, and is exactly where the banner would vanish. Writing to\n * stderr as well is the same belt-and-braces the kernel already uses for\n * plugin startup failures.\n *\n * A second reason used to be load-bearing and no longer is: `os serve` blanked\n * ALL of stdout while the kernel booted, and `Logger` routes `warn` to stdout,\n * so a boot-phase banner was swallowed at every level. That was framework#4012\n * and is fixed — the boot window buffers and replays `warn`-and-above instead\n * of discarding it. Do not re-derive this helper's necessity from the\n * boot-quiet capture; the level filter is what keeps it alive.\n *\n * Best-effort and never throws: falls back to `console.error`, then to silence\n * on runtimes that have neither (the logger still carries the structured\n * record either way).\n */\nexport function emitDegradedBootBanner(message: string): void {\n const proc = (globalThis as {\n process?: { stderr?: { write?: (chunk: string) => unknown } };\n }).process;\n try {\n if (typeof proc?.stderr?.write === 'function') {\n proc.stderr.write(`${message}\\n`);\n return;\n }\n } catch {\n /* stderr unavailable / closed — fall through to console */\n }\n try {\n (globalThis as { console?: { error?: (msg: string) => void } }).console?.error?.(message);\n } catch {\n /* no output channel at all — the logger record is the remaining trace */\n }\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * [#11343 / #12751] Verified-email predicate over a stored `sys_user` row — a\n * fail-closed ALLOW-LIST over the representations a driver may hand back for\n * the `sys_user.email_verified` boolean column (JS `true`, SQLite `1`, and\n * their stringified forms). Everything else — `false`/`0`, `null`, an ABSENT\n * field on an imported/legacy row, or any representation not listed — reads\n * as UNVERIFIED. Absent-means-unverified is deliberate: treating a missing\n * column as verified would re-open the exact hole this predicate closes for\n * every row that predates the column.\n *\n * ONE resolution, several consumers, by design (#12751) — and since the\n * #11663 platform-admin re-anchor (leg L4) the walled platform-admin\n * ELEVATION GATE this paragraph used to name first is RETIRED: under a\n * walled posture `bootstrapPlatformAdmin` writes no grant row and elevates\n * nobody, it reports. Standing is derived PER REQUEST instead — from a\n * config-anchored verified email, or the legacy unscoped grant row — so the\n * consumer set now includes the authorization derivation itself:\n *\n * - `matchesConfiguredPlatformAdmin` (`@objectstack/core`\n * `security/platform-admin.ts`), read at the one derivation site\n * (`resolve-authz-context.ts` §6b-config), where an UNVERIFIED account\n * holding a declared address confers nothing — and, through it,\n * `plugin-auth`'s last-admin guard, whose administrator enumeration must\n * answer the same question the resolver does;\n * - `resolvePlatformAdminStanding` (`plugin-security`\n * `platform-admin-service.ts`), the read-only standing/audit answer the\n * walled boot reports from, and `isVerifiedPlatformOwnerRow` beside it\n * (`platform-owner-wall-bypass.ts`), the Layer 0 wall bypass;\n * - the walled owner-verification boot diagnostic (`plugin-auth`\n * `walled-owner-verification-path.ts`, where the check decides whether the\n * declared owner's account is already past needing a verification path).\n *\n * They must all answer \"is this row verified?\" identically — a drift is no\n * longer just a boot warning forecasting a refusal that will not be made, it\n * is a diagnostic, an audit surface or a guard disagreeing with who actually\n * resolves PLATFORM_ADMIN on the next request. `@objectstack/types` is the\n * shared home every one of those packages already resolves\n * `OS_PLATFORM_OWNER_EMAIL` from (`env.ts`).\n */\nexport function isEmailVerifiedUserRow(row: unknown): boolean {\n const v = (row as { email_verified?: unknown } | null | undefined)?.email_verified;\n return v === true || v === 1 || v === '1' || v === 'true';\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Environment-variable helpers shared across `@objectstack/*` packages.\n *\n * The framework standardises on `OS_*` prefixed env vars (see AGENTS.md\n * \"Environment Variables\" section). Some historical names predate this\n * convention — `AUTH_SECRET`, `ROOT_DOMAIN`, `OBJECTSTACK_*`, …\n *\n * To migrate without breaking user `.env` files mid-release, call\n * {@link readEnvWithDeprecation} at every legacy read site:\n *\n * const v = readEnvWithDeprecation('OS_AUTH_SECRET', 'AUTH_SECRET');\n *\n * If only the legacy name is set, the value is still returned but a\n * one-shot `console.warn` fires (per-process per-variable) telling\n * operators to rename it.\n */\n\nimport {\n normalizeTenancyPosture,\n TENANCY_POSTURES,\n type TenancyPosture,\n} from '@objectstack/spec/security';\n\nconst _warnedKeys = new Set<string>();\n\n/**\n * Read an env var, preferring the canonical `OS_*` name and falling\n * back to one or more legacy aliases.\n *\n * When only a legacy alias is set, emits a one-shot deprecation warning.\n * The warning is process-wide deduplicated: identical (preferred, legacy)\n * pairs will only warn once even if read from multiple call sites.\n *\n * Legacy aliases are checked in order; the first one with a defined\n * value wins (and triggers the warning for that specific alias).\n *\n * Safe to call from environments where `process` is unavailable (returns\n * `undefined`); the warning is suppressed when running outside Node-like\n * runtimes that lack `console.warn`.\n *\n * @param preferred Canonical OS_*-prefixed env var name.\n * @param legacy Older name (or array of older names) to fall back on.\n * @param options Optional behaviour flags. Set `silent: true` for aliases\n * that remain accepted conventions rather than true legacy\n * names — e.g. `PORT`, which PaaS platforms (Render, Railway,\n * Heroku, Fly, …) inject automatically. Warning on those\n * would nag operators about env they never set.\n * @returns The resolved value, or `undefined` if neither is set.\n */\nexport function readEnvWithDeprecation(\n preferred: string,\n legacy: string | readonly string[],\n options?: { silent?: boolean },\n): string | undefined {\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } })\n .process?.env;\n if (!env) return undefined;\n\n const preferredValue = env[preferred];\n if (preferredValue !== undefined) return preferredValue;\n\n const legacyList = typeof legacy === 'string' ? [legacy] : legacy;\n for (const legacyName of legacyList) {\n const legacyValue = env[legacyName];\n if (legacyValue !== undefined) {\n const dedupeKey = `${preferred}|${legacyName}`;\n if (!options?.silent && !_warnedKeys.has(dedupeKey)) {\n _warnedKeys.add(dedupeKey);\n const consoleRef = (globalThis as { console?: { warn?: (msg: string) => void } }).console;\n try {\n consoleRef?.warn?.(\n `[ObjectStack] Env var \\`${legacyName}\\` is deprecated; rename it to \\`${preferred}\\`. ` +\n `The legacy name still works for now but will be removed in a future major release.`,\n );\n } catch {\n /* `console.warn` unavailable (exotic runtime) — ignore */\n }\n }\n return legacyValue;\n }\n }\n\n return undefined;\n}\n\n/**\n * Read the LEGACY `OS_MULTI_ORG_ENABLED` boolean.\n *\n * ⚠️ **[ADR-0105 D1] DEMOTED — not the knob to gate on.** `OS_TENANCY_POSTURE`\n * superseded this flag and is the authoritative one;\n * {@link resolveTenancyPosture} is where the two are reconciled (posture when\n * set, else this boolean). This function only reports the legacy input, so a\n * deployment that sets ONLY the canonical `OS_TENANCY_POSTURE` reads `false`\n * here while genuinely running a walled multi-organization posture.\n *\n * **Answering \"is this deployment multi-org?\" with this function is a bug.**\n * Ask the posture instead — `postureEnforcesWall(resolveTenancyPosture())`\n * (`@objectstack/spec/security`) — or, inside a running kernel, the `tenancy`\n * service, which additionally knows whether the requested wall is actually\n * ENFORCED (ADR-0093 D4/D5). Two shipped defects came from gating on this\n * boolean after the demotion: cloud#1020 (the EE licence gate) and #5233\n * (`organization/create` 403'd on a posture-only deployment whose organization\n * wall was fully mounted — the guided \"create your workspace\" path dead-ended).\n * The sentence this paragraph replaced actively instructed both.\n *\n * Legitimate remaining callers are the ones that specifically mean *the legacy\n * input*: {@link resolveTenancyPosture}'s own back-compat fallback, and\n * back-compat/reporting surfaces that must echo what the operator typed.\n *\n * Resolution: `OS_MULTI_ORG_ENABLED`; else `false`. Any value other than a\n * case-insensitive `'false'` enables it. (The legacy `OS_MULTI_TENANT` alias was\n * removed in 11.0.)\n *\n * Reads `process.env` live on each call; memoise at the call site if the\n * result must be stable for the process lifetime.\n */\nexport function resolveMultiOrgEnabled(): boolean {\n const raw = readEnvWithDeprecation('OS_MULTI_ORG_ENABLED', []);\n return String(raw ?? 'false').toLowerCase() !== 'false';\n}\n\n/**\n * [ADR-0105 D1] Resolve the deployment's REQUESTED tenancy posture —\n * `single` | `group` | `isolated`.\n *\n * `OS_TENANCY_POSTURE` is the canonical knob and generalizes the boolean\n * `OS_MULTI_ORG_ENABLED` it supersedes:\n *\n * - set → that posture (the legacy spelling `multi` normalizes to `isolated`)\n * - unset → derived from `OS_MULTI_ORG_ENABLED`: `true` ⇒ `isolated`, else `single`\n *\n * so every existing deployment keeps its current posture with no config change.\n *\n * An unrecognized value THROWS rather than falling back. A typo'd posture that\n * quietly resolved to `single` would silently remove the organization wall —\n * the deployment-layer form of the \"declared but unenforced\" defect ADR-0049\n * forbids, and the same reasoning behind ADR-0093 D5's refusal to boot into\n * undeclared degradation.\n *\n * This resolves what the operator ASKED FOR. Whether the posture is actually\n * enforced is the `tenancy` service's answer (`isolationActive` / `degraded`).\n */\nexport function resolveTenancyPosture(): TenancyPosture {\n // Read through `globalThis` like `readEnvWithDeprecation` does — this package\n // targets non-Node runtimes too, where a bare `process` reference throws.\n const raw = (globalThis as { process?: { env?: Record<string, string | undefined> } })\n .process?.env?.OS_TENANCY_POSTURE;\n if (raw != null && String(raw).trim() !== '') {\n const posture = normalizeTenancyPosture(raw);\n if (!posture) {\n throw new Error(\n `Invalid OS_TENANCY_POSTURE=${JSON.stringify(String(raw))}. ` +\n `Expected one of: ${TENANCY_POSTURES.join(', ')} (or the legacy alias 'multi' = 'isolated'). ` +\n 'Refusing to boot rather than silently falling back to a posture with no organization wall.',\n );\n }\n return posture;\n }\n return resolveMultiOrgEnabled() ? 'isolated' : 'single';\n}\n\n/**\n * The env variable naming the deployment's PLATFORM OWNER account\n * (#11184, the framework leg of cloud#1509).\n *\n * Exported as a constant so every message that names it quotes exactly one\n * spelling: the walled boot guard in plugin-auth, and plugin-security's\n * `bootstrapPlatformAdmin` — its fail-closed backstop for an undeclared or\n * refused config, and the config-derived standing it logs beside it.\n *\n * ⚠️ That second site is no longer an ELEVATION refusal. Since the #11663\n * platform-admin re-anchor (leg L4) the walled `bootstrapPlatformAdmin` writes\n * no grant row and elevates nobody — it reports. Standing is derived PER\n * REQUEST at `resolve-authz-context.ts` §6b-config, from a declared address\n * held on a VERIFIED `sys_user` row.\n */\nexport const PLATFORM_OWNER_EMAIL_ENV = 'OS_PLATFORM_OWNER_EMAIL';\n\n/**\n * [#11184 / cloud#1509] Resolve the env-declared platform OWNER email —\n * `OS_PLATFORM_OWNER_EMAIL`.\n *\n * Under a WALLED tenancy posture (`group` / `isolated`) the \"first registrant\n * becomes owner/platform admin\" bootstrap path is REMOVED (maintainer ruling\n * 2026-08-23, verbatim: 「1509 选择 env 指定 owner 邮箱」): on a walled\n * deployment with self-registration reachable, whoever curls the sign-up\n * endpoint first would otherwise receive the cross-tenant `admin_full_access`\n * grant — measured on a real walled SaaS in cloud#1509. Platform admin is\n * granted ONLY to the account whose email matches this variable, and a walled\n * posture with no value declared REFUSES STARTUP (fail-closed, same reasoning\n * as {@link resolveTenancyPosture}'s throw and ADR-0093 D5) rather than\n * silently reverting to first-registrant elevation.\n *\n * The `single` posture never consults this: \"first user is owner\" is ruled\n * reasonable there and unchanged.\n *\n * Returns the operator's value trimmed, or `undefined` when unset/blank.\n * Comparison against `sys_user.email` is the CONSUMER's job and must be\n * case-insensitive (this resolver echoes what the operator typed so refusal\n * messages can quote it verbatim).\n *\n * Reads `process.env` live on each call, through `globalThis` like the other\n * resolvers here (this package targets non-Node runtimes too).\n */\nexport function resolvePlatformOwnerEmail(): string | undefined {\n const raw = (globalThis as { process?: { env?: Record<string, string | undefined> } })\n .process?.env?.[PLATFORM_OWNER_EMAIL_ENV];\n if (raw == null) return undefined;\n const trimmed = String(raw).trim();\n return trimmed === '' ? undefined : trimmed;\n}\n\n/**\n * Escape hatch for the degraded-tenancy boot guard (ADR-0093 D5).\n *\n * When `OS_MULTI_ORG_ENABLED=true` but the enterprise `@objectstack/organizations`\n * package cannot provide tenant isolation, the platform refuses to boot — a\n * deployment that asked for tenant isolation must not serve traffic pretending\n * to have it (ADR-0049 at the deployment layer). Setting this to a truthy value\n * (`true`/`1`/`on`/`yes`, case-insensitive) boots anyway in an explicitly\n * *degraded* state that is branded everywhere an operator looks. Defaults OFF —\n * an unset flag means \"fail fast\".\n */\nexport function resolveAllowDegradedTenancy(): boolean {\n const raw = readEnvWithDeprecation('OS_ALLOW_DEGRADED_TENANCY', [], { silent: true });\n if (raw == null) return false;\n return ['1', 'true', 'on', 'yes'].includes(String(raw).trim().toLowerCase());\n}\n\n/**\n * Escape hatch for the driver-connect boot guard (framework#3741).\n *\n * `ObjectQLEngine.init()` connects every boot-registered driver and, by\n * default, refuses to boot when any of them fails — a server whose database is\n * unreachable must not report itself started and then 500 every request with an\n * error that reads nothing like \"the database is down\". Failing there is also\n * what gives a driver the ability to REFUSE STARTUP at all: any fatal startup\n * check a driver wants to run (licence, server version, incompatible\n * configuration, missing capability) can simply throw from `connect()`.\n *\n * Setting this to a truthy value (`true`/`1`/`on`/`yes`, case-insensitive)\n * boots anyway, in an explicitly degraded state that is logged loudly at\n * startup. Every query routed to a failed driver fails until the datasource\n * becomes reachable — the underlying clients do re-establish connections on\n * their own (framework#3759) — but the boot-time schema sync those drivers\n * missed is never re-run, so their tables may simply not exist afterwards.\n * Defaults OFF — an unset flag means \"fail fast\".\n */\nexport function resolveAllowDriverConnectFailure(): boolean {\n const raw = readEnvWithDeprecation('OS_ALLOW_DRIVER_CONNECT_FAILURE', [], { silent: true });\n if (raw == null) return false;\n return ['1', 'true', 'on', 'yes'].includes(String(raw).trim().toLowerCase());\n}\n\n/**\n * Escape hatch for plugin-dev's production boot guard (ADR-0115 D6, #3900).\n *\n * `DevPlugin.init()` refuses to run under `NODE_ENV=production`: the stack it\n * assembles is built around an auth secret published inside the npm package and\n * an in-memory driver with persistence off, neither of which a production\n * deployment should acquire by accident. Setting this to a truthy value\n * (`true`/`1`/`on`/`yes`, case-insensitive) boots anyway, in an explicitly\n * degraded state that is branded in the boot log and on the ready banner.\n * Defaults OFF — an unset flag means \"fail fast\".\n *\n * Lives here rather than as a bare `process.env[…] === '1'` inside plugin-dev so\n * that the whole `OS_ALLOW_*` family answers to one truthy vocabulary: the\n * strict `=== '1'` it replaced fails CLOSED on `OS_ALLOW_DEV_PLUGIN=true`, which\n * is safe but reads to an operator as the flag being broken.\n */\nexport function resolveAllowDevPlugin(): boolean {\n const raw = readEnvWithDeprecation('OS_ALLOW_DEV_PLUGIN', [], { silent: true });\n if (raw == null) return false;\n return ['1', 'true', 'on', 'yes'].includes(String(raw).trim().toLowerCase());\n}\n\n/**\n * SINGLE decision point for \"is the MCP HTTP surface (`/api/v1/mcp`) on?\".\n *\n * MCP is a core platform capability and defaults ON: an unset\n * `OS_MCP_SERVER_ENABLED` means the surface is served. Operators opt OUT with\n * an explicit falsy value (`false`/`0`/`off`/`no`, case-insensitive); any\n * other value — including the historical `true` — keeps it on.\n *\n * Every consumer of the flag — the runtime dispatcher's `/mcp` route gate,\n * the CLI's MCP plugin auto-load, the REST `/discovery` advertisement, and\n * the auth service's OAuth/DCR follow-defaults — MUST call this instead of\n * re-reading the env, so the served route, the advertised route, and the\n * authorization track can never disagree.\n *\n * Note the asymmetry with the MCP plugin's *stdio* auto-start\n * ({@link resolveMcpStdioAutoStart}), which stays opt-in and is gated by a\n * SEPARATE switch: attaching a long-lived stdio transport to every process is\n * a side effect no default should impose, while the HTTP surface is served\n * statelessly per-request.\n */\nexport function isMcpServerEnabled(): boolean {\n const raw = readEnvWithDeprecation('OS_MCP_SERVER_ENABLED', 'MCP_SERVER_ENABLED', {\n silent: true,\n });\n if (raw == null) return true;\n return !['0', 'false', 'off', 'no'].includes(raw.trim().toLowerCase());\n}\n\n/**\n * SINGLE decision point for \"should the MCP plugin auto-start a long-lived\n * (stdio) transport?\" — distinct from {@link isMcpServerEnabled}, which governs\n * the stateless HTTP surface.\n *\n * The stdio transport is a different, stricter posture: the plugin bridges the\n * RAW metadata service + data engine onto the long-lived server with NO\n * per-request principal (unscoped — see the `mcp-stdio-authority` conformance\n * row), so it is safe only as a single-operator LOCAL tool and MUST stay\n * opt-in. It defaults OFF.\n *\n * Canonical switch: `OS_MCP_STDIO_ENABLED` (truthy). The plugin also starts it\n * when constructed with `{ autoStart: true }` (that path is checked by the\n * caller, not here).\n *\n * DEPRECATED alias: `OS_MCP_SERVER_ENABLED=true` historically ALSO started\n * stdio — overloading the very var that gates the HTTP surface, so an operator\n * setting it to \"make sure MCP is on\" silently attached an unscoped transport.\n * That trigger still works (with a one-time warning from the caller) for one\n * release; prefer the dedicated var. Note `OS_MCP_SERVER_ENABLED=false` only\n * ever gated the HTTP surface and never started stdio, so it is unaffected.\n *\n * @returns `enabled` — whether stdio auto-start is requested by the env; and\n * `viaDeprecatedAlias` — whether it came through the legacy\n * `OS_MCP_SERVER_ENABLED=true` trigger (so the caller can warn once).\n */\nexport function resolveMcpStdioAutoStart(): { enabled: boolean; viaDeprecatedAlias: boolean } {\n const stdio = readEnvWithDeprecation('OS_MCP_STDIO_ENABLED', [], { silent: true });\n if (stdio != null && ['1', 'true', 'on', 'yes'].includes(stdio.trim().toLowerCase())) {\n return { enabled: true, viaDeprecatedAlias: false };\n }\n // Legacy trigger: only the literal `true` ever started stdio (preserved\n // exactly). `OS_MCP_SERVER_ENABLED=false`/other values never did.\n const legacy = readEnvWithDeprecation('OS_MCP_SERVER_ENABLED', 'MCP_SERVER_ENABLED', { silent: true });\n if (legacy != null && legacy.trim().toLowerCase() === 'true') {\n return { enabled: true, viaDeprecatedAlias: true };\n }\n return { enabled: false, viaDeprecatedAlias: false };\n}\n\n/**\n * Maximum number of organizations a single user may CREATE, from `OS_ORG_LIMIT`.\n * The auth plugin forwards this as better-auth's `organizationLimit` in function\n * form, counting only the caller's `role=owner` memberships — so it caps\n * self-created orgs (each of which can auto-provision a free environment on the\n * cloud control plane) without penalising a user invited into many orgs.\n *\n * Only meaningful under a posture that enforces an organization wall, i.e.\n * `postureEnforcesWall({@link resolveTenancyPosture}())` — NOT the demoted\n * `resolveMultiOrgEnabled()` boolean (ADR-0105 D1, #5233).\n * Returns `undefined` when unset or non-positive → no limit (better-auth treats\n * an absent `organizationLimit` as unlimited), preserving self-host behaviour.\n * Deployments that let users self-create orgs SHOULD set a generous cap.\n */\nexport function resolveOrgLimit(): number | undefined {\n const raw = readEnvWithDeprecation('OS_ORG_LIMIT', [], { silent: true });\n if (raw == null || String(raw).trim() === '') return undefined;\n const n = Number.parseInt(String(raw), 10);\n return Number.isFinite(n) && n > 0 ? n : undefined;\n}\n\n/**\n * Maximum number of MEMBERS a single organization may hold, from\n * `OS_ORG_MEMBERSHIP_LIMIT`. A different question from {@link resolveOrgLimit},\n * which caps how many organizations one user may create.\n *\n * Unset → `undefined`, which the auth plugin forwards as \"no cap\". That default\n * is a product decision, not an omission: seat entitlements are metered on AI\n * seats, and plain membership is not a billed axis, so nothing about the\n * platform wants a member ceiling.\n *\n * It has to be stated explicitly because better-auth's organization plugin\n * substitutes a vendor default of **100** for an absent `membershipLimit`\n * (`count >= (membershipLimit || 100)`), which reaches the operator as\n * `Organization membership limit reached` — a refusal nobody in this codebase\n * ever chose, on an axis the product does not limit.\n *\n * A deployment that DOES want a ceiling (a pilot, a trial tenant) sets a\n * positive integer here. Non-positive or unparsable values read as unset rather\n * than as zero: a typo must not be the thing that locks an organization.\n */\nexport function resolveOrgMembershipLimit(): number | undefined {\n const raw = readEnvWithDeprecation('OS_ORG_MEMBERSHIP_LIMIT', [], { silent: true });\n if (raw == null || String(raw).trim() === '') return undefined;\n const n = Number.parseInt(String(raw), 10);\n return Number.isFinite(n) && n > 0 ? n : undefined;\n}\n\n/**\n * SINGLE decision point for \"is pinyin search recall on?\" (#2486).\n *\n * Pinyin search is a deployment/locale-level capability, not field metadata:\n * Chinese deployments want it, pure-Japanese/English deployments don't. The\n * flag gates the whole feature end-to-end — the SchemaRegistry's compile-time\n * `__search` companion-column seam AND the `plugin-pinyin-search` populate\n * hooks — so there is no half-state where a column exists but nobody fills it\n * (ADR-0049: no declared-but-unenforced capability).\n *\n * Resolution:\n * 1. An explicit `OS_SEARCH_PINYIN_ENABLED` always wins — truthy\n * (`1`/`true`/`on`/`yes`) enables, anything else disables.\n * 2. When unset, the default derives from the deployment's configured\n * locales (`opts.locales`, e.g. the stack's `i18n.defaultLocale` +\n * `supportedLocales`): any `zh-*` locale turns it on.\n * 3. No env var and no `zh-*` locale → off. OSS / non-Chinese deployments\n * never load `pinyin-pro` and pay zero compute cost.\n *\n * Hosts that know the stack's i18n config — the CLI `serve` boot path AND the\n * standalone artifact boot (`createStandaloneStack`, which `os migrate`\n * plan/apply and embedders go through) — resolve once with locales and stamp\n * the decision back into the env via {@link stampSearchPinyinEnabled}, so\n * downstream consumers constructed without config access (per-engine\n * SchemaRegistry) read the same answer via the no-arg form (#3955).\n */\nexport function resolveSearchPinyinEnabled(opts?: { locales?: readonly string[] }): boolean {\n const raw = readEnvWithDeprecation('OS_SEARCH_PINYIN_ENABLED', [], { silent: true });\n if (raw != null && String(raw).trim() !== '') {\n return ['1', 'true', 'on', 'yes'].includes(String(raw).trim().toLowerCase());\n }\n return (opts?.locales ?? []).some((l) => /^zh([-_]|$)/i.test(String(l ?? '').trim()));\n}\n\n/**\n * The locales a stack's `i18n` config declares — `defaultLocale`,\n * `fallbackLocale`, then `supportedLocales`. Accepts the config loosely typed\n * (`unknown`) so any boot path can pass whatever its stack config or compiled\n * artifact carries without importing spec schemas; non-string entries and a\n * non-object config collapse to `[]`.\n */\nexport function collectConfiguredLocales(i18n: unknown): string[] {\n const cfg = (i18n && typeof i18n === 'object' ? i18n : {}) as {\n defaultLocale?: unknown;\n fallbackLocale?: unknown;\n supportedLocales?: unknown;\n };\n return [\n cfg.defaultLocale,\n cfg.fallbackLocale,\n ...(Array.isArray(cfg.supportedLocales) ? cfg.supportedLocales : []),\n ].filter((l): l is string => typeof l === 'string');\n}\n\n/**\n * Resolve the pinyin-search decision from a stack's `i18n` config and stamp a\n * positive result back into `OS_SEARCH_PINYIN_ENABLED` (#2486, #3955).\n *\n * Every boot path that SEES the stack config must stamp, because consumers\n * constructed later without config access (each engine's `SchemaRegistry`\n * provisioning the `__search` companion column, the `plugin-pinyin-search`\n * gate) read the decision through the no-arg\n * {@link resolveSearchPinyinEnabled}. A boot path that skips the stamp\n * computes a schema view WITHOUT the companion columns — which is how\n * `os migrate` came to flag the dev runtime's live `__search` columns as\n * destructive orphans (#3955). Call sites: the CLI `serve`/`dev` boot\n * (`objectstack.config.ts`) and `createStandaloneStack` (compiled artifact —\n * `os migrate plan`/`apply`, embedders).\n *\n * An explicit `OS_SEARCH_PINYIN_ENABLED` always wins — the resolver reads it\n * before consulting locales, so the stamp only materializes the\n * locale-derived default. Only a positive decision is written: \"unset\" and\n * \"off\" read identically through the no-arg resolver, and leaving the var\n * untouched keeps a later boot free to re-derive from ITS config.\n */\nexport function stampSearchPinyinEnabled(i18n: unknown): boolean {\n const enabled = resolveSearchPinyinEnabled({ locales: collectConfiguredLocales(i18n) });\n // Write through `globalThis` like `readEnvWithDeprecation` reads — this\n // package has no Node type dependency (edge-safe); no env object → no stamp.\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } })\n .process?.env;\n if (enabled && env) env.OS_SEARCH_PINYIN_ENABLED = 'true';\n return enabled;\n}\n\n/**\n * SINGLE decision point for a sandbox script-runner DEFAULT (ms), resolved from\n * the environment (framework#3259 / ADR-0102).\n *\n * The QuickJS sandbox meters each hook/action invocation against a per-invocation\n * budget. Two dimensions are env-tunable:\n * - the **CPU-time budget** for hooks / actions — how much *VM-active* time a\n * body may burn (built-in 250ms hooks / 5000ms actions); and\n * - the **wall-clock ceiling** — the backstop bounding a body parked forever on\n * a host call that never settles (built-in 30_000ms).\n *\n * The built-in defaults suit a warm, idle host; a heavily loaded or slow host\n * (an oversubscribed CI runner, constrained production hardware) may need a\n * higher floor. This lets an operator raise it once, deployment-wide, instead of\n * re-tuning every call site.\n *\n * Canonical vars (OS_{DOMAIN}_{NAME}, DOMAIN=SANDBOX):\n * - hook → `OS_SANDBOX_HOOK_TIMEOUT_MS`\n * - action → `OS_SANDBOX_ACTION_TIMEOUT_MS`\n * - wallCeiling → `OS_SANDBOX_WALL_CEILING_MS`\n *\n * Only a positive integer is honored; unset / empty / non-numeric / non-positive\n * falls back to `fallback`, so behaviour is byte-for-byte unchanged when the var\n * is absent. This is a FALLBACK default ONLY: an explicit constructor option\n * still wins over it, and (for the CPU budget) a body's own declared `timeoutMs`\n * still wins over the resolved default per the runner's resolution rule.\n */\nexport function resolveSandboxTimeoutMs(\n kind: 'hook' | 'action' | 'wallCeiling',\n fallback: number,\n): number {\n const name =\n kind === 'hook'\n ? 'OS_SANDBOX_HOOK_TIMEOUT_MS'\n : kind === 'action'\n ? 'OS_SANDBOX_ACTION_TIMEOUT_MS'\n : 'OS_SANDBOX_WALL_CEILING_MS';\n const raw = readEnvWithDeprecation(name, [], { silent: true });\n if (raw == null || String(raw).trim() === '') return fallback;\n const n = Number.parseInt(String(raw).trim(), 10);\n return Number.isFinite(n) && n > 0 ? n : fallback;\n}\n\n/**\n * Internal: clear the dedupe set. Test-only; exposed so suite-wide\n * deprecation warnings don't bleed between tests.\n *\n * @internal\n */\nexport function _resetEnvDeprecationWarnings(): void {\n _warnedKeys.clear();\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Shared \"does this error message leak server internals?\" heuristic (#3867).\n *\n * ObjectStack has more than one HTTP boundary. `@objectstack/rest` guards the\n * REST data routes inside `mapDataError`; the dispatcher-plugin routes\n * (`/analytics`, `/packages`, `/i18n`, `/automation`, …) exit\n * through `errorResponseBase`. Before #3867 only the first of those sanitised\n * anything, so a driver error raised under `/analytics/query` reached the\n * client verbatim — a real SQL statement in the response body:\n *\n * ```\n * {\"success\":false,\"error\":{\"message\":\"SELECT FROM \\\"sqlite_sequence\\\" - near \\\"FROM\\\": syntax error\",\"code\":500}}\n * ```\n *\n * \"Do not ship driver internals to clients\" is a property of the HTTP\n * boundary, not of one router, so the predicate lives here — the package both\n * `@objectstack/rest` and `@objectstack/runtime` already depend on — and each\n * boundary applies it in its own envelope. One heuristic, one place to widen\n * when a new dialect's phrasing shows up.\n *\n * Deliberately a *heuristic over the message*, not a driver taxonomy: these\n * errors arrive as plain `Error`s from a half-dozen dialects with no shared\n * shape. It is applied only where the outcome is already a 5xx, so a false\n * positive costs a caller nothing but detail on a response that was a server\n * fault anyway — while the full text still reaches server logs and the\n * error reporter.\n *\n * [#5811] {@link declaresServerFault} joins it here for the same reason and\n * answers the other half of the question: the heuristic asks whether a message\n * *sounds* internal, the declaration asks whether the producer *said so*.\n */\n\n/** Generic replacement text for a message that trips {@link looksLikeInternalErrorLeak}. */\nexport const INTERNAL_ERROR_MESSAGE = 'Internal server error';\n\n/**\n * [#8132, #8739] The dialect phrasings this list COVERS — the SQLite family,\n * Postgres and MySQL/MariaDB — each anchored on the driver's own errmsg\n * template rather than on its tail. Coverage, not a census of what this repo\n * runs: see \"What this list covers, and what it does not\" below, which is the\n * load-bearing half for anyone sizing a disclosure residual.\n *\n * The gap that forced these: the keyword set below caught SQLite's\n * `SQLITE_ERROR: no such table: sys_metadata` through the `sqlite_` limb, while\n * the Postgres phrasing of *the same condition* —\n * `relation \"sys_metadata\" does not exist` — matched nothing and shipped a\n * physical table name to the client from every boundary that applies the\n * predicate.\n *\n * **Why anchored, and never on the bare tail.** `does not exist` is ordinary\n * business English: \"user does not exist\", \"record does not exist\". Matching\n * that substring would replace legitimate answers with `Internal server error`,\n * so each pattern requires what the DRIVER always emits and prose usually does\n * not — a quoted identifier, or the trailing colon of SQLite's template. The\n * negative cases in `error-leak.test.ts` pin that distinction.\n *\n * **What this list covers, and what it does not.** The module note above argues\n * against growing a driver taxonomy, and that reason still holds on its own: a\n * phrasing list is unbounded *across dialects*, because every dialect spells\n * every one of these conditions its own way. So these entries are a COVERAGE\n * statement, not a census — the two spellings #8132 measured the gap on, plus\n * the three MySQL templates #8739 added — and {@link declaresServerFault}\n * remains the answer that does not depend on phrasing at all.\n *\n * **Covered as of #8739: MySQL/MariaDB.** Under the maintainer's 2026-08-15\n * ruling on #8739, MySQL is a SUPPORTED DEPLOYMENT TARGET, not merely a tested\n * dialect — the answer the published surface already implied\n * (`OS_DATABASE_DRIVER=mysql` is a documented deployment knob, `MysqlConfig` is\n * authorable datasource config, `types.mdx` specifies per-field MySQL DDL) and\n * the one CI's required live-MySQL check already behaves as if. A supported\n * target's driver text reaches these boundaries in production, so its\n * templates belong here. Three are covered, one per condition the other two\n * dialects are already covered for:\n *\n * - `Table 'app.t' doesn't exist` (ER_NO_SUCH_TABLE 1146) — the missing-object\n * condition SQLite spells `no such table:` and Postgres spells\n * `relation \"t\" does not exist`.\n * - `Unknown column 'c' in 'field list'` (ER_BAD_FIELD_ERROR 1054) — the same\n * condition for a column. The clause name varies (`field list`,\n * `where clause`, `order clause`, `on clause`) and is REQUIRED by the\n * pattern; it is what separates the driver's template from prose.\n * - `Duplicate entry 'x' for key 'i'` (ER_DUP_ENTRY 1062) — the\n * unique-violation condition the `constraint failed` / `unique constraint`\n * keyword limbs already catch for SQLite and Postgres and cannot catch here,\n * because MySQL's spelling shares no word with either. It is also the ONLY\n * one of the three whose text embeds a CALLER'S VALUE rather than an\n * identifier, which is what made the pre-#8739 gap worth closing rather than\n * documenting.\n *\n * ⛔ Adding this limb does NOT re-open #6250's decision one package over.\n * `@objectstack/rest` answers the 409 conflict question with\n * `isUniqueViolationError` (`unique-violation.ts`), ABOVE and independently of\n * this predicate, precisely so a disclosure rule never decides a status. That\n * ordering is what keeps the two unentangled now that both recognise the same\n * MySQL sentence; `rest-unique-violation-dialects.test.ts` pins it.\n *\n * ⚠️ **This list's silence is STILL NOT evidence that a dialect is\n * unreachable, and MySQL is why the warning is worded that way.** Until #8739\n * this paragraph said \"nobody here runs\" MySQL/MSSQL/Oracle, and a reviewer\n * sizing a disclosure residual read it as one. It was false for MySQL,\n * measurably, on the same tree:\n *\n * - `driver-sql` branches on `mysql`/`mysql2` — the `isMysql` getter,\n * `withUtcSession`, and the `dialect === 'mysql'` arms of\n * `textMatchPredicate` / `likePatternPredicate`.\n * - CI stands up a live `mysql:8.0` service for the job named\n * `Temporal Conformance (live PG + MySQL)`, which IS a required check, and\n * its `OS_EXPECT_LIVE_DIALECT_MATRIX` flag turns a missing MySQL URL into a\n * named red rather than a quiet skip.\n * - Live MySQL 8.0.46 measurements produced merged driver fixes (#8621,\n * #8622), and `unique-violation.ts` — one file over — names sqlite /\n * postgres / mysql as the three dialect families `sql-driver.ts` recognises.\n *\n * The rule that outlives any particular dialect: **a `false` from this\n * predicate means UNCOVERED, never \"safe\"**, and the reachability of an\n * uncovered dialect is a separate question this file cannot answer. MSSQL and\n * Oracle are uncovered today — `Invalid object name 'sys_metadata'.`,\n * `ORA-00942: table or view does not exist` both return FALSE — and that is a\n * statement about this list, not about them; `error-leak.test.ts` pins those\n * two as the standing example so the distinction keeps a live subject.\n * {@link declaresServerFault} is the phrasing-independent answer, and\n * `metadata-protocol`'s `protocol.driver-text-disclosure.test.ts` is the worked\n * demonstration that a producer which withholds by DECLARATION needs no dialect\n * list at all.\n *\n * ⛔ **What is deliberately NOT added here, and why.** MySQL's ACL family\n * (`Access denied for user 'u'@'h' to database 'd'`, ER_DBACCESS_DENIED_ERROR\n * 1044; `SELECT command denied to user … for table 't'`,\n * ER_TABLEACCESS_DENIED_ERROR 1142) is the counterpart of the Postgres\n * `permission denied for table` limb above and is NOT covered: nothing in this\n * repo has raised one off a live server, and `unique-violation.ts`' standing\n * rule for this neighbourhood is that a dialect's spelling is added when it has\n * been MEASURED off a thrown error, never on a plausible reading of the\n * dialect's manual. `Access denied` also collides with this platform's own\n * security prose (`[Security] Access denied: …`, pinned as a negative case), so\n * a guessed pattern here is the over-match direction, which suppresses\n * diagnostics an operator needs. Measure one, then add it.\n *\n * ⚠️ Related but NOT reusable: `relation-sub-object.ts` owns the same Postgres\n * sentence for two other questions (which column? / is this a sub-object?), and\n * its note warns that its two widths must never be collapsed. Neither answers\n * \"is this a leak\", and its central problem does not arise here: a message like\n * `column \"label\" of relation \"sys_team\" does not exist` contains a complete\n * missing-TABLE phrase as a substring, which is a hazard when you are deciding\n * WHICH object is missing and a non-issue when the verdict is \"leak\" either way.\n * That is why this asks its own question with its own patterns.\n */\nconst DIALECT_LEAK_PHRASINGS: readonly RegExp[] = [\n // Postgres 42P01 / 42703 (and, as a superstring, the `… of relation \"…\"`\n // sub-object family: 42704 and friends). The quotes are required because\n // Postgres always emits them here.\n /\\b(?:relation|column)\\s+[\"'`][^\"'`]+[\"'`]\\s+does not exist/i,\n // Postgres 42501. Restricted to physical object kinds: `schema`, `view`,\n // `function` and `column` are all ObjectStack AUTHORING vocabulary, so a\n // product message could legitimately use them and a miss is the cheap\n // direction (the outcome is already a 5xx).\n /\\bpermission denied for (?:table|relation|sequence|database)\\b/i,\n // SQLite/libsql, message-only form. The `sqlite_` limb below catches these\n // only when the driver prefixed its code; `better-sqlite3` and libsql both\n // raise them bare, which is the shape measured across this repo.\n /\\bno such (?:table|column):/i,\n // [#8739] MySQL/MariaDB ER_NO_SUCH_TABLE (1146): `Table 'app.t' doesn't\n // exist`. Its own template, not a spelling of the Postgres one — MySQL\n // contracts the verb and quotes `db.table` as a single identifier — so the\n // `relation|column … does not exist` limb above cannot reach it. The quotes\n // are required for the same reason they are there: the driver always emits\n // them and prose about a table usually does not.\n /\\btable\\s+[\"'`][^\"'`]+[\"'`]\\s+doesn't exist/i,\n // [#8739] MySQL/MariaDB ER_BAD_FIELD_ERROR (1054): `Unknown column 'c' in\n // 'field list'`. BOTH quoted parts are required. The second is MySQL's clause\n // name — `field list`, `where clause`, `order clause`, `on clause` — and it\n // is the half that makes this the driver's template rather than a sentence\n // that merely calls a column unknown, which an import or mapping feature has\n // every right to say.\n /\\bunknown column\\s+[\"'`][^\"'`]+[\"'`]\\s+in\\s+[\"'`][^\"'`]+[\"'`]/i,\n // [#8739] MySQL/MariaDB ER_DUP_ENTRY (1062): `Duplicate entry\n // 'acme@example.com' for key 'crm_account.email'`. `for key` + a quoted index\n // is the anchor; the VALUE half is matched loosely and lazily because it is\n // the caller's own text and MySQL does not escape a quote inside it\n // (`Duplicate entry 'O'Brien' for key 'i'` is a real shape). A bare\n // `duplicate entry` with no `for key '…'` tail is not this template and is\n // left alone.\n /\\bduplicate entry\\s+[\"'`].*?[\"'`]\\s+for key\\s+[\"'`][^\"'`]+[\"'`]/i,\n];\n\n/**\n * Whether `message` looks like a raw SQL statement or driver/engine dump that\n * must not be returned to an API client.\n *\n * Matches: dialect error codes (`SQLSTATE`, `sqlite_*`), bare statements\n * (a message that *starts* as `SELECT`/`INSERT INTO`/`UPDATE`/`DELETE FROM` —\n * drivers prefix the offending SQL to their message), constraint-violation\n * dumps, which name physical tables and columns, and the\n * {@link DIALECT_LEAK_PHRASINGS} the list covers — the SQLite family, Postgres\n * and, since #8739, MySQL/MariaDB. A dialect outside that coverage (MSSQL and\n * Oracle are the standing examples) makes this return FALSE without meaning the\n * text is safe; read {@link DIALECT_LEAK_PHRASINGS}' note before sizing\n * anything on a `false`.\n *\n * Does NOT match ordinary business or validation messages, which is why the\n * statement forms are anchored with `startsWith` and the dialect phrasings on\n * the driver's template: a legitimate message may *mention* \"update\", or say\n * \"does not exist\" about a business record, without being either.\n */\nexport function looksLikeInternalErrorLeak(message: string | undefined | null): boolean {\n if (!message) return false;\n const lower = String(message).toLowerCase();\n return (\n lower.includes('sqlite_') ||\n lower.includes('sqlstate') ||\n lower.startsWith('insert into ') ||\n lower.startsWith('update ') ||\n lower.startsWith('select ') ||\n lower.startsWith('delete from ') ||\n lower.includes('constraint failed') ||\n lower.includes('unique constraint') ||\n lower.includes('foreign key') ||\n DIALECT_LEAK_PHRASINGS.some((pattern) => pattern.test(lower))\n );\n}\n\n/**\n * Whether the thrown error **declares a server fault** in the ADR-0112 envelope:\n * `status >= 500` *and* a non-empty `code`.\n *\n * The counterpart to {@link looksLikeInternalErrorLeak}, and deliberately not a\n * message test at all. Some server faults are dangerous to echo while saying\n * nothing a phrasing heuristic can recognise — the motivating family is\n * `service-analytics`' `read-scope-sql.ts`, whose ten fail-closed RLS lowering\n * refusals name the FIELD NAMES AND COMPARANDS OF THE RLS POLICY:\n *\n * ```\n * [read-scope-sql] unsafe field identifier \"secret_policy_field\" — refusing to\n * build read scope (fail-closed).\n * ```\n *\n * That text comes from an administrator's sharing rule compiled by the security\n * service; the tenant who receives it never wrote it and must not be able to read\n * it out of an error body. Measured, all eleven of its message shapes return\n * FALSE from `looksLikeInternalErrorLeak` — they look nothing like a driver dump —\n * so a boundary that only ran the heuristic echoed every one of them verbatim\n * (#5811 measured 11/11 through `errorResponseBase`). Teaching the heuristic to\n * recognise `[read-scope-sql]` would have been *more* message sniffing, which is\n * the mechanism #5352/#5367 exist to remove. So the withhold keys on the\n * DECLARATION instead: a producer that says `status >= 500` with a `code` has\n * declared that this is the server's fault, and a server fault's detail belongs in\n * the operator's log, not in the caller's body.\n *\n * **Both halves are required, and it is deliberately NOT \"any 5xx\".** #5667 kept\n * UNDECLARED 5xx errors legible on purpose — a bare `Error` from our own code\n * (\"no strategy can handle query …\") is the operator's own bug report, carries\n * nothing tenant-sensitive, and still falls to `looksLikeInternalErrorLeak`.\n * Widening this to every 500 would delete that decision.\n *\n * **Reads `status`, not `statusCode`.** `status` is the channel ADR-0112 declares;\n * `statusCode` is an alternate spelling some boundaries tolerate when *deriving*\n * an HTTP status. Accepting it here would make the disclosure rule depend on which\n * spelling a producer happened to use — consumer-side leniency of exactly the kind\n * Prime Directive #12 removes. A producer that wants its detail withheld declares\n * the envelope.\n *\n * Costs no diagnostics: every boundary that applies this still logs the untouched\n * error and hands it to the error reporter.\n *\n * @param err - the thrown value, of any shape (a non-object is simply not a\n * declaration).\n */\nexport function declaresServerFault(err: unknown): boolean {\n if (typeof err !== 'object' || err === null) return false;\n const { status, code } = err as { status?: unknown; code?: unknown };\n return typeof status === 'number' && status >= 500 && typeof code === 'string' && code.length > 0;\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Seek-based (keyset) pagination for the batch walks that read a whole object.\n *\n * # Why this exists rather than `limit`/`offset`\n *\n * A background walk that pages with a growing `offset` — rebuild an index,\n * verify file references, backfill a projection — is wrong in two ways that a\n * seek fixes at once.\n *\n * **It can skip rows.** `LIMIT n OFFSET k` is a slice of an arrangement, and\n * the arrangement has to be the *same* one on every page for the slices to\n * partition the set. Drivers now guarantee that for a single read\n * (objectstack#4363), but not across a walk that *mutates as it goes*: a\n * backfill that updates each page, or a rebuild that deletes, changes the very\n * set the next offset counts into. Rows shift past the cursor and are never\n * visited. For a verifier that decides which files are still referenced, or an\n * index rebuild that deletes what it did not see, a skipped row is not a slow\n * page — it is a wrong answer that looks like a clean run. A seek predicate\n * carries the position *in the data* instead of counting from the start, so an\n * update cannot move a row past it and a delete cannot shift one under it.\n *\n * **It is quadratic.** The database must produce and discard every skipped row\n * to honor an offset, so walking n rows in pages of p costs O(n²/p). On a\n * 2M-row table the last pages were measured at ~1.1 s each against ~0.09 s for\n * the first. A seek starts each page at the cursor, so every page costs the\n * same: O(n) for the walk, and index-served throughout.\n *\n * # What it requires\n *\n * A column that is **unique and orderable** — `id` by default, which every\n * object this driver-managed platform creates carries. An object without one\n * (a federated table, ADR-0015) cannot be walked this way; callers that scan\n * arbitrary registry objects already skip what they cannot read, and that is\n * the correct outcome here too rather than a silent partial scan.\n *\n * # Shape\n *\n * `read` is the caller's own query — this owns the loop, the cursor and the\n * `where` merge, and nothing else. Deliberately one implementation rather than\n * the six hand-rolled copies it replaces: the cursor merge is the part that is\n * easy to get subtly wrong (an object whose own `where` already constrains the\n * key), and six copies of it drift silently.\n *\n * @example\n * const walk = keysetWalk<Row>(\n * (q) => engine.find('sys_approval_request', { ...q, fields: ['id'], context: SYSTEM_CTX }),\n * { where: { status: 'pending' }, pageSize: 500 },\n * );\n * for await (const page of walk.pages()) { … }\n * if (walk.truncated) { … }\n */\n\n/** The query a {@link keysetWalk} hands its reader: the caller's `where`, narrowed by the cursor. */\nexport interface KeysetPageQuery {\n /** The caller's `where`, AND-ed with the seek predicate once the walk has a cursor. */\n where?: unknown;\n /** Always ascending on the key column — the walk's order IS the seek order. */\n orderBy: Array<{ field: string; order: 'asc' }>;\n /** Page size. */\n limit: number;\n}\n\nexport interface KeysetWalkOptions {\n /** The caller's filter, applied to every page. */\n where?: unknown;\n /** Rows per page. */\n pageSize: number;\n /**\n * Stop after this many rows and set {@link KeysetWalk.truncated}. Omit for an\n * unbounded walk. A cap is not a failure — it is how a scan bounds its own\n * cost — but it must be reported, or a partial scan reads as a complete one.\n */\n max?: number;\n /** Unique, orderable column to seek on. Defaults to `id`. */\n key?: string;\n}\n\nexport interface KeysetWalk<T> {\n /** Pages, in key order, until the source is exhausted or `max` is reached. */\n pages(): AsyncGenerator<T[]>;\n /** Rows yielded so far. */\n readonly scanned: number;\n /** True when `max` stopped the walk before the source was exhausted. */\n readonly truncated: boolean;\n}\n\n/**\n * AND the seek predicate onto the caller's filter.\n *\n * Uses `$and` rather than spreading the key into the same object: a caller\n * whose own `where` already constrains the key column (`{ id: { $in: [...] } }`)\n * would otherwise have that constraint silently overwritten by the cursor, and\n * the walk would return rows the caller excluded. `$and` composes instead of\n * colliding, and every driver executes it.\n */\nfunction withCursor(where: unknown, key: string, cursor: unknown): unknown {\n const seek = { [key]: { $gt: cursor } };\n if (where == null) return seek;\n if (typeof where === 'object' && Object.keys(where as object).length === 0) return seek;\n return { $and: [where, seek] };\n}\n\n/**\n * Walk an object by seeking past the last key rather than counting from the\n * start. See the module comment for why every batch scan should.\n *\n * `read` receives a {@link KeysetPageQuery} and returns the page; the caller\n * owns everything else about the query (projection, context, object name).\n */\nexport function keysetWalk<T extends Record<string, unknown>>(\n read: (query: KeysetPageQuery) => Promise<T[]>,\n options: KeysetWalkOptions,\n): KeysetWalk<T> {\n const key = options.key ?? 'id';\n const pageSize = options.pageSize;\n let scanned = 0;\n let truncated = false;\n\n async function* pages(): AsyncGenerator<T[]> {\n let cursor: unknown = undefined;\n for (;;) {\n const want = options.max == null ? pageSize : Math.min(pageSize, options.max - scanned);\n if (want <= 0) {\n truncated = true;\n return;\n }\n\n // When `max` clips this page, ask for ONE more row than we will yield.\n // That extra row is the difference between \"the cap stopped us\" and \"the\n // source ended at exactly the cap\" — without it a walk that read\n // everything still reports `truncated`, and a caller acting on that goes\n // looking for rows that were never withheld.\n const clipped = options.max != null && want < pageSize;\n const page = await read({\n where: cursor === undefined ? options.where : withCursor(options.where, key, cursor),\n orderBy: [{ field: key, order: 'asc' }],\n limit: clipped ? want + 1 : want,\n });\n if (!Array.isArray(page) || page.length === 0) return;\n\n const overflow = clipped && page.length > want;\n const emit = overflow ? page.slice(0, want) : page;\n scanned += emit.length;\n yield emit;\n\n if (overflow) {\n truncated = true;\n return;\n }\n\n const last = emit[emit.length - 1]?.[key];\n // A row without the key column cannot advance the cursor, and continuing\n // would re-read the same page forever. Stop and report it as truncation\n // rather than spin: a walk that cannot seek is not a walk that finished.\n if (last === undefined || last === null) {\n truncated = true;\n return;\n }\n // The same stop for a reader that did not APPLY the seek — the cursor\n // comes back no further along than it went in, so the next page would be\n // this page again, forever. Production drivers execute the predicate;\n // a test double or a future reader that quietly drops it would otherwise\n // hang rather than fail, and a hang is the one failure nobody can read.\n if (cursor !== undefined && !(String(last) > String(cursor))) {\n truncated = true;\n return;\n }\n cursor = last;\n\n // A short page means the source is exhausted.\n if (emit.length < want) return;\n // Reaching the cap on a full, unclipped page: more rows may remain, and\n // the next iteration's `want <= 0` reports that as truncation.\n if (options.max != null && scanned >= options.max && !clipped) continue;\n if (options.max != null && scanned >= options.max) return;\n }\n }\n\n return {\n pages,\n get scanned() {\n return scanned;\n },\n get truncated() {\n return truncated;\n },\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * True when a dynamic `import()` / `require.resolve()` failed because the\n * module is simply NOT INSTALLED — as opposed to the module being present but\n * throwing while it loads (a real crash). Checking `err.code` FIRST matters:\n * ESM reports a missing package as `err.code === 'ERR_MODULE_NOT_FOUND'` with\n * the human message `Cannot find package '...'`; matching only the older\n * `Cannot find module` string mis-classifies that as a crash (framework#1595).\n *\n * Single shared owner for this classification (framework#3265): the CLI's\n * optional-plugin guards and `requires` capability resolver delegate here, and\n * cloud's `objectos-runtime` capability loader is expected to adopt it at its\n * next framework pin bump — so the parallel loaders cannot drift apart and\n * re-introduce the #1595 false-alarm class.\n */\nexport function isModuleNotFoundError(err: unknown): boolean {\n const code = (err as { code?: string } | null | undefined)?.code;\n if (code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') return true;\n const msg = err instanceof Error ? err.message : String(err);\n return msg.includes('Cannot find module') || msg.includes('Cannot find package');\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * [#14310] The one rule for \"a 5xx must never be silent\", shared by every\n * transport that turns a fault into an HTTP envelope.\n *\n * ## The hole this closes\n *\n * A 500 that leaves no server-side line is diagnosed from the browser or not\n * at all. Measured on `main`: a plain `Error` thrown out of a dispatcher route\n * answered `500 INTERNAL_ERROR` with **zero** log records at any level — the\n * only evidence was the client's console and the response body. The failure\n * that motivated this had been reachable for a week and nobody saw it, which\n * is AGENTS.md \"Route & surface ownership §3 — absence must be loud\" inverted.\n *\n * The reporting that DID exist was not a substitute, in two independent ways:\n *\n * 1. `ErrorReporter.captureException` is an APM channel and defaults to\n * `NoopErrorReporter`. A dev server — the surface an operator actually\n * watches — wires no reporter, so the capture was a no-op every time.\n * 2. It is fed by `res.__obsRecordedError`, which only the THROWN exit sets.\n * A dispatcher route that catches its own fault and RETURNS a 5xx envelope\n * (`deps.errorFromThrown`, which is how every `/packages` handler answers)\n * records nothing, so even a wired reporter never saw those.\n *\n * This module is the log half, and it is deliberately not the reporter half:\n * an APM capture is opt-in telemetry, a log line is the operator's floor.\n *\n * ## Why it lives here\n *\n * Same argument, and the same package, as `resolveThrownHttpError` one file\n * over: a rule two doors must agree on cannot live inside one of them.\n * `@objectstack/runtime` depends on `@objectstack/rest`, so an import between\n * the two doors could only ever point one way — which is exactly why the\n * \"what status does this throw mean\" rule was moved here in #8016. \"Is this\n * answer worth an operator's attention\" is the same kind of rule, read by the\n * same two doors, so it gets the same home rather than a second one.\n *\n * Living beside {@link sendError} is what makes the REST side automatic: that\n * writer is the single exit for every nested-envelope 5xx, so the direct-mount\n * registrars need no per-door call and cannot forget one. Each transport logs\n * at its own single exit, so a fault costs one line and never two.\n *\n * ## `error` level, and why that clears the default\n *\n * The requirement is that the line survives `--log-level`'s DEFAULT. The CLI\n * default is `warn` (`packages/cli/src/utils/log-level.ts`) and `error` (40)\n * outranks `warn` (30) in `LEVEL_PRIORITY`, so an `error` record passes the\n * default threshold without any bypass of the level system. An operator who\n * asks for `--log-level silent` still gets silence: that is a deliberate\n * instruction, not the default this issue is about.\n *\n * ## 5xx only\n *\n * 4xx stays quiet, deliberately and at this one gate rather than at each call\n * site. A client error is the caller's mistake and the response already\n * explains it; logging them is how the `/meta` `?state=draft` probe once\n * printed 45 stack traces in one browsing session. `isServerFault` is the\n * whole rule: at or above 500.\n */\n\nimport type { Logger } from '@objectstack/spec/contracts';\n\n/** The request coordinates an operator needs to find the failing call. */\nexport interface ServerFaultRequest {\n /** HTTP method, e.g. `GET`. */\n method?: string;\n /** Request path as served, e.g. `/api/v1/packages`. */\n path?: string;\n /** Correlation id — the `X-Request-Id` echoed on the response. */\n requestId?: string;\n}\n\n/** One fault, as the emitting door knows it. */\nexport interface ServerFaultLogInput {\n /** The HTTP status about to be written. Below 500 nothing is logged. */\n status: number;\n /**\n * The original thrown value, when the door still holds it. Carries the\n * stack; the wire body never does, because a 5xx message is withheld.\n */\n error?: unknown;\n /** The envelope's `code`, when the door resolved one. */\n code?: string;\n /**\n * The message to print when {@link ServerFaultLogInput.error} carries\n * none — a declared fault built from a string rather than a throw.\n */\n message?: string;\n /** Where the call came in. */\n request?: ServerFaultRequest;\n}\n\n/** The prefix every fault line carries, so an operator can grep one token. */\nexport const SERVER_FAULT_LOG_PREFIX = '[5xx]';\n\n/**\n * THE predicate. A response is a server fault worth a line exactly when its\n * status is 5xx. Exported so a door can decide without restating `>= 500`.\n */\nexport function isServerFault(status: number): boolean {\n return typeof status === 'number' && status >= 500;\n}\n\n/**\n * Normalize a thrown value to an `Error`, because `Logger.error`'s second\n * parameter is typed to one and a `throw 'string'` must not cost the line.\n * Returns `undefined` when there was no throw at all (a declared fault), so\n * the logger is not handed an empty synthetic stack.\n */\nfunction toError(thrown: unknown): Error | undefined {\n if (thrown === undefined || thrown === null) return undefined;\n if (thrown instanceof Error) return thrown;\n const wrapped = new Error(typeof thrown === 'string' ? thrown : safeStringify(thrown));\n // The synthetic stack points at THIS file and would mislead; the value's\n // own text is the whole of what the producer gave us.\n wrapped.stack = undefined;\n return wrapped;\n}\n\nfunction safeStringify(value: unknown): string {\n try {\n return JSON.stringify(value) ?? String(value);\n } catch {\n return String(value);\n }\n}\n\n/**\n * The human half of the line: `[5xx] 500 GET /api/v1/packages — <message>`.\n * Split out so both the emitted record and a test can name the same string.\n */\nexport function serverFaultLogMessage(input: ServerFaultLogInput): string {\n const err = toError(input.error);\n const text = err?.message || input.message || 'Unhandled server fault';\n const where = [input.request?.method, input.request?.path].filter(Boolean).join(' ');\n return `${SERVER_FAULT_LOG_PREFIX} ${input.status}${where ? ` ${where}` : ''} — ${text}`;\n}\n\n/**\n * The structured half. `status`/`code`/`requestId` are what a log search keys\n * on; `method`/`path` repeat the message's coordinates because a JSON sink\n * indexes fields, not prose.\n */\nexport function serverFaultLogMeta(input: ServerFaultLogInput): Record<string, unknown> {\n return {\n status: input.status,\n ...(input.code !== undefined ? { code: input.code } : {}),\n ...(input.request?.method !== undefined ? { method: input.request.method } : {}),\n ...(input.request?.path !== undefined ? { path: input.request.path } : {}),\n ...(input.request?.requestId !== undefined ? { requestId: input.request.requestId } : {}),\n };\n}\n\n/**\n * Emit EXACTLY ONE `error`-level record for a 5xx, or nothing at all.\n *\n * Returns whether a record was emitted, so a caller that must not double-log\n * can branch on the answer rather than re-deriving the 5xx test.\n *\n * `logger` is optional: a door with no injected logger falls back to\n * `console.error`, because the point of this function is that the line exists\n * even on a surface nobody configured. Emission never throws — a logging\n * failure must not become a second fault on top of the one being reported.\n */\nexport function logServerFault(\n input: ServerFaultLogInput,\n logger?: Logger,\n): boolean {\n if (!isServerFault(input.status)) return false;\n const message = serverFaultLogMessage(input);\n const meta = serverFaultLogMeta(input);\n const err = toError(input.error);\n try {\n if (logger) {\n logger.error(message, err, meta);\n return true;\n }\n const sink = (globalThis as { console?: { error?: (...args: unknown[]) => void } }).console;\n sink?.error?.(message, { ...meta, ...(err?.stack ? { stack: err.stack } : {}) });\n return true;\n } catch {\n // Log emission must never throw — the original fault is still answered.\n return false;\n }\n}\n\n/**\n * Read request coordinates off whatever request object the transport hands\n * the door. Adapters disagree on the spelling (`path` / `url` /\n * `originalUrl`), and the request id may be on the object (set by\n * `instrumentRouteHandler`) or only on the incoming header — so both are\n * read here, once, instead of at each call site.\n */\nexport function describeFaultRequest(req: unknown): ServerFaultRequest {\n const r = req as {\n method?: unknown;\n path?: unknown;\n url?: unknown;\n originalUrl?: unknown;\n requestId?: unknown;\n headers?: Record<string, unknown>;\n } | undefined | null;\n if (!r || typeof r !== 'object') return {};\n const str = (v: unknown): string | undefined => (typeof v === 'string' && v ? v : undefined);\n const headerId = r.headers\n ? str(r.headers['x-request-id']) ?? str(r.headers['X-Request-Id'])\n : undefined;\n const method = str(r.method);\n const path = str(r.path) ?? str(r.url) ?? str(r.originalUrl);\n const requestId = str(r.requestId) ?? headerId;\n return {\n ...(method !== undefined ? { method } : {}),\n ...(path !== undefined ? { path } : {}),\n ...(requestId !== undefined ? { requestId } : {}),\n };\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * The ONE writer for the declared REST response envelope (#3973).\n *\n * `BaseResponseSchema` (`packages/spec/src/api/contract.zod.ts`) declares one\n * envelope for every REST body the platform emits:\n *\n * { success: true, data }\n * { success: false, error: { code, message } }\n *\n * The schema declares it once. Until this file, the code that *wrote* it was\n * copied per route module — seven `sendOk` / `sendError` pairs after #3843 and\n * #3983 converted the last drifting one, so the envelope's shape lived in\n * fourteen places rather than one.\n *\n * ## Why a shared builder rather than seven agreeing copies\n *\n * `scripts/check-route-envelope.mjs` proves the copies agree today, and that is\n * exactly why this is a cleanup and not a bug fix. But a guard proves agreement;\n * it does not create it. An eighth module starts by copying the pair again —\n * which is not hypothetical, it is the observed history: `share-link-routes.ts`\n * was found by the repo-wide scan already drifting, and its drift had broken\n * `client.shareLinks.create()` / `.list()` through `unwrapResponse` (#3983).\n *\n * ## Why here\n *\n * Placement was the open question in #3973, not design. `packages/spec` is\n * schemas-only (Prime Directive #2), and the callers span `packages/rest`, four\n * `services/*` and one `plugins/*`, which rules out anything that depends on\n * them. `@objectstack/types` depends on nothing but `@objectstack/spec`, so\n * every caller can reach it, and it is where the repo already puts a helper the\n * HTTP boundaries share: {@link looksLikeInternalErrorLeak} lives one file over\n * for the same reason, and made the same argument first — \"do not ship driver\n * internals to clients\" is a property of the boundary, not of one router.\n *\n * Writing the declared envelope is the same kind of property.\n *\n * ## What this does NOT change\n *\n * Every byte on the wire. The seven pairs were already identical modulo the\n * optional `status` and `extra` parameters unioned below; this file is their\n * union, and each module's driven conformance suite still parses its real\n * bodies against the real spec schemas.\n *\n * The dispatcher surface (`packages/runtime/src/domains/*`) is deliberately not\n * a caller: those handlers RETURN `{ status, body }` for a central sender rather\n * than writing to a response, so they are already consolidated behind their own\n * `deps.success` / `deps.error` helpers and audited by the other half of\n * `check-route-envelope.mjs`.\n */\n\nimport type { ApiError, ErrorCode } from '@objectstack/spec/api';\nimport { logServerFault } from './server-fault-log.js';\n\n/**\n * The only thing an envelope builder needs from a response object.\n *\n * Structural on purpose, so this file depends on no HTTP contract at all:\n * `IHttpResponse` (`@objectstack/spec/contracts`) satisfies it, and so does the\n * `any`-typed `res` the three older route modules still carry. That is what lets\n * a package import the builders without also importing a server abstraction.\n */\nexport interface EnvelopeResponse {\n status(code: number): EnvelopeResponse;\n json(body: unknown): unknown;\n}\n\n/**\n * Emit a success body in the DECLARED envelope — `{ success: true, data }`.\n *\n * `data` carries the route's payload; it is not spread. A payload duplicated\n * into a stray top-level key (`{ success: true, data: link, link }`) parses\n * clean against `BaseResponseSchema` and is still drift — that shipped on\n * `/share-links` for as long as nobody looked (#4038), which is why\n * `envelopeViolations` exists beside the schema and why there is one `data`\n * slot here rather than a spread.\n *\n * `status` defaults to 200 and is set explicitly even then. Five of the seven\n * modules already did that; the two that called `res.json(...)` bare are\n * unaffected, because the default they were relying on is the value now passed.\n */\nexport function sendOk(res: EnvelopeResponse, data: unknown, status = 200): void {\n res.status(status).json({ success: true, data });\n}\n\n/**\n * Emit an error in the DECLARED envelope — `{ success: false, error: { code,\n * message } }`, with `code` a semantic STRING and `message` a field OF `error`\n * rather than a sibling of it.\n *\n * Both halves of that sentence were once wrong somewhere: `error` was a bare\n * string in `service-storage` and `admin-routes` (so `body.error.message` read\n * `undefined`), and `code` was the human message in `package-routes` (#3675 →\n * #3689 → #3843).\n *\n * ## `code` is the closed ADR-0112 vocabulary, not `string`\n *\n * All seven copies typed this parameter `string`, so an invented code was caught\n * only at runtime, by a conformance suite parsing a driven body against\n * `ApiErrorSchema` — i.e. only on the routes a test happened to drive. `ErrorCode`\n * is `StandardErrorCode ∪ ERROR_CODE_LEDGER` (`error-code-ledger.zod.ts`), the\n * same union that schema validates against, so consolidating here moves the check\n * to compile time for every call site at once. It cost no call-site churn: every\n * code the seven modules emit was already registered.\n *\n * A new code is registered in `ERROR_CODE_LEDGER` under its owning package —\n * and if the condition is generic (not found / permission / validation), the\n * standard catalog is used instead of registering a synonym for it.\n *\n * ## `extra` is `ApiError`'s own optional fields, not a `Record`\n *\n * Merged into `error`, and typed as exactly what `ApiErrorSchema` declares\n * beside `code` and `message` — `details`, `category`, `requestId`,\n * `httpStatus`, `declaredCode`, `userMessage`.\n * `details` is the slot for structured context: `package-routes` puts a partial\n * delete's per-item failures there, `settings-routes` the whole\n * `SettingsActionResult`.\n *\n * This started as `Record<string, unknown>`, because `settings-routes` also hung\n * `namespace` / `key` / `reason` / `fields` beside `code`, which the schema does\n * not declare. Those bodies passed every gate anyway — `ApiErrorSchema` is a\n * plain `z.object`, so unknown keys were STRIPPED rather than rejected, and\n * `envelopeViolations` inspects only the body's top level — making them\n * conformant *by stripping* rather than by declaration. #4224 moved that module's\n * four branches onto `details`, which is what lets the parameter close here.\n *\n * Closing it at the shared builder is the part that lasts: an undeclared sibling\n * is now a compile error in every module at once, rather than a key that quietly\n * evaporates at the schema boundary in whichever module reintroduces it.\n *\n * ## `declaredCode` — declared by the schema, barred by this writer\n *\n * ADR-0112's 2026-08-17 amendment (#9106, extended to the flat `/data` door by\n * #9232) rules the demote at EVERY door: `code` stays the closed vocabulary,\n * and a thrown code that is not a member is demoted to a declared sibling,\n * `ApiError.declaredCode` — the open, author-authored channel that carries a\n * metadata app's OWN `.code` across the QuickJS boundary (#7867) and onto the\n * wire.\n *\n * `ApiErrorSchema` has declared that field since #9106 and the flat door emits\n * it, but it was absent from the `Pick` above — so it was a COMPILE ERROR for\n * any route answering the NESTED envelope to pass one, and every such route\n * dropped the producer's spelling. Nothing invalid shipped (the closed `code`\n * still carried the derived member), which is what made the loss silent and\n * one-directional: the author's spelling gone, and a consumer told by the ADR\n * to read `declaredCode` finding nothing there. Declared-but-unemittable is a\n * `declared = enforced` gap, and admitting the field closes it at the ONE\n * writer rather than in each module that later notices.\n *\n * ⛔ Presence MEANS demotion, and this writer does not re-derive that — the\n * CALLER does, with `demotedDeclaredCode` (`thrown-http-error.ts`, one file\n * over), exactly as the flat door's `thrownCodeFields` already does. That\n * helper answers `undefined` when the producer's spelling IS the vocabulary\n * member already sitting in `code`, which is what stops a registered refusal\n * from carrying two spellings of one fact — `ApiErrorSchema.declaredCode`'s\n * documented invariant. Passing a raw `thrown.declaredCode` re-opens exactly\n * that, and no type here can catch it: vocabulary and position stay two\n * decisions (#9232), so the demotion rule stays with the resolver that owns\n * it rather than being restated in the envelope writer.\n *\n * ## `userMessage` — the second declared channel, and why this `Pick` stays explicit\n *\n * #9934's producer-side opt-in (maintainer ruling 2026-08-19 on objectui#5210,\n * option 1) declares `ApiError.userMessage`: the text a producer marked, AT\n * THROW TIME, as addressed to the END USER. Presence IS the marking — a\n * consumer that sees the field renders it verbatim and keeps its generic\n * substitution (#3821) for everything unmarked.\n *\n * The schema declared it and this writer barred it, with the same\n * one-directional silence `declaredCode` had: the other two doors already emit\n * it — the flat `/data` door through `withDeclaredUserMessage`\n * (`rest/error-response.ts`) and the dispatcher door through\n * `thrown.userMessage` (`runtime/http-dispatcher.ts`) — while a route\n * answering the NESTED envelope could not, so an author's deliberate,\n * localized refusal text was dropped on this door alone. Nothing invalid\n * shipped; the text simply was not there.\n *\n * The channel is live on both ends, which is what makes admitting it a repair\n * rather than a new declared-but-dead surface: a hook sets it at throw time —\n * host-side, or a metadata app's sandboxed body whose `e.userMessage` crosses\n * the QuickJS boundary through `SANDBOX_ERROR_PASSTHROUGH`\n * (`runtime/sandbox/quickjs-runner.ts`) — and `resolveThrownHttpError` already\n * carries it onto `ThrownHttpError` for every caller of the shared resolver.\n *\n * ⛔ Unlike `declaredCode`, this field carries NO invariant for the caller to\n * re-derive. `declaredCode`'s presence MEANS demotion, so its caller passes\n * `demotedDeclaredCode(thrown)` rather than the raw field; `userMessage`'s\n * presence means only that the producer opted in, and `declaredUserMessage`\n * has already decided that (a non-empty string, or nothing at all). The caller\n * passes `thrown.userMessage` straight through, exactly as the dispatcher door\n * does.\n *\n * That difference is why `extra` stays an explicit `Pick` rather than becoming\n * \"every optional field of `ApiError`\". A derivation would admit each future\n * optional on the day it lands, with nobody asked whether that channel should\n * cross this door or what obligation it hands the caller — and the two fields\n * above needed opposite answers to exactly that question. Recorded for the next\n * reader, because it is the honest cost: with `userMessage` admitted the `Pick`\n * now names ALL SIX of `ApiError`'s optional fields, so this gate has to date\n * rejected none. What it has produced is a different caller obligation per\n * field, which a derivation cannot produce at all.\n */\nexport function sendError(\n res: EnvelopeResponse,\n status: number,\n code: ErrorCode,\n message: string,\n extra?: Pick<ApiError, 'category' | 'httpStatus' | 'details' | 'requestId' | 'declaredCode' | 'userMessage'>,\n): void {\n // [#14310] A 5xx is never silent. This writer is the single exit for every\n // nested-envelope error in the repo, so the rule is applied ONCE here rather\n // than at each registrar's catch block — a per-door call is a thing a new\n // door can forget, and the `/api/v1/packages` 500 that motivated the card\n // went unlogged for a week through exactly such a door.\n //\n // ⛔ Not a second opinion about the answer: `logServerFault` reads the same\n // `status` this call is about to write, and its own 5xx gate keeps every\n // deliberate 4xx refusal — the coded `409 DESTRUCTIVE_CHANGE`, the\n // `403 FORBIDDEN` capability denials above it — as quiet as they were.\n //\n // The thrown value is not available here (callers resolve it into `message`\n // before arriving), so this line carries the message and code rather than a\n // stack. A door still holding the throw can call `logServerFault` itself for\n // the stack-bearing line; none does today, and the transports that DO hold\n // it log at their own exits instead.\n logServerFault({ status, code, message, ...(extra?.requestId ? { request: { requestId: extra.requestId } } : {}) });\n res.status(status).json({ success: false, error: { code, message, ...extra } });\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * The ONE rule for \"what HTTP answer does a THROWN error declare?\" (#8016).\n *\n * A service or protocol throw that carries its own `.status` / `.statusCode`\n * and its own semantic `.code` is a *refusal*, not a fault: the caller asked\n * for something the platform will not do, and the honest answer is that status\n * with that code. A throw carrying neither is a fault, and the honest answer is\n * the caller's fallback — 500 `INTERNAL_ERROR` at an HTTP boundary.\n *\n * ## Why this is shared rather than restated per door\n *\n * `/api/v1/packages` has **two** HTTP doors. The runtime dispatcher's\n * `HttpDispatcher.errorFromThrown` read `.status` first and answered `409\n * DESTRUCTIVE_CHANGE` for a `metadata-protocol` refusal. The direct-mount REST\n * registrar (`packages/rest/src/package-routes.ts`) had four catch-alls that\n * answered `500 INTERNAL_ERROR` regardless — and *that* registrar mounts first\n * in the production stack, so 500 was what production actually returned. One\n * throw, two answers, and the wrong one was the live one (#8016).\n *\n * The rule therefore lives in ONE function that both doors call. It could not\n * live in `packages/runtime`: `@objectstack/runtime` depends on\n * `@objectstack/rest`, so the arrow only points one way and `errorFromThrown`\n * is unreachable from the REST door by construction. `@objectstack/types`\n * depends on nothing but `@objectstack/spec`, which is exactly why the other\n * shared HTTP-boundary helpers already live here — `looksLikeInternalErrorLeak`\n * (\"do not ship driver internals to clients\") and `sendOk`/`sendError` (\"write\n * the declared envelope\"). \"What status does this throw mean?\" is the same kind\n * of property: it belongs to the boundary, not to one router.\n *\n * ## Two spellings of the code, because the two envelopes are not equally closed\n *\n * {@link ThrownHttpError.code} is narrowed to `StandardErrorCode ∪\n * ERROR_CODE_LEDGER` — the union `ApiErrorSchema` validates against — so a\n * throw whose `.code` is not a registered member does not get to name itself;\n * it falls to the code the status derives. That is the same rule\n * `metadata-protocol`'s `toRowApiError` applies to a per-row batch error, and\n * it is what lets `sendError`'s closed `ErrorCode` parameter be satisfied\n * without a cast. The direct-mount REST door needs exactly this: its bodies are\n * parsed against `BaseResponseSchema` by its own conformance suite, so an\n * unregistered code there is a failing test, not a wire answer.\n *\n * {@link ThrownHttpError.declaredCode} is the producer's own string, verbatim\n * and un-narrowed. Until #9106 it was what the dispatcher door put in\n * `error.code`; since the #9106 ruling it is what BOTH doors surface as the\n * wire's `declaredCode` when it is not a vocabulary member (see below).\n *\n * [#8087] The first ruling on that gap (maintainer, 2026-08-12) kept the\n * dispatcher's verbatim spelling and delivered a GATE — the unregistered\n * producers are measured and classified\n * (`packages/runtime/src/dispatcher-error-vocabulary.ts`,\n * `pnpm check:dispatcher-error-vocabulary`) instead of named in prose here.\n * The gate's own first derivation then measured the limb no registration can\n * close: a metadata app's action code crosses the sandbox boundary carrying\n * the app's OWN `.code` (#7867), authored by tenants at runtime.\n *\n * [#9106] That limb was ruled (maintainer, 2026-08-16): **`error.code` is a\n * closed vocabulary at every door.** The dispatcher door now takes\n * {@link ThrownHttpError.code} — the demote this resolver has always computed,\n * and the REST door's spelling since #8016 — and a producer's unregistered\n * string rides the wire's `declaredCode` (declared on `ApiErrorSchema`)\n * instead of `error.code`. #7867's capability is preserved: the author's code\n * still crosses the sandbox and still reaches the wire — in the open,\n * author-authored channel, not the closed one. Use\n * {@link demotedDeclaredCode} to read the spelling a boundary should surface\n * beside the closed `code`.\n *\n * So the doors agree on **status** and on **code** unconditionally now — both\n * answers come from ONE function, which is what keeps agreement a construction\n * rather than two suites agreeing about literals.\n *\n * [#12509] And the channel has a SCOPE, ruled 2026-08-27 (option D): on a 5xx\n * the producer did not declare, the demoted spelling came off an undeclared\n * producer and is withheld with the prose, while an author-declared code\n * survives. The discriminator is {@link serverFaultProvenance} — one function,\n * read by {@link demotedDeclaredCode}, which every door already calls, so no\n * registrar carries a variant. Read that function's note for why the status\n * channel is the only honest signal here.\n *\n * ## What this deliberately does NOT decide\n *\n * - **Message disclosure.** A 5xx message may name physical tables or carry a\n * driver dump; withholding it is `looksLikeInternalErrorLeak`'s job, applied\n * by the caller (the dispatcher does; see #3867). This function returns the\n * thrown message verbatim.\n * - **Whether a declared status is *plausible*.** No 400-599 band is imposed,\n * because the dispatcher never imposed one and this function exists to make\n * the two doors agree. Narrowing the accepted band is a change to the rule,\n * and it belongs here — in one place, for both doors — if it is ever made.\n */\n\nimport { ErrorCode, standardErrorCodeForHttpStatus } from '@objectstack/spec/api';\nimport { validationFailureDetails, VALIDATION_FAILED_STATUS } from './validation-failure.js';\n\n/** The HTTP answer a thrown error declares. See {@link resolveThrownHttpError}. */\nexport interface ThrownHttpError {\n /** The producer's own `status`/`statusCode`, or the caller's fallback. */\n status: number;\n /**\n * The status the THROW ITSELF declared — `.status`, `.statusCode`, or the\n * 400 a validation-shaped throw declares by shape — and **absent** when it\n * declared none, i.e. when {@link ThrownHttpError.status} above is the\n * caller's `fallbackStatus`.\n *\n * ## Why `status` cannot answer this\n *\n * A producer that declares `500` and one that declares nothing both resolve\n * to `status: 500`, so a caller that must tell \"the producer said so\" from\n * \"I supplied the default\" cannot read it off the value. The workaround in\n * the repo was to probe this function with a fallback no producer declares\n * — `resolveThrownHttpError(e, 0).status !== 0`. That is a magic number\n * standing in for a fact this function already computed, and it fails\n * silently the day a producer declares the sentinel. So the fact is stated;\n * `packages/rest`'s publish-classification suite now reads\n * `resolveThrownHttpError(error).declaredStatus !== undefined` instead of\n * hand-spelling the workaround.\n *\n * ## Who needs the distinction\n *\n * A sink that mirrors the status onto RESPONSE DATA instead of into the\n * response's own status line — where the fallback would not be a default but\n * an invention. `metadata-protocol`'s `toRowApiError` is the measured one\n * (#8570): a batch row rides a **200**, so stamping `status` there would put\n * `httpStatus: 500` on every undeclared driver fault, an ADDITION to the\n * wire, where stamping `declaredStatus` restores only what a producer really\n * declared. Boundaries that answer with the status itself keep reading\n * `status` — the fallback is exactly what they want.\n */\n declaredStatus?: number;\n /**\n * A member of the declared ADR-0112 vocabulary — for a boundary whose\n * envelope is checked against it. Never the HTTP status.\n */\n code: ErrorCode;\n /**\n * The producer's own code, verbatim and un-narrowed, or `undefined` when it\n * declared none. Never for `error.code` — that slot takes {@link code} at\n * every door (#9106) — but for the wire's `declaredCode` channel when the\n * spelling is not a vocabulary member ({@link demotedDeclaredCode}). See the\n * module note on why there are two.\n *\n * ⚠️ This field records what the producer WROTE, not what a boundary may\n * emit: since #12509 a demoted spelling is withheld on an undeclared 5xx.\n * ⛔ Read {@link demotedDeclaredCode}, never this field, when deciding what\n * goes on a wire.\n */\n declaredCode?: string;\n /** The thrown message, UNSANITISED — see the module note on disclosure. */\n message: string;\n /**\n * The producer's user-facing refusal text, verbatim — present exactly when\n * the throw carried a non-empty string `userMessage` (#9934).\n *\n * This is the producer-side opt-in the objectui#5210 ruling asked for\n * (maintainer, 2026-08-19, option 1): an application hook's refusal has no\n * way to distinguish author-written user guidance from platform diagnostics,\n * so the console substitutes a generic string on 403 (the recorded #3821\n * fix) and every author-written remedy is suppressed with the diagnostics.\n * A producer that sets `userMessage` on the thrown error is saying, at throw\n * time, \"this exact text is addressed to the END USER\" — a consumer renders\n * it verbatim and keeps the generic substitution for everything unmarked.\n *\n * Deliberately a FIELD carrying the text, not a boolean beside `message`:\n * the mark and the marked text are one value, so a boundary that rewraps or\n * substitutes `message` (sanitisation, truncation, the sandbox debug\n * wrapper) can never accidentally promote platform prose into the marked\n * channel — the #3821 protection holds by construction. Read through\n * {@link declaredUserMessage}, never with an inline `typeof` probe.\n *\n * Status-agnostic on purpose (the ruling's second constraint): a 400, 403,\n * 409 or 503 refusal may all carry it. It never REPLACES `message` — the\n * diagnostic channel keeps its wording for logs and developers.\n */\n userMessage?: string;\n /**\n * Structured context: spec-validation `issues[]`, record-validation\n * `fields[]`. Absent rather than `{}` when the throw carried none, so an\n * empty object never reads as \"there is context here\".\n */\n details?: Record<string, unknown>;\n}\n\n/**\n * Resolve a thrown error into the status, code, message and structured context\n * an HTTP boundary should answer with.\n *\n * Precedence, in order:\n *\n * | Question | Answer |\n * |---|---|\n * | status | `.status` → `.statusCode` → 400 if it is a validation failure → `fallbackStatus` |\n * | declaredStatus | the same chain WITHOUT the fallback — absent when the throw declared none |\n * | code | `VALIDATION_FAILED` if it is one → a REGISTERED `.code` → derived from the status |\n * | declaredCode | `VALIDATION_FAILED` if it is one → any non-empty string `.code` → absent |\n * | message | `.message` when it is a string → `String(error)` |\n * | userMessage | a non-empty string `.userMessage` → absent (see {@link declaredUserMessage}) |\n *\n * Both status spellings are read because both are produced in this repo:\n * `plugin-approvals`' lifecycle hooks and `metadata-protocol` throw\n * `statusCode`, `metadata-protocol`'s conflicts throw `status`. Reading one\n * spelling is how `/api/v1/data` answered 500 for a deliberate `409\n * RECORD_LOCKED` until #7525.\n */\nexport function resolveThrownHttpError(error: unknown, fallbackStatus = 500): ThrownHttpError {\n const e = error as any;\n const validation = validationFailureDetails(e);\n\n // The validation SHAPE is a declaration too: `ValidationError` carries no\n // status because deciding it means 400 is the boundary's job, but the\n // producer did say \"this is a client's input problem\" — which is the fact\n // `declaredStatus` reports. Only the `fallbackStatus` limb below is the\n // caller's own invention, and it is the only one left out.\n const declaredStatus =\n typeof e?.status === 'number' ? e.status\n : typeof e?.statusCode === 'number' ? e.statusCode\n : validation ? VALIDATION_FAILED_STATUS\n : undefined;\n const status = declaredStatus ?? fallbackStatus;\n\n const spelled = typeof e?.code === 'string' && e.code !== '' ? e.code : undefined;\n // A `.code` the ledger does not know cannot go in a slot typed as the closed\n // vocabulary — see the module note on why there are two spellings.\n const registered = spelled !== undefined && ErrorCode.safeParse(spelled).success\n ? (spelled as ErrorCode)\n : undefined;\n const code: ErrorCode = validation\n ? validation.code\n : (registered ?? standardErrorCodeForHttpStatus(status));\n const declaredCode = validation ? validation.code : spelled;\n\n const issues = Array.isArray(e?.issues) ? e.issues : undefined;\n const details: Record<string, unknown> = {\n // A truthy NON-string `code` (a driver errno, say) is context and stays\n // context — promoting it would put a number in the field callers branch on,\n // which is the drift #3842 removed.\n ...(!validation && e?.code && typeof e.code !== 'string' ? { code: e.code } : {}),\n ...(issues ? { issues } : {}),\n ...(validation ? { fields: validation.fields } : {}),\n };\n\n const userMessage = declaredUserMessage(error);\n\n return {\n status,\n ...(declaredStatus !== undefined ? { declaredStatus } : {}),\n code,\n ...(declaredCode !== undefined ? { declaredCode } : {}),\n message: typeof e?.message === 'string' ? e.message : String(error),\n ...(userMessage !== undefined ? { userMessage } : {}),\n ...(Object.keys(details).length > 0 ? { details } : {}),\n };\n}\n\n/**\n * The user-facing refusal text a thrown error DECLARED, or `undefined` when it\n * declared none (#9934). See {@link ThrownHttpError.userMessage} for what the\n * declaration means and why it is a text-carrying field rather than a flag.\n *\n * The ONE read every boundary applies — the REST classification door, the\n * dispatcher door, and the sandbox side-channel all call this rather than\n * probing `error.userMessage` themselves, so \"what counts as marked\" cannot\n * fork per door the way the `status`/`statusCode` spelling once did (#7525).\n *\n * A non-string or blank `userMessage` is NOT a declaration: `undefined`, a\n * number, `''` and whitespace-only all answer `undefined`, so nothing invents\n * a marked message for a producer that never wrote one — absent means the\n * consumer keeps its generic substitution (#3821 preserved by construction).\n */\nexport function declaredUserMessage(error: unknown): string | undefined {\n const declared = (error as { userMessage?: unknown } | null | undefined)?.userMessage;\n return typeof declared === 'string' && declared.trim().length > 0 ? declared : undefined;\n}\n\n/**\n * [#12509] WHO named this 5xx — the producer, or this resolver's fallback.\n * `undefined` for anything below 500, where nothing is sanitised at all.\n *\n * This is the ONE definition of the distinction ADR-0112's 5xx-sanitisation\n * scope turns on (maintainer ruling 2026-08-27, option D), and it exists as a\n * named function rather than as an inline conjunction because TWO rules read\n * it and they read opposite limbs:\n *\n * - `'undeclared'` — the throw declared no HTTP answer, so\n * {@link ThrownHttpError.status} is the caller's `fallbackStatus` and\n * EVERYTHING this resolver picked up off that throw is the producer's\n * internals rather than an answer it composed. A driver errno\n * (`SQLITE_ERROR`, `42P01`) is the measured case, and it is why\n * {@link demotedDeclaredCode} withholds the code here: the spelling names\n * the backend, which is one of the two disclosures the 5xx message\n * withhold exists to prevent (`looksLikeInternalErrorLeak`; the other,\n * identifiers, is already covered).\n * - `'declared'` — the producer named a 5xx ITSELF, so its code is authored\n * and survives. #11718's `{ status: 503, code: 'SERVICE_UNAVAILABLE' }`\n * relay is this limb, and so is a metadata app's own 5xx refusal spelling\n * (#7867), which the ADR-0112 amendment wrote `declaredCode` for.\n *\n * ⚠️ The DISCRIMINATOR is the status channel, not the code's shape. There is\n * no other structural signal: a driver errno and an app's own spelling both\n * arrive on `.code` as a plain string, so anything that told them apart by\n * LOOKING at the string would be a heuristic over an open channel — the\n * consumer-side tolerance ADR-0112 exists to forbid, and unfalsifiable besides\n * (nothing stops an app from spelling `SQLITE_ERROR`). The cost of the\n * structural answer is stated rather than hidden: a producer that spells a\n * code but declares NO status loses that code on a 5xx. It keeps it by\n * declaring the status it means, which is the shape the ADR already asks for.\n *\n * ⛔ NOT gated on whether `looksLikeInternalErrorLeak` actually fired on the\n * message. That predicate is a heuristic over a DIFFERENT channel, and gating\n * here on it would leak the errno for exactly the dialects whose prose the\n * heuristic misses — the ceiling `sendThrownError`'s note records. The 5xx\n * sanitisation REGIME is the condition, not one of its two outcomes.\n *\n * ⭐ #12281 — the prose axis of the same 2026-08-27 ruling — is the\n * `'declared'` limb of this same function: the dispatcher door withholds the\n * message of EVERY declared 5xx, aligning to `/data`. It is a separate card\n * with its own measurement-first step, so nothing here applies it; this\n * function is the shape it will read rather than a second copy it would have\n * to grow.\n */\nexport type ServerFaultProvenance = 'declared' | 'undeclared';\n\n/** See {@link ServerFaultProvenance}. */\nexport function serverFaultProvenance(thrown: ThrownHttpError): ServerFaultProvenance | undefined {\n if (thrown.status < 500) return undefined;\n return thrown.declaredStatus === undefined ? 'undeclared' : 'declared';\n}\n\n/**\n * The producer's spelling a boundary should surface as the wire's\n * `declaredCode` beside the closed `code` — or `undefined` when there is\n * nothing to surface (#9106).\n *\n * Present exactly when the throw spelled a code that did NOT survive into\n * {@link ThrownHttpError.code} — i.e. the demote happened — AND the answer is\n * not an undeclared server fault. A registered code is already in `code`, so\n * emitting it again would put two spellings of one fact on every refusal; a\n * throw with no code has nothing to declare. Spelled once here rather than as\n * three `!==` comparisons at three exits, so \"presence means demotion\"\n * (`ApiErrorSchema.declaredCode`'s documented semantics) has one definition.\n *\n * [#12509] The withhold limb, ruled 2026-08-27 (option D): on a 5xx the\n * producer did NOT declare, the spelling this resolver demoted came off an\n * undeclared producer — a driver errno, measured on the wire at three of this\n * repo's doors — and it is withheld along with the prose. An AUTHOR-declared\n * code survives at every status. The judgement lives in\n * {@link serverFaultProvenance}; it is applied HERE, in the one read every\n * boundary already makes, so all of them inherit it without a door growing a\n * rule of its own. ⛔ Do not re-derive the condition at a door: a per-door\n * variant is the divergence this channel has now been repaired for twice.\n */\nexport function demotedDeclaredCode(thrown: ThrownHttpError): string | undefined {\n if (serverFaultProvenance(thrown) === 'undeclared') return undefined;\n return thrown.declaredCode !== undefined && thrown.declaredCode !== thrown.code\n ? thrown.declaredCode\n : undefined;\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Recognising a record-validation failure at an HTTP boundary.\n *\n * `ValidationError` (`@objectstack/objectql`'s record/rule validators) carries\n * `.code = 'VALIDATION_FAILED'` and `.fields[]` — one entry per offending\n * field — but deliberately carries NO `.status` / `.statusCode` and no\n * `.issues`. It is a plain domain error; deciding it means \"400\" is the job of\n * whichever boundary serves it.\n *\n * `@objectstack/rest` has always done that (`mapDataError` → 400 with\n * `fields[]`). The runtime dispatcher's two error exits did not (#3918): with\n * no `.status` to read they fell back to **500**, and both read only `.issues`\n * for structured detail — which a `ValidationError` never has — so `fields[]`\n * was dropped and the caller got a generic \"internal error\" for what was\n * really a user-input mistake. That forecloses per-field error display on every\n * surface the dispatcher serves.\n *\n * Matched by duck-typing on `code` / `name` — exactly the predicate\n * `mapDataError` uses — so this module stays free of a runtime dependency on\n * `objectql`, and so hand-rolled errors of the same shape (e.g. a hook that\n * throws `{ code: 'VALIDATION_FAILED', fields }`) are served identically.\n *\n * ## Why it lives in `@objectstack/types` (#8016)\n *\n * It was `packages/runtime/src/validation-failure.ts` until the *package* door's\n * four status-blind catch-alls were converged onto the dispatcher's mapping\n * ({@link resolveThrownHttpError}, one file over). That resolver has to answer\n * \"is this throw a validation failure?\" the same way on both doors, and\n * `@objectstack/rest` cannot import `@objectstack/runtime` — runtime depends on\n * rest, so the arrow only points one way. `@objectstack/types` depends on\n * nothing but `@objectstack/spec`, which is why the shared HTTP-boundary\n * helpers (`looksLikeInternalErrorLeak`, `sendOk`/`sendError`) already live\n * here. This module moved for the same reason and is unchanged otherwise;\n * `packages/runtime/src/validation-failure.ts` re-exports it, so every runtime\n * import site still reads the name it always did.\n */\n\nimport { zodIssuesToFields } from '@objectstack/spec/api';\nimport type { FieldErrorCode } from '@objectstack/spec/api';\n\n/** The HTTP status a validation failure maps to when the error names none. */\nexport const VALIDATION_FAILED_STATUS = 400;\n\nexport interface ValidationFailureDetails {\n code: 'VALIDATION_FAILED';\n /** Per-field envelopes, passed through verbatim. `[]` when absent/malformed. */\n fields: unknown[];\n}\n\n/**\n * Structured `details` for a thrown validation failure, or `undefined` when\n * `err` is not one. Callers use the `undefined` result as the predicate and the\n * returned object as the `details` payload, so the two can never disagree.\n */\nexport function validationFailureDetails(err: any): ValidationFailureDetails | undefined {\n if (!err) return undefined;\n if (err.code !== 'VALIDATION_FAILED' && err.name !== 'ValidationError') return undefined;\n return {\n code: 'VALIDATION_FAILED',\n fields: Array.isArray(err.fields) ? err.fields : [],\n };\n}\n\n/**\n * [#3878/#3899] The CONSTRUCTOR for the shape {@link validationFailureDetails}\n * recognises — kept in the same module so the two can never drift. Thrown from\n * a domain handler, both dispatcher error exits map it to\n * `400 VALIDATION_FAILED` + `details.fields[]` (#3918) with no new error\n * channel and no runtime dependency on objectql's `ValidationError` class.\n * First built inline by the analytics domain; hoisted here when notifications\n * and automation grew the same entry gates rather than a third copy.\n */\nexport function validationFailure(message: string, fields: unknown[]): Error {\n const err = new Error(message) as Error & { code: string; fields: unknown[] };\n err.name = 'ValidationError';\n err.code = 'VALIDATION_FAILED';\n err.fields = fields;\n return err;\n}\n\n/**\n * Zod issues → the dispatcher's `fields[]` envelope entries\n * (`{ field, code, message }`). `'(body)'` names a root-level failure — a body\n * that is the wrong TYPE entirely has no path to point at.\n *\n * ## The `code` is an ADR-0114 `FieldErrorCode`, not Zod's (#8124)\n *\n * This used to assign `issue.code` verbatim, which put Zod's own vocabulary\n * (`unrecognized_keys`, `too_small`, …) on a wire position\n * `FieldErrorSchema.code` declares as a CLOSED catalog — the exact\n * pass-through ADR-0114 D3 closed on the REST transport. It now maps through\n * `zodIssuesToFields`, the one D3 implementation in the repo, which lives in\n * `@objectstack/spec` beside the catalog it is total over (this package cannot\n * import `@objectstack/rest`, where the compliant copy grew up — the\n * dependency arrow points the other way, which is what #8124 moved it for).\n *\n * Two things ride along, both additive:\n *\n * - **The optional `input`** (the value that was parsed) buys the D3\n * `invalid_type` split: with it a MISSING required property is reported as\n * `required` instead of the `invalid_type` Zod spells it as. Callers without\n * the input at hand degrade per the D3 table — every code is still a\n * catalog member.\n * - **Union expansion (#5014)**: a rejection behind a `z.union` yields the\n * union's own entry PLUS the branch entries that explain it, so entry count\n * is not issue count. Read `fields.length` as the number of field errors.\n */\nexport function fieldsFromZodIssues(\n issues: Array<{ path: Array<string | number | symbol>; code: string; message: string }>,\n ...input: [] | [unknown]\n): Array<{ field: string; code: FieldErrorCode; message: string }> {\n return zodIssuesToFields(issues, ...input).map((entry) =>\n entry.field === '' ? { ...entry, field: '(body)' } : entry,\n );\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * The one home for Postgres' `«sub-object» \"x\" of relation \"y\" …` phrasing\n * (#6615).\n *\n * ## The superstring hole, stated once\n *\n * Postgres phrases a failure about something *inside* a relation by naming the\n * relation too:\n *\n * ```\n * column \"label\" of relation \"sys_team\" does not exist (42703)\n * constraint \"uq_sys_team_name\" of relation \"sys_team\" does not exist (42704)\n * column \"environment_id\" of relation \"sys_metadata\" already exists (42701)\n * ```\n *\n * Every one of those **contains a complete, legal missing-TABLE phrase** —\n * `relation \"sys_team\" does not exist` — as a substring, while meaning the\n * opposite: the relation is right there, which is precisely why it could be\n * named. No amount of tightening a \"does this say a relation is missing?\"\n * regex can remove that match, because the phrase really is in there. The only\n * repair is to ask the more specific question FIRST. That makes the ORDER the\n * fix, not the pattern — and it is why three packages each grew their own copy\n * of this phrase (#5352, #6035/PR #6346, #6347/PR #6613) before it was given a\n * home.\n *\n * ## Two widths, on purpose — never collapse them\n *\n * The three consumers do not want the same regex, and the difference is not\n * sloppiness: it is **which direction of error is safe** at each site.\n *\n * | consumer | asks | uses | a MISS costs |\n * |:---|:---|:---|:---|\n * | `@objectstack/rest` `mapDataError` (#5352) | which column? | {@link matchMissingColumnOfRelation} | a vaguer message (`404` instead of `400 INVALID_FIELD`) |\n * | `@objectstack/service-analytics` `isMissingSourceError` / `missingSourceRelation` (#6035) | is this a missing COLUMN, so keep it hard? | {@link matchMissingColumnOfRelation} | a mistyped column degrades to a confident empty chart |\n * | `@objectstack/metadata` `MISSING_TABLE.excludes` (#6347) | is this about a sub-object, so not a missing table? | {@link isRelationSubObjectPhrase} | a corruption verdict returns (`event_seq` restarts at 1) |\n *\n * The first two **extract**, so they must be strict: over-matching there would\n * turn a genuinely missing table into a hard failure and regress #5033's\n * deliberate leniency, while under-matching merely keeps today's verdict. The\n * third **excludes**, so it is deliberately wider — any sub-object, any quoted\n * identifier, any verdict — because over-matching there only ever converts a\n * benign verdict into a loud one, and a miss restores data corruption.\n *\n * Collapsing the two into one regex would therefore be wrong for one caller\n * whichever width won. They are two exports for that reason, and the reason is\n * load-bearing rather than stylistic.\n *\n * ## Home\n *\n * `@objectstack/types`, following `isUniqueViolationError`'s move\n * (#6250 — four hand-written answers to one question) and\n * `isModuleNotFoundError`'s (framework#3265 — \"single shared owner … so the\n * parallel loaders cannot drift apart\"). This module deliberately imports\n * nothing.\n *\n * ⚠️ Unlike #6250, adopting this **does** add one dependency edge:\n * `@objectstack/service-analytics` did not depend on `@objectstack/types`\n * before #6615. It is acyclic by construction — `@objectstack/types` depends\n * only on `@objectstack/spec`, which depends on nothing in-repo, so no package\n * except `spec` itself can form a cycle by consuming it — and 25 of the repo's\n * 73 packages (5 of 16 services) already carry the same edge. Recorded here\n * rather than left for a reader to rediscover.\n */\n\n/**\n * Postgres' missing-COLUMN template, strictly. Returns the column name, or\n * `undefined` when the message is not that phrase.\n *\n * Anchored to `column \"%s\" of relation \"%s\" does not exist` — the exact errmsg\n * template Postgres emits for SQLSTATE 42703 on the write path\n * (`INSERT` / `UPDATE` / `ALTER`). Both quotes are required because Postgres\n * always emits them here, and requiring them is the safe direction of error for\n * the two consumers that call this.\n *\n * Deliberately narrow in two further ways, both preserved verbatim from the\n * open-coded copies this replaces:\n *\n * - the identifier is `[a-z0-9_]+` (case-insensitive), so a quoted identifier\n * carrying a space or punctuation is NOT matched. Postgres can quote such\n * names; the consumers accept the miss because a miss is the cheap direction.\n * - the relation is `\\S+` — quoted or bare, unparsed. This function answers\n * \"which COLUMN\", never \"which relation\".\n *\n * The read-path phrasing `column \"bogus\" does not exist` is a different\n * sentence with no relation in it, so it does not match — and it does not need\n * to: it carries no missing-table substring, which is the whole hole this\n * module exists for.\n */\nexport function matchMissingColumnOfRelation(message: string): string | undefined {\n return MISSING_COLUMN_OF_RELATION.exec(message)?.[1];\n}\n\n/**\n * The same quirk, **wider**: does this message talk about any sub-object of a\n * relation, in any verdict?\n *\n * Drops all three of {@link matchMissingColumnOfRelation}'s anchors — the\n * literal `column`, the `[a-z0-9_]+` identifier shape, and the trailing\n * `does not exist` — so it also recognises `constraint \"uq_x\" of relation \"y\"\n * does not exist` (42704), `column \"x\" of relation \"y\" already exists` (42701),\n * and every other sub-object Postgres phrases this way.\n *\n * For **exclusion** callers only. A `true` here means \"the relation is present,\n * so whatever else this error is, it is not a missing table\"; it does not mean\n * the error is benign and it names nothing. Using it to extract would be a\n * category error — there is no capture group precisely so that it cannot be.\n */\nexport function isRelationSubObjectPhrase(message: string): boolean {\n return RELATION_SUB_OBJECT.test(message);\n}\n\n/**\n * The strict extractor's pattern. Module-private: exported behaviour is the two\n * functions above, so a consumer cannot read the wrong capture group, re-flag\n * the regex, or quietly widen one width toward the other.\n */\nconst MISSING_COLUMN_OF_RELATION =\n /column\\s+[\"'`]([a-z0-9_]+)[\"'`]\\s+of relation\\s+\\S+\\s+does not exist/i;\n\n/** The wide detector's pattern. Module-private for the same reason. */\nconst RELATION_SUB_OBJECT = /[\"'`][^\"'`]+[\"'`]\\s+of relation\\s/i;\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * The one named predicate for \"is this driver error a unique-constraint\n * violation?\" (#6250).\n *\n * ## The defect this retires\n *\n * Before this module the repo carried **four** hand-written, mutually different\n * answers to that single question — no two covering the same dialects:\n *\n * | where | judged by | covered |\n * |:---|:---|:---|\n * | `service-messaging`'s `isUniqueViolation()` | 3 codes + 3 message substrings | all three |\n * | `@objectstack/rest`'s `mapDataError` | `unique constraint` / `unique violation` only | **no MySQL** |\n * | `@objectstack/rest`'s `sanitizeRowError` | three column-extracting regexes | all three |\n * | `driver-sql`'s inline regex | `unique constraint failed\\|duplicate entry\\|duplicate key value` | all three |\n *\n * The REST row is the one a user could feel. Its verdict decides whether a\n * conflict comes back as the API contract's `409 UNIQUE_VIOLATION` (a\n * registered code in `packages/spec/src/api/error-code-ledger.zod.ts`) or as a\n * generic `500 INTERNAL_ERROR`, and MySQL's phrasing —\n * `ER_DUP_ENTRY: Duplicate entry 'acme@example.com' for key 'idx_email_unique'`\n * — matches neither substring. Measured on `origin/main` before this change,\n * through the real `mapDataError`:\n *\n * ```\n * mysql, bare message => 500 INTERNAL_ERROR ← the reported defect\n * mysql, knex-prefixed SQL => 500 DATABASE_ERROR ← second spelling, same hole\n * postgres, SQLSTATE only => 500 INTERNAL_ERROR ← the code channel was unread\n * sqlite, message => 409 UNIQUE_VIOLATION\n * postgres, message => 409 UNIQUE_VIOLATION\n * ```\n *\n * So the hole was never MySQL-only: it was \"the mapping reads one channel\n * (message substrings) of the two that drivers actually use\". SQLite and\n * Postgres were invisible survivors because their prose happens to contain the\n * words the substring test looks for.\n *\n * ## Why a predicate rather than a wider heuristic\n *\n * `looksLikeInternalErrorLeak` (one file over) answers a **different**\n * question — \"would echoing this text leak server internals?\" — and the 409\n * mapping used to be nested *inside* its true-branch, so a message had to look\n * like a leak before it could be recognised as a conflict. Those two questions\n * have no reason to agree, and MySQL is the case where they don't. Widening the\n * leak heuristic to reach the conflict branch would have coupled them harder\n * and quietly reclassified unrelated driver text as safe-to-expose; naming the\n * conflict question separately unpicks them instead. Same move as #5841's\n * `isMissingTableError`, and the same reason.\n *\n * ## Home\n *\n * `@objectstack/types` because every consumer of the question already depends\n * on it, so adopting the predicate never adds an edge. This module deliberately\n * imports nothing.\n *\n * ## The second question, answered separately\n *\n * `isUniqueViolationError` answers yes/no. **Which column** conflicted is a\n * different question with a different failure mode, so it is a different export:\n * {@link uniqueViolationColumn}, added by #6544 under the maintainer's\n * 2026-08-08 ruling. Read its doc comment before touching either — the two are\n * gated on each other and the column answer is deliberately narrower than the\n * boolean.\n *\n * ## ⚠️ The INVERSE question lives next door — do not merge them\n *\n * `isUnbackedConflictTargetError` (`unbacked-conflict-target.ts`, #8567) asks\n * whether the database refused an `ON CONFLICT` target because **no unique\n * index exists** for it. This predicate asks whether one **exists and was\n * violated**. Same neighbourhood, same vocabulary, inverse verdicts:\n * answering an unbacked target with a 409 `UNIQUE_VIOLATION` tells the client\n * to change a value when nothing collided, and answering a real conflict with\n * \"add a unique index\" sends an operator after an index that is already there.\n * Neither predicate may grow a limb belonging to the other.\n *\n * ⚠️ This predicate WAS on the wrong side of that line, and #8590 moved it\n * back. The `message` limb used to read `unique constraint` as a bare word\n * pair, which matched every sentence containing those two words **including\n * the ones saying the constraint is absent**. Since #8590 the limb requires a\n * VIOLATION phrasing — `unique constraint failed` (SQLite) or\n * `violates unique constraint` (Postgres) — so a sentence that merely mentions\n * a unique constraint no longer answers yes. The reasoning, and the measured\n * sentences that forced it, are on {@link UNIQUE_VIOLATION} below;\n * `unique-violation-absence-sentences.test.ts` pins the absence sentences and\n * `unbacked-conflict-target.test.ts` pins both predicates' verdicts per dialect,\n * so neither the fix nor a fresh drift can land silently in either direction.\n */\n\n/**\n * One dialect vocabulary, in the three channels drivers actually use.\n *\n * Same shape as `@objectstack/metadata`'s `DriverErrorSignature` — deliberately,\n * because it is the shape the drivers force: Postgres puts SQLSTATE on `code`,\n * mysql2 puts a symbolic name on `code` *and* a number on `errno`, and the\n * SQLite family often gives nothing but prose.\n */\ninterface UniqueViolationSignature {\n /** `error.code` — Postgres SQLSTATE, mysql2's symbolic name, SQLite's extended result code. */\n readonly codes: ReadonlySet<string>;\n /** `error.errno` — MySQL/MariaDB's numeric equivalent of the same condition. */\n readonly errnos: ReadonlySet<number>;\n /** `error.message` — the only channel a knex-wrapped or SQLite-family error reliably carries. */\n readonly message: RegExp;\n}\n\n/**\n * The union of every unique-violation signal the four pre-existing\n * implementations encoded, plus the `errno` channel their `code`-only reads\n * missed.\n *\n * **Seeded from what real drivers emit, not invented here.** Every entry traces\n * to one of the four inventoried implementations; nothing was added on a guess:\n *\n * - `23505` — PostgreSQL SQLSTATE `unique_violation` (from `service-messaging`).\n * - `ER_DUP_ENTRY` — mysql2's symbolic name for 1062 (from `service-messaging`).\n * - `SQLITE_CONSTRAINT_UNIQUE` — better-sqlite3 / libsql extended result code\n * (from `service-messaging`).\n * - `1062` — the same MySQL condition on the channel mysql2 *also* sets. The\n * one addition, and not a new dialect: `@objectstack/metadata`'s\n * `driver-error-classification.ts` (this package since #13279; it was\n * `metadata/src/utils/schema-sync-errors.ts`) already reads `errno`\n * alongside `code` for exactly\n * these drivers, so a code-only read is a known gap rather than a decision.\n * - `UNIQUE_VIOLATION` — the PLATFORM's own registered code\n * (`error-code-ledger.zod.ts`), added by #13197 when `driver-memory` grew\n * field-level uniqueness. It is not a dialect and not a heuristic: it is\n * the value the platform already uses to MEAN \"unique violation\", so a\n * limb reading it is a tautology, with none of the false-positive risk the\n * message limbs are rationed against. It is also load-bearing rather than\n * cosmetic — see the note below.\n *\n * The message limbs are a **superset of what `mapDataError` already treated as\n * 409**, which is what makes routing REST through this predicate incapable of\n * narrowing a verdict a client relies on today:\n *\n * - `unique constraint failed` — SQLite's `UNIQUE constraint failed: t.c`.\n * - `violates unique constraint` — Postgres' `... violates unique constraint\n * \"...\"`. These two replaced a single bare `unique constraint` limb that was\n * inherited verbatim from the REST branch; see \"Why a VIOLATION phrasing\"\n * below for the sentences that forced the split. Both dialects' genuine\n * spellings are preserved exactly — that was the constraint on the fix.\n * - `unique violation` — inherited verbatim from the same limb (SQLSTATE\n * 23505's condition name, which some transports render as prose).\n * - `duplicate key` — Postgres' `duplicate key value violates ...`\n * (from `service-messaging` and `driver-sql`).\n * - `duplicate entry` — MySQL's `Duplicate entry 'x' for key 'i'`\n * (from `service-messaging` and `driver-sql`). **This is the limb whose\n * absence made every MySQL conflict a 500.**\n *\n * Deliberately NOT here: bare `constraint failed`, which SQLite emits for\n * NOT NULL and FOREIGN KEY too. A predicate that says \"unique\" too often is a\n * worse bug than the one being fixed — a not-null violation answered as\n * `409 UNIQUE_VIOLATION` tells the client to change a value that is not the\n * problem, and 409 is a status an SDK will not retry.\n *\n * ## Why a VIOLATION phrasing, not the word pair (#8590)\n *\n * The limb used to be a bare `unique constraint`, and a word pair is not a\n * condition: databases put those two words in sentences that say a unique\n * constraint is **ABSENT** just as readily as in ones that say a row broke it.\n * Both spellings below were raised on real servers — SQLite via\n * better-sqlite3, PostgreSQL 16.13 via `pg` 8.22.0, both through knex 3.3.0 —\n * and the bare limb answered `true` to every one of them:\n *\n * ```\n * # SQLITE — no unique index backs the ON CONFLICT target (#8445, #8590)\n * ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint\n *\n * # POSTGRES 42830 — a FOREIGN KEY referencing a non-unique column\n * there is no unique constraint matching given keys for referenced table \"t\"\n * ```\n *\n * The Postgres sentence is why this is an allowlist of violation phrasings and\n * not a negative lookahead on SQLite's sentence. #8590 was filed believing the\n * collision was SQLite-only and that Postgres escaped \"by luck of word order\";\n * measuring the dialects for the fix found 42830, where Postgres puts the same\n * two words adjacent in its own absence sentence. A lookahead keyed on the\n * SQLite wording answers `true` there — it is a blocklist, and it can only ever\n * enumerate the absence sentences somebody already tripped over. Requiring a\n * violation phrasing inverts the default to match this module's stated one:\n * **unrecognised is `false`**, so a sentence nobody has measured is not a\n * conflict until a limb says it is.\n *\n * ⚠️ The three supported dialect families are exactly sqlite / postgres / mysql\n * (`sql-driver.ts` recognises no others), and all three were measured on live\n * servers for #8590, in both directions, including MySQL's `Duplicate entry`\n * path. A dialect added later needs its violation spelling added HERE, measured\n * off a thrown error — not a loosened limb.\n *\n * ## Why an in-process driver's refusal had to be recognised here (#13197)\n *\n * A driver that raises a conflict this predicate does not recognise is not\n * merely \"less well mapped\" — it WEDGES the engine's autonumber resync.\n * `ObjectQL.createWithAutonumberResync` drops the stale counter, re-seeds from\n * the store and re-issues only when `isUniqueViolationError` says the rejection\n * was a conflict; when it says no, the error propagates with the counter still\n * warm, so the next insert collides too, one number at a time — #5495's PROBE3\n * storm, which that branch exists to eliminate. Before #13197 `driver-memory`\n * enforced no uniqueness at all and the question never arose; the moment it\n * refuses a duplicate, an unrecognised refusal would trade a silent duplicate\n * for a non-converging insert loop. Recognising the platform's own code is what\n * keeps the trade honest, and it is why the limb belongs on the `codes` channel\n * rather than in prose the driver would have to imitate a dialect to emit.\n */\nconst UNIQUE_VIOLATION: UniqueViolationSignature = {\n codes: new Set(['23505', 'ER_DUP_ENTRY', 'SQLITE_CONSTRAINT_UNIQUE', 'UNIQUE_VIOLATION']),\n errnos: new Set([1062]),\n message: /unique constraint failed|violates unique constraint|unique violation|duplicate key|duplicate entry/i,\n};\n\n/** How far to follow an `error.cause` chain — drivers wrap, but not deeply. */\nconst MAX_CAUSE_DEPTH = 4;\n\n/**\n * Whether a thrown driver error is a unique/primary-key constraint violation.\n *\n * Reads all three channels in turn — `code`, `errno`, `message` — then one step\n * down the `cause` chain, because pool and query-builder layers re-throw with\n * the original attached. A plain string is judged on the message channel, so a\n * caller that has already unwrapped `err.message` can pass it straight in.\n *\n * **Unrecognised is always `false`.** The default has to be \"not a conflict\":\n * a false positive relabels an unrelated failure as the client's fault (a 409\n * an SDK will not retry, pointing at a value that is fine), while a false\n * negative costs only the generic envelope that was the status quo.\n *\n * @param error - the thrown value, of any shape.\n *\n * @example\n * ```ts\n * catch (error) {\n * if (isUniqueViolationError(error)) return conflict(); // 409 UNIQUE_VIOLATION\n * throw error;\n * }\n * ```\n */\nexport function isUniqueViolationError(error: unknown): boolean {\n return matchesUniqueViolation(error, 0);\n}\n\nfunction matchesUniqueViolation(error: unknown, depth: number): boolean {\n if (error === null || error === undefined || depth > MAX_CAUSE_DEPTH) return false;\n\n if (typeof error === 'string') return UNIQUE_VIOLATION.message.test(error);\n if (typeof error !== 'object') return false;\n\n const err = error as { code?: unknown; errno?: unknown; message?: unknown; cause?: unknown };\n\n if (typeof err.code === 'string' && UNIQUE_VIOLATION.codes.has(err.code)) return true;\n // Postgres drivers hand SQLSTATE back as a string; a numeric `code` is\n // MySQL's errno wearing the other field's name, so it is judged as one.\n if (typeof err.code === 'number' && UNIQUE_VIOLATION.errnos.has(err.code)) return true;\n if (typeof err.errno === 'number' && UNIQUE_VIOLATION.errnos.has(err.errno)) return true;\n if (typeof err.message === 'string' && UNIQUE_VIOLATION.message.test(err.message)) return true;\n\n return matchesUniqueViolation(err.cause, depth + 1);\n}\n\n/* ------------------------------------------------------------------------- *\n * #6544 — which column conflicted\n * ------------------------------------------------------------------------- */\n\n/**\n * SQLite names the offending **columns** directly, as `table.column` pairs:\n * `UNIQUE constraint failed: sys_user.email`. Captured to end-of-line because\n * knex prefixes the failing statement, so the useful part is always the tail.\n */\nconst SQLITE_TARGETS = /unique constraint failed:\\s*([^\\n]*)/i;\n\n/**\n * Postgres names the offending **columns** only in its `DETAIL:` line —\n * `Key (email)=(acme@example.com) already exists.` — which node-postgres puts\n * on `error.detail` and knex flattens into the message. The trailing `=(`\n * is required: it is what separates this form from the constraint-name form\n * (`violates unique constraint \"sys_user_email_key\"`), which names an INDEX.\n *\n * An expression index (`Key (lower(email))=(…)`) cannot match, because the\n * capture forbids `)` — which is the correct answer: `lower(email)` is not a\n * column.\n */\nconst POSTGRES_DETAIL_TARGETS = /\\bkey \\(([^)]+)\\)=\\(/i;\n\n/** SQLite's other spelling, for a partial or expression index: `UNIQUE constraint failed: index 'x'`. */\nconst SQLITE_INDEX_FORM = /^index\\b/i;\n\n/** What a column name may look like once the table qualifier and quoting are stripped. */\nconst PLAIN_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;\n\n/** Strip quoting and any `table.` qualifier from one constraint target. */\nfunction bareIdentifier(raw: string): string {\n const stripped = raw.trim().replace(/[`\"'[\\]]/g, '');\n const dot = stripped.lastIndexOf('.');\n return dot >= 0 ? stripped.slice(dot + 1) : stripped;\n}\n\n/**\n * Reduce one dialect's list of constraint targets to THE conflicting column,\n * or `undefined` when there is not exactly one that is determinably a column.\n *\n * A composite key resolves to `undefined` on purpose: there is no single\n * offending column, and picking the first is the same class of wrong answer as\n * returning an index name — it points a form at `tenant_id` when what the user\n * typed twice was `email`.\n */\nfunction soleColumn(targets: string): string | undefined {\n const names = targets.split(',').map(bareIdentifier);\n if (names.length !== 1) return undefined;\n const [name] = names;\n return PLAIN_IDENTIFIER.test(name) ? name : undefined;\n}\n\nfunction columnFromText(text: string): string | undefined {\n const sqlite = SQLITE_TARGETS.exec(text);\n if (sqlite) {\n const targets = sqlite[1].trim();\n // `index 'idx_email_unique'` is an index name, not a column. Refuse.\n return SQLITE_INDEX_FORM.test(targets) ? undefined : soleColumn(targets);\n }\n\n const postgres = POSTGRES_DETAIL_TARGETS.exec(text);\n if (postgres) return soleColumn(postgres[1]);\n\n // MySQL deliberately has no limb here — see the doc comment on\n // `uniqueViolationColumn`. `Duplicate entry 'x' for key 'i'` names `i`,\n // which is an INDEX, and this function does not guess columns from indexes.\n return undefined;\n}\n\nfunction findUniqueViolationColumn(error: unknown, depth: number): string | undefined {\n if (error === null || error === undefined || depth > MAX_CAUSE_DEPTH) return undefined;\n\n if (typeof error === 'string') return columnFromText(error);\n if (typeof error !== 'object') return undefined;\n\n const err = error as { message?: unknown; detail?: unknown; cause?: unknown };\n\n if (typeof err.message === 'string') {\n const fromMessage = columnFromText(err.message);\n if (fromMessage !== undefined) return fromMessage;\n }\n // node-postgres keeps the `DETAIL:` line off the message and on its own\n // field, so for the driver we actually ship this is where the column is.\n if (typeof err.detail === 'string') {\n const fromDetail = columnFromText(err.detail);\n if (fromDetail !== undefined) return fromDetail;\n }\n\n return findUniqueViolationColumn(err.cause, depth + 1);\n}\n\n/**\n * Which column a unique-constraint violation was raised on — or `undefined`\n * when the dialect did not determinably name one (#6544).\n *\n * ## The contract, and why it is this narrow\n *\n * **A value comes back only when the identifier the driver printed is\n * determinably a COLUMN.** When a dialect names an *index* instead — MySQL's\n * `Duplicate entry 'a@b.com' for key 'idx_email_unique'`, Postgres'\n * `violates unique constraint \"sys_user_email_key\"`, SQLite's\n * `UNIQUE constraint failed: index 'idx_lower_email'` — the answer is\n * `undefined`, never the index name.\n *\n * That is the maintainer's 2026-08-08 ruling on #6544, and the reasoning is the\n * caller's, not this module's: **an index name mistaken for a column is worse\n * than no answer at all.**\n *\n * - `@objectstack/rest`'s import runner renders this into a form field —\n * \"A record with this `email` already exists.\" An index name there points\n * the user at a field that does not exist on the object, so they cannot act\n * on it; `undefined` degrades to generic copy, which is merely less helpful.\n * - #5495's autonumber-retry branch asks a yes/no question of the answer —\n * \"is the conflicting column the autonumber field?\" — and an index name\n * produces a *wrong retry decision*, not a vaguer one.\n *\n * ⛔ **The accepted cost: MySQL deployments usually get no column.** MySQL's\n * duplicate-entry message names the index and never the column, so there is\n * nothing here to read. That is deliberate. Do not \"improve\" this by deriving a\n * column from an index name (`idx_email_unique` → `email`, or MySQL 8's\n * `for key 'sys_user.email'` → `email`): index names are free-form, a\n * deployment's may match no column at all, and a plausible-looking wrong field\n * is exactly the failure this export exists to avoid. If MySQL must name\n * columns, the answer is a schema lookup of the index — a different, wider\n * contract — not a guess in this function.\n *\n * A **composite** key is `undefined` for the same reason: `Key (tenant_id,\n * email)=(…)` has no single offending column, and naming the first is the same\n * class of wrong answer.\n *\n * ## What it reads\n *\n * Gated on {@link isUniqueViolationError}, so a NOT NULL or FOREIGN KEY failure\n * can never reach the extraction — SQLite's `NOT NULL constraint failed: t.c`\n * shares its shape with the positive and is refused at the gate, not by the\n * patterns. Then `message`, then `detail` (node-postgres keeps its `DETAIL:`\n * line there), then one step down the `cause` chain, bounded exactly as the\n * predicate's walk is. A bare string is read as a message, so a caller holding\n * only `err.message` can pass it straight in.\n *\n * @param error - the thrown value, of any shape.\n * @returns the conflicting column, or `undefined` when none is determinable.\n *\n * @example\n * ```ts\n * const column = uniqueViolationColumn(error);\n * return column\n * ? `A record with this ${column} already exists.`\n * : 'A record with this value already exists.';\n * ```\n */\nexport function uniqueViolationColumn(error: unknown): string | undefined {\n if (!isUniqueViolationError(error)) return undefined;\n return findUniqueViolationColumn(error, 0);\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Driver-error classification: \"which driver failures may be silenced?\"\n * (#4728, #4825; rule from #4632).\n *\n * ## Home — `@objectstack/types`, since #13279\n *\n * This module was born in `@objectstack/metadata` and lived there through\n * #4728 / #4825 / #5841. `@objectstack/metadata/errors`' own docblock recorded\n * the move now made as **option 2** — \"sink it into a common dependency\n * (`@objectstack/types`) … architecturally attractive and explicitly *not*\n * precluded by this module — but out of scope on the round that needed it\" —\n * and kept its export as \"a single, greppable seam to delete if the maintainer\n * later takes option 2\". The maintainer took option 2 on 2026-08-30.\n *\n * What forced it: `resolveAuthzContext` (`@objectstack/core`) must ask\n * {@link isMissingTableError} to tell a permission-store OUTAGE from a\n * deployment whose `sys_*` tables were never provisioned (#13279). Core cannot\n * import `@objectstack/metadata` — metadata **depends on** core — so the\n * predicate had to move to a package both sides already depend on, or be\n * copied. Copying was measured and rejected: two vocabularies of \"which driver\n * errors are benign\", one of them on a security path, is the exact\n * duplication-drift this module was built to retire.\n *\n * `@objectstack/types` is the repo's stated Home rule for a cross-package error\n * predicate — see `unique-violation.ts`: \"because every consumer of the\n * question already depends on it, so adopting the predicate never adds an\n * edge\", which cites *this* predicate's #5841 move as its own precedent. The\n * edge was already there in the other direction too: the front-exclusion below\n * has read {@link isRelationSubObjectPhrase} from this package since #6615, so\n * the move puts the phrase and the predicate that excludes on it in one place.\n *\n * `@objectstack/metadata/errors` — the published subpath — remains, and now\n * re-exports from here, so no out-of-repo consumer changed. The package's\n * INTERNAL `utils/schema-sync-errors.ts` is gone rather than left as a\n * forwarding stub: it carried no promise to anyone, and a file that exists only\n * to forward is the thing that rots. Its two in-package readers\n * (`errors.ts`, `loaders/database-loader.ts`) import this module directly.\n *\n * ## Both predicates moved, not one\n *\n * The ruling names {@link isMissingTableError}. Its sibling\n * {@link isSchemaAlreadyExistsError} came with it because they are **not two\n * modules** — they are two signatures over one {@link matchesDriverError}, and\n * that sharing is the point (see the paragraph below). Leaving the sibling\n * behind would have meant either exporting the matcher as machinery or\n * re-rolling it in `metadata`, and the second is the duplication this module\n * exists to prevent. `@objectstack/types` therefore publishes both; the\n * \"exported symbol nobody imports\" objection recorded in\n * `@objectstack/metadata/errors` does not apply, because after the move\n * `metadata`'s `DatabaseLoader` **is** an outside consumer of it.\n *\n * Two questions live here, and they share one mechanism on purpose. A second\n * hand-rolled `catch`-and-guess elsewhere in this package would be a second\n * de-facto vocabulary of \"which driver errors are benign\" — the exact debt this\n * module exists to retire. Both predicates below are thin wrappers over one\n * signature matcher, so a driver quirk is taught to the package once.\n *\n * 1. {@link isSchemaAlreadyExistsError} — \"was this DDL failure just the table\n * already being there?\" (#4728, `ensureSchema` / `ensureHistorySchema`).\n * 2. {@link isMissingTableError} — \"did this READ fail because the table has\n * not been provisioned yet?\" (#4825, `nextEventSeq`).\n *\n * They are deliberately **not** each other's negation. Each answers \"is this\n * the one benign reason?\" and defaults to *not benign*, so an error neither\n * recognises is loud under both.\n *\n * ---\n *\n * ## 1. DDL failure classification (#4728)\n *\n * `IDataDriver.syncSchema()` is contractually **idempotent** (\"creates tables if\n * missing, adds columns, updates indexes\"), so in principle a re-sync of an\n * existing table should not throw at all. In practice a driver may surface the\n * already-provisioned case as an error instead of a no-op — `CREATE TABLE`\n * without `IF NOT EXISTS`, an `ALTER TABLE ADD COLUMN` for a column that is\n * already there. That single failure reason is benign: the table and its columns\n * exist, so the bytes will land.\n *\n * **Every other** DDL failure is not benign, and the difference is the whole\n * point of this module. Insufficient privileges, a datasource that never\n * connected, an incompatible column type — after those, the table or column does\n * not exist, yet the process keeps looking healthy while everything it claims to\n * persist has nowhere to land. That is the #4420 shape, and AGENTS.md →\n * \"Degradation log levels\" requires it to be reported at `error`.\n *\n * The defect this replaces was a `catch` whose comment named the benign reason\n * (\"e.g. table already exists\") and used it to excuse **all** of them. Callers\n * must therefore ask the question by error *type*:\n *\n * ```ts\n * catch (error) {\n * if (!isSchemaAlreadyExistsError(error)) {\n * console.error('… consequence … fix …', error); // loud, and stay not-ready\n * return;\n * }\n * // benign only: the table is already provisioned, carry on\n * }\n * ```\n *\n * Classification is deliberately conservative — anything not positively\n * recognised as \"already exists\" is treated as a real failure, because the cost\n * of a false \"benign\" (silent data loss) is far higher than the cost of a false\n * \"real\" (one extra error line).\n *\n * ---\n *\n * ## 2. Missing-table classification for reads (#4825)\n *\n * `DatabaseLoader.nextEventSeq()` reads `sys_metadata_history` to decide what\n * `event_seq` the NEXT history row gets. Its `catch` named both reasons a read\n * can fail — \"table not provisioned yet\" (benign: 1 really is the next number)\n * and \"driver error\" (**not** benign) — and answered both with `return 1`.\n *\n * That is the #4728 shape one layer down, but the damage is the opposite kind\n * and worse. #4728 was *bytes that never landed*; this is **bytes that land\n * wrong**: with N rows already in the table, one flaky read hands the next row\n * `event_seq = 1`, colliding with an existing row. The insert **succeeds**, no\n * line is logged, and `event_seq` — the ordering key that history listing and\n * rollback targeting both stand on — is now silently untrustworthy.\n *\n * So the read seam gets the same treatment, with the same conservative default:\n *\n * ```ts\n * catch (error) {\n * if (isMissingTableError(error)) return 1; // benign: nothing to collide with\n * throw error; // caller reports the consequence\n * }\n * ```\n */\n\n// [#6615] The Postgres `\"x\" of relation \"y\"` phrase, owned once — see\n// `relation-sub-object.ts` next door for the superstring hole it closes and for\n// why the exclusion's width deliberately differs from the extractor's. That\n// module was already this one's dependency across the package boundary; since\n// #13279 moved this file into `@objectstack/types`, the two are siblings.\nimport { isRelationSubObjectPhrase } from './relation-sub-object.js';\n\n/**\n * The relation name each missing-table phrase puts on display, one capture per\n * dialect spelling in {@link MISSING_TABLE.message}.\n *\n * Extraction is deliberately partial. A phrase whose name cannot be read back\n * out — `unknown table` with nothing quoted after it, a bare SQLSTATE, an\n * errno — yields nothing, and yielding nothing must stay *silent* rather than\n * become evidence: see {@link phraseNamesAnotherRelation}.\n */\nconst RELATION_IN_PHRASE: readonly RegExp[] = [\n // SQLite / libsql: `no such table: sys_metadata_history`, and the\n // schema-qualified `no such table: main.orders` it uses when it resolved\n // the name itself (views, triggers) or the caller qualified it.\n /no such table:\\s*([^\\s'\"`;,()]+)/i,\n // PostgreSQL: `relation \"sys_metadata_history\" does not exist`\n /relation\\s+[\"'`]([^\"'`]+)[\"'`]\\s+does not exist/i,\n // MySQL / MariaDB: `Table 'app.sys_metadata_history' doesn't exist`\n /table\\s+[\"'`]([^\"'`]+)[\"'`]\\s+doesn'?t exist/i,\n // MySQL / MariaDB: `Unknown table 'app.t'`\n /unknown table\\s+[\"'`]([^\"'`]+)[\"'`]/i,\n];\n\n/**\n * Reduce a relation name to the part two dialects can be expected to agree on.\n *\n * Drops a leading qualifier (`main.orders`, `app.orders` — SQLite's schema,\n * MySQL's database) and the legacy `{namespace}__{shortName}` prefix that\n * `StorageNameMapping.resolveTableName` strips to get from an object name to\n * its physical table, then case-folds.\n *\n * Every step here makes the comparison MORE likely to match, and that\n * direction is chosen on purpose: a match keeps today's benign verdict, so an\n * over-eager normaliser can only ever leave the gap open, while an over-strict\n * one would manufacture a loud verdict for a genuine missing table — the one\n * regression this repair is not allowed to cause.\n */\nfunction normaliseRelationName(name: string): string {\n const afterQualifier = name.slice(name.lastIndexOf('.') + 1);\n const namespaceEnd = afterQualifier.lastIndexOf('__');\n const bare = namespaceEnd === -1 ? afterQualifier : afterQualifier.slice(namespaceEnd + 2);\n return bare.toLowerCase();\n}\n\n/**\n * Does this message name a relation OTHER than the one the caller read?\n *\n * The gap this closes: the message test recognises the *shape* of \"no such\n * table\" and never asks WHICH table. A view over a dropped base table answers\n * the shape perfectly — measured on libsql, reading a view named `sys_metadata`\n * whose base table is gone raises `no such table: main.<base>` — so a read of a\n * relation that very much exists was classified as \"not provisioned yet\", and\n * every fail-soft consumer took the empty answer as the truth. That is a false\n * *benign*, the direction the module docblock calls far more expensive than a\n * false \"real\".\n *\n * ⛔ Not answerable from the phrase's shape alone, and that is why this channel\n * takes the read's name rather than a regex. Measured on libsql, a view over a\n * missing base table and a genuine missing table the caller happened to qualify\n * produce byte-identical spellings (`no such table: main.absent_base` vs\n * `no such table: main.orders`); the only thing that separates them is whether\n * the name in the phrase is the name that was asked for.\n *\n * Conservative in the direction the rest of the module already errs in — it\n * can only ever SUBTRACT benign verdicts, never add one:\n * - no name extractable, or no name supplied -> `false` (stay as we were)\n * - any extracted name matches the read -> `false` (a true positive)\n * - names found, none of them the read's -> `true` (the phrase is about\n * something else; be loud)\n */\nfunction phraseNamesAnotherRelation(message: string, readObject: string): boolean {\n const expected = normaliseRelationName(readObject);\n if (expected === '') return false;\n\n let named = false;\n for (const pattern of RELATION_IN_PHRASE) {\n const captured = pattern.exec(message)?.[1];\n if (captured === undefined) continue;\n const candidate = normaliseRelationName(captured);\n if (candidate === '') continue;\n // One agreeing name is enough to keep the benign verdict.\n if (candidate === expected) return false;\n named = true;\n }\n return named;\n}\n\n/** One \"which errors mean X?\" vocabulary, in the three forms drivers use. */\ninterface DriverErrorSignature {\n /** `error.code` — Postgres SQLSTATE, or mysql2's symbolic name. */\n readonly codes: ReadonlySet<string>;\n /** `error.errno` — MySQL/MariaDB numeric equivalents. */\n readonly errnos: ReadonlySet<number>;\n /** `error.message` — the only signal SQLite-family drivers give. */\n readonly message: RegExp;\n /**\n * Optional **front-exclusion**, evaluated before any positive test (#6347).\n *\n * A message test can never exclude a *superstring*: once a legal phrase for\n * X appears inside a longer phrase that means NOT-X, no amount of widening\n * the X regex removes the match — the phrase really is in there. The only\n * repair is to recognise the not-X shape first and stop. So this is a\n * separate channel rather than another alternation in {@link message}.\n */\n readonly excludes?: {\n /** SQLSTATEs / driver codes that positively mean \"**not** this case\". */\n readonly codes: ReadonlySet<string>;\n /**\n * Message shapes whose named relation is not the one that was READ.\n *\n * The third exclusion channel, and the only one that needs a fact from\n * the caller: the message alone cannot say whether the relation it\n * names is the one the caller asked for. Given the read's own object\n * name, it answers \"this phrase is about something else\" — which is\n * the same not-X-first move {@link matchesMessage} makes, one step out.\n *\n * Absent (or given no object name) it never fires, so a signature that\n * does not carry it, and a caller that cannot name what it read, both\n * keep the pure-shape behaviour.\n */\n readonly namesAnotherRelation?: (message: string, readObject: string) => boolean;\n /**\n * Message shapes that carry a legal match for this case as a substring.\n *\n * A predicate rather than a `RegExp` since #6615, so this channel can be\n * satisfied by a shared, named question from `@objectstack/types` instead\n * of a pattern this file owns alone. The phrase it tests is the same one\n * `@objectstack/rest` and `@objectstack/service-analytics` read.\n */\n readonly matchesMessage: (message: string) => boolean;\n };\n}\n\n/**\n * Driver/SQLSTATE codes that mean \"the thing you asked me to create is already\n * there\". Postgres reports SQLSTATE on `code`; mysql2 reports its symbolic name.\n */\nconst ALREADY_EXISTS: DriverErrorSignature = {\n codes: new Set([\n // PostgreSQL SQLSTATE (class 42 — syntax error or access rule violation)\n '42P07', // duplicate_table\n '42701', // duplicate_column\n '42710', // duplicate_object — index / constraint already exists\n // MySQL / MariaDB (mysql2 puts the symbolic name on `code`)\n 'ER_TABLE_EXISTS_ERROR', // 1050\n 'ER_DUP_FIELDNAME', // 1060\n 'ER_DUP_KEYNAME', // 1061\n ]),\n errnos: new Set([1050, 1060, 1061]),\n /**\n * Message fallback for drivers that carry no machine-readable code —\n * notably SQLite, whose `code` is the undifferentiated `SQLITE_ERROR` for\n * every DDL failure, so the message is the only signal available:\n * - `table sys_metadata already exists`\n * - `duplicate column name: environment_id`\n * - `index idx_x already exists`\n * Postgres phrases its own as `relation \"x\" already exists` /\n * `column \"x\" of relation \"y\" already exists`, which matches the same test.\n */\n message: /already exists|duplicate column name|duplicate key name/i,\n};\n\n/**\n * Codes/messages that mean \"the table you tried to READ has not been created\".\n *\n * Narrower than it looks, on purpose. `does not exist` on its own also covers\n * `role \"x\" does not exist` (42704), `database \"x\" does not exist` (3D000) and\n * `column \"x\" does not exist` (42703) — every one of them a **real** failure\n * that must stay loud, and every one of them a case where \"start numbering at\n * 1\" would be the wrong answer against a table that may be full of rows. So the\n * message test demands the word table/relation next to the phrase rather than\n * the phrase alone, and the code set carries only the table-scoped SQLSTATEs.\n *\n * That was not enough on its own, and #6347 is why. Postgres has **two**\n * missing-column phrasings, one per direction:\n *\n * | path | phrase | SQLSTATE | matched the message test? |\n * |:---|:---|:---|:---|\n * | read (`SELECT`) | `column \"bogus\" does not exist` | 42703 | no |\n * | write (`INSERT`/`UPDATE`/`ALTER`) | `column \"label\" of relation \"sys_team\" does not exist` | 42703 | **yes** |\n *\n * The write-path phrase contains a complete, legal missing-table phrase —\n * `relation \"sys_team\" does not exist` — as a substring, so the table-scoped\n * test above matched it and answered *benign* about an error the docblock two\n * paragraphs up already named as one that must stay loud. The same holds for\n * every other sub-object of a relation Postgres phrases this way, e.g.\n * `constraint \"uq_x\" of relation \"sys_team\" does not exist` (42704). And\n * code-first does not rescue it: {@link matchesDriverError} is a sequential OR,\n * so a `code: '42703'` error simply falls past the two code lines and is\n * decided by the message.\n *\n * Hence {@link DriverErrorSignature.excludes}: the not-a-table shapes are\n * recognised FIRST, and recognition ends the question with `false`.\n */\nconst MISSING_TABLE: DriverErrorSignature = {\n codes: new Set([\n '42P01', // PostgreSQL undefined_table\n 'ER_NO_SUCH_TABLE', // MySQL / MariaDB 1146\n ]),\n errnos: new Set([1146]),\n /**\n * - SQLite / libsql: `no such table: sys_metadata_history`\n * - PostgreSQL: `relation \"sys_metadata_history\" does not exist`\n * - MySQL/MariaDB: `Table 'app.sys_metadata_history' doesn't exist`\n */\n message:\n /no such table|relation [\"'`][^\"'`]+[\"'`] does not exist|table [\"'`][^\"'`]+[\"'`] doesn'?t exist|unknown table/i,\n excludes: {\n /**\n * Exactly the three SQLSTATEs the docblock above already names as\n * must-stay-loud neighbours of `does not exist`. They are listed here\n * rather than merely trusted to miss the message test, because two of\n * them (42703 columns, 42704 constraints/triggers) have a phrasing that\n * *does* hit it, and because a code is a fact where prose is a guess.\n *\n * Postgres-shaped on purpose: measured, neither MySQL\n * (`Unknown column 'label' in 'field list'`) nor SQLite\n * (`no such column: bogus`, `table t has no column named label`)\n * phrases a sub-object failure so that a missing-table phrase falls out\n * of it, so there is nothing there to exclude. Adding their codes would\n * be surface with no defect behind it.\n */\n codes: new Set([\n '42703', // undefined_column\n '42704', // undefined_object — constraint, trigger, role, type, …\n '3D000', // invalid_catalog_name — `database \"x\" does not exist`\n ]),\n /**\n * `«sub-object» \"x\" of relation \"y\" …` — Postgres' phrasing for a\n * failure about something *inside* a relation, which therefore says the\n * relation itself is present. The two in-repo siblings that carry this\n * phrase are `mapDataError` (`packages/rest`, #5352) and\n * `service-analytics`'s missing-column subtraction (#6035/PR #6346).\n *\n * [#6615] All three now read one home — `@objectstack/types` — instead\n * of three hand-kept copies, so the phrase can no longer be taught to\n * the repo a fourth time or drift in one package only. [#13279] This\n * file now lives in that same home, so the read is a sibling import. The **width**\n * difference that used to justify the copy is preserved and is the\n * reason the home exports two functions rather than one: those two\n * *extract* the column name to phrase a better error, so a miss costs a\n * vaguer message; this one *excludes*, so a miss restores the\n * corruption. {@link isRelationSubObjectPhrase} is therefore the wider\n * question — it drops their `column`/`[a-z0-9_]+`/`does not exist`\n * anchors: any sub-object, any quoted identifier, any verdict.\n * Over-matching here only ever converts a benign verdict into a loud\n * one, which is the direction this whole module already errs in.\n */\n matchesMessage: isRelationSubObjectPhrase,\n /**\n * [#13324] \"…and the relation it names is not the one you read.\"\n *\n * The sibling of the phrase above, reached one step further out. That\n * one recognises a failure about something INSIDE a relation, which\n * therefore says the relation is present; this one recognises a failure\n * about a DIFFERENT relation, which says nothing at all about the one\n * the caller read. Both end the question with `false` for the same\n * reason: the licence this predicate grants — \"there are no rows, so\n * there is nothing to be inconsistent with\" — is about the table that\n * was READ, and neither phrase is evidence about it.\n */\n namesAnotherRelation: phraseNamesAnotherRelation,\n },\n};\n\n/** How far to follow an `error.cause` chain — drivers wrap, but not deeply. */\nconst MAX_CAUSE_DEPTH = 4;\n\n/**\n * [#13438] The physical table a driver's statement TARGETED, declared on the\n * error envelope by the producer that knows it.\n *\n * `readObject` closed the #13324 hole for callers that can name what they read\n * — and left a residual one layer down. A caller names its OBJECT (the API\n * name); a driver compiles the statement against the PHYSICAL table, and for a\n * federated object (ADR-0015, `external.remoteName`) those are two different\n * names. `driver-sql` reads `crm_order` from `legacy_orders`, so when that\n * remote is genuinely absent the dialect phrase names `legacy_orders`, the\n * caller names `crm_order`, and the comparison called a real missing table\n * \"about something else\" — loud, for the one case the licence was built for.\n *\n * Nothing at a call site can fold that away: the mapping lives on the driver\n * instance, and asking every caller to consult it is the guessing this channel\n * exists to remove (maintainer ruling 2026-09-01, option 2 on the card). So the\n * fact is declared where it is known — the driver that composed the envelope\n * stamps the table its statement targeted onto it — and the predicate PREFERS\n * a declared table over the caller-supplied `readObject`. The caller never\n * needs to know a federated object's remote name, and a driver that declares\n * nothing gets exactly the #13324 behaviour.\n *\n * A symbol key from the global registry, held non-enumerable: the carrier\n * discipline `driver-sql` already applies to its withheld-diagnostic symbols\n * and to the envelope's own `cause`. Readable by code; invisible to\n * `JSON.stringify`, `{ ...err }`, `Object.keys`, `for…in` and the\n * structured-clone boundary — so the physical table name, the very thing the\n * envelope's composed message withholds, can never ride back onto a wire that\n * serialises the error. `Symbol.for` so a duplicated copy of this package\n * resolves the same key.\n *\n * ⚠️ A declaration is EVIDENCE, so it also narrows the one-argument form: an\n * envelope declaring `legacy_orders` whose dialect phrase names some other\n * relation reads not-benign even with no `readObject` — the driver supplied\n * the fact the caller could not. That is the #13324 verdict reached without\n * the caller's help, in the direction the module docblock calls cheap.\n */\nexport const DRIVER_TARGETED_TABLE: symbol = Symbol.for('objectstack.driver.targetedTable');\n\n/**\n * Declare, on `error`, the physical table the statement that raised it targeted.\n *\n * The producer's half of {@link DRIVER_TARGETED_TABLE} — for a driver composing\n * an error envelope over a dialect failure. `table` is the name the statement\n * was compiled against (a federated object's `external.remoteName`, otherwise\n * the object's own table), bare: the comparison folds away schema and database\n * qualifiers on both sides, so none is needed here.\n *\n * Non-enumerable and non-writable, and the FIRST declaration wins: the actor\n * that compiled the statement is the one that knows its target, and a later,\n * more distant wrapper re-declaring it would be re-introducing the guess. (The\n * predicate applies the same rule across a `cause` chain: the declaration\n * NEAREST the dialect phrase is the one compared.) An empty or non-string\n * `table` declares nothing — silently, because this runs on an error path\n * where a thrown `TypeError` would replace the envelope it was meant to\n * annotate; the predicate then falls back to `readObject` exactly as if no\n * driver had spoken.\n *\n * @returns `error`, for chaining.\n */\nexport function declareTargetedTable<E extends object>(error: E, table: string): E {\n if (typeof table !== 'string' || table === '') return error;\n if (targetedTableOf(error) !== null) return error;\n Object.defineProperty(error, DRIVER_TARGETED_TABLE, { value: table, enumerable: false });\n return error;\n}\n\n/**\n * The table `error` declares its statement targeted, or `null` when it declares\n * none — the reading half of {@link declareTargetedTable}. Tolerant of bare\n * input: any non-object, and any object without a non-empty string under the\n * key, is \"no declaration\".\n */\nexport function targetedTableOf(error: unknown): string | null {\n if (error === null || (typeof error !== 'object' && typeof error !== 'function')) return null;\n const table = (error as Record<symbol, unknown>)[DRIVER_TARGETED_TABLE];\n return typeof table === 'string' && table !== '' ? table : null;\n}\n\n/**\n * The {@link DriverErrorSignature.excludes.namesAnotherRelation} channel, in the\n * one place both the string and the object node reach it.\n *\n * `relation` is the name the phrase must be about: the table the node (or an\n * outer node) DECLARED its statement targeted (#13438), else the caller's\n * `readObject`. Guards the caller-supplied half rather than trusting it: the\n * parameter is optional on the public predicate, so `undefined` (a caller that\n * cannot name what it read) and a non-string (a stale positional `depth`\n * argument from before this parameter existed) must both mean \"no evidence\",\n * never \"loud\".\n */\nfunction excludedByReadObject(\n message: string,\n signature: DriverErrorSignature,\n relation: string | undefined,\n): boolean {\n if (typeof relation !== 'string' || relation === '') return false;\n return signature.excludes?.namesAnotherRelation?.(message, relation) === true;\n}\n\n/**\n * The single matcher both predicates run on: exclusions, then code, then errno,\n * then message, then one step down the `cause` chain.\n *\n * Unrecognised is always `false` — a benign verdict must be *earned*, never\n * defaulted to, because a false \"benign\" corrupts data while a false \"real\"\n * costs one error line.\n *\n * The exclusion runs at every node and, when it fires, returns `false` **without\n * descending into `cause`** (#6347). Two reasons, both the conservative\n * direction: an error that positively identifies as \"a column of an existing\n * relation\" *is* that error, whatever it wraps; and stopping can only ever\n * subtract benign verdicts, never add one.\n *\n * `readObject` is the relation the phrase is compared against at this node —\n * the caller's own name at the top of the chain, or, once a node has DECLARED\n * the table its statement targeted (#13438), that declaration for the node and\n * everything it wraps. A string node cannot declare anything and compares\n * against what it inherited.\n */\nfunction matchesDriverError(\n error: unknown,\n signature: DriverErrorSignature,\n depth: number,\n readObject?: string,\n): boolean {\n if (error === null || error === undefined || depth > MAX_CAUSE_DEPTH) return false;\n\n if (typeof error === 'string') {\n if (signature.excludes?.matchesMessage(error)) return false;\n if (excludedByReadObject(error, signature, readObject)) return false;\n return signature.message.test(error);\n }\n if (typeof error !== 'object') return false;\n\n const err = error as {\n code?: unknown;\n errno?: unknown;\n message?: unknown;\n cause?: unknown;\n };\n\n // [#13438] A declared target replaces the caller's name outright — from\n // this node down, the phrase is compared against what the driver compiled\n // the statement for. The NEAREST declaration to the phrase wins, because\n // that is the actor that knows.\n const relation = targetedTableOf(err) ?? readObject;\n\n const excludes = signature.excludes;\n if (excludes) {\n if (typeof err.code === 'string' && excludes.codes.has(err.code)) return false;\n if (typeof err.message === 'string' && excludes.matchesMessage(err.message)) return false;\n if (typeof err.message === 'string' && excludedByReadObject(err.message, signature, relation))\n return false;\n }\n\n if (typeof err.code === 'string' && signature.codes.has(err.code)) return true;\n if (typeof err.errno === 'number' && signature.errnos.has(err.errno)) return true;\n if (typeof err.message === 'string' && signature.message.test(err.message)) return true;\n\n // Drivers commonly re-throw with the original attached as `cause`.\n return matchesDriverError(err.cause, signature, depth + 1, relation);\n}\n\n/**\n * Is this DDL error the benign \"already provisioned\" case?\n *\n * @param error - The value thrown by `syncSchema()` (or any DDL call).\n * @param depth - Internal `cause`-chain recursion counter; callers pass nothing.\n * @returns `true` only when the error positively identifies as\n * table/column/index-already-exists. Anything else — including an\n * unrecognised error, `undefined`, or a permission/connection failure —\n * returns `false` and MUST be reported loudly by the caller.\n */\nexport function isSchemaAlreadyExistsError(error: unknown, depth = 0): boolean {\n return matchesDriverError(error, ALREADY_EXISTS, depth);\n}\n\n/**\n * Is this READ error the benign \"table has not been provisioned yet\" case?\n *\n * The only failure that licenses a caller to treat an empty table as the truth\n * — there are no rows, so there is nothing to be inconsistent with. A\n * connection drop, a timeout, a permission denial or a query error all mean the\n * rows may well exist and simply were not seen; those return `false` and the\n * caller must report the consequence and give up rather than compute an answer\n * from data it never read (#4825).\n *\n * A failure about a **column** of a relation is never this case, in either of\n * Postgres' two phrasings — the relation is right there in the message because\n * it exists (#6347). See {@link MISSING_TABLE}'s `excludes`.\n *\n * [#13324] Neither is a failure that names a **different relation**, and that\n * one cannot be seen without `readObject`. The message test asks what the\n * phrase LOOKS like and never which table it names, so a read of a view whose\n * base table has been dropped — `no such table: main.<base>`, measured on\n * libsql for a view that itself exists — answered benign for a relation that is\n * present and may be backed by rows. Naming the read closes it: the phrase must\n * be about the table the caller asked for, or it is not evidence about it.\n *\n * Pass `readObject` from every in-repo call site. It is **optional** so that\n * omitting it is exactly the pre-#13324 behaviour rather than a new loud\n * failure — this is a published export (`@objectstack/types`, and still\n * `@objectstack/metadata/errors` by re-export), and a required parameter would\n * be a breaking change to it. The cost of the choice\n * is that the narrowing is opt-in per call site: a new caller that forgets it\n * silently gets the old, wider verdict.\n *\n * [#13440] That last sentence is no longer only a warning. In-repo callers are\n * held to it by `driver-error-classification.callers.test.ts`, which walks every\n * TypeScript source under `packages/` and fails any call of this function that\n * omits `readObject` or passes it as `undefined`/`null`. The exemption is this\n * module's own contract tests, which exercise the one-argument PUBLISHED form on\n * purpose; read that file's header before adding to the exemption, because\n * widening it is how the enforcement becomes prose again. External consumers are\n * untouched: the signature below is unchanged, and the gate binds only callers\n * inside this repository.\n *\n * [#13438] `readObject` is the caller's name for what it read, and for a\n * federated object (ADR-0015) that is not the name the driver put in the\n * statement — `crm_order` reads `external.remoteName: 'legacy_orders'`, so a\n * genuinely absent remote raised a phrase naming `legacy_orders` against a\n * caller naming `crm_order`, and the #13324 comparison read it loud. A driver\n * that knows the table it targeted now DECLARES it on the envelope\n * ({@link declareTargetedTable}), and a declared table is preferred over\n * `readObject` outright: the phrase is compared against the declared name, and\n * the caller-supplied one is not consulted at that node or below it. Absent a\n * declaration the comparison is the #13324 one, unchanged. Two consequences,\n * both pinned: a genuinely absent federated remote reads benign again without\n * the caller learning the mapping; and — because a declaration is evidence the\n * caller did not have — an envelope whose phrase names a relation other than\n * its declared table reads NOT benign even through the one-argument form.\n *\n * @param error - The value thrown by a driver/engine read (`find`, `findOne`, …).\n * @param readObject - The object/table whose emptiness the caller is about to\n * treat as the truth — its own API name is fine, the comparison folds\n * away schema qualifiers, the legacy `ns__short` prefix and case.\n * Omitted (or not a string) means \"cannot say\", never \"be loud\".\n * Superseded, at any node of the `cause` chain that declares the\n * table its statement targeted, by that declaration (#13438).\n * @param depth - Internal `cause`-chain recursion counter; callers pass nothing.\n * @returns `true` only when the error positively identifies as\n * table/relation-does-not-exist **for the table that was read** —\n * the declared target where a driver supplied one, else `readObject`.\n */\nexport function isMissingTableError(error: unknown, readObject?: string, depth = 0): boolean {\n return matchesDriverError(error, MISSING_TABLE, depth, readObject);\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * The one named predicate for \"did the database refuse this `ON CONFLICT`\n * target because no PRIMARY KEY or UNIQUE index backs it?\" (#8567).\n *\n * ## ⚠️ This is NOT `isUniqueViolationError` — it is the OPPOSITE condition\n *\n * Read this before touching either predicate. They are one file apart and one\n * word apart in English, and they answer inverse questions:\n *\n * | predicate | the index | the row |\n * |:---|:---|:---|\n * | {@link isUniqueViolationError} | **exists** | violated it |\n * | `isUnbackedConflictTargetError` | **does not exist** | never got compared |\n *\n * Merging them — or reaching for whichever one autocomplete offers — reports a\n * *working* constraint as a missing one, which sends an operator to add an\n * index that is already there while the real duplicate goes unexplained. The\n * warning is repeated at both call sites and in `unique-violation.ts` because\n * it is the most expensive mistake available anywhere near this question.\n *\n * ⛔ **Nothing below may take a limb from that vocabulary, or give one to it.**\n * Unconditional, and permanent: the two predicates answer inverse questions, so\n * a limb that travels between them produces a confident inverted answer. This\n * prohibition was once written as holding \"while #8590 is open\", which was\n * wrong twice over — it reads as expiring, and #8590 has since closed.\n *\n * ⚠️ The separation **was** broken in the pre-existing direction, and pinning\n * it is what found that: `isUniqueViolationError` claimed SQLite's\n * unbacked-target error, because that sentence ends `…PRIMARY KEY or UNIQUE\n * constraint` and its vocabulary matched the word pair `unique constraint`\n * wherever it appeared — including inside a sentence saying the constraint is\n * ABSENT. #8567 filed that as #8590 and pinned it rather than fixing it, which\n * would have moved verdicts in six consuming packages on a card that measured a\n * different question. **#8590 has since closed it**: that predicate's message\n * limb now requires a VIOLATION phrasing — `unique constraint failed` (SQLite)\n * or `violates unique constraint` (Postgres) — so merely mentioning a unique\n * constraint no longer answers yes.\n *\n * ⚠️ Postgres was believed to escape that collision \"by luck of word order\",\n * its `unique or exclusion constraint` not being adjacent. #8590's dialect\n * sweep disproved it: PG **42830**, `there is no unique constraint matching\n * given keys for referenced table \"t\"` — a FOREIGN KEY referencing a non-unique\n * column — puts the pair adjacent in Postgres' own ABSENCE sentence. Both\n * dialects had the collision; only SQLite's instance sat on the path this file\n * measures. That is why the fix is an allowlist of violation phrasings and not\n * a negative lookahead on SQLite's sentence, which would still answer `true`\n * there.\n *\n * `unbacked-conflict-target.test.ts` records both predicates' verdicts on every\n * measured text, per dialect, and `unique-violation-absence-sentences.test.ts`\n * pins the absence sentences on both sides — so neither a fix nor a fresh drift\n * can land silently in either direction.\n *\n * ## What each dialect actually says — measured, never transcribed\n *\n * #8445 landed this recognition for SQLite alone and said so: the container\n * that implemented it had no other server, and transcribing another dialect's\n * wording from memory was ruled out as evidence. #8567 raised the condition on\n * a real Postgres 16.13 (system PG16 binaries, `initdb` + `pg_ctl`, no\n * container runtime) through the same knex + `pg` path `SqlDriver.upsert`\n * uses, and read the fields off the thrown error object:\n *\n * ```\n * # POSTGRES 16.13, knex 3.3.0 + pg 8.22.0\n * upsert('plain', { email: 'a@b.com', title: 'x' }, ['email'])\n * -> name=error (DatabaseError) code=42P10 severity=ERROR status=undefined\n * routine=infer_arbiter_indexes constraint=undefined detail=undefined\n * msg=insert into \"plain\" (\"email\", \"id\", \"title\") values ($1, $2, $3)\n * on conflict (\"email\") do update set \"title\" = excluded.\"title\"\n * - there is no unique or exclusion constraint matching the ON CONFLICT specification\n *\n * # SQLITE 3.x, knex 3.3.0 + better-sqlite3 (#8445's measurement, unchanged)\n * upsert('plain', { email: 'a@b.com', title: 'x' }, ['email'])\n * -> name=SqliteError code=SQLITE_ERROR status=undefined\n * msg=insert into `plain` (...) values (...) on conflict (`email`) do update set ...\n * - ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint\n * ```\n *\n * Two dialects, two unrelated sentences, and the same envelope from knex: the\n * STATEMENT, then ` - `, then the server's own text. That tail is what both\n * limbs below are anchored on, so a knex-prefixed message and a bare driver\n * message are recognised identically.\n *\n * ## Why the `code` channel is unused — also measured\n *\n * The obvious predicate is `code === '42P10'`, and it is wrong in both\n * directions:\n *\n * - **SQLite has no code to read.** It answers plain `SQLITE_ERROR`, the same\n * generic code a syntax error or a missing table carries. `driver-sql`'s\n * own suite pins that: an upsert against a table that was never created\n * must come back as itself, and it is a `SQLITE_ERROR` too.\n * - **Postgres' code OVER-matches.** `42P10` is `invalid_column_reference`,\n * not \"unbacked conflict target\". Measured on the same cluster, same\n * session:\n *\n * ```\n * select id from plain order by 7 -> code=42P10 \"ORDER BY position 7 is not in select list\"\n * select id from plain group by 9 -> code=42P10 \"GROUP BY position 9 is not in select list\"\n * ```\n *\n * A code-only limb would answer `VALIDATION_ERROR` \"no unique index backs\n * your conflict keys\" to a caller whose real defect is an out-of-range sort\n * position — a refusal pointing at the wrong thing entirely. So the message\n * is not a fallback for a missing code here; it is the only channel that\n * identifies the condition, on both dialects, and the code channel is\n * deliberately left unread rather than ANDed in for a narrowing it does not\n * provide.\n *\n * The Postgres limb is safe to match on prose because the sentence has exactly\n * one source: `infer_arbiter_indexes` (`plancat.c`), reached only while\n * planning an `ON CONFLICT` inference, which the measured `routine` field\n * confirms. The SQLite limb has the same property, stated at #8445.\n *\n * ## MySQL: the condition cannot arise, and that is measured too\n *\n * MySQL has no `ON CONFLICT` syntax. knex compiles the driver's exact call to\n * `ON DUPLICATE KEY UPDATE`, which takes **no conflict target** — the named\n * keys are dropped from the statement before it leaves the process, so the\n * server is never asked to find an index for them and cannot complain that\n * none exists. Compiled with knex 3.3.0 on the `mysql2` dialect, no server\n * needed (`.toSQL()`), and pinned by\n * `sql-driver-upsert-conflict-target-dialects.test.ts`:\n *\n * ```\n * knex('plain').insert({...}).onConflict(['email']).merge(['title']).toSQL()\n * mysql2 -> insert into `plain` (`email`, `id`, `title`) values (?, ?, ?)\n * on duplicate key update `title` = values(`title`) ← no `email` target\n * pg -> insert into \"plain\" (...) values ($1, $2, $3)\n * on conflict (\"email\") do update set \"title\" = excluded.\"title\"\n * ```\n *\n * So there is no MySQL limb to write, and its absence is a finding rather than\n * a gap. ⚠️ What MySQL does *instead* — merge on whichever unique key the row\n * happens to collide with, or insert a second row — is a different defect with\n * a different fix, and is NOT this predicate's business.\n *\n * ## Home\n *\n * `@objectstack/types`, beside {@link isUniqueViolationError}, for the reason\n * that module records: every consumer of the question already depends on this\n * package, so naming it here never adds an edge, and this module deliberately\n * imports nothing. The alternative — a second private regex in each driver\n * that meets the condition — is exactly the state `unique-violation.ts` was\n * written to retire, where four hand-written vocabularies disagreed about\n * MySQL and nobody could see it.\n */\n\n/**\n * One dialect vocabulary for this condition, in the channel that carries it.\n *\n * Deliberately **message-only**, unlike `UniqueViolationSignature`'s\n * three-channel table — the module head records the measurements: SQLite's\n * `code` is the generic `SQLITE_ERROR`, and Postgres' `42P10` is\n * `invalid_column_reference`, which an out-of-range `ORDER BY` position also\n * raises. Neither channel narrows anything, and a `codes` set standing empty\n * beside them would read as \"nobody has filled this in yet\" rather than as the\n * decision it is.\n */\ninterface UnbackedConflictTargetSignature {\n /**\n * `error.message` — matched on the server's own sentence, which knex leaves\n * as the tail after the statement and ` - `.\n */\n readonly message: RegExp;\n}\n\n/**\n * Every wording measured for this condition, one limb per dialect that can\n * raise it. Nothing here is inferred: each limb was read off a thrown error\n * object, and the transcript is in the module head above.\n *\n * - SQLite: `ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE\n * constraint` — stable since `ON CONFLICT` arrived in 3.24 (#8445).\n * - Postgres: `there is no unique or exclusion constraint matching the ON\n * CONFLICT specification` — `infer_arbiter_indexes`, PG 16.13 (#8567).\n *\n * Deliberately NOT here: any limb for MySQL (the condition cannot reach the\n * server — see the module head), and any bare `ON CONFLICT` fragment. A limb\n * loose enough to match `on conflict` alone would match the driver's own\n * *statement* text, which knex prefixes onto every upsert failure — including\n * a unique violation, which is the opposite condition.\n */\nconst UNBACKED_CONFLICT_TARGET: UnbackedConflictTargetSignature = {\n message:\n /ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint|there is no unique or exclusion constraint matching the ON CONFLICT specification/i,\n};\n\n/** How far to follow an `error.cause` chain — drivers wrap, but not deeply. */\nconst MAX_CAUSE_DEPTH = 4;\n\n/**\n * Whether a thrown driver error says the `ON CONFLICT` target it was given is\n * backed by no PRIMARY KEY or UNIQUE index.\n *\n * Reads the message channel, then one step at a time down the `cause` chain —\n * pool and query-builder layers re-throw with the original attached, and the\n * refusal this predicate gates keeps the raw error as its own `cause`. A plain\n * string is judged directly, so a caller that already unwrapped `err.message`\n * can pass it in.\n *\n * **Unrecognised is always `false`.** A false positive is the expensive\n * direction: it tells a caller to go add an index when the real failure was a\n * syntax error, a missing table, or — worst — a genuine unique violation on an\n * index that exists and works. A false negative costs only the raw error that\n * was the status quo before recognition existed.\n *\n * @param error - the thrown value, of any shape.\n *\n * @example\n * ```ts\n * catch (error) {\n * // ⚠️ NOT isUniqueViolationError — that is the opposite condition.\n * if (isUnbackedConflictTargetError(error)) throw refuseUnbackedConflictTarget(object, keys, error);\n * throw error;\n * }\n * ```\n */\nexport function isUnbackedConflictTargetError(error: unknown): boolean {\n return matchesUnbackedConflictTarget(error, 0);\n}\n\nfunction matchesUnbackedConflictTarget(error: unknown, depth: number): boolean {\n if (error === null || error === undefined || depth > MAX_CAUSE_DEPTH) return false;\n\n if (typeof error === 'string') return UNBACKED_CONFLICT_TARGET.message.test(error);\n if (typeof error !== 'object') return false;\n\n const err = error as { message?: unknown; cause?: unknown };\n\n if (typeof err.message === 'string' && UNBACKED_CONFLICT_TARGET.message.test(err.message)) return true;\n\n return matchesUnbackedConflictTarget(err.cause, depth + 1);\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * [ADR-0120 D5e] The `isolated`-posture install gate for `'global'` uniqueness.\n *\n * ## Why a gate exists at all\n *\n * ADR-0120's scope vocabulary is deliberately **posture-invariant**: the author\n * states a business boundary (`'organization'` = one holder per organization,\n * `'global'` = one holder across the whole installation) and the same app\n * package runs unmodified under every tenancy posture (ADR-0105 D1\n * `single | group | isolated`). No index shape reads the posture — a posture\n * flip has zero automatic schema consequences, which is exactly what makes one\n * app package serve all three.\n *\n * One residual survives that invariance, and only in one direction\n * (ADR-0120 §Posture portability, Resolved question #4):\n *\n * - Under `single` / `group`, `'global'` means \"the installation\" — which for a\n * `group` deployment IS the customer company (集团). An app business rule\n * spelled `'global'` is correct there.\n * - Under `isolated`, organizations are **separate customers**. The identical\n * declaration now crosses customers: it over-constrains (customer B cannot\n * reuse customer A's material code) and it becomes a cross-tenant existence\n * oracle — the very leak #3696 closed for field-level uniques (S10).\n *\n * `'global'` is therefore physically posture-invariant but not *safety*-invariant,\n * and the ADR's S14 row records the honest cost: \"unique across the whole\n * company\" is not expressible in metadata alone, because it means the\n * installation under `group` and one organization under `isolated`. A third,\n * posture-resolved word (`'company'`) was designed and **rejected** — it is the\n * one token that cannot be used without first understanding the posture\n * spectrum, exactly the cognitive load an AI-authored vocabulary must not carry.\n * The scenario is handled **here**, at the deployment seam, instead.\n *\n * ## Why a HARD stop and not an advisory\n *\n * Maintainer decision, 2026-08-04 (ADR-0120 Resolved #4). An advisory that\n * nobody reads leaves a cross-customer constraint enforced in production — the\n * ADR-0049/0078 class this whole ADR exists to close. So installing an app that\n * carries `'global'` uniques on non-`sys` objects into an `isolated` environment\n * **stops**, lists each index, and asks the installer (typically an AI agent) to\n * either confirm it as genuinely platform-wide or rewrite it to\n * `'organization'`. The confirmation is recorded in the install manifest\n * (ADR-0104 attestation style) so it is **never re-asked**.\n *\n * ⛔ **Never a boot-time warning** (#4884 discipline). A deployment whose apps\n * were installed before this gate existed, or whose posture changed after\n * install, is reached by the ADVISORY form in `os doctor` / `os migrate plan` —\n * the two cases a gate at the install seam structurally cannot see. Turning\n * this into a startup diagnostic would fire on every boot of every deployment\n * forever, which is the false-alarm class #4884 retired.\n *\n * ## What counts as a finding\n *\n * | Declaration | Finding? | Why |\n * |:---|:---|:---|\n * | field `unique: 'global'` | ✅ | one holder across the installation — crosses customers under `isolated` |\n * | declared index `unique: 'global'` | ✅ | same boundary, spelled on the index |\n * | declared index `unique: true` | ✅ | ADR-0120 D1: bare `true` **is** the deprecated positional spelling of `'global'`; identical physical shape, identical hazard. Excluding it would leave the gate bypassable by spelling for the whole of 17.x |\n * | field `unique: true` / `'organization'` | ❌ | per-organization — correct under every posture |\n * | declared index `unique: 'organization'` | ❌ | per-organization (D3 NULL-safe key part) |\n * | anything on a `sys_*` object | ❌ | engine idempotency / dedup keys (the ADR's S5 inventory) are platform-wide **by construction**; asking about them on every install is the false-alarm class again |\n *\n * The enumeration is a pure projection of declared metadata — no tenancy\n * inference, no database access — which is what lets the identical function\n * serve the hard gate, `os doctor` and `os migrate plan`.\n */\n\nimport { normalizeTenancyPosture, type TenancyPosture } from '@objectstack/spec/security';\n\n/** Objects owned by the platform itself never raise a finding. */\nconst SYS_OBJECT_PREFIXES = ['sys_', 'base_'] as const;\n\n/**\n * Is this object platform-owned (the ADR's \"`sys` objects\")?\n *\n * The ADR scopes the gate to **non-`sys`** objects because the platform's own\n * `'global'` uniques are the S5 inventory — `sys_job.name`,\n * `sys_notification.dedup_key`, `http_delivery (source, dedup_key)` and the rest\n * — engine idempotency keys that are platform-wide on purpose and identical\n * under every posture. Re-confirming them on every app install would be the\n * #4884 false-alarm class with extra steps.\n *\n * `base_` is included alongside `sys_`: it is the platform's other reserved\n * object prefix, carrying the same \"owned by the framework, not the app\"\n * meaning. An app object can never legitimately claim either.\n */\nexport function isPlatformOwnedObject(objectName: unknown): boolean {\n const name = typeof objectName === 'string' ? objectName.trim().toLowerCase() : '';\n if (!name) return false;\n return SYS_OBJECT_PREFIXES.some((prefix) => name.startsWith(prefix));\n}\n\n/**\n * Does a FIELD-level `unique` value ask for the installation-wide boundary?\n *\n * Only the explicit `'global'` does. Bare `true` at field level is the\n * documented, unambiguous synonym of `'organization'` (ADR-0120 D1 —\n * \"field-level bare `true` stays valid indefinitely\", Resolved #2), so it is\n * never a finding.\n */\nexport function fieldUniqueIsGlobal(unique: unknown): boolean {\n return unique === 'global';\n}\n\n/**\n * Does a DECLARED-INDEX `unique` value ask for the installation-wide boundary?\n *\n * `'global'` and bare `true` both do. Per ADR-0120 D1 the bare spelling **is**\n * `'global'` — \"today's verbatim semantics, materialized over exactly the listed\n * columns\" — deprecated (lint `unique/unscoped-declared-index` warns in 17.x,\n * protocol 18 rejects it, #5082) but physically identical while it lasts. A gate\n * that judged only the explicit word would be bypassable by writing the\n * deprecated one, which is the #4986 trap wearing the gate's own uniform.\n */\nexport function declaredIndexUniqueIsGlobal(unique: unknown): boolean {\n return unique === 'global' || unique === true;\n}\n\n/** One installation-wide unique declaration found on an app (non-`sys`) object. */\nexport interface GlobalUniqueFinding {\n /** Stable identity for the attestation record — see {@link globalUniqueFindingId}. */\n readonly id: string;\n /** Object (and therefore table) the declaration sits on. */\n readonly object: string;\n /** Which spelling carried it. */\n readonly kind: 'field' | 'index';\n /** Field name for `kind: 'field'`; the index's declared name (when it has one) otherwise. */\n readonly name?: string;\n /** The columns the constraint spans, in declaration order. */\n readonly columns: readonly string[];\n /** The exact authored value (`true` | `'global'`) — quoted back in the stop message. */\n readonly spelling: true | 'global';\n}\n\n/**\n * Stable id for one finding, used as the attestation key.\n *\n * Keyed by object + kind + **columns**, deliberately NOT by the index's optional\n * `name`: a declared index may be anonymous, and renaming an index does not\n * change which constraint the installer confirmed. Two indexes on the same\n * object spanning the same columns are the same constraint by any physical\n * reading, so collapsing them is correct rather than lossy.\n */\nexport function globalUniqueFindingId(\n objectName: string,\n kind: 'field' | 'index',\n columns: readonly string[],\n): string {\n return `${objectName}:${kind}:${columns.join('+')}`;\n}\n\n/** Field map or field array — both authoring shapes are accepted. */\nfunction fieldEntriesOf(fields: unknown): Array<{ name: string; def: any }> {\n if (!fields) return [];\n if (Array.isArray(fields)) {\n return fields\n .filter((f: any) => f && f.name != null)\n .map((f: any) => ({ name: String(f.name), def: f }));\n }\n if (typeof fields !== 'object') return [];\n return Object.entries(fields as Record<string, any>).map(([name, def]) => ({ name, def }));\n}\n\n/**\n * Enumerate every installation-wide unique declared on an app's non-`sys`\n * objects (ADR-0120 D5e).\n *\n * Pure and posture-agnostic on purpose: the CALLER decides whether the posture\n * makes these findings a hard stop (`isolated`, at install) or an advisory\n * (`os doctor` / `os migrate plan`). Deterministic order — objects as supplied,\n * fields before indexes within an object — so the stop message and the\n * attestation record are reproducible across runs.\n */\nexport function collectGlobalUniques(objects: unknown): GlobalUniqueFinding[] {\n if (!Array.isArray(objects)) return [];\n const findings: GlobalUniqueFinding[] = [];\n\n for (const obj of objects as any[]) {\n const objectName = typeof obj?.name === 'string' ? obj.name.trim() : '';\n if (!objectName) continue;\n if (isPlatformOwnedObject(objectName)) continue;\n\n for (const { name, def } of fieldEntriesOf(obj?.fields)) {\n if (!fieldUniqueIsGlobal(def?.unique)) continue;\n findings.push({\n id: globalUniqueFindingId(objectName, 'field', [name]),\n object: objectName,\n kind: 'field',\n name,\n columns: [name],\n spelling: 'global',\n });\n }\n\n const declaredIndexes = Array.isArray(obj?.indexes) ? obj.indexes : [];\n for (const idx of declaredIndexes as any[]) {\n if (!declaredIndexUniqueIsGlobal(idx?.unique)) continue;\n const columns = Array.isArray(idx?.fields)\n ? idx.fields.filter((f: unknown) => typeof f === 'string').map((f: string) => f)\n : [];\n if (columns.length === 0) continue;\n const indexName = typeof idx?.name === 'string' && idx.name.trim() ? idx.name.trim() : undefined;\n findings.push({\n id: globalUniqueFindingId(objectName, 'index', columns),\n object: objectName,\n kind: 'index',\n ...(indexName ? { name: indexName } : {}),\n columns,\n spelling: idx.unique === true ? true : 'global',\n });\n }\n }\n\n return findings;\n}\n\n/**\n * The attestation recorded in the install manifest once an installer has\n * confirmed a set of findings as genuinely platform-wide (ADR-0104 style).\n *\n * Shape follows the ADR-0104 precedent rather than inventing one: the FACT\n * observed (which constraint ids a human/agent affirmed), WHO affirmed it, WHEN,\n * and under WHICH posture the question was asked. That last field is what keeps\n * the record honest — an attestation given under `isolated` is evidence about\n * `isolated`, and nothing else.\n *\n * Never rewritten in place: confirmations ACCUMULATE. A later install of a newer\n * version that adds a new `'global'` index asks about the new one only — the\n * earlier answers stand, which is the \"之后不复问\" half of the decision.\n */\nexport interface GlobalUniqueAttestation {\n /** Posture the confirmation was given under. */\n readonly posture: TenancyPosture;\n /** Finding ids affirmed as genuinely platform-wide. */\n readonly confirmed: readonly string[];\n /** ISO timestamp of the most recent confirmation. */\n readonly attestedAt: string;\n /** Identity of the confirming installer, when the seam knows one. */\n readonly attestedBy?: string | null;\n}\n\n/**\n * Which findings still need an answer, given an existing attestation.\n *\n * Returns the findings NOT covered by `attestation.confirmed`. An empty result\n * means the install proceeds silently — this is the mechanism behind \"never\n * re-asked\".\n *\n * An attestation recorded under a DIFFERENT posture does not carry over: the\n * question \"is this genuinely platform-wide, knowing organizations here are\n * separate customers?\" was never asked. Confirmations made under `isolated` are\n * the only ones that answer it, so a `single`-posture record is treated as\n * absent rather than as consent — the conservative direction, and the only one\n * that cannot silently admit a cross-customer constraint.\n */\nexport function unconfirmedGlobalUniques(\n findings: readonly GlobalUniqueFinding[],\n attestation: GlobalUniqueAttestation | undefined | null,\n posture: TenancyPosture,\n): GlobalUniqueFinding[] {\n if (!attestation || attestation.posture !== posture) return [...findings];\n const confirmed = new Set(attestation.confirmed ?? []);\n return findings.filter((f) => !confirmed.has(f.id));\n}\n\n/**\n * Merge a new set of confirmations into an existing attestation.\n *\n * Additive by construction — see {@link GlobalUniqueAttestation}. A record from\n * another posture is replaced rather than merged: its `confirmed` ids answered a\n * different question.\n */\nexport function recordGlobalUniqueAttestation(\n previous: GlobalUniqueAttestation | undefined | null,\n confirmedIds: readonly string[],\n posture: TenancyPosture,\n attestedBy?: string | null,\n now: string = new Date().toISOString(),\n): GlobalUniqueAttestation {\n const carried = previous && previous.posture === posture ? previous.confirmed ?? [] : [];\n const merged = Array.from(new Set([...carried, ...confirmedIds])).sort();\n return {\n posture,\n confirmed: merged,\n attestedAt: now,\n ...(attestedBy !== undefined ? { attestedBy } : {}),\n };\n}\n\n/** Render one finding the way both the hard stop and the advisory quote it. */\nexport function describeGlobalUniqueFinding(finding: GlobalUniqueFinding): string {\n const spelling = finding.spelling === true ? '`unique: true`' : \"`unique: 'global'`\";\n const deprecated = finding.spelling === true ? ' [deprecated bare spelling of \\'global\\']' : '';\n if (finding.kind === 'field') {\n return `${finding.object}.${finding.name} — field-level ${spelling}`;\n }\n const label = finding.name ? ` '${finding.name}'` : '';\n return `${finding.object} — declared index${label} [${finding.columns.join(', ')}] ${spelling}${deprecated}`;\n}\n\n/**\n * The prescription every surface repeats verbatim, so the hard stop and the two\n * advisories cannot drift into three different pieces of advice.\n */\nexport const GLOBAL_UNIQUE_ISOLATED_PRESCRIPTION =\n \"Under the 'isolated' posture organizations are separate CUSTOMERS, so an installation-wide unique \" +\n 'constrains across customers and can reveal that another customer already holds a value (ADR-0120 S10/S14). ' +\n 'For each index above, either (a) confirm it is genuinely platform-wide — an infrastructure/dedup key, a DNS ' +\n 'hostname, an external provider id — or (b) rewrite it to `unique: \\'organization\\'` so it is one holder per ' +\n 'organization. See ADR-0120 §Posture portability.';\n\n/**\n * The full hard-stop message for an install into an `isolated` environment.\n *\n * Built here rather than at the install seam so the CLI, the HTTP surface and\n * the tests all quote one text.\n */\nexport function buildGlobalUniqueStopMessage(\n appLabel: string,\n findings: readonly GlobalUniqueFinding[],\n): string {\n const lines = findings.map((f) => ` • ${describeGlobalUniqueFinding(f)}`);\n return (\n `'${appLabel}' declares ${findings.length} installation-wide unique constraint(s) on its own objects, and this ` +\n \"environment runs the 'isolated' tenancy posture (ADR-0120 D5e):\\n\" +\n `${lines.join('\\n')}\\n` +\n `${GLOBAL_UNIQUE_ISOLATED_PRESCRIPTION}\\n` +\n 'Re-run the install with the confirmation to record it in the install manifest — it is asked once, ' +\n 'never again for the same constraints.'\n );\n}\n\n/** Error code the install seam returns when the gate stops an install. */\nexport const GLOBAL_UNIQUE_CONFIRMATION_REQUIRED = 'UNIQUE_SCOPE_CONFIRMATION_REQUIRED';\n\n/**\n * Does this posture make `'global'` uniques a decision point at all?\n *\n * `isolated` only. Under `single` there is one customer; under `group` the\n * installation IS the customer company, which is what `'global'` means there —\n * both are the benign direction the ADR leaves to the app's install notes.\n */\nexport function postureGatesGlobalUniques(posture: unknown): boolean {\n return normalizeTenancyPosture(posture) === 'isolated';\n}\n"],"mappings":"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","names":["import_api","MAX_CAUSE_DEPTH","MAX_CAUSE_DEPTH","import_security"]}
|