@rebasepro/server-postgres 0.20.1-canary.g4d882ca → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/PostgresBackendDriver.d.ts +33 -3
- package/dist/auth/services.d.ts +19 -1
- package/dist/{backup-cli-DSpQyqcG.js → backup-cli-Bp-ou6o2.js} +2 -2
- package/dist/{backup-cli-DSpQyqcG.js.map → backup-cli-Bp-ou6o2.js.map} +1 -1
- package/dist/{backup-service-DA7a6SUV.js → backup-service-BNLwvxuy.js} +2 -2
- package/dist/{backup-service-DA7a6SUV.js.map → backup-service-BNLwvxuy.js.map} +1 -1
- package/dist/cli.js +7 -7
- package/dist/column-plan-helpers-CpILzHJS.js.map +1 -1
- package/dist/{doctor-D5SrGJ5P.js → doctor-ByFWL_ET.js} +25 -6
- package/dist/doctor-ByFWL_ET.js.map +1 -0
- package/dist/{ensure-collection-policies-CHO0moXQ.js → ensure-collection-policies-CQGHln-y.js} +2 -2
- package/dist/{ensure-collection-policies-CHO0moXQ.js.map → ensure-collection-policies-CQGHln-y.js.map} +1 -1
- package/dist/{ensure-tables-CYMtuuOd.js → ensure-tables-BnEvEJPr.js} +2 -2
- package/dist/ensure-tables-BnEvEJPr.js.map +1 -0
- package/dist/history/HistoryService.d.ts +16 -2
- package/dist/history/ensure-history-table.d.ts +7 -1
- package/dist/index.es.js +1580 -1085
- package/dist/index.es.js.map +1 -1
- package/dist/plan-schema-Hgl62S-w.js.map +1 -1
- package/dist/{rls-bootstrap-sql-BlzsOUtz.js → rls-bootstrap-sql-BHA6jLtj.js} +4 -3
- package/dist/{rls-bootstrap-sql-BlzsOUtz.js.map → rls-bootstrap-sql-BHA6jLtj.js.map} +1 -1
- package/dist/{rls-enforcement-BV12vBwQ.js → rls-enforcement-C6Xk0lA6.js} +20 -20
- package/dist/{rls-enforcement-BV12vBwQ.js.map → rls-enforcement-C6Xk0lA6.js.map} +1 -1
- package/dist/schema/doctor-cli.js +2 -2
- package/dist/services/PersistService.d.ts +11 -0
- package/dist/services/RelationService.d.ts +39 -0
- package/dist/services/RelationWriteService.d.ts +32 -1
- package/dist/services/dataService.d.ts +2 -0
- package/dist/services/junction-writes.d.ts +58 -2
- package/dist/services/write-transaction-scope.d.ts +68 -0
- package/dist/types.d.ts +18 -0
- package/package.json +7 -7
- package/dist/doctor-D5SrGJ5P.js.map +0 -1
- package/dist/ensure-tables-CYMtuuOd.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/server-postgres",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "PostgreSQL data source backend implementation for Rebase with Drizzle ORM",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cms",
|
|
@@ -50,11 +50,11 @@
|
|
|
50
50
|
"execa": "^9.6.1",
|
|
51
51
|
"pg": "^8.22.0",
|
|
52
52
|
"ws": "^8.21.1",
|
|
53
|
-
"@rebasepro/
|
|
54
|
-
"@rebasepro/
|
|
55
|
-
"@rebasepro/
|
|
56
|
-
"@rebasepro/
|
|
57
|
-
"@rebasepro/
|
|
53
|
+
"@rebasepro/codegen": "0.21.0",
|
|
54
|
+
"@rebasepro/utils": "0.21.0",
|
|
55
|
+
"@rebasepro/server": "0.21.0",
|
|
56
|
+
"@rebasepro/common": "0.21.0",
|
|
57
|
+
"@rebasepro/types": "0.21.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@electric-sql/pglite": "0.5.6",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"typescript": "^6.0.3",
|
|
71
71
|
"vite": "^8.1.5",
|
|
72
72
|
"vitest": "^4.1.10",
|
|
73
|
-
"@rebasepro/client": "0.
|
|
73
|
+
"@rebasepro/client": "0.21.0"
|
|
74
74
|
},
|
|
75
75
|
"gitHead": "d935eefa5aa8d1009a2398cfac2c1e4ee9aeb6b6",
|
|
76
76
|
"publishConfig": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"doctor-D5SrGJ5P.js","names":[],"sources":["../../codegen/dist/index.es.js","../src/schema/doctor.ts"],"sourcesContent":["import { effectiveAccess, fieldKeyForColumn, findRelation, isRelationRequired, resolveCollectionRelations, sortCollectionsBySlug } from \"@rebasepro/common\";\n//#region src/utils.ts\n/**\n* Utility functions for the SDK generator\n*/\n/**\n* Convert a slug/snake_case string to PascalCase\n* e.g. \"private_notes\" → \"PrivateNotes\"\n*\n* Capitals already inside a word are meaningful and are kept: lowercasing the\n* tail of every chunk turned \"TestEntities\" into \"Testentities\", which is what\n* ended up in the generated type names. SHOUTING_CASE is the one shape where\n* the tail is not meaningful, so it is folded down.\n*/\nfunction toPascalCase(str) {\n\treturn str.split(/[_\\-\\s]+/).filter(Boolean).map((word) => {\n\t\tconst rest = /^[A-Z0-9]+$/.test(word) ? word.slice(1).toLowerCase() : word.slice(1);\n\t\treturn word.charAt(0).toUpperCase() + rest;\n\t}).join(\"\");\n}\n/**\n* Convert a slug/snake_case string to camelCase\n* e.g. \"private_notes\" → \"privateNotes\"\n*/\nfunction toCamelCase(str) {\n\tif (!/[_\\-\\s]/.test(str)) return str.charAt(0).toLowerCase() + str.slice(1);\n\tconst pascal = toPascalCase(str);\n\treturn pascal.charAt(0).toLowerCase() + pascal.slice(1);\n}\n/**\n* Convert a slug to a safe JS identifier\n* e.g. \"private-notes\" → \"privateNotes\"\n*/\nfunction toSafeIdentifier(str) {\n\treturn toCamelCase(str.replace(/[^a-zA-Z0-9_]/g, \"_\"));\n}\n/**\n* Indent a block of text by a given number of spaces\n*/\nfunction indent(text, spaces) {\n\tconst pad = \" \".repeat(spaces);\n\treturn text.split(\"\\n\").map((line) => line.trim() ? pad + line : line).join(\"\\n\");\n}\n//#endregion\n//#region src/generate-types.ts\n/**\n* A schema that cannot be expressed as a valid TypeScript file.\n*\n* Thrown rather than emitted. The generator used to concatenate whatever it was\n* given, so a slug that collided with another one, or that was not an\n* identifier, produced a file that either failed to compile or — worse —\n* compiled while quietly routing one collection to another's slug.\n*/\nvar CodegenError = class extends Error {\n\tconstructor(message) {\n\t\tsuper(message);\n\t\tthis.name = \"CodegenError\";\n\t}\n};\nvar IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n/**\n* A property name for the emitted TypeScript: verbatim when it is a valid\n* identifier, quoted otherwise.\n*\n* Every key that reaches the output goes through here. Column names are not\n* required to be identifiers — `\"order\"`, `\"user id\"`, a quoted Postgres\n* identifier — and the previous behaviour of camel-casing them into shape\n* renamed the column in the type while the wire kept the original, so the\n* generated `Row` described fields that did not exist.\n*/\nfunction emitKey(key) {\n\treturn IDENTIFIER.test(key) ? key : JSON.stringify(key);\n}\n/**\n* A string literal, escaped.\n*\n* `\"${value}\"` was the previous form. A value containing a quote closed the\n* literal early, which at best broke the file and at worst let a slug from a\n* remote contract inject top-level statements into a file the developer\n* compiles and bundles.\n*/\nfunction emitString(value) {\n\treturn JSON.stringify(value);\n}\n/** The `id`s of an enum declared as an array, an array of `{ id }`, or an object map. */\nfunction enumIds(raw) {\n\tif (Array.isArray(raw)) return raw.map((entry) => entry && typeof entry === \"object\" ? entry.id : entry);\n\tif (raw && typeof raw === \"object\") return Object.keys(raw);\n\treturn [];\n}\nfunction propertyToTypeScriptType(prop) {\n\tswitch (prop.type) {\n\t\tcase \"string\": {\n\t\t\tconst sp = prop;\n\t\t\tif (sp.enum) {\n\t\t\t\tconst ids = enumIds(sp.enum);\n\t\t\t\tif (ids.length === 0) return \"string\";\n\t\t\t\treturn ids.map((v) => emitString(String(v))).join(\" | \");\n\t\t\t}\n\t\t\treturn \"string\";\n\t\t}\n\t\tcase \"number\": {\n\t\t\tconst np = prop;\n\t\t\tif (np.enum) {\n\t\t\t\tconst ids = enumIds(np.enum);\n\t\t\t\tconst numbers = ids.map(Number);\n\t\t\t\tif (ids.length === 0 || numbers.some((n) => !Number.isFinite(n))) return \"number\";\n\t\t\t\treturn numbers.map((n) => String(n)).join(\" | \");\n\t\t\t}\n\t\t\treturn \"number\";\n\t\t}\n\t\tcase \"boolean\": return \"boolean\";\n\t\tcase \"date\": return \"string\";\n\t\tcase \"geopoint\": return \"{ latitude: number; longitude: number; }\";\n\t\tcase \"reference\": return \"string | number\";\n\t\tcase \"relation\": return \"string | number\";\n\t\tcase \"map\": {\n\t\t\tconst mapProp = prop;\n\t\t\tif (mapProp.properties) return `{ ${Object.entries(mapProp.properties).map(([k, v]) => {\n\t\t\t\tconst child = v;\n\t\t\t\tconst optional = !child.validation?.required;\n\t\t\t\tconst type = propertyToTypeScriptType(child);\n\t\t\t\treturn `${emitKey(k)}${optional ? \"?\" : \"\"}: ${optional ? `${type} | null` : type};`;\n\t\t\t}).join(\" \")} }`;\n\t\t\treturn \"Record<string, unknown>\";\n\t\t}\n\t\tcase \"array\": {\n\t\t\tconst arrProp = prop;\n\t\t\tif (arrProp.of) return `Array<${propertyToTypeScriptType(arrProp.of)}>`;\n\t\t\treturn \"Array<unknown>\";\n\t\t}\n\t\tcase \"vector\": return \"number[]\";\n\t\tcase \"binary\": return \"string\";\n\t\tdefault: return \"unknown\";\n\t}\n}\n/**\n* Unwrap a relation target that arrived as a module namespace rather than the\n* collection itself — `target: () => import(\"./authors\")` is a common slip.\n*/\nfunction resolveTargetCollection(relation) {\n\ttry {\n\t\tlet target = relation.target();\n\t\tif (target && (target.default || target.__esModule)) target = target.default ?? target;\n\t\treturn target;\n\t} catch {\n\t\treturn;\n\t}\n}\n/** The TypeScript type of a foreign key: whatever the target's primary key is. */\nfunction foreignKeyType(relation) {\n\tconst target = resolveTargetCollection(relation);\n\tif (!target?.properties) return \"string | number\";\n\tconst idProp = Object.entries(target.properties).find(([_, p]) => p.isId);\n\tif (!idProp) return \"string | number\";\n\treturn idProp[1].type === \"number\" ? \"number\" : \"string\";\n}\n/** Whether a property is the collection's primary key. */\nfunction isPrimaryKey(prop) {\n\treturn Boolean(prop.isId);\n}\n/**\n* Whether the server assigns this primary key, so a write does not have to.\n* `true` and `\"manual\"` both mean the caller supplies it.\n*/\nfunction isAutoAssignedId(prop) {\n\tconst isId = prop.isId;\n\treturn Boolean(isId) && isId !== \"manual\" && isId !== true;\n}\n/**\n* The type an *included* relation arrives as: the target's own row, inlined.\n*\n* This is what the read pipeline actually serves — `toRestRow` puts the\n* target's flat columns where the relation was, and the SDK and the HTTP API\n* both go through it. It is deliberately *not* a `{ __type: \"relation\" }`\n* envelope: that shape is the admin's view-model and never reaches a\n* developer's `find()`.\n*\n* Falls back to an open record when the target is not part of this generation\n* run, since there is no `Row` to point at.\n*/\nfunction includedRelationType(relation, accessors) {\n\tconst slug = resolveTargetCollection(relation)?.slug ?? relation.targetSlug;\n\tconst accessor = slug ? accessors.get(slug) : void 0;\n\tconst rowType = accessor ? `Database[${emitString(accessor)}][\"Row\"]` : \"Record<string, unknown>\";\n\treturn relation.cardinality === \"many\" ? `Array<${rowType}>` : rowType;\n}\n/**\n* Map every slug to the property name it is reachable under on `client.data`.\n*\n* The accessor is a safe identifier because `client.data.myNotes` is the point\n* of generating this at all, and `collectionsDictionary` maps it back to the\n* slug the wire uses. Two slugs that safe down to the same identifier cannot\n* both have it: the interface would not compile, and the dictionary — an object\n* literal — would silently keep only the last, routing one collection's reads\n* to the other's table. There is no defensible way to pick, so this refuses.\n*/\nfunction buildAccessors(collections) {\n\tconst accessors = /* @__PURE__ */ new Map();\n\tconst bySafeName = /* @__PURE__ */ new Map();\n\tfor (const collection of collections) {\n\t\tconst slug = collection.slug;\n\t\tif (typeof slug !== \"string\" || slug.length === 0) throw new CodegenError(\"A collection has no slug, so it has no name to generate a type for. Every collection needs a unique `slug`.\");\n\t\tconst safe = toSafeIdentifier(slug);\n\t\tif (safe.length === 0) throw new CodegenError(`The slug ${emitString(slug)} has no characters that can form a property name, so it cannot be reached as \\`client.data.<name>\\`. Use a slug containing letters, digits, underscores or dashes.`);\n\t\tconst existing = bySafeName.get(safe);\n\t\tif (existing !== void 0) throw new CodegenError(`The collections ${emitString(existing)} and ${emitString(slug)} both generate the accessor \"${safe}\", so only one of them could be reached from the generated client and the other's reads would silently go to the wrong table. Rename one of the slugs.`);\n\t\tbySafeName.set(safe, slug);\n\t\taccessors.set(slug, safe);\n\t}\n\treturn accessors;\n}\n/** One emitted `key: type;` line, already indented. */\nfunction line(key, type, optional) {\n\treturn ` ${emitKey(key)}${optional ? \"?\" : \"\"}: ${type};`;\n}\n/**\n* The keys nobody can reach take off the API surface — in *both* directions.\n*\n* `excludeFromApi` means one thing: the API surface does not mention this\n* property. `Row` already honoured that; `Insert` and `Update` deliberately did\n* not, on the reading that the column is stripped from responses rather than\n* from writes. That left the generated types as the one place a password hash\n* was still named, and it invited a client to send one. The server still\n* *accepts* such a field on a write — this describes the surface, it does not\n* add an enforcement point — but nothing generated advertises it.\n*\n* Read through `effectiveAccess`, so the flag and its longhand\n* `access: { read: [], write: [] }` produce the same file. A *role* rule is\n* deliberately not honoured here and `Row` is unchanged by one: a generated type\n* is one shape for every caller, and there is no `Row` that is right for both a\n* reader who holds `hr` and one who does not. The server is the enforcement\n* point; the types describe the surface a caller may name.\n*\n* Keyed by the property name *and* by its column name, the same pair the\n* server's `stripUnreadable` deletes, so a foreign key or a relation addressed\n* under the column name cannot put the property back.\n*/\nfunction excludedApiKeys(properties) {\n\tconst excluded = /* @__PURE__ */ new Set();\n\tfor (const [key, rawProp] of Object.entries(properties)) {\n\t\tconst prop = rawProp;\n\t\tconst access = effectiveAccess(prop);\n\t\tif (access?.read?.length !== 0 || access?.write?.length !== 0) continue;\n\t\texcluded.add(key);\n\t\tif (prop.columnName) excluded.add(prop.columnName);\n\t}\n\treturn excluded;\n}\nfunction generateTypedefs(input) {\n\tconst collections = sortCollectionsBySlug(input);\n\tconst accessors = buildAccessors(collections);\n\tconst lines = [\n\t\t\"/**\",\n\t\t\" * This file was auto-generated by Rebase.\",\n\t\t\" * Do not make direct changes to the file.\",\n\t\t\" */\",\n\t\t\"\",\n\t\t\"export interface Database {\"\n\t];\n\tfor (const collection of collections) {\n\t\tconst properties = collection.properties ?? {};\n\t\tlet resolvedRelations = {};\n\t\ttry {\n\t\t\tresolvedRelations = resolveCollectionRelations(collection);\n\t\t} catch (e) {\n\t\t\tconsole.warn(`[rebase] Could not resolve the relations of \"${collection.slug}\", so its generated type has no relation fields and none of their foreign-key columns. This is usually a circular import in the collection files — make sure the target is \\`() => otherCollection\\` and not evaluated at module load.\\n ${e instanceof Error ? e.message : String(e)}`);\n\t\t}\n\t\tconst subcollections = collection.subcollections;\n\t\tif (Array.isArray(subcollections) && subcollections.length > 0) console.warn(`[rebase] \"${collection.slug}\" declares ${subcollections.length} subcollection(s), which are not part of the generated Database: they are reached over a nested path (\\`data/${collection.slug}/<id>/<relation>\\`), not as a top-level accessor. Register a subcollection as a collection of its own if you want a typed accessor for it.`);\n\t\tlines.push(` ${emitKey(accessors.get(collection.slug))}: {`);\n\t\tlines.push(\" Row: {\");\n\t\tconst emittedKeys = /* @__PURE__ */ new Set();\n\t\tconst excluded = excludedApiKeys(properties);\n\t\tfor (const key of excluded) emittedKeys.add(key);\n\t\tfor (const [key, rawProp] of Object.entries(properties)) {\n\t\t\tconst prop = rawProp;\n\t\t\tif (prop.type === \"relation\") continue;\n\t\t\tif (excluded.has(key)) continue;\n\t\t\tconst tsType = propertyToTypeScriptType(prop);\n\t\t\tconst isRequired = Boolean(prop.validation?.required) || isPrimaryKey(prop);\n\t\t\tlines.push(line(key, isRequired ? tsType : `${tsType} | null`, !isRequired));\n\t\t\temittedKeys.add(key);\n\t\t}\n\t\tfor (const [relKey, relation] of Object.entries(resolvedRelations)) if (relation.kind === \"belongsTo\" && relation.localKey) {\n\t\t\tconst fkKey = fieldKeyForColumn(collection, relation.localKey);\n\t\t\tif (emittedKeys.has(fkKey)) continue;\n\t\t\tconst fkType = foreignKeyType(relation);\n\t\t\tconst shadowedByInclude = relKey === fkKey;\n\t\t\tconst tsType = shadowedByInclude ? `${fkType} | ${includedRelationType(relation, accessors)}` : fkType;\n\t\t\tconst isRequired = isRelationRequired(collection, relation) && !shadowedByInclude;\n\t\t\tlines.push(line(fkKey, isRequired ? tsType : `${tsType} | null`, !isRequired));\n\t\t\temittedKeys.add(fkKey);\n\t\t}\n\t\tfor (const [key, relation] of Object.entries(resolvedRelations)) {\n\t\t\tif (emittedKeys.has(key)) continue;\n\t\t\tlines.push(line(key, includedRelationType(relation, accessors), true));\n\t\t\temittedKeys.add(key);\n\t\t}\n\t\tfor (const [key, rawProp] of Object.entries(properties)) {\n\t\t\tif (rawProp.type !== \"relation\") continue;\n\t\t\tif (emittedKeys.has(key)) continue;\n\t\t\tlines.push(line(key, \"Record<string, unknown>\", true));\n\t\t\temittedKeys.add(key);\n\t\t}\n\t\tlines.push(\" };\");\n\t\tlines.push(\" Insert: {\");\n\t\temittedKeys.clear();\n\t\tfor (const key of excluded) emittedKeys.add(key);\n\t\tfor (const [key, rawProp] of Object.entries(properties)) {\n\t\t\tconst prop = rawProp;\n\t\t\tif (prop.type === \"relation\") continue;\n\t\t\tif (excluded.has(key)) continue;\n\t\t\tconst tsType = propertyToTypeScriptType(prop);\n\t\t\tconst isOptional = !prop.validation?.required || isAutoAssignedId(prop);\n\t\t\tlines.push(line(key, tsType, isOptional));\n\t\t\temittedKeys.add(key);\n\t\t}\n\t\temitWritableRelations(lines, collection, properties, resolvedRelations, emittedKeys, false);\n\t\tlines.push(\" };\");\n\t\tlines.push(\" Update: {\");\n\t\temittedKeys.clear();\n\t\tfor (const key of excluded) emittedKeys.add(key);\n\t\tfor (const [key, rawProp] of Object.entries(properties)) {\n\t\t\tconst prop = rawProp;\n\t\t\tif (prop.type === \"relation\") continue;\n\t\t\tif (isPrimaryKey(prop)) continue;\n\t\t\tif (excluded.has(key)) continue;\n\t\t\tlines.push(line(key, propertyToTypeScriptType(prop), true));\n\t\t\temittedKeys.add(key);\n\t\t}\n\t\temitWritableRelations(lines, collection, properties, resolvedRelations, emittedKeys, true);\n\t\tlines.push(\" };\");\n\t\tlines.push(\" Relations: {\");\n\t\tfor (const [key, relation] of Object.entries(resolvedRelations)) {\n\t\t\tconst slug = resolveTargetCollection(relation)?.slug ?? relation.targetSlug;\n\t\t\tconst accessor = slug ? accessors.get(slug) : void 0;\n\t\t\tlines.push(` ${emitKey(key)}: ${accessor ? emitString(accessor) : \"never\"};`);\n\t\t}\n\t\tlines.push(\" };\");\n\t\tlines.push(\" };\");\n\t}\n\tlines.push(\"}\");\n\tlines.push(\"\");\n\tlines.push(\"export type CollectionName = keyof Database;\");\n\tlines.push(\"\");\n\tlines.push(\"export const collectionsDictionary = {\");\n\tfor (const collection of collections) lines.push(` ${emitKey(accessors.get(collection.slug))}: ${emitString(collection.slug)},`);\n\tlines.push(\"} as const;\");\n\tlines.push(\"\");\n\tlines.push(\"export type CollectionsDictionary = typeof collectionsDictionary;\");\n\tlines.push(\"\");\n\tlines.push(...includeHelperLines());\n\treturn lines.join(\"\\n\");\n}\n/**\n* The `include` type machinery, emitted into the generated file.\n*\n* It lives here rather than in `@rebasepro/types` because it is the *graph*\n* that makes it work, and the graph only exists once a project's collections\n* have been generated. `IncludeSpec` in `@rebasepro/types` is deliberately\n* unconstrained — a hand-written row type has no relations in it to check\n* against — and these narrow it for a project that does have them.\n*\n* Two things a caller gets:\n*\n* - `IncludeFor<\"posts\">` — an include whose keys are relations that exist,\n* recursively, so `include: { comments: { include: { authr: true } } }` is a\n* compile error rather than a 400 at runtime.\n* - `RowWith<\"posts\", I>` — the row a read with that include returns, where\n* every included relation is **present** rather than optional. `Row` types a\n* relation as optional because it is absent from every read that did not ask\n* for it; once a read has asked, `row.author.name` should not need a `?.`.\n*\n* The depth bound matches the server's (`MAX_INCLUDE_DEPTH`), and is spelled as\n* a decrementing tuple because TypeScript has no arithmetic — `Prev[3]` is `2`.\n* Without a bound, a self-referencing relation makes the type infinite and the\n* compiler gives up with \"type instantiation is excessively deep\".\n*/\nfunction includeHelperLines() {\n\treturn [\n\t\t\"/** Which collection each of `A`'s relations reaches. */\",\n\t\t\"export type RelationsOf<A extends CollectionName> =\",\n\t\t\" Database[A] extends { Relations: infer R } ? R : Record<string, never>;\",\n\t\t\"\",\n\t\t\"/** The names of `A`'s relations. */\",\n\t\t\"export type RelationKeys<A extends CollectionName> = keyof RelationsOf<A> & string;\",\n\t\t\"\",\n\t\t\"/** The collection a relation reaches, or `never` when it left this project. */\",\n\t\t\"export type RelationTarget<A extends CollectionName, K extends RelationKeys<A>> =\",\n\t\t\" RelationsOf<A>[K] extends CollectionName ? RelationsOf<A>[K] : never;\",\n\t\t\"\",\n\t\t\"/** Counts `IncludeFor` down; TypeScript has no arithmetic. */\",\n\t\t\"type Prev = [never, 0, 1, 2, 3];\",\n\t\t\"\",\n\t\t\"/**\",\n\t\t\" * Per-relation options, minus `include` — which `IncludeFor` supplies at\",\n\t\t\" * the next depth so the nested keys are checked against the *target's*\",\n\t\t\" * relations rather than this collection's.\",\n\t\t\" */\",\n\t\t\"export interface IncludeOptionsFor<A extends CollectionName, K extends RelationKeys<A>, D extends number> {\",\n\t\t\" limit?: number;\",\n\t\t\" where?: Record<string, unknown>;\",\n\t\t\" logical?: unknown;\",\n\t\t\" orderBy?: unknown;\",\n\t\t\" fields?: string[];\",\n\t\t\" include?: RelationTarget<A, K> extends CollectionName\",\n\t\t\" ? IncludeFor<RelationTarget<A, K>, Prev[D]>\",\n\t\t\" : never;\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"/**\",\n\t\t\" * An `include` for collection `A`: its relation names, dotted paths, or the\",\n\t\t\" * parametrised tree — with every key checked against the relations that\",\n\t\t\" * actually exist, at every level.\",\n\t\t\" */\",\n\t\t\"export type IncludeFor<A extends CollectionName, D extends number = 3> =\",\n\t\t\" D extends 0\",\n\t\t\" ? never\",\n\t\t\" : | readonly (RelationKeys<A> | \\\"*\\\")[]\",\n\t\t\" | { [K in RelationKeys<A>]?: true | IncludeOptionsFor<A, K, D> };\",\n\t\t\"\",\n\t\t\"/** The relation names an include asks for at the top level. */\",\n\t\t\"type IncludedKeys<A extends CollectionName, I> =\",\n\t\t\" I extends readonly (infer K)[]\",\n\t\t\" ? Extract<K, RelationKeys<A>>\",\n\t\t\" : Extract<keyof I, RelationKeys<A>>;\",\n\t\t\"\",\n\t\t\"/**\",\n\t\t\" * The row a read with include `I` returns: `Row`, with every included\",\n\t\t\" * relation made **required**.\",\n\t\t\" *\",\n\t\t\" * `Row` types a relation as optional because it is absent from every read\",\n\t\t\" * that did not name it. Once a read has named it, it is there — and having\",\n\t\t\" * to write `row.author?.name` after asking for the author is the type\",\n\t\t\" * describing a possibility the query already ruled out.\",\n\t\t\" */\",\n\t\t\"export type RowWith<A extends CollectionName, I> =\",\n\t\t\" Database[A][\\\"Row\\\"] & Required<Pick<Database[A][\\\"Row\\\"], IncludedKeys<A, I> & keyof Database[A][\\\"Row\\\"]>>;\",\n\t\t\"\"\n\t];\n}\n/**\n* The two ways a write can name a `belongsTo` target, both of which the server\n* accepts: the foreign key under its own wire name (`{ authorId: 5 }`, which\n* passes through to the `author_id` column untouched) and the relation\n* *property* (`{ author: 5 }`, which the write transformer maps onto that\n* column).\n*\n* Only the first was generated, so the documented and idiomatic write shape was\n* a type error.\n*\n* The second form is emitted under the **property key**, not the resolved\n* relation name, because that is what the transformer keys off: it looks the\n* payload key up in `properties` and only treats it as a relation if what it\n* finds there is one. A relation whose `relationName` differs from its property\n* key is reachable as the property and not as the name, so emitting the name\n* would have offered a key that writes to a column that does not exist.\n*/\nfunction emitWritableRelations(lines, collection, properties, resolvedRelations, emittedKeys, allOptional) {\n\tconst emit = (key, relation) => {\n\t\tif (emittedKeys.has(key)) return;\n\t\tconst optional = allOptional || !isRelationRequired(collection, relation);\n\t\tlines.push(line(key, foreignKeyType(relation), optional));\n\t\temittedKeys.add(key);\n\t};\n\tfor (const relation of Object.values(resolvedRelations)) if (relation.kind === \"belongsTo\" && relation.localKey) emit(fieldKeyForColumn(collection, relation.localKey), relation);\n\tfor (const [key, rawProp] of Object.entries(properties)) {\n\t\tif (rawProp.type !== \"relation\") continue;\n\t\tconst relation = findRelation(resolvedRelations, key);\n\t\tif (relation?.kind === \"belongsTo\" && relation.localKey) emit(key, relation);\n\t}\n}\n//#endregion\n//#region src/index.ts\nfunction generateSDK(collections, options = {}) {\n\tconst files = [];\n\tfiles.push({\n\t\tpath: \"database.types.ts\",\n\t\tcontent: generateTypedefs(collections)\n\t});\n\tif (options.includeReadme !== false) files.push({\n\t\tpath: \"README.md\",\n\t\tcontent: `# Rebase SDK\n\n> Auto-generated by \\`rebase generate-sdk\\`. Do not edit manually.\n\n## Usage\n\n1. Install the client package:\n \\`\\`\\`bash\n npm install @rebasepro/client\n \\`\\`\\`\n\n2. Initialize with your generated types:\n \\`\\`\\`typescript\n import { createRebaseClient } from '@rebasepro/client';\n import { collectionsDictionary, type Database } from './database.types';\n\n const rebase = createRebaseClient<Database>({\n baseUrl: 'http://localhost:3001',\n // Maps each accessor back to the slug the wire uses. Without it a\n // hyphenated slug is not resolvable from the property name alone.\n collections: collectionsDictionary,\n });\n\n // Property access is the typed surface: rows, filters and sorts are all\n // checked against the generated Database.\n const { data: users } = await rebase.data.users.find();\n console.log(users[0].email); // flat access — no .values wrapper\n \\`\\`\\`\n\n## Field names are the ones the API serves\n\nThe generated \\`Row\\` uses each field's **wire** name — the key it arrives under in\nJSON — and nothing here renames anything.\n\n- **A declared property is its key in the collection.** A property keyed\n \\`createdAt\\` is \\`row.createdAt\\`, whatever \\`columnName\\` says. A column name is\n the name of a different thing: where the value lives, not what the API calls it.\n- **A foreign key derived from a relation is camelCase**, because that is what\n the wire carries. A \\`belongsTo\\` named \\`author\\` gives you \\`row.authorId\\`, not\n the column spelling.\n- **A collection accessor is camelCase too** (\\`my-notes\\` → \\`rebase.data.myNotes\\`),\n which is what \\`collectionsDictionary\\` maps back to the slug.\n\n\\`where\\` and \\`orderBy\\` are keyed off the same type, so what compiles is what the\nbackend answers to.\n\n## \\`Row\\` vs \\`Insert\\` vs \\`Update\\`\n\n| Type | What it describes |\n|---|---|\n| \\`Row\\` | What a read serves. Nullable columns are \\`T \\\\| null\\`; relations appear only when \\`include\\` names them. |\n| \\`Insert\\` | What \\`create()\\` accepts. Server-assigned ids are optional; a \\`belongsTo\\` target may be named either way (\\`{ author: 5 }\\` or \\`{ authorId: 5 }\\`). |\n| \\`Update\\` | What \\`update()\\` accepts. Everything optional, and the primary key is not settable. |\n\nA property marked \\`excludeFromApi\\` is absent from all three: the API surface\ndoes not mention it, in either direction. The server holds the same line — a\nread never serves the column and a write naming it is refused — so this is a\nguarantee rather than a description, and nothing generated names a password\nhash.\n\nIf you need an untyped escape hatch, \\`rebase.data.collection(slug)\\` still works —\nbut it is generic over \\`Record<string, unknown>\\` and gives up everything above.\n`\n\t});\n\treturn files;\n}\n//#endregion\nexport { CodegenError, generateSDK, generateTypedefs, indent, toCamelCase, toPascalCase, toSafeIdentifier };\n\n//# sourceMappingURL=index.es.js.map","/**\n * Rebase Schema Doctor — Three-way schema drift detection.\n *\n * Compares:\n * 1. Collection definitions → Generated Drizzle schema (staleness check)\n * 2. Collection definitions → Live PostgreSQL database (structural drift)\n *\n * Run via: rebase doctor\n */\nimport { promises as fsPromises } from \"fs\";\nimport * as fs from \"fs\";\nimport path from \"path\";\nimport { pathToFileURL } from \"url\";\nimport chalk from \"chalk\";\nimport { computeSchemaVersion, CollectionConfig, isPostgresCollectionConfig, Property, NumberProperty, StringProperty, DateProperty, ArrayProperty, MapProperty, RelationProperty, type ResolvedManyToMany, type ResolvedBelongsTo, isManyToMany } from \"@rebasepro/types\";\nimport { generateSchema } from \"./generate-drizzle-schema-logic\";\nimport { compareGeneratedDeclarations, describeDeclarationDifferences } from \"./generated-schema-staleness\";\nimport { columnPgType } from \"./plan/plan-schema\";\nimport type { PgType } from \"./plan/types\";\nimport { generateTypedefs } from \"@rebasepro/codegen\";\nimport { getTableName, resolveCollectionRelations, findRelation, relationalCollections } from \"@rebasepro/common\";\nimport { toSnakeCase } from \"@rebasepro/utils\";\nimport { loadCollectionsFromDirectory } from \"@rebasepro/server\";\n// The report is CLI output, not application logging — see cli-output.ts.\nimport { out, outError } from \"../cli-output\";\nimport { deepestErrorMessage, diagnoseDbError, parseHostInfo } from \"../cli-errors\";\n\n/**\n * Resolve the SQL column name for a property.\n * Uses the explicit `columnName` when set (e.g. from introspection),\n * falling back to `toSnakeCase(propName)` for manually-authored collections.\n */\nconst resolveColumnName = (propName: string, prop?: Property | null): string => {\n if (prop && \"columnName\" in prop && typeof prop.columnName === \"string\") {\n return prop.columnName;\n }\n return toSnakeCase(propName);\n};\n\n// ── Types ────────────────────────────────────────────────────────────────\n\nexport type IssueSeverity = \"error\" | \"warning\" | \"info\";\n\nexport interface DoctorIssue {\n severity: IssueSeverity;\n category: \"missing_table\" | \"missing_column\" | \"type_mismatch\" | \"missing_constraint\" | \"schema_stale\" | \"missing_enum\" | \"enum_value_mismatch\" | \"missing_foreign_key\" | \"sdk_stale\" | \"sdk_not_generated\" | \"sdk_ungeneratable\";\n table?: string;\n column?: string;\n expected?: string;\n actual?: string;\n message: string;\n fix: string;\n}\n\nexport interface DoctorPhase {\n passed: boolean;\n issues: DoctorIssue[];\n /**\n * Why this phase never ran, when it did not.\n *\n * A check that did not happen is a third state, not a passing one. While a\n * skipped phase initialised to `{ passed: true, issues: [] }` it rendered as\n * `✅ Collections → Database: In sync` and counted towards\n * `✓ All schemas are in sync!` — so a project whose connection string was\n * spelled `POSTGRES_URL`, or a CI job that never exported one, got two green\n * ticks and exit 0 against a database with no tables in it.\n */\n skipped?: string;\n /**\n * Why this phase had nothing to compare against, when it had nothing.\n *\n * Distinct from `skipped`, which means \"the check could not run, and that\n * is probably worth fixing\". This one means \"the artifact is optional and\n * you have not asked for it\": no drift is possible, so the run is still a\n * clean bill of health.\n *\n * It exists because the alternative was a contradiction. The typed-SDK\n * phase returned `{ passed: true }` when `generated/sdk/database.types.ts`\n * did not exist, so a fresh project's report read\n * `✅ Collections → SDK Types: In sync` directly above\n * `ℹ Typed SDK not generated (optional).` — one line calling a file\n * synchronised and the next saying it is absent. \"In sync\" is a claim about\n * a comparison, and no comparison happened.\n */\n notApplicable?: string;\n /**\n * The phase was skipped by a fault rather than by a choice.\n *\n * `skipped` covers both: \"no DATABASE_URL, so nothing to compare against\"\n * is a local situation a developer may be perfectly happy with, and \"the\n * DATABASE_URL you set refuses connections\" is not. The report renders them\n * the same way — a check that did not run is a check that did not run — but\n * the exit code must not, or `rebase doctor` in CI goes green against a\n * database it never reached.\n */\n blocked?: boolean;\n}\n\nexport interface DoctorReport {\n collectionsToSchema: DoctorPhase;\n collectionsToSdk: DoctorPhase;\n schemaToDatabase: DoctorPhase;\n summary: { passed: number; skipped: number; notApplicable: number; warnings: number; errors: number };\n}\n\n// ── Column type mapping ──────────────────────────────────────────────────\n\n/**\n * How `information_schema.columns.data_type` spells a planned column type.\n *\n * The catalogue's names are its own — `character varying`, not `varchar`;\n * `ARRAY` for every array whatever its element; `USER-DEFINED` for an enum, a\n * `vector`, and anything else identified by name rather than by shape. So the\n * translation has to exist somewhere. What must *not* exist twice is the step\n * before it: which column a property compiles to.\n *\n * This function was that second step — a fourth copy of the property switch,\n * kept \"in sync\" with the generators by a comment, and already out of step\n * (`geopoint` answered `null` here, so the one type that was genuinely\n * unpersistable was the one the doctor never checked). It reads the schema plan\n * now.\n */\nconst INFORMATION_SCHEMA_NAMES: Record<PgType[\"kind\"], string> = {\n text: \"text\",\n varchar: \"character varying\",\n char: \"character\",\n uuid: \"uuid\",\n // A `CREATE TYPE … AS ENUM` is a name, not a shape.\n enum: \"USER-DEFINED\",\n smallint: \"smallint\",\n integer: \"integer\",\n bigint: \"bigint\",\n // A serial is an integer with a sequence default; the catalogue reports the\n // underlying width and nothing about the sequence.\n smallserial: \"smallint\",\n serial: \"integer\",\n bigserial: \"bigint\",\n real: \"real\",\n doublePrecision: \"double precision\",\n numeric: \"numeric\",\n boolean: \"boolean\",\n timestamptz: \"timestamp with time zone\",\n date: \"date\",\n time: \"time without time zone\",\n json: \"json\",\n jsonb: \"jsonb\",\n // pgvector's type, like every extension type.\n vector: \"USER-DEFINED\",\n bytea: \"bytea\",\n tsvector: \"USER-DEFINED\",\n array: \"ARRAY\"\n};\n\n/**\n * The `data_type` the database should report for a property's column, or `null`\n * when there is nothing to compare.\n *\n * `null` covers the two honest cases: a `relation`, whose column is derived from\n * the relation rather than from the property, and a configuration the planner\n * refuses outright — which is a problem the doctor's other phases report with a\n * message, not a type mismatch to invent one for.\n */\nexport function getExpectedColumnType(prop: Property): string | null {\n // The FK column a relation owns is planned from the relation, and its type\n // is the *target's* key — which a bare property cannot resolve.\n if (prop.type === \"relation\") return null;\n\n // A one-property collection, because the planner asks the questions a\n // column needs in context: is this the primary key, what is the enum type\n // called, what does the link point at. A `reference` whose target is not\n // here resolves to `text`, exactly as it does in a real bundle.\n const collection = {\n slug: \"__doctor__\",\n table: \"__doctor__\",\n properties: { __column__: prop }\n } as unknown as CollectionConfig;\n\n try {\n const type = columnPgType(\"__column__\", prop, collection, () => undefined);\n return INFORMATION_SCHEMA_NAMES[type.kind];\n } catch {\n return null;\n }\n}\n\n// ── Collection loading ───────────────────────────────────────────────────\n\n/**\n * Re-exported so callers keep importing it from here, but the implementation is\n * now shared with the runtime and the generators — a doctor that disagreed with\n * the policy generator about which files are collections would compare the\n * wrong thing.\n */\nexport async function loadCollections(collectionsPath: string): Promise<CollectionConfig[]> {\n return loadCollectionsFromDirectory(collectionsPath);\n}\n\n// ── Phase 1: Collections ↔ Generated Schema ─────────────────────────────\n\nexport async function checkCollectionsVsSchema(\n collections: CollectionConfig[],\n schemaFilePath: string\n): Promise<DoctorPhase> {\n const issues: DoctorIssue[] = [];\n\n // Check if schema file exists\n if (!fs.existsSync(schemaFilePath)) {\n issues.push({\n severity: \"error\",\n category: \"schema_stale\",\n message: \"Generated schema file does not exist.\",\n fix: \"Run `rebase schema generate`\"\n });\n return { passed: false,\nissues };\n }\n\n // Re-generate schema in-memory and compare with file on disk. Only the\n // collections the generator itself will emit — a Firestore collection has\n // no table in the generated file and must not be reported as missing one.\n const postgresCollections = relationalCollections(collections);\n if (postgresCollections.length === 0) {\n return { passed: true,\nissues };\n }\n\n try {\n // Declaration by declaration, not byte by byte. The generated file is a\n // flat list of `export const`s, each the rendering of one piece of the\n // schema plan, so comparing them says *which* table or enum moved —\n // which is what the reader is about to go looking for — and ignores the\n // formatting and commentary the renderer is free to vary on its own.\n const differences = compareGeneratedDeclarations(\n generateSchema(postgresCollections),\n await fsPromises.readFile(schemaFilePath, \"utf-8\")\n );\n\n if (differences.length > 0) {\n issues.push({\n severity: \"warning\",\n category: \"schema_stale\",\n message: \"Generated schema is out of date — collection definitions have changed since last generation. \"\n + `Differs in: ${describeDeclarationDifferences(differences)}.`,\n fix: \"Run `rebase schema generate`\"\n });\n }\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n issues.push({\n severity: \"warning\",\n category: \"schema_stale\",\n message: `Could not regenerate schema for comparison: ${message}`,\n fix: \"Run `rebase schema generate` to verify\"\n });\n }\n\n return { passed: issues.length === 0,\nissues };\n}\n\nexport async function checkCollectionsVsSdk(\n collections: CollectionConfig[],\n sdkFilePath: string\n): Promise<DoctorPhase> {\n const issues: DoctorIssue[] = [];\n\n // The typed SDK is opt-in — nothing in a scaffolded project imports it until\n // you choose to. A project that never generated one isn't drifting, so report\n // it as information rather than a warning; otherwise `doctor` can never come\n // back clean on a fresh project and users learn to ignore its output.\n if (!fs.existsSync(sdkFilePath)) {\n issues.push({\n severity: \"info\",\n category: \"sdk_not_generated\",\n message: \"Typed SDK not generated (optional).\",\n fix: \"Run `rebase generate-sdk` if you want typed collection access\"\n });\n return { passed: true,\nissues,\nnotApplicable: \"not generated (optional)\" };\n }\n\n try {\n const expectedSdk = generateTypedefs(collections);\n const actualSdk = await fsPromises.readFile(sdkFilePath, \"utf-8\");\n\n // Normalize whitespace for comparison\n const normalize = (s: string) =>\n s\n .replace(/\\/\\/.*$/gm, \"\") // strip single-line comments\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\") // strip multi-line comments\n .replace(/\\s+/g, \" \")\n .trim();\n\n if (normalize(expectedSdk) !== normalize(actualSdk)) {\n issues.push({\n severity: \"warning\",\n category: \"sdk_stale\",\n message: \"Generated SDK types are out of date — collection definitions have changed since last SDK generation.\",\n fix: \"Run `rebase generate-sdk`\"\n });\n }\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n\n // A `CodegenError` is not \"the comparison did not run\" — it is the\n // schema saying it cannot produce a valid typed client at all, most\n // often two slugs collapsing onto one accessor. Reporting that as a\n // warning about staleness described the wrong problem and pointed at a\n // command that would fail the same way.\n if (err instanceof Error && err.name === \"CodegenError\") {\n issues.push({\n severity: \"error\",\n category: \"sdk_ungeneratable\",\n message: `The typed SDK cannot be generated from these collections: ${message}`,\n fix: \"Fix the collection definitions named above, then run `rebase generate-sdk`\"\n });\n } else {\n issues.push({\n severity: \"warning\",\n category: \"sdk_stale\",\n message: `Could not regenerate SDK types for comparison: ${message}`,\n fix: \"Run `rebase generate-sdk` to verify\"\n });\n }\n }\n\n return { passed: issues.length === 0,\nissues };\n}\n\n// ── Phase 2: Collections ↔ Database ──────────────────────────────────────\n\ninterface DbColumn {\n table_schema: string;\n table_name: string;\n column_name: string;\n data_type: string;\n is_nullable: string;\n udt_name: string;\n}\n\n\ninterface DbEnumValue {\n enum_name: string;\n enum_value: string;\n}\n\n/**\n * Extensions the collections need, that the database does not have installed.\n *\n * Only `vector` today, and only because a `{ type: \"vector\" }` property plans a\n * column of a type that will not exist. Rebase installs it only where a project\n * has said it may (`database({ extensions: [\"vector\"] })`), so the absence is\n * often correct configuration and a wrong deployment — which is exactly why the\n * report has to name it rather than the first failing INSERT.\n *\n * Exported for its test.\n */\nexport function missingExtensionIssues(\n collections: CollectionConfig[],\n installed: ReadonlySet<string>\n): DoctorIssue[] {\n const usesVector = collections.some(collection =>\n Object.values(collection.properties ?? {})\n .some(property => (property as { type?: string } | null)?.type === \"vector\"));\n\n if (!usesVector || installed.has(\"vector\")) return [];\n\n return [{\n severity: \"error\",\n category: \"missing_table\",\n message: 'A collection declares a `{ type: \"vector\" }` property and the pgvector extension is not installed on this database.',\n fix: 'Declare it — `database({ extensions: [\"vector\"] })` in config/resources.ts — and use an image that ships the library '\n + \"(the scaffold's pgvector/pgvector:pg18; a stock postgres:18 does not). Or install it once by hand: CREATE EXTENSION vector;\"\n }];\n}\n\n/**\n * The stamp the runtime wrote, against the collections on disk.\n *\n * A stamp that disagrees means this database was provisioned from a different\n * set of collections than the ones in this checkout — a colleague's branch, an\n * older deploy, a tenant somebody else migrated. Everything else in this report\n * is then describing a schema somebody else created, which is worth knowing\n * before acting on any of it.\n *\n * A warning, not an error, and never a direction: the stamp is a hash, so it\n * can say the two disagree and never which is ahead. `null` — never stamped —\n * is not a finding: every database provisioned before the stamp existed reads\n * that way, and so does every fresh one.\n */\nexport async function schemaStampIssues(\n pool: { query<T>(text: string): Promise<{ rows: T[] }> },\n collections: CollectionConfig[]\n): Promise<DoctorIssue[]> {\n let stamped: string | null = null;\n try {\n // `to_regclass` so a missing schema or table is a null rather than a\n // thrown 42P01 — the commonest state on a fresh database is neither.\n const present = await pool.query<{ present: boolean }>(\n `SELECT to_regclass('\"rebase\".\"schema_meta\"') IS NOT NULL AS present`\n );\n if (!present.rows[0]?.present) return [];\n const row = await pool.query<{ value: string }>(\n `SELECT value FROM \"rebase\".\"schema_meta\" WHERE key = 'collections_schema_version' LIMIT 1`\n );\n stamped = row.rows[0]?.value ?? null;\n } catch {\n // The stamp is a diagnostic, not a gate. A database that will not\n // answer this question has bigger problems, and they are reported by\n // the checks around this one.\n return [];\n }\n\n if (!stamped) return [];\n const expected = computeSchemaVersion(collections);\n if (stamped === expected) return [];\n\n return [{\n severity: \"warning\",\n category: \"schema_stale\",\n message: `This database was provisioned from a different set of collections (stamp ${stamped.slice(0, 12)}…, this checkout computes ${expected.slice(0, 12)}…).`,\n fix: \"A hash says the two disagree, never which is ahead. Check you are pointed at the database you think you are; then `rebase db push` to bring it to these collections.\"\n }];\n}\n\nexport async function checkCollectionsVsDatabase(\n collections: CollectionConfig[],\n databaseUrl: string\n): Promise<DoctorPhase> {\n const issues: DoctorIssue[] = [];\n\n // Dynamic import to avoid loading pg when not needed\n const pgModule = await import(\"pg\");\n const { Pool } = pgModule.default ?? pgModule;\n const pool = new Pool({ connectionString: databaseUrl });\n\n // Determine all schemas defined by the collections, plus public and rebase\n const schemas = Array.from(new Set([\n \"public\",\n \"rebase\",\n ...relationalCollections(collections)\n .filter(isPostgresCollectionConfig)\n .map(c => c.schema)\n .filter((s): s is string => !!s)\n ]));\n\n try {\n // Fetch all tables in the defined schemas\n const tablesResult = await pool.query<{ table_schema: string; table_name: string }>(\n `SELECT table_schema, table_name \n FROM information_schema.tables \n WHERE table_schema = ANY($1) AND table_type = 'BASE TABLE'`,\n [schemas]\n );\n const existingTables = new Set(tablesResult.rows.map((r) =>\n r.table_schema === \"public\" ? r.table_name : `${r.table_schema}.${r.table_name}`\n ));\n\n // An extension the collections need and the database does not have.\n //\n // The symptom without this is `type \"vector\" does not exist` on the\n // first write to a table that looks perfectly well-formed in the\n // report: the column is planned, the extension is not, and the two\n // facts live in different places. Extensions are opt-in by design —\n // installing one needs an image that ships the library and a role\n // allowed to install it — so this reports, it does not install.\n const extensionsResult = await pool.query<{ extname: string }>(\"SELECT extname FROM pg_extension\");\n const installedExtensions = new Set(extensionsResult.rows.map(r => r.extname));\n issues.push(...missingExtensionIssues(collections, installedExtensions));\n\n // A database provisioned from a different set of collections than the\n // ones on disk.\n //\n // The runtime stamps what it applied; nothing compared it outside boot,\n // and a stamp that disagrees is exactly the state where the rest of\n // this report is describing a schema somebody else's deploy created.\n issues.push(...await schemaStampIssues(pool, collections));\n\n // Fetch all columns in the defined schemas\n const columnsResult = await pool.query<DbColumn>(\n `SELECT table_schema, table_name, column_name, data_type, is_nullable, udt_name\n FROM information_schema.columns\n WHERE table_schema = ANY($1)\n ORDER BY table_schema, table_name, ordinal_position`,\n [schemas]\n );\n const columnsByTable = new Map<string, DbColumn[]>();\n for (const row of columnsResult.rows) {\n const tableSchema = row.table_schema;\n const tableName = row.table_name;\n const key = tableSchema === \"public\" ? tableName : `${tableSchema}.${tableName}`;\n if (!columnsByTable.has(key)) {\n columnsByTable.set(key, []);\n }\n columnsByTable.get(key)!.push(row);\n }\n\n // Fetch enums\n const enumsResult = await pool.query<DbEnumValue>(\n `SELECT t.typname as enum_name, e.enumlabel as enum_value\n FROM pg_type t\n JOIN pg_enum e ON t.oid = e.enumtypid\n ORDER BY t.typname, e.enumsortorder`\n );\n const enumsByName = new Map<string, string[]>();\n for (const row of enumsResult.rows) {\n if (!enumsByName.has(row.enum_name)) {\n enumsByName.set(row.enum_name, []);\n }\n enumsByName.get(row.enum_name)!.push(row.enum_value);\n }\n\n // Fetch foreign key constraints in the defined schemas\n const fksResult = await pool.query<{\n constraint_name: string;\n table_schema: string;\n table_name: string;\n column_name: string;\n foreign_table_schema: string;\n foreign_table_name: string;\n foreign_column_name: string;\n }>(\n `SELECT\n tc.constraint_name,\n tc.table_schema,\n tc.table_name,\n kcu.column_name,\n ccu.table_schema AS foreign_table_schema,\n ccu.table_name AS foreign_table_name,\n ccu.column_name AS foreign_column_name\n FROM information_schema.table_constraints AS tc\n JOIN information_schema.key_column_usage AS kcu\n ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema\n JOIN information_schema.constraint_column_usage AS ccu\n ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema\n WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = ANY($1)`,\n [schemas]\n );\n const fksByTable = new Map<string, typeof fksResult.rows>();\n for (const row of fksResult.rows) {\n const tableSchema = row.table_schema;\n const tableName = row.table_name;\n const key = tableSchema === \"public\" ? tableName : `${tableSchema}.${tableName}`;\n if (!fksByTable.has(key)) {\n fksByTable.set(key, []);\n }\n fksByTable.get(key)!.push(row);\n }\n\n // ── Compare each collection against the database ─────────────────\n\n const postgresCollections = relationalCollections(collections);\n\n for (const collection of postgresCollections) {\n const tableName = getTableName(collection);\n const schemaName = (collection as { schema?: string }).schema || \"public\";\n const fullTableName = schemaName === \"public\" ? tableName : `${schemaName}.${tableName}`;\n\n // Check table existence\n if (!existingTables.has(fullTableName)) {\n issues.push({\n severity: \"error\",\n category: \"missing_table\",\n table: fullTableName,\n message: `Table \"${fullTableName}\" does not exist in the database.`,\n fix: \"Run `rebase db push` or `rebase db generate && rebase db migrate`\"\n });\n continue; // Skip column checks for missing tables\n }\n\n const dbColumns = columnsByTable.get(fullTableName) ?? [];\n const dbColumnMap = new Map(dbColumns.map((c) => [c.column_name, c]));\n\n // System columns that Rebase always creates\n const systemColumns = new Set([\"id\", \"created_on\", \"updated_on\"]);\n\n // Check properties → columns\n for (const [propName, prop] of Object.entries(collection.properties ?? {})) {\n if (prop.type === \"relation\") {\n // ResolvedRelation columns are derived from localKey\n const resolvedRelations = resolveCollectionRelations(collection);\n const relation = findRelation(resolvedRelations, (prop as RelationProperty).relation?.relationName ?? propName);\n if (relation?.kind === \"belongsTo\") {\n const fkColName = relation.localKey;\n if (!dbColumnMap.has(fkColName)) {\n issues.push({\n severity: \"error\",\n category: \"missing_column\",\n table: fullTableName,\n column: fkColName,\n message: `Foreign key column \"${fkColName}\" for relation \"${propName}\" is missing from table \"${fullTableName}\".`,\n fix: \"Run `rebase db push` or `rebase db generate && rebase db migrate`\"\n });\n }\n\n // Check FK constraint exists\n const tableFks = fksByTable.get(fullTableName) ?? [];\n let targetTableName = \"unknown\";\n let targetSchemaName = \"public\";\n try {\n const targetColl = relation.target();\n targetTableName = getTableName(targetColl);\n targetSchemaName = (targetColl as { schema?: string }).schema || \"public\";\n } catch { /* ignore */ }\n\n const hasFk = tableFks.some((fk) =>\n fk.column_name === fkColName &&\n fk.foreign_table_name === targetTableName &&\n fk.foreign_table_schema === targetSchemaName\n );\n\n if (dbColumnMap.has(fkColName) && !hasFk) {\n issues.push({\n severity: \"warning\",\n category: \"missing_foreign_key\",\n table: fullTableName,\n column: fkColName,\n message: `Column \"${fkColName}\" exists but has no FOREIGN KEY constraint referencing \"${targetSchemaName === \"public\" ? targetTableName : `${targetSchemaName}.${targetTableName}`}\".`,\n fix: \"Run `rebase db push` or add the constraint manually\"\n });\n }\n }\n continue;\n }\n\n const colName = resolveColumnName(propName, prop);\n\n // Skip system columns — they're handled automatically\n if (systemColumns.has(colName)) continue;\n\n const dbCol = dbColumnMap.get(colName);\n if (!dbCol) {\n issues.push({\n severity: \"error\",\n category: \"missing_column\",\n table: fullTableName,\n column: colName,\n message: `Column \"${colName}\" is defined in collection \"${collection.slug}\" but missing from table \"${fullTableName}\".`,\n fix: \"Run `rebase db push` or `rebase db generate && rebase db migrate`\"\n });\n continue;\n }\n\n // Type check\n const expectedType = getExpectedColumnType(prop);\n if (expectedType) {\n const actualType = dbCol.data_type;\n let isMismatch = actualType !== expectedType;\n if (prop.type === \"vector\" && dbCol.udt_name !== \"vector\") {\n isMismatch = true;\n }\n if (prop.type === \"array\") {\n const ap = prop as ArrayProperty;\n let expectedColType = ap.columnType;\n if (!expectedColType && ap.of && !Array.isArray(ap.of)) {\n const ofProp = ap.of as Property;\n if (ofProp.type === \"string\") expectedColType = \"text[]\";\n else if (ofProp.type === \"number\") expectedColType = ofProp.validation?.integer ? \"integer[]\" : \"numeric[]\";\n else if (ofProp.type === \"boolean\") expectedColType = \"boolean[]\";\n }\n if (expectedColType && expectedColType.endsWith(\"[]\")) {\n if (actualType !== \"ARRAY\") {\n isMismatch = true;\n } else {\n const expectedUdt = expectedColType === \"text[]\" ? \"_text\" :\n expectedColType === \"integer[]\" ? \"_int4\" :\n expectedColType === \"boolean[]\" ? \"_bool\" :\n expectedColType === \"numeric[]\" ? \"_numeric\" : \"\";\n if (expectedUdt && dbCol.udt_name !== expectedUdt) {\n isMismatch = true;\n }\n }\n }\n }\n if (isMismatch) {\n issues.push({\n severity: \"warning\",\n category: \"type_mismatch\",\n table: fullTableName,\n column: colName,\n expected: prop.type === \"vector\" ? \"vector\" : expectedType,\n actual: dbCol.udt_name === \"vector\" ? \"vector\" : actualType,\n message: `Column \"${colName}\" in table \"${fullTableName}\": expected type \"${prop.type === \"vector\" ? \"vector\" : expectedType}\" but found \"${dbCol.udt_name === \"vector\" ? \"vector\" : actualType}\".`,\n fix: \"Review collection property type or run a migration\"\n });\n }\n }\n\n // Enum value check\n if (prop.type === \"string\" && (prop as StringProperty).enum) {\n const enumValues = (prop as StringProperty).enum;\n if (enumValues) {\n const enumName = `${tableName}_${colName}`;\n const dbEnumValues = enumsByName.get(enumName);\n if (!dbEnumValues) {\n issues.push({\n severity: \"warning\",\n category: \"missing_enum\",\n table: fullTableName,\n column: colName,\n expected: enumName,\n message: `Enum type \"${enumName}\" is defined in collection but not found in the database.`,\n fix: \"Run `rebase db push` or `rebase db generate && rebase db migrate`\"\n });\n } else {\n // Compare enum values\n const expectedValues = Array.isArray(enumValues)\n ? enumValues.map((v) => (typeof v === \"string\" ? v : String(v.id)))\n : Object.keys(enumValues);\n\n const missing = expectedValues.filter((v) => !dbEnumValues.includes(v));\n const extra = dbEnumValues.filter((v) => !expectedValues.includes(v));\n\n if (missing.length > 0 || extra.length > 0) {\n const parts: string[] = [];\n if (missing.length > 0) parts.push(`missing: ${missing.join(\", \")}`);\n if (extra.length > 0) parts.push(`extra in DB: ${extra.join(\", \")}`);\n issues.push({\n severity: \"warning\",\n category: \"enum_value_mismatch\",\n table: fullTableName,\n column: colName,\n expected: expectedValues.join(\", \"),\n actual: dbEnumValues.join(\", \"),\n message: `Enum values for \"${colName}\" in table \"${fullTableName}\" are out of sync (${parts.join(\"; \")}).`,\n fix: \"Run `rebase db push` to update the enum\"\n });\n }\n }\n }\n }\n }\n\n // Also check junction tables for many-to-many relations\n const resolvedRelations = resolveCollectionRelations(collection);\n for (const relation of Object.values(resolvedRelations)) {\n if (isManyToMany(relation)) {\n const junctionTable = relation.through.table;\n const junctionSchema = (collection as { schema?: string }).schema || \"public\";\n const fullJunctionTable = junctionSchema === \"public\" ? junctionTable : `${junctionSchema}.${junctionTable}`;\n if (!existingTables.has(fullJunctionTable)) {\n issues.push({\n severity: \"error\",\n category: \"missing_table\",\n table: fullJunctionTable,\n message: `Junction table \"${fullJunctionTable}\" for many-to-many relation \"${relation.relationName}\" is missing.`,\n fix: \"Run `rebase db push` or `rebase db generate && rebase db migrate`\"\n });\n }\n }\n }\n }\n } finally {\n await pool.end();\n }\n\n return { passed: issues.length === 0,\nissues };\n}\n\n// ── Report Rendering ─────────────────────────────────────────────────────\n\nexport function renderReport(report: DoctorReport): void {\n out();\n out(chalk.bold(\" 🩺 Rebase Schema Doctor\"));\n out(chalk.gray(\" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"));\n out();\n\n // Phase 1\n renderPhase(\"Collections → Generated Schema\", report.collectionsToSchema);\n\n // Phase 2\n renderPhase(\"Collections → Database\", report.schemaToDatabase);\n\n // Phase 3\n renderPhase(\"Collections → SDK Types\", report.collectionsToSdk);\n\n // Summary\n out(chalk.gray(\" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"));\n const { passed, skipped, notApplicable, warnings, errors } = report.summary;\n\n const parts: string[] = [];\n parts.push(chalk.green(`${passed} passed`));\n if (skipped > 0) parts.push(chalk.yellow(`${skipped} skipped`));\n if (notApplicable > 0) parts.push(chalk.gray(`${notApplicable} not applicable`));\n if (warnings > 0) parts.push(chalk.yellow(`${warnings} warnings`));\n if (errors > 0) parts.push(chalk.red(`${errors} errors`));\n\n out(` Summary: ${parts.join(\", \")}`);\n out();\n\n if (errors > 0) {\n out(chalk.red.bold(\" ✗ Schema drift detected. Run the suggested fixes above.\"));\n } else if (warnings > 0) {\n out(chalk.yellow.bold(\" ⚠ Minor issues detected. Consider running the suggested fixes.\"));\n } else if (skipped > 0) {\n // Never \"all schemas are in sync\" off the back of a check that did not\n // run: a clean bill of health has to come from measurement.\n out(chalk.yellow.bold(` ⚠ Nothing wrong in the checks that ran, but ${skipped} did not run — this is not a clean bill of health.`));\n } else {\n out(chalk.green.bold(\" ✓ All schemas are in sync!\"));\n }\n out();\n}\n\nfunction renderPhase(label: string, phase: DoctorPhase): void {\n // A phase that never ran gets its own marker and its reason. It is neither\n // \"In sync\" nor a drift report; conflating it with the former is what let\n // doctor certify a database it had not opened.\n if (phase.skipped) {\n out(` ${chalk.yellow(\"⏭\")} ${label}: ${chalk.yellow(`skipped (${phase.skipped})`)}`);\n out();\n return;\n }\n\n // Nothing to compare against. Its own marker, and only the remedy under it\n // — the phase header already carries the reason, so re-printing the note\n // would say the same thing twice. See DoctorPhase.notApplicable.\n if (phase.notApplicable) {\n out(` ${chalk.gray(\"➖\")} ${label}: ${chalk.gray(phase.notApplicable)}`);\n for (const issue of phase.issues.filter((i) => i.severity === \"info\")) {\n out(` ${chalk.gray(issue.fix)}`);\n }\n out();\n return;\n }\n\n const issues = phase.issues;\n const errorCount = issues.filter((i) => i.severity === \"error\").length;\n const warnCount = issues.filter((i) => i.severity === \"warning\").length;\n const infoIssues = issues.filter((i) => i.severity === \"info\");\n\n // Informational notes don't make a phase unhealthy, so key the header off\n // real problems rather than `phase.passed` alone.\n if (errorCount === 0 && warnCount === 0) {\n out(` ${chalk.green(\"✅\")} ${label}: ${chalk.green(\"In sync\")}`);\n } else {\n const parts: string[] = [];\n if (errorCount > 0) parts.push(`${errorCount} error${errorCount > 1 ? \"s\" : \"\"}`);\n if (warnCount > 0) parts.push(`${warnCount} warning${warnCount > 1 ? \"s\" : \"\"}`);\n out(` ${chalk.yellow(\"⚠️\")} ${label}: ${chalk.yellow(parts.join(\", \"))}`);\n }\n out();\n\n // Notes render as a quiet one-liner, not a full drift box.\n for (const issue of infoIssues) {\n const fixPart = issue.fix ? chalk.gray(` — ${issue.fix}`) : \"\";\n out(` ${chalk.gray(\"ℹ\")} ${chalk.gray(issue.message)}${fixPart}`);\n out();\n }\n\n for (const issue of issues.filter((i) => i.severity !== \"info\")) {\n const severityIcon = issue.severity === \"error\" ? chalk.red(\"✗\") : chalk.yellow(\"⚠\");\n const categoryLabel = formatCategory(issue.category);\n out(` ${chalk.gray(\"┌─\")} ${severityIcon} ${chalk.bold(categoryLabel)} ${chalk.gray(\"─\".repeat(Math.max(0, 42 - categoryLabel.length)))}`);\n\n if (issue.table) {\n const colPart = issue.column ? ` │ Column: ${chalk.cyan(issue.column)}` : \"\";\n out(` ${chalk.gray(\"│\")} Table: ${chalk.cyan(issue.table)}${colPart}`);\n }\n\n if (issue.expected && issue.actual) {\n out(` ${chalk.gray(\"│\")} Expected: ${chalk.green(issue.expected)} │ Actual: ${chalk.red(issue.actual)}`);\n }\n\n out(` ${chalk.gray(\"│\")} ${issue.message}`);\n out(` ${chalk.gray(\"│\")} Fix: ${chalk.blue(issue.fix)}`);\n out(` ${chalk.gray(\"└\" + \"─\".repeat(48))}`);\n out();\n }\n}\n\nfunction formatCategory(cat: DoctorIssue[\"category\"]): string {\n const labels: Record<DoctorIssue[\"category\"], string> = {\n missing_table: \"Missing Table\",\n missing_column: \"Missing Column\",\n type_mismatch: \"Type Mismatch\",\n missing_constraint: \"Missing Constraint\",\n schema_stale: \"Stale Schema\",\n missing_enum: \"Missing Enum\",\n enum_value_mismatch: \"Enum Value Mismatch\",\n missing_foreign_key: \"Missing Foreign Key\",\n sdk_stale: \"Stale SDK Types\",\n sdk_not_generated: \"SDK Types Not Generated\",\n sdk_ungeneratable: \"SDK Cannot Be Generated\"\n };\n return labels[cat];\n}\n\n// ── Main entry point ─────────────────────────────────────────────────────\n\nexport async function runDoctor(options: {\n collectionsPath: string;\n schemaPath: string;\n sdkPath: string;\n databaseUrl?: string;\n}): Promise<DoctorReport> {\n out();\n out(chalk.bold(\" 🩺 Loading collections...\"));\n const collections = await loadCollections(options.collectionsPath);\n if (collections.length === 0) {\n outError(chalk.red(\" ✗ No collections found.\"));\n process.exit(1);\n }\n out(chalk.gray(` Found ${collections.length} collection(s)`));\n out();\n\n // Phase 1: Collections ↔ Generated Schema\n out(chalk.gray(\" Checking Collections → Generated Schema...\"));\n const collectionsToSchema = await checkCollectionsVsSchema(collections, options.schemaPath);\n\n // Phase 2: Collections ↔ Database (only if we have a DATABASE_URL)\n let schemaToDatabase: DoctorPhase;\n if (options.databaseUrl) {\n out(chalk.gray(\" Checking Collections → Database...\"));\n try {\n schemaToDatabase = await checkCollectionsVsDatabase(collections, options.databaseUrl);\n } catch (err: unknown) {\n // A database that cannot be reached used to end the whole run: the\n // error escaped `runDoctor` and the CLI's last-resort catch printed\n // a stack. So the two phases that need no database — the generated\n // schema and the SDK types, both of which are read off disk — never\n // ran, and the one command whose job is to say what is wrong said\n // only \"Doctor failed\" over a drizzle stack trace.\n const banner = diagnoseDbError(err, options.databaseUrl);\n if (banner) outError(banner);\n const reason = deepestErrorMessage(err)\n ?? (err instanceof Error ? err.message : String(err));\n schemaToDatabase = {\n passed: false,\n issues: [],\n skipped: `could not reach ${parseHostInfo(options.databaseUrl)} — ${reason}`,\n blocked: true\n };\n }\n } else {\n // Not `{ passed: true }`: see DoctorPhase.skipped.\n schemaToDatabase = { passed: false,\nissues: [],\nskipped: \"DATABASE_URL not set\" };\n out(chalk.yellow(\" ⚠ DATABASE_URL not set — skipping database comparison.\"));\n out(chalk.gray(\" Set DATABASE_URL in your .env to enable full drift detection.\"));\n }\n\n // Phase 3: Collections ↔ SDK Types\n out(chalk.gray(\" Checking Collections → SDK Types...\"));\n const collectionsToSdk = await checkCollectionsVsSdk(collections, options.sdkPath);\n\n const phases = [collectionsToSchema, schemaToDatabase, collectionsToSdk];\n const allIssues = phases.flatMap((p) => p.issues);\n const summary = {\n // A skipped phase is not a passing one, however few issues it collected.\n // Nor is one with nothing to compare against: \"passed\" would put a\n // never-generated SDK in the same column as a verified one.\n passed: phases.filter((p) => p.passed && !p.skipped && !p.notApplicable).length,\n skipped: phases.filter((p) => p.skipped).length,\n notApplicable: phases.filter((p) => p.notApplicable && !p.skipped).length,\n warnings: allIssues.filter((i) => i.severity === \"warning\").length,\n errors: allIssues.filter((i) => i.severity === \"error\").length\n };\n\n const report: DoctorReport = { collectionsToSchema,\ncollectionsToSdk,\nschemaToDatabase,\nsummary };\n renderReport(report);\n\n return report;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAcA,SAAS,aAAa,KAAK;CAC1B,OAAO,IAAI,MAAM,UAAU,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,SAAS;EAC1D,MAAM,OAAO,cAAc,KAAK,IAAI,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,YAAY,IAAI,KAAK,MAAM,CAAC;EAClF,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI;CACvC,CAAC,CAAC,CAAC,KAAK,EAAE;AACX;;;;;AAKA,SAAS,YAAY,KAAK;CACzB,IAAI,CAAC,UAAU,KAAK,GAAG,GAAG,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,IAAI,MAAM,CAAC;CAC1E,MAAM,SAAS,aAAa,GAAG;CAC/B,OAAO,OAAO,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,OAAO,MAAM,CAAC;AACvD;;;;;AAKA,SAAS,iBAAiB,KAAK;CAC9B,OAAO,YAAY,IAAI,QAAQ,kBAAkB,GAAG,CAAC;AACtD;;;;;;;;;AAkBA,IAAI,eAAe,cAAc,MAAM;CACtC,YAAY,SAAS;EACpB,MAAM,OAAO;EACb,KAAK,OAAO;CACb;AACD;AACA,IAAI,aAAa;;;;;;;;;;;AAWjB,SAAS,QAAQ,KAAK;CACrB,OAAO,WAAW,KAAK,GAAG,IAAI,MAAM,KAAK,UAAU,GAAG;AACvD;;;;;;;;;AASA,SAAS,WAAW,OAAO;CAC1B,OAAO,KAAK,UAAU,KAAK;AAC5B;;AAEA,SAAS,QAAQ,KAAK;CACrB,IAAI,MAAM,QAAQ,GAAG,GAAG,OAAO,IAAI,KAAK,UAAU,SAAS,OAAO,UAAU,WAAW,MAAM,KAAK,KAAK;CACvG,IAAI,OAAO,OAAO,QAAQ,UAAU,OAAO,OAAO,KAAK,GAAG;CAC1D,OAAO,CAAC;AACT;AACA,SAAS,yBAAyB,MAAM;CACvC,QAAQ,KAAK,MAAb;EACC,KAAK,UAAU;GACd,MAAM,KAAK;GACX,IAAI,GAAG,MAAM;IACZ,MAAM,MAAM,QAAQ,GAAG,IAAI;IAC3B,IAAI,IAAI,WAAW,GAAG,OAAO;IAC7B,OAAO,IAAI,KAAK,MAAM,WAAW,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK;GACxD;GACA,OAAO;EACR;EACA,KAAK,UAAU;GACd,MAAM,KAAK;GACX,IAAI,GAAG,MAAM;IACZ,MAAM,MAAM,QAAQ,GAAG,IAAI;IAC3B,MAAM,UAAU,IAAI,IAAI,MAAM;IAC9B,IAAI,IAAI,WAAW,KAAK,QAAQ,MAAM,MAAM,CAAC,OAAO,SAAS,CAAC,CAAC,GAAG,OAAO;IACzE,OAAO,QAAQ,KAAK,MAAM,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK;GAChD;GACA,OAAO;EACR;EACA,KAAK,WAAW,OAAO;EACvB,KAAK,QAAQ,OAAO;EACpB,KAAK,YAAY,OAAO;EACxB,KAAK,aAAa,OAAO;EACzB,KAAK,YAAY,OAAO;EACxB,KAAK,OAAO;GACX,MAAM,UAAU;GAChB,IAAI,QAAQ,YAAY,OAAO,KAAK,OAAO,QAAQ,QAAQ,UAAU,CAAC,CAAC,KAAK,CAAC,GAAG,OAAO;IACtF,MAAM,QAAQ;IACd,MAAM,WAAW,CAAC,MAAM,YAAY;IACpC,MAAM,OAAO,yBAAyB,KAAK;IAC3C,OAAO,GAAG,QAAQ,CAAC,IAAI,WAAW,MAAM,GAAG,IAAI,WAAW,GAAG,KAAK,WAAW,KAAK;GACnF,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE;GACb,OAAO;EACR;EACA,KAAK,SAAS;GACb,MAAM,UAAU;GAChB,IAAI,QAAQ,IAAI,OAAO,SAAS,yBAAyB,QAAQ,EAAE,EAAE;GACrE,OAAO;EACR;EACA,KAAK,UAAU,OAAO;EACtB,KAAK,UAAU,OAAO;EACtB,SAAS,OAAO;CACjB;AACD;;;;;AAKA,SAAS,wBAAwB,UAAU;CAC1C,IAAI;EACH,IAAI,SAAS,SAAS,OAAO;EAC7B,IAAI,WAAW,OAAO,WAAW,OAAO,aAAa,SAAS,OAAO,WAAW;EAChF,OAAO;CACR,QAAQ;EACP;CACD;AACD;;AAEA,SAAS,eAAe,UAAU;CACjC,MAAM,SAAS,wBAAwB,QAAQ;CAC/C,IAAI,CAAC,QAAQ,YAAY,OAAO;CAChC,MAAM,SAAS,OAAO,QAAQ,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,GAAG,OAAO,EAAE,IAAI;CACxE,IAAI,CAAC,QAAQ,OAAO;CACpB,OAAO,OAAO,EAAE,CAAC,SAAS,WAAW,WAAW;AACjD;;AAEA,SAAS,aAAa,MAAM;CAC3B,OAAO,QAAQ,KAAK,IAAI;AACzB;;;;;AAKA,SAAS,iBAAiB,MAAM;CAC/B,MAAM,OAAO,KAAK;CAClB,OAAO,QAAQ,IAAI,KAAK,SAAS,YAAY,SAAS;AACvD;;;;;;;;;;;;;AAaA,SAAS,qBAAqB,UAAU,WAAW;CAClD,MAAM,OAAO,wBAAwB,QAAQ,CAAC,EAAE,QAAQ,SAAS;CACjE,MAAM,WAAW,OAAO,UAAU,IAAI,IAAI,IAAI,KAAK;CACnD,MAAM,UAAU,WAAW,YAAY,WAAW,QAAQ,EAAE,YAAY;CACxE,OAAO,SAAS,gBAAgB,SAAS,SAAS,QAAQ,KAAK;AAChE;;;;;;;;;;;AAWA,SAAS,eAAe,aAAa;CACpC,MAAM,4BAA4B,IAAI,IAAI;CAC1C,MAAM,6BAA6B,IAAI,IAAI;CAC3C,KAAK,MAAM,cAAc,aAAa;EACrC,MAAM,OAAO,WAAW;EACxB,IAAI,OAAO,SAAS,YAAY,KAAK,WAAW,GAAG,MAAM,IAAI,aAAa,6GAA6G;EACvL,MAAM,OAAO,iBAAiB,IAAI;EAClC,IAAI,KAAK,WAAW,GAAG,MAAM,IAAI,aAAa,YAAY,WAAW,IAAI,EAAE,mKAAmK;EAC9O,MAAM,WAAW,WAAW,IAAI,IAAI;EACpC,IAAI,aAAa,KAAK,GAAG,MAAM,IAAI,aAAa,mBAAmB,WAAW,QAAQ,EAAE,OAAO,WAAW,IAAI,EAAE,+BAA+B,KAAK,uJAAuJ;EAC3S,WAAW,IAAI,MAAM,IAAI;EACzB,UAAU,IAAI,MAAM,IAAI;CACzB;CACA,OAAO;AACR;;AAEA,SAAS,KAAK,KAAK,MAAM,UAAU;CAClC,OAAO,SAAS,QAAQ,GAAG,IAAI,WAAW,MAAM,GAAG,IAAI,KAAK;AAC7D;;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAS,gBAAgB,YAAY;CACpC,MAAM,2BAA2B,IAAI,IAAI;CACzC,KAAK,MAAM,CAAC,KAAK,YAAY,OAAO,QAAQ,UAAU,GAAG;EACxD,MAAM,OAAO;EACb,MAAM,SAAS,gBAAgB,IAAI;EACnC,IAAI,QAAQ,MAAM,WAAW,KAAK,QAAQ,OAAO,WAAW,GAAG;EAC/D,SAAS,IAAI,GAAG;EAChB,IAAI,KAAK,YAAY,SAAS,IAAI,KAAK,UAAU;CAClD;CACA,OAAO;AACR;AACA,SAAS,iBAAiB,OAAO;CAChC,MAAM,cAAc,sBAAsB,KAAK;CAC/C,MAAM,YAAY,eAAe,WAAW;CAC5C,MAAM,QAAQ;EACb;EACA;EACA;EACA;EACA;EACA;CACD;CACA,KAAK,MAAM,cAAc,aAAa;EACrC,MAAM,aAAa,WAAW,cAAc,CAAC;EAC7C,IAAI,oBAAoB,CAAC;EACzB,IAAI;GACH,oBAAoB,2BAA2B,UAAU;EAC1D,SAAS,GAAG;GACX,QAAQ,KAAK,gDAAgD,WAAW,KAAK,4OAA4O,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,GAAG;EACtW;EACA,MAAM,iBAAiB,WAAW;EAClC,IAAI,MAAM,QAAQ,cAAc,KAAK,eAAe,SAAS,GAAG,QAAQ,KAAK,aAAa,WAAW,KAAK,aAAa,eAAe,OAAO,+GAA+G,WAAW,KAAK,2IAA2I;EACvZ,MAAM,KAAK,KAAK,QAAQ,UAAU,IAAI,WAAW,IAAI,CAAC,EAAE,IAAI;EAC5D,MAAM,KAAK,YAAY;EACvB,MAAM,8BAA8B,IAAI,IAAI;EAC5C,MAAM,WAAW,gBAAgB,UAAU;EAC3C,KAAK,MAAM,OAAO,UAAU,YAAY,IAAI,GAAG;EAC/C,KAAK,MAAM,CAAC,KAAK,YAAY,OAAO,QAAQ,UAAU,GAAG;GACxD,MAAM,OAAO;GACb,IAAI,KAAK,SAAS,YAAY;GAC9B,IAAI,SAAS,IAAI,GAAG,GAAG;GACvB,MAAM,SAAS,yBAAyB,IAAI;GAC5C,MAAM,aAAa,QAAQ,KAAK,YAAY,QAAQ,KAAK,aAAa,IAAI;GAC1E,MAAM,KAAK,KAAK,KAAK,aAAa,SAAS,GAAG,OAAO,UAAU,CAAC,UAAU,CAAC;GAC3E,YAAY,IAAI,GAAG;EACpB;EACA,KAAK,MAAM,CAAC,QAAQ,aAAa,OAAO,QAAQ,iBAAiB,GAAG,IAAI,SAAS,SAAS,eAAe,SAAS,UAAU;GAC3H,MAAM,QAAQ,kBAAkB,YAAY,SAAS,QAAQ;GAC7D,IAAI,YAAY,IAAI,KAAK,GAAG;GAC5B,MAAM,SAAS,eAAe,QAAQ;GACtC,MAAM,oBAAoB,WAAW;GACrC,MAAM,SAAS,oBAAoB,GAAG,OAAO,KAAK,qBAAqB,UAAU,SAAS,MAAM;GAChG,MAAM,aAAa,mBAAmB,YAAY,QAAQ,KAAK,CAAC;GAChE,MAAM,KAAK,KAAK,OAAO,aAAa,SAAS,GAAG,OAAO,UAAU,CAAC,UAAU,CAAC;GAC7E,YAAY,IAAI,KAAK;EACtB;EACA,KAAK,MAAM,CAAC,KAAK,aAAa,OAAO,QAAQ,iBAAiB,GAAG;GAChE,IAAI,YAAY,IAAI,GAAG,GAAG;GAC1B,MAAM,KAAK,KAAK,KAAK,qBAAqB,UAAU,SAAS,GAAG,IAAI,CAAC;GACrE,YAAY,IAAI,GAAG;EACpB;EACA,KAAK,MAAM,CAAC,KAAK,YAAY,OAAO,QAAQ,UAAU,GAAG;GACxD,IAAI,QAAQ,SAAS,YAAY;GACjC,IAAI,YAAY,IAAI,GAAG,GAAG;GAC1B,MAAM,KAAK,KAAK,KAAK,2BAA2B,IAAI,CAAC;GACrD,YAAY,IAAI,GAAG;EACpB;EACA,MAAM,KAAK,QAAQ;EACnB,MAAM,KAAK,eAAe;EAC1B,YAAY,MAAM;EAClB,KAAK,MAAM,OAAO,UAAU,YAAY,IAAI,GAAG;EAC/C,KAAK,MAAM,CAAC,KAAK,YAAY,OAAO,QAAQ,UAAU,GAAG;GACxD,MAAM,OAAO;GACb,IAAI,KAAK,SAAS,YAAY;GAC9B,IAAI,SAAS,IAAI,GAAG,GAAG;GACvB,MAAM,SAAS,yBAAyB,IAAI;GAC5C,MAAM,aAAa,CAAC,KAAK,YAAY,YAAY,iBAAiB,IAAI;GACtE,MAAM,KAAK,KAAK,KAAK,QAAQ,UAAU,CAAC;GACxC,YAAY,IAAI,GAAG;EACpB;EACA,sBAAsB,OAAO,YAAY,YAAY,mBAAmB,aAAa,KAAK;EAC1F,MAAM,KAAK,QAAQ;EACnB,MAAM,KAAK,eAAe;EAC1B,YAAY,MAAM;EAClB,KAAK,MAAM,OAAO,UAAU,YAAY,IAAI,GAAG;EAC/C,KAAK,MAAM,CAAC,KAAK,YAAY,OAAO,QAAQ,UAAU,GAAG;GACxD,MAAM,OAAO;GACb,IAAI,KAAK,SAAS,YAAY;GAC9B,IAAI,aAAa,IAAI,GAAG;GACxB,IAAI,SAAS,IAAI,GAAG,GAAG;GACvB,MAAM,KAAK,KAAK,KAAK,yBAAyB,IAAI,GAAG,IAAI,CAAC;GAC1D,YAAY,IAAI,GAAG;EACpB;EACA,sBAAsB,OAAO,YAAY,YAAY,mBAAmB,aAAa,IAAI;EACzF,MAAM,KAAK,QAAQ;EACnB,MAAM,KAAK,kBAAkB;EAC7B,KAAK,MAAM,CAAC,KAAK,aAAa,OAAO,QAAQ,iBAAiB,GAAG;GAChE,MAAM,OAAO,wBAAwB,QAAQ,CAAC,EAAE,QAAQ,SAAS;GACjE,MAAM,WAAW,OAAO,UAAU,IAAI,IAAI,IAAI,KAAK;GACnD,MAAM,KAAK,SAAS,QAAQ,GAAG,EAAE,IAAI,WAAW,WAAW,QAAQ,IAAI,QAAQ,EAAE;EAClF;EACA,MAAM,KAAK,QAAQ;EACnB,MAAM,KAAK,MAAM;CAClB;CACA,MAAM,KAAK,GAAG;CACd,MAAM,KAAK,EAAE;CACb,MAAM,KAAK,8CAA8C;CACzD,MAAM,KAAK,EAAE;CACb,MAAM,KAAK,wCAAwC;CACnD,KAAK,MAAM,cAAc,aAAa,MAAM,KAAK,KAAK,QAAQ,UAAU,IAAI,WAAW,IAAI,CAAC,EAAE,IAAI,WAAW,WAAW,IAAI,EAAE,EAAE;CAChI,MAAM,KAAK,aAAa;CACxB,MAAM,KAAK,EAAE;CACb,MAAM,KAAK,mEAAmE;CAC9E,MAAM,KAAK,EAAE;CACb,MAAM,KAAK,GAAG,mBAAmB,CAAC;CAClC,OAAO,MAAM,KAAK,IAAI;AACvB;;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAS,qBAAqB;CAC7B,OAAO;EACN;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACD;AACD;;;;;;;;;;;;;;;;;;AAkBA,SAAS,sBAAsB,OAAO,YAAY,YAAY,mBAAmB,aAAa,aAAa;CAC1G,MAAM,QAAQ,KAAK,aAAa;EAC/B,IAAI,YAAY,IAAI,GAAG,GAAG;EAC1B,MAAM,WAAW,eAAe,CAAC,mBAAmB,YAAY,QAAQ;EACxE,MAAM,KAAK,KAAK,KAAK,eAAe,QAAQ,GAAG,QAAQ,CAAC;EACxD,YAAY,IAAI,GAAG;CACpB;CACA,KAAK,MAAM,YAAY,OAAO,OAAO,iBAAiB,GAAG,IAAI,SAAS,SAAS,eAAe,SAAS,UAAU,KAAK,kBAAkB,YAAY,SAAS,QAAQ,GAAG,QAAQ;CAChL,KAAK,MAAM,CAAC,KAAK,YAAY,OAAO,QAAQ,UAAU,GAAG;EACxD,IAAI,QAAQ,SAAS,YAAY;EACjC,MAAM,WAAW,aAAa,mBAAmB,GAAG;EACpD,IAAI,UAAU,SAAS,eAAe,SAAS,UAAU,KAAK,KAAK,QAAQ;CAC5E;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACxbA,IAAM,qBAAqB,UAAkB,SAAmC;CAC5E,IAAI,QAAQ,gBAAgB,QAAQ,OAAO,KAAK,eAAe,UAC3D,OAAO,KAAK;CAEhB,OAAO,YAAY,QAAQ;AAC/B;;;;;;;;;;;;;;;;AAqFA,IAAM,2BAA2D;CAC7D,MAAM;CACN,SAAS;CACT,MAAM;CACN,MAAM;CAEN,MAAM;CACN,UAAU;CACV,SAAS;CACT,QAAQ;CAGR,aAAa;CACb,QAAQ;CACR,WAAW;CACX,MAAM;CACN,iBAAiB;CACjB,SAAS;CACT,SAAS;CACT,aAAa;CACb,MAAM;CACN,MAAM;CACN,MAAM;CACN,OAAO;CAEP,QAAQ;CACR,OAAO;CACP,UAAU;CACV,OAAO;AACX;;;;;;;;;;AAWA,SAAgB,sBAAsB,MAA+B;CAGjE,IAAI,KAAK,SAAS,YAAY,OAAO;CAMrC,MAAM,aAAa;EACf,MAAM;EACN,OAAO;EACP,YAAY,EAAE,YAAY,KAAK;CACnC;CAEA,IAAI;EAEA,OAAO,yBADM,aAAa,cAAc,MAAM,kBAAkB,KAAA,CAChC,CAAA,CAAK;CACzC,QAAQ;EACJ,OAAO;CACX;AACJ;;;;;;;AAUA,eAAsB,gBAAgB,iBAAsD;CACxF,OAAO,6BAA6B,eAAe;AACvD;AAIA,eAAsB,yBAClB,aACA,gBACoB;CACpB,MAAM,SAAwB,CAAC;CAG/B,IAAI,CAAC,KAAG,WAAW,cAAc,GAAG;EAChC,OAAO,KAAK;GACR,UAAU;GACV,UAAU;GACV,SAAS;GACT,KAAK;EACT,CAAC;EACD,OAAO;GAAE,QAAQ;GACzB;EAAO;CACH;CAKA,MAAM,sBAAsB,sBAAsB,WAAW;CAC7D,IAAI,oBAAoB,WAAW,GAC/B,OAAO;EAAE,QAAQ;EACzB;CAAO;CAGH,IAAI;EAMA,MAAM,cAAc,6BAChB,eAAe,mBAAmB,GAClC,MAAM,SAAW,SAAS,gBAAgB,OAAO,CACrD;EAEA,IAAI,YAAY,SAAS,GACrB,OAAO,KAAK;GACR,UAAU;GACV,UAAU;GACV,SAAS,4GACY,+BAA+B,WAAW,EAAE;GACjE,KAAK;EACT,CAAC;CAET,SAAS,KAAc;EACnB,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;EAC/D,OAAO,KAAK;GACR,UAAU;GACV,UAAU;GACV,SAAS,+CAA+C;GACxD,KAAK;EACT,CAAC;CACL;CAEA,OAAO;EAAE,QAAQ,OAAO,WAAW;EACvC;CAAO;AACP;AAEA,eAAsB,sBAClB,aACA,aACoB;CACpB,MAAM,SAAwB,CAAC;CAM/B,IAAI,CAAC,KAAG,WAAW,WAAW,GAAG;EAC7B,OAAO,KAAK;GACR,UAAU;GACV,UAAU;GACV,SAAS;GACT,KAAK;EACT,CAAC;EACD,OAAO;GAAE,QAAQ;GACzB;GACA,eAAe;EAA2B;CACtC;CAEA,IAAI;EACA,MAAM,cAAc,iBAAiB,WAAW;EAChD,MAAM,YAAY,MAAM,SAAW,SAAS,aAAa,OAAO;EAGhE,MAAM,aAAa,MACf,EACK,QAAQ,aAAa,EAAE,CAAC,CACxB,QAAQ,qBAAqB,EAAE,CAAC,CAChC,QAAQ,QAAQ,GAAG,CAAC,CACpB,KAAK;EAEd,IAAI,UAAU,WAAW,MAAM,UAAU,SAAS,GAC9C,OAAO,KAAK;GACR,UAAU;GACV,UAAU;GACV,SAAS;GACT,KAAK;EACT,CAAC;CAET,SAAS,KAAc;EACnB,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;EAO/D,IAAI,eAAe,SAAS,IAAI,SAAS,gBACrC,OAAO,KAAK;GACR,UAAU;GACV,UAAU;GACV,SAAS,6DAA6D;GACtE,KAAK;EACT,CAAC;OAED,OAAO,KAAK;GACR,UAAU;GACV,UAAU;GACV,SAAS,kDAAkD;GAC3D,KAAK;EACT,CAAC;CAET;CAEA,OAAO;EAAE,QAAQ,OAAO,WAAW;EACvC;CAAO;AACP;;;;;;;;;;;;AA8BA,SAAgB,uBACZ,aACA,WACa;CAKb,IAAI,CAJe,YAAY,MAAK,eAChC,OAAO,OAAO,WAAW,cAAc,CAAC,CAAC,CAAC,CACrC,MAAK,aAAa,UAAuC,SAAS,QAAQ,CAE9E,KAAc,UAAU,IAAI,QAAQ,GAAG,OAAO,CAAC;CAEpD,OAAO,CAAC;EACJ,UAAU;EACV,UAAU;EACV,SAAS;EACT,KAAK;CAET,CAAC;AACL;;;;;;;;;;;;;;;AAgBA,eAAsB,kBAClB,MACA,aACsB;CACtB,IAAI,UAAyB;CAC7B,IAAI;EAMA,IAAI,EAAC,MAHiB,KAAK,MACvB,qEACJ,EAAA,CACa,KAAK,EAAE,EAAE,SAAS,OAAO,CAAC;EAIvC,WAAU,MAHQ,KAAK,MACnB,2FACJ,EAAA,CACc,KAAK,EAAE,EAAE,SAAS;CACpC,QAAQ;EAIJ,OAAO,CAAC;CACZ;CAEA,IAAI,CAAC,SAAS,OAAO,CAAC;CACtB,MAAM,WAAW,qBAAqB,WAAW;CACjD,IAAI,YAAY,UAAU,OAAO,CAAC;CAElC,OAAO,CAAC;EACJ,UAAU;EACV,UAAU;EACV,SAAS,4EAA4E,QAAQ,MAAM,GAAG,EAAE,EAAE,4BAA4B,SAAS,MAAM,GAAG,EAAE,EAAE;EAC5J,KAAK;CACT,CAAC;AACL;AAEA,eAAsB,2BAClB,aACA,aACoB;CACpB,MAAM,SAAwB,CAAC;CAG/B,MAAM,WAAW,MAAM,OAAO;CAC9B,MAAM,EAAE,SAAS,SAAS,WAAW;CACrC,MAAM,OAAO,IAAI,KAAK,EAAE,kBAAkB,YAAY,CAAC;CAGvD,MAAM,UAAU,MAAM,qBAAK,IAAI,IAAI;EAC/B;EACA;EACA,GAAG,sBAAsB,WAAW,CAAC,CAChC,OAAO,0BAA0B,CAAC,CAClC,KAAI,MAAK,EAAE,MAAM,CAAC,CAClB,QAAQ,MAAmB,CAAC,CAAC,CAAC;CACvC,CAAC,CAAC;CAEF,IAAI;EAEA,MAAM,eAAe,MAAM,KAAK,MAC5B;;0EAGA,CAAC,OAAO,CACZ;EACA,MAAM,iBAAiB,IAAI,IAAI,aAAa,KAAK,KAAK,MAClD,EAAE,iBAAiB,WAAW,EAAE,aAAa,GAAG,EAAE,aAAa,GAAG,EAAE,YACxE,CAAC;EAUD,MAAM,mBAAmB,MAAM,KAAK,MAA2B,kCAAkC;EACjG,MAAM,sBAAsB,IAAI,IAAI,iBAAiB,KAAK,KAAI,MAAK,EAAE,OAAO,CAAC;EAC7E,OAAO,KAAK,GAAG,uBAAuB,aAAa,mBAAmB,CAAC;EAQvE,OAAO,KAAK,GAAG,MAAM,kBAAkB,MAAM,WAAW,CAAC;EAGzD,MAAM,gBAAgB,MAAM,KAAK,MAC7B;;;mEAIA,CAAC,OAAO,CACZ;EACA,MAAM,iCAAiB,IAAI,IAAwB;EACnD,KAAK,MAAM,OAAO,cAAc,MAAM;GAClC,MAAM,cAAc,IAAI;GACxB,MAAM,YAAY,IAAI;GACtB,MAAM,MAAM,gBAAgB,WAAW,YAAY,GAAG,YAAY,GAAG;GACrE,IAAI,CAAC,eAAe,IAAI,GAAG,GACvB,eAAe,IAAI,KAAK,CAAC,CAAC;GAE9B,eAAe,IAAI,GAAG,CAAC,CAAE,KAAK,GAAG;EACrC;EAGA,MAAM,cAAc,MAAM,KAAK,MAC3B;;;iDAIJ;EACA,MAAM,8BAAc,IAAI,IAAsB;EAC9C,KAAK,MAAM,OAAO,YAAY,MAAM;GAChC,IAAI,CAAC,YAAY,IAAI,IAAI,SAAS,GAC9B,YAAY,IAAI,IAAI,WAAW,CAAC,CAAC;GAErC,YAAY,IAAI,IAAI,SAAS,CAAC,CAAE,KAAK,IAAI,UAAU;EACvD;EAGA,MAAM,YAAY,MAAM,KAAK,MASzB;;;;;;;;;;;;;sFAcA,CAAC,OAAO,CACZ;EACA,MAAM,6BAAa,IAAI,IAAmC;EAC1D,KAAK,MAAM,OAAO,UAAU,MAAM;GAC9B,MAAM,cAAc,IAAI;GACxB,MAAM,YAAY,IAAI;GACtB,MAAM,MAAM,gBAAgB,WAAW,YAAY,GAAG,YAAY,GAAG;GACrE,IAAI,CAAC,WAAW,IAAI,GAAG,GACnB,WAAW,IAAI,KAAK,CAAC,CAAC;GAE1B,WAAW,IAAI,GAAG,CAAC,CAAE,KAAK,GAAG;EACjC;EAIA,MAAM,sBAAsB,sBAAsB,WAAW;EAE7D,KAAK,MAAM,cAAc,qBAAqB;GAC1C,MAAM,YAAY,aAAa,UAAU;GACzC,MAAM,aAAc,WAAmC,UAAU;GACjE,MAAM,gBAAgB,eAAe,WAAW,YAAY,GAAG,WAAW,GAAG;GAG7E,IAAI,CAAC,eAAe,IAAI,aAAa,GAAG;IACpC,OAAO,KAAK;KACR,UAAU;KACV,UAAU;KACV,OAAO;KACP,SAAS,UAAU,cAAc;KACjC,KAAK;IACT,CAAC;IACD;GACJ;GAEA,MAAM,YAAY,eAAe,IAAI,aAAa,KAAK,CAAC;GACxD,MAAM,cAAc,IAAI,IAAI,UAAU,KAAK,MAAM,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC;GAGpE,MAAM,gCAAgB,IAAI,IAAI;IAAC;IAAM;IAAc;GAAY,CAAC;GAGhE,KAAK,MAAM,CAAC,UAAU,SAAS,OAAO,QAAQ,WAAW,cAAc,CAAC,CAAC,GAAG;IACxE,IAAI,KAAK,SAAS,YAAY;KAG1B,MAAM,WAAW,aADS,2BAA2B,UACvB,GAAoB,KAA0B,UAAU,gBAAgB,QAAQ;KAC9G,IAAI,UAAU,SAAS,aAAa;MAChC,MAAM,YAAY,SAAS;MAC3B,IAAI,CAAC,YAAY,IAAI,SAAS,GAC1B,OAAO,KAAK;OACR,UAAU;OACV,UAAU;OACV,OAAO;OACP,QAAQ;OACR,SAAS,uBAAuB,UAAU,kBAAkB,SAAS,2BAA2B,cAAc;OAC9G,KAAK;MACT,CAAC;MAIL,MAAM,WAAW,WAAW,IAAI,aAAa,KAAK,CAAC;MACnD,IAAI,kBAAkB;MACtB,IAAI,mBAAmB;MACvB,IAAI;OACA,MAAM,aAAa,SAAS,OAAO;OACnC,kBAAkB,aAAa,UAAU;OACzC,mBAAoB,WAAmC,UAAU;MACrE,QAAQ,CAAe;MAEvB,MAAM,QAAQ,SAAS,MAAM,OACzB,GAAG,gBAAgB,aACnB,GAAG,uBAAuB,mBAC1B,GAAG,yBAAyB,gBAChC;MAEA,IAAI,YAAY,IAAI,SAAS,KAAK,CAAC,OAC/B,OAAO,KAAK;OACR,UAAU;OACV,UAAU;OACV,OAAO;OACP,QAAQ;OACR,SAAS,WAAW,UAAU,0DAA0D,qBAAqB,WAAW,kBAAkB,GAAG,iBAAiB,GAAG,kBAAkB;OACnL,KAAK;MACT,CAAC;KAET;KACA;IACJ;IAEA,MAAM,UAAU,kBAAkB,UAAU,IAAI;IAGhD,IAAI,cAAc,IAAI,OAAO,GAAG;IAEhC,MAAM,QAAQ,YAAY,IAAI,OAAO;IACrC,IAAI,CAAC,OAAO;KACR,OAAO,KAAK;MACR,UAAU;MACV,UAAU;MACV,OAAO;MACP,QAAQ;MACR,SAAS,WAAW,QAAQ,8BAA8B,WAAW,KAAK,4BAA4B,cAAc;MACpH,KAAK;KACT,CAAC;KACD;IACJ;IAGA,MAAM,eAAe,sBAAsB,IAAI;IAC/C,IAAI,cAAc;KACd,MAAM,aAAa,MAAM;KACzB,IAAI,aAAa,eAAe;KAChC,IAAI,KAAK,SAAS,YAAY,MAAM,aAAa,UAC7C,aAAa;KAEjB,IAAI,KAAK,SAAS,SAAS;MACvB,MAAM,KAAK;MACX,IAAI,kBAAkB,GAAG;MACzB,IAAI,CAAC,mBAAmB,GAAG,MAAM,CAAC,MAAM,QAAQ,GAAG,EAAE,GAAG;OACpD,MAAM,SAAS,GAAG;OAClB,IAAI,OAAO,SAAS,UAAU,kBAAkB;YAC3C,IAAI,OAAO,SAAS,UAAU,kBAAkB,OAAO,YAAY,UAAU,cAAc;YAC3F,IAAI,OAAO,SAAS,WAAW,kBAAkB;MAC1D;MACA,IAAI,mBAAmB,gBAAgB,SAAS,IAAI,GAChD,IAAI,eAAe,SACf,aAAa;WACV;OACH,MAAM,cAAc,oBAAoB,WAAW,UAC/B,oBAAoB,cAAc,UAClC,oBAAoB,cAAc,UAClC,oBAAoB,cAAc,aAAa;OACnE,IAAI,eAAe,MAAM,aAAa,aAClC,aAAa;MAErB;KAER;KACA,IAAI,YACA,OAAO,KAAK;MACR,UAAU;MACV,UAAU;MACV,OAAO;MACP,QAAQ;MACR,UAAU,KAAK,SAAS,WAAW,WAAW;MAC9C,QAAQ,MAAM,aAAa,WAAW,WAAW;MACjD,SAAS,WAAW,QAAQ,cAAc,cAAc,oBAAoB,KAAK,SAAS,WAAW,WAAW,aAAa,eAAe,MAAM,aAAa,WAAW,WAAW,WAAW;MAChM,KAAK;KACT,CAAC;IAET;IAGA,IAAI,KAAK,SAAS,YAAa,KAAwB,MAAM;KACzD,MAAM,aAAc,KAAwB;KAC5C,IAAI,YAAY;MACZ,MAAM,WAAW,GAAG,UAAU,GAAG;MACjC,MAAM,eAAe,YAAY,IAAI,QAAQ;MAC7C,IAAI,CAAC,cACD,OAAO,KAAK;OACR,UAAU;OACV,UAAU;OACV,OAAO;OACP,QAAQ;OACR,UAAU;OACV,SAAS,cAAc,SAAS;OAChC,KAAK;MACT,CAAC;WACE;OAEH,MAAM,iBAAiB,MAAM,QAAQ,UAAU,IACzC,WAAW,KAAK,MAAO,OAAO,MAAM,WAAW,IAAI,OAAO,EAAE,EAAE,CAAE,IAChE,OAAO,KAAK,UAAU;OAE5B,MAAM,UAAU,eAAe,QAAQ,MAAM,CAAC,aAAa,SAAS,CAAC,CAAC;OACtE,MAAM,QAAQ,aAAa,QAAQ,MAAM,CAAC,eAAe,SAAS,CAAC,CAAC;OAEpE,IAAI,QAAQ,SAAS,KAAK,MAAM,SAAS,GAAG;QACxC,MAAM,QAAkB,CAAC;QACzB,IAAI,QAAQ,SAAS,GAAG,MAAM,KAAK,YAAY,QAAQ,KAAK,IAAI,GAAG;QACnE,IAAI,MAAM,SAAS,GAAG,MAAM,KAAK,gBAAgB,MAAM,KAAK,IAAI,GAAG;QACnE,OAAO,KAAK;SACR,UAAU;SACV,UAAU;SACV,OAAO;SACP,QAAQ;SACR,UAAU,eAAe,KAAK,IAAI;SAClC,QAAQ,aAAa,KAAK,IAAI;SAC9B,SAAS,oBAAoB,QAAQ,cAAc,cAAc,qBAAqB,MAAM,KAAK,IAAI,EAAE;SACvG,KAAK;QACT,CAAC;OACL;MACJ;KACJ;IACJ;GACJ;GAGA,MAAM,oBAAoB,2BAA2B,UAAU;GAC/D,KAAK,MAAM,YAAY,OAAO,OAAO,iBAAiB,GAClD,IAAI,aAAa,QAAQ,GAAG;IACxB,MAAM,gBAAgB,SAAS,QAAQ;IACvC,MAAM,iBAAkB,WAAmC,UAAU;IACrE,MAAM,oBAAoB,mBAAmB,WAAW,gBAAgB,GAAG,eAAe,GAAG;IAC7F,IAAI,CAAC,eAAe,IAAI,iBAAiB,GACrC,OAAO,KAAK;KACR,UAAU;KACV,UAAU;KACV,OAAO;KACP,SAAS,mBAAmB,kBAAkB,+BAA+B,SAAS,aAAa;KACnG,KAAK;IACT,CAAC;GAET;EAER;CACJ,UAAU;EACN,MAAM,KAAK,IAAI;CACnB;CAEA,OAAO;EAAE,QAAQ,OAAO,WAAW;EACvC;CAAO;AACP;AAIA,SAAgB,aAAa,QAA4B;CACrD,IAAI;CACJ,IAAI,MAAM,KAAK,2BAA2B,CAAC;CAC3C,IAAI,MAAM,KAAK,oDAAoD,CAAC;CACpE,IAAI;CAGJ,YAAY,kCAAkC,OAAO,mBAAmB;CAGxE,YAAY,0BAA0B,OAAO,gBAAgB;CAG7D,YAAY,2BAA2B,OAAO,gBAAgB;CAG9D,IAAI,MAAM,KAAK,oDAAoD,CAAC;CACpE,MAAM,EAAE,QAAQ,SAAS,eAAe,UAAU,WAAW,OAAO;CAEpE,MAAM,QAAkB,CAAC;CACzB,MAAM,KAAK,MAAM,MAAM,GAAG,OAAO,QAAQ,CAAC;CAC1C,IAAI,UAAU,GAAG,MAAM,KAAK,MAAM,OAAO,GAAG,QAAQ,SAAS,CAAC;CAC9D,IAAI,gBAAgB,GAAG,MAAM,KAAK,MAAM,KAAK,GAAG,cAAc,gBAAgB,CAAC;CAC/E,IAAI,WAAW,GAAG,MAAM,KAAK,MAAM,OAAO,GAAG,SAAS,UAAU,CAAC;CACjE,IAAI,SAAS,GAAG,MAAM,KAAK,MAAM,IAAI,GAAG,OAAO,QAAQ,CAAC;CAExD,IAAI,cAAc,MAAM,KAAK,IAAI,GAAG;CACpC,IAAI;CAEJ,IAAI,SAAS,GACT,IAAI,MAAM,IAAI,KAAK,2DAA2D,CAAC;MAC5E,IAAI,WAAW,GAClB,IAAI,MAAM,OAAO,KAAK,kEAAkE,CAAC;MACtF,IAAI,UAAU,GAGjB,IAAI,MAAM,OAAO,KAAK,iDAAiD,QAAQ,mDAAmD,CAAC;MAEnI,IAAI,MAAM,MAAM,KAAK,8BAA8B,CAAC;CAExD,IAAI;AACR;AAEA,SAAS,YAAY,OAAe,OAA0B;CAI1D,IAAI,MAAM,SAAS;EACf,IAAI,KAAK,MAAM,OAAO,GAAG,EAAE,IAAI,MAAM,IAAI,MAAM,OAAO,YAAY,MAAM,QAAQ,EAAE,GAAG;EACrF,IAAI;EACJ;CACJ;CAKA,IAAI,MAAM,eAAe;EACrB,IAAI,KAAK,MAAM,KAAK,GAAG,EAAE,GAAG,MAAM,IAAI,MAAM,KAAK,MAAM,aAAa,GAAG;EACvE,KAAK,MAAM,SAAS,MAAM,OAAO,QAAQ,MAAM,EAAE,aAAa,MAAM,GAChE,IAAI,QAAQ,MAAM,KAAK,MAAM,GAAG,GAAG;EAEvC,IAAI;EACJ;CACJ;CAEA,MAAM,SAAS,MAAM;CACrB,MAAM,aAAa,OAAO,QAAQ,MAAM,EAAE,aAAa,OAAO,CAAC,CAAC;CAChE,MAAM,YAAY,OAAO,QAAQ,MAAM,EAAE,aAAa,SAAS,CAAC,CAAC;CACjE,MAAM,aAAa,OAAO,QAAQ,MAAM,EAAE,aAAa,MAAM;CAI7D,IAAI,eAAe,KAAK,cAAc,GAClC,IAAI,KAAK,MAAM,MAAM,GAAG,EAAE,GAAG,MAAM,IAAI,MAAM,MAAM,SAAS,GAAG;MAC5D;EACH,MAAM,QAAkB,CAAC;EACzB,IAAI,aAAa,GAAG,MAAM,KAAK,GAAG,WAAW,QAAQ,aAAa,IAAI,MAAM,IAAI;EAChF,IAAI,YAAY,GAAG,MAAM,KAAK,GAAG,UAAU,UAAU,YAAY,IAAI,MAAM,IAAI;EAC/E,IAAI,KAAK,MAAM,OAAO,IAAI,EAAE,IAAI,MAAM,IAAI,MAAM,OAAO,MAAM,KAAK,IAAI,CAAC,GAAG;CAC9E;CACA,IAAI;CAGJ,KAAK,MAAM,SAAS,YAAY;EAC5B,MAAM,UAAU,MAAM,MAAM,MAAM,KAAK,MAAM,MAAM,KAAK,IAAI;EAC5D,IAAI,KAAK,MAAM,KAAK,GAAG,EAAE,GAAG,MAAM,KAAK,MAAM,OAAO,IAAI,SAAS;EACjE,IAAI;CACR;CAEA,KAAK,MAAM,SAAS,OAAO,QAAQ,MAAM,EAAE,aAAa,MAAM,GAAG;EAC7D,MAAM,eAAe,MAAM,aAAa,UAAU,MAAM,IAAI,GAAG,IAAI,MAAM,OAAO,GAAG;EACnF,MAAM,gBAAgB,eAAe,MAAM,QAAQ;EACnD,IAAI,KAAK,MAAM,KAAK,IAAI,EAAE,GAAG,aAAa,GAAG,MAAM,KAAK,aAAa,EAAE,GAAG,MAAM,KAAK,IAAI,OAAO,KAAK,IAAI,GAAG,KAAK,cAAc,MAAM,CAAC,CAAC,GAAG;EAE1I,IAAI,MAAM,OAAO;GACb,MAAM,UAAU,MAAM,SAAS,cAAc,MAAM,KAAK,MAAM,MAAM,MAAM;GAC1E,IAAI,KAAK,MAAM,KAAK,GAAG,EAAE,UAAU,MAAM,KAAK,MAAM,KAAK,IAAI,SAAS;EAC1E;EAEA,IAAI,MAAM,YAAY,MAAM,QACxB,IAAI,KAAK,MAAM,KAAK,GAAG,EAAE,aAAa,MAAM,MAAM,MAAM,QAAQ,EAAE,aAAa,MAAM,IAAI,MAAM,MAAM,GAAG;EAG5G,IAAI,KAAK,MAAM,KAAK,GAAG,EAAE,GAAG,MAAM,SAAS;EAC3C,IAAI,KAAK,MAAM,KAAK,GAAG,EAAE,QAAQ,MAAM,KAAK,MAAM,GAAG,GAAG;EACxD,IAAI,KAAK,MAAM,KAAK,MAAM,IAAI,OAAO,EAAE,CAAC,GAAG;EAC3C,IAAI;CACR;AACJ;AAEA,SAAS,eAAe,KAAsC;CAc1D,OAAO;EAZH,eAAe;EACf,gBAAgB;EAChB,eAAe;EACf,oBAAoB;EACpB,cAAc;EACd,cAAc;EACd,qBAAqB;EACrB,qBAAqB;EACrB,WAAW;EACX,mBAAmB;EACnB,mBAAmB;CAEhB,EAAO;AAClB;AAIA,eAAsB,UAAU,SAKN;CACtB,IAAI;CACJ,IAAI,MAAM,KAAK,6BAA6B,CAAC;CAC7C,MAAM,cAAc,MAAM,gBAAgB,QAAQ,eAAe;CACjE,IAAI,YAAY,WAAW,GAAG;EAC1B,SAAS,MAAM,IAAI,2BAA2B,CAAC;EAC/C,QAAQ,KAAK,CAAC;CAClB;CACA,IAAI,MAAM,KAAK,WAAW,YAAY,OAAO,eAAe,CAAC;CAC7D,IAAI;CAGJ,IAAI,MAAM,KAAK,8CAA8C,CAAC;CAC9D,MAAM,sBAAsB,MAAM,yBAAyB,aAAa,QAAQ,UAAU;CAG1F,IAAI;CACJ,IAAI,QAAQ,aAAa;EACrB,IAAI,MAAM,KAAK,sCAAsC,CAAC;EACtD,IAAI;GACA,mBAAmB,MAAM,2BAA2B,aAAa,QAAQ,WAAW;EACxF,SAAS,KAAc;GAOnB,MAAM,SAAS,gBAAgB,KAAK,QAAQ,WAAW;GACvD,IAAI,QAAQ,SAAS,MAAM;GAC3B,MAAM,SAAS,oBAAoB,GAAG,MAC9B,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;GACvD,mBAAmB;IACf,QAAQ;IACR,QAAQ,CAAC;IACT,SAAS,mBAAmB,cAAc,QAAQ,WAAW,EAAE,KAAK;IACpE,SAAS;GACb;EACJ;CACJ,OAAO;EAEH,mBAAmB;GAAE,QAAQ;GACrC,QAAQ,CAAC;GACT,SAAS;EAAuB;EACxB,IAAI,MAAM,OAAO,0DAA0D,CAAC;EAC5E,IAAI,MAAM,KAAK,mEAAmE,CAAC;CACvF;CAGA,IAAI,MAAM,KAAK,uCAAuC,CAAC;CACvD,MAAM,mBAAmB,MAAM,sBAAsB,aAAa,QAAQ,OAAO;CAEjF,MAAM,SAAS;EAAC;EAAqB;EAAkB;CAAgB;CACvE,MAAM,YAAY,OAAO,SAAS,MAAM,EAAE,MAAM;CAChD,MAAM,UAAU;EAIZ,QAAQ,OAAO,QAAQ,MAAM,EAAE,UAAU,CAAC,EAAE,WAAW,CAAC,EAAE,aAAa,CAAC,CAAC;EACzE,SAAS,OAAO,QAAQ,MAAM,EAAE,OAAO,CAAC,CAAC;EACzC,eAAe,OAAO,QAAQ,MAAM,EAAE,iBAAiB,CAAC,EAAE,OAAO,CAAC,CAAC;EACnE,UAAU,UAAU,QAAQ,MAAM,EAAE,aAAa,SAAS,CAAC,CAAC;EAC5D,QAAQ,UAAU,QAAQ,MAAM,EAAE,aAAa,OAAO,CAAC,CAAC;CAC5D;CAEA,MAAM,SAAuB;EAAE;EACnC;EACA;EACA;CAAQ;CACJ,aAAa,MAAM;CAEnB,OAAO;AACX"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ensure-tables-CYMtuuOd.js","names":[],"sources":["../src/auth/schema-version.ts","../src/auth/ensure-tables.ts"],"sourcesContent":["import { sql } from \"drizzle-orm\";\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type { CollectionConfig } from \"@rebasepro/types\";\n\n/**\n * The auth schema version this runtime expects to find in the database.\n *\n * Bump this whenever a migration in `ensureAuthTablesExist` makes the schema\n * unreadable by the runtime that came before it — that is, whenever a *previous*\n * version's auth queries would break against the migrated shape. Additive\n * changes (a new nullable column nobody older references) do not need a bump.\n *\n * History. Note that 1 is a label for an era, not a value any database holds:\n * stamping did not exist then, so an era-1 database reads as unstamped\n * (`null`), and 2 is the first version ever actually written. The numbering\n * starts at 2 only because two schema eras already existed when it was\n * introduced; it could just as well have started at 1. It is not worth\n * renumbering now — deployed databases already carry 2, and lowering the\n * constant would make them look newer than the runtime and refuse the boot.\n *\n * 1 — Device-session refresh tokens. A row *was* a session, identified by\n * `unique_device_session UNIQUE (uid, user_agent, ip_address)`, and\n * `createToken` upserted with `ON CONFLICT (uid, user_agent, ip_address)`.\n * 2 — Session-scoped, rotation-safe refresh tokens: `session_id`, `revoked`,\n * `rotated_at`, `session_started_at`, and `unique_device_session`\n * dropped because two live tokens of one session share all three columns.\n *\n * The 1 → 2 migration is why this file exists. Dropping the constraint is\n * one-way: a version-1 runtime deployed afterwards boots perfectly, logs\n * `✅ Auth tables ready` (its `CREATE TABLE IF NOT EXISTS` never revisits the\n * existing table, so it cannot re-add the constraint), answers `/health` with\n * 200 — and then fails every single login and refresh with SQLSTATE 42P10,\n * because its `ON CONFLICT` names a constraint that no longer exists. A silent\n * total auth outage behind a green health check. The stamp below turns that\n * into a boot refusal.\n */\nexport const AUTH_SCHEMA_VERSION = 2;\n\n/** Key under which the version is stored in the auth schema's meta table. */\nconst VERSION_KEY = \"auth_schema_version\";\n\n/**\n * Columns `refresh_tokens` must have for the current runtime's auth write path\n * to work. Checked by the health probe so a database that drifted *below* this\n * runtime is reported as unhealthy rather than discovered one failed login at a\n * time. Kept in step with the migration in `ensureAuthTablesExist`.\n */\nconst REQUIRED_REFRESH_TOKEN_COLUMNS = [\"session_id\", \"revoked\", \"rotated_at\", \"session_started_at\"];\n\n/**\n * A constraint whose *presence* means the database is still at version 1, in a\n * shape this runtime's rotation logic cannot write to: it makes two live tokens\n * of one rotating session collide.\n */\nconst RETIRED_REFRESH_TOKEN_CONSTRAINT = \"unique_device_session\";\n\n/**\n * Thrown when the database was migrated by a runtime newer than this one.\n *\n * Distinct class rather than a bare `Error` because `ensureAuthTablesExist`\n * wraps its migrations in a catch that deliberately swallows failures and\n * continues — every other problem there is better survived than crashed on.\n * This one is not, so the catch rethrows on this type specifically.\n */\nexport class AuthSchemaVersionError extends Error {\n readonly databaseVersion: number;\n readonly runtimeVersion: number;\n\n constructor(databaseVersion: number, runtimeVersion: number) {\n super(\n `Auth schema version mismatch: the database is at version ${databaseVersion}, ` +\n `but this runtime understands version ${runtimeVersion}.\\n\\n` +\n \"A newer version of the framework has already migrated this database. Running this \" +\n \"older runtime against it would boot cleanly and then fail every login and token \" +\n \"refresh, because the auth schema it expects no longer exists.\\n\\n\" +\n \"Refusing to start. Deploy a framework version at or above the one that migrated \" +\n \"this database, or restore the database from a backup taken before the upgrade.\"\n );\n this.name = \"AuthSchemaVersionError\";\n this.databaseVersion = databaseVersion;\n this.runtimeVersion = runtimeVersion;\n }\n}\n\n/**\n * The schema the auth tables live in, derived exactly as `ensureAuthTablesExist`\n * derives it. Shared so the two cannot drift: a stamp written to one schema and\n * read from another would read as \"never stamped\" forever.\n */\nexport function resolveAuthSchema(collection?: CollectionConfig): string {\n if (!collection) return \"rebase\";\n const usersSchema = (\"schema\" in collection && typeof collection.schema === \"string\")\n ? collection.schema\n : \"public\";\n return usersSchema === \"public\" ? \"rebase\" : usersSchema;\n}\n\n/**\n * Read the stamped version, or `null` when the database has never been stamped.\n *\n * `null` is not an error and must not be treated as one: every database\n * provisioned before this file existed is unstamped, and so is every fresh one.\n * Uses `to_regclass` rather than selecting straight from the table so a missing\n * schema or table is a `null` rather than a thrown 42P01.\n */\nexport async function readAuthSchemaVersion(\n db: NodePgDatabase,\n authSchema: string\n): Promise<number | null> {\n const qualified = `\"${authSchema}\".\"schema_meta\"`;\n const exists = await db.execute(sql`SELECT to_regclass(${qualified}) IS NOT NULL AS present`);\n if (!(exists.rows[0] as { present: boolean } | undefined)?.present) return null;\n\n const result = await db.execute(sql`\n SELECT value FROM ${sql.raw(qualified)} WHERE key = ${VERSION_KEY}\n `);\n const raw = (result.rows[0] as { value: string } | undefined)?.value;\n if (raw === undefined) return null;\n\n const parsed = Number.parseInt(raw, 10);\n // A meta row we cannot parse is treated as unstamped rather than as version\n // 0: refusing to boot over a garbled string would be a worse failure than\n // the drift it is meant to catch.\n return Number.isFinite(parsed) ? parsed : null;\n}\n\n/**\n * Refuse to run against a database a newer runtime has already migrated.\n *\n * Deliberately one-directional. A database *older* than this runtime is the\n * normal upgrade path — the migrations in `ensureAuthTablesExist` are about to\n * bring it forward, so it is not an error. Only the reverse is unrecoverable.\n */\nexport async function assertAuthSchemaCompatible(\n db: NodePgDatabase,\n authSchema: string\n): Promise<void> {\n const databaseVersion = await readAuthSchemaVersion(db, authSchema);\n if (databaseVersion !== null && databaseVersion > AUTH_SCHEMA_VERSION) {\n throw new AuthSchemaVersionError(databaseVersion, AUTH_SCHEMA_VERSION);\n }\n}\n\n/**\n * Record that this runtime's migrations have been applied.\n *\n * Called at the end of `ensureAuthTablesExist`, so a boot that failed partway\n * through leaves the older stamp in place and the next boot migrates again.\n */\nexport async function stampAuthSchemaVersion(\n db: NodePgDatabase,\n authSchema: string\n): Promise<void> {\n const qualified = `\"${authSchema}\".\"schema_meta\"`;\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS ${sql.raw(qualified)} (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL\n )\n `);\n await db.execute(sql`\n INSERT INTO ${sql.raw(qualified)} (key, value)\n VALUES (${VERSION_KEY}, ${String(AUTH_SCHEMA_VERSION)})\n ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()\n `);\n}\n\n/** What {@link probeAuthSchema} found. */\nexport interface AuthSchemaProbeResult {\n /** False when this runtime cannot be trusted to serve auth against this database. */\n healthy: boolean;\n /** The stamped version, or `null` on a database that predates stamping. */\n databaseVersion: number | null;\n /** {@link AUTH_SCHEMA_VERSION}. */\n runtimeVersion: number;\n /** Human-readable descriptions of each mismatch found. Empty when healthy. */\n problems: string[];\n}\n\n/**\n * Check that the auth schema is one this runtime can actually write to.\n *\n * Two independent checks, because either alone has a blind spot:\n *\n * - The **stamp** catches a runtime older than the database. It is the precise\n * signal, but it is blind on every database provisioned before stamping\n * existed — which today is all of them.\n * - The **structure** catches a database older than the runtime, and works on\n * unstamped databases. It is what makes this useful immediately rather than\n * one upgrade cycle from now.\n *\n * Never throws: a probe that fails to run reports unhealthy with the reason, so\n * a broken check surfaces as a degraded health response rather than a 500 from\n * the health endpoint itself.\n */\nexport async function probeAuthSchema(\n db: NodePgDatabase,\n authSchema: string\n): Promise<AuthSchemaProbeResult> {\n const problems: string[] = [];\n let databaseVersion: number | null = null;\n\n try {\n databaseVersion = await readAuthSchemaVersion(db, authSchema);\n if (databaseVersion !== null && databaseVersion > AUTH_SCHEMA_VERSION) {\n problems.push(\n `database is at auth schema version ${databaseVersion}, this runtime understands ` +\n `${AUTH_SCHEMA_VERSION} — it was migrated by a newer framework version`\n );\n }\n\n const refreshTokens = `\"${authSchema}\".\"refresh_tokens\"`;\n const present = await db.execute(sql`SELECT to_regclass(${refreshTokens}) IS NOT NULL AS present`);\n if (!(present.rows[0] as { present: boolean } | undefined)?.present) {\n // Not a problem in itself: auth may simply not be configured on this\n // deployment, and the table is created on demand at boot when it is.\n return { healthy: problems.length === 0, databaseVersion, runtimeVersion: AUTH_SCHEMA_VERSION, problems };\n }\n\n const columns = await db.execute(sql`\n SELECT column_name FROM information_schema.columns\n WHERE table_schema = ${authSchema} AND table_name = 'refresh_tokens'\n `);\n const found = new Set((columns.rows as { column_name: string }[]).map(row => row.column_name));\n const missing = REQUIRED_REFRESH_TOKEN_COLUMNS.filter(column => !found.has(column));\n if (missing.length > 0) {\n problems.push(\n `refresh_tokens is missing ${missing.join(\", \")} — the auth migrations have not been ` +\n \"applied to this database, so token rotation will fail\"\n );\n }\n\n const retired = await db.execute(sql`\n SELECT 1 FROM pg_constraint c\n JOIN pg_class t ON t.oid = c.conrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n WHERE n.nspname = ${authSchema}\n AND t.relname = 'refresh_tokens'\n AND c.conname = ${RETIRED_REFRESH_TOKEN_CONSTRAINT}\n `);\n if (retired.rows.length > 0) {\n problems.push(\n `refresh_tokens still carries ${RETIRED_REFRESH_TOKEN_CONSTRAINT} — concurrent token ` +\n \"rotation for one session will fail on it\"\n );\n }\n } catch (error: unknown) {\n problems.push(\n `auth schema probe failed: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n\n return {\n healthy: problems.length === 0,\n databaseVersion,\n runtimeVersion: AUTH_SCHEMA_VERSION,\n problems\n };\n}\n","import { sql } from \"drizzle-orm\";\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { logger } from \"@rebasepro/server\";\nimport { revokeInternalTableAccess } from \"@rebasepro/common\";\nimport type { CollectionConfig } from \"@rebasepro/types\";\nimport { AUTH_USERS_COLUMNS, authUsersColumnSql } from \"../schema/auth-users-columns\";\nimport { RLS_BOOTSTRAP_STATEMENTS } from \"../schema/rls-bootstrap-sql\";\nimport {\n AuthSchemaVersionError,\n assertAuthSchemaCompatible,\n resolveAuthSchema,\n stampAuthSchemaVersion\n} from \"./schema-version\";\n\n\n/**\n * Auto-create auth tables if they don't exist.\n *\n * @param db — Drizzle database instance\n * @param collection — The collection that represents auth users.\n * When omitted, a default `rebase.users` table is created.\n */\nexport async function ensureAuthTablesExist(db: NodePgDatabase, collection?: CollectionConfig): Promise<void> {\n logger.debug(\"🔍 Checking auth tables...\");\n\n // Before anything else, and deliberately outside the catch below: refuse to\n // run against a database that a newer framework version has already\n // migrated. Everything past this point is best-effort by design, which is\n // exactly the wrong posture for an incompatibility that would otherwise\n // surface as a fully booted server failing every login.\n await assertAuthSchemaCompatible(db, resolveAuthSchema(collection));\n\n try {\n // Resolve dynamic user table name and ID type from the collection\n let usersTableName = '\"rebase\".\"users\"';\n let userIdType = \"TEXT\";\n let usersSchema = \"rebase\";\n let resolvedTable = \"users\";\n if (collection) {\n resolvedTable = (\"table\" in collection && typeof collection.table === \"string\")\n ? collection.table\n : collection.slug;\n usersSchema = (\"schema\" in collection && typeof collection.schema === \"string\")\n ? collection.schema\n : \"public\";\n usersTableName = usersSchema === \"public\"\n ? `\"${resolvedTable}\"`\n : `\"${usersSchema}\".\"${resolvedTable}\"`;\n\n // Derive ID column type from collection properties.\n //\n // `\"increment\"`, not `\"autoincrement\"`. The latter was tested for\n // here and exists nowhere in the type system — the union is\n // `boolean | \"manual\" | \"increment\" | string` — so the INTEGER branch\n // was unreachable and an integer-keyed auth collection fell through\n // to TEXT. Introspection below hid it whenever the table already\n // existed; on a database where it did not, this created\n // `id TEXT DEFAULT gen_random_uuid()::text` for a collection that\n // declares a number, and every `uid` foreign key was typed to match\n // the wrong thing.\n const idProp = collection.properties?.id;\n if (idProp) {\n const isId = (\"isId\" in idProp) ? (idProp as unknown as Record<string, unknown>).isId : undefined;\n if (isId === \"uuid\") {\n userIdType = \"UUID\";\n } else if (isId === \"increment\") {\n userIdType = \"INTEGER\";\n }\n // Otherwise keep TEXT as default\n }\n }\n\n // Introspect the database to find the actual type of usersTableName's ID column if the table exists\n try {\n const result = await db.execute(sql`\n SELECT data_type \n FROM information_schema.columns \n WHERE table_schema = ${usersSchema} \n AND table_name = ${resolvedTable} \n AND column_name = 'id'\n `);\n if (result && result.rows && result.rows.length > 0) {\n const dbType = String((result.rows[0] as { data_type: string }).data_type).toUpperCase();\n if (dbType === \"UUID\") {\n userIdType = \"UUID\";\n } else if (dbType === \"INTEGER\" || dbType === \"SMALLINT\" || dbType === \"BIGINT\") {\n userIdType = \"INTEGER\";\n } else {\n userIdType = \"TEXT\";\n }\n logger.debug(`✨ Detected ${usersTableName}.id type from database: ${dbType}. Using user_id type: ${userIdType}`);\n }\n } catch (err) {\n // Ignore introspection errors, fallback to derived/default type\n logger.warn(`⚠️ Failed to introspect ${usersTableName}.id type from database, falling back to config type: ${userIdType}`, { error: err });\n }\n\n\n // ── Create schemas (idempotent) ──────────────────────────────────\n if (usersSchema !== \"public\") {\n await db.execute(sql`CREATE SCHEMA IF NOT EXISTS ${sql.raw(usersSchema)}`);\n }\n await db.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);\n\n const authSchema = usersSchema === \"public\" ? \"rebase\" : usersSchema;\n const userIdentitiesTable = `\"${authSchema}\".\"user_identities\"`;\n const refreshTokensTableName = `\"${authSchema}\".\"refresh_tokens\"`;\n const passwordResetTokensTableName = `\"${authSchema}\".\"password_reset_tokens\"`;\n const appConfigTableName = `\"${authSchema}\".\"app_config\"`;\n\n // ── Create users table (idempotent) ─────────────────────────────\n // The users table MUST be created before any dependent auth tables\n // (user_identities, refresh_tokens, etc.) because they all hold\n // foreign keys referencing users(id). When a developer runs\n // `pnpm dev` for the first time without `db:migrate`, this ensures\n // the server can self-bootstrap.\n const idDefault = userIdType === \"UUID\"\n ? \"DEFAULT gen_random_uuid()\"\n : userIdType === \"INTEGER\"\n ? \"GENERATED ALWAYS AS IDENTITY\"\n : \"DEFAULT gen_random_uuid()::text\";\n\n // Identifiers for the constraint and indexes reconciled further down.\n // Derived from the resolved table name so two auth tables in different\n // schemas cannot collide, and truncated to Postgres's 63-byte identifier\n // limit here rather than letting the server truncate silently — the\n // `IF NOT EXISTS` guards below have to compare against the same name\n // Postgres actually stored, or they re-run forever.\n const authIdentifier = (suffix: string) => `${resolvedTable}_${suffix}`.slice(0, 63);\n const emailLengthConstraint = `\"${authIdentifier(\"email_length_check\")}\"`;\n const emailLowerUniqueIndex = authIdentifier(\"email_lower_key\");\n const verificationTokenIndex = authIdentifier(\"email_verification_token_idx\");\n\n // Every string column here is TEXT, deliberately. In Postgres VARCHAR(n)\n // and TEXT are the same type with the same storage and the same\n // performance; the only difference is a length check, and none of these\n // columns wants one. The widths this table used to carry were inherited\n // MySQL habit (255) and they were all wrong in the same direction —\n // `password_hash VARCHAR(255)` against a 193-char scrypt string left 62\n // characters of headroom in front of a KEY_LENGTH constant living in\n // another package, and `photo_url VARCHAR(500)` rejected the `data:` URIs\n // and long signed URLs that OAuth providers hand back. A limit worth\n // having is a CHECK — alterable without a table rewrite, unlike a type\n // modifier — which is why `email` has one and nothing else does.\n //\n // The column list comes from AUTH_USERS_COLUMNS rather than being spelled\n // out here, because this is not the only place that creates this table:\n // `db push` and the boot-time collection ensure do too, and when the\n // three lists were maintained separately they disagreed and boot order\n // silently decided which shape the database got. The `email` CHECK is\n // appended rather than listed there — it is a named constraint the\n // migration below has to be able to add separately, `NOT VALID`, to a\n // table that already holds rows.\n const usersColumnDdl = AUTH_USERS_COLUMNS\n .map((spec) => spec.column === \"email\"\n ? `${spec.column} ${authUsersColumnSql(spec)} CONSTRAINT ${emailLengthConstraint} CHECK (length(email) <= 320)`\n : `${spec.column} ${authUsersColumnSql(spec)}`)\n .join(\",\\n \");\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS ${sql.raw(usersTableName)} (\n id ${sql.raw(userIdType)} PRIMARY KEY ${sql.raw(idDefault)},\n ${sql.raw(usersColumnDdl)}\n )\n `);\n\n // ── Migration: auth FK column user_id → uid, phase 1 (expand) ───────\n // Must run BEFORE the dependent tables below: CREATE TABLE IF NOT\n // EXISTS never revisits an existing table, so a database provisioned\n // before this migration still lacks `uid` — and the\n // CREATE INDEX ... (uid) statements that follow would fail on it.\n //\n // Deliberately NOT a plain RENAME. Both Cloud Run and Kubernetes roll\n // deploys, so old and new pods serve the same database at the same time,\n // and a rollback puts old code back in front of a migrated database. A\n // rename breaks every auth query on whichever side is out of step.\n // Instead: add `uid`, backfill it, drop the NOT NULL on `user_id`, and\n // keep the two in sync with a trigger, so a backend of either era can\n // read and write. `scripts/drop-legacy-auth-user-id.sql` removes the\n // column once no old backend remains (phase 2, contract).\n //\n // Idempotent throughout: every step is guarded on catalogue state.\n const legacyFkTables = [\n \"user_identities\",\n \"refresh_tokens\",\n \"password_reset_tokens\",\n \"magic_link_tokens\",\n \"mfa_factors\",\n \"recovery_codes\"\n ];\n\n // Only on a database that actually carries the legacy column. This whole\n // block is a 0.x compatibility shim, and it used to run unconditionally —\n // so every brand-new database was provisioned with a trigger function\n // written to reconcile a column it can never have, permanently, as part\n // of its first boot. A fresh install should not ship someone else's\n // migration history.\n // The table list is inlined rather than bound: drizzle expands a JS\n // array into a parameter TUPLE — `ANY(($2, $3, …))` — which Postgres\n // rejects, and the thrown error is swallowed by the catch around this\n // whole function, so auth would silently stop provisioning. These are\n // module-level constants, not input.\n const legacyFkTableList = legacyFkTables.map(t => `'${t}'`).join(\", \");\n const legacyUserIdPresent = await db.execute(sql`\n SELECT 1\n FROM information_schema.columns\n WHERE table_schema = ${authSchema}\n AND table_name IN (${sql.raw(legacyFkTableList)})\n AND column_name = 'user_id'\n LIMIT 1\n `);\n\n if (legacyUserIdPresent.rows.length > 0) {\n await db.execute(sql`\n CREATE OR REPLACE FUNCTION ${sql.raw(`\"${authSchema}\"`)}.sync_uid_user_id() RETURNS trigger AS $$\n BEGIN\n IF NEW.uid IS NULL AND NEW.user_id IS NOT NULL THEN\n NEW.uid := NEW.user_id;\n ELSIF NEW.user_id IS NULL AND NEW.uid IS NOT NULL THEN\n NEW.user_id := NEW.uid;\n END IF;\n RETURN NEW;\n END $$ LANGUAGE plpgsql\n `);\n }\n\n for (const authTable of legacyUserIdPresent.rows.length > 0 ? legacyFkTables : []) {\n const qualified = `\"${authSchema}\".\"${authTable}\"`;\n await db.execute(sql`\n DO $$\n DECLARE\n has_legacy boolean;\n has_uid boolean;\n BEGIN\n SELECT\n bool_or(column_name = 'user_id'),\n bool_or(column_name = 'uid')\n INTO has_legacy, has_uid\n FROM information_schema.columns\n WHERE table_schema = ${sql.raw(`'${authSchema}'`)}\n AND table_name = ${sql.raw(`'${authTable}'`)};\n\n -- Table absent, or already uid-only (a fresh install, or\n -- phase 2 already run): nothing to do.\n IF has_legacy IS NOT TRUE THEN\n RETURN;\n END IF;\n\n IF has_uid IS NOT TRUE THEN\n EXECUTE ${sql.raw(`'ALTER TABLE ${qualified} ADD COLUMN uid ${userIdType} REFERENCES ${usersTableName}(id) ON DELETE CASCADE'`)};\n EXECUTE ${sql.raw(`'UPDATE ${qualified} SET uid = user_id WHERE uid IS NULL'`)};\n EXECUTE ${sql.raw(`'CREATE INDEX IF NOT EXISTS idx_${authTable}_uid ON ${qualified}(uid)'`)};\n END IF;\n\n -- New code inserts uid and never user_id, so the legacy\n -- column can no longer be NOT NULL. The trigger below\n -- backfills it, but the constraint is checked first.\n EXECUTE ${sql.raw(`'ALTER TABLE ${qualified} ALTER COLUMN user_id DROP NOT NULL'`)};\n\n EXECUTE ${sql.raw(`'DROP TRIGGER IF EXISTS sync_uid_user_id ON ${qualified}'`)};\n EXECUTE ${sql.raw(`'CREATE TRIGGER sync_uid_user_id BEFORE INSERT OR UPDATE ON ${qualified} FOR EACH ROW EXECUTE FUNCTION \"${authSchema}\".sync_uid_user_id()'`)};\n END $$\n `);\n }\n\n // ── Create dependent auth tables (idempotent) ───────────────────\n\n // Create user_identities table\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS ${sql.raw(userIdentitiesTable)} (\n id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,\n uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,\n provider TEXT NOT NULL,\n provider_id TEXT NOT NULL,\n profile_data JSONB,\n created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),\n updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),\n UNIQUE(provider, provider_id)\n )\n `);\n\n // Create indexes on user_identities\n await db.execute(sql`\n CREATE INDEX IF NOT EXISTS idx_user_identities_user \n ON ${sql.raw(userIdentitiesTable)}(uid)\n `);\n\n\n // Create refresh tokens table. One row per TOKEN, grouped into a\n // sign-in by session_id — deliberately without a uniqueness rule on\n // (uid, user_agent, ip_address); see the schema module for why that\n // constraint had to go.\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS ${sql.raw(refreshTokensTableName)} (\n id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,\n uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,\n session_id TEXT NOT NULL DEFAULT gen_random_uuid()::text,\n token_hash TEXT NOT NULL UNIQUE,\n expires_at TIMESTAMP WITH TIME ZONE NOT NULL,\n revoked BOOLEAN DEFAULT FALSE NOT NULL,\n rotated_at TIMESTAMP WITH TIME ZONE,\n session_started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,\n user_agent TEXT,\n ip_address TEXT,\n created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n )\n `);\n\n // Create index on token_hash for faster lookups\n await db.execute(sql`\n CREATE INDEX IF NOT EXISTS idx_refresh_tokens_hash \n ON ${sql.raw(refreshTokensTableName)}(token_hash)\n `);\n\n // Create index on uid for cleanup operations\n await db.execute(sql`\n CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user \n ON ${sql.raw(refreshTokensTableName)}(uid)\n `);\n\n // Create password reset tokens table\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS ${sql.raw(passwordResetTokensTableName)} (\n id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,\n uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,\n token_hash TEXT NOT NULL UNIQUE,\n expires_at TIMESTAMP WITH TIME ZONE NOT NULL,\n used_at TIMESTAMP WITH TIME ZONE,\n created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n )\n `);\n\n // Create index on token_hash for password reset lookups\n await db.execute(sql`\n CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_hash \n ON ${sql.raw(passwordResetTokensTableName)}(token_hash)\n `);\n\n // Create index on uid for password reset cleanup\n await db.execute(sql`\n CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user \n ON ${sql.raw(passwordResetTokensTableName)}(uid)\n `);\n\n // Create magic link tokens table\n const magicLinkTokensTableName = `\"${authSchema}\".\"magic_link_tokens\"`;\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS ${sql.raw(magicLinkTokensTableName)} (\n id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,\n uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,\n token_hash TEXT NOT NULL UNIQUE,\n expires_at TIMESTAMP WITH TIME ZONE NOT NULL,\n used_at TIMESTAMP WITH TIME ZONE,\n created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n )\n `);\n\n // Create index on token_hash for magic link lookups\n await db.execute(sql`\n CREATE INDEX IF NOT EXISTS idx_magic_link_tokens_hash \n ON ${sql.raw(magicLinkTokensTableName)}(token_hash)\n `);\n\n // Create index on uid for magic link cleanup\n await db.execute(sql`\n CREATE INDEX IF NOT EXISTS idx_magic_link_tokens_user \n ON ${sql.raw(magicLinkTokensTableName)}(uid)\n `);\n\n // Create app config table\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS ${sql.raw(appConfigTableName)} (\n key TEXT PRIMARY KEY,\n value JSONB NOT NULL,\n updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n )\n `);\n\n // The RLS helper functions every generated policy calls. They live in\n // `rebase`, alongside the tables above — Rebase creates exactly one\n // schema in a user's database. Advisory-locked so concurrent HMR\n // reloads cannot race on `CREATE OR REPLACE`.\n //\n // The same statements the migration preamble carries, from the same\n // constant — these definitions being identical across the boot path and\n // the migration stream is the whole point of having them in one place.\n // One call per statement: this handle speaks the extended query\n // protocol, which rejects multi-command strings.\n await db.transaction(async (tx) => {\n await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext('rebase_auth_functions_init'))`);\n for (const statement of RLS_BOOTSTRAP_STATEMENTS) {\n await tx.execute(sql.raw(statement));\n }\n });\n\n // Seed default roles if none exist\n // (no-op: roles are now stored inline on the users table)\n\n // ── Migration: reconcile the full users column set (safe for existing tables) ──\n // CREATE TABLE IF NOT EXISTS never revisits an existing table, so a\n // database provisioned by an older framework era is missing every\n // column added since. Each column the auth services read or write must\n // be back-filled here, or upgraded deployments break on the first\n // statement that references it.\n //\n // `email` is skipped: it has existed since the first era, so it is never\n // the missing one, and `ADD COLUMN … NOT NULL` with no default fails on\n // a table with rows.\n for (const spec of AUTH_USERS_COLUMNS) {\n if (spec.column === \"email\") continue;\n await db.execute(sql`\n ALTER TABLE ${sql.raw(usersTableName)}\n ADD COLUMN IF NOT EXISTS ${sql.raw(`${spec.column} ${authUsersColumnSql(spec)}`)}\n `);\n }\n\n // Which of the columns below the table actually has. An adopted table —\n // one this framework did not create, which the column-name resolution in\n // `services.ts` exists to support — may be missing any of them, and every\n // statement past this point has to tolerate that rather than abort the\n // whole migration block.\n const usersColumns = await db.execute(sql`\n SELECT column_name, data_type, is_nullable, column_default\n FROM information_schema.columns\n WHERE table_schema = ${usersSchema} AND table_name = ${resolvedTable}\n `);\n type UsersColumnRow = {\n column_name: string;\n data_type: string;\n is_nullable: \"YES\" | \"NO\";\n column_default: string | null;\n };\n const usersColumnRows = usersColumns.rows as UsersColumnRow[];\n const usersColumnTypes = new Map(usersColumnRows.map(row => [row.column_name, row.data_type]));\n const usersColumnState = new Map(usersColumnRows.map(row => [row.column_name, row]));\n\n // ── Migration: restore defaults and NOT NULL that another creator dropped ──\n // `ADD COLUMN IF NOT EXISTS` above only creates what is MISSING. A column\n // that exists with the wrong shape stays wrong forever — and until\n // AUTH_USERS_COLUMNS became the single source, that was the normal\n // outcome rather than an edge case: whichever of `db push`, boot-ensure\n // and this function reached the table first decided its constraints, so\n // a managed deploy ended up with a nullable `email`, a `roles` with no\n // `'{}'` default, and an `email_verified` that could be NULL.\n //\n // Ordered DEFAULT → back-fill → SET NOT NULL, because SET NOT NULL is\n // checked against existing rows: without the back-fill it throws on the\n // very databases that need it. `email` can carry no default, so a NULL\n // there is not repairable automatically — say so and leave the column\n // alone rather than inventing an address.\n for (const spec of AUTH_USERS_COLUMNS) {\n const state = usersColumnState.get(spec.column);\n if (!state) continue;\n\n if (spec.default !== undefined && state.column_default === null) {\n await db.execute(sql`\n ALTER TABLE ${sql.raw(usersTableName)}\n ALTER COLUMN ${sql.raw(`\"${spec.column}\"`)} SET DEFAULT ${sql.raw(spec.default)}\n `);\n logger.info(`🔧 Restored the default on ${usersTableName}.${spec.column}`);\n }\n\n if (!spec.notNull || state.is_nullable !== \"YES\") continue;\n\n if (spec.default !== undefined) {\n await db.execute(sql`\n UPDATE ${sql.raw(usersTableName)}\n SET ${sql.raw(`\"${spec.column}\"`)} = ${sql.raw(spec.default)}\n WHERE ${sql.raw(`\"${spec.column}\"`)} IS NULL\n `);\n }\n try {\n await db.execute(sql`\n ALTER TABLE ${sql.raw(usersTableName)}\n ALTER COLUMN ${sql.raw(`\"${spec.column}\"`)} SET NOT NULL\n `);\n logger.info(`🔧 Restored NOT NULL on ${usersTableName}.${spec.column}`);\n } catch (err) {\n logger.warn(\n `⚠️ ${usersTableName}.${spec.column} should be NOT NULL but still holds NULLs, so the ` +\n \"constraint was not applied. Fill or remove those rows and restart: \" +\n (err instanceof Error ? err.message : String(err))\n );\n }\n }\n\n // ── Migration: VARCHAR(n) → TEXT on the users string columns ────────\n // Tables created before the widths came off still carry them. Postgres\n // treats varchar(n) → text as binary-coercible with no stricter\n // constraint, so this is a catalogue-only change: no table rewrite, no\n // index rebuild, just a brief ACCESS EXCLUSIVE lock. Guarded on the\n // current type so it runs once and is a pure catalogue read thereafter.\n for (const column of [\"email\", \"display_name\", \"photo_url\", \"password_hash\", \"email_verification_token\"]) {\n if (usersColumnTypes.get(column) !== \"character varying\") continue;\n await db.execute(sql`\n ALTER TABLE ${sql.raw(usersTableName)}\n ALTER COLUMN ${sql.raw(`\"${column}\"`)} TYPE TEXT\n `);\n logger.info(`🔧 Widened ${usersTableName}.${column} from VARCHAR(n) to TEXT`);\n }\n\n // ── Migration: case-insensitive email identity ──────────────────────\n // `getUserByEmail` has always searched `email.toLowerCase()` while the\n // write path stored whatever it was handed, leaving normalisation to a\n // convention every caller had to remember. A row that reached the table\n // with mixed case is then invisible to every lookup — the account exists,\n // login reports no such user, and the plain UNIQUE on `email` does not\n // stop a second row differing only in case, because it compares bytes.\n //\n // Fixed on both sides: `mapPayload` now folds on write, and this index\n // makes the database agree. A unique index on lower(email) is strictly\n // stronger than the byte-exact UNIQUE that older tables carry, so the\n // old constraint is left alone — it can no longer fire on anything the\n // new one would allow.\n //\n // Deliberately no AUTH_SCHEMA_VERSION bump: a runtime that predates this\n // migration keeps working against the migrated table (all of its own\n // write paths already lower-cased), which is exactly the additive case\n // the version stamp is documented not to cover.\n if (usersColumnTypes.has(\"email\")) {\n const indexPresent = await db.execute(sql`\n SELECT 1 FROM pg_class c\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE n.nspname = ${usersSchema} AND c.relname = ${emailLowerUniqueIndex} AND c.relkind = 'i'\n `);\n if (indexPresent.rows.length === 0) {\n // Case-collisions already in the table would make the unique\n // index impossible to build. Report them and leave the table\n // alone: the next boot retries, so fixing the rows is all the\n // operator has to do. Failing loudly beats folding the emails\n // and letting CREATE INDEX pick which account survives.\n const collisions = await db.execute(sql`\n SELECT lower(email) AS normalized, count(*)::int AS occurrences\n FROM ${sql.raw(usersTableName)}\n WHERE email IS NOT NULL\n GROUP BY lower(email)\n HAVING count(*) > 1\n LIMIT 10\n `);\n if (collisions.rows.length > 0) {\n const sample = (collisions.rows as { normalized: string; occurrences: number }[])\n .map(row => `${row.normalized} (×${row.occurrences})`)\n .join(\", \");\n logger.error(\n `❌ Cannot enforce case-insensitive email uniqueness on ${usersTableName}: ` +\n `these addresses already exist more than once, differing only in case — ${sample}. ` +\n \"Merge or delete the duplicates and restart; until then two accounts can share \" +\n \"one address and only the lower-cased one is reachable by login.\"\n );\n } else {\n const folded = await db.execute(sql`\n UPDATE ${sql.raw(usersTableName)}\n SET email = lower(email)\n WHERE email IS NOT NULL AND email <> lower(email)\n `);\n if (folded.rowCount) {\n logger.info(`🔧 Lower-cased ${folded.rowCount} email address(es) in ${usersTableName}`);\n }\n await db.execute(sql`\n CREATE UNIQUE INDEX IF NOT EXISTS ${sql.raw(`\"${emailLowerUniqueIndex}\"`)}\n ON ${sql.raw(usersTableName)} (lower(email))\n `);\n logger.info(`✅ Email uniqueness on ${usersTableName} is now case-insensitive`);\n }\n }\n }\n\n // ── Migration: bound the email column's length ──────────────────────\n // The only length limit on this table worth keeping. 320 is the RFC 5321\n // maximum (64-char local part + @ + 255-char domain), and it matters here\n // beyond tidiness: `email` carries a btree index, and a sufficiently long\n // value fails index insertion with an error that says nothing about\n // email. NOT VALID so an adopted table with a long row still migrates —\n // it binds all new writes, which is the part that matters.\n if (usersColumnTypes.has(\"email\")) {\n const checkPresent = await db.execute(sql`\n SELECT 1 FROM pg_constraint c\n JOIN pg_class t ON t.oid = c.conrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n WHERE n.nspname = ${usersSchema}\n AND t.relname = ${resolvedTable}\n AND c.conname = ${authIdentifier(\"email_length_check\")}\n `);\n if (checkPresent.rows.length === 0) {\n await db.execute(sql`\n ALTER TABLE ${sql.raw(usersTableName)}\n ADD CONSTRAINT ${sql.raw(emailLengthConstraint)} CHECK (length(email) <= 320) NOT VALID\n `);\n }\n }\n\n // ── Index: email verification token lookups ─────────────────────────\n // `getUserByVerificationToken` filters on this column, which had no\n // index — every click of a verification link was a sequential scan of\n // the whole users table. Partial, because the column is NULL for every\n // user who is not mid-verification, which is nearly all of them.\n if (usersColumnTypes.has(\"email_verification_token\")) {\n await db.execute(sql`\n CREATE INDEX IF NOT EXISTS ${sql.raw(`\"${verificationTokenIndex}\"`)}\n ON ${sql.raw(usersTableName)} (email_verification_token)\n WHERE email_verification_token IS NOT NULL\n `);\n }\n\n // ── Migration: refresh_tokens become session-scoped, rotation-safe ──\n // Two shapes are reconciled here, on EVERY table named refresh_tokens\n // in whatever schema it lives (a database provisioned by an older era\n // can carry the table in a different schema than the one this run\n // derives, and auth would then read a table nobody migrated):\n //\n // 1. The new columns. A token is now a member of a session\n // (`session_id`) and is retained after rotation (`revoked`,\n // `rotated_at`) so a replayed token can be recognised instead of\n // looking like a forgery. `session_started_at` is carried across\n // rotations so `users.tokens_valid_after` cannot be outrun.\n // 2. The removal of `unique_device_session`. It made (uid,\n // user_agent, ip_address) the identity of a session, which evicted\n // a second browser profile behind one NAT and churned rows as\n // phones changed networks. UA and IP are metadata now.\n //\n // Every existing row is adopted rather than dropped: it keeps its\n // token_hash, gets a session of its own, and stays unrevoked — so the\n // sessions live in browsers right now survive the upgrade rather than\n // everyone being signed out by the fix for being signed out.\n try {\n const rtTables = await db.execute(sql`\n SELECT table_schema, table_name\n FROM information_schema.tables\n WHERE table_name = 'refresh_tokens'\n `);\n const found = (rtTables.rows as { table_schema: string; table_name: string }[]);\n logger.debug(`🔍 refresh_tokens reconcile: found ${found.length} table(s): ${found.map(r => `\"${r.table_schema}\".\"${r.table_name}\"`).join(\", \") || \"(none)\"}`);\n for (const { table_schema } of found) {\n const qualified = `\"${table_schema}\".\"refresh_tokens\"`;\n try {\n // Added nullable, then back-filled, then constrained: adding\n // `session_id NOT NULL DEFAULT gen_random_uuid()` in one step\n // would stamp every existing row with the SAME uuid on some\n // Postgres versions, silently merging every live session into\n // one that a single logout would then wipe.\n await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS session_id TEXT`);\n await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS revoked BOOLEAN DEFAULT FALSE NOT NULL`);\n await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS rotated_at TIMESTAMP WITH TIME ZONE`);\n await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS session_started_at TIMESTAMP WITH TIME ZONE`);\n // Nullable with no default and no back-fill: a row written\n // before this column existed says nothing about whether a\n // second factor was presented, and the reader treats \"says\n // nothing\" as `aal1` — the restrictive answer. Stamping\n // every existing row would be inventing evidence.\n await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS aal TEXT`);\n\n // One session per pre-existing row: under the old model a row\n // WAS a device session, and there is no record of which rows\n // descended from the same sign-in.\n await db.execute(sql`\n UPDATE ${sql.raw(qualified)}\n SET session_id = gen_random_uuid()::text\n WHERE session_id IS NULL\n `);\n await db.execute(sql`\n UPDATE ${sql.raw(qualified)}\n SET session_started_at = COALESCE(created_at, NOW())\n WHERE session_started_at IS NULL\n `);\n await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ALTER COLUMN session_id SET DEFAULT gen_random_uuid()::text`);\n await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ALTER COLUMN session_started_at SET DEFAULT NOW()`);\n // SET NOT NULL only once the back-fill above has definitely\n // run; on a table that somehow still holds a NULL this throws\n // and is caught below rather than failing the boot.\n await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ALTER COLUMN session_id SET NOT NULL`);\n await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ALTER COLUMN session_started_at SET NOT NULL`);\n\n await db.execute(sql`\n CREATE INDEX IF NOT EXISTS idx_refresh_tokens_session\n ON ${sql.raw(qualified)}(session_id)\n `);\n\n // The device-session constraint is now actively harmful: two\n // live tokens of one session (a rotation in flight) share a\n // uid, and usually a user agent and IP too.\n await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} DROP CONSTRAINT IF EXISTS unique_device_session`);\n logger.debug(`✅ refresh_tokens reconciled for session-scoped rotation: ${qualified}`);\n } catch (perTableError: unknown) {\n logger.warn(`⚠️ refresh_tokens reconcile failed for ${qualified}: ${perTableError instanceof Error ? perTableError.message : String(perTableError)}`);\n }\n }\n } catch (migrationError: unknown) {\n logger.warn(`⚠️ refresh_tokens session migration skipped: ${migrationError instanceof Error ? migrationError.message : String(migrationError)}`);\n }\n\n // ── Migration: Copy roles from legacy junction table to inline column ──\n // If the old rebase.user_roles and rebase.roles tables exist, migrate\n // the data into the new TEXT[] column then drop the legacy tables.\n try {\n const legacyCheck = await db.execute(sql`\n SELECT EXISTS (\n SELECT 1 FROM information_schema.tables\n WHERE table_schema = 'rebase' AND table_name = 'user_roles'\n ) AS has_user_roles\n `);\n const hasLegacyTables = (legacyCheck.rows[0] as { has_user_roles: boolean }).has_user_roles;\n\n if (hasLegacyTables) {\n logger.info(\"🔄 Migrating roles from legacy user_roles table...\");\n // Update users' roles column from the junction table\n await db.execute(sql`\n UPDATE ${sql.raw(usersTableName)} u\n SET roles = COALESCE((\n SELECT array_agg(ur.role_id)\n FROM \"rebase\".\"user_roles\" ur\n WHERE ur.user_id = u.id\n ), '{}')\n WHERE u.roles = '{}' OR u.roles IS NULL\n `);\n\n // Drop legacy tables (junction first due to FK)\n await db.execute(sql`DROP TABLE IF EXISTS \"rebase\".\"user_roles\" CASCADE`);\n await db.execute(sql`DROP TABLE IF EXISTS \"rebase\".\"roles\" CASCADE`);\n logger.info(\"✅ Legacy roles tables migrated and dropped\");\n }\n } catch (migrationError: unknown) {\n // Non-fatal: log and continue — the column exists and will work\n logger.warn(`⚠️ Legacy roles migration skipped: ${migrationError instanceof Error ? migrationError.message : String(migrationError)}`);\n }\n\n // ── MFA tables ──────────────────────────────────────────────────────\n const mfaFactorsTableName = `\"${authSchema}\".\"mfa_factors\"`;\n const mfaChallengesTableName = `\"${authSchema}\".\"mfa_challenges\"`;\n const recoveryCodesTableName = `\"${authSchema}\".\"recovery_codes\"`;\n\n // Create mfa_factors table\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS ${sql.raw(mfaFactorsTableName)} (\n id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,\n uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,\n factor_type TEXT NOT NULL DEFAULT 'totp',\n secret_encrypted TEXT NOT NULL,\n friendly_name TEXT,\n verified BOOLEAN DEFAULT FALSE,\n last_used_counter BIGINT,\n created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),\n updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n )\n `);\n\n // Create indexes on mfa_factors\n await db.execute(sql`\n CREATE INDEX IF NOT EXISTS idx_mfa_factors_user\n ON ${sql.raw(mfaFactorsTableName)}(uid)\n `);\n\n // Create mfa_challenges table\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS ${sql.raw(mfaChallengesTableName)} (\n id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,\n factor_id TEXT NOT NULL REFERENCES ${sql.raw(mfaFactorsTableName)}(id) ON DELETE CASCADE,\n created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),\n verified_at TIMESTAMP WITH TIME ZONE,\n ip_address TEXT,\n attempts INTEGER NOT NULL DEFAULT 0,\n expires_at TIMESTAMP WITH TIME ZONE NOT NULL\n )\n `);\n\n // Create indexes on mfa_challenges\n await db.execute(sql`\n CREATE INDEX IF NOT EXISTS idx_mfa_challenges_factor\n ON ${sql.raw(mfaChallengesTableName)}(factor_id)\n `);\n\n // ── Migration: replay and brute-force state on the MFA tables ───────\n // Both are additive and nullable-or-defaulted, so a runtime that\n // predates them reads the tables unchanged. `last_used_counter` records\n // the TOTP step a factor has already spent (RFC 6238 §5.2); `attempts`\n // bounds how many guesses one challenge will take before it is dead.\n // Without them the code paths degrade to \"no replay protection, rate\n // limiters only\" rather than failing, which is why this is a warn.\n try {\n await db.execute(sql`ALTER TABLE ${sql.raw(mfaFactorsTableName)} ADD COLUMN IF NOT EXISTS last_used_counter BIGINT`);\n await db.execute(sql`ALTER TABLE ${sql.raw(mfaChallengesTableName)} ADD COLUMN IF NOT EXISTS attempts INTEGER NOT NULL DEFAULT 0`);\n } catch (mfaMigrationError: unknown) {\n logger.warn(`⚠️ MFA hardening columns skipped: ${mfaMigrationError instanceof Error ? mfaMigrationError.message : String(mfaMigrationError)}`);\n }\n\n // Create recovery_codes table\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS ${sql.raw(recoveryCodesTableName)} (\n id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,\n uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,\n code_hash TEXT NOT NULL,\n used_at TIMESTAMP WITH TIME ZONE,\n created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n )\n `);\n\n // Create indexes on recovery_codes\n await db.execute(sql`\n CREATE INDEX IF NOT EXISTS idx_recovery_codes_user\n ON ${sql.raw(recoveryCodesTableName)}(uid)\n `);\n\n // ── Migration: clear stale FORCE ROW LEVEL SECURITY (older RLS model) ──\n // The current model never emits FORCE: privileged auth writes run as\n // the table owner and rely on the owner bypassing plain ENABLE RLS\n // (see generate-postgres-ddl-logic). A table still carrying FORCE from\n // an older framework era binds the owner too, so the first user\n // registration after an upgrade fails with SQLSTATE 42501. Reconcile\n // on boot; only tables actually flagged get the ALTER (and its lock).\n try {\n // Every table this function creates, not a subset. `magic_link_tokens`\n // and `schema_meta` were missing here while their six siblings were\n // listed — so on a database carrying FORCE from the older RLS model,\n // magic-link sign-in kept failing 42501 after the upgrade that was\n // supposed to fix exactly that, and only for the one auth method.\n const authTablePairs: [string, string][] = [\n [usersSchema, resolvedTable],\n [authSchema, \"user_identities\"],\n [authSchema, \"refresh_tokens\"],\n [authSchema, \"password_reset_tokens\"],\n [authSchema, \"magic_link_tokens\"],\n [authSchema, \"app_config\"],\n [authSchema, \"mfa_factors\"],\n [authSchema, \"mfa_challenges\"],\n [authSchema, \"recovery_codes\"],\n [authSchema, \"schema_meta\"]\n ];\n for (const [schemaName, tableName] of authTablePairs) {\n const forced = await db.execute(sql`\n SELECT 1\n FROM pg_class c\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE n.nspname = ${schemaName}\n AND c.relname = ${tableName}\n AND c.relforcerowsecurity\n `);\n if (forced.rows.length > 0) {\n await db.execute(sql`\n ALTER TABLE ${sql.raw(`\"${schemaName}\".\"${tableName}\"`)}\n NO FORCE ROW LEVEL SECURITY\n `);\n logger.warn(\n `🔧 Cleared stale FORCE ROW LEVEL SECURITY on \"${schemaName}\".\"${tableName}\" ` +\n \"(legacy RLS model — it binds the owner connection and breaks privileged auth writes)\"\n );\n }\n }\n } catch (rlsReconcileError: unknown) {\n // Non-fatal: the connection may lack ownership on a pre-provisioned\n // table; registration will still fail loudly (42501) if FORCE remains.\n logger.warn(\n `⚠️ Could not reconcile FORCE ROW LEVEL SECURITY on auth tables: ` +\n `${rlsReconcileError instanceof Error ? rlsReconcileError.message : String(rlsReconcileError)}`\n );\n }\n\n // Stamped last of the MIGRATIONS, so a boot that died partway through\n // the ones above leaves the older stamp in place and the next boot runs\n // them again.\n await stampAuthSchemaVersion(db, authSchema);\n\n // ── Keep the end-user role out of auth's tables ─────────────────────\n // These carry session token hashes, TOTP secrets and recovery codes, and\n // none of them has RLS — they are not collections, so nothing ever\n // compiled a policy for them. Meanwhile the role provisioning grants\n // `rebase_user` DML on every table in this schema, and its\n // ALTER DEFAULT PRIVILEGES reaches the ones created right here, after it\n // ran. So the grant has to come back off; see `revokeInternalTableSql`\n // for why a revoke rather than an empty RLS policy set.\n //\n // After the stamp deliberately: `schema_meta` is created BY the stamp,\n // so revoking first would leave the one table holding this database's\n // schema version writable by every signed-in user until the next boot.\n // Nothing below re-runs the migrations, so the stamp's guarantee holds.\n await revokeInternalTableAccess(\n async (text) => { await db.execute(sql.raw(text)); },\n authSchema,\n {\n onError: (table, err) => logger.warn(\n `🔐 Could not revoke authenticated-role access to \"${authSchema}\".\"${table}\": ` +\n (err instanceof Error ? err.message : String(err))\n )\n }\n );\n\n logger.debug(\"✅ Auth tables ready\");\n } catch (error) {\n // The one failure that must not be survived. Continuing here is what\n // produced a server that answered /health with 200 while every login\n // returned 500 — the incompatibility is total, so crashing is the\n // kinder outcome: an orchestrator will not route traffic to a pod that\n // never came up.\n if (error instanceof AuthSchemaVersionError) throw error;\n logger.error(\"❌ Failed to create auth tables\", { error });\n logger.warn(\"⚠️ Continuing without creating auth tables.\");\n }\n}\n\n"],"mappings":"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"}
|