@rebasepro/server-postgres 0.24.1-canary.gcb918fc → 0.25.0-canary.g70dad01
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BranchService-Bsn2wfaj.js → BranchService-DO_3_CXt.js} +118 -18
- package/dist/BranchService-DO_3_CXt.js.map +1 -0
- package/dist/PostgresBackendDriver.d.ts +13 -0
- package/dist/PostgresBootstrapper.d.ts +10 -0
- package/dist/backup/backup-cli.d.ts +120 -2
- package/dist/backup/backup-service.d.ts +74 -1
- package/dist/backup/pg-binary.d.ts +2 -2
- package/dist/backup/pg-tools.d.ts +65 -0
- package/dist/backup-cli-Ap2WX2uW.js +647 -0
- package/dist/backup-cli-Ap2WX2uW.js.map +1 -0
- package/dist/{backup-service-BTBB7RXn.js → backup-service-DytMimsL.js} +414 -24
- package/dist/backup-service-DytMimsL.js.map +1 -0
- package/dist/{boot-ddl-DVpsFN6r.js → boot-ddl-D1lMCZLd.js} +3 -3
- package/dist/{boot-ddl-DVpsFN6r.js.map → boot-ddl-D1lMCZLd.js.map} +1 -1
- package/dist/branch-argv.d.ts +18 -6
- package/dist/branch-prune.d.ts +18 -0
- package/dist/cli-console-CQGsBzSY.js +262 -0
- package/dist/cli-console-CQGsBzSY.js.map +1 -0
- package/dist/cli-console.d.ts +11 -0
- package/dist/{cli-errors-CaI4ntyD.js → cli-errors-CJTKAKax.js} +499 -107
- package/dist/cli-errors-CJTKAKax.js.map +1 -0
- package/dist/cli-errors.d.ts +42 -5
- package/dist/{cli-flags-JNI-pwSf.js → cli-flags-D-b0qL9T.js} +71 -6
- package/dist/cli-flags-D-b0qL9T.js.map +1 -0
- package/dist/cli-flags.d.ts +21 -0
- package/dist/cli-helpers.d.ts +53 -24
- package/dist/{arg-Dni7MzLB.js → cli-prompt-egtNKkZl.js} +49 -3
- package/dist/cli-prompt-egtNKkZl.js.map +1 -0
- package/dist/cli-prompt.d.ts +19 -0
- package/dist/cli-scratch-database.d.ts +47 -8
- package/dist/cli.js +615 -100
- package/dist/cli.js.map +1 -1
- package/dist/collections/validate-relations.d.ts +8 -2
- package/dist/{column-plan-helpers-drZIsSNf.js → column-plan-helpers-D_V4oaZh.js} +2 -2
- package/dist/{column-plan-helpers-drZIsSNf.js.map → column-plan-helpers-D_V4oaZh.js.map} +1 -1
- package/dist/{connection-C9mT8bgf.js → connection-C938nR3E.js} +2 -2
- package/dist/{connection-C9mT8bgf.js.map → connection-C938nR3E.js.map} +1 -1
- package/dist/{databasePoolManager-BIuewidb.js → databasePoolManager-Yqdw14wY.js} +4 -4
- package/dist/{databasePoolManager-BIuewidb.js.map → databasePoolManager-Yqdw14wY.js.map} +1 -1
- package/dist/{deferred-index-build-DP1_SNFo.js → deferred-index-build-BiTqxqdl.js} +8 -8
- package/dist/{deferred-index-build-DP1_SNFo.js.map → deferred-index-build-BiTqxqdl.js.map} +1 -1
- package/dist/{doctor-C4WiftHm.js → doctor-N3J88BN_.js} +8 -8
- package/dist/doctor-N3J88BN_.js.map +1 -0
- package/dist/{ensure-collection-policies-CXpbeNag.js → ensure-collection-policies-CVP6kIPT.js} +125 -18
- package/dist/ensure-collection-policies-CVP6kIPT.js.map +1 -0
- package/dist/{ensure-collection-tables-BhsU6qUE.js → ensure-collection-tables-1WVNALOW.js} +10 -10
- package/dist/{ensure-collection-tables-BhsU6qUE.js.map → ensure-collection-tables-1WVNALOW.js.map} +1 -1
- package/dist/{ensure-tables-CVm_yvzb.js → ensure-tables-D-Nm2k3S.js} +5 -5
- package/dist/{ensure-tables-CVm_yvzb.js.map → ensure-tables-D-Nm2k3S.js.map} +1 -1
- package/dist/{enum-labels-kept-D8fI1MoX.js → enum-labels-kept-e_93v2Ic.js} +3 -3
- package/dist/{enum-labels-kept-D8fI1MoX.js.map → enum-labels-kept-e_93v2Ic.js.map} +1 -1
- package/dist/{generate-drizzle-schema-Bz52jH2h.js → generate-drizzle-schema-C3EAP9HE.js} +3 -3
- package/dist/{generate-drizzle-schema-Bz52jH2h.js.map → generate-drizzle-schema-C3EAP9HE.js.map} +1 -1
- package/dist/{generate-drizzle-schema-logic-B74WN37S.js → generate-drizzle-schema-logic-DA3zM8zc.js} +4 -4
- package/dist/{generate-drizzle-schema-logic-B74WN37S.js.map → generate-drizzle-schema-logic-DA3zM8zc.js.map} +1 -1
- package/dist/{generate-files-CdEckX6c.js → generate-files-ClmOUHXj.js} +6 -47
- package/dist/generate-files-ClmOUHXj.js.map +1 -0
- package/dist/generate-next-step-DCeS4LrJ.js +74 -0
- package/dist/generate-next-step-DCeS4LrJ.js.map +1 -0
- package/dist/{generate-postgres-ddl-logic-B64O2oZF.js → generate-postgres-ddl-logic-4eUP4t8w.js} +4 -4
- package/dist/{generate-postgres-ddl-logic-B64O2oZF.js.map → generate-postgres-ddl-logic-4eUP4t8w.js.map} +1 -1
- package/dist/{generated-schema-staleness-Di9c4ZxN.js → generated-schema-staleness-FlxM2i4i.js} +102 -12
- package/dist/{generated-schema-staleness-Di9c4ZxN.js.map → generated-schema-staleness-FlxM2i4i.js.map} +1 -1
- package/dist/index.es.js +11 -11
- package/dist/{introspect-db-logic-C-Di8dA9.js → introspect-db-logic-CI5CU_n0.js} +145 -9
- package/dist/introspect-db-logic-CI5CU_n0.js.map +1 -0
- package/dist/{introspect-db-queries-D6kAqReI.js → introspect-db-queries-DevUrzll.js} +41 -6
- package/dist/introspect-db-queries-DevUrzll.js.map +1 -0
- package/dist/{open-rebase-CbOur0TU.js → open-rebase-Cwnqo5LU.js} +116 -51
- package/dist/open-rebase-Cwnqo5LU.js.map +1 -0
- package/dist/open-rebase.d.ts +12 -4
- package/dist/{pg-error-utils-vJezlXdI.js → pg-error-utils-Cmo8wZPy.js} +19 -5
- package/dist/pg-error-utils-Cmo8wZPy.js.map +1 -0
- package/dist/{plan-schema-B1YF_tYq.js → plan-schema-CXOucCw-.js} +4 -4
- package/dist/{plan-schema-B1YF_tYq.js.map → plan-schema-CXOucCw-.js.map} +1 -1
- package/dist/{policy-drift-Dm8i-s1J.js → policy-drift-vm0BcvcE.js} +28 -14
- package/dist/{policy-drift-Dm8i-s1J.js.map → policy-drift-vm0BcvcE.js.map} +1 -1
- package/dist/{push-index-prebuild-Db4AQF4i.js → push-index-prebuild-SXRlWBjc.js} +4 -4
- package/dist/{push-index-prebuild-Db4AQF4i.js.map → push-index-prebuild-SXRlWBjc.js.map} +1 -1
- package/dist/{push-legacy-renames-BK6ilQq2.js → push-legacy-renames-B37yKDac.js} +4 -4
- package/dist/{push-legacy-renames-BK6ilQq2.js.map → push-legacy-renames-B37yKDac.js.map} +1 -1
- package/dist/push-policy-plan-jXzHAWSu.js +127 -0
- package/dist/push-policy-plan-jXzHAWSu.js.map +1 -0
- package/dist/{render-ddl-CtC3y9Ol.js → render-ddl-CQNwZdse.js} +10 -9
- package/dist/render-ddl-CQNwZdse.js.map +1 -0
- package/dist/{rls-bootstrap-sql-RQUljnJ1.js → rls-bootstrap-sql-Bd-hJk08.js} +3 -58
- package/dist/rls-bootstrap-sql-Bd-hJk08.js.map +1 -0
- package/dist/{rls-enforcement-DvzqlaL1.js → rls-enforcement-D7P4-ZYX.js} +2 -2
- package/dist/{rls-enforcement-DvzqlaL1.js.map → rls-enforcement-D7P4-ZYX.js.map} +1 -1
- package/dist/{__vite-browser-external-BnuHet1e.js → rolldown-runtime-V7rFbMRe.js} +1 -8
- package/dist/schema/atlas-apply-report.d.ts +23 -0
- package/dist/schema/atlas-argv.d.ts +24 -1
- package/dist/schema/atlas-exclude.d.ts +42 -0
- package/dist/schema/atlas-revisions.d.ts +4 -0
- package/dist/schema/carved-out-migration.d.ts +5 -4
- package/dist/schema/destructive-sql.d.ts +23 -1
- package/dist/schema/doctor-cli.js +5 -5
- package/dist/schema/ensure-collection-policies.d.ts +74 -0
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-next-step.d.ts +23 -0
- package/dist/schema/generate-postgres-ddl.js +2 -2
- package/dist/schema/generated-schema-diff.d.ts +7 -0
- package/dist/schema/generated-schema-staleness.d.ts +7 -1
- package/dist/schema/introspect-db-logic.d.ts +32 -1
- package/dist/schema/introspect-db-queries.d.ts +18 -5
- package/dist/schema/introspect-db.js +55 -53
- package/dist/schema/introspect-db.js.map +1 -1
- package/dist/schema/migrate-baseline.d.ts +51 -0
- package/dist/schema/plan/render-ddl.d.ts +2 -1
- package/dist/schema/push-policy-plan.d.ts +70 -0
- package/dist/schema/undeclarable-constraints.d.ts +59 -0
- package/dist/security/policy-drift.d.ts +5 -0
- package/dist/services/BranchService.d.ts +38 -0
- package/dist/{source-DBDbBTL4.js → source-Bv5KPeIx.js} +109 -4
- package/dist/source-Bv5KPeIx.js.map +1 -0
- package/dist/utils/connection-string.d.ts +47 -0
- package/dist/{vector-index-CMJKb3C9.js → vector-index-BFhyEaiz.js} +2 -2
- package/dist/{vector-index-CMJKb3C9.js.map → vector-index-BFhyEaiz.js.map} +1 -1
- package/dist/{websocket-BLGc5yhs.js → websocket-BT85Yv_G.js} +41 -13
- package/dist/websocket-BT85Yv_G.js.map +1 -0
- package/package.json +7 -7
- package/dist/BranchService-Bsn2wfaj.js.map +0 -1
- package/dist/__vite-browser-external-BnuHet1e.js.map +0 -1
- package/dist/arg-Dni7MzLB.js.map +0 -1
- package/dist/backup-cli-DnpJi6I7.js +0 -420
- package/dist/backup-cli-DnpJi6I7.js.map +0 -1
- package/dist/backup-service-BTBB7RXn.js.map +0 -1
- package/dist/cli-console-Ca6ZlAVU.js +0 -154
- package/dist/cli-console-Ca6ZlAVU.js.map +0 -1
- package/dist/cli-errors-CaI4ntyD.js.map +0 -1
- package/dist/cli-flags-JNI-pwSf.js.map +0 -1
- package/dist/doctor-C4WiftHm.js.map +0 -1
- package/dist/ensure-collection-policies-CXpbeNag.js.map +0 -1
- package/dist/generate-files-CdEckX6c.js.map +0 -1
- package/dist/introspect-db-logic-C-Di8dA9.js.map +0 -1
- package/dist/introspect-db-queries-D6kAqReI.js.map +0 -1
- package/dist/open-rebase-CbOur0TU.js.map +0 -1
- package/dist/pg-error-utils-vJezlXdI.js.map +0 -1
- package/dist/render-ddl-CtC3y9Ol.js.map +0 -1
- package/dist/rls-bootstrap-sql-RQUljnJ1.js.map +0 -1
- package/dist/source-DBDbBTL4.js.map +0 -1
- package/dist/websocket-BLGc5yhs.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vector-index-CMJKb3C9.js","names":[],"sources":["../src/schema/vector-index.ts"],"sourcesContent":["/**\n * The one place a `{ type: \"vector\" }` property becomes an ANN index.\n *\n * Without an index, pgvector answers `ORDER BY embedding <=> $1` by computing\n * the distance to every row and sorting — exact, and linear. That is the right\n * answer at ten thousand rows and the wrong one at a million, which is why the\n * default here is to build an index rather than to leave the column bare.\n *\n * ## The operator class is not a detail\n *\n * An index is built for exactly one operator class, and the planner uses it\n * only for the operator that class implements. `vector_cosine_ops` answers\n * `<=>` and nothing else; a query asking for `<->` against a cosine index gets\n * a sequential scan and no error. So the default indexed distance here is\n * `cosine`, because `cosine` is what `vectorSearch` measures with when the\n * caller does not say — see `DrizzleConditionBuilder.buildVectorSearch`. The\n * two defaults have to agree, and this comment is the reason they do.\n *\n * ## Why 2000 dimensions is a ceiling and not an error\n *\n * pgvector cannot index a `vector` wider than 2000 dimensions with either\n * method. A 3072-dimension embedding (`text-embedding-3-large`) is therefore\n * storable and searchable but not indexable. Refusing the boot over that would\n * make a working configuration unbootable; silently indexing it is impossible.\n * So the column is created, the index is skipped, and the reason is reported.\n *\n * Like `search-column.ts`, this module exists so that the DDL generator and the\n * boot-time ensure render the *same* specification rather than describing the\n * same index twice, differently. `contracts/derived-names.txt` records the\n * names both produce, and CI fails if they diverge.\n *\n * ## And why the column itself is described here too\n *\n * `VECTOR(n)` is a type Atlas cannot be shown. It has to materialise the\n * desired state in a dev database to diff against, that database is created\n * empty and *emptied again* by Atlas at the start of every run, and nothing in\n * the free tier can put `CREATE EXTENSION vector` back — so a `schema.sql`\n * mentioning the type fails with `type \"vector\" does not exist` on every push,\n * for good. Search hit the same wall for its own reasons and took the same way\n * out: the objects leave `schema.sql`, Atlas is told to exclude them, and\n * Rebase applies them itself. See `generatePostgresVectorDdl`.\n */\nimport type { CollectionConfig, Property, VectorDistance, VectorIndexConfig } from \"@rebasepro/types\";\nimport { isPostgresCollectionConfig } from \"@rebasepro/types\";\nimport { getTableName } from \"@rebasepro/common\";\nimport { toPostgresIdentifier } from \"@rebasepro/utils\";\n\n/**\n * The widest `vector` pgvector will build an HNSW or IVFFlat index over.\n * Storage and exact search are unaffected by this limit.\n */\nexport const MAX_INDEXABLE_VECTOR_DIMENSIONS = 2000;\n\n/** The distance indexed when a property does not name one. */\nexport const DEFAULT_VECTOR_DISTANCE: VectorDistance = \"cosine\";\n\n/** The index method used when a property does not name one. */\nexport const DEFAULT_VECTOR_INDEX_METHOD = \"hnsw\" as const;\n\n/**\n * Operator class per distance. These strings are part of the database contract:\n * they appear in `CREATE INDEX`, so renaming one renames an index.\n */\nconst OPERATOR_CLASS: Record<VectorDistance, string> = {\n cosine: \"vector_cosine_ops\",\n l2: \"vector_l2_ops\",\n inner_product: \"vector_ip_ops\"\n};\n\n/** Short, stable tag per distance, used to name the index. */\nconst DISTANCE_TAG: Record<VectorDistance, string> = {\n cosine: \"cosine\",\n l2: \"l2\",\n inner_product: \"ip\"\n};\n\nexport interface VectorIndexSpec {\n schema: string;\n table: string;\n column: string;\n indexName: string;\n method: \"hnsw\" | \"ivfflat\";\n distance: VectorDistance;\n operatorClass: string;\n /** Rendered into `WITH (...)`; empty when every parameter is defaulted. */\n parameters: Array<[string, number]>;\n}\n\n/** A vector column that will not be indexed, and why. */\nexport interface SkippedVectorIndex {\n schema: string;\n table: string;\n column: string;\n dimensions: number;\n reason: string;\n}\n\nexport interface VectorIndexPlan {\n specs: VectorIndexSpec[];\n skipped: SkippedVectorIndex[];\n}\n\nexport class VectorIndexConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"VectorIndexConfigError\";\n }\n}\n\nconst isVectorProperty = (prop: unknown): prop is Property & {\n type: \"vector\";\n dimensions: number;\n index?: VectorIndexConfig | false;\n} => !!prop && typeof prop === \"object\" && (prop as { type?: string }).type === \"vector\";\n\nconst asDistances = (config: VectorIndexConfig, label: string): VectorDistance[] => {\n const raw = config.distance ?? DEFAULT_VECTOR_DISTANCE;\n const list = Array.isArray(raw) ? raw : [raw];\n if (list.length === 0) {\n throw new VectorIndexConfigError(\n `${label}: \\`index.distance\\` is an empty array. Name at least one distance, or set \\`index: false\\` to create no index.`\n );\n }\n const seen = new Set<VectorDistance>();\n for (const distance of list) {\n if (!(distance in OPERATOR_CLASS)) {\n throw new VectorIndexConfigError(\n `${label}: \\`index.distance\\` is \"${distance}\", which is not a pgvector distance. Use ${Object.keys(OPERATOR_CLASS).map(d => `\"${d}\"`).join(\", \")}.`\n );\n }\n if (seen.has(distance)) {\n throw new VectorIndexConfigError(`${label}: \\`index.distance\\` lists \"${distance}\" twice.`);\n }\n seen.add(distance);\n }\n return list;\n};\n\nconst assertPositiveInteger = (value: number | undefined, key: string, label: string): void => {\n if (value === undefined) return;\n if (!Number.isInteger(value) || value <= 0) {\n throw new VectorIndexConfigError(\n `${label}: \\`index.${key}\\` is ${JSON.stringify(value)}. It must be a positive integer.`\n );\n }\n};\n\n/**\n * Index parameters for one method. Parameters belonging to the *other* method\n * are rejected rather than ignored, because a silently dropped `lists` on an\n * HNSW index reads, from the config, exactly like a tuned index.\n */\nconst parametersFor = (\n method: \"hnsw\" | \"ivfflat\",\n config: VectorIndexConfig,\n label: string\n): Array<[string, number]> => {\n assertPositiveInteger(config.m, \"m\", label);\n assertPositiveInteger(config.efConstruction, \"efConstruction\", label);\n assertPositiveInteger(config.lists, \"lists\", label);\n\n if (method === \"hnsw\") {\n if (config.lists !== undefined) {\n throw new VectorIndexConfigError(\n `${label}: \\`index.lists\\` only applies to \\`method: \"ivfflat\"\\`. Remove it, or switch the method.`\n );\n }\n const params: Array<[string, number]> = [];\n if (config.m !== undefined) params.push([\"m\", config.m]);\n if (config.efConstruction !== undefined) params.push([\"ef_construction\", config.efConstruction]);\n return params;\n }\n\n for (const key of [\"m\", \"efConstruction\"] as const) {\n if (config[key] !== undefined) {\n throw new VectorIndexConfigError(\n `${label}: \\`index.${key}\\` only applies to \\`method: \"hnsw\"\\`. Remove it, or switch the method.`\n );\n }\n }\n return config.lists !== undefined ? [[\"lists\", config.lists]] : [];\n};\n\n/**\n * Every ANN index a collection's vector properties call for.\n *\n * `resolveColumn` is passed in rather than imported so that this module stays\n * free of the DDL generator, which imports *it*. Both callers hand it the same\n * `resolveColumnName`, and a contract test asserts the names agree.\n */\nexport const buildVectorIndexPlan = (\n collection: CollectionConfig,\n resolveColumn: (propName: string, prop?: Property | null) => string\n): VectorIndexPlan => {\n const specs: VectorIndexSpec[] = [];\n const skipped: SkippedVectorIndex[] = [];\n const properties = collection.properties ?? {};\n\n const table = getTableName(collection);\n const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\";\n\n for (const [propName, prop] of Object.entries(properties)) {\n if (!isVectorProperty(prop)) continue;\n if (prop.index === false) continue;\n\n const label = `${collection.slug}.${propName}`;\n const column = resolveColumn(propName, prop as Property);\n const config: VectorIndexConfig = prop.index ?? {};\n const method = config.method ?? DEFAULT_VECTOR_INDEX_METHOD;\n\n if (method !== \"hnsw\" && method !== \"ivfflat\") {\n throw new VectorIndexConfigError(\n `${label}: \\`index.method\\` is \"${method}\". Use \"hnsw\" or \"ivfflat\".`\n );\n }\n\n // Read before the dimension gate so a malformed block is reported even\n // on a column too wide to index — the config is wrong either way.\n const distances = asDistances(config, label);\n const parameters = parametersFor(method, config, label);\n\n if (!Number.isInteger(prop.dimensions) || prop.dimensions <= 0) {\n throw new VectorIndexConfigError(\n `${label}: \\`dimensions\\` is ${JSON.stringify(prop.dimensions)}. It must be a positive integer.`\n );\n }\n\n if (prop.dimensions > MAX_INDEXABLE_VECTOR_DIMENSIONS) {\n skipped.push({\n schema,\n table,\n column,\n dimensions: prop.dimensions,\n reason:\n `pgvector cannot index a vector wider than ${MAX_INDEXABLE_VECTOR_DIMENSIONS} dimensions, and ` +\n `${label} declares ${prop.dimensions}. The column works and \\`vectorSearch\\` still answers, as an ` +\n `exact scan. To index it, reduce the dimensions (many embedding models support a shorter output) ` +\n `or set \\`index: false\\` to state that the scan is intended.`\n });\n continue;\n }\n\n for (const distance of distances) {\n specs.push({\n schema,\n table,\n column,\n // Distance is in the name because one column may carry an index\n // per distance, and two indexes cannot share a name. The method\n // is in it because switching methods is a different index, not\n // a redefinition of the same one — `CREATE INDEX IF NOT EXISTS`\n // would otherwise keep the old one and report success.\n indexName: toPostgresIdentifier(`${table}_${column}_${method}_${DISTANCE_TAG[distance]}`),\n method,\n distance,\n operatorClass: OPERATOR_CLASS[distance],\n parameters\n });\n }\n }\n\n return { specs, skipped };\n};\n\n/**\n * The `CREATE INDEX` for one spec.\n *\n * `CONCURRENTLY` is deliberately absent, for the same reason it is absent from\n * `searchIndexStatements`: this form is replayed as part of a migration, where\n * a concurrent build is not allowed. The boot-time ensure rewrites it — see\n * `ensureCollectionTables`.\n */\nexport const vectorIndexStatement = (spec: VectorIndexSpec): string => {\n const params = spec.parameters.length\n ? ` WITH (${spec.parameters.map(([key, value]) => `${key} = ${value}`).join(\", \")})`\n : \"\";\n return (\n `CREATE INDEX IF NOT EXISTS \"${spec.indexName}\" ON \"${spec.schema}\".\"${spec.table}\" ` +\n `USING ${spec.method} (\"${spec.column}\" ${spec.operatorClass})${params};`\n );\n};\n\n/** Every statement for a plan, in a stable order. */\nexport const vectorIndexStatements = (plan: VectorIndexPlan): string[] =>\n plan.specs.map(vectorIndexStatement);\n\n/** The index names a plan creates — what the derived-names contract records. */\nexport const vectorIndexNames = (plan: VectorIndexPlan): string[] =>\n plan.specs.map(spec => spec.indexName);\n\n// ── The column, for the file Rebase applies itself ──────────────────────────\n\nconst quote = (v: string): string => `'${v.replace(/'/g, \"''\")}'`;\n\n/**\n * The schema pgvector's types are installed into.\n *\n * `WITH SCHEMA public` for the same load-bearing reason `searchExtensionStatements`\n * gives: an unqualified `CREATE EXTENSION` lands in the first schema on\n * `search_path`, which is `\"$user\", public` — and the scaffold's role is named\n * `rebase`, the same as a schema the generator creates. Left to itself the\n * extension would install into `rebase`, and every unqualified `VECTOR(n)`\n * below would fail to resolve.\n */\nexport const VECTOR_EXTENSION_SCHEMA = \"public\";\n\n/** The extension name a database has to name to let Rebase install pgvector. */\nexport const VECTOR_EXTENSION = \"vector\";\n\n/**\n * How a project says Rebase may install pgvector, quoted into the messages that\n * have to name it. Spelled once so the option and the advice cannot drift.\n */\nexport const VECTOR_EXTENSION_OPT_IN = `database({ extensions: [\"${VECTOR_EXTENSION}\"] })`;\n\n/** Did the project give Rebase leave to install pgvector? */\nexport const vectorExtensionDeclared = (extensions: readonly string[] | undefined): boolean =>\n (extensions ?? []).includes(VECTOR_EXTENSION);\n\n/**\n * Install pgvector — emitted only when the database asked for it.\n *\n * Opt-in because installing an extension is a decision with a deployment behind\n * it: the image has to ship the library, the role has to be allowed to install\n * it, and a managed provider has to have it on an allow-list. None of that is\n * visible from inside the connection, so Rebase does not decide it. See\n * `DatabaseOptions.extensions`.\n *\n * Withholding the statement is not withholding the *column*: the column is\n * still created, and Postgres refuses it with `type \"vector\" does not exist`\n * on a database where pgvector was never installed by hand. That error is the\n * one this design accepts, and `vectorExtensionHint` is what makes it name\n * {@link VECTOR_EXTENSION_OPT_IN} rather than nothing.\n */\nexport const vectorExtensionStatement = (): string =>\n `CREATE EXTENSION IF NOT EXISTS ${VECTOR_EXTENSION} WITH SCHEMA ${VECTOR_EXTENSION_SCHEMA};`;\n\n/**\n * The missing-pgvector explanation, appended to whichever error revealed it.\n *\n * Two readers, needing opposite things, which is why the branch is on the error\n * text rather than on the configuration:\n *\n * - **`type \"vector\" does not exist`** — nobody opted in and the database has\n * no pgvector. The fix is one line of config they cannot guess, so naming\n * the option *is* the hint.\n * - **the install itself failed** — the config is already right, and repeating\n * the option would send them to edit a correct line. What is missing is the\n * library on the server, or the grant.\n *\n * Lives here rather than beside either caller because both need it: the boot\n * ensure raises the first through its action applier, and `rebase db push`\n * raises it out of `applyVectorDdl`. A hint on only one path is how a bare\n * `type \"vector\" does not exist` reaches somebody — which is the thing this\n * exists to prevent.\n */\nexport const vectorExtensionHint = (message: string): string => {\n const installRefused = /extension \"vector\" is not available/i.test(message)\n || /(permission denied to create extension|must be (superuser|owner).{0,40}extension)/i.test(message);\n if (installRefused) {\n return (\n \"\\n pgvector was declared and could not be installed. It is a server extension, so it needs an image \" +\n \"that ships the library (the scaffold's `pgvector/pgvector:pg18` does; a stock `postgres:18` does not) \" +\n \"and a role allowed to run `CREATE EXTENSION vector;`. Managed Postgres usually allows it once the \" +\n \"extension is on the provider's allow-list.\"\n );\n }\n if (!/type \"(vector|halfvec|sparsevec)\" does not exist/i.test(message)) return \"\";\n return (\n \"\\n pgvector is not installed on this database, and Rebase installs it only where a database says it may: \" +\n `add \\`${VECTOR_EXTENSION_OPT_IN}\\` in config/resources.ts, or install it once by hand with ` +\n \"`CREATE EXTENSION vector;`. Either way the server needs an image that ships the library — the scaffold's \" +\n \"`pgvector/pgvector:pg18` does, a stock `postgres:18` does not. Rebase then creates the column and its ANN \" +\n \"index automatically — see the `index` option on the property.\"\n );\n};\n\n/** One `{ type: \"vector\" }` property, as a column. */\nexport interface VectorColumnSpec {\n schema: string;\n table: string;\n column: string;\n dimensions: number;\n /** Rendered after the type, in the generator's order: UNIQUE then NOT NULL. */\n modifiers: string;\n}\n\n/**\n * Every vector column a collection declares.\n *\n * Wider than {@link buildVectorIndexPlan} on purpose: that one answers \"what\n * gets an ANN index\", and skips both a property with `index: false` and one too\n * wide to index. Either still needs its column.\n */\nexport const buildVectorColumnSpecs = (\n collection: CollectionConfig,\n resolveColumn: (propName: string, prop?: Property | null) => string\n): VectorColumnSpec[] => {\n const table = getTableName(collection);\n const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\";\n const specs: VectorColumnSpec[] = [];\n\n for (const [propName, prop] of Object.entries(collection.properties ?? {})) {\n if (!isVectorProperty(prop)) continue;\n // Assembled in the CREATE TABLE generator's order, so the column this\n // file adds and the column `schema.sql` used to declare are the same\n // column. A vector property is never a primary key and carries no\n // SQL-level default, which is what leaves only these two.\n const validation = (prop as { validation?: { required?: boolean; unique?: boolean } }).validation;\n let modifiers = \"\";\n if (validation?.unique) modifiers += \" UNIQUE\";\n if (validation?.required) modifiers += \" NOT NULL\";\n specs.push({\n schema,\n table,\n column: resolveColumn(propName, prop as Property),\n dimensions: prop.dimensions,\n modifiers\n });\n }\n\n return specs;\n};\n\n/** The column type — the one place `VECTOR(n)` is spelled. */\nexport const vectorColumnType = (spec: Pick<VectorColumnSpec, \"dimensions\">): string =>\n `VECTOR(${spec.dimensions})`;\n\n/** The column definition as it appears inside `CREATE TABLE`. */\nexport const vectorColumnDefinition = (spec: VectorColumnSpec): string =>\n `\"${spec.column}\" ${vectorColumnType(spec)}${spec.modifiers}`;\n\n/**\n * Refuse — or perform — a `dimensions` change that `ADD COLUMN IF NOT EXISTS`\n * would otherwise swallow.\n *\n * Without this the file *launders* the change: the ADD is a no-op against a\n * column that exists, so a project that went from 384 to 768 dimensions would\n * push clean, keep a 384-wide column, and fail on the next insert with a\n * message about the row rather than about the config. Atlas used to catch this\n * — it owned the column and planned the `ALTER … TYPE` — and taking the column\n * out of its sight is exactly what makes the guard necessary.\n *\n * The widening is performed when the column holds no values, because that is\n * the case Atlas handled and it is the common one: a developer changing\n * embedding models before there are any embeddings. With values present the\n * conversion is pgvector's to reject — every stored vector is the old width —\n * so this refuses first and names the statement to run afterwards.\n *\n * `atttypmod` on a `vector` column *is* the dimension count: pgvector stores it\n * directly rather than offsetting it the way `varchar` does. `-1` means the\n * column was declared as a bare `vector`, which is drift in the same sense.\n */\nexport const vectorDimensionGuard = (spec: VectorColumnSpec): string => {\n const relation = `\"${spec.schema}\".\"${spec.table}\"`;\n return `DO $rebase_vector$\nDECLARE actual int;\nBEGIN\n SELECT a.atttypmod INTO actual\n FROM pg_attribute a\n JOIN pg_type t ON t.oid = a.atttypid\n WHERE a.attrelid = ${quote(relation)}::regclass\n AND a.attname = ${quote(spec.column)}\n AND NOT a.attisdropped\n AND t.typname = 'vector';\n IF actual IS NOT NULL AND actual <> ${spec.dimensions} THEN\n IF EXISTS (SELECT 1 FROM ${relation} WHERE \"${spec.column}\" IS NOT NULL) THEN\n RAISE EXCEPTION 'Rebase: ${spec.schema}.${spec.table}.\"${spec.column}\" declares ${spec.dimensions} dimensions but the column is vector(%), and it already holds values of the old width. pgvector cannot convert them. Re-embed the rows at ${spec.dimensions} dimensions (or clear the column), then re-apply: ALTER TABLE ${relation} ALTER COLUMN \"${spec.column}\" TYPE ${vectorColumnType(spec)};', actual;\n END IF;\n ALTER TABLE ${relation} ALTER COLUMN \"${spec.column}\" TYPE ${vectorColumnType(spec)};\n END IF;\nEND\n$rebase_vector$;`;\n};\n\n/**\n * The object names a collection's vector properties own, unqualified.\n *\n * What Atlas has to be told to exclude. Only the column and the ANN indexes: a\n * `UNIQUE` or `NOT NULL` on the column is a property *of* the column, and\n * excluding the column takes them with it — measured against Atlas 1.2.3 and\n * 1.3.2, where a target carrying `vector(3) NOT NULL UNIQUE` and a `schema.sql`\n * carrying neither reported \"Schema is synced, no changes to be made\".\n */\nexport const vectorObjectNames = (\n collection: CollectionConfig,\n resolveColumn: (propName: string, prop?: Property | null) => string\n): string[] => [\n ...buildVectorColumnSpecs(collection, resolveColumn).map(spec => spec.column),\n ...vectorIndexNames(buildVectorIndexPlan(collection, resolveColumn))\n];\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"vector-index-BFhyEaiz.js","names":[],"sources":["../src/schema/vector-index.ts"],"sourcesContent":["/**\n * The one place a `{ type: \"vector\" }` property becomes an ANN index.\n *\n * Without an index, pgvector answers `ORDER BY embedding <=> $1` by computing\n * the distance to every row and sorting — exact, and linear. That is the right\n * answer at ten thousand rows and the wrong one at a million, which is why the\n * default here is to build an index rather than to leave the column bare.\n *\n * ## The operator class is not a detail\n *\n * An index is built for exactly one operator class, and the planner uses it\n * only for the operator that class implements. `vector_cosine_ops` answers\n * `<=>` and nothing else; a query asking for `<->` against a cosine index gets\n * a sequential scan and no error. So the default indexed distance here is\n * `cosine`, because `cosine` is what `vectorSearch` measures with when the\n * caller does not say — see `DrizzleConditionBuilder.buildVectorSearch`. The\n * two defaults have to agree, and this comment is the reason they do.\n *\n * ## Why 2000 dimensions is a ceiling and not an error\n *\n * pgvector cannot index a `vector` wider than 2000 dimensions with either\n * method. A 3072-dimension embedding (`text-embedding-3-large`) is therefore\n * storable and searchable but not indexable. Refusing the boot over that would\n * make a working configuration unbootable; silently indexing it is impossible.\n * So the column is created, the index is skipped, and the reason is reported.\n *\n * Like `search-column.ts`, this module exists so that the DDL generator and the\n * boot-time ensure render the *same* specification rather than describing the\n * same index twice, differently. `contracts/derived-names.txt` records the\n * names both produce, and CI fails if they diverge.\n *\n * ## And why the column itself is described here too\n *\n * `VECTOR(n)` is a type Atlas cannot be shown. It has to materialise the\n * desired state in a dev database to diff against, that database is created\n * empty and *emptied again* by Atlas at the start of every run, and nothing in\n * the free tier can put `CREATE EXTENSION vector` back — so a `schema.sql`\n * mentioning the type fails with `type \"vector\" does not exist` on every push,\n * for good. Search hit the same wall for its own reasons and took the same way\n * out: the objects leave `schema.sql`, Atlas is told to exclude them, and\n * Rebase applies them itself. See `generatePostgresVectorDdl`.\n */\nimport type { CollectionConfig, Property, VectorDistance, VectorIndexConfig } from \"@rebasepro/types\";\nimport { isPostgresCollectionConfig } from \"@rebasepro/types\";\nimport { getTableName } from \"@rebasepro/common\";\nimport { toPostgresIdentifier } from \"@rebasepro/utils\";\n\n/**\n * The widest `vector` pgvector will build an HNSW or IVFFlat index over.\n * Storage and exact search are unaffected by this limit.\n */\nexport const MAX_INDEXABLE_VECTOR_DIMENSIONS = 2000;\n\n/** The distance indexed when a property does not name one. */\nexport const DEFAULT_VECTOR_DISTANCE: VectorDistance = \"cosine\";\n\n/** The index method used when a property does not name one. */\nexport const DEFAULT_VECTOR_INDEX_METHOD = \"hnsw\" as const;\n\n/**\n * Operator class per distance. These strings are part of the database contract:\n * they appear in `CREATE INDEX`, so renaming one renames an index.\n */\nconst OPERATOR_CLASS: Record<VectorDistance, string> = {\n cosine: \"vector_cosine_ops\",\n l2: \"vector_l2_ops\",\n inner_product: \"vector_ip_ops\"\n};\n\n/** Short, stable tag per distance, used to name the index. */\nconst DISTANCE_TAG: Record<VectorDistance, string> = {\n cosine: \"cosine\",\n l2: \"l2\",\n inner_product: \"ip\"\n};\n\nexport interface VectorIndexSpec {\n schema: string;\n table: string;\n column: string;\n indexName: string;\n method: \"hnsw\" | \"ivfflat\";\n distance: VectorDistance;\n operatorClass: string;\n /** Rendered into `WITH (...)`; empty when every parameter is defaulted. */\n parameters: Array<[string, number]>;\n}\n\n/** A vector column that will not be indexed, and why. */\nexport interface SkippedVectorIndex {\n schema: string;\n table: string;\n column: string;\n dimensions: number;\n reason: string;\n}\n\nexport interface VectorIndexPlan {\n specs: VectorIndexSpec[];\n skipped: SkippedVectorIndex[];\n}\n\nexport class VectorIndexConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"VectorIndexConfigError\";\n }\n}\n\nconst isVectorProperty = (prop: unknown): prop is Property & {\n type: \"vector\";\n dimensions: number;\n index?: VectorIndexConfig | false;\n} => !!prop && typeof prop === \"object\" && (prop as { type?: string }).type === \"vector\";\n\nconst asDistances = (config: VectorIndexConfig, label: string): VectorDistance[] => {\n const raw = config.distance ?? DEFAULT_VECTOR_DISTANCE;\n const list = Array.isArray(raw) ? raw : [raw];\n if (list.length === 0) {\n throw new VectorIndexConfigError(\n `${label}: \\`index.distance\\` is an empty array. Name at least one distance, or set \\`index: false\\` to create no index.`\n );\n }\n const seen = new Set<VectorDistance>();\n for (const distance of list) {\n if (!(distance in OPERATOR_CLASS)) {\n throw new VectorIndexConfigError(\n `${label}: \\`index.distance\\` is \"${distance}\", which is not a pgvector distance. Use ${Object.keys(OPERATOR_CLASS).map(d => `\"${d}\"`).join(\", \")}.`\n );\n }\n if (seen.has(distance)) {\n throw new VectorIndexConfigError(`${label}: \\`index.distance\\` lists \"${distance}\" twice.`);\n }\n seen.add(distance);\n }\n return list;\n};\n\nconst assertPositiveInteger = (value: number | undefined, key: string, label: string): void => {\n if (value === undefined) return;\n if (!Number.isInteger(value) || value <= 0) {\n throw new VectorIndexConfigError(\n `${label}: \\`index.${key}\\` is ${JSON.stringify(value)}. It must be a positive integer.`\n );\n }\n};\n\n/**\n * Index parameters for one method. Parameters belonging to the *other* method\n * are rejected rather than ignored, because a silently dropped `lists` on an\n * HNSW index reads, from the config, exactly like a tuned index.\n */\nconst parametersFor = (\n method: \"hnsw\" | \"ivfflat\",\n config: VectorIndexConfig,\n label: string\n): Array<[string, number]> => {\n assertPositiveInteger(config.m, \"m\", label);\n assertPositiveInteger(config.efConstruction, \"efConstruction\", label);\n assertPositiveInteger(config.lists, \"lists\", label);\n\n if (method === \"hnsw\") {\n if (config.lists !== undefined) {\n throw new VectorIndexConfigError(\n `${label}: \\`index.lists\\` only applies to \\`method: \"ivfflat\"\\`. Remove it, or switch the method.`\n );\n }\n const params: Array<[string, number]> = [];\n if (config.m !== undefined) params.push([\"m\", config.m]);\n if (config.efConstruction !== undefined) params.push([\"ef_construction\", config.efConstruction]);\n return params;\n }\n\n for (const key of [\"m\", \"efConstruction\"] as const) {\n if (config[key] !== undefined) {\n throw new VectorIndexConfigError(\n `${label}: \\`index.${key}\\` only applies to \\`method: \"hnsw\"\\`. Remove it, or switch the method.`\n );\n }\n }\n return config.lists !== undefined ? [[\"lists\", config.lists]] : [];\n};\n\n/**\n * Every ANN index a collection's vector properties call for.\n *\n * `resolveColumn` is passed in rather than imported so that this module stays\n * free of the DDL generator, which imports *it*. Both callers hand it the same\n * `resolveColumnName`, and a contract test asserts the names agree.\n */\nexport const buildVectorIndexPlan = (\n collection: CollectionConfig,\n resolveColumn: (propName: string, prop?: Property | null) => string\n): VectorIndexPlan => {\n const specs: VectorIndexSpec[] = [];\n const skipped: SkippedVectorIndex[] = [];\n const properties = collection.properties ?? {};\n\n const table = getTableName(collection);\n const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\";\n\n for (const [propName, prop] of Object.entries(properties)) {\n if (!isVectorProperty(prop)) continue;\n if (prop.index === false) continue;\n\n const label = `${collection.slug}.${propName}`;\n const column = resolveColumn(propName, prop as Property);\n const config: VectorIndexConfig = prop.index ?? {};\n const method = config.method ?? DEFAULT_VECTOR_INDEX_METHOD;\n\n if (method !== \"hnsw\" && method !== \"ivfflat\") {\n throw new VectorIndexConfigError(\n `${label}: \\`index.method\\` is \"${method}\". Use \"hnsw\" or \"ivfflat\".`\n );\n }\n\n // Read before the dimension gate so a malformed block is reported even\n // on a column too wide to index — the config is wrong either way.\n const distances = asDistances(config, label);\n const parameters = parametersFor(method, config, label);\n\n if (!Number.isInteger(prop.dimensions) || prop.dimensions <= 0) {\n throw new VectorIndexConfigError(\n `${label}: \\`dimensions\\` is ${JSON.stringify(prop.dimensions)}. It must be a positive integer.`\n );\n }\n\n if (prop.dimensions > MAX_INDEXABLE_VECTOR_DIMENSIONS) {\n skipped.push({\n schema,\n table,\n column,\n dimensions: prop.dimensions,\n reason:\n `pgvector cannot index a vector wider than ${MAX_INDEXABLE_VECTOR_DIMENSIONS} dimensions, and ` +\n `${label} declares ${prop.dimensions}. The column works and \\`vectorSearch\\` still answers, as an ` +\n `exact scan. To index it, reduce the dimensions (many embedding models support a shorter output) ` +\n `or set \\`index: false\\` to state that the scan is intended.`\n });\n continue;\n }\n\n for (const distance of distances) {\n specs.push({\n schema,\n table,\n column,\n // Distance is in the name because one column may carry an index\n // per distance, and two indexes cannot share a name. The method\n // is in it because switching methods is a different index, not\n // a redefinition of the same one — `CREATE INDEX IF NOT EXISTS`\n // would otherwise keep the old one and report success.\n indexName: toPostgresIdentifier(`${table}_${column}_${method}_${DISTANCE_TAG[distance]}`),\n method,\n distance,\n operatorClass: OPERATOR_CLASS[distance],\n parameters\n });\n }\n }\n\n return { specs, skipped };\n};\n\n/**\n * The `CREATE INDEX` for one spec.\n *\n * `CONCURRENTLY` is deliberately absent, for the same reason it is absent from\n * `searchIndexStatements`: this form is replayed as part of a migration, where\n * a concurrent build is not allowed. The boot-time ensure rewrites it — see\n * `ensureCollectionTables`.\n */\nexport const vectorIndexStatement = (spec: VectorIndexSpec): string => {\n const params = spec.parameters.length\n ? ` WITH (${spec.parameters.map(([key, value]) => `${key} = ${value}`).join(\", \")})`\n : \"\";\n return (\n `CREATE INDEX IF NOT EXISTS \"${spec.indexName}\" ON \"${spec.schema}\".\"${spec.table}\" ` +\n `USING ${spec.method} (\"${spec.column}\" ${spec.operatorClass})${params};`\n );\n};\n\n/** Every statement for a plan, in a stable order. */\nexport const vectorIndexStatements = (plan: VectorIndexPlan): string[] =>\n plan.specs.map(vectorIndexStatement);\n\n/** The index names a plan creates — what the derived-names contract records. */\nexport const vectorIndexNames = (plan: VectorIndexPlan): string[] =>\n plan.specs.map(spec => spec.indexName);\n\n// ── The column, for the file Rebase applies itself ──────────────────────────\n\nconst quote = (v: string): string => `'${v.replace(/'/g, \"''\")}'`;\n\n/**\n * The schema pgvector's types are installed into.\n *\n * `WITH SCHEMA public` for the same load-bearing reason `searchExtensionStatements`\n * gives: an unqualified `CREATE EXTENSION` lands in the first schema on\n * `search_path`, which is `\"$user\", public` — and the scaffold's role is named\n * `rebase`, the same as a schema the generator creates. Left to itself the\n * extension would install into `rebase`, and every unqualified `VECTOR(n)`\n * below would fail to resolve.\n */\nexport const VECTOR_EXTENSION_SCHEMA = \"public\";\n\n/** The extension name a database has to name to let Rebase install pgvector. */\nexport const VECTOR_EXTENSION = \"vector\";\n\n/**\n * How a project says Rebase may install pgvector, quoted into the messages that\n * have to name it. Spelled once so the option and the advice cannot drift.\n */\nexport const VECTOR_EXTENSION_OPT_IN = `database({ extensions: [\"${VECTOR_EXTENSION}\"] })`;\n\n/** Did the project give Rebase leave to install pgvector? */\nexport const vectorExtensionDeclared = (extensions: readonly string[] | undefined): boolean =>\n (extensions ?? []).includes(VECTOR_EXTENSION);\n\n/**\n * Install pgvector — emitted only when the database asked for it.\n *\n * Opt-in because installing an extension is a decision with a deployment behind\n * it: the image has to ship the library, the role has to be allowed to install\n * it, and a managed provider has to have it on an allow-list. None of that is\n * visible from inside the connection, so Rebase does not decide it. See\n * `DatabaseOptions.extensions`.\n *\n * Withholding the statement is not withholding the *column*: the column is\n * still created, and Postgres refuses it with `type \"vector\" does not exist`\n * on a database where pgvector was never installed by hand. That error is the\n * one this design accepts, and `vectorExtensionHint` is what makes it name\n * {@link VECTOR_EXTENSION_OPT_IN} rather than nothing.\n */\nexport const vectorExtensionStatement = (): string =>\n `CREATE EXTENSION IF NOT EXISTS ${VECTOR_EXTENSION} WITH SCHEMA ${VECTOR_EXTENSION_SCHEMA};`;\n\n/**\n * The missing-pgvector explanation, appended to whichever error revealed it.\n *\n * Two readers, needing opposite things, which is why the branch is on the error\n * text rather than on the configuration:\n *\n * - **`type \"vector\" does not exist`** — nobody opted in and the database has\n * no pgvector. The fix is one line of config they cannot guess, so naming\n * the option *is* the hint.\n * - **the install itself failed** — the config is already right, and repeating\n * the option would send them to edit a correct line. What is missing is the\n * library on the server, or the grant.\n *\n * Lives here rather than beside either caller because both need it: the boot\n * ensure raises the first through its action applier, and `rebase db push`\n * raises it out of `applyVectorDdl`. A hint on only one path is how a bare\n * `type \"vector\" does not exist` reaches somebody — which is the thing this\n * exists to prevent.\n */\nexport const vectorExtensionHint = (message: string): string => {\n const installRefused = /extension \"vector\" is not available/i.test(message)\n || /(permission denied to create extension|must be (superuser|owner).{0,40}extension)/i.test(message);\n if (installRefused) {\n return (\n \"\\n pgvector was declared and could not be installed. It is a server extension, so it needs an image \" +\n \"that ships the library (the scaffold's `pgvector/pgvector:pg18` does; a stock `postgres:18` does not) \" +\n \"and a role allowed to run `CREATE EXTENSION vector;`. Managed Postgres usually allows it once the \" +\n \"extension is on the provider's allow-list.\"\n );\n }\n if (!/type \"(vector|halfvec|sparsevec)\" does not exist/i.test(message)) return \"\";\n return (\n \"\\n pgvector is not installed on this database, and Rebase installs it only where a database says it may: \" +\n `add \\`${VECTOR_EXTENSION_OPT_IN}\\` in config/resources.ts, or install it once by hand with ` +\n \"`CREATE EXTENSION vector;`. Either way the server needs an image that ships the library — the scaffold's \" +\n \"`pgvector/pgvector:pg18` does, a stock `postgres:18` does not. Rebase then creates the column and its ANN \" +\n \"index automatically — see the `index` option on the property.\"\n );\n};\n\n/** One `{ type: \"vector\" }` property, as a column. */\nexport interface VectorColumnSpec {\n schema: string;\n table: string;\n column: string;\n dimensions: number;\n /** Rendered after the type, in the generator's order: UNIQUE then NOT NULL. */\n modifiers: string;\n}\n\n/**\n * Every vector column a collection declares.\n *\n * Wider than {@link buildVectorIndexPlan} on purpose: that one answers \"what\n * gets an ANN index\", and skips both a property with `index: false` and one too\n * wide to index. Either still needs its column.\n */\nexport const buildVectorColumnSpecs = (\n collection: CollectionConfig,\n resolveColumn: (propName: string, prop?: Property | null) => string\n): VectorColumnSpec[] => {\n const table = getTableName(collection);\n const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\";\n const specs: VectorColumnSpec[] = [];\n\n for (const [propName, prop] of Object.entries(collection.properties ?? {})) {\n if (!isVectorProperty(prop)) continue;\n // Assembled in the CREATE TABLE generator's order, so the column this\n // file adds and the column `schema.sql` used to declare are the same\n // column. A vector property is never a primary key and carries no\n // SQL-level default, which is what leaves only these two.\n const validation = (prop as { validation?: { required?: boolean; unique?: boolean } }).validation;\n let modifiers = \"\";\n if (validation?.unique) modifiers += \" UNIQUE\";\n if (validation?.required) modifiers += \" NOT NULL\";\n specs.push({\n schema,\n table,\n column: resolveColumn(propName, prop as Property),\n dimensions: prop.dimensions,\n modifiers\n });\n }\n\n return specs;\n};\n\n/** The column type — the one place `VECTOR(n)` is spelled. */\nexport const vectorColumnType = (spec: Pick<VectorColumnSpec, \"dimensions\">): string =>\n `VECTOR(${spec.dimensions})`;\n\n/** The column definition as it appears inside `CREATE TABLE`. */\nexport const vectorColumnDefinition = (spec: VectorColumnSpec): string =>\n `\"${spec.column}\" ${vectorColumnType(spec)}${spec.modifiers}`;\n\n/**\n * Refuse — or perform — a `dimensions` change that `ADD COLUMN IF NOT EXISTS`\n * would otherwise swallow.\n *\n * Without this the file *launders* the change: the ADD is a no-op against a\n * column that exists, so a project that went from 384 to 768 dimensions would\n * push clean, keep a 384-wide column, and fail on the next insert with a\n * message about the row rather than about the config. Atlas used to catch this\n * — it owned the column and planned the `ALTER … TYPE` — and taking the column\n * out of its sight is exactly what makes the guard necessary.\n *\n * The widening is performed when the column holds no values, because that is\n * the case Atlas handled and it is the common one: a developer changing\n * embedding models before there are any embeddings. With values present the\n * conversion is pgvector's to reject — every stored vector is the old width —\n * so this refuses first and names the statement to run afterwards.\n *\n * `atttypmod` on a `vector` column *is* the dimension count: pgvector stores it\n * directly rather than offsetting it the way `varchar` does. `-1` means the\n * column was declared as a bare `vector`, which is drift in the same sense.\n */\nexport const vectorDimensionGuard = (spec: VectorColumnSpec): string => {\n const relation = `\"${spec.schema}\".\"${spec.table}\"`;\n return `DO $rebase_vector$\nDECLARE actual int;\nBEGIN\n SELECT a.atttypmod INTO actual\n FROM pg_attribute a\n JOIN pg_type t ON t.oid = a.atttypid\n WHERE a.attrelid = ${quote(relation)}::regclass\n AND a.attname = ${quote(spec.column)}\n AND NOT a.attisdropped\n AND t.typname = 'vector';\n IF actual IS NOT NULL AND actual <> ${spec.dimensions} THEN\n IF EXISTS (SELECT 1 FROM ${relation} WHERE \"${spec.column}\" IS NOT NULL) THEN\n RAISE EXCEPTION 'Rebase: ${spec.schema}.${spec.table}.\"${spec.column}\" declares ${spec.dimensions} dimensions but the column is vector(%), and it already holds values of the old width. pgvector cannot convert them. Re-embed the rows at ${spec.dimensions} dimensions (or clear the column), then re-apply: ALTER TABLE ${relation} ALTER COLUMN \"${spec.column}\" TYPE ${vectorColumnType(spec)};', actual;\n END IF;\n ALTER TABLE ${relation} ALTER COLUMN \"${spec.column}\" TYPE ${vectorColumnType(spec)};\n END IF;\nEND\n$rebase_vector$;`;\n};\n\n/**\n * The object names a collection's vector properties own, unqualified.\n *\n * What Atlas has to be told to exclude. Only the column and the ANN indexes: a\n * `UNIQUE` or `NOT NULL` on the column is a property *of* the column, and\n * excluding the column takes them with it — measured against Atlas 1.2.3 and\n * 1.3.2, where a target carrying `vector(3) NOT NULL UNIQUE` and a `schema.sql`\n * carrying neither reported \"Schema is synced, no changes to be made\".\n */\nexport const vectorObjectNames = (\n collection: CollectionConfig,\n resolveColumn: (propName: string, prop?: Property | null) => string\n): string[] => [\n ...buildVectorColumnSpecs(collection, resolveColumn).map(spec => spec.column),\n ...vectorIndexNames(buildVectorIndexPlan(collection, resolveColumn))\n];\n"],"mappings":"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"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
|
-
import {
|
|
4
|
-
import { A as budgetSendBuffers, M as terminateIfBacklogged, N as assertReadRequestReadable, ct as SqlResultTooLargeError, it as effectiveSqlRole, j as reapSilentSockets, lt as isNestedPath, ot as sqlRoleRanAs, st as SQL_RESULT_BYTE_BUDGET, ut as resolveNestedPath } from "./open-rebase-
|
|
5
|
-
import { r as BranchingUnsupportedError } from "./BranchService-
|
|
6
|
-
import { ApiError, RUNTIME_DEFAULT_MAX_BODY_SIZE, assertFieldOpsValid, assertNestedWriteAllowed, assertNoFieldOpsOnCreate, assertNoUserUpsert, assertUserCreationBodyValid, assertWriteRequestValid, createUserThroughAuthCollection, decideImpersonation, declaredErrorAnswer, extractUserFromToken, getAccessModel, logger, resolveConflictTarget, resolveRequireAuth, safeCompare } from "@rebasepro/server";
|
|
3
|
+
import { n as __exportAll } from "./rolldown-runtime-V7rFbMRe.js";
|
|
4
|
+
import { A as budgetSendBuffers, M as terminateIfBacklogged, N as assertReadRequestReadable, ct as SqlResultTooLargeError, it as effectiveSqlRole, j as reapSilentSockets, lt as isNestedPath, ot as sqlRoleRanAs, st as SQL_RESULT_BYTE_BUDGET, ut as resolveNestedPath } from "./open-rebase-Cwnqo5LU.js";
|
|
5
|
+
import { r as BranchingUnsupportedError } from "./BranchService-DO_3_CXt.js";
|
|
6
|
+
import { ApiError, DATABASE_UNAVAILABLE_MESSAGE, RUNTIME_DEFAULT_MAX_BODY_SIZE, assertFieldOpsValid, assertNestedWriteAllowed, assertNoFieldOpsOnCreate, assertNoUserUpsert, assertUserCreationBodyValid, assertWriteRequestValid, createUserThroughAuthCollection, decideImpersonation, declaredErrorAnswer, extractUserFromToken, getAccessModel, isDatabaseUnreachable, logger, resolveConflictTarget, resolveRequireAuth, safeCompare } from "@rebasepro/server";
|
|
7
7
|
import { ANONYMOUS_USER_ID, ListLimitError, hasAdminRole, isSQLAdmin, isSchemaAdmin, resolveClientListLimit, scopeGrants, scopesForRoles } from "@rebasepro/types";
|
|
8
8
|
import { UnknownFilterOperatorError, deserializeFilter, redactSqlLiterals } from "@rebasepro/common";
|
|
9
9
|
import { unref } from "@rebasepro/utils";
|
|
@@ -257,6 +257,27 @@ function jwtExpiryMs(token) {
|
|
|
257
257
|
return;
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* The refusal for an auth store that could not answer, as the HTTP doors give
|
|
262
|
+
* it: the 503 the store's own error declares (`DATABASE_UNAVAILABLE`,
|
|
263
|
+
* `ROLE_LOOKUP_FAILED`), else the database unreachable when that is what the
|
|
264
|
+
* error says, else the lookup failing.
|
|
265
|
+
*/
|
|
266
|
+
function storeUnavailableAnswer(error) {
|
|
267
|
+
const declared = declaredErrorAnswer(error);
|
|
268
|
+
if (declared && declared.status === 503) return {
|
|
269
|
+
code: declared.code,
|
|
270
|
+
message: declared.message
|
|
271
|
+
};
|
|
272
|
+
if (isDatabaseUnreachable(error)) return {
|
|
273
|
+
code: "DATABASE_UNAVAILABLE",
|
|
274
|
+
message: DATABASE_UNAVAILABLE_MESSAGE
|
|
275
|
+
};
|
|
276
|
+
return {
|
|
277
|
+
code: "ROLE_LOOKUP_FAILED",
|
|
278
|
+
message: "Could not verify your permissions. Please try again."
|
|
279
|
+
};
|
|
280
|
+
}
|
|
260
281
|
/** Everything about an identity that decides what its frames may do. */
|
|
261
282
|
function identityFingerprint(user) {
|
|
262
283
|
return JSON.stringify([
|
|
@@ -680,8 +701,9 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
|
|
|
680
701
|
* and the socket's subscriptions are re-scoped to them.
|
|
681
702
|
* - `"ended"`: refused — revoked, signed out, deleted, expired. The
|
|
682
703
|
* socket is closed.
|
|
683
|
-
* - `
|
|
684
|
-
*
|
|
704
|
+
* - `{ unavailable }`: the store could not answer, and this is the
|
|
705
|
+
* refusal that says why. The frame is refused, the session kept, and
|
|
706
|
+
* the next frame or sweep asks again.
|
|
685
707
|
*/
|
|
686
708
|
async function recheckIdentity(clientId, resolve = resolveIdentity) {
|
|
687
709
|
const session = clientSessions.get(clientId);
|
|
@@ -690,8 +712,8 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
|
|
|
690
712
|
let outcome;
|
|
691
713
|
try {
|
|
692
714
|
outcome = await resolve(credential, session.impersonate);
|
|
693
|
-
} catch {
|
|
694
|
-
return
|
|
715
|
+
} catch (error) {
|
|
716
|
+
return { unavailable: storeUnavailableAnswer(error) };
|
|
695
717
|
}
|
|
696
718
|
if (session.credential !== credential || !session.user) return session.user ? "live" : "ended";
|
|
697
719
|
session.checkedAt = Date.now();
|
|
@@ -927,7 +949,13 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
|
|
|
927
949
|
clientId
|
|
928
950
|
});
|
|
929
951
|
} catch (error) {
|
|
930
|
-
const
|
|
952
|
+
const declared = declaredErrorAnswer(error);
|
|
953
|
+
if (declared && declared.status === 503 || isDatabaseUnreachable(error)) {
|
|
954
|
+
const unavailable = storeUnavailableAnswer(error);
|
|
955
|
+
sendError("ERROR", unavailable.code, unavailable.message);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
const answer = impersonate === void 0 ? void 0 : declared;
|
|
931
959
|
if (answer) {
|
|
932
960
|
sendError("AUTH_ERROR", answer.code, answer.message);
|
|
933
961
|
return;
|
|
@@ -982,8 +1010,8 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
|
|
|
982
1010
|
if (session?.user && session.credential && Date.now() - session.checkedAt >= (CHANNEL_MESSAGE_TYPES.has(type) ? CHANNEL_RECHECK_MIN_MS : 0)) {
|
|
983
1011
|
const outcome = await recheckIdentityForFrame(clientId);
|
|
984
1012
|
if (outcome === "ended") return;
|
|
985
|
-
if (outcome
|
|
986
|
-
sendError("ERROR",
|
|
1013
|
+
if (outcome !== "live") {
|
|
1014
|
+
sendError("ERROR", outcome.unavailable.code, outcome.unavailable.message);
|
|
987
1015
|
return;
|
|
988
1016
|
}
|
|
989
1017
|
}
|
|
@@ -1557,7 +1585,7 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
|
|
|
1557
1585
|
}));
|
|
1558
1586
|
return;
|
|
1559
1587
|
}
|
|
1560
|
-
const answer = declaredErrorAnswer(error);
|
|
1588
|
+
const answer = declaredErrorAnswer(error) ?? (isDatabaseUnreachable(error) ? declaredErrorAnswer(ApiError.databaseUnavailable()) : void 0);
|
|
1561
1589
|
if (answer) {
|
|
1562
1590
|
const line = `[WebSocket Server] Refused: ${answer.code} ${answer.message}`;
|
|
1563
1591
|
if (answer.expected) logger.debug(line);
|
|
@@ -1601,4 +1629,4 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
|
|
|
1601
1629
|
//#endregion
|
|
1602
1630
|
export { createPostgresWebSocket as a, websocket_exports as c, TOO_MANY_SOCKETS_CLOSE_CODE as i, PUBLIC_TYPES as n, isSocketRealtimeService as o, SESSION_ENDED_CLOSE_CODE as r, socketRealtimeMissing as s, ADMIN_PLANE_TYPES as t };
|
|
1603
1631
|
|
|
1604
|
-
//# sourceMappingURL=websocket-
|
|
1632
|
+
//# sourceMappingURL=websocket-BT85Yv_G.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket-BT85Yv_G.js","names":[],"sources":["../src/socket-opt-ins.ts","../src/websocket.ts"],"sourcesContent":["import { WebSocket } from \"ws\";\nimport { unref } from \"@rebasepro/utils\";\n\n/**\n * Behaviours a client opts into when it opens the socket.\n *\n * The client names them on the socket URL, `?rebase_caps=heartbeat,channel_seq,cancel_sql`,\n * and the server answers with a `hello` frame listing the ones it will honour:\n *\n * ```json\n * { \"type\": \"hello\", \"capabilities\": [\"heartbeat\", \"channel_seq\", \"cancel_sql\"], \"heartbeatMs\": 30000, \"clientId\": \"client_…\" }\n * ```\n *\n * Both halves are needed, so both directions of a version mismatch keep\n * working. An older client names none and gets no `hello`, no heartbeat and no\n * acknowledgement frames — nothing it would not recognise. An older server\n * ignores the query parameter and sends no `hello`, and a newer client that\n * hears none behaves as it always did.\n *\n * One list for both kinds: what the server sends (`heartbeat`, `channel_seq`)\n * and the frames it takes beyond the base protocol (`cancel_sql`). Named before\n * sign-in, so a socket that never signs in is served as one that does.\n *\n * - `heartbeat`: a `{ \"type\": \"heartbeat\" }` frame every `heartbeatMs`. A\n * browser cannot send a WebSocket ping or see one, so a connection that went\n * half-open — a NAT that forgot it, a laptop that slept — looks connected to\n * it forever. With these, two missed beats tell it to redial.\n * - `channel_seq`: on a retained channel, the sender of a broadcast gets\n * `{ \"type\": \"broadcast_ack\", \"channel\", \"seq\" }` where every other member\n * gets the message. Its own messages then leave no hole in the sequence it\n * watches, so a hole means a message it really missed — see\n * `RealtimeChannel` in the SDK.\n * - `cancel_sql`: the server takes `CANCEL_SQL { requestId }`, which stops a\n * console run (`EXECUTE_SQL`, `mode: \"script\"`) of the same socket. A client\n * sends it only to a server whose `hello` listed it.\n *\n * @module\n */\n\n/** The query parameter a client names its opt-ins in. */\nexport const SOCKET_OPT_IN_PARAM = \"rebase_caps\";\n\n/** Every behaviour this server can be asked for. */\nexport const SOCKET_OPT_INS = [\"heartbeat\", \"channel_seq\", \"cancel_sql\"] as const;\n\nexport type SocketOptIn = typeof SOCKET_OPT_INS[number];\n\n/** How often a socket that asked for heartbeats is sent one, by default. */\nexport const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;\n\nfunction isSocketOptIn(name: string): name is SocketOptIn {\n return (SOCKET_OPT_INS as readonly string[]).includes(name);\n}\n\n/**\n * What the client that opened this connection asked for, from its request\n * URL: `undefined` when it named nothing (a client that predates this, which\n * is sent no `hello`), otherwise the names this server honours — possibly\n * none.\n */\nexport function requestedOptIns(url: string | undefined): Set<SocketOptIn> | undefined {\n if (!url) return undefined;\n const query = url.indexOf(\"?\");\n if (query === -1) return undefined;\n const raw = new URLSearchParams(url.slice(query + 1)).get(SOCKET_OPT_IN_PARAM);\n if (raw === null) return undefined;\n return new Set(raw.split(\",\").map((name) => name.trim()).filter(isSocketOptIn));\n}\n\n/**\n * The `hello` frame for a client that asked for `optIns`. With `channel_seq`\n * it also names the socket's own id, the `senderId` its broadcasts carry in a\n * channel's history, so a client filling a hole from history can tell its own\n * messages (which it is never sent live) from the ones it missed.\n */\nexport function helloFrame(optIns: ReadonlySet<SocketOptIn>, heartbeatMs: number, clientId: string): string {\n return JSON.stringify({\n type: \"hello\",\n capabilities: [...optIns],\n ...(optIns.has(\"heartbeat\") ? { heartbeatMs } : {}),\n ...(optIns.has(\"channel_seq\") ? { clientId } : {})\n });\n}\n\nconst HEARTBEAT_FRAME = JSON.stringify({ type: \"heartbeat\" });\n\n/**\n * Send a heartbeat every `intervalMs` to each socket `sockets()` lists. One\n * timer for the whole server, unref'd so it never holds the process open.\n *\n * @returns Stops it.\n */\nexport function startHeartbeats(sockets: () => Iterable<WebSocket>, intervalMs: number): () => void {\n const timer = setInterval(() => {\n for (const ws of sockets()) {\n if (ws.readyState !== WebSocket.OPEN) continue;\n try {\n ws.send(HEARTBEAT_FRAME);\n } catch {\n // A socket closing under the send: its close handler cleans up.\n }\n }\n }, intervalMs);\n unref(timer);\n return () => clearInterval(timer);\n}\n","import type { SubscriptionAuthContext } from \"./services/realtimeService\";\nimport { BranchingUnsupportedError } from \"./services/BranchService\";\nimport { SQL_RESULT_BYTE_BUDGET, SqlResultTooLargeError } from \"./services/sql-script\";\nimport { PostgresBackendDriver, effectiveSqlRole, sqlRoleRanAs } from \"./PostgresBackendDriver\";\nimport { assertReadRequestReadable } from \"./services/read-field-access\";\nimport { isNestedPath, resolveNestedPath } from \"./services/nested-path\";\nimport type { PostgresCollectionRegistry } from \"./collections/PostgresCollectionRegistry\";\nimport type { CollectionConfig, DataDriver, DeleteProps, FetchCollectionProps, FetchOneProps, SaveProps, TableMetadata, BranchInfo, AuthAdapter, DataRateLimitCaller, RealtimeProvider, RealtimeSocketOptions, WebSocketMessage } from \"@rebasepro/types\";\nimport type { SqlScriptResult } from \"@rebasepro/types\";\nimport { deserializeFilter, redactSqlLiterals, UnknownFilterOperatorError } from \"@rebasepro/common\";\nimport { budgetSendBuffers, reapSilentSockets, terminateIfBacklogged } from \"./services/socket-liveness\";\nimport { DEFAULT_HEARTBEAT_INTERVAL_MS, helloFrame, requestedOptIns, startHeartbeats, type SocketOptIn } from \"./socket-opt-ins\";\nimport { ANONYMOUS_USER_ID, hasAdminRole, isSQLAdmin, isSchemaAdmin, resolveClientListLimit, ListLimitError, scopeGrants, scopesForRoles, type AccessModel, type AdminScope } from \"@rebasepro/types\";\nimport type { User } from \"@rebasepro/types\";\n\nimport { WebSocketServer, WebSocket, type RawData } from \"ws\";\nimport type { Server, IncomingMessage } from \"http\";\nimport type { Duplex } from \"stream\";\nimport { inspect } from \"util\";\nimport { extractUserFromToken, AccessTokenPayload, safeCompare, resolveRequireAuth, assertWriteRequestValid, assertFieldOpsValid, assertNoFieldOpsOnCreate, declaredErrorAnswer, RUNTIME_DEFAULT_MAX_BODY_SIZE, ApiError, resolveConflictTarget, assertNestedWriteAllowed, assertNoUserUpsert, assertUserCreationBodyValid, createUserThroughAuthCollection, type NestedWriteKind } from \"@rebasepro/server\";\nimport { logger, getAccessModel, decideImpersonation, isDatabaseUnreachable, DATABASE_UNAVAILABLE_MESSAGE, type ImpersonationCredential } from \"@rebasepro/server\";\n\n/**\n * What this socket calls on the realtime side, and all of it.\n *\n * One `RealtimeService`, or — on a project with several realtime-capable\n * sources — the routed composite over them (`createRoutedRealtimeService`).\n * The parameter used to be typed as the one implementation and the composite\n * was cast to it, so when `AUTHENTICATE` began calling `rescopeClient`, which\n * the composite never had, nothing failed until every multi-source sign-in\n * answered INTERNAL_ERROR. Declared here, by the caller, so what is handed in\n * is checked against what is called.\n */\nexport interface SocketRealtimeService {\n addClient(clientId: string, ws: WebSocket): void;\n rescopeClient(clientId: string, authContext: SubscriptionAuthContext): Promise<void>;\n handleClientMessage(clientId: string, message: WebSocketMessage, authContext?: SubscriptionAuthContext): Promise<void> | void;\n /**\n * What the client opted into when it opened the socket (see\n * `socket-opt-ins.ts`). Optional: a service that sends nothing that\n * depends on one need not hear about them.\n */\n setClientOptIns?(clientId: string, optIns: ReadonlySet<SocketOptIn>): void;\n}\n\n/**\n * Narrow a bootstrapper's realtime provider to what the socket calls.\n *\n * The provider arrives typed as the engine-agnostic `RealtimeProvider`, which\n * declares none of the client-connection methods. Checked rather than cast, so\n * a provider missing one refuses at boot, naming it, instead of on a client's\n * first frame.\n */\nexport function socketRealtimeMissing(provider: RealtimeProvider): string[] {\n return ([\"addClient\", \"rescopeClient\", \"handleClientMessage\"] as const)\n .filter((method) => !(method in provider) || typeof Reflect.get(provider, method) !== \"function\");\n}\n\nexport function isSocketRealtimeService(provider: RealtimeProvider): provider is RealtimeProvider & SocketRealtimeService {\n return socketRealtimeMissing(provider).length === 0;\n}\n\n/** What a script run says besides its rows, which an `EXECUTE_SQL` answer carries as `result`. */\nfunction scriptDescription({ rows: _rows, ...description }: SqlScriptResult): Omit<SqlScriptResult, \"rows\"> {\n return description;\n}\n\n/** Minimal subset of RebaseAuthConfig used by the WebSocket layer. */\ninterface WsAuthConfig {\n requireAuth?: boolean;\n jwtSecret?: string;\n /**\n * Same static server-to-server secret the HTTP middleware accepts. Without\n * it here, a service key authenticates over HTTP but not over the socket —\n * so any SDK client using one (scripts, cron, server-to-server) connects,\n * fails realtime auth with \"jwt malformed\", and silently gets no events.\n */\n serviceKey?: string;\n}\n\n/**\n * Normalized user identity for WebSocket sessions.\n */\ninterface WsUserIdentity {\n uid: string;\n roles: string[];\n isAdmin: boolean;\n /**\n * What this session may do, when its credential narrows its person — an\n * API key. Absent for a person's own session, who holds what their roles\n * hold.\n */\n scopes?: string[];\n /**\n * Whether this session is a guest — anonymous sign-in rather than an\n * account. Read from the token, because a socket has no request to look\n * anything up on and every refetch it triggers needs the same principal the\n * initial HTTP fetch had. Required, so every way of signing in has to say:\n * the adapter path left it out, and its guests were accounts.\n */\n isAnonymous: boolean;\n /**\n * The token's custom claims, for the same reason `isAnonymous` is here: a\n * socket has no request to look anything up on, and every refetch it\n * triggers has to evaluate the same policies against the same principal.\n * A tenancy policy reads a claim.\n */\n claims?: Record<string, unknown>;\n /**\n * The administrator this session acts for, when it authenticated with\n * `impersonate`: every other field is then the impersonated user's.\n */\n impersonator?: string;\n /**\n * The API key this session authenticated with: its frames count in the\n * key's own rate-limit bucket, at the key's own `rate_limit`, as its HTTP\n * requests do.\n */\n apiKey?: { id: string; rate_limit: number | null };\n}\n\n/**\n * The uid the service key authenticates as, here and through the built-in\n * auth adapter — which is how the socket tells that credential apart from a\n * person's session.\n */\nconst SERVICE_KEY_UID = \"service\";\n\ninterface ClientSession {\n ws: WebSocket;\n user?: WsUserIdentity;\n authenticated: boolean;\n /**\n * The credential `user` was resolved from, kept so the identity can be\n * re-read: every frame asks again, and so does the sweep for a socket\n * that only listens. See `recheckIdentity`.\n */\n credential?: string;\n /**\n * The uid `AUTHENTICATE` asked to act as, kept beside the credential so\n * every re-check decides the impersonation again — a demoted\n * administrator, or a disabled target, ends the session.\n */\n impersonate?: string;\n /** When `user` was last confirmed against the credential, in ms. */\n checkedAt: number;\n /** Ends the session the instant the credential expires. */\n expiryTimer?: ReturnType<typeof setTimeout>;\n /** Sliding window message counter for rate limiting */\n messageCount: number;\n messageWindowStart: number;\n /** The same window, counted separately for channel frames. */\n channelMessageCount: number;\n channelWindowStart: number;\n /**\n * How many more fetching realtime frames this socket may send before\n * they count in its caller's data bucket, and when that was last topped\n * up. See {@link SOCKET_FETCH_ALLOWANCE}.\n */\n fetchAllowance: number;\n fetchAllowanceAt: number;\n}\n\n\n/** Maximum messages per client per window */\nconst WS_RATE_LIMIT = 2000;\n/** Rate limit window in milliseconds (60 seconds) */\nconst WS_RATE_WINDOW_MS = 60_000;\n\n/**\n * Channel frames get their own budget, because they are a different workload.\n *\n * 2000/minute is 33/second, which is generous for queries and subscriptions and\n * an order of magnitude below what the documented channel idiom asks for: the\n * capacity note in `docs/backend/realtime.md` uses 60 fps cursor movement as\n * its worked example, and the presence idiom re-`track()`s on every move, so\n * one client sustaining that sends ~120 frames/second — 7200 a minute. Sharing\n * one counter meant the cursor stream ate the query budget and then froze for\n * the rest of the window.\n *\n * The number is sized to that documented workload and nothing more; it is not\n * a considered product limit (see `docs/channel-authorization.md`).\n */\nconst WS_CHANNEL_RATE_LIMIT = 7200;\n\n/**\n * Realtime frames that read the database as they arrive: a subscription's\n * first frame is a fetch and a count, run as the subscriber, and a history\n * request reads the channel's retained messages. Each is a data request by\n * another name, and counts in the caller's data bucket like a\n * `FETCH_COLLECTION` — past the socket's own allowance.\n */\nconst FETCHING_REALTIME_TYPES = new Set([\"subscribe_collection\", \"subscribe_one\", \"channel_history\"]);\n\n/**\n * How many fetching realtime frames one socket may send without counting them\n * in its caller's data bucket: at least as many as a socket may hold\n * subscriptions, so a reconnect that re-subscribes everything it held is\n * never refused, and never spends the budget the person's next request needs.\n * The default data bucket is 1,000 requests per 15 minutes, so counting a\n * reconnect against it would have refused most of one, or locked the person\n * out of the data API until the window turned.\n */\nconst SOCKET_FETCH_ALLOWANCE = 1000;\n\n/**\n * How long a spent allowance takes to come back in full, in ms — the data\n * API's default window. A socket churning subscriptions gets its allowance\n * once, and then this much a quarter-hour; past that, every subscribe is a\n * request in its person's bucket, which their HTTP requests and other sockets\n * share.\n */\nconst SOCKET_FETCH_ALLOWANCE_REFILL_MS = 15 * 60_000;\n\n/**\n * The largest frame a socket that has not authenticated may send, in bytes.\n *\n * All such a socket can usefully say is `AUTHENTICATE` with a token — a few\n * KiB with custom claims — and everything else is answered `UNAUTHORIZED`.\n * Without this, each of those answers came after the whole frame was buffered,\n * stringified and parsed, up to the body limit: a handful of connections that\n * never signed in could hold the process's memory. A larger frame closes the\n * socket with 1009 before it is read.\n */\nconst MAX_UNAUTHENTICATED_FRAME_BYTES = 64 * 1024;\n\n/**\n * How often a socket that only listens has its identity re-read, in ms.\n *\n * A socket that sends frames is re-checked before each one, as an HTTP request\n * is; a socket that only receives pushes sends none, so without this a\n * subscription opened by an identity that has since signed out everywhere, been\n * demoted or deleted went on receiving rows as that identity for as long as the\n * connection lasted.\n */\nconst DEFAULT_IDENTITY_RECHECK_MS = 30_000;\n\n/**\n * How many auth-store lookups the sweep runs at once. It shares the pool with\n * every REST request — 20 connections by default — so it takes a few of them,\n * however many sockets are due: 5,000 sockets at ~0.3 ms a lookup is under\n * half a second of sweep, rather than a burst that held the whole pool.\n */\nconst IDENTITY_SWEEP_CONCURRENCY = 4;\n\n/**\n * Channel frames are re-checked at most this often, in ms. They are the\n * high-rate traffic (see `WS_CHANNEL_RATE_LIMIT`): a database read for every\n * cursor position would make realtime presence the busiest query a backend\n * runs, for a window no wider than this.\n */\nconst CHANNEL_RECHECK_MIN_MS = 1_000;\n\n/**\n * The close code for a socket whose identity ended: signed out, revoked,\n * demoted out of existence, deleted or expired. In the 4000–4999 range the\n * protocol leaves to applications. The SDK reconnects and authenticates with\n * whatever session it holds now — a refreshed token, or none.\n */\nexport const SESSION_ENDED_CLOSE_CODE = 4001;\n\n/**\n * The close code for a socket refused because its user already holds\n * `REALTIME_MAX_SOCKETS_PER_USER` sockets on this server. The SDK reconnects\n * with backoff, so a tab refused this way gets in once another one closes.\n */\nexport const TOO_MANY_SOCKETS_CLOSE_CODE = 4002;\n\n/**\n * When a JWT expires, in ms, or undefined for a credential that is not one\n * (an API key, the service key, an adapter's opaque token).\n *\n * Read without verifying, and only ever for a token that was verified a moment\n * ago by the same request: the signature is not in question, only the instant.\n */\nfunction jwtExpiryMs(token: string): number | undefined {\n const parts = token.split(\".\");\n if (parts.length !== 3) return undefined;\n try {\n const claims: unknown = JSON.parse(Buffer.from(parts[1], \"base64url\").toString(\"utf8\"));\n if (typeof claims !== \"object\" || claims === null || !(\"exp\" in claims)) return undefined;\n return typeof claims.exp === \"number\" ? claims.exp * 1000 : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * A socket's identity, asked again: still honoured, ended, or not answerable\n * right now — with the refusal that says why.\n */\ntype IdentityCheck = \"live\" | \"ended\" | { unavailable: { code: string; message: string } };\n\n/**\n * The refusal for an auth store that could not answer, as the HTTP doors give\n * it: the 503 the store's own error declares (`DATABASE_UNAVAILABLE`,\n * `ROLE_LOOKUP_FAILED`), else the database unreachable when that is what the\n * error says, else the lookup failing.\n */\nfunction storeUnavailableAnswer(error: unknown): { code: string; message: string } {\n const declared = declaredErrorAnswer(error);\n if (declared && declared.status === 503) return { code: declared.code, message: declared.message };\n if (isDatabaseUnreachable(error)) return { code: \"DATABASE_UNAVAILABLE\", message: DATABASE_UNAVAILABLE_MESSAGE };\n return { code: \"ROLE_LOOKUP_FAILED\", message: \"Could not verify your permissions. Please try again.\" };\n}\n\n/** Everything about an identity that decides what its frames may do. */\nfunction identityFingerprint(user: WsUserIdentity): string {\n return JSON.stringify([user.uid, [...user.roles].sort(), user.isAdmin, user.isAnonymous, user.scopes ?? null, user.claims ?? null, user.apiKey ?? null]);\n}\n\n/** Frames counted against the channel budget rather than the general one. */\nconst CHANNEL_MESSAGE_TYPES = new Set([\n \"join_channel\",\n \"leave_channel\",\n \"broadcast\",\n \"presence_track\",\n \"presence_untrack\",\n \"presence_state\",\n \"channel_history\"\n]);\n\n/**\n * WebSocket message types on the admin plane, and the scope each needs.\n *\n * Exported so the test can READ it. A privileged verb added without a scope\n * check is what this list exists to make impossible: `PUBLIC_TYPES` below is\n * its counterpart, and a test asserts that every `case` this file handles\n * appears in exactly one of the two — so a verb added to neither fails rather\n * than defaulting to reachable.\n *\n * All of these run on the owner connection, outside RLS. `database:read`\n * covers the catalogue — including `FETCH_APPLICATION_ROLES`, which runs\n * `SELECT DISTINCT unnest(roles)` over the users table — and\n * `database:write` covers running SQL and creating or deleting branches.\n */\nexport const ADMIN_PLANE_TYPES: ReadonlyMap<string, AdminScope> = new Map<string, AdminScope>([\n [\"EXECUTE_SQL\", \"database:write\"],\n // Stops a console run of the same socket, which `EXECUTE_SQL` started.\n [\"CANCEL_SQL\", \"database:write\"],\n [\"CREATE_BRANCH\", \"database:write\"],\n [\"DELETE_BRANCH\", \"database:write\"],\n [\"FETCH_DATABASES\", \"database:read\"],\n [\"FETCH_ROLES\", \"database:read\"],\n [\"FETCH_APPLICATION_ROLES\", \"database:read\"],\n [\"FETCH_UNMAPPED_TABLES\", \"database:read\"],\n [\"FETCH_TABLE_METADATA\", \"database:read\"],\n [\"FETCH_CURRENT_DATABASE\", \"database:read\"],\n [\"LIST_BRANCHES\", \"database:read\"]\n]);\n\n/**\n * The data scope each data verb needs, for a session whose credential narrows\n * it. A person's own session holds the whole data plane; RLS decides its rows.\n */\nconst DATA_VERB_SCOPES: ReadonlyMap<string, string> = new Map([\n [\"FETCH_COLLECTION\", \"data:read\"],\n [\"FETCH_ONE\", \"data:read\"],\n [\"COUNT\", \"data:read\"],\n [\"CHECK_UNIQUE_FIELD\", \"data:read\"],\n [\"subscribe_collection\", \"data:read\"],\n [\"subscribe_one\", \"data:read\"],\n [\"SAVE\", \"data:write\"],\n [\"DELETE\", \"data:delete\"]\n]);\n\n/**\n * Message types deliberately reachable by a non-admin session.\n *\n * Data operations, gated per row by RLS and per request by the same write\n * checks the REST layer applies — not by this list. It exists so that \"which\n * bucket is this verb in\" is a question with an answer for every verb, and\n * adding one without answering it is a test failure.\n */\nexport const PUBLIC_TYPES = new Set([\n \"FETCH_COLLECTION\",\n \"FETCH_ONE\",\n \"COUNT\",\n \"SAVE\",\n \"DELETE\",\n \"CHECK_UNIQUE_FIELD\"\n]);\n\n/**\n * Refuse a path whose collection — its first segment, for a nested path — this\n * server does not have, with the answer REST gives (`NOT_FOUND`).\n */\nfunction assertKnownCollection(\n registry: Pick<PostgresCollectionRegistry, \"getCollectionByPath\"> | undefined,\n path: unknown\n): void {\n if (!registry || typeof path !== \"string\") return;\n const root = path.split(\"/\").find(segment => segment !== \"\");\n if (root !== undefined && !registry.getCollectionByPath(root)) {\n throw ApiError.notFound(`Unknown collection '${root}'.`);\n }\n}\n\n/**\n * Recursively extract the deepest error message from an error's cause chain (e.g., Drizzle wrapping a PG error).\n */\nfunction extractErrorMessage(error: unknown): string {\n if (!error) return \"Unknown error\";\n if (error instanceof Error) {\n if (\"cause\" in error && error.cause) {\n return extractErrorMessage(error.cause);\n }\n return error.message;\n }\n if (typeof error === \"object\" && \"message\" in error && typeof (error as { message: unknown }).message === \"string\") {\n return (error as { message: string }).message;\n }\n return String(error);\n}\n\n/**\n * Who a socket reads and writes as, for its request frames and its\n * subscriptions alike.\n *\n * One answer because there were two, and they disagreed about guests. The\n * subscriptions carried the session's `isAnonymous`; the request frames wrote\n * `false` for every signed-in session, so a guest read and wrote as an account\n * through `FETCH_*`, `SAVE` and `DELETE` while its subscriptions did not. And a\n * socket with no session was a guest on the request frames (`true`) but not on\n * its subscriptions or over REST: an unauthenticated caller is not a guest\n * session.\n */\nfunction sessionAuthContext(session: ClientSession | undefined): SubscriptionAuthContext {\n if (!session?.user) {\n return { uid: ANONYMOUS_USER_ID, roles: [\"anon\"], isAnonymous: false };\n }\n return {\n uid: session.user.uid,\n roles: session.user.roles ?? [],\n isAnonymous: session.user.isAnonymous === true,\n // A tenancy policy reads a claim, so a frame without them answers\n // from the tenant of nobody.\n ...(session.user.claims ? { claims: session.user.claims } : {})\n };\n}\n\n/**\n * Everything a session may do: its credential's scopes, or what its roles hold\n * in `model` — the backend's access model.\n */\nfunction sessionScopes(session: ClientSession | undefined, model: AccessModel): string[] {\n if (!session?.user) return [];\n return session.user.scopes ?? scopesForRoles(session.user.roles, model);\n}\n\n/**\n * The collection a data frame addresses, for a narrowed session's scope check.\n * A nested path (`authors/1/posts`) answers undefined: its target is a\n * relation's, not a slug on the wire, so only an unqualified scope covers it.\n */\nfunction frameCollection(type: string, payload: unknown): string | undefined {\n if (typeof payload !== \"object\" || payload === null) return undefined;\n let path: unknown = \"path\" in payload ? payload.path : undefined;\n if (type === \"DELETE\" && \"row\" in payload && typeof payload.row === \"object\" && payload.row !== null && \"path\" in payload.row) {\n path = payload.row.path;\n }\n if (typeof path !== \"string\" || path === \"\" || isNestedPath(path)) return undefined;\n return path;\n}\n\n/**\n * A URL path with its trailing `/` dropped, so `/app` and `/app/` are one\n * path — and `/` stays `/`.\n */\nfunction normalizeSocketPath(path: string): string {\n const withLeadingSlash = path.startsWith(\"/\") ? path : `/${path}`;\n const trimmed = withLeadingSlash.replace(/\\/+$/, \"\");\n return trimmed === \"\" ? \"/\" : trimmed;\n}\n\n/** The path an upgrade request asks for, without its query. */\nfunction upgradePathOf(request: IncomingMessage): string {\n const url = request.url ?? \"/\";\n const queryAt = url.indexOf(\"?\");\n return normalizeSocketPath(queryAt === -1 ? url : url.slice(0, queryAt));\n}\n\n/**\n * Answer an upgrade nobody will take with a plain HTTP 404 and close it, so\n * the client sees a refusal instead of a socket that never opens.\n */\nfunction refuseUpgrade(socket: Duplex): void {\n if (!socket.writable) {\n socket.destroy();\n return;\n }\n // Destroyed once the answer is flushed — destroying straight away could\n // drop it, and the client would read a reset instead of the 404.\n socket.once(\"finish\", () => socket.destroy());\n socket.end(\"HTTP/1.1 404 Not Found\\r\\nConnection: close\\r\\nContent-Length: 0\\r\\n\\r\\n\");\n}\n\n/** A frame's size in bytes, however `ws` delivered it. */\nfunction frameBytes(data: RawData): number {\n if (Array.isArray(data)) return data.reduce((total, chunk) => total + chunk.length, 0);\n return data.byteLength;\n}\n\n/**\n * Where a connection comes from, as the data API's rate limiter reads an\n * address: the upgrade request's headers (believed only as far as proxies are\n * declared) and the socket's own address.\n */\nfunction connectionOrigin(request: IncomingMessage | undefined): Omit<DataRateLimitCaller, \"uid\"> {\n return {\n header: (name) => {\n const value = request?.headers[name];\n return Array.isArray(value) ? value.join(\", \") : value;\n },\n socketAddress: request?.socket?.remoteAddress\n };\n}\n\n/**\n * A request frame with its `filter` decoded by the codec every other door uses.\n *\n * REST, MCP and the in-process SDK read `where` through `deserializeFilter`,\n * so `{ title: [\"foo\", \"bar\"] }` is an implicit `in` and `[\"!!\", \"x\"]` is\n * refused by name. The socket handed the frame's filter to the driver raw,\n * which read the first element as an operator: one filter, two meanings,\n * depending on the transport. A typed `[op, value]` tuple decodes to itself.\n */\nfunction withDecodedFilter(request: FetchCollectionProps): FetchCollectionProps {\n if (!request?.filter) return request;\n try {\n return { ...request, filter: deserializeFilter(request.filter) };\n } catch (e) {\n if (e instanceof UnknownFilterOperatorError) throw ApiError.badRequest(e.message, e.code, e.details);\n throw e;\n }\n}\n\n/**\n * `options` carry the data API's limits, for the same rows reached through this door:\n * frames larger than its body limit close the socket with 1009 before they are\n * read, and every data frame counts in the caller's data-API bucket — the one\n * their HTTP requests count in — rather than only in a budget per connection,\n * which a caller multiplied by opening more.\n */\nexport function createPostgresWebSocket(\n server: Server,\n realtimeService: SocketRealtimeService,\n driver: PostgresBackendDriver,\n authConfig?: WsAuthConfig,\n authAdapter?: AuthAdapter,\n options?: RealtimeSocketOptions\n) {\n // Session map scoped to this factory invocation — prevents stale sessions\n // leaking across hot reloads or multiple factory calls.\n const clientSessions = new Map<string, ClientSession>();\n\n /** The sockets that opted into heartbeats — see `socket-opt-ins.ts`. */\n const heartbeatSockets = new Set<WebSocket>();\n const heartbeatIntervalMs = options?.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;\n /** How many sockets one identity may hold here; unset is no ceiling. See `REALTIME_MAX_SOCKETS_PER_USER`. */\n const maxSocketsPerUser = options?.maxSocketsPerUser;\n\n const isProduction = process.env.NODE_ENV === \"production\";\n /**\n * Debug tracing for this socket.\n *\n * Through `logger.debug`, not `console.debug`. A bare console call sits\n * outside the one level system, so `LOG_LEVEL=warn` — a line the scaffold's\n * own `.env.example` ships — silenced every structured line in the process\n * and left these ones printing; the deletion of the old `console.debug`\n * override is what exposed that. It also skipped the redactor, and the\n * extras these calls carry are rows.\n *\n * The production guard stays on top of the level gate rather than being\n * replaced by it: these lines trace requests and their payloads, and\n * `LOG_LEVEL=debug` on a deployment is not consent to put a user's row on\n * stdout.\n */\n const wsDebug = (message: string, ...extras: unknown[]) => {\n if (isProduction) return;\n if (extras.length === 0) logger.debug(message);\n else logger.debug(message, { details: extras.length === 1 ? extras[0] : extras });\n };\n // `ws` defaults to 100 MiB. The data API refuses a body over its limit\n // before any handler runs; this is the same limit, enforced by `ws` while\n // it reads the frame. `0` is \"none\" to both.\n const maxPayload = options?.maxPayload ?? RUNTIME_DEFAULT_MAX_BODY_SIZE;\n const wss = new WebSocketServer({ noServer: true, maxPayload: maxPayload > 0 ? maxPayload : 0 });\n\n wss.on(\"error\", (err: Error) => {\n logger.error(\"❌ [WebSocket Server] Error\", { error: err });\n });\n\n // The host's `http.Server` is shared, so this socket takes the upgrades\n // on its own path and no others. Given the server, `ws` would claim every\n // upgrade on it — and refuse a path it was not given with a 400 — so a\n // host's own WebSocket on `/chat` had both servers answering one socket.\n //\n // An upgrade to another path is left alone for whichever listener it is\n // for. When this listener is the only one, nothing else will answer it,\n // so it is refused rather than left open until the client gives up. A\n // dispatcher that lifts this listener off and calls it for the upgrades\n // it does not claim itself (the cloud control plane does) is then the\n // only listener, and gets the same refusal.\n const socketPath = normalizeSocketPath(options?.path ?? \"/\");\n const onUpgrade = (request: IncomingMessage, socket: Duplex, head: Buffer) => {\n if (upgradePathOf(request) !== socketPath) {\n if (server.listenerCount(\"upgrade\") <= 1) refuseUpgrade(socket);\n return;\n }\n wss.handleUpgrade(request, socket, head, (client) => wss.emit(\"connection\", client, request));\n };\n if (typeof server.on === \"function\") {\n server.on(\"upgrade\", onUpgrade);\n wss.on(\"close\", () => server.off(\"upgrade\", onUpgrade));\n }\n\n // The same predicate the HTTP data routes use, from the same function —\n // this socket is the other enforcement point for one product decision, and\n // while it computed the answer itself it computed a different one. See\n // `resolveRequireAuth` for what its local copy got wrong and why a `false`\n // here grants access rather than skipping a check.\n const requireAuth = !!authAdapter || resolveRequireAuth(authConfig as never);\n\n // The backend's own keys and roles, as the server passes them; a socket\n // built by hand without them answers for the process default.\n const verifySessionToken = options?.verifyAccessToken ?? extractUserFromToken;\n const accessModel = (): AccessModel => options?.accessModel ?? getAccessModel();\n\n if (requireAuth && !authAdapter && !authConfig?.jwtSecret && !authConfig?.serviceKey) {\n logger.warn(\n \"🔐 [WebSocket Server] Authentication is required but no adapter, jwtSecret or \" +\n \"serviceKey is configured — no client can complete AUTH, so every realtime \" +\n \"message will be refused with UNAUTHORIZED.\"\n );\n }\n\n /**\n * Who a credential is, now: the same verification `AUTHENTICATE` runs, so\n * re-asking it later answers what signing in would answer. A refusal is a\n * value; a store that cannot answer throws.\n */\n async function resolveCaller(token: string): Promise<{ user: WsUserIdentity; credential: ImpersonationCredential } | { refused: string; apiKey?: true }> {\n if (token.startsWith(\"rk_\")) {\n // An API key: the same verification the HTTP middlewares run, so a\n // key means one thing on both — revoked or expired included.\n const resolved = options?.resolveApiKey ? await options.resolveApiKey(token) : undefined;\n if (!resolved || !(\"uid\" in resolved)) {\n return { refused: resolved?.message ?? \"API keys are not enabled on this server\", apiKey: true };\n }\n return {\n credential: \"api-key\",\n user: {\n uid: resolved.uid,\n roles: resolved.roles,\n isAdmin: hasAdminRole(resolved.roles),\n isAnonymous: false,\n scopes: resolved.scopes,\n ...(resolved.apiKey ? { apiKey: { id: resolved.apiKey.id, rate_limit: resolved.apiKey.rate_limit } } : {})\n }\n };\n }\n\n if (authAdapter) {\n // Custom auth, Clerk, the built-in auth — anything that is an\n // adapter. One that throws could not answer (its store is\n // unreachable); that is not a refusal, and the caller decides\n // what a question nobody could answer means.\n const adapterUser = authAdapter.verifyToken\n ? await authAdapter.verifyToken(token)\n : await authAdapter.verifyRequest(new Request(\"http://localhost/_ws_auth\", {\n headers: { Authorization: `Bearer ${token}` }\n }));\n if (!adapterUser) return { refused: \"Invalid or expired token\" };\n return {\n // The built-in adapter verifies the service key as its own\n // identity rather than as a person.\n credential: adapterUser.uid === SERVICE_KEY_UID ? \"service-key\" : \"session\",\n user: {\n uid: adapterUser.uid,\n roles: adapterUser.roles,\n isAdmin: adapterUser.isAdmin,\n // Read, as the JWT path reads it: dropped here, a guest\n // signed in through an adapter subscribed as an account.\n // Absent from an adapter with no such concept, and absent\n // reads as \"not a guest\".\n isAnonymous: adapterUser.isAnonymous === true,\n // Kept, as the JWT branch keeps them: the built-in auth is\n // an adapter, so dropping them here left every\n // claim-tenanted collection with no tenant on the socket.\n ...(adapterUser.claims ? { claims: adapterUser.claims } : {})\n }\n };\n }\n\n if (authConfig?.serviceKey && safeCompare(token, authConfig.serviceKey)) {\n // Service key: a static secret, not a JWT. Checked before\n // verification, mirroring the HTTP middleware — verifying it as a\n // JWT can only ever fail.\n return { credential: \"service-key\", user: { uid: SERVICE_KEY_UID, roles: [\"admin\"], isAdmin: true, isAnonymous: false } };\n }\n\n // Standard JWT path: the signature and the expiry, nothing more —\n // there is no account store on this path to ask. Verified with the\n // backend's own keys when the server handed them over.\n const jwtPayload = await verifySessionToken(token);\n if (!jwtPayload) return { refused: \"Invalid or expired token\" };\n return {\n credential: \"session\",\n user: {\n uid: jwtPayload.uid,\n roles: jwtPayload.roles ?? [],\n isAdmin: hasAdminRole(jwtPayload.roles ?? []),\n isAnonymous: jwtPayload.isAnonymous === true,\n claims: jwtPayload.claims\n }\n };\n }\n\n /**\n * Who this socket runs as: its caller, or — when it asked to `impersonate`\n * — the user it named, by the same decision the data API takes for\n * `x-rebase-impersonate`. A refusal carries the code to answer with.\n *\n * `audit` is passed by `AUTHENTICATE` only: the re-check before every frame\n * decides again without writing a security event each time.\n */\n async function resolveIdentity(\n token: string,\n impersonate?: string,\n audit?: Record<string, unknown>\n ): Promise<{ user: WsUserIdentity } | { refused: string; apiKey?: true; code?: string }> {\n const caller = await resolveCaller(token);\n if (!(\"user\" in caller) || impersonate === undefined) return caller;\n\n const decision = await decideImpersonation({\n requestedUid: impersonate,\n credential: caller.credential,\n caller: caller.user,\n resolveUser: authAdapter?.resolveUser ? authAdapter.resolveUser.bind(authAdapter) : undefined,\n audit\n });\n if (\"refused\" in decision) return { refused: decision.refused.message, code: decision.refused.code };\n const target = decision.granted;\n return {\n user: {\n uid: target.uid,\n roles: target.roles,\n isAdmin: hasAdminRole(target.roles),\n isAnonymous: target.isAnonymous === true,\n ...(target.claims ? { claims: target.claims } : {}),\n impersonator: decision.impersonator\n }\n };\n }\n\n /** Stop honouring the session at the instant its token expires. */\n function scheduleExpiry(clientId: string, token: string): void {\n const session = clientSessions.get(clientId);\n if (!session) return;\n if (session.expiryTimer) clearTimeout(session.expiryTimer);\n session.expiryTimer = undefined;\n const expiresAt = jwtExpiryMs(token);\n if (expiresAt === undefined) return;\n // `setTimeout` overflows past 2^31-1 ms (~24.8 days) and fires at once;\n // a token living longer than that is re-checked by the sweep instead.\n const delay = Math.max(0, expiresAt - Date.now());\n if (delay > 2_147_483_647) return;\n session.expiryTimer = setTimeout(() => {\n const current = clientSessions.get(clientId);\n if (current?.credential === token) {\n endSession(clientId, \"TOKEN_EXPIRED\", \"The token this socket authenticated with has expired.\");\n }\n }, delay);\n session.expiryTimer.unref?.();\n }\n\n /**\n * The identity this socket authenticated as is over: say so, forget it,\n * and close. Closing rather than carrying on anonymously, because every\n * subscription the socket holds was opened as that identity — the SDK\n * reconnects, authenticates with whatever session it has now, and\n * re-subscribes as that.\n */\n function endSession(clientId: string, code: string, message: string): void {\n const session = clientSessions.get(clientId);\n if (!session) return;\n if (session.expiryTimer) clearTimeout(session.expiryTimer);\n session.expiryTimer = undefined;\n session.user = undefined;\n session.credential = undefined;\n session.authenticated = !requireAuth;\n wsDebug(`[WS] ${clientId} session ended: ${code}`);\n try {\n session.ws.send(JSON.stringify({ type: \"AUTH_ERROR\", payload: { error: { code, message } } }));\n } catch {\n // Already closing.\n }\n session.ws.close(SESSION_ENDED_CLOSE_CODE, message.slice(0, 120));\n }\n\n /**\n * Is the identity this socket authenticated as still the one its\n * credential names?\n *\n * Asked of the database, through the same verification as\n * `AUTHENTICATE`, before each frame and by the sweep — never by an event\n * one instance publishes to the others. A sign-out, a demotion or a\n * deletion is written to the database by whichever instance served it;\n * re-reading there is correct on every instance with nothing in between,\n * where an event would reach only the instances a bus connects (and the\n * bus is opt-in).\n *\n * - `\"live\"`: still honoured; roles or claims that changed are applied,\n * and the socket's subscriptions are re-scoped to them.\n * - `\"ended\"`: refused — revoked, signed out, deleted, expired. The\n * socket is closed.\n * - `{ unavailable }`: the store could not answer, and this is the\n * refusal that says why. The frame is refused, the session kept, and\n * the next frame or sweep asks again.\n */\n async function recheckIdentity(\n clientId: string,\n resolve: (token: string, impersonate?: string) => ReturnType<typeof resolveIdentity> = resolveIdentity\n ): Promise<IdentityCheck> {\n const session = clientSessions.get(clientId);\n const credential = session?.credential;\n if (!session?.user || !credential) return \"live\";\n let outcome: Awaited<ReturnType<typeof resolveIdentity>>;\n try {\n outcome = await resolve(credential, session.impersonate);\n } catch (error) {\n return { unavailable: storeUnavailableAnswer(error) };\n }\n // Re-authenticated while this was in flight: that answer is newer.\n if (session.credential !== credential || !session.user) return session.user ? \"live\" : \"ended\";\n session.checkedAt = Date.now();\n if (!(\"user\" in outcome)) {\n // An impersonation that no longer holds — the administrator was\n // demoted, the user disabled — says which; anything else is the\n // credential itself.\n if (outcome.code) endSession(clientId, outcome.code, outcome.refused);\n else endSession(clientId, \"SESSION_ENDED\", \"This session has ended: it was signed out, revoked, or its account changed. Authenticate again.\");\n return \"ended\";\n }\n if (identityFingerprint(outcome.user) !== identityFingerprint(session.user)) {\n session.user = outcome.user;\n await realtimeService.rescopeClient(clientId, sessionAuthContext(session));\n }\n return \"live\";\n }\n\n /** The per-frame check in flight on each socket, and what it asks. */\n const frameChecks = new Map<string, { credential: string; impersonate?: string; outcome: Promise<IdentityCheck> }>();\n\n /**\n * {@link recheckIdentity} for a frame, shared by the frames of one socket\n * that arrive while it is in flight.\n *\n * A client that reconnects after a deploy re-sends all its subscriptions\n * at once, and each frame asked the auth store the same question at the\n * same moment: a thousand clients with ten subscriptions were ten thousand\n * lookups where a thousand answer. A frame that arrives while this\n * socket's check of the same credential and impersonation target is under\n * way waits for that answer instead. The answer is the flight's only:\n * dropped as it settles, so the next frame asks afresh. What this trades\n * is bounded by one lookup: a demotion that lands while the shared lookup\n * runs is seen by the next frame rather than by these.\n */\n function recheckIdentityForFrame(clientId: string): Promise<IdentityCheck> {\n const session = clientSessions.get(clientId);\n const credential = session?.credential;\n if (!session || !credential) return recheckIdentity(clientId);\n const inFlight = frameChecks.get(clientId);\n if (inFlight && inFlight.credential === credential && inFlight.impersonate === session.impersonate) {\n return inFlight.outcome;\n }\n const outcome = recheckIdentity(clientId).finally(() => {\n if (frameChecks.get(clientId)?.outcome === outcome) frameChecks.delete(clientId);\n });\n frameChecks.set(clientId, { credential, impersonate: session.impersonate, outcome });\n return outcome;\n }\n\n /**\n * A socket's whole allowance of fetching realtime frames: the default, or\n * the realtime service's subscription ceiling where a deployment raised\n * it, so a full socket still re-subscribes without being charged.\n */\n function fetchAllowanceCapacity(): number {\n const ceiling: unknown = Reflect.get(realtimeService, \"maxSubscriptionsPerSocket\");\n return typeof ceiling === \"number\" && Number.isFinite(ceiling)\n ? Math.max(SOCKET_FETCH_ALLOWANCE, ceiling)\n : SOCKET_FETCH_ALLOWANCE;\n }\n\n /**\n * Take one fetching realtime frame from the socket's own allowance,\n * topped up for the time since it was last asked. False when none is\n * left: the frame then counts in the caller's data bucket.\n */\n function spendFetchAllowance(clientId: string): boolean {\n const session = clientSessions.get(clientId);\n if (!session) return false;\n const now = Date.now();\n const capacity = fetchAllowanceCapacity();\n const refilled = Math.max(0, now - session.fetchAllowanceAt) * capacity / SOCKET_FETCH_ALLOWANCE_REFILL_MS;\n session.fetchAllowance = Math.min(capacity, session.fetchAllowance + refilled);\n session.fetchAllowanceAt = now;\n if (session.fetchAllowance < 1) return false;\n session.fetchAllowance -= 1;\n return true;\n }\n\n // The sweep: a socket that only listens sends nothing to be re-checked\n // before, so it is re-checked on a timer instead.\n const identityRecheckMs = Math.max(250, options?.identityRecheckIntervalMs ?? DEFAULT_IDENTITY_RECHECK_MS);\n\n /**\n * Re-check every socket that is due, asking the auth store once per\n * question.\n *\n * Each socket used to be asked about on its own, all in the same tick: S\n * listening sockets were S concurrent lookups on the pool REST shares,\n * and a person with five tabs was five identical lookups of one token.\n * Sockets are grouped by what they would ask — their credential and the\n * user they impersonate — and each group asks once, at most\n * {@link IDENTITY_SWEEP_CONCURRENCY} groups at a time.\n *\n * The answer is the group's alone, made when the group's turn comes and\n * dropped when it is done; nothing is kept from one sweep to the next, so\n * a credential revoked after a sweep is asked about afresh, and refused,\n * by the next one. A socket that authenticated again while its group\n * waited is asked about the credential it holds now — see `answerFor`.\n */\n async function sweepIdentities(): Promise<void> {\n const now = Date.now();\n const groups = new Map<string, { credential: string; impersonate?: string; clientIds: string[] }>();\n for (const [id, session] of clientSessions) {\n if (!session.user || !session.credential || now - session.checkedAt < identityRecheckMs / 2) continue;\n const key = JSON.stringify([session.credential, session.impersonate ?? null]);\n const group = groups.get(key);\n if (group) group.clientIds.push(id);\n else groups.set(key, { credential: session.credential, impersonate: session.impersonate, clientIds: [id] });\n }\n const queue = [...groups.values()];\n const work = async (): Promise<void> => {\n for (let group = queue.shift(); group; group = queue.shift()) {\n let answer: ReturnType<typeof resolveIdentity> | undefined;\n // The group's question is answered once. Any other — a socket\n // that has since authenticated with another token, or asked to\n // act as someone else — is asked on its own.\n const answerFor = (token: string, impersonate?: string): ReturnType<typeof resolveIdentity> => {\n if (token !== group!.credential || impersonate !== group!.impersonate) return resolveIdentity(token, impersonate);\n answer ??= resolveIdentity(token, impersonate);\n return answer;\n };\n for (const id of group.clientIds) {\n await recheckIdentity(id, answerFor).catch(() => undefined);\n }\n }\n };\n await Promise.all(Array.from({ length: Math.min(IDENTITY_SWEEP_CONCURRENCY, queue.length) }, work));\n }\n\n // A sweep still working through a large number of sockets when the next\n // is due lets that one pass, rather than asking about them twice at once.\n let sweeping = false;\n const identitySweep = setInterval(() => {\n if (sweeping) return;\n sweeping = true;\n void sweepIdentities().catch(() => undefined).finally(() => {\n sweeping = false;\n });\n }, identityRecheckMs);\n // Unref'd, so it never holds a process open; stopped with whichever goes\n // first, the socket server or the HTTP server it is attached to.\n identitySweep.unref?.();\n const stopSweep = () => clearInterval(identitySweep);\n wss.on(\"close\", stopSweep);\n if (typeof server.on === \"function\") server.on(\"close\", stopSweep);\n\n wss.on(\"connection\", (ws, request?: IncomingMessage) => {\n const clientId = `client_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;\n const origin = connectionOrigin(request);\n wsDebug(`WebSocket client connected: ${clientId}`);\n\n // What the client opted into on its URL, answered with a `hello`\n // before anything else. A client that named nothing — one that\n // predates the opt-ins — is sent neither the `hello` nor anything it\n // would have had to ask for.\n const optIns = requestedOptIns(request?.url);\n if (optIns) {\n ws.send(helloFrame(optIns, heartbeatIntervalMs, clientId));\n realtimeService.setClientOptIns?.(clientId, optIns);\n if (optIns.has(\"heartbeat\")) {\n heartbeatSockets.add(ws);\n ws.once(\"close\", () => heartbeatSockets.delete(ws));\n }\n }\n\n // Initialize client session\n clientSessions.set(clientId, { ws,\nauthenticated: !requireAuth,\ncheckedAt: Date.now(),\nmessageCount: 0,\nmessageWindowStart: Date.now(),\nchannelMessageCount: 0,\nchannelWindowStart: Date.now(),\nfetchAllowance: fetchAllowanceCapacity(),\nfetchAllowanceAt: Date.now() });\n realtimeService.addClient(clientId, ws);\n\n // This socket's console runs in flight, by the `requestId` that\n // started each: what `CANCEL_SQL` can stop. One socket's map, so a\n // frame can only ever stop a run its own socket started.\n const sqlRuns = new Map<string, AbortController>();\n\n ws.on(\"close\", () => {\n wsDebug(`WebSocket client disconnected: ${clientId}`);\n const session = clientSessions.get(clientId);\n if (session?.expiryTimer) clearTimeout(session.expiryTimer);\n clientSessions.delete(clientId);\n });\n\n // A frame over `maxPayload` is refused by `ws` as an `error` on this\n // socket, then closed with 1009. An `error` with no listener is thrown\n // by the emitter — out of the process — so the socket that set the\n // limit listens for it rather than trusting a realtime provider to.\n ws.on(\"error\", (error: Error) => {\n wsDebug(`WebSocket client ${clientId} error: ${error.message}`);\n });\n\n // Route all messages through RealtimeService for unified handling\n ws.on(\"message\", async (message: RawData) => {\n // Every frame is answered, so one from a client that has not read\n // its earlier answers would only add to a backlog it is not draining.\n if (terminateIfBacklogged(ws, clientId)) return;\n let requestId: string | undefined;\n try {\n if (!clientSessions.get(clientId)?.authenticated && frameBytes(message) > MAX_UNAUTHENTICATED_FRAME_BYTES) {\n ws.close(1009, \"Authenticate before sending anything larger than a token.\");\n return;\n }\n const {\n type,\n payload,\n requestId: reqId\n } = JSON.parse(message.toString());\n requestId = reqId; // Capture requestId for use in catch block\n\n wsDebug(`[WS] ${clientId} → ${type}`, requestId ? `(${requestId})` : \"\");\n\n // Handle authentication first\n // Helper: send a canonical error frame\n // `channel` is carried for a channel frame so the client can\n // deliver the error to the channel it is about. Channel frames\n // are fire-and-forget — no pending request to reject, no\n // subscription id — so without it a `RATE_LIMITED` on a\n // broadcast had nowhere to go but a console warning.\n const sendError = (errType: \"ERROR\" | \"AUTH_ERROR\", code: string, msg: string) => {\n const channel = CHANNEL_MESSAGE_TYPES.has(type)\n ? (payload as { channel?: unknown } | undefined)?.channel\n : undefined;\n ws.send(JSON.stringify({\n type: errType,\n requestId,\n ...(typeof channel === \"string\" && { channel }),\n payload: {\n error: { message: msg, code },\n ...(typeof channel === \"string\" && { channel })\n }\n }));\n };\n\n // Rate limiting: reject if client exceeds message limit.\n // Channel frames are counted against their own budget — see\n // WS_CHANNEL_RATE_LIMIT for why one shared counter starved them.\n //\n // Counted before the session is asked whether it has\n // authenticated, so a socket that never does — sending\n // `AUTHENTICATE` after `AUTHENTICATE`, each a token\n // verification — spends this budget like any other.\n {\n const session = clientSessions.get(clientId);\n if (session) {\n const now = Date.now();\n const isChannelFrame = CHANNEL_MESSAGE_TYPES.has(type);\n if (isChannelFrame) {\n if (now - session.channelWindowStart > WS_RATE_WINDOW_MS) {\n session.channelMessageCount = 0;\n session.channelWindowStart = now;\n }\n session.channelMessageCount++;\n if (session.channelMessageCount > WS_CHANNEL_RATE_LIMIT) {\n sendError(\"ERROR\", \"RATE_LIMITED\", \"Too many channel messages. Please slow down.\");\n return;\n }\n } else {\n if (now - session.messageWindowStart > WS_RATE_WINDOW_MS) {\n session.messageCount = 0;\n session.messageWindowStart = now;\n }\n session.messageCount++;\n if (session.messageCount > WS_RATE_LIMIT) {\n sendError(\"ERROR\", \"RATE_LIMITED\", \"Too many requests. Please slow down.\");\n return;\n }\n }\n }\n }\n\n if (type === \"AUTHENTICATE\") {\n const { token, impersonate } = payload || {};\n if (!token) {\n sendError(\"AUTH_ERROR\", \"INVALID_INPUT\", \"Token is required\");\n return;\n }\n if (impersonate !== undefined && typeof impersonate !== \"string\") {\n sendError(\"AUTH_ERROR\", \"INVALID_INPUT\", \"impersonate must be the uid of the user to act as\");\n return;\n }\n\n // A lookup for an impersonation that throws is refused\n // with the answer it carries when it has one, and is never\n // authenticated as the caller. A store that cannot answer\n // at all — the database unreachable — is said as that, in\n // an `ERROR`: an `AUTH_ERROR` would tell the SDK the token\n // is bad and send it to refresh a session that is fine.\n // Any other throw is an invalid token, as it always was:\n // there is no session yet to keep.\n let resolved: Awaited<ReturnType<typeof resolveIdentity>>;\n try {\n resolved = await resolveIdentity(token, impersonate, impersonate === undefined ? undefined : { door: \"socket\", clientId });\n } catch (error) {\n const declared = declaredErrorAnswer(error);\n if ((declared && declared.status === 503) || isDatabaseUnreachable(error)) {\n const unavailable = storeUnavailableAnswer(error);\n sendError(\"ERROR\", unavailable.code, unavailable.message);\n return;\n }\n const answer = impersonate === undefined ? undefined : declared;\n if (answer) {\n sendError(\"AUTH_ERROR\", answer.code, answer.message);\n return;\n }\n if (token.startsWith(\"rk_\") || !authAdapter) throw error;\n resolved = { refused: \"Invalid or expired token\" };\n }\n if (\"refused\" in resolved && (resolved.apiKey || resolved.code)) {\n sendError(\"AUTH_ERROR\", resolved.code ?? \"INVALID_TOKEN\", resolved.refused);\n return;\n }\n const verifiedUser = \"user\" in resolved ? resolved.user : null;\n\n if (verifiedUser) {\n const session = clientSessions.get(clientId);\n // The per-identity ceiling, when one is set: a socket\n // re-authenticating as whom it already is does not\n // count against itself. Refused with an `ERROR`, not\n // an `AUTH_ERROR` — the credential is fine, and an\n // auth error sends the SDK to refresh the session —\n // then closed, so the tab does not run on as nobody.\n if (maxSocketsPerUser !== undefined && session) {\n let held = 0;\n for (const other of clientSessions.values()) {\n if (other !== session && other.user?.uid === verifiedUser.uid) held++;\n }\n if (held >= maxSocketsPerUser) {\n logger.warn(`[WebSocket Server] Refused a socket for ${verifiedUser.uid}: ${held} already open, ceiling ${maxSocketsPerUser}`);\n sendError(\n \"ERROR\",\n \"TOO_MANY_SOCKETS\",\n `This user already has ${held} realtime connections open on this server, the most it allows ` +\n \"(REALTIME_MAX_SOCKETS_PER_USER). Close another tab or client and try again.\"\n );\n ws.close(TOO_MANY_SOCKETS_CLOSE_CODE, \"Too many realtime connections for this user\");\n return;\n }\n }\n if (session) {\n session.user = verifiedUser;\n session.authenticated = true;\n // Kept to be asked again: before every frame, by\n // the sweep, and — for a token that expires — at\n // the instant it does.\n session.credential = token;\n session.impersonate = impersonate;\n session.checkedAt = Date.now();\n scheduleExpiry(clientId, token);\n // What this socket already holds open — a session\n // refresh, a demotion, another account — is read\n // as the identity it has now, before it is told\n // the sign-in took.\n await realtimeService.rescopeClient(clientId, sessionAuthContext(session));\n }\n wsDebug(`[WS] replying AUTH_SUCCESS for requestId ${requestId}`);\n ws.send(JSON.stringify({\n type: \"AUTH_SUCCESS\",\n requestId,\n payload: { uid: verifiedUser.uid,\nroles: verifiedUser.roles }\n }));\n wsDebug(`🔐 [WebSocket Server] Client ${clientId} authenticated as ${verifiedUser.uid}`);\n } else {\n wsDebug(`[WS] replying AUTH_ERROR for requestId ${requestId} (invalid token)`);\n sendError(\"AUTH_ERROR\", \"INVALID_TOKEN\", \"Invalid or expired token\");\n }\n return;\n }\n\n // The identity this socket authenticated as is asked again\n // before the frame runs, as an HTTP request asks on every\n // request: a sign-out everywhere, a revoked session, a\n // demotion, a deleted account or an expired token stops being\n // honoured here, not when the socket happens to reconnect.\n {\n const session = clientSessions.get(clientId);\n if (session?.user && session.credential\n && Date.now() - session.checkedAt >= (CHANNEL_MESSAGE_TYPES.has(type) ? CHANNEL_RECHECK_MIN_MS : 0)) {\n const outcome = await recheckIdentityForFrame(clientId);\n if (outcome === \"ended\") return;\n if (outcome !== \"live\") {\n sendError(\"ERROR\", outcome.unavailable.code, outcome.unavailable.message);\n return;\n }\n }\n }\n\n // Check authentication for protected operations\n if (requireAuth) {\n const session = clientSessions.get(clientId);\n if (!session?.authenticated) {\n sendError(\"ERROR\", \"UNAUTHORIZED\", \"Authentication required\");\n return;\n }\n }\n\n // Admin-plane verbs need their scope; a narrowed session needs\n // the data scope for the collection a data frame addresses, and\n // cannot use channels, which no scope covers.\n const adminScope = ADMIN_PLANE_TYPES.get(type);\n if (adminScope) {\n const session = clientSessions.get(clientId);\n if (!scopeGrants(sessionScopes(session, accessModel()), adminScope)) {\n sendError(\"ERROR\", \"SCOPE_MISSING\", `This operation needs the \"${adminScope}\" scope`);\n return;\n }\n }\n const narrowedScopes = clientSessions.get(clientId)?.user?.scopes;\n if (narrowedScopes) {\n const dataScope = DATA_VERB_SCOPES.get(type);\n if (dataScope) {\n const collection = frameCollection(type, payload);\n if (!scopeGrants(narrowedScopes, dataScope, collection)) {\n const wanted = collection ? `${dataScope}:${collection}` : dataScope;\n sendError(\"ERROR\", \"SCOPE_MISSING\", `This API key does not hold the \"${wanted}\" scope`);\n return;\n }\n } else if (CHANNEL_MESSAGE_TYPES.has(type)) {\n sendError(\"ERROR\", \"SCOPE_MISSING\", \"An API key cannot use channels\");\n return;\n }\n }\n\n // A data frame is a data-API request by another door, and\n // counts in the bucket that request would: this person's — or\n // the API key's, at its own rate limit, or with no account this\n // address's — shared with their HTTP requests and their other\n // sockets. The counter above is per connection, so on its own\n // a caller bought budget by opening sockets.\n //\n // A subscribe and a history read are reads too, and count the\n // same way once the socket's own allowance is spent — see\n // SOCKET_FETCH_ALLOWANCE for why a socket has one.\n if (options?.dataRateLimit && (PUBLIC_TYPES.has(type)\n || (FETCHING_REALTIME_TYPES.has(type) && !spendFetchAllowance(clientId)))) {\n const decision = await options.dataRateLimit({\n ...origin,\n uid: clientSessions.get(clientId)?.user?.uid,\n apiKey: clientSessions.get(clientId)?.user?.apiKey\n });\n if (decision && !decision.allowed) {\n sendError(\"ERROR\", \"RATE_LIMITED\", \"Too many requests, please try again later.\");\n return;\n }\n }\n\n /**\n * Apply the REST layer's write checks to a socket payload.\n *\n * Silent when the path names no registered collection: the\n * driver decides what a path means, and refusing here would\n * turn \"unknown collection\" into a validation error.\n */\n const assertWriteRequest = (path: string | undefined, values: unknown, status?: SaveProps[\"status\"]): void => {\n if (!path || !values || typeof values !== \"object\") return;\n const collection = driver.registry?.getCollectionByPath(path);\n if (!collection) return;\n // The session's roles, and `[\"anon\"]` for a socket that\n // never authenticated — the same list `getScopedDelegate`\n // below scopes such a socket's driver with. Never\n // `undefined`: that means the trusted server plane, which\n // satisfies every role list, and a socket is not it.\n const session = clientSessions.get(clientId);\n assertWriteRequestValid(values as Record<string, unknown>, collection, {\n viewer: { roles: session?.user?.roles ?? [\"anon\"] },\n // `required` holds on a create, as on `POST`.\n status\n });\n // Field operations (`{ views: { $inc: 1 } }`) reach the\n // driver through this door exactly as they reach it through\n // `PATCH`, and are compiled to SQL in the same place. What\n // the REST route adds is the type check — `$push` on a\n // number is a 400 naming the field, not a Postgres error\n // from inside the transaction — so the socket runs it too,\n // or the same payload is refused at one boundary and\n // accepted at the other.\n assertFieldOpsValid(values as Record<string, unknown>, collection);\n };\n\n /**\n * Apply the REST layer's read check to a socket request: no\n * filter, sort, projection or uniqueness probe over a field\n * this caller may not read. The strip keeps the value off the\n * wire; without this a `COUNT` still answers whether a hidden\n * value matches a pattern, one character at a time.\n *\n * Same viewer as `assertWriteRequest`, and silent in the same\n * case: a path that names no registered collection is the\n * driver's to answer.\n */\n const assertReadRequest = (\n path: string | undefined,\n request: Parameters<typeof assertReadRequestReadable>[0]\n ): void => {\n if (!path) return;\n let collection;\n try {\n collection = driver.registry?.getCollectionByPath(path);\n } catch {\n // A malformed nested path: the driver refuses it with\n // its own message.\n return;\n }\n if (!collection) return;\n const session = clientSessions.get(clientId);\n assertReadRequestReadable(request, collection, {\n roles: session?.user?.roles ?? [\"anon\"]\n });\n };\n\n /**\n * Refuse a write through a nested path that sets a relation\n * this caller may not write — the rule REST's nested routes\n * apply, from the same function. The body is checked by\n * `assertWriteRequest`; this is what the address writes.\n *\n * Silent for a root path, and for a nested one the registry\n * cannot walk: the driver refuses that with its own message.\n */\n const assertNestedWrite = (path: unknown, kind: NestedWriteKind): void => {\n if (typeof path !== \"string\" || !isNestedPath(path) || !driver.registry) return;\n let hop;\n try {\n hop = resolveNestedPath(path, driver.registry);\n } catch {\n return;\n }\n if (!hop) return;\n const session = clientSessions.get(clientId);\n assertNestedWriteAllowed(hop, kind, path, { roles: session?.user?.roles ?? [\"anon\"] });\n };\n\n /** The auth collection at `path`, or `undefined` for any other. */\n const authCollectionAt = (path: unknown): CollectionConfig | undefined => {\n if (typeof path !== \"string\") return undefined;\n let collection;\n try {\n collection = driver.registry?.getCollectionByPath(path);\n } catch {\n return undefined;\n }\n const auth = collection?.auth;\n const isAuthCollection = auth === true || (!!auth && typeof auth === \"object\" && auth.enabled === true);\n return isAuthCollection ? collection : undefined;\n };\n /**\n * The auth collection's rows are the users, so a write to one\n * is user administration — the auth adapter's to check, as it\n * is on the REST routes and `/admin/users`. `undefined` for any\n * other collection, or an adapter with nothing to say.\n */\n const userAdminFor = (path: unknown): AuthAdapter | undefined =>\n authAdapter && authCollectionAt(path) ? authAdapter : undefined;\n\n /**\n * An upsert's options, under the rules `POST /api/data`\n * applies to `?on_conflict=`: refused on a nested path,\n * whose insert branch stamps the parent's key into the\n * DO UPDATE and so moves the row it matched under this\n * parent; and a target that is a declared unique key of\n * the registry's collection, or a 400 naming the ones\n * there are rather than Postgres's 42P10.\n */\n const upsertOptions = (request: SaveProps): Pick<SaveProps, \"upsert\" | \"onConflict\"> => {\n if (request.upsert !== true) return {};\n if (typeof request.path === \"string\" && isNestedPath(request.path)) {\n throw ApiError.badRequest(\n \"An upsert is not accepted on a nested path: a row it matched would be moved \" +\n \"under this parent. Send the upsert to the collection directly.\",\n \"INVALID_CONFLICT_TARGET\"\n );\n }\n const collection = driver.registry?.getCollectionByPath(request.path);\n const onConflict = collection\n ? resolveConflictTarget(request.onConflict, collection, { where: \"`onConflict`\" })\n : undefined;\n return { upsert: true, ...(onConflict && { onConflict }) };\n };\n\n // Helper to get correctly scoped delegate for the current request\n const getScopedDelegate = async (): Promise<DataDriver> => {\n // Check if the driver supports RLS-scoped delegates\n if (typeof driver.withAuth === \"function\") {\n try {\n const who = sessionAuthContext(clientSessions.get(clientId));\n const userForAuth: User = {\n uid: who.uid,\n displayName: null,\n email: null,\n photoURL: null,\n providerId: \"websocket\",\n isAnonymous: who.isAnonymous === true,\n roles: who.roles,\n ...(who.claims ? { claims: who.claims } : {})\n };\n return await driver.withAuth(userForAuth);\n } catch (e) {\n logger.error(\"Failed to create RLS scoped delegate for WS request\", { error: e });\n throw new Error(\"Internal authentication error\");\n }\n }\n return driver;\n };\n\n // A data frame naming a collection this server does not have\n // is the 404 REST gives for it. Left to the driver it was a\n // plain error — `INTERNAL_ERROR`, logged as an incident, its\n // message replaced in production.\n if (PUBLIC_TYPES.has(type)) {\n assertKnownCollection(driver.registry, type === \"DELETE\" ? payload?.row?.path : payload?.path);\n }\n\n switch (type) {\n case \"FETCH_COLLECTION\": {\n wsDebug(\"📋 [WebSocket Server] Processing FETCH_COLLECTION request\");\n const request: FetchCollectionProps = withDecodedFilter(payload);\n assertReadRequest(request.path, request);\n const delegate = await getScopedDelegate();\n // Bound the client-supplied limit with the SAME guarantee\n // the REST ingress and `subscribe_collection` apply\n // (`resolveClientListLimit`). Without it an absent limit\n // reached the driver as `undefined`, which emits no LIMIT\n // clause — one socket frame streamed the whole table, on\n // the one transport that skipped the ceiling every other\n // read path enforces.\n const rows = await delegate.fetchCollection({\n ...request,\n limit: resolveClientListLimit(request.limit, {\n vectorSearch: !!request.vectorSearch\n })\n });\n wsDebug(\"📋 [WebSocket Server] FETCH_COLLECTION result - rows count:\", rows.length);\n const response = {\n type: \"FETCH_COLLECTION_SUCCESS\",\n payload: { rows },\n requestId\n };\n wsDebug(\"📋 [WebSocket Server] Sending FETCH_COLLECTION_SUCCESS response\");\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"FETCH_ONE\": {\n wsDebug(\"📄 [WebSocket Server] Processing FETCH_ENTITY request\");\n const request: FetchOneProps = payload;\n const delegate = await getScopedDelegate();\n const row = await delegate.fetchOne(request);\n wsDebug(\"📄 [WebSocket Server] FETCH_ENTITY result:\", row);\n const response = {\n type: \"FETCH_ONE_SUCCESS\",\n payload: { row: row ?? null },\n requestId\n };\n wsDebug(\"📄 [WebSocket Server] Sending FETCH_ENTITY_SUCCESS response\");\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"SAVE\": {\n wsDebug(\"💾 [WebSocket Server] Processing SAVE_ENTITY request\");\n const request: SaveProps = payload;\n wsDebug(\"💾 [WebSocket Server] Saving row with request:\", inspect(request, { depth: null,\ncolors: true }));\n // The same two checks the REST write routes run, on the\n // same input, at the same point. This socket is the\n // other request boundary — the comment on `requireAuth`\n // above says so — and it used to hand the client's\n // payload straight to the driver, so a value the HTTP\n // API answers 400 for was written when it arrived here.\n //\n // The collection comes from the registry by path, never\n // from `request.collection`: that field is client-\n // supplied, and reading the rules out of it would let\n // the caller choose which rules to be checked against.\n //\n // A create on the auth collection is a user creation,\n // as on `POST /users`: the adapter's step, not a plain\n // insert, and the body checked against what that step\n // consumes (`password` is not a column). An upsert\n // there is refused, as over HTTP: it may be a create or\n // an update, and each has its own rules.\n const upsertedUsers = request.upsert === true ? authCollectionAt(request.path) : undefined;\n if (upsertedUsers) assertNoUserUpsert(authAdapter, upsertedUsers);\n const usersCollection = request.status !== \"existing\" && request.upsert !== true && authAdapter?.prepareUserCreation\n ? authCollectionAt(request.path)\n : undefined;\n const caller = { roles: clientSessions.get(clientId)?.user?.roles ?? [\"anon\"] };\n if (usersCollection) {\n const body = (request.values ?? {}) as Record<string, unknown>;\n assertUserCreationBodyValid(authAdapter, usersCollection, body, caller);\n assertNoFieldOpsOnCreate(body, \"A create\", usersCollection);\n } else {\n assertWriteRequest(request.path, request.values as Record<string, unknown>, request.status);\n }\n // A create is one when the driver takes it for one:\n // no id, or a status that asks for a new row.\n assertNestedWrite(request.path, request.id === undefined || request.status !== \"existing\" ? \"create\" : \"update\");\n const delegate = await getScopedDelegate();\n // The address, the values and the status, and nothing\n // else the frame says — the DELETE case below is\n // narrowed for the same reason. A `collection` here was\n // merged under the registry's in the driver, so a key\n // the registry leaves unset was the caller's to set\n // (`history: true` on a collection declaring none).\n const userAdmin = request.status === \"existing\" && request.id !== undefined\n ? userAdminFor(request.path)\n : undefined;\n const [values] = userAdmin?.prepareUserUpdates\n ? await userAdmin.prepareUserUpdates([{ uid: String(request.id), values: request.values as Record<string, unknown> }])\n : [request.values];\n const write = (toWrite: SaveProps[\"values\"]) => delegate.save({\n path: request.path,\n ...(request.id !== undefined && { id: request.id }),\n values: toWrite,\n status: request.status,\n ...upsertOptions(request)\n });\n // The user creation answers with what `POST /users`\n // does: the row, and whether an invitation went out or\n // the temporary password that did not.\n const created = usersCollection\n ? await createUserThroughAuthCollection(authAdapter, usersCollection, (values ?? {}) as Record<string, unknown>, caller, write)\n : undefined;\n const row = created ? { ...created.row, ...created.delivery } : await write(values);\n wsDebug(\"💾 [WebSocket Server] SAVE_ENTITY result:\", inspect(row, { depth: null,\ncolors: true }));\n const response = {\n type: \"SAVE_SUCCESS\",\n payload: { row },\n requestId\n };\n wsDebug(\"💾 [WebSocket Server] Sending SAVE_ENTITY_SUCCESS response\");\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"DELETE\": {\n wsDebug(\"🗑️ [WebSocket Server] Processing DELETE_ENTITY request\");\n const request: DeleteProps = payload;\n wsDebug(\"🗑️ [WebSocket Server] Deleting row:\", request.row);\n // Through a many-to-many path this drops the link — a\n // write of the parent's relation.\n assertNestedWrite(request.row?.path, \"unlink\");\n const delegate = await getScopedDelegate();\n // The address and `hard`, and nothing else the frame\n // says. The driver reads the row itself, and resolves\n // the collection from the registry by path — a\n // `collection` passed along was merged under the\n // registry's, so a key the registry does not declare\n // survived: `softDelete: { field: \"title\" }` turned\n // this DELETE into an UPDATE of `title` that no\n // `beforeSave` and no write validator saw.\n const userAdmin = userAdminFor(request.row?.path);\n const uids = [String(request.row.id)];\n await userAdmin?.prepareUserDeletions?.(uids);\n await delegate.delete({\n row: { id: request.row.id, path: request.row.path },\n hard: request.hard\n });\n await userAdmin?.finalizeUserDeletions?.(uids);\n wsDebug(\"🗑️ [WebSocket Server] DELETE_ENTITY completed successfully\");\n const response = {\n type: \"DELETE_SUCCESS\",\n payload: { success: true },\n requestId\n };\n wsDebug(\"🗑️ [WebSocket Server] Sending DELETE_ENTITY_SUCCESS response\");\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"CHECK_UNIQUE_FIELD\": {\n wsDebug(\"🔍 [WebSocket Server] Processing CHECK_UNIQUE_FIELD request\");\n const {\n path,\n name,\n value,\n id,\n collection\n } = payload;\n // \"Does anybody hold this value\" is a read of the field,\n // answered for every row in the table.\n if (typeof name === \"string\") assertReadRequest(path, { filter: { [name]: [\"==\", value] } });\n const delegate = await getScopedDelegate();\n const isUnique = await delegate.checkUniqueField(path, name, value, id, collection);\n wsDebug(\"🔍 [WebSocket Server] CHECK_UNIQUE_FIELD result:\", isUnique);\n const response = {\n type: \"CHECK_UNIQUE_FIELD_SUCCESS\",\n payload: { isUnique },\n requestId\n };\n wsDebug(\"🔍 [WebSocket Server] Sending CHECK_UNIQUE_FIELD_SUCCESS response\");\n ws.send(JSON.stringify(response));\n }\n break;\n\n\n case \"COUNT\": {\n // Deliberately NOT routed through `resolveClientListLimit`:\n // this answers with a scalar, and the driver drops `limit`\n // on the way to `SELECT count(*)`. Clamping here could only\n // ever make `total` describe fewer rows than the collection\n // holds — the page size is the caller's business, the total\n // is not.\n const request: FetchCollectionProps = withDecodedFilter(payload);\n assertReadRequest(request.path, request);\n const delegate = await getScopedDelegate();\n const count = await delegate.count!(request);\n const response = {\n type: \"COUNT_SUCCESS\",\n payload: { count },\n requestId\n };\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"EXECUTE_SQL\": {\n const { sql, options, mode } = payload;\n // A console run can be stopped by `CANCEL_SQL` while\n // it is in flight — one with a requestId to name it.\n const cancel = mode === \"script\" && typeof requestId === \"string\" && !sqlRuns.has(requestId)\n ? new AbortController()\n : undefined;\n if (cancel && requestId) sqlRuns.set(requestId, cancel);\n try {\n const delegate = await getScopedDelegate();\n const admin = delegate.admin;\n if (!isSQLAdmin(admin)) {\n sendError(\"ERROR\", \"NOT_SUPPORTED\", \"SQL execution is not available for this driver.\");\n break;\n }\n // The console's own runs: the result says, per\n // column, which table column the database read it\n // from — what an edit of a cell is written back\n // by. The rows still go in `result`, where a\n // client that reads nothing else finds them.\n const script = mode === \"script\" && typeof admin.runSqlScript === \"function\"\n ? await admin.runSqlScript(String(sql), {\n database: typeof options?.database === \"string\" ? options.database : undefined,\n role: typeof options?.role === \"string\" ? options.role : undefined,\n ...(cancel ? { signal: cancel.signal } : {})\n })\n : undefined;\n // A session of its own, reset before it goes back\n // to the pool: what a person types may `SET ROLE`,\n // and that must not reach the next request on\n // that connection. And a result past the byte\n // budget refused, never sent in part. Both set\n // here, after the client's options, so a frame\n // cannot turn them off.\n const result = script\n ? script.rows\n : await admin.executeSql(sql, { ...options, isolateSession: true, maxResultBytes: SQL_RESULT_BYTE_BUDGET });\n // Which role the rows were read as, said beside them:\n // the role picked, unless the server runs every\n // statement as the connection owner.\n const ranAs = typeof options?.role === \"string\" && options.role\n ? await sqlRoleRanAs(admin, options.role, typeof options?.database === \"string\" ? options.database : undefined)\n : undefined;\n if (process.env.NODE_ENV !== \"production\") {\n wsDebug(`⚡ [WebSocket Server] SQL executed. Returned ${Array.isArray(result) ? result.length : \"non-array\"} rows.`);\n }\n const auditSession = clientSessions.get(clientId);\n // Through `logger`, not `console.log`: this line is\n // emitted in production, and a bare console call\n // has no severity, no timestamp, no JSON envelope\n // and no LOG_LEVEL gate, so it lands in Cloud\n // Logging as unstructured text the queries written\n // for every other line cannot match.\n //\n // The bound values are counted, never written — the\n // statement is the audit signal, the parameters are\n // whatever row the operator was touching. (stdout is\n // not an audit sink either; a real trail belongs in\n // a table with an actor and a retention policy.)\n //\n // And for the same reason the values written inline\n // as literals are masked: `ALTER ROLE app PASSWORD\n // '…'` put the password in the production logs.\n // Masked before the cut, so the cut cannot land\n // inside a literal and keep half of it.\n logger.info(\"[SQL Audit] WebSocket SQL execution\", {\n sql: redactSqlLiterals(String(sql)).substring(0, 500),\n database: options?.database,\n role: options?.role,\n // The role the statement asked for is above;\n // this is the one it ran as. They used to be\n // assumed identical, so an execution that fell\n // back to the owner was audited under the role\n // it had failed to assume. That fallback is now\n // an error everywhere except the documented\n // `DISABLE_DB_ROLE_SWITCHING` opt-out — which is\n // precisely the case this line still has to\n // report honestly.\n effectiveRole: effectiveSqlRole(options?.role),\n paramCount: Array.isArray(options?.params) ? options.params.length : 0,\n resultRows: Array.isArray(result) ? result.length : \"unknown\",\n uid: auditSession?.user?.uid ?? \"unknown\",\n roles: auditSession?.user?.roles ?? [],\n isAdmin: auditSession?.user?.isAdmin ?? false,\n ...(auditSession?.user?.impersonator ? { impersonatedBy: auditSession.user.impersonator } : {}),\n requestId\n });\n const response = {\n type: \"EXECUTE_SQL_SUCCESS\",\n // The rows once, as `result`; the rest of the\n // script's description beside them.\n payload: {\n ...(script ? { ...scriptDescription(script), result } : { result }),\n ...(ranAs ? { effectiveRole: ranAs } : {})\n },\n requestId\n };\n ws.send(JSON.stringify(response));\n } catch (sqlError: unknown) {\n // A refusal, not a failure of the SQL: its own code,\n // and a message that names the limit and the way\n // round it.\n if (sqlError instanceof SqlResultTooLargeError) {\n ws.send(JSON.stringify({\n type: \"ERROR\",\n requestId,\n payload: { error: { message: sqlError.message, code: \"RESULT_TOO_LARGE\" } }\n }));\n break;\n }\n // This is a query execution error (e.g., syntax error, permission denied).\n // We return it cleanly to the client without logging a server stack trace.\n const errMsg = extractErrorMessage(sqlError);\n sendError(\"ERROR\", \"SQL_ERROR\", errMsg);\n } finally {\n if (cancel && requestId && sqlRuns.get(requestId) === cancel) sqlRuns.delete(requestId);\n }\n }\n break;\n\n case \"CANCEL_SQL\": {\n // The run named is this socket's, or nothing: the map\n // is per socket. `cancelled` says whether one was\n // still in flight; the run itself answers on its own\n // requestId, with the database's \"canceling statement\".\n const target = (payload as { requestId?: unknown } | undefined)?.requestId;\n const run = typeof target === \"string\" ? sqlRuns.get(target) : undefined;\n run?.abort();\n ws.send(JSON.stringify({\n type: \"CANCEL_SQL_SUCCESS\",\n requestId,\n payload: { cancelled: run !== undefined }\n }));\n }\n break;\n\n case \"FETCH_DATABASES\": {\n wsDebug(\"📚 [WebSocket Server] Processing FETCH_DATABASES request\");\n const delegate = await getScopedDelegate();\n const admin = delegate.admin;\n let databases: string[] = [];\n if (isSQLAdmin(admin) && admin.fetchAvailableDatabases) {\n databases = await admin.fetchAvailableDatabases();\n }\n wsDebug(`📚 [WebSocket Server] Fetched ${databases.length} databases.`);\n const response = {\n type: \"FETCH_DATABASES_SUCCESS\",\n payload: { databases },\n requestId\n };\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"FETCH_ROLES\": {\n wsDebug(\"👤 [WebSocket Server] Processing FETCH_ROLES request\");\n const delegate = await getScopedDelegate();\n const admin = delegate.admin;\n let roles: string[] = [];\n if (isSQLAdmin(admin) && admin.fetchAvailableRoles) {\n roles = await admin.fetchAvailableRoles();\n }\n wsDebug(`👤 [WebSocket Server] Fetched ${roles.length} roles.`);\n const response = {\n type: \"FETCH_ROLES_SUCCESS\",\n payload: { roles },\n requestId\n };\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"FETCH_APPLICATION_ROLES\": {\n wsDebug(\"👤 [WebSocket Server] Processing FETCH_APPLICATION_ROLES request\");\n const delegate = await getScopedDelegate();\n const admin = delegate.admin;\n let roles: string[] = [];\n if (isSQLAdmin(admin) && admin.fetchApplicationRoles) {\n roles = await admin.fetchApplicationRoles();\n }\n wsDebug(`👤 [WebSocket Server] Fetched ${roles.length} application roles.`);\n const response = {\n type: \"FETCH_APPLICATION_ROLES_SUCCESS\",\n payload: { roles },\n requestId\n };\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"FETCH_CURRENT_DATABASE\": {\n wsDebug(\"📚 [WebSocket Server] Processing FETCH_CURRENT_DATABASE request\");\n const delegate = await getScopedDelegate();\n const admin = delegate.admin;\n let database: string | undefined = undefined;\n if (isSQLAdmin(admin) && admin.fetchCurrentDatabase) {\n database = await admin.fetchCurrentDatabase();\n }\n const response = {\n type: \"FETCH_CURRENT_DATABASE_SUCCESS\",\n payload: { database },\n requestId\n };\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"FETCH_UNMAPPED_TABLES\": {\n wsDebug(\"📋 [WebSocket Server] Processing FETCH_UNMAPPED_TABLES request\");\n const delegate = await getScopedDelegate();\n const admin = delegate.admin;\n let tables: string[] = [];\n if (isSchemaAdmin(admin) && admin.fetchUnmappedTables) {\n tables = await admin.fetchUnmappedTables(payload?.mappedPaths);\n }\n wsDebug(`📋 [WebSocket Server] Fetched ${tables.length} unmapped tables.`);\n const response = {\n type: \"FETCH_UNMAPPED_TABLES_SUCCESS\",\n payload: { tables },\n requestId\n };\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"FETCH_TABLE_METADATA\": {\n wsDebug(\"📋 [WebSocket Server] Processing FETCH_TABLE_METADATA request\");\n const { tableName } = payload;\n const delegate = await getScopedDelegate();\n const admin = delegate.admin;\n let metadata: TableMetadata | undefined;\n if (isSchemaAdmin(admin) && admin.fetchTableMetadata) {\n metadata = await admin.fetchTableMetadata(tableName) as TableMetadata;\n }\n wsDebug(`📋 [WebSocket Server] Fetched metadata for table '${tableName}'. (${metadata?.columns?.length ?? 0} columns)`);\n const response = {\n type: \"FETCH_TABLE_METADATA_SUCCESS\",\n payload: { metadata },\n requestId\n };\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"CREATE_BRANCH\": {\n wsDebug(\"🌿 [WebSocket Server] Processing CREATE_BRANCH request\");\n const { name, options } = payload;\n const delegate = await getScopedDelegate();\n if (!delegate.admin?.createBranch) {\n sendError(\"ERROR\", \"NOT_SUPPORTED\", \"Database branching is not available. Configure adminConnectionString.\");\n break;\n }\n const branch: BranchInfo = await delegate.admin.createBranch(name, options);\n wsDebug(`🌿 [WebSocket Server] Branch created: ${branch.name}`);\n const response = {\n type: \"CREATE_BRANCH_SUCCESS\",\n payload: { branch },\n requestId\n };\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"DELETE_BRANCH\": {\n wsDebug(\"🗑️ [WebSocket Server] Processing DELETE_BRANCH request\");\n const { name: branchName } = payload;\n const delegate = await getScopedDelegate();\n if (!delegate.admin?.deleteBranch) {\n sendError(\"ERROR\", \"NOT_SUPPORTED\", \"Database branching is not available.\");\n break;\n }\n await delegate.admin.deleteBranch(branchName);\n wsDebug(`🗑️ [WebSocket Server] Branch deleted: ${branchName}`);\n const response = {\n type: \"DELETE_BRANCH_SUCCESS\",\n payload: { success: true },\n requestId\n };\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"LIST_BRANCHES\": {\n wsDebug(\"🌿 [WebSocket Server] Processing LIST_BRANCHES request\");\n const delegate = await getScopedDelegate();\n let branches: BranchInfo[] = [];\n if (delegate.admin?.listBranches) {\n branches = await delegate.admin.listBranches();\n }\n wsDebug(`🌿 [WebSocket Server] Listed ${branches.length} branches.`);\n const response = {\n type: \"LIST_BRANCHES_SUCCESS\",\n payload: { branches },\n requestId\n };\n ws.send(JSON.stringify(response));\n }\n break;\n\n // Route subscription messages, broadcast channels, and presence to RealtimeService\n case \"subscribe_collection\":\n case \"subscribe_one\":\n case \"unsubscribe\":\n case \"join_channel\":\n case \"leave_channel\":\n case \"broadcast\":\n case \"presence_track\":\n case \"presence_untrack\":\n case \"presence_state\":\n case \"channel_history\": {\n wsDebug(\"🔄 [WebSocket Server] Routing realtime message to RealtimeService:\", type);\n // Attach auth context from the WS session so RLS-aware\n // refetches read as the same principal the request\n // frames do.\n const authContext = sessionAuthContext(clientSessions.get(clientId));\n // Let RealtimeService handle these messages\n await realtimeService.handleClientMessage(clientId, {\n type,\n payload,\n subscriptionId: payload?.subscriptionId\n }, authContext);\n break;\n }\n\n default:\n // Answered, so the client's request settles with a\n // reason rather than timing out on silence.\n logger.warn(\"[WebSocket Server] Unknown message type\", { detail: type });\n throw ApiError.badRequest(\n `Unknown message type '${String(type)}'.`,\n \"UNKNOWN_MESSAGE_TYPE\"\n );\n }\n } catch (error: unknown) {\n // A refused `limit` is the caller's mistake, not a server fault.\n // Left to the generic branch below it answers INTERNAL_ERROR\n // with the message suppressed in production — so the one thing\n // that would tell the caller what to send instead is exactly\n // what gets dropped. Answered here the way\n // `subscribe_collection` already answers it: INVALID_LIMIT,\n // message intact. The text names the ceiling and nothing else.\n if (error instanceof ListLimitError) {\n logger.warn(`[WebSocket Server] Refused a list read: ${error.message}`);\n ws.send(JSON.stringify({\n type: \"ERROR\",\n requestId,\n payload: { error: { message: error.message,\ncode: \"INVALID_LIMIT\" } }\n }));\n return;\n }\n // \"This server cannot branch\" is a refusal too, and the same\n // reasoning applies twice over: the generic branch would drop\n // the text in production, and the text is the only thing that\n // says what to do instead. Studio renders it where the success\n // toast used to be.\n if (error instanceof BranchingUnsupportedError\n || (error as Error)?.name === \"BranchingUnsupportedError\") {\n const refusal = error as BranchingUnsupportedError;\n logger.warn(`[WebSocket Server] Refused a branch operation: ${refusal.message}`);\n ws.send(JSON.stringify({\n type: \"ERROR\",\n requestId,\n payload: { error: { message: refusal.message,\ncode: \"BRANCHING_UNSUPPORTED\" } }\n }));\n return;\n }\n // A refused write is the caller's mistake, and its message is\n // the only thing that says what to send instead — the same\n // reasoning as `ListLimitError` above. Left to the generic\n // branch it becomes INTERNAL_ERROR with the text dropped in\n // production, so the socket would refuse the write and decline\n // to say why.\n //\n // \"Refused\" means whatever REST answers with the error's own\n // status, by the same predicate: an `ApiError`, and the\n // `RebaseApiError` every collection-callback veto is. Listing\n // only the first here is how a `beforeDelete` veto reached the\n // admin panel as \"An unexpected error occurred\".\n // The database unreachable is said as that, the answer the\n // HTTP doors give, rather than as a server fault.\n const answer = declaredErrorAnswer(error)\n ?? (isDatabaseUnreachable(error) ? declaredErrorAnswer(ApiError.databaseUnavailable()) : undefined);\n if (answer) {\n const line = `[WebSocket Server] Refused: ${answer.code} ${answer.message}`;\n if (answer.expected) logger.debug(line);\n else logger.warn(line);\n ws.send(JSON.stringify({\n type: \"ERROR\",\n requestId,\n payload: { error: {\n message: answer.message,\n code: answer.code,\n ...(answer.details !== undefined && { details: answer.details })\n } }\n }));\n return;\n }\n logger.error(\"💥 [WebSocket Server] Error handling message\", { error: error });\n if (error instanceof Error) {\n logger.error(\"Stack trace\", { detail: error.stack });\n }\n // Unwrap the cause chain: a Drizzle failure reports itself as\n // \"Failed query: <sql> params:\", which tells the user nothing and\n // echoes the statement back at them. The reason is in the cause.\n const errorMessage = process.env.NODE_ENV === \"production\"\n ? \"An unexpected error occurred\"\n : extractErrorMessage(error);\n const errorResponse = {\n type: \"ERROR\",\n requestId,\n payload: {\n error: {\n message: errorMessage,\n code: \"INTERNAL_ERROR\"\n }\n }\n };\n ws.send(JSON.stringify(errorResponse));\n }\n });\n });\n\n // A peer that vanished without a close — lost signal, sleep, a forgotten\n // NAT mapping — is let go of once it stops answering pings. Stopped with\n // whichever goes first, like the identity sweep.\n const stopReaping = reapSilentSockets(wss);\n wss.on(\"close\", stopReaping);\n if (typeof server.on === \"function\") server.on(\"close\", stopReaping);\n\n // And what its sockets hold unread counts in the budget for all of the\n // process's sockets, not only in each one's own ceiling.\n const stopBudgeting = budgetSendBuffers(wss);\n wss.on(\"close\", stopBudgeting);\n if (typeof server.on === \"function\") server.on(\"close\", stopBudgeting);\n\n // The heartbeat a browser needs to notice a half-open socket, for the\n // sockets that asked for it. One timer for all of them.\n const stopHeartbeats = startHeartbeats(() => heartbeatSockets, heartbeatIntervalMs);\n wss.on(\"close\", stopHeartbeats);\n if (typeof server.on === \"function\") server.on(\"close\", stopHeartbeats);\n}\n"],"mappings":"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"}
|