@rebasepro/server-postgres 0.16.1-canary.gef08a6e → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/{backup-service-BL5x6Fj5.js → backup-service-BtgHxfFm.js} +2 -1
  2. package/dist/{backup-service-BL5x6Fj5.js.map → backup-service-BtgHxfFm.js.map} +1 -1
  3. package/dist/cli-helpers.d.ts +41 -0
  4. package/dist/{ensure-collection-tables-DpGX_25A.js → collection-index-DxJBvVTH.js} +240 -1913
  5. package/dist/collection-index-DxJBvVTH.js.map +1 -0
  6. package/dist/{ensure-collection-policies-RK8-SFLs.js → ensure-collection-policies-DFpOl8SM.js} +3 -3
  7. package/dist/{ensure-collection-policies-RK8-SFLs.js.map → ensure-collection-policies-DFpOl8SM.js.map} +1 -1
  8. package/dist/ensure-collection-tables-DMjOkeRy.js +1952 -0
  9. package/dist/ensure-collection-tables-DMjOkeRy.js.map +1 -0
  10. package/dist/index.es.js +10 -9
  11. package/dist/index.es.js.map +1 -1
  12. package/dist/{rls-enforcement-da7ekLw-.js → rls-enforcement-CInuYj1-.js} +2 -2
  13. package/dist/{rls-enforcement-da7ekLw-.js.map → rls-enforcement-CInuYj1-.js.map} +1 -1
  14. package/dist/schema/collection-index.d.ts +182 -0
  15. package/dist/schema/introspect-db-inference.d.ts +1 -1
  16. package/dist/schema/introspect-db-logic.d.ts +4 -4
  17. package/dist/schema/introspect-db-project.d.ts +2 -2
  18. package/dist/src-DiDgtX8P.js.map +1 -1
  19. package/dist/{websocket-6b7Iy4TP.js → websocket-HcyLl1ZM.js} +5 -4
  20. package/dist/{websocket-6b7Iy4TP.js.map → websocket-HcyLl1ZM.js.map} +1 -1
  21. package/package.json +6 -6
  22. package/src/cli-helpers.ts +114 -0
  23. package/src/cli.ts +22 -0
  24. package/src/schema/collection-index.ts +427 -0
  25. package/src/schema/ensure-collection-tables.ts +21 -0
  26. package/src/schema/generate-postgres-ddl-logic.ts +17 -5
  27. package/src/schema/introspect-db-inference.ts +1 -1
  28. package/src/schema/introspect-db-logic.ts +4 -4
  29. package/src/schema/introspect-db-project.ts +2 -2
  30. package/src/schema/introspect-db.ts +2 -2
  31. package/src/services/realtimeService.ts +17 -4
  32. package/src/websocket.ts +1 -1
  33. package/dist/ensure-collection-tables-DpGX_25A.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.es.js","names":[],"sources":["../../types/src/types/channel_bus.ts","../../common/src/util/email.ts","../src/databasePoolManager.ts","../src/schema/auth-schema.ts","../src/cli-output.ts","../src/schema/generate-drizzle-schema.ts","../src/services/cdc/junction-tables.ts","../src/services/cdc/trigger-cdc.ts","../src/services/pg-notify-listener.ts","../src/services/cdc/CdcListener.ts","../src/schema/drizzle-ddl.ts","../src/services/channel-history.ts","../src/services/channel-presence.ts","../src/services/channel-bus/ChannelBus.ts","../src/services/channel-bus/PostgresChannelBus.ts","../src/services/channel-bus/index.ts","../src/services/realtimeService.ts","../src/collections/PostgresCollectionRegistry.ts","../src/backup/backup-cron.ts","../src/collections/validate-relations.ts","../src/collections/buildRegistry.ts","../src/auth/schema-version.ts","../src/auth/ensure-tables.ts","../src/auth/services.ts","../src/history/HistoryService.ts","../src/history/ensure-history-table.ts","../src/utils/pg-array-null-patch.ts","../src/schema/introspect-db-naming.ts","../src/schema/introspect-db-types.ts","../src/schema/introspect-db-logic.ts","../src/schema/introspect-runtime.ts","../src/schema/dynamic-tables.ts","../src/cli-errors.ts","../src/PostgresBootstrapper.ts","../src/PostgresAdapter.ts"],"sourcesContent":["/**\n * The cross-instance transport for channel broadcast and presence, and the\n * contract anyone implementing one has to meet.\n *\n * These types live in `@rebasepro/types` rather than in the Postgres adapter on\n * purpose: a transport package should depend on the contract, not on the\n * database driver that happens to ship the default implementation. A\n * `@rebasepro/channel-bus-<something>` package needs this file and nothing else.\n *\n * Why a transport exists at all: entity/collection realtime already spans\n * instances (CDC, or per-mutation LISTEN/NOTIFY). Channel broadcast and presence\n * did not — they fanned out from per-process maps, so two clients served by\n * different replicas could not see each other, and nothing errored. The bus is\n * the missing hop, and deliberately *only* that hop: which local clients receive\n * a frame stays in the realtime service, so a transport never has to know what a\n * subscription, a WebSocket or a presence roster is.\n */\n\n/**\n * A frame in flight between instances.\n *\n * `sid` identifies the publishing instance. The realtime service drops frames\n * carrying its own `sid` on arrival — local fan-out already happened before the\n * publish — so a transport that echoes a publisher's own messages back to it is\n * still correct, merely wasteful.\n *\n * Keys are spelled out rather than abbreviated. The one shipped transport with a\n * size limit has a pointer path for anything that would approach it, so shaving\n * bytes off key names buys nothing worth the opacity.\n */\nexport type ChannelBusFrame =\n /** A broadcast carrying its payload. */\n | {\n kind: \"broadcast\";\n sid: string;\n channel: string;\n event: string;\n /** Originating client, echoed so receivers can skip it if it is theirs. */\n from?: string;\n /** Sequence number, present only on retained channels. */\n seq?: number;\n payload: unknown;\n }\n /**\n * A broadcast too large for the transport to carry inline: the body is\n * already durable in `rebase.channel_messages`, so the frame carries only\n * its address and each receiver reads it back. Only ever emitted for\n * retained channels, and only by a transport with a finite\n * {@link ChannelBus.maxFrameBytes}.\n */\n | {\n kind: \"broadcast_ref\";\n sid: string;\n channel: string;\n from?: string;\n seq: number;\n }\n /** A presence join/leave/update, small by construction. */\n | {\n kind: \"presence_diff\";\n sid: string;\n channel: string;\n joins: Record<string, Record<string, unknown>>;\n leaves: Record<string, Record<string, unknown>>;\n };\n\n/** Receives frames published by *other* instances. */\nexport type ChannelBusHandler = (frame: ChannelBusFrame) => void | Promise<void>;\n\n/**\n * A cross-instance transport.\n *\n * ## What an implementation must guarantee\n *\n * - **`start()` rejects if the transport is unusable.** The caller falls back to\n * in-process delivery when it does. Resolving while disconnected produces a\n * cluster that believes it is connected and silently is not, which is the\n * exact failure this whole mechanism exists to remove.\n * - **`publish()` reaches every *other* instance, or rejects.** Delivery back to\n * the publisher is permitted but pointless (see {@link ChannelBusFrame.sid}).\n * - **`stop()` is idempotent** and releases everything, including anything\n * holding the event loop open.\n * - **A malformed message never throws out of the transport.** Parsing happens\n * inside the implementation; drop and log what you cannot understand, so one\n * bad frame cannot take the listener down.\n *\n * ## What it does *not* have to guarantee\n *\n * - **Ordering.** Retained channels carry `seq`, and the client SDK orders by\n * it. Unsequenced broadcasts are cursor-grade traffic where order is not\n * meaningful.\n * - **Durability.** A frame lost in transit is a missed live update; retained\n * channels repair themselves through the client's `channel_history` replay.\n * - **Exactly-once.** Duplicates are tolerated — retained frames are deduped by\n * `seq`, and presence diffs are idempotent by construction.\n */\nexport interface ChannelBus {\n /**\n * Identifies the transport in logs and in `getChannelBusKind()`. Use your\n * own name; the framework only compares against `\"memory\"` to decide\n * whether publishing is worth attempting at all.\n */\n readonly kind: string;\n\n /**\n * Largest frame this transport will carry, in bytes of encoded JSON, or\n * `Infinity` when there is no meaningful ceiling.\n *\n * A broadcast that exceeds it is published as a `broadcast_ref` pointer when\n * the channel is retained, and refused with an error to the sender when it\n * is not. Implementations with no limit should return `Infinity` rather than\n * a large number, so the pointer path is never taken needlessly.\n */\n readonly maxFrameBytes: number;\n\n /** Connect and begin delivering remote frames to `handler`. */\n start(handler: ChannelBusHandler): Promise<void>;\n\n /** Publish a frame to the other instances. */\n publish(frame: ChannelBusFrame): Promise<void>;\n\n /** Disconnect and release resources. Idempotent. */\n stop(): Promise<void>;\n}\n\n/**\n * Which transport to use, for the two that ship with the Postgres adapter.\n *\n * To use one that does not ship here — a Redis package, or your own class —\n * pass the {@link ChannelBus} instance itself instead of a config object.\n *\n * There are deliberately only two built in, and neither adds a service to a\n * deployment. Rebase deploys as Postgres + backend + frontend; a bus that\n * required a message broker would put a second stateful service into every\n * `docker-compose.yml` the CLI scaffolds, for a feature most applications never\n * use. Measured across two backend instances against one Postgres container,\n * the Postgres bus carried ~10k cross-instance messages/second with no losses,\n * and stayed flat out to eight instances — comfortably past what live-cursor\n * collaboration generates. The extension point below is the answer for anyone\n * who does outgrow it.\n */\nexport type ChannelBusConfig =\n /**\n * In-process only — the historical behaviour. Broadcast and presence reach\n * the clients connected to *this* instance and no further.\n */\n | { type: \"memory\" }\n /**\n * Postgres LISTEN/NOTIFY, reusing infrastructure the deployment already has.\n *\n * `pg_notify` caps a payload at 8000 bytes, so a broadcast larger than that\n * is delivered cross-instance only on a *retained* channel, where the\n * notification carries a pointer (`seq`) instead of the message and each\n * receiver reads the body back from `rebase.channel_messages`. An oversized\n * broadcast on an ephemeral channel is refused rather than silently\n * delivered to half the cluster.\n *\n * NOTE: `LISTEN` needs a session-mode connection. Behind PgBouncer in\n * transaction mode this must point at the database directly\n * (`DATABASE_DIRECT_URL`), not at the pooler.\n */\n | {\n type: \"postgres\";\n /** Direct connection for the LISTEN client. Defaults to `DATABASE_DIRECT_URL`. */\n connectionString?: string;\n /**\n * How long to coalesce outgoing frames into a single notification, in\n * milliseconds. Defaults to 10.\n *\n * A notify is a query on your primary database, so under load this is\n * the difference between one query per message and one per window. The\n * window is leading-edge: a frame arriving when none is open goes out\n * immediately, so an idle channel pays no added latency and only a\n * sustained stream is batched.\n *\n * Set to 0 to disable coalescing and send every frame on its own.\n */\n batchWindowMs?: number;\n };\n\n/**\n * What `realtime.bus` accepts: a built-in transport by name, or any\n * {@link ChannelBus} instance.\n *\n * ```typescript\n * realtime: { bus: { type: \"postgres\" } } // shipped\n * realtime: { bus: new MyRedisChannelBus(url) } // a separate package, or your own\n * ```\n */\nexport type ChannelBusSetting = ChannelBusConfig | ChannelBus;\n\n/**\n * Whether `setting` is an already-constructed transport rather than a request\n * for a built-in one.\n *\n * Structural rather than nominal so that an instance from a *different copy* of\n * `@rebasepro/types` — an entirely normal outcome of a separately versioned\n * transport package — is still recognised.\n */\nexport function isChannelBusInstance(setting: ChannelBusSetting | undefined): setting is ChannelBus {\n return typeof (setting as ChannelBus | undefined)?.publish === \"function\";\n}\n","/**\n * Email normalization — one implementation, because the database enforces it.\n *\n * `ensureAuthTablesExist` puts a `UNIQUE INDEX ON users (lower(email))` on the\n * auth table. That index decides what \"the same address\" means, and it does not\n * trim: to Postgres, `' foo@bar.com'` and `'foo@bar.com'` are two addresses and\n * both may exist. So every write that reaches the column has to agree with\n * every read, exactly, or the two disagree in the one direction that matters —\n * a row that exists and cannot be found.\n *\n * That is not hypothetical. The lookup path trimmed and the admin create paths\n * did not, so a user created through `POST /api/data/users` or\n * `POST /api/auth/admin/users` with a stray space was stored untrimmed,\n * survived the unique index alongside the real address, and was unreachable by\n * login forever after. The HTTP auth routes were unaffected only because Zod's\n * `.email()` happens to reject surrounding whitespace — a guard on a different\n * layer, for a different reason, that the admin paths do not sit behind.\n *\n * It lives in `common` because `server`, `server-postgres` and `server-mongo`\n * all write this column and must agree exactly, and `common` is the only\n * package all three already depend on.\n */\n\n/**\n * Canonical form of an email address: trimmed, lower-cased.\n *\n * Non-strings pass through untouched, so this is safe to apply to a value out\n * of a partial update payload whose type is not known yet.\n */\nexport function normalizeEmail<T>(email: T): T | string {\n return typeof email === \"string\" ? email.trim().toLowerCase() : email;\n}\n","import { Pool } from \"pg\";\nimport { drizzle } from \"drizzle-orm/node-postgres\";\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { logger } from \"@rebasepro/server\";\nimport { guardPoolAgainstDirtyRelease, pinSearchPath, cappedPoolMax } from \"./connection\";\n\nexport class DatabasePoolManager {\n private pools: Map<string, Pool> = new Map();\n private drizzleInstances: Map<string, NodePgDatabase> = new Map();\n public readonly defaultDatabaseName: string;\n private readonly rootConnectionString: string;\n\n constructor(adminConnectionString: string) {\n this.rootConnectionString = adminConnectionString;\n try {\n const url = new URL(adminConnectionString);\n this.defaultDatabaseName = url.pathname.slice(1);\n } catch (e) {\n throw new Error(`Invalid adminConnectionString provided: ${e}`);\n }\n }\n\n public getDrizzle(databaseName: string): NodePgDatabase<Record<string, never>> {\n const existing = this.drizzleInstances.get(databaseName);\n if (existing) {\n return existing;\n }\n\n const pool = this.getPool(databaseName);\n const db = drizzle(pool);\n this.drizzleInstances.set(databaseName, db);\n return db;\n }\n\n public getPool(databaseName: string): Pool {\n if (this.pools.has(databaseName)) {\n return this.pools.get(databaseName)!;\n }\n\n const url = new URL(this.rootConnectionString);\n url.pathname = `/${databaseName}`;\n\n const pool = new Pool({\n // Same pin as the primary pool: these are branch/multi-database\n // connections to the *same* server, so they inherit the same\n // `\"$user\"` hazard. See `pinSearchPath`.\n connectionString: pinSearchPath(url.toString()),\n // Capped by REBASE_DB_POOL_MAX, which the managed development\n // database sets to 1: PGlite multiplexes onto a single session and\n // overlapping transactions deadlock there.\n max: cappedPoolMax(10),\n idleTimeoutMillis: 10000, // Reduced from 30000 for aggressive cleanup\n allowExitOnIdle: true // Prevent idle clients from hanging the Node.js process\n });\n\n // Prevent idle client errors from crashing the Node.js process\n pool.on(\"error\", (err) => {\n logger.error(`[DatabasePoolManager] Unexpected error on idle client for db ${databaseName}`, { error: err });\n });\n guardPoolAgainstDirtyRelease(pool, `pg-pool:${databaseName}`);\n\n this.pools.set(databaseName, pool);\n return pool;\n }\n\n /**\n * Disconnect and remove the pool for a specific database.\n * Required before `CREATE DATABASE ... TEMPLATE` or `DROP DATABASE`,\n * which need exclusive access to the target database.\n */\n public async disconnectDatabase(databaseName: string): Promise<void> {\n const pool = this.pools.get(databaseName);\n if (pool) {\n await pool.end();\n this.pools.delete(databaseName);\n this.drizzleInstances.delete(databaseName);\n }\n }\n\n /** Check if a pool exists for a given database name. */\n public hasPool(databaseName: string): boolean {\n return this.pools.has(databaseName);\n }\n\n public async shutdown(): Promise<void> {\n const promises = [];\n for (const [dbName, pool] of this.pools.entries()) {\n logger.info(`[DatabasePoolManager] Shutting down pool for ${dbName}`);\n promises.push(pool.end());\n }\n await Promise.all(promises);\n this.pools.clear();\n this.drizzleInstances.clear();\n }\n}\n","import { pgSchema, pgTable, uuid, timestamp, boolean, jsonb, text, unique, index, integer, bigint } from \"drizzle-orm/pg-core\";\nimport { relations } from \"drizzle-orm\";\n\n/**\n * Factory function to dynamically create the auth tables bound to the specified schema names.\n *\n * This module builds queries; it does not create tables. `ensureAuthTablesExist`\n * owns the DDL, which makes everything here a *claim* about a database it cannot\n * enforce — and the claims drifted. Every column below was declared\n * `varchar(n)` while the DDL created it as `TEXT`: `user_agent` as varchar(500),\n * `ip_address` as varchar(45), `secret_encrypted` as varchar(500), every\n * `token_hash` as varchar(255). None of it was true of any database this\n * framework ever provisioned. Harmless at runtime — drizzle does not enforce a\n * length client-side, so the widths only ever misled the next reader — but a\n * schema module that describes columns that do not exist is worse than no\n * schema module. They are `text` here now because they are TEXT there.\n */\nexport function createAuthSchema(usersSchemaName = \"rebase\") {\n const usersSchema = usersSchemaName === \"public\" ? null : pgSchema(usersSchemaName);\n\n const tableCreator = (usersSchema ? usersSchema.table.bind(usersSchema) : pgTable) as typeof pgTable;\n const usersTableCreator = tableCreator;\n\n /**\n * Users table - stores both email/password and OAuth users\n */\n const users = usersTableCreator(\"users\", {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n email: text(\"email\").notNull().unique(),\n passwordHash: text(\"password_hash\"), // NULL for OAuth-only users\n displayName: text(\"display_name\"),\n photoUrl: text(\"photo_url\"),\n emailVerified: boolean(\"email_verified\").default(false).notNull(),\n emailVerificationToken: text(\"email_verification_token\"),\n emailVerificationSentAt: timestamp(\"email_verification_sent_at\"),\n isAnonymous: boolean(\"is_anonymous\").default(false).notNull(),\n roles: text(\"roles\").array().default([]).notNull(),\n metadata: jsonb(\"metadata\").$type<Record<string, unknown>>().default({}).notNull(),\n /**\n * Sessions that began before this instant are dead, whatever tokens\n * they still hold. Password resets and admin revocations stamp it.\n *\n * Deleting the user's refresh-token rows (which we also do) is not\n * sufficient on its own: a request already in flight can insert a\n * freshly rotated row microseconds after the delete and survive it.\n * This timestamp cannot be outrun that way — it is checked against\n * `refresh_tokens.session_started_at`, which rotation carries forward.\n */\n tokensValidAfter: timestamp(\"tokens_valid_after\"),\n createdAt: timestamp(\"created_at\").defaultNow().notNull(),\n updatedAt: timestamp(\"updated_at\").defaultNow().notNull()\n });\n\n\n /**\n * Refresh tokens for long-lived sessions.\n *\n * A row is one token, not one device. Every token minted from the same\n * sign-in shares a `sessionId`, and rotation ADDS a row rather than\n * replacing one: the superseded token stays on file, flagged `revoked`\n * with a `rotatedAt` stamp. That record is what lets the refresh endpoint\n * tell a client replaying a token it never got an answer for (a response\n * lost to a redeploy, a second tab racing on boot) apart from a stranger\n * presenting a token that was never issued. Deleting the old row on sight\n * — the previous behaviour — made those two cases indistinguishable, and\n * the legitimate one is overwhelmingly the common one.\n *\n * There is deliberately NO unique constraint on (uid, user_agent,\n * ip_address). Keying a session on the IP meant one row per \"device\",\n * so a second browser profile behind the same NAT silently evicted the\n * first, and a phone changing networks orphaned a row on every hop.\n * User agent and IP are descriptive metadata for the sessions list;\n * `sessionId` is the identity.\n */\n const refreshTokens = tableCreator(\"refresh_tokens\", {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n uid: uuid(\"uid\").notNull().references(() => users.id, { onDelete: \"cascade\" }),\n sessionId: uuid(\"session_id\").defaultRandom().notNull(),\n tokenHash: text(\"token_hash\").notNull().unique(),\n expiresAt: timestamp(\"expires_at\").notNull(),\n revoked: boolean(\"revoked\").default(false).notNull(),\n rotatedAt: timestamp(\"rotated_at\"),\n /**\n * When the sign-in this token descends from happened — carried across\n * every rotation, unlike `createdAt`. `users.tokensValidAfter` is\n * compared against this, so a revocation cannot be outrun by a token\n * that rotates immediately after it.\n */\n sessionStartedAt: timestamp(\"session_started_at\").defaultNow().notNull(),\n /**\n * The assurance level the sign-in was established at — `aal2` only\n * where a second factor was actually presented. Carried across\n * rotations, because refresh is not a new authentication and has\n * nothing else to read the level from.\n */\n aal: text(\"aal\"),\n userAgent: text(\"user_agent\"),\n ipAddress: text(\"ip_address\"),\n createdAt: timestamp(\"created_at\").defaultNow().notNull()\n }, (table) => ({\n sessionIdx: index(\"idx_refresh_tokens_session\").on(table.sessionId)\n }));\n\n /**\n * Password reset tokens for forgot password flow\n */\n const passwordResetTokens = tableCreator(\"password_reset_tokens\", {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n uid: uuid(\"uid\").notNull().references(() => users.id, { onDelete: \"cascade\" }),\n tokenHash: text(\"token_hash\").notNull().unique(),\n expiresAt: timestamp(\"expires_at\").notNull(),\n usedAt: timestamp(\"used_at\"),\n createdAt: timestamp(\"created_at\").defaultNow().notNull()\n });\n\n /**\n * App config - key/value store for custom settings\n */\n const appConfig = tableCreator(\"app_config\", {\n key: text(\"key\").primaryKey(),\n value: jsonb(\"value\").notNull(),\n updatedAt: timestamp(\"updated_at\").defaultNow().notNull()\n });\n\n /**\n * User identities - maps external OAuth profiles back to local users\n */\n const userIdentities = tableCreator(\"user_identities\", {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n uid: uuid(\"uid\").notNull().references(() => users.id, { onDelete: \"cascade\" }),\n provider: text(\"provider\").notNull(), // e.g. 'google', 'linkedin'\n providerId: text(\"provider_id\").notNull(),\n profileData: jsonb(\"profile_data\"),\n createdAt: timestamp(\"created_at\").defaultNow().notNull(),\n updatedAt: timestamp(\"updated_at\").defaultNow().notNull()\n }, (table) => ({\n uniqueProviderId: unique(\"unique_provider_id\").on(table.provider, table.providerId)\n }));\n\n /**\n * MFA factors table - stores enrolled MFA methods\n */\n const mfaFactors = tableCreator(\"mfa_factors\", {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n uid: uuid(\"uid\").notNull().references(() => users.id, { onDelete: \"cascade\" }),\n factorType: text(\"factor_type\").notNull(), // 'totp'\n secretEncrypted: text(\"secret_encrypted\").notNull(),\n friendlyName: text(\"friendly_name\"),\n verified: boolean(\"verified\").default(false).notNull(),\n /**\n * The highest TOTP time step ever accepted for this factor. RFC 6238\n * §5.2 forbids accepting an OTP twice, and the ±1 step window that\n * exists for clock drift is also a 90-second replay window: without\n * this, one observed code buys a fresh session for a minute and a half.\n */\n lastUsedCounter: bigint(\"last_used_counter\", { mode: \"number\" }),\n createdAt: timestamp(\"created_at\").defaultNow().notNull(),\n updatedAt: timestamp(\"updated_at\").defaultNow().notNull()\n });\n\n /**\n * MFA challenges table - tracks active MFA verification attempts\n */\n const mfaChallenges = tableCreator(\"mfa_challenges\", {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n factorId: uuid(\"factor_id\").notNull().references(() => mfaFactors.id, { onDelete: \"cascade\" }),\n createdAt: timestamp(\"created_at\").defaultNow().notNull(),\n verifiedAt: timestamp(\"verified_at\"),\n ipAddress: text(\"ip_address\"),\n /** Failed guesses recorded against this challenge; bounded by the route. */\n attempts: integer(\"attempts\").default(0).notNull(),\n expiresAt: timestamp(\"expires_at\").notNull()\n });\n\n /**\n * Recovery codes table - backup codes for MFA\n */\n const recoveryCodes = tableCreator(\"recovery_codes\", {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n uid: uuid(\"uid\").notNull().references(() => users.id, { onDelete: \"cascade\" }),\n codeHash: text(\"code_hash\").notNull(),\n usedAt: timestamp(\"used_at\"),\n createdAt: timestamp(\"created_at\").defaultNow().notNull()\n });\n\n /**\n * Magic link tokens for passwordless email login\n */\n const magicLinkTokens = tableCreator(\"magic_link_tokens\", {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n uid: uuid(\"uid\").notNull().references(() => users.id, { onDelete: \"cascade\" }),\n tokenHash: text(\"token_hash\").notNull().unique(),\n expiresAt: timestamp(\"expires_at\").notNull(),\n usedAt: timestamp(\"used_at\"),\n createdAt: timestamp(\"created_at\").defaultNow().notNull()\n });\n\n return {\n usersSchema,\n users,\n refreshTokens,\n passwordResetTokens,\n appConfig,\n userIdentities,\n mfaFactors,\n mfaChallenges,\n recoveryCodes,\n magicLinkTokens\n };\n}\n\n// Instantiate default schema and tables using the default \"rebase\" schema\nconst defaultAuthSchema = createAuthSchema(\"rebase\");\n\nexport const usersSchema = defaultAuthSchema.usersSchema;\n\nexport const users = defaultAuthSchema.users;\nexport const refreshTokens = defaultAuthSchema.refreshTokens;\nexport const passwordResetTokens = defaultAuthSchema.passwordResetTokens;\nexport const appConfig = defaultAuthSchema.appConfig;\nexport const userIdentities = defaultAuthSchema.userIdentities;\nexport const mfaFactors = defaultAuthSchema.mfaFactors;\nexport const mfaChallenges = defaultAuthSchema.mfaChallenges;\nexport const recoveryCodes = defaultAuthSchema.recoveryCodes;\nexport const magicLinkTokens = defaultAuthSchema.magicLinkTokens;\n\n// Relations\nexport const usersRelations = relations(users, ({ many }) => ({\n refreshTokens: many(refreshTokens),\n passwordResetTokens: many(passwordResetTokens),\n userIdentities: many(userIdentities),\n mfaFactors: many(mfaFactors),\n recoveryCodes: many(recoveryCodes),\n magicLinkTokens: many(magicLinkTokens)\n}));\n\nexport const refreshTokensRelations = relations(refreshTokens, ({ one }) => ({\n user: one(users, {\n fields: [refreshTokens.uid],\n references: [users.id]\n })\n}));\n\nexport const passwordResetTokensRelations = relations(passwordResetTokens, ({ one }) => ({\n user: one(users, {\n fields: [passwordResetTokens.uid],\n references: [users.id]\n })\n}));\n\nexport const userIdentitiesRelations = relations(userIdentities, ({ one }) => ({\n user: one(users, {\n fields: [userIdentities.uid],\n references: [users.id]\n })\n}));\n\nexport const mfaFactorsRelations = relations(mfaFactors, ({ one, many }) => ({\n user: one(users, {\n fields: [mfaFactors.uid],\n references: [users.id]\n }),\n challenges: many(mfaChallenges)\n}));\n\nexport const mfaChallengesRelations = relations(mfaChallenges, ({ one }) => ({\n factor: one(mfaFactors, {\n fields: [mfaChallenges.factorId],\n references: [mfaFactors.id]\n })\n}));\n\nexport const recoveryCodesRelations = relations(recoveryCodes, ({ one }) => ({\n user: one(users, {\n fields: [recoveryCodes.uid],\n references: [users.id]\n })\n}));\n\nexport const magicLinkTokensRelations = relations(magicLinkTokens, ({ one }) => ({\n user: one(users, {\n fields: [magicLinkTokens.uid],\n references: [users.id]\n })\n}));\n\n// Type exports\nexport type User = typeof users.$inferSelect;\nexport type NewUser = typeof users.$inferInsert;\nexport type RefreshToken = typeof refreshTokens.$inferSelect;\nexport type PasswordResetToken = typeof passwordResetTokens.$inferSelect;\nexport type AppConfig = typeof appConfig.$inferSelect;\nexport type UserIdentity = typeof userIdentities.$inferSelect;\nexport type NewUserIdentity = typeof userIdentities.$inferInsert;\nexport type MfaFactorRow = typeof mfaFactors.$inferSelect;\nexport type MfaChallengeRow = typeof mfaChallenges.$inferSelect;\nexport type RecoveryCodeRow = typeof recoveryCodes.$inferSelect;\nexport type MagicLinkToken = typeof magicLinkTokens.$inferSelect;\n","/**\n * Terminal output for the `rebase db|schema|doctor` commands.\n *\n * These commands used to write every line through `logger`, and that is a\n * category error with three separate consequences:\n *\n * - `logger` prefixes each line with its own level, so a box-drawn report\n * arrived as `ℹ️ [INFO] ┌─ ✗ Missing Column ───` and the frame no longer\n * lined up with anything;\n * - `logger` is gated by `LOG_LEVEL`, which ships in the scaffold's own\n * `.env.example` — a developer who quietened their dev server with\n * `LOG_LEVEL=warn` got a `rebase db push` that printed almost nothing and\n * still exited non-zero, indistinguishable from a crash;\n * - under `NODE_ENV=production` `logger` emits JSON, so the whole report\n * became log records with the chalk escape codes embedded in them.\n *\n * A CLI's report *is* its return value. It goes to the terminal unconditionally\n * and unadorned. `packages/cli` has always written its output this way; this is\n * the same three functions for the plugin CLI that `rebase` delegates to.\n *\n * `logger` still belongs in this package's *runtime* — a request handler has no\n * terminal and its lines want levels, timestamps and redaction. The rule is the\n * caller, not the severity: anything a developer reads because they typed a\n * command goes here, anything a server emits while running goes to `logger`.\n *\n * Errors and warnings go to stderr so `rebase db push > plan.txt` keeps the\n * diagnosis on the terminal where it is readable.\n */\n\n/** One line of human-facing output on stdout. */\nexport const out = (line = \"\"): void => {\n console.log(line);\n};\n\n/** One line of human-facing warning output on stderr. */\nexport const outWarn = (line = \"\"): void => {\n console.warn(line);\n};\n\n/** One line of human-facing error output on stderr. */\nexport const outError = (line = \"\"): void => {\n console.error(line);\n};\n","import { promises as fsPromises } from \"fs\";\nimport * as fs from \"fs\";\nimport path from \"path\";\nimport { pathToFileURL } from \"url\";\nimport { generateSchema } from \"./generate-drizzle-schema-logic\";\nimport { CollectionConfig } from \"@rebasepro/types\";\nimport { loadCollectionsFromDirectory } from \"@rebasepro/server\";\nimport { out, outError } from \"../cli-output\";\n\n\n// --- Helper Functions ---\n\nconst formatTerminalText = (text: string, options: {\n bold?: boolean;\n backgroundColor?: \"blue\" | \"green\" | \"red\" | \"yellow\" | \"cyan\" | \"magenta\";\n textColor?: \"white\" | \"black\" | \"red\" | \"green\" | \"yellow\" | \"blue\" | \"magenta\" | \"cyan\";\n} = {}): string => {\n let codes = \"\";\n if (options.bold) codes += \"\\x1b[1m\";\n if (options.backgroundColor) {\n const bgColors = {\n blue: \"\\x1b[44m\",\n green: \"\\x1b[42m\",\n red: \"\\x1b[41m\",\n yellow: \"\\x1b[43m\",\n cyan: \"\\x1b[46m\",\n magenta: \"\\x1b[45m\"\n } as const;\n codes += bgColors[options.backgroundColor];\n }\n if (options.textColor) {\n const textColors = {\n white: \"\\x1b[37m\",\n black: \"\\x1b[30m\",\n red: \"\\x1b[31m\",\n green: \"\\x1b[32m\",\n yellow: \"\\x1b[33m\",\n blue: \"\\x1b[34m\",\n magenta: \"\\x1b[35m\",\n cyan: \"\\x1b[36m\"\n } as const;\n codes += textColors[options.textColor];\n }\n return `${codes}${text}\\x1b[0m`;\n};\n\n// --- Execution and Watch Logic ---\n\nconst runGeneration = async (collectionsFilePath?: string, outputPath?: string) => {\n try {\n if (!collectionsFilePath) {\n outError(\"Error: No collections file path provided. Skipping schema generation.\");\n return;\n }\n\n const resolvedPath = path.resolve(collectionsFilePath);\n\n // Shared with the runtime and the doctor: what gets generated here must\n // be exactly what the server serves, including directory-level defaults.\n let collections: CollectionConfig[] = await loadCollectionsFromDirectory(resolvedPath);\n\n\n // If collections directory is empty but exists, or failed to find any, we still want to inject defaults\n if (!collections || !Array.isArray(collections)) {\n collections = [];\n }\n\n\n // Sort collections by slug alphabetically to ensure deterministic schema generation\n collections.sort((a, b) => a.slug.localeCompare(b.slug));\n\n const schemaContent = await generateSchema(collections);\n\n if (outputPath) {\n const outputDir = path.dirname(outputPath);\n await fsPromises.mkdir(outputDir, { recursive: true });\n await fsPromises.writeFile(outputPath, schemaContent);\n out(`✅ Drizzle schema generated successfully at ${outputPath}`);\n } else {\n out(\"✅ Drizzle schema generated successfully.\");\n out(String(schemaContent));\n }\n\n out(`You can now run ${formatTerminalText(\"rebase db generate\", {\n bold: true,\n backgroundColor: \"blue\",\n textColor: \"black\"\n })} to generate the SQL migration files.`);\n\n } catch (error) {\n outError(`Error generating schema: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);\n }\n};\n\nconst main = async () => {\n const collectionsFilePathArg = process.argv.find(arg => arg.startsWith(\"--collections=\"));\n const collectionsFilePath = collectionsFilePathArg ? collectionsFilePathArg.split(\"=\")[1] : process.argv[2];\n\n const outputPathArg = process.argv.find(arg => arg.startsWith(\"--output=\"));\n const outputPath = outputPathArg ? outputPathArg.split(\"=\")[1] : undefined;\n\n const watch = process.argv.includes(\"--watch\");\n\n if (!collectionsFilePath) {\n out(\"Usage: ts-node generate-drizzle-schema.ts <path-to-collections-file> [--output <path-to-output-file>] [--watch]\");\n return;\n }\n\n const resolvedPath = path.resolve(process.cwd(), collectionsFilePath);\n const resolvedOutputPath = outputPath ? path.resolve(process.cwd(), outputPath) : undefined;\n\n if (watch) {\n out(`Watching for changes in ${resolvedPath}...`);\n // Imported here rather than at module scope, and this is not a style\n // choice: chokidar is needed only by `--watch`, which is a\n // schema-authoring path that never runs inside the runtime image. A\n // top-level import puts it on the boot path of the published driver\n // bundle, and the image installs a hand-listed set of runtime\n // dependencies that does not include it — so the whole driver failed to\n // load with \"Cannot find package 'chokidar'\", and every self-hosted\n // container answered 500 with a stack trace about a file watcher.\n //\n // Same reasoning the image already applies to @ariga/atlas: an\n // authoring-only dependency does not belong on a boot path.\n const { default: chokidar } = await import(\"chokidar\");\n const watcher = chokidar.watch(resolvedPath, {\n persistent: true,\n ignoreInitial: false\n });\n\n watcher.on(\"all\", (event, filePath) => {\n out(`[${event}] ${filePath}. Regenerating schema...`);\n runGeneration(resolvedPath, resolvedOutputPath);\n });\n } else {\n runGeneration(resolvedPath, resolvedOutputPath);\n }\n};\n\n// This check ensures the script only runs when executed directly\nif (import.meta.url.endsWith(process.argv[1])) {\n main();\n}\n","import { CollectionConfig, ResolvedRelation, isManyToMany } from \"@rebasepro/types\";\nimport { resolveCollectionRelations } from \"@rebasepro/common\";\n\nimport { PostgresCollectionRegistry } from \"../../collections/PostgresCollectionRegistry\";\n\n/**\n * One end of a many-to-many, as seen from the junction table.\n *\n * A junction table is not a collection, so nothing in the registry maps it to\n * one — which is why a change to it was invisible to change capture. But its\n * rows are exactly the contents of a parent's child list, so a write to it is a\n * change to `<parentSlug>/<sourceId>/<relationKey>` and to nothing else.\n */\nexport interface JunctionLink {\n schema: string;\n /** The junction table itself, e.g. `posts_tags`. */\n table: string;\n /** The collection whose relation this is, e.g. `posts`. */\n parentCollection: CollectionConfig;\n /** The relation's key — the path segment a child list is addressed by. */\n relationKey: string;\n /** Junction column holding the parent's id. */\n sourceColumn: string;\n /** Junction column holding the target's id. */\n targetColumn: string;\n}\n\n/**\n * Every junction table reachable from a registered collection, once per\n * relation that uses it.\n *\n * A junction is listed once per *direction* when both sides declare it, because\n * each direction addresses a different child list: `posts/1/tags` and\n * `tags/t/posts` both change when one link is written.\n */\nexport function collectJunctionLinks(registry: PostgresCollectionRegistry): JunctionLink[] {\n const links: JunctionLink[] = [];\n const seen = new Set<string>();\n\n for (const collection of registry.getCollections()) {\n let relations: Record<string, ResolvedRelation>;\n try {\n relations = resolveCollectionRelations(collection);\n } catch {\n // A collection whose relations cannot be resolved (an unresolvable\n // target, typically mid-migration) simply contributes none.\n continue;\n }\n\n for (const [relationKey, relation] of Object.entries(relations)) {\n if (!isManyToMany(relation)) continue;\n const through = relation.through;\n\n // Same relation registered under both its canonical name and the\n // declaring property key would otherwise notify the same path twice.\n const key = `${collection.slug}::${relationKey}::${through.table}`;\n if (seen.has(key)) continue;\n seen.add(key);\n\n links.push({\n schema: (collection as { schema?: string }).schema ?? \"public\",\n table: through.table,\n parentCollection: collection,\n relationKey,\n sourceColumn: through.sourceColumn,\n targetColumn: through.targetColumn\n });\n }\n }\n\n return links;\n}\n\n/**\n * Index {@link collectJunctionLinks} by table, under both the qualified and the\n * bare name — a change event carries whatever the trigger reports, and a\n * collection need not declare a schema.\n */\nexport function buildJunctionLinkMap(registry: PostgresCollectionRegistry): Map<string, JunctionLink[]> {\n const map = new Map<string, JunctionLink[]>();\n\n for (const link of collectJunctionLinks(registry)) {\n for (const key of [`${link.schema}.${link.table}`, link.table]) {\n const existing = map.get(key);\n if (existing) existing.push(link);\n else map.set(key, [link]);\n }\n }\n\n return map;\n}\n","import { logger } from \"@rebasepro/server\";\nimport type { RawSqlRunner } from \"../../security/rls-enforcement\";\n\n/**\n * Trigger-based Change Data Capture (CDC).\n *\n * The preferred CDC source is the write-ahead log (logical replication), which\n * — like Supabase Realtime — sees *every* commit regardless of how it was made.\n * When logical replication is unavailable (managed Postgres without\n * `wal_level=logical`, no replication privilege, no `REPLICA IDENTITY`), this\n * trigger-based fallback provides the same guarantee at the row level:\n *\n * AFTER INSERT/UPDATE/DELETE trigger → pg_notify('rebase_cdc', payload)\n *\n * A single dedicated LISTEN client per backend instance consumes the channel\n * (see {@link CdcListener}) and feeds the change into the existing\n * `RealtimeService.notifyUpdate` pipeline, so subscribers see the change no\n * matter what wrote it — psql, a cron in another service, raw Drizzle/SQL, or\n * the Studio SQL editor.\n *\n * Provisioning runs from the framework's own bootstrap as the owner (server)\n * context, alongside the RLS role provisioning. It is idempotent.\n */\n\n/** Postgres NOTIFY channel carrying database-level change events. */\nexport const CDC_CHANNEL = \"rebase_cdc\";\n\n/** Schema-qualified name of the generic trigger function. */\nexport const CDC_TRIGGER_FUNCTION = \"rebase.rebase_cdc_notify\";\n\n/** Name of the per-table trigger (unqualified — triggers are namespaced by table). */\nexport const CDC_TRIGGER_NAME = \"rebase_cdc_trigger\";\n\n/**\n * pg_notify hard-caps payloads at 8000 bytes and *aborts the triggering\n * statement* if the limit is exceeded. We stay comfortably under it and, for\n * wide rows, fall back to an identity-only payload so CDC can never break a\n * write. 7900 leaves headroom for the JSON envelope keys.\n */\nconst MAX_NOTIFY_BYTES = 7900;\n\nconst quoteIdent = (name: string): string => `\"${name.replace(/\"/g, \"\\\"\\\"\")}\"`;\nconst quoteLiteral = (value: string): string => `'${value.replace(/'/g, \"''\")}'`;\n\n/**\n * SQL that (re)creates the generic CDC trigger function. Safe to run repeatedly:\n * `CREATE OR REPLACE` updates in place without dropping dependent triggers.\n *\n * The function emits `{ schema, table, op, row }`. The `row` is the full changed\n * tuple (NEW for insert/update, OLD for delete) so the consumer can route it to\n * a collection and extract the primary key. It is *not* trusted for delivery:\n * the consumer marks the row invalidated and each subscriber re-reads it under\n * its own RLS context, so a subscriber never receives a row it cannot read.\n */\nexport function buildCdcFunctionSql(): string {\n return `\nCREATE SCHEMA IF NOT EXISTS rebase;\n\nCREATE OR REPLACE FUNCTION ${CDC_TRIGGER_FUNCTION}() RETURNS trigger\nLANGUAGE plpgsql AS $rebase_cdc$\nDECLARE\n rec jsonb;\n payload text;\nBEGIN\n IF (TG_OP = 'DELETE') THEN\n rec := to_jsonb(OLD);\n ELSE\n rec := to_jsonb(NEW);\n END IF;\n\n payload := json_build_object(\n 'schema', TG_TABLE_SCHEMA,\n 'table', TG_TABLE_NAME,\n 'op', TG_OP,\n 'row', rec\n )::text;\n\n -- Never let CDC abort the write: if the full row overflows the pg_notify\n -- 8000-byte cap, emit an identity-only payload the consumer can still route\n -- (and refetch the authoritative row from).\n IF (octet_length(payload) > ${MAX_NOTIFY_BYTES}) THEN\n payload := json_build_object(\n 'schema', TG_TABLE_SCHEMA,\n 'table', TG_TABLE_NAME,\n 'op', TG_OP,\n 'row', CASE WHEN rec ? 'id' THEN jsonb_build_object('id', rec->'id') ELSE '{}'::jsonb END,\n 'truncated', true\n )::text;\n END IF;\n\n PERFORM pg_notify(${quoteLiteral(CDC_CHANNEL)}, payload);\n RETURN NULL;\nEND;\n$rebase_cdc$;\n`.trim();\n}\n\n/**\n * SQL that (re)attaches the CDC trigger to a single table. `DROP ... IF EXISTS`\n * before `CREATE` keeps it idempotent and picks up any function signature change.\n */\nexport function buildCdcTriggerSql(schema: string, table: string): string {\n const qualified = `${quoteIdent(schema)}.${quoteIdent(table)}`;\n return (\n `DROP TRIGGER IF EXISTS ${quoteIdent(CDC_TRIGGER_NAME)} ON ${qualified};\\n` +\n `CREATE TRIGGER ${quoteIdent(CDC_TRIGGER_NAME)} ` +\n `AFTER INSERT OR UPDATE OR DELETE ON ${qualified} ` +\n `FOR EACH ROW EXECUTE FUNCTION ${CDC_TRIGGER_FUNCTION}();`\n );\n}\n\nexport interface CdcTableRef {\n schema: string;\n table: string;\n}\n\nexport interface ProvisionResult {\n /** Tables the trigger was successfully attached to. */\n installed: CdcTableRef[];\n /** Tables that could not be provisioned (e.g. not yet migrated), with the error. */\n skipped: Array<CdcTableRef & { reason: string }>;\n}\n\n/**\n * Idempotently install the CDC trigger function and per-table triggers.\n *\n * Runs as the owner (server) connection at bootstrap. A table that does not yet\n * exist in the database (schema drift) is skipped with a warning rather than\n * aborting the whole install, so one un-migrated collection cannot disable CDC\n * for the rest.\n */\nexport async function provisionTriggerCdc(\n run: RawSqlRunner,\n tables: CdcTableRef[]\n): Promise<ProvisionResult> {\n // 1. The shared trigger function (once).\n await run(buildCdcFunctionSql());\n\n // 2. One trigger per managed table. De-duplicate identical refs.\n const seen = new Set<string>();\n const installed: CdcTableRef[] = [];\n const skipped: ProvisionResult[\"skipped\"] = [];\n\n for (const ref of tables) {\n const key = `${ref.schema}.${ref.table}`;\n if (seen.has(key)) continue;\n seen.add(key);\n\n try {\n await run(buildCdcTriggerSql(ref.schema, ref.table));\n installed.push(ref);\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n skipped.push({ ...ref, reason });\n logger.warn(\n `⚠️ [CDC] Could not attach change-capture trigger to \"${key}\" — ` +\n `is the table migrated? Writes to it won't emit database-level events.`,\n { detail: reason }\n );\n }\n }\n\n // Wiring detail. The single `Realtime source = …` line in the\n // bootstrapper is the fact a developer acts on; how many triggers it\n // took is for a diagnosis, and the skipped-table warning above still\n // fires on its own.\n logger.debug(\n `📡 [CDC] Trigger-based change capture provisioned on ${installed.length} table(s)` +\n (skipped.length ? ` (${skipped.length} skipped)` : \"\") + \".\"\n );\n\n return { installed, skipped };\n}\n","/**\n * A dedicated, self-healing Postgres `LISTEN` connection.\n *\n * Every cross-instance feature in the backend needs the same thing: one\n * connection *outside* the Drizzle pool that stays open, holds a `LISTEN`, and\n * comes back on its own after the database or the network drops it. CDC needed\n * it first; the channel bus needs it too. This is that connection, with the one\n * behaviour that matters to callers preserved: the **first** connect is\n * validated and rethrown, so a caller can fall back to a different strategy,\n * while every later drop is repaired quietly in the background.\n *\n * `LISTEN` is session state, so this connection must not go through a\n * transaction-mode pooler (PgBouncer): give it the direct database URL.\n */\n\nimport { Client as PgClient } from \"pg\";\nimport { logger } from \"@rebasepro/server\";\n\nexport interface PgNotifyListenerOptions {\n /** Direct Postgres connection string (must bypass a transaction-mode pooler). */\n connectionString: string;\n /** NOTIFY channel to LISTEN on. Must be a plain identifier — it is interpolated. */\n channel: string;\n /** Called for every notification payload received. */\n onPayload: (payload: string) => void | Promise<void>;\n /** Prefix for log lines, e.g. `\"[CDC]\"`. */\n logLabel: string;\n /** Delay before a reconnect attempt. */\n reconnectDelayMs?: number;\n}\n\nconst DEFAULT_RECONNECT_DELAY_MS = 3000;\n/** Guards the identifier interpolated into `LISTEN`. */\nconst SAFE_CHANNEL = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\nexport class PgNotifyListener {\n private client?: PgClient;\n private running = false;\n private reconnectTimer?: ReturnType<typeof setTimeout>;\n\n constructor(private readonly options: PgNotifyListenerOptions) {\n if (!SAFE_CHANNEL.test(options.channel)) {\n throw new Error(`Unsafe NOTIFY channel name \"${options.channel}\" — expected a plain SQL identifier.`);\n }\n }\n\n /** Whether the listener is meant to be connected right now. */\n get active(): boolean {\n return this.running;\n }\n\n /**\n * Connect and begin listening. Idempotent.\n *\n * Rejects if the *initial* connection or `LISTEN` fails, leaving the\n * listener stopped — callers use that to degrade deliberately instead of\n * running blind against a channel nothing is delivering.\n */\n async start(): Promise<void> {\n if (this.running) return;\n this.running = true;\n try {\n await this.connect({ initial: true });\n } catch (err) {\n this.running = false;\n throw err;\n }\n }\n\n /** Stop listening and release the connection. Idempotent. */\n async stop(): Promise<void> {\n this.running = false;\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = undefined;\n }\n if (this.client) {\n try {\n await this.client.end();\n } catch { /* ignore close errors */ }\n this.client = undefined;\n }\n }\n\n private async connect({ initial = false }: { initial?: boolean } = {}): Promise<void> {\n const { connectionString, channel, onPayload, logLabel } = this.options;\n // Held here rather than only inside the `try` so the failure path can\n // still reach it: everything below `connect()` can throw, and until\n // `this.client` is assigned nothing else in this class knows the\n // connection exists. Left unreleased it stays open on the server while\n // `scheduleReconnect` opens another — one leaked backend per attempt,\n // every few seconds, for as long as the failure lasts.\n let pending: PgClient | undefined;\n try {\n const client = new PgClient({ connectionString });\n pending = client;\n\n client.on(\"error\", (err) => {\n logger.error(`❌ ${logLabel} LISTEN client error`, { detail: err.message });\n this.scheduleReconnect();\n });\n\n client.on(\"end\", () => {\n if (this.running) {\n logger.warn(`⚠️ ${logLabel} LISTEN client disconnected unexpectedly.`);\n this.scheduleReconnect();\n }\n });\n\n client.on(\"notification\", (msg) => {\n if (!msg.payload) return;\n // A handler rejection must never surface as an unhandled\n // rejection inside the pg client's event emitter.\n Promise.resolve(onPayload(msg.payload)).catch((err) =>\n logger.error(`❌ ${logLabel} Error handling notification`, { error: err })\n );\n });\n\n await client.connect();\n await client.query(`LISTEN ${channel}`);\n this.client = client;\n // Adopted: `stop()` and `scheduleReconnect` will close it now.\n pending = undefined;\n logger.debug(`📡 ${logLabel} Listening on channel \"${channel}\".`);\n } catch (err) {\n // Never adopted, so nothing else will ever close it.\n if (pending) {\n try { await pending.end(); } catch { /* already dead */ }\n }\n // Surface the initial failure so callers can choose to fall back;\n // for reconnects, keep retrying quietly in the background.\n if (initial) throw err;\n logger.error(`❌ ${logLabel} Failed to connect LISTEN client`, { error: err });\n this.scheduleReconnect();\n }\n }\n\n private scheduleReconnect(): void {\n if (!this.running || this.reconnectTimer) return;\n\n this.reconnectTimer = setTimeout(async () => {\n this.reconnectTimer = undefined;\n if (!this.running) return;\n if (this.client) {\n try { await this.client.end(); } catch { /* ignore */ }\n this.client = undefined;\n }\n await this.connect();\n }, this.options.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS);\n }\n}\n","import { logger } from \"@rebasepro/server\";\nimport { CDC_CHANNEL } from \"./trigger-cdc\";\nimport { PgNotifyListener } from \"../pg-notify-listener\";\n\n/**\n * A single database change captured by the CDC triggers and delivered over the\n * `rebase_cdc` NOTIFY channel.\n */\nexport interface CdcChangeEvent {\n schema: string;\n table: string;\n op: \"INSERT\" | \"UPDATE\" | \"DELETE\";\n /**\n * The changed tuple (NEW for insert/update, OLD for delete). May be a\n * partial identity-only object when the full row overflowed the pg_notify\n * size cap — see {@link truncated}.\n */\n row: Record<string, unknown>;\n /** True when the row was reduced to its identity because it was too large to notify. */\n truncated?: boolean;\n}\n\n/**\n * Parse a `rebase_cdc` NOTIFY payload. Returns `null` for anything malformed so\n * a single bad message can never crash the listener.\n */\nexport function parseCdcPayload(payload: string): CdcChangeEvent | null {\n let parsed: unknown;\n try {\n parsed = JSON.parse(payload);\n } catch {\n return null;\n }\n if (!parsed || typeof parsed !== \"object\") return null;\n\n const obj = parsed as Record<string, unknown>;\n const schema = typeof obj.schema === \"string\" ? obj.schema : undefined;\n const table = typeof obj.table === \"string\" ? obj.table : undefined;\n const op = obj.op;\n if (!schema || !table) return null;\n if (op !== \"INSERT\" && op !== \"UPDATE\" && op !== \"DELETE\") return null;\n\n const row = obj.row && typeof obj.row === \"object\" ? (obj.row as Record<string, unknown>) : {};\n\n return {\n schema,\n table,\n op,\n row,\n truncated: obj.truncated === true\n };\n}\n\n/**\n * Dedicated Postgres LISTEN client for database-level CDC.\n *\n * A {@link PgNotifyListener} — a connection outside the Drizzle pool that stays\n * open and repairs itself — plus the parsing that turns a `rebase_cdc` payload\n * into a change event. Each backend instance runs one, so every instance\n * observes every committed change regardless of which instance (or external\n * process) made the write.\n */\nexport class CdcListener {\n private readonly listener: PgNotifyListener;\n\n constructor(connectionString: string, onEvent: (event: CdcChangeEvent) => void | Promise<void>) {\n this.listener = new PgNotifyListener({\n connectionString,\n channel: CDC_CHANNEL,\n logLabel: \"[CDC]\",\n onPayload: (payload) => {\n const event = parseCdcPayload(payload);\n if (!event) {\n logger.warn(\"⚠️ [CDC] Dropping unparseable change notification.\");\n return;\n }\n return onEvent(event);\n }\n });\n }\n\n /**\n * Connect and begin listening. Idempotent.\n *\n * The **initial** connection is validated synchronously: if it cannot be\n * established (or `LISTEN` is refused), this rejects so callers — notably\n * `REALTIME_CDC=auto` — can detect an unusable connection and fall back to\n * app-level realtime. Once the initial connection succeeds, later drops\n * self-heal in the background.\n */\n async start(): Promise<void> {\n if (this.listener.active) {\n logger.warn(\"⚠️ [CDC] CdcListener.start() called but already running. Ignoring.\");\n return;\n }\n await this.listener.start();\n }\n\n /** Stop listening and release the connection. */\n async stop(): Promise<void> {\n await this.listener.stop();\n }\n}\n","/**\n * The server's DDL bootstrapper, over a Drizzle handle.\n *\n * `createDdlBootstrapper` in `@rebasepro/server` wants a plain\n * `(sql: string) => Promise<rows>`; the driver's internal stores hold a Drizzle\n * database. This is the adapter between them, and it exists so the retry policy\n * has exactly one definition. A second copy of the SQLSTATE list living in the\n * driver is how the two drift apart, and the drift is invisible: both versions\n * work perfectly on every single-instance deployment.\n */\nimport { sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { createDdlBootstrapper, type DdlBootstrapper } from \"@rebasepro/server\";\n\n/**\n * A {@link DdlBootstrapper} that runs its statements through `db.execute`.\n *\n * @param db the Drizzle handle the calling store already holds\n * @param scope log prefix identifying the caller, e.g. `\"channel-presence\"`\n */\nexport function drizzleDdlBootstrapper(\n db: NodePgDatabase<Record<string, unknown>>,\n scope: string\n): DdlBootstrapper {\n return createDdlBootstrapper(async (statement: string) => {\n // `sql.raw`, because everything reaching this path is DDL assembled from\n // identifiers that were validated before they got here — there is no\n // parameter to bind, and Drizzle's tagged template would treat the whole\n // statement as one.\n const result = await db.execute(sql.raw(statement));\n return (result as unknown as { rows?: Record<string, unknown>[] }).rows ?? [];\n }, scope);\n}\n","/**\n * Ordered, replayable per-channel message history.\n *\n * Broadcast on its own is fire-and-forget to whoever is connected at the\n * instant it is sent: fine for presence and for \"someone saved\" notifications,\n * not enough for op-based collaborative editing, where a client that blinks\n * out for two seconds has to resync a whole document rather than catch up on\n * the four operations it missed. This adds the missing half — every retained\n * broadcast gets a per-channel sequence number, and a client can ask for\n * everything after the last one it saw.\n *\n * Three decisions worth stating, because each rules out a simpler-looking one:\n *\n * - **Retention is server-side and opt-in.** A channel is created by whoever\n * names it, so a client-supplied history depth would let any visitor commit\n * the backend to unbounded storage. And presence channels — the common case\n * — must not pay for this: with no rules configured nothing is written, no\n * table is created, and `broadcast` runs exactly the code it ran before.\n *\n * - **Sequence numbers come from the database, not from a counter in this\n * process.** They have to survive a restart and be shared across instances;\n * an in-memory counter would restart at 1 after a deploy and hand a\n * reconnecting client a replay from the wrong era, silently.\n *\n * - **The cursor row outlives the messages it numbered.** Pruning is what\n * makes retention affordable, but pruning the cursor along with the messages\n * would restart the sequence and make `sinceSeq` mean something different\n * before and after — the worst kind of bug, because replay would still\n * return rows and they would look plausible. Cursors are tiny and are kept\n * forever; see {@link prune}, which touches only `channel_messages`.\n */\n\nimport { sql } from \"drizzle-orm\";\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type { ChannelHistoryEntry, ChannelRetentionRule } from \"@rebasepro/types\";\nimport { logger } from \"@rebasepro/server\";\nimport { revokeInternalTableSql } from \"@rebasepro/common\";\nimport { drizzleDdlBootstrapper } from \"../schema/drizzle-ddl\";\n\n/** How many messages a replay returns when the caller does not say. */\nconst DEFAULT_REPLAY_LIMIT = 200;\n\n/**\n * Hard ceiling on one replay, whatever the caller asks for.\n *\n * A reconnecting client names its own `limit`, so this is the only thing\n * standing between a stale `sinceSeq` and a single frame carrying a channel's\n * entire retained history. A client that is further behind than this is told so\n * via `latestSeq` and can decide to resync wholesale instead of paging.\n */\nconst MAX_REPLAY_LIMIT = 1000;\n\n/** Minimum gap between two prunes of the same channel. */\nconst PRUNE_THROTTLE_MS = 30_000;\n\n/**\n * Parse a retention TTL into milliseconds.\n *\n * Accepts a raw millisecond count or a short duration string (`\"30s\"`, `\"15m\"`,\n * `\"24h\"`, `\"7d\"`). Returns undefined for anything unparseable, which the\n * caller treats as \"no TTL\" — a misspelt duration must not silently become an\n * aggressive one.\n */\nexport function parseTtlMs(ttl: number | string | undefined): number | undefined {\n if (ttl === undefined || ttl === null) return undefined;\n if (typeof ttl === \"number\") return Number.isFinite(ttl) && ttl > 0 ? ttl : undefined;\n\n const match = /^\\s*(\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h|d)\\s*$/i.exec(ttl);\n if (!match) {\n logger.warn(`⚠️ [ChannelHistory] Ignoring unparseable retention ttl \"${ttl}\" — expected e.g. \"30s\", \"15m\", \"24h\", \"7d\".`);\n return undefined;\n }\n const value = parseFloat(match[1]);\n const unit = match[2].toLowerCase();\n const multiplier = unit === \"ms\" ? 1\n : unit === \"s\" ? 1_000\n : unit === \"m\" ? 60_000\n : unit === \"h\" ? 3_600_000\n : 86_400_000;\n const ms = value * multiplier;\n return ms > 0 ? ms : undefined;\n}\n\n/**\n * Whether `channel` is covered by `rule`.\n *\n * Exact match, or a trailing `*` acting as a prefix. Not a general glob: this\n * decides what reaches disk, and a pattern language whose reach is not obvious\n * at a glance is the wrong tool for that job.\n */\nexport function channelMatchesRule(channel: string, rule: ChannelRetentionRule): boolean {\n const pattern = rule.match;\n if (pattern === \"*\") return true;\n if (pattern.endsWith(\"*\")) return channel.startsWith(pattern.slice(0, -1));\n return channel === pattern;\n}\n\n/** A rule with its TTL already resolved to milliseconds. */\nexport interface ResolvedRetention {\n limit?: number;\n ttlMs?: number;\n}\n\n/**\n * Persistence and replay for retained channels.\n *\n * Inert unless constructed with at least one rule: {@link enabled} is false,\n * {@link ensureTables} does nothing, and {@link retentionFor} answers undefined\n * for every channel, so the realtime service never reaches the SQL below.\n */\nexport class ChannelHistoryStore {\n private rules: ChannelRetentionRule[];\n /** Resolved rule per channel name, so the match runs once per channel. */\n private resolved = new Map<string, ResolvedRetention | null>();\n /** Channel → timestamp of its last prune, for {@link PRUNE_THROTTLE_MS}. */\n private lastPruned = new Map<string, number>();\n private tablesReady = false;\n\n constructor(private db: NodePgDatabase<Record<string, unknown>>, rules: ChannelRetentionRule[] = []) {\n this.rules = rules.filter(rule => {\n if (!rule?.match) {\n logger.warn(\"⚠️ [ChannelHistory] Ignoring a retention rule with no `match`.\");\n return false;\n }\n const hasBound = rule.limit !== undefined || rule.ttl !== undefined;\n if (!hasBound) {\n // Unbounded retention is almost never intended and cannot be\n // walked back once the table has grown, so it is refused rather\n // than honoured.\n logger.warn(`⚠️ [ChannelHistory] Retention rule \"${rule.match}\" sets neither \\`limit\\` nor \\`ttl\\` — ignoring it, as it would retain forever.`);\n return false;\n }\n return true;\n });\n }\n\n /** Whether any channel retains anything at all. */\n get enabled(): boolean {\n return this.rules.length > 0;\n }\n\n /**\n * The retention that applies to `channel`, or undefined when none does.\n *\n * First matching rule wins, so callers order them most-specific first.\n */\n retentionFor(channel: string): ResolvedRetention | undefined {\n if (!this.enabled || !channel) return undefined;\n\n const cached = this.resolved.get(channel);\n if (cached !== undefined) return cached ?? undefined;\n\n const rule = this.rules.find(r => channelMatchesRule(channel, r));\n const resolved: ResolvedRetention | null = rule\n ? { limit: rule.limit, ttlMs: parseTtlMs(rule.ttl) }\n : null;\n\n // Bounded by the number of distinct channel names seen, which is the\n // same thing the in-memory channel and presence maps are bounded by.\n this.resolved.set(channel, resolved);\n return resolved ?? undefined;\n }\n\n /**\n * Create the history tables. Idempotent, and a no-op when no rule is set —\n * a deployment that never retains anything gets no schema for it.\n */\n async ensureTables(): Promise<void> {\n if (!this.enabled || this.tablesReady) return;\n\n // Contained, retrying steps rather than one straight sequence — see the\n // note on `ChannelPresenceStore.ensureTables`. The failure mode here is\n // the same and the stakes are the same: the two `REVOKE`s at the end are\n // what keep retained broadcasts off the end-user role, and a lost create\n // race used to skip them.\n const ddl = drizzleDdlBootstrapper(this.db, \"channel-history\");\n\n await ddl.ensureObject(\"rebase schema\", \"CREATE SCHEMA IF NOT EXISTS rebase\");\n\n // The primary key is exactly the replay query's access path\n // (`channel = $1 AND seq > $2 ORDER BY seq`), so it needs no further\n // index of its own.\n await ddl.ensureObject(\"channel_messages table\", `\n CREATE TABLE IF NOT EXISTS rebase.channel_messages (\n channel TEXT NOT NULL,\n seq BIGINT NOT NULL,\n event TEXT NOT NULL,\n payload JSONB,\n sender_id TEXT,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n PRIMARY KEY (channel, seq)\n )\n `);\n\n // Only for the TTL arm of pruning; the limit arm rides the primary key.\n await ddl.ensureObject(\"channel_messages created_at index\", `\n CREATE INDEX IF NOT EXISTS idx_channel_messages_created\n ON rebase.channel_messages (created_at)\n `);\n\n // Never pruned — see the note at the top of this file. One row per\n // channel that has ever retained a message.\n await ddl.ensureObject(\"channel_cursors table\", `\n CREATE TABLE IF NOT EXISTS rebase.channel_cursors (\n channel TEXT PRIMARY KEY,\n last_seq BIGINT NOT NULL\n )\n `);\n\n // Retained broadcasts for every channel in one table, with no RLS: who\n // may replay a channel is decided before the read, by the channel gate\n // in `realtimeService.authorizeChannelAction` — a replay is answered\n // only for a client that has joined the channel, plus whatever an\n // installed `ChannelAuthorizer` adds. That gate is the entire reason\n // this table can sit outside the RLS model, so it fails closed; the\n // rule language it does not yet have is written up in\n // `docs/channel-authorization.md`. The driver's schema-wide\n // grant reaches these (created here, after it ran), so take the\n // privilege back.\n //\n // Driven off a probe of what exists rather than off who won each create.\n const [messagesReady, cursorsReady] = await Promise.all([\n ddl.isReadable(\"rebase.channel_messages\"),\n ddl.isReadable(\"rebase.channel_cursors\")\n ]);\n if (messagesReady) {\n await ddl.step(\"channel_messages revoke\", () =>\n this.db.execute(sql.raw(revokeInternalTableSql(\"rebase\", \"channel_messages\")))\n );\n }\n if (cursorsReady) {\n await ddl.step(\"channel_cursors revoke\", () =>\n this.db.execute(sql.raw(revokeInternalTableSql(\"rebase\", \"channel_cursors\")))\n );\n }\n\n if (!messagesReady || !cursorsReady) {\n // Left un-ready on purpose so the next call retries. Announcing\n // \"ready\" here is what would turn a half-created schema into replays\n // that answer empty forever.\n logger.warn(\n \"[ChannelHistory] Retained-channel tables are not both present; history is not ready yet.\"\n );\n return;\n }\n\n this.tablesReady = true;\n logger.info(`✅ [ChannelHistory] Retained channels ready (${this.rules.length} rule(s)).`);\n }\n\n /**\n * Append a broadcast and return the sequence number it was given.\n *\n * The sequence is allocated by the same statement that stores the message,\n * so a crash between the two is not a possibility. `ON CONFLICT DO UPDATE`\n * takes a row lock on the channel's cursor, which is what makes concurrent\n * broadcasts to one channel line up in a single order — and what keeps\n * different channels from contending with each other at all.\n */\n async append(\n channel: string,\n event: string,\n payload: unknown,\n senderId?: string\n ): Promise<{ seq: number; at: string }> {\n const result = await this.db.execute(sql`\n WITH next AS (\n INSERT INTO rebase.channel_cursors (channel, last_seq)\n VALUES (${channel}, 1)\n ON CONFLICT (channel)\n DO UPDATE SET last_seq = rebase.channel_cursors.last_seq + 1\n RETURNING last_seq\n )\n INSERT INTO rebase.channel_messages (channel, seq, event, payload, sender_id)\n SELECT ${channel}, next.last_seq, ${event}, ${JSON.stringify(payload ?? null)}::jsonb, ${senderId ?? null}\n FROM next\n RETURNING seq, created_at\n `);\n\n const row = result.rows[0] as { seq: string | number; created_at: Date | string } | undefined;\n if (!row) throw new Error(`Failed to append to channel history for \"${channel}\"`);\n\n return {\n // BIGINT comes back as a string from node-postgres; the wire type is\n // a number, and a channel would need 2^53 messages to notice.\n seq: Number(row.seq),\n at: row.created_at instanceof Date ? row.created_at.toISOString() : String(row.created_at)\n };\n }\n\n /**\n * Everything retained for `channel` after `sinceSeq`, oldest first.\n *\n * `latestSeq` is reported whether or not the messages were capped, so a\n * client that is further behind than one page can tell.\n */\n async replay(\n channel: string,\n sinceSeq = 0,\n limit = DEFAULT_REPLAY_LIMIT\n ): Promise<{ messages: ChannelHistoryEntry[]; latestSeq: number }> {\n const capped = Math.max(1, Math.min(Math.floor(limit) || DEFAULT_REPLAY_LIMIT, MAX_REPLAY_LIMIT));\n const after = Number.isFinite(sinceSeq) && sinceSeq > 0 ? Math.floor(sinceSeq) : 0;\n\n const result = await this.db.execute(sql`\n SELECT seq, event, payload, sender_id, created_at\n FROM rebase.channel_messages\n WHERE channel = ${channel} AND seq > ${after}\n ORDER BY seq ASC\n LIMIT ${capped}\n `);\n\n const messages = (result.rows as Array<{\n seq: string | number;\n event: string;\n payload: unknown;\n sender_id: string | null;\n created_at: Date | string;\n }>).map(row => ({\n seq: Number(row.seq),\n event: row.event,\n payload: row.payload,\n senderId: row.sender_id ?? undefined,\n at: row.created_at instanceof Date ? row.created_at.toISOString() : String(row.created_at)\n }));\n\n // Read from the cursor rather than from the messages: the cursor is the\n // authority on how far the channel has got, and still says so after\n // pruning has removed the messages it counted.\n const cursor = await this.db.execute(sql`\n SELECT last_seq FROM rebase.channel_cursors WHERE channel = ${channel}\n `);\n const cursorRow = cursor.rows[0] as { last_seq: string | number } | undefined;\n const latestSeq = cursorRow ? Number(cursorRow.last_seq) : 0;\n\n return { messages, latestSeq };\n }\n\n /**\n * One retained message by its address.\n *\n * This is what makes the cross-instance pointer path work: a broadcast too\n * large to travel inside a `pg_notify` payload is already stored here, so\n * the notification carries `(channel, seq)` and each receiving instance\n * reads the body back. Returns null when the message has since been pruned\n * — a receiver that is that far behind has nothing useful to deliver, and\n * the client's own `channel_history` replay is the repair path.\n */\n async getBySeq(channel: string, seq: number): Promise<ChannelHistoryEntry | null> {\n const result = await this.db.execute(sql`\n SELECT seq, event, payload, sender_id, created_at\n FROM rebase.channel_messages\n WHERE channel = ${channel} AND seq = ${seq}\n `);\n\n const row = result.rows[0] as {\n seq: string | number;\n event: string;\n payload: unknown;\n sender_id: string | null;\n created_at: Date | string;\n } | undefined;\n if (!row) return null;\n\n return {\n seq: Number(row.seq),\n event: row.event,\n payload: row.payload,\n senderId: row.sender_id ?? undefined,\n at: row.created_at instanceof Date ? row.created_at.toISOString() : String(row.created_at)\n };\n }\n\n /**\n * Enforce a channel's retention bounds.\n *\n * Throttled per channel, so a burst of operations prunes once rather than\n * once per message — the cost then tracks elapsed time instead of write\n * volume, which is what makes retention affordable on a hot channel.\n */\n async prune(channel: string, retention: ResolvedRetention): Promise<number> {\n const now = Date.now();\n const last = this.lastPruned.get(channel) ?? 0;\n if (now - last < PRUNE_THROTTLE_MS) return 0;\n this.lastPruned.set(channel, now);\n\n let deleted = 0;\n\n if (retention.ttlMs !== undefined) {\n const result = await this.db.execute(sql`\n DELETE FROM rebase.channel_messages\n WHERE channel = ${channel}\n AND created_at < NOW() - MAKE_INTERVAL(secs => ${retention.ttlMs / 1000})\n `);\n deleted += result.rowCount ?? 0;\n }\n\n if (retention.limit !== undefined && retention.limit > 0) {\n // OFFSET past the newest `limit` rows to find the highest seq that\n // is no longer wanted, then delete everything at or below it. Fewer\n // rows than the limit leaves the subquery empty, and the comparison\n // with NULL deletes nothing.\n const result = await this.db.execute(sql`\n DELETE FROM rebase.channel_messages\n WHERE channel = ${channel}\n AND seq <= (\n SELECT seq FROM rebase.channel_messages\n WHERE channel = ${channel}\n ORDER BY seq DESC\n OFFSET ${Math.floor(retention.limit)} LIMIT 1\n )\n `);\n deleted += result.rowCount ?? 0;\n }\n\n return deleted;\n }\n\n /** Forget throttle and match caches. Called on shutdown. */\n clear(): void {\n this.resolved.clear();\n this.lastPruned.clear();\n }\n}\n","/**\n * The shared presence roster.\n *\n * Broadcast only ever needed *fan-out* to work across instances — a frame goes\n * out, whoever is connected receives it. Presence needs more than that, because\n * `presence_state` is a question (\"who is in this document?\") and a per-process\n * `Map` can only answer for the clients that happen to share a replica with the\n * asker. Two people editing the same scene through different pods would each\n * see an empty room while broadcasting cursors at each other perfectly.\n *\n * So presence gets one row per tracked client, in Postgres, readable by every\n * instance. Three consequences worth stating:\n *\n * - **The table is the roster; the in-process map is a cache of our own\n * clients.** Reads answer from the table when this store is active, so the\n * answer is the same whichever instance is asked.\n *\n * - **`last_seen` is the liveness signal, and it is already there.** The client\n * heartbeats presence every ~20 s against a 30 s window; the sweep that has\n * always reaped local stale entries now also reaps rows belonging to\n * instances that stopped writing — which is exactly what a crashed pod looks\n * like. Crash recovery is a property of the TTL, not a separate mechanism.\n *\n * - **The sweep deletes with `RETURNING`.** Whichever instance wins the delete\n * is the one that announces the departures, so a stale client produces one\n * `presence_diff` for the cluster rather than one per replica.\n */\n\nimport { sql } from \"drizzle-orm\";\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { revokeInternalTableSql } from \"@rebasepro/common\";\nimport { drizzleDdlBootstrapper } from \"../schema/drizzle-ddl\";\n\n/** A tracked client, as any instance sees it. */\nexport interface PresenceRow {\n channel: string;\n clientId: string;\n state: Record<string, unknown>;\n}\n\nexport class ChannelPresenceStore {\n private tablesReady = false;\n\n constructor(\n private readonly db: NodePgDatabase<Record<string, unknown>>,\n private readonly instanceId: string\n ) {}\n\n /**\n * Create the roster table. Idempotent, and safe to run on every instance at\n * once.\n *\n * Written as separate contained steps rather than one straight sequence for\n * a reason that only bites with more than one replica, which is exactly the\n * deployment shape this table exists to serve: `CREATE … IF NOT EXISTS`\n * reads the catalog and then writes to it non-atomically, so peers booting\n * together collide, and the loser used to abandon everything after it —\n * including the trailing `REVOKE`. That revoke is the only thing keeping the\n * roster off the end-user role, so losing a boot race silently left the\n * whole channel roster readable by every signed-in user.\n *\n * `tablesReady` is now set from a probe of what exists, not from having been\n * the instance that created it.\n */\n async ensureTables(): Promise<void> {\n if (this.tablesReady) return;\n\n const ddl = drizzleDdlBootstrapper(this.db, \"channel-presence\");\n\n await ddl.ensureObject(\"rebase schema\", \"CREATE SCHEMA IF NOT EXISTS rebase\");\n\n // Keyed by (channel, client_id): a client id is globally unique, so the\n // instance is a column rather than part of the identity — a client that\n // reconnects onto another replica replaces its own row instead of\n // appearing twice in the roster.\n await ddl.ensureObject(\"channel_presence table\", `\n CREATE TABLE IF NOT EXISTS rebase.channel_presence (\n channel TEXT NOT NULL,\n client_id TEXT NOT NULL,\n instance_id TEXT NOT NULL,\n state JSONB NOT NULL DEFAULT '{}'::jsonb,\n last_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n PRIMARY KEY (channel, client_id)\n )\n `);\n\n // The sweep's access path; the roster read rides the primary key.\n await ddl.ensureObject(\"channel_presence last_seen index\", `\n CREATE INDEX IF NOT EXISTS idx_channel_presence_last_seen\n ON rebase.channel_presence (last_seen)\n `);\n\n // The roster of every client on every channel, with no RLS — a row\n // policy has nothing to match on here, since a channel name is a string\n // a client invents rather than a row anyone owns. What guards it is the\n // channel gate in `realtimeService.authorizeChannelAction`: presence is\n // readable only to a client that has joined the channel, plus whatever\n // an installed `ChannelAuthorizer` adds. That gate is the entire reason\n // this table can sit outside the RLS model, so it fails closed —\n // see `docs/channel-authorization.md` for what it does *not*\n // yet decide. Revoke the schema-wide grant the driver handed out\n // before this table existed.\n //\n // Driven off the probe, not off who won the create: the privilege has to\n // come off whether this instance created the table or found it.\n if (await ddl.isReadable(\"rebase.channel_presence\")) {\n await ddl.step(\"channel_presence revoke\", () =>\n this.db.execute(sql.raw(revokeInternalTableSql(\"rebase\", \"channel_presence\")))\n );\n this.tablesReady = true;\n }\n }\n\n /** Record (or refresh) a client's presence. */\n async track(channel: string, clientId: string, state: Record<string, unknown>): Promise<void> {\n await this.db.execute(sql`\n INSERT INTO rebase.channel_presence (channel, client_id, instance_id, state, last_seen)\n VALUES (${channel}, ${clientId}, ${this.instanceId}, ${JSON.stringify(state ?? {})}::jsonb, NOW())\n ON CONFLICT (channel, client_id) DO UPDATE\n SET state = EXCLUDED.state,\n instance_id = EXCLUDED.instance_id,\n last_seen = NOW()\n `);\n }\n\n /** Drop one client's presence in one channel. */\n async remove(channel: string, clientId: string): Promise<void> {\n await this.db.execute(sql`\n DELETE FROM rebase.channel_presence\n WHERE channel = ${channel} AND client_id = ${clientId}\n `);\n }\n\n /** Drop a client from every channel — used when its socket closes. */\n async removeClient(clientId: string): Promise<void> {\n await this.db.execute(sql`\n DELETE FROM rebase.channel_presence WHERE client_id = ${clientId}\n `);\n }\n\n /** The global roster for a channel. */\n async roster(channel: string): Promise<Record<string, Record<string, unknown>>> {\n const result = await this.db.execute(sql`\n SELECT client_id, state FROM rebase.channel_presence WHERE channel = ${channel}\n `);\n\n const presences: Record<string, Record<string, unknown>> = {};\n for (const row of result.rows as Array<{ client_id: string; state: Record<string, unknown> | null }>) {\n presences[row.client_id] = row.state ?? {};\n }\n return presences;\n }\n\n /**\n * Reap rows this instance is not responsible for and that have gone quiet.\n *\n * Own rows are excluded because the in-process sweep already handles them —\n * and handles them better, since it can tell \"the socket is gone\" from \"the\n * heartbeat is late\". What is left is precisely the interesting case: rows\n * written by an instance that is no longer writing.\n *\n * Returns what was removed, so the caller can announce it.\n */\n async sweepStale(ttlMs: number): Promise<PresenceRow[]> {\n const result = await this.db.execute(sql`\n DELETE FROM rebase.channel_presence\n WHERE instance_id <> ${this.instanceId}\n AND last_seen < NOW() - MAKE_INTERVAL(secs => ${ttlMs / 1000})\n RETURNING channel, client_id, state\n `);\n\n return (result.rows as Array<{ channel: string; client_id: string; state: Record<string, unknown> | null }>)\n .map(row => ({ channel: row.channel, clientId: row.client_id, state: row.state ?? {} }));\n }\n\n /**\n * Remove every row this instance owns. Called on graceful shutdown so a\n * rolling deploy does not leave a TTL window of ghosts in every roster.\n */\n async removeInstance(): Promise<void> {\n await this.db.execute(sql`\n DELETE FROM rebase.channel_presence WHERE instance_id = ${this.instanceId}\n `);\n }\n}\n","/**\n * Runtime pieces of the channel bus that are not the contract itself.\n *\n * The interface, the frame shape and the implementer's contract live in\n * `@rebasepro/types` (`types/channel_bus.ts`), so a transport shipped as its own\n * package — a Redis one, say — depends on the contract and not on this database\n * adapter. They are re-exported here for convenience: code already importing\n * from the adapter should not have to know where the types are declared.\n */\n\nimport type { ChannelBusFrame } from \"@rebasepro/types\";\n\nexport type {\n ChannelBus,\n ChannelBusFrame,\n ChannelBusHandler,\n ChannelBusConfig,\n ChannelBusSetting\n} from \"@rebasepro/types\";\nexport { isChannelBusInstance } from \"@rebasepro/types\";\n\n/**\n * The default: no cross-instance delivery at all.\n *\n * This is what every deployment ran before the bus existed, and what a\n * single-instance deployment should keep running — `publish` resolves without\n * touching the network, so the broadcast path is the same handful of `ws.send`\n * calls it always was.\n */\nexport class MemoryChannelBus {\n readonly kind = \"memory\" as const;\n readonly maxFrameBytes = Infinity;\n\n async start(): Promise<void> { /* nothing to connect */ }\n\n async publish(): Promise<void> { /* nowhere to publish to */ }\n\n async stop(): Promise<void> { /* nothing to release */ }\n}\n\n/** Encoded size of a frame, for a transport's size check. */\nexport function frameByteLength(frame: ChannelBusFrame): number {\n return Buffer.byteLength(JSON.stringify(frame), \"utf8\");\n}\n","/**\n * Channel bus over Postgres LISTEN/NOTIFY.\n *\n * Chosen because it needs nothing that a Rebase deployment does not already\n * have — the same database, the same direct URL the CDC listener uses. Three\n * properties of `NOTIFY` shape everything below:\n *\n * - **8000 bytes per payload.** Presence and cursors fit with room to spare; a\n * scene snapshot does not. Rather than truncate or drop, an oversized frame\n * on a *retained* channel is published as a pointer — the body is already in\n * `rebase.channel_messages` with a sequence number, so the receiver reads it\n * back. That is the same trick the entity path uses (notify an address,\n * refetch the row), applied to a different table. On an ephemeral channel\n * there is nothing to point at, so the publish is refused loudly instead of\n * reaching some instances and not others.\n *\n * - **A notify is a query on the primary database.** Not a slow one, but it\n * competes with the application's real queries, and that — not throughput —\n * is what actually limits this transport. Measured, it carried ~10k\n * cross-instance messages/second and stayed flat out to eight instances; what\n * it should not do is spend 10k queries/second of the database's budget on\n * cursor movement. Hence the batching below.\n *\n * - **Delivery is best-effort.** Retained channels repair themselves through\n * the client's history replay, so a lost frame costs a live update rather\n * than correctness. That is what makes coalescing safe.\n */\n\nimport { sql } from \"drizzle-orm\";\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { logger } from \"@rebasepro/server\";\nimport { PgNotifyListener } from \"../pg-notify-listener\";\nimport { ChannelBus, ChannelBusFrame, ChannelBusHandler, frameByteLength } from \"./ChannelBus\";\n\n/** NOTIFY channel carrying channel-bus frames. */\nexport const CHANNEL_BUS_NOTIFY_CHANNEL = \"rebase_channel_bus\";\n\n/**\n * Postgres refuses a NOTIFY payload of 8000 bytes or more. The margin below it\n * is for nothing in particular — it is there so that a payload which passes this\n * check cannot fail at the server for being a few bytes over.\n */\nexport const PG_NOTIFY_MAX_PAYLOAD_BYTES = 7500;\n\n/**\n * How long a batching window stays open.\n *\n * Ten milliseconds is below the threshold where a human notices a cursor lag,\n * and it is the difference between one query per message and one query per\n * window under load. Set to 0 to disable coalescing entirely.\n */\nexport const DEFAULT_BATCH_WINDOW_MS = 10;\n\n/** JSON overhead per frame inside a batch: the wrapping array's comma. */\nconst BATCH_SEPARATOR_BYTES = 1;\n/** JSON overhead of the batch envelope itself: `{\"batch\":[]}`. */\nconst BATCH_ENVELOPE_BYTES = 12;\n\ninterface PendingFrame {\n frame: ChannelBusFrame;\n bytes: number;\n resolve: () => void;\n reject: (error: unknown) => void;\n}\n\nexport class PostgresChannelBus implements ChannelBus {\n readonly kind = \"postgres\" as const;\n readonly maxFrameBytes = PG_NOTIFY_MAX_PAYLOAD_BYTES;\n\n private listener?: PgNotifyListener;\n private readonly batchWindowMs: number;\n\n /**\n * Frames waiting for the current window to close.\n *\n * The window is opened by a publish that found none open, and that publish\n * is sent *immediately* rather than joining a batch — see {@link publish}.\n */\n private pending: PendingFrame[] = [];\n private pendingBytes = BATCH_ENVELOPE_BYTES;\n private windowTimer?: ReturnType<typeof setTimeout>;\n private stopped = false;\n\n constructor(\n private readonly db: NodePgDatabase<Record<string, unknown>>,\n private readonly connectionString: string,\n options: { batchWindowMs?: number } = {}\n ) {\n const configured = options.batchWindowMs;\n this.batchWindowMs = typeof configured === \"number\" && configured >= 0\n ? configured\n : DEFAULT_BATCH_WINDOW_MS;\n }\n\n async start(handler: ChannelBusHandler): Promise<void> {\n this.stopped = false;\n this.listener = new PgNotifyListener({\n connectionString: this.connectionString,\n channel: CHANNEL_BUS_NOTIFY_CHANNEL,\n logLabel: \"[ChannelBus]\",\n onPayload: async (payload) => {\n const frames = parseChannelBusPayload(payload);\n if (!frames.length) {\n logger.warn(\"⚠️ [ChannelBus] Dropping unparseable payload.\");\n return;\n }\n // In order: a batch preserves the sender's publish order, and a\n // retained channel's consumers rely on it.\n for (const frame of frames) await handler(frame);\n }\n });\n await this.listener.start();\n }\n\n /**\n * Publish, coalescing under load.\n *\n * The window is *leading edge*: a publish arriving when no window is open is\n * sent straight away and opens one, so an idle channel pays no added latency\n * at all. Frames arriving while it is open are collected and leave together\n * when it closes. The effect is that cost tracks elapsed time rather than\n * message count — one query per window instead of one per message — which is\n * the same shape as the retention pruning throttle, for the same reason.\n *\n * The returned promise settles when the frame has actually left, not when it\n * was queued, so the contract (\"reaches the other instances, or rejects\")\n * still holds.\n */\n async publish(frame: ChannelBusFrame): Promise<void> {\n if (this.batchWindowMs === 0 || this.stopped) {\n await this.send([frame]);\n return;\n }\n\n if (!this.windowTimer) {\n this.openWindow();\n await this.send([frame]);\n return;\n }\n\n const bytes = frameByteLength(frame) + BATCH_SEPARATOR_BYTES;\n\n // A batch is one NOTIFY payload, so the 8 KB ceiling applies to the\n // whole batch. Send what we have rather than let the frame push it over.\n if (this.pending.length && this.pendingBytes + bytes > this.maxFrameBytes) {\n this.flush();\n }\n\n return new Promise<void>((resolve, reject) => {\n this.pending.push({ frame, bytes, resolve, reject });\n this.pendingBytes += bytes;\n });\n }\n\n async stop(): Promise<void> {\n this.stopped = true;\n if (this.windowTimer) {\n clearTimeout(this.windowTimer);\n this.windowTimer = undefined;\n }\n // Anything still queued belongs to clients that are already waiting on\n // it; dropping it on shutdown would be a silent loss where a flush costs\n // one more query.\n this.flush();\n await this.listener?.stop();\n this.listener = undefined;\n }\n\n private openWindow(): void {\n this.windowTimer = setTimeout(() => {\n this.windowTimer = undefined;\n if (this.pending.length) {\n // Still busy: send this window's frames and open the next one,\n // so a sustained stream keeps costing one query per window.\n this.flush();\n this.openWindow();\n }\n // Otherwise leave it closed, so the next publish after a quiet\n // moment goes out immediately.\n }, this.batchWindowMs);\n\n // Housekeeping must never hold the process open.\n (this.windowTimer as unknown as { unref?: () => void }).unref?.();\n }\n\n /** Send everything queued and settle the promises waiting on it. */\n private flush(): void {\n if (!this.pending.length) return;\n\n const batch = this.pending;\n this.pending = [];\n this.pendingBytes = BATCH_ENVELOPE_BYTES;\n\n this.send(batch.map(p => p.frame))\n .then(() => { for (const p of batch) p.resolve(); })\n .catch((error) => { for (const p of batch) p.reject(error); });\n }\n\n /**\n * One NOTIFY.\n *\n * A single frame goes out in the plain, unwrapped shape. That is not just\n * economy: during a rolling deploy an instance running the previous build\n * understands only that shape, and low-rate traffic — presence, the tail of\n * a session — is exactly what is flowing while pods restart. Batching only\n * appears under load, which shrinks the mixed-version window to almost\n * nothing.\n */\n private async send(frames: ChannelBusFrame[]): Promise<void> {\n if (!frames.length) return;\n const payload = frames.length === 1\n ? JSON.stringify(frames[0])\n : JSON.stringify({ batch: frames });\n\n await this.db.execute(sql`SELECT pg_notify(${CHANNEL_BUS_NOTIFY_CHANNEL}, ${payload})`);\n }\n}\n\n/**\n * Parse a bus payload into the frames it carries.\n *\n * Accepts both wire shapes — a bare frame and a `{ batch: [...] }` envelope —\n * so an instance on the new build understands one on the old. Returns an empty\n * array for anything unrecognisable: a malformed or future-versioned message\n * must never take the listener down.\n */\nexport function parseChannelBusPayload(payload: string): ChannelBusFrame[] {\n let parsed: unknown;\n try {\n parsed = JSON.parse(payload);\n } catch {\n return [];\n }\n if (!parsed || typeof parsed !== \"object\") return [];\n\n const batch = (parsed as { batch?: unknown }).batch;\n if (Array.isArray(batch)) {\n return batch\n .map(entry => coerceFrame(entry))\n .filter((frame): frame is ChannelBusFrame => frame !== null);\n }\n\n const single = coerceFrame(parsed);\n return single ? [single] : [];\n}\n\n/**\n * Parse a single bus frame, returning null for anything that is not a frame we\n * understand.\n */\nexport function parseChannelBusFrame(payload: string): ChannelBusFrame | null {\n try {\n return coerceFrame(JSON.parse(payload));\n } catch {\n return null;\n }\n}\n\nfunction coerceFrame(value: unknown): ChannelBusFrame | null {\n if (!value || typeof value !== \"object\") return null;\n\n const obj = value as Record<string, unknown>;\n const sid = typeof obj.sid === \"string\" ? obj.sid : undefined;\n const channel = typeof obj.channel === \"string\" ? obj.channel : undefined;\n if (!sid || !channel) return null;\n\n switch (obj.kind) {\n case \"broadcast\":\n if (typeof obj.event !== \"string\") return null;\n return {\n kind: \"broadcast\",\n sid,\n channel,\n event: obj.event,\n from: typeof obj.from === \"string\" ? obj.from : undefined,\n seq: typeof obj.seq === \"number\" ? obj.seq : undefined,\n payload: obj.payload\n };\n case \"broadcast_ref\":\n if (typeof obj.seq !== \"number\") return null;\n return {\n kind: \"broadcast_ref\",\n sid,\n channel,\n from: typeof obj.from === \"string\" ? obj.from : undefined,\n seq: obj.seq\n };\n case \"presence_diff\":\n return {\n kind: \"presence_diff\",\n sid,\n channel,\n joins: (obj.joins ?? {}) as Record<string, Record<string, unknown>>,\n leaves: (obj.leaves ?? {}) as Record<string, Record<string, unknown>>\n };\n default:\n return null;\n }\n}\n","/**\n * Resolution of the channel bus from config, environment, or a supplied instance.\n *\n * Opt-in, like every other cross-cutting realtime switch here: with nothing\n * configured a deployment gets the memory bus and behaves exactly as it did\n * before this existed. Unlike `REALTIME_CDC=auto`, there is no \"try it and see\"\n * default — a bus changes where messages go, and quietly turning on a Postgres\n * NOTIFY per broadcast because a direct URL happened to be set is not a\n * decision to make on the user's behalf.\n *\n * Two transports ship, and neither adds a service to a deployment. A third is\n * not a code change here: `realtime.bus` also accepts an already-constructed\n * {@link ChannelBus}, so a transport published as its own package plugs in\n * without this file learning about it. See `@rebasepro/types` →\n * `types/channel_bus.ts` for the contract such a package implements.\n */\n\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { isChannelBusInstance, type ChannelBus, type ChannelBusConfig, type ChannelBusSetting } from \"@rebasepro/types\";\nimport { logger } from \"@rebasepro/server\";\nimport { MemoryChannelBus } from \"./ChannelBus\";\nimport { PostgresChannelBus } from \"./PostgresChannelBus\";\n\nexport * from \"./ChannelBus\";\nexport {\n PostgresChannelBus,\n CHANNEL_BUS_NOTIFY_CHANNEL,\n PG_NOTIFY_MAX_PAYLOAD_BYTES,\n DEFAULT_BATCH_WINDOW_MS,\n parseChannelBusFrame,\n parseChannelBusPayload\n} from \"./PostgresChannelBus\";\n\nexport interface ChannelBusDeps {\n db: NodePgDatabase<Record<string, unknown>>;\n /**\n * Direct (non-pooled) Postgres URL for the LISTEN client. `LISTEN` is\n * session state, so behind PgBouncer in transaction mode this must be the\n * database itself and not the pooler.\n */\n directUrl?: string;\n}\n\n/**\n * Merge `REALTIME_CHANNEL_BUS` into the configured bus.\n *\n * The environment wins over a *named* built-in, so the transport can be changed\n * per deployment without a rebuild — the same reason `REALTIME_CDC` is an env\n * var. It does **not** win over a supplied instance: the env var can only name\n * transports this package knows how to construct, so honouring it there would\n * mean silently discarding the object the application handed us.\n */\nexport function resolveChannelBusSetting(configured?: ChannelBusSetting): ChannelBusSetting {\n const raw = (process.env.REALTIME_CHANNEL_BUS || \"\").trim().toLowerCase();\n\n if (isChannelBusInstance(configured)) {\n if (raw && raw !== configured.kind) {\n logger.warn(\n `⚠️ [ChannelBus] REALTIME_CHANNEL_BUS=\"${raw}\" is ignored because realtime.bus was given a ` +\n `\"${configured.kind}\" transport instance directly. Remove one of the two to make the intent clear.`\n );\n }\n return configured;\n }\n\n if (!raw) return configured ?? { type: \"memory\" };\n\n if (raw !== \"memory\" && raw !== \"postgres\") {\n logger.warn(\n `⚠️ [ChannelBus] Unknown REALTIME_CHANNEL_BUS value \"${raw}\" — expected memory|postgres, or pass a ` +\n \"ChannelBus instance as realtime.bus for a transport that ships separately. Falling back to the \" +\n \"configured bus.\"\n );\n return configured ?? { type: \"memory\" };\n }\n\n // Keep the configured options (an explicit connection string) when the env\n // var only restates the type it was already set to.\n if (configured?.type === raw) return configured;\n return raw === \"memory\" ? { type: \"memory\" } : { type: \"postgres\" };\n}\n\n/**\n * Produce the bus a setting asks for.\n *\n * An instance is handed straight back — constructing it was the application's\n * job, and this function has nothing to add. A named built-in that turns out to\n * be unusable degrades to the memory bus, with the reason logged, rather than\n * throwing: a misconfigured bus should cost a deployment its cross-instance\n * fan-out, not its ability to boot.\n */\nexport function createChannelBus(setting: ChannelBusSetting, deps: ChannelBusDeps): ChannelBus {\n if (isChannelBusInstance(setting)) return setting;\n\n switch (setting.type) {\n case \"postgres\": {\n const connectionString = setting.connectionString || deps.directUrl;\n if (!connectionString) {\n logger.warn(\n \"⚠️ [ChannelBus] realtime.bus is \\\"postgres\\\" but no direct database URL is available \" +\n \"(set DATABASE_DIRECT_URL or realtime.bus.connectionString) — channel broadcast and presence \" +\n \"stay per-instance.\"\n );\n return new MemoryChannelBus();\n }\n return new PostgresChannelBus(deps.db, connectionString, {\n batchWindowMs: setting.batchWindowMs\n });\n }\n case \"memory\":\n default:\n return new MemoryChannelBus();\n }\n}\n","import { WebSocket } from \"ws\";\nimport { EventEmitter } from \"events\";\nimport { Client as PgClient } from \"pg\";\nimport { randomUUID } from \"crypto\";\nimport { DataService } from \"./dataService\";\n\nimport { ANONYMOUS_USER_ID, FetchCollectionProps, ListenCollectionProps, ListenOneProps, DataDriver, CollectionUpdateMessage, SingleUpdateMessage, CollectionPatchMessage, WebSocketMessage, FilterValues, LogicalCondition, OrderByTuple, CollectionConfig, RebaseCallContext, resolveClientListLimit, ListLimitError } from \"@rebasepro/types\";\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { sql as drizzleSql } from \"drizzle-orm\";\nimport { RealtimeProvider, CollectionSubscriptionConfig, SingleSubscriptionConfig } from \"../interfaces\";\nimport { PostgresCollectionRegistry } from \"../collections/PostgresCollectionRegistry\";\nimport { buildPropertyCallbacks, getTableName, OrderBySpecError, parseOrderBySpecStrict } from \"@rebasepro/common\";\nimport { applyAuthContext } from \"../security/rls-enforcement\";\nimport { buildJunctionLinkMap, type JunctionLink } from \"./cdc/junction-tables\";\nimport { logger } from \"@rebasepro/server\";\nimport { sanitizeErrorForClient } from \"../utils/pg-error-utils\";\nimport { CdcListener, type CdcChangeEvent } from \"./cdc/CdcListener\";\nimport { deriveRowAddress, getPrimaryKeys, type PrimaryKeyInfo } from \"./collection-helpers\";\nimport { ChannelHistoryStore, type ResolvedRetention } from \"./channel-history\";\nimport { ChannelPresenceStore } from \"./channel-presence\";\nimport { ChannelBus, ChannelBusFrame, MemoryChannelBus, frameByteLength } from \"./channel-bus\";\nimport type { ChannelHistoryEntry, ChannelRetentionRule } from \"@rebasepro/types\";\n\n/** Channel name used for Postgres LISTEN/NOTIFY cross-instance realtime. */\nconst PG_NOTIFY_CHANNEL = \"rebase_entity_changes\";\n\n/**\n * Auth context stored per-subscription so real-time refetches respect RLS.\n * Mirrors the session variables set by PostgresBackendDriver.withAuth().\n */\nexport interface SubscriptionAuthContext {\n uid: string;\n roles: string[];\n}\n\n/** What a channel frame is asking to do. */\nexport type ChannelAction = \"join\" | \"broadcast\" | \"presence\" | \"history\";\n\n/** Everything an authorizer is told about the frame it is asked to allow. */\nexport interface ChannelAuthorizationRequest {\n /** The channel the frame names, exactly as the client wrote it. */\n channel: string;\n action: ChannelAction;\n /** The socket, not the principal — one user may hold several. */\n clientId: string;\n /** The socket's authenticated principal, or the anonymous one. */\n user?: SubscriptionAuthContext;\n}\n\n/**\n * The extension point for channel access rules.\n *\n * **This is deliberately not a product API yet.** The rule *language* — a\n * config key, a per-pattern DSL, how it composes with `securityRules` — is an\n * open design question (see `docs/channel-authorization.md`), and\n * inventing one here would be inventing the answer. What exists is the single\n * place every channel frame passes through, so that whatever shape the rules\n * eventually take has exactly one seam to plug into and no arm of the switch\n * can be forgotten.\n *\n * Returning `false` — or throwing — refuses the frame. It is consulted *after*\n * the membership floor below, so an authorizer can only ever narrow access,\n * never widen it.\n */\nexport type ChannelAuthorizer = (request: ChannelAuthorizationRequest) => boolean | Promise<boolean>;\n\ninterface DataDriverWithData extends DataDriver {\n data: unknown;\n}\n\ntype RealTimeListenCollectionProps = ListenCollectionProps & {\n subscriptionId: string\n};\n\n/**\n * The narrowing a collection subscription was created with, kept so that every\n * refetch answers the same query the initial fetch did.\n *\n * Named once because it used to be written out inline in five places, and a\n * field missing from one of them is accepted over the wire and then silently\n * ignored: `offset` was declared on the incoming props and never stored, so a\n * live list on page three served page one, and `logical` was never stored\n * either, so an `or(...)` subscription was pushed every row in the table.\n */\ntype StoredCollectionRequest = {\n filter?: Record<string, unknown>;\n logical?: LogicalCondition;\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n offset?: number;\n startAfter?: Record<string, unknown>;\n databaseId?: string;\n searchString?: string;\n /** Ask each row which declared search field matched — populates `_matches`. */\n searchExplain?: boolean;\n};\n\ntype RealTimeListenEntityProps = ListenOneProps & { subscriptionId: string };\n\n/**\n * A registered subscription, plus the two counters that order its deliveries.\n *\n * Every update a subscription delivers is a full re-fetch, and more than one\n * thing starts one for the same subscription without coordinating: the initial\n * fetch at subscribe time, and a debounced refetch per notification (app\n * mutation, cross-instance NOTIFY, or CDC). A fetch that started earlier can\n * finish later, and the delivery replaces everything the subscriber has — so\n * the subscriber goes back to the state before the change and stays there,\n * silently, until the next write to that collection.\n *\n * The debounce is not a fix for this. It collapses a burst into one refetch and\n * does nothing about two refetches that overlap: notification A fires its timer\n * and starts fetch A, notification B arrives while A is still in flight, and B's\n * timer fires and starts fetch B regardless. See class 44 in\n * `docs/bug-classes.md`.\n *\n * `started` is taken before the work, `delivered` after it — which makes the\n * last delivery *started* the last one *delivered*.\n */\ntype Subscription = {\n clientId: string;\n type: \"collection\" | \"single\";\n path: string;\n id?: string | number;\n // Store full collection request parameters for proper refetching\n collectionRequest?: StoredCollectionRequest;\n // Auth context for RLS — when set, refetches run in a transaction\n // with set_config('app.uid', ...) / set_config('app.user_roles', ...)\n authContext?: SubscriptionAuthContext;\n /** How many deliveries have been started for this subscription. */\n started: number;\n /** The highest started-sequence that has already reached the subscriber. */\n delivered: number;\n};\n\n/**\n * PostgreSQL-specific realtime service.\n * Handles WebSocket connections and subscriptions for real-time row updates.\n *\n * Implements the RealtimeProvider interface for database abstraction.\n */\nexport class RealtimeService extends EventEmitter implements RealtimeProvider {\n /**\n * Declares to the multi-engine router that channel frames can be handled\n * here. Read by `createRoutedRealtimeService`, which otherwise would have to\n * guess — and guessed \"the default provider\", whichever engine that is.\n */\n public readonly supportsChannels = true;\n\n private clients = new Map<string, WebSocket>();\n\n // Broadcast channels: channel name → set of client IDs\n private channels = new Map<string, Set<string>>();\n\n // Presence: channel → Map<clientId, { state, lastSeen }>\n private presence = new Map<string, Map<string, { state: Record<string, unknown>; lastSeen: number }>>();\n\n /**\n * Ordered, replayable history for channels that opt into it.\n *\n * Undefined until {@link configureChannelHistory} is called, and inert even\n * then unless retention rules were supplied — so presence and ephemeral\n * notification channels never touch it. See `channel-history.ts`.\n */\n private channelHistory?: ChannelHistoryStore;\n\n /**\n * One promise chain per retained channel, so that assigning a sequence\n * number and fanning the message out happen in the same order for every\n * message on that channel.\n *\n * Without it, two concurrent broadcasts can be numbered 4 and 5 by the\n * database and still reach subscribers as 5 then 4 — live order and replay\n * order would disagree, which is exactly the divergence sequence numbers\n * are supposed to rule out. Keyed by channel, so unrelated channels never\n * wait on each other.\n */\n private channelSendQueues = new Map<string, Promise<void>>();\n\n /**\n * Cross-instance transport for channel frames and presence.\n *\n * Defaults to the memory bus, which publishes nowhere — so a single-instance\n * deployment runs the same fan-out it always did, with one resolved promise\n * per broadcast for company. See `channel-bus/ChannelBus.ts`.\n */\n private bus: ChannelBus = new MemoryChannelBus();\n\n /**\n * The shared presence roster, present only when a real bus is active.\n *\n * Fan-out alone is not enough for presence: `presence_state` has to answer\n * with everyone in the channel, and per-process maps can only answer for\n * this replica's clients. See `channel-presence.ts`.\n */\n private presenceStore?: ChannelPresenceStore;\n\n /** Sweeps roster rows left behind by instances that stopped heartbeating. */\n private presenceSweepInterval?: ReturnType<typeof setInterval>;\n\n /**\n * Channels whose oversized ephemeral broadcasts have already been reported,\n * so a hot channel logs the problem once rather than once per message.\n */\n private oversizedBroadcastWarned = new Set<string>();\n\n /**\n * Optional narrowing on top of the membership floor — see\n * {@link ChannelAuthorizer}. Unset by default, which leaves membership as\n * the whole of the rule.\n */\n private channelAuthorizer?: ChannelAuthorizer;\n\n /**\n * Whether a notification from another instance has ever arrived.\n *\n * The entity LISTEN handler sees a foreign `sid` on every cross-instance\n * change, which is proof that this deployment runs more than one pod — the\n * one fact needed to tell \"the memory bus is fine here\" from \"broadcast and\n * presence silently reach a fraction of your users\".\n */\n private foreignInstanceSeen = false;\n\n /** So the multi-pod memory-bus warning is emitted once, not once per join. */\n private memoryBusWarned = false;\n\n private presenceInterval?: ReturnType<typeof setInterval>;\n private static readonly PRESENCE_TIMEOUT_MS = 30000; // 30s\n /** How often stale roster rows from other instances are reaped. */\n private static readonly PRESENCE_SWEEP_INTERVAL_MS = 10000; // 10s\n private dataService: DataService;\n // Enhanced subscriptions storage with full request parameters\n private _subscriptions = new Map<string, Subscription>();\n\n // Add callback storage for DataDriver subscriptions\n private subscriptionCallbacks = new Map<string, (data: Record<string, unknown>[] | Record<string, unknown> | null) => void>();\n\n private driver?: DataDriver;\n\n // ── Cross-instance LISTEN/NOTIFY ──\n /** Unique identifier for this process instance, used to skip own notifications. */\n private readonly instanceId = `inst_${randomUUID().slice(0, 8)}`;\n /** Dedicated pg.Client for LISTEN (outside the Drizzle pool). */\n private listenClient?: PgClient;\n /** Connection string used for reconnecting the LISTEN client. */\n private listenConnectionString?: string;\n /** Whether cross-instance broadcasting is active. */\n private broadcasting = false;\n /** Reconnection timer handle. */\n private reconnectTimer?: ReturnType<typeof setTimeout>;\n /** Debounce timers for collection refetches to prevent refetch storms. */\n private refetchTimers = new Map<string, ReturnType<typeof setTimeout>>();\n /** Debounce window (ms) for coalescing rapid row updates into a single correctness refetch. */\n private static readonly REFETCH_DEBOUNCE_MS = 300;\n\n // ── Database-level Change Data Capture (CDC) ──\n /** Dedicated LISTEN client for DB-level change events (undefined unless CDC is enabled). */\n private cdcListener?: CdcListener;\n /** Whether database-level CDC is the active cross-instance change source. */\n private cdcActive = false;\n /** Junction table → the child lists its rows belong to, built when CDC starts. */\n private junctionLinkMap?: Map<string, JunctionLink[]>;\n\n /** Reverse lookup: `schema.table` (and bare `table`) → collection, built when CDC starts. */\n private cdcTableMap?: Map<string, CollectionConfig>;\n /**\n * Short-lived record of `path/id` keys this instance just fanned out via the\n * app path (a Rebase-API mutation). When CDC echoes the same committed change\n * back to *this* instance, we suppress the duplicate — the change was already\n * delivered locally. Other instances have no such record, so they still\n * deliver the CDC event. External writes (psql, cron, SQL editor) never match\n * and always flow through. Keyed → expiry timestamp (ms).\n */\n private recentAppEmits = new Map<string, number>();\n /** How long an app-emit key suppresses its own CDC echo. Covers NOTIFY round-trip latency. */\n private static readonly CDC_DEDUP_WINDOW_MS = 5000;\n\n constructor(private db: NodePgDatabase<any>, private registry: PostgresCollectionRegistry) {\n super();\n this.dataService = new DataService(db, registry);\n }\n\n /**\n * Restricted role that auth-scoped refetches run as (via `SET LOCAL ROLE`)\n * so RLS `select` policies bind. Set by the bootstrapper alongside\n * `PostgresBackendDriver.rlsUserRole`; undefined when the connection\n * is already subject to RLS natively. Without this, realtime refetches\n * would leak rows the initial (isolated) fetch correctly hid.\n */\n public rlsUserRole?: string;\n\n /** Whether to emit verbose debug logs (disabled in production). */\n private static readonly DEBUG = process.env.NODE_ENV !== \"production\";\n private debugLog(...args: unknown[]) {\n if (RealtimeService.DEBUG) console.debug(...args);\n }\n\n setDataDriver(driver: DataDriver) {\n this.driver = driver;\n }\n\n // Make subscriptions accessible for DataDriver\n get subscriptions() {\n return this._subscriptions;\n }\n\n /**\n * Claim a delivery slot for a subscription, before doing the work.\n *\n * Returns the check to run immediately before delivering. It refuses in\n * three cases, all of which used to deliver:\n *\n * - **Out of order.** A newer refetch has already delivered, so this one is\n * stale — the subscriber would go back to the state before the change.\n * - **Unsubscribed.** The subscription was cancelled while the fetch was in\n * flight. The `has(subscriptionId)` check the debounced refetches ran\n * *before* the await cannot answer this; only a check after it can.\n * - **Replaced.** The same id can name a *different* subscription by the\n * time a fetch lands — a re-subscribe overwrites the map entry, and the\n * old filter's rows would be delivered to the new subscriber.\n *\n * The last two are identity, not presence: the map has to still hold *this\n * exact object*, not merely something under this id.\n */\n private beginDelivery(subscriptionId: string, subscription: Subscription): () => boolean {\n const seq = ++subscription.started;\n return () => {\n if (this._subscriptions.get(subscriptionId) !== subscription) return false;\n if (seq <= subscription.delivered) return false;\n subscription.delivered = seq;\n return true;\n };\n }\n\n // Add public method to register DataDriver subscriptions\n registerDataDriverSubscription(subscriptionId: string, subscription: {\n clientId: string;\n type: \"collection\" | \"single\";\n path: string;\n id?: string | number;\n collectionRequest?: StoredCollectionRequest;\n authContext?: SubscriptionAuthContext;\n }) {\n this.debugLog(\"📋 [RealtimeService] Registering DataDriver subscription:\", subscriptionId, subscription.authContext ? \"(with auth)\" : \"(no auth)\");\n this._subscriptions.set(subscriptionId, { ...subscription, started: 0, delivered: 0 });\n }\n\n // Add callback management methods\n addSubscriptionCallback(subscriptionId: string, callback: (data: Record<string, unknown>[] | Record<string, unknown> | null) => void) {\n this.debugLog(\"📋 [RealtimeService] Adding callback for subscription:\", subscriptionId);\n this.subscriptionCallbacks.set(subscriptionId, callback);\n }\n\n removeSubscriptionCallback(subscriptionId: string) {\n this.debugLog(\"📋 [RealtimeService] Removing callback for subscription:\", subscriptionId);\n this.subscriptionCallbacks.delete(subscriptionId);\n }\n\n // =============================================================================\n // RealtimeProvider Interface Methods\n // =============================================================================\n\n /**\n * Subscribe to collection changes (RealtimeProvider interface)\n */\n subscribeToCollection(\n subscriptionId: string,\n config: CollectionSubscriptionConfig,\n callback?: (rows: Record<string, unknown>[]) => void\n ): void {\n this._subscriptions.set(subscriptionId, {\n clientId: config.clientId,\n type: \"collection\",\n path: config.path,\n collectionRequest: {\n filter: config.filter as Record<string, unknown> | undefined,\n orderBy: config.orderBy,\n order: config.order,\n limit: config.limit,\n startAfter: config.startAfter as Record<string, unknown> | undefined,\n databaseId: config.databaseId,\n searchString: config.searchString,\n searchExplain: config.searchExplain\n },\n started: 0,\n delivered: 0\n });\n\n if (callback) {\n this.subscriptionCallbacks.set(subscriptionId, callback as (data: Record<string, unknown>[] | Record<string, unknown> | null) => void);\n }\n }\n\n /**\n * Subscribe to single row changes (RealtimeProvider interface)\n */\n subscribeToOne(\n subscriptionId: string,\n config: SingleSubscriptionConfig,\n callback?: (row: Record<string, unknown> | null) => void\n ): void {\n this._subscriptions.set(subscriptionId, {\n clientId: config.clientId,\n type: \"single\",\n path: config.path,\n id: config.id,\n started: 0,\n delivered: 0\n });\n\n if (callback) {\n this.subscriptionCallbacks.set(subscriptionId, callback as (data: Record<string, unknown>[] | Record<string, unknown> | null) => void);\n }\n }\n\n /**\n * Unsubscribe from a subscription (RealtimeProvider interface)\n */\n unsubscribe(subscriptionId: string): void {\n this._subscriptions.delete(subscriptionId);\n this.subscriptionCallbacks.delete(subscriptionId);\n }\n\n // =============================================================================\n // WebSocket Client Management\n // =============================================================================\n\n addClient(clientId: string, ws: WebSocket) {\n this.clients.set(clientId, ws);\n\n ws.on(\"close\", () => {\n this.removeClient(clientId);\n });\n\n ws.on(\"error\", (error) => {\n logger.error(\"WebSocket error for client\", { detail: clientId, error });\n this.removeClient(clientId);\n });\n }\n\n // Public method to handle messages from external sources (like main WebSocket handler)\n async handleClientMessage(clientId: string, message: WebSocketMessage, authContext?: SubscriptionAuthContext) {\n await this.handleMessage(clientId, message, authContext);\n }\n\n async removeClient(clientId: string) {\n this.clients.delete(clientId);\n\n // Remove all subscriptions, callbacks, and pending refetch timers for this client\n for (const [subscriptionId, subscription] of this._subscriptions.entries()) {\n if (subscription.clientId === clientId) {\n this._subscriptions.delete(subscriptionId);\n this.subscriptionCallbacks.delete(subscriptionId);\n\n // Cancel any pending debounced refetch timers\n for (const prefix of [\"ws_\", \"drv_\", \"wse_\", \"drve_\"]) {\n const key = `${prefix}${subscriptionId}`;\n const timer = this.refetchTimers.get(key);\n if (timer) { clearTimeout(timer); this.refetchTimers.delete(key); }\n }\n }\n }\n\n // Remove from all broadcast channels\n for (const [channel, members] of this.channels.entries()) {\n if (members.has(clientId)) {\n members.delete(clientId);\n this.removePresence(clientId, channel, { skipStore: true });\n if (members.size === 0) this.channels.delete(channel);\n }\n }\n\n // Remove from all presence channels\n for (const [channel] of this.presence) {\n this.removePresence(clientId, channel, { skipStore: true });\n }\n\n // One statement for every channel the client was in, rather than one\n // per channel above — a disconnect is the common case, not a rare one.\n void this.presenceStoreOp(() => this.presenceStore!.removeClient(clientId), \"client removal\");\n }\n\n private async handleMessage(clientId: string, message: WebSocketMessage, authContext?: SubscriptionAuthContext) {\n const payload = message.payload as Record<string, unknown> | undefined;\n switch (message.type) {\n case \"subscribe_collection\":\n await this.handleCollectionSubscription(clientId, message.payload as RealTimeListenCollectionProps, authContext);\n break;\n case \"subscribe_one\":\n await this.handleEntitySubscription(clientId, message.payload as RealTimeListenEntityProps, authContext);\n break;\n case \"unsubscribe\":\n await this.handleUnsubscribe(clientId, message.subscriptionId!);\n break;\n\n // ── Broadcast Channels & Presence ──\n //\n // One arm for all of them, because every one has to pass the same\n // gate and a switch with seven arms is a place to forget it once.\n // See `handleChannelMessage`.\n case \"join_channel\":\n case \"leave_channel\":\n case \"broadcast\":\n case \"channel_history\":\n case \"presence_track\":\n case \"presence_untrack\":\n case \"presence_state\":\n await this.handleChannelMessage(clientId, message.type, payload, authContext);\n break;\n\n default:\n this.sendError(clientId, \"Unknown message type \" + message.type, message.subscriptionId);\n }\n }\n\n private async handleCollectionSubscription(clientId: string, request: RealTimeListenCollectionProps, authContext?: SubscriptionAuthContext) {\n const subscriptionId = request.subscriptionId;\n\n try {\n // Early validation: ensure the requested collection exists in the registry\n const collection = this.registry.getCollectionByPath(request.path);\n if (!collection) {\n const registered = this.registry.getCollections().map(c => c.slug).join(\", \");\n const msg = `Collection not found: '${request.path}'. Registered: [${registered}]`;\n logger.error(`[RealtimeService] ${msg}`);\n this.sendError(clientId, msg, subscriptionId);\n return;\n }\n\n // A vector search cannot be served here, and the parameter used to\n // be read for one thing only — the limit default below — and then\n // dropped: the stored request carries no `vectorSearch` and the\n // refetch has no branch for one. So `.vectorSearch(…).listen()`\n // delivered an ordinary `id DESC` listing, with no `_distance` and\n // no error, forever. Refusing says what the silence did not.\n if (request.vectorSearch) {\n const msg =\n \"Realtime subscriptions do not support vector search: a subscription is re-run on every \" +\n \"matching write, and nothing here computes distances. Use `.vectorSearch(...).find()` for \" +\n \"the query, and subscribe without it if you need live updates.\";\n logger.warn(`[RealtimeService] ${msg}`);\n this.sendError(clientId, msg, subscriptionId, \"VECTOR_SEARCH_NOT_LIVE\");\n return;\n }\n\n // Bound the client-supplied limit with the SAME guarantee the REST\n // ingress applies (`resolveClientListLimit`): default an absent\n // limit by mode, refuse one above the ceiling. A subscription is\n // re-fetched on every matching write, so an unbounded one is a DoS\n // amplified per write — resolve it once and reuse for the stored\n // request and the initial fetch.\n //\n // Refusing matters more here than on the REST route: a\n // `collection_update` frame carries rows and nothing else — no\n // `total`, no `hasMore` — so a subscriber handed a quietly smaller\n // page has no way at all to learn it is not seeing the collection.\n let boundedLimit: number;\n try {\n boundedLimit = resolveClientListLimit(request.limit);\n } catch (e) {\n if (!(e instanceof ListLimitError)) throw e;\n logger.warn(`[RealtimeService] Refused subscription to '${request.path}': ${e.message}`);\n this.sendError(clientId, e.message, subscriptionId, \"INVALID_LIMIT\");\n return;\n }\n\n // The sort arrives as whatever JSON the client put in the frame, so\n // its *shape* is checked here the way the REST ingress checks the\n // query parameter. Unchecked, a malformed entry reads as a field\n // name that resolves to no column, and under lenient unknown-field\n // handling the subscription then streams rows in no order at all\n // while reporting nothing wrong.\n let orderBy: OrderByTuple[] | undefined;\n try {\n orderBy = parseOrderBySpecStrict(request.orderBy, request.order);\n } catch (e) {\n if (!(e instanceof OrderBySpecError)) throw e;\n logger.warn(`[RealtimeService] Refused subscription to '${request.path}': ${e.message}`);\n this.sendError(clientId, e.message, subscriptionId, e.code);\n return;\n }\n\n // Store subscription with full request parameters and auth context for RLS\n const subscription: Subscription = {\n clientId,\n type: \"collection\",\n path: request.path,\n collectionRequest: {\n filter: request.filter,\n logical: request.logical,\n orderBy,\n order: request.order,\n limit: boundedLimit,\n offset: request.offset,\n startAfter: request.startAfter as Record<string, unknown> | undefined,\n databaseId: request.collection?.databaseId,\n searchString: request.searchString,\n searchExplain: request.searchExplain\n },\n authContext,\n started: 0,\n delivered: 0\n };\n this._subscriptions.set(subscriptionId, subscription);\n\n // The subscription is registered before this fetch runs, so a write\n // arriving in that window starts a refetch of its own — with nothing\n // ordering the two. Claim a slot first: this fetch is the oldest, so\n // if the refetch answers first, this one no longer delivers.\n const canDeliver = this.beginDelivery(subscriptionId, subscription);\n\n // Send initial data. Built from the request the subscription just\n // stored, so the first answer and every refetch after it cannot\n // describe different queries.\n const rows = await this.fetchCollectionWithAuth(\n request.path,\n subscription.collectionRequest!,\n authContext\n );\n\n if (canDeliver()) {\n this.sendCollectionUpdate(clientId, subscriptionId, rows, request.path);\n }\n\n } catch (error) {\n const sanitized = sanitizeErrorForClient(error, request.path);\n this.sendError(clientId, sanitized.message, subscriptionId, sanitized.code);\n }\n }\n\n private async handleEntitySubscription(clientId: string, request: RealTimeListenEntityProps, authContext?: SubscriptionAuthContext) {\n const subscriptionId = request.subscriptionId;\n\n try {\n // Early validation: ensure the requested collection exists in the registry\n const collection = this.registry.getCollectionByPath(request.path);\n if (!collection) {\n const registered = this.registry.getCollections().map(c => c.slug).join(\", \");\n const msg = `Collection not found: '${request.path}'. Registered: [${registered}]`;\n logger.error(`[RealtimeService] ${msg}`);\n this.sendError(clientId, msg, subscriptionId);\n return;\n }\n\n // Store subscription in memory with auth context for RLS\n const subscription: Subscription = {\n clientId,\n type: \"single\",\n path: request.path,\n id: request.id,\n authContext,\n started: 0,\n delivered: 0\n };\n this._subscriptions.set(subscriptionId, subscription);\n\n // Same race as the collection case: a write landing between the\n // registration above and this fetch starts a refetch that can answer\n // first, and this one must not overwrite it afterwards.\n const canDeliver = this.beginDelivery(subscriptionId, subscription);\n\n // Send initial data\n const row = await this.fetchEntityWithAuth(\n request.path,\n String(request.id),\n authContext\n );\n\n if (canDeliver()) {\n this.sendSingleUpdate(clientId, subscriptionId, row || null);\n }\n\n } catch (error) {\n const sanitized = sanitizeErrorForClient(error, request.path);\n this.sendError(clientId, sanitized.message, subscriptionId, sanitized.code);\n }\n }\n\n private async handleUnsubscribe(_clientId: string, subscriptionId: string) {\n this._subscriptions.delete(subscriptionId);\n this.subscriptionCallbacks.delete(subscriptionId);\n // Cancel any pending debounced refetch\n for (const prefix of [\"ws_\", \"drv_\", \"wse_\", \"drve_\"]) {\n const key = `${prefix}${subscriptionId}`;\n const timer = this.refetchTimers.get(key);\n if (timer) { clearTimeout(timer); this.refetchTimers.delete(key); }\n }\n }\n\n /**\n * Enhanced notification method that handles nested relation updates.\n * @param broadcast When true (default), also sends a pg_notify so other instances\n * pick up the change. Set to false when handling an incoming\n * cross-instance notification to avoid infinite loops.\n * @param origin `\"app\"` (default) — a Rebase-API mutation on this instance;\n * `\"cdc\"` — a database-level change observed via CDC (any writer,\n * any instance). The origin drives de-duplication: an app emit\n * records the change so this instance can suppress the matching\n * CDC echo, while an unmatched CDC event is delivered normally.\n */\n async notifyUpdate(path: string, id: string, row: Record<string, unknown> | null, databaseId?: string, broadcast = true, origin: \"app\" | \"cdc\" = \"app\") {\n this.debugLog(\"🔔 [RealtimeService] notifyUpdate called for path:\", path, \"id:\", id, \"isDelete:\", row === null, \"origin:\", origin);\n\n // De-duplicate against database-level CDC. The app path (a mutation made\n // through the Rebase API) fans out locally AND, once CDC is active, the\n // same committed change is echoed back to this instance via the WAL /\n // trigger stream. Record app emits so we can drop that echo here; deliver\n // any CDC event we did not originate (external writes, other instances).\n if (this.cdcActive) {\n const key = this.dedupKey(path, id, databaseId);\n if (origin === \"cdc\") {\n if (this.consumeAppEmit(key)) {\n this.debugLog(\"🔁 [RealtimeService] Suppressing CDC echo of local app mutation:\", key);\n return;\n }\n } else {\n this.markAppEmit(key);\n }\n }\n\n // Get all paths that need to be notified - the direct path plus any parent paths\n const pathsToNotify = [path];\n\n // If this is a nested relation path (like \"posts/70/tags\"), also notify parent paths\n if (path.includes(\"/\") && path.split(\"/\").length > 1) {\n const parentPaths = this.getParentPaths(path);\n pathsToNotify.push(...parentPaths);\n this.debugLog(`🔗 [RealtimeService] Nested path detected. Will notify paths: ${pathsToNotify.join(\", \")}`);\n }\n\n // Process each path that needs notification\n for (const notifyPath of pathsToNotify) {\n await this.notifyPathUpdate(notifyPath, path, id, row, databaseId);\n }\n\n // Broadcast to other instances via pg_notify (only for local mutations).\n // When CDC is active it IS the cross-instance channel — every instance\n // observes every commit through the change stream — so the legacy\n // per-mutation broadcast is redundant (and would double-deliver). Skip it.\n if (broadcast && this.broadcasting && !this.cdcActive) {\n try {\n await this.broadcastChange(path, id, databaseId);\n } catch (err) {\n logger.error(\"❌ [RealtimeService] Failed to broadcast change via pg_notify\", { error: err });\n }\n }\n\n this.debugLog(\"🔔 [RealtimeService] notifyUpdate completed for path:\", path);\n }\n\n /**\n * Notify subscriptions for a specific path.\n *\n * **A subscriber only ever receives rows re-read under its own scope.**\n * `row` is used to decide *that* something changed, never to say *what* —\n * every delivery below goes through a refetch that binds the subscription's\n * own auth context.\n *\n * It used to be conditional. The CDC path already did the right thing: it\n * discards the captured tuple and emits `{_rebase_invalidated: true}`, and\n * that marker selected the refetch branch. But the marker is produced in\n * exactly two places, and the *other* side of each branch here shipped the\n * row it was handed straight to the socket. Two of the three entry paths\n * took that side — every API mutation (`PostgresBackendDriver.save` passes\n * the row it just wrote, read under the **writer's** scope) and the legacy\n * cross-instance LISTEN handler (which re-reads on the owner connection,\n * bypassing RLS altogether). Path matching was the only filter applied: the\n * subscription's own `filter`/`logical` was never evaluated, and any\n * `afterRead` redaction was the writer's rather than the reader's.\n *\n * A single-row subscription was the sharpest case. `subscribe_one` on a row\n * RLS denies is accepted and answered `null`; the next update then pushed\n * the full row with no later correction. The collection variant was merely\n * papered over ~300 ms later by the debounced refetch — after the bytes had\n * already reached the browser.\n *\n * The same defect was found and fixed on the Mongo driver in `065e2b615`\n * (see `packages/server-mongo/test/realtime-authorization.test.ts`); this is\n * the Postgres half, stated as one rule rather than three patched branches.\n *\n * The cost is the instant row-level patch that used to precede the refetch:\n * cross-tab feedback now waits for the debounce. That is the price of not\n * being able to know, without asking the database as this subscriber,\n * whether this subscriber may see the row at all.\n */\n private async notifyPathUpdate(notifyPath: string, originalPath: string, id: string, row: Record<string, unknown> | null, _databaseId?: string) {\n this.debugLog(`📡 [RealtimeService] Notifying path: ${notifyPath} (original: ${originalPath})`);\n\n // Find all relevant subscriptions for this specific path\n const allSubscriptions = Array.from(this._subscriptions.entries()).filter(([, sub]) => {\n const isPathMatch = sub.path === notifyPath;\n\n // For row subscriptions, check if the id matches (only for exact path matches)\n if (sub.type === \"single\") {\n return isPathMatch && (notifyPath === originalPath ? sub.id === id : true);\n }\n // For collection subscriptions, it's always relevant if the path matches\n if (sub.type === \"collection\") {\n return isPathMatch;\n }\n return false;\n });\n\n this.debugLog(`📡 [RealtimeService] Found ${allSubscriptions.length} subscriptions for path: ${notifyPath}`);\n\n // Separate WebSocket subscriptions from DataDriver callback subscriptions\n const webSocketSubscriptions = allSubscriptions.filter(([, sub]) =>\n sub.clientId !== \"driver\" && this.clients.has(sub.clientId)\n );\n\n const driverSubscriptions = allSubscriptions.filter(([subscriptionId, sub]) =>\n sub.clientId === \"driver\" && this.subscriptionCallbacks.has(subscriptionId)\n );\n\n // Handle WebSocket subscriptions\n for (const [subscriptionId, subscription] of webSocketSubscriptions) {\n try {\n if (subscription.type === \"single\" && notifyPath === originalPath) {\n this.debouncedSingleRefetch(subscriptionId, notifyPath, id, subscription);\n } else if (subscription.type === \"collection\" && subscription.collectionRequest) {\n this.debouncedCollectionRefetch(subscriptionId, notifyPath, subscription);\n }\n } catch (error) {\n const sanitized = sanitizeErrorForClient(error, notifyPath);\n this.sendError(subscription.clientId, sanitized.message, subscriptionId, sanitized.code);\n }\n }\n\n // Handle DataDriver callback subscriptions\n for (const [subscriptionId, subscription] of driverSubscriptions) {\n try {\n const callback = this.subscriptionCallbacks.get(subscriptionId);\n if (!callback) continue;\n\n if (subscription.type === \"single\" && notifyPath === originalPath) {\n this.debouncedSingleDriverRefetch(subscriptionId, notifyPath, id, subscription, callback);\n } else if (subscription.type === \"collection\" && subscription.collectionRequest) {\n // Debounce collection refetches for DataDriver subscriptions too\n this.debouncedDriverRefetch(subscriptionId, notifyPath, subscription, callback);\n }\n } catch (error) {\n logger.error(`❌ [RealtimeService] Error processing DataDriver subscription ${subscriptionId}`, { error: error });\n }\n }\n }\n\n /**\n * Debounce a collection refetch for a WebSocket subscription.\n * Coalesces rapid row mutations into a single database query.\n */\n private debouncedCollectionRefetch(\n subscriptionId: string,\n notifyPath: string,\n subscription: Subscription\n ) {\n const timerKey = `ws_${subscriptionId}`;\n const existing = this.refetchTimers.get(timerKey);\n if (existing) clearTimeout(existing);\n\n this.refetchTimers.set(timerKey, setTimeout(async () => {\n this.refetchTimers.delete(timerKey);\n // Cheap bail before spending a query: the client may have\n // disconnected, or re-subscribed under the same id. It is only an\n // optimisation — `canDeliver()` after the await is what makes the\n // delivery safe, because the same things can happen *during* it.\n if (this._subscriptions.get(subscriptionId) !== subscription) return;\n // Claimed here rather than when the timer was scheduled: the\n // debounce coalesces, and no work exists to order until it fires.\n const canDeliver = this.beginDelivery(subscriptionId, subscription);\n try {\n const rows = await this.fetchCollectionWithAuth(notifyPath, subscription.collectionRequest!, subscription.authContext);\n if (canDeliver()) {\n this.sendCollectionUpdate(subscription.clientId, subscriptionId, rows, notifyPath);\n }\n } catch (error) {\n const sanitized = sanitizeErrorForClient(error, notifyPath);\n this.sendError(subscription.clientId, sanitized.message, subscriptionId, sanitized.code);\n }\n }, RealtimeService.REFETCH_DEBOUNCE_MS));\n }\n\n /**\n * Debounce a collection refetch for a DataDriver callback subscription.\n */\n private debouncedDriverRefetch(\n subscriptionId: string,\n notifyPath: string,\n subscription: Subscription,\n callback: (data: Record<string, unknown>[] | Record<string, unknown> | null) => void\n ) {\n const timerKey = `drv_${subscriptionId}`;\n const existing = this.refetchTimers.get(timerKey);\n if (existing) clearTimeout(existing);\n\n this.refetchTimers.set(timerKey, setTimeout(async () => {\n this.refetchTimers.delete(timerKey);\n if (this._subscriptions.get(subscriptionId) !== subscription) return;\n const canDeliver = this.beginDelivery(subscriptionId, subscription);\n try {\n const rows = await this.fetchCollectionWithAuth(notifyPath, subscription.collectionRequest!, subscription.authContext);\n if (canDeliver()) callback(rows);\n } catch (error) {\n logger.error(`❌ [RealtimeService] Error in debounced driver refetch for ${subscriptionId}`, { error: error });\n }\n }, RealtimeService.REFETCH_DEBOUNCE_MS));\n }\n\n /**\n * Fetch a collection with optional RLS auth context.\n * When authContext is provided, the fetch runs inside a transaction\n * with set_config calls so PostgreSQL RLS policies are enforced.\n */\n private async fetchCollectionWithAuth(\n notifyPath: string,\n collectionRequest: StoredCollectionRequest,\n authContext?: SubscriptionAuthContext\n ): Promise<Record<string, unknown>[]> {\n if (this.driver) {\n const collection = this.registry.getCollectionByPath(notifyPath);\n const fetchFn = async () => this.driver!.fetchCollection({\n path: notifyPath,\n collection: collection,\n filter: collectionRequest.filter as FetchCollectionProps[\"filter\"],\n logical: collectionRequest.logical,\n orderBy: collectionRequest.orderBy,\n order: collectionRequest.order,\n limit: collectionRequest.limit,\n offset: collectionRequest.offset,\n startAfter: collectionRequest.startAfter,\n searchString: collectionRequest.searchString,\n searchExplain: collectionRequest.searchExplain\n });\n\n // Always wrap in a transaction with session vars, defaulting to anonymous context if missing.\n // Refetches are reads: apply the same GUCs + reader-role downgrade as the\n // driver's read path, so realtime cannot leak rows the initial fetch hid.\n const activeAuth = authContext || { uid: ANONYMOUS_USER_ID,\nroles: [\"anon\"] };\n return await this.db.transaction(async (tx) => {\n await applyAuthContext(tx, { uid: activeAuth.uid, roles: activeAuth.roles }, this.rlsUserRole);\n const txEntityService = new DataService(tx, this.registry);\n let fetchedEntities;\n if (collectionRequest.searchString) {\n fetchedEntities = await txEntityService.searchRows(\n notifyPath,\n collectionRequest.searchString,\n {\n filter: collectionRequest.filter as FilterValues<string>,\n // The subscription stored a group; the search branch\n // did not pass it on, so a filtered live search\n // widened to every row matching the text.\n logical: collectionRequest.logical,\n orderBy: collectionRequest.orderBy,\n order: collectionRequest.order,\n limit: collectionRequest.limit,\n databaseId: collectionRequest.databaseId,\n searchExplain: collectionRequest.searchExplain\n }\n );\n } else {\n fetchedEntities = await txEntityService.fetchCollection(notifyPath, {\n filter: collectionRequest.filter as FilterValues<string>,\n logical: collectionRequest.logical,\n orderBy: collectionRequest.orderBy,\n order: collectionRequest.order,\n limit: collectionRequest.limit,\n offset: collectionRequest.offset,\n startAfter: collectionRequest.startAfter,\n databaseId: collectionRequest.databaseId\n });\n }\n\n // Re-apply `afterRead` lifecycle hooks to ensure consistent data structures\n // between the initial driver fetch and this RLS-bound refetch.\n const registryCollection = this.registry.getCollectionByPath(notifyPath);\n const resolvedCollection = collection ? { ...collection,\n...registryCollection } as CollectionConfig : registryCollection as CollectionConfig;\n\n const callbacks = resolvedCollection?.callbacks;\n const globalCallbacks = this.registry?.getGlobalCallbacks();\n const propertyCallbacks = resolvedCollection?.properties ? buildPropertyCallbacks(resolvedCollection.properties) : undefined;\n\n if (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead) {\n const contextForCallback = {\n user: { uid: activeAuth.uid,\nroles: activeAuth.roles },\n driver: this.driver,\n data: (this.driver && \"data\" in this.driver) ? (this.driver as DataDriverWithData).data : undefined\n } as unknown as RebaseCallContext;\n\n return await Promise.all(fetchedEntities.map(async (fetchedRow) => {\n let processedEntity = fetchedRow;\n // 1. Global callbacks first\n if (globalCallbacks?.afterRead) {\n processedEntity = await globalCallbacks.afterRead({\n collection: resolvedCollection,\n path: notifyPath,\n row: processedEntity,\n context: contextForCallback\n }) ?? processedEntity;\n }\n // 2. Collection callbacks second\n if (callbacks?.afterRead) {\n processedEntity = await callbacks.afterRead({\n collection: resolvedCollection,\n path: notifyPath,\n row: processedEntity,\n context: contextForCallback\n }) ?? processedEntity;\n }\n // 3. Property callbacks third\n if (propertyCallbacks?.afterRead) {\n processedEntity = await propertyCallbacks.afterRead({\n collection: resolvedCollection,\n path: notifyPath,\n row: processedEntity,\n context: contextForCallback\n }) ?? processedEntity;\n }\n return processedEntity;\n }));\n }\n\n return fetchedEntities;\n });\n }\n\n // No driver — use dataService directly (no auth wrapping possible).\n // The `logical` group is carried here as well: this branch answers the\n // same subscription as the one above, and a fallback that drops a\n // condition returns *more* rows than the path it stands in for.\n if (collectionRequest.searchString) {\n return await this.dataService.searchRows(\n notifyPath,\n collectionRequest.searchString,\n {\n filter: collectionRequest.filter as FilterValues<string>,\n logical: collectionRequest.logical,\n orderBy: collectionRequest.orderBy,\n order: collectionRequest.order,\n limit: collectionRequest.limit,\n databaseId: collectionRequest.databaseId,\n searchExplain: collectionRequest.searchExplain\n }\n );\n }\n return await this.dataService.fetchCollection(notifyPath, {\n filter: collectionRequest.filter as FilterValues<string>,\n logical: collectionRequest.logical,\n orderBy: collectionRequest.orderBy,\n order: collectionRequest.order,\n limit: collectionRequest.limit,\n offset: collectionRequest.offset,\n startAfter: collectionRequest.startAfter,\n databaseId: collectionRequest.databaseId\n });\n }\n\n /**\n * Debounce an row refetch for a WebSocket subscription.\n */\n private debouncedSingleRefetch(\n subscriptionId: string,\n notifyPath: string,\n id: string,\n subscription: Subscription\n ) {\n const timerKey = `wse_${subscriptionId}`;\n const existing = this.refetchTimers.get(timerKey);\n if (existing) clearTimeout(existing);\n\n this.refetchTimers.set(timerKey, setTimeout(async () => {\n this.refetchTimers.delete(timerKey);\n if (this._subscriptions.get(subscriptionId) !== subscription) return;\n const canDeliver = this.beginDelivery(subscriptionId, subscription);\n try {\n const row = await this.fetchEntityWithAuth(notifyPath, id, subscription.authContext);\n if (canDeliver()) {\n this.sendSingleUpdate(subscription.clientId, subscriptionId, row || null);\n }\n } catch (error) {\n const sanitized = sanitizeErrorForClient(error, notifyPath);\n this.sendError(subscription.clientId, sanitized.message, subscriptionId, sanitized.code);\n }\n }, RealtimeService.REFETCH_DEBOUNCE_MS));\n }\n\n /**\n * Debounce an row refetch for a Driver callback subscription.\n */\n private debouncedSingleDriverRefetch(\n subscriptionId: string,\n notifyPath: string,\n id: string,\n subscription: Subscription,\n callback: (data: Record<string, unknown>[] | Record<string, unknown> | null) => void\n ) {\n const timerKey = `drve_${subscriptionId}`;\n const existing = this.refetchTimers.get(timerKey);\n if (existing) clearTimeout(existing);\n\n this.refetchTimers.set(timerKey, setTimeout(async () => {\n this.refetchTimers.delete(timerKey);\n if (this._subscriptions.get(subscriptionId) !== subscription) return;\n const canDeliver = this.beginDelivery(subscriptionId, subscription);\n try {\n const row = await this.fetchEntityWithAuth(notifyPath, id, subscription.authContext);\n if (canDeliver()) callback(row || null);\n } catch (error) {\n logger.error(`❌ [RealtimeService] Error in debounced row driver refetch for ${subscriptionId}`, { error: error });\n }\n }, RealtimeService.REFETCH_DEBOUNCE_MS));\n }\n\n /**\n * Fetch a single row with optional RLS auth context.\n */\n private async fetchEntityWithAuth(\n notifyPath: string,\n id: string | number,\n authContext?: SubscriptionAuthContext\n ): Promise<Record<string, unknown> | undefined> {\n if (this.driver) {\n const collection = this.registry.getCollectionByPath(notifyPath);\n const fetchFn = async () => this.driver!.fetchOne({\n path: notifyPath,\n id,\n collection\n });\n\n // Always wrap in a transaction with session vars, defaulting to anonymous context if missing.\n // Same read isolation as collection refetches: GUCs + reader-role downgrade.\n const activeAuth = authContext || { uid: ANONYMOUS_USER_ID,\nroles: [\"anon\"] };\n return await this.db.transaction(async (tx) => {\n await applyAuthContext(tx, { uid: activeAuth.uid, roles: activeAuth.roles }, this.rlsUserRole);\n const txEntityService = new DataService(tx, this.registry);\n let processedEntity = await txEntityService.fetchOne(notifyPath, id, collection?.databaseId);\n\n if (processedEntity) {\n const registryCollection = this.registry.getCollectionByPath(notifyPath);\n const resolvedCollection = collection ? { ...collection,\n...registryCollection } as CollectionConfig : registryCollection as CollectionConfig;\n\n const callbacks = resolvedCollection?.callbacks;\n const globalCallbacks = this.registry?.getGlobalCallbacks();\n const propertyCallbacks = resolvedCollection?.properties ? buildPropertyCallbacks(resolvedCollection.properties) : undefined;\n\n if (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead) {\n const contextForCallback = {\n user: { uid: activeAuth.uid,\nroles: activeAuth.roles },\n driver: this.driver,\n data: (this.driver && \"data\" in this.driver) ? (this.driver as DataDriverWithData).data : undefined\n } as unknown as RebaseCallContext;\n\n // 1. Global callbacks first\n if (globalCallbacks?.afterRead) {\n processedEntity = await globalCallbacks.afterRead({\n collection: resolvedCollection,\n path: notifyPath,\n row: processedEntity,\n context: contextForCallback\n }) ?? processedEntity;\n }\n // 2. Collection callbacks second\n if (callbacks?.afterRead) {\n processedEntity = await callbacks.afterRead({\n collection: resolvedCollection,\n path: notifyPath,\n row: processedEntity,\n context: contextForCallback\n }) ?? processedEntity;\n }\n // 3. Property callbacks third\n if (propertyCallbacks?.afterRead) {\n processedEntity = await propertyCallbacks.afterRead({\n collection: resolvedCollection,\n path: notifyPath,\n row: processedEntity,\n context: contextForCallback\n }) ?? processedEntity;\n }\n }\n }\n\n return processedEntity;\n });\n }\n\n return await this.dataService.fetchOne(notifyPath, id);\n }\n\n private sendCollectionUpdate(clientId: string, subscriptionId: string, rows: Record<string, unknown>[], path: string) {\n const message: CollectionUpdateMessage = {\n type: \"collection_update\",\n subscriptionId,\n rows: rows,\n pks: this.primaryKeysForPath(path)\n };\n this.sendMessage(clientId, message);\n }\n\n private sendSingleUpdate(clientId: string, subscriptionId: string, row: Record<string, unknown> | null) {\n const message: SingleUpdateMessage = {\n type: \"single_update\",\n subscriptionId,\n row: row\n };\n this.sendMessage(clientId, message);\n }\n\n /**\n * Send a lightweight row-level patch to a collection subscriber.\n * The client can merge this into its cached data for instant feedback.\n *\n * The key columns ride along: the patch names a row by address, and the\n * client has to find that row among the ones it cached — which carry\n * columns and no address. The SDK holds no collection config to derive one\n * from, so this is the only place the mapping can come from.\n */\n /** The key columns of the collection at `path`, if they can be resolved. */\n private primaryKeysForPath(path: string): PrimaryKeyInfo[] | undefined {\n try {\n const collection = this.registry.getCollectionByPath(path);\n if (!collection) return undefined;\n const keys = getPrimaryKeys(collection, this.registry);\n return keys.length > 0 ? keys : undefined;\n } catch {\n // `getCollectionByPath` throws on a path it cannot walk — and this\n // is called for parent paths too, which include entity paths like\n // `posts/1` that name no collection. Telling the subscriber nothing\n // is right here; letting it throw would drop the notification.\n return undefined;\n }\n }\n\n private sendError(clientId: string, error: string, subscriptionId?: string, code?: string) {\n const message = {\n type: \"error\" as const,\n subscriptionId,\n payload: {\n error: code ? { message: error, code } : error\n },\n error\n };\n this.sendMessage(clientId, message);\n }\n\n private sendMessage(clientId: string, message: CollectionUpdateMessage | SingleUpdateMessage | CollectionPatchMessage | { type: string; subscriptionId?: string; error?: string; payload?: unknown }) {\n const client = this.clients.get(clientId);\n if (client && client.readyState === WebSocket.OPEN) {\n client.send(JSON.stringify(message));\n }\n }\n\n /**\n * Extract parent paths from a nested path like \"posts/70/tags\"\n * Returns [\"posts\", \"posts/70\"] for the example above\n */\n private getParentPaths(path: string): string[] {\n const segments = path.split(\"/\").filter(s => s.length > 0);\n const parentPaths: string[] = [];\n\n // Build parent paths progressively\n for (let i = 1; i < segments.length; i += 2) {\n const parentPath = segments.slice(0, i).join(\"/\");\n if (parentPath) {\n parentPaths.push(parentPath);\n }\n\n // If there's an row ID, add the path including the row\n if (i + 1 < segments.length) {\n const pathWithEntity = segments.slice(0, i + 1).join(\"/\");\n parentPaths.push(pathWithEntity);\n }\n }\n\n return parentPaths;\n }\n\n // =============================================================================\n // Broadcast Channels\n // =============================================================================\n\n /**\n * Install a channel authorizer — see {@link ChannelAuthorizer}.\n *\n * Nothing in the framework calls this yet: it is the seam a rules API will\n * be built on, kept deliberately separate from the membership floor so the\n * floor holds whether or not anyone uses it.\n */\n setChannelAuthorizer(authorizer: ChannelAuthorizer | undefined): void {\n this.channelAuthorizer = authorizer;\n }\n\n /** Which action each channel frame is asking to perform. */\n private static readonly CHANNEL_ACTIONS: Record<string, ChannelAction> = {\n join_channel: \"join\",\n broadcast: \"broadcast\",\n channel_history: \"history\",\n presence_track: \"join\",\n presence_state: \"presence\"\n };\n\n /**\n * The one door every channel frame comes through.\n *\n * Returns synchronously — and so dispatches synchronously — unless an\n * authorizer is installed. That matters: a client sends `join_channel`,\n * `presence_state` and `channel_history` back to back on connect, and the\n * socket's message handler processes each frame up to its first `await`,\n * so a gate that always yielded would let the reads overtake the join that\n * is about to authorize them.\n */\n private handleChannelMessage(\n clientId: string,\n type: string,\n payload: Record<string, unknown> | undefined,\n authContext?: SubscriptionAuthContext\n ): void | Promise<void> {\n const channel = payload?.channel as string;\n\n // Leaving and untracking only ever remove the caller's own state, so\n // they need no permission — refusing them could only strand a client.\n if (type === \"leave_channel\") {\n this.leaveChannel(clientId, channel);\n return;\n }\n if (type === \"presence_untrack\") {\n this.removePresence(clientId, channel);\n return;\n }\n\n const action = RealtimeService.CHANNEL_ACTIONS[type];\n const allowed = this.authorizeChannelAction(clientId, channel, action, authContext);\n if (allowed === false) return;\n if (allowed === true) return this.dispatchChannelMessage(clientId, type, channel, payload);\n return allowed.then((ok) => {\n if (ok) return this.dispatchChannelMessage(clientId, type, channel, payload);\n });\n }\n\n /** Perform an already-authorized channel frame. */\n private dispatchChannelMessage(\n clientId: string,\n type: string,\n channel: string,\n payload: Record<string, unknown> | undefined\n ): void | Promise<void> {\n switch (type) {\n case \"join_channel\":\n this.joinChannel(clientId, channel);\n return;\n case \"broadcast\":\n this.broadcastToChannel(clientId, channel, payload?.event as string, payload?.payload);\n return;\n case \"channel_history\":\n return this.handleChannelHistoryRequest(\n clientId,\n channel,\n payload?.sinceSeq as number | undefined,\n payload?.limit as number | undefined\n );\n case \"presence_track\":\n // Auto-join the channel so presence works without a separate join\n this.joinChannel(clientId, channel);\n this.trackPresence(clientId, channel, payload?.state as Record<string, unknown> ?? {});\n return;\n case \"presence_state\":\n this.sendPresenceState(clientId, channel);\n return;\n }\n }\n\n /**\n * Decide whether a client may perform an action on a channel.\n *\n * **Membership is the floor.** Reading a channel's presence roster, replaying\n * its retained history and broadcasting into it all require that this client\n * has joined it. That is a low bar — joining is open to anyone who can name\n * the channel — but it is not the bar that was there before, which was none\n * at all: `channel_history` and `presence_state` answered any socket about\n * any channel, and a broadcast fanned out to members the sender had never\n * joined. Two internal tables (`rebase.channel_presence`,\n * `rebase.channel_messages`) are held outside RLS on the strength of this\n * check, so it fails closed: an authorizer that throws refuses the frame.\n *\n * Anything richer than membership belongs in a {@link ChannelAuthorizer};\n * this method is where it is consulted, and the only place.\n */\n private authorizeChannelAction(\n clientId: string,\n channel: string,\n action: ChannelAction,\n authContext?: SubscriptionAuthContext\n ): boolean | Promise<boolean> {\n // Joining is what establishes membership, so it cannot require it.\n if (action !== \"join\" && !this.channels.get(channel)?.has(clientId)) {\n this.denyChannelAction(clientId, channel, action, \"not a member of the channel\");\n return false;\n }\n\n const authorizer = this.channelAuthorizer;\n if (!authorizer) return true;\n\n let verdict: boolean | Promise<boolean>;\n try {\n verdict = authorizer({ channel, action, clientId, user: authContext });\n } catch (error) {\n logger.error(`❌ [Channels] Authorizer threw for ${action} on \"${channel}\" — refusing`, { error });\n this.denyChannelAction(clientId, channel, action, \"channel authorization failed\");\n return false;\n }\n\n if (typeof verdict === \"boolean\") {\n if (!verdict) this.denyChannelAction(clientId, channel, action, \"refused by the channel authorizer\");\n return verdict;\n }\n\n return verdict.then(\n (ok) => {\n if (!ok) this.denyChannelAction(clientId, channel, action, \"refused by the channel authorizer\");\n return ok;\n },\n (error) => {\n logger.error(`❌ [Channels] Authorizer rejected for ${action} on \"${channel}\" — refusing`, { error });\n this.denyChannelAction(clientId, channel, action, \"channel authorization failed\");\n return false;\n }\n );\n }\n\n /** Tell the client why its channel frame went nowhere, and say so in the log. */\n private denyChannelAction(clientId: string, channel: string, action: ChannelAction, reason: string): void {\n this.debugLog(`🚫 [Channels] Refused ${action} on \"${channel}\" for ${clientId}: ${reason}`);\n this.sendError(\n clientId,\n `Refused ${action} on channel \"${channel}\": ${reason}`,\n undefined,\n \"CHANNEL_FORBIDDEN\"\n );\n }\n\n /** Join a broadcast channel */\n joinChannel(clientId: string, channel: string): void {\n if (!this.channels.has(channel)) {\n this.channels.set(channel, new Set());\n }\n this.channels.get(channel)!.add(clientId);\n this.warnIfMemoryBusOnMultiplePods();\n this.debugLog(`📡 [Broadcast] Client ${clientId} joined channel: ${channel}`);\n }\n\n /**\n * Say something the first time channels are used on a deployment that is\n * demonstrably multi-pod while the bus is still the in-memory default.\n *\n * Every other warning in this subsystem covers a *configured* bus failing —\n * the case where the operator already knew a bus mattered. The common\n * misconfiguration is the opposite one: scaled to two replicas, never\n * touched `realtime.bus`, and broadcast and presence quietly serve a\n * fraction of the room. The evidence is already in the process, so use it.\n */\n private warnIfMemoryBusOnMultiplePods(): void {\n if (this.memoryBusWarned) return;\n if (this.bus.kind !== \"memory\" || !this.foreignInstanceSeen) return;\n this.memoryBusWarned = true;\n logger.warn(\n \"⚠️ [ChannelBus] Channels are in use with the in-memory bus, but notifications from another \" +\n \"instance have been seen — this deployment runs more than one process. Broadcast and presence \" +\n \"reach only the clients connected to this one. Set `realtime.bus` (or REALTIME_CHANNEL_BUS=postgres) \" +\n \"to make channels cross-instance.\"\n );\n }\n\n /** Leave a broadcast channel */\n leaveChannel(clientId: string, channel: string): void {\n const members = this.channels.get(channel);\n if (members) {\n members.delete(clientId);\n if (members.size === 0) this.channels.delete(channel);\n }\n // Also remove presence\n this.removePresence(clientId, channel);\n }\n\n /**\n * Broadcast a message to all clients in a channel except the sender.\n *\n * On a channel with no retention rule this is what it always was: a\n * synchronous fan-out to whoever is connected, with no sequence number, no\n * SQL and no await — the body below runs to completion before returning.\n *\n * On a retained channel the message is durably numbered first and only then\n * delivered, through a per-channel queue so that delivery order matches\n * sequence order. That ordering is the whole point: a client that catches up\n * with `sinceSeq` has to arrive at the same state as one that never\n * disconnected.\n */\n broadcastToChannel(clientId: string, channel: string, event: string, payload: unknown): void {\n const retention = this.channelHistory?.retentionFor(channel);\n if (!retention) {\n this.fanOutBroadcast(clientId, channel, event, payload);\n // Other instances get the same frame, but never before the clients\n // on this one: the local fan-out above is synchronous and the\n // publish is not, which is also what keeps the ephemeral path free\n // of any await for a single-instance deployment.\n this.publishBroadcast(clientId, channel, event, payload);\n return;\n }\n\n const previous = this.channelSendQueues.get(channel) ?? Promise.resolve();\n const next = previous\n // A failed predecessor must not poison the chain — the next message\n // on this channel is independent and still deserves to be sent.\n .catch(() => { /* already reported below */ })\n .then(() => this.persistAndFanOut(clientId, channel, event, payload, retention));\n\n this.channelSendQueues.set(channel, next);\n void next.finally(() => {\n // Only clear if nothing has queued behind us in the meantime.\n if (this.channelSendQueues.get(channel) === next) this.channelSendQueues.delete(channel);\n });\n }\n\n /**\n * Number a broadcast, store it, then deliver it.\n *\n * A message that cannot be stored is **not** delivered. Delivering it would\n * put it in front of live subscribers while leaving it absent from every\n * future replay — the two views of the channel would disagree permanently,\n * and no later message could repair the gap. Failing loudly to the sender\n * instead lets it retry, which for an operation stream is the only outcome\n * that keeps clients convergent.\n */\n private async persistAndFanOut(\n clientId: string,\n channel: string,\n event: string,\n payload: unknown,\n retention: ResolvedRetention\n ): Promise<void> {\n let seq: number;\n try {\n ({ seq } = await this.channelHistory!.append(channel, event, payload, clientId));\n } catch (error) {\n logger.error(`❌ [ChannelHistory] Could not persist broadcast on \"${channel}\" — message dropped`, { error });\n this.sendError(\n clientId,\n `Could not persist broadcast on retained channel \"${channel}\"`,\n undefined,\n \"CHANNEL_HISTORY_WRITE_FAILED\"\n );\n return;\n }\n\n this.fanOutBroadcast(clientId, channel, event, payload, seq);\n this.publishBroadcast(clientId, channel, event, payload, seq);\n\n try {\n await this.channelHistory!.prune(channel, retention);\n } catch (error) {\n // Retention is a housekeeping concern; the message is already\n // delivered and durable, so a failed prune must not surface as a\n // broadcast failure. It will be retried on the next message.\n logger.warn(`⚠️ [ChannelHistory] Prune failed for \"${channel}\"`, { error });\n }\n }\n\n /** Deliver a broadcast frame to every member of a channel but the sender. */\n private fanOutBroadcast(clientId: string, channel: string, event: string, payload: unknown, seq?: number): void {\n const members = this.channels.get(channel);\n if (!members) return;\n\n const message = JSON.stringify({\n type: \"broadcast\",\n channel,\n event,\n payload,\n ...(seq !== undefined ? { seq } : {})\n });\n\n for (const memberId of members) {\n if (memberId === clientId) continue; // Don't echo back to sender\n const ws = this.clients.get(memberId);\n if (ws && ws.readyState === WebSocket.OPEN) {\n ws.send(message);\n }\n }\n }\n\n // =============================================================================\n // Cross-Instance Channel Bus\n // =============================================================================\n\n /**\n * Install the transport that carries channel frames between instances.\n *\n * Called once at boot. A bus that cannot start is reported and replaced with\n * the memory bus: losing cross-instance fan-out degrades collaboration to\n * what it was before this existed, whereas refusing to boot takes the whole\n * backend down for it.\n */\n async configureChannelBus(bus: ChannelBus): Promise<void> {\n if (bus.kind === \"memory\") {\n this.bus = bus;\n return;\n }\n\n try {\n await bus.start((frame) => this.handleBusFrame(frame));\n } catch (error) {\n logger.warn(\n `⚠️ [ChannelBus] Could not start the \"${bus.kind}\" channel bus — channel broadcast and presence ` +\n \"stay per-instance. Clients served by different replicas will not see each other.\",\n { error }\n );\n await bus.stop().catch(() => { /* best effort */ });\n this.bus = new MemoryChannelBus();\n return;\n }\n\n this.bus = bus;\n\n // Presence needs shared *state*, not just shared fan-out — see\n // `channel-presence.ts`. It comes up with the bus and only with it.\n try {\n const store = new ChannelPresenceStore(this.db, this.instanceId);\n await store.ensureTables();\n this.presenceStore = store;\n this.ensurePresenceSweep();\n } catch (error) {\n logger.warn(\n \"⚠️ [ChannelBus] Could not create the shared presence table — presence rosters will only list \" +\n \"clients connected to this instance (broadcast is unaffected).\",\n { error }\n );\n this.presenceStore = undefined;\n }\n\n logger.info(\n `📡 [ChannelBus] Cross-instance channels active via ${bus.kind} (instanceId: ${this.instanceId}).`\n );\n }\n\n /** Which transport is in use — `\"memory\"` means per-instance only. */\n public getChannelBusKind(): ChannelBus[\"kind\"] {\n return this.bus.kind;\n }\n\n /**\n * Send a broadcast to the other instances.\n *\n * Fire-and-forget by design: the clients on this instance have already been\n * served, and a bus that is briefly unreachable must not turn a broadcast\n * into an error for the sender.\n */\n private publishBroadcast(clientId: string, channel: string, event: string, payload: unknown, seq?: number): void {\n if (this.bus.kind === \"memory\") return;\n\n const frame: ChannelBusFrame = {\n kind: \"broadcast\",\n sid: this.instanceId,\n channel,\n event,\n from: clientId,\n ...(seq !== undefined ? { seq } : {}),\n payload\n };\n\n // Postgres caps a NOTIFY payload at 8 KB. A retained message is already\n // durable and addressable, so it travels as a pointer and each receiver\n // reads the body back — the same shape as the entity path, which\n // notifies an address and refetches the row.\n if (frameByteLength(frame) > this.bus.maxFrameBytes) {\n if (seq === undefined) {\n this.reportOversizedBroadcast(clientId, channel);\n return;\n }\n void this.publishFrame({\n kind: \"broadcast_ref\",\n sid: this.instanceId,\n channel,\n from: clientId,\n seq\n });\n return;\n }\n\n void this.publishFrame(frame);\n }\n\n private async publishFrame(frame: ChannelBusFrame): Promise<void> {\n try {\n await this.bus.publish(frame);\n } catch (error) {\n logger.error(\"❌ [ChannelBus] Failed to publish frame — other instances did not receive it\", {\n detail: `${frame.kind} on \"${frame.channel}\"`,\n error\n });\n }\n }\n\n /**\n * Tell the sender that a message was delivered locally but nowhere else.\n *\n * Staying quiet here would be the worst option available: on one instance\n * the app works, on two it works for half the users, and nothing in the\n * logs connects the two. The fix is a one-liner in config — give the\n * channel a retention rule and the message travels as a pointer instead —\n * so the message says exactly that.\n */\n private reportOversizedBroadcast(clientId: string, channel: string): void {\n const remedy =\n `Add a retention rule for \"${channel}\" (realtime.channels) — retained messages travel by reference ` +\n \"and have no size limit.\";\n\n if (!this.oversizedBroadcastWarned.has(channel)) {\n this.oversizedBroadcastWarned.add(channel);\n logger.warn(\n `⚠️ [ChannelBus] A broadcast on ephemeral channel \"${channel}\" exceeds the ` +\n `${this.bus.maxFrameBytes}-byte limit of the ${this.bus.kind} bus and reached only this instance. ` +\n remedy\n );\n }\n this.sendError(\n clientId,\n `Broadcast on \"${channel}\" was too large to reach other instances. ${remedy}`,\n undefined,\n \"CHANNEL_BUS_PAYLOAD_TOO_LARGE\"\n );\n }\n\n /**\n * Deliver a frame published by another instance to this one's clients.\n *\n * Frames we published ourselves are dropped on arrival — the local fan-out\n * happened before the publish — exactly as the entity-change handler skips\n * its own `sid`.\n */\n private async handleBusFrame(frame: ChannelBusFrame): Promise<void> {\n if (frame.sid === this.instanceId) return;\n\n switch (frame.kind) {\n case \"broadcast\":\n this.fanOutBroadcast(frame.from ?? \"\", frame.channel, frame.event, frame.payload, frame.seq);\n return;\n\n case \"broadcast_ref\": {\n // Nothing to read back for: skip the query rather than pay for\n // a message no client here is waiting for.\n if (!this.channels.get(frame.channel)?.size) return;\n\n const entry = await this.channelHistory?.getBySeq(frame.channel, frame.seq);\n if (!entry) {\n logger.warn(\n `⚠️ [ChannelBus] Message ${frame.seq} on \"${frame.channel}\" is no longer retained — ` +\n \"clients on this instance will need to replay (channel_history) to catch up.\"\n );\n return;\n }\n this.fanOutBroadcast(frame.from ?? \"\", frame.channel, entry.event, entry.payload, entry.seq);\n return;\n }\n\n case \"presence_diff\":\n this.deliverPresenceDiff(frame.channel, frame.joins, frame.leaves);\n return;\n }\n }\n\n // =============================================================================\n // Channel History\n // =============================================================================\n\n /**\n * Install retention rules and create the tables they need.\n *\n * Safe to call with no rules (and safe not to call at all): the store stays\n * inert, no schema is created, and broadcast keeps its original\n * fire-and-forget path.\n */\n async configureChannelHistory(\n rules: ChannelRetentionRule[] | undefined,\n options?: { provision?: boolean }\n ): Promise<void> {\n // The store is built in every process, whether or not this one creates\n // the tables: retaining a message is what a process does when it\n // *publishes* to a retained channel, and a function handler publishes as\n // readily as a websocket client does. Only the DDL is owned.\n this.channelHistory = new ChannelHistoryStore(this.db, rules ?? []);\n if (!this.channelHistory.enabled) return;\n if (options?.provision === false) return;\n await this.channelHistory.ensureTables();\n }\n\n /** Whether any channel is configured to retain messages. */\n public isChannelHistoryEnabled(): boolean {\n return this.channelHistory?.enabled ?? false;\n }\n\n /**\n * Answer a client's catch-up request.\n *\n * A channel with no retention rule is answered with `retained: false`\n * rather than an empty list, so the client can tell \"you missed nothing\"\n * apart from \"this channel never keeps anything\" — the second means its\n * reconnect strategy has to be a full resync, and silence would leave it\n * guessing.\n */\n private async handleChannelHistoryRequest(\n clientId: string,\n channel: string,\n sinceSeq?: number,\n limit?: number\n ): Promise<void> {\n if (!channel) return;\n\n const retention = this.channelHistory?.retentionFor(channel);\n if (!retention) {\n this.sendChannelHistory(clientId, channel, [], false);\n return;\n }\n\n try {\n const { messages, latestSeq } = await this.channelHistory!.replay(channel, sinceSeq, limit);\n this.sendChannelHistory(clientId, channel, messages, true, latestSeq);\n } catch (error) {\n logger.error(`❌ [ChannelHistory] Replay failed for \"${channel}\"`, { error });\n this.sendError(clientId, `Could not replay history for channel \"${channel}\"`, undefined, \"CHANNEL_HISTORY_READ_FAILED\");\n }\n }\n\n private sendChannelHistory(\n clientId: string,\n channel: string,\n messages: ChannelHistoryEntry[],\n retained: boolean,\n latestSeq?: number\n ): void {\n const ws = this.clients.get(clientId);\n if (ws && ws.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({\n type: \"channel_history\",\n channel,\n messages,\n retained,\n ...(latestSeq !== undefined ? { latestSeq } : {})\n }));\n }\n }\n\n // =============================================================================\n // Presence\n // =============================================================================\n\n /**\n * Track presence in a channel.\n *\n * The client re-sends this every ~20s as a heartbeat against the 30s\n * timeout, so most calls carry the state that is already recorded. Those\n * refresh `last_seen` and stop there: re-announcing an unchanged state to\n * every instance would put a bus message per client per heartbeat on the\n * wire to tell everyone nothing happened.\n */\n trackPresence(clientId: string, channel: string, state: Record<string, unknown>): void {\n if (!this.presence.has(channel)) {\n this.presence.set(channel, new Map());\n }\n\n const channelPresence = this.presence.get(channel)!;\n const previous = channelPresence.get(clientId);\n const changed = !previous || JSON.stringify(previous.state) !== JSON.stringify(state);\n channelPresence.set(clientId, { state,\nlastSeen: Date.now() });\n\n // Refresh the shared roster on every heartbeat — that timestamp is what\n // tells other instances this client is still here.\n void this.presenceStoreOp(() => this.presenceStore!.track(channel, clientId, state), \"track\");\n\n // Broadcast join / state update to channel\n this.deliverPresenceDiff(channel, { [clientId]: state }, {});\n if (changed) {\n this.publishPresenceDiff(channel, { [clientId]: state }, {});\n }\n\n // Start cleanup interval if not running\n this.ensurePresenceCleanup();\n }\n\n /**\n * Remove presence from a channel.\n *\n * `skipStore` is for the socket-close path, which clears every channel at\n * once and then deletes the client's rows in a single statement instead of\n * one per channel.\n */\n removePresence(clientId: string, channel: string, options?: { skipStore?: boolean }): void {\n const channelPresence = this.presence.get(channel);\n if (!channelPresence) return;\n\n const entry = channelPresence.get(clientId);\n if (entry) {\n channelPresence.delete(clientId);\n this.deliverPresenceDiff(channel, {}, { [clientId]: entry.state });\n this.publishPresenceDiff(channel, {}, { [clientId]: entry.state });\n if (!options?.skipStore) {\n void this.presenceStoreOp(() => this.presenceStore!.remove(channel, clientId), \"remove\");\n }\n }\n\n if (channelPresence.size === 0) {\n this.presence.delete(channel);\n }\n }\n\n /**\n * Send the full roster for a channel to one client.\n *\n * Answered from the shared table when there is one, because \"who is in this\n * document?\" has a single answer that must not depend on which replica the\n * asker happens to be connected to. Without a bus there is nothing to share\n * and the local map *is* the roster — that path stays synchronous, which is\n * what it always was.\n */\n sendPresenceState(clientId: string, channel: string): void {\n if (!this.presenceStore) {\n this.sendPresenceStateMessage(clientId, channel, this.localPresences(channel));\n return;\n }\n\n void this.presenceStore.roster(channel)\n .then((presences) => {\n this.sendPresenceStateMessage(clientId, channel, presences);\n })\n .catch((error) => {\n // A roster the asker can act on beats none: fall back to the\n // clients we can see rather than leaving the request unanswered.\n logger.warn(`⚠️ [Presence] Could not read the shared roster for \"${channel}\" — answering with this instance's clients only.`, { error });\n this.sendPresenceStateMessage(clientId, channel, this.localPresences(channel));\n });\n }\n\n /** Presence of the clients connected to this instance. */\n private localPresences(channel: string): Record<string, Record<string, unknown>> {\n const channelPresence = this.presence.get(channel);\n const presences: Record<string, Record<string, unknown>> = {};\n if (channelPresence) {\n for (const [id, { state }] of channelPresence) {\n presences[id] = state;\n }\n }\n return presences;\n }\n\n private sendPresenceStateMessage(\n clientId: string,\n channel: string,\n presences: Record<string, Record<string, unknown>>\n ): void {\n const ws = this.clients.get(clientId);\n if (ws && ws.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({\n type: \"presence_state\",\n channel,\n presences\n }));\n }\n }\n\n /** Deliver a presence diff to this instance's members of the channel. */\n private deliverPresenceDiff(\n channel: string,\n joins: Record<string, Record<string, unknown>>,\n leaves: Record<string, Record<string, unknown>>\n ): void {\n const members = this.channels.get(channel);\n if (!members) return;\n\n const message = JSON.stringify({\n type: \"presence_diff\",\n channel,\n joins,\n leaves\n });\n\n for (const memberId of members) {\n const ws = this.clients.get(memberId);\n if (ws && ws.readyState === WebSocket.OPEN) {\n ws.send(message);\n }\n }\n }\n\n /** Tell the other instances about a presence change. */\n private publishPresenceDiff(\n channel: string,\n joins: Record<string, Record<string, unknown>>,\n leaves: Record<string, Record<string, unknown>>\n ): void {\n if (this.bus.kind === \"memory\") return;\n void this.publishFrame({ kind: \"presence_diff\", sid: this.instanceId, channel, joins, leaves });\n }\n\n /** Run a roster write when there is a roster, and never let it throw. */\n private async presenceStoreOp(op: () => Promise<void>, label: string): Promise<void> {\n if (!this.presenceStore) return;\n try {\n await op();\n } catch (error) {\n logger.warn(`⚠️ [Presence] Shared roster ${label} failed`, { error });\n }\n }\n\n /** Periodic cleanup for stale presences */\n private ensurePresenceCleanup(): void {\n if (this.presenceInterval) return;\n this.presenceInterval = setInterval(() => {\n const now = Date.now();\n for (const [channel, channelPresence] of this.presence) {\n for (const [clientId, entry] of channelPresence) {\n if (now - entry.lastSeen > RealtimeService.PRESENCE_TIMEOUT_MS) {\n this.removePresence(clientId, channel);\n }\n }\n }\n // Stop interval if no presences tracked\n if (this.presence.size === 0 && this.presenceInterval) {\n clearInterval(this.presenceInterval);\n this.presenceInterval = undefined;\n }\n }, 10000); // Check every 10s\n }\n\n /**\n * Reap roster rows whose owning instance stopped heartbeating.\n *\n * This is the cross-instance half of the sweep above, and it doubles as\n * crash recovery: a pod that dies takes its clients with it but leaves\n * their rows behind, and after one TTL window they look exactly like any\n * other client that went quiet. The delete returns what it removed, so\n * whichever instance wins the race is the one that announces the\n * departures — once for the cluster, not once per replica.\n */\n private ensurePresenceSweep(): void {\n if (this.presenceSweepInterval || !this.presenceStore) return;\n\n this.presenceSweepInterval = setInterval(\n () => void this.sweepStalePresence(),\n RealtimeService.PRESENCE_SWEEP_INTERVAL_MS\n );\n\n // Never hold the process open for housekeeping.\n (this.presenceSweepInterval as unknown as { unref?: () => void }).unref?.();\n }\n\n /** One pass of the stale-roster sweep. See {@link ensurePresenceSweep}. */\n private async sweepStalePresence(): Promise<void> {\n if (!this.presenceStore) return;\n try {\n const removed = await this.presenceStore.sweepStale(RealtimeService.PRESENCE_TIMEOUT_MS);\n for (const row of removed) {\n this.debugLog(`👻 [Presence] Reaped stale presence ${row.clientId} on \"${row.channel}\"`);\n this.deliverPresenceDiff(row.channel, {}, { [row.clientId]: row.state });\n this.publishPresenceDiff(row.channel, {}, { [row.clientId]: row.state });\n }\n } catch (error) {\n logger.warn(\"⚠️ [Presence] Stale-roster sweep failed\", { error });\n }\n }\n\n // =============================================================================\n // Lifecycle / Cleanup\n // =============================================================================\n\n /**\n * Gracefully tear down all realtime resources.\n *\n * This MUST be called during process shutdown, **before** `pool.end()`.\n * It ensures:\n * 1. All debounced refetch timers are cancelled (prevents queries after pool closes).\n * 2. All subscription state and callbacks are cleared.\n * 3. The dedicated LISTEN client (outside the pool) is disconnected.\n * 4. All WebSocket clients are removed (but not forcefully closed — the\n * HTTP server close will handle that).\n */\n async destroy(): Promise<void> {\n // 1. Cancel every pending debounced refetch timer\n for (const [key, timer] of this.refetchTimers) {\n clearTimeout(timer);\n this.refetchTimers.delete(key);\n }\n\n // 2. Clear subscriptions and callbacks\n this._subscriptions.clear();\n this.subscriptionCallbacks.clear();\n\n // 3. Clear broadcast channels and presence\n this.channels.clear();\n this.presence.clear();\n // Pending history writes hold the pool open; let them settle before the\n // caller closes it, but never let a rejected one break shutdown.\n await Promise.allSettled([...this.channelSendQueues.values()]);\n this.channelSendQueues.clear();\n this.channelHistory?.clear();\n if (this.presenceInterval) {\n clearInterval(this.presenceInterval);\n this.presenceInterval = undefined;\n }\n if (this.presenceSweepInterval) {\n clearInterval(this.presenceSweepInterval);\n this.presenceSweepInterval = undefined;\n }\n this.oversizedBroadcastWarned.clear();\n\n // Drop this instance's roster rows now rather than leaving every other\n // replica to wait out a TTL window on ghosts — a rolling deploy would\n // otherwise show 30s of departed users on every restart.\n if (this.presenceStore) {\n try {\n await this.presenceStore.removeInstance();\n } catch (error) {\n logger.warn(\"⚠️ [Presence] Could not clear this instance's roster rows on shutdown\", { error });\n }\n this.presenceStore = undefined;\n }\n\n // 4. Disconnect the dedicated LISTEN client(s)\n await this.stopListening();\n await this.stopCdc();\n await this.bus.stop().catch((error) =>\n logger.warn(\"⚠️ [ChannelBus] Error while stopping the channel bus\", { error }));\n this.bus = new MemoryChannelBus();\n\n // 5. Drop client references (don't close — server.close drains them)\n this.clients.clear();\n\n this.debugLog(\"🧹 [RealtimeService] destroy() complete — all resources released.\");\n }\n\n // =============================================================================\n // Database-level Change Data Capture (CDC)\n // =============================================================================\n\n /** Whether database-level change capture is currently the active source. */\n public isCdcActive(): boolean {\n return this.cdcActive;\n }\n\n /**\n * Enable database-level change capture as the realtime source.\n *\n * A dedicated LISTEN client consumes committed changes from the `rebase_cdc`\n * channel (fed by CDC triggers — see {@link provisionTriggerCdc}) and routes\n * them into the same {@link notifyUpdate} pipeline used by API mutations. The\n * effect: subscribers see a change no matter how it was written — psql, a\n * cron in another service, raw SQL, or the Studio SQL editor — exactly like\n * Supabase Realtime tailing the WAL.\n *\n * Because CDC observes every commit on every instance, it also *replaces* the\n * legacy per-mutation cross-instance broadcast (see the guard in\n * {@link notifyUpdate}); callers should not also call {@link startListening}.\n *\n * @param connectionString Direct Postgres connection for the LISTEN client\n * (bypass PgBouncer — LISTEN needs a session connection).\n */\n async enableCdc(connectionString: string): Promise<void> {\n if (this.cdcActive) {\n logger.warn(\"⚠️ [CDC] enableCdc called but CDC is already active. Ignoring.\");\n return;\n }\n this.cdcTableMap = this.buildCdcTableMap();\n this.junctionLinkMap = buildJunctionLinkMap(this.registry);\n this.cdcListener = new CdcListener(connectionString, (event) => this.handleCdcEvent(event));\n try {\n // start() validates the initial connection; if it can't be established\n // it rejects here, and we leave CDC inactive so the caller can fall\n // back to app-level realtime rather than silently dropping events.\n await this.cdcListener.start();\n } catch (err) {\n await this.cdcListener.stop().catch(() => { /* best effort */ });\n this.cdcListener = undefined;\n this.cdcTableMap = undefined;\n this.junctionLinkMap = undefined;\n throw err;\n }\n this.cdcActive = true;\n // The bootstrapper says the same thing one line later, in the\n // vocabulary of the setting that produced it (REALTIME_CDC).\n logger.debug(\n `📡 [RealtimeService] Database-level change capture ACTIVE — writes from ANY source now emit realtime events ` +\n `(${this.cdcTableMap.size} mapped table key(s)).`\n );\n }\n\n /** Stop the CDC listener and clear its state. */\n async stopCdc(): Promise<void> {\n this.cdcActive = false;\n if (this.cdcListener) {\n await this.cdcListener.stop();\n this.cdcListener = undefined;\n }\n this.cdcTableMap = undefined;\n this.junctionLinkMap = undefined;\n this.recentAppEmits.clear();\n }\n\n /**\n * Build the reverse map from database table → collection. A change event\n * carries `schema` + `table`; realtime subscriptions are keyed by collection\n * path (slug). We index by both `schema.table` and bare `table` so the lookup\n * works whether or not the collection declares an explicit schema.\n */\n private buildCdcTableMap(): Map<string, CollectionConfig> {\n const map = new Map<string, CollectionConfig>();\n for (const collection of this.registry.getCollections()) {\n const table = getTableName(collection);\n if (!table) continue;\n const schema = (collection as { schema?: string }).schema ?? \"public\";\n map.set(`${schema}.${table}`, collection);\n // Bare-table fallback; first registration wins to keep it deterministic.\n if (!map.has(table)) map.set(table, collection);\n }\n return map;\n }\n\n private resolveCollectionForTable(schema: string, table: string): CollectionConfig | undefined {\n if (!this.cdcTableMap) return undefined;\n return this.cdcTableMap.get(`${schema}.${table}`) ?? this.cdcTableMap.get(table);\n }\n\n /**\n * Route a captured database change into the realtime pipeline.\n *\n * Delivery is RLS-safe by construction: the raw tuple from the WAL/trigger is\n * NOT forwarded to subscribers. Instead the change is marked invalidated, so\n * every matching subscription re-reads the row under its own auth context via\n * {@link fetchCollectionWithAuth} / {@link fetchEntityWithAuth}. A subscriber\n * therefore only ever receives rows its RLS policies permit — filtering is per\n * subscriber, never per publisher.\n */\n private async handleCdcEvent(event: CdcChangeEvent): Promise<void> {\n const collection = this.resolveCollectionForTable(event.schema, event.table);\n if (!collection) {\n // A junction table backs no collection, but its rows *are* a child\n // list. Route the change to the lists it changes before giving up.\n if (await this.handleJunctionCdcEvent(event)) return;\n\n // Unmapped table (not backed by a collection) — nothing to deliver.\n this.debugLog(`📡 [CDC] Ignoring change on unmapped table ${event.schema}.${event.table}`);\n return;\n }\n\n const path = collection.slug;\n const databaseId = (collection as { databaseId?: string }).databaseId;\n const id = this.extractIdFromCdcRow(collection, event.row);\n\n // Deletes carry a null row (subscribers drop the id); inserts/updates carry\n // an invalidation marker that forces a per-subscriber RLS-bound refetch.\n const row = event.op === \"DELETE\" ? null : { _rebase_invalidated: true };\n\n await this.notifyUpdate(path, id, row, databaseId, /* broadcast */ false, /* origin */ \"cdc\");\n }\n\n /**\n * Deliver a change on a many-to-many junction table as a change to the child\n * lists it belongs to.\n *\n * Linking a tag to a post writes only `posts_tags`. That table backs no\n * collection, so change capture dropped the event as unmapped and the\n * subscribers of `posts/1/tags` never heard about it — every other write in\n * the system was realtime, and this one silently was not. The junction row\n * carries both ids, so it names its own paths exactly.\n *\n * Notifies the nested path rather than either endpoint collection, because\n * invalidation walks *parent* paths and never child ones: telling `tags` it\n * changed would not reach a subscription on `posts/1/tags`.\n *\n * Returns whether the table was recognised as a junction.\n */\n private async handleJunctionCdcEvent(event: CdcChangeEvent): Promise<boolean> {\n const links = this.junctionLinkMap?.get(`${event.schema}.${event.table}`)\n ?? this.junctionLinkMap?.get(event.table);\n if (!links?.length) return false;\n\n for (const link of links) {\n const sourceId = event.row?.[link.sourceColumn];\n const targetId = event.row?.[link.targetColumn];\n if (sourceId === undefined || sourceId === null || targetId === undefined || targetId === null) {\n this.debugLog(\n `📡 [CDC] Junction row on ${event.table} is missing '${link.sourceColumn}'/'${link.targetColumn}' — skipping.`\n );\n continue;\n }\n\n const path = `${link.parentCollection.slug}/${String(sourceId)}/${link.relationKey}`;\n // An unlink removes the target from this list; a link invalidates it\n // so each subscriber refetches under its own RLS context.\n const row = event.op === \"DELETE\" ? null : { _rebase_invalidated: true };\n\n await this.notifyUpdate(\n path,\n String(targetId),\n row,\n (link.parentCollection as { databaseId?: string }).databaseId,\n /* broadcast */ false,\n /* origin */ \"cdc\"\n );\n }\n\n return true;\n }\n\n /** Compute the canonical (possibly composite) id string from a captured row. */\n private extractIdFromCdcRow(collection: CollectionConfig, row: Record<string, unknown>): string {\n // Unaddressable falls back to a collection-level invalidation: single-row\n // subs won't match, but collection subs still refetch.\n return deriveRowAddress(row, collection, this.registry) || \"*\";\n }\n\n // ── App/CDC de-duplication ──\n\n private dedupKey(path: string, id: string, databaseId?: string): string {\n return `${databaseId ?? \"\"}::${path}::${id}`;\n }\n\n /** Record that this instance just delivered `key` via the app path. */\n private markAppEmit(key: string): void {\n const now = Date.now();\n this.recentAppEmits.set(key, now + RealtimeService.CDC_DEDUP_WINDOW_MS);\n // Opportunistic purge so the map cannot grow unbounded under write load.\n if (this.recentAppEmits.size > 1000) {\n for (const [k, expiry] of this.recentAppEmits) {\n if (expiry <= now) this.recentAppEmits.delete(k);\n }\n }\n }\n\n /** Consume a matching app-emit record if present and unexpired; true ⇒ suppress the CDC echo. */\n private consumeAppEmit(key: string): boolean {\n const expiry = this.recentAppEmits.get(key);\n if (expiry === undefined) return false;\n this.recentAppEmits.delete(key);\n return expiry > Date.now();\n }\n\n // =============================================================================\n // Cross-Instance LISTEN/NOTIFY\n // =============================================================================\n\n /**\n * Enable cross-instance realtime broadcasting via Postgres LISTEN/NOTIFY.\n * Creates a dedicated pg.Client (outside the Drizzle pool) that stays\n * connected and listens for change notifications from other instances.\n *\n * This is an **optional** feature — if never called, the backend operates\n * in single-instance mode (the default, perfectly fine for most setups).\n *\n * @param connectionString Raw Postgres connection string for the LISTEN client.\n */\n async startListening(connectionString: string): Promise<void> {\n if (this.broadcasting) {\n logger.warn(\"⚠️ [RealtimeService] startListening called but already listening. Ignoring.\");\n return;\n }\n\n this.listenConnectionString = connectionString;\n // Set broadcasting BEFORE connecting so that scheduleReconnect()\n // works correctly if the initial connection attempt fails.\n this.broadcasting = true;\n await this.connectListenClient();\n logger.info(`📡 [RealtimeService] Cross-instance realtime enabled (instanceId: ${this.instanceId})`);\n }\n\n /**\n * Stop listening and clean up the dedicated LISTEN connection.\n */\n async stopListening(): Promise<void> {\n this.broadcasting = false;\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = undefined;\n }\n if (this.listenClient) {\n try {\n await this.listenClient.end();\n } catch { /* ignore close errors */ }\n this.listenClient = undefined;\n }\n logger.info(\"📡 [RealtimeService] Cross-instance realtime disabled.\");\n }\n\n /**\n * Broadcast a change notification to other instances via pg_notify.\n * Uses the main Drizzle connection (pooled) — NOT the LISTEN client.\n */\n private async broadcastChange(path: string, id: string, databaseId?: string): Promise<void> {\n const payload = JSON.stringify({\n sid: this.instanceId,\n p: path,\n eid: id,\n db: databaseId ?? null\n });\n await this.db.execute(drizzleSql`SELECT pg_notify(${PG_NOTIFY_CHANNEL}, ${payload})`);\n }\n\n /**\n * Create and connect the dedicated LISTEN client with auto-reconnect.\n */\n private async connectListenClient(): Promise<void> {\n if (!this.listenConnectionString) return;\n\n let pending: PgClient | undefined;\n try {\n // See `PgNotifyListener.connect` — same shape, same reason. Until\n // `this.listenClient` is assigned, nothing else in this class knows\n // the connection exists, so a throw between `connect()` and that\n // assignment leaks a live backend and `scheduleReconnect` opens\n // another one three seconds later.\n const client = new PgClient({ connectionString: this.listenConnectionString });\n pending = client;\n\n client.on(\"error\", (err) => {\n logger.error(\"❌ [RealtimeService] LISTEN client error\", { detail: err.message });\n this.scheduleReconnect();\n });\n\n client.on(\"end\", () => {\n if (this.broadcasting) {\n logger.warn(\"⚠️ [RealtimeService] LISTEN client disconnected unexpectedly.\");\n this.scheduleReconnect();\n }\n });\n\n client.on(\"notification\", async (msg) => {\n if (!msg.payload) return;\n try {\n const { sid, p, eid, db } = JSON.parse(msg.payload) as {\n sid: string;\n p: string;\n eid: string;\n db: string | null;\n };\n\n // Skip our own notifications — already processed locally\n if (sid === this.instanceId) return;\n\n // A foreign sid is proof of a second process. Nothing here\n // needs that fact, but the channel path does — see\n // `warnIfMemoryBusOnMultiplePods`.\n this.foreignInstanceSeen = true;\n\n this.debugLog(`📡 [RealtimeService] Received cross-instance notification: path=${p}, id=${eid}, from=${sid}`);\n\n // Refetch the row from the DB so row subscriptions\n // receive the actual data instead of null (which the client\n // would interpret as \"deleted\").\n let refetchedRow: Record<string, unknown> | null = null;\n try {\n if (this.driver) {\n const collection = this.registry.getCollectionByPath(p);\n const fetched = await this.driver.fetchOne({\n path: p,\n id: eid,\n collection: collection\n });\n refetchedRow = fetched ?? null;\n } else {\n const fetched = await this.dataService.fetchOne(\n p, eid, db ?? undefined\n );\n refetchedRow = fetched ?? null;\n }\n } catch (fetchErr) {\n // If the fetch fails (e.g. row was deleted), refetchedRow stays null\n this.debugLog(`📡 [RealtimeService] Could not refetch row ${eid} from ${p} — treating as deleted`, fetchErr);\n }\n\n // Trigger local fan-out with broadcast=false to avoid re-broadcasting\n await this.notifyUpdate(p, eid, refetchedRow, db ?? undefined, false);\n } catch (err) {\n logger.error(\"❌ [RealtimeService] Error processing cross-instance notification\", { error: err });\n }\n });\n\n await client.connect();\n await client.query(`LISTEN ${PG_NOTIFY_CHANNEL}`);\n this.listenClient = client;\n // Adopted: `destroy()` and `scheduleReconnect` close it now.\n pending = undefined;\n\n this.debugLog(`📡 [RealtimeService] LISTEN client connected on channel \"${PG_NOTIFY_CHANNEL}\"`);\n } catch (err) {\n if (pending) {\n try { await pending.end(); } catch { /* already dead */ }\n }\n logger.error(\"❌ [RealtimeService] Failed to connect LISTEN client\", { error: err });\n this.scheduleReconnect();\n }\n }\n\n /**\n * Schedule a reconnection attempt with a fixed 3s delay.\n */\n private scheduleReconnect(): void {\n if (!this.broadcasting || this.reconnectTimer) return;\n\n const delay = 3000; // Fixed 3s delay; simple and predictable\n this.debugLog(`📡 [RealtimeService] Scheduling LISTEN reconnect in ${delay}ms...`);\n\n this.reconnectTimer = setTimeout(async () => {\n this.reconnectTimer = undefined;\n if (!this.broadcasting) return;\n\n // Clean up old client\n if (this.listenClient) {\n try { await this.listenClient.end(); } catch { /* ignore */ }\n this.listenClient = undefined;\n }\n\n await this.connectListenClient();\n }, delay);\n }\n}\n\n/**\n * Alias for RealtimeService for consistent naming with other database implementations.\n * This allows code to use PostgresRealtimeProvider alongside future MongoRealtimeProvider, etc.\n */\nexport const PostgresRealtimeProvider = RealtimeService;\n","import { CollectionRegistry, resolveCollectionRelations } from \"@rebasepro/common\";\nimport { type CollectionConfig } from \"@rebasepro/types\";\nimport { PgEnum, PgTable } from \"drizzle-orm/pg-core\";\nimport { Relations } from \"drizzle-orm\";\nimport { CollectionRegistryInterface } from \"../interfaces\";\nimport { getTableName } from \"@rebasepro/common\";\n\n/**\n * PostgreSQL-specific collection registry.\n * Extends the base CollectionRegistry with support for Drizzle ORM tables, enums, and relations.\n *\n * Satisfies CollectionRegistryInterface through inheritance from CollectionRegistry.\n */\nexport class PostgresCollectionRegistry extends CollectionRegistry implements CollectionRegistryInterface {\n\n private tables = new Map<string, PgTable>();\n private enums = new Map<string, PgEnum<[string, ...string[]]>>();\n private relations = new Map<string, Relations>();\n\n registerTable(table: PgTable, tableName: string) {\n this.tables.set(tableName, table);\n }\n\n getTable(tableName: string): PgTable | undefined {\n return this.tables.get(tableName);\n }\n\n /**\n * Checks if a specific collection has a registered table\n */\n hasTableForCollection(tableName: string): boolean {\n return this.tables.has(tableName);\n }\n\n /**\n * Returns all registered table names.\n */\n getTableNames(): string[] {\n return Array.from(this.tables.keys());\n }\n\n /**\n * Finds collections assigned to a specific data source that do not have a registered table.\n */\n getCollectionsWithoutTables(dataSourceKey = \"(default)\"): CollectionConfig[] {\n const collections = this.getCollections().filter(\n c => c.dataSource === dataSourceKey || (!c.dataSource && dataSourceKey === \"(default)\")\n );\n return collections.filter(c => !this.tables.has(getTableName(c)));\n }\n\n registerEnums(enums: Record<string, PgEnum<[string, ...string[]]>>) {\n Object.entries(enums).forEach(([name, value]) => this.enums.set(name, value));\n }\n\n registerRelations(relations: Record<string, Relations>) {\n Object.entries(relations).forEach(([name, value]) => this.relations.set(name, value));\n }\n\n getEnum(name: string): PgEnum<[string, ...string[]]> | undefined {\n return this.enums.get(name);\n }\n\n getRelation(name: string): Relations | undefined {\n return this.relations.get(name);\n }\n\n getAllEnums(): Record<string, PgEnum<[string, ...string[]]>> {\n return Object.fromEntries(this.enums.entries());\n }\n\n getAllRelations(): Record<string, Relations> {\n return Object.fromEntries(this.relations.entries());\n }\n\n /**\n * Get the merged schema object (tables + relations) for use with Drizzle's\n * relational query API (`db.query`).\n */\n getMergedSchema(): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const [name, table] of this.tables.entries()) {\n result[name] = table;\n }\n for (const [name, relation] of this.relations.entries()) {\n result[name] = relation;\n }\n return result;\n }\n\n /**\n * Get the available Drizzle relation keys for a given collection path.\n * Maps from the collection's relation property names to the Drizzle relation names\n * defined in the schema.\n */\n getRelationKeysForCollection(collectionPath: string): string[] {\n const collection = this.getCollectionByPath(collectionPath);\n if (!collection) return [];\n // Resolved, not authored. `relationName` is optional at the authoring\n // surface and defaults to the property key or the target's slug, so\n // reading the raw field dropped every relation that relied on the\n // default — and never saw relations declared inline on a property at\n // all, since those are not in the `relations` array.\n return Object.keys(resolveCollectionRelations(collection));\n }\n\n}\n\n","/**\n * Scheduled backups, wired into the server cron system.\n *\n * A backend enables nightly (or custom-schedule) backups by dropping a cron\n * file that default-exports {@link createBackupCron}. The heavy lifting —\n * dump, upload, prune — reuses the same primitives as the CLI so behaviour\n * is identical between a manual `rebase db backup` and an automated run.\n */\nimport fs from \"fs\";\nimport type { CronJobDefinition } from \"@rebasepro/types\";\nimport type { StorageController } from \"@rebasepro/server\";\nimport {\n BackupDestination,\n parseBackupDestination,\n parseDbNameFromUrl,\n resolveConnectionString\n} from \"./pg-tools\";\n// NOTE: `./backup-service` pulls in `execa` (ESM-only). It is imported\n// lazily inside the handler so the pure `backupCronConfigFromEnv` parser can\n// be unit-tested under CommonJS without loading it.\n\nexport interface BackupCronConfig {\n /** Cron expression, e.g. `\"0 3 * * *\"` for 03:00 daily. */\n schedule: string;\n /** Postgres connection string. Defaults to `DATABASE_URL`. */\n connectionString: string;\n /** Where backups are written: a local path or an `s3://` / `gs://` URL. */\n destination: BackupDestination;\n /**\n * Storage controller used for `s3`/`gcs` destinations. Reuse the one the\n * backend already configured (S3/GCS/Local StorageController). Not needed\n * for local destinations.\n */\n storage?: StorageController;\n /** Delete backups older than this many days. `0` disables pruning. */\n retentionDays?: number;\n /** Always keep at least this many recent backups regardless of age. */\n keepMinimum?: number;\n /** Schemas to exclude from the dump (defaults to Atlas revision schema). */\n excludeSchemas?: string[];\n /** Cron job display name. */\n name?: string;\n /** Whether the job is enabled. */\n enabled?: boolean;\n}\n\nexport interface EnvResolution {\n /** Resolved config, present when a schedule is configured. */\n config?: Omit<BackupCronConfig, \"storage\">;\n /** True when `BACKUP_SCHEDULE` is unset — the cron should be a no-op. */\n disabled?: boolean;\n /** Human-readable reason the config could not be built. */\n error?: string;\n}\n\n/**\n * Build backup-cron config purely from environment variables.\n * Recognised keys:\n * - `BACKUP_SCHEDULE` cron expression (required to enable)\n * - `BACKUP_DESTINATION` local path or `s3://` / `gs://` URL (required)\n * - `BACKUP_RETENTION_DAYS` integer days (optional)\n * - `BACKUP_KEEP_MINIMUM` integer count (optional)\n * - `DATABASE_URL` connection string\n *\n * Pure and side-effect free so it can be unit-tested without a database.\n */\nexport function backupCronConfigFromEnv(env: Record<string, string | undefined>): EnvResolution {\n const schedule = env.BACKUP_SCHEDULE?.trim();\n if (!schedule) {\n return { disabled: true };\n }\n\n const connectionString = resolveConnectionString(env);\n if (!connectionString) {\n return { error: \"BACKUP_SCHEDULE is set but DATABASE_URL is not configured.\" };\n }\n\n const destinationRaw = env.BACKUP_DESTINATION?.trim();\n if (!destinationRaw) {\n return { error: \"BACKUP_SCHEDULE is set but BACKUP_DESTINATION is not configured.\" };\n }\n const destination = parseBackupDestination(destinationRaw);\n\n const retentionDays = parseOptionalInt(env.BACKUP_RETENTION_DAYS);\n if (retentionDays === \"invalid\") {\n return { error: `BACKUP_RETENTION_DAYS must be an integer, got \"${env.BACKUP_RETENTION_DAYS}\".` };\n }\n const keepMinimum = parseOptionalInt(env.BACKUP_KEEP_MINIMUM);\n if (keepMinimum === \"invalid\") {\n return { error: `BACKUP_KEEP_MINIMUM must be an integer, got \"${env.BACKUP_KEEP_MINIMUM}\".` };\n }\n\n return {\n config: {\n schedule,\n connectionString,\n destination,\n retentionDays: retentionDays ?? undefined,\n keepMinimum: keepMinimum ?? undefined\n }\n };\n}\n\nfunction parseOptionalInt(value: string | undefined): number | null | \"invalid\" {\n if (value === undefined || value.trim() === \"\") return null;\n const n = Number(value);\n if (!Number.isInteger(n) || n < 0) return \"invalid\";\n return n;\n}\n\n/**\n * Create a {@link CronJobDefinition} that dumps the database, uploads the\n * result to the configured destination, and prunes old backups. Object\n * destinations require {@link BackupCronConfig.storage}.\n */\nexport function createBackupCron(config: BackupCronConfig): CronJobDefinition {\n const dbName = parseDbNameFromUrl(config.connectionString) ?? \"database\";\n const excludeSchemas = config.excludeSchemas ?? [\"rebase\"];\n\n return {\n name: config.name ?? \"Scheduled database backup\",\n schedule: config.schedule,\n description: \"Dumps the Postgres database and uploads it to the configured backup destination.\",\n enabled: config.enabled ?? true,\n // Backups of a large database can take a while; allow up to an hour.\n timeoutSeconds: 3600,\n async handler({ log }) {\n const { createDump, pruneBackups, uploadBackup, validateDump } = await import(\"./backup-service\");\n const { destination } = config;\n\n if (destination.kind !== \"local\" && !config.storage) {\n throw new Error(\n `Backup destination is ${destination.kind} but no storage controller was provided. ` +\n \"Pass the backend's configured StorageController to createBackupCron({ storage }).\"\n );\n }\n\n log(`Starting backup of \"${dbName}\"…`);\n const outDir = destination.kind === \"local\" ? destination.path : undefined;\n const dump = await createDump({\n connectionString: config.connectionString,\n dbName,\n outDir,\n excludeSchemas\n });\n log(`Dump created: ${dump.fileName} (${formatBytes(dump.sizeBytes)})`);\n\n // Validate BEFORE pruning: a corrupt-but-exit-0 dump must never be\n // the reason the last good backup gets deleted.\n const check = await validateDump(dump.localFile);\n if (!check.ok) {\n // Clean up the bad temp file for object destinations.\n if (destination.kind !== \"local\" && fs.existsSync(dump.localFile)) {\n fs.unlinkSync(dump.localFile);\n }\n if (dump.globalsFile && destination.kind !== \"local\" && fs.existsSync(dump.globalsFile)) {\n fs.unlinkSync(dump.globalsFile);\n }\n throw new Error(`New backup failed validation — skipping upload and pruning to protect existing backups. ${check.reason}`);\n }\n\n let storedKey = dump.localFile;\n try {\n if (destination.kind !== \"local\") {\n const uploaded = await uploadBackup(config.storage!, dump.localFile, destination);\n storedKey = uploaded.storageUrl;\n log(`Uploaded to ${uploaded.storageUrl}`);\n // Upload the roles sidecar so a restore can recreate the\n // roles the dump's GRANT/RLS statements depend on.\n if (dump.globalsFile && fs.existsSync(dump.globalsFile)) {\n const g = await uploadBackup(config.storage!, dump.globalsFile, destination);\n log(`Uploaded roles sidecar to ${g.storageUrl}`);\n }\n }\n } finally {\n // For object-storage destinations the local dump was a temp\n // file — remove it once uploaded (or on failure).\n if (destination.kind !== \"local\" && fs.existsSync(dump.localFile)) {\n fs.unlinkSync(dump.localFile);\n }\n if (dump.globalsFile && destination.kind !== \"local\" && fs.existsSync(dump.globalsFile)) {\n fs.unlinkSync(dump.globalsFile);\n }\n }\n\n let pruned: string[] = [];\n if (config.retentionDays && config.retentionDays > 0) {\n pruned = await pruneBackups(\n destination,\n { retentionDays: config.retentionDays, keepMinimum: config.keepMinimum },\n config.storage\n );\n if (pruned.length > 0) {\n log(`Pruned ${pruned.length} backup(s) older than ${config.retentionDays} day(s).`);\n }\n }\n\n return {\n backup: storedKey,\n sizeBytes: dump.sizeBytes,\n pruned: pruned.length\n };\n }\n };\n}\n\nfunction formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;\n}\n","import { getTableColumns } from \"drizzle-orm\";\nimport { PgTable } from \"drizzle-orm/pg-core\";\nimport { CollectionConfig, ResolvedRelation } from \"@rebasepro/types\";\nimport { getTableName, resolveCollectionRelations } from \"@rebasepro/common\";\nimport { generateForeignKeyName, legacyForeignKeyName } from \"@rebasepro/utils\";\n\nimport { PostgresCollectionRegistry } from \"./PostgresCollectionRegistry\";\n\n/**\n * Check every relation against the schema it actually runs on, at boot.\n *\n * The tagged union made the *shape* of a relation impossible to get wrong: a\n * `manyToMany` cannot carry a `foreignKeyOnTarget`, a to-many cannot carry a\n * `localKey`. What it cannot know is whether any of the names are real —\n * whether `posts_tags` is a table, whether `author_id` is a column, whether a\n * `joinPath` connects the tables it claims to. Those are facts about the\n * database, and the type system never sees them.\n *\n * Until now nothing checked them until a query ran, and the failures were the\n * quiet kind. A missing junction table logged a warning and returned no rows,\n * so `posts/1/tags` answered `[]` — indistinguishable from a post with no tags.\n * The relation looked configured, the admin drew the tab, the tab was empty,\n * and nothing anywhere said why.\n *\n * The junction default is the sharp edge this exists for. `through.table`\n * defaults to the two table names sorted and joined, so renaming a table\n * silently re-points the relation at a name that was never created. It is the\n * one default whose output changes when you edit something that looks\n * unrelated.\n */\nexport interface RelationDefect {\n /** Slug of the collection declaring the relation. */\n collection: string;\n relationName: string;\n kind: ResolvedRelation[\"kind\"];\n /** What is wrong, in terms of the schema. */\n problem: string;\n /** The edit that fixes it. */\n fix: string;\n}\n\n/**\n * Every name a column answers to: the key in the drizzle schema and the real\n * column name in Postgres. A relation may legitimately be written with either,\n * and reporting a working relation as broken is worse than not checking.\n */\nfunction columnNames(table: PgTable): Set<string> {\n const names = new Set<string>();\n for (const [key, col] of Object.entries(getTableColumns(table))) {\n names.add(key);\n const dbName = (col as { name?: string })?.name;\n if (dbName) names.add(dbName);\n }\n return names;\n}\n\nconst quote = (xs: Iterable<string>) => Array.from(xs).map(s => `\\`${s}\\``).join(\", \");\n\n/** `on.from` / `on.to` accept a single column or a composite tuple. */\nconst asColumns = (value: string | string[]): string[] => Array.isArray(value) ? value : [value];\n\n/**\n * Distinguish \"this column name is wrong\" from \"the generated schema is old\".\n *\n * They present identically here — a relation asks for a column the registered\n * table does not have — but they are opposite problems with opposite fixes, and\n * getting them the wrong way round is how the 0.12 → 0.13 upgrade bricked\n * projects.\n *\n * The registered table is not the database. It comes from the project's\n * checked-in `backend/src/schema.generated.ts`, and 0.13 changed the rule that\n * derives foreign-key names: `categories` yields `category_id` where it used to\n * yield `categorie_id`. Boot-ensure renames the database column to match, so by\n * the time this runs the *database* is correct and the *generated module* is the\n * stale one. Reporting \"not a column\" then points at the wrong artifact, and the\n * generic fix — \"set `through.targetColumn` to one of: …\", listing the legacy\n * name because that is what the stale module still has — talks the reader into\n * pinning a column that no longer exists.\n *\n * So when the wanted name is what the current rule derives, and the table\n * carries what the *previous* rule would have derived from the same source, say\n * that instead.\n *\n * @param wanted the column the relation asks for\n * @param available every column the registered table has\n * @param sources names the default could have been derived from (a slug, a\n * relation name) — checking against these rather than guessing\n * backwards from `wanted` keeps the match exact\n */\nfunction staleCodegenRename(\n wanted: string,\n available: Set<string>,\n sources: string[]\n): { legacy: string; current: string } | null {\n for (const source of sources) {\n if (!source) continue;\n const current = generateForeignKeyName(source);\n const legacy = legacyForeignKeyName(source);\n // Only a name that actually moved, and only when the table still has the\n // old spelling and not the new one.\n if (current !== wanted || legacy === current) continue;\n if (available.has(legacy) && !available.has(current)) return { legacy, current };\n }\n return null;\n}\n\n/** The shared explanation, so every relation kind reports it identically. */\nfunction staleCodegenDefect(\n table: string,\n { legacy, current }: { legacy: string; current: string }\n): Pick<RelationDefect, \"problem\" | \"fix\"> {\n return {\n problem:\n `the generated Drizzle schema still declares \\`${legacy}\\` on \\`${table}\\`, but this ` +\n `release derives \\`${current}\\` — the generated schema predates the foreign-key ` +\n \"naming fix and no longer describes the database\",\n fix:\n \"regenerate it with `rebase schema generate` (or `pnpm run schema:generate`). The \" +\n \"database column has already been renamed for you at boot, so nothing else is needed. \" +\n `To keep \\`${legacy}\\` instead, name it explicitly on the relation and regenerate.`\n };\n}\n\n/**\n * Relations whose names do not resolve against the registered schema.\n *\n * Fails open wherever it cannot see enough to be sure — an unregistered source\n * table, a target belonging to another backend — because a false alarm here\n * costs more than a missed one: it would block boot on a working app.\n */\nexport function findRelationDefects(\n collections: CollectionConfig[],\n registry: PostgresCollectionRegistry\n): RelationDefect[] {\n const defects: RelationDefect[] = [];\n const registeredSlugs = new Set(registry.getCollections().map(c => c.slug));\n\n for (const collection of collections) {\n const sourceTableName = getTableName(collection);\n const sourceTable = registry.getTable(sourceTableName);\n // Nothing to check against. Another boot warning already covers this.\n if (!sourceTable) continue;\n\n const sourceColumns = columnNames(sourceTable);\n const relations = resolveCollectionRelations(collection);\n\n for (const relation of Object.values(relations)) {\n const at = { collection: collection.slug,\nrelationName: relation.relationName,\nkind: relation.kind };\n\n let targetCollection: CollectionConfig;\n try {\n targetCollection = relation.target();\n } catch (e) {\n defects.push({\n ...at,\n problem: `its \\`target()\\` threw: ${e instanceof Error ? e.message : String(e)}`,\n fix: \"a target thunk usually throws because of a circular import — make sure it is `() => otherCollection` and not evaluated at module load\"\n });\n continue;\n }\n\n // A target this registry has never heard of belongs to another\n // backend; its tables are not ours to check.\n if (!registeredSlugs.has(targetCollection.slug)) continue;\n\n const targetTableName = getTableName(targetCollection);\n const targetTable = registry.getTable(targetTableName);\n if (!targetTable) {\n defects.push({\n ...at,\n problem: `it points at collection \\`${targetCollection.slug}\\`, which has no table \\`${targetTableName}\\` in the schema`,\n fix: `create the \\`${targetTableName}\\` table, or correct \\`table\\` on the \\`${targetCollection.slug}\\` collection`\n });\n continue;\n }\n const targetColumns = columnNames(targetTable);\n\n switch (relation.kind) {\n case \"belongsTo\": {\n if (!sourceColumns.has(relation.localKey)) {\n // `localKey` defaults to the relation name run through\n // the foreign-key rule, so it moves with that rule.\n const stale = staleCodegenRename(\n relation.localKey,\n sourceColumns,\n [relation.relationName, targetCollection.slug]\n );\n defects.push(stale\n ? { ...at, ...staleCodegenDefect(sourceTableName, stale) }\n : {\n ...at,\n problem: `\\`localKey: \"${relation.localKey}\"\\` is not a column on \\`${sourceTableName}\\``,\n fix: `add the column, or set \\`localKey\\` to one of: ${quote(sourceColumns)}`\n });\n }\n break;\n }\n\n case \"hasOne\":\n case \"hasMany\": {\n if (!targetColumns.has(relation.foreignKeyOnTarget)) {\n // The default is derived from *this* collection's slug —\n // the column on the target that points back here.\n const stale = staleCodegenRename(\n relation.foreignKeyOnTarget,\n targetColumns,\n [collection.slug]\n );\n defects.push(stale\n ? { ...at, ...staleCodegenDefect(targetTableName, stale) }\n : {\n ...at,\n problem: `\\`foreignKeyOnTarget: \"${relation.foreignKeyOnTarget}\"\\` is not a column on the target table \\`${targetTableName}\\``,\n fix: `add the column, or set \\`foreignKeyOnTarget\\` to one of: ${quote(targetColumns)}`\n });\n }\n // `sourceKey` is the easiest of the two to put on the wrong\n // side — it is the only column in a `hasMany` that lives\n // here rather than on the target, and naming a target column\n // reads perfectly well right next to `foreignKeyOnTarget`.\n if (relation.sourceKey && !sourceColumns.has(relation.sourceKey)) {\n defects.push({\n ...at,\n problem: `\\`sourceKey: \"${relation.sourceKey}\"\\` is not a column on \\`${sourceTableName}\\``,\n fix: targetColumns.has(relation.sourceKey)\n ? `it is a column on the *target* table \\`${targetTableName}\\` — \\`sourceKey\\` names ` +\n \"the column on this collection that the target's foreign key points at, so it \" +\n `must be one of: ${quote(sourceColumns)}`\n : `add the column, or set \\`sourceKey\\` to one of: ${quote(sourceColumns)}`\n });\n }\n break;\n }\n\n case \"manyToMany\": {\n const { table, sourceColumn, targetColumn } = relation.through;\n const junction = registry.getTable(table);\n if (!junction) {\n defects.push({\n ...at,\n problem: `its junction table \\`${table}\\` does not exist`,\n fix: `create \\`${table}\\`, or name the real one with \\`through: { table: \"...\" }\\`. ` +\n \"Note that an omitted `through.table` is derived from the two table names sorted \" +\n \"and joined, so renaming a table changes it\"\n });\n break;\n }\n const junctionColumns = columnNames(junction);\n // Junction columns are the ones that actually moved in 0.13:\n // each defaults to its endpoint collection's *slug* run\n // through the foreign-key rule, and slugs are plural.\n const derivedFrom = {\n sourceColumn: [collection.slug],\n targetColumn: [targetCollection.slug]\n } as const;\n for (const [label, column] of [[\"sourceColumn\", sourceColumn], [\"targetColumn\", targetColumn]] as const) {\n if (!junctionColumns.has(column)) {\n const stale = staleCodegenRename(column, junctionColumns, [...derivedFrom[label]]);\n defects.push(stale\n ? { ...at, ...staleCodegenDefect(table, stale) }\n : {\n ...at,\n problem: `\\`through.${label}: \"${column}\"\\` is not a column on the junction table \\`${table}\\``,\n fix: `set \\`through.${label}\\` to one of: ${quote(junctionColumns)}` +\n (label === \"sourceColumn\" ? \" — it is the column naming *this* collection\" : \"\")\n });\n }\n }\n break;\n }\n\n case \"via\": {\n if (relation.joinPath.length === 0) {\n defects.push({\n ...at,\n problem: \"its `joinPath` is empty, so it joins nothing\",\n fix: \"add at least one step, ending at the target's table\"\n });\n break;\n }\n\n // Walk the chain: each step's `from` names columns on the\n // previous table, its `to` names columns on its own.\n let prevName = sourceTableName;\n let prevColumns = sourceColumns;\n let broken = false;\n\n for (const [i, step] of relation.joinPath.entries()) {\n const stepTable = registry.getTable(step.table);\n if (!stepTable) {\n defects.push({\n ...at,\n problem: `step ${i + 1} of its \\`joinPath\\` joins \\`${step.table}\\`, which is not a table in the schema`,\n fix: `correct \\`joinPath[${i}].table\\``\n });\n broken = true;\n break;\n }\n const stepColumns = columnNames(stepTable);\n\n for (const column of asColumns(step.on.from)) {\n if (!prevColumns.has(column)) {\n defects.push({\n ...at,\n problem: `step ${i + 1} joins \\`${prevName}.${column}\\` → \\`${step.table}\\`, but \\`${column}\\` is not a column on \\`${prevName}\\``,\n fix: `\\`joinPath[${i}].on.from\\` names columns on ${i === 0 ? \"this collection's table\" : `the previous step's table (\\`${prevName}\\`)`}: ${quote(prevColumns)}`\n });\n }\n }\n for (const column of asColumns(step.on.to)) {\n if (!stepColumns.has(column)) {\n defects.push({\n ...at,\n problem: `step ${i + 1} joins into \\`${step.table}.${column}\\`, but \\`${column}\\` is not a column on \\`${step.table}\\``,\n fix: `\\`joinPath[${i}].on.to\\` names columns on \\`${step.table}\\`: ${quote(stepColumns)}`\n });\n }\n }\n\n if (asColumns(step.on.from).length !== asColumns(step.on.to).length) {\n defects.push({\n ...at,\n problem: `step ${i + 1} compares ${asColumns(step.on.from).length} column(s) against ${asColumns(step.on.to).length}`,\n fix: `\\`from\\` and \\`to\\` must name the same number of columns in \\`joinPath[${i}]\\``\n });\n }\n\n prevName = step.table;\n prevColumns = stepColumns;\n }\n\n // The chain has to end where the relation says it points,\n // or the rows it returns are not the target's rows.\n if (!broken && prevName !== targetTableName) {\n defects.push({\n ...at,\n problem: `its \\`joinPath\\` ends at \\`${prevName}\\`, but it targets \\`${targetCollection.slug}\\` (table \\`${targetTableName}\\`)`,\n fix: `make the last step join \\`${targetTableName}\\`, or point \\`target\\` at the collection backed by \\`${prevName}\\``\n });\n }\n break;\n }\n\n default: {\n const exhaustive: never = relation;\n throw new Error(`Unhandled relation kind: ${JSON.stringify(exhaustive)}`);\n }\n }\n }\n }\n\n return defects;\n}\n\n/**\n * Fail boot on any relation that cannot resolve, listing all of them at once.\n *\n * Deliberately fatal rather than a warning. Every one of these produces an\n * empty result at query time and nothing else — an empty tab, an empty\n * `include`, a subcollection that looks like it has no rows. A server that\n * refuses to start is recoverable in a minute; a relation that quietly answers\n * \"nothing\" is the kind of bug found in production, weeks later, by a user\n * asking where their data went.\n */\nexport function assertRelationsResolve(\n collections: CollectionConfig[],\n registry: PostgresCollectionRegistry\n): void {\n const defects = findRelationDefects(collections, registry);\n if (defects.length === 0) return;\n\n const lines = defects.map(d =>\n ` • ${d.collection}.${d.relationName} (${d.kind})\\n` +\n ` ${d.problem}\\n` +\n ` fix: ${d.fix}`\n );\n\n throw new Error(\n `${defects.length} relation${defects.length === 1 ? \"\" : \"s\"} cannot resolve against ` +\n \"`backend/src/schema.generated.ts`.\\n\\n\" +\n \"Each of these would return no rows at query time rather than reporting an error, \" +\n \"so they are fatal at boot instead.\\n\\n\" +\n // This reads the *generated file*, not the database, and the difference\n // is the whole diagnosis after an upgrade. Boot-ensure renames columns\n // in the database — a 0.12 → 0.13 upgrade singularises a junction key,\n // `categorie_id` → `category_id` — and the checked-in file still\n // declares the old name. The config is then correct and the file is\n // stale, so the per-defect advice below, which lists the columns this\n // file has, names a column that no longer exists in the database.\n // Following it turns a recoverable state into a broken config.\n //\n // Hence the ordering: regenerate first, and only then consider that the\n // collection might be the thing that is wrong.\n \"If the database was migrated recently — an upgrade, a `db push`, a restore — this file is\\n\" +\n \"probably older than the schema it describes. Regenerate it before changing anything else:\\n\\n\" +\n \" rebase schema generate\\n\\n\" +\n \"If it is already current, then the collection is what disagrees with it:\\n\\n\" +\n lines.join(\"\\n\\n\") + \"\\n\"\n );\n}\n","import { isTable, getTableName, Relations } from \"drizzle-orm\";\nimport { PgEnum, PgTable } from \"drizzle-orm/pg-core\";\nimport { CollectionConfig } from \"@rebasepro/types\";\nimport { logger } from \"@rebasepro/server\";\nimport { PostgresCollectionRegistry } from \"./PostgresCollectionRegistry\";\nimport { warnOnKeysTheAdminCannotResolve } from \"../services/collection-helpers\";\nimport { assertRelationsResolve } from \"./validate-relations\";\n\n/**\n * Everything a registry is built from: the collections, and the drizzle schema\n * they are backed by. In BaaS mode all of it is introspected from the live\n * database; when collections are declared it comes from the config and the generated schema.\n */\nexport interface RegistrySchema {\n collections?: CollectionConfig[];\n tables?: Record<string, unknown>;\n enums?: Record<string, PgEnum<[string, ...string[]]>>;\n relations?: Record<string, Relations>;\n}\n\n/**\n * Build the collection registry for a driver.\n *\n * The order matters and is the reason this is one function rather than a run of\n * statements in the bootstrapper. Keys are resolved from the drizzle schema, so\n * anything that inspects them has to run *after* the tables are registered —\n * and `warnOnKeysTheAdminCannotResolve` fails open if it does not, because a\n * collection whose table it cannot look up is one it has nothing to say about.\n * Warned too early, it would skip every collection and report nothing, which\n * reads exactly like having nothing to report.\n */\nexport function buildCollectionRegistry(schema: RegistrySchema): PostgresCollectionRegistry {\n const registry = new PostgresCollectionRegistry();\n\n if (schema.collections) {\n registry.registerMultiple(schema.collections);\n // `Auto-discovered collections` already reports the count and the\n // directory they came from; this is the same fact with the names.\n logger.debug(\n `📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: ` +\n `[${registry.getCollections().map(c => c.slug).join(\", \")}]`\n );\n }\n\n if (schema.tables) {\n Object.values(schema.tables).forEach((table) => {\n if (isTable(table)) {\n registry.registerTable(table as PgTable, getTableName(table));\n }\n });\n }\n\n if (schema.enums) registry.registerEnums(schema.enums);\n if (schema.relations) registry.registerRelations(schema.relations);\n\n // Now that the keys resolve: say which of them the admin cannot see. It\n // compiles the same collection files into its bundle but never the drizzle\n // schema, and nothing serves it one, so only an edit to the config fixes it.\n warnOnKeysTheAdminCannotResolve(registry.getCollections(), registry);\n\n // And now that the tables resolve: refuse to start on a relation whose\n // names do not exist. The union checks a relation's shape at compile time;\n // only here is there a schema to check its *names* against. Every one of\n // these used to surface as an empty result at query time and nothing else.\n assertRelationsResolve(registry.getCollections(), registry);\n\n return registry;\n}\n","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","import { eq, getTableName, sql } from \"drizzle-orm\";\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { getTableConfig } from \"drizzle-orm/pg-core\";\nimport type { RebasePgTable } from \"../types\";\nimport { users, refreshTokens, passwordResetTokens, userIdentities, magicLinkTokens } from \"../schema/auth-schema\";\nimport {\n UserRepository,\n RoleRepository,\n TokenRepository,\n MfaRepository,\n AuthRepository,\n UserData,\n CreateUserData,\n RoleData,\n CreateRoleData,\n RefreshTokenInfo,\n RefreshTokenSession,\n PasswordResetTokenInfo,\n MagicLinkTokenInfo,\n UserIdentityData,\n ListUsersOptions,\n PaginatedUsersResult,\n MfaFactor,\n MfaChallengeInfo,\n RoleData as Role,\n ApiError\n} from \"@rebasepro/server\";\nimport { toSnakeCase, camelCase } from \"@rebasepro/utils\";\nimport { escapeLikePattern } from \"../utils/drizzle-conditions\";\nimport { extractPgError } from \"../utils/pg-error-utils\";\n\nexport type { Role };\n\nexport interface AuthSchemaTables {\n users: RebasePgTable;\n refreshTokens: RebasePgTable;\n passwordResetTokens: RebasePgTable;\n appConfig: RebasePgTable;\n userIdentities: RebasePgTable;\n}\n\nfunction getColumnKey(table: RebasePgTable | undefined, ...keys: string[]): string | undefined {\n if (!table) return undefined;\n for (const key of keys) {\n if (key in table) return key;\n const snake = toSnakeCase(key);\n if (snake in table) return snake;\n const camel = camelCase(key);\n if (camel in table) return camel;\n }\n return undefined;\n}\n\nfunction getColumn(table: RebasePgTable | undefined, ...keys: string[]): RebasePgTable[string] | undefined {\n if (!table) return undefined;\n const key = getColumnKey(table, ...keys);\n return key ? table[key] : undefined;\n}\n\n/**\n * The single definition of what an email address looks like in storage.\n *\n * Reads have always folded case; writes did not, and normalising was left to\n * each caller. That asymmetry is only ever one forgotten `.toLowerCase()` away\n * from a row no lookup can find — the account exists, every sign-in path\n * reports no such user, and the byte-exact UNIQUE on the column does not stop a\n * duplicate differing only in case. Applied on both sides here so the guarantee\n * belongs to the repository rather than to its callers' discipline; the\n * `lower(email)` unique index added in `ensureAuthTablesExist` is the database\n * half of the same rule.\n *\n * Whitespace goes too: a trailing space survives the fold and reproduces the\n * problem exactly.\n *\n * Re-exported rather than defined here: `@rebasepro/server` and\n * `@rebasepro/server-mongo` write this column too, and a second copy of this\n * rule is the defect it exists to prevent.\n */\nimport { normalizeEmail } from \"@rebasepro/common\";\nexport { normalizeEmail };\n\n/**\n * PostgreSQL implementation of UserRepository.\n * Handles all user-related database operations using Drizzle ORM.\n */\nexport class UserService implements UserRepository {\n private usersTable: RebasePgTable;\n private userIdentitiesTable: RebasePgTable;\n\n constructor(\n private db: NodePgDatabase,\n tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>\n ) {\n if (tableOrTables && ((tableOrTables as Partial<AuthSchemaTables>).users)) {\n const tables = tableOrTables as Partial<AuthSchemaTables>;\n this.usersTable = (tables.users || users) as RebasePgTable;\n this.userIdentitiesTable = (tables.userIdentities || userIdentities) as RebasePgTable;\n } else {\n const table = tableOrTables as RebasePgTable | undefined;\n this.usersTable = table || (users as unknown as RebasePgTable);\n this.userIdentitiesTable = userIdentities as unknown as RebasePgTable;\n }\n }\n\n private getQualifiedUsersTableName(): string {\n const name = getTableName(this.usersTable);\n const schema = getTableConfig(this.usersTable).schema || \"public\";\n return `\"${schema}\".\"${name}\"`;\n }\n\n /**\n * Run a privileged auth write with an explicitly cleared RLS context.\n *\n * The auth services run on the base/owner connection, which by design\n * carries a NULL `app.uid` so the `rebase.uid() IS NULL` server-escape\n * in the default policies applies. That NULL is normally guaranteed by\n * `set_config(..., is_local = true)` resetting at transaction end — but a\n * GUC that survives on a pooled connection (or a connection role that\n * doesn't bypass RLS: FORCE ROW LEVEL SECURITY, or a non-owner role)\n * turns the trusted write into an RLS-scoped one and denies it with\n * SQLSTATE 42501. Clearing the GUCs here, transaction-locally at the\n * single chokepoint, makes the server context deterministic instead of\n * trusting whatever state the pool hands us. `rebase.uid()` reads '' as\n * NULL via NULLIF, so '' is the server context.\n */\n private async withServerContext<T>(fn: (db: NodePgDatabase) => Promise<T>): Promise<T> {\n return await this.db.transaction(async (tx) => {\n await tx.execute(sql`\n SELECT set_config('app.uid', '', true),\n set_config('app.user_id', '', true),\n set_config('app.user_roles', '', true),\n set_config('app.jwt', '', true)\n `);\n return await fn(tx as unknown as NodePgDatabase);\n });\n }\n\n private mapRowToUser(row: Record<string, unknown>): UserData {\n if (!row) return row as UserData;\n\n const id = (row.id ?? row.uid) as string;\n const email = row.email as string;\n const passwordHash = (row.password_hash ?? row.passwordHash ?? null) as string | null | undefined;\n const displayName = (row.display_name ?? row.displayName ?? null) as string | null | undefined;\n const photoUrl = (row.photo_url ?? row.photoUrl ?? row.photoURL ?? null) as string | null | undefined;\n const emailVerified = (row.email_verified ?? row.emailVerified ?? false) as boolean;\n const emailVerificationToken = (row.email_verification_token ?? row.emailVerificationToken ?? null) as string | null | undefined;\n const emailVerificationSentAt = (row.email_verification_sent_at ?? row.emailVerificationSentAt ?? null) as string | number | Date | null;\n const isAnonymous = (row.is_anonymous ?? row.isAnonymous ?? false) as boolean;\n const createdAt = (row.created_at ?? row.createdAt) as string | number | Date | undefined;\n const updatedAt = (row.updated_at ?? row.updatedAt) as string | number | Date | undefined;\n\n const metadata: Record<string, any> = { ...((row.metadata as Record<string, any> | undefined) || {}) };\n\n const knownKeys = new Set([\n \"id\", \"uid\", \"email\",\n \"password_hash\", \"passwordHash\",\n \"display_name\", \"displayName\",\n \"photo_url\", \"photoUrl\", \"photoURL\",\n \"email_verified\", \"emailVerified\",\n \"email_verification_token\", \"emailVerificationToken\",\n \"email_verification_sent_at\", \"emailVerificationSentAt\",\n \"is_anonymous\", \"isAnonymous\",\n \"roles\",\n \"created_at\", \"createdAt\",\n \"updated_at\", \"updatedAt\",\n \"metadata\"\n ]);\n\n for (const [key, val] of Object.entries(row)) {\n if (!knownKeys.has(key)) {\n const camelKey = camelCase(key);\n metadata[camelKey] = val;\n }\n }\n\n return {\n id,\n email,\n passwordHash,\n displayName,\n photoUrl,\n emailVerified,\n emailVerificationToken,\n emailVerificationSentAt: emailVerificationSentAt ? new Date(emailVerificationSentAt) : null,\n isAnonymous,\n createdAt: createdAt ? new Date(createdAt) : new Date(),\n updatedAt: updatedAt ? new Date(updatedAt) : new Date(),\n metadata\n };\n }\n\n private mapPayload(data: Partial<CreateUserData>): Record<string, unknown> {\n if (!data) return {};\n\n const payload: Record<string, unknown> = {};\n\n const idKey = getColumnKey(this.usersTable, \"id\") || \"id\";\n const emailKey = getColumnKey(this.usersTable, \"email\") || \"email\";\n const passwordHashKey = getColumnKey(this.usersTable, \"passwordHash\", \"password_hash\") || \"passwordHash\";\n const displayNameKey = getColumnKey(this.usersTable, \"displayName\", \"display_name\") || \"displayName\";\n const photoUrlKey = getColumnKey(this.usersTable, \"photoUrl\", \"photo_url\") || \"photoUrl\";\n const emailVerifiedKey = getColumnKey(this.usersTable, \"emailVerified\", \"email_verified\") || \"emailVerified\";\n const emailVerificationTokenKey = getColumnKey(this.usersTable, \"emailVerificationToken\", \"email_verification_token\") || \"emailVerificationToken\";\n const emailVerificationSentAtKey = getColumnKey(this.usersTable, \"emailVerificationSentAt\", \"email_verification_sent_at\") || \"emailVerificationSentAt\";\n const isAnonymousKey = getColumnKey(this.usersTable, \"isAnonymous\", \"is_anonymous\") || \"isAnonymous\";\n const createdAtKey = getColumnKey(this.usersTable, \"createdAt\", \"created_at\") || \"createdAt\";\n const updatedAtKey = getColumnKey(this.usersTable, \"updatedAt\", \"updated_at\") || \"updatedAt\";\n const metadataKey = getColumnKey(this.usersTable, \"metadata\") || \"metadata\";\n\n if (\"id\" in data) payload[idKey] = data.id;\n if (\"email\" in data) payload[emailKey] = normalizeEmail(data.email);\n if (\"passwordHash\" in data) payload[passwordHashKey] = data.passwordHash;\n if (\"displayName\" in data) payload[displayNameKey] = data.displayName;\n if (\"photoUrl\" in data) payload[photoUrlKey] = data.photoUrl;\n if (\"emailVerified\" in data) payload[emailVerifiedKey] = data.emailVerified;\n if (\"emailVerificationToken\" in data) payload[emailVerificationTokenKey] = data.emailVerificationToken;\n if (\"emailVerificationSentAt\" in data) payload[emailVerificationSentAtKey] = data.emailVerificationSentAt;\n if (\"isAnonymous\" in data) payload[isAnonymousKey] = data.isAnonymous;\n if (\"createdAt\" in data) payload[createdAtKey] = data.createdAt;\n if (\"updatedAt\" in data) payload[updatedAtKey] = data.updatedAt;\n\n const metadata: Record<string, any> = { ...(data.metadata || {}) };\n const remainingMetadata: Record<string, any> = {};\n\n for (const [key, val] of Object.entries(metadata)) {\n const tableColKey = getColumnKey(this.usersTable, key);\n if (tableColKey &&\n tableColKey !== idKey &&\n tableColKey !== emailKey &&\n tableColKey !== passwordHashKey &&\n tableColKey !== displayNameKey &&\n tableColKey !== photoUrlKey &&\n tableColKey !== emailVerifiedKey &&\n tableColKey !== emailVerificationTokenKey &&\n tableColKey !== emailVerificationSentAtKey &&\n tableColKey !== isAnonymousKey &&\n tableColKey !== createdAtKey &&\n tableColKey !== updatedAtKey &&\n tableColKey !== metadataKey) {\n payload[tableColKey] = val;\n } else {\n remainingMetadata[key] = val;\n }\n }\n\n if (metadataKey in this.usersTable) {\n payload[metadataKey] = remainingMetadata;\n }\n\n return payload;\n }\n\n /**\n * @see UserRepository.createUser — an email already in use is a 409.\n *\n * The route checks first and answers 409; this is the same answer for the\n * requests that get past the check, which two clicks on a signup button\n * are enough to produce. `PersistService` has mapped `23505` to a conflict\n * for collection writes since the layer that holds the SQLSTATE was made\n * responsible for saying whose fault a failure is; the auth writes never\n * got the same treatment and reached the client as \"Internal Server Error\".\n */\n async createUser(data: CreateUserData): Promise<UserData> {\n const payload = this.mapPayload(data);\n try {\n const [row] = await this.withServerContext(async (db) =>\n (await db.insert(this.usersTable).values(payload).returning()) as Record<string, unknown>[]\n );\n return this.mapRowToUser(row);\n } catch (error) {\n // Drizzle wraps the pg error, so the SQLSTATE is down the `cause`\n // chain rather than on the error itself.\n if (extractPgError(error)?.code === \"23505\") {\n throw ApiError.conflict(\"Email already registered\", \"EMAIL_EXISTS\");\n }\n throw error;\n }\n }\n\n async getUserById(id: string): Promise<UserData | null> {\n const idCol = getColumn(this.usersTable, \"id\");\n if (!idCol) return null;\n const [row] = await this.db.select().from(this.usersTable).where(eq(idCol, id));\n return row ? this.mapRowToUser(row as Record<string, unknown>) : null;\n }\n\n async getUserByEmail(email: string): Promise<UserData | null> {\n const emailCol = getColumn(this.usersTable, \"email\");\n if (!emailCol) return null;\n const [row] = await this.db.select().from(this.usersTable).where(eq(emailCol, normalizeEmail(email)));\n return row ? this.mapRowToUser(row as Record<string, unknown>) : null;\n }\n\n async getUserByIdentity(provider: string, providerId: string): Promise<UserData | null> {\n const userIdCol = getColumn(this.usersTable, \"id\");\n if (!userIdCol) return null;\n\n const result = await this.db\n .select({ user: this.usersTable })\n .from(this.usersTable)\n .innerJoin(this.userIdentitiesTable, eq(userIdCol, this.userIdentitiesTable.uid))\n .where(\n sql`${this.userIdentitiesTable.provider} = ${provider} AND ${this.userIdentitiesTable.providerId} = ${providerId}`\n )\n .limit(1);\n\n if (result.length === 0) return null;\n return this.mapRowToUser(result[0].user as Record<string, unknown>);\n }\n\n async getUserIdentities(uid: string): Promise<UserIdentityData[]> {\n const schema = getTableConfig(this.userIdentitiesTable).schema || \"public\";\n const result = await this.db.execute(sql`\n SELECT id, uid, provider, provider_id, profile_data, created_at, updated_at\n FROM ${sql.raw(`\"${schema}\".\"user_identities\"`)}\n WHERE uid = ${uid}\n `);\n\n return result.rows.map((row: Record<string, unknown>) => ({\n id: row.id as string,\n uid: row.uid as string,\n provider: row.provider as string,\n providerId: row.provider_id as string,\n profileData: (row.profile_data as Record<string, unknown> | null) ?? null,\n createdAt: row.created_at as Date,\n updatedAt: row.updated_at as Date\n }));\n }\n\n async linkUserIdentity(uid: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void> {\n await this.withServerContext(async (db) => db.insert(this.userIdentitiesTable).values({\n uid,\n provider,\n providerId,\n profileData: profileData || null\n }).onConflictDoNothing({ target: [this.userIdentitiesTable.provider, this.userIdentitiesTable.providerId] }));\n }\n\n async updateUser(id: string, data: Partial<Omit<CreateUserData, \"id\">>): Promise<UserData | null> {\n const idCol = getColumn(this.usersTable, \"id\");\n if (!idCol) return null;\n const payload = this.mapPayload(data);\n const updatedAtKey = getColumnKey(this.usersTable, \"updatedAt\", \"updated_at\") || \"updatedAt\";\n payload[updatedAtKey] = new Date();\n\n const [row] = await this.withServerContext(async (db) =>\n (await db\n .update(this.usersTable)\n .set(payload)\n .where(eq(idCol, id))\n .returning()) as Record<string, unknown>[]\n );\n return row ? this.mapRowToUser(row) : null;\n }\n\n async deleteUser(id: string): Promise<void> {\n const idCol = getColumn(this.usersTable, \"id\");\n if (!idCol) return;\n await this.withServerContext(async (db) => db.delete(this.usersTable).where(eq(idCol, id)));\n }\n\n async listUsers(): Promise<UserData[]> {\n const rows = await this.db.select().from(this.usersTable);\n return (rows as Record<string, unknown>[]).map(row => this.mapRowToUser(row));\n }\n\n async listUsersPaginated(options?: ListUsersOptions): Promise<PaginatedUsersResult> {\n const limit = options?.limit ?? 25;\n const offset = options?.offset ?? 0;\n const search = options?.search?.trim() || \"\";\n const orderBy = options?.orderBy || \"createdAt\";\n const orderDir = options?.orderDir || \"desc\";\n const roleId = options?.roleId;\n\n const orderCol = getColumn(this.usersTable, orderBy);\n const orderColumn = orderCol ? orderCol.name : \"created_at\";\n const direction = orderDir === \"asc\" ? sql`ASC` : sql`DESC`;\n\n const emailCol = getColumn(this.usersTable, \"email\");\n const emailColumn = emailCol ? emailCol.name : \"email\";\n const displayNameCol = getColumn(this.usersTable, \"displayName\", \"display_name\");\n const displayNameColumn = displayNameCol ? displayNameCol.name : \"display_name\";\n const idCol = getColumn(this.usersTable, \"id\");\n const idColumn = idCol ? idCol.name : \"id\";\n\n const usersTableName = this.getQualifiedUsersTableName();\n const conditions = [];\n if (roleId) {\n conditions.push(sql`${roleId} = ANY(${sql.raw(usersTableName)}.roles)`);\n }\n if (search) {\n // `search` is a substring search over the admin user list, not a\n // pattern the caller writes: the same reasoning as the collection\n // search path, so it shares that path's helper rather than growing\n // a second copy that can drift. See `escapeLikePattern`.\n const pattern = `%${escapeLikePattern(search)}%`;\n conditions.push(sql`(${sql.raw(usersTableName)}.${sql.raw(emailColumn)} ILIKE ${pattern} OR ${sql.raw(usersTableName)}.${sql.raw(displayNameColumn)} ILIKE ${pattern})`);\n }\n\n const whereClause = conditions.length > 0 ? sql`WHERE ${sql.join(conditions, sql` AND `)}` : sql``;\n\n // Sorting: users with roles first if no role filter, then by requested order\n const orderByClause = roleId\n ? sql`ORDER BY ${sql.raw(usersTableName)}.${sql.raw(orderColumn)} ${direction}`\n : sql`ORDER BY array_length(${sql.raw(usersTableName)}.roles, 1) DESC NULLS LAST, ${sql.raw(usersTableName)}.${sql.raw(orderColumn)} ${direction}`;\n\n const countResult = await this.db.execute(sql`\n SELECT count(*)::int as total FROM ${sql.raw(usersTableName)}\n ${whereClause}\n `);\n const total = (countResult.rows[0] as { total: number }).total;\n\n const dataResult = await this.db.execute(sql`\n SELECT * FROM ${sql.raw(usersTableName)}\n ${whereClause}\n ${orderByClause}\n LIMIT ${limit} OFFSET ${offset}\n `);\n const rows = dataResult.rows;\n\n // Map rows to camelCase UserData\n const mappedUsers: UserData[] = (rows as Record<string, unknown>[]).map((row) => this.mapRowToUser(row));\n\n return { users: mappedUsers,\n total,\n limit,\n offset };\n }\n\n /**\n * Update user's password hash\n */\n async updatePassword(id: string, passwordHash: string): Promise<void> {\n const idCol = getColumn(this.usersTable, \"id\");\n if (!idCol) return;\n const passwordHashColKey = getColumnKey(this.usersTable, \"passwordHash\", \"password_hash\") || \"passwordHash\";\n const updatedAtColKey = getColumnKey(this.usersTable, \"updatedAt\", \"updated_at\") || \"updatedAt\";\n\n await this.withServerContext(async (db) => db\n .update(this.usersTable)\n .set({\n [passwordHashColKey]: passwordHash,\n [updatedAtColKey]: new Date()\n })\n .where(eq(idCol, id)));\n }\n\n /**\n * Set email verification status\n */\n async setEmailVerified(id: string, verified: boolean): Promise<void> {\n const idCol = getColumn(this.usersTable, \"id\");\n if (!idCol) return;\n const emailVerifiedColKey = getColumnKey(this.usersTable, \"emailVerified\", \"email_verified\") || \"emailVerified\";\n const emailVerificationTokenColKey = getColumnKey(this.usersTable, \"emailVerificationToken\", \"email_verification_token\") || \"emailVerificationToken\";\n const updatedAtColKey = getColumnKey(this.usersTable, \"updatedAt\", \"updated_at\") || \"updatedAt\";\n\n await this.withServerContext(async (db) => db\n .update(this.usersTable)\n .set({\n [emailVerifiedColKey]: verified,\n [emailVerificationTokenColKey]: null,\n [updatedAtColKey]: new Date()\n })\n .where(eq(idCol, id)));\n }\n\n /**\n * Set email verification token\n */\n async setVerificationToken(id: string, token: string | null): Promise<void> {\n const idCol = getColumn(this.usersTable, \"id\");\n if (!idCol) return;\n const emailVerificationTokenColKey = getColumnKey(this.usersTable, \"emailVerificationToken\", \"email_verification_token\") || \"emailVerificationToken\";\n const emailVerificationSentAtColKey = getColumnKey(this.usersTable, \"emailVerificationSentAt\", \"email_verification_sent_at\") || \"emailVerificationSentAt\";\n const updatedAtColKey = getColumnKey(this.usersTable, \"updatedAt\", \"updated_at\") || \"updatedAt\";\n\n await this.withServerContext(async (db) => db\n .update(this.usersTable)\n .set({\n [emailVerificationTokenColKey]: token,\n [emailVerificationSentAtColKey]: token ? new Date() : null,\n [updatedAtColKey]: new Date()\n })\n .where(eq(idCol, id)));\n }\n\n /**\n * Find user by email verification token\n */\n async getUserByVerificationToken(token: string): Promise<UserData | null> {\n const tokenCol = getColumn(this.usersTable, \"emailVerificationToken\", \"email_verification_token\");\n if (!tokenCol) return null;\n const [row] = await this.db\n .select()\n .from(this.usersTable)\n .where(eq(tokenCol, token));\n return row ? this.mapRowToUser(row as Record<string, unknown>) : null;\n }\n\n /**\n * Get roles for a user from database (inline TEXT[] column)\n */\n async getUserRoles(uid: string): Promise<Role[]> {\n const usersTableName = this.getQualifiedUsersTableName();\n const result = await this.db.execute(sql`\n SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${uid}\n `);\n\n if (result.rows.length === 0) return [];\n\n const row = result.rows[0] as { roles: string[] | null };\n const roleIds = row.roles ?? [];\n\n return roleIds.map(id => ({\n id,\n name: id,\n isAdmin: id === \"admin\",\n defaultPermissions: null,\n collectionPermissions: null\n }));\n }\n\n /**\n * Get role IDs for a user\n */\n async getUserRoleIds(uid: string): Promise<string[]> {\n const usersTableName = this.getQualifiedUsersTableName();\n const result = await this.db.execute(sql`\n SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${uid}\n `);\n\n if (result.rows.length === 0) return [];\n\n const row = result.rows[0] as { roles: string[] | null };\n return row.roles ?? [];\n }\n\n /**\n * Set roles for a user (replaces existing roles)\n */\n async setUserRoles(uid: string, roleIds: string[]): Promise<void> {\n const usersTableName = this.getQualifiedUsersTableName();\n const rolesArray = `{${roleIds.join(\",\")}}`;\n await this.withServerContext(async (db) => db.execute(sql`\n UPDATE ${sql.raw(usersTableName)}\n SET roles = ${rolesArray}::text[], updated_at = NOW()\n WHERE id = ${uid}\n `));\n }\n\n /**\n * Assign a specific role to new user (appends if not present)\n */\n async assignDefaultRole(uid: string, roleId: string): Promise<void> {\n const usersTableName = this.getQualifiedUsersTableName();\n await this.withServerContext(async (db) => db.execute(sql`\n UPDATE ${sql.raw(usersTableName)}\n SET roles = array_append(roles, ${roleId}), updated_at = NOW()\n WHERE id = ${uid} AND NOT (${roleId} = ANY(roles))\n `));\n }\n\n /**\n * Get user with their roles\n */\n async getUserWithRoles(uid: string): Promise<{ user: UserData; roles: Role[] } | null> {\n const user = await this.getUserById(uid);\n if (!user) return null;\n\n const roles = await this.getUserRoles(uid);\n return { user,\n roles };\n }\n}\n\n\nexport class RefreshTokenService {\n private refreshTokensTable: RebasePgTable;\n private usersTable: RebasePgTable | null;\n\n constructor(\n private db: NodePgDatabase,\n tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>\n ) {\n if (tableOrTables && ((tableOrTables as Partial<AuthSchemaTables>).refreshTokens || (tableOrTables as Partial<AuthSchemaTables>).users)) {\n this.refreshTokensTable = ((tableOrTables as Partial<AuthSchemaTables>).refreshTokens || refreshTokens) as RebasePgTable;\n this.usersTable = ((tableOrTables as Partial<AuthSchemaTables>).users || users) as RebasePgTable;\n } else {\n this.refreshTokensTable = (tableOrTables as RebasePgTable) || (refreshTokens as unknown as RebasePgTable);\n this.usersTable = users as unknown as RebasePgTable;\n }\n }\n\n /**\n * Whether the table actually carries a column, so a host application that\n * supplied its own `refresh_tokens` table — one that predates session\n * grouping — degrades instead of throwing on every sign-in.\n */\n private has(column: string): boolean {\n return Boolean((this.refreshTokensTable as unknown as Record<string, unknown>)[column]);\n }\n\n private col(column: string) {\n return (this.refreshTokensTable as unknown as Record<string, never>)[column];\n }\n\n /** The columns to read back, narrowed to the ones this table has. */\n private selection() {\n const selection: Record<string, never> = {\n id: this.refreshTokensTable.id,\n uid: this.refreshTokensTable.uid,\n tokenHash: this.refreshTokensTable.tokenHash,\n expiresAt: this.refreshTokensTable.expiresAt,\n createdAt: this.refreshTokensTable.createdAt,\n userAgent: this.refreshTokensTable.userAgent,\n ipAddress: this.refreshTokensTable.ipAddress\n } as unknown as Record<string, never>;\n for (const optional of [\"sessionId\", \"rotatedAt\", \"revoked\", \"sessionStartedAt\", \"aal\"]) {\n if (this.has(optional)) selection[optional] = this.col(optional);\n }\n return selection;\n }\n\n async createToken(\n uid: string,\n tokenHash: string,\n expiresAt: Date,\n userAgent?: string,\n ipAddress?: string,\n session?: RefreshTokenSession\n ): Promise<void> {\n // Empty strings rather than NULLs: the device-session UNIQUE constraint\n // that needed them is gone, but sessions-list UIs already render \"\" as\n // \"unknown device\" and would start showing blanks otherwise.\n const safeUserAgent = userAgent || \"\";\n const safeIpAddress = ipAddress || \"\";\n\n // A plain INSERT. Rotation ADDS a token; it does not replace a device's\n // row. Two refreshes racing on the same session therefore both succeed\n // and both end holding a usable token, where the previous upsert had\n // them overwrite each other and logged one of the two tabs out.\n const values: Record<string, unknown> = {\n uid,\n tokenHash,\n expiresAt,\n userAgent: safeUserAgent,\n ipAddress: safeIpAddress\n };\n if (session && this.has(\"sessionId\")) values.sessionId = session.id;\n if (session && this.has(\"sessionStartedAt\")) values.sessionStartedAt = session.startedAt;\n // Written on every token of the session, including the ones rotation\n // mints, because refresh reads the level off whichever row was\n // presented. A table without the column degrades to `aal1` on read,\n // which is the restrictive answer rather than a bypass.\n if (session?.aal && this.has(\"aal\")) values.aal = session.aal;\n\n await this.db.insert(this.refreshTokensTable).values(values);\n }\n\n async findByHash(tokenHash: string): Promise<RefreshTokenInfo | null> {\n const [token] = await this.db\n .select(this.selection())\n .from(this.refreshTokensTable)\n .where(eq(this.refreshTokensTable.tokenHash, tokenHash));\n\n return (token as unknown as RefreshTokenInfo) || null;\n }\n\n /**\n * Record that a token was rotated away, keeping the row.\n *\n * The row is what lets `/auth/refresh` distinguish \"you already used this,\n * here is a fresh one\" from \"no idea what this is\". Deleting it — which is\n * what this used to do — collapsed both into a 401 and signed the user out\n * for the crime of losing a response.\n */\n async markRotated(tokenHash: string): Promise<void> {\n if (!this.has(\"rotatedAt\")) {\n await this.deleteByHash(tokenHash);\n return;\n }\n await this.db\n .update(this.refreshTokensTable)\n .set({ rotatedAt: new Date() })\n .where(eq(this.refreshTokensTable.tokenHash, tokenHash));\n }\n\n /** Final kill of one sign-in: logout, or revoking a device remotely. */\n async revokeSession(sessionId: string): Promise<void> {\n if (!this.has(\"sessionId\")) return;\n if (this.has(\"revoked\")) {\n await this.db\n .update(this.refreshTokensTable)\n .set({ revoked: true, ...(this.has(\"rotatedAt\") ? { rotatedAt: new Date() } : {}) })\n .where(eq(this.col(\"sessionId\"), sessionId));\n return;\n }\n await this.db.delete(this.refreshTokensTable).where(eq(this.col(\"sessionId\"), sessionId));\n }\n\n /**\n * Housekeeping: rotation would otherwise leave a row per refresh forever.\n * Superseded rows are only needed for as long as a straggler might still\n * present them, and expired ones are dead weight everywhere.\n */\n async prune(uid: string, sessionId: string, supersededBefore: Date): Promise<void> {\n const uidCol = this.refreshTokensTable.uid;\n const expiresCol = this.refreshTokensTable.expiresAt;\n if (!this.has(\"rotatedAt\") || !this.has(\"sessionId\")) {\n await this.db.delete(this.refreshTokensTable)\n .where(sql`${uidCol} = ${uid} AND ${expiresCol} < NOW()`);\n return;\n }\n const rotatedCol = this.col(\"rotatedAt\");\n const sessionCol = this.col(\"sessionId\");\n await this.db.delete(this.refreshTokensTable).where(sql`\n ${uidCol} = ${uid}\n AND (\n ${expiresCol} < NOW()\n OR (\n ${sessionCol} = ${sessionId}\n AND ${rotatedCol} IS NOT NULL\n AND ${rotatedCol} < ${supersededBefore}\n )\n )\n `);\n }\n\n async getTokensValidAfter(uid: string): Promise<Date | null> {\n if (!this.usersTable || !(this.usersTable as unknown as Record<string, unknown>).tokensValidAfter) return null;\n const [row] = await this.db\n .select({ tokensValidAfter: (this.usersTable as unknown as Record<string, never>).tokensValidAfter })\n .from(this.usersTable)\n .where(eq(this.usersTable.id, uid));\n const value = (row as { tokensValidAfter?: Date | string | null } | undefined)?.tokensValidAfter;\n return value ? new Date(value) : null;\n }\n\n async setTokensValidAfter(uid: string, at: Date): Promise<void> {\n if (!this.usersTable || !(this.usersTable as unknown as Record<string, unknown>).tokensValidAfter) return;\n await this.db\n .update(this.usersTable)\n .set({ tokensValidAfter: at })\n .where(eq(this.usersTable.id, uid));\n }\n\n async deleteByHash(tokenHash: string): Promise<void> {\n await this.db.delete(this.refreshTokensTable).where(eq(this.refreshTokensTable.tokenHash, tokenHash));\n }\n\n async deleteAllForUser(uid: string): Promise<void> {\n await this.db.delete(this.refreshTokensTable).where(eq(this.refreshTokensTable.uid, uid));\n }\n\n async listForUser(uid: string): Promise<RefreshTokenInfo[]> {\n const tokens = await this.db\n .select(this.selection())\n .from(this.refreshTokensTable)\n .where(eq(this.refreshTokensTable.uid, uid))\n .orderBy(this.refreshTokensTable.createdAt);\n\n return tokens as unknown as RefreshTokenInfo[];\n }\n\n async deleteById(id: string, uid: string): Promise<void> {\n await this.db.delete(this.refreshTokensTable)\n .where(sql`${this.refreshTokensTable.id} = ${id} AND ${this.refreshTokensTable.uid} = ${uid}`);\n }\n}\n\n/**\n * Password reset token service\n */\nexport class PasswordResetTokenService {\n private passwordResetTokensTable: RebasePgTable;\n\n constructor(\n private db: NodePgDatabase,\n tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>\n ) {\n if (tableOrTables && ((tableOrTables as Partial<AuthSchemaTables>).passwordResetTokens || (tableOrTables as Partial<AuthSchemaTables>).users)) {\n this.passwordResetTokensTable = ((tableOrTables as Partial<AuthSchemaTables>).passwordResetTokens || passwordResetTokens) as RebasePgTable;\n } else {\n this.passwordResetTokensTable = (tableOrTables as RebasePgTable) || (passwordResetTokens as unknown as RebasePgTable);\n }\n }\n\n private getQualifiedPasswordResetTokensTableName(): string {\n const name = getTableName(this.passwordResetTokensTable);\n const schema = getTableConfig(this.passwordResetTokensTable).schema || \"public\";\n return `\"${schema}\".\"${name}\"`;\n }\n\n /**\n * Create a password reset token\n */\n async createToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {\n // Delete any existing unused tokens for this user\n const tableName = this.getQualifiedPasswordResetTokensTableName();\n await this.db.execute(sql`\n DELETE FROM ${sql.raw(tableName)} \n WHERE uid = ${uid} AND used_at IS NULL\n `);\n\n await this.db.insert(this.passwordResetTokensTable).values({\n uid,\n tokenHash,\n expiresAt\n });\n }\n\n /**\n * Find a valid (not expired, not used) token by hash\n */\n async findValidByHash(tokenHash: string): Promise<{ uid: string; expiresAt: Date } | null> {\n const [token] = await this.db\n .select({\n uid: this.passwordResetTokensTable.uid,\n expiresAt: this.passwordResetTokensTable.expiresAt\n })\n .from(this.passwordResetTokensTable)\n .where(eq(this.passwordResetTokensTable.tokenHash, tokenHash)) as unknown as Array<{ uid: string; expiresAt: Date }>;\n\n if (!token) return null;\n\n // Check if expired or used\n const tableName = this.getQualifiedPasswordResetTokensTableName();\n const result = await this.db.execute(sql`\n SELECT uid, expires_at \n FROM ${sql.raw(tableName)} \n WHERE token_hash = ${tokenHash} \n AND used_at IS NULL \n AND expires_at > NOW()\n `);\n\n if (result.rows.length === 0) return null;\n\n const row = result.rows[0] as { uid: string; expires_at: string | number | Date };\n return {\n uid: row.uid,\n expiresAt: new Date(row.expires_at)\n };\n }\n\n /**\n * Mark token as used\n */\n async markAsUsed(tokenHash: string): Promise<void> {\n await this.db\n .update(this.passwordResetTokensTable)\n .set({ usedAt: new Date() })\n .where(eq(this.passwordResetTokensTable.tokenHash, tokenHash));\n }\n\n /**\n * Delete all tokens for a user\n */\n async deleteAllForUser(uid: string): Promise<void> {\n await this.db.delete(this.passwordResetTokensTable).where(eq(this.passwordResetTokensTable.uid, uid));\n }\n\n /**\n * Clean up expired tokens\n */\n async deleteExpired(): Promise<void> {\n const tableName = this.getQualifiedPasswordResetTokensTableName();\n await this.db.execute(sql`\n DELETE FROM ${sql.raw(tableName)} \n WHERE expires_at < NOW()\n `);\n }\n}\n\n/**\n * Magic link token service.\n * Handles magic link token storage for passwordless email login.\n */\nexport class MagicLinkTokenService {\n private magicLinkTokensTable: RebasePgTable;\n\n constructor(\n private db: NodePgDatabase,\n tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>\n ) {\n this.magicLinkTokensTable = (magicLinkTokens as unknown as RebasePgTable);\n }\n\n private getQualifiedTableName(): string {\n const name = getTableName(this.magicLinkTokensTable);\n const schema = getTableConfig(this.magicLinkTokensTable).schema || \"public\";\n return `\"${schema}\".\"${name}\"`;\n }\n\n async createToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {\n // Delete any existing unused tokens for this user\n const tableName = this.getQualifiedTableName();\n await this.db.execute(sql`\n DELETE FROM ${sql.raw(tableName)} \n WHERE uid = ${uid} AND used_at IS NULL\n `);\n\n await this.db.insert(this.magicLinkTokensTable).values({\n uid,\n tokenHash,\n expiresAt\n });\n }\n\n async findValidByHash(tokenHash: string): Promise<MagicLinkTokenInfo | null> {\n const tableName = this.getQualifiedTableName();\n const result = await this.db.execute(sql`\n SELECT uid, expires_at \n FROM ${sql.raw(tableName)} \n WHERE token_hash = ${tokenHash} \n AND used_at IS NULL \n AND expires_at > NOW()\n `);\n\n if (result.rows.length === 0) return null;\n\n const row = result.rows[0] as { uid: string; expires_at: string | number | Date };\n return {\n uid: row.uid,\n expiresAt: new Date(row.expires_at)\n };\n }\n\n async markAsUsed(tokenHash: string): Promise<void> {\n await this.db\n .update(this.magicLinkTokensTable)\n .set({ usedAt: new Date() })\n .where(eq(this.magicLinkTokensTable.tokenHash, tokenHash));\n }\n}\n\n/**\n * PostgreSQL implementation of TokenRepository.\n * Combines refresh token and password reset token operations.\n */\nexport class PostgresTokenRepository implements TokenRepository {\n private refreshTokenService: RefreshTokenService;\n private passwordResetTokenService: PasswordResetTokenService;\n private magicLinkTokenService: MagicLinkTokenService;\n\n constructor(\n private db: NodePgDatabase,\n tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>\n ) {\n this.refreshTokenService = new RefreshTokenService(db, tableOrTables);\n this.passwordResetTokenService = new PasswordResetTokenService(db, tableOrTables);\n this.magicLinkTokenService = new MagicLinkTokenService(db, tableOrTables);\n }\n\n // Refresh token operations\n\n async createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise<void> {\n await this.refreshTokenService.createToken(uid, tokenHash, expiresAt, userAgent, ipAddress, session);\n }\n\n async markRefreshTokenRotated(tokenHash: string): Promise<void> {\n await this.refreshTokenService.markRotated(tokenHash);\n }\n\n async revokeRefreshTokenSession(sessionId: string): Promise<void> {\n await this.refreshTokenService.revokeSession(sessionId);\n }\n\n async pruneRefreshTokens(uid: string, sessionId: string, supersededBefore: Date): Promise<void> {\n await this.refreshTokenService.prune(uid, sessionId, supersededBefore);\n }\n\n async getTokensValidAfter(uid: string): Promise<Date | null> {\n return this.refreshTokenService.getTokensValidAfter(uid);\n }\n\n async setTokensValidAfter(uid: string, at: Date): Promise<void> {\n await this.refreshTokenService.setTokensValidAfter(uid, at);\n }\n\n async findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null> {\n return this.refreshTokenService.findByHash(tokenHash);\n }\n\n async deleteRefreshToken(tokenHash: string): Promise<void> {\n await this.refreshTokenService.deleteByHash(tokenHash);\n }\n\n async deleteAllRefreshTokensForUser(uid: string): Promise<void> {\n await this.refreshTokenService.deleteAllForUser(uid);\n }\n\n async listRefreshTokensForUser(uid: string): Promise<RefreshTokenInfo[]> {\n return this.refreshTokenService.listForUser(uid);\n }\n\n async deleteRefreshTokenById(id: string, uid: string): Promise<void> {\n await this.refreshTokenService.deleteById(id, uid);\n }\n\n // Password reset token operations\n\n async createPasswordResetToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {\n await this.passwordResetTokenService.createToken(uid, tokenHash, expiresAt);\n }\n\n async findValidPasswordResetToken(tokenHash: string): Promise<PasswordResetTokenInfo | null> {\n return this.passwordResetTokenService.findValidByHash(tokenHash);\n }\n\n async markPasswordResetTokenUsed(tokenHash: string): Promise<void> {\n await this.passwordResetTokenService.markAsUsed(tokenHash);\n }\n\n async deleteAllPasswordResetTokensForUser(uid: string): Promise<void> {\n await this.passwordResetTokenService.deleteAllForUser(uid);\n }\n\n async deleteExpiredTokens(): Promise<void> {\n await this.passwordResetTokenService.deleteExpired();\n }\n\n // Magic link token operations\n\n async createMagicLinkToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {\n await this.magicLinkTokenService.createToken(uid, tokenHash, expiresAt);\n }\n\n async findValidMagicLinkToken(tokenHash: string): Promise<MagicLinkTokenInfo | null> {\n return this.magicLinkTokenService.findValidByHash(tokenHash);\n }\n\n async markMagicLinkTokenUsed(tokenHash: string): Promise<void> {\n await this.magicLinkTokenService.markAsUsed(tokenHash);\n }\n}\n\n/**\n * PostgreSQL implementation of AuthRepository.\n * Combines user, role, and token repository operations.\n * This provides a convenient single-class interface for all auth operations.\n */\nexport class PostgresAuthRepository implements AuthRepository {\n private userService: UserService;\n private tokenRepository: PostgresTokenRepository;\n\n constructor(\n private db: NodePgDatabase,\n tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>\n ) {\n this.userService = new UserService(db, tableOrTables);\n this.tokenRepository = new PostgresTokenRepository(db, tableOrTables);\n }\n\n // User operations (delegate to UserService)\n\n async createUser(data: CreateUserData): Promise<UserData> {\n return this.userService.createUser(data);\n }\n\n async getUserById(id: string): Promise<UserData | null> {\n return this.userService.getUserById(id);\n }\n\n async getUserByEmail(email: string): Promise<UserData | null> {\n return this.userService.getUserByEmail(email);\n }\n\n async getUserByIdentity(provider: string, providerId: string): Promise<UserData | null> {\n return this.userService.getUserByIdentity(provider, providerId);\n }\n\n async getUserIdentities(uid: string): Promise<UserIdentityData[]> {\n return this.userService.getUserIdentities(uid);\n }\n\n async linkUserIdentity(uid: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void> {\n return this.userService.linkUserIdentity(uid, provider, providerId, profileData);\n }\n\n async updateUser(id: string, data: Partial<Omit<CreateUserData, \"id\">>): Promise<UserData | null> {\n return this.userService.updateUser(id, data);\n }\n\n async deleteUser(id: string): Promise<void> {\n await this.userService.deleteUser(id);\n }\n\n async listUsers(): Promise<UserData[]> {\n return this.userService.listUsers();\n }\n\n async listUsersPaginated(options?: ListUsersOptions): Promise<PaginatedUsersResult> {\n return this.userService.listUsersPaginated(options);\n }\n\n async updatePassword(id: string, passwordHash: string): Promise<void> {\n await this.userService.updatePassword(id, passwordHash);\n }\n\n async setEmailVerified(id: string, verified: boolean): Promise<void> {\n await this.userService.setEmailVerified(id, verified);\n }\n\n async setVerificationToken(id: string, token: string | null): Promise<void> {\n await this.userService.setVerificationToken(id, token);\n }\n\n async getUserByVerificationToken(token: string): Promise<UserData | null> {\n return this.userService.getUserByVerificationToken(token);\n }\n\n async getUserRoles(uid: string): Promise<RoleData[]> {\n return this.userService.getUserRoles(uid);\n }\n\n async getUserRoleIds(uid: string): Promise<string[]> {\n return this.userService.getUserRoleIds(uid);\n }\n\n async setUserRoles(uid: string, roleIds: string[]): Promise<void> {\n await this.userService.setUserRoles(uid, roleIds);\n }\n\n async assignDefaultRole(uid: string, roleId: string): Promise<void> {\n await this.userService.assignDefaultRole(uid, roleId);\n }\n\n async getUserWithRoles(uid: string): Promise<{ user: UserData; roles: RoleData[] } | null> {\n return this.userService.getUserWithRoles(uid);\n }\n\n // Role operations (roles are inline on users, synthesized from string IDs)\n\n async getRoleById(id: string): Promise<RoleData | null> {\n return {\n id,\n name: id,\n isAdmin: id === \"admin\",\n defaultPermissions: null,\n collectionPermissions: null\n };\n }\n\n async listRoles(): Promise<RoleData[]> {\n return [\n { id: \"admin\",\nname: \"Admin\",\nisAdmin: true,\ndefaultPermissions: null,\ncollectionPermissions: null },\n { id: \"editor\",\nname: \"Editor\",\nisAdmin: false,\ndefaultPermissions: null,\ncollectionPermissions: null },\n { id: \"viewer\",\nname: \"Viewer\",\nisAdmin: false,\ndefaultPermissions: null,\ncollectionPermissions: null }\n ];\n }\n\n async createRole(_data: CreateRoleData): Promise<RoleData> {\n return {\n id: _data.id,\n name: _data.name,\n isAdmin: _data.isAdmin ?? false,\n defaultPermissions: _data.defaultPermissions ?? null,\n collectionPermissions: _data.collectionPermissions ?? null\n };\n }\n\n async updateRole(id: string, data: Partial<Omit<RoleData, \"id\">>): Promise<RoleData | null> {\n return {\n id,\n name: data.name ?? id,\n isAdmin: data.isAdmin ?? (id === \"admin\"),\n defaultPermissions: data.defaultPermissions ?? null,\n collectionPermissions: data.collectionPermissions ?? null\n };\n }\n\n async deleteRole(_id: string): Promise<void> {\n // No-op: roles are inline strings on users\n }\n\n // Token operations (delegate to PostgresTokenRepository)\n\n async createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise<void> {\n await this.tokenRepository.createRefreshToken(uid, tokenHash, expiresAt, userAgent, ipAddress, session);\n }\n\n async markRefreshTokenRotated(tokenHash: string): Promise<void> {\n await this.tokenRepository.markRefreshTokenRotated(tokenHash);\n }\n\n async revokeRefreshTokenSession(sessionId: string): Promise<void> {\n await this.tokenRepository.revokeRefreshTokenSession(sessionId);\n }\n\n async pruneRefreshTokens(uid: string, sessionId: string, supersededBefore: Date): Promise<void> {\n await this.tokenRepository.pruneRefreshTokens(uid, sessionId, supersededBefore);\n }\n\n async getTokensValidAfter(uid: string): Promise<Date | null> {\n return this.tokenRepository.getTokensValidAfter(uid);\n }\n\n async setTokensValidAfter(uid: string, at: Date): Promise<void> {\n await this.tokenRepository.setTokensValidAfter(uid, at);\n }\n\n async findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null> {\n return this.tokenRepository.findRefreshTokenByHash(tokenHash);\n }\n\n async deleteRefreshToken(tokenHash: string): Promise<void> {\n await this.tokenRepository.deleteRefreshToken(tokenHash);\n }\n\n async deleteAllRefreshTokensForUser(uid: string): Promise<void> {\n await this.tokenRepository.deleteAllRefreshTokensForUser(uid);\n }\n\n async listRefreshTokensForUser(uid: string): Promise<RefreshTokenInfo[]> {\n return this.tokenRepository.listRefreshTokensForUser(uid);\n }\n\n async deleteRefreshTokenById(id: string, uid: string): Promise<void> {\n await this.tokenRepository.deleteRefreshTokenById(id, uid);\n }\n\n async createPasswordResetToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {\n await this.tokenRepository.createPasswordResetToken(uid, tokenHash, expiresAt);\n }\n\n async findValidPasswordResetToken(tokenHash: string): Promise<PasswordResetTokenInfo | null> {\n return this.tokenRepository.findValidPasswordResetToken(tokenHash);\n }\n\n async markPasswordResetTokenUsed(tokenHash: string): Promise<void> {\n await this.tokenRepository.markPasswordResetTokenUsed(tokenHash);\n }\n\n async deleteAllPasswordResetTokensForUser(uid: string): Promise<void> {\n await this.tokenRepository.deleteAllPasswordResetTokensForUser(uid);\n }\n\n async deleteExpiredTokens(): Promise<void> {\n await this.tokenRepository.deleteExpiredTokens();\n }\n\n // Magic link token operations\n\n async createMagicLinkToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {\n await this.tokenRepository.createMagicLinkToken(uid, tokenHash, expiresAt);\n }\n\n async findValidMagicLinkToken(tokenHash: string): Promise<MagicLinkTokenInfo | null> {\n return this.tokenRepository.findValidMagicLinkToken(tokenHash);\n }\n\n async markMagicLinkTokenUsed(tokenHash: string): Promise<void> {\n await this.tokenRepository.markMagicLinkTokenUsed(tokenHash);\n }\n\n // MFA operations (delegate to MfaService)\n\n private _mfaService: MfaService | null = null;\n private getMfaService(): MfaService {\n if (!this._mfaService) {\n this._mfaService = new MfaService(this.db);\n }\n return this._mfaService;\n }\n\n async createMfaFactor(uid: string, factorType: \"totp\", secretEncrypted: string, friendlyName?: string): Promise<MfaFactor> {\n return this.getMfaService().createMfaFactor(uid, factorType, secretEncrypted, friendlyName);\n }\n\n async getMfaFactors(uid: string): Promise<MfaFactor[]> {\n return this.getMfaService().getMfaFactors(uid);\n }\n\n async getMfaFactorById(factorId: string): Promise<(MfaFactor & { secretEncrypted: string }) | null> {\n return this.getMfaService().getMfaFactorById(factorId);\n }\n\n async verifyMfaFactor(factorId: string): Promise<void> {\n return this.getMfaService().verifyMfaFactor(factorId);\n }\n\n async updateMfaFactorSecret(factorId: string, secretEncrypted: string): Promise<void> {\n return this.getMfaService().updateMfaFactorSecret(factorId, secretEncrypted);\n }\n\n async deleteMfaFactor(factorId: string, uid: string): Promise<void> {\n return this.getMfaService().deleteMfaFactor(factorId, uid);\n }\n\n async createMfaChallenge(factorId: string, ipAddress?: string): Promise<MfaChallengeInfo> {\n return this.getMfaService().createMfaChallenge(factorId, ipAddress);\n }\n\n async getMfaChallengeById(challengeId: string): Promise<MfaChallengeInfo | null> {\n return this.getMfaService().getMfaChallengeById(challengeId);\n }\n\n async verifyMfaChallenge(challengeId: string): Promise<void> {\n return this.getMfaService().verifyMfaChallenge(challengeId);\n }\n\n async createRecoveryCodes(uid: string, codeHashes: string[]): Promise<void> {\n return this.getMfaService().createRecoveryCodes(uid, codeHashes);\n }\n\n async useRecoveryCode(uid: string, codeHash: string): Promise<boolean> {\n return this.getMfaService().useRecoveryCode(uid, codeHash);\n }\n\n async getUnusedRecoveryCodeCount(uid: string): Promise<number> {\n return this.getMfaService().getUnusedRecoveryCodeCount(uid);\n }\n\n async deleteAllRecoveryCodes(uid: string): Promise<void> {\n return this.getMfaService().deleteAllRecoveryCodes(uid);\n }\n\n async hasVerifiedMfaFactors(uid: string): Promise<boolean> {\n return this.getMfaService().hasVerifiedMfaFactors(uid);\n }\n\n async claimMfaFactorCounter(factorId: string, counter: number): Promise<boolean> {\n return this.getMfaService().claimMfaFactorCounter(factorId, counter);\n }\n\n async recordMfaChallengeAttempt(challengeId: string): Promise<number> {\n return this.getMfaService().recordMfaChallengeAttempt(challengeId);\n }\n}\n\n// =============================================================================\n// MFA SERVICE\n// =============================================================================\n\n/**\n * PostgreSQL implementation of MfaRepository.\n * Handles all MFA-related database operations.\n */\nexport class MfaService implements MfaRepository {\n constructor(private db: NodePgDatabase, private schemaName = \"rebase\") {}\n\n private qualify(tableName: string): string {\n return `\"${this.schemaName}\".\"${tableName}\"`;\n }\n\n async createMfaFactor(\n uid: string,\n factorType: \"totp\",\n secretEncrypted: string,\n friendlyName?: string\n ): Promise<MfaFactor> {\n const tableName = this.qualify(\"mfa_factors\");\n const result = await this.db.execute(sql`\n INSERT INTO ${sql.raw(tableName)} (uid, factor_type, secret_encrypted, friendly_name)\n VALUES (${uid}, ${factorType}, ${secretEncrypted}, ${friendlyName ?? null})\n RETURNING id, uid, factor_type, friendly_name, verified, created_at, updated_at\n `);\n\n const row = result.rows[0] as Record<string, unknown>;\n return {\n id: row.id as string,\n uid: row.uid as string,\n factorType: row.factor_type as \"totp\",\n friendlyName: (row.friendly_name as string | null) ?? undefined,\n verified: row.verified as boolean,\n createdAt: new Date(row.created_at as string),\n updatedAt: new Date(row.updated_at as string)\n };\n }\n\n async getMfaFactors(uid: string): Promise<MfaFactor[]> {\n const tableName = this.qualify(\"mfa_factors\");\n const result = await this.db.execute(sql`\n SELECT id, uid, factor_type, friendly_name, verified, created_at, updated_at\n FROM ${sql.raw(tableName)}\n WHERE uid = ${uid}\n ORDER BY created_at\n `);\n\n return (result.rows as Array<Record<string, unknown>>).map(row => ({\n id: row.id as string,\n uid: row.uid as string,\n factorType: row.factor_type as \"totp\",\n friendlyName: (row.friendly_name as string | null) ?? undefined,\n verified: row.verified as boolean,\n createdAt: new Date(row.created_at as string),\n updatedAt: new Date(row.updated_at as string)\n }));\n }\n\n async getMfaFactorById(factorId: string): Promise<(MfaFactor & { secretEncrypted: string }) | null> {\n const tableName = this.qualify(\"mfa_factors\");\n const result = await this.db.execute(sql`\n SELECT id, uid, factor_type, secret_encrypted, friendly_name, verified, last_used_counter, created_at, updated_at\n FROM ${sql.raw(tableName)}\n WHERE id = ${factorId}\n `);\n\n if (result.rows.length === 0) return null;\n\n const row = result.rows[0] as Record<string, unknown>;\n return {\n id: row.id as string,\n uid: row.uid as string,\n factorType: row.factor_type as \"totp\",\n secretEncrypted: row.secret_encrypted as string,\n friendlyName: (row.friendly_name as string | null) ?? undefined,\n verified: row.verified as boolean,\n // BIGINT comes back as a string from node-postgres.\n lastUsedCounter: row.last_used_counter === null || row.last_used_counter === undefined\n ? null\n : Number(row.last_used_counter),\n createdAt: new Date(row.created_at as string),\n updatedAt: new Date(row.updated_at as string)\n };\n }\n\n /**\n * Spend a TOTP time step, once and only once.\n *\n * One statement: the `WHERE` is the check, the `UPDATE` is the act, and\n * `RETURNING` reports which of two concurrent requests carrying the same\n * six digits won. Reading the counter and then writing it would let both\n * pass — the exact replay this closes.\n */\n async claimMfaFactorCounter(factorId: string, counter: number): Promise<boolean> {\n const tableName = this.qualify(\"mfa_factors\");\n const result = await this.db.execute(sql`\n UPDATE ${sql.raw(tableName)}\n SET last_used_counter = ${counter}, updated_at = NOW()\n WHERE id = ${factorId}\n AND (last_used_counter IS NULL OR last_used_counter < ${counter})\n RETURNING id\n `);\n\n return result.rows.length > 0;\n }\n\n async verifyMfaFactor(factorId: string): Promise<void> {\n const tableName = this.qualify(\"mfa_factors\");\n await this.db.execute(sql`\n UPDATE ${sql.raw(tableName)}\n SET verified = TRUE, updated_at = NOW()\n WHERE id = ${factorId}\n `);\n }\n\n async updateMfaFactorSecret(factorId: string, secretEncrypted: string): Promise<void> {\n const tableName = this.qualify(\"mfa_factors\");\n await this.db.execute(sql`\n UPDATE ${sql.raw(tableName)}\n SET secret_encrypted = ${secretEncrypted}, updated_at = NOW()\n WHERE id = ${factorId}\n `);\n }\n\n async deleteMfaFactor(factorId: string, uid: string): Promise<void> {\n const tableName = this.qualify(\"mfa_factors\");\n await this.db.execute(sql`\n DELETE FROM ${sql.raw(tableName)}\n WHERE id = ${factorId} AND uid = ${uid}\n `);\n }\n\n async createMfaChallenge(factorId: string, ipAddress?: string): Promise<MfaChallengeInfo> {\n const tableName = this.qualify(\"mfa_challenges\");\n // Challenges expire in 5 minutes\n const expiresAt = new Date(Date.now() + 5 * 60 * 1000);\n const result = await this.db.execute(sql`\n INSERT INTO ${sql.raw(tableName)} (factor_id, ip_address, expires_at)\n VALUES (${factorId}, ${ipAddress ?? null}, ${expiresAt})\n RETURNING id, factor_id, created_at, verified_at, ip_address\n `);\n\n const row = result.rows[0] as Record<string, unknown>;\n return {\n id: row.id as string,\n factorId: row.factor_id as string,\n createdAt: new Date(row.created_at as string),\n verifiedAt: row.verified_at ? new Date(row.verified_at as string) : undefined,\n ipAddress: (row.ip_address as string | null) ?? undefined\n };\n }\n\n async getMfaChallengeById(challengeId: string): Promise<MfaChallengeInfo | null> {\n const tableName = this.qualify(\"mfa_challenges\");\n const result = await this.db.execute(sql`\n SELECT id, factor_id, created_at, verified_at, ip_address, attempts, expires_at\n FROM ${sql.raw(tableName)}\n WHERE id = ${challengeId} AND expires_at > NOW() AND verified_at IS NULL\n `);\n\n if (result.rows.length === 0) return null;\n\n const row = result.rows[0] as Record<string, unknown>;\n return {\n id: row.id as string,\n factorId: row.factor_id as string,\n createdAt: new Date(row.created_at as string),\n verifiedAt: row.verified_at ? new Date(row.verified_at as string) : undefined,\n ipAddress: (row.ip_address as string | null) ?? undefined,\n attempts: Number(row.attempts ?? 0)\n };\n }\n\n /**\n * Count one failed guess against a challenge and report the new total.\n *\n * Incremented in the database rather than in the route so that guesses\n * arriving in parallel — the shape any real brute-force takes — cannot\n * share a single increment.\n */\n async recordMfaChallengeAttempt(challengeId: string): Promise<number> {\n const tableName = this.qualify(\"mfa_challenges\");\n const result = await this.db.execute(sql`\n UPDATE ${sql.raw(tableName)}\n SET attempts = attempts + 1\n WHERE id = ${challengeId}\n RETURNING attempts\n `);\n\n if (result.rows.length === 0) return 0;\n return Number((result.rows[0] as { attempts: number | string }).attempts);\n }\n\n async verifyMfaChallenge(challengeId: string): Promise<void> {\n const tableName = this.qualify(\"mfa_challenges\");\n await this.db.execute(sql`\n UPDATE ${sql.raw(tableName)}\n SET verified_at = NOW()\n WHERE id = ${challengeId}\n `);\n }\n\n async createRecoveryCodes(uid: string, codeHashes: string[]): Promise<void> {\n const tableName = this.qualify(\"recovery_codes\");\n // Delete existing codes first\n await this.db.execute(sql`\n DELETE FROM ${sql.raw(tableName)} WHERE uid = ${uid}\n `);\n\n // Insert new codes\n for (const hash of codeHashes) {\n await this.db.execute(sql`\n INSERT INTO ${sql.raw(tableName)} (uid, code_hash)\n VALUES (${uid}, ${hash})\n `);\n }\n }\n\n async useRecoveryCode(uid: string, codeHash: string): Promise<boolean> {\n const tableName = this.qualify(\"recovery_codes\");\n const result = await this.db.execute(sql`\n UPDATE ${sql.raw(tableName)}\n SET used_at = NOW()\n WHERE uid = ${uid} AND code_hash = ${codeHash} AND used_at IS NULL\n RETURNING id\n `);\n\n return result.rows.length > 0;\n }\n\n async getUnusedRecoveryCodeCount(uid: string): Promise<number> {\n const tableName = this.qualify(\"recovery_codes\");\n const result = await this.db.execute(sql`\n SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}\n WHERE uid = ${uid} AND used_at IS NULL\n `);\n\n return (result.rows[0] as { count: number }).count;\n }\n\n async deleteAllRecoveryCodes(uid: string): Promise<void> {\n const tableName = this.qualify(\"recovery_codes\");\n await this.db.execute(sql`\n DELETE FROM ${sql.raw(tableName)} WHERE uid = ${uid}\n `);\n }\n\n async hasVerifiedMfaFactors(uid: string): Promise<boolean> {\n const tableName = this.qualify(\"mfa_factors\");\n const result = await this.db.execute(sql`\n SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}\n WHERE uid = ${uid} AND verified = TRUE\n `);\n\n return (result.rows[0] as { count: number }).count > 0;\n }\n}\n\n// =============================================================================\n// PostgreSQL Type Aliases (for consistent naming with other implementations)\n// =============================================================================\n\n/** PostgreSQL user repository implementation */\nexport type PostgresUserRepository = UserService;\n","import { sql } from \"drizzle-orm\";\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { logger } from \"@rebasepro/server\";\nimport type { EntityHistoryEntry } from \"@rebasepro/types\";\n\nexport type {\n RecordHistoryParams,\n FetchHistoryOptions,\n HistoryRetentionConfig\n} from \"@rebasepro/types\";\nimport type { RecordHistoryParams, FetchHistoryOptions, HistoryRetentionConfig } from \"@rebasepro/types\";\n\n/**\n * A Postgres history row is already the wire shape — `updated_at` comes back\n * from the driver as a string. Kept as an alias because the name is used\n * throughout this package and in `PostgresBackendDriver`.\n */\nexport type HistoryEntry = EntityHistoryEntry;\n\nconst DEFAULT_RETENTION: HistoryRetentionConfig = {\n maxEntries: 200,\n ttlDays: 90\n};\n\n/**\n * Service for recording and querying row change history.\n * Stores history entries in the `rebase.entity_history` table.\n */\nexport class HistoryService {\n public retention: HistoryRetentionConfig;\n\n constructor(\n private db: NodePgDatabase,\n retention?: Partial<HistoryRetentionConfig>\n ) {\n this.retention = { ...DEFAULT_RETENTION,\n...retention };\n }\n\n /**\n * Record a history entry for an row change.\n * This is intentionally fire-and-forget safe — errors are logged but never\n * bubble up to block the main save/delete operation.\n *\n * After inserting, kicks off a non-blocking pruning pass for this row.\n */\n async recordHistory(params: RecordHistoryParams): Promise<void> {\n const {\n tableName,\n id,\n action,\n values,\n previousValues,\n updatedBy\n } = params;\n\n const changedFields = previousValues && values\n ? findChangedFields(previousValues, values)\n : null;\n\n\n // Skip recording if this is an update with zero actual changes\n\n if (action === \"update\" && (!changedFields || changedFields.length === 0)) {\n return;\n }\n\n try {\n await this.db.execute(sql`\n INSERT INTO rebase.entity_history \n (table_name, entity_id, action, changed_fields, \"values\", previous_values, updated_by)\n VALUES (\n ${tableName},\n ${String(id)},\n ${action},\n ${changedFields ? sql`ARRAY[${sql.join(changedFields.map(f => sql`${f}`), sql`, `)}]::text[]` : sql`NULL`},\n ${values ? sql`${JSON.stringify(values)}::jsonb` : sql`NULL`},\n ${previousValues ? sql`${JSON.stringify(previousValues)}::jsonb` : sql`NULL`},\n ${updatedBy ?? null}\n )\n `);\n\n // Non-blocking prune for this specific row\n this.pruneEntity(tableName, id).catch(err =>\n logger.error(\"History prune failed\", { error: err })\n );\n } catch (error) {\n logger.error(\"Failed to record row history\", { error: error });\n }\n }\n\n /**\n * Fetch history entries for an row, ordered by most recent first.\n */\n async fetchHistory(\n tableName: string,\n id: string,\n options: FetchHistoryOptions = {}\n ): Promise<{ data: HistoryEntry[]; total: number }> {\n const limit = options.limit ?? 20;\n const offset = options.offset ?? 0;\n\n const [countResult, dataResult] = await Promise.all([\n this.db.execute(sql`\n SELECT COUNT(*) as count\n FROM rebase.entity_history\n WHERE table_name = ${tableName}\n AND entity_id = ${String(id)}\n `),\n this.db.execute(sql`\n SELECT id, table_name, entity_id, action, changed_fields,\n \"values\", previous_values, updated_by, updated_at\n FROM rebase.entity_history\n WHERE table_name = ${tableName}\n AND entity_id = ${String(id)}\n ORDER BY updated_at DESC\n LIMIT ${limit}\n OFFSET ${offset}\n `)\n ]);\n\n const total = parseInt(\n (countResult.rows[0] as Record<string, string>)?.count ?? \"0\",\n 10\n );\n\n return {\n data: dataResult.rows as unknown as HistoryEntry[],\n total\n };\n }\n\n /**\n * Fetch a single history entry by ID.\n */\n async fetchHistoryEntry(historyId: string): Promise<HistoryEntry | null> {\n const result = await this.db.execute(sql`\n SELECT id, table_name, entity_id, action, changed_fields,\n \"values\", previous_values, updated_by, updated_at\n FROM rebase.entity_history\n WHERE id = ${historyId}\n `);\n\n if (result.rows.length === 0) return null;\n return result.rows[0] as unknown as HistoryEntry;\n }\n\n // ───────── Retention / Pruning ─────────\n\n /**\n * Prune history for a single row: enforce maxEntries and TTL.\n */\n async pruneEntity(tableName: string, id: string): Promise<number> {\n let deleted = 0;\n\n // 1. TTL — delete entries older than ttlDays\n const ttlResult = await this.db.execute(sql`\n DELETE FROM rebase.entity_history\n WHERE table_name = ${tableName}\n AND entity_id = ${String(id)}\n AND updated_at < NOW() - MAKE_INTERVAL(days => ${this.retention.ttlDays})\n `);\n deleted += ttlResult.rowCount ?? 0;\n\n // 2. Max entries — keep the newest maxEntries, delete the rest\n const maxResult = await this.db.execute(sql`\n DELETE FROM rebase.entity_history\n WHERE id IN (\n SELECT id FROM rebase.entity_history\n WHERE table_name = ${tableName}\n AND entity_id = ${String(id)}\n ORDER BY updated_at DESC\n OFFSET ${this.retention.maxEntries}\n )\n `);\n deleted += maxResult.rowCount ?? 0;\n\n return deleted;\n }\n\n /**\n * Global prune: enforce TTL across ALL rows in a single sweep.\n * Intended to be called periodically (e.g. once per hour or daily).\n */\n async pruneExpired(): Promise<number> {\n const result = await this.db.execute(sql`\n DELETE FROM rebase.entity_history\n WHERE updated_at < NOW() - MAKE_INTERVAL(days => ${this.retention.ttlDays})\n `);\n return result.rowCount ?? 0;\n }\n}\n\n\n/**\n * Deep equality without JSON.stringify.\n * Handles primitives, arrays, Dates, and plain objects recursively.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a == null || b == null) return false;\n if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n return a.every((v, i) => deepEqual(v, b[i]));\n }\n if (typeof a === \"object\" && typeof b === \"object\") {\n const aObj = a as Record<string, unknown>;\n const bObj = b as Record<string, unknown>;\n const aKeys = Object.keys(aObj);\n const bKeys = Object.keys(bObj);\n if (aKeys.length !== bKeys.length) return false;\n return aKeys.every(k => deepEqual(aObj[k], bObj[k]));\n }\n return false;\n}\n\n/**\n * Shallow comparison to find top-level keys that changed between two objects.\n */\nexport function findChangedFields(\n oldValues: Record<string, unknown>,\n newValues: Record<string, unknown>\n): string[] | null {\n const changed: string[] = [];\n const allKeys = new Set([\n ...Object.keys(oldValues),\n ...Object.keys(newValues)\n ]);\n\n for (const key of allKeys) {\n const oldVal = oldValues[key];\n const newVal = newValues[key];\n\n // Skip internal metadata\n if (key.startsWith(\"__\")) continue;\n\n if (oldVal !== newVal) {\n // For objects/arrays, use structural comparison\n if (\n typeof oldVal === \"object\" && oldVal !== null &&\n typeof newVal === \"object\" && newVal !== null\n ) {\n if (!deepEqual(oldVal, newVal)) {\n changed.push(key);\n }\n } else {\n changed.push(key);\n }\n }\n }\n\n return changed.length > 0 ? changed : null;\n}\n","import { sql } from \"drizzle-orm\";\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { logger } from \"@rebasepro/server\";\nimport { revokeInternalTableSql } from \"@rebasepro/common\";\n\n/**\n * Auto-create the row history table if it doesn't exist.\n * This runs on startup when history is enabled, following the same\n * pattern as `ensureAuthTablesExist`.\n */\nexport async function ensureHistoryTableExists(db: NodePgDatabase): Promise<void> {\n logger.debug(\"🔍 Checking row history table...\");\n\n try {\n // Create the rebase schema (idempotent — may already exist from auth init)\n await db.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);\n\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS rebase.entity_history (\n id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,\n table_name TEXT NOT NULL,\n entity_id TEXT NOT NULL,\n action TEXT NOT NULL,\n changed_fields TEXT[],\n \"values\" JSONB,\n previous_values JSONB,\n updated_by TEXT,\n updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n )\n `);\n\n await db.execute(sql`\n CREATE INDEX IF NOT EXISTS idx_history_entity\n ON rebase.entity_history(table_name, entity_id)\n `);\n\n await db.execute(sql`\n CREATE INDEX IF NOT EXISTS idx_history_time\n ON rebase.entity_history(table_name, entity_id, updated_at DESC)\n `);\n\n // Every previous value of every audited row, in one table with no RLS\n // and no tenant scoping — so a readable copy defeats the row policies on\n // the tables it shadows. The driver's schema-wide grant reaches it\n // (created here, after that grant ran), so take it back.\n await db.execute(sql.raw(revokeInternalTableSql(\"rebase\", \"entity_history\")));\n\n logger.debug(\"✅ Entity history table ready\");\n } catch (error) {\n logger.error(\"❌ Failed to create row history table\", { error: error });\n logger.warn(\"⚠️ Continuing without creating history table.\");\n }\n}\n","import { getTableColumns } from \"drizzle-orm\";\nimport { PgArray, PgTable } from \"drizzle-orm/pg-core\";\nimport { logger } from \"@rebasepro/server\";\n\n/**\n * Patches all PgArray columns on the given tables to handle NULL values safely.\n *\n * Drizzle ORM's `PgArray.mapFromDriverValue` calls `value.map(...)` without\n * guarding against `null`. When a PostgreSQL native array column (`text[]`,\n * `integer[]`, etc.) contains NULL, the pg driver returns `null` in JavaScript,\n * and `null.map(...)` throws `TypeError: value.map is not a function`.\n *\n * This function walks every column of every registered table and, for any\n * `PgArray` column, wraps its `mapFromDriverValue` to return `null` when the\n * database value is nullish.\n *\n * This is a workaround for a known Drizzle ORM issue. Should be removed once\n * Drizzle handles nullable arrays natively.\n */\nexport function patchPgArrayNullSafety(tables: Record<string, unknown>): void {\n let patchedCount = 0;\n\n for (const tableOrRelation of Object.values(tables)) {\n if (!(tableOrRelation instanceof PgTable)) continue;\n\n const columns = getTableColumns(tableOrRelation);\n for (const column of Object.values(columns)) {\n if (column instanceof PgArray) {\n const original = column.mapFromDriverValue.bind(column);\n column.mapFromDriverValue = function (value: unknown) {\n if (value == null) return null;\n return original(value as string | unknown[]);\n };\n patchedCount++;\n }\n }\n }\n\n if (patchedCount > 0) {\n logger.debug(`[PgArray] Patched ${patchedCount} array column(s) for null-safety`);\n }\n}\n","/**\n * Naming helpers shared by the introspection modules. These live apart from\n * `introspect-db-logic.ts` because the inference pass needs them too, and\n * importing them from there would close a cycle back through this module.\n */\n\n/**\n * Convert a snake_case name to a human-readable Title Case label.\n * e.g. \"created_at\" -> \"Created At\", \"customer_id\" -> \"Customer Id\"\n */\nexport function humanize(snakeName: string): string {\n return snakeName\n .replace(/_/g, \" \")\n .replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n","/**\n * The PostgreSQL type → Rebase property type mapping.\n *\n * Split out of `introspect-db-logic` so that the structural analysis can use it\n * without importing the generator, which imports the analysis. Re-exported from\n * `introspect-db-logic` so existing callers keep their import path.\n */\n\n/**\n * Map a PostgreSQL data type to a Rebase property type.\n */\nexport function mapPgType(dataType: string): string {\n const dt = dataType.toLowerCase();\n\n // Interval MUST be checked before numeric (\"interval\" contains \"int\")\n if (dt === \"interval\") return \"string\";\n\n // Array types MUST be checked before numeric (\"_int4\" contains \"int\")\n if (dt === \"array\" || dt.startsWith(\"_\")) return \"array\";\n\n // Numeric types\n if (\n dt.includes(\"int\") || // integer, smallint, bigint\n dt.includes(\"numeric\") ||\n dt.includes(\"decimal\") ||\n dt.includes(\"serial\") || // serial, bigserial\n dt === \"real\" ||\n dt === \"float4\" ||\n dt === \"float8\" ||\n dt === \"double precision\" ||\n dt === \"money\"\n ) {\n return \"number\";\n }\n\n // Boolean\n if (dt.includes(\"bool\")) return \"boolean\";\n\n // Date / Time\n if (dt.includes(\"time\") || dt.includes(\"date\")) return \"date\";\n\n // JSON\n if (dt === \"json\" || dt === \"jsonb\") return \"map\";\n\n // Binary\n if (dt === \"bytea\") return \"binary\";\n\n // Network types\n if (dt === \"inet\" || dt === \"cidr\" || dt === \"macaddr\" || dt === \"macaddr8\") return \"string\";\n\n // UUID\n if (dt === \"uuid\") return \"string\";\n\n // Text/varchar/char — default to string\n return \"string\";\n}\n","/**\n * Introspection logic — pure functions and the pipeline that transforms\n * raw PostgreSQL metadata into Rebase collection definition files.\n *\n * This module contains NO side-effects: no fs writes, no pg.Client creation,\n * no process.exit. It is imported by introspect-db.ts (the CLI entry-point)\n * and consumed directly by tests.\n */\nimport { firstFreeKey, toWireKey } from \"@rebasepro/utils\";\nimport { inferPropertyFromData } from \"./introspect-db-inference\";\nimport { humanize } from \"./introspect-db-naming\";\nimport { mapPgType } from \"./introspect-db-types\";\nimport type { CheckFactsByTable } from \"./introspect-db-constraints\";\nimport type { TableClassification } from \"./introspect-db-structure\";\nimport {\n buildColumnFacts,\n deriveKanbanProperty,\n deriveListProperties,\n deriveSort,\n deriveTitleProperty,\n isDerivedIndexColumn,\n isReadOnlyColumn\n} from \"./introspect-db-structure\";\n\n// ── Typed interfaces for SQL query results ────────────────────────────\n\nexport interface TableRow {\n table_name: string;\n /** True for the parent of a partitioned table (`relkind = 'p'`). */\n is_partitioned?: boolean;\n}\n\nexport interface TableColumn {\n table_name: string;\n column_name: string;\n data_type: string;\n udt_name: string;\n is_nullable: string;\n column_default: string | null;\n atttypmod: number | null;\n /** 1-based position in the table, as declared. */\n ordinal_position?: number;\n /** `\"ALWAYS\"` for a generated column, `\"NEVER\"` otherwise. */\n is_generated?: string;\n /** `\"YES\"` for an identity column. */\n is_identity?: string;\n /** `\"ALWAYS\"` or `\"BY DEFAULT\"` on an identity column. */\n identity_generation?: string | null;\n /** The declared `varchar(n)` / `char(n)` bound, if any. */\n character_maximum_length?: number | null;\n numeric_precision?: number | null;\n numeric_scale?: number | null;\n}\n\nexport interface EnumValue {\n enum_name: string;\n enum_value: string;\n sort_order: number;\n}\n\nexport interface PrimaryKeyRow {\n table_name: string;\n column_name: string;\n}\n\nexport interface ForeignKeyRow {\n table_name: string;\n column_name: string;\n foreign_table_name: string;\n foreign_column_name: string;\n /** Name of the FK constraint — the only way to tell composite keys apart. */\n constraint_name?: string;\n /** 1-based position of this column within its constraint. */\n ordinal?: number;\n /** `\"CASCADE\"`, `\"RESTRICT\"`, `\"SET NULL\"`, `\"SET DEFAULT\"`, `\"NO ACTION\"`. */\n delete_rule?: string;\n}\n\n/** A unique constraint or unique index, as an ordered column list. */\nexport interface UniqueConstraintRow {\n table_name: string;\n constraint_name: string;\n column_names: string[];\n}\n\n/** A CHECK constraint, as `pg_get_constraintdef` renders it. */\nexport interface CheckConstraintRow {\n table_name: string;\n constraint_name: string;\n definition: string;\n}\n\n/** A `COMMENT ON TABLE` (null `column_name`) or `COMMENT ON COLUMN`. */\nexport interface CommentRow {\n table_name: string;\n column_name: string | null;\n comment: string;\n}\n\n/**\n * Everything one introspection run reads from the database.\n *\n * Passed around as one value so a new signal means a new field here rather than\n * a new parameter on every function between the query and the generator — the\n * shape `generateCollectionFile` had grown to seven positional arguments by.\n */\nexport interface SchemaMetadata {\n schema: string;\n tables: TableRow[];\n columns: TableColumn[];\n enumValues: EnumValue[];\n pks: PrimaryKeyRow[];\n fks: ForeignKeyRow[];\n uniques: UniqueConstraintRow[];\n checks: CheckConstraintRow[];\n comments: CommentRow[];\n /**\n * Row counts for the tables that needed one, capped — see `countRowsUpTo`.\n * Absent for every table introspection never had a reason to count.\n */\n rowCounts: Record<string, number>;\n}\n\nexport interface TableMeta {\n name: string;\n columns: TableColumn[];\n pks: string[];\n fks: ForeignKeyRow[];\n}\n\n// ── Irregular plurals that naive rules can't handle ───────────────────\n\nconst IRREGULAR_SINGULARS: Record<string, string> = {\n people: \"person\",\n children: \"child\",\n men: \"man\",\n women: \"woman\",\n mice: \"mouse\",\n geese: \"goose\",\n teeth: \"tooth\",\n feet: \"foot\",\n data: \"datum\",\n media: \"medium\",\n criteria: \"criterion\",\n phenomena: \"phenomenon\"\n};\n\n/**\n * Plurals in \"-ves\" whose singular really ends in f/fe, and which of the two.\n *\n * A blanket \"-ves\" -> \"-f\" rule gets `knives` -> `knif`, and mangles every\n * ordinary \"-ive\" noun that happens to be plural along with it: `archives` ->\n * `archif`, `objectives` -> `objectif`. The set of English words that genuinely\n * swap f/fe for ves is small and closed, so it is listed rather than guessed —\n * anything else ending in \"ves\" drops the trailing 's' like any other plural.\n *\n * Matched on the whole word, not on the suffix: `olives` ends in `lives`.\n */\nconst VES_SINGULAR_ENDINGS: Record<string, \"f\" | \"fe\"> = {\n calves: \"f\", dwarves: \"f\", elves: \"f\", halves: \"f\", hooves: \"f\",\n leaves: \"f\", loaves: \"f\", scarves: \"f\", selves: \"f\", sheaves: \"f\",\n shelves: \"f\", thieves: \"f\", wharves: \"f\", wolves: \"f\",\n knives: \"fe\", lives: \"fe\", wives: \"fe\"\n};\n\n/** Words ending in 's' that are already singular. */\nconst UNCOUNTABLE = new Set([\n \"status\", \"campus\", \"virus\", \"bus\", \"plus\", \"census\",\n \"diagnosis\", \"analysis\", \"basis\", \"crisis\", \"thesis\",\n \"synopsis\", \"parenthesis\", \"hypothesis\", \"emphasis\",\n \"news\", \"series\", \"species\", \"means\", \"athletics\",\n \"economics\", \"electronics\", \"mathematics\", \"physics\",\n \"politics\", \"statistics\"\n]);\n\nexport function singularize(word: string): string {\n const lower = word.toLowerCase();\n\n // Check irregular forms\n if (IRREGULAR_SINGULARS[lower]) {\n // Preserve the original casing of the first character\n const singular = IRREGULAR_SINGULARS[lower];\n return word[0] === word[0].toUpperCase()\n ? singular.charAt(0).toUpperCase() + singular.slice(1)\n : singular;\n }\n\n // Check uncountable\n if (UNCOUNTABLE.has(lower)) return word;\n\n // Latin/Greek -es endings (diagnosis -> diagnosis is uncountable, but \"addresses\" -> \"address\")\n if (lower.endsWith(\"ices\") && lower.length > 5) {\n // e.g. \"indices\" -> \"index\", \"vertices\" -> \"vertex\"\n return word.slice(0, -4) + \"ex\";\n }\n if (lower.endsWith(\"ies\") && lower.length > 3) {\n return word.slice(0, -3) + \"y\";\n }\n if (VES_SINGULAR_ENDINGS[lower]) {\n // e.g. \"wolves\" -> \"wolf\", \"knives\" -> \"knife\"\n return word.slice(0, -3) + VES_SINGULAR_ENDINGS[lower];\n }\n if (lower.endsWith(\"ches\") || lower.endsWith(\"shes\") || lower.endsWith(\"sses\") || lower.endsWith(\"xes\") || lower.endsWith(\"zes\")) {\n return word.slice(0, -2);\n }\n if (lower.endsWith(\"ses\") && !lower.endsWith(\"sses\")) {\n // e.g. \"responses\" -> \"response\", \"databases\" -> \"database\"\n return word.slice(0, -1);\n }\n if (lower.endsWith(\"s\") && !lower.endsWith(\"ss\") && !lower.endsWith(\"us\") && !lower.endsWith(\"is\")) {\n return word.slice(0, -1);\n }\n\n return word;\n}\n\n/**\n * Convert a snake_case table name to a camelCase + \"Collection\" variable name.\n * e.g. \"company_token\" -> \"companyTokenCollection\"\n */\nexport function toCollectionVarName(tableName: string): string {\n const camel = tableName.replace(/_([a-z])/g, (_g, letter: string) => letter.toUpperCase()) + \"Collection\";\n // Only reshapes names that are not identifiers already, so every table that\n // generated a working file keeps the exact variable name it had. A table\n // called `2024 archive` used to emit `const 2024 archiveCollection`, which\n // is three syntax errors rather than a declaration.\n if (JS_IDENTIFIER.test(camel)) return camel;\n const sanitized = camel.replace(/[^A-Za-z0-9_$]/g, \"_\");\n return /^[0-9]/.test(sanitized) ? `_${sanitized}` : sanitized;\n}\n\nexport function getIconForTable(tableName: string): string {\n const table = tableName.toLowerCase();\n if (table.includes(\"user\") || table.includes(\"account\") || table.includes(\"member\") || table.includes(\"customer\") || table.includes(\"client\") || table.includes(\"patient\")) return \"Users\";\n if (table.includes(\"post\") || table.includes(\"article\") || table.includes(\"blog\") || table.includes(\"page\")) return \"FileText\";\n if (table.includes(\"product\") || table.includes(\"item\")) return \"Package\";\n if (table.includes(\"order\") || table.includes(\"cart\") || table.includes(\"purchase\") || table.includes(\"invoice\")) return \"ShoppingCart\";\n if (table.includes(\"setting\") || table.includes(\"config\")) return \"Settings\";\n if (table.includes(\"tag\") || table.includes(\"categor\")) return \"Tag\";\n if (table.includes(\"image\") || table.includes(\"photo\") || table.includes(\"media\") || table.includes(\"asset\")) return \"Image\";\n if (table.includes(\"notification\") || table.includes(\"message\") || table.includes(\"email\")) return \"Mail\";\n if (table.includes(\"log\") || table.includes(\"audit\") || table.includes(\"event\")) return \"Activity\";\n if (table.includes(\"subscription\") || table.includes(\"plan\") || table.includes(\"billing\")) return \"CreditCard\";\n if (table.includes(\"comment\") || table.includes(\"review\") || table.includes(\"feedback\")) return \"MessageCircle\";\n return \"Database\";\n}\n\nexport { mapPgType };\n\n// ── Build the enum map from query results ─────────────────────────────\n\nexport function buildEnumMap(enumValues: EnumValue[]): Map<string, string[]> {\n const enumMap = new Map<string, string[]>();\n for (const ev of enumValues) {\n const existing = enumMap.get(ev.enum_name);\n if (existing) {\n existing.push(ev.enum_value);\n } else {\n enumMap.set(ev.enum_name, [ev.enum_value]);\n }\n }\n return enumMap;\n}\n\n// ── Build the tables map from raw query results ───────────────────────\n\nexport function buildTablesMap(\n tables: TableRow[],\n columns: TableColumn[],\n pks: PrimaryKeyRow[],\n fks: ForeignKeyRow[]\n): Map<string, TableMeta> {\n const tablesMap = new Map<string, TableMeta>();\n for (const t of tables) {\n tablesMap.set(t.table_name, {\n name: t.table_name,\n columns: columns.filter((c) => c.table_name === t.table_name),\n pks: pks.filter((pk) => pk.table_name === t.table_name).map((pk) => pk.column_name),\n fks: fks.filter((fk) => fk.table_name === t.table_name)\n });\n }\n return tablesMap;\n}\n\n// ── Identify join tables ──────────────────────────────────────────────\n\n/**\n * Join tables, identified by column name.\n *\n * Superseded for the CLI by `classifyTables` in `./introspect-db-structure`,\n * which asks the database instead: two single-column keys, unique together, no\n * payload column, nothing referencing the table. This rule folds away\n * `northwind.order_details` — which has the key shape and carries unit price,\n * quantity and discount — because it recognises `id`, `created_at` and\n * `updated_at` by name and calls everything else a foreign key.\n *\n * Still used by `./introspect-runtime`, which builds collections in memory from\n * a narrower set of catalog queries and has no unique-constraint or row-count\n * data to reason with.\n */\nexport function identifyJoinTables(tablesMap: Map<string, TableMeta>): Set<string> {\n const joinTables = new Set<string>();\n for (const [tableName, meta] of tablesMap.entries()) {\n if (meta.fks.length === 2) {\n const isLikelyJoinTable = meta.columns.every((c) =>\n meta.fks.some((fk) => fk.column_name === c.column_name) ||\n c.column_name === \"id\" ||\n c.column_name === \"created_at\" ||\n c.column_name === \"updated_at\"\n );\n\n if (isLikelyJoinTable) {\n joinTables.add(tableName);\n }\n }\n }\n return joinTables;\n}\n\n// ── Property ordering heuristics ──────────────────────────────────────\n\n/**\n * Property metadata used to compute display priority.\n * Keeps computePropertyPriority free of any TableMeta coupling.\n */\nexport interface PropertyOrderingContext {\n /** The resolved Rebase property type (e.g. \"string\", \"number\", \"date\", \"relation\"). */\n propType: string;\n /** Whether this column is a primary key. */\n isPk: boolean;\n /** Whether this column is an enum (USER-DEFINED with matching values). */\n isEnum: boolean;\n /** Whether this is a storage/file-upload field (detected from column name). */\n isStorage: boolean;\n /** The PostgreSQL data_type (e.g. \"text\", \"character varying\", \"jsonb\"). */\n pgDataType: string;\n /** The original column index in PostgreSQL (for stable tiebreaking). */\n originalIndex: number;\n}\n\n// — Tier 0: Identity (0–9) ————————————————————————————————————————————\nconst IDENTITY_EXACT: Record<string, number> = {\n id: 0,\n uuid: 1,\n _id: 2\n};\n\n// — Tier 1: Title / Name — the \"display column\" (10–19) ———————————————\nconst TITLE_EXACT: Record<string, number> = {\n name: 10,\n title: 11,\n label: 12,\n display_name: 13,\n displayname: 13,\n headline: 14,\n subject: 15,\n heading: 16\n};\n\n// — Tier 2: Human identity fields (20–29) —————————————————————————————\nconst HUMAN_IDENTITY_EXACT: Record<string, number> = {\n first_name: 20,\n firstname: 20,\n last_name: 21,\n lastname: 21,\n full_name: 22,\n fullname: 22,\n given_name: 22,\n family_name: 23,\n middle_name: 24,\n username: 25,\n user_name: 25,\n email: 26,\n email_address: 26,\n phone: 27,\n phone_number: 27,\n mobile: 27\n};\n\n// — Tier 3: Core descriptors (30–39) ——————————————————————————————————\nconst DESCRIPTOR_EXACT: Record<string, number> = {\n slug: 30,\n code: 31,\n sku: 32,\n reference: 33,\n ref: 33,\n type: 34,\n kind: 34,\n status: 35,\n state: 35,\n role: 36,\n category: 37,\n group: 38,\n priority: 39,\n order: 39,\n sort_order: 39,\n position: 39\n};\n\n// — Tier 12: System timestamps (120–129) ——————————————————————————————\nconst SYSTEM_TIMESTAMP_EXACT: Record<string, number> = {\n created_at: 120,\n createdat: 120,\n creation_date: 120,\n inserted_at: 121,\n updated_at: 122,\n updatedat: 122,\n modified_at: 122,\n last_modified: 122,\n deleted_at: 123,\n deletedat: 123,\n archived_at: 124\n};\n\n// — Pattern-based rules for partial matches ———————————————————————————\nconst TITLE_PATTERNS = [\"name\", \"title\", \"label\"];\nconst LONG_TEXT_NAMES = new Set([\"description\", \"summary\", \"excerpt\", \"abstract\", \"overview\", \"bio\", \"biography\", \"about\"]);\nconst RICH_CONTENT_NAMES = new Set([\"content\", \"body\", \"html\", \"markup\", \"text\", \"article_body\", \"post_body\"]);\nconst MEDIA_PATTERNS = [\"image\", \"avatar\", \"photo\", \"logo\", \"cover\", \"thumbnail\", \"banner\", \"icon\", \"picture\", \"poster\"];\nconst JSON_MAP_NAMES = new Set([\"metadata\", \"meta\", \"config\", \"configuration\", \"settings\", \"options\", \"preferences\", \"data\", \"payload\", \"attributes\", \"extra\", \"additional_info\"]);\n\n/**\n * Compute a numeric priority score for a property.\n * Lower scores appear first in the generated `propertiesOrder` array.\n *\n * The system uses 14 tiers (0–139), with the original column index\n * added as a fractional tiebreaker (originalIndex / 10000) to\n * guarantee stable ordering within the same tier.\n *\n * Pure function — no side effects.\n */\nexport function computePropertyPriority(\n columnName: string,\n ctx: PropertyOrderingContext\n): number {\n // Normalize camelCase/PascalCase to snake_case, then lowercase\n const col = columnName.replace(/([a-z0-9])([A-Z])/g, \"$1_$2\").toLowerCase();\n const tiebreaker = ctx.originalIndex / 10000;\n\n // ── Tier 0: Primary key identity fields\n if (ctx.isPk) {\n const exactScore = IDENTITY_EXACT[col];\n return (exactScore ?? 5) + tiebreaker;\n }\n\n // ── Tier 12: System timestamps (check early to prevent false matches)\n const systemTs = SYSTEM_TIMESTAMP_EXACT[col];\n if (systemTs !== undefined) {\n return systemTs + tiebreaker;\n }\n\n // ── Tier 1: Title / Name exact matches\n const titleExact = TITLE_EXACT[col];\n if (titleExact !== undefined) {\n return titleExact + tiebreaker;\n }\n\n // ── Tier 2: Human identity exact matches\n const humanExact = HUMAN_IDENTITY_EXACT[col];\n if (humanExact !== undefined) {\n return humanExact + tiebreaker;\n }\n\n // ── Tier 3: Core descriptor exact matches\n const descriptorExact = DESCRIPTOR_EXACT[col];\n if (descriptorExact !== undefined) {\n return descriptorExact + tiebreaker;\n }\n\n // ── Tier 1b: Title-like partial matches (e.g. \"product_name\", \"page_title\")\n // Score 17–19 so they rank after exact matches but still in tier 1.\n for (const pattern of TITLE_PATTERNS) {\n if (col.includes(pattern) && col !== pattern) {\n return 17 + tiebreaker;\n }\n }\n\n // ── Tier 9: Media / file upload fields (check before general strings)\n if (ctx.isStorage) {\n return 90 + tiebreaker;\n }\n for (const pattern of MEDIA_PATTERNS) {\n if (col.includes(pattern)) {\n return 91 + tiebreaker;\n }\n }\n if (col.endsWith(\"_url\") || col.endsWith(\"_uri\") || col.endsWith(\"_link\")) {\n return 92 + tiebreaker;\n }\n\n // ── Tier 7: Long text fields\n if (LONG_TEXT_NAMES.has(col)) {\n return 70 + tiebreaker;\n }\n\n // ── Tier 8: Rich content fields\n if (RICH_CONTENT_NAMES.has(col)) {\n return 80 + tiebreaker;\n }\n\n // ── Tier 10: JSON / Map types\n if (ctx.propType === \"map\") {\n return JSON_MAP_NAMES.has(col) ? 100 + tiebreaker : 105 + tiebreaker;\n }\n\n // ── Tier 11: Array types\n if (ctx.propType === \"array\") {\n return 110 + tiebreaker;\n }\n\n // ── Tier 6: Owning relations\n if (ctx.propType === \"relation\") {\n return 60 + tiebreaker;\n }\n\n // ── Tier 4: Short text, enums, booleans — \"quick glance\" fields\n if (ctx.isEnum) {\n return 40 + tiebreaker;\n }\n if (ctx.propType === \"boolean\") {\n return 45 + tiebreaker;\n }\n if (ctx.propType === \"string\" && ctx.pgDataType !== \"text\") {\n // Short string (varchar, char, uuid that's not a PK)\n return 42 + tiebreaker;\n }\n\n // ── Tier 5: Numbers & user-facing dates\n if (ctx.propType === \"number\") {\n return 50 + tiebreaker;\n }\n if (ctx.propType === \"date\") {\n // A date that isn't a system timestamp (already handled above)\n return 55 + tiebreaker;\n }\n\n // ── Tier 7b: text data_type that didn't match long-text names\n if (ctx.propType === \"string\" && ctx.pgDataType === \"text\") {\n return 75 + tiebreaker;\n }\n\n // ── Tier 13: Fallback / unknown\n return 130 + tiebreaker;\n}\n\n/**\n * Sort a `propertiesOrder` array using the priority heuristic.\n * Returns a new sorted array; does not mutate the input.\n *\n * @param entries - Array of { key, columnName, propType, ... } objects\n * carrying the information needed to compute priority.\n */\nexport interface PropertyOrderEntry {\n /** The property key in the generated collection (may differ from columnName for relations). */\n key: string;\n /** The ordering context for this property. */\n ctx: PropertyOrderingContext;\n}\n\nexport function sortPropertiesOrder(entries: PropertyOrderEntry[]): string[] {\n return [...entries]\n .sort((a, b) => computePropertyPriority(a.key, a.ctx) - computePropertyPriority(b.key, b.ctx))\n .map((e) => e.key);\n}\n\n// ── Generate collection file content ──────────────────────────────────\n\nexport interface GeneratedFile {\n tableName: string;\n fileName: string;\n content: string;\n}\n\n/**\n * The structural analysis a run can hand the generator.\n *\n * Optional in full, and the generator degrades to exactly its previous output\n * without it. That is not politeness towards old callers: three existing test\n * suites and the `rebase init` scaffold path build a `TableMeta` by hand and\n * have no database to read constraints or row counts from, and they must keep\n * producing a valid collection.\n */\n/**\n * Which `defineCollection` — if any — the project being generated into can import.\n *\n * A bare `const x: PostgresCollectionConfig = { … }` annotation widens `properties`\n * to `Record<string, …>`, and every key-shaped field in the admin block —\n * `titleProperty`, `sort`, `propertiesOrder`, `listProperties`, `fixedFilter` — is\n * derived from those keys. Annotated, they accept any string: introspection was\n * emitting a `propertiesOrder` array that nothing checked, so renaming a column and\n * re-introspecting left a stale key that compiled silently. `defineCollection` is\n * the identity function whose `const P` type parameter keeps the keys literal, which\n * is what turns that checking on.\n *\n * There are two of them and they are not interchangeable:\n *\n * - `admin-types` — `@rebasepro/admin-types`. Its index side-effect-imports\n * `augment.ts`, so importing it is also what *declares* the `admin` block. Only a\n * project that depends on the package can resolve it.\n * - `common` — `@rebasepro/common`. Same key inference, no admin surface, no React\n * anywhere in its graph (`scripts/headless-guard` lists it as core). This is the\n * headless flavour.\n * - `annotation` — neither package is declared, so neither import would resolve and\n * the old annotation is the only honest thing to emit. Projects scaffolded before\n * `@rebasepro/common` joined the headless config package land here.\n *\n * The last two emit **no admin block, on the collection or on any property**. That is\n * not a downgrade: `@rebasepro/types` declares no `admin` field at all, so the block\n * introspection used to emit was a type error in every headless project it was\n * written into. See `packages/admin-types/src/augment.ts`.\n */\nexport type CollectionBuilder = \"admin-types\" | \"common\" | \"annotation\";\n\n/**\n * The package specifiers the generated files name, spelled once.\n *\n * Written as constants rather than inline in the import templates below because\n * `scripts/headless-guard/check-types.mjs` scans core sources for `from\n * \"@rebasepro/admin-types\"` and cannot tell a real import from one this module\n * *writes*. It is right to be that blunt — the guard's whole value is that it\n * cannot be reasoned around — so the string simply never appears in that shape\n * here. Inlining them back into the templates re-breaks `check:types-headless`.\n */\nexport const ADMIN_TYPES_PACKAGE = \"@rebasepro/admin-types\";\nexport const COMMON_PACKAGE = \"@rebasepro/common\";\nexport const TYPES_PACKAGE = \"@rebasepro/types\";\n\nexport interface GenerationContext {\n metadata?: SchemaMetadata;\n classifications?: Map<string, TableClassification>;\n checkFacts?: CheckFactsByTable;\n /**\n * Defaults to `admin-types`, which is what the generator has always emitted.\n * The CLI never relies on the default — `introspect-db.ts` detects the flavour\n * from the target project and passes it. See `detectCollectionBuilder`.\n */\n builder?: CollectionBuilder;\n}\n\n/** Adds entries to a property's `validation` block, creating it if absent. */\nfunction withValidation(extra: string, entries: string[]): string {\n if (entries.length === 0) return extra;\n const block = entries.map((e) => ` ${e}`).join(\",\\n\");\n if (extra.includes(\"validation: {\")) {\n return extra.replace(\"validation: {\", `validation: {\\n${block},`);\n }\n return `${extra}\\n validation: {\\n${block}\\n },`;\n}\n\n/**\n * Whether a property's generated text already sets `key:` as an object key.\n *\n * A bare `extra.includes(\"min:\")` looks like it answers this and does not:\n * `admin:` ends in `min:`, so every property with an admin block claimed to\n * have a minimum already and silently lost the one the database declared. The\n * leading-delimiter requirement is the whole point — a key is preceded by a\n * newline, a brace or a comma, never by another identifier character.\n */\nfunction hasGeneratedKey(extra: string, key: string): boolean {\n return new RegExp(`(^|[\\\\s{,])${key}\\\\s*:`).test(extra);\n}\n\n/**\n * Adds entries to a property's `admin` block, creating it if absent.\n *\n * `emitAdmin` is false for the headless flavours, where `BaseProperty` has no\n * `admin` field to put them in — see {@link CollectionBuilder}. The options are\n * dropped rather than relocated: every one of them (`readOnly`, `multiline`,\n * `hideFromCollection`, `urlPreview`) describes a form widget, and there is no\n * form.\n */\nfunction withAdminOptions(extra: string, entries: string[], emitAdmin = true): string {\n if (!emitAdmin) return extra;\n if (entries.length === 0) return extra;\n const block = entries.map((e) => ` ${e}`).join(\",\\n\");\n if (extra.includes(\"admin: {\")) {\n return extra.replace(\"admin: {\", `admin: {\\n${block},`);\n }\n return `${extra}\\n admin: {\\n${block}\\n },`;\n}\n\n/** A TypeScript string literal, escaped. */\nfunction quote(value: string): string {\n return JSON.stringify(value);\n}\n\nconst JS_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n\n/**\n * An object key for the generated file: verbatim when the name is a JavaScript\n * identifier, quoted otherwise.\n *\n * Postgres constrains an identifier only by quoting, so `order`, `full name`\n * and `2fa_enabled` are all ordinary column names — and all three produced a\n * file that did not parse when written as a bare key.\n *\n * Still needed now that keys are camel-cased rather than copied from the\n * column: `toWireKey` splits on separators and joins, which fixes `full name`\n * but cannot fix a name that is not an identifier for some other reason —\n * `2fa_enabled` becomes `2faEnabled`, still leading with a digit, and `order`\n * was never a separator problem at all.\n */\nfunction propKey(name: string): string {\n return JS_IDENTIFIER.test(name) ? name : quote(name);\n}\n\n/**\n * Text safe to put after `//`.\n *\n * A table comment or a classification reason is free text out of the database.\n * A newline in one ended the comment and let the rest of the value continue as\n * code.\n */\nfunction commentText(value: string): string {\n return value.replace(/\\s*[\\r\\n]+\\s*/g, \" \");\n}\n\n/** A property-key array, one key per line, indented for the admin block. */\nfunction formatKeyList(keys: string[]): string {\n if (keys.length === 0) return \"[]\";\n return `[\\n${keys.map((k) => ` ${quote(k)}`).join(\",\\n\")}\\n ]`;\n}\n\n/**\n * Generate the full TypeScript file content for a single collection.\n * Pure function — no I/O.\n */\nexport function generateCollectionFile(\n tableName: string,\n meta: TableMeta,\n allFks: ForeignKeyRow[],\n joinTables: Set<string>,\n tablesMap: Map<string, TableMeta>,\n enumMap: Map<string, string[]>,\n sampleData?: Record<string, unknown>[],\n context: GenerationContext = {}\n): string {\n const collectionName = humanize(tableName);\n const singular = singularize(collectionName);\n const icon = getIconForTable(tableName);\n\n const classification = context.classifications?.get(tableName);\n const checkFacts: CheckFactsByTable = context.checkFacts ?? new Map();\n const tableChecks = checkFacts.get(tableName);\n const columnComments = new Map<string, string>();\n let tableComment: string | undefined;\n for (const comment of context.metadata?.comments ?? []) {\n if (comment.table_name !== tableName) continue;\n if (comment.column_name === null) tableComment = comment.comment;\n else columnComments.set(comment.column_name, comment.comment);\n }\n const singleColumnUniques = new Set(\n (context.metadata?.uniques ?? [])\n .filter((u) => u.table_name === tableName && u.column_names.length === 1)\n .map((u) => u.column_names[0])\n );\n\n const builder: CollectionBuilder = context.builder ?? \"admin-types\";\n /** Whether the target project has an `admin` field to write into at all. */\n const emitAdmin = builder === \"admin-types\";\n\n const BUILDER_IMPORT: Record<CollectionBuilder, string> = {\n \"admin-types\": `import { defineCollection } from ${quote(ADMIN_TYPES_PACKAGE)};`,\n common: `import { defineCollection } from ${quote(COMMON_PACKAGE)};`,\n annotation: `import { PostgresCollectionConfig } from ${quote(TYPES_PACKAGE)};`\n };\n const imports = new Set<string>([BUILDER_IMPORT[builder]]);\n\n /**\n * Imports the collection a relation points at — unless it is this one.\n *\n * A self-referencing key (`employees.reports_to -> employees`, which both\n * northwind and chinook have) otherwise made the file import its own default\n * export under the name it declares three lines later: `TS2440: Import\n * declaration conflicts with local declaration`. The relation target is a\n * thunk, so referring to the local const directly is fine — it is only\n * dereferenced after the module has finished evaluating.\n */\n const importCollection = (otherTable: string): string => {\n const varName = toCollectionVarName(otherTable);\n if (otherTable !== tableName) imports.add(`import ${varName} from ${quote(`./${otherTable}`)};`);\n return varName;\n };\n\n /**\n * A relation's `target` thunk, with its return type spelled out.\n *\n * The annotation is what makes `defineCollection` survive a relational schema.\n * Without an explicit type on the const, the collection's type is *inferred*,\n * and a relation cycle — `posts` belongs to `authors`, `authors` has many\n * `posts`; or `employees.reports_to -> employees`, which northwind, chinook and\n * musicbrainz all have — makes that inference circular: `TS7022: implicitly has\n * type 'any' because it is referenced directly or indirectly in its own\n * initializer`, plus `TS7023` on the thunk and `TS2303` on the import alias.\n * Naming the return type lets the checker type the thunk without resolving the\n * collection it points at, which breaks the cycle. Nothing else is given up:\n * the inference that matters runs over `properties`, not over `relations`.\n *\n * The annotated flavour has no cycle to break — its const is already typed — so\n * it keeps the plainer thunk it has always emitted.\n */\n const relationTarget = (targetVarName: string): string => {\n if (builder === \"annotation\") return `() => ${targetVarName}`;\n imports.add(`import type { AnyCollectionConfig } from ${quote(TYPES_PACKAGE)};`);\n return `(): AnyCollectionConfig => ${targetVarName}`;\n };\n\n let propsOutput = \"\";\n let relationsOutput = \"\";\n const orderEntries: PropertyOrderEntry[] = [];\n const propertyBlocks = new Map<string, string>();\n /**\n * Column → the property key it was generated under.\n *\n * Needed because the structural helpers below (`deriveTitleProperty`,\n * `deriveKanbanProperty`, `deriveSort`) answer in *columns* — they read\n * `pg_attribute` — while `display.title`, `kanban.columnProperty` and\n * `sort` name **properties**. The two used to be the same string, so\n * nothing carried the translation; now `full_name` is generated as\n * `fullName` and a title pointing at `full_name` points at nothing.\n */\n const keyByColumn = new Map<string, string>();\n /** Properties the list view will not render, so `listProperties` skips them. */\n const hiddenFromCollection = new Set<string>();\n let columnIndex = 0;\n\n // Detect composite primary keys\n const isCompositePk = meta.pks.length > 1;\n\n // Map columns\n for (const col of meta.columns) {\n // Skip foreign keys since we handle them as relations\n // Exception: Do not skip if it's part of the primary key!\n if (meta.fks.some((fk) => fk.column_name === col.column_name) && !meta.pks.includes(col.column_name)) continue;\n\n const currentIndex = columnIndex++;\n\n // The key this column is generated under — its *wire* name, which is\n // not its column name. `columnName` below carries the column, so the\n // two never have to agree and the API stops carrying `user_id` next to\n // `displayName`.\n //\n // Camel-casing makes collisions possible where none existed: `user_id`\n // and `userId` are two columns and one key, which is a duplicate key in\n // an object literal — a TypeScript error that stops the whole generated\n // collection compiling. Resolved the same way the foreign-key loop\n // below resolves its own: first free candidate, then a numbered tail,\n // so no column is ever dropped. The raw column name is the second\n // candidate, so the loser of a collision still gets a name that means\n // something. Deterministic for a given database: the columns arrive in\n // ordinal order, so the same schema always yields the same keys.\n const propertyKey = firstFreeKey([toWireKey(col.column_name), col.column_name], propertyBlocks);\n keyByColumn.set(col.column_name, propertyKey);\n\n // Check if this column uses a PostgreSQL enum type\n const colEnumValues = enumMap.get(col.udt_name);\n const isEnumColumn = col.data_type === \"USER-DEFINED\" && colEnumValues !== undefined;\n const isVectorColumn = col.udt_name === \"vector\";\n\n const propType = isEnumColumn ? \"string\" : (isVectorColumn ? \"vector\" : mapPgType(col.data_type));\n let extra = \"\";\n\n const colNameLower = col.column_name.toLowerCase();\n\n // ── Data Inference Engine ────────────────────────────────────────────\n let finalPropType = propType;\n let inferenceExtra = \"\";\n\n if (!isEnumColumn && sampleData && sampleData.length > 0) {\n const values = sampleData.map(r => r[col.column_name]);\n const inferred = inferPropertyFromData(col.column_name, col.data_type, propType, values, meta.pks.includes(col.column_name), emitAdmin);\n if (inferred.propType) finalPropType = inferred.propType;\n if (inferred.extra) inferenceExtra = inferred.extra;\n }\n\n const columnChecks = tableChecks?.get(col.column_name);\n\n // Enum values — generate real enum from the PG enum\n if (isEnumColumn && colEnumValues) {\n const enumEntries = colEnumValues\n .map((v) => `{ id: ${quote(v)}, label: ${quote(humanize(v))} }`)\n .join(\", \");\n extra += `\\n enum: [${enumEntries}],`;\n } else if (columnChecks?.enumValues && !inferenceExtra.includes(\"enum:\") && propType === \"string\") {\n // `CHECK (col IN (…))` is the other way a schema declares a closed\n // set. It is the same statement as a Postgres enum type, made by an\n // author who did not want a type — and until now the form offered a\n // free-text box for it and let the database reject the write.\n const enumEntries = columnChecks.enumValues\n .map((v) => `{ id: ${quote(v)}, label: ${quote(humanize(v))} }`)\n .join(\", \");\n extra += `\\n enum: [${enumEntries}],`;\n }\n\n // Date auto-value heuristics\n if (finalPropType === \"date\") {\n if (colNameLower === \"created_at\" || colNameLower === \"createdat\") {\n extra += \"\\n autoValue: \\\"on_create\\\",\";\n extra = withAdminOptions(extra, [\"readOnly: true\", \"hideFromCollection: true\"], emitAdmin);\n hiddenFromCollection.add(propertyKey);\n } else if (colNameLower === \"updated_at\" || colNameLower === \"updatedat\") {\n extra += \"\\n autoValue: \\\"on_update\\\",\";\n extra = withAdminOptions(extra, [\"readOnly: true\", \"hideFromCollection: true\"], emitAdmin);\n hiddenFromCollection.add(propertyKey);\n } else if (col.column_default && (col.column_default.includes(\"now()\") || col.column_default.includes(\"CURRENT_TIMESTAMP\"))) {\n extra += \"\\n autoValue: \\\"on_create\\\",\";\n extra = withAdminOptions(extra, [\"readOnly: true\"], emitAdmin);\n }\n }\n\n // Array/Map heuristics (Fallback if not inferred)\n if (finalPropType === \"array\" && !inferenceExtra.includes(\"of: {\")) {\n let innerType = \"string\";\n let colType = \"\";\n if (col.udt_name.startsWith(\"_\")) {\n const baseType = col.udt_name.substring(1);\n innerType = mapPgType(baseType);\n if (innerType === \"string\") colType = \"text[]\";\n else if (innerType === \"number\") colType = col.udt_name === \"_numeric\" ? \"numeric[]\" : \"integer[]\";\n else if (innerType === \"boolean\") colType = \"boolean[]\";\n }\n if (colType) {\n extra += `\\n columnType: ${quote(colType)},`;\n }\n extra += `\\n of: { name: ${quote(`${humanize(col.column_name)} Item`)}, type: ${quote(innerType)} },`;\n } else if (finalPropType === \"map\" && !inferenceExtra.includes(\"keyValue: true\") && !inferenceExtra.includes(\"properties: {\")) {\n extra += \"\\n keyValue: true,\";\n }\n\n // String sub-type heuristics (Fallback if not handled by inference or enum)\n if (finalPropType === \"string\" && !isEnumColumn && !inferenceExtra) {\n const isUrl = colNameLower.endsWith(\"_url\") || colNameLower.endsWith(\"_uri\") || colNameLower.endsWith(\"_link\");\n const isMedia = colNameLower.includes(\"image\") || colNameLower.includes(\"avatar\") || colNameLower.includes(\"photo\") || colNameLower.includes(\"logo\") || colNameLower.includes(\"cover\");\n\n if (isMedia) {\n extra += `\\n storage: {\\n storagePath: ${quote(`${tableName}/${col.column_name}`)}\\n },`;\n } else if (isUrl) {\n extra += \"\\n url: true,\";\n } else if (colNameLower === \"description\" || colNameLower === \"summary\" || colNameLower === \"excerpt\") {\n extra = withAdminOptions(extra, [\"multiline: true\"], emitAdmin);\n } else if (colNameLower === \"content\" || colNameLower === \"body\") {\n // Inside `admin`, because that is where both options live. At the\n // top of the property — where these were — the generated file\n // does not compile: `StringProperty` declares neither. Six of\n // OpenStreetMap's tables have a `body` column, which is how this\n // surfaced.\n extra = withAdminOptions(extra, [\"multiline: true\", \"markdown: true\"], emitAdmin);\n } else if (col.data_type === \"text\") {\n extra = withAdminOptions(extra, [\"multiline: true\"], emitAdmin);\n }\n }\n\n // Append inference results\n if (inferenceExtra) {\n extra += inferenceExtra;\n if (!extra.endsWith(\",\")) extra += \",\";\n }\n\n // ── Rules the database already enforces ──────────────────────────────\n // Everything below is read from the catalog, not guessed from the data\n // or the column's name. Each one is a constraint a write would hit\n // anyway; surfacing it means the form says no before the database does.\n const declaredValidation: string[] = [];\n\n // `varchar(n)` — a bound the author wrote down and nothing has read.\n if (finalPropType === \"string\" &&\n typeof col.character_maximum_length === \"number\" &&\n col.character_maximum_length > 0 &&\n !hasGeneratedKey(extra, \"max\")) {\n declaredValidation.push(`max: ${col.character_maximum_length}`);\n }\n\n if (columnChecks) {\n if (finalPropType === \"number\") {\n if (columnChecks.min !== undefined && !hasGeneratedKey(extra, \"min\")) declaredValidation.push(`min: ${columnChecks.min}`);\n if (columnChecks.max !== undefined && !hasGeneratedKey(extra, \"max\")) declaredValidation.push(`max: ${columnChecks.max}`);\n if (columnChecks.moreThan !== undefined) declaredValidation.push(`moreThan: ${columnChecks.moreThan}`);\n if (columnChecks.lessThan !== undefined) declaredValidation.push(`lessThan: ${columnChecks.lessThan}`);\n }\n if (finalPropType === \"string\") {\n if (columnChecks.lengthMin !== undefined && !hasGeneratedKey(extra, \"min\")) declaredValidation.push(`min: ${columnChecks.lengthMin}`);\n if (columnChecks.lengthMax !== undefined && !declaredValidation.some((v) => v.startsWith(\"max:\")) && !hasGeneratedKey(extra, \"max\")) {\n declaredValidation.push(`max: ${columnChecks.lengthMax}`);\n }\n }\n }\n\n // A single-column unique index is the same promise `validation.unique`\n // makes. Composite uniqueness is not: it constrains the combination, and\n // marking either column unique on its own would reject valid rows.\n if (singleColumnUniques.has(col.column_name) && !meta.pks.includes(col.column_name)) {\n declaredValidation.push(\"unique: true\");\n }\n\n extra = withValidation(extra, declaredValidation);\n\n // A generated column rejects every write, and a tsvector holds lexeme\n // positions rather than text, so an editable field for either is a field\n // that can only ever produce an error.\n if (isReadOnlyColumn(col) && !hasGeneratedKey(extra, \"readOnly\")) {\n const options = [\"readOnly: true\"];\n if (isDerivedIndexColumn(col) && !hasGeneratedKey(extra, \"hideFromCollection\")) {\n options.push(\"hideFromCollection: true\");\n hiddenFromCollection.add(propertyKey);\n }\n extra = withAdminOptions(extra, options, emitAdmin);\n }\n\n // `COMMENT ON COLUMN` — documentation the author already wrote, which\n // introspection has never carried across.\n const columnComment = columnComments.get(col.column_name);\n if (columnComment) {\n extra = `\\n description: ${quote(columnComment)},${extra}`;\n }\n\n // Identify IDs (unless already inferred as UUID/CUID by inferenceEngine)\n if (meta.pks.includes(col.column_name)) {\n if (isCompositePk) {\n extra += `\\n // Part of composite primary key (${commentText(meta.pks.join(\", \"))})`;\n } else if (finalPropType === \"number\" && !inferenceExtra.includes(\"isId:\")) {\n extra += \"\\n isId: \\\"increment\\\",\";\n } else if (col.data_type.toLowerCase() === \"uuid\" && !inferenceExtra.includes(\"isId:\")) {\n extra += \"\\n isId: \\\"uuid\\\",\";\n } else if (!inferenceExtra.includes(\"isId:\")) {\n extra += \"\\n isId: \\\"uuid\\\", // Verify if this is a UUID or CUID\";\n }\n }\n\n if (finalPropType === \"vector\") {\n const dims = col.atttypmod && col.atttypmod > 0 ? col.atttypmod : 1536;\n extra += `\\n dimensions: ${dims},`;\n }\n\n // `required` on a column the user cannot write is a form that cannot be\n // submitted: pagila's `film.fulltext` is NOT NULL and maintained by a\n // trigger, so demanding it of the user blocks every create.\n if (col.is_nullable === \"NO\" && !meta.pks.includes(col.column_name) && !col.column_default && !isReadOnlyColumn(col)) {\n if (extra.includes(\"validation: {\")) {\n extra = extra.replace(\"validation: {\", \"validation: {\\n required: true,\");\n } else {\n extra += \"\\n validation: {\\n required: true\\n },\";\n }\n }\n\n const humanName = humanize(col.column_name);\n\n orderEntries.push({\n key: propertyKey,\n ctx: {\n propType: finalPropType,\n isPk: meta.pks.includes(col.column_name),\n isEnum: isEnumColumn,\n isStorage: extra.includes(\"storage: {\") || inferenceExtra.includes(\"storage: {\"),\n pgDataType: col.data_type,\n originalIndex: currentIndex\n }\n });\n\n propertyBlocks.set(propertyKey, `\n ${propKey(propertyKey)}: {\n name: ${quote(humanName)},\n columnName: ${quote(col.column_name)},\n type: ${quote(finalPropType)},${extra}\n },`);\n }\n\n // Map Owning Relations (from this table's FKs to other tables)\n for (const fk of meta.fks) {\n const targetTableName = fk.foreign_table_name;\n if (!joinTables.has(targetTableName)) {\n // The relation gets its own property key, and it must not be one this\n // file has already used — a duplicate key in an object literal is a\n // TypeScript error, so the whole collection stops compiling.\n //\n // The collision needs three things at once and is invisible without\n // all three: a foreign key column that does *not* end in `_id`, that\n // column also being part of the primary key (which is what keeps it\n // as a property of its own rather than folding it into the relation),\n // and the stripped name matching the target table. MusicBrainz names\n // every foreign key after the table it points at — `area_tag (area,\n // tag)` — so 67 of its 339 collections came out with a property\n // declared twice.\n //\n // Camel-cased for the same reason the columns above are: this is a\n // property key, it sits in the same object literal, and a\n // `blog_author` beside a `publishedAt` is the two-conventions\n // defect reproduced inside a single collection.\n const stripped = toWireKey(fk.column_name.replace(/_id$/, \"\"));\n const relName = firstFreeKey(\n [\n stripped,\n toWireKey(targetTableName),\n `${stripped}Relation`\n ],\n propertyBlocks\n );\n // Push the relation property key, not the FK column name\n orderEntries.push({\n key: relName,\n ctx: {\n propType: \"relation\",\n isPk: false,\n isEnum: false,\n isStorage: false,\n pgDataType: \"\",\n originalIndex: columnIndex++\n }\n });\n\n const targetCollectionCamel = importCollection(targetTableName);\n\n const relHumanName = humanize(relName);\n\n propertyBlocks.set(relName, `\n ${propKey(relName)}: {\n name: ${quote(relHumanName)},\n type: \"relation\",\n // mapped from foreign key: ${commentText(fk.column_name)} -> ${commentText(targetTableName)}(${commentText(fk.foreign_column_name)})\n relation: {\n kind: \"belongsTo\",\n target: ${relationTarget(targetCollectionCamel)},\n localKey: ${quote(fk.column_name)}\n }\n },`);\n }\n }\n\n // Map Inverse Relations (1-to-many where OTHER table points to THIS table)\n // These go into the `relations` array so they render as subcollection tabs.\n const inverseFks = allFks.filter((fk) => fk.foreign_table_name === tableName && !joinTables.has(fk.table_name));\n for (const fk of inverseFks) {\n const sourceTableName = fk.table_name;\n\n const targetCollectionCamel = importCollection(sourceTableName);\n\n relationsOutput += `\n {\n kind: \"hasMany\",\n relationName: ${quote(sourceTableName)},\n target: ${relationTarget(targetCollectionCamel)},\n // the ${commentText(sourceTableName)}.${commentText(fk.column_name)} FK points back here\n foreignKeyOnTarget: ${quote(fk.column_name)}\n },`;\n }\n\n // Map Many-to-Many Relations (Join Tables)\n // These also go into the `relations` array so they render as subcollection tabs.\n const relatedJoinTables = Array.from(joinTables).filter((jt) => {\n const jtMeta = tablesMap.get(jt);\n return jtMeta ? jtMeta.fks.some((fk) => fk.foreign_table_name === tableName) : false;\n });\n\n for (const jt of relatedJoinTables) {\n const jtMeta = tablesMap.get(jt);\n if (!jtMeta) continue;\n\n const joinFks = jtMeta.fks;\n\n // Handle self-referencing M2M: both FKs point to the same table\n const selfRefFks = joinFks.filter((fk) => fk.foreign_table_name === tableName);\n if (selfRefFks.length === 2) {\n // Self-referencing M2M — generate a single owning relation\n const thisFk = selfRefFks[0];\n const otherFk = selfRefFks[1];\n\n const relPropName = `${tableName}_via_${otherFk.column_name.replace(/_id$/, \"\")}`;\n\n relationsOutput += `\n {\n kind: \"manyToMany\",\n relationName: ${quote(relPropName)},\n target: ${relationTarget(toCollectionVarName(tableName))},\n through: {\n table: ${quote(jt)},\n sourceColumn: ${quote(thisFk.column_name)},\n targetColumn: ${quote(otherFk.column_name)}\n }\n },`;\n continue;\n }\n\n const otherFk = joinFks.find((fk) => fk.foreign_table_name !== tableName);\n\n if (otherFk) {\n const targetTableName = otherFk.foreign_table_name;\n\n const targetCollectionCamel = importCollection(targetTableName);\n\n // Both sides of a many-to-many are `manyToMany`. There is no owning\n // and inverse side to pick between any more, so this no longer\n // guesses one from table-name ordering and no longer emits a\n // half-configured relation on the losing side with a comment asking\n // the reader to finish it by hand. Introspection already knows both\n // junction columns; each side just names them from its own end.\n const thisFk = joinFks.find((fk) => fk.foreign_table_name === tableName);\n\n const throughCode = thisFk\n ? `\\n through: {\\n table: ${quote(jt)},\\n sourceColumn: ${quote(thisFk.column_name)},\\n targetColumn: ${quote(otherFk.column_name)}\\n }`\n : \"\";\n\n relationsOutput += `\n {\n kind: \"manyToMany\",\n relationName: ${quote(targetTableName)},\n target: ${relationTarget(targetCollectionCamel)},${throughCode}\n },`;\n }\n }\n\n const relationsBlock = relationsOutput\n ? `\\n relations: [${relationsOutput}\\n ],`\n : \"\";\n\n const sortedPropertiesOrder = sortPropertiesOrder(orderEntries);\n for (const key of sortedPropertiesOrder) {\n propsOutput += propertyBlocks.get(key) || \"\";\n }\n\n // ── The admin block ──────────────────────────────────────────────────\n // `icon` and `propertiesOrder` used to be emitted at the *top level* of the\n // config, where they have not belonged since the admin block was split out:\n // `PostgresCollectionConfig` does not declare them, so every generated file\n // was a type error, and the panel — which reads the block — never saw them.\n const adminEntries: string[] = [`icon: ${quote(icon)}`];\n\n if (classification) {\n const derivedFacts = context.metadata\n ? buildColumnFacts(meta, context.metadata, enumMap, checkFacts)\n : undefined;\n\n if (classification.role === \"owned-child\") {\n // The rows are already reachable: every inbound foreign key renders\n // as a tab on the parent. A second, top-level entry for them is what\n // turns a navigation of eight nouns into a list of thirty tables.\n adminEntries.push(\"hideFromNavigation: true\");\n } else if (classification.role === \"lookup\") {\n adminEntries.push('group: \"Reference\"');\n }\n\n if (derivedFacts) {\n // Each of these comes back as a column and is emitted as a property.\n const asProperty = (column: string): string => keyByColumn.get(column) ?? toWireKey(column);\n\n const titleProperty = deriveTitleProperty(derivedFacts);\n if (titleProperty) adminEntries.push(`display: { title: ${quote(asProperty(titleProperty))} }`);\n\n const kanbanProperty = deriveKanbanProperty(derivedFacts);\n if (kanbanProperty) adminEntries.push(`kanban: {\\n columnProperty: ${quote(asProperty(kanbanProperty))}\\n }`);\n\n const sort = deriveSort(derivedFacts);\n if (sort) adminEntries.push(`sort: [${quote(asProperty(sort[0]))}, \"desc\"]`);\n }\n\n const listProperties = deriveListProperties(sortedPropertiesOrder, hiddenFromCollection);\n if (listProperties) {\n adminEntries.push(`listProperties: ${formatKeyList(listProperties)}`);\n }\n }\n\n adminEntries.push(`propertiesOrder: ${formatKeyList(sortedPropertiesOrder)}`);\n\n // Every entry above names a property key, and with `defineCollection` those\n // keys are now checked against `properties` — a stale `propertiesOrder` entry\n // left behind by a renamed column is a compile error rather than a silent\n // no-op. Which is also why the block cannot be emitted where the field is not\n // declared: see {@link CollectionBuilder}.\n const adminBlock = emitAdmin\n ? `\\n admin: {\\n ${adminEntries.join(\",\\n \")}\\n }`\n : \"\";\n\n const descriptionBlock = tableComment\n ? `\\n description: ${quote(tableComment)},`\n : \"\";\n\n // The classification is stated in the file because it is a *decision*, and a\n // decision the reader may disagree with. Naming the evidence tells them\n // which line to delete when they do.\n const classificationNote = classification && classification.role !== \"entity\"\n ? `\\n// Introspected as a ${commentText(classification.role)}: ${commentText(classification.reason)}.\\n`\n : \"\";\n\n const collectionVarName = toCollectionVarName(tableName);\n // `const x = defineCollection({ … })` is also the shape the ts-morph schema\n // editor in `@rebasepro/server` expects — `COLLECTION_FACTORIES` — so an\n // introspected collection is now editable from the panel the way a scaffolded\n // one is.\n const [open, close] = builder === \"annotation\"\n ? [`const ${collectionVarName}: PostgresCollectionConfig = {`, \"};\"]\n : [`const ${collectionVarName} = defineCollection({`, \"});\"];\n // Package imports first, then siblings. `AnyCollectionConfig` is added the\n // moment the first relation needs it — which is after the sibling collections\n // it points at have already been added — and only when a relation needs it, so\n // a project with `noUnusedLocals` never sees an import it does not use.\n const importLines = Array.from(imports);\n const orderedImports = [\n ...importLines.filter((line) => !line.includes('from \"./')),\n ...importLines.filter((line) => line.includes('from \"./'))\n ];\n const fileContent = `${orderedImports.join(\"\\n\")}\n${classificationNote}\n${open}\n name: ${quote(collectionName)},\n singularName: ${quote(singular)},\n slug: ${quote(tableName)},\n table: ${quote(tableName)},${descriptionBlock}\n properties: {${propsOutput}\n },${relationsBlock}${adminBlock}\n${close}\n\nexport default ${collectionVarName};\n`;\n\n return fileContent;\n}\n\n/**\n * Generate the content for an index.ts file that re-exports all collections.\n */\nexport function generateIndexContent(fileNames: string[]): string {\n const sorted = [...fileNames].sort();\n let imports = \"\";\n let arrayElements = \"\";\n for (const f of sorted) {\n const varName = toCollectionVarName(f);\n imports += `import ${varName} from ${quote(`./${f}`)};\\n`;\n arrayElements += ` ${varName},\\n`;\n }\n return `${imports}\\nexport const collections = [\\n${arrayElements}];\\n`;\n}\n\n/**\n * Merge new exports into existing index.ts content.\n * Returns the merged content string.\n */\nexport function mergeIndexContent(existingContent: string, newFileNames: string[]): string {\n const existingImports = new Set(\n [...existingContent.matchAll(/import\\s+([a-zA-Z0-9_]+)\\s+from\\s+\"\\.\\/([^\"]+)\"/g)].map((m) => m[2])\n );\n const sorted = [...newFileNames].sort();\n\n let newImports = \"\";\n let newElements = \"\";\n\n for (const f of sorted) {\n if (!existingImports.has(f)) {\n const varName = toCollectionVarName(f);\n newImports += `import ${varName} from ${quote(`./${f}`)};\\n`;\n newElements += ` ${varName},\\n`;\n }\n }\n\n if (!newImports) return existingContent;\n\n // Simple injection logic:\n // Add new imports below the last import or at the top\n const importRegex = /import\\s+.*?;/g;\n let lastImportMatch;\n let match;\n while ((match = importRegex.exec(existingContent)) !== null) {\n lastImportMatch = match;\n }\n\n let contentWithImports = existingContent;\n if (lastImportMatch) {\n const pos = lastImportMatch.index + lastImportMatch[0].length;\n contentWithImports = existingContent.slice(0, pos) + \"\\n\" + newImports.trimEnd() + existingContent.slice(pos);\n } else {\n contentWithImports = newImports + \"\\n\" + existingContent;\n }\n\n // Inject into the `collections = [...]` array\n const arrayRegex = /export\\s+const\\s+collections\\s*=\\s*\\[([\\s\\S]*?)\\];/;\n return contentWithImports.replace(arrayRegex, (fullMatch, arrayContent) => {\n let mergedArray = arrayContent.trimEnd();\n if (mergedArray && !mergedArray.endsWith(\",\")) mergedArray += \",\";\n if (mergedArray) mergedArray += \"\\n\";\n mergedArray += newElements.trimEnd();\n return `export const collections = [\\n ${mergedArray.trim()}\\n];`;\n });\n}\n\n/**\n * Safely extract the host portion of a database URL for logging.\n */\nexport function safeHostFromUrl(url: string): string {\n return url.includes(\"@\") ? url.split(\"@\")[1] : \"(local connection)\";\n}\n","/**\n * Runtime introspection — builds collections in memory from the live database.\n *\n * This is what makes BaaS mode work with zero configuration: instead of loading\n * collection files from disk, the server reads `information_schema` at boot and\n * derives a collection per table, so any database is served over REST without a\n * single config file.\n *\n * Distinct from `introspect-db.ts`, which runs the same queries but emits\n * TypeScript *source* for a developer to edit and commit (declared collections). The two\n * share the mapping helpers in `introspect-db-logic.ts` so a table is described\n * the same way whether it was generated or introspected.\n */\nimport type { PostgresCollectionConfig } from \"@rebasepro/types\";\n\nimport {\n TableRow,\n TableColumn,\n EnumValue,\n PrimaryKeyRow,\n ForeignKeyRow,\n TableMeta,\n buildTablesMap,\n buildEnumMap,\n identifyJoinTables,\n singularize,\n mapPgType,\n getIconForTable\n} from \"./introspect-db-logic\";\nimport { humanize } from \"./introspect-db-naming\";\nimport { firstFreeKey, toWireKey } from \"@rebasepro/utils\";\n\nexport interface IntrospectedSchema {\n tablesMap: Map<string, TableMeta>;\n enumMap: Map<string, string[]>;\n joinTables: Set<string>;\n}\n\n/** Whether a table carries an authorization model of its own. */\nexport interface TableRlsStatus {\n table: string;\n /** ALTER TABLE … ENABLE ROW LEVEL SECURITY has been run. */\n rlsEnabled: boolean;\n /** Policies attached to it. RLS enabled with none = nothing is visible. */\n policyCount: number;\n}\n\n/**\n * Read the RLS posture of each table in a schema.\n *\n * This is what decides whether baas mode may serve a table. A table with RLS\n * disabled has no authorization model: since every authenticated request runs\n * as `rebase_user`, and that role is granted DML on the schema, serving such a\n * table hands every row to every logged-in user.\n */\nexport async function readRlsStatus(client: Queryable, pgSchema: string): Promise<Map<string, TableRlsStatus>> {\n const { rows } = await client.query<{ table: string; rls_enabled: boolean; policy_count: string | number }>(\n `SELECT c.relname AS table,\n c.relrowsecurity AS rls_enabled,\n (SELECT count(*) FROM pg_policy p WHERE p.polrelid = c.oid) AS policy_count\n FROM pg_class c\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE n.nspname = $1 AND c.relkind = 'r'`,\n [pgSchema]\n );\n\n return new Map(\n rows.map((r) => [\n r.table,\n { table: r.table, rlsEnabled: r.rls_enabled === true, policyCount: Number(r.policy_count ?? 0) }\n ])\n );\n}\n\n/** Minimal query surface — satisfied by pg.Client and pg.Pool alike. */\nexport interface Queryable {\n query<R>(text: string, values?: unknown[]): Promise<{ rows: R[] }>;\n}\n\n/**\n * Read tables, columns, enums, primary keys and foreign keys for a schema.\n * Mirrors the queries in introspect-db.ts.\n */\nexport async function introspectSchema(client: Queryable, pgSchema: string): Promise<IntrospectedSchema> {\n const { rows: tables } = await client.query<TableRow>(\n `SELECT table_name\n FROM information_schema.tables\n WHERE table_schema = $1 AND table_type = 'BASE TABLE'\n AND table_name NOT LIKE 'drizzle_%'\n AND table_name NOT LIKE 'rebase_%'\n ORDER BY table_name`,\n [pgSchema]\n );\n\n const { rows: columns } = await client.query<TableColumn>(\n `SELECT\n c.table_name,\n c.column_name,\n c.data_type,\n c.udt_name,\n c.is_nullable,\n c.column_default,\n (SELECT a.atttypmod FROM pg_attribute a\n JOIN pg_class pc ON a.attrelid = pc.oid\n WHERE pc.relname = c.table_name\n AND a.attname = c.column_name\n AND pc.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = c.table_schema)) as atttypmod\n FROM information_schema.columns c\n WHERE c.table_schema = $1`,\n [pgSchema]\n );\n\n const { rows: enumValues } = await client.query<EnumValue>(\n `SELECT t.typname AS enum_name,\n e.enumlabel AS enum_value,\n e.enumsortorder AS sort_order\n FROM pg_type t\n JOIN pg_enum e ON t.oid = e.enumtypid\n JOIN pg_namespace n ON t.typnamespace = n.oid\n WHERE n.nspname = $1\n ORDER BY t.typname, e.enumsortorder`,\n [pgSchema]\n );\n\n const { rows: pks } = await client.query<PrimaryKeyRow>(\n `SELECT t.relname as table_name, a.attname as column_name\n FROM pg_index i\n JOIN pg_attribute a ON a.attrelid = i.indrelid\n AND a.attnum = ANY(i.indkey)\n JOIN pg_class t ON t.oid = i.indrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n WHERE i.indisprimary AND n.nspname = $1`,\n [pgSchema]\n );\n\n const { rows: fks } = await client.query<ForeignKeyRow>(\n `SELECT\n tc.table_name,\n kcu.column_name,\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\n AND tc.table_schema = kcu.table_schema\n JOIN information_schema.constraint_column_usage AS ccu\n ON ccu.constraint_name = tc.constraint_name\n AND ccu.table_schema = tc.table_schema\n WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = $1`,\n [pgSchema]\n );\n\n const tablesMap = buildTablesMap(tables, columns, pks, fks);\n return {\n tablesMap,\n enumMap: buildEnumMap(enumValues),\n joinTables: identifyJoinTables(tablesMap)\n };\n}\n\n/** Derive the `isId` flavour for a primary-key column. */\nfunction idKindFor(col: TableColumn, propType: string): \"uuid\" | \"increment\" | true {\n if (col.data_type.toLowerCase() === \"uuid\") return \"uuid\";\n if (propType === \"number\") return \"increment\";\n return true;\n}\n\nfunction buildProperties(\n meta: TableMeta,\n enumMap: Map<string, string[]>\n): Record<string, Record<string, unknown>> {\n const properties: Record<string, Record<string, unknown>> = {};\n const takenKeys = new Set<string>();\n\n for (const col of meta.columns) {\n const isPk = meta.pks.includes(col.column_name);\n // Foreign keys surface as relations below, unless they are also part of\n // the primary key, in which case the column itself must stay.\n const isFk = meta.fks.some((fk) => fk.column_name === col.column_name);\n if (isFk && !isPk) continue;\n\n const enumValues = enumMap.get(col.udt_name);\n const isEnum = col.data_type === \"USER-DEFINED\" && enumValues !== undefined;\n const isVector = col.udt_name === \"vector\";\n const propType = isEnum ? \"string\" : isVector ? \"vector\" : mapPgType(col.data_type);\n\n const property: Record<string, unknown> = {\n name: humanize(col.column_name),\n columnName: col.column_name,\n type: propType\n };\n\n // The wire name, with `columnName` above carrying the column. The two\n // are different names for different things — this used to serve the\n // column, so a runtime-introspected collection answered `user_id` while\n // every authored one beside it answered `displayName`.\n //\n // `user_id` and `userId` as two real columns camel-case to one key, so\n // the first free candidate wins and the second falls back to its own\n // column name, then to a numbered tail. Never dropped, and stable for a\n // given database: columns arrive in ordinal order.\n const key = firstFreeKey([toWireKey(col.column_name), col.column_name], takenKeys);\n takenKeys.add(key);\n\n if (isPk) {\n property.isId = idKindFor(col, propType);\n } else if (col.is_nullable === \"NO\" && col.column_default === null) {\n property.validation = { required: true };\n }\n\n if (isEnum && enumValues) {\n property.enum = enumValues.map((value) => ({ id: value, label: humanize(value) }));\n }\n\n properties[key] = property;\n }\n\n return properties;\n}\n\n/**\n * Owning relations, derived from this table's foreign keys — the same shape\n * `generateCollectionFile` writes into a collection file: a `relation` property\n * whose nested descriptor carries `kind`, a `target` thunk and the `localKey`.\n *\n * The shape is load-bearing, not cosmetic. `resolveCollectionRelations` reads\n * relations from `property.relation` and `resolveRelation` requires `target` to\n * be a thunk; this used to emit `target`/`cardinality`/`localKey` flat on the\n * property with the slug as a bare string, which satisfies neither. Nothing\n * threw — the resolver simply skipped every such property and reported that the\n * collection had no relations. So an introspected BaaS collection had its FK\n * columns removed from `properties` (they \"surface as relations\") and then no\n * resolvable relation to surface as, which is why writing the FK column\n * directly came back as `has no field 'product_id'`: `assertKnownWriteFields`\n * learns that column from the resolved relation's `localKey`.\n *\n * The thunk closes over the collections being built in this same pass rather\n * than importing a module, which is what a runtime introspection has instead of\n * generated files. It is called lazily, after the map is fully populated, so a\n * table may reference one introspected later.\n */\nfunction buildRelations(\n meta: TableMeta,\n slugByTable: Map<string, string>,\n collectionBySlug: Map<string, PostgresCollectionConfig>\n): Record<string, Record<string, unknown>> {\n const relations: Record<string, Record<string, unknown>> = {};\n\n for (const fk of meta.fks) {\n const targetSlug = slugByTable.get(fk.foreign_table_name);\n if (!targetSlug) continue;\n\n // Strip the conventional _id suffix: author_id -> author\n let key = toWireKey(fk.column_name.replace(/_id$/, \"\"));\n if (meta.pks.includes(fk.column_name) && key === fk.column_name) {\n // The FK is also the PK and its name doesn't imply a relation (e.g.\n // \"id\"), so naming the relation after the column would collide with\n // the primary-key property.\n key = fk.foreign_table_name;\n }\n\n relations[key] = {\n name: humanize(key),\n type: \"relation\",\n relation: {\n kind: \"belongsTo\",\n target: () => collectionBySlug.get(targetSlug),\n localKey: fk.column_name\n }\n };\n }\n\n return relations;\n}\n\n/**\n * Turn an introspected schema into collections.\n *\n * Join tables are skipped: they carry no identity of their own and exist to\n * express a many-to-many edge between two other tables.\n */\nexport function buildCollectionsFromSchema(\n { tablesMap, enumMap, joinTables }: IntrospectedSchema,\n pgSchema: string\n): PostgresCollectionConfig[] {\n const slugByTable = new Map<string, string>();\n for (const tableName of tablesMap.keys()) {\n if (!joinTables.has(tableName)) slugByTable.set(tableName, tableName);\n }\n\n const collections: PostgresCollectionConfig[] = [];\n // Filled as we go; the relation thunks read it lazily, so a table may point\n // at one that has not been built yet at the moment its relation is created.\n const collectionBySlug = new Map<string, PostgresCollectionConfig>();\n\n for (const [tableName, meta] of tablesMap) {\n if (joinTables.has(tableName)) continue;\n\n const collectionName = humanize(tableName);\n const collection = {\n name: collectionName,\n singularName: singularize(collectionName),\n slug: tableName,\n table: tableName,\n schema: pgSchema,\n icon: getIconForTable(tableName),\n properties: {\n ...buildProperties(meta, enumMap),\n ...buildRelations(meta, slugByTable, collectionBySlug)\n }\n } as unknown as PostgresCollectionConfig;\n\n collections.push(collection);\n collectionBySlug.set(tableName, collection);\n }\n\n return collections;\n}\n\n/** Introspect the database and return ready-to-serve collections. */\nexport async function introspectCollections(\n client: Queryable,\n pgSchema: string\n): Promise<PostgresCollectionConfig[]> {\n const schema = await introspectSchema(client, pgSchema);\n return buildCollectionsFromSchema(schema, pgSchema);\n}\n","/**\n * Build drizzle tables at runtime from an introspected schema.\n *\n * A project with declared collections gets its drizzle tables from a generated `schema.generated.ts` that\n * the developer commits. BaaS mode has no such file — it points at a database\n * and serves it — so the equivalent table objects are constructed here from\n * `information_schema` metadata.\n *\n * These are handed to drizzle as its schema, which keeps the relational query\n * path (`db.query.*`) working; without them FetchService would fall back to\n * plain selects and lose relation loading.\n */\nimport {\n bigint,\n boolean,\n char,\n cidr,\n customType,\n date,\n doublePrecision,\n geometry,\n inet,\n integer,\n interval,\n json,\n jsonb,\n line,\n macaddr,\n macaddr8,\n numeric,\n pgSchema,\n pgTable,\n point,\n primaryKey,\n real,\n smallint,\n text,\n time,\n timestamp,\n uuid,\n varchar,\n vector,\n type PgColumnBuilderBase,\n type PgTable\n} from \"drizzle-orm/pg-core\";\n\nimport { relations, type Relations } from \"drizzle-orm\";\n\nimport type { TableColumn, TableMeta } from \"./introspect-db-logic\";\n\n/** drizzle ships no bytea builder; binary must round-trip as a Buffer. */\nconst bytea = customType<{ data: Buffer; driverData: Buffer }>({\n dataType: () => \"bytea\"\n});\n\n/**\n * Postgres stores a column's type modifier (varchar length, vector dimensions)\n * in `atttypmod`. For length-carrying string types it is length + VARHDRSZ(4);\n * for pgvector it is the dimension count as-is. -1 means unspecified.\n */\nfunction varlenLength(col: TableColumn): number | undefined {\n return col.atttypmod && col.atttypmod > 4 ? col.atttypmod - 4 : undefined;\n}\n\n/**\n * Map a Postgres type to a drizzle column builder, keyed on `udt_name` — the\n * concrete underlying type, which is exact where `data_type` reports umbrella\n * values like \"ARRAY\" or \"USER-DEFINED\".\n *\n * Unknown types fall back to `text`: the driver still reads and writes them,\n * with the value passing through as a string, which beats dropping the column.\n */\nfunction scalarBuilder(udtName: string, name: string, col: TableColumn): PgColumnBuilderBase {\n switch (udtName) {\n case \"uuid\":\n return uuid(name);\n case \"bool\":\n return boolean(name);\n case \"int2\":\n return smallint(name);\n case \"int4\":\n return integer(name);\n case \"int8\":\n return bigint(name, { mode: \"number\" });\n case \"float4\":\n return real(name);\n case \"float8\":\n return doublePrecision(name);\n case \"numeric\":\n case \"money\":\n return numeric(name);\n case \"json\":\n return json(name);\n case \"jsonb\":\n return jsonb(name);\n case \"date\":\n return date(name);\n case \"time\":\n return time(name);\n case \"timetz\":\n return time(name, { withTimezone: true });\n case \"timestamp\":\n return timestamp(name);\n case \"timestamptz\":\n return timestamp(name, { withTimezone: true });\n case \"interval\":\n return interval(name);\n case \"bytea\":\n return bytea(name);\n case \"inet\":\n return inet(name);\n case \"cidr\":\n return cidr(name);\n case \"macaddr\":\n return macaddr(name);\n case \"macaddr8\":\n return macaddr8(name);\n case \"point\":\n return point(name);\n case \"line\":\n return line(name);\n case \"geometry\":\n return geometry(name);\n case \"vector\": {\n // drizzle requires the dimension count; without it fall back to text\n // rather than declaring a vector of unknown width.\n const dimensions = col.atttypmod && col.atttypmod > 0 ? col.atttypmod : undefined;\n return dimensions ? vector(name, { dimensions }) : text(name);\n }\n case \"bpchar\": {\n const length = varlenLength(col);\n return length ? char(name, { length }) : char(name);\n }\n case \"varchar\": {\n const length = varlenLength(col);\n return length ? varchar(name, { length }) : text(name);\n }\n default:\n // Includes text, enums (pg enums are strings on the wire), citext,\n // geography, and anything else this driver hasn't met yet.\n return text(name);\n }\n}\n\nfunction columnBuilderFor(col: TableColumn): PgColumnBuilderBase {\n // Array types are named after their element with a leading underscore\n // (_int4 = int4[]), so the element mapping is reused verbatim.\n if (col.udt_name.startsWith(\"_\")) {\n const element = scalarBuilder(col.udt_name.slice(1), col.column_name, col);\n return (element as unknown as { array(): PgColumnBuilderBase }).array();\n }\n return scalarBuilder(col.udt_name, col.column_name, col);\n}\n\n/**\n * Build one drizzle table per introspected table, keyed by table name.\n */\nexport function buildDrizzleTablesFromSchema(\n tablesMap: Map<string, TableMeta>,\n pgSchemaName = \"public\"\n): Record<string, PgTable> {\n const schema = pgSchemaName === \"public\" ? null : pgSchema(pgSchemaName);\n // The column set is only known at runtime, so drizzle's generic table\n // signature can't be satisfied statically; call it through a loose type.\n const createTable = (schema ? schema.table.bind(schema) : pgTable) as unknown as (\n name: string,\n columns: Record<string, PgColumnBuilderBase>,\n extras?: (self: Record<string, unknown>) => unknown[]\n ) => PgTable;\n\n const tables: Record<string, PgTable> = {};\n\n for (const [tableName, meta] of tablesMap) {\n const columns: Record<string, PgColumnBuilderBase> = {};\n\n for (const col of meta.columns) {\n let builder = columnBuilderFor(col);\n\n if (col.is_nullable === \"NO\") {\n builder = (builder as unknown as { notNull(): PgColumnBuilderBase }).notNull();\n }\n // Single-column primary keys are marked inline; composite keys are\n // declared in the table extras below.\n if (meta.pks.length === 1 && meta.pks[0] === col.column_name) {\n builder = (builder as unknown as { primaryKey(): PgColumnBuilderBase }).primaryKey();\n }\n\n columns[col.column_name] = builder;\n }\n\n const isComposite = meta.pks.length > 1;\n tables[tableName] = createTable(\n tableName,\n columns,\n isComposite\n ? (t) => [primaryKey({ columns: meta.pks.map((pk) => t[pk]) as never })]\n : undefined\n );\n }\n\n return tables;\n}\n\n/**\n * Build drizzle `relations()` for the foreign keys, so the relational query\n * path can actually load them.\n *\n * FetchService asks drizzle for `with: { <key>: true }`, keyed by the relation\n * property on the collection. Tables alone don't satisfy that — without these,\n * `?include=author` silently returns the raw `author_id` and no author.\n *\n * The keys here must match `buildRelations` in introspect-runtime.ts, which is\n * what names the collection's relation properties.\n */\nexport function buildDrizzleRelationsFromSchema(\n tablesMap: Map<string, TableMeta>,\n tables: Record<string, PgTable>\n): Record<string, Relations> {\n /** Owning side, per table: the `one()` relations from its foreign keys. */\n const owning = new Map<string, { key: string; targetTable: string; fkColumn: string; targetColumn: string; relationName: string }[]>();\n /** Inverse side, per referenced table: the matching `many()` back-references. */\n const inverse = new Map<string, { key: string; sourceTable: string; relationName: string }[]>();\n\n for (const [tableName, meta] of tablesMap) {\n if (!tables[tableName]) continue;\n const columnNames = new Set(meta.columns.map((c) => c.column_name));\n\n for (const fk of meta.fks) {\n if (!tables[fk.foreign_table_name]) continue;\n\n // Mirrors buildRelations in introspect-runtime: author_id -> author,\n // falling back to the target table when the column name carries no hint.\n let key = fk.column_name.replace(/_id$/, \"\");\n if (meta.pks.includes(fk.column_name) && key === fk.column_name) {\n key = fk.foreign_table_name;\n }\n // A relation key must not shadow a real column.\n if (columnNames.has(key)) continue;\n\n // Pairs the two sides. Drizzle matches a named one() to the many()\n // carrying the same name, and disambiguates multiple foreign keys\n // into the same table.\n const relationName = `${tableName}_${fk.column_name}`;\n\n owning.set(tableName, [\n ...(owning.get(tableName) ?? []),\n { key, targetTable: fk.foreign_table_name, fkColumn: fk.column_name, targetColumn: fk.foreign_column_name, relationName }\n ]);\n\n const backKey = tableName;\n const targetColumns = new Set((tablesMap.get(fk.foreign_table_name)?.columns ?? []).map((c) => c.column_name));\n if (targetColumns.has(backKey)) continue;\n\n inverse.set(fk.foreign_table_name, [\n ...(inverse.get(fk.foreign_table_name) ?? []),\n { key: backKey, sourceTable: tableName, relationName }\n ]);\n }\n }\n\n const built: Record<string, Relations> = {};\n\n for (const tableName of tablesMap.keys()) {\n const table = tables[tableName];\n const ones = owning.get(tableName) ?? [];\n const manys = inverse.get(tableName) ?? [];\n if (!table || (ones.length === 0 && manys.length === 0)) continue;\n\n built[`${tableName}Relations`] = relations(table, ({ one, many }) => {\n const map: Record<string, unknown> = {};\n\n for (const rel of ones) {\n map[rel.key] = one(tables[rel.targetTable], {\n fields: [(table as unknown as Record<string, never>)[rel.fkColumn]],\n references: [(tables[rel.targetTable] as unknown as Record<string, never>)[rel.targetColumn]],\n relationName: rel.relationName\n });\n }\n\n // Drizzle needs the inverse of every named one(); without it,\n // normalizeRelation throws and the relational path is dead.\n for (const rel of manys) {\n // An inverse must not collide with an owning key on this table.\n if (map[rel.key]) continue;\n map[rel.key] = many(tables[rel.sourceTable], { relationName: rel.relationName });\n }\n\n return map as never;\n });\n }\n\n return built;\n}\n","import chalk from \"chalk\";\nimport { outWarn, outError } from \"./cli-output\";\n\n/**\n * Detect whether an error (or AggregateError wrapping multiple attempts)\n * represents an ECONNREFUSED — i.e. the database is simply not running.\n *\n * Handles:\n * - Direct `{ code: \"ECONNREFUSED\" }` errors from Node `net`\n * - `AggregateError` from dual-stack IPv4+IPv6 connection attempts\n * - Drizzle's `cause`-wrapped pg errors\n */\nexport function isEconnrefused(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as { code?: string; cause?: unknown; errors?: unknown[] };\n if (e.code === \"ECONNREFUSED\") return true;\n // AggregateError from Node net (dual-stack IPv4 + IPv6)\n if (Array.isArray(e.errors)) {\n return e.errors.some(inner =>\n inner && typeof inner === \"object\" && (inner as { code?: string }).code === \"ECONNREFUSED\"\n );\n }\n // Drizzle wraps the pg error in `cause`\n if (e.cause && typeof e.cause === \"object\") {\n return isEconnrefused(e.cause);\n }\n return false;\n}\n\n/**\n * Detect PostgreSQL authentication failures.\n * PG error codes: 28P01 (invalid_password), 28000 (invalid_authorization_specification)\n */\nexport function isAuthFailure(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as { code?: string; cause?: unknown };\n if (e.code === \"28P01\" || e.code === \"28000\") return true;\n if (e.cause && typeof e.cause === \"object\") {\n return isAuthFailure(e.cause);\n }\n // Also check the message for common pg auth failure text\n if (\"message\" in e && typeof (e as { message?: string }).message === \"string\") {\n const msg = (e as { message: string }).message.toLowerCase();\n if (msg.includes(\"password authentication failed\") || msg.includes(\"no pg_hba.conf entry\")) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Detect the \"SSL is not enabled on the server\" failure — the client attempted\n * an SSL handshake against a Postgres server that doesn't support it (common\n * with a plain local dev database). The fix is `?sslmode=disable` on the URL.\n */\nexport function isSslNotEnabled(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as { message?: string; cause?: unknown };\n if (typeof e.message === \"string\" && e.message.toLowerCase().includes(\"ssl is not enabled on the server\")) {\n return true;\n }\n if (e.cause && typeof e.cause === \"object\") {\n return isSslNotEnabled(e.cause);\n }\n return false;\n}\n\n/**\n * Detect PostgreSQL \"cannot drop ... because other objects depend on it\"\n * (error code 2BP01, dependent_objects_still_exist). This is the failure that\n * strands a declarative `db push` half-applied when a collection is removed but\n * an enum type it defined is still referenced by another object.\n */\nexport function isDependencyDropError(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as { code?: string; message?: string; cause?: unknown };\n if (e.code === \"2BP01\") return true;\n if (typeof e.message === \"string\") {\n const msg = e.message.toLowerCase();\n if (msg.includes(\"other objects depend on it\") || msg.includes(\"cannot drop type\")) {\n return true;\n }\n }\n if (e.cause && typeof e.cause === \"object\") {\n return isDependencyDropError(e.cause);\n }\n return false;\n}\n\n/**\n * Parse host:port from a DATABASE_URL for display purposes.\n */\nfunction parseHostInfo(databaseUrl: string): string {\n try {\n const parsed = new URL(databaseUrl);\n return `${parsed.hostname}:${parsed.port || 5432}`;\n } catch {\n return \"unknown\";\n }\n}\n\n/**\n * Format a diagnostic banner for ECONNREFUSED errors.\n */\nfunction formatConnectionRefusedBanner(databaseUrl: string): string {\n const hostInfo = parseHostInfo(databaseUrl);\n return (\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ Cannot connect to PostgreSQL at ${hostInfo}\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n ` The database server is not running or is not accepting\\n` +\n ` connections. Common fixes:\\n` +\n `\\n` +\n ` • docker compose up -d db (the service a Rebase scaffold ships)\\n` +\n ` • brew services start postgresql@18\\n` +\n ` • Verify DATABASE_URL in your .env file\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`\n );\n}\n\n/**\n * Format a diagnostic banner for authentication failures.\n */\nfunction formatAuthFailureBanner(databaseUrl: string): string {\n const hostInfo = parseHostInfo(databaseUrl);\n let username = \"unknown\";\n try {\n username = new URL(databaseUrl).username || \"unknown\";\n } catch { /* ignore */ }\n\n return (\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ Authentication failed for user \"${username}\" at ${hostInfo}\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n ` PostgreSQL rejected the credentials. Common fixes:\\n` +\n `\\n` +\n ` • Check the username and password in DATABASE_URL\\n` +\n ` • Verify the user exists: psql -c \"\\\\du\"\\n` +\n ` • Reset the password: ALTER USER ${username} PASSWORD 'new_password';\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`\n );\n}\n\n/**\n * Format a diagnostic banner for \"SSL is not enabled on the server\".\n */\nfunction formatSslNotEnabledBanner(databaseUrl: string): string {\n const hostInfo = parseHostInfo(databaseUrl);\n const suggestion = databaseUrl.includes(\"?\") ? \"&sslmode=disable\" : \"?sslmode=disable\";\n return (\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ SSL is not enabled on the PostgreSQL server at ${hostInfo}\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n ` The client tried to connect over SSL, but the server does not\\n` +\n ` support it. This is normal for a plain local dev database.\\n` +\n `\\n` +\n ` Fix: append ${chalk.bold(\"sslmode=disable\")} to DATABASE_URL, e.g.\\n` +\n `\\n` +\n ` DATABASE_URL=...${suggestion}\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`\n );\n}\n\n/**\n * Format a diagnostic banner for a dependency-drop failure during `db push`.\n * Explains that the database may be left partially migrated and how to recover.\n */\nfunction formatDependencyDropBanner(): string {\n return (\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ Schema push failed: a type/table could not be dropped\\n` +\n ` because other objects still depend on it.\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n ` ${chalk.yellow(\"The database may now be partially migrated.\")} Atlas applies\\n` +\n ` statements individually, so earlier changes in this push may\\n` +\n ` already be committed while later ones failed.\\n` +\n `\\n` +\n ` This commonly happens when a collection is removed but an enum\\n` +\n ` type it defined is still referenced. To recover:\\n` +\n `\\n` +\n ` 1. Inspect the leftover object named in the error above.\\n` +\n ` 2. Drop it with CASCADE, e.g.:\\n` +\n ` psql \"$DATABASE_URL\" -c 'DROP TYPE \"<name>\" CASCADE;'\\n` +\n ` 3. Re-run: ${chalk.bold.green(\"rebase db push\")}\\n` +\n `\\n` +\n ` Prefer a safe, versioned workflow? Use ${chalk.bold(\"rebase db generate\")}\\n` +\n ` + ${chalk.bold(\"rebase db migrate\")} instead of push for destructive changes.\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`\n );\n}\n\n/**\n * Pre-flight check: verify that the database is reachable before running\n * a heavy subprocess (Atlas, migrations, etc.).\n *\n * Exits with code 1 and a friendly banner on known failure modes.\n * On unknown errors, logs a warning and allows the caller to proceed.\n */\nexport async function checkDatabaseConnectivity(databaseUrl: string): Promise<void> {\n let client: import(\"pg\").Client | undefined;\n try {\n const { Client } = await import(\"pg\");\n client = new Client({\n connectionString: databaseUrl,\n connectionTimeoutMillis: 5000\n });\n await client.connect();\n await client.query(\"SELECT 1\");\n } catch (err: unknown) {\n if (isEconnrefused(err)) {\n outError(formatConnectionRefusedBanner(databaseUrl));\n process.exit(1);\n }\n if (isAuthFailure(err)) {\n outError(formatAuthFailureBanner(databaseUrl));\n process.exit(1);\n }\n if (isSslNotEnabled(err)) {\n outError(formatSslNotEnabledBanner(databaseUrl));\n process.exit(1);\n }\n // Unknown error — warn but don't block; let the downstream tool surface details\n outWarn(chalk.yellow(` ⚠ Could not verify database connectivity: ${err instanceof Error ? err.message : String(err)}`));\n outWarn(chalk.gray(\" Proceeding anyway — the command may fail if the database is unreachable.\"));\n } finally {\n try {\n await client?.end();\n } catch {\n // ignore cleanup errors\n }\n }\n}\n\n/**\n * Post-hoc error diagnosis for direct database operations (e.g. applyPolicies).\n * Returns a formatted diagnostic string if the error matches a known pattern,\n * or null if unrecognized.\n */\nexport function diagnoseDbError(err: unknown, databaseUrl?: string): string | null {\n if (isEconnrefused(err)) {\n return formatConnectionRefusedBanner(databaseUrl || \"\");\n }\n if (isAuthFailure(err)) {\n return formatAuthFailureBanner(databaseUrl || \"\");\n }\n if (isSslNotEnabled(err)) {\n return formatSslNotEnabledBanner(databaseUrl || \"\");\n }\n if (isDependencyDropError(err)) {\n return formatDependencyDropBanner();\n }\n return null;\n}\n","/**\n * PostgresBootstrapper\n *\n * Implements the `BackendBootstrapper` interface for PostgreSQL.\n */\n\nimport { Relations, sql } from \"drizzle-orm\";\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { PgEnum, PgTable } from \"drizzle-orm/pg-core\";\nimport type { RebasePgTable } from \"./types\";\nimport {\n type AuthAdapter,\n BackendBootstrapper,\n BootstrappedAuth,\n DatabaseAdmin,\n type DataDriver,\n CollectionConfig,\n isRelationalCollectionConfig,\n type HistoryConfig,\n InitializedDriver,\n RealtimeProvider,\n type RealtimeChannelsConfig\n} from \"@rebasepro/types\";\nimport { PostgresBackendDriver } from \"./PostgresBackendDriver\";\nimport { RealtimeService } from \"./services/realtimeService\";\nimport { buildCollectionRegistry } from \"./collections/buildRegistry\";\nimport { DatabasePoolManager } from \"./databasePoolManager\";\nimport { PostgresCollectionRegistry } from \"./collections/PostgresCollectionRegistry\";\nimport { createEmailService, type EmailConfig, type EmailService, logger } from \"@rebasepro/server\";\nimport { getTableName as getCollectionTableName } from \"@rebasepro/common\";\nimport { ensureAuthTablesExist } from \"./auth/ensure-tables\";\nimport { probeAuthSchema, resolveAuthSchema } from \"./auth/schema-version\";\nimport { AuthSchemaTables, PostgresAuthRepository, UserService } from \"./auth/services\";\nimport { createAuthSchema } from \"./schema/auth-schema\";\nimport { HistoryService } from \"./history/HistoryService\";\nimport { ensureHistoryTableExists } from \"./history/ensure-history-table\";\nimport { patchPgArrayNullSafety } from \"./utils/pg-array-null-patch\";\nimport { buildCollectionsFromSchema, introspectSchema, readRlsStatus } from \"./schema/introspect-runtime\";\nimport { buildDrizzleTablesFromSchema, buildDrizzleRelationsFromSchema } from \"./schema/dynamic-tables\";\nimport { detectConnectionPosture, ensureAppRole, validatePolicyPgRoles, warnOnAnonymousGrants, warnOnLegacyRlsFunctions, warnOnRoleSchemaCollision, REBASE_USER_ROLE, type RawSqlRunner } from \"./security/rls-enforcement\";\nimport { provisionTriggerCdc, type CdcTableRef } from \"./services/cdc/trigger-cdc\";\nimport { collectJunctionLinks } from \"./services/cdc/junction-tables\";\nimport { createChannelBus, resolveChannelBusSetting } from \"./services/channel-bus\";\nimport { isChannelBusInstance } from \"@rebasepro/types\";\nimport { configureUnknownFilterFields, type UnknownFilterFieldsMode } from \"./utils/drizzle-conditions\";\n\nexport interface PostgresDriverConfig {\n connectionString?: string;\n adminConnectionString?: string;\n readConnectionString?: string;\n connection?: unknown;\n schema?: {\n tables?: Record<string, unknown>;\n enums?: Record<string, unknown>;\n relations?: Record<string, unknown>;\n };\n /**\n * PostgreSQL schema to read when deriving collections from the database\n * (BaaS mode). Defaults to `public`.\n */\n introspectionSchema?: string;\n /**\n * Realtime options, both opt-in:\n *\n * - `channels` — retention. Without rules no channel keeps any history and\n * broadcast stays fire-and-forget. See {@link ChannelRetentionRule}.\n * - `bus` — the cross-instance transport for channel broadcast and\n * presence. Defaults to in-process only, which is correct for a single\n * instance and wrong for two. See {@link ChannelBusConfig}.\n */\n realtime?: RealtimeChannelsConfig;\n /**\n * What to do with a filter field that resolves to no column at all.\n * Defaults to `\"error\"` — a filter that cannot be compiled would otherwise\n * be dropped, and a dropped condition can only widen the result set.\n * Set to `\"warn\"` to restore the pre-fix behaviour of dropping it silently.\n */\n unknownFilterFields?: UnknownFilterFieldsMode;\n}\n\n/**\n * Opaque internals bag that PostgresBootstrapper stores during `initializeDriver()`\n * and re-uses in subsequent lifecycle hooks.\n */\nexport interface PostgresDriverInternals {\n db: NodePgDatabase<any>;\n readDb?: NodePgDatabase<any>;\n registry: PostgresCollectionRegistry;\n realtimeService: RealtimeService;\n driver: PostgresBackendDriver;\n poolManager?: DatabasePoolManager;\n /**\n * Attach CDC triggers to tables that did not exist when the driver\n * bootstrapped. Only set when database-level capture is actually active.\n *\n * Auth owns its own tables and creates them later in boot, so at driver\n * bootstrap they are legitimately missing and get skipped; without this\n * they would stay uninstrumented until the next restart.\n */\n provisionCdcForTables?: (tables: CdcTableRef[]) => Promise<void>;\n}\n\n// Re-export from shared CLI error utilities\nimport { isEconnrefused } from \"./cli-errors\";\nimport { classifyConnectFailure } from \"./utils/pg-error-utils\";\n\n/**\n * Which table name the boot-time drift check should look for, for one collection.\n *\n * A declared `table` IS the table name, not a hint to be second-guessed. This\n * used to ask the registry whether it had indexed the declared name and fall\n * back to the SLUG when it had not — but \"the registry does not know this table\"\n * is exactly the condition the drift check exists to report, so the fallback\n * fired precisely when it was most harmful.\n *\n * A collection with `slug: \"usage-daily\", table: \"usage_daily\"` was reported as\n * missing table `usage-daily`: a name that does not exist, should never exist,\n * and that nobody can find by looking. Worse, the remediation the caller prints\n * says to run `rebase db push` — which would then CREATE that invented table\n * beside the correct one, the same \"second copy\" hazard the misplaced-schema\n * branch further down exists to prevent. Seen in production, where a correctly\n * migrated database reported drift on every boot.\n *\n * The slug is used only when nothing was declared, which is the config shape\n * where the slug genuinely is the table name.\n *\n * Exported for its own test: the caller needs a live pool and a real database,\n * and this is the part that was wrong.\n */\nexport function resolveDriftCheckName(\n col: CollectionConfig,\n registeredTableNames: string[]\n): string {\n const declaredTable = isRelationalCollectionConfig(col) ? col.table : undefined;\n return declaredTable\n ?? registeredTableNames.find((k) => k === col.slug)\n ?? col.slug;\n}\n\n/**\n * Why the tables this backend serves are not in the database — the part of the\n * drift warning that has to be true rather than merely plausible.\n *\n * The three answers need three different actions, and only the caller knows\n * which one applies. This warning used to assert the first (\"this runtime\n * applies the collection schema at boot unless REBASE_MIGRATE_ON_BOOT=none\")\n * and then point at that variable and at driver-version skew. For an app whose\n * boot path contained no provisioning step at all, every word of that was a\n * dead end: nothing read the variable, and the driver was current. The advice\n * cost an investigation, which is a strictly worse outcome than saying less.\n *\n * Exported for its own test: the surrounding check needs a live pool and a real\n * database, and this is the part that was wrong.\n */\nexport function describeSchemaDriftCause(\n provisioning: { attempted: boolean; reason?: string } | undefined\n): string[] {\n // A caller too old to send the signal gets no claim either way — just where\n // to look. Guessing is what got this wrong the first time.\n if (provisioning === undefined) {\n return [\n \" This runtime could not determine whether a schema-creation step ran\",\n \" before this check (the caller predates that signal).\",\n \" • Look for a \\\"Collection schema:\\\" line above. No such line at all\",\n \" means nothing tried to create these tables in this process.\"\n ];\n }\n if (provisioning.attempted) {\n return [\n \" A schema-creation step DID run this boot and these tables are still\",\n \" missing, so it did not create them — check the \\\"schema:\\\" lines above\",\n \" for what it did instead, and for DDL errors.\",\n \" • A collection routed to another engine or data source is not\",\n \" created here; that is reported separately at boot.\",\n \" • Otherwise this is a bug worth reporting, with those lines.\"\n ];\n }\n return [\n \" No schema-creation step ran this boot:\",\n ` ${provisioning.reason ?? \"no reason was given.\"}`,\n \" Resolve that reason — the drift is its consequence, not a separate\",\n \" problem, and re-running a migration tool will not change it.\"\n ];\n}\n\n/**\n * Is this the local database `rebase init` scaffolds — i.e. the one case where\n * \"you are connected as a superuser\" is not news?\n *\n * The scaffold's own `docker-compose.yml` sets `POSTGRES_USER: rebase_app`,\n * which makes that role the cluster superuser, so the superuser advisory below\n * was the only WARN a brand-new project ever saw and it was about a decision\n * the tool had made for the developer.\n *\n * Of the two available fixes — provision a non-superuser table-owner role in\n * the scaffold, or recognise the local shape and stay quiet — this is the\n * second, because the first breaks the scaffold it is meant to improve: a\n * non-superuser owner cannot `CREATE EXTENSION` (search collections need\n * `pg_trgm`/`unaccent`, applied by `rebase db push` and again by the boot\n * schema-ensure), so the very first `pnpm run db:push` on a scaffolded project\n * with a search block would fail. Trading a working first run for a quieter log\n * line is the wrong trade.\n *\n * The condition is deliberately narrow — a *non-production* process talking to\n * a database on the loopback interface. A genuine production superuser\n * connection still warns, and so does a non-production process pointed at a\n * remote database (the usual \"my dev machine writes to staging\" mistake, where\n * the advisory is exactly right). NODE_ENV alone would not do: the scaffold\n * ships `NODE_ENV=development` and some deployments inherit it.\n */\nexport function isScaffoldedLocalDatabase(connectionString: string | undefined): boolean {\n if (process.env.NODE_ENV === \"production\") return false;\n if (!connectionString) return false;\n let host: string;\n try {\n host = new URL(connectionString).hostname;\n } catch {\n return false;\n }\n // `new URL` keeps IPv6 literals in brackets.\n const bare = host.replace(/^\\[|\\]$/g, \"\").toLowerCase();\n return bare === \"localhost\"\n || bare === \"::1\"\n || bare === \"0.0.0.0\"\n || bare === \"\"\n || /^127\\./.test(bare)\n || bare.endsWith(\".localhost\");\n}\n\n/**\n * Default PostgreSQL bootstrapper.\n *\n * Use it to register Postgres with `initializeRebaseBackend()`:\n * ```typescript\n * initializeRebaseBackend({\n * ...config,\n * bootstrappers: [postgresBootstrapper()]\n * });\n * ```\n */\n/**\n * Where the collections schema stamp lives.\n *\n * The runtime's own internal schema, always — unlike the auth stamp, which\n * follows the users collection. `rebase` and `auth` sit outside\n * `introspectionSchema` by construction, so nothing here is ever served as a\n * collection.\n */\nconst SCHEMA_META_SCHEMA = \"rebase\";\n\nexport function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): BackendBootstrapper {\n // Applied at construction rather than threaded through every read: the\n // condition builder's static methods are reached from call sites that\n // carry no config. See `UnknownFilterFieldsMode`.\n if (pgConfig.unknownFilterFields) {\n configureUnknownFilterFields(pgConfig.unknownFilterFields);\n }\n\n /**\n * The handle the schema/policy hooks issue their DDL through.\n *\n * Both hooks run BEFORE `initializeDriver`, so `driverResult` is a stand-in\n * the caller may not have: the bundle path can synthesize one from the\n * connection its coordinator opened, but an application that built this\n * adapter itself never handed the framework a connection — it handed it to\n * *us*, as `pgConfig.connection`. Falling back to that is what lets a\n * self-built adapter provision at all; requiring the argument is what left\n * those apps with no tables and a 500 on every data route.\n *\n * Either handle is equivalent here. The driver's `schemaAwareDb` differs\n * only by the drizzle schema object registered on it — relevant to the query\n * builder, not to `execute(sql.raw(...))` — and every statement these hooks\n * emit is schema-qualified DDL, so neither depends on `search_path`.\n */\n /**\n * The drizzle handle itself, for the statements that want parameters.\n *\n * `provisioningQueryable` below hands back a `query(text)` shim because the\n * DDL it serves is built as text. The schema stamp writes a *value*, so it\n * wants the parameterised form — building that string by hand would be one\n * more place a quoted literal has to be got right for no benefit.\n */\n const provisioningDb = (driverResult?: InitializedDriver) => {\n const internals = driverResult?.internals as PostgresDriverInternals | undefined;\n return internals?.db ?? (pgConfig.connection as PostgresDriverInternals[\"db\"] | undefined);\n };\n\n const provisioningQueryable = (driverResult?: InitializedDriver) => {\n const internals = driverResult?.internals as PostgresDriverInternals | undefined;\n const db = internals?.db ?? (pgConfig.connection as PostgresDriverInternals[\"db\"] | undefined);\n if (!db) {\n throw new Error(\n \"Cannot provision the collection schema: this Postgres adapter was created without a \" +\n \"`connection`, and no initialized driver was supplied to fall back on. Pass `connection` \" +\n \"to `createPostgresAdapter` (see `createPostgresDatabaseConnection`).\"\n );\n }\n return {\n async query<T>(text: string): Promise<{ rows: T[] }> {\n const result = await db.execute(sql.raw(text));\n const rows = (result as unknown as { rows?: T[] }).rows;\n return { rows: rows ?? (Array.isArray(result) ? (result as T[]) : []) };\n }\n };\n };\n\n return {\n type: \"postgres\",\n\n async initializeDriver(config: unknown): Promise<InitializedDriver> {\n // config is passed from coordinator, we merge it with our internal pgConfig if needed\n // Currently config from init.ts is `{ collections, collectionRegistry, mode }`\n const { collections, collectionRegistry, introspectCollections, baas, schemaProvisioning, realtime } = config as {\n collections?: CollectionConfig[];\n collectionRegistry?: unknown;\n introspectCollections?: boolean;\n baas?: { unprotectedTables?: \"exclude\" | \"serve\" };\n schemaProvisioning?: { attempted: boolean; reason?: string };\n realtime?: { subscribe: boolean; provision: boolean };\n };\n // Absent means a caller that predates the field, and every one of\n // those is a single process that both serves websockets and owns the\n // schema. Defaulting to false here would silently disable realtime\n // for them — the failure this whole area is prone to.\n const realtimeSubscribes = realtime?.subscribe ?? true;\n const realtimeProvisions = realtime?.provision ?? true;\n // Secure by default: a table with no RLS is not served.\n const unprotectedTables = baas?.unprotectedTables ?? \"exclude\";\n\n const connection = pgConfig.connection;\n const rawClient = (connection && typeof connection === \"object\" && \"$client\" in connection\n ? (connection as Record<string, unknown>).$client\n : connection) as import(\"pg\").Pool;\n\n // ── No declared collections: derive the schema from the database ──\n // No collection files and no generated drizzle schema exist, so read\n // the live database and build both from what is actually there.\n let introspectedCollections: CollectionConfig[] | undefined;\n let introspectedTables: Record<string, PgTable> | undefined;\n let introspectedRelations: Record<string, Relations> | undefined;\n if (introspectCollections && (!collections || collections.length === 0)) {\n const pgSchemaName = pgConfig.introspectionSchema ?? \"public\";\n const schema = await introspectSchema(rawClient, pgSchemaName);\n\n // ── Only serve what the database protects ────────────────\n // Requests run as rebase_user, which is granted DML on the\n // schema. A table with RLS disabled therefore has no\n // authorization model at all: serving it hands every row to\n // every authenticated user. baas never runs `db push`, so\n // nothing here would have enabled RLS on the user's behalf.\n const rlsStatus = await readRlsStatus(rawClient, pgSchemaName);\n const unprotected = [...schema.tablesMap.keys()].filter(\n (t) => !schema.joinTables.has(t) && !rlsStatus.get(t)?.rlsEnabled\n );\n const policyless = [...schema.tablesMap.keys()].filter(\n (t) => !schema.joinTables.has(t) && rlsStatus.get(t)?.rlsEnabled && rlsStatus.get(t)?.policyCount === 0\n );\n\n if (unprotected.length > 0) {\n if (unprotectedTables === \"serve\") {\n logger.warn(\n `🔓 [rls] Serving ${unprotected.length} table(s) with row-level security DISABLED: ${unprotected.join(\", \")}. ` +\n \"Every authenticated request can read and write every row of these. \" +\n \"This is baas.unprotectedTables: \\\"serve\\\".\"\n );\n } else {\n logger.warn(\n `🔒 [rls] Not serving ${unprotected.length} table(s) — row-level security is disabled, so they have no ` +\n `authorization model: ${unprotected.join(\", \")}\\n` +\n unprotected.map((t) => ` ALTER TABLE \"${pgSchemaName}\".\"${t}\" ENABLE ROW LEVEL SECURITY; -- then add a policy`).join(\"\\n\") +\n \"\\n Set baas.unprotectedTables: \\\"serve\\\" to expose them regardless.\"\n );\n for (const t of unprotected) schema.tablesMap.delete(t);\n }\n }\n if (policyless.length > 0) {\n // Legal, and silently returns nothing — worth saying out loud,\n // since an empty table reads exactly like one with no rows.\n logger.warn(\n `🔒 [rls] ${policyless.length} table(s) have RLS enabled but no policies, so they will return no rows: ${policyless.join(\", \")}`\n );\n }\n\n introspectedCollections = buildCollectionsFromSchema(schema, pgSchemaName);\n introspectedTables = buildDrizzleTablesFromSchema(schema.tablesMap, pgSchemaName);\n // Without these, drizzle's relational path can't resolve the\n // relations the collections above advertise.\n introspectedRelations = buildDrizzleRelationsFromSchema(schema.tablesMap, introspectedTables);\n logger.info(\n `🔍 [PostgresRegistry] BaaS mode: derived ${introspectedCollections.length} collections from schema \"${pgSchemaName}\" [${introspectedCollections.map(c => c.slug).join(\", \")}]`\n );\n }\n\n const activeCollections = introspectedCollections ?? collections;\n const schemaTables = introspectedTables ?? pgConfig.schema?.tables;\n const schemaRelations = introspectedRelations ?? (pgConfig.schema?.relations as Record<string, Relations> | undefined);\n\n // Create a fresh registry for this driver. Registration order is\n // load-bearing, so it lives in one place — see `buildCollectionRegistry`.\n const registry = buildCollectionRegistry({\n collections: activeCollections,\n tables: schemaTables,\n enums: pgConfig.schema?.enums as Record<string, PgEnum<[string, ...string[]]>> | undefined,\n relations: schemaRelations\n });\n\n // Patch Drizzle's PgArray columns to handle NULL values safely.\n // Drizzle's mapFromDriverValue crashes with \"value.map is not a function\"\n // when a native array column (text[], integer[], etc.) contains NULL.\n if (schemaTables) {\n patchPgArrayNullSafety(schemaTables as Record<string, unknown>);\n }\n\n // Build schema-aware Drizzle connection\n const mergedSchema: Record<string, unknown> = {\n ...schemaTables,\n ...(schemaRelations || {})\n };\n const { drizzle: createDrizzle } = await import(\"drizzle-orm/node-postgres\");\n const schemaAwareDb = createDrizzle(rawClient, { schema: mergedSchema });\n\n // Verify connection — fail fast if the database is unreachable\n try {\n await schemaAwareDb.execute(sql`SELECT 1`);\n } catch (err: unknown) {\n const isConnectionRefused = isEconnrefused(err);\n if (isConnectionRefused) {\n // Parse host/port from connection string for a helpful message\n let hostInfo = pgConfig.connectionString || \"unknown\";\n try {\n const parsed = new URL(pgConfig.connectionString || \"\");\n hostInfo = `${parsed.hostname}:${parsed.port || 5432}`;\n } catch { /* use raw string */ }\n\n const message =\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ Cannot connect to PostgreSQL at ${hostInfo}\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n ` The database server is not running or is not accepting\\n` +\n ` connections. Common fixes:\\n` +\n `\\n` +\n ` • docker compose up -d db (the service a Rebase scaffold ships)\\n` +\n ` • brew services start postgresql@18\\n` +\n ` • Verify DATABASE_URL in your .env file\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`;\n logger.error(message);\n throw new Error(`Cannot connect to PostgreSQL at ${hostInfo}: connection refused. Is the database running?`);\n }\n\n /*\n * Everything else.\n *\n * Two problems with what used to happen here. First, the logged\n * error was Drizzle's wrapper — `Failed query: SELECT 1` and a\n * stack through drizzle internals — while the sentence that\n * actually says what is wrong (\"password authentication failed\n * for user …\", \"database … does not exist\") sits in `.cause`\n * and was never printed. A developer with a typo in their\n * DATABASE_URL got two walls of stack trace and no cause.\n *\n * Second, \"continuing… the pool may recover\" is only true for\n * a transient fault. A wrong password or a missing database is\n * settled: the next query fails the same way, so the process\n * died seconds later anyway — after printing a reassurance.\n * Those now fail here, where the message can be about the\n * cause rather than about whichever query ran next.\n */\n const { fatal, reason, code } = classifyConnectFailure(err);\n const detail = code ? ` [${code}]` : \"\";\n\n if (fatal) {\n logger.error(\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ PostgreSQL refused the connection\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n ` ${reason}${detail}\\n` +\n `\\n` +\n ` The server is reachable, so this is the credentials or the\\n` +\n ` database name in DATABASE_URL — check them in your .env.\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`\n );\n throw new Error(`PostgreSQL refused the connection: ${reason}${detail}`);\n }\n\n logger.error(`❌ Failed to connect to PostgreSQL: ${reason}${detail}`, { error: err });\n logger.warn(\"⚠️ Continuing without initial database verification. Drizzle/PG will attempt to connect on subsequent queries.\");\n }\n\n // Create services\n const realtimeService = new RealtimeService(schemaAwareDb, registry);\n\n // Initialize read replica connection if configured\n let readDb: import(\"drizzle-orm/node-postgres\").NodePgDatabase<any> | undefined;\n const readUrl = process.env.DATABASE_READ_URL;\n if (readUrl && readUrl !== pgConfig.connectionString) {\n try {\n const { createReadReplicaConnection } = await import(\"./connection\");\n const readResources = createReadReplicaConnection(readUrl, mergedSchema);\n readDb = readResources.db;\n logger.info(\"📖 [PostgresBootstrapper] Read replica connection established\");\n } catch (err) {\n logger.warn(\"⚠️ Could not connect to read replica, falling back to primary for all queries\", { error: err });\n }\n }\n const poolManager = pgConfig.adminConnectionString\n ? new DatabasePoolManager(pgConfig.adminConnectionString)\n : undefined;\n const driver = new PostgresBackendDriver(schemaAwareDb, realtimeService, registry, undefined, poolManager);\n realtimeService.setDataDriver(driver);\n\n // ── RLS enforcement (user context) ───────────────────────────────\n // Authenticated requests are authorized entirely by RLS policies,\n // but a privileged connection (superuser / BYPASSRLS / table owner)\n // bypasses RLS. Detect the posture and, when privileged, provision\n // the restricted `rebase_user` role and route authenticated\n // requests (reads AND writes) through it. The server context (base\n // driver / auth flows / `rebase.sql`) stays on the owner connection\n // and bypasses; `rebase.dataAsAdmin` does NOT — it is scoped with\n // `withAuth({ uid: \"service\", roles: [\"admin\"] })` at boot, so it\n // runs as `rebase_user` and its policies are evaluated.\n // Default-on: a privileged connection that cannot be\n // isolated fails the boot rather than serving unenforced requests.\n {\n const runSql: RawSqlRunner = async (text) => {\n const res = await schemaAwareDb.execute(sql.raw(text));\n return (res.rows ?? []) as Record<string, unknown>[];\n };\n // Said before anything else touches the schema: if the role and\n // a schema share a name, unqualified SQL from any tool that does\n // not pin `search_path` has been landing in the wrong place, and\n // that is worth knowing before reading the drift report below.\n await warnOnRoleSchemaCollision(runSql);\n\n const posture = await detectConnectionPosture(runSql);\n if (posture.privileged) {\n const collectionSchemas = registry.getCollections()\n .map((c) => (c as { schema?: string }).schema)\n .filter((s): s is string => typeof s === \"string\");\n // `auth` is deliberately absent: the RLS helpers moved into\n // `rebase`, and granting USAGE on a schema Rebase does not\n // own would, on a Supabase database, hand the end-user role\n // access to theirs.\n await ensureAppRole(runSql, [\"public\", \"rebase\", ...collectionSchemas]);\n driver.rlsUserRole = REBASE_USER_ROLE;\n realtimeService.rlsUserRole = REBASE_USER_ROLE;\n logger.info(`🔐 RLS enforcement active: authenticated requests run as \"${REBASE_USER_ROLE}\" (connection \"${posture.role}\" bypasses RLS: ${posture.superuser ? \"superuser\" : posture.bypassRLS ? \"BYPASSRLS\" : \"table owner\"})`);\n if (posture.superuser || posture.bypassRLS) {\n const message =\n `The database connection runs as ${posture.superuser ? \"a superuser\" : \"a BYPASSRLS role\"} (\"${posture.role}\"). ` +\n `User requests are isolated via SET LOCAL ROLE, but connect as a non-superuser ` +\n `table-owner role in production so the server/owner context is least-privilege.`;\n if (isScaffoldedLocalDatabase(pgConfig.connectionString)) {\n logger.debug(`🔐 ${message}`);\n } else {\n logger.warn(`⚠️ ${message}`);\n }\n }\n } else {\n logger.info(`🔐 RLS enforcement: connection role \"${posture.role}\" is subject to RLS natively; no role switch needed.`);\n }\n\n // Independent of posture: a policy targeting a role the request\n // never runs as filters every row, so the collection reads as\n // empty rather than erroring. Applies to both branches — the\n // role that matters is whichever one requests actually use.\n await validatePolicyPgRoles(\n runSql,\n registry.getCollections() as never,\n driver.rlsUserRole ?? posture.role\n );\n\n // The same habit one surface over, and the dangerous direction:\n // a rule that reads as \"signed in only\" but is true for every\n // caller grants the data away rather than hiding it.\n warnOnAnonymousGrants(registry.getCollections() as never);\n\n // Raw policy SQL written against the pre-1.0 helper schema. It\n // is rewritten on compile, so this is the only place the project\n // is ever told the spelling moved.\n warnOnLegacyRlsFunctions(registry.getCollections() as never);\n }\n\n // Ensure branch metadata table exists when branching is available\n if (driver.branchService) {\n try {\n await driver.branchService.ensureBranchMetadataTable();\n } catch (err) {\n logger.warn(\"⚠️ Could not initialize branch metadata table\", { error: err });\n }\n }\n\n // ── Channel history ──────────────────────────────────────────────\n // Opt-in per channel pattern. With no rules this creates no tables\n // and leaves broadcast on its original fire-and-forget path, so\n // presence-only apps pay nothing for it.\n try {\n await realtimeService.configureChannelHistory(\n pgConfig.realtime?.channels,\n { provision: realtimeProvisions }\n );\n } catch (err) {\n logger.warn(\"⚠️ Could not initialize channel history tables — retained channels will not replay\", { error: err });\n }\n\n // ── Realtime change source ───────────────────────────────────────\n // Prefer DATABASE_DIRECT_URL to bypass PgBouncer for LISTEN/NOTIFY.\n const directUrl = process.env.DATABASE_DIRECT_URL || pgConfig.connectionString;\n\n // ── Cross-instance channel bus ───────────────────────────────────\n // Entity changes already span instances (CDC / LISTEN below).\n // Channel broadcast and presence did not — they lived in per-process\n // maps, so behind two replicas the clients of one were invisible to\n // the other. Opt-in, and a no-op when left at \"memory\".\n try {\n const busSetting = resolveChannelBusSetting(pgConfig.realtime?.bus);\n // A supplied instance is always installed; a named built-in only\n // when it is not the in-process default, so the common case\n // touches none of this machinery.\n const wantsBus = isChannelBusInstance(busSetting) || busSetting.type !== \"memory\";\n if (wantsBus) {\n await realtimeService.configureChannelBus(\n createChannelBus(busSetting, {\n db: schemaAwareDb as unknown as NodePgDatabase<Record<string, unknown>>,\n directUrl\n })\n );\n }\n } catch (err) {\n logger.warn(\"⚠️ [ChannelBus] Could not configure the channel bus — channel broadcast and presence stay per-instance\", { error: err });\n }\n\n // Database-level Change Data Capture. When active, realtime events are\n // emitted for EVERY committed write — including ones that bypass the\n // Rebase API (psql, another service's cron, raw SQL, the Studio SQL\n // editor) — matching Supabase Realtime's WAL-tailing model. CDC also\n // becomes the cross-instance channel, so the legacy per-mutation\n // LISTEN/NOTIFY is not started alongside it.\n // REALTIME_CDC=auto → default: enable where the connection supports\n // it; silently fall back to app-level otherwise\n // REALTIME_CDC=trigger → force trigger-based capture (warns if it can't)\n // REALTIME_CDC=wal → prefer WAL logical replication (degrades to trigger)\n // REALTIME_CDC=off → app-level realtime only\n const validModes = new Set([\"auto\", \"wal\", \"trigger\", \"off\"]);\n let cdcMode = (process.env.REALTIME_CDC || \"auto\").trim().toLowerCase();\n if (!validModes.has(cdcMode)) {\n logger.warn(`⚠️ [CDC] Unknown REALTIME_CDC value \"${cdcMode}\" — expected auto|wal|trigger|off. Defaulting to \"auto\".`);\n cdcMode = \"auto\";\n }\n\n // `auto` tries CDC but treats \"can't\" as a normal outcome (info log);\n // explicit trigger/wal was asked for, so a failure is worth a warning.\n // A process that consumes nothing needs no capture *started*, and a\n // process that does not own the schema installs no triggers. They\n // come apart: the `api` in a split with an external migration Job\n // subscribes without provisioning, and both answers are correct.\n const wantsCdc = cdcMode !== \"off\" && (realtimeSubscribes || realtimeProvisions);\n const explicitCdc = cdcMode === \"trigger\" || cdcMode === \"wal\";\n let cdcEnabled = false;\n let provisionCdcForTables: PostgresDriverInternals[\"provisionCdcForTables\"];\n\n if (wantsCdc && !directUrl) {\n const reason = \"no direct database connection is available for the realtime LISTEN client (set DATABASE_DIRECT_URL)\";\n if (explicitCdc) logger.warn(`⚠️ [CDC] REALTIME_CDC=${cdcMode} but ${reason} — using app-level realtime.`);\n else logger.info(`ℹ️ [CDC] Using app-level realtime — ${reason}.`);\n } else if (wantsCdc && directUrl) {\n if (cdcMode === \"wal\") {\n // Native WAL logical-replication streaming requires wal_level=logical,\n // a replication-privileged role and a replication slot, none of which\n // are bundled with this adapter yet. Degrade to trigger-based capture,\n // which provides equivalent database-level coverage.\n logger.warn(\n \"⚠️ [CDC] REALTIME_CDC=wal: native WAL streaming is not bundled in this build; \" +\n \"using trigger-based change capture instead (equivalent database-level coverage).\"\n );\n }\n try {\n const cdcRunSql: RawSqlRunner = async (text) => {\n const res = await schemaAwareDb.execute(sql.raw(text));\n return (res.rows ?? []) as Record<string, unknown>[];\n };\n const cdcTables: CdcTableRef[] = registry.getCollections()\n .map((c) => ({\n schema: (c as { schema?: string }).schema ?? \"public\",\n table: getCollectionTableName(c)\n }))\n .filter((t) => Boolean(t.table) && registry.hasTableForCollection(t.table));\n // Junction tables back no collection, so the list above misses\n // them — and a link or unlink is a write nobody would hear\n // about. Their rows are the contents of a child list, which is\n // as much a change as a write to the rows themselves.\n for (const link of collectJunctionLinks(registry)) {\n cdcTables.push({ schema: link.schema,\ntable: link.table });\n }\n // Provisioning throws only when the connection can't create the\n // trigger function (insufficient privilege); enableCdc throws when\n // the LISTEN connection can't be established. Either → fall back.\n if (realtimeProvisions) await provisionTriggerCdc(cdcRunSql, cdcTables);\n if (realtimeSubscribes) await realtimeService.enableCdc(directUrl);\n cdcEnabled = true;\n // Boot steps that create their own tables (auth) run after\n // this one and use it to instrument what they just created.\n // Left undefined where this process installs nothing, so a\n // later boot step cannot re-enter the DDL path by the side\n // door — the callers already treat it as optional, because a\n // driver without CDC never sets it either.\n if (realtimeProvisions) {\n provisionCdcForTables = async (tables) => {\n await provisionTriggerCdc(cdcRunSql, tables);\n };\n }\n // Say which half ran. \"All writes now emit realtime events\"\n // is a claim about the database and stays true for a process\n // that only installed the triggers; what changes is whether\n // *this* process is listening, and an operator reading one\n // pod's log should not have to infer that from its role.\n logger.info(\n `📡 [CDC] Realtime source = database-level change capture (mode: ${cdcMode === \"wal\" ? \"wal→trigger\" : \"trigger\"}). ` +\n `All writes now emit realtime events regardless of origin.` +\n (realtimeSubscribes ? \"\" : \" This process installs the capture but does not consume it.\")\n );\n } catch (err) {\n if (explicitCdc) {\n logger.warn(\"⚠️ [CDC] Could not enable database-level change capture — falling back to app-level realtime.\", { error: err });\n } else {\n logger.info(\n \"ℹ️ [CDC] Database-level change capture unavailable (likely insufficient privileges to create triggers, \" +\n \"or the LISTEN connection was refused) — using app-level realtime. Set REALTIME_CDC=off to silence this.\",\n { detail: err instanceof Error ? err.message : String(err) }\n );\n }\n }\n }\n\n // Legacy cross-instance realtime (app-level). Skipped when CDC is\n // active because CDC already spans instances.\n if (!cdcEnabled && directUrl && realtimeSubscribes) {\n try {\n await realtimeService.startListening(directUrl);\n } catch (err) {\n logger.warn(\"⚠️ Cross-instance realtime could not be started\", { error: err });\n }\n }\n\n // ── Startup Schema Validation ────────────────────────────────────\n // One-directional: only checks collections → DB (extra DB tables\n // that aren't mapped to collections are perfectly fine).\n try {\n const registeredCollections = registry.getCollections();\n if (registeredCollections.length > 0) {\n // Deliberately unfiltered by schema: a table that is missing\n // from where the collection says it lives is very often\n // sitting in another schema entirely (see the misplaced-table\n // report below), and knowing *which* is the whole answer.\n const result = await schemaAwareDb.execute(sql.raw(`\n SELECT table_name, table_schema\n FROM information_schema.tables\n WHERE table_schema NOT IN ('pg_catalog', 'information_schema')\n AND table_type = 'BASE TABLE'\n `));\n const tablesByName = new Map<string, string[]>();\n for (const row of result.rows as Array<{ table_name: string; table_schema: string }>) {\n const schemas = tablesByName.get(row.table_name) ?? [];\n schemas.push(row.table_schema);\n tablesByName.set(row.table_name, schemas);\n }\n const dbTables = new Set(\n (result.rows as Array<{ table_name: string; table_schema: string }>).map(r =>\n r.table_schema === \"public\" ? r.table_name : `${r.table_schema}.${r.table_name}`\n )\n );\n const missing: Array<{ slug: string; table: string; foundIn: string[] }> = [];\n for (const col of registeredCollections) {\n // Auth owns its table and creates it later in this same\n // boot (initializeAuth → ensureAuthTablesExist), so it is\n // legitimately absent right now. Reporting it as drift\n // tells the user to `db:push` a table that is about to\n // exist — and on an introspected database, one that the\n // database was never supposed to hold.\n if ((col as { auth?: { enabled?: boolean } }).auth?.enabled) continue;\n\n const schemaName = \"schema\" in col && col.schema ? col.schema : \"public\";\n const checkName = resolveDriftCheckName(col, registry.getTableNames());\n const fullCheckName = schemaName === \"public\" ? checkName : `${schemaName}.${checkName}`;\n if (!dbTables.has(fullCheckName)) {\n // Report what was actually looked up: an unqualified\n // \"users\" sends people hunting for public.users.\n missing.push({ slug: col.slug,\ntable: fullCheckName,\nfoundIn: (tablesByName.get(checkName) ?? []).filter(s => s !== schemaName) });\n }\n }\n if (missing.length > 0) {\n const lines = missing.map(\n m => ` • collection \"${m.slug}\" → table \"${m.table}\"` +\n (m.foundIn.length > 0\n ? ` — but a table of that name exists in ${m.foundIn.map(s => `\"${s}\"`).join(\", \")}`\n : \"\")\n );\n // A table that exists under the same name in another\n // schema is not ordinary drift: it is almost always this\n // framework's own `search_path` hazard. Postgres resolves\n // unqualified SQL through `\"$user\", public`, and this\n // project creates a schema named `rebase` while every\n // template names the database role `rebase` too — so an\n // unqualified CREATE TABLE (a hand-written migration, the\n // SQL editor, drizzle-kit) lands in `rebase`, and the\n // runtime, which now pins `search_path=public`, cannot see\n // it. Say so, because \"missing table\" sends people to\n // re-run a push that will create a *second* copy.\n const misplaced = missing.filter(m => m.foundIn.length > 0);\n const misplacedHelp = misplaced.length === 0 ? [] : [\n \" Those tables exist — in the wrong schema. Unqualified SQL run by a\",\n \" role whose name matches a schema (`rebase` is both, by default)\",\n \" resolves through search_path's \\\"$user\\\" and creates there. Move them:\",\n ...misplaced.map(m =>\n ` ALTER TABLE \"${m.foundIn[0]}\".\"${m.table.split(\".\").pop()}\" SET SCHEMA \"${m.table.includes(\".\") ? m.table.split(\".\")[0] : \"public\"}\";`\n ),\n \" and qualify the SQL that created them, or pin search_path in DATABASE_URL\",\n \" (`?options=-c%20search_path%3Dpublic`).\",\n \"\"\n ];\n // What to tell the operator depends entirely on whether a\n // create step ran in this process, and the caller is the\n // only thing that knows. This warning used to assert that\n // it had (\"this runtime applies the collection schema at\n // boot unless REBASE_MIGRATE_ON_BOOT=none\") and send\n // people to that variable and to driver-version skew. For\n // an app whose boot path contained no provisioning step,\n // both were dead ends: nothing read that variable, and the\n // driver was current. Say which case this is instead of\n // guessing, and say nothing when the caller is too old to\n // tell us.\n const cause = describeSchemaDriftCause(schemaProvisioning);\n logger.warn([\n \"\",\n \"⚠️ SCHEMA DRIFT — the database is missing tables this backend serves:\",\n ...lines,\n \"\",\n ...misplacedHelp,\n ...cause,\n \"\",\n \" To apply this project's schema:\",\n \" • Managed cloud: the runtime creates tables and RLS at boot. `rebase db\",\n \" push` cannot reach a tenant's in-cluster database — redeploy instead.\",\n \" • Self-host: run `rebase db push` (dev) or `rebase db migrate` (prod)\",\n \" against DATABASE_URL.\",\n \"\"\n ].join(\"\\n\"));\n }\n }\n } catch (err) {\n logger.warn(\"⚠️ Startup schema validation could not run\", {\n error: err instanceof Error ? err.message : String(err)\n });\n }\n\n const internals: PostgresDriverInternals = {\n db: schemaAwareDb,\n readDb,\n registry,\n realtimeService,\n driver,\n poolManager,\n provisionCdcForTables\n };\n\n return {\n driver,\n realtimeProvider: realtimeService,\n collectionRegistry: registry,\n // Only set in baas mode — tells the server which collections the\n // database turned out to have.\n collections: introspectedCollections,\n internals\n };\n },\n\n async initializeAuth(config: unknown, driverResult: InitializedDriver): Promise<BootstrappedAuth | undefined> {\n const authConfig = config as Record<string, unknown> | undefined;\n if (!authConfig) return undefined;\n\n const internals = driverResult.internals as PostgresDriverInternals;\n const db = internals.db;\n const registry = internals.registry;\n\n // Resolve the auth collection from the explicit config.\n // This replaces the old `registry.getTable(\"users\")` magic string lookup.\n const authCollection = authConfig.collection as CollectionConfig | undefined;\n\n // ensureAuthTablesExist works with the collection abstraction — no Drizzle leakage.\n await ensureAuthTablesExist(db, authCollection);\n\n // The driver bootstrapped before these tables existed, so CDC skipped\n // them. Instrument them now, or writes to the user table emit no\n // realtime events until the next restart.\n if (authCollection && internals.provisionCdcForTables) {\n const authSchema = \"schema\" in authCollection && typeof authCollection.schema === \"string\"\n ? authCollection.schema\n : \"rebase\";\n const authTable = \"table\" in authCollection && typeof authCollection.table === \"string\"\n ? authCollection.table\n : authCollection.slug;\n if (authTable) {\n try {\n await internals.provisionCdcForTables([{ schema: authSchema, table: authTable }]);\n } catch (err) {\n logger.warn(\n `⚠️ [CDC] Could not attach change-capture to the auth table \"${authSchema}.${authTable}\" — ` +\n \"writes to it won't emit database-level events.\",\n { detail: err instanceof Error ? err.message : String(err) }\n );\n }\n }\n }\n\n let emailService: EmailService | undefined;\n if (authConfig.email) {\n emailService = createEmailService(authConfig.email as EmailConfig);\n }\n\n // Resolve the Drizzle table for the internal UserService/AuthRepository.\n // These are internal Postgres-specific services that need the Drizzle table reference.\n const tableName = authCollection\n ? (\"table\" in authCollection && typeof authCollection.table === \"string\"\n ? authCollection.table\n : authCollection.slug)\n : undefined;\n const usersTable = tableName\n ? registry.getTable(tableName) as RebasePgTable | undefined\n : undefined;\n\n let usersSchemaName = \"rebase\";\n if (authCollection && \"schema\" in authCollection && typeof authCollection.schema === \"string\") {\n usersSchemaName = authCollection.schema;\n }\n\n const authTables = createAuthSchema(usersSchemaName) as unknown as AuthSchemaTables;\n if (usersTable) {\n authTables.users = usersTable as RebasePgTable;\n }\n\n const userService = new UserService(db, authTables);\n const authRepository = new PostgresAuthRepository(db, authTables);\n\n return { userService,\nroleService: userService,\nemailService,\nauthRepository,\n// Bound to the same schema `ensureAuthTablesExist` just migrated, so the\n// health endpoint reports on the tables auth actually reads.\nschemaHealthCheck: () => probeAuthSchema(db, resolveAuthSchema(authCollection)) };\n },\n\n async initializeHistory(config: HistoryConfig, driverResult: InitializedDriver): Promise<{ historyService: HistoryService } | undefined> {\n if (!config) return undefined;\n\n const internals = driverResult.internals as PostgresDriverInternals;\n const db = internals.db;\n\n await ensureHistoryTableExists(db);\n\n const retention = typeof config === \"object\" ? config.retention : undefined;\n const historyService = new HistoryService(db, retention ? { ttlDays: retention } : undefined);\n\n return { historyService };\n },\n\n async initializeRealtime(_config: unknown, driverResult: InitializedDriver): Promise<RealtimeProvider | undefined> {\n const internals = driverResult.internals as PostgresDriverInternals;\n return internals.realtimeService;\n },\n\n /**\n * Create any collection tables, columns and enum types the database is\n * missing — additively, never destructively.\n *\n * This is what lets the managed runtime boot a project against a fresh\n * database and actually serve it. Before this, only auth tables were\n * ensured, so a managed tenant came up with working sign-in and a 500 on\n * every data route.\n *\n * Runs through the drizzle handle's underlying session so it uses the\n * same connection (and therefore the same privileges) the driver already\n * proved it can bootstrap with.\n */\n async ensureCollectionSchema(\n collections: unknown[],\n driverResult?: InitializedDriver,\n log?: (message: string) => void\n ): Promise<{ applied: number }> {\n const { ensureCollectionTables } = await import(\"./schema/ensure-collection-tables\");\n // Runs through the drizzle handle the driver already bootstrapped\n // with, so it uses exactly the connection and privileges that were\n // proven to work. Every statement is DDL or a catalogue read with no\n // bindable values (schema names are identifiers), and the module\n // validates them before they reach a string.\n const queryable = provisioningQueryable(driverResult);\n const plan = await ensureCollectionTables(\n queryable,\n collections as Parameters<typeof ensureCollectionTables>[1],\n log\n );\n for (const failure of plan.failures) {\n logger.warn(\n failure.kind === \"comment-column\"\n // The stamp records which `search` block the generated\n // column was built from. Without it the next boot cannot\n // tell a changed block from an unchanged one and adopts\n // the column again instead of refusing.\n ? `🔍 [schema] Could not record the search fingerprint on \"${failure.target}\" — search works, ` +\n `but a later change to the \\`search\\` block will not be detected: ${failure.error}`\n : `🔗 [schema] Could not add foreign key \"${failure.target}\" — the column exists and the ` +\n `collection still serves, but rows are not policed by this constraint: ${failure.error}`\n );\n }\n return { applied: plan.actions.length - plan.failures.length };\n },\n\n /**\n * Apply the collections' RLS policies — ENABLE ROW LEVEL SECURITY and the\n * `securityRules` compiled to `CREATE POLICY` — so a freshly provisioned\n * database serves data instead of denying every user-context read.\n *\n * The companion to {@link ensureCollectionSchema}: that creates the\n * tables, this makes them servable. Boot runs it *after* auth\n * initialization, because the generated policies call `rebase.uid()` /\n * `rebase.roles()`, and `CREATE POLICY` validates those functions exist.\n *\n * Runs through the same drizzle handle, one statement at a time (that\n * handle speaks the extended query protocol, which rejects multi-command\n * strings). Failures are per-table and non-fatal: a table left un-policed\n * stays RLS-enabled, so it denies rather than leaks.\n */\n async ensureCollectionPolicies(\n collections: unknown[],\n driverResult?: InitializedDriver,\n log?: (message: string) => void\n ): Promise<{ applied: number }> {\n const { ensureCollectionPolicies } = await import(\"./schema/ensure-collection-policies\");\n const queryable = provisioningQueryable(driverResult);\n const outcome = await ensureCollectionPolicies(\n queryable,\n collections as CollectionConfig[],\n log\n );\n\n for (const skip of outcome.skipped) {\n logger.warn(`🔐 [rls] Policies not applied to \"${skip.table}\": ${skip.reason}`);\n }\n for (const failure of outcome.failures) {\n logger.warn(\n `🔐 [rls] Could not fully apply policies to \"${failure.table}\" — RLS is on, so it denies until this is resolved: ${failure.error}`\n );\n }\n\n // RLS could not be turned on at all. The schema-wide grant to the\n // user role has already run by this point, so without the revoke\n // below the table is readable and writable by every authenticated\n // request with no row filtering — the one state this boot path must\n // never leave behind, and the one it used to describe as \"locked\".\n const unrevoked = outcome.unsecured.filter(u => !u.grantWithdrawn);\n for (const u of outcome.unsecured.filter(u => u.grantWithdrawn)) {\n logger.error(\n `🔐 [rls] Could not enable row-level security on \"${u.table}\": ${u.error}. ` +\n `Its privileges have been revoked from ${REBASE_USER_ROLE}, so the table is ` +\n \"unreachable rather than unprotected. Reads and writes to that collection will \" +\n \"fail until RLS can be enabled.\"\n );\n }\n if (unrevoked.length > 0) {\n // Neither securable nor closable. Serving here would mean\n // handing every authenticated caller unfiltered access, so the\n // boot fails instead — this is the one failure that is not\n // survivable per-table.\n throw new Error(\n \"Refusing to start: row-level security could not be enabled on \" +\n unrevoked.map(u => `\"${u.table}\" (${u.error})`).join(\", \") +\n `, and the privileges granted to ${REBASE_USER_ROLE} could not be revoked either. ` +\n \"The table would be served with no row filtering. Fix the database permissions \" +\n \"(the connection role must own these tables, or be able to ALTER them) and boot again.\"\n );\n }\n\n // Retire the pre-1.0 `auth` schema now that the policies above no\n // longer call into it. Deliberately after, and deliberately quiet:\n // Postgres refuses to drop a function an RLS policy still\n // references, so on a database where some table has not been\n // recompiled yet this is expected to fail and succeed on a later\n // boot. See DROP_LEGACY_AUTH_SCHEMA_SQL for the guards that keep it\n // off a Supabase `auth` schema.\n try {\n const { dropLegacyAuthSchema } = await import(\"./schema/rls-bootstrap-sql\");\n await dropLegacyAuthSchema(\n async (text) => (await queryable.query<Record<string, unknown>>(text)).rows,\n { info: (m) => logger.info(m), warn: (m) => logger.warn(m) }\n );\n } catch (err) {\n logger.info(\n \"Left the legacy `auth` schema in place: \" +\n (err instanceof Error ? err.message : String(err))\n );\n }\n\n return { applied: outcome.policiesApplied };\n },\n\n /**\n * Read what the last provisioning boot recorded, or `null`.\n *\n * The meta schema is `rebase` rather than the auth stamp's — see\n * `schema/collections-schema-version.ts` for why the two can differ.\n */\n async readCollectionsSchemaVersion(driverResult?: InitializedDriver): Promise<string | null> {\n const db = provisioningDb(driverResult);\n if (!db) return null;\n const { readCollectionsSchemaVersion } = await import(\"./schema/collections-schema-version\");\n return readCollectionsSchemaVersion(db as never, SCHEMA_META_SCHEMA);\n },\n\n /** Record what this process just applied. Only the provisioning process calls this. */\n async stampCollectionsSchemaVersion(version: string, driverResult?: InitializedDriver): Promise<void> {\n const db = provisioningDb(driverResult);\n if (!db) return;\n const { stampCollectionsSchemaVersion } = await import(\"./schema/collections-schema-version\");\n await stampCollectionsSchemaVersion(db as never, SCHEMA_META_SCHEMA, version);\n },\n\n getAdmin(driverResult: InitializedDriver): DatabaseAdmin | undefined {\n const internals = driverResult.internals as PostgresDriverInternals;\n return internals.driver.admin;\n },\n\n mountRoutes(app: unknown, basePath: string, driverResult: InitializedDriver): void {\n // The coordinator handles auth/storage/data routes.\n // This hook is for driver-specific extensions only.\n // Currently Postgres doesn't need additional routes beyond what the coordinator mounts.\n },\n\n async initializeWebsockets(server: unknown, realtimeService: RealtimeProvider, driver: DataDriver, config?: unknown, adapter?: unknown): Promise<void> {\n const { createPostgresWebSocket } = await import(\"./websocket\");\n createPostgresWebSocket(\n server as import(\"http\").Server,\n realtimeService as RealtimeService,\n driver as PostgresBackendDriver,\n config as { requireAuth?: boolean; jwtSecret?: string; serviceKey?: string },\n adapter as AuthAdapter | undefined\n );\n }\n };\n}\n","import { DatabaseAdapter, InitializedDriver, RealtimeProvider, DataDriver, DatabaseAdmin, BootstrappedAuth } from \"@rebasepro/types\";\nimport { createPostgresBootstrapper } from \"./PostgresBootstrapper\";\nimport type { PostgresDriverConfig } from \"./PostgresBootstrapper\";\n\n/**\n * Creates a Postgres database adapter for Rebase.\n */\nexport function createPostgresAdapter(pgConfig: PostgresDriverConfig): DatabaseAdapter {\n const bootstrapper = createPostgresBootstrapper(pgConfig);\n\n return {\n type: bootstrapper.type,\n\n async initializeDriver(config) {\n return bootstrapper.initializeDriver(config);\n },\n\n async initializeRealtime(driverResult) {\n if (bootstrapper.initializeRealtime) {\n return bootstrapper.initializeRealtime({}, driverResult);\n }\n return undefined;\n },\n\n async initializeAuth(config, driverResult) {\n if (bootstrapper.initializeAuth) {\n return bootstrapper.initializeAuth(config, driverResult);\n }\n return undefined;\n },\n\n async initializeHistory(config, driverResult) {\n if (bootstrapper.initializeHistory) {\n return bootstrapper.initializeHistory(config, driverResult);\n }\n return undefined;\n },\n\n // `adapter` is forwarded for the same reason the schema hooks below are:\n // dropping an argument here is invisible at the call site and silent at\n // runtime. This one decided whether the realtime socket authenticates at\n // all — without it, a server whose auth comes from an AuthAdapter fell\n // back to \"is a jwtSecret set?\", answered no, and admitted every client.\n initializeWebsockets(server, realtimeService, driver, config, adapter) {\n if (bootstrapper.initializeWebsockets) {\n return bootstrapper.initializeWebsockets(server, realtimeService, driver, config, adapter);\n }\n },\n\n // Forwarded so the boot-time schema/RLS provisioning is reachable when\n // this adapter is wrapped back into a bootstrapper. Omitting either left\n // a managed tenant with no tables (500 on every data route) or tables\n // but no policies (401 on every read) — the create step never ran.\n ensureCollectionSchema: bootstrapper.ensureCollectionSchema\n ? (collections, driverResult, log) =>\n bootstrapper.ensureCollectionSchema!(collections, driverResult, log)\n : undefined,\n\n ensureCollectionPolicies: bootstrapper.ensureCollectionPolicies\n ? (collections, driverResult, log) =>\n bootstrapper.ensureCollectionPolicies!(collections, driverResult, log)\n : undefined,\n\n // Same forwarding rule, third instance. Dropping these is not a type\n // error and not a runtime error: the stamp is simply never written and\n // never read, so a split deployment loses the only thing that would tell\n // it a unit is serving against a schema it was not built for — and a\n // check that is off looks exactly like a check that passed. This one was\n // in fact dropped on the first attempt, and the e2e caught it.\n readCollectionsSchemaVersion: bootstrapper.readCollectionsSchemaVersion\n ? (driverResult) => bootstrapper.readCollectionsSchemaVersion!(driverResult)\n : undefined,\n\n stampCollectionsSchemaVersion: bootstrapper.stampCollectionsSchemaVersion\n ? (version, driverResult) => bootstrapper.stampCollectionsSchemaVersion!(version, driverResult)\n : undefined,\n\n getAdmin(driverResult) {\n if (bootstrapper.getAdmin) {\n return bootstrapper.getAdmin(driverResult);\n }\n return undefined;\n },\n\n mountRoutes(app, basePath, driverResult) {\n if (bootstrapper.mountRoutes) {\n bootstrapper.mountRoutes(app, basePath, driverResult);\n }\n }\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuMA,SAAgB,qBAAqB,SAA+D;CAChG,OAAO,OAAQ,SAAoC,YAAY;AACnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC5KA,SAAgB,eAAkB,OAAsB;CACpD,OAAO,OAAO,UAAU,WAAW,MAAM,KAAK,CAAC,CAAC,YAAY,IAAI;AACpE;;;ACzBA,IAAa,sBAAb,MAAiC;CAC7B,wBAAmC,IAAI,IAAI;CAC3C,mCAAwD,IAAI,IAAI;CAChE;CACA;CAEA,YAAY,uBAA+B;EACvC,KAAK,uBAAuB;EAC5B,IAAI;GACA,MAAM,MAAM,IAAI,IAAI,qBAAqB;GACzC,KAAK,sBAAsB,IAAI,SAAS,MAAM,CAAC;EACnD,SAAS,GAAG;GACR,MAAM,IAAI,MAAM,2CAA2C,GAAG;EAClE;CACJ;CAEA,WAAkB,cAA6D;EAC3E,MAAM,WAAW,KAAK,iBAAiB,IAAI,YAAY;EACvD,IAAI,UACA,OAAO;EAIX,MAAM,KAAK,QADE,KAAK,QAAQ,YACP,CAAI;EACvB,KAAK,iBAAiB,IAAI,cAAc,EAAE;EAC1C,OAAO;CACX;CAEA,QAAe,cAA4B;EACvC,IAAI,KAAK,MAAM,IAAI,YAAY,GAC3B,OAAO,KAAK,MAAM,IAAI,YAAY;EAGtC,MAAM,MAAM,IAAI,IAAI,KAAK,oBAAoB;EAC7C,IAAI,WAAW,IAAI;EAEnB,MAAM,OAAO,IAAI,KAAK;GAIlB,kBAAkB,cAAc,IAAI,SAAS,CAAC;GAI9C,KAAK,cAAc,EAAE;GACrB,mBAAmB;GACnB,iBAAiB;EACrB,CAAC;EAGD,KAAK,GAAG,UAAU,QAAQ;GACtB,OAAO,MAAM,gEAAgE,gBAAgB,EAAE,OAAO,IAAI,CAAC;EAC/G,CAAC;EACD,6BAA6B,MAAM,WAAW,cAAc;EAE5D,KAAK,MAAM,IAAI,cAAc,IAAI;EACjC,OAAO;CACX;;;;;;CAOA,MAAa,mBAAmB,cAAqC;EACjE,MAAM,OAAO,KAAK,MAAM,IAAI,YAAY;EACxC,IAAI,MAAM;GACN,MAAM,KAAK,IAAI;GACf,KAAK,MAAM,OAAO,YAAY;GAC9B,KAAK,iBAAiB,OAAO,YAAY;EAC7C;CACJ;;CAGA,QAAe,cAA+B;EAC1C,OAAO,KAAK,MAAM,IAAI,YAAY;CACtC;CAEA,MAAa,WAA0B;EACnC,MAAM,WAAW,CAAC;EAClB,KAAK,MAAM,CAAC,QAAQ,SAAS,KAAK,MAAM,QAAQ,GAAG;GAC/C,OAAO,KAAK,gDAAgD,QAAQ;GACpE,SAAS,KAAK,KAAK,IAAI,CAAC;EAC5B;EACA,MAAM,QAAQ,IAAI,QAAQ;EAC1B,KAAK,MAAM,MAAM;EACjB,KAAK,iBAAiB,MAAM;CAChC;AACJ;;;;;;;;;;;;;;;;;AC7EA,SAAgB,iBAAiB,kBAAkB,UAAU;CACzD,MAAM,cAAc,oBAAoB,WAAW,OAAO,SAAS,eAAe;CAElF,MAAM,eAAgB,cAAc,YAAY,MAAM,KAAK,WAAW,IAAI;;;;CAM1E,MAAM,QAAQ,aAAkB,SAAS;EACrC,IAAI,KAAK,IAAI,CAAC,CAAC,cAAc,CAAC,CAAC,WAAW;EAC1C,OAAO,KAAK,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,OAAO;EACtC,cAAc,KAAK,eAAe;EAClC,aAAa,KAAK,cAAc;EAChC,UAAU,KAAK,WAAW;EAC1B,eAAe,QAAQ,gBAAgB,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,QAAQ;EAChE,wBAAwB,KAAK,0BAA0B;EACvD,yBAAyB,UAAU,4BAA4B;EAC/D,aAAa,QAAQ,cAAc,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,QAAQ;EAC5D,OAAO,KAAK,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ;EACjD,UAAU,MAAM,UAAU,CAAC,CAAC,MAA+B,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ;;;;;;;;;;;EAWjF,kBAAkB,UAAU,oBAAoB;EAChD,WAAW,UAAU,YAAY,CAAC,CAAC,WAAW,CAAC,CAAC,QAAQ;EACxD,WAAW,UAAU,YAAY,CAAC,CAAC,WAAW,CAAC,CAAC,QAAQ;CAC5D,CAAC;;;;;;;;;;;;;;;;;;;;;CAuBD,MAAM,gBAAgB,aAAa,kBAAkB;EACjD,IAAI,KAAK,IAAI,CAAC,CAAC,cAAc,CAAC,CAAC,WAAW;EAC1C,KAAK,KAAK,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,iBAAiB,MAAM,IAAI,EAAE,UAAU,UAAU,CAAC;EAC7E,WAAW,KAAK,YAAY,CAAC,CAAC,cAAc,CAAC,CAAC,QAAQ;EACtD,WAAW,KAAK,YAAY,CAAC,CAAC,QAAQ,CAAC,CAAC,OAAO;EAC/C,WAAW,UAAU,YAAY,CAAC,CAAC,QAAQ;EAC3C,SAAS,QAAQ,SAAS,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,QAAQ;EACnD,WAAW,UAAU,YAAY;;;;;;;EAOjC,kBAAkB,UAAU,oBAAoB,CAAC,CAAC,WAAW,CAAC,CAAC,QAAQ;;;;;;;EAOvE,KAAK,KAAK,KAAK;EACf,WAAW,KAAK,YAAY;EAC5B,WAAW,KAAK,YAAY;EAC5B,WAAW,UAAU,YAAY,CAAC,CAAC,WAAW,CAAC,CAAC,QAAQ;CAC5D,IAAI,WAAW,EACX,YAAY,MAAM,4BAA4B,CAAC,CAAC,GAAG,MAAM,SAAS,EACtE,EAAE;;;;CAKF,MAAM,sBAAsB,aAAa,yBAAyB;EAC9D,IAAI,KAAK,IAAI,CAAC,CAAC,cAAc,CAAC,CAAC,WAAW;EAC1C,KAAK,KAAK,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,iBAAiB,MAAM,IAAI,EAAE,UAAU,UAAU,CAAC;EAC7E,WAAW,KAAK,YAAY,CAAC,CAAC,QAAQ,CAAC,CAAC,OAAO;EAC/C,WAAW,UAAU,YAAY,CAAC,CAAC,QAAQ;EAC3C,QAAQ,UAAU,SAAS;EAC3B,WAAW,UAAU,YAAY,CAAC,CAAC,WAAW,CAAC,CAAC,QAAQ;CAC5D,CAAC;;;;CAKD,MAAM,YAAY,aAAa,cAAc;EACzC,KAAK,KAAK,KAAK,CAAC,CAAC,WAAW;EAC5B,OAAO,MAAM,OAAO,CAAC,CAAC,QAAQ;EAC9B,WAAW,UAAU,YAAY,CAAC,CAAC,WAAW,CAAC,CAAC,QAAQ;CAC5D,CAAC;;;;CAKD,MAAM,iBAAiB,aAAa,mBAAmB;EACnD,IAAI,KAAK,IAAI,CAAC,CAAC,cAAc,CAAC,CAAC,WAAW;EAC1C,KAAK,KAAK,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,iBAAiB,MAAM,IAAI,EAAE,UAAU,UAAU,CAAC;EAC7E,UAAU,KAAK,UAAU,CAAC,CAAC,QAAQ;EACnC,YAAY,KAAK,aAAa,CAAC,CAAC,QAAQ;EACxC,aAAa,MAAM,cAAc;EACjC,WAAW,UAAU,YAAY,CAAC,CAAC,WAAW,CAAC,CAAC,QAAQ;EACxD,WAAW,UAAU,YAAY,CAAC,CAAC,WAAW,CAAC,CAAC,QAAQ;CAC5D,IAAI,WAAW,EACX,kBAAkB,OAAO,oBAAoB,CAAC,CAAC,GAAG,MAAM,UAAU,MAAM,UAAU,EACtF,EAAE;;;;CAKF,MAAM,aAAa,aAAa,eAAe;EAC3C,IAAI,KAAK,IAAI,CAAC,CAAC,cAAc,CAAC,CAAC,WAAW;EAC1C,KAAK,KAAK,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,iBAAiB,MAAM,IAAI,EAAE,UAAU,UAAU,CAAC;EAC7E,YAAY,KAAK,aAAa,CAAC,CAAC,QAAQ;EACxC,iBAAiB,KAAK,kBAAkB,CAAC,CAAC,QAAQ;EAClD,cAAc,KAAK,eAAe;EAClC,UAAU,QAAQ,UAAU,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,QAAQ;;;;;;;EAOrD,iBAAiB,OAAO,qBAAqB,EAAE,MAAM,SAAS,CAAC;EAC/D,WAAW,UAAU,YAAY,CAAC,CAAC,WAAW,CAAC,CAAC,QAAQ;EACxD,WAAW,UAAU,YAAY,CAAC,CAAC,WAAW,CAAC,CAAC,QAAQ;CAC5D,CAAC;CAuCD,OAAO;EACH;EACA;EACA;EACA;EACA;EACA;EACA;EACA,eA1CkB,aAAa,kBAAkB;GACjD,IAAI,KAAK,IAAI,CAAC,CAAC,cAAc,CAAC,CAAC,WAAW;GAC1C,UAAU,KAAK,WAAW,CAAC,CAAC,QAAQ,CAAC,CAAC,iBAAiB,WAAW,IAAI,EAAE,UAAU,UAAU,CAAC;GAC7F,WAAW,UAAU,YAAY,CAAC,CAAC,WAAW,CAAC,CAAC,QAAQ;GACxD,YAAY,UAAU,aAAa;GACnC,WAAW,KAAK,YAAY;;GAE5B,UAAU,QAAQ,UAAU,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ;GACjD,WAAW,UAAU,YAAY,CAAC,CAAC,QAAQ;EAC/C,CAiCI;EACA,eA7BkB,aAAa,kBAAkB;GACjD,IAAI,KAAK,IAAI,CAAC,CAAC,cAAc,CAAC,CAAC,WAAW;GAC1C,KAAK,KAAK,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,iBAAiB,MAAM,IAAI,EAAE,UAAU,UAAU,CAAC;GAC7E,UAAU,KAAK,WAAW,CAAC,CAAC,QAAQ;GACpC,QAAQ,UAAU,SAAS;GAC3B,WAAW,UAAU,YAAY,CAAC,CAAC,WAAW,CAAC,CAAC,QAAQ;EAC5D,CAuBI;EACA,iBAnBoB,aAAa,qBAAqB;GACtD,IAAI,KAAK,IAAI,CAAC,CAAC,cAAc,CAAC,CAAC,WAAW;GAC1C,KAAK,KAAK,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,iBAAiB,MAAM,IAAI,EAAE,UAAU,UAAU,CAAC;GAC7E,WAAW,KAAK,YAAY,CAAC,CAAC,QAAQ,CAAC,CAAC,OAAO;GAC/C,WAAW,UAAU,YAAY,CAAC,CAAC,QAAQ;GAC3C,QAAQ,UAAU,SAAS;GAC3B,WAAW,UAAU,YAAY,CAAC,CAAC,WAAW,CAAC,CAAC,QAAQ;EAC5D,CAYI;CACJ;AACJ;AAGA,IAAM,oBAAoB,iBAAiB,QAAQ;AAEnD,IAAa,cAAc,kBAAkB;AAE7C,IAAa,QAAQ,kBAAkB;AACvC,IAAa,gBAAgB,kBAAkB;AAC/C,IAAa,sBAAsB,kBAAkB;AACrD,IAAa,YAAY,kBAAkB;AAC3C,IAAa,iBAAiB,kBAAkB;AAChD,IAAa,aAAa,kBAAkB;AAC5C,IAAa,gBAAgB,kBAAkB;AAC/C,IAAa,gBAAgB,kBAAkB;AAC/C,IAAa,kBAAkB,kBAAkB;AAGjD,IAAa,iBAAiB,UAAU,QAAQ,EAAE,YAAY;CAC1D,eAAe,KAAK,aAAa;CACjC,qBAAqB,KAAK,mBAAmB;CAC7C,gBAAgB,KAAK,cAAc;CACnC,YAAY,KAAK,UAAU;CAC3B,eAAe,KAAK,aAAa;CACjC,iBAAiB,KAAK,eAAe;AACzC,EAAE;AAEF,IAAa,yBAAyB,UAAU,gBAAgB,EAAE,WAAW,EACzE,MAAM,IAAI,OAAO;CACb,QAAQ,CAAC,cAAc,GAAG;CAC1B,YAAY,CAAC,MAAM,EAAE;AACzB,CAAC,EACL,EAAE;AAEF,IAAa,+BAA+B,UAAU,sBAAsB,EAAE,WAAW,EACrF,MAAM,IAAI,OAAO;CACb,QAAQ,CAAC,oBAAoB,GAAG;CAChC,YAAY,CAAC,MAAM,EAAE;AACzB,CAAC,EACL,EAAE;AAEF,IAAa,0BAA0B,UAAU,iBAAiB,EAAE,WAAW,EAC3E,MAAM,IAAI,OAAO;CACb,QAAQ,CAAC,eAAe,GAAG;CAC3B,YAAY,CAAC,MAAM,EAAE;AACzB,CAAC,EACL,EAAE;AAEF,IAAa,sBAAsB,UAAU,aAAa,EAAE,KAAK,YAAY;CACzE,MAAM,IAAI,OAAO;EACb,QAAQ,CAAC,WAAW,GAAG;EACvB,YAAY,CAAC,MAAM,EAAE;CACzB,CAAC;CACD,YAAY,KAAK,aAAa;AAClC,EAAE;AAEF,IAAa,yBAAyB,UAAU,gBAAgB,EAAE,WAAW,EACzE,QAAQ,IAAI,YAAY;CACpB,QAAQ,CAAC,cAAc,QAAQ;CAC/B,YAAY,CAAC,WAAW,EAAE;AAC9B,CAAC,EACL,EAAE;AAEF,IAAa,yBAAyB,UAAU,gBAAgB,EAAE,WAAW,EACzE,MAAM,IAAI,OAAO;CACb,QAAQ,CAAC,cAAc,GAAG;CAC1B,YAAY,CAAC,MAAM,EAAE;AACzB,CAAC,EACL,EAAE;AAEF,IAAa,2BAA2B,UAAU,kBAAkB,EAAE,WAAW,EAC7E,MAAM,IAAI,OAAO;CACb,QAAQ,CAAC,gBAAgB,GAAG;CAC5B,YAAY,CAAC,MAAM,EAAE;AACzB,CAAC,EACL,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC9PF,IAAa,OAAO,OAAO,OAAa;CACpC,QAAQ,IAAI,IAAI;AACpB;;AAQA,IAAa,YAAY,OAAO,OAAa;CACzC,QAAQ,MAAM,IAAI;AACtB;;;AC9BA,IAAM,sBAAsB,MAAc,UAItC,CAAC,MAAc;CACf,IAAI,QAAQ;CACZ,IAAI,QAAQ,MAAM,SAAS;CAC3B,IAAI,QAAQ,iBASR,SAAS;EAPL,MAAM;EACN,OAAO;EACP,KAAK;EACL,QAAQ;EACR,MAAM;EACN,SAAS;CAEJ,EAAS,QAAQ;CAE9B,IAAI,QAAQ,WAWR,SAAS;EATL,OAAO;EACP,OAAO;EACP,KAAK;EACL,OAAO;EACP,QAAQ;EACR,MAAM;EACN,SAAS;EACT,MAAM;CAED,EAAW,QAAQ;CAEhC,OAAO,GAAG,QAAQ,KAAK;AAC3B;AAIA,IAAM,gBAAgB,OAAO,qBAA8B,eAAwB;CAC/E,IAAI;EACA,IAAI,CAAC,qBAAqB;GACtB,SAAS,uEAAuE;GAChF;EACJ;EAMA,IAAI,cAAkC,MAAM,6BAJvB,KAAK,QAAQ,mBAIuC,CAAY;EAIrF,IAAI,CAAC,eAAe,CAAC,MAAM,QAAQ,WAAW,GAC1C,cAAc,CAAC;EAKnB,YAAY,MAAM,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;EAEvD,MAAM,gBAAgB,MAAM,eAAe,WAAW;EAEtD,IAAI,YAAY;GACZ,MAAM,YAAY,KAAK,QAAQ,UAAU;GACzC,MAAM,SAAW,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;GACrD,MAAM,SAAW,UAAU,YAAY,aAAa;GACpD,IAAI,8CAA8C,YAAY;EAClE,OAAO;GACH,IAAI,0CAA0C;GAC9C,IAAI,OAAO,aAAa,CAAC;EAC7B;EAEA,IAAI,mBAAmB,mBAAmB,sBAAsB;GAC5D,MAAM;GACN,iBAAiB;GACjB,WAAW;EACf,CAAC,EAAE,sCAAsC;CAE7C,SAAS,OAAO;EACZ,SAAS,4BAA4B,iBAAiB,QAAS,MAAM,SAAS,MAAM,UAAW,OAAO,KAAK,GAAG;CAClH;AACJ;AAEA,IAAM,OAAO,YAAY;CACrB,MAAM,yBAAyB,QAAQ,KAAK,MAAK,QAAO,IAAI,WAAW,gBAAgB,CAAC;CACxF,MAAM,sBAAsB,yBAAyB,uBAAuB,MAAM,GAAG,CAAC,CAAC,KAAK,QAAQ,KAAK;CAEzG,MAAM,gBAAgB,QAAQ,KAAK,MAAK,QAAO,IAAI,WAAW,WAAW,CAAC;CAC1E,MAAM,aAAa,gBAAgB,cAAc,MAAM,GAAG,CAAC,CAAC,KAAK,KAAA;CAEjE,MAAM,QAAQ,QAAQ,KAAK,SAAS,SAAS;CAE7C,IAAI,CAAC,qBAAqB;EACtB,IAAI,iHAAiH;EACrH;CACJ;CAEA,MAAM,eAAe,KAAK,QAAQ,QAAQ,IAAI,GAAG,mBAAmB;CACpE,MAAM,qBAAqB,aAAa,KAAK,QAAQ,QAAQ,IAAI,GAAG,UAAU,IAAI,KAAA;CAElF,IAAI,OAAO;EACP,IAAI,2BAA2B,aAAa,IAAI;EAYhD,MAAM,EAAE,SAAS,aAAa,MAAM,OAAO;EAM3C,SALyB,MAAM,cAAc;GACzC,YAAY;GACZ,eAAe;EACnB,CAEA,CAAA,CAAQ,GAAG,QAAQ,OAAO,aAAa;GACnC,IAAI,IAAI,MAAM,IAAI,SAAS,yBAAyB;GACpD,cAAc,cAAc,kBAAkB;EAClD,CAAC;CACL,OACI,cAAc,cAAc,kBAAkB;AAEtD;AAGA,IAAI,OAAO,KAAK,IAAI,SAAS,QAAQ,KAAK,EAAE,GACxC,KAAK;;;;;;;;;;;AC1GT,SAAgB,qBAAqB,UAAsD;CACvF,MAAM,QAAwB,CAAC;CAC/B,MAAM,uBAAO,IAAI,IAAY;CAE7B,KAAK,MAAM,cAAc,SAAS,eAAe,GAAG;EAChD,IAAI;EACJ,IAAI;GACA,YAAY,2BAA2B,UAAU;EACrD,QAAQ;GAGJ;EACJ;EAEA,KAAK,MAAM,CAAC,aAAa,aAAa,OAAO,QAAQ,SAAS,GAAG;GAC7D,IAAI,CAAC,aAAa,QAAQ,GAAG;GAC7B,MAAM,UAAU,SAAS;GAIzB,MAAM,MAAM,GAAG,WAAW,KAAK,IAAI,YAAY,IAAI,QAAQ;GAC3D,IAAI,KAAK,IAAI,GAAG,GAAG;GACnB,KAAK,IAAI,GAAG;GAEZ,MAAM,KAAK;IACP,QAAS,WAAmC,UAAU;IACtD,OAAO,QAAQ;IACf,kBAAkB;IAClB;IACA,cAAc,QAAQ;IACtB,cAAc,QAAQ;GAC1B,CAAC;EACL;CACJ;CAEA,OAAO;AACX;;;;;;AAOA,SAAgB,qBAAqB,UAAmE;CACpG,MAAM,sBAAM,IAAI,IAA4B;CAE5C,KAAK,MAAM,QAAQ,qBAAqB,QAAQ,GAC5C,KAAK,MAAM,OAAO,CAAC,GAAG,KAAK,OAAO,GAAG,KAAK,SAAS,KAAK,KAAK,GAAG;EAC5D,MAAM,WAAW,IAAI,IAAI,GAAG;EAC5B,IAAI,UAAU,SAAS,KAAK,IAAI;OAC3B,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC;CAC5B;CAGJ,OAAO;AACX;;;;;;;;;;;;;;;;;;;;;;;;ACjEA,IAAa,cAAc;;AAG3B,IAAa,uBAAuB;;AAGpC,IAAa,mBAAmB;;;;;;;AAQhC,IAAM,mBAAmB;AAEzB,IAAM,cAAc,SAAyB,IAAI,KAAK,QAAQ,MAAM,MAAM,EAAE;AAC5E,IAAM,gBAAgB,UAA0B,IAAI,MAAM,QAAQ,MAAM,IAAI,EAAE;;;;;;;;;;;AAY9E,SAAgB,sBAA8B;CAC1C,OAAO;;;6BAGkB,qBAAqB;;;;;;;;;;;;;;;;;;;;;;kCAsBhB,iBAAiB;;;;;;;;;;wBAU3B,aAAa,WAAW,EAAE;;;;EAIhD,KAAK;AACP;;;;;AAMA,SAAgB,mBAAmB,QAAgB,OAAuB;CACtE,MAAM,YAAY,GAAG,WAAW,MAAM,EAAE,GAAG,WAAW,KAAK;CAC3D,OACI,0BAA0B,WAAW,gBAAgB,EAAE,MAAM,UAAU,oBACrD,WAAW,gBAAgB,EAAE,uCACR,UAAU,iCAChB,qBAAqB;AAE9D;;;;;;;;;AAsBA,eAAsB,oBAClB,KACA,QACwB;CAExB,MAAM,IAAI,oBAAoB,CAAC;CAG/B,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,YAA2B,CAAC;CAClC,MAAM,UAAsC,CAAC;CAE7C,KAAK,MAAM,OAAO,QAAQ;EACtB,MAAM,MAAM,GAAG,IAAI,OAAO,GAAG,IAAI;EACjC,IAAI,KAAK,IAAI,GAAG,GAAG;EACnB,KAAK,IAAI,GAAG;EAEZ,IAAI;GACA,MAAM,IAAI,mBAAmB,IAAI,QAAQ,IAAI,KAAK,CAAC;GACnD,UAAU,KAAK,GAAG;EACtB,SAAS,KAAK;GACV,MAAM,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;GAC9D,QAAQ,KAAK;IAAE,GAAG;IAAK;GAAO,CAAC;GAC/B,OAAO,KACH,wDAAwD,IAAI,4EAE5D,EAAE,QAAQ,OAAO,CACrB;EACJ;CACJ;CAMA,OAAO,MACH,wDAAwD,UAAU,OAAO,cACxE,QAAQ,SAAS,KAAK,QAAQ,OAAO,aAAa,MAAM,GAC7D;CAEA,OAAO;EAAE;EAAW;CAAQ;AAChC;;;;;;;;;;;;;;;;;AC7IA,IAAM,6BAA6B;;AAEnC,IAAM,eAAe;AAErB,IAAa,mBAAb,MAA8B;CAKG;CAJ7B;CACA,UAAkB;CAClB;CAEA,YAAY,SAAmD;EAAlC,KAAA,UAAA;EACzB,IAAI,CAAC,aAAa,KAAK,QAAQ,OAAO,GAClC,MAAM,IAAI,MAAM,+BAA+B,QAAQ,QAAQ,qCAAqC;CAE5G;;CAGA,IAAI,SAAkB;EAClB,OAAO,KAAK;CAChB;;;;;;;;CASA,MAAM,QAAuB;EACzB,IAAI,KAAK,SAAS;EAClB,KAAK,UAAU;EACf,IAAI;GACA,MAAM,KAAK,QAAQ,EAAE,SAAS,KAAK,CAAC;EACxC,SAAS,KAAK;GACV,KAAK,UAAU;GACf,MAAM;EACV;CACJ;;CAGA,MAAM,OAAsB;EACxB,KAAK,UAAU;EACf,IAAI,KAAK,gBAAgB;GACrB,aAAa,KAAK,cAAc;GAChC,KAAK,iBAAiB,KAAA;EAC1B;EACA,IAAI,KAAK,QAAQ;GACb,IAAI;IACA,MAAM,KAAK,OAAO,IAAI;GAC1B,QAAQ,CAA4B;GACpC,KAAK,SAAS,KAAA;EAClB;CACJ;CAEA,MAAc,QAAQ,EAAE,UAAU,UAAiC,CAAC,GAAkB;EAClF,MAAM,EAAE,kBAAkB,SAAS,WAAW,aAAa,KAAK;EAOhE,IAAI;EACJ,IAAI;GACA,MAAM,SAAS,IAAI,OAAS,EAAE,iBAAiB,CAAC;GAChD,UAAU;GAEV,OAAO,GAAG,UAAU,QAAQ;IACxB,OAAO,MAAM,KAAK,SAAS,uBAAuB,EAAE,QAAQ,IAAI,QAAQ,CAAC;IACzE,KAAK,kBAAkB;GAC3B,CAAC;GAED,OAAO,GAAG,aAAa;IACnB,IAAI,KAAK,SAAS;KACd,OAAO,KAAK,MAAM,SAAS,0CAA0C;KACrE,KAAK,kBAAkB;IAC3B;GACJ,CAAC;GAED,OAAO,GAAG,iBAAiB,QAAQ;IAC/B,IAAI,CAAC,IAAI,SAAS;IAGlB,QAAQ,QAAQ,UAAU,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,QAC3C,OAAO,MAAM,KAAK,SAAS,+BAA+B,EAAE,OAAO,IAAI,CAAC,CAC5E;GACJ,CAAC;GAED,MAAM,OAAO,QAAQ;GACrB,MAAM,OAAO,MAAM,UAAU,SAAS;GACtC,KAAK,SAAS;GAEd,UAAU,KAAA;GACV,OAAO,MAAM,MAAM,SAAS,yBAAyB,QAAQ,GAAG;EACpE,SAAS,KAAK;GAEV,IAAI,SACA,IAAI;IAAE,MAAM,QAAQ,IAAI;GAAG,QAAQ,CAAqB;GAI5D,IAAI,SAAS,MAAM;GACnB,OAAO,MAAM,KAAK,SAAS,mCAAmC,EAAE,OAAO,IAAI,CAAC;GAC5E,KAAK,kBAAkB;EAC3B;CACJ;CAEA,oBAAkC;EAC9B,IAAI,CAAC,KAAK,WAAW,KAAK,gBAAgB;EAE1C,KAAK,iBAAiB,WAAW,YAAY;GACzC,KAAK,iBAAiB,KAAA;GACtB,IAAI,CAAC,KAAK,SAAS;GACnB,IAAI,KAAK,QAAQ;IACb,IAAI;KAAE,MAAM,KAAK,OAAO,IAAI;IAAG,QAAQ,CAAe;IACtD,KAAK,SAAS,KAAA;GAClB;GACA,MAAM,KAAK,QAAQ;EACvB,GAAG,KAAK,QAAQ,oBAAoB,0BAA0B;CAClE;AACJ;;;;;;;AC5HA,SAAgB,gBAAgB,SAAwC;CACpE,IAAI;CACJ,IAAI;EACA,SAAS,KAAK,MAAM,OAAO;CAC/B,QAAQ;EACJ,OAAO;CACX;CACA,IAAI,CAAC,UAAU,OAAO,WAAW,UAAU,OAAO;CAElD,MAAM,MAAM;CACZ,MAAM,SAAS,OAAO,IAAI,WAAW,WAAW,IAAI,SAAS,KAAA;CAC7D,MAAM,QAAQ,OAAO,IAAI,UAAU,WAAW,IAAI,QAAQ,KAAA;CAC1D,MAAM,KAAK,IAAI;CACf,IAAI,CAAC,UAAU,CAAC,OAAO,OAAO;CAC9B,IAAI,OAAO,YAAY,OAAO,YAAY,OAAO,UAAU,OAAO;CAIlE,OAAO;EACH;EACA;EACA;EACA,KANQ,IAAI,OAAO,OAAO,IAAI,QAAQ,WAAY,IAAI,MAAkC,CAAC;EAOzF,WAAW,IAAI,cAAc;CACjC;AACJ;;;;;;;;;;AAWA,IAAa,cAAb,MAAyB;CACrB;CAEA,YAAY,kBAA0B,SAA0D;EAC5F,KAAK,WAAW,IAAI,iBAAiB;GACjC;GACA,SAAS;GACT,UAAU;GACV,YAAY,YAAY;IACpB,MAAM,QAAQ,gBAAgB,OAAO;IACrC,IAAI,CAAC,OAAO;KACR,OAAO,KAAK,oDAAoD;KAChE;IACJ;IACA,OAAO,QAAQ,KAAK;GACxB;EACJ,CAAC;CACL;;;;;;;;;;CAWA,MAAM,QAAuB;EACzB,IAAI,KAAK,SAAS,QAAQ;GACtB,OAAO,KAAK,oEAAoE;GAChF;EACJ;EACA,MAAM,KAAK,SAAS,MAAM;CAC9B;;CAGA,MAAM,OAAsB;EACxB,MAAM,KAAK,SAAS,KAAK;CAC7B;AACJ;;;;;;;;;;;;;;;;;;;AClFA,SAAgB,uBACZ,IACA,OACe;CACf,OAAO,sBAAsB,OAAO,cAAsB;EAMtD,QAAQ,MADa,GAAG,QAAQ,IAAI,IAAI,SAAS,CAAC,EAAA,CACiB,QAAQ,CAAC;CAChF,GAAG,KAAK;AACZ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACQA,IAAM,uBAAuB;;;;;;;;;AAU7B,IAAM,mBAAmB;;AAGzB,IAAM,oBAAoB;;;;;;;;;AAU1B,SAAgB,WAAW,KAAsD;CAC7E,IAAI,QAAQ,KAAA,KAAa,QAAQ,MAAM,OAAO,KAAA;CAC9C,IAAI,OAAO,QAAQ,UAAU,OAAO,OAAO,SAAS,GAAG,KAAK,MAAM,IAAI,MAAM,KAAA;CAE5E,MAAM,QAAQ,0CAA0C,KAAK,GAAG;CAChE,IAAI,CAAC,OAAO;EACR,OAAO,KAAK,2DAA2D,IAAI,6CAA6C;EACxH;CACJ;CACA,MAAM,QAAQ,WAAW,MAAM,EAAE;CACjC,MAAM,OAAO,MAAM,EAAE,CAAC,YAAY;CAMlC,MAAM,KAAK,SALQ,SAAS,OAAO,IAC7B,SAAS,MAAM,MACX,SAAS,MAAM,MACX,SAAS,MAAM,OACX;CAElB,OAAO,KAAK,IAAI,KAAK,KAAA;AACzB;;;;;;;;AASA,SAAgB,mBAAmB,SAAiB,MAAqC;CACrF,MAAM,UAAU,KAAK;CACrB,IAAI,YAAY,KAAK,OAAO;CAC5B,IAAI,QAAQ,SAAS,GAAG,GAAG,OAAO,QAAQ,WAAW,QAAQ,MAAM,GAAG,EAAE,CAAC;CACzE,OAAO,YAAY;AACvB;;;;;;;;AAeA,IAAa,sBAAb,MAAiC;CAQT;CAPpB;;CAEA,2BAAmB,IAAI,IAAsC;;CAE7D,6BAAqB,IAAI,IAAoB;CAC7C,cAAsB;CAEtB,YAAY,IAAqD,QAAgC,CAAC,GAAG;EAAjF,KAAA,KAAA;EAChB,KAAK,QAAQ,MAAM,QAAO,SAAQ;GAC9B,IAAI,CAAC,MAAM,OAAO;IACd,OAAO,KAAK,gEAAgE;IAC5E,OAAO;GACX;GAEA,IAAI,EADa,KAAK,UAAU,KAAA,KAAa,KAAK,QAAQ,KAAA,IAC3C;IAIX,OAAO,KAAK,uCAAuC,KAAK,MAAM,gFAAgF;IAC9I,OAAO;GACX;GACA,OAAO;EACX,CAAC;CACL;;CAGA,IAAI,UAAmB;EACnB,OAAO,KAAK,MAAM,SAAS;CAC/B;;;;;;CAOA,aAAa,SAAgD;EACzD,IAAI,CAAC,KAAK,WAAW,CAAC,SAAS,OAAO,KAAA;EAEtC,MAAM,SAAS,KAAK,SAAS,IAAI,OAAO;EACxC,IAAI,WAAW,KAAA,GAAW,OAAO,UAAU,KAAA;EAE3C,MAAM,OAAO,KAAK,MAAM,MAAK,MAAK,mBAAmB,SAAS,CAAC,CAAC;EAChE,MAAM,WAAqC,OACrC;GAAE,OAAO,KAAK;GAAO,OAAO,WAAW,KAAK,GAAG;EAAE,IACjD;EAIN,KAAK,SAAS,IAAI,SAAS,QAAQ;EACnC,OAAO,YAAY,KAAA;CACvB;;;;;CAMA,MAAM,eAA8B;EAChC,IAAI,CAAC,KAAK,WAAW,KAAK,aAAa;EAOvC,MAAM,MAAM,uBAAuB,KAAK,IAAI,iBAAiB;EAE7D,MAAM,IAAI,aAAa,iBAAiB,oCAAoC;EAK5E,MAAM,IAAI,aAAa,0BAA0B;;;;;;;;;;SAUhD;EAGD,MAAM,IAAI,aAAa,qCAAqC;;;SAG3D;EAID,MAAM,IAAI,aAAa,yBAAyB;;;;;SAK/C;EAcD,MAAM,CAAC,eAAe,gBAAgB,MAAM,QAAQ,IAAI,CACpD,IAAI,WAAW,yBAAyB,GACxC,IAAI,WAAW,wBAAwB,CAC3C,CAAC;EACD,IAAI,eACA,MAAM,IAAI,KAAK,iCACX,KAAK,GAAG,QAAQ,IAAI,IAAI,uBAAuB,UAAU,kBAAkB,CAAC,CAAC,CACjF;EAEJ,IAAI,cACA,MAAM,IAAI,KAAK,gCACX,KAAK,GAAG,QAAQ,IAAI,IAAI,uBAAuB,UAAU,iBAAiB,CAAC,CAAC,CAChF;EAGJ,IAAI,CAAC,iBAAiB,CAAC,cAAc;GAIjC,OAAO,KACH,0FACJ;GACA;EACJ;EAEA,KAAK,cAAc;EACnB,OAAO,KAAK,+CAA+C,KAAK,MAAM,OAAO,WAAW;CAC5F;;;;;;;;;;CAWA,MAAM,OACF,SACA,OACA,SACA,UACoC;EAepC,MAAM,OAAM,MAdS,KAAK,GAAG,QAAQ,GAAG;;;0BAGtB,QAAQ;;;;;;qBAMb,QAAQ,mBAAmB,MAAM,IAAI,KAAK,UAAU,WAAW,IAAI,EAAE,WAAW,YAAY,KAAK;;;SAG7G,EAAA,CAEkB,KAAK;EACxB,IAAI,CAAC,KAAK,MAAM,IAAI,MAAM,4CAA4C,QAAQ,EAAE;EAEhF,OAAO;GAGH,KAAK,OAAO,IAAI,GAAG;GACnB,IAAI,IAAI,sBAAsB,OAAO,IAAI,WAAW,YAAY,IAAI,OAAO,IAAI,UAAU;EAC7F;CACJ;;;;;;;CAQA,MAAM,OACF,SACA,WAAW,GACX,QAAQ,sBACuD;EAC/D,MAAM,SAAS,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,MAAM,KAAK,KAAK,sBAAsB,gBAAgB,CAAC;EAChG,MAAM,QAAQ,OAAO,SAAS,QAAQ,KAAK,WAAW,IAAI,KAAK,MAAM,QAAQ,IAAI;EAUjF,MAAM,YAAY,MARG,KAAK,GAAG,QAAQ,GAAG;;;8BAGlB,QAAQ,aAAa,MAAM;;oBAErC,OAAO;SAClB,EAAA,CAEwB,KAMrB,KAAI,SAAQ;GACZ,KAAK,OAAO,IAAI,GAAG;GACnB,OAAO,IAAI;GACX,SAAS,IAAI;GACb,UAAU,IAAI,aAAa,KAAA;GAC3B,IAAI,IAAI,sBAAsB,OAAO,IAAI,WAAW,YAAY,IAAI,OAAO,IAAI,UAAU;EAC7F,EAAE;EAQF,MAAM,aAAY,MAHG,KAAK,GAAG,QAAQ,GAAG;0EAC0B,QAAQ;SACzE,EAAA,CACwB,KAAK;EAG9B,OAAO;GAAE;GAAU,WAFD,YAAY,OAAO,UAAU,QAAQ,IAAI;EAE9B;CACjC;;;;;;;;;;;CAYA,MAAM,SAAS,SAAiB,KAAkD;EAO9E,MAAM,OAAM,MANS,KAAK,GAAG,QAAQ,GAAG;;;8BAGlB,QAAQ,aAAa,IAAI;SAC9C,EAAA,CAEkB,KAAK;EAOxB,IAAI,CAAC,KAAK,OAAO;EAEjB,OAAO;GACH,KAAK,OAAO,IAAI,GAAG;GACnB,OAAO,IAAI;GACX,SAAS,IAAI;GACb,UAAU,IAAI,aAAa,KAAA;GAC3B,IAAI,IAAI,sBAAsB,OAAO,IAAI,WAAW,YAAY,IAAI,OAAO,IAAI,UAAU;EAC7F;CACJ;;;;;;;;CASA,MAAM,MAAM,SAAiB,WAA+C;EACxE,MAAM,MAAM,KAAK,IAAI;EAErB,IAAI,OADS,KAAK,WAAW,IAAI,OAAO,KAAK,KAC5B,mBAAmB,OAAO;EAC3C,KAAK,WAAW,IAAI,SAAS,GAAG;EAEhC,IAAI,UAAU;EAEd,IAAI,UAAU,UAAU,KAAA,GAAW;GAC/B,MAAM,SAAS,MAAM,KAAK,GAAG,QAAQ,GAAG;;kCAElB,QAAQ;mEACyB,UAAU,QAAQ,IAAK;aAC7E;GACD,WAAW,OAAO,YAAY;EAClC;EAEA,IAAI,UAAU,UAAU,KAAA,KAAa,UAAU,QAAQ,GAAG;GAKtD,MAAM,SAAS,MAAM,KAAK,GAAG,QAAQ,GAAG;;kCAElB,QAAQ;;;wCAGF,QAAQ;;+BAEjB,KAAK,MAAM,UAAU,KAAK,EAAE;;aAE9C;GACD,WAAW,OAAO,YAAY;EAClC;EAEA,OAAO;CACX;;CAGA,QAAc;EACV,KAAK,SAAS,MAAM;EACpB,KAAK,WAAW,MAAM;CAC1B;AACJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC/XA,IAAa,uBAAb,MAAkC;CAIT;CACA;CAJrB,cAAsB;CAEtB,YACI,IACA,YACF;EAFmB,KAAA,KAAA;EACA,KAAA,aAAA;CAClB;;;;;;;;;;;;;;;;;CAkBH,MAAM,eAA8B;EAChC,IAAI,KAAK,aAAa;EAEtB,MAAM,MAAM,uBAAuB,KAAK,IAAI,kBAAkB;EAE9D,MAAM,IAAI,aAAa,iBAAiB,oCAAoC;EAM5E,MAAM,IAAI,aAAa,0BAA0B;;;;;;;;;SAShD;EAGD,MAAM,IAAI,aAAa,oCAAoC;;;SAG1D;EAeD,IAAI,MAAM,IAAI,WAAW,yBAAyB,GAAG;GACjD,MAAM,IAAI,KAAK,iCACX,KAAK,GAAG,QAAQ,IAAI,IAAI,uBAAuB,UAAU,kBAAkB,CAAC,CAAC,CACjF;GACA,KAAK,cAAc;EACvB;CACJ;;CAGA,MAAM,MAAM,SAAiB,UAAkB,OAA+C;EAC1F,MAAM,KAAK,GAAG,QAAQ,GAAG;;sBAEX,QAAQ,IAAI,SAAS,IAAI,KAAK,WAAW,IAAI,KAAK,UAAU,SAAS,CAAC,CAAC,EAAE;;;;;SAKtF;CACL;;CAGA,MAAM,OAAO,SAAiB,UAAiC;EAC3D,MAAM,KAAK,GAAG,QAAQ,GAAG;;8BAEH,QAAQ,mBAAmB,SAAS;SACzD;CACL;;CAGA,MAAM,aAAa,UAAiC;EAChD,MAAM,KAAK,GAAG,QAAQ,GAAG;oEACmC,SAAS;SACpE;CACL;;CAGA,MAAM,OAAO,SAAmE;EAC5E,MAAM,SAAS,MAAM,KAAK,GAAG,QAAQ,GAAG;mFACmC,QAAQ;SAClF;EAED,MAAM,YAAqD,CAAC;EAC5D,KAAK,MAAM,OAAO,OAAO,MACrB,UAAU,IAAI,aAAa,IAAI,SAAS,CAAC;EAE7C,OAAO;CACX;;;;;;;;;;;CAYA,MAAM,WAAW,OAAuC;EAQpD,QAAQ,MAPa,KAAK,GAAG,QAAQ,GAAG;;mCAEb,KAAK,WAAW;8DACW,QAAQ,IAAK;;SAElE,EAAA,CAEc,KACV,KAAI,SAAQ;GAAE,SAAS,IAAI;GAAS,UAAU,IAAI;GAAW,OAAO,IAAI,SAAS,CAAC;EAAE,EAAE;CAC/F;;;;;CAMA,MAAM,iBAAgC;EAClC,MAAM,KAAK,GAAG,QAAQ,GAAG;sEACqC,KAAK,WAAW;SAC7E;CACL;AACJ;;;;;;;;;;;AC3JA,IAAa,mBAAb,MAA8B;CAC1B,OAAgB;CAChB,gBAAyB;CAEzB,MAAM,QAAuB,CAA2B;CAExD,MAAM,UAAyB,CAA8B;CAE7D,MAAM,OAAsB,CAA2B;AAC3D;;AAGA,SAAgB,gBAAgB,OAAgC;CAC5D,OAAO,OAAO,WAAW,KAAK,UAAU,KAAK,GAAG,MAAM;AAC1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACRA,IAAa,6BAA6B;;;;;;AAO1C,IAAa,8BAA8B;;;;;;;;AAS3C,IAAa,0BAA0B;;AAGvC,IAAM,wBAAwB;;AAE9B,IAAM,uBAAuB;AAS7B,IAAa,qBAAb,MAAsD;CAmB7B;CACA;CAnBrB,OAAgB;CAChB,gBAAyB;CAEzB;CACA;;;;;;;CAQA,UAAkC,CAAC;CACnC,eAAuB;CACvB;CACA,UAAkB;CAElB,YACI,IACA,kBACA,UAAsC,CAAC,GACzC;EAHmB,KAAA,KAAA;EACA,KAAA,mBAAA;EAGjB,MAAM,aAAa,QAAQ;EAC3B,KAAK,gBAAgB,OAAO,eAAe,YAAY,cAAc,IAC/D,aAAA;CAEV;CAEA,MAAM,MAAM,SAA2C;EACnD,KAAK,UAAU;EACf,KAAK,WAAW,IAAI,iBAAiB;GACjC,kBAAkB,KAAK;GACvB,SAAS;GACT,UAAU;GACV,WAAW,OAAO,YAAY;IAC1B,MAAM,SAAS,uBAAuB,OAAO;IAC7C,IAAI,CAAC,OAAO,QAAQ;KAChB,OAAO,KAAK,+CAA+C;KAC3D;IACJ;IAGA,KAAK,MAAM,SAAS,QAAQ,MAAM,QAAQ,KAAK;GACnD;EACJ,CAAC;EACD,MAAM,KAAK,SAAS,MAAM;CAC9B;;;;;;;;;;;;;;;CAgBA,MAAM,QAAQ,OAAuC;EACjD,IAAI,KAAK,kBAAkB,KAAK,KAAK,SAAS;GAC1C,MAAM,KAAK,KAAK,CAAC,KAAK,CAAC;GACvB;EACJ;EAEA,IAAI,CAAC,KAAK,aAAa;GACnB,KAAK,WAAW;GAChB,MAAM,KAAK,KAAK,CAAC,KAAK,CAAC;GACvB;EACJ;EAEA,MAAM,QAAQ,gBAAgB,KAAK,IAAI;EAIvC,IAAI,KAAK,QAAQ,UAAU,KAAK,eAAe,QAAQ,KAAK,eACxD,KAAK,MAAM;EAGf,OAAO,IAAI,SAAe,SAAS,WAAW;GAC1C,KAAK,QAAQ,KAAK;IAAE;IAAO;IAAO;IAAS;GAAO,CAAC;GACnD,KAAK,gBAAgB;EACzB,CAAC;CACL;CAEA,MAAM,OAAsB;EACxB,KAAK,UAAU;EACf,IAAI,KAAK,aAAa;GAClB,aAAa,KAAK,WAAW;GAC7B,KAAK,cAAc,KAAA;EACvB;EAIA,KAAK,MAAM;EACX,MAAM,KAAK,UAAU,KAAK;EAC1B,KAAK,WAAW,KAAA;CACpB;CAEA,aAA2B;EACvB,KAAK,cAAc,iBAAiB;GAChC,KAAK,cAAc,KAAA;GACnB,IAAI,KAAK,QAAQ,QAAQ;IAGrB,KAAK,MAAM;IACX,KAAK,WAAW;GACpB;EAGJ,GAAG,KAAK,aAAa;EAGrB,KAAM,YAAkD,QAAQ;CACpE;;CAGA,QAAsB;EAClB,IAAI,CAAC,KAAK,QAAQ,QAAQ;EAE1B,MAAM,QAAQ,KAAK;EACnB,KAAK,UAAU,CAAC;EAChB,KAAK,eAAe;EAEpB,KAAK,KAAK,MAAM,KAAI,MAAK,EAAE,KAAK,CAAC,CAAC,CAC7B,WAAW;GAAE,KAAK,MAAM,KAAK,OAAO,EAAE,QAAQ;EAAG,CAAC,CAAC,CACnD,OAAO,UAAU;GAAE,KAAK,MAAM,KAAK,OAAO,EAAE,OAAO,KAAK;EAAG,CAAC;CACrE;;;;;;;;;;;CAYA,MAAc,KAAK,QAA0C;EACzD,IAAI,CAAC,OAAO,QAAQ;EACpB,MAAM,UAAU,OAAO,WAAW,IAC5B,KAAK,UAAU,OAAO,EAAE,IACxB,KAAK,UAAU,EAAE,OAAO,OAAO,CAAC;EAEtC,MAAM,KAAK,GAAG,QAAQ,GAAG,oBAAoB,2BAA2B,IAAI,QAAQ,EAAE;CAC1F;AACJ;;;;;;;;;AAUA,SAAgB,uBAAuB,SAAoC;CACvE,IAAI;CACJ,IAAI;EACA,SAAS,KAAK,MAAM,OAAO;CAC/B,QAAQ;EACJ,OAAO,CAAC;CACZ;CACA,IAAI,CAAC,UAAU,OAAO,WAAW,UAAU,OAAO,CAAC;CAEnD,MAAM,QAAS,OAA+B;CAC9C,IAAI,MAAM,QAAQ,KAAK,GACnB,OAAO,MACF,KAAI,UAAS,YAAY,KAAK,CAAC,CAAC,CAChC,QAAQ,UAAoC,UAAU,IAAI;CAGnE,MAAM,SAAS,YAAY,MAAM;CACjC,OAAO,SAAS,CAAC,MAAM,IAAI,CAAC;AAChC;;;;;AAMA,SAAgB,qBAAqB,SAAyC;CAC1E,IAAI;EACA,OAAO,YAAY,KAAK,MAAM,OAAO,CAAC;CAC1C,QAAQ;EACJ,OAAO;CACX;AACJ;AAEA,SAAS,YAAY,OAAwC;CACzD,IAAI,CAAC,SAAS,OAAO,UAAU,UAAU,OAAO;CAEhD,MAAM,MAAM;CACZ,MAAM,MAAM,OAAO,IAAI,QAAQ,WAAW,IAAI,MAAM,KAAA;CACpD,MAAM,UAAU,OAAO,IAAI,YAAY,WAAW,IAAI,UAAU,KAAA;CAChE,IAAI,CAAC,OAAO,CAAC,SAAS,OAAO;CAE7B,QAAQ,IAAI,MAAZ;EACI,KAAK;GACD,IAAI,OAAO,IAAI,UAAU,UAAU,OAAO;GAC1C,OAAO;IACH,MAAM;IACN;IACA;IACA,OAAO,IAAI;IACX,MAAM,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO,KAAA;IAChD,KAAK,OAAO,IAAI,QAAQ,WAAW,IAAI,MAAM,KAAA;IAC7C,SAAS,IAAI;GACjB;EACJ,KAAK;GACD,IAAI,OAAO,IAAI,QAAQ,UAAU,OAAO;GACxC,OAAO;IACH,MAAM;IACN;IACA;IACA,MAAM,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO,KAAA;IAChD,KAAK,IAAI;GACb;EACJ,KAAK,iBACD,OAAO;GACH,MAAM;GACN;GACA;GACA,OAAQ,IAAI,SAAS,CAAC;GACtB,QAAS,IAAI,UAAU,CAAC;EAC5B;EACJ,SACI,OAAO;CACf;AACJ;;;;;;;;;;;;ACtPA,SAAgB,yBAAyB,YAAmD;CACxF,MAAM,OAAO,QAAQ,IAAI,wBAAwB,GAAA,CAAI,KAAK,CAAC,CAAC,YAAY;CAExE,IAAI,qBAAqB,UAAU,GAAG;EAClC,IAAI,OAAO,QAAQ,WAAW,MAC1B,OAAO,KACH,yCAAyC,IAAI,iDACzC,WAAW,KAAK,+EACxB;EAEJ,OAAO;CACX;CAEA,IAAI,CAAC,KAAK,OAAO,cAAc,EAAE,MAAM,SAAS;CAEhD,IAAI,QAAQ,YAAY,QAAQ,YAAY;EACxC,OAAO,KACH,uDAAuD,IAAI,uJAG/D;EACA,OAAO,cAAc,EAAE,MAAM,SAAS;CAC1C;CAIA,IAAI,YAAY,SAAS,KAAK,OAAO;CACrC,OAAO,QAAQ,WAAW,EAAE,MAAM,SAAS,IAAI,EAAE,MAAM,WAAW;AACtE;;;;;;;;;;AAWA,SAAgB,iBAAiB,SAA4B,MAAkC;CAC3F,IAAI,qBAAqB,OAAO,GAAG,OAAO;CAE1C,QAAQ,QAAQ,MAAhB;EACI,KAAK,YAAY;GACb,MAAM,mBAAmB,QAAQ,oBAAoB,KAAK;GAC1D,IAAI,CAAC,kBAAkB;IACnB,OAAO,KACH,qMAGJ;IACA,OAAO,IAAI,iBAAiB;GAChC;GACA,OAAO,IAAI,mBAAmB,KAAK,IAAI,kBAAkB,EACrD,eAAe,QAAQ,cAC3B,CAAC;EACL;EAEA,SACI,OAAO,IAAI,iBAAiB;CACpC;AACJ;;;;ACzFA,IAAM,oBAAoB;;;;;;;AAsH1B,IAAa,kBAAb,MAAa,wBAAwB,aAAyC;CAwItD;CAAiC;;;;;;CAlIrD,mBAAmC;CAEnC,0BAAkB,IAAI,IAAuB;CAG7C,2BAAmB,IAAI,IAAyB;CAGhD,2BAAmB,IAAI,IAA+E;;;;;;;;CAStG;;;;;;;;;;;;CAaA,oCAA4B,IAAI,IAA2B;;;;;;;;CAS3D,MAA0B,IAAI,iBAAiB;;;;;;;;CAS/C;;CAGA;;;;;CAMA,2CAAmC,IAAI,IAAY;;;;;;CAOnD;;;;;;;;;CAUA,sBAA8B;;CAG9B,kBAA0B;CAE1B;CACA,OAAwB,sBAAsB;;CAE9C,OAAwB,6BAA6B;CACrD;CAEA,iCAAyB,IAAI,IAA0B;CAGvD,wCAAgC,IAAI,IAAwF;CAE5H;;CAIA,aAA8B,QAAQ,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC;;CAE7D;;CAEA;;CAEA,eAAuB;;CAEvB;;CAEA,gCAAwB,IAAI,IAA2C;;CAEvE,OAAwB,sBAAsB;;CAI9C;;CAEA,YAAoB;;CAEpB;;CAGA;;;;;;;;;CASA,iCAAyB,IAAI,IAAoB;;CAEjD,OAAwB,sBAAsB;CAE9C,YAAY,IAAiC,UAA8C;EACvF,MAAM;EADU,KAAA,KAAA;EAAiC,KAAA,WAAA;EAEjD,KAAK,cAAc,IAAI,YAAY,IAAI,QAAQ;CACnD;;;;;;;;CASA;;CAGA,OAAwB,QAAA,QAAA,IAAA,aAAiC;CACzD,SAAiB,GAAG,MAAiB;EACjC,IAAI,gBAAgB,OAAO,QAAQ,MAAM,GAAG,IAAI;CACpD;CAEA,cAAc,QAAoB;EAC9B,KAAK,SAAS;CAClB;CAGA,IAAI,gBAAgB;EAChB,OAAO,KAAK;CAChB;;;;;;;;;;;;;;;;;;;CAoBA,cAAsB,gBAAwB,cAA2C;EACrF,MAAM,MAAM,EAAE,aAAa;EAC3B,aAAa;GACT,IAAI,KAAK,eAAe,IAAI,cAAc,MAAM,cAAc,OAAO;GACrE,IAAI,OAAO,aAAa,WAAW,OAAO;GAC1C,aAAa,YAAY;GACzB,OAAO;EACX;CACJ;CAGA,+BAA+B,gBAAwB,cAOpD;EACC,KAAK,SAAS,6DAA6D,gBAAgB,aAAa,cAAc,gBAAgB,WAAW;EACjJ,KAAK,eAAe,IAAI,gBAAgB;GAAE,GAAG;GAAc,SAAS;GAAG,WAAW;EAAE,CAAC;CACzF;CAGA,wBAAwB,gBAAwB,UAAsF;EAClI,KAAK,SAAS,0DAA0D,cAAc;EACtF,KAAK,sBAAsB,IAAI,gBAAgB,QAAQ;CAC3D;CAEA,2BAA2B,gBAAwB;EAC/C,KAAK,SAAS,4DAA4D,cAAc;EACxF,KAAK,sBAAsB,OAAO,cAAc;CACpD;;;;CASA,sBACI,gBACA,QACA,UACI;EACJ,KAAK,eAAe,IAAI,gBAAgB;GACpC,UAAU,OAAO;GACjB,MAAM;GACN,MAAM,OAAO;GACb,mBAAmB;IACf,QAAQ,OAAO;IACf,SAAS,OAAO;IAChB,OAAO,OAAO;IACd,OAAO,OAAO;IACd,YAAY,OAAO;IACnB,YAAY,OAAO;IACnB,cAAc,OAAO;IACrB,eAAe,OAAO;GAC1B;GACA,SAAS;GACT,WAAW;EACf,CAAC;EAED,IAAI,UACA,KAAK,sBAAsB,IAAI,gBAAgB,QAAsF;CAE7I;;;;CAKA,eACI,gBACA,QACA,UACI;EACJ,KAAK,eAAe,IAAI,gBAAgB;GACpC,UAAU,OAAO;GACjB,MAAM;GACN,MAAM,OAAO;GACb,IAAI,OAAO;GACX,SAAS;GACT,WAAW;EACf,CAAC;EAED,IAAI,UACA,KAAK,sBAAsB,IAAI,gBAAgB,QAAsF;CAE7I;;;;CAKA,YAAY,gBAA8B;EACtC,KAAK,eAAe,OAAO,cAAc;EACzC,KAAK,sBAAsB,OAAO,cAAc;CACpD;CAMA,UAAU,UAAkB,IAAe;EACvC,KAAK,QAAQ,IAAI,UAAU,EAAE;EAE7B,GAAG,GAAG,eAAe;GACjB,KAAK,aAAa,QAAQ;EAC9B,CAAC;EAED,GAAG,GAAG,UAAU,UAAU;GACtB,OAAO,MAAM,8BAA8B;IAAE,QAAQ;IAAU;GAAM,CAAC;GACtE,KAAK,aAAa,QAAQ;EAC9B,CAAC;CACL;CAGA,MAAM,oBAAoB,UAAkB,SAA2B,aAAuC;EAC1G,MAAM,KAAK,cAAc,UAAU,SAAS,WAAW;CAC3D;CAEA,MAAM,aAAa,UAAkB;EACjC,KAAK,QAAQ,OAAO,QAAQ;EAG5B,KAAK,MAAM,CAAC,gBAAgB,iBAAiB,KAAK,eAAe,QAAQ,GACrE,IAAI,aAAa,aAAa,UAAU;GACpC,KAAK,eAAe,OAAO,cAAc;GACzC,KAAK,sBAAsB,OAAO,cAAc;GAGhD,KAAK,MAAM,UAAU;IAAC;IAAO;IAAQ;IAAQ;GAAO,GAAG;IACnD,MAAM,MAAM,GAAG,SAAS;IACxB,MAAM,QAAQ,KAAK,cAAc,IAAI,GAAG;IACxC,IAAI,OAAO;KAAE,aAAa,KAAK;KAAG,KAAK,cAAc,OAAO,GAAG;IAAG;GACtE;EACJ;EAIJ,KAAK,MAAM,CAAC,SAAS,YAAY,KAAK,SAAS,QAAQ,GACnD,IAAI,QAAQ,IAAI,QAAQ,GAAG;GACvB,QAAQ,OAAO,QAAQ;GACvB,KAAK,eAAe,UAAU,SAAS,EAAE,WAAW,KAAK,CAAC;GAC1D,IAAI,QAAQ,SAAS,GAAG,KAAK,SAAS,OAAO,OAAO;EACxD;EAIJ,KAAK,MAAM,CAAC,YAAY,KAAK,UACzB,KAAK,eAAe,UAAU,SAAS,EAAE,WAAW,KAAK,CAAC;EAK9D,KAAU,sBAAsB,KAAK,cAAe,aAAa,QAAQ,GAAG,gBAAgB;CAChG;CAEA,MAAc,cAAc,UAAkB,SAA2B,aAAuC;EAC5G,MAAM,UAAU,QAAQ;EACxB,QAAQ,QAAQ,MAAhB;GACI,KAAK;IACD,MAAM,KAAK,6BAA6B,UAAU,QAAQ,SAA0C,WAAW;IAC/G;GACJ,KAAK;IACD,MAAM,KAAK,yBAAyB,UAAU,QAAQ,SAAsC,WAAW;IACvG;GACJ,KAAK;IACD,MAAM,KAAK,kBAAkB,UAAU,QAAQ,cAAe;IAC9D;GAOJ,KAAK;GACL,KAAK;GACL,KAAK;GACL,KAAK;GACL,KAAK;GACL,KAAK;GACL,KAAK;IACD,MAAM,KAAK,qBAAqB,UAAU,QAAQ,MAAM,SAAS,WAAW;IAC5E;GAEJ,SACI,KAAK,UAAU,UAAU,0BAA0B,QAAQ,MAAM,QAAQ,cAAc;EAC/F;CACJ;CAEA,MAAc,6BAA6B,UAAkB,SAAwC,aAAuC;EACxI,MAAM,iBAAiB,QAAQ;EAE/B,IAAI;GAGA,IAAI,CADe,KAAK,SAAS,oBAAoB,QAAQ,IACxD,GAAY;IACb,MAAM,aAAa,KAAK,SAAS,eAAe,CAAC,CAAC,KAAI,MAAK,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI;IAC5E,MAAM,MAAM,0BAA0B,QAAQ,KAAK,kBAAkB,WAAW;IAChF,OAAO,MAAM,qBAAqB,KAAK;IACvC,KAAK,UAAU,UAAU,KAAK,cAAc;IAC5C;GACJ;GAQA,IAAI,QAAQ,cAAc;IACtB,MAAM,MACF;IAGJ,OAAO,KAAK,qBAAqB,KAAK;IACtC,KAAK,UAAU,UAAU,KAAK,gBAAgB,wBAAwB;IACtE;GACJ;GAaA,IAAI;GACJ,IAAI;IACA,eAAe,uBAAuB,QAAQ,KAAK;GACvD,SAAS,GAAG;IACR,IAAI,EAAE,aAAa,iBAAiB,MAAM;IAC1C,OAAO,KAAK,8CAA8C,QAAQ,KAAK,KAAK,EAAE,SAAS;IACvF,KAAK,UAAU,UAAU,EAAE,SAAS,gBAAgB,eAAe;IACnE;GACJ;GAQA,IAAI;GACJ,IAAI;IACA,UAAU,uBAAuB,QAAQ,SAAS,QAAQ,KAAK;GACnE,SAAS,GAAG;IACR,IAAI,EAAE,aAAa,mBAAmB,MAAM;IAC5C,OAAO,KAAK,8CAA8C,QAAQ,KAAK,KAAK,EAAE,SAAS;IACvF,KAAK,UAAU,UAAU,EAAE,SAAS,gBAAgB,EAAE,IAAI;IAC1D;GACJ;GAGA,MAAM,eAA6B;IAC/B;IACA,MAAM;IACN,MAAM,QAAQ;IACd,mBAAmB;KACf,QAAQ,QAAQ;KAChB,SAAS,QAAQ;KACjB;KACA,OAAO,QAAQ;KACf,OAAO;KACP,QAAQ,QAAQ;KAChB,YAAY,QAAQ;KACpB,YAAY,QAAQ,YAAY;KAChC,cAAc,QAAQ;KACtB,eAAe,QAAQ;IAC3B;IACA;IACA,SAAS;IACT,WAAW;GACf;GACA,KAAK,eAAe,IAAI,gBAAgB,YAAY;GAMpD,MAAM,aAAa,KAAK,cAAc,gBAAgB,YAAY;GAKlE,MAAM,OAAO,MAAM,KAAK,wBACpB,QAAQ,MACR,aAAa,mBACb,WACJ;GAEA,IAAI,WAAW,GACX,KAAK,qBAAqB,UAAU,gBAAgB,MAAM,QAAQ,IAAI;EAG9E,SAAS,OAAO;GACZ,MAAM,YAAY,uBAAuB,OAAO,QAAQ,IAAI;GAC5D,KAAK,UAAU,UAAU,UAAU,SAAS,gBAAgB,UAAU,IAAI;EAC9E;CACJ;CAEA,MAAc,yBAAyB,UAAkB,SAAoC,aAAuC;EAChI,MAAM,iBAAiB,QAAQ;EAE/B,IAAI;GAGA,IAAI,CADe,KAAK,SAAS,oBAAoB,QAAQ,IACxD,GAAY;IACb,MAAM,aAAa,KAAK,SAAS,eAAe,CAAC,CAAC,KAAI,MAAK,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI;IAC5E,MAAM,MAAM,0BAA0B,QAAQ,KAAK,kBAAkB,WAAW;IAChF,OAAO,MAAM,qBAAqB,KAAK;IACvC,KAAK,UAAU,UAAU,KAAK,cAAc;IAC5C;GACJ;GAGA,MAAM,eAA6B;IAC/B;IACA,MAAM;IACN,MAAM,QAAQ;IACd,IAAI,QAAQ;IACZ;IACA,SAAS;IACT,WAAW;GACf;GACA,KAAK,eAAe,IAAI,gBAAgB,YAAY;GAKpD,MAAM,aAAa,KAAK,cAAc,gBAAgB,YAAY;GAGlE,MAAM,MAAM,MAAM,KAAK,oBACnB,QAAQ,MACR,OAAO,QAAQ,EAAE,GACjB,WACJ;GAEA,IAAI,WAAW,GACX,KAAK,iBAAiB,UAAU,gBAAgB,OAAO,IAAI;EAGnE,SAAS,OAAO;GACZ,MAAM,YAAY,uBAAuB,OAAO,QAAQ,IAAI;GAC5D,KAAK,UAAU,UAAU,UAAU,SAAS,gBAAgB,UAAU,IAAI;EAC9E;CACJ;CAEA,MAAc,kBAAkB,WAAmB,gBAAwB;EACvE,KAAK,eAAe,OAAO,cAAc;EACzC,KAAK,sBAAsB,OAAO,cAAc;EAEhD,KAAK,MAAM,UAAU;GAAC;GAAO;GAAQ;GAAQ;EAAO,GAAG;GACnD,MAAM,MAAM,GAAG,SAAS;GACxB,MAAM,QAAQ,KAAK,cAAc,IAAI,GAAG;GACxC,IAAI,OAAO;IAAE,aAAa,KAAK;IAAG,KAAK,cAAc,OAAO,GAAG;GAAG;EACtE;CACJ;;;;;;;;;;;;CAaA,MAAM,aAAa,MAAc,IAAY,KAAqC,YAAqB,YAAY,MAAM,SAAwB,OAAO;EACpJ,KAAK,SAAS,sDAAsD,MAAM,OAAO,IAAI,aAAa,QAAQ,MAAM,WAAW,MAAM;EAOjI,IAAI,KAAK,WAAW;GAChB,MAAM,MAAM,KAAK,SAAS,MAAM,IAAI,UAAU;GAC9C,IAAI,WAAW;QACP,KAAK,eAAe,GAAG,GAAG;KAC1B,KAAK,SAAS,oEAAoE,GAAG;KACrF;IACJ;UAEA,KAAK,YAAY,GAAG;EAE5B;EAGA,MAAM,gBAAgB,CAAC,IAAI;EAG3B,IAAI,KAAK,SAAS,GAAG,KAAK,KAAK,MAAM,GAAG,CAAC,CAAC,SAAS,GAAG;GAClD,MAAM,cAAc,KAAK,eAAe,IAAI;GAC5C,cAAc,KAAK,GAAG,WAAW;GACjC,KAAK,SAAS,iEAAiE,cAAc,KAAK,IAAI,GAAG;EAC7G;EAGA,KAAK,MAAM,cAAc,eACrB,MAAM,KAAK,iBAAiB,YAAY,MAAM,IAAI,KAAK,UAAU;EAOrE,IAAI,aAAa,KAAK,gBAAgB,CAAC,KAAK,WACxC,IAAI;GACA,MAAM,KAAK,gBAAgB,MAAM,IAAI,UAAU;EACnD,SAAS,KAAK;GACV,OAAO,MAAM,gEAAgE,EAAE,OAAO,IAAI,CAAC;EAC/F;EAGJ,KAAK,SAAS,yDAAyD,IAAI;CAC/E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCA,MAAc,iBAAiB,YAAoB,cAAsB,IAAY,KAAqC,aAAsB;EAC5I,KAAK,SAAS,wCAAwC,WAAW,cAAc,aAAa,EAAE;EAG9F,MAAM,mBAAmB,MAAM,KAAK,KAAK,eAAe,QAAQ,CAAC,CAAC,CAAC,QAAQ,GAAG,SAAS;GACnF,MAAM,cAAc,IAAI,SAAS;GAGjC,IAAI,IAAI,SAAS,UACb,OAAO,gBAAgB,eAAe,eAAe,IAAI,OAAO,KAAK;GAGzE,IAAI,IAAI,SAAS,cACb,OAAO;GAEX,OAAO;EACX,CAAC;EAED,KAAK,SAAS,8BAA8B,iBAAiB,OAAO,2BAA2B,YAAY;EAG3G,MAAM,yBAAyB,iBAAiB,QAAQ,GAAG,SACvD,IAAI,aAAa,YAAY,KAAK,QAAQ,IAAI,IAAI,QAAQ,CAC9D;EAEA,MAAM,sBAAsB,iBAAiB,QAAQ,CAAC,gBAAgB,SAClE,IAAI,aAAa,YAAY,KAAK,sBAAsB,IAAI,cAAc,CAC9E;EAGA,KAAK,MAAM,CAAC,gBAAgB,iBAAiB,wBACzC,IAAI;GACA,IAAI,aAAa,SAAS,YAAY,eAAe,cACjD,KAAK,uBAAuB,gBAAgB,YAAY,IAAI,YAAY;QACrE,IAAI,aAAa,SAAS,gBAAgB,aAAa,mBAC1D,KAAK,2BAA2B,gBAAgB,YAAY,YAAY;EAEhF,SAAS,OAAO;GACZ,MAAM,YAAY,uBAAuB,OAAO,UAAU;GAC1D,KAAK,UAAU,aAAa,UAAU,UAAU,SAAS,gBAAgB,UAAU,IAAI;EAC3F;EAIJ,KAAK,MAAM,CAAC,gBAAgB,iBAAiB,qBACzC,IAAI;GACA,MAAM,WAAW,KAAK,sBAAsB,IAAI,cAAc;GAC9D,IAAI,CAAC,UAAU;GAEf,IAAI,aAAa,SAAS,YAAY,eAAe,cACjD,KAAK,6BAA6B,gBAAgB,YAAY,IAAI,cAAc,QAAQ;QACrF,IAAI,aAAa,SAAS,gBAAgB,aAAa,mBAE1D,KAAK,uBAAuB,gBAAgB,YAAY,cAAc,QAAQ;EAEtF,SAAS,OAAO;GACZ,OAAO,MAAM,gEAAgE,kBAAkB,EAAS,MAAM,CAAC;EACnH;CAER;;;;;CAMA,2BACI,gBACA,YACA,cACF;EACE,MAAM,WAAW,MAAM;EACvB,MAAM,WAAW,KAAK,cAAc,IAAI,QAAQ;EAChD,IAAI,UAAU,aAAa,QAAQ;EAEnC,KAAK,cAAc,IAAI,UAAU,WAAW,YAAY;GACpD,KAAK,cAAc,OAAO,QAAQ;GAKlC,IAAI,KAAK,eAAe,IAAI,cAAc,MAAM,cAAc;GAG9D,MAAM,aAAa,KAAK,cAAc,gBAAgB,YAAY;GAClE,IAAI;IACA,MAAM,OAAO,MAAM,KAAK,wBAAwB,YAAY,aAAa,mBAAoB,aAAa,WAAW;IACrH,IAAI,WAAW,GACX,KAAK,qBAAqB,aAAa,UAAU,gBAAgB,MAAM,UAAU;GAEzF,SAAS,OAAO;IACZ,MAAM,YAAY,uBAAuB,OAAO,UAAU;IAC1D,KAAK,UAAU,aAAa,UAAU,UAAU,SAAS,gBAAgB,UAAU,IAAI;GAC3F;EACJ,GAAG,gBAAgB,mBAAmB,CAAC;CAC3C;;;;CAKA,uBACI,gBACA,YACA,cACA,UACF;EACE,MAAM,WAAW,OAAO;EACxB,MAAM,WAAW,KAAK,cAAc,IAAI,QAAQ;EAChD,IAAI,UAAU,aAAa,QAAQ;EAEnC,KAAK,cAAc,IAAI,UAAU,WAAW,YAAY;GACpD,KAAK,cAAc,OAAO,QAAQ;GAClC,IAAI,KAAK,eAAe,IAAI,cAAc,MAAM,cAAc;GAC9D,MAAM,aAAa,KAAK,cAAc,gBAAgB,YAAY;GAClE,IAAI;IACA,MAAM,OAAO,MAAM,KAAK,wBAAwB,YAAY,aAAa,mBAAoB,aAAa,WAAW;IACrH,IAAI,WAAW,GAAG,SAAS,IAAI;GACnC,SAAS,OAAO;IACZ,OAAO,MAAM,6DAA6D,kBAAkB,EAAS,MAAM,CAAC;GAChH;EACJ,GAAG,gBAAgB,mBAAmB,CAAC;CAC3C;;;;;;CAOA,MAAc,wBACV,YACA,mBACA,aACkC;EAClC,IAAI,KAAK,QAAQ;GACb,MAAM,aAAa,KAAK,SAAS,oBAAoB,UAAU;GAkB/D,MAAM,aAAa,eAAe;IAAE,KAAA;IAChD,OAAO,CAAC,MAAM;GAAE;GACJ,OAAO,MAAM,KAAK,GAAG,YAAY,OAAO,OAAO;IAC3C,MAAM,iBAAiB,IAAI;KAAE,KAAK,WAAW;KAAK,OAAO,WAAW;IAAM,GAAG,KAAK,WAAW;IAC7F,MAAM,kBAAkB,IAAI,YAAY,IAAI,KAAK,QAAQ;IACzD,IAAI;IACJ,IAAI,kBAAkB,cAClB,kBAAkB,MAAM,gBAAgB,WACpC,YACA,kBAAkB,cAClB;KACI,QAAQ,kBAAkB;KAI1B,SAAS,kBAAkB;KAC3B,SAAS,kBAAkB;KAC3B,OAAO,kBAAkB;KACzB,OAAO,kBAAkB;KACzB,YAAY,kBAAkB;KAC9B,eAAe,kBAAkB;IACrC,CACJ;SAEA,kBAAkB,MAAM,gBAAgB,gBAAgB,YAAY;KAChE,QAAQ,kBAAkB;KAC1B,SAAS,kBAAkB;KAC3B,SAAS,kBAAkB;KAC3B,OAAO,kBAAkB;KACzB,OAAO,kBAAkB;KACzB,QAAQ,kBAAkB;KAC1B,YAAY,kBAAkB;KAC9B,YAAY,kBAAkB;IAClC,CAAC;IAKL,MAAM,qBAAqB,KAAK,SAAS,oBAAoB,UAAU;IACvE,MAAM,qBAAqB,aAAa;KAAE,GAAG;KAC7D,GAAG;IAAmB,IAAwB;IAE9B,MAAM,YAAY,oBAAoB;IACtC,MAAM,kBAAkB,KAAK,UAAU,mBAAmB;IAC1D,MAAM,oBAAoB,oBAAoB,aAAa,uBAAuB,mBAAmB,UAAU,IAAI,KAAA;IAEnH,IAAI,iBAAiB,aAAa,WAAW,aAAa,mBAAmB,WAAW;KACpF,MAAM,qBAAqB;MACvB,MAAM;OAAE,KAAK,WAAW;OAChD,OAAO,WAAW;MAAM;MACA,QAAQ,KAAK;MACb,MAAO,KAAK,UAAU,UAAU,KAAK,SAAW,KAAK,OAA8B,OAAO,KAAA;KAC9F;KAEA,OAAO,MAAM,QAAQ,IAAI,gBAAgB,IAAI,OAAO,eAAe;MAC/D,IAAI,kBAAkB;MAEtB,IAAI,iBAAiB,WACjB,kBAAkB,MAAM,gBAAgB,UAAU;OAC9C,YAAY;OACZ,MAAM;OACN,KAAK;OACL,SAAS;MACb,CAAC,KAAK;MAGV,IAAI,WAAW,WACX,kBAAkB,MAAM,UAAU,UAAU;OACxC,YAAY;OACZ,MAAM;OACN,KAAK;OACL,SAAS;MACb,CAAC,KAAK;MAGV,IAAI,mBAAmB,WACnB,kBAAkB,MAAM,kBAAkB,UAAU;OAChD,YAAY;OACZ,MAAM;OACN,KAAK;OACL,SAAS;MACb,CAAC,KAAK;MAEV,OAAO;KACX,CAAC,CAAC;IACN;IAEA,OAAO;GACX,CAAC;EACL;EAMA,IAAI,kBAAkB,cAClB,OAAO,MAAM,KAAK,YAAY,WAC1B,YACA,kBAAkB,cAClB;GACI,QAAQ,kBAAkB;GAC1B,SAAS,kBAAkB;GAC3B,SAAS,kBAAkB;GAC3B,OAAO,kBAAkB;GACzB,OAAO,kBAAkB;GACzB,YAAY,kBAAkB;GAC9B,eAAe,kBAAkB;EACrC,CACJ;EAEJ,OAAO,MAAM,KAAK,YAAY,gBAAgB,YAAY;GACtD,QAAQ,kBAAkB;GAC1B,SAAS,kBAAkB;GAC3B,SAAS,kBAAkB;GAC3B,OAAO,kBAAkB;GACzB,OAAO,kBAAkB;GACzB,QAAQ,kBAAkB;GAC1B,YAAY,kBAAkB;GAC9B,YAAY,kBAAkB;EAClC,CAAC;CACL;;;;CAKA,uBACI,gBACA,YACA,IACA,cACF;EACE,MAAM,WAAW,OAAO;EACxB,MAAM,WAAW,KAAK,cAAc,IAAI,QAAQ;EAChD,IAAI,UAAU,aAAa,QAAQ;EAEnC,KAAK,cAAc,IAAI,UAAU,WAAW,YAAY;GACpD,KAAK,cAAc,OAAO,QAAQ;GAClC,IAAI,KAAK,eAAe,IAAI,cAAc,MAAM,cAAc;GAC9D,MAAM,aAAa,KAAK,cAAc,gBAAgB,YAAY;GAClE,IAAI;IACA,MAAM,MAAM,MAAM,KAAK,oBAAoB,YAAY,IAAI,aAAa,WAAW;IACnF,IAAI,WAAW,GACX,KAAK,iBAAiB,aAAa,UAAU,gBAAgB,OAAO,IAAI;GAEhF,SAAS,OAAO;IACZ,MAAM,YAAY,uBAAuB,OAAO,UAAU;IAC1D,KAAK,UAAU,aAAa,UAAU,UAAU,SAAS,gBAAgB,UAAU,IAAI;GAC3F;EACJ,GAAG,gBAAgB,mBAAmB,CAAC;CAC3C;;;;CAKA,6BACI,gBACA,YACA,IACA,cACA,UACF;EACE,MAAM,WAAW,QAAQ;EACzB,MAAM,WAAW,KAAK,cAAc,IAAI,QAAQ;EAChD,IAAI,UAAU,aAAa,QAAQ;EAEnC,KAAK,cAAc,IAAI,UAAU,WAAW,YAAY;GACpD,KAAK,cAAc,OAAO,QAAQ;GAClC,IAAI,KAAK,eAAe,IAAI,cAAc,MAAM,cAAc;GAC9D,MAAM,aAAa,KAAK,cAAc,gBAAgB,YAAY;GAClE,IAAI;IACA,MAAM,MAAM,MAAM,KAAK,oBAAoB,YAAY,IAAI,aAAa,WAAW;IACnF,IAAI,WAAW,GAAG,SAAS,OAAO,IAAI;GAC1C,SAAS,OAAO;IACZ,OAAO,MAAM,iEAAiE,kBAAkB,EAAS,MAAM,CAAC;GACpH;EACJ,GAAG,gBAAgB,mBAAmB,CAAC;CAC3C;;;;CAKA,MAAc,oBACV,YACA,IACA,aAC4C;EAC5C,IAAI,KAAK,QAAQ;GACb,MAAM,aAAa,KAAK,SAAS,oBAAoB,UAAU;GAS/D,MAAM,aAAa,eAAe;IAAE,KAAA;IAChD,OAAO,CAAC,MAAM;GAAE;GACJ,OAAO,MAAM,KAAK,GAAG,YAAY,OAAO,OAAO;IAC3C,MAAM,iBAAiB,IAAI;KAAE,KAAK,WAAW;KAAK,OAAO,WAAW;IAAM,GAAG,KAAK,WAAW;IAE7F,IAAI,kBAAkB,MAAM,IADA,YAAY,IAAI,KAAK,QACrB,CAAA,CAAgB,SAAS,YAAY,IAAI,YAAY,UAAU;IAE3F,IAAI,iBAAiB;KACjB,MAAM,qBAAqB,KAAK,SAAS,oBAAoB,UAAU;KACvE,MAAM,qBAAqB,aAAa;MAAE,GAAG;MACjE,GAAG;KAAmB,IAAwB;KAE1B,MAAM,YAAY,oBAAoB;KACtC,MAAM,kBAAkB,KAAK,UAAU,mBAAmB;KAC1D,MAAM,oBAAoB,oBAAoB,aAAa,uBAAuB,mBAAmB,UAAU,IAAI,KAAA;KAEnH,IAAI,iBAAiB,aAAa,WAAW,aAAa,mBAAmB,WAAW;MACpF,MAAM,qBAAqB;OACvB,MAAM;QAAE,KAAK,WAAW;QACpD,OAAO,WAAW;OAAM;OACI,QAAQ,KAAK;OACb,MAAO,KAAK,UAAU,UAAU,KAAK,SAAW,KAAK,OAA8B,OAAO,KAAA;MAC9F;MAGA,IAAI,iBAAiB,WACjB,kBAAkB,MAAM,gBAAgB,UAAU;OAC9C,YAAY;OACZ,MAAM;OACN,KAAK;OACL,SAAS;MACb,CAAC,KAAK;MAGV,IAAI,WAAW,WACX,kBAAkB,MAAM,UAAU,UAAU;OACxC,YAAY;OACZ,MAAM;OACN,KAAK;OACL,SAAS;MACb,CAAC,KAAK;MAGV,IAAI,mBAAmB,WACnB,kBAAkB,MAAM,kBAAkB,UAAU;OAChD,YAAY;OACZ,MAAM;OACN,KAAK;OACL,SAAS;MACb,CAAC,KAAK;KAEd;IACJ;IAEA,OAAO;GACX,CAAC;EACL;EAEA,OAAO,MAAM,KAAK,YAAY,SAAS,YAAY,EAAE;CACzD;CAEA,qBAA6B,UAAkB,gBAAwB,MAAiC,MAAc;EAClH,MAAM,UAAmC;GACrC,MAAM;GACN;GACM;GACN,KAAK,KAAK,mBAAmB,IAAI;EACrC;EACA,KAAK,YAAY,UAAU,OAAO;CACtC;CAEA,iBAAyB,UAAkB,gBAAwB,KAAqC;EACpG,MAAM,UAA+B;GACjC,MAAM;GACN;GACK;EACT;EACA,KAAK,YAAY,UAAU,OAAO;CACtC;;;;;;;;;;;CAYA,mBAA2B,MAA4C;EACnE,IAAI;GACA,MAAM,aAAa,KAAK,SAAS,oBAAoB,IAAI;GACzD,IAAI,CAAC,YAAY,OAAO,KAAA;GACxB,MAAM,OAAO,eAAe,YAAY,KAAK,QAAQ;GACrD,OAAO,KAAK,SAAS,IAAI,OAAO,KAAA;EACpC,QAAQ;GAKJ;EACJ;CACJ;CAEA,UAAkB,UAAkB,OAAe,gBAAyB,MAAe;EACvF,MAAM,UAAU;GACZ,MAAM;GACN;GACA,SAAS,EACL,OAAO,OAAO;IAAE,SAAS;IAAO;GAAK,IAAI,MAC7C;GACA;EACJ;EACA,KAAK,YAAY,UAAU,OAAO;CACtC;CAEA,YAAoB,UAAkB,SAAgK;EAClM,MAAM,SAAS,KAAK,QAAQ,IAAI,QAAQ;EACxC,IAAI,UAAU,OAAO,eAAe,UAAU,MAC1C,OAAO,KAAK,KAAK,UAAU,OAAO,CAAC;CAE3C;;;;;CAMA,eAAuB,MAAwB;EAC3C,MAAM,WAAW,KAAK,MAAM,GAAG,CAAC,CAAC,QAAO,MAAK,EAAE,SAAS,CAAC;EACzD,MAAM,cAAwB,CAAC;EAG/B,KAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK,GAAG;GACzC,MAAM,aAAa,SAAS,MAAM,GAAG,CAAC,CAAC,CAAC,KAAK,GAAG;GAChD,IAAI,YACA,YAAY,KAAK,UAAU;GAI/B,IAAI,IAAI,IAAI,SAAS,QAAQ;IACzB,MAAM,iBAAiB,SAAS,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG;IACxD,YAAY,KAAK,cAAc;GACnC;EACJ;EAEA,OAAO;CACX;;;;;;;;CAaA,qBAAqB,YAAiD;EAClE,KAAK,oBAAoB;CAC7B;;CAGA,OAAwB,kBAAiD;EACrE,cAAc;EACd,WAAW;EACX,iBAAiB;EACjB,gBAAgB;EAChB,gBAAgB;CACpB;;;;;;;;;;;CAYA,qBACI,UACA,MACA,SACA,aACoB;EACpB,MAAM,UAAU,SAAS;EAIzB,IAAI,SAAS,iBAAiB;GAC1B,KAAK,aAAa,UAAU,OAAO;GACnC;EACJ;EACA,IAAI,SAAS,oBAAoB;GAC7B,KAAK,eAAe,UAAU,OAAO;GACrC;EACJ;EAEA,MAAM,SAAS,gBAAgB,gBAAgB;EAC/C,MAAM,UAAU,KAAK,uBAAuB,UAAU,SAAS,QAAQ,WAAW;EAClF,IAAI,YAAY,OAAO;EACvB,IAAI,YAAY,MAAM,OAAO,KAAK,uBAAuB,UAAU,MAAM,SAAS,OAAO;EACzF,OAAO,QAAQ,MAAM,OAAO;GACxB,IAAI,IAAI,OAAO,KAAK,uBAAuB,UAAU,MAAM,SAAS,OAAO;EAC/E,CAAC;CACL;;CAGA,uBACI,UACA,MACA,SACA,SACoB;EACpB,QAAQ,MAAR;GACI,KAAK;IACD,KAAK,YAAY,UAAU,OAAO;IAClC;GACJ,KAAK;IACD,KAAK,mBAAmB,UAAU,SAAS,SAAS,OAAiB,SAAS,OAAO;IACrF;GACJ,KAAK,mBACD,OAAO,KAAK,4BACR,UACA,SACA,SAAS,UACT,SAAS,KACb;GACJ,KAAK;IAED,KAAK,YAAY,UAAU,OAAO;IAClC,KAAK,cAAc,UAAU,SAAS,SAAS,SAAoC,CAAC,CAAC;IACrF;GACJ,KAAK;IACD,KAAK,kBAAkB,UAAU,OAAO;IACxC;EACR;CACJ;;;;;;;;;;;;;;;;;CAkBA,uBACI,UACA,SACA,QACA,aAC0B;EAE1B,IAAI,WAAW,UAAU,CAAC,KAAK,SAAS,IAAI,OAAO,CAAC,EAAE,IAAI,QAAQ,GAAG;GACjE,KAAK,kBAAkB,UAAU,SAAS,QAAQ,6BAA6B;GAC/E,OAAO;EACX;EAEA,MAAM,aAAa,KAAK;EACxB,IAAI,CAAC,YAAY,OAAO;EAExB,IAAI;EACJ,IAAI;GACA,UAAU,WAAW;IAAE;IAAS;IAAQ;IAAU,MAAM;GAAY,CAAC;EACzE,SAAS,OAAO;GACZ,OAAO,MAAM,qCAAqC,OAAO,OAAO,QAAQ,eAAe,EAAE,MAAM,CAAC;GAChG,KAAK,kBAAkB,UAAU,SAAS,QAAQ,8BAA8B;GAChF,OAAO;EACX;EAEA,IAAI,OAAO,YAAY,WAAW;GAC9B,IAAI,CAAC,SAAS,KAAK,kBAAkB,UAAU,SAAS,QAAQ,mCAAmC;GACnG,OAAO;EACX;EAEA,OAAO,QAAQ,MACV,OAAO;GACJ,IAAI,CAAC,IAAI,KAAK,kBAAkB,UAAU,SAAS,QAAQ,mCAAmC;GAC9F,OAAO;EACX,IACC,UAAU;GACP,OAAO,MAAM,wCAAwC,OAAO,OAAO,QAAQ,eAAe,EAAE,MAAM,CAAC;GACnG,KAAK,kBAAkB,UAAU,SAAS,QAAQ,8BAA8B;GAChF,OAAO;EACX,CACJ;CACJ;;CAGA,kBAA0B,UAAkB,SAAiB,QAAuB,QAAsB;EACtG,KAAK,SAAS,yBAAyB,OAAO,OAAO,QAAQ,QAAQ,SAAS,IAAI,QAAQ;EAC1F,KAAK,UACD,UACA,WAAW,OAAO,eAAe,QAAQ,KAAK,UAC9C,KAAA,GACA,mBACJ;CACJ;;CAGA,YAAY,UAAkB,SAAuB;EACjD,IAAI,CAAC,KAAK,SAAS,IAAI,OAAO,GAC1B,KAAK,SAAS,IAAI,yBAAS,IAAI,IAAI,CAAC;EAExC,KAAK,SAAS,IAAI,OAAO,CAAC,CAAE,IAAI,QAAQ;EACxC,KAAK,8BAA8B;EACnC,KAAK,SAAS,yBAAyB,SAAS,mBAAmB,SAAS;CAChF;;;;;;;;;;;CAYA,gCAA8C;EAC1C,IAAI,KAAK,iBAAiB;EAC1B,IAAI,KAAK,IAAI,SAAS,YAAY,CAAC,KAAK,qBAAqB;EAC7D,KAAK,kBAAkB;EACvB,OAAO,KACH,8TAIJ;CACJ;;CAGA,aAAa,UAAkB,SAAuB;EAClD,MAAM,UAAU,KAAK,SAAS,IAAI,OAAO;EACzC,IAAI,SAAS;GACT,QAAQ,OAAO,QAAQ;GACvB,IAAI,QAAQ,SAAS,GAAG,KAAK,SAAS,OAAO,OAAO;EACxD;EAEA,KAAK,eAAe,UAAU,OAAO;CACzC;;;;;;;;;;;;;;CAeA,mBAAmB,UAAkB,SAAiB,OAAe,SAAwB;EACzF,MAAM,YAAY,KAAK,gBAAgB,aAAa,OAAO;EAC3D,IAAI,CAAC,WAAW;GACZ,KAAK,gBAAgB,UAAU,SAAS,OAAO,OAAO;GAKtD,KAAK,iBAAiB,UAAU,SAAS,OAAO,OAAO;GACvD;EACJ;EAGA,MAAM,QADW,KAAK,kBAAkB,IAAI,OAAO,KAAK,QAAQ,QAAQ,EAAA,CAInE,YAAY,CAA+B,CAAC,CAAC,CAC7C,WAAW,KAAK,iBAAiB,UAAU,SAAS,OAAO,SAAS,SAAS,CAAC;EAEnF,KAAK,kBAAkB,IAAI,SAAS,IAAI;EACxC,KAAU,cAAc;GAEpB,IAAI,KAAK,kBAAkB,IAAI,OAAO,MAAM,MAAM,KAAK,kBAAkB,OAAO,OAAO;EAC3F,CAAC;CACL;;;;;;;;;;;CAYA,MAAc,iBACV,UACA,SACA,OACA,SACA,WACa;EACb,IAAI;EACJ,IAAI;GACA,CAAC,CAAE,OAAQ,MAAM,KAAK,eAAgB,OAAO,SAAS,OAAO,SAAS,QAAQ;EAClF,SAAS,OAAO;GACZ,OAAO,MAAM,sDAAsD,QAAQ,sBAAsB,EAAE,MAAM,CAAC;GAC1G,KAAK,UACD,UACA,oDAAoD,QAAQ,IAC5D,KAAA,GACA,8BACJ;GACA;EACJ;EAEA,KAAK,gBAAgB,UAAU,SAAS,OAAO,SAAS,GAAG;EAC3D,KAAK,iBAAiB,UAAU,SAAS,OAAO,SAAS,GAAG;EAE5D,IAAI;GACA,MAAM,KAAK,eAAgB,MAAM,SAAS,SAAS;EACvD,SAAS,OAAO;GAIZ,OAAO,KAAK,yCAAyC,QAAQ,IAAI,EAAE,MAAM,CAAC;EAC9E;CACJ;;CAGA,gBAAwB,UAAkB,SAAiB,OAAe,SAAkB,KAAoB;EAC5G,MAAM,UAAU,KAAK,SAAS,IAAI,OAAO;EACzC,IAAI,CAAC,SAAS;EAEd,MAAM,UAAU,KAAK,UAAU;GAC3B,MAAM;GACN;GACA;GACA;GACA,GAAI,QAAQ,KAAA,IAAY,EAAE,IAAI,IAAI,CAAC;EACvC,CAAC;EAED,KAAK,MAAM,YAAY,SAAS;GAC5B,IAAI,aAAa,UAAU;GAC3B,MAAM,KAAK,KAAK,QAAQ,IAAI,QAAQ;GACpC,IAAI,MAAM,GAAG,eAAe,UAAU,MAClC,GAAG,KAAK,OAAO;EAEvB;CACJ;;;;;;;;;CAcA,MAAM,oBAAoB,KAAgC;EACtD,IAAI,IAAI,SAAS,UAAU;GACvB,KAAK,MAAM;GACX;EACJ;EAEA,IAAI;GACA,MAAM,IAAI,OAAO,UAAU,KAAK,eAAe,KAAK,CAAC;EACzD,SAAS,OAAO;GACZ,OAAO,KACH,wCAAwC,IAAI,KAAK,kIAEjD,EAAE,MAAM,CACZ;GACA,MAAM,IAAI,KAAK,CAAC,CAAC,YAAY,CAAoB,CAAC;GAClD,KAAK,MAAM,IAAI,iBAAiB;GAChC;EACJ;EAEA,KAAK,MAAM;EAIX,IAAI;GACA,MAAM,QAAQ,IAAI,qBAAqB,KAAK,IAAI,KAAK,UAAU;GAC/D,MAAM,MAAM,aAAa;GACzB,KAAK,gBAAgB;GACrB,KAAK,oBAAoB;EAC7B,SAAS,OAAO;GACZ,OAAO,KACH,8JAEA,EAAE,MAAM,CACZ;GACA,KAAK,gBAAgB,KAAA;EACzB;EAEA,OAAO,KACH,sDAAsD,IAAI,KAAK,gBAAgB,KAAK,WAAW,GACnG;CACJ;;CAGA,oBAA+C;EAC3C,OAAO,KAAK,IAAI;CACpB;;;;;;;;CASA,iBAAyB,UAAkB,SAAiB,OAAe,SAAkB,KAAoB;EAC7G,IAAI,KAAK,IAAI,SAAS,UAAU;EAEhC,MAAM,QAAyB;GAC3B,MAAM;GACN,KAAK,KAAK;GACV;GACA;GACA,MAAM;GACN,GAAI,QAAQ,KAAA,IAAY,EAAE,IAAI,IAAI,CAAC;GACnC;EACJ;EAMA,IAAI,gBAAgB,KAAK,IAAI,KAAK,IAAI,eAAe;GACjD,IAAI,QAAQ,KAAA,GAAW;IACnB,KAAK,yBAAyB,UAAU,OAAO;IAC/C;GACJ;GACA,KAAU,aAAa;IACnB,MAAM;IACN,KAAK,KAAK;IACV;IACA,MAAM;IACN;GACJ,CAAC;GACD;EACJ;EAEA,KAAU,aAAa,KAAK;CAChC;CAEA,MAAc,aAAa,OAAuC;EAC9D,IAAI;GACA,MAAM,KAAK,IAAI,QAAQ,KAAK;EAChC,SAAS,OAAO;GACZ,OAAO,MAAM,+EAA+E;IACxF,QAAQ,GAAG,MAAM,KAAK,OAAO,MAAM,QAAQ;IAC3C;GACJ,CAAC;EACL;CACJ;;;;;;;;;;CAWA,yBAAiC,UAAkB,SAAuB;EACtE,MAAM,SACF,6BAA6B,QAAQ;EAGzC,IAAI,CAAC,KAAK,yBAAyB,IAAI,OAAO,GAAG;GAC7C,KAAK,yBAAyB,IAAI,OAAO;GACzC,OAAO,KACH,qDAAqD,QAAQ,gBAC1D,KAAK,IAAI,cAAc,qBAAqB,KAAK,IAAI,KAAK,yCAC7D,MACJ;EACJ;EACA,KAAK,UACD,UACA,iBAAiB,QAAQ,4CAA4C,UACrE,KAAA,GACA,+BACJ;CACJ;;;;;;;;CASA,MAAc,eAAe,OAAuC;EAChE,IAAI,MAAM,QAAQ,KAAK,YAAY;EAEnC,QAAQ,MAAM,MAAd;GACI,KAAK;IACD,KAAK,gBAAgB,MAAM,QAAQ,IAAI,MAAM,SAAS,MAAM,OAAO,MAAM,SAAS,MAAM,GAAG;IAC3F;GAEJ,KAAK,iBAAiB;IAGlB,IAAI,CAAC,KAAK,SAAS,IAAI,MAAM,OAAO,CAAC,EAAE,MAAM;IAE7C,MAAM,QAAQ,MAAM,KAAK,gBAAgB,SAAS,MAAM,SAAS,MAAM,GAAG;IAC1E,IAAI,CAAC,OAAO;KACR,OAAO,KACH,2BAA2B,MAAM,IAAI,OAAO,MAAM,QAAQ,sGAE9D;KACA;IACJ;IACA,KAAK,gBAAgB,MAAM,QAAQ,IAAI,MAAM,SAAS,MAAM,OAAO,MAAM,SAAS,MAAM,GAAG;IAC3F;GACJ;GAEA,KAAK;IACD,KAAK,oBAAoB,MAAM,SAAS,MAAM,OAAO,MAAM,MAAM;IACjE;EACR;CACJ;;;;;;;;CAaA,MAAM,wBACF,OACA,SACa;EAKb,KAAK,iBAAiB,IAAI,oBAAoB,KAAK,IAAI,SAAS,CAAC,CAAC;EAClE,IAAI,CAAC,KAAK,eAAe,SAAS;EAClC,IAAI,SAAS,cAAc,OAAO;EAClC,MAAM,KAAK,eAAe,aAAa;CAC3C;;CAGA,0BAA0C;EACtC,OAAO,KAAK,gBAAgB,WAAW;CAC3C;;;;;;;;;;CAWA,MAAc,4BACV,UACA,SACA,UACA,OACa;EACb,IAAI,CAAC,SAAS;EAGd,IAAI,CADc,KAAK,gBAAgB,aAAa,OAAO,GAC3C;GACZ,KAAK,mBAAmB,UAAU,SAAS,CAAC,GAAG,KAAK;GACpD;EACJ;EAEA,IAAI;GACA,MAAM,EAAE,UAAU,cAAc,MAAM,KAAK,eAAgB,OAAO,SAAS,UAAU,KAAK;GAC1F,KAAK,mBAAmB,UAAU,SAAS,UAAU,MAAM,SAAS;EACxE,SAAS,OAAO;GACZ,OAAO,MAAM,yCAAyC,QAAQ,IAAI,EAAE,MAAM,CAAC;GAC3E,KAAK,UAAU,UAAU,yCAAyC,QAAQ,IAAI,KAAA,GAAW,6BAA6B;EAC1H;CACJ;CAEA,mBACI,UACA,SACA,UACA,UACA,WACI;EACJ,MAAM,KAAK,KAAK,QAAQ,IAAI,QAAQ;EACpC,IAAI,MAAM,GAAG,eAAe,UAAU,MAClC,GAAG,KAAK,KAAK,UAAU;GACnB,MAAM;GACN;GACA;GACA;GACA,GAAI,cAAc,KAAA,IAAY,EAAE,UAAU,IAAI,CAAC;EACnD,CAAC,CAAC;CAEV;;;;;;;;;;CAeA,cAAc,UAAkB,SAAiB,OAAsC;EACnF,IAAI,CAAC,KAAK,SAAS,IAAI,OAAO,GAC1B,KAAK,SAAS,IAAI,yBAAS,IAAI,IAAI,CAAC;EAGxC,MAAM,kBAAkB,KAAK,SAAS,IAAI,OAAO;EACjD,MAAM,WAAW,gBAAgB,IAAI,QAAQ;EAC7C,MAAM,UAAU,CAAC,YAAY,KAAK,UAAU,SAAS,KAAK,MAAM,KAAK,UAAU,KAAK;EACpF,gBAAgB,IAAI,UAAU;GAAE;GACxC,UAAU,KAAK,IAAI;EAAE,CAAC;EAId,KAAU,sBAAsB,KAAK,cAAe,MAAM,SAAS,UAAU,KAAK,GAAG,OAAO;EAG5F,KAAK,oBAAoB,SAAS,GAAG,WAAW,MAAM,GAAG,CAAC,CAAC;EAC3D,IAAI,SACA,KAAK,oBAAoB,SAAS,GAAG,WAAW,MAAM,GAAG,CAAC,CAAC;EAI/D,KAAK,sBAAsB;CAC/B;;;;;;;;CASA,eAAe,UAAkB,SAAiB,SAAyC;EACvF,MAAM,kBAAkB,KAAK,SAAS,IAAI,OAAO;EACjD,IAAI,CAAC,iBAAiB;EAEtB,MAAM,QAAQ,gBAAgB,IAAI,QAAQ;EAC1C,IAAI,OAAO;GACP,gBAAgB,OAAO,QAAQ;GAC/B,KAAK,oBAAoB,SAAS,CAAC,GAAG,GAAG,WAAW,MAAM,MAAM,CAAC;GACjE,KAAK,oBAAoB,SAAS,CAAC,GAAG,GAAG,WAAW,MAAM,MAAM,CAAC;GACjE,IAAI,CAAC,SAAS,WACV,KAAU,sBAAsB,KAAK,cAAe,OAAO,SAAS,QAAQ,GAAG,QAAQ;EAE/F;EAEA,IAAI,gBAAgB,SAAS,GACzB,KAAK,SAAS,OAAO,OAAO;CAEpC;;;;;;;;;;CAWA,kBAAkB,UAAkB,SAAuB;EACvD,IAAI,CAAC,KAAK,eAAe;GACrB,KAAK,yBAAyB,UAAU,SAAS,KAAK,eAAe,OAAO,CAAC;GAC7E;EACJ;EAEA,KAAU,cAAc,OAAO,OAAO,CAAC,CAClC,MAAM,cAAc;GACjB,KAAK,yBAAyB,UAAU,SAAS,SAAS;EAC9D,CAAC,CAAC,CACD,OAAO,UAAU;GAGd,OAAO,KAAK,uDAAuD,QAAQ,mDAAmD,EAAE,MAAM,CAAC;GACvI,KAAK,yBAAyB,UAAU,SAAS,KAAK,eAAe,OAAO,CAAC;EACjF,CAAC;CACT;;CAGA,eAAuB,SAA0D;EAC7E,MAAM,kBAAkB,KAAK,SAAS,IAAI,OAAO;EACjD,MAAM,YAAqD,CAAC;EAC5D,IAAI,iBACA,KAAK,MAAM,CAAC,IAAI,EAAE,YAAY,iBAC1B,UAAU,MAAM;EAGxB,OAAO;CACX;CAEA,yBACI,UACA,SACA,WACI;EACJ,MAAM,KAAK,KAAK,QAAQ,IAAI,QAAQ;EACpC,IAAI,MAAM,GAAG,eAAe,UAAU,MAClC,GAAG,KAAK,KAAK,UAAU;GACnB,MAAM;GACN;GACA;EACJ,CAAC,CAAC;CAEV;;CAGA,oBACI,SACA,OACA,QACI;EACJ,MAAM,UAAU,KAAK,SAAS,IAAI,OAAO;EACzC,IAAI,CAAC,SAAS;EAEd,MAAM,UAAU,KAAK,UAAU;GAC3B,MAAM;GACN;GACA;GACA;EACJ,CAAC;EAED,KAAK,MAAM,YAAY,SAAS;GAC5B,MAAM,KAAK,KAAK,QAAQ,IAAI,QAAQ;GACpC,IAAI,MAAM,GAAG,eAAe,UAAU,MAClC,GAAG,KAAK,OAAO;EAEvB;CACJ;;CAGA,oBACI,SACA,OACA,QACI;EACJ,IAAI,KAAK,IAAI,SAAS,UAAU;EAChC,KAAU,aAAa;GAAE,MAAM;GAAiB,KAAK,KAAK;GAAY;GAAS;GAAO;EAAO,CAAC;CAClG;;CAGA,MAAc,gBAAgB,IAAyB,OAA8B;EACjF,IAAI,CAAC,KAAK,eAAe;EACzB,IAAI;GACA,MAAM,GAAG;EACb,SAAS,OAAO;GACZ,OAAO,KAAK,+BAA+B,MAAM,UAAU,EAAE,MAAM,CAAC;EACxE;CACJ;;CAGA,wBAAsC;EAClC,IAAI,KAAK,kBAAkB;EAC3B,KAAK,mBAAmB,kBAAkB;GACtC,MAAM,MAAM,KAAK,IAAI;GACrB,KAAK,MAAM,CAAC,SAAS,oBAAoB,KAAK,UAC1C,KAAK,MAAM,CAAC,UAAU,UAAU,iBAC5B,IAAI,MAAM,MAAM,WAAW,gBAAgB,qBACvC,KAAK,eAAe,UAAU,OAAO;GAKjD,IAAI,KAAK,SAAS,SAAS,KAAK,KAAK,kBAAkB;IACnD,cAAc,KAAK,gBAAgB;IACnC,KAAK,mBAAmB,KAAA;GAC5B;EACJ,GAAG,GAAK;CACZ;;;;;;;;;;;CAYA,sBAAoC;EAChC,IAAI,KAAK,yBAAyB,CAAC,KAAK,eAAe;EAEvD,KAAK,wBAAwB,kBACnB,KAAK,KAAK,mBAAmB,GACnC,gBAAgB,0BACpB;EAGA,KAAM,sBAA4D,QAAQ;CAC9E;;CAGA,MAAc,qBAAoC;EAC9C,IAAI,CAAC,KAAK,eAAe;EACzB,IAAI;GACA,MAAM,UAAU,MAAM,KAAK,cAAc,WAAW,gBAAgB,mBAAmB;GACvF,KAAK,MAAM,OAAO,SAAS;IACvB,KAAK,SAAS,uCAAuC,IAAI,SAAS,OAAO,IAAI,QAAQ,EAAE;IACvF,KAAK,oBAAoB,IAAI,SAAS,CAAC,GAAG,GAAG,IAAI,WAAW,IAAI,MAAM,CAAC;IACvE,KAAK,oBAAoB,IAAI,SAAS,CAAC,GAAG,GAAG,IAAI,WAAW,IAAI,MAAM,CAAC;GAC3E;EACJ,SAAS,OAAO;GACZ,OAAO,KAAK,2CAA2C,EAAE,MAAM,CAAC;EACpE;CACJ;;;;;;;;;;;;CAiBA,MAAM,UAAyB;EAE3B,KAAK,MAAM,CAAC,KAAK,UAAU,KAAK,eAAe;GAC3C,aAAa,KAAK;GAClB,KAAK,cAAc,OAAO,GAAG;EACjC;EAGA,KAAK,eAAe,MAAM;EAC1B,KAAK,sBAAsB,MAAM;EAGjC,KAAK,SAAS,MAAM;EACpB,KAAK,SAAS,MAAM;EAGpB,MAAM,QAAQ,WAAW,CAAC,GAAG,KAAK,kBAAkB,OAAO,CAAC,CAAC;EAC7D,KAAK,kBAAkB,MAAM;EAC7B,KAAK,gBAAgB,MAAM;EAC3B,IAAI,KAAK,kBAAkB;GACvB,cAAc,KAAK,gBAAgB;GACnC,KAAK,mBAAmB,KAAA;EAC5B;EACA,IAAI,KAAK,uBAAuB;GAC5B,cAAc,KAAK,qBAAqB;GACxC,KAAK,wBAAwB,KAAA;EACjC;EACA,KAAK,yBAAyB,MAAM;EAKpC,IAAI,KAAK,eAAe;GACpB,IAAI;IACA,MAAM,KAAK,cAAc,eAAe;GAC5C,SAAS,OAAO;IACZ,OAAO,KAAK,yEAAyE,EAAE,MAAM,CAAC;GAClG;GACA,KAAK,gBAAgB,KAAA;EACzB;EAGA,MAAM,KAAK,cAAc;EACzB,MAAM,KAAK,QAAQ;EACnB,MAAM,KAAK,IAAI,KAAK,CAAC,CAAC,OAAO,UACzB,OAAO,KAAK,wDAAwD,EAAE,MAAM,CAAC,CAAC;EAClF,KAAK,MAAM,IAAI,iBAAiB;EAGhC,KAAK,QAAQ,MAAM;EAEnB,KAAK,SAAS,mEAAmE;CACrF;;CAOA,cAA8B;EAC1B,OAAO,KAAK;CAChB;;;;;;;;;;;;;;;;;;CAmBA,MAAM,UAAU,kBAAyC;EACrD,IAAI,KAAK,WAAW;GAChB,OAAO,KAAK,gEAAgE;GAC5E;EACJ;EACA,KAAK,cAAc,KAAK,iBAAiB;EACzC,KAAK,kBAAkB,qBAAqB,KAAK,QAAQ;EACzD,KAAK,cAAc,IAAI,YAAY,mBAAmB,UAAU,KAAK,eAAe,KAAK,CAAC;EAC1F,IAAI;GAIA,MAAM,KAAK,YAAY,MAAM;EACjC,SAAS,KAAK;GACV,MAAM,KAAK,YAAY,KAAK,CAAC,CAAC,YAAY,CAAoB,CAAC;GAC/D,KAAK,cAAc,KAAA;GACnB,KAAK,cAAc,KAAA;GACnB,KAAK,kBAAkB,KAAA;GACvB,MAAM;EACV;EACA,KAAK,YAAY;EAGjB,OAAO,MACH,gHACI,KAAK,YAAY,KAAK,uBAC9B;CACJ;;CAGA,MAAM,UAAyB;EAC3B,KAAK,YAAY;EACjB,IAAI,KAAK,aAAa;GAClB,MAAM,KAAK,YAAY,KAAK;GAC5B,KAAK,cAAc,KAAA;EACvB;EACA,KAAK,cAAc,KAAA;EACnB,KAAK,kBAAkB,KAAA;EACvB,KAAK,eAAe,MAAM;CAC9B;;;;;;;CAQA,mBAA0D;EACtD,MAAM,sBAAM,IAAI,IAA8B;EAC9C,KAAK,MAAM,cAAc,KAAK,SAAS,eAAe,GAAG;GACrD,MAAM,QAAQ,eAAa,UAAU;GACrC,IAAI,CAAC,OAAO;GACZ,MAAM,SAAU,WAAmC,UAAU;GAC7D,IAAI,IAAI,GAAG,OAAO,GAAG,SAAS,UAAU;GAExC,IAAI,CAAC,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,OAAO,UAAU;EAClD;EACA,OAAO;CACX;CAEA,0BAAkC,QAAgB,OAA6C;EAC3F,IAAI,CAAC,KAAK,aAAa,OAAO,KAAA;EAC9B,OAAO,KAAK,YAAY,IAAI,GAAG,OAAO,GAAG,OAAO,KAAK,KAAK,YAAY,IAAI,KAAK;CACnF;;;;;;;;;;;CAYA,MAAc,eAAe,OAAsC;EAC/D,MAAM,aAAa,KAAK,0BAA0B,MAAM,QAAQ,MAAM,KAAK;EAC3E,IAAI,CAAC,YAAY;GAGb,IAAI,MAAM,KAAK,uBAAuB,KAAK,GAAG;GAG9C,KAAK,SAAS,8CAA8C,MAAM,OAAO,GAAG,MAAM,OAAO;GACzF;EACJ;EAEA,MAAM,OAAO,WAAW;EACxB,MAAM,aAAc,WAAuC;EAC3D,MAAM,KAAK,KAAK,oBAAoB,YAAY,MAAM,GAAG;EAIzD,MAAM,MAAM,MAAM,OAAO,WAAW,OAAO,EAAE,qBAAqB,KAAK;EAEvE,MAAM,KAAK,aAAa,MAAM,IAAI,KAAK,YAA4B,OAAoB,KAAK;CAChG;;;;;;;;;;;;;;;;;CAkBA,MAAc,uBAAuB,OAAyC;EAC1E,MAAM,QAAQ,KAAK,iBAAiB,IAAI,GAAG,MAAM,OAAO,GAAG,MAAM,OAAO,KACjE,KAAK,iBAAiB,IAAI,MAAM,KAAK;EAC5C,IAAI,CAAC,OAAO,QAAQ,OAAO;EAE3B,KAAK,MAAM,QAAQ,OAAO;GACtB,MAAM,WAAW,MAAM,MAAM,KAAK;GAClC,MAAM,WAAW,MAAM,MAAM,KAAK;GAClC,IAAI,aAAa,KAAA,KAAa,aAAa,QAAQ,aAAa,KAAA,KAAa,aAAa,MAAM;IAC5F,KAAK,SACD,4BAA4B,MAAM,MAAM,eAAe,KAAK,aAAa,KAAK,KAAK,aAAa,cACpG;IACA;GACJ;GAEA,MAAM,OAAO,GAAG,KAAK,iBAAiB,KAAK,GAAG,OAAO,QAAQ,EAAE,GAAG,KAAK;GAGvE,MAAM,MAAM,MAAM,OAAO,WAAW,OAAO,EAAE,qBAAqB,KAAK;GAEvE,MAAM,KAAK,aACP,MACA,OAAO,QAAQ,GACf,KACC,KAAK,iBAA6C,YACnC,OACH,KACjB;EACJ;EAEA,OAAO;CACX;;CAGA,oBAA4B,YAA8B,KAAsC;EAG5F,OAAO,iBAAiB,KAAK,YAAY,KAAK,QAAQ,KAAK;CAC/D;CAIA,SAAiB,MAAc,IAAY,YAA6B;EACpE,OAAO,GAAG,cAAc,GAAG,IAAI,KAAK,IAAI;CAC5C;;CAGA,YAAoB,KAAmB;EACnC,MAAM,MAAM,KAAK,IAAI;EACrB,KAAK,eAAe,IAAI,KAAK,MAAM,gBAAgB,mBAAmB;EAEtE,IAAI,KAAK,eAAe,OAAO;QACtB,MAAM,CAAC,GAAG,WAAW,KAAK,gBAC3B,IAAI,UAAU,KAAK,KAAK,eAAe,OAAO,CAAC;EAAA;CAG3D;;CAGA,eAAuB,KAAsB;EACzC,MAAM,SAAS,KAAK,eAAe,IAAI,GAAG;EAC1C,IAAI,WAAW,KAAA,GAAW,OAAO;EACjC,KAAK,eAAe,OAAO,GAAG;EAC9B,OAAO,SAAS,KAAK,IAAI;CAC7B;;;;;;;;;;;CAgBA,MAAM,eAAe,kBAAyC;EAC1D,IAAI,KAAK,cAAc;GACnB,OAAO,KAAK,6EAA6E;GACzF;EACJ;EAEA,KAAK,yBAAyB;EAG9B,KAAK,eAAe;EACpB,MAAM,KAAK,oBAAoB;EAC/B,OAAO,KAAK,qEAAqE,KAAK,WAAW,EAAE;CACvG;;;;CAKA,MAAM,gBAA+B;EACjC,KAAK,eAAe;EACpB,IAAI,KAAK,gBAAgB;GACrB,aAAa,KAAK,cAAc;GAChC,KAAK,iBAAiB,KAAA;EAC1B;EACA,IAAI,KAAK,cAAc;GACnB,IAAI;IACA,MAAM,KAAK,aAAa,IAAI;GAChC,QAAQ,CAA4B;GACpC,KAAK,eAAe,KAAA;EACxB;EACA,OAAO,KAAK,wDAAwD;CACxE;;;;;CAMA,MAAc,gBAAgB,MAAc,IAAY,YAAoC;EACxF,MAAM,UAAU,KAAK,UAAU;GAC3B,KAAK,KAAK;GACV,GAAG;GACH,KAAK;GACL,IAAI,cAAc;EACtB,CAAC;EACD,MAAM,KAAK,GAAG,QAAQ,GAAU,oBAAoB,kBAAkB,IAAI,QAAQ,EAAE;CACxF;;;;CAKA,MAAc,sBAAqC;EAC/C,IAAI,CAAC,KAAK,wBAAwB;EAElC,IAAI;EACJ,IAAI;GAMA,MAAM,SAAS,IAAI,OAAS,EAAE,kBAAkB,KAAK,uBAAuB,CAAC;GAC7E,UAAU;GAEV,OAAO,GAAG,UAAU,QAAQ;IACxB,OAAO,MAAM,2CAA2C,EAAE,QAAQ,IAAI,QAAQ,CAAC;IAC/E,KAAK,kBAAkB;GAC3B,CAAC;GAED,OAAO,GAAG,aAAa;IACnB,IAAI,KAAK,cAAc;KACnB,OAAO,KAAK,+DAA+D;KAC3E,KAAK,kBAAkB;IAC3B;GACJ,CAAC;GAED,OAAO,GAAG,gBAAgB,OAAO,QAAQ;IACrC,IAAI,CAAC,IAAI,SAAS;IAClB,IAAI;KACA,MAAM,EAAE,KAAK,GAAG,KAAK,OAAO,KAAK,MAAM,IAAI,OAAO;KAQlD,IAAI,QAAQ,KAAK,YAAY;KAK7B,KAAK,sBAAsB;KAE3B,KAAK,SAAS,mEAAmE,EAAE,OAAO,IAAI,SAAS,KAAK;KAK5G,IAAI,eAA+C;KACnD,IAAI;MACA,IAAI,KAAK,QAAQ;OACb,MAAM,aAAa,KAAK,SAAS,oBAAoB,CAAC;OAMtD,eAAe,MALO,KAAK,OAAO,SAAS;QACvC,MAAM;QACN,IAAI;QACQ;OAChB,CAAC,KACyB;MAC9B,OAII,eAAe,MAHO,KAAK,YAAY,SACnC,GAAG,KAAK,MAAM,KAAA,CAClB,KAC0B;KAElC,SAAS,UAAU;MAEf,KAAK,SAAS,8CAA8C,IAAI,QAAQ,EAAE,yBAAyB,QAAQ;KAC/G;KAGA,MAAM,KAAK,aAAa,GAAG,KAAK,cAAc,MAAM,KAAA,GAAW,KAAK;IACxE,SAAS,KAAK;KACV,OAAO,MAAM,oEAAoE,EAAE,OAAO,IAAI,CAAC;IACnG;GACJ,CAAC;GAED,MAAM,OAAO,QAAQ;GACrB,MAAM,OAAO,MAAM,UAAU,mBAAmB;GAChD,KAAK,eAAe;GAEpB,UAAU,KAAA;GAEV,KAAK,SAAS,4DAA4D,kBAAkB,EAAE;EAClG,SAAS,KAAK;GACV,IAAI,SACA,IAAI;IAAE,MAAM,QAAQ,IAAI;GAAG,QAAQ,CAAqB;GAE5D,OAAO,MAAM,uDAAuD,EAAE,OAAO,IAAI,CAAC;GAClF,KAAK,kBAAkB;EAC3B;CACJ;;;;CAKA,oBAAkC;EAC9B,IAAI,CAAC,KAAK,gBAAgB,KAAK,gBAAgB;EAE/C,MAAM,QAAQ;EACd,KAAK,SAAS,uDAAuD,MAAM,MAAM;EAEjF,KAAK,iBAAiB,WAAW,YAAY;GACzC,KAAK,iBAAiB,KAAA;GACtB,IAAI,CAAC,KAAK,cAAc;GAGxB,IAAI,KAAK,cAAc;IACnB,IAAI;KAAE,MAAM,KAAK,aAAa,IAAI;IAAG,QAAQ,CAAe;IAC5D,KAAK,eAAe,KAAA;GACxB;GAEA,MAAM,KAAK,oBAAoB;EACnC,GAAG,KAAK;CACZ;AACJ;;;;;AAMA,IAAa,2BAA2B;;;;;;;;;ACt9ExC,IAAa,6BAAb,cAAgD,mBAA0D;CAEtG,yBAAiB,IAAI,IAAqB;CAC1C,wBAAgB,IAAI,IAA2C;CAC/D,4BAAoB,IAAI,IAAuB;CAE/C,cAAc,OAAgB,WAAmB;EAC7C,KAAK,OAAO,IAAI,WAAW,KAAK;CACpC;CAEA,SAAS,WAAwC;EAC7C,OAAO,KAAK,OAAO,IAAI,SAAS;CACpC;;;;CAKA,sBAAsB,WAA4B;EAC9C,OAAO,KAAK,OAAO,IAAI,SAAS;CACpC;;;;CAKA,gBAA0B;EACtB,OAAO,MAAM,KAAK,KAAK,OAAO,KAAK,CAAC;CACxC;;;;CAKA,4BAA4B,gBAAgB,aAAiC;EAIzE,OAHoB,KAAK,eAAe,CAAC,CAAC,QACtC,MAAK,EAAE,eAAe,iBAAkB,CAAC,EAAE,cAAc,kBAAkB,WAExE,CAAA,CAAY,QAAO,MAAK,CAAC,KAAK,OAAO,IAAI,eAAa,CAAC,CAAC,CAAC;CACpE;CAEA,cAAc,OAAsD;EAChE,OAAO,QAAQ,KAAK,CAAC,CAAC,SAAS,CAAC,MAAM,WAAW,KAAK,MAAM,IAAI,MAAM,KAAK,CAAC;CAChF;CAEA,kBAAkB,WAAsC;EACpD,OAAO,QAAQ,SAAS,CAAC,CAAC,SAAS,CAAC,MAAM,WAAW,KAAK,UAAU,IAAI,MAAM,KAAK,CAAC;CACxF;CAEA,QAAQ,MAAyD;EAC7D,OAAO,KAAK,MAAM,IAAI,IAAI;CAC9B;CAEA,YAAY,MAAqC;EAC7C,OAAO,KAAK,UAAU,IAAI,IAAI;CAClC;CAEA,cAA6D;EACzD,OAAO,OAAO,YAAY,KAAK,MAAM,QAAQ,CAAC;CAClD;CAEA,kBAA6C;EACzC,OAAO,OAAO,YAAY,KAAK,UAAU,QAAQ,CAAC;CACtD;;;;;CAMA,kBAA2C;EACvC,MAAM,SAAkC,CAAC;EACzC,KAAK,MAAM,CAAC,MAAM,UAAU,KAAK,OAAO,QAAQ,GAC5C,OAAO,QAAQ;EAEnB,KAAK,MAAM,CAAC,MAAM,aAAa,KAAK,UAAU,QAAQ,GAClD,OAAO,QAAQ;EAEnB,OAAO;CACX;;;;;;CAOA,6BAA6B,gBAAkC;EAC3D,MAAM,aAAa,KAAK,oBAAoB,cAAc;EAC1D,IAAI,CAAC,YAAY,OAAO,CAAC;EAMzB,OAAO,OAAO,KAAK,2BAA2B,UAAU,CAAC;CAC7D;AAEJ;;;;;;;;;;;;;;;;;;;;;;ACxCA,SAAgB,wBAAwB,KAAwD;CAC5F,MAAM,WAAW,IAAI,iBAAiB,KAAK;CAC3C,IAAI,CAAC,UACD,OAAO,EAAE,UAAU,KAAK;CAG5B,MAAM,mBAAmB,wBAAwB,GAAG;CACpD,IAAI,CAAC,kBACD,OAAO,EAAE,OAAO,6DAA6D;CAGjF,MAAM,iBAAiB,IAAI,oBAAoB,KAAK;CACpD,IAAI,CAAC,gBACD,OAAO,EAAE,OAAO,mEAAmE;CAEvF,MAAM,cAAc,uBAAuB,cAAc;CAEzD,MAAM,gBAAgB,iBAAiB,IAAI,qBAAqB;CAChE,IAAI,kBAAkB,WAClB,OAAO,EAAE,OAAO,kDAAkD,IAAI,sBAAsB,IAAI;CAEpG,MAAM,cAAc,iBAAiB,IAAI,mBAAmB;CAC5D,IAAI,gBAAgB,WAChB,OAAO,EAAE,OAAO,gDAAgD,IAAI,oBAAoB,IAAI;CAGhG,OAAO,EACH,QAAQ;EACJ;EACA;EACA;EACA,eAAe,iBAAiB,KAAA;EAChC,aAAa,eAAe,KAAA;CAChC,EACJ;AACJ;AAEA,SAAS,iBAAiB,OAAsD;CAC5E,IAAI,UAAU,KAAA,KAAa,MAAM,KAAK,MAAM,IAAI,OAAO;CACvD,MAAM,IAAI,OAAO,KAAK;CACtB,IAAI,CAAC,OAAO,UAAU,CAAC,KAAK,IAAI,GAAG,OAAO;CAC1C,OAAO;AACX;;;;;;AAOA,SAAgB,iBAAiB,QAA6C;CAC1E,MAAM,SAAS,mBAAmB,OAAO,gBAAgB,KAAK;CAC9D,MAAM,iBAAiB,OAAO,kBAAkB,CAAC,QAAQ;CAEzD,OAAO;EACH,MAAM,OAAO,QAAQ;EACrB,UAAU,OAAO;EACjB,aAAa;EACb,SAAS,OAAO,WAAW;EAE3B,gBAAgB;EAChB,MAAM,QAAQ,EAAE,OAAO;GACnB,MAAM,EAAE,YAAY,cAAc,cAAc,iBAAiB,MAAM,OAAO,+BAAA,CAAA,MAAA,MAAA,EAAA,CAAA;GAC9E,MAAM,EAAE,gBAAgB;GAExB,IAAI,YAAY,SAAS,WAAW,CAAC,OAAO,SACxC,MAAM,IAAI,MACN,yBAAyB,YAAY,KAAK,2HAE9C;GAGJ,IAAI,uBAAuB,OAAO,GAAG;GACrC,MAAM,SAAS,YAAY,SAAS,UAAU,YAAY,OAAO,KAAA;GACjE,MAAM,OAAO,MAAM,WAAW;IAC1B,kBAAkB,OAAO;IACzB;IACA;IACA;GACJ,CAAC;GACD,IAAI,iBAAiB,KAAK,SAAS,IAAI,YAAY,KAAK,SAAS,EAAE,EAAE;GAIrE,MAAM,QAAQ,MAAM,aAAa,KAAK,SAAS;GAC/C,IAAI,CAAC,MAAM,IAAI;IAEX,IAAI,YAAY,SAAS,WAAW,GAAG,WAAW,KAAK,SAAS,GAC5D,GAAG,WAAW,KAAK,SAAS;IAEhC,IAAI,KAAK,eAAe,YAAY,SAAS,WAAW,GAAG,WAAW,KAAK,WAAW,GAClF,GAAG,WAAW,KAAK,WAAW;IAElC,MAAM,IAAI,MAAM,2FAA2F,MAAM,QAAQ;GAC7H;GAEA,IAAI,YAAY,KAAK;GACrB,IAAI;IACA,IAAI,YAAY,SAAS,SAAS;KAC9B,MAAM,WAAW,MAAM,aAAa,OAAO,SAAU,KAAK,WAAW,WAAW;KAChF,YAAY,SAAS;KACrB,IAAI,eAAe,SAAS,YAAY;KAGxC,IAAI,KAAK,eAAe,GAAG,WAAW,KAAK,WAAW,GAElD,IAAI,8BAA6B,MADjB,aAAa,OAAO,SAAU,KAAK,aAAa,WAAW,EAAA,CACxC,YAAY;IAEvD;GACJ,UAAU;IAGN,IAAI,YAAY,SAAS,WAAW,GAAG,WAAW,KAAK,SAAS,GAC5D,GAAG,WAAW,KAAK,SAAS;IAEhC,IAAI,KAAK,eAAe,YAAY,SAAS,WAAW,GAAG,WAAW,KAAK,WAAW,GAClF,GAAG,WAAW,KAAK,WAAW;GAEtC;GAEA,IAAI,SAAmB,CAAC;GACxB,IAAI,OAAO,iBAAiB,OAAO,gBAAgB,GAAG;IAClD,SAAS,MAAM,aACX,aACA;KAAE,eAAe,OAAO;KAAe,aAAa,OAAO;IAAY,GACvE,OAAO,OACX;IACA,IAAI,OAAO,SAAS,GAChB,IAAI,UAAU,OAAO,OAAO,wBAAwB,OAAO,cAAc,SAAS;GAE1F;GAEA,OAAO;IACH,QAAQ;IACR,WAAW,KAAK;IAChB,QAAQ,OAAO;GACnB;EACJ;CACJ;AACJ;AAEA,SAAS,YAAY,OAAuB;CACxC,IAAI,QAAQ,MAAM,OAAO,GAAG,MAAM;CAClC,IAAI,QAAQ,OAAO,MAAM,OAAO,IAAI,QAAQ,KAAA,CAAM,QAAQ,CAAC,EAAE;CAC7D,IAAI,QAAQ,OAAO,OAAO,MAAM,OAAO,IAAI,SAAS,OAAO,MAAA,CAAO,QAAQ,CAAC,EAAE;CAC7E,OAAO,IAAI,SAAS,OAAO,OAAO,MAAA,CAAO,QAAQ,CAAC,EAAE;AACxD;;;;;;;;ACrKA,SAAS,YAAY,OAA6B;CAC9C,MAAM,wBAAQ,IAAI,IAAY;CAC9B,KAAK,MAAM,CAAC,KAAK,QAAQ,OAAO,QAAQ,gBAAgB,KAAK,CAAC,GAAG;EAC7D,MAAM,IAAI,GAAG;EACb,MAAM,SAAU,KAA2B;EAC3C,IAAI,QAAQ,MAAM,IAAI,MAAM;CAChC;CACA,OAAO;AACX;AAEA,IAAM,SAAS,OAAyB,MAAM,KAAK,EAAE,CAAC,CAAC,KAAI,MAAK,KAAK,EAAE,GAAG,CAAC,CAAC,KAAK,IAAI;;AAGrF,IAAM,aAAa,UAAuC,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8B/F,SAAS,mBACL,QACA,WACA,SAC0C;CAC1C,KAAK,MAAM,UAAU,SAAS;EAC1B,IAAI,CAAC,QAAQ;EACb,MAAM,UAAU,uBAAuB,MAAM;EAC7C,MAAM,SAAS,qBAAqB,MAAM;EAG1C,IAAI,YAAY,UAAU,WAAW,SAAS;EAC9C,IAAI,UAAU,IAAI,MAAM,KAAK,CAAC,UAAU,IAAI,OAAO,GAAG,OAAO;GAAE;GAAQ;EAAQ;CACnF;CACA,OAAO;AACX;;AAGA,SAAS,mBACL,OACA,EAAE,QAAQ,WAC6B;CACvC,OAAO;EACH,SACI,iDAAiD,OAAO,UAAU,MAAM,iCACnD,QAAQ;EAEjC,KACI,uLAEa,OAAO;CAC5B;AACJ;;;;;;;;AASA,SAAgB,oBACZ,aACA,UACgB;CAChB,MAAM,UAA4B,CAAC;CACnC,MAAM,kBAAkB,IAAI,IAAI,SAAS,eAAe,CAAC,CAAC,KAAI,MAAK,EAAE,IAAI,CAAC;CAE1E,KAAK,MAAM,cAAc,aAAa;EAClC,MAAM,kBAAkB,eAAa,UAAU;EAC/C,MAAM,cAAc,SAAS,SAAS,eAAe;EAErD,IAAI,CAAC,aAAa;EAElB,MAAM,gBAAgB,YAAY,WAAW;EAC7C,MAAM,YAAY,2BAA2B,UAAU;EAEvD,KAAK,MAAM,YAAY,OAAO,OAAO,SAAS,GAAG;GAC7C,MAAM,KAAK;IAAE,YAAY,WAAW;IAChD,cAAc,SAAS;IACvB,MAAM,SAAS;GAAK;GAER,IAAI;GACJ,IAAI;IACA,mBAAmB,SAAS,OAAO;GACvC,SAAS,GAAG;IACR,QAAQ,KAAK;KACT,GAAG;KACH,SAAS,2BAA2B,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;KAC7E,KAAK;IACT,CAAC;IACD;GACJ;GAIA,IAAI,CAAC,gBAAgB,IAAI,iBAAiB,IAAI,GAAG;GAEjD,MAAM,kBAAkB,eAAa,gBAAgB;GACrD,MAAM,cAAc,SAAS,SAAS,eAAe;GACrD,IAAI,CAAC,aAAa;IACd,QAAQ,KAAK;KACT,GAAG;KACH,SAAS,6BAA6B,iBAAiB,KAAK,2BAA2B,gBAAgB;KACvG,KAAK,gBAAgB,gBAAgB,0CAA0C,iBAAiB,KAAK;IACzG,CAAC;IACD;GACJ;GACA,MAAM,gBAAgB,YAAY,WAAW;GAE7C,QAAQ,SAAS,MAAjB;IACI,KAAK;KACD,IAAI,CAAC,cAAc,IAAI,SAAS,QAAQ,GAAG;MAGvC,MAAM,QAAQ,mBACV,SAAS,UACT,eACA,CAAC,SAAS,cAAc,iBAAiB,IAAI,CACjD;MACA,QAAQ,KAAK,QACP;OAAE,GAAG;OAAI,GAAG,mBAAmB,iBAAiB,KAAK;MAAE,IACvD;OACE,GAAG;OACH,SAAS,gBAAgB,SAAS,SAAS,2BAA2B,gBAAgB;OACtF,KAAK,kDAAkD,MAAM,aAAa;MAC9E,CAAC;KACT;KACA;IAGJ,KAAK;IACL,KAAK;KACD,IAAI,CAAC,cAAc,IAAI,SAAS,kBAAkB,GAAG;MAGjD,MAAM,QAAQ,mBACV,SAAS,oBACT,eACA,CAAC,WAAW,IAAI,CACpB;MACA,QAAQ,KAAK,QACP;OAAE,GAAG;OAAI,GAAG,mBAAmB,iBAAiB,KAAK;MAAE,IACvD;OACE,GAAG;OACH,SAAS,0BAA0B,SAAS,mBAAmB,4CAA4C,gBAAgB;OAC3H,KAAK,4DAA4D,MAAM,aAAa;MACxF,CAAC;KACT;KAKA,IAAI,SAAS,aAAa,CAAC,cAAc,IAAI,SAAS,SAAS,GAC3D,QAAQ,KAAK;MACT,GAAG;MACH,SAAS,iBAAiB,SAAS,UAAU,2BAA2B,gBAAgB;MACxF,KAAK,cAAc,IAAI,SAAS,SAAS,IACnC,0CAA0C,gBAAgB,wHAEvC,MAAM,aAAa,MACtC,mDAAmD,MAAM,aAAa;KAChF,CAAC;KAEL;IAGJ,KAAK,cAAc;KACf,MAAM,EAAE,OAAO,cAAc,iBAAiB,SAAS;KACvD,MAAM,WAAW,SAAS,SAAS,KAAK;KACxC,IAAI,CAAC,UAAU;MACX,QAAQ,KAAK;OACT,GAAG;OACH,SAAS,wBAAwB,MAAM;OACvC,KAAK,YAAY,MAAM;MAG3B,CAAC;MACD;KACJ;KACA,MAAM,kBAAkB,YAAY,QAAQ;KAI5C,MAAM,cAAc;MAChB,cAAc,CAAC,WAAW,IAAI;MAC9B,cAAc,CAAC,iBAAiB,IAAI;KACxC;KACA,KAAK,MAAM,CAAC,OAAO,WAAW,CAAC,CAAC,gBAAgB,YAAY,GAAG,CAAC,gBAAgB,YAAY,CAAC,GACzF,IAAI,CAAC,gBAAgB,IAAI,MAAM,GAAG;MAC9B,MAAM,QAAQ,mBAAmB,QAAQ,iBAAiB,CAAC,GAAG,YAAY,MAAM,CAAC;MACjF,QAAQ,KAAK,QACP;OAAE,GAAG;OAAI,GAAG,mBAAmB,OAAO,KAAK;MAAE,IAC7C;OACE,GAAG;OACH,SAAS,aAAa,MAAM,KAAK,OAAO,8CAA8C,MAAM;OAC5F,KAAK,iBAAiB,MAAM,gBAAgB,MAAM,eAAe,OAC5D,UAAU,iBAAiB,iDAAiD;MACrF,CAAC;KACT;KAEJ;IACJ;IAEA,KAAK,OAAO;KACR,IAAI,SAAS,SAAS,WAAW,GAAG;MAChC,QAAQ,KAAK;OACT,GAAG;OACH,SAAS;OACT,KAAK;MACT,CAAC;MACD;KACJ;KAIA,IAAI,WAAW;KACf,IAAI,cAAc;KAClB,IAAI,SAAS;KAEb,KAAK,MAAM,CAAC,GAAG,SAAS,SAAS,SAAS,QAAQ,GAAG;MACjD,MAAM,YAAY,SAAS,SAAS,KAAK,KAAK;MAC9C,IAAI,CAAC,WAAW;OACZ,QAAQ,KAAK;QACT,GAAG;QACH,SAAS,QAAQ,IAAI,EAAE,+BAA+B,KAAK,MAAM;QACjE,KAAK,sBAAsB,EAAE;OACjC,CAAC;OACD,SAAS;OACT;MACJ;MACA,MAAM,cAAc,YAAY,SAAS;MAEzC,KAAK,MAAM,UAAU,UAAU,KAAK,GAAG,IAAI,GACvC,IAAI,CAAC,YAAY,IAAI,MAAM,GACvB,QAAQ,KAAK;OACT,GAAG;OACH,SAAS,QAAQ,IAAI,EAAE,WAAW,SAAS,GAAG,OAAO,SAAS,KAAK,MAAM,YAAY,OAAO,0BAA0B,SAAS;OAC/H,KAAK,cAAc,EAAE,+BAA+B,MAAM,IAAI,4BAA4B,gCAAgC,SAAS,KAAK,IAAI,MAAM,WAAW;MACjK,CAAC;MAGT,KAAK,MAAM,UAAU,UAAU,KAAK,GAAG,EAAE,GACrC,IAAI,CAAC,YAAY,IAAI,MAAM,GACvB,QAAQ,KAAK;OACT,GAAG;OACH,SAAS,QAAQ,IAAI,EAAE,gBAAgB,KAAK,MAAM,GAAG,OAAO,YAAY,OAAO,0BAA0B,KAAK,MAAM;OACpH,KAAK,cAAc,EAAE,+BAA+B,KAAK,MAAM,MAAM,MAAM,WAAW;MAC1F,CAAC;MAIT,IAAI,UAAU,KAAK,GAAG,IAAI,CAAC,CAAC,WAAW,UAAU,KAAK,GAAG,EAAE,CAAC,CAAC,QACzD,QAAQ,KAAK;OACT,GAAG;OACH,SAAS,QAAQ,IAAI,EAAE,YAAY,UAAU,KAAK,GAAG,IAAI,CAAC,CAAC,OAAO,qBAAqB,UAAU,KAAK,GAAG,EAAE,CAAC,CAAC;OAC7G,KAAK,0EAA0E,EAAE;MACrF,CAAC;MAGL,WAAW,KAAK;MAChB,cAAc;KAClB;KAIA,IAAI,CAAC,UAAU,aAAa,iBACxB,QAAQ,KAAK;MACT,GAAG;MACH,SAAS,8BAA8B,SAAS,uBAAuB,iBAAiB,KAAK,cAAc,gBAAgB;MAC3H,KAAK,6BAA6B,gBAAgB,wDAAwD,SAAS;KACvH,CAAC;KAEL;IACJ;IAEA,SAEI,MAAM,IAAI,MAAM,4BAA4B,KAAK,UAAU,QAAU,GAAG;GAEhF;EACJ;CACJ;CAEA,OAAO;AACX;;;;;;;;;;;AAYA,SAAgB,uBACZ,aACA,UACI;CACJ,MAAM,UAAU,oBAAoB,aAAa,QAAQ;CACzD,IAAI,QAAQ,WAAW,GAAG;CAE1B,MAAM,QAAQ,QAAQ,KAAI,MACtB,OAAO,EAAE,WAAW,GAAG,EAAE,aAAa,IAAI,EAAE,KAAK,WACxC,EAAE,QAAQ,eACL,EAAE,KACpB;CAEA,MAAM,IAAI,MACN,GAAG,QAAQ,OAAO,WAAW,QAAQ,WAAW,IAAI,KAAK,IAAI;;;;;;;;;;;IAmB7D,MAAM,KAAK,MAAM,IAAI,IACzB;AACJ;;;;;;;;;;;;;;AClXA,SAAgB,wBAAwB,QAAoD;CACxF,MAAM,WAAW,IAAI,2BAA2B;CAEhD,IAAI,OAAO,aAAa;EACpB,SAAS,iBAAiB,OAAO,WAAW;EAG5C,OAAO,MACH,oCAAoC,SAAS,eAAe,CAAC,CAAC,OAAO,iBACjE,SAAS,eAAe,CAAC,CAAC,KAAI,MAAK,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,EAC9D;CACJ;CAEA,IAAI,OAAO,QACP,OAAO,OAAO,OAAO,MAAM,CAAC,CAAC,SAAS,UAAU;EAC5C,IAAI,QAAQ,KAAK,GACb,SAAS,cAAc,OAAkB,aAAa,KAAK,CAAC;CAEpE,CAAC;CAGL,IAAI,OAAO,OAAO,SAAS,cAAc,OAAO,KAAK;CACrD,IAAI,OAAO,WAAW,SAAS,kBAAkB,OAAO,SAAS;CAKjE,gCAAgC,SAAS,eAAe,GAAG,QAAQ;CAMnE,uBAAuB,SAAS,eAAe,GAAG,QAAQ;CAE1D,OAAO;AACX;;AC5BA,IAAM,cAAc;;;;;;;AAQpB,IAAM,iCAAiC;CAAC;CAAc;CAAW;CAAc;AAAoB;;;;;;AAOnG,IAAM,mCAAmC;;;;;;;;;AAUzC,IAAa,yBAAb,cAA4C,MAAM;CAC9C;CACA;CAEA,YAAY,iBAAyB,gBAAwB;EACzD,MACI,4DAA4D,gBAAgB,yCACpC,eAAe;;+JAM3D;EACA,KAAK,OAAO;EACZ,KAAK,kBAAkB;EACvB,KAAK,iBAAiB;CAC1B;AACJ;;;;;;AAOA,SAAgB,kBAAkB,YAAuC;CACrE,IAAI,CAAC,YAAY,OAAO;CACxB,MAAM,cAAe,YAAY,cAAc,OAAO,WAAW,WAAW,WACtE,WAAW,SACX;CACN,OAAO,gBAAgB,WAAW,WAAW;AACjD;;;;;;;;;AAUA,eAAsB,sBAClB,IACA,YACsB;CACtB,MAAM,YAAY,IAAI,WAAW;CAEjC,IAAI,EAAE,MADe,GAAG,QAAQ,GAAG,sBAAsB,UAAU,yBAAyB,EAAA,CAC/E,KAAK,EAAE,EAAuC,SAAS,OAAO;CAK3E,MAAM,OAAO,MAHQ,GAAG,QAAQ,GAAG;4BACX,IAAI,IAAI,SAAS,EAAE,eAAe,YAAY;KACrE,EAAA,CACmB,KAAK,EAAE,EAAoC;CAC/D,IAAI,QAAQ,KAAA,GAAW,OAAO;CAE9B,MAAM,SAAS,OAAO,SAAS,KAAK,EAAE;CAItC,OAAO,OAAO,SAAS,MAAM,IAAI,SAAS;AAC9C;;;;;;;;AASA,eAAsB,2BAClB,IACA,YACa;CACb,MAAM,kBAAkB,MAAM,sBAAsB,IAAI,UAAU;CAClE,IAAI,oBAAoB,QAAQ,kBAAA,GAC5B,MAAM,IAAI,uBAAuB,iBAAA,CAAoC;AAE7E;;;;;;;AAQA,eAAsB,uBAClB,IACA,YACa;CACb,MAAM,YAAY,IAAI,WAAW;CACjC,MAAM,GAAG,QAAQ,GAAG;qCACa,IAAI,IAAI,SAAS,EAAE;;;;;KAKnD;CACD,MAAM,GAAG,QAAQ,GAAG;sBACF,IAAI,IAAI,SAAS,EAAE;kBACvB,YAAY,IAAI,OAAA,CAA0B,EAAE;;KAEzD;AACL;;;;;;;;;;;;;;;;;AA8BA,eAAsB,gBAClB,IACA,YAC8B;CAC9B,MAAM,WAAqB,CAAC;CAC5B,IAAI,kBAAiC;CAErC,IAAI;EACA,kBAAkB,MAAM,sBAAsB,IAAI,UAAU;EAC5D,IAAI,oBAAoB,QAAQ,kBAAA,GAC5B,SAAS,KACL,sCAAsC,gBAAgB,4EAE1D;EAGJ,MAAM,gBAAgB,IAAI,WAAW;EAErC,IAAI,EAAE,MADgB,GAAG,QAAQ,GAAG,sBAAsB,cAAc,yBAAyB,EAAA,CACnF,KAAK,EAAE,EAAuC,SAGxD,OAAO;GAAE,SAAS,SAAS,WAAW;GAAG;GAAiB,gBAAA;GAAqC;EAAS;EAG5G,MAAM,UAAU,MAAM,GAAG,QAAQ,GAAG;;mCAET,WAAW;SACrC;EACD,MAAM,QAAQ,IAAI,IAAK,QAAQ,KAAmC,KAAI,QAAO,IAAI,WAAW,CAAC;EAC7F,MAAM,UAAU,+BAA+B,QAAO,WAAU,CAAC,MAAM,IAAI,MAAM,CAAC;EAClF,IAAI,QAAQ,SAAS,GACjB,SAAS,KACL,6BAA6B,QAAQ,KAAK,IAAI,EAAE,2FAEpD;EAWJ,KAAI,MARkB,GAAG,QAAQ,GAAG;;;;gCAIZ,WAAW;;gCAEX,iCAAiC;SACxD,EAAA,CACW,KAAK,SAAS,GACtB,SAAS,KACL,gCAAgC,iCAAiC,6DAErE;CAER,SAAS,OAAgB;EACrB,SAAS,KACL,6BAA6B,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GACtF;CACJ;CAEA,OAAO;EACH,SAAS,SAAS,WAAW;EAC7B;EACA,gBAAA;EACA;CACJ;AACJ;;;;;;;;;;AC7OA,eAAsB,sBAAsB,IAAoB,YAA8C;CAC1G,OAAO,MAAM,4BAA4B;CAOzC,MAAM,2BAA2B,IAAI,kBAAkB,UAAU,CAAC;CAElE,IAAI;EAEA,IAAI,iBAAiB;EACrB,IAAI,aAAa;EACjB,IAAI,cAAc;EAClB,IAAI,gBAAgB;EACpB,IAAI,YAAY;GACZ,gBAAiB,WAAW,cAAc,OAAO,WAAW,UAAU,WAChE,WAAW,QACX,WAAW;GACjB,cAAe,YAAY,cAAc,OAAO,WAAW,WAAW,WAChE,WAAW,SACX;GACN,iBAAiB,gBAAgB,WAC3B,IAAI,cAAc,KAClB,IAAI,YAAY,KAAK,cAAc;GAazC,MAAM,SAAS,WAAW,YAAY;GACtC,IAAI,QAAQ;IACR,MAAM,OAAQ,UAAU,SAAW,OAA8C,OAAO,KAAA;IACxF,IAAI,SAAS,QACT,aAAa;SACV,IAAI,SAAS,aAChB,aAAa;GAGrB;EACJ;EAGA,IAAI;GACA,MAAM,SAAS,MAAM,GAAG,QAAQ,GAAG;;;uCAGR,YAAY;qCACd,cAAc;;aAEtC;GACD,IAAI,UAAU,OAAO,QAAQ,OAAO,KAAK,SAAS,GAAG;IACjD,MAAM,SAAS,OAAQ,OAAO,KAAK,EAAE,CAA2B,SAAS,CAAC,CAAC,YAAY;IACvF,IAAI,WAAW,QACX,aAAa;SACV,IAAI,WAAW,aAAa,WAAW,cAAc,WAAW,UACnE,aAAa;SAEb,aAAa;IAEjB,OAAO,MAAM,cAAc,eAAe,0BAA0B,OAAO,wBAAwB,YAAY;GACnH;EACJ,SAAS,KAAK;GAEV,OAAO,KAAK,2BAA2B,eAAe,uDAAuD,cAAc,EAAE,OAAO,IAAI,CAAC;EAC7I;EAIA,IAAI,gBAAgB,UAChB,MAAM,GAAG,QAAQ,GAAG,+BAA+B,IAAI,IAAI,WAAW,GAAG;EAE7E,MAAM,GAAG,QAAQ,GAAG,oCAAoC;EAExD,MAAM,aAAa,gBAAgB,WAAW,WAAW;EACzD,MAAM,sBAAsB,IAAI,WAAW;EAC3C,MAAM,yBAAyB,IAAI,WAAW;EAC9C,MAAM,+BAA+B,IAAI,WAAW;EACpD,MAAM,qBAAqB,IAAI,WAAW;EAQ1C,MAAM,YAAY,eAAe,SAC3B,8BACA,eAAe,YACX,iCACA;EAQV,MAAM,kBAAkB,WAAmB,GAAG,cAAc,GAAG,SAAS,MAAM,GAAG,EAAE;EACnF,MAAM,wBAAwB,IAAI,eAAe,oBAAoB,EAAE;EACvE,MAAM,wBAAwB,eAAe,iBAAiB;EAC9D,MAAM,yBAAyB,eAAe,8BAA8B;EAsB5E,MAAM,iBAAiB,mBAClB,KAAK,SAAS,KAAK,WAAW,UACzB,GAAG,KAAK,OAAO,GAAG,mBAAmB,IAAI,EAAE,cAAc,sBAAsB,iCAC/E,GAAG,KAAK,OAAO,GAAG,mBAAmB,IAAI,GAAG,CAAC,CAClD,KAAK,qBAAqB;EAC/B,MAAM,GAAG,QAAQ,GAAG;yCACa,IAAI,IAAI,cAAc,EAAE;qBAC5C,IAAI,IAAI,UAAU,EAAE,eAAe,IAAI,IAAI,SAAS,EAAE;kBACzD,IAAI,IAAI,cAAc,EAAE;;SAEjC;EAkBD,MAAM,iBAAiB;GACnB;GACA;GACA;GACA;GACA;GACA;EACJ;EAaA,MAAM,oBAAoB,eAAe,KAAI,MAAK,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI;EACrE,MAAM,sBAAsB,MAAM,GAAG,QAAQ,GAAG;;;mCAGrB,WAAW;mCACX,IAAI,IAAI,iBAAiB,EAAE;;;SAGrD;EAED,IAAI,oBAAoB,KAAK,SAAS,GAClC,MAAM,GAAG,QAAQ,GAAG;6CACa,IAAI,IAAI,IAAI,WAAW,EAAE,EAAE;;;;;;;;;aAS3D;EAGL,KAAK,MAAM,aAAa,oBAAoB,KAAK,SAAS,IAAI,iBAAiB,CAAC,GAAG;GAC/E,MAAM,YAAY,IAAI,WAAW,KAAK,UAAU;GAChD,MAAM,GAAG,QAAQ,GAAG;;;;;;;;;;;2CAWW,IAAI,IAAI,IAAI,WAAW,EAAE,EAAE;yCAC7B,IAAI,IAAI,IAAI,UAAU,EAAE,EAAE;;;;;;;;;kCASjC,IAAI,IAAI,gBAAgB,UAAU,kBAAkB,WAAW,cAAc,eAAe,wBAAwB,EAAE;kCACtH,IAAI,IAAI,WAAW,UAAU,sCAAsC,EAAE;kCACrE,IAAI,IAAI,mCAAmC,UAAU,UAAU,UAAU,OAAO,EAAE;;;;;;8BAMtF,IAAI,IAAI,gBAAgB,UAAU,qCAAqC,EAAE;;8BAEzE,IAAI,IAAI,+CAA+C,UAAU,EAAE,EAAE;8BACrE,IAAI,IAAI,+DAA+D,UAAU,kCAAkC,WAAW,sBAAsB,EAAE;;aAEvK;EACL;EAKA,MAAM,GAAG,QAAQ,GAAG;yCACa,IAAI,IAAI,mBAAmB,EAAE;;sBAEhD,IAAI,IAAI,UAAU,EAAE,uBAAuB,IAAI,IAAI,cAAc,EAAE;;;;;;;;SAQhF;EAGD,MAAM,GAAG,QAAQ,GAAG;;iBAEX,IAAI,IAAI,mBAAmB,EAAE;SACrC;EAOD,MAAM,GAAG,QAAQ,GAAG;yCACa,IAAI,IAAI,sBAAsB,EAAE;;sBAEnD,IAAI,IAAI,UAAU,EAAE,uBAAuB,IAAI,IAAI,cAAc,EAAE;;;;;;;;;;;SAWhF;EAGD,MAAM,GAAG,QAAQ,GAAG;;iBAEX,IAAI,IAAI,sBAAsB,EAAE;SACxC;EAGD,MAAM,GAAG,QAAQ,GAAG;;iBAEX,IAAI,IAAI,sBAAsB,EAAE;SACxC;EAGD,MAAM,GAAG,QAAQ,GAAG;yCACa,IAAI,IAAI,4BAA4B,EAAE;;sBAEzD,IAAI,IAAI,UAAU,EAAE,uBAAuB,IAAI,IAAI,cAAc,EAAE;;;;;;SAMhF;EAGD,MAAM,GAAG,QAAQ,GAAG;;iBAEX,IAAI,IAAI,4BAA4B,EAAE;SAC9C;EAGD,MAAM,GAAG,QAAQ,GAAG;;iBAEX,IAAI,IAAI,4BAA4B,EAAE;SAC9C;EAGD,MAAM,2BAA2B,IAAI,WAAW;EAChD,MAAM,GAAG,QAAQ,GAAG;yCACa,IAAI,IAAI,wBAAwB,EAAE;;sBAErD,IAAI,IAAI,UAAU,EAAE,uBAAuB,IAAI,IAAI,cAAc,EAAE;;;;;;SAMhF;EAGD,MAAM,GAAG,QAAQ,GAAG;;iBAEX,IAAI,IAAI,wBAAwB,EAAE;SAC1C;EAGD,MAAM,GAAG,QAAQ,GAAG;;iBAEX,IAAI,IAAI,wBAAwB,EAAE;SAC1C;EAGD,MAAM,GAAG,QAAQ,GAAG;yCACa,IAAI,IAAI,kBAAkB,EAAE;;;;;SAK5D;EAYD,MAAM,GAAG,YAAY,OAAO,OAAO;GAC/B,MAAM,GAAG,QAAQ,GAAG,sEAAsE;GAC1F,KAAK,MAAM,aAAa,0BACpB,MAAM,GAAG,QAAQ,IAAI,IAAI,SAAS,CAAC;EAE3C,CAAC;EAeD,KAAK,MAAM,QAAQ,oBAAoB;GACnC,IAAI,KAAK,WAAW,SAAS;GAC7B,MAAM,GAAG,QAAQ,GAAG;8BACF,IAAI,IAAI,cAAc,EAAE;2CACX,IAAI,IAAI,GAAG,KAAK,OAAO,GAAG,mBAAmB,IAAI,GAAG,EAAE;aACpF;EACL;EAkBA,MAAM,mBAAkB,MAXG,GAAG,QAAQ,GAAG;;;mCAGd,YAAY,oBAAoB,cAAc;SACxE,EAAA,CAOoC;EACrC,MAAM,mBAAmB,IAAI,IAAI,gBAAgB,KAAI,QAAO,CAAC,IAAI,aAAa,IAAI,SAAS,CAAC,CAAC;EAC7F,MAAM,mBAAmB,IAAI,IAAI,gBAAgB,KAAI,QAAO,CAAC,IAAI,aAAa,GAAG,CAAC,CAAC;EAgBnF,KAAK,MAAM,QAAQ,oBAAoB;GACnC,MAAM,QAAQ,iBAAiB,IAAI,KAAK,MAAM;GAC9C,IAAI,CAAC,OAAO;GAEZ,IAAI,KAAK,YAAY,KAAA,KAAa,MAAM,mBAAmB,MAAM;IAC7D,MAAM,GAAG,QAAQ,GAAG;kCACF,IAAI,IAAI,cAAc,EAAE;mCACvB,IAAI,IAAI,IAAI,KAAK,OAAO,EAAE,EAAE,eAAe,IAAI,IAAI,KAAK,OAAO,EAAE;iBACnF;IACD,OAAO,KAAK,8BAA8B,eAAe,GAAG,KAAK,QAAQ;GAC7E;GAEA,IAAI,CAAC,KAAK,WAAW,MAAM,gBAAgB,OAAO;GAElD,IAAI,KAAK,YAAY,KAAA,GACjB,MAAM,GAAG,QAAQ,GAAG;6BACP,IAAI,IAAI,cAAc,EAAE;0BAC3B,IAAI,IAAI,IAAI,KAAK,OAAO,EAAE,EAAE,KAAK,IAAI,IAAI,KAAK,OAAO,EAAE;4BACrD,IAAI,IAAI,IAAI,KAAK,OAAO,EAAE,EAAE;iBACvC;GAEL,IAAI;IACA,MAAM,GAAG,QAAQ,GAAG;kCACF,IAAI,IAAI,cAAc,EAAE;mCACvB,IAAI,IAAI,IAAI,KAAK,OAAO,EAAE,EAAE;iBAC9C;IACD,OAAO,KAAK,2BAA2B,eAAe,GAAG,KAAK,QAAQ;GAC1E,SAAS,KAAK;IACV,OAAO,KACH,MAAM,eAAe,GAAG,KAAK,OAAO,0HAEnC,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EACpD;GACJ;EACJ;EAQA,KAAK,MAAM,UAAU;GAAC;GAAS;GAAgB;GAAa;GAAiB;EAA0B,GAAG;GACtG,IAAI,iBAAiB,IAAI,MAAM,MAAM,qBAAqB;GAC1D,MAAM,GAAG,QAAQ,GAAG;8BACF,IAAI,IAAI,cAAc,EAAE;+BACvB,IAAI,IAAI,IAAI,OAAO,EAAE,EAAE;aACzC;GACD,OAAO,KAAK,cAAc,eAAe,GAAG,OAAO,yBAAyB;EAChF;EAoBA,IAAI,iBAAiB,IAAI,OAAO;QAMxB,MALuB,GAAG,QAAQ,GAAG;;;oCAGjB,YAAY,mBAAmB,sBAAsB;aAC5E,EAAA,CACgB,KAAK,WAAW,GAAG;IAMhC,MAAM,aAAa,MAAM,GAAG,QAAQ,GAAG;;2BAE5B,IAAI,IAAI,cAAc,EAAE;;;;;iBAKlC;IACD,IAAI,WAAW,KAAK,SAAS,GAAG;KAC5B,MAAM,SAAU,WAAW,KACtB,KAAI,QAAO,GAAG,IAAI,WAAW,KAAK,IAAI,YAAY,EAAE,CAAC,CACrD,KAAK,IAAI;KACd,OAAO,MACH,yDAAyD,eAAe,2EACE,OAAO,gJAGrF;IACJ,OAAO;KACH,MAAM,SAAS,MAAM,GAAG,QAAQ,GAAG;iCACtB,IAAI,IAAI,cAAc,EAAE;;;qBAGpC;KACD,IAAI,OAAO,UACP,OAAO,KAAK,kBAAkB,OAAO,SAAS,wBAAwB,gBAAgB;KAE1F,MAAM,GAAG,QAAQ,GAAG;4DACoB,IAAI,IAAI,IAAI,sBAAsB,EAAE,EAAE;6BACrE,IAAI,IAAI,cAAc,EAAE;qBAChC;KACD,OAAO,KAAK,yBAAyB,eAAe,yBAAyB;IACjF;GACJ;;EAUJ,IAAI,iBAAiB,IAAI,OAAO;QASxB,MARuB,GAAG,QAAQ,GAAG;;;;oCAIjB,YAAY;oCACZ,cAAc;oCACd,eAAe,oBAAoB,EAAE;aAC5D,EAAA,CACgB,KAAK,WAAW,GAC7B,MAAM,GAAG,QAAQ,GAAG;kCACF,IAAI,IAAI,cAAc,EAAE;qCACrB,IAAI,IAAI,qBAAqB,EAAE;iBACnD;EAAA;EAST,IAAI,iBAAiB,IAAI,0BAA0B,GAC/C,MAAM,GAAG,QAAQ,GAAG;6CACa,IAAI,IAAI,IAAI,uBAAuB,EAAE,EAAE;qBAC/D,IAAI,IAAI,cAAc,EAAE;;aAEhC;EAuBL,IAAI;GAMA,MAAM,SAAS,MALQ,GAAG,QAAQ,GAAG;;;;aAIpC,EAAA,CACuB;GACxB,OAAO,MAAM,sCAAsC,MAAM,OAAO,aAAa,MAAM,KAAI,MAAK,IAAI,EAAE,aAAa,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC,KAAK,IAAI,KAAK,UAAU;GAC7J,KAAK,MAAM,EAAE,kBAAkB,OAAO;IAClC,MAAM,YAAY,IAAI,aAAa;IACnC,IAAI;KAMA,MAAM,GAAG,QAAQ,GAAG,eAAe,IAAI,IAAI,SAAS,EAAE,0CAA0C;KAChG,MAAM,GAAG,QAAQ,GAAG,eAAe,IAAI,IAAI,SAAS,EAAE,iEAAiE;KACvH,MAAM,GAAG,QAAQ,GAAG,eAAe,IAAI,IAAI,SAAS,EAAE,8DAA8D;KACpH,MAAM,GAAG,QAAQ,GAAG,eAAe,IAAI,IAAI,SAAS,EAAE,sEAAsE;KAM5H,MAAM,GAAG,QAAQ,GAAG,eAAe,IAAI,IAAI,SAAS,EAAE,mCAAmC;KAKzF,MAAM,GAAG,QAAQ,GAAG;iCACP,IAAI,IAAI,SAAS,EAAE;;;qBAG/B;KACD,MAAM,GAAG,QAAQ,GAAG;iCACP,IAAI,IAAI,SAAS,EAAE;;;qBAG/B;KACD,MAAM,GAAG,QAAQ,GAAG,eAAe,IAAI,IAAI,SAAS,EAAE,6DAA6D;KACnH,MAAM,GAAG,QAAQ,GAAG,eAAe,IAAI,IAAI,SAAS,EAAE,mDAAmD;KAIzG,MAAM,GAAG,QAAQ,GAAG,eAAe,IAAI,IAAI,SAAS,EAAE,sCAAsC;KAC5F,MAAM,GAAG,QAAQ,GAAG,eAAe,IAAI,IAAI,SAAS,EAAE,8CAA8C;KAEpG,MAAM,GAAG,QAAQ,GAAG;;6BAEX,IAAI,IAAI,SAAS,EAAE;qBAC3B;KAKD,MAAM,GAAG,QAAQ,GAAG,eAAe,IAAI,IAAI,SAAS,EAAE,iDAAiD;KACvG,OAAO,MAAM,4DAA4D,WAAW;IACxF,SAAS,eAAwB;KAC7B,OAAO,KAAK,2CAA2C,UAAU,IAAI,yBAAyB,QAAQ,cAAc,UAAU,OAAO,aAAa,GAAG;IACzJ;GACJ;EACJ,SAAS,gBAAyB;GAC9B,OAAO,KAAK,iDAAiD,0BAA0B,QAAQ,eAAe,UAAU,OAAO,cAAc,GAAG;EACpJ;EAKA,IAAI;GASA,KAFyB,MANC,GAAG,QAAQ,GAAG;;;;;aAKvC,EAAA,CACoC,KAAK,EAAE,CAAiC,gBAExD;IACjB,OAAO,KAAK,oDAAoD;IAEhE,MAAM,GAAG,QAAQ,GAAG;6BACP,IAAI,IAAI,cAAc,EAAE;;;;;;;iBAOpC;IAGD,MAAM,GAAG,QAAQ,GAAG,oDAAoD;IACxE,MAAM,GAAG,QAAQ,GAAG,+CAA+C;IACnE,OAAO,KAAK,4CAA4C;GAC5D;EACJ,SAAS,gBAAyB;GAE9B,OAAO,KAAK,uCAAuC,0BAA0B,QAAQ,eAAe,UAAU,OAAO,cAAc,GAAG;EAC1I;EAGA,MAAM,sBAAsB,IAAI,WAAW;EAC3C,MAAM,yBAAyB,IAAI,WAAW;EAC9C,MAAM,yBAAyB,IAAI,WAAW;EAG9C,MAAM,GAAG,QAAQ,GAAG;yCACa,IAAI,IAAI,mBAAmB,EAAE;;sBAEhD,IAAI,IAAI,UAAU,EAAE,uBAAuB,IAAI,IAAI,cAAc,EAAE;;;;;;;;;SAShF;EAGD,MAAM,GAAG,QAAQ,GAAG;;iBAEX,IAAI,IAAI,mBAAmB,EAAE;SACrC;EAGD,MAAM,GAAG,QAAQ,GAAG;yCACa,IAAI,IAAI,sBAAsB,EAAE;;qDAEpB,IAAI,IAAI,mBAAmB,EAAE;;;;;;;SAOzE;EAGD,MAAM,GAAG,QAAQ,GAAG;;iBAEX,IAAI,IAAI,sBAAsB,EAAE;SACxC;EASD,IAAI;GACA,MAAM,GAAG,QAAQ,GAAG,eAAe,IAAI,IAAI,mBAAmB,EAAE,mDAAmD;GACnH,MAAM,GAAG,QAAQ,GAAG,eAAe,IAAI,IAAI,sBAAsB,EAAE,8DAA8D;EACrI,SAAS,mBAA4B;GACjC,OAAO,KAAK,sCAAsC,6BAA6B,QAAQ,kBAAkB,UAAU,OAAO,iBAAiB,GAAG;EAClJ;EAGA,MAAM,GAAG,QAAQ,GAAG;yCACa,IAAI,IAAI,sBAAsB,EAAE;;sBAEnD,IAAI,IAAI,UAAU,EAAE,uBAAuB,IAAI,IAAI,cAAc,EAAE;;;;;SAKhF;EAGD,MAAM,GAAG,QAAQ,GAAG;;iBAEX,IAAI,IAAI,sBAAsB,EAAE;SACxC;EASD,IAAI;GAMA,MAAM,iBAAqC;IACvC,CAAC,aAAa,aAAa;IAC3B,CAAC,YAAY,iBAAiB;IAC9B,CAAC,YAAY,gBAAgB;IAC7B,CAAC,YAAY,uBAAuB;IACpC,CAAC,YAAY,mBAAmB;IAChC,CAAC,YAAY,YAAY;IACzB,CAAC,YAAY,aAAa;IAC1B,CAAC,YAAY,gBAAgB;IAC7B,CAAC,YAAY,gBAAgB;IAC7B,CAAC,YAAY,aAAa;GAC9B;GACA,KAAK,MAAM,CAAC,YAAY,cAAc,gBASlC,KAAI,MARiB,GAAG,QAAQ,GAAG;;;;wCAIX,WAAW;wCACX,UAAU;;iBAEjC,EAAA,CACU,KAAK,SAAS,GAAG;IACxB,MAAM,GAAG,QAAQ,GAAG;sCACF,IAAI,IAAI,IAAI,WAAW,KAAK,UAAU,EAAE,EAAE;;qBAE3D;IACD,OAAO,KACH,iDAAiD,WAAW,KAAK,UAAU,uFAE/E;GACJ;EAER,SAAS,mBAA4B;GAGjC,OAAO,KACH,oEACG,6BAA6B,QAAQ,kBAAkB,UAAU,OAAO,iBAAiB,GAChG;EACJ;EAKA,MAAM,uBAAuB,IAAI,UAAU;EAe3C,MAAM,0BACF,OAAO,SAAS;GAAE,MAAM,GAAG,QAAQ,IAAI,IAAI,IAAI,CAAC;EAAG,GACnD,YACA,EACI,UAAU,OAAO,QAAQ,OAAO,KAC5B,qDAAqD,WAAW,KAAK,MAAM,QAC1E,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EACpD,EACJ,CACJ;EAEA,OAAO,MAAM,qBAAqB;CACtC,SAAS,OAAO;EAMZ,IAAI,iBAAiB,wBAAwB,MAAM;EACnD,OAAO,MAAM,kCAAkC,EAAE,MAAM,CAAC;EACxD,OAAO,KAAK,6CAA6C;CAC7D;AACJ;;;;;;;;;;;;;;;;;;;;;;ACr1BA,SAAS,aAAa,OAAkC,GAAG,MAAoC;CAC3F,IAAI,CAAC,OAAO,OAAO,KAAA;CACnB,KAAK,MAAM,OAAO,MAAM;EACpB,IAAI,OAAO,OAAO,OAAO;EACzB,MAAM,QAAQ,YAAY,GAAG;EAC7B,IAAI,SAAS,OAAO,OAAO;EAC3B,MAAM,QAAQ,UAAU,GAAG;EAC3B,IAAI,SAAS,OAAO,OAAO;CAC/B;AAEJ;AAEA,SAAS,UAAU,OAAkC,GAAG,MAAmD;CACvG,IAAI,CAAC,OAAO,OAAO,KAAA;CACnB,MAAM,MAAM,aAAa,OAAO,GAAG,IAAI;CACvC,OAAO,MAAM,MAAM,OAAO,KAAA;AAC9B;;;;;AA4BA,IAAa,cAAb,MAAmD;CAKnC;CAJZ;CACA;CAEA,YACI,IACA,eACF;EAFU,KAAA,KAAA;EAGR,IAAI,iBAAmB,cAA4C,OAAQ;GACvE,MAAM,SAAS;GACf,KAAK,aAAc,OAAO,SAAS;GACnC,KAAK,sBAAuB,OAAO,kBAAkB;EACzD,OAAO;GACH,MAAM,QAAQ;GACd,KAAK,aAAa,SAAU;GAC5B,KAAK,sBAAsB;EAC/B;CACJ;CAEA,6BAA6C;EACzC,MAAM,OAAO,aAAa,KAAK,UAAU;EAEzC,OAAO,IADQ,eAAe,KAAK,UAAU,CAAC,CAAC,UAAU,SACvC,KAAK,KAAK;CAChC;;;;;;;;;;;;;;;;CAiBA,MAAc,kBAAqB,IAAoD;EACnF,OAAO,MAAM,KAAK,GAAG,YAAY,OAAO,OAAO;GAC3C,MAAM,GAAG,QAAQ,GAAG;;;;;aAKnB;GACD,OAAO,MAAM,GAAG,EAA+B;EACnD,CAAC;CACL;CAEA,aAAqB,KAAwC;EACzD,IAAI,CAAC,KAAK,OAAO;EAEjB,MAAM,KAAM,IAAI,MAAM,IAAI;EAC1B,MAAM,QAAQ,IAAI;EAClB,MAAM,eAAgB,IAAI,iBAAiB,IAAI,gBAAgB;EAC/D,MAAM,cAAe,IAAI,gBAAgB,IAAI,eAAe;EAC5D,MAAM,WAAY,IAAI,aAAa,IAAI,YAAY,IAAI,YAAY;EACnE,MAAM,gBAAiB,IAAI,kBAAkB,IAAI,iBAAiB;EAClE,MAAM,yBAA0B,IAAI,4BAA4B,IAAI,0BAA0B;EAC9F,MAAM,0BAA2B,IAAI,8BAA8B,IAAI,2BAA2B;EAClG,MAAM,cAAe,IAAI,gBAAgB,IAAI,eAAe;EAC5D,MAAM,YAAa,IAAI,cAAc,IAAI;EACzC,MAAM,YAAa,IAAI,cAAc,IAAI;EAEzC,MAAM,WAAgC,EAAE,GAAK,IAAI,YAAgD,CAAC,EAAG;EAErG,MAAM,4BAAY,IAAI,IAAI;GACtB;GAAM;GAAO;GACb;GAAiB;GACjB;GAAgB;GAChB;GAAa;GAAY;GACzB;GAAkB;GAClB;GAA4B;GAC5B;GAA8B;GAC9B;GAAgB;GAChB;GACA;GAAc;GACd;GAAc;GACd;EACJ,CAAC;EAED,KAAK,MAAM,CAAC,KAAK,QAAQ,OAAO,QAAQ,GAAG,GACvC,IAAI,CAAC,UAAU,IAAI,GAAG,GAAG;GACrB,MAAM,WAAW,UAAU,GAAG;GAC9B,SAAS,YAAY;EACzB;EAGJ,OAAO;GACH;GACA;GACA;GACA;GACA;GACA;GACA;GACA,yBAAyB,0BAA0B,IAAI,KAAK,uBAAuB,IAAI;GACvF;GACA,WAAW,YAAY,IAAI,KAAK,SAAS,oBAAI,IAAI,KAAK;GACtD,WAAW,YAAY,IAAI,KAAK,SAAS,oBAAI,IAAI,KAAK;GACtD;EACJ;CACJ;CAEA,WAAmB,MAAwD;EACvE,IAAI,CAAC,MAAM,OAAO,CAAC;EAEnB,MAAM,UAAmC,CAAC;EAE1C,MAAM,QAAQ,aAAa,KAAK,YAAY,IAAI,KAAK;EACrD,MAAM,WAAW,aAAa,KAAK,YAAY,OAAO,KAAK;EAC3D,MAAM,kBAAkB,aAAa,KAAK,YAAY,gBAAgB,eAAe,KAAK;EAC1F,MAAM,iBAAiB,aAAa,KAAK,YAAY,eAAe,cAAc,KAAK;EACvF,MAAM,cAAc,aAAa,KAAK,YAAY,YAAY,WAAW,KAAK;EAC9E,MAAM,mBAAmB,aAAa,KAAK,YAAY,iBAAiB,gBAAgB,KAAK;EAC7F,MAAM,4BAA4B,aAAa,KAAK,YAAY,0BAA0B,0BAA0B,KAAK;EACzH,MAAM,6BAA6B,aAAa,KAAK,YAAY,2BAA2B,4BAA4B,KAAK;EAC7H,MAAM,iBAAiB,aAAa,KAAK,YAAY,eAAe,cAAc,KAAK;EACvF,MAAM,eAAe,aAAa,KAAK,YAAY,aAAa,YAAY,KAAK;EACjF,MAAM,eAAe,aAAa,KAAK,YAAY,aAAa,YAAY,KAAK;EACjF,MAAM,cAAc,aAAa,KAAK,YAAY,UAAU,KAAK;EAEjE,IAAI,QAAQ,MAAM,QAAQ,SAAS,KAAK;EACxC,IAAI,WAAW,MAAM,QAAQ,YAAY,eAAe,KAAK,KAAK;EAClE,IAAI,kBAAkB,MAAM,QAAQ,mBAAmB,KAAK;EAC5D,IAAI,iBAAiB,MAAM,QAAQ,kBAAkB,KAAK;EAC1D,IAAI,cAAc,MAAM,QAAQ,eAAe,KAAK;EACpD,IAAI,mBAAmB,MAAM,QAAQ,oBAAoB,KAAK;EAC9D,IAAI,4BAA4B,MAAM,QAAQ,6BAA6B,KAAK;EAChF,IAAI,6BAA6B,MAAM,QAAQ,8BAA8B,KAAK;EAClF,IAAI,iBAAiB,MAAM,QAAQ,kBAAkB,KAAK;EAC1D,IAAI,eAAe,MAAM,QAAQ,gBAAgB,KAAK;EACtD,IAAI,eAAe,MAAM,QAAQ,gBAAgB,KAAK;EAEtD,MAAM,WAAgC,EAAE,GAAI,KAAK,YAAY,CAAC,EAAG;EACjE,MAAM,oBAAyC,CAAC;EAEhD,KAAK,MAAM,CAAC,KAAK,QAAQ,OAAO,QAAQ,QAAQ,GAAG;GAC/C,MAAM,cAAc,aAAa,KAAK,YAAY,GAAG;GACrD,IAAI,eACA,gBAAgB,SAChB,gBAAgB,YAChB,gBAAgB,mBAChB,gBAAgB,kBAChB,gBAAgB,eAChB,gBAAgB,oBAChB,gBAAgB,6BAChB,gBAAgB,8BAChB,gBAAgB,kBAChB,gBAAgB,gBAChB,gBAAgB,gBAChB,gBAAgB,aAChB,QAAQ,eAAe;QAEvB,kBAAkB,OAAO;EAEjC;EAEA,IAAI,eAAe,KAAK,YACpB,QAAQ,eAAe;EAG3B,OAAO;CACX;;;;;;;;;;;CAYA,MAAM,WAAW,MAAyC;EACtD,MAAM,UAAU,KAAK,WAAW,IAAI;EACpC,IAAI;GACA,MAAM,CAAC,OAAO,MAAM,KAAK,kBAAkB,OAAO,OAC7C,MAAM,GAAG,OAAO,KAAK,UAAU,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,UAAU,CAChE;GACA,OAAO,KAAK,aAAa,GAAG;EAChC,SAAS,OAAO;GAGZ,IAAI,eAAe,KAAK,CAAC,EAAE,SAAS,SAChC,MAAM,SAAS,SAAS,4BAA4B,cAAc;GAEtE,MAAM;EACV;CACJ;CAEA,MAAM,YAAY,IAAsC;EACpD,MAAM,QAAQ,UAAU,KAAK,YAAY,IAAI;EAC7C,IAAI,CAAC,OAAO,OAAO;EACnB,MAAM,CAAC,OAAO,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,KAAK,KAAK,UAAU,CAAC,CAAC,MAAM,GAAG,OAAO,EAAE,CAAC;EAC9E,OAAO,MAAM,KAAK,aAAa,GAA8B,IAAI;CACrE;CAEA,MAAM,eAAe,OAAyC;EAC1D,MAAM,WAAW,UAAU,KAAK,YAAY,OAAO;EACnD,IAAI,CAAC,UAAU,OAAO;EACtB,MAAM,CAAC,OAAO,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,KAAK,KAAK,UAAU,CAAC,CAAC,MAAM,GAAG,UAAU,eAAe,KAAK,CAAC,CAAC;EACpG,OAAO,MAAM,KAAK,aAAa,GAA8B,IAAI;CACrE;CAEA,MAAM,kBAAkB,UAAkB,YAA8C;EACpF,MAAM,YAAY,UAAU,KAAK,YAAY,IAAI;EACjD,IAAI,CAAC,WAAW,OAAO;EAEvB,MAAM,SAAS,MAAM,KAAK,GACrB,OAAO,EAAE,MAAM,KAAK,WAAW,CAAC,CAAC,CACjC,KAAK,KAAK,UAAU,CAAC,CACrB,UAAU,KAAK,qBAAqB,GAAG,WAAW,KAAK,oBAAoB,GAAG,CAAC,CAAC,CAChF,MACG,GAAG,GAAG,KAAK,oBAAoB,SAAS,KAAK,SAAS,OAAO,KAAK,oBAAoB,WAAW,KAAK,YAC1G,CAAC,CACA,MAAM,CAAC;EAEZ,IAAI,OAAO,WAAW,GAAG,OAAO;EAChC,OAAO,KAAK,aAAa,OAAO,EAAE,CAAC,IAA+B;CACtE;CAEA,MAAM,kBAAkB,KAA0C;EAC9D,MAAM,SAAS,eAAe,KAAK,mBAAmB,CAAC,CAAC,UAAU;EAOlE,QAAO,MANc,KAAK,GAAG,QAAQ,GAAG;;mBAE7B,IAAI,IAAI,IAAI,OAAO,oBAAoB,EAAE;0BAClC,IAAI;SACrB,EAAA,CAEa,KAAK,KAAK,SAAkC;GACtD,IAAI,IAAI;GACR,KAAK,IAAI;GACT,UAAU,IAAI;GACd,YAAY,IAAI;GAChB,aAAc,IAAI,gBAAmD;GACrE,WAAW,IAAI;GACf,WAAW,IAAI;EACnB,EAAE;CACN;CAEA,MAAM,iBAAiB,KAAa,UAAkB,YAAoB,aAAsD;EAC5H,MAAM,KAAK,kBAAkB,OAAO,OAAO,GAAG,OAAO,KAAK,mBAAmB,CAAC,CAAC,OAAO;GAClF;GACA;GACA;GACA,aAAa,eAAe;EAChC,CAAC,CAAC,CAAC,oBAAoB,EAAE,QAAQ,CAAC,KAAK,oBAAoB,UAAU,KAAK,oBAAoB,UAAU,EAAE,CAAC,CAAC;CAChH;CAEA,MAAM,WAAW,IAAY,MAAqE;EAC9F,MAAM,QAAQ,UAAU,KAAK,YAAY,IAAI;EAC7C,IAAI,CAAC,OAAO,OAAO;EACnB,MAAM,UAAU,KAAK,WAAW,IAAI;EACpC,MAAM,eAAe,aAAa,KAAK,YAAY,aAAa,YAAY,KAAK;EACjF,QAAQ,gCAAgB,IAAI,KAAK;EAEjC,MAAM,CAAC,OAAO,MAAM,KAAK,kBAAkB,OAAO,OAC7C,MAAM,GACF,OAAO,KAAK,UAAU,CAAC,CACvB,IAAI,OAAO,CAAC,CACZ,MAAM,GAAG,OAAO,EAAE,CAAC,CAAC,CACpB,UAAU,CACnB;EACA,OAAO,MAAM,KAAK,aAAa,GAAG,IAAI;CAC1C;CAEA,MAAM,WAAW,IAA2B;EACxC,MAAM,QAAQ,UAAU,KAAK,YAAY,IAAI;EAC7C,IAAI,CAAC,OAAO;EACZ,MAAM,KAAK,kBAAkB,OAAO,OAAO,GAAG,OAAO,KAAK,UAAU,CAAC,CAAC,MAAM,GAAG,OAAO,EAAE,CAAC,CAAC;CAC9F;CAEA,MAAM,YAAiC;EAEnC,QAAQ,MADW,KAAK,GAAG,OAAO,CAAC,CAAC,KAAK,KAAK,UAAU,EAAA,CACb,KAAI,QAAO,KAAK,aAAa,GAAG,CAAC;CAChF;CAEA,MAAM,mBAAmB,SAA2D;EAChF,MAAM,QAAQ,SAAS,SAAS;EAChC,MAAM,SAAS,SAAS,UAAU;EAClC,MAAM,SAAS,SAAS,QAAQ,KAAK,KAAK;EAC1C,MAAM,UAAU,SAAS,WAAW;EACpC,MAAM,WAAW,SAAS,YAAY;EACtC,MAAM,SAAS,SAAS;EAExB,MAAM,WAAW,UAAU,KAAK,YAAY,OAAO;EACnD,MAAM,cAAc,WAAW,SAAS,OAAO;EAC/C,MAAM,YAAY,aAAa,QAAQ,GAAG,QAAQ,GAAG;EAErD,MAAM,WAAW,UAAU,KAAK,YAAY,OAAO;EACnD,MAAM,cAAc,WAAW,SAAS,OAAO;EAC/C,MAAM,iBAAiB,UAAU,KAAK,YAAY,eAAe,cAAc;EAC/E,MAAM,oBAAoB,iBAAiB,eAAe,OAAO;EACjE,MAAM,QAAQ,UAAU,KAAK,YAAY,IAAI;EAC5B,SAAQ,MAAM;EAE/B,MAAM,iBAAiB,KAAK,2BAA2B;EACvD,MAAM,aAAa,CAAC;EACpB,IAAI,QACA,WAAW,KAAK,GAAG,GAAG,OAAO,SAAS,IAAI,IAAI,cAAc,EAAE,QAAQ;EAE1E,IAAI,QAAQ;GAKR,MAAM,UAAU,IAAI,kBAAkB,MAAM,EAAE;GAC9C,WAAW,KAAK,GAAG,IAAI,IAAI,IAAI,cAAc,EAAE,GAAG,IAAI,IAAI,WAAW,EAAE,SAAS,QAAQ,MAAM,IAAI,IAAI,cAAc,EAAE,GAAG,IAAI,IAAI,iBAAiB,EAAE,SAAS,QAAQ,EAAE;EAC3K;EAEA,MAAM,cAAc,WAAW,SAAS,IAAI,GAAG,SAAS,IAAI,KAAK,YAAY,GAAG,OAAO,MAAM,GAAG;EAGhG,MAAM,gBAAgB,SAChB,GAAG,YAAY,IAAI,IAAI,cAAc,EAAE,GAAG,IAAI,IAAI,WAAW,EAAE,GAAG,cAClE,GAAG,yBAAyB,IAAI,IAAI,cAAc,EAAE,8BAA8B,IAAI,IAAI,cAAc,EAAE,GAAG,IAAI,IAAI,WAAW,EAAE,GAAG;EAM3I,MAAM,SAAS,MAJW,KAAK,GAAG,QAAQ,GAAG;iDACJ,IAAI,IAAI,cAAc,EAAE;cAC3D,YAAY;SACjB,EAAA,CAC0B,KAAK,EAAE,CAAuB;EAazD,OAAO;GAAE,QALI,MANY,KAAK,GAAG,QAAQ,GAAG;4BACxB,IAAI,IAAI,cAAc,EAAE;cACtC,YAAY;cACZ,cAAc;oBACR,MAAM,UAAU,OAAO;SAClC,EAAA,CACuB,KAG4C,KAAK,QAAQ,KAAK,aAAa,GAAG,CAEtF;GACZ;GACA;GACA;EAAO;CACf;;;;CAKA,MAAM,eAAe,IAAY,cAAqC;EAClE,MAAM,QAAQ,UAAU,KAAK,YAAY,IAAI;EAC7C,IAAI,CAAC,OAAO;EACZ,MAAM,qBAAqB,aAAa,KAAK,YAAY,gBAAgB,eAAe,KAAK;EAC7F,MAAM,kBAAkB,aAAa,KAAK,YAAY,aAAa,YAAY,KAAK;EAEpF,MAAM,KAAK,kBAAkB,OAAO,OAAO,GACtC,OAAO,KAAK,UAAU,CAAC,CACvB,IAAI;IACA,qBAAqB;IACrB,kCAAkB,IAAI,KAAK;EAChC,CAAC,CAAC,CACD,MAAM,GAAG,OAAO,EAAE,CAAC,CAAC;CAC7B;;;;CAKA,MAAM,iBAAiB,IAAY,UAAkC;EACjE,MAAM,QAAQ,UAAU,KAAK,YAAY,IAAI;EAC7C,IAAI,CAAC,OAAO;EACZ,MAAM,sBAAsB,aAAa,KAAK,YAAY,iBAAiB,gBAAgB,KAAK;EAChG,MAAM,+BAA+B,aAAa,KAAK,YAAY,0BAA0B,0BAA0B,KAAK;EAC5H,MAAM,kBAAkB,aAAa,KAAK,YAAY,aAAa,YAAY,KAAK;EAEpF,MAAM,KAAK,kBAAkB,OAAO,OAAO,GACtC,OAAO,KAAK,UAAU,CAAC,CACvB,IAAI;IACA,sBAAsB;IACtB,+BAA+B;IAC/B,kCAAkB,IAAI,KAAK;EAChC,CAAC,CAAC,CACD,MAAM,GAAG,OAAO,EAAE,CAAC,CAAC;CAC7B;;;;CAKA,MAAM,qBAAqB,IAAY,OAAqC;EACxE,MAAM,QAAQ,UAAU,KAAK,YAAY,IAAI;EAC7C,IAAI,CAAC,OAAO;EACZ,MAAM,+BAA+B,aAAa,KAAK,YAAY,0BAA0B,0BAA0B,KAAK;EAC5H,MAAM,gCAAgC,aAAa,KAAK,YAAY,2BAA2B,4BAA4B,KAAK;EAChI,MAAM,kBAAkB,aAAa,KAAK,YAAY,aAAa,YAAY,KAAK;EAEpF,MAAM,KAAK,kBAAkB,OAAO,OAAO,GACtC,OAAO,KAAK,UAAU,CAAC,CACvB,IAAI;IACA,+BAA+B;IAC/B,gCAAgC,wBAAQ,IAAI,KAAK,IAAI;IACrD,kCAAkB,IAAI,KAAK;EAChC,CAAC,CAAC,CACD,MAAM,GAAG,OAAO,EAAE,CAAC,CAAC;CAC7B;;;;CAKA,MAAM,2BAA2B,OAAyC;EACtE,MAAM,WAAW,UAAU,KAAK,YAAY,0BAA0B,0BAA0B;EAChG,IAAI,CAAC,UAAU,OAAO;EACtB,MAAM,CAAC,OAAO,MAAM,KAAK,GACpB,OAAO,CAAC,CACR,KAAK,KAAK,UAAU,CAAC,CACrB,MAAM,GAAG,UAAU,KAAK,CAAC;EAC9B,OAAO,MAAM,KAAK,aAAa,GAA8B,IAAI;CACrE;;;;CAKA,MAAM,aAAa,KAA8B;EAC7C,MAAM,iBAAiB,KAAK,2BAA2B;EACvD,MAAM,SAAS,MAAM,KAAK,GAAG,QAAQ,GAAG;gCAChB,IAAI,IAAI,cAAc,EAAE,cAAc,IAAI;SACjE;EAED,IAAI,OAAO,KAAK,WAAW,GAAG,OAAO,CAAC;EAKtC,QAHY,OAAO,KAAK,EACR,CAAI,SAAS,CAAC,EAAA,CAEf,KAAI,QAAO;GACtB;GACA,MAAM;GACN,SAAS,OAAO;GAChB,oBAAoB;GACpB,uBAAuB;EAC3B,EAAE;CACN;;;;CAKA,MAAM,eAAe,KAAgC;EACjD,MAAM,iBAAiB,KAAK,2BAA2B;EACvD,MAAM,SAAS,MAAM,KAAK,GAAG,QAAQ,GAAG;gCAChB,IAAI,IAAI,cAAc,EAAE,cAAc,IAAI;SACjE;EAED,IAAI,OAAO,KAAK,WAAW,GAAG,OAAO,CAAC;EAGtC,OADY,OAAO,KAAK,EACjB,CAAI,SAAS,CAAC;CACzB;;;;CAKA,MAAM,aAAa,KAAa,SAAkC;EAC9D,MAAM,iBAAiB,KAAK,2BAA2B;EACvD,MAAM,aAAa,IAAI,QAAQ,KAAK,GAAG,EAAE;EACzC,MAAM,KAAK,kBAAkB,OAAO,OAAO,GAAG,QAAQ,GAAG;qBAC5C,IAAI,IAAI,cAAc,EAAE;0BACnB,WAAW;yBACZ,IAAI;SACpB,CAAC;CACN;;;;CAKA,MAAM,kBAAkB,KAAa,QAA+B;EAChE,MAAM,iBAAiB,KAAK,2BAA2B;EACvD,MAAM,KAAK,kBAAkB,OAAO,OAAO,GAAG,QAAQ,GAAG;qBAC5C,IAAI,IAAI,cAAc,EAAE;8CACC,OAAO;yBAC5B,IAAI,YAAY,OAAO;SACvC,CAAC;CACN;;;;CAKA,MAAM,iBAAiB,KAAgE;EACnF,MAAM,OAAO,MAAM,KAAK,YAAY,GAAG;EACvC,IAAI,CAAC,MAAM,OAAO;EAGlB,OAAO;GAAE;GACL,OAAA,MAFgB,KAAK,aAAa,GAAG;EAE/B;CACd;AACJ;AAGA,IAAa,sBAAb,MAAiC;CAKjB;CAJZ;CACA;CAEA,YACI,IACA,eACF;EAFU,KAAA,KAAA;EAGR,IAAI,kBAAmB,cAA4C,iBAAkB,cAA4C,QAAQ;GACrI,KAAK,qBAAuB,cAA4C,iBAAiB;GACzF,KAAK,aAAe,cAA4C,SAAS;EAC7E,OAAO;GACH,KAAK,qBAAsB,iBAAoC;GAC/D,KAAK,aAAa;EACtB;CACJ;;;;;;CAOA,IAAY,QAAyB;EACjC,OAAO,QAAS,KAAK,mBAA0D,OAAO;CAC1F;CAEA,IAAY,QAAgB;EACxB,OAAQ,KAAK,mBAAwD;CACzE;;CAGA,YAAoB;EAChB,MAAM,YAAmC;GACrC,IAAI,KAAK,mBAAmB;GAC5B,KAAK,KAAK,mBAAmB;GAC7B,WAAW,KAAK,mBAAmB;GACnC,WAAW,KAAK,mBAAmB;GACnC,WAAW,KAAK,mBAAmB;GACnC,WAAW,KAAK,mBAAmB;GACnC,WAAW,KAAK,mBAAmB;EACvC;EACA,KAAK,MAAM,YAAY;GAAC;GAAa;GAAa;GAAW;GAAoB;EAAK,GAClF,IAAI,KAAK,IAAI,QAAQ,GAAG,UAAU,YAAY,KAAK,IAAI,QAAQ;EAEnE,OAAO;CACX;CAEA,MAAM,YACF,KACA,WACA,WACA,WACA,WACA,SACa;EAWb,MAAM,SAAkC;GACpC;GACA;GACA;GACA,WAXkB,aAAa;GAY/B,WAXkB,aAAa;EAYnC;EACA,IAAI,WAAW,KAAK,IAAI,WAAW,GAAG,OAAO,YAAY,QAAQ;EACjE,IAAI,WAAW,KAAK,IAAI,kBAAkB,GAAG,OAAO,mBAAmB,QAAQ;EAK/E,IAAI,SAAS,OAAO,KAAK,IAAI,KAAK,GAAG,OAAO,MAAM,QAAQ;EAE1D,MAAM,KAAK,GAAG,OAAO,KAAK,kBAAkB,CAAC,CAAC,OAAO,MAAM;CAC/D;CAEA,MAAM,WAAW,WAAqD;EAClE,MAAM,CAAC,SAAS,MAAM,KAAK,GACtB,OAAO,KAAK,UAAU,CAAC,CAAC,CACxB,KAAK,KAAK,kBAAkB,CAAC,CAC7B,MAAM,GAAG,KAAK,mBAAmB,WAAW,SAAS,CAAC;EAE3D,OAAQ,SAAyC;CACrD;;;;;;;;;CAUA,MAAM,YAAY,WAAkC;EAChD,IAAI,CAAC,KAAK,IAAI,WAAW,GAAG;GACxB,MAAM,KAAK,aAAa,SAAS;GACjC;EACJ;EACA,MAAM,KAAK,GACN,OAAO,KAAK,kBAAkB,CAAC,CAC/B,IAAI,EAAE,2BAAW,IAAI,KAAK,EAAE,CAAC,CAAC,CAC9B,MAAM,GAAG,KAAK,mBAAmB,WAAW,SAAS,CAAC;CAC/D;;CAGA,MAAM,cAAc,WAAkC;EAClD,IAAI,CAAC,KAAK,IAAI,WAAW,GAAG;EAC5B,IAAI,KAAK,IAAI,SAAS,GAAG;GACrB,MAAM,KAAK,GACN,OAAO,KAAK,kBAAkB,CAAC,CAC/B,IAAI;IAAE,SAAS;IAAM,GAAI,KAAK,IAAI,WAAW,IAAI,EAAE,2BAAW,IAAI,KAAK,EAAE,IAAI,CAAC;GAAG,CAAC,CAAC,CACnF,MAAM,GAAG,KAAK,IAAI,WAAW,GAAG,SAAS,CAAC;GAC/C;EACJ;EACA,MAAM,KAAK,GAAG,OAAO,KAAK,kBAAkB,CAAC,CAAC,MAAM,GAAG,KAAK,IAAI,WAAW,GAAG,SAAS,CAAC;CAC5F;;;;;;CAOA,MAAM,MAAM,KAAa,WAAmB,kBAAuC;EAC/E,MAAM,SAAS,KAAK,mBAAmB;EACvC,MAAM,aAAa,KAAK,mBAAmB;EAC3C,IAAI,CAAC,KAAK,IAAI,WAAW,KAAK,CAAC,KAAK,IAAI,WAAW,GAAG;GAClD,MAAM,KAAK,GAAG,OAAO,KAAK,kBAAkB,CAAC,CACxC,MAAM,GAAG,GAAG,OAAO,KAAK,IAAI,OAAO,WAAW,SAAS;GAC5D;EACJ;EACA,MAAM,aAAa,KAAK,IAAI,WAAW;EACvC,MAAM,aAAa,KAAK,IAAI,WAAW;EACvC,MAAM,KAAK,GAAG,OAAO,KAAK,kBAAkB,CAAC,CAAC,MAAM,GAAG;cACjD,OAAO,KAAK,IAAI;;kBAEZ,WAAW;;sBAEP,WAAW,KAAK,UAAU;0BACtB,WAAW;0BACX,WAAW,KAAK,iBAAiB;;;SAGlD;CACL;CAEA,MAAM,oBAAoB,KAAmC;EACzD,IAAI,CAAC,KAAK,cAAc,CAAE,KAAK,WAAkD,kBAAkB,OAAO;EAC1G,MAAM,CAAC,OAAO,MAAM,KAAK,GACpB,OAAO,EAAE,kBAAmB,KAAK,WAAgD,iBAAiB,CAAC,CAAC,CACpG,KAAK,KAAK,UAAU,CAAC,CACrB,MAAM,GAAG,KAAK,WAAW,IAAI,GAAG,CAAC;EACtC,MAAM,QAAS,KAAiE;EAChF,OAAO,QAAQ,IAAI,KAAK,KAAK,IAAI;CACrC;CAEA,MAAM,oBAAoB,KAAa,IAAyB;EAC5D,IAAI,CAAC,KAAK,cAAc,CAAE,KAAK,WAAkD,kBAAkB;EACnG,MAAM,KAAK,GACN,OAAO,KAAK,UAAU,CAAC,CACvB,IAAI,EAAE,kBAAkB,GAAG,CAAC,CAAC,CAC7B,MAAM,GAAG,KAAK,WAAW,IAAI,GAAG,CAAC;CAC1C;CAEA,MAAM,aAAa,WAAkC;EACjD,MAAM,KAAK,GAAG,OAAO,KAAK,kBAAkB,CAAC,CAAC,MAAM,GAAG,KAAK,mBAAmB,WAAW,SAAS,CAAC;CACxG;CAEA,MAAM,iBAAiB,KAA4B;EAC/C,MAAM,KAAK,GAAG,OAAO,KAAK,kBAAkB,CAAC,CAAC,MAAM,GAAG,KAAK,mBAAmB,KAAK,GAAG,CAAC;CAC5F;CAEA,MAAM,YAAY,KAA0C;EAOxD,OAAO,MANc,KAAK,GACrB,OAAO,KAAK,UAAU,CAAC,CAAC,CACxB,KAAK,KAAK,kBAAkB,CAAC,CAC7B,MAAM,GAAG,KAAK,mBAAmB,KAAK,GAAG,CAAC,CAAC,CAC3C,QAAQ,KAAK,mBAAmB,SAAS;CAGlD;CAEA,MAAM,WAAW,IAAY,KAA4B;EACrD,MAAM,KAAK,GAAG,OAAO,KAAK,kBAAkB,CAAC,CACxC,MAAM,GAAG,GAAG,KAAK,mBAAmB,GAAG,KAAK,GAAG,OAAO,KAAK,mBAAmB,IAAI,KAAK,KAAK;CACrG;AACJ;;;;AAKA,IAAa,4BAAb,MAAuC;CAIvB;CAHZ;CAEA,YACI,IACA,eACF;EAFU,KAAA,KAAA;EAGR,IAAI,kBAAmB,cAA4C,uBAAwB,cAA4C,QACnI,KAAK,2BAA6B,cAA4C,uBAAuB;OAErG,KAAK,2BAA4B,iBAAoC;CAE7E;CAEA,2CAA2D;EACvD,MAAM,OAAO,aAAa,KAAK,wBAAwB;EAEvD,OAAO,IADQ,eAAe,KAAK,wBAAwB,CAAC,CAAC,UAAU,SACrD,KAAK,KAAK;CAChC;;;;CAKA,MAAM,YAAY,KAAa,WAAmB,WAAgC;EAE9E,MAAM,YAAY,KAAK,yCAAyC;EAChE,MAAM,KAAK,GAAG,QAAQ,GAAG;0BACP,IAAI,IAAI,SAAS,EAAE;0BACnB,IAAI;SACrB;EAED,MAAM,KAAK,GAAG,OAAO,KAAK,wBAAwB,CAAC,CAAC,OAAO;GACvD;GACA;GACA;EACJ,CAAC;CACL;;;;CAKA,MAAM,gBAAgB,WAAqE;EACvF,MAAM,CAAC,SAAS,MAAM,KAAK,GACtB,OAAO;GACJ,KAAK,KAAK,yBAAyB;GACnC,WAAW,KAAK,yBAAyB;EAC7C,CAAC,CAAC,CACD,KAAK,KAAK,wBAAwB,CAAC,CACnC,MAAM,GAAG,KAAK,yBAAyB,WAAW,SAAS,CAAC;EAEjE,IAAI,CAAC,OAAO,OAAO;EAGnB,MAAM,YAAY,KAAK,yCAAyC;EAChE,MAAM,SAAS,MAAM,KAAK,GAAG,QAAQ,GAAG;;mBAE7B,IAAI,IAAI,SAAS,EAAE;iCACL,UAAU;;;SAGlC;EAED,IAAI,OAAO,KAAK,WAAW,GAAG,OAAO;EAErC,MAAM,MAAM,OAAO,KAAK;EACxB,OAAO;GACH,KAAK,IAAI;GACT,WAAW,IAAI,KAAK,IAAI,UAAU;EACtC;CACJ;;;;CAKA,MAAM,WAAW,WAAkC;EAC/C,MAAM,KAAK,GACN,OAAO,KAAK,wBAAwB,CAAC,CACrC,IAAI,EAAE,wBAAQ,IAAI,KAAK,EAAE,CAAC,CAAC,CAC3B,MAAM,GAAG,KAAK,yBAAyB,WAAW,SAAS,CAAC;CACrE;;;;CAKA,MAAM,iBAAiB,KAA4B;EAC/C,MAAM,KAAK,GAAG,OAAO,KAAK,wBAAwB,CAAC,CAAC,MAAM,GAAG,KAAK,yBAAyB,KAAK,GAAG,CAAC;CACxG;;;;CAKA,MAAM,gBAA+B;EACjC,MAAM,YAAY,KAAK,yCAAyC;EAChE,MAAM,KAAK,GAAG,QAAQ,GAAG;0BACP,IAAI,IAAI,SAAS,EAAE;;SAEpC;CACL;AACJ;;;;;AAMA,IAAa,wBAAb,MAAmC;CAInB;CAHZ;CAEA,YACI,IACA,eACF;EAFU,KAAA,KAAA;EAGR,KAAK,uBAAwB;CACjC;CAEA,wBAAwC;EACpC,MAAM,OAAO,aAAa,KAAK,oBAAoB;EAEnD,OAAO,IADQ,eAAe,KAAK,oBAAoB,CAAC,CAAC,UAAU,SACjD,KAAK,KAAK;CAChC;CAEA,MAAM,YAAY,KAAa,WAAmB,WAAgC;EAE9E,MAAM,YAAY,KAAK,sBAAsB;EAC7C,MAAM,KAAK,GAAG,QAAQ,GAAG;0BACP,IAAI,IAAI,SAAS,EAAE;0BACnB,IAAI;SACrB;EAED,MAAM,KAAK,GAAG,OAAO,KAAK,oBAAoB,CAAC,CAAC,OAAO;GACnD;GACA;GACA;EACJ,CAAC;CACL;CAEA,MAAM,gBAAgB,WAAuD;EACzE,MAAM,YAAY,KAAK,sBAAsB;EAC7C,MAAM,SAAS,MAAM,KAAK,GAAG,QAAQ,GAAG;;mBAE7B,IAAI,IAAI,SAAS,EAAE;iCACL,UAAU;;;SAGlC;EAED,IAAI,OAAO,KAAK,WAAW,GAAG,OAAO;EAErC,MAAM,MAAM,OAAO,KAAK;EACxB,OAAO;GACH,KAAK,IAAI;GACT,WAAW,IAAI,KAAK,IAAI,UAAU;EACtC;CACJ;CAEA,MAAM,WAAW,WAAkC;EAC/C,MAAM,KAAK,GACN,OAAO,KAAK,oBAAoB,CAAC,CACjC,IAAI,EAAE,wBAAQ,IAAI,KAAK,EAAE,CAAC,CAAC,CAC3B,MAAM,GAAG,KAAK,qBAAqB,WAAW,SAAS,CAAC;CACjE;AACJ;;;;;AAMA,IAAa,0BAAb,MAAgE;CAMhD;CALZ;CACA;CACA;CAEA,YACI,IACA,eACF;EAFU,KAAA,KAAA;EAGR,KAAK,sBAAsB,IAAI,oBAAoB,IAAI,aAAa;EACpE,KAAK,4BAA4B,IAAI,0BAA0B,IAAI,aAAa;EAChF,KAAK,wBAAwB,IAAI,sBAAsB,IAAI,aAAa;CAC5E;CAIA,MAAM,mBAAmB,KAAa,WAAmB,WAAiB,WAAoB,WAAoB,SAA8C;EAC5J,MAAM,KAAK,oBAAoB,YAAY,KAAK,WAAW,WAAW,WAAW,WAAW,OAAO;CACvG;CAEA,MAAM,wBAAwB,WAAkC;EAC5D,MAAM,KAAK,oBAAoB,YAAY,SAAS;CACxD;CAEA,MAAM,0BAA0B,WAAkC;EAC9D,MAAM,KAAK,oBAAoB,cAAc,SAAS;CAC1D;CAEA,MAAM,mBAAmB,KAAa,WAAmB,kBAAuC;EAC5F,MAAM,KAAK,oBAAoB,MAAM,KAAK,WAAW,gBAAgB;CACzE;CAEA,MAAM,oBAAoB,KAAmC;EACzD,OAAO,KAAK,oBAAoB,oBAAoB,GAAG;CAC3D;CAEA,MAAM,oBAAoB,KAAa,IAAyB;EAC5D,MAAM,KAAK,oBAAoB,oBAAoB,KAAK,EAAE;CAC9D;CAEA,MAAM,uBAAuB,WAAqD;EAC9E,OAAO,KAAK,oBAAoB,WAAW,SAAS;CACxD;CAEA,MAAM,mBAAmB,WAAkC;EACvD,MAAM,KAAK,oBAAoB,aAAa,SAAS;CACzD;CAEA,MAAM,8BAA8B,KAA4B;EAC5D,MAAM,KAAK,oBAAoB,iBAAiB,GAAG;CACvD;CAEA,MAAM,yBAAyB,KAA0C;EACrE,OAAO,KAAK,oBAAoB,YAAY,GAAG;CACnD;CAEA,MAAM,uBAAuB,IAAY,KAA4B;EACjE,MAAM,KAAK,oBAAoB,WAAW,IAAI,GAAG;CACrD;CAIA,MAAM,yBAAyB,KAAa,WAAmB,WAAgC;EAC3F,MAAM,KAAK,0BAA0B,YAAY,KAAK,WAAW,SAAS;CAC9E;CAEA,MAAM,4BAA4B,WAA2D;EACzF,OAAO,KAAK,0BAA0B,gBAAgB,SAAS;CACnE;CAEA,MAAM,2BAA2B,WAAkC;EAC/D,MAAM,KAAK,0BAA0B,WAAW,SAAS;CAC7D;CAEA,MAAM,oCAAoC,KAA4B;EAClE,MAAM,KAAK,0BAA0B,iBAAiB,GAAG;CAC7D;CAEA,MAAM,sBAAqC;EACvC,MAAM,KAAK,0BAA0B,cAAc;CACvD;CAIA,MAAM,qBAAqB,KAAa,WAAmB,WAAgC;EACvF,MAAM,KAAK,sBAAsB,YAAY,KAAK,WAAW,SAAS;CAC1E;CAEA,MAAM,wBAAwB,WAAuD;EACjF,OAAO,KAAK,sBAAsB,gBAAgB,SAAS;CAC/D;CAEA,MAAM,uBAAuB,WAAkC;EAC3D,MAAM,KAAK,sBAAsB,WAAW,SAAS;CACzD;AACJ;;;;;;AAOA,IAAa,yBAAb,MAA8D;CAK9C;CAJZ;CACA;CAEA,YACI,IACA,eACF;EAFU,KAAA,KAAA;EAGR,KAAK,cAAc,IAAI,YAAY,IAAI,aAAa;EACpD,KAAK,kBAAkB,IAAI,wBAAwB,IAAI,aAAa;CACxE;CAIA,MAAM,WAAW,MAAyC;EACtD,OAAO,KAAK,YAAY,WAAW,IAAI;CAC3C;CAEA,MAAM,YAAY,IAAsC;EACpD,OAAO,KAAK,YAAY,YAAY,EAAE;CAC1C;CAEA,MAAM,eAAe,OAAyC;EAC1D,OAAO,KAAK,YAAY,eAAe,KAAK;CAChD;CAEA,MAAM,kBAAkB,UAAkB,YAA8C;EACpF,OAAO,KAAK,YAAY,kBAAkB,UAAU,UAAU;CAClE;CAEA,MAAM,kBAAkB,KAA0C;EAC9D,OAAO,KAAK,YAAY,kBAAkB,GAAG;CACjD;CAEA,MAAM,iBAAiB,KAAa,UAAkB,YAAoB,aAAsD;EAC5H,OAAO,KAAK,YAAY,iBAAiB,KAAK,UAAU,YAAY,WAAW;CACnF;CAEA,MAAM,WAAW,IAAY,MAAqE;EAC9F,OAAO,KAAK,YAAY,WAAW,IAAI,IAAI;CAC/C;CAEA,MAAM,WAAW,IAA2B;EACxC,MAAM,KAAK,YAAY,WAAW,EAAE;CACxC;CAEA,MAAM,YAAiC;EACnC,OAAO,KAAK,YAAY,UAAU;CACtC;CAEA,MAAM,mBAAmB,SAA2D;EAChF,OAAO,KAAK,YAAY,mBAAmB,OAAO;CACtD;CAEA,MAAM,eAAe,IAAY,cAAqC;EAClE,MAAM,KAAK,YAAY,eAAe,IAAI,YAAY;CAC1D;CAEA,MAAM,iBAAiB,IAAY,UAAkC;EACjE,MAAM,KAAK,YAAY,iBAAiB,IAAI,QAAQ;CACxD;CAEA,MAAM,qBAAqB,IAAY,OAAqC;EACxE,MAAM,KAAK,YAAY,qBAAqB,IAAI,KAAK;CACzD;CAEA,MAAM,2BAA2B,OAAyC;EACtE,OAAO,KAAK,YAAY,2BAA2B,KAAK;CAC5D;CAEA,MAAM,aAAa,KAAkC;EACjD,OAAO,KAAK,YAAY,aAAa,GAAG;CAC5C;CAEA,MAAM,eAAe,KAAgC;EACjD,OAAO,KAAK,YAAY,eAAe,GAAG;CAC9C;CAEA,MAAM,aAAa,KAAa,SAAkC;EAC9D,MAAM,KAAK,YAAY,aAAa,KAAK,OAAO;CACpD;CAEA,MAAM,kBAAkB,KAAa,QAA+B;EAChE,MAAM,KAAK,YAAY,kBAAkB,KAAK,MAAM;CACxD;CAEA,MAAM,iBAAiB,KAAoE;EACvF,OAAO,KAAK,YAAY,iBAAiB,GAAG;CAChD;CAIA,MAAM,YAAY,IAAsC;EACpD,OAAO;GACH;GACA,MAAM;GACN,SAAS,OAAO;GAChB,oBAAoB;GACpB,uBAAuB;EAC3B;CACJ;CAEA,MAAM,YAAiC;EACnC,OAAO;GACH;IAAE,IAAI;IAClB,MAAM;IACN,SAAS;IACT,oBAAoB;IACpB,uBAAuB;GAAK;GAChB;IAAE,IAAI;IAClB,MAAM;IACN,SAAS;IACT,oBAAoB;IACpB,uBAAuB;GAAK;GAChB;IAAE,IAAI;IAClB,MAAM;IACN,SAAS;IACT,oBAAoB;IACpB,uBAAuB;GAAK;EACpB;CACJ;CAEA,MAAM,WAAW,OAA0C;EACvD,OAAO;GACH,IAAI,MAAM;GACV,MAAM,MAAM;GACZ,SAAS,MAAM,WAAW;GAC1B,oBAAoB,MAAM,sBAAsB;GAChD,uBAAuB,MAAM,yBAAyB;EAC1D;CACJ;CAEA,MAAM,WAAW,IAAY,MAA+D;EACxF,OAAO;GACH;GACA,MAAM,KAAK,QAAQ;GACnB,SAAS,KAAK,WAAY,OAAO;GACjC,oBAAoB,KAAK,sBAAsB;GAC/C,uBAAuB,KAAK,yBAAyB;EACzD;CACJ;CAEA,MAAM,WAAW,KAA4B,CAE7C;CAIA,MAAM,mBAAmB,KAAa,WAAmB,WAAiB,WAAoB,WAAoB,SAA8C;EAC5J,MAAM,KAAK,gBAAgB,mBAAmB,KAAK,WAAW,WAAW,WAAW,WAAW,OAAO;CAC1G;CAEA,MAAM,wBAAwB,WAAkC;EAC5D,MAAM,KAAK,gBAAgB,wBAAwB,SAAS;CAChE;CAEA,MAAM,0BAA0B,WAAkC;EAC9D,MAAM,KAAK,gBAAgB,0BAA0B,SAAS;CAClE;CAEA,MAAM,mBAAmB,KAAa,WAAmB,kBAAuC;EAC5F,MAAM,KAAK,gBAAgB,mBAAmB,KAAK,WAAW,gBAAgB;CAClF;CAEA,MAAM,oBAAoB,KAAmC;EACzD,OAAO,KAAK,gBAAgB,oBAAoB,GAAG;CACvD;CAEA,MAAM,oBAAoB,KAAa,IAAyB;EAC5D,MAAM,KAAK,gBAAgB,oBAAoB,KAAK,EAAE;CAC1D;CAEA,MAAM,uBAAuB,WAAqD;EAC9E,OAAO,KAAK,gBAAgB,uBAAuB,SAAS;CAChE;CAEA,MAAM,mBAAmB,WAAkC;EACvD,MAAM,KAAK,gBAAgB,mBAAmB,SAAS;CAC3D;CAEA,MAAM,8BAA8B,KAA4B;EAC5D,MAAM,KAAK,gBAAgB,8BAA8B,GAAG;CAChE;CAEA,MAAM,yBAAyB,KAA0C;EACrE,OAAO,KAAK,gBAAgB,yBAAyB,GAAG;CAC5D;CAEA,MAAM,uBAAuB,IAAY,KAA4B;EACjE,MAAM,KAAK,gBAAgB,uBAAuB,IAAI,GAAG;CAC7D;CAEA,MAAM,yBAAyB,KAAa,WAAmB,WAAgC;EAC3F,MAAM,KAAK,gBAAgB,yBAAyB,KAAK,WAAW,SAAS;CACjF;CAEA,MAAM,4BAA4B,WAA2D;EACzF,OAAO,KAAK,gBAAgB,4BAA4B,SAAS;CACrE;CAEA,MAAM,2BAA2B,WAAkC;EAC/D,MAAM,KAAK,gBAAgB,2BAA2B,SAAS;CACnE;CAEA,MAAM,oCAAoC,KAA4B;EAClE,MAAM,KAAK,gBAAgB,oCAAoC,GAAG;CACtE;CAEA,MAAM,sBAAqC;EACvC,MAAM,KAAK,gBAAgB,oBAAoB;CACnD;CAIA,MAAM,qBAAqB,KAAa,WAAmB,WAAgC;EACvF,MAAM,KAAK,gBAAgB,qBAAqB,KAAK,WAAW,SAAS;CAC7E;CAEA,MAAM,wBAAwB,WAAuD;EACjF,OAAO,KAAK,gBAAgB,wBAAwB,SAAS;CACjE;CAEA,MAAM,uBAAuB,WAAkC;EAC3D,MAAM,KAAK,gBAAgB,uBAAuB,SAAS;CAC/D;CAIA,cAAyC;CACzC,gBAAoC;EAChC,IAAI,CAAC,KAAK,aACN,KAAK,cAAc,IAAI,WAAW,KAAK,EAAE;EAE7C,OAAO,KAAK;CAChB;CAEA,MAAM,gBAAgB,KAAa,YAAoB,iBAAyB,cAA2C;EACvH,OAAO,KAAK,cAAc,CAAC,CAAC,gBAAgB,KAAK,YAAY,iBAAiB,YAAY;CAC9F;CAEA,MAAM,cAAc,KAAmC;EACnD,OAAO,KAAK,cAAc,CAAC,CAAC,cAAc,GAAG;CACjD;CAEA,MAAM,iBAAiB,UAA6E;EAChG,OAAO,KAAK,cAAc,CAAC,CAAC,iBAAiB,QAAQ;CACzD;CAEA,MAAM,gBAAgB,UAAiC;EACnD,OAAO,KAAK,cAAc,CAAC,CAAC,gBAAgB,QAAQ;CACxD;CAEA,MAAM,sBAAsB,UAAkB,iBAAwC;EAClF,OAAO,KAAK,cAAc,CAAC,CAAC,sBAAsB,UAAU,eAAe;CAC/E;CAEA,MAAM,gBAAgB,UAAkB,KAA4B;EAChE,OAAO,KAAK,cAAc,CAAC,CAAC,gBAAgB,UAAU,GAAG;CAC7D;CAEA,MAAM,mBAAmB,UAAkB,WAA+C;EACtF,OAAO,KAAK,cAAc,CAAC,CAAC,mBAAmB,UAAU,SAAS;CACtE;CAEA,MAAM,oBAAoB,aAAuD;EAC7E,OAAO,KAAK,cAAc,CAAC,CAAC,oBAAoB,WAAW;CAC/D;CAEA,MAAM,mBAAmB,aAAoC;EACzD,OAAO,KAAK,cAAc,CAAC,CAAC,mBAAmB,WAAW;CAC9D;CAEA,MAAM,oBAAoB,KAAa,YAAqC;EACxE,OAAO,KAAK,cAAc,CAAC,CAAC,oBAAoB,KAAK,UAAU;CACnE;CAEA,MAAM,gBAAgB,KAAa,UAAoC;EACnE,OAAO,KAAK,cAAc,CAAC,CAAC,gBAAgB,KAAK,QAAQ;CAC7D;CAEA,MAAM,2BAA2B,KAA8B;EAC3D,OAAO,KAAK,cAAc,CAAC,CAAC,2BAA2B,GAAG;CAC9D;CAEA,MAAM,uBAAuB,KAA4B;EACrD,OAAO,KAAK,cAAc,CAAC,CAAC,uBAAuB,GAAG;CAC1D;CAEA,MAAM,sBAAsB,KAA+B;EACvD,OAAO,KAAK,cAAc,CAAC,CAAC,sBAAsB,GAAG;CACzD;CAEA,MAAM,sBAAsB,UAAkB,SAAmC;EAC7E,OAAO,KAAK,cAAc,CAAC,CAAC,sBAAsB,UAAU,OAAO;CACvE;CAEA,MAAM,0BAA0B,aAAsC;EAClE,OAAO,KAAK,cAAc,CAAC,CAAC,0BAA0B,WAAW;CACrE;AACJ;;;;;AAUA,IAAa,aAAb,MAAiD;CACzB;CAA4B;CAAhD,YAAY,IAA4B,aAAqB,UAAU;EAAnD,KAAA,KAAA;EAA4B,KAAA,aAAA;CAAwB;CAExE,QAAgB,WAA2B;EACvC,OAAO,IAAI,KAAK,WAAW,KAAK,UAAU;CAC9C;CAEA,MAAM,gBACF,KACA,YACA,iBACA,cACkB;EAClB,MAAM,YAAY,KAAK,QAAQ,aAAa;EAO5C,MAAM,OAAM,MANS,KAAK,GAAG,QAAQ,GAAG;0BACtB,IAAI,IAAI,SAAS,EAAE;sBACvB,IAAI,IAAI,WAAW,IAAI,gBAAgB,IAAI,gBAAgB,KAAK;;SAE7E,EAAA,CAEkB,KAAK;EACxB,OAAO;GACH,IAAI,IAAI;GACR,KAAK,IAAI;GACT,YAAY,IAAI;GAChB,cAAe,IAAI,iBAAmC,KAAA;GACtD,UAAU,IAAI;GACd,WAAW,IAAI,KAAK,IAAI,UAAoB;GAC5C,WAAW,IAAI,KAAK,IAAI,UAAoB;EAChD;CACJ;CAEA,MAAM,cAAc,KAAmC;EACnD,MAAM,YAAY,KAAK,QAAQ,aAAa;EAQ5C,QAAQ,MAPa,KAAK,GAAG,QAAQ,GAAG;;mBAE7B,IAAI,IAAI,SAAS,EAAE;0BACZ,IAAI;;SAErB,EAAA,CAEc,KAAwC,KAAI,SAAQ;GAC/D,IAAI,IAAI;GACR,KAAK,IAAI;GACT,YAAY,IAAI;GAChB,cAAe,IAAI,iBAAmC,KAAA;GACtD,UAAU,IAAI;GACd,WAAW,IAAI,KAAK,IAAI,UAAoB;GAC5C,WAAW,IAAI,KAAK,IAAI,UAAoB;EAChD,EAAE;CACN;CAEA,MAAM,iBAAiB,UAA6E;EAChG,MAAM,YAAY,KAAK,QAAQ,aAAa;EAC5C,MAAM,SAAS,MAAM,KAAK,GAAG,QAAQ,GAAG;;mBAE7B,IAAI,IAAI,SAAS,EAAE;yBACb,SAAS;SACzB;EAED,IAAI,OAAO,KAAK,WAAW,GAAG,OAAO;EAErC,MAAM,MAAM,OAAO,KAAK;EACxB,OAAO;GACH,IAAI,IAAI;GACR,KAAK,IAAI;GACT,YAAY,IAAI;GAChB,iBAAiB,IAAI;GACrB,cAAe,IAAI,iBAAmC,KAAA;GACtD,UAAU,IAAI;GAEd,iBAAiB,IAAI,sBAAsB,QAAQ,IAAI,sBAAsB,KAAA,IACvE,OACA,OAAO,IAAI,iBAAiB;GAClC,WAAW,IAAI,KAAK,IAAI,UAAoB;GAC5C,WAAW,IAAI,KAAK,IAAI,UAAoB;EAChD;CACJ;;;;;;;;;CAUA,MAAM,sBAAsB,UAAkB,SAAmC;EAC7E,MAAM,YAAY,KAAK,QAAQ,aAAa;EAS5C,QAAO,MARc,KAAK,GAAG,QAAQ,GAAG;qBAC3B,IAAI,IAAI,SAAS,EAAE;sCACF,QAAQ;yBACrB,SAAS;sEACoC,QAAQ;;SAErE,EAAA,CAEa,KAAK,SAAS;CAChC;CAEA,MAAM,gBAAgB,UAAiC;EACnD,MAAM,YAAY,KAAK,QAAQ,aAAa;EAC5C,MAAM,KAAK,GAAG,QAAQ,GAAG;qBACZ,IAAI,IAAI,SAAS,EAAE;;yBAEf,SAAS;SACzB;CACL;CAEA,MAAM,sBAAsB,UAAkB,iBAAwC;EAClF,MAAM,YAAY,KAAK,QAAQ,aAAa;EAC5C,MAAM,KAAK,GAAG,QAAQ,GAAG;qBACZ,IAAI,IAAI,SAAS,EAAE;qCACH,gBAAgB;yBAC5B,SAAS;SACzB;CACL;CAEA,MAAM,gBAAgB,UAAkB,KAA4B;EAChE,MAAM,YAAY,KAAK,QAAQ,aAAa;EAC5C,MAAM,KAAK,GAAG,QAAQ,GAAG;0BACP,IAAI,IAAI,SAAS,EAAE;yBACpB,SAAS,aAAa,IAAI;SAC1C;CACL;CAEA,MAAM,mBAAmB,UAAkB,WAA+C;EACtF,MAAM,YAAY,KAAK,QAAQ,gBAAgB;EAE/C,MAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,MAAS,GAAI;EAOrD,MAAM,OAAM,MANS,KAAK,GAAG,QAAQ,GAAG;0BACtB,IAAI,IAAI,SAAS,EAAE;sBACvB,SAAS,IAAI,aAAa,KAAK,IAAI,UAAU;;SAE1D,EAAA,CAEkB,KAAK;EACxB,OAAO;GACH,IAAI,IAAI;GACR,UAAU,IAAI;GACd,WAAW,IAAI,KAAK,IAAI,UAAoB;GAC5C,YAAY,IAAI,cAAc,IAAI,KAAK,IAAI,WAAqB,IAAI,KAAA;GACpE,WAAY,IAAI,cAAgC,KAAA;EACpD;CACJ;CAEA,MAAM,oBAAoB,aAAuD;EAC7E,MAAM,YAAY,KAAK,QAAQ,gBAAgB;EAC/C,MAAM,SAAS,MAAM,KAAK,GAAG,QAAQ,GAAG;;mBAE7B,IAAI,IAAI,SAAS,EAAE;yBACb,YAAY;SAC5B;EAED,IAAI,OAAO,KAAK,WAAW,GAAG,OAAO;EAErC,MAAM,MAAM,OAAO,KAAK;EACxB,OAAO;GACH,IAAI,IAAI;GACR,UAAU,IAAI;GACd,WAAW,IAAI,KAAK,IAAI,UAAoB;GAC5C,YAAY,IAAI,cAAc,IAAI,KAAK,IAAI,WAAqB,IAAI,KAAA;GACpE,WAAY,IAAI,cAAgC,KAAA;GAChD,UAAU,OAAO,IAAI,YAAY,CAAC;EACtC;CACJ;;;;;;;;CASA,MAAM,0BAA0B,aAAsC;EAClE,MAAM,YAAY,KAAK,QAAQ,gBAAgB;EAC/C,MAAM,SAAS,MAAM,KAAK,GAAG,QAAQ,GAAG;qBAC3B,IAAI,IAAI,SAAS,EAAE;;yBAEf,YAAY;;SAE5B;EAED,IAAI,OAAO,KAAK,WAAW,GAAG,OAAO;EACrC,OAAO,OAAQ,OAAO,KAAK,EAAE,CAAmC,QAAQ;CAC5E;CAEA,MAAM,mBAAmB,aAAoC;EACzD,MAAM,YAAY,KAAK,QAAQ,gBAAgB;EAC/C,MAAM,KAAK,GAAG,QAAQ,GAAG;qBACZ,IAAI,IAAI,SAAS,EAAE;;yBAEf,YAAY;SAC5B;CACL;CAEA,MAAM,oBAAoB,KAAa,YAAqC;EACxE,MAAM,YAAY,KAAK,QAAQ,gBAAgB;EAE/C,MAAM,KAAK,GAAG,QAAQ,GAAG;0BACP,IAAI,IAAI,SAAS,EAAE,eAAe,IAAI;SACvD;EAGD,KAAK,MAAM,QAAQ,YACf,MAAM,KAAK,GAAG,QAAQ,GAAG;8BACP,IAAI,IAAI,SAAS,EAAE;0BACvB,IAAI,IAAI,KAAK;aAC1B;CAET;CAEA,MAAM,gBAAgB,KAAa,UAAoC;EACnE,MAAM,YAAY,KAAK,QAAQ,gBAAgB;EAQ/C,QAAO,MAPc,KAAK,GAAG,QAAQ,GAAG;qBAC3B,IAAI,IAAI,SAAS,EAAE;;0BAEd,IAAI,mBAAmB,SAAS;;SAEjD,EAAA,CAEa,KAAK,SAAS;CAChC;CAEA,MAAM,2BAA2B,KAA8B;EAC3D,MAAM,YAAY,KAAK,QAAQ,gBAAgB;EAM/C,QAAQ,MALa,KAAK,GAAG,QAAQ,GAAG;iDACC,IAAI,IAAI,SAAS,EAAE;0BAC1C,IAAI;SACrB,EAAA,CAEc,KAAK,EAAE,CAAuB;CACjD;CAEA,MAAM,uBAAuB,KAA4B;EACrD,MAAM,YAAY,KAAK,QAAQ,gBAAgB;EAC/C,MAAM,KAAK,GAAG,QAAQ,GAAG;0BACP,IAAI,IAAI,SAAS,EAAE,eAAe,IAAI;SACvD;CACL;CAEA,MAAM,sBAAsB,KAA+B;EACvD,MAAM,YAAY,KAAK,QAAQ,aAAa;EAM5C,QAAQ,MALa,KAAK,GAAG,QAAQ,GAAG;iDACC,IAAI,IAAI,SAAS,EAAE;0BAC1C,IAAI;SACrB,EAAA,CAEc,KAAK,EAAE,CAAuB,QAAQ;CACzD;AACJ;;;AC/iDA,IAAM,oBAA4C;CAC9C,YAAY;CACZ,SAAS;AACb;;;;;AAMA,IAAa,iBAAb,MAA4B;CAIZ;CAHZ;CAEA,YACI,IACA,WACF;EAFU,KAAA,KAAA;EAGR,KAAK,YAAY;GAAE,GAAG;GAC9B,GAAG;EAAU;CACT;;;;;;;;CASA,MAAM,cAAc,QAA4C;EAC5D,MAAM,EACF,WACA,IACA,QACA,QACA,gBACA,cACA;EAEJ,MAAM,gBAAgB,kBAAkB,SAClC,kBAAkB,gBAAgB,MAAM,IACxC;EAKN,IAAI,WAAW,aAAa,CAAC,iBAAiB,cAAc,WAAW,IACnE;EAGJ,IAAI;GACA,MAAM,KAAK,GAAG,QAAQ,GAAG;;;;sBAIf,UAAU;sBACV,OAAO,EAAE,EAAE;sBACX,OAAO;sBACP,gBAAgB,GAAG,SAAS,IAAI,KAAK,cAAc,KAAI,MAAK,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI,EAAE,aAAa,GAAG,OAAO;sBACxG,SAAS,GAAG,GAAG,KAAK,UAAU,MAAM,EAAE,WAAW,GAAG,OAAO;sBAC3D,iBAAiB,GAAG,GAAG,KAAK,UAAU,cAAc,EAAE,WAAW,GAAG,OAAO;sBAC3E,aAAa,KAAK;;aAE3B;GAGD,KAAK,YAAY,WAAW,EAAE,CAAC,CAAC,OAAM,QAClC,OAAO,MAAM,wBAAwB,EAAE,OAAO,IAAI,CAAC,CACvD;EACJ,SAAS,OAAO;GACZ,OAAO,MAAM,gCAAgC,EAAS,MAAM,CAAC;EACjE;CACJ;;;;CAKA,MAAM,aACF,WACA,IACA,UAA+B,CAAC,GACgB;EAChD,MAAM,QAAQ,QAAQ,SAAS;EAC/B,MAAM,SAAS,QAAQ,UAAU;EAEjC,MAAM,CAAC,aAAa,cAAc,MAAM,QAAQ,IAAI,CAChD,KAAK,GAAG,QAAQ,GAAG;;;qCAGM,UAAU;oCACX,OAAO,EAAE,EAAE;aAClC,GACD,KAAK,GAAG,QAAQ,GAAG;;;;qCAIM,UAAU;oCACX,OAAO,EAAE,EAAE;;wBAEvB,MAAM;yBACL,OAAO;aACnB,CACL,CAAC;EAED,MAAM,QAAQ,SACT,YAAY,KAAK,EAAE,EAA6B,SAAS,KAC1D,EACJ;EAEA,OAAO;GACH,MAAM,WAAW;GACjB;EACJ;CACJ;;;;CAKA,MAAM,kBAAkB,WAAiD;EACrE,MAAM,SAAS,MAAM,KAAK,GAAG,QAAQ,GAAG;;;;yBAIvB,UAAU;SAC1B;EAED,IAAI,OAAO,KAAK,WAAW,GAAG,OAAO;EACrC,OAAO,OAAO,KAAK;CACvB;;;;CAOA,MAAM,YAAY,WAAmB,IAA6B;EAC9D,IAAI,UAAU;EAGd,MAAM,YAAY,MAAM,KAAK,GAAG,QAAQ,GAAG;;iCAElB,UAAU;gCACX,OAAO,EAAE,EAAE;+DACoB,KAAK,UAAU,QAAQ;SAC7E;EACD,WAAW,UAAU,YAAY;EAGjC,MAAM,YAAY,MAAM,KAAK,GAAG,QAAQ,GAAG;;;;qCAId,UAAU;oCACX,OAAO,EAAE,EAAE;;yBAEtB,KAAK,UAAU,WAAW;;SAE1C;EACD,WAAW,UAAU,YAAY;EAEjC,OAAO;CACX;;;;;CAMA,MAAM,eAAgC;EAKlC,QAAO,MAJc,KAAK,GAAG,QAAQ,GAAG;;+DAEe,KAAK,UAAU,QAAQ;SAC7E,EAAA,CACa,YAAY;CAC9B;AACJ;;;;;AAOA,SAAS,UAAU,GAAY,GAAqB;CAChD,IAAI,MAAM,GAAG,OAAO;CACpB,IAAI,KAAK,QAAQ,KAAK,MAAM,OAAO;CACnC,IAAI,aAAa,QAAQ,aAAa,MAAM,OAAO,EAAE,QAAQ,MAAM,EAAE,QAAQ;CAC7E,IAAI,MAAM,QAAQ,CAAC,KAAK,MAAM,QAAQ,CAAC,GAAG;EACtC,IAAI,EAAE,WAAW,EAAE,QAAQ,OAAO;EAClC,OAAO,EAAE,OAAO,GAAG,MAAM,UAAU,GAAG,EAAE,EAAE,CAAC;CAC/C;CACA,IAAI,OAAO,MAAM,YAAY,OAAO,MAAM,UAAU;EAChD,MAAM,OAAO;EACb,MAAM,OAAO;EACb,MAAM,QAAQ,OAAO,KAAK,IAAI;EAC9B,MAAM,QAAQ,OAAO,KAAK,IAAI;EAC9B,IAAI,MAAM,WAAW,MAAM,QAAQ,OAAO;EAC1C,OAAO,MAAM,OAAM,MAAK,UAAU,KAAK,IAAI,KAAK,EAAE,CAAC;CACvD;CACA,OAAO;AACX;;;;AAKA,SAAgB,kBACZ,WACA,WACe;CACf,MAAM,UAAoB,CAAC;CAC3B,MAAM,0BAAU,IAAI,IAAI,CACpB,GAAG,OAAO,KAAK,SAAS,GACxB,GAAG,OAAO,KAAK,SAAS,CAC5B,CAAC;CAED,KAAK,MAAM,OAAO,SAAS;EACvB,MAAM,SAAS,UAAU;EACzB,MAAM,SAAS,UAAU;EAGzB,IAAI,IAAI,WAAW,IAAI,GAAG;EAE1B,IAAI,WAAW,QAEX,IACI,OAAO,WAAW,YAAY,WAAW,QACzC,OAAO,WAAW,YAAY,WAAW;OAErC,CAAC,UAAU,QAAQ,MAAM,GACzB,QAAQ,KAAK,GAAG;EAAA,OAGpB,QAAQ,KAAK,GAAG;CAG5B;CAEA,OAAO,QAAQ,SAAS,IAAI,UAAU;AAC1C;;;;;;;;ACnPA,eAAsB,yBAAyB,IAAmC;CAC9E,OAAO,MAAM,kCAAkC;CAE/C,IAAI;EAEA,MAAM,GAAG,QAAQ,GAAG,oCAAoC;EAExD,MAAM,GAAG,QAAQ,GAAG;;;;;;;;;;;;SAYnB;EAED,MAAM,GAAG,QAAQ,GAAG;;;SAGnB;EAED,MAAM,GAAG,QAAQ,GAAG;;;SAGnB;EAMD,MAAM,GAAG,QAAQ,IAAI,IAAI,uBAAuB,UAAU,gBAAgB,CAAC,CAAC;EAE5E,OAAO,MAAM,8BAA8B;CAC/C,SAAS,OAAO;EACZ,OAAO,MAAM,wCAAwC,EAAS,MAAM,CAAC;EACrE,OAAO,KAAK,+CAA+C;CAC/D;AACJ;;;;;;;;;;;;;;;;;;ACjCA,SAAgB,uBAAuB,QAAuC;CAC1E,IAAI,eAAe;CAEnB,KAAK,MAAM,mBAAmB,OAAO,OAAO,MAAM,GAAG;EACjD,IAAI,EAAE,2BAA2B,UAAU;EAE3C,MAAM,UAAU,gBAAgB,eAAe;EAC/C,KAAK,MAAM,UAAU,OAAO,OAAO,OAAO,GACtC,IAAI,kBAAkB,SAAS;GAC3B,MAAM,WAAW,OAAO,mBAAmB,KAAK,MAAM;GACtD,OAAO,qBAAqB,SAAU,OAAgB;IAClD,IAAI,SAAS,MAAM,OAAO;IAC1B,OAAO,SAAS,KAA2B;GAC/C;GACA;EACJ;CAER;CAEA,IAAI,eAAe,GACf,OAAO,MAAM,qBAAqB,aAAa,iCAAiC;AAExF;;;;;;;;;;;;AC/BA,SAAgB,SAAS,WAA2B;CAChD,OAAO,UACF,QAAQ,MAAM,GAAG,CAAC,CAClB,QAAQ,UAAU,MAAM,EAAE,YAAY,CAAC;AAChD;;;;;;;;;;;;;ACHA,SAAgB,UAAU,UAA0B;CAChD,MAAM,KAAK,SAAS,YAAY;CAGhC,IAAI,OAAO,YAAY,OAAO;CAG9B,IAAI,OAAO,WAAW,GAAG,WAAW,GAAG,GAAG,OAAO;CAGjD,IACI,GAAG,SAAS,KAAK,KACjB,GAAG,SAAS,SAAS,KACrB,GAAG,SAAS,SAAS,KACrB,GAAG,SAAS,QAAQ,KACpB,OAAO,UACP,OAAO,YACP,OAAO,YACP,OAAO,sBACP,OAAO,SAEP,OAAO;CAIX,IAAI,GAAG,SAAS,MAAM,GAAG,OAAO;CAGhC,IAAI,GAAG,SAAS,MAAM,KAAK,GAAG,SAAS,MAAM,GAAG,OAAO;CAGvD,IAAI,OAAO,UAAU,OAAO,SAAS,OAAO;CAG5C,IAAI,OAAO,SAAS,OAAO;CAG3B,IAAI,OAAO,UAAU,OAAO,UAAU,OAAO,aAAa,OAAO,YAAY,OAAO;CAGpF,IAAI,OAAO,QAAQ,OAAO;CAG1B,OAAO;AACX;;;AC6EA,IAAM,sBAA8C;CAChD,QAAQ;CACR,UAAU;CACV,KAAK;CACL,OAAO;CACP,MAAM;CACN,OAAO;CACP,OAAO;CACP,MAAM;CACN,MAAM;CACN,OAAO;CACP,UAAU;CACV,WAAW;AACf;;;;;;;;;;;;AAaA,IAAM,uBAAmD;CACrD,QAAQ;CAAK,SAAS;CAAK,OAAO;CAAK,QAAQ;CAAK,QAAQ;CAC5D,QAAQ;CAAK,QAAQ;CAAK,SAAS;CAAK,QAAQ;CAAK,SAAS;CAC9D,SAAS;CAAK,SAAS;CAAK,SAAS;CAAK,QAAQ;CAClD,QAAQ;CAAM,OAAO;CAAM,OAAO;AACtC;;AAGA,IAAM,8BAAc,IAAI,IAAI;CACxB;CAAU;CAAU;CAAS;CAAO;CAAQ;CAC5C;CAAa;CAAY;CAAS;CAAU;CAC5C;CAAY;CAAe;CAAc;CACzC;CAAQ;CAAU;CAAW;CAAS;CACtC;CAAa;CAAe;CAAe;CAC3C;CAAY;AAChB,CAAC;AAED,SAAgB,YAAY,MAAsB;CAC9C,MAAM,QAAQ,KAAK,YAAY;CAG/B,IAAI,oBAAoB,QAAQ;EAE5B,MAAM,WAAW,oBAAoB;EACrC,OAAO,KAAK,OAAO,KAAK,EAAE,CAAC,YAAY,IACjC,SAAS,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,SAAS,MAAM,CAAC,IACnD;CACV;CAGA,IAAI,YAAY,IAAI,KAAK,GAAG,OAAO;CAGnC,IAAI,MAAM,SAAS,MAAM,KAAK,MAAM,SAAS,GAEzC,OAAO,KAAK,MAAM,GAAG,EAAE,IAAI;CAE/B,IAAI,MAAM,SAAS,KAAK,KAAK,MAAM,SAAS,GACxC,OAAO,KAAK,MAAM,GAAG,EAAE,IAAI;CAE/B,IAAI,qBAAqB,QAErB,OAAO,KAAK,MAAM,GAAG,EAAE,IAAI,qBAAqB;CAEpD,IAAI,MAAM,SAAS,MAAM,KAAK,MAAM,SAAS,MAAM,KAAK,MAAM,SAAS,MAAM,KAAK,MAAM,SAAS,KAAK,KAAK,MAAM,SAAS,KAAK,GAC3H,OAAO,KAAK,MAAM,GAAG,EAAE;CAE3B,IAAI,MAAM,SAAS,KAAK,KAAK,CAAC,MAAM,SAAS,MAAM,GAE/C,OAAO,KAAK,MAAM,GAAG,EAAE;CAE3B,IAAI,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,SAAS,IAAI,KAAK,CAAC,MAAM,SAAS,IAAI,KAAK,CAAC,MAAM,SAAS,IAAI,GAC7F,OAAO,KAAK,MAAM,GAAG,EAAE;CAG3B,OAAO;AACX;AAiBA,SAAgB,gBAAgB,WAA2B;CACvD,MAAM,QAAQ,UAAU,YAAY;CACpC,IAAI,MAAM,SAAS,MAAM,KAAK,MAAM,SAAS,SAAS,KAAK,MAAM,SAAS,QAAQ,KAAK,MAAM,SAAS,UAAU,KAAK,MAAM,SAAS,QAAQ,KAAK,MAAM,SAAS,SAAS,GAAG,OAAO;CACnL,IAAI,MAAM,SAAS,MAAM,KAAK,MAAM,SAAS,SAAS,KAAK,MAAM,SAAS,MAAM,KAAK,MAAM,SAAS,MAAM,GAAG,OAAO;CACpH,IAAI,MAAM,SAAS,SAAS,KAAK,MAAM,SAAS,MAAM,GAAG,OAAO;CAChE,IAAI,MAAM,SAAS,OAAO,KAAK,MAAM,SAAS,MAAM,KAAK,MAAM,SAAS,UAAU,KAAK,MAAM,SAAS,SAAS,GAAG,OAAO;CACzH,IAAI,MAAM,SAAS,SAAS,KAAK,MAAM,SAAS,QAAQ,GAAG,OAAO;CAClE,IAAI,MAAM,SAAS,KAAK,KAAK,MAAM,SAAS,SAAS,GAAG,OAAO;CAC/D,IAAI,MAAM,SAAS,OAAO,KAAK,MAAM,SAAS,OAAO,KAAK,MAAM,SAAS,OAAO,KAAK,MAAM,SAAS,OAAO,GAAG,OAAO;CACrH,IAAI,MAAM,SAAS,cAAc,KAAK,MAAM,SAAS,SAAS,KAAK,MAAM,SAAS,OAAO,GAAG,OAAO;CACnG,IAAI,MAAM,SAAS,KAAK,KAAK,MAAM,SAAS,OAAO,KAAK,MAAM,SAAS,OAAO,GAAG,OAAO;CACxF,IAAI,MAAM,SAAS,cAAc,KAAK,MAAM,SAAS,MAAM,KAAK,MAAM,SAAS,SAAS,GAAG,OAAO;CAClG,IAAI,MAAM,SAAS,SAAS,KAAK,MAAM,SAAS,QAAQ,KAAK,MAAM,SAAS,UAAU,GAAG,OAAO;CAChG,OAAO;AACX;AAMA,SAAgB,aAAa,YAAgD;CACzE,MAAM,0BAAU,IAAI,IAAsB;CAC1C,KAAK,MAAM,MAAM,YAAY;EACzB,MAAM,WAAW,QAAQ,IAAI,GAAG,SAAS;EACzC,IAAI,UACA,SAAS,KAAK,GAAG,UAAU;OAE3B,QAAQ,IAAI,GAAG,WAAW,CAAC,GAAG,UAAU,CAAC;CAEjD;CACA,OAAO;AACX;AAIA,SAAgB,eACZ,QACA,SACA,KACA,KACsB;CACtB,MAAM,4BAAY,IAAI,IAAuB;CAC7C,KAAK,MAAM,KAAK,QACZ,UAAU,IAAI,EAAE,YAAY;EACxB,MAAM,EAAE;EACR,SAAS,QAAQ,QAAQ,MAAM,EAAE,eAAe,EAAE,UAAU;EAC5D,KAAK,IAAI,QAAQ,OAAO,GAAG,eAAe,EAAE,UAAU,CAAC,CAAC,KAAK,OAAO,GAAG,WAAW;EAClF,KAAK,IAAI,QAAQ,OAAO,GAAG,eAAe,EAAE,UAAU;CAC1D,CAAC;CAEL,OAAO;AACX;;;;;;;;;;;;;;;AAkBA,SAAgB,mBAAmB,WAAgD;CAC/E,MAAM,6BAAa,IAAI,IAAY;CACnC,KAAK,MAAM,CAAC,WAAW,SAAS,UAAU,QAAQ,GAC9C,IAAI,KAAK,IAAI,WAAW;MACM,KAAK,QAAQ,OAAO,MAC1C,KAAK,IAAI,MAAM,OAAO,GAAG,gBAAgB,EAAE,WAAW,KACtD,EAAE,gBAAgB,QAClB,EAAE,gBAAgB,gBAClB,EAAE,gBAAgB,YAGlB,GACA,WAAW,IAAI,SAAS;CAAA;CAIpC,OAAO;AACX;;;;;;;;;;;ACtQA,eAAsB,cAAc,QAAmB,UAAwD;CAC3G,MAAM,EAAE,SAAS,MAAM,OAAO,MAC1B;;;;;oDAMA,CAAC,QAAQ,CACb;CAEA,OAAO,IAAI,IACP,KAAK,KAAK,MAAM,CACZ,EAAE,OACF;EAAE,OAAO,EAAE;EAAO,YAAY,EAAE,gBAAgB;EAAM,aAAa,OAAO,EAAE,gBAAgB,CAAC;CAAE,CACnG,CAAC,CACL;AACJ;;;;;AAWA,eAAsB,iBAAiB,QAAmB,UAA+C;CACrG,MAAM,EAAE,MAAM,WAAW,MAAM,OAAO,MAClC;;;;;+BAMA,CAAC,QAAQ,CACb;CAEA,MAAM,EAAE,MAAM,YAAY,MAAM,OAAO,MACnC;;;;;;;;;;;;;qCAcA,CAAC,QAAQ,CACb;CAEA,MAAM,EAAE,MAAM,eAAe,MAAM,OAAO,MACtC;;;;;;;+CAQA,CAAC,QAAQ,CACb;CAEA,MAAM,EAAE,MAAM,QAAQ,MAAM,OAAO,MAC/B;;;;;;oDAOA,CAAC,QAAQ,CACb;CAEA,MAAM,EAAE,MAAM,QAAQ,MAAM,OAAO,MAC/B;;;;;;;;;;;;6EAaA,CAAC,QAAQ,CACb;CAEA,MAAM,YAAY,eAAe,QAAQ,SAAS,KAAK,GAAG;CAC1D,OAAO;EACH;EACA,SAAS,aAAa,UAAU;EAChC,YAAY,mBAAmB,SAAS;CAC5C;AACJ;;AAGA,SAAS,UAAU,KAAkB,UAA+C;CAChF,IAAI,IAAI,UAAU,YAAY,MAAM,QAAQ,OAAO;CACnD,IAAI,aAAa,UAAU,OAAO;CAClC,OAAO;AACX;AAEA,SAAS,gBACL,MACA,SACuC;CACvC,MAAM,aAAsD,CAAC;CAC7D,MAAM,4BAAY,IAAI,IAAY;CAElC,KAAK,MAAM,OAAO,KAAK,SAAS;EAC5B,MAAM,OAAO,KAAK,IAAI,SAAS,IAAI,WAAW;EAI9C,IADa,KAAK,IAAI,MAAM,OAAO,GAAG,gBAAgB,IAAI,WACtD,KAAQ,CAAC,MAAM;EAEnB,MAAM,aAAa,QAAQ,IAAI,IAAI,QAAQ;EAC3C,MAAM,SAAS,IAAI,cAAc,kBAAkB,eAAe,KAAA;EAClE,MAAM,WAAW,IAAI,aAAa;EAClC,MAAM,WAAW,SAAS,WAAW,WAAW,WAAW,UAAU,IAAI,SAAS;EAElF,MAAM,WAAoC;GACtC,MAAM,SAAS,IAAI,WAAW;GAC9B,YAAY,IAAI;GAChB,MAAM;EACV;EAWA,MAAM,MAAM,aAAa,CAAC,UAAU,IAAI,WAAW,GAAG,IAAI,WAAW,GAAG,SAAS;EACjF,UAAU,IAAI,GAAG;EAEjB,IAAI,MACA,SAAS,OAAO,UAAU,KAAK,QAAQ;OACpC,IAAI,IAAI,gBAAgB,QAAQ,IAAI,mBAAmB,MAC1D,SAAS,aAAa,EAAE,UAAU,KAAK;EAG3C,IAAI,UAAU,YACV,SAAS,OAAO,WAAW,KAAK,WAAW;GAAE,IAAI;GAAO,OAAO,SAAS,KAAK;EAAE,EAAE;EAGrF,WAAW,OAAO;CACtB;CAEA,OAAO;AACX;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAS,eACL,MACA,aACA,kBACuC;CACvC,MAAM,YAAqD,CAAC;CAE5D,KAAK,MAAM,MAAM,KAAK,KAAK;EACvB,MAAM,aAAa,YAAY,IAAI,GAAG,kBAAkB;EACxD,IAAI,CAAC,YAAY;EAGjB,IAAI,MAAM,UAAU,GAAG,YAAY,QAAQ,QAAQ,EAAE,CAAC;EACtD,IAAI,KAAK,IAAI,SAAS,GAAG,WAAW,KAAK,QAAQ,GAAG,aAIhD,MAAM,GAAG;EAGb,UAAU,OAAO;GACb,MAAM,SAAS,GAAG;GAClB,MAAM;GACN,UAAU;IACN,MAAM;IACN,cAAc,iBAAiB,IAAI,UAAU;IAC7C,UAAU,GAAG;GACjB;EACJ;CACJ;CAEA,OAAO;AACX;;;;;;;AAQA,SAAgB,2BACZ,EAAE,WAAW,SAAS,cACtB,UAC0B;CAC1B,MAAM,8BAAc,IAAI,IAAoB;CAC5C,KAAK,MAAM,aAAa,UAAU,KAAK,GACnC,IAAI,CAAC,WAAW,IAAI,SAAS,GAAG,YAAY,IAAI,WAAW,SAAS;CAGxE,MAAM,cAA0C,CAAC;CAGjD,MAAM,mCAAmB,IAAI,IAAsC;CAEnE,KAAK,MAAM,CAAC,WAAW,SAAS,WAAW;EACvC,IAAI,WAAW,IAAI,SAAS,GAAG;EAE/B,MAAM,iBAAiB,SAAS,SAAS;EACzC,MAAM,aAAa;GACf,MAAM;GACN,cAAc,YAAY,cAAc;GACxC,MAAM;GACN,OAAO;GACP,QAAQ;GACR,MAAM,gBAAgB,SAAS;GAC/B,YAAY;IACR,GAAG,gBAAgB,MAAM,OAAO;IAChC,GAAG,eAAe,MAAM,aAAa,gBAAgB;GACzD;EACJ;EAEA,YAAY,KAAK,UAAU;EAC3B,iBAAiB,IAAI,WAAW,UAAU;CAC9C;CAEA,OAAO;AACX;;;;;;;;;;;;;;;;AC1QA,IAAM,QAAQ,WAAiD,EAC3D,gBAAgB,QACpB,CAAC;;;;;;AAOD,SAAS,aAAa,KAAsC;CACxD,OAAO,IAAI,aAAa,IAAI,YAAY,IAAI,IAAI,YAAY,IAAI,KAAA;AACpE;;;;;;;;;AAUA,SAAS,cAAc,SAAiB,MAAc,KAAuC;CACzF,QAAQ,SAAR;EACI,KAAK,QACD,OAAO,KAAK,IAAI;EACpB,KAAK,QACD,OAAO,QAAQ,IAAI;EACvB,KAAK,QACD,OAAO,SAAS,IAAI;EACxB,KAAK,QACD,OAAO,QAAQ,IAAI;EACvB,KAAK,QACD,OAAO,OAAO,MAAM,EAAE,MAAM,SAAS,CAAC;EAC1C,KAAK,UACD,OAAO,KAAK,IAAI;EACpB,KAAK,UACD,OAAO,gBAAgB,IAAI;EAC/B,KAAK;EACL,KAAK,SACD,OAAO,QAAQ,IAAI;EACvB,KAAK,QACD,OAAO,KAAK,IAAI;EACpB,KAAK,SACD,OAAO,MAAM,IAAI;EACrB,KAAK,QACD,OAAO,KAAK,IAAI;EACpB,KAAK,QACD,OAAO,KAAK,IAAI;EACpB,KAAK,UACD,OAAO,KAAK,MAAM,EAAE,cAAc,KAAK,CAAC;EAC5C,KAAK,aACD,OAAO,UAAU,IAAI;EACzB,KAAK,eACD,OAAO,UAAU,MAAM,EAAE,cAAc,KAAK,CAAC;EACjD,KAAK,YACD,OAAO,SAAS,IAAI;EACxB,KAAK,SACD,OAAO,MAAM,IAAI;EACrB,KAAK,QACD,OAAO,KAAK,IAAI;EACpB,KAAK,QACD,OAAO,KAAK,IAAI;EACpB,KAAK,WACD,OAAO,QAAQ,IAAI;EACvB,KAAK,YACD,OAAO,SAAS,IAAI;EACxB,KAAK,SACD,OAAO,MAAM,IAAI;EACrB,KAAK,QACD,OAAO,KAAK,IAAI;EACpB,KAAK,YACD,OAAO,SAAS,IAAI;EACxB,KAAK,UAAU;GAGX,MAAM,aAAa,IAAI,aAAa,IAAI,YAAY,IAAI,IAAI,YAAY,KAAA;GACxE,OAAO,aAAa,OAAO,MAAM,EAAE,WAAW,CAAC,IAAI,KAAK,IAAI;EAChE;EACA,KAAK,UAAU;GACX,MAAM,SAAS,aAAa,GAAG;GAC/B,OAAO,SAAS,KAAK,MAAM,EAAE,OAAO,CAAC,IAAI,KAAK,IAAI;EACtD;EACA,KAAK,WAAW;GACZ,MAAM,SAAS,aAAa,GAAG;GAC/B,OAAO,SAAS,QAAQ,MAAM,EAAE,OAAO,CAAC,IAAI,KAAK,IAAI;EACzD;EACA,SAGI,OAAO,KAAK,IAAI;CACxB;AACJ;AAEA,SAAS,iBAAiB,KAAuC;CAG7D,IAAI,IAAI,SAAS,WAAW,GAAG,GAE3B,OADgB,cAAc,IAAI,SAAS,MAAM,CAAC,GAAG,IAAI,aAAa,GAC9D,CAAA,CAAwD,MAAM;CAE1E,OAAO,cAAc,IAAI,UAAU,IAAI,aAAa,GAAG;AAC3D;;;;AAKA,SAAgB,6BACZ,WACA,eAAe,UACQ;CACvB,MAAM,SAAS,iBAAiB,WAAW,OAAO,SAAS,YAAY;CAGvE,MAAM,cAAe,SAAS,OAAO,MAAM,KAAK,MAAM,IAAI;CAM1D,MAAM,SAAkC,CAAC;CAEzC,KAAK,MAAM,CAAC,WAAW,SAAS,WAAW;EACvC,MAAM,UAA+C,CAAC;EAEtD,KAAK,MAAM,OAAO,KAAK,SAAS;GAC5B,IAAI,UAAU,iBAAiB,GAAG;GAElC,IAAI,IAAI,gBAAgB,MACpB,UAAW,QAA0D,QAAQ;GAIjF,IAAI,KAAK,IAAI,WAAW,KAAK,KAAK,IAAI,OAAO,IAAI,aAC7C,UAAW,QAA6D,WAAW;GAGvF,QAAQ,IAAI,eAAe;EAC/B;EAGA,OAAO,aAAa,YAChB,WACA,SAHgB,KAAK,IAAI,SAAS,KAK3B,MAAM,CAAC,WAAW,EAAE,SAAS,KAAK,IAAI,KAAK,OAAO,EAAE,GAAG,EAAW,CAAC,CAAC,IACrE,KAAA,CACV;CACJ;CAEA,OAAO;AACX;;;;;;;;;;;;AAaA,SAAgB,gCACZ,WACA,QACyB;;CAEzB,MAAM,yBAAS,IAAI,IAAkH;;CAErI,MAAM,0BAAU,IAAI,IAA0E;CAE9F,KAAK,MAAM,CAAC,WAAW,SAAS,WAAW;EACvC,IAAI,CAAC,OAAO,YAAY;EACxB,MAAM,cAAc,IAAI,IAAI,KAAK,QAAQ,KAAK,MAAM,EAAE,WAAW,CAAC;EAElE,KAAK,MAAM,MAAM,KAAK,KAAK;GACvB,IAAI,CAAC,OAAO,GAAG,qBAAqB;GAIpC,IAAI,MAAM,GAAG,YAAY,QAAQ,QAAQ,EAAE;GAC3C,IAAI,KAAK,IAAI,SAAS,GAAG,WAAW,KAAK,QAAQ,GAAG,aAChD,MAAM,GAAG;GAGb,IAAI,YAAY,IAAI,GAAG,GAAG;GAK1B,MAAM,eAAe,GAAG,UAAU,GAAG,GAAG;GAExC,OAAO,IAAI,WAAW,CAClB,GAAI,OAAO,IAAI,SAAS,KAAK,CAAC,GAC9B;IAAE;IAAK,aAAa,GAAG;IAAoB,UAAU,GAAG;IAAa,cAAc,GAAG;IAAqB;GAAa,CAC5H,CAAC;GAED,MAAM,UAAU;GAEhB,IAAI,IADsB,KAAK,UAAU,IAAI,GAAG,kBAAkB,CAAC,EAAE,WAAW,CAAC,EAAA,CAAG,KAAK,MAAM,EAAE,WAAW,CACxG,CAAA,CAAc,IAAI,OAAO,GAAG;GAEhC,QAAQ,IAAI,GAAG,oBAAoB,CAC/B,GAAI,QAAQ,IAAI,GAAG,kBAAkB,KAAK,CAAC,GAC3C;IAAE,KAAK;IAAS,aAAa;IAAW;GAAa,CACzD,CAAC;EACL;CACJ;CAEA,MAAM,QAAmC,CAAC;CAE1C,KAAK,MAAM,aAAa,UAAU,KAAK,GAAG;EACtC,MAAM,QAAQ,OAAO;EACrB,MAAM,OAAO,OAAO,IAAI,SAAS,KAAK,CAAC;EACvC,MAAM,QAAQ,QAAQ,IAAI,SAAS,KAAK,CAAC;EACzC,IAAI,CAAC,SAAU,KAAK,WAAW,KAAK,MAAM,WAAW,GAAI;EAEzD,MAAM,GAAG,UAAU,cAAc,UAAU,QAAQ,EAAE,KAAK,WAAW;GACjE,MAAM,MAA+B,CAAC;GAEtC,KAAK,MAAM,OAAO,MACd,IAAI,IAAI,OAAO,IAAI,OAAO,IAAI,cAAc;IACxC,QAAQ,CAAE,MAA2C,IAAI,SAAS;IAClE,YAAY,CAAE,OAAO,IAAI,YAAY,CAAsC,IAAI,aAAa;IAC5F,cAAc,IAAI;GACtB,CAAC;GAKL,KAAK,MAAM,OAAO,OAAO;IAErB,IAAI,IAAI,IAAI,MAAM;IAClB,IAAI,IAAI,OAAO,KAAK,OAAO,IAAI,cAAc,EAAE,cAAc,IAAI,aAAa,CAAC;GACnF;GAEA,OAAO;EACX,CAAC;CACL;CAEA,OAAO;AACX;;;;;;;;;;;ACxRA,SAAgB,eAAe,KAAuB;CAClD,IAAI,CAAC,OAAO,OAAO,QAAQ,UAAU,OAAO;CAC5C,MAAM,IAAI;CACV,IAAI,EAAE,SAAS,gBAAgB,OAAO;CAEtC,IAAI,MAAM,QAAQ,EAAE,MAAM,GACtB,OAAO,EAAE,OAAO,MAAK,UACjB,SAAS,OAAO,UAAU,YAAa,MAA4B,SAAS,cAChF;CAGJ,IAAI,EAAE,SAAS,OAAO,EAAE,UAAU,UAC9B,OAAO,eAAe,EAAE,KAAK;CAEjC,OAAO;AACX;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACsGA,SAAgB,sBACZ,KACA,sBACM;CAEN,QADsB,6BAA6B,GAAG,IAAI,IAAI,QAAQ,KAAA,MAE/D,qBAAqB,MAAM,MAAM,MAAM,IAAI,IAAI,KAC/C,IAAI;AACf;;;;;;;;;;;;;;;;AAiBA,SAAgB,yBACZ,cACQ;CAGR,IAAI,iBAAiB,KAAA,GACjB,OAAO;EACH;EACA;EACA;EACA;CACJ;CAEJ,IAAI,aAAa,WACb,OAAO;EACH;EACA;EACA;EACA;EACA;EACA;CACJ;CAEJ,OAAO;EACH;EACA,OAAO,aAAa,UAAU;EAC9B;EACA;CACJ;AACJ;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,0BAA0B,kBAA+C;CACrF,IAAA,QAAA,IAAA,aAA6B,cAAc,OAAO;CAClD,IAAI,CAAC,kBAAkB,OAAO;CAC9B,IAAI;CACJ,IAAI;EACA,OAAO,IAAI,IAAI,gBAAgB,CAAC,CAAC;CACrC,QAAQ;EACJ,OAAO;CACX;CAEA,MAAM,OAAO,KAAK,QAAQ,YAAY,EAAE,CAAC,CAAC,YAAY;CACtD,OAAO,SAAS,eACT,SAAS,SACT,SAAS,aACT,SAAS,MACT,SAAS,KAAK,IAAI,KAClB,KAAK,SAAS,YAAY;AACrC;;;;;;;;;;;;;;;;;;;;AAqBA,IAAM,qBAAqB;AAE3B,SAAgB,2BAA2B,UAAqD;CAI5F,IAAI,SAAS,qBACT,6BAA6B,SAAS,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;CA2B7D,MAAM,kBAAkB,iBAAqC;EAEzD,QADkB,cAAc,UAAA,EACd,MAAO,SAAS;CACtC;CAEA,MAAM,yBAAyB,iBAAqC;EAEhE,MAAM,MADY,cAAc,UAAA,EACV,MAAO,SAAS;EACtC,IAAI,CAAC,IACD,MAAM,IAAI,MACN,kPAGJ;EAEJ,OAAO,EACH,MAAM,MAAS,MAAsC;GACjD,MAAM,SAAS,MAAM,GAAG,QAAQ,IAAI,IAAI,IAAI,CAAC;GAE7C,OAAO,EAAE,MADK,OAAqC,SAC3B,MAAM,QAAQ,MAAM,IAAK,SAAiB,CAAC,GAAG;EAC1E,EACJ;CACJ;CAEA,OAAO;EACH,MAAM;EAEN,MAAM,iBAAiB,QAA6C;GAGhE,MAAM,EAAE,aAAa,oBAAoB,uBAAuB,MAAM,oBAAoB,aAAa;GAYvG,MAAM,qBAAqB,UAAU,aAAa;GAClD,MAAM,qBAAqB,UAAU,aAAa;GAElD,MAAM,oBAAoB,MAAM,qBAAqB;GAErD,MAAM,aAAa,SAAS;GAC5B,MAAM,YAAa,cAAc,OAAO,eAAe,YAAY,aAAa,aACzE,WAAuC,UACxC;GAKN,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI,0BAA0B,CAAC,eAAe,YAAY,WAAW,IAAI;IACrE,MAAM,eAAe,SAAS,uBAAuB;IACrD,MAAM,SAAS,MAAM,iBAAiB,WAAW,YAAY;IAQ7D,MAAM,YAAY,MAAM,cAAc,WAAW,YAAY;IAC7D,MAAM,cAAc,CAAC,GAAG,OAAO,UAAU,KAAK,CAAC,CAAC,CAAC,QAC5C,MAAM,CAAC,OAAO,WAAW,IAAI,CAAC,KAAK,CAAC,UAAU,IAAI,CAAC,CAAC,EAAE,UAC3D;IACA,MAAM,aAAa,CAAC,GAAG,OAAO,UAAU,KAAK,CAAC,CAAC,CAAC,QAC3C,MAAM,CAAC,OAAO,WAAW,IAAI,CAAC,KAAK,UAAU,IAAI,CAAC,CAAC,EAAE,cAAc,UAAU,IAAI,CAAC,CAAC,EAAE,gBAAgB,CAC1G;IAEA,IAAI,YAAY,SAAS,GACrB,IAAI,sBAAsB,SACtB,OAAO,KACH,oBAAoB,YAAY,OAAO,8CAA8C,YAAY,KAAK,IAAI,EAAE,8GAGhH;SACG;KACH,OAAO,KACH,wBAAwB,YAAY,OAAO,mFACnB,YAAY,KAAK,IAAI,EAAE,MAC/C,YAAY,KAAK,MAAM,sBAAsB,aAAa,KAAK,EAAE,mDAAmD,CAAC,CAAC,KAAK,IAAI,IAC/H,0EACJ;KACA,KAAK,MAAM,KAAK,aAAa,OAAO,UAAU,OAAO,CAAC;IAC1D;IAEJ,IAAI,WAAW,SAAS,GAGpB,OAAO,KACH,YAAY,WAAW,OAAO,2EAA2E,WAAW,KAAK,IAAI,GACjI;IAGJ,0BAA0B,2BAA2B,QAAQ,YAAY;IACzE,qBAAqB,6BAA6B,OAAO,WAAW,YAAY;IAGhF,wBAAwB,gCAAgC,OAAO,WAAW,kBAAkB;IAC5F,OAAO,KACH,4CAA4C,wBAAwB,OAAO,4BAA4B,aAAa,KAAK,wBAAwB,KAAI,MAAK,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,EACjL;GACJ;GAEA,MAAM,oBAAoB,2BAA2B;GACrD,MAAM,eAAe,sBAAsB,SAAS,QAAQ;GAC5D,MAAM,kBAAkB,yBAA0B,SAAS,QAAQ;GAInE,MAAM,WAAW,wBAAwB;IACrC,aAAa;IACb,QAAQ;IACR,OAAO,SAAS,QAAQ;IACxB,WAAW;GACf,CAAC;GAKD,IAAI,cACA,uBAAuB,YAAuC;GAIlE,MAAM,eAAwC;IAC1C,GAAG;IACH,GAAI,mBAAmB,CAAC;GAC5B;GACA,MAAM,EAAE,SAAS,kBAAkB,MAAM,OAAO;GAChD,MAAM,gBAAgB,cAAc,WAAW,EAAE,QAAQ,aAAa,CAAC;GAGvE,IAAI;IACA,MAAM,cAAc,QAAQ,GAAG,UAAU;GAC7C,SAAS,KAAc;IAEnB,IAD4B,eAAe,GACvC,GAAqB;KAErB,IAAI,WAAW,SAAS,oBAAoB;KAC5C,IAAI;MACA,MAAM,SAAS,IAAI,IAAI,SAAS,oBAAoB,EAAE;MACtD,WAAW,GAAG,OAAO,SAAS,GAAG,OAAO,QAAQ;KACpD,QAAQ,CAAuB;KAE/B,MAAM,UACF;;uCAEwC,SAAS;KAWrD,OAAO,MAAM,OAAO;KACpB,MAAM,IAAI,MAAM,mCAAmC,SAAS,+CAA+C;IAC/G;IAoBA,MAAM,EAAE,OAAO,QAAQ,SAAS,uBAAuB,GAAG;IAC1D,MAAM,SAAS,OAAO,KAAK,KAAK,KAAK;IAErC,IAAI,OAAO;KACP,OAAO,MACH;;;;;IAKK,SAAS,OAAO,+LAMzB;KACA,MAAM,IAAI,MAAM,sCAAsC,SAAS,QAAQ;IAC3E;IAEA,OAAO,MAAM,sCAAsC,SAAS,UAAU,EAAE,OAAO,IAAI,CAAC;IACpF,OAAO,KAAK,gHAAgH;GAChI;GAGA,MAAM,kBAAkB,IAAI,gBAAgB,eAAe,QAAQ;GAGnE,IAAI;GACJ,MAAM,UAAU,QAAQ,IAAI;GAC5B,IAAI,WAAW,YAAY,SAAS,kBAChC,IAAI;IACA,MAAM,EAAE,gCAAgC,MAAM,OAAO,2BAAA,CAAA,MAAA,MAAA,EAAA,CAAA;IAErD,SADsB,4BAA4B,SAAS,YAClD,CAAA,CAAc;IACvB,OAAO,KAAK,+DAA+D;GAC/E,SAAS,KAAK;IACV,OAAO,KAAK,iFAAiF,EAAE,OAAO,IAAI,CAAC;GAC/G;GAEJ,MAAM,cAAc,SAAS,wBACvB,IAAI,oBAAoB,SAAS,qBAAqB,IACtD,KAAA;GACN,MAAM,SAAS,IAAI,sBAAsB,eAAe,iBAAiB,UAAU,KAAA,GAAW,WAAW;GACzG,gBAAgB,cAAc,MAAM;GAcpC;IACI,MAAM,SAAuB,OAAO,SAAS;KAEzC,QAAQ,MADU,cAAc,QAAQ,IAAI,IAAI,IAAI,CAAC,EAAA,CACzC,QAAQ,CAAC;IACzB;IAKA,MAAM,0BAA0B,MAAM;IAEtC,MAAM,UAAU,MAAM,wBAAwB,MAAM;IACpD,IAAI,QAAQ,YAAY;KAQpB,MAAM,cAAc,QAAQ;MAAC;MAAU;MAAU,GAPvB,SAAS,eAAe,CAAC,CAC9C,KAAK,MAAO,EAA0B,MAAM,CAAC,CAC7C,QAAQ,MAAmB,OAAO,MAAM,QAKO;KAAiB,CAAC;KACtE,OAAO,cAAc;KACrB,gBAAgB,cAAc;KAC9B,OAAO,KAAK,6DAA6D,iBAAiB,iBAAiB,QAAQ,KAAK,kBAAkB,QAAQ,YAAY,cAAc,QAAQ,YAAY,cAAc,cAAc,EAAE;KAC9N,IAAI,QAAQ,aAAa,QAAQ,WAAW;MACxC,MAAM,UACF,mCAAmC,QAAQ,YAAY,gBAAgB,mBAAmB,KAAK,QAAQ,KAAK;MAGhH,IAAI,0BAA0B,SAAS,gBAAgB,GACnD,OAAO,MAAM,MAAM,SAAS;WAE5B,OAAO,KAAK,MAAM,SAAS;KAEnC;IACJ,OACI,OAAO,KAAK,wCAAwC,QAAQ,KAAK,qDAAqD;IAO1H,MAAM,sBACF,QACA,SAAS,eAAe,GACxB,OAAO,eAAe,QAAQ,IAClC;IAKA,sBAAsB,SAAS,eAAe,CAAU;IAKxD,yBAAyB,SAAS,eAAe,CAAU;GAC/D;GAGA,IAAI,OAAO,eACP,IAAI;IACA,MAAM,OAAO,cAAc,0BAA0B;GACzD,SAAS,KAAK;IACV,OAAO,KAAK,iDAAiD,EAAE,OAAO,IAAI,CAAC;GAC/E;GAOJ,IAAI;IACA,MAAM,gBAAgB,wBAClB,SAAS,UAAU,UACnB,EAAE,WAAW,mBAAmB,CACpC;GACJ,SAAS,KAAK;IACV,OAAO,KAAK,sFAAsF,EAAE,OAAO,IAAI,CAAC;GACpH;GAIA,MAAM,YAAY,QAAQ,IAAI,uBAAuB,SAAS;GAO9D,IAAI;IACA,MAAM,aAAa,yBAAyB,SAAS,UAAU,GAAG;IAKlE,IADiB,qBAAqB,UAAU,KAAK,WAAW,SAAS,UAErE,MAAM,gBAAgB,oBAClB,iBAAiB,YAAY;KACzB,IAAI;KACJ;IACJ,CAAC,CACL;GAER,SAAS,KAAK;IACV,OAAO,KAAK,0GAA0G,EAAE,OAAO,IAAI,CAAC;GACxI;GAaA,MAAM,6BAAa,IAAI,IAAI;IAAC;IAAQ;IAAO;IAAW;GAAK,CAAC;GAC5D,IAAI,WAAW,QAAQ,IAAI,gBAAgB,OAAA,CAAQ,KAAK,CAAC,CAAC,YAAY;GACtE,IAAI,CAAC,WAAW,IAAI,OAAO,GAAG;IAC1B,OAAO,KAAK,wCAAwC,QAAQ,yDAAyD;IACrH,UAAU;GACd;GAQA,MAAM,WAAW,YAAY,UAAU,sBAAsB;GAC7D,MAAM,cAAc,YAAY,aAAa,YAAY;GACzD,IAAI,aAAa;GACjB,IAAI;GAEJ,IAAI,YAAY,CAAC,WAAW;IACxB,MAAM,SAAS;IACf,IAAI,aAAa,OAAO,KAAK,yBAAyB,QAAQ,OAAO,OAAO,6BAA6B;SACpG,OAAO,KAAK,uCAAuC,OAAO,EAAE;GACrE,OAAO,IAAI,YAAY,WAAW;IAC9B,IAAI,YAAY,OAKZ,OAAO,KACH,gKAEJ;IAEJ,IAAI;KACA,MAAM,YAA0B,OAAO,SAAS;MAE5C,QAAQ,MADU,cAAc,QAAQ,IAAI,IAAI,IAAI,CAAC,EAAA,CACzC,QAAQ,CAAC;KACzB;KACA,MAAM,YAA2B,SAAS,eAAe,CAAC,CACrD,KAAK,OAAO;MACT,QAAS,EAA0B,UAAU;MAC7C,OAAO,eAAuB,CAAC;KACnC,EAAE,CAAC,CACF,QAAQ,MAAM,QAAQ,EAAE,KAAK,KAAK,SAAS,sBAAsB,EAAE,KAAK,CAAC;KAK9E,KAAK,MAAM,QAAQ,qBAAqB,QAAQ,GAC5C,UAAU,KAAK;MAAE,QAAQ,KAAK;MACtD,OAAO,KAAK;KAAM,CAAC;KAKC,IAAI,oBAAoB,MAAM,oBAAoB,WAAW,SAAS;KACtE,IAAI,oBAAoB,MAAM,gBAAgB,UAAU,SAAS;KACjE,aAAa;KAOb,IAAI,oBACA,wBAAwB,OAAO,WAAW;MACtC,MAAM,oBAAoB,WAAW,MAAM;KAC/C;KAOJ,OAAO,KACH,mEAAmE,YAAY,QAAQ,gBAAgB,UAAU,iEAEhH,qBAAqB,KAAK,8DAC/B;IACJ,SAAS,KAAK;KACV,IAAI,aACA,OAAO,KAAK,iGAAiG,EAAE,OAAO,IAAI,CAAC;UAE3H,OAAO,KACH,kNAEA,EAAE,QAAQ,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,CAC/D;IAER;GACJ;GAIA,IAAI,CAAC,cAAc,aAAa,oBAC5B,IAAI;IACA,MAAM,gBAAgB,eAAe,SAAS;GAClD,SAAS,KAAK;IACV,OAAO,KAAK,mDAAmD,EAAE,OAAO,IAAI,CAAC;GACjF;GAMJ,IAAI;IACA,MAAM,wBAAwB,SAAS,eAAe;IACtD,IAAI,sBAAsB,SAAS,GAAG;KAKlC,MAAM,SAAS,MAAM,cAAc,QAAQ,IAAI,IAAI;;;;;qBAKlD,CAAC;KACF,MAAM,+BAAe,IAAI,IAAsB;KAC/C,KAAK,MAAM,OAAO,OAAO,MAA6D;MAClF,MAAM,UAAU,aAAa,IAAI,IAAI,UAAU,KAAK,CAAC;MACrD,QAAQ,KAAK,IAAI,YAAY;MAC7B,aAAa,IAAI,IAAI,YAAY,OAAO;KAC5C;KACA,MAAM,WAAW,IAAI,IAChB,OAAO,KAA6D,KAAI,MACrE,EAAE,iBAAiB,WAAW,EAAE,aAAa,GAAG,EAAE,aAAa,GAAG,EAAE,YACxE,CACJ;KACA,MAAM,UAAqE,CAAC;KAC5E,KAAK,MAAM,OAAO,uBAAuB;MAOrC,IAAK,IAAyC,MAAM,SAAS;MAE7D,MAAM,aAAa,YAAY,OAAO,IAAI,SAAS,IAAI,SAAS;MAChE,MAAM,YAAY,sBAAsB,KAAK,SAAS,cAAc,CAAC;MACrE,MAAM,gBAAgB,eAAe,WAAW,YAAY,GAAG,WAAW,GAAG;MAC7E,IAAI,CAAC,SAAS,IAAI,aAAa,GAG3B,QAAQ,KAAK;OAAE,MAAM,IAAI;OACrD,OAAO;OACP,UAAU,aAAa,IAAI,SAAS,KAAK,CAAC,EAAA,CAAG,QAAO,MAAK,MAAM,UAAU;MAAE,CAAC;KAExD;KACA,IAAI,QAAQ,SAAS,GAAG;MACpB,MAAM,QAAQ,QAAQ,KAClB,MAAK,qBAAqB,EAAE,KAAK,aAAa,EAAE,MAAM,MACjD,EAAE,QAAQ,SAAS,IACd,yCAAyC,EAAE,QAAQ,KAAI,MAAK,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,MAC/E,GACd;MAYA,MAAM,YAAY,QAAQ,QAAO,MAAK,EAAE,QAAQ,SAAS,CAAC;MAC1D,MAAM,gBAAgB,UAAU,WAAW,IAAI,CAAC,IAAI;OAChD;OACA;OACA;OACA,GAAG,UAAU,KAAI,MACb,sBAAsB,EAAE,QAAQ,GAAG,KAAK,EAAE,MAAM,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,gBAAgB,EAAE,MAAM,SAAS,GAAG,IAAI,EAAE,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,SAAS,GAC9I;OACA;OACA;OACA;MACJ;MAYA,MAAM,QAAQ,yBAAyB,kBAAkB;MACzD,OAAO,KAAK;OACR;OACA;OACA,GAAG;OACH;OACA,GAAG;OACH,GAAG;OACH;OACA;OACA;OACA;OACA;OACA;OACA;MACJ,CAAC,CAAC,KAAK,IAAI,CAAC;KAChB;IACJ;GACJ,SAAS,KAAK;IACV,OAAO,KAAK,8CAA8C,EACtD,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAC1D,CAAC;GACL;GAYA,OAAO;IACH;IACA,kBAAkB;IAClB,oBAAoB;IAGpB,aAAa;IACb,WAAA;KAhBA,IAAI;KACJ;KACA;KACA;KACA;KACA;KACA;IAUA;GACJ;EACJ;EAEA,MAAM,eAAe,QAAiB,cAAwE;GAC1G,MAAM,aAAa;GACnB,IAAI,CAAC,YAAY,OAAO,KAAA;GAExB,MAAM,YAAY,aAAa;GAC/B,MAAM,KAAK,UAAU;GACrB,MAAM,WAAW,UAAU;GAI3B,MAAM,iBAAiB,WAAW;GAGlC,MAAM,sBAAsB,IAAI,cAAc;GAK9C,IAAI,kBAAkB,UAAU,uBAAuB;IACnD,MAAM,aAAa,YAAY,kBAAkB,OAAO,eAAe,WAAW,WAC5E,eAAe,SACf;IACN,MAAM,YAAY,WAAW,kBAAkB,OAAO,eAAe,UAAU,WACzE,eAAe,QACf,eAAe;IACrB,IAAI,WACA,IAAI;KACA,MAAM,UAAU,sBAAsB,CAAC;MAAE,QAAQ;MAAY,OAAO;KAAU,CAAC,CAAC;IACpF,SAAS,KAAK;KACV,OAAO,KACH,+DAA+D,WAAW,GAAG,UAAU,qDAEvF,EAAE,QAAQ,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,CAC/D;IACJ;GAER;GAEA,IAAI;GACJ,IAAI,WAAW,OACX,eAAe,mBAAmB,WAAW,KAAoB;GAKrE,MAAM,YAAY,iBACX,WAAW,kBAAkB,OAAO,eAAe,UAAU,WAC1D,eAAe,QACf,eAAe,OACnB,KAAA;GACN,MAAM,aAAa,YACb,SAAS,SAAS,SAAS,IAC3B,KAAA;GAEN,IAAI,kBAAkB;GACtB,IAAI,kBAAkB,YAAY,kBAAkB,OAAO,eAAe,WAAW,UACjF,kBAAkB,eAAe;GAGrC,MAAM,aAAa,iBAAiB,eAAe;GACnD,IAAI,YACA,WAAW,QAAQ;GAGvB,MAAM,cAAc,IAAI,YAAY,IAAI,UAAU;GAClD,MAAM,iBAAiB,IAAI,uBAAuB,IAAI,UAAU;GAEhE,OAAO;IAAE;IACrB,aAAa;IACb;IACA;IAGA,yBAAyB,gBAAgB,IAAI,kBAAkB,cAAc,CAAC;GAAE;EACxE;EAEA,MAAM,kBAAkB,QAAuB,cAA0F;GACrI,IAAI,CAAC,QAAQ,OAAO,KAAA;GAGpB,MAAM,KADY,aAAa,UACV;GAErB,MAAM,yBAAyB,EAAE;GAEjC,MAAM,YAAY,OAAO,WAAW,WAAW,OAAO,YAAY,KAAA;GAGlE,OAAO,EAAE,gBAAA,IAFkB,eAAe,IAAI,YAAY,EAAE,SAAS,UAAU,IAAI,KAAA,CAE1E,EAAe;EAC5B;EAEA,MAAM,mBAAmB,SAAkB,cAAwE;GAE/G,OADkB,aAAa,UACd;EACrB;;;;;;;;;;;;;;EAeA,MAAM,uBACF,aACA,cACA,KAC4B;GAC5B,MAAM,EAAE,2BAA2B,MAAM,OAAO,yCAAA,CAAA,MAAA,MAAA,EAAA,CAAA;GAOhD,MAAM,OAAO,MAAM,uBADD,sBAAsB,YAEpC,GACA,aACA,GACJ;GACA,KAAK,MAAM,WAAW,KAAK,UACvB,OAAO,KACH,QAAQ,SAAS,mBAKX,2DAA2D,QAAQ,OAAO,qFACN,QAAQ,UAC5E,0CAA0C,QAAQ,OAAO,sGACgB,QAAQ,OAC3F;GAEJ,OAAO,EAAE,SAAS,KAAK,QAAQ,SAAS,KAAK,SAAS,OAAO;EACjE;;;;;;;;;;;;;;;;EAiBA,MAAM,yBACF,aACA,cACA,KAC4B;GAC5B,MAAM,EAAE,6BAA6B,MAAM,OAAO;GAClD,MAAM,YAAY,sBAAsB,YAAY;GACpD,MAAM,UAAU,MAAM,yBAClB,WACA,aACA,GACJ;GAEA,KAAK,MAAM,QAAQ,QAAQ,SACvB,OAAO,KAAK,qCAAqC,KAAK,MAAM,KAAK,KAAK,QAAQ;GAElF,KAAK,MAAM,WAAW,QAAQ,UAC1B,OAAO,KACH,+CAA+C,QAAQ,MAAM,sDAAsD,QAAQ,OAC/H;GAQJ,MAAM,YAAY,QAAQ,UAAU,QAAO,MAAK,CAAC,EAAE,cAAc;GACjE,KAAK,MAAM,KAAK,QAAQ,UAAU,QAAO,MAAK,EAAE,cAAc,GAC1D,OAAO,MACH,oDAAoD,EAAE,MAAM,KAAK,EAAE,MAAM,0CAChC,iBAAiB,+HAG9D;GAEJ,IAAI,UAAU,SAAS,GAKnB,MAAM,IAAI,MACN,mEACA,UAAU,KAAI,MAAK,IAAI,EAAE,MAAM,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,KAAK,IAAI,IACzD,mCAAmC,iBAAiB,kMAGxD;GAUJ,IAAI;IACA,MAAM,EAAE,yBAAyB,MAAM,OAAO,kCAAA,CAAA,MAAA,MAAA,EAAA,CAAA;IAC9C,MAAM,qBACF,OAAO,UAAU,MAAM,UAAU,MAA+B,IAAI,EAAA,CAAG,MACvE;KAAE,OAAO,MAAM,OAAO,KAAK,CAAC;KAAG,OAAO,MAAM,OAAO,KAAK,CAAC;IAAE,CAC/D;GACJ,SAAS,KAAK;IACV,OAAO,KACH,8CACC,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EACpD;GACJ;GAEA,OAAO,EAAE,SAAS,QAAQ,gBAAgB;EAC9C;;;;;;;EAQA,MAAM,6BAA6B,cAA0D;GACzF,MAAM,KAAK,eAAe,YAAY;GACtC,IAAI,CAAC,IAAI,OAAO;GAChB,MAAM,EAAE,iCAAiC,MAAM,OAAO;GACtD,OAAO,6BAA6B,IAAa,kBAAkB;EACvE;;EAGA,MAAM,8BAA8B,SAAiB,cAAiD;GAClG,MAAM,KAAK,eAAe,YAAY;GACtC,IAAI,CAAC,IAAI;GACT,MAAM,EAAE,kCAAkC,MAAM,OAAO;GACvD,MAAM,8BAA8B,IAAa,oBAAoB,OAAO;EAChF;EAEA,SAAS,cAA4D;GAEjE,OADkB,aAAa,UACd,OAAO;EAC5B;EAEA,YAAY,KAAc,UAAkB,cAAuC,CAInF;EAEA,MAAM,qBAAqB,QAAiB,iBAAmC,QAAoB,QAAkB,SAAkC;GACnJ,MAAM,EAAE,4BAA4B,MAAM,OAAO,0BAAA,CAAA,MAAA,MAAA,EAAA,CAAA;GACjD,wBACI,QACA,iBACA,QACA,QACA,OACJ;EACJ;CACJ;AACJ;;;;;;AC7nCA,SAAgB,sBAAsB,UAAiD;CACnF,MAAM,eAAe,2BAA2B,QAAQ;CAExD,OAAO;EACH,MAAM,aAAa;EAEnB,MAAM,iBAAiB,QAAQ;GAC3B,OAAO,aAAa,iBAAiB,MAAM;EAC/C;EAEA,MAAM,mBAAmB,cAAc;GACnC,IAAI,aAAa,oBACb,OAAO,aAAa,mBAAmB,CAAC,GAAG,YAAY;EAG/D;EAEA,MAAM,eAAe,QAAQ,cAAc;GACvC,IAAI,aAAa,gBACb,OAAO,aAAa,eAAe,QAAQ,YAAY;EAG/D;EAEA,MAAM,kBAAkB,QAAQ,cAAc;GAC1C,IAAI,aAAa,mBACb,OAAO,aAAa,kBAAkB,QAAQ,YAAY;EAGlE;EAOA,qBAAqB,QAAQ,iBAAiB,QAAQ,QAAQ,SAAS;GACnE,IAAI,aAAa,sBACb,OAAO,aAAa,qBAAqB,QAAQ,iBAAiB,QAAQ,QAAQ,OAAO;EAEjG;EAMA,wBAAwB,aAAa,0BAC9B,aAAa,cAAc,QAC1B,aAAa,uBAAwB,aAAa,cAAc,GAAG,IACrE,KAAA;EAEN,0BAA0B,aAAa,4BAChC,aAAa,cAAc,QAC1B,aAAa,yBAA0B,aAAa,cAAc,GAAG,IACvE,KAAA;EAQN,8BAA8B,aAAa,gCACpC,iBAAiB,aAAa,6BAA8B,YAAY,IACzE,KAAA;EAEN,+BAA+B,aAAa,iCACrC,SAAS,iBAAiB,aAAa,8BAA+B,SAAS,YAAY,IAC5F,KAAA;EAEN,SAAS,cAAc;GACnB,IAAI,aAAa,UACb,OAAO,aAAa,SAAS,YAAY;EAGjD;EAEA,YAAY,KAAK,UAAU,cAAc;GACrC,IAAI,aAAa,aACb,aAAa,YAAY,KAAK,UAAU,YAAY;EAE5D;CACJ;AACJ"}
1
+ {"version":3,"file":"index.es.js","names":[],"sources":["../../types/src/types/channel_bus.ts","../../common/src/util/email.ts","../src/databasePoolManager.ts","../src/schema/auth-schema.ts","../src/cli-output.ts","../src/schema/generate-drizzle-schema.ts","../src/services/cdc/junction-tables.ts","../src/services/cdc/trigger-cdc.ts","../src/services/pg-notify-listener.ts","../src/services/cdc/CdcListener.ts","../src/schema/drizzle-ddl.ts","../src/services/channel-history.ts","../src/services/channel-presence.ts","../src/services/channel-bus/ChannelBus.ts","../src/services/channel-bus/PostgresChannelBus.ts","../src/services/channel-bus/index.ts","../src/services/realtimeService.ts","../src/collections/PostgresCollectionRegistry.ts","../src/backup/backup-cron.ts","../src/collections/validate-relations.ts","../src/collections/buildRegistry.ts","../src/auth/schema-version.ts","../src/auth/ensure-tables.ts","../src/auth/services.ts","../src/history/HistoryService.ts","../src/history/ensure-history-table.ts","../src/utils/pg-array-null-patch.ts","../src/schema/introspect-db-naming.ts","../src/schema/introspect-db-types.ts","../src/schema/introspect-db-logic.ts","../src/schema/introspect-runtime.ts","../src/schema/dynamic-tables.ts","../src/cli-errors.ts","../src/PostgresBootstrapper.ts","../src/PostgresAdapter.ts"],"sourcesContent":["/**\n * The cross-instance transport for channel broadcast and presence, and the\n * contract anyone implementing one has to meet.\n *\n * These types live in `@rebasepro/types` rather than in the Postgres adapter on\n * purpose: a transport package should depend on the contract, not on the\n * database driver that happens to ship the default implementation. A\n * `@rebasepro/channel-bus-<something>` package needs this file and nothing else.\n *\n * Why a transport exists at all: entity/collection realtime already spans\n * instances (CDC, or per-mutation LISTEN/NOTIFY). Channel broadcast and presence\n * did not — they fanned out from per-process maps, so two clients served by\n * different replicas could not see each other, and nothing errored. The bus is\n * the missing hop, and deliberately *only* that hop: which local clients receive\n * a frame stays in the realtime service, so a transport never has to know what a\n * subscription, a WebSocket or a presence roster is.\n */\n\n/**\n * A frame in flight between instances.\n *\n * `sid` identifies the publishing instance. The realtime service drops frames\n * carrying its own `sid` on arrival — local fan-out already happened before the\n * publish — so a transport that echoes a publisher's own messages back to it is\n * still correct, merely wasteful.\n *\n * Keys are spelled out rather than abbreviated. The one shipped transport with a\n * size limit has a pointer path for anything that would approach it, so shaving\n * bytes off key names buys nothing worth the opacity.\n */\nexport type ChannelBusFrame =\n /** A broadcast carrying its payload. */\n | {\n kind: \"broadcast\";\n sid: string;\n channel: string;\n event: string;\n /** Originating client, echoed so receivers can skip it if it is theirs. */\n from?: string;\n /** Sequence number, present only on retained channels. */\n seq?: number;\n payload: unknown;\n }\n /**\n * A broadcast too large for the transport to carry inline: the body is\n * already durable in `rebase.channel_messages`, so the frame carries only\n * its address and each receiver reads it back. Only ever emitted for\n * retained channels, and only by a transport with a finite\n * {@link ChannelBus.maxFrameBytes}.\n */\n | {\n kind: \"broadcast_ref\";\n sid: string;\n channel: string;\n from?: string;\n seq: number;\n }\n /** A presence join/leave/update, small by construction. */\n | {\n kind: \"presence_diff\";\n sid: string;\n channel: string;\n joins: Record<string, Record<string, unknown>>;\n leaves: Record<string, Record<string, unknown>>;\n };\n\n/** Receives frames published by *other* instances. */\nexport type ChannelBusHandler = (frame: ChannelBusFrame) => void | Promise<void>;\n\n/**\n * A cross-instance transport.\n *\n * ## What an implementation must guarantee\n *\n * - **`start()` rejects if the transport is unusable.** The caller falls back to\n * in-process delivery when it does. Resolving while disconnected produces a\n * cluster that believes it is connected and silently is not, which is the\n * exact failure this whole mechanism exists to remove.\n * - **`publish()` reaches every *other* instance, or rejects.** Delivery back to\n * the publisher is permitted but pointless (see {@link ChannelBusFrame.sid}).\n * - **`stop()` is idempotent** and releases everything, including anything\n * holding the event loop open.\n * - **A malformed message never throws out of the transport.** Parsing happens\n * inside the implementation; drop and log what you cannot understand, so one\n * bad frame cannot take the listener down.\n *\n * ## What it does *not* have to guarantee\n *\n * - **Ordering.** Retained channels carry `seq`, and the client SDK orders by\n * it. Unsequenced broadcasts are cursor-grade traffic where order is not\n * meaningful.\n * - **Durability.** A frame lost in transit is a missed live update; retained\n * channels repair themselves through the client's `channel_history` replay.\n * - **Exactly-once.** Duplicates are tolerated — retained frames are deduped by\n * `seq`, and presence diffs are idempotent by construction.\n */\nexport interface ChannelBus {\n /**\n * Identifies the transport in logs and in `getChannelBusKind()`. Use your\n * own name; the framework only compares against `\"memory\"` to decide\n * whether publishing is worth attempting at all.\n */\n readonly kind: string;\n\n /**\n * Largest frame this transport will carry, in bytes of encoded JSON, or\n * `Infinity` when there is no meaningful ceiling.\n *\n * A broadcast that exceeds it is published as a `broadcast_ref` pointer when\n * the channel is retained, and refused with an error to the sender when it\n * is not. Implementations with no limit should return `Infinity` rather than\n * a large number, so the pointer path is never taken needlessly.\n */\n readonly maxFrameBytes: number;\n\n /** Connect and begin delivering remote frames to `handler`. */\n start(handler: ChannelBusHandler): Promise<void>;\n\n /** Publish a frame to the other instances. */\n publish(frame: ChannelBusFrame): Promise<void>;\n\n /** Disconnect and release resources. Idempotent. */\n stop(): Promise<void>;\n}\n\n/**\n * Which transport to use, for the two that ship with the Postgres adapter.\n *\n * To use one that does not ship here — a Redis package, or your own class —\n * pass the {@link ChannelBus} instance itself instead of a config object.\n *\n * There are deliberately only two built in, and neither adds a service to a\n * deployment. Rebase deploys as Postgres + backend + frontend; a bus that\n * required a message broker would put a second stateful service into every\n * `docker-compose.yml` the CLI scaffolds, for a feature most applications never\n * use. Measured across two backend instances against one Postgres container,\n * the Postgres bus carried ~10k cross-instance messages/second with no losses,\n * and stayed flat out to eight instances — comfortably past what live-cursor\n * collaboration generates. The extension point below is the answer for anyone\n * who does outgrow it.\n */\nexport type ChannelBusConfig =\n /**\n * In-process only — the historical behaviour. Broadcast and presence reach\n * the clients connected to *this* instance and no further.\n */\n | { type: \"memory\" }\n /**\n * Postgres LISTEN/NOTIFY, reusing infrastructure the deployment already has.\n *\n * `pg_notify` caps a payload at 8000 bytes, so a broadcast larger than that\n * is delivered cross-instance only on a *retained* channel, where the\n * notification carries a pointer (`seq`) instead of the message and each\n * receiver reads the body back from `rebase.channel_messages`. An oversized\n * broadcast on an ephemeral channel is refused rather than silently\n * delivered to half the cluster.\n *\n * NOTE: `LISTEN` needs a session-mode connection. Behind PgBouncer in\n * transaction mode this must point at the database directly\n * (`DATABASE_DIRECT_URL`), not at the pooler.\n */\n | {\n type: \"postgres\";\n /** Direct connection for the LISTEN client. Defaults to `DATABASE_DIRECT_URL`. */\n connectionString?: string;\n /**\n * How long to coalesce outgoing frames into a single notification, in\n * milliseconds. Defaults to 10.\n *\n * A notify is a query on your primary database, so under load this is\n * the difference between one query per message and one per window. The\n * window is leading-edge: a frame arriving when none is open goes out\n * immediately, so an idle channel pays no added latency and only a\n * sustained stream is batched.\n *\n * Set to 0 to disable coalescing and send every frame on its own.\n */\n batchWindowMs?: number;\n };\n\n/**\n * What `realtime.bus` accepts: a built-in transport by name, or any\n * {@link ChannelBus} instance.\n *\n * ```typescript\n * realtime: { bus: { type: \"postgres\" } } // shipped\n * realtime: { bus: new MyRedisChannelBus(url) } // a separate package, or your own\n * ```\n */\nexport type ChannelBusSetting = ChannelBusConfig | ChannelBus;\n\n/**\n * Whether `setting` is an already-constructed transport rather than a request\n * for a built-in one.\n *\n * Structural rather than nominal so that an instance from a *different copy* of\n * `@rebasepro/types` — an entirely normal outcome of a separately versioned\n * transport package — is still recognised.\n */\nexport function isChannelBusInstance(setting: ChannelBusSetting | undefined): setting is ChannelBus {\n return typeof (setting as ChannelBus | undefined)?.publish === \"function\";\n}\n","/**\n * Email normalization — one implementation, because the database enforces it.\n *\n * `ensureAuthTablesExist` puts a `UNIQUE INDEX ON users (lower(email))` on the\n * auth table. That index decides what \"the same address\" means, and it does not\n * trim: to Postgres, `' foo@bar.com'` and `'foo@bar.com'` are two addresses and\n * both may exist. So every write that reaches the column has to agree with\n * every read, exactly, or the two disagree in the one direction that matters —\n * a row that exists and cannot be found.\n *\n * That is not hypothetical. The lookup path trimmed and the admin create paths\n * did not, so a user created through `POST /api/data/users` or\n * `POST /api/auth/admin/users` with a stray space was stored untrimmed,\n * survived the unique index alongside the real address, and was unreachable by\n * login forever after. The HTTP auth routes were unaffected only because Zod's\n * `.email()` happens to reject surrounding whitespace — a guard on a different\n * layer, for a different reason, that the admin paths do not sit behind.\n *\n * It lives in `common` because `server`, `server-postgres` and `server-mongo`\n * all write this column and must agree exactly, and `common` is the only\n * package all three already depend on.\n */\n\n/**\n * Canonical form of an email address: trimmed, lower-cased.\n *\n * Non-strings pass through untouched, so this is safe to apply to a value out\n * of a partial update payload whose type is not known yet.\n */\nexport function normalizeEmail<T>(email: T): T | string {\n return typeof email === \"string\" ? email.trim().toLowerCase() : email;\n}\n","import { Pool } from \"pg\";\nimport { drizzle } from \"drizzle-orm/node-postgres\";\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { logger } from \"@rebasepro/server\";\nimport { guardPoolAgainstDirtyRelease, pinSearchPath, cappedPoolMax } from \"./connection\";\n\nexport class DatabasePoolManager {\n private pools: Map<string, Pool> = new Map();\n private drizzleInstances: Map<string, NodePgDatabase> = new Map();\n public readonly defaultDatabaseName: string;\n private readonly rootConnectionString: string;\n\n constructor(adminConnectionString: string) {\n this.rootConnectionString = adminConnectionString;\n try {\n const url = new URL(adminConnectionString);\n this.defaultDatabaseName = url.pathname.slice(1);\n } catch (e) {\n throw new Error(`Invalid adminConnectionString provided: ${e}`);\n }\n }\n\n public getDrizzle(databaseName: string): NodePgDatabase<Record<string, never>> {\n const existing = this.drizzleInstances.get(databaseName);\n if (existing) {\n return existing;\n }\n\n const pool = this.getPool(databaseName);\n const db = drizzle(pool);\n this.drizzleInstances.set(databaseName, db);\n return db;\n }\n\n public getPool(databaseName: string): Pool {\n if (this.pools.has(databaseName)) {\n return this.pools.get(databaseName)!;\n }\n\n const url = new URL(this.rootConnectionString);\n url.pathname = `/${databaseName}`;\n\n const pool = new Pool({\n // Same pin as the primary pool: these are branch/multi-database\n // connections to the *same* server, so they inherit the same\n // `\"$user\"` hazard. See `pinSearchPath`.\n connectionString: pinSearchPath(url.toString()),\n // Capped by REBASE_DB_POOL_MAX, which the managed development\n // database sets to 1: PGlite multiplexes onto a single session and\n // overlapping transactions deadlock there.\n max: cappedPoolMax(10),\n idleTimeoutMillis: 10000, // Reduced from 30000 for aggressive cleanup\n allowExitOnIdle: true // Prevent idle clients from hanging the Node.js process\n });\n\n // Prevent idle client errors from crashing the Node.js process\n pool.on(\"error\", (err) => {\n logger.error(`[DatabasePoolManager] Unexpected error on idle client for db ${databaseName}`, { error: err });\n });\n guardPoolAgainstDirtyRelease(pool, `pg-pool:${databaseName}`);\n\n this.pools.set(databaseName, pool);\n return pool;\n }\n\n /**\n * Disconnect and remove the pool for a specific database.\n * Required before `CREATE DATABASE ... TEMPLATE` or `DROP DATABASE`,\n * which need exclusive access to the target database.\n */\n public async disconnectDatabase(databaseName: string): Promise<void> {\n const pool = this.pools.get(databaseName);\n if (pool) {\n await pool.end();\n this.pools.delete(databaseName);\n this.drizzleInstances.delete(databaseName);\n }\n }\n\n /** Check if a pool exists for a given database name. */\n public hasPool(databaseName: string): boolean {\n return this.pools.has(databaseName);\n }\n\n public async shutdown(): Promise<void> {\n const promises = [];\n for (const [dbName, pool] of this.pools.entries()) {\n logger.info(`[DatabasePoolManager] Shutting down pool for ${dbName}`);\n promises.push(pool.end());\n }\n await Promise.all(promises);\n this.pools.clear();\n this.drizzleInstances.clear();\n }\n}\n","import { pgSchema, pgTable, uuid, timestamp, boolean, jsonb, text, unique, index, integer, bigint } from \"drizzle-orm/pg-core\";\nimport { relations } from \"drizzle-orm\";\n\n/**\n * Factory function to dynamically create the auth tables bound to the specified schema names.\n *\n * This module builds queries; it does not create tables. `ensureAuthTablesExist`\n * owns the DDL, which makes everything here a *claim* about a database it cannot\n * enforce — and the claims drifted. Every column below was declared\n * `varchar(n)` while the DDL created it as `TEXT`: `user_agent` as varchar(500),\n * `ip_address` as varchar(45), `secret_encrypted` as varchar(500), every\n * `token_hash` as varchar(255). None of it was true of any database this\n * framework ever provisioned. Harmless at runtime — drizzle does not enforce a\n * length client-side, so the widths only ever misled the next reader — but a\n * schema module that describes columns that do not exist is worse than no\n * schema module. They are `text` here now because they are TEXT there.\n */\nexport function createAuthSchema(usersSchemaName = \"rebase\") {\n const usersSchema = usersSchemaName === \"public\" ? null : pgSchema(usersSchemaName);\n\n const tableCreator = (usersSchema ? usersSchema.table.bind(usersSchema) : pgTable) as typeof pgTable;\n const usersTableCreator = tableCreator;\n\n /**\n * Users table - stores both email/password and OAuth users\n */\n const users = usersTableCreator(\"users\", {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n email: text(\"email\").notNull().unique(),\n passwordHash: text(\"password_hash\"), // NULL for OAuth-only users\n displayName: text(\"display_name\"),\n photoUrl: text(\"photo_url\"),\n emailVerified: boolean(\"email_verified\").default(false).notNull(),\n emailVerificationToken: text(\"email_verification_token\"),\n emailVerificationSentAt: timestamp(\"email_verification_sent_at\"),\n isAnonymous: boolean(\"is_anonymous\").default(false).notNull(),\n roles: text(\"roles\").array().default([]).notNull(),\n metadata: jsonb(\"metadata\").$type<Record<string, unknown>>().default({}).notNull(),\n /**\n * Sessions that began before this instant are dead, whatever tokens\n * they still hold. Password resets and admin revocations stamp it.\n *\n * Deleting the user's refresh-token rows (which we also do) is not\n * sufficient on its own: a request already in flight can insert a\n * freshly rotated row microseconds after the delete and survive it.\n * This timestamp cannot be outrun that way — it is checked against\n * `refresh_tokens.session_started_at`, which rotation carries forward.\n */\n tokensValidAfter: timestamp(\"tokens_valid_after\"),\n createdAt: timestamp(\"created_at\").defaultNow().notNull(),\n updatedAt: timestamp(\"updated_at\").defaultNow().notNull()\n });\n\n\n /**\n * Refresh tokens for long-lived sessions.\n *\n * A row is one token, not one device. Every token minted from the same\n * sign-in shares a `sessionId`, and rotation ADDS a row rather than\n * replacing one: the superseded token stays on file, flagged `revoked`\n * with a `rotatedAt` stamp. That record is what lets the refresh endpoint\n * tell a client replaying a token it never got an answer for (a response\n * lost to a redeploy, a second tab racing on boot) apart from a stranger\n * presenting a token that was never issued. Deleting the old row on sight\n * — the previous behaviour — made those two cases indistinguishable, and\n * the legitimate one is overwhelmingly the common one.\n *\n * There is deliberately NO unique constraint on (uid, user_agent,\n * ip_address). Keying a session on the IP meant one row per \"device\",\n * so a second browser profile behind the same NAT silently evicted the\n * first, and a phone changing networks orphaned a row on every hop.\n * User agent and IP are descriptive metadata for the sessions list;\n * `sessionId` is the identity.\n */\n const refreshTokens = tableCreator(\"refresh_tokens\", {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n uid: uuid(\"uid\").notNull().references(() => users.id, { onDelete: \"cascade\" }),\n sessionId: uuid(\"session_id\").defaultRandom().notNull(),\n tokenHash: text(\"token_hash\").notNull().unique(),\n expiresAt: timestamp(\"expires_at\").notNull(),\n revoked: boolean(\"revoked\").default(false).notNull(),\n rotatedAt: timestamp(\"rotated_at\"),\n /**\n * When the sign-in this token descends from happened — carried across\n * every rotation, unlike `createdAt`. `users.tokensValidAfter` is\n * compared against this, so a revocation cannot be outrun by a token\n * that rotates immediately after it.\n */\n sessionStartedAt: timestamp(\"session_started_at\").defaultNow().notNull(),\n /**\n * The assurance level the sign-in was established at — `aal2` only\n * where a second factor was actually presented. Carried across\n * rotations, because refresh is not a new authentication and has\n * nothing else to read the level from.\n */\n aal: text(\"aal\"),\n userAgent: text(\"user_agent\"),\n ipAddress: text(\"ip_address\"),\n createdAt: timestamp(\"created_at\").defaultNow().notNull()\n }, (table) => ({\n sessionIdx: index(\"idx_refresh_tokens_session\").on(table.sessionId)\n }));\n\n /**\n * Password reset tokens for forgot password flow\n */\n const passwordResetTokens = tableCreator(\"password_reset_tokens\", {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n uid: uuid(\"uid\").notNull().references(() => users.id, { onDelete: \"cascade\" }),\n tokenHash: text(\"token_hash\").notNull().unique(),\n expiresAt: timestamp(\"expires_at\").notNull(),\n usedAt: timestamp(\"used_at\"),\n createdAt: timestamp(\"created_at\").defaultNow().notNull()\n });\n\n /**\n * App config - key/value store for custom settings\n */\n const appConfig = tableCreator(\"app_config\", {\n key: text(\"key\").primaryKey(),\n value: jsonb(\"value\").notNull(),\n updatedAt: timestamp(\"updated_at\").defaultNow().notNull()\n });\n\n /**\n * User identities - maps external OAuth profiles back to local users\n */\n const userIdentities = tableCreator(\"user_identities\", {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n uid: uuid(\"uid\").notNull().references(() => users.id, { onDelete: \"cascade\" }),\n provider: text(\"provider\").notNull(), // e.g. 'google', 'linkedin'\n providerId: text(\"provider_id\").notNull(),\n profileData: jsonb(\"profile_data\"),\n createdAt: timestamp(\"created_at\").defaultNow().notNull(),\n updatedAt: timestamp(\"updated_at\").defaultNow().notNull()\n }, (table) => ({\n uniqueProviderId: unique(\"unique_provider_id\").on(table.provider, table.providerId)\n }));\n\n /**\n * MFA factors table - stores enrolled MFA methods\n */\n const mfaFactors = tableCreator(\"mfa_factors\", {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n uid: uuid(\"uid\").notNull().references(() => users.id, { onDelete: \"cascade\" }),\n factorType: text(\"factor_type\").notNull(), // 'totp'\n secretEncrypted: text(\"secret_encrypted\").notNull(),\n friendlyName: text(\"friendly_name\"),\n verified: boolean(\"verified\").default(false).notNull(),\n /**\n * The highest TOTP time step ever accepted for this factor. RFC 6238\n * §5.2 forbids accepting an OTP twice, and the ±1 step window that\n * exists for clock drift is also a 90-second replay window: without\n * this, one observed code buys a fresh session for a minute and a half.\n */\n lastUsedCounter: bigint(\"last_used_counter\", { mode: \"number\" }),\n createdAt: timestamp(\"created_at\").defaultNow().notNull(),\n updatedAt: timestamp(\"updated_at\").defaultNow().notNull()\n });\n\n /**\n * MFA challenges table - tracks active MFA verification attempts\n */\n const mfaChallenges = tableCreator(\"mfa_challenges\", {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n factorId: uuid(\"factor_id\").notNull().references(() => mfaFactors.id, { onDelete: \"cascade\" }),\n createdAt: timestamp(\"created_at\").defaultNow().notNull(),\n verifiedAt: timestamp(\"verified_at\"),\n ipAddress: text(\"ip_address\"),\n /** Failed guesses recorded against this challenge; bounded by the route. */\n attempts: integer(\"attempts\").default(0).notNull(),\n expiresAt: timestamp(\"expires_at\").notNull()\n });\n\n /**\n * Recovery codes table - backup codes for MFA\n */\n const recoveryCodes = tableCreator(\"recovery_codes\", {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n uid: uuid(\"uid\").notNull().references(() => users.id, { onDelete: \"cascade\" }),\n codeHash: text(\"code_hash\").notNull(),\n usedAt: timestamp(\"used_at\"),\n createdAt: timestamp(\"created_at\").defaultNow().notNull()\n });\n\n /**\n * Magic link tokens for passwordless email login\n */\n const magicLinkTokens = tableCreator(\"magic_link_tokens\", {\n id: uuid(\"id\").defaultRandom().primaryKey(),\n uid: uuid(\"uid\").notNull().references(() => users.id, { onDelete: \"cascade\" }),\n tokenHash: text(\"token_hash\").notNull().unique(),\n expiresAt: timestamp(\"expires_at\").notNull(),\n usedAt: timestamp(\"used_at\"),\n createdAt: timestamp(\"created_at\").defaultNow().notNull()\n });\n\n return {\n usersSchema,\n users,\n refreshTokens,\n passwordResetTokens,\n appConfig,\n userIdentities,\n mfaFactors,\n mfaChallenges,\n recoveryCodes,\n magicLinkTokens\n };\n}\n\n// Instantiate default schema and tables using the default \"rebase\" schema\nconst defaultAuthSchema = createAuthSchema(\"rebase\");\n\nexport const usersSchema = defaultAuthSchema.usersSchema;\n\nexport const users = defaultAuthSchema.users;\nexport const refreshTokens = defaultAuthSchema.refreshTokens;\nexport const passwordResetTokens = defaultAuthSchema.passwordResetTokens;\nexport const appConfig = defaultAuthSchema.appConfig;\nexport const userIdentities = defaultAuthSchema.userIdentities;\nexport const mfaFactors = defaultAuthSchema.mfaFactors;\nexport const mfaChallenges = defaultAuthSchema.mfaChallenges;\nexport const recoveryCodes = defaultAuthSchema.recoveryCodes;\nexport const magicLinkTokens = defaultAuthSchema.magicLinkTokens;\n\n// Relations\nexport const usersRelations = relations(users, ({ many }) => ({\n refreshTokens: many(refreshTokens),\n passwordResetTokens: many(passwordResetTokens),\n userIdentities: many(userIdentities),\n mfaFactors: many(mfaFactors),\n recoveryCodes: many(recoveryCodes),\n magicLinkTokens: many(magicLinkTokens)\n}));\n\nexport const refreshTokensRelations = relations(refreshTokens, ({ one }) => ({\n user: one(users, {\n fields: [refreshTokens.uid],\n references: [users.id]\n })\n}));\n\nexport const passwordResetTokensRelations = relations(passwordResetTokens, ({ one }) => ({\n user: one(users, {\n fields: [passwordResetTokens.uid],\n references: [users.id]\n })\n}));\n\nexport const userIdentitiesRelations = relations(userIdentities, ({ one }) => ({\n user: one(users, {\n fields: [userIdentities.uid],\n references: [users.id]\n })\n}));\n\nexport const mfaFactorsRelations = relations(mfaFactors, ({ one, many }) => ({\n user: one(users, {\n fields: [mfaFactors.uid],\n references: [users.id]\n }),\n challenges: many(mfaChallenges)\n}));\n\nexport const mfaChallengesRelations = relations(mfaChallenges, ({ one }) => ({\n factor: one(mfaFactors, {\n fields: [mfaChallenges.factorId],\n references: [mfaFactors.id]\n })\n}));\n\nexport const recoveryCodesRelations = relations(recoveryCodes, ({ one }) => ({\n user: one(users, {\n fields: [recoveryCodes.uid],\n references: [users.id]\n })\n}));\n\nexport const magicLinkTokensRelations = relations(magicLinkTokens, ({ one }) => ({\n user: one(users, {\n fields: [magicLinkTokens.uid],\n references: [users.id]\n })\n}));\n\n// Type exports\nexport type User = typeof users.$inferSelect;\nexport type NewUser = typeof users.$inferInsert;\nexport type RefreshToken = typeof refreshTokens.$inferSelect;\nexport type PasswordResetToken = typeof passwordResetTokens.$inferSelect;\nexport type AppConfig = typeof appConfig.$inferSelect;\nexport type UserIdentity = typeof userIdentities.$inferSelect;\nexport type NewUserIdentity = typeof userIdentities.$inferInsert;\nexport type MfaFactorRow = typeof mfaFactors.$inferSelect;\nexport type MfaChallengeRow = typeof mfaChallenges.$inferSelect;\nexport type RecoveryCodeRow = typeof recoveryCodes.$inferSelect;\nexport type MagicLinkToken = typeof magicLinkTokens.$inferSelect;\n","/**\n * Terminal output for the `rebase db|schema|doctor` commands.\n *\n * These commands used to write every line through `logger`, and that is a\n * category error with three separate consequences:\n *\n * - `logger` prefixes each line with its own level, so a box-drawn report\n * arrived as `ℹ️ [INFO] ┌─ ✗ Missing Column ───` and the frame no longer\n * lined up with anything;\n * - `logger` is gated by `LOG_LEVEL`, which ships in the scaffold's own\n * `.env.example` — a developer who quietened their dev server with\n * `LOG_LEVEL=warn` got a `rebase db push` that printed almost nothing and\n * still exited non-zero, indistinguishable from a crash;\n * - under `NODE_ENV=production` `logger` emits JSON, so the whole report\n * became log records with the chalk escape codes embedded in them.\n *\n * A CLI's report *is* its return value. It goes to the terminal unconditionally\n * and unadorned. `packages/cli` has always written its output this way; this is\n * the same three functions for the plugin CLI that `rebase` delegates to.\n *\n * `logger` still belongs in this package's *runtime* — a request handler has no\n * terminal and its lines want levels, timestamps and redaction. The rule is the\n * caller, not the severity: anything a developer reads because they typed a\n * command goes here, anything a server emits while running goes to `logger`.\n *\n * Errors and warnings go to stderr so `rebase db push > plan.txt` keeps the\n * diagnosis on the terminal where it is readable.\n */\n\n/** One line of human-facing output on stdout. */\nexport const out = (line = \"\"): void => {\n console.log(line);\n};\n\n/** One line of human-facing warning output on stderr. */\nexport const outWarn = (line = \"\"): void => {\n console.warn(line);\n};\n\n/** One line of human-facing error output on stderr. */\nexport const outError = (line = \"\"): void => {\n console.error(line);\n};\n","import { promises as fsPromises } from \"fs\";\nimport * as fs from \"fs\";\nimport path from \"path\";\nimport { pathToFileURL } from \"url\";\nimport { generateSchema } from \"./generate-drizzle-schema-logic\";\nimport { CollectionConfig } from \"@rebasepro/types\";\nimport { loadCollectionsFromDirectory } from \"@rebasepro/server\";\nimport { out, outError } from \"../cli-output\";\n\n\n// --- Helper Functions ---\n\nconst formatTerminalText = (text: string, options: {\n bold?: boolean;\n backgroundColor?: \"blue\" | \"green\" | \"red\" | \"yellow\" | \"cyan\" | \"magenta\";\n textColor?: \"white\" | \"black\" | \"red\" | \"green\" | \"yellow\" | \"blue\" | \"magenta\" | \"cyan\";\n} = {}): string => {\n let codes = \"\";\n if (options.bold) codes += \"\\x1b[1m\";\n if (options.backgroundColor) {\n const bgColors = {\n blue: \"\\x1b[44m\",\n green: \"\\x1b[42m\",\n red: \"\\x1b[41m\",\n yellow: \"\\x1b[43m\",\n cyan: \"\\x1b[46m\",\n magenta: \"\\x1b[45m\"\n } as const;\n codes += bgColors[options.backgroundColor];\n }\n if (options.textColor) {\n const textColors = {\n white: \"\\x1b[37m\",\n black: \"\\x1b[30m\",\n red: \"\\x1b[31m\",\n green: \"\\x1b[32m\",\n yellow: \"\\x1b[33m\",\n blue: \"\\x1b[34m\",\n magenta: \"\\x1b[35m\",\n cyan: \"\\x1b[36m\"\n } as const;\n codes += textColors[options.textColor];\n }\n return `${codes}${text}\\x1b[0m`;\n};\n\n// --- Execution and Watch Logic ---\n\nconst runGeneration = async (collectionsFilePath?: string, outputPath?: string) => {\n try {\n if (!collectionsFilePath) {\n outError(\"Error: No collections file path provided. Skipping schema generation.\");\n return;\n }\n\n const resolvedPath = path.resolve(collectionsFilePath);\n\n // Shared with the runtime and the doctor: what gets generated here must\n // be exactly what the server serves, including directory-level defaults.\n let collections: CollectionConfig[] = await loadCollectionsFromDirectory(resolvedPath);\n\n\n // If collections directory is empty but exists, or failed to find any, we still want to inject defaults\n if (!collections || !Array.isArray(collections)) {\n collections = [];\n }\n\n\n // Sort collections by slug alphabetically to ensure deterministic schema generation\n collections.sort((a, b) => a.slug.localeCompare(b.slug));\n\n const schemaContent = await generateSchema(collections);\n\n if (outputPath) {\n const outputDir = path.dirname(outputPath);\n await fsPromises.mkdir(outputDir, { recursive: true });\n await fsPromises.writeFile(outputPath, schemaContent);\n out(`✅ Drizzle schema generated successfully at ${outputPath}`);\n } else {\n out(\"✅ Drizzle schema generated successfully.\");\n out(String(schemaContent));\n }\n\n out(`You can now run ${formatTerminalText(\"rebase db generate\", {\n bold: true,\n backgroundColor: \"blue\",\n textColor: \"black\"\n })} to generate the SQL migration files.`);\n\n } catch (error) {\n outError(`Error generating schema: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);\n }\n};\n\nconst main = async () => {\n const collectionsFilePathArg = process.argv.find(arg => arg.startsWith(\"--collections=\"));\n const collectionsFilePath = collectionsFilePathArg ? collectionsFilePathArg.split(\"=\")[1] : process.argv[2];\n\n const outputPathArg = process.argv.find(arg => arg.startsWith(\"--output=\"));\n const outputPath = outputPathArg ? outputPathArg.split(\"=\")[1] : undefined;\n\n const watch = process.argv.includes(\"--watch\");\n\n if (!collectionsFilePath) {\n out(\"Usage: ts-node generate-drizzle-schema.ts <path-to-collections-file> [--output <path-to-output-file>] [--watch]\");\n return;\n }\n\n const resolvedPath = path.resolve(process.cwd(), collectionsFilePath);\n const resolvedOutputPath = outputPath ? path.resolve(process.cwd(), outputPath) : undefined;\n\n if (watch) {\n out(`Watching for changes in ${resolvedPath}...`);\n // Imported here rather than at module scope, and this is not a style\n // choice: chokidar is needed only by `--watch`, which is a\n // schema-authoring path that never runs inside the runtime image. A\n // top-level import puts it on the boot path of the published driver\n // bundle, and the image installs a hand-listed set of runtime\n // dependencies that does not include it — so the whole driver failed to\n // load with \"Cannot find package 'chokidar'\", and every self-hosted\n // container answered 500 with a stack trace about a file watcher.\n //\n // Same reasoning the image already applies to @ariga/atlas: an\n // authoring-only dependency does not belong on a boot path.\n const { default: chokidar } = await import(\"chokidar\");\n const watcher = chokidar.watch(resolvedPath, {\n persistent: true,\n ignoreInitial: false\n });\n\n watcher.on(\"all\", (event, filePath) => {\n out(`[${event}] ${filePath}. Regenerating schema...`);\n runGeneration(resolvedPath, resolvedOutputPath);\n });\n } else {\n runGeneration(resolvedPath, resolvedOutputPath);\n }\n};\n\n// This check ensures the script only runs when executed directly\nif (import.meta.url.endsWith(process.argv[1])) {\n main();\n}\n","import { CollectionConfig, ResolvedRelation, isManyToMany } from \"@rebasepro/types\";\nimport { resolveCollectionRelations } from \"@rebasepro/common\";\n\nimport { PostgresCollectionRegistry } from \"../../collections/PostgresCollectionRegistry\";\n\n/**\n * One end of a many-to-many, as seen from the junction table.\n *\n * A junction table is not a collection, so nothing in the registry maps it to\n * one — which is why a change to it was invisible to change capture. But its\n * rows are exactly the contents of a parent's child list, so a write to it is a\n * change to `<parentSlug>/<sourceId>/<relationKey>` and to nothing else.\n */\nexport interface JunctionLink {\n schema: string;\n /** The junction table itself, e.g. `posts_tags`. */\n table: string;\n /** The collection whose relation this is, e.g. `posts`. */\n parentCollection: CollectionConfig;\n /** The relation's key — the path segment a child list is addressed by. */\n relationKey: string;\n /** Junction column holding the parent's id. */\n sourceColumn: string;\n /** Junction column holding the target's id. */\n targetColumn: string;\n}\n\n/**\n * Every junction table reachable from a registered collection, once per\n * relation that uses it.\n *\n * A junction is listed once per *direction* when both sides declare it, because\n * each direction addresses a different child list: `posts/1/tags` and\n * `tags/t/posts` both change when one link is written.\n */\nexport function collectJunctionLinks(registry: PostgresCollectionRegistry): JunctionLink[] {\n const links: JunctionLink[] = [];\n const seen = new Set<string>();\n\n for (const collection of registry.getCollections()) {\n let relations: Record<string, ResolvedRelation>;\n try {\n relations = resolveCollectionRelations(collection);\n } catch {\n // A collection whose relations cannot be resolved (an unresolvable\n // target, typically mid-migration) simply contributes none.\n continue;\n }\n\n for (const [relationKey, relation] of Object.entries(relations)) {\n if (!isManyToMany(relation)) continue;\n const through = relation.through;\n\n // Same relation registered under both its canonical name and the\n // declaring property key would otherwise notify the same path twice.\n const key = `${collection.slug}::${relationKey}::${through.table}`;\n if (seen.has(key)) continue;\n seen.add(key);\n\n links.push({\n schema: (collection as { schema?: string }).schema ?? \"public\",\n table: through.table,\n parentCollection: collection,\n relationKey,\n sourceColumn: through.sourceColumn,\n targetColumn: through.targetColumn\n });\n }\n }\n\n return links;\n}\n\n/**\n * Index {@link collectJunctionLinks} by table, under both the qualified and the\n * bare name — a change event carries whatever the trigger reports, and a\n * collection need not declare a schema.\n */\nexport function buildJunctionLinkMap(registry: PostgresCollectionRegistry): Map<string, JunctionLink[]> {\n const map = new Map<string, JunctionLink[]>();\n\n for (const link of collectJunctionLinks(registry)) {\n for (const key of [`${link.schema}.${link.table}`, link.table]) {\n const existing = map.get(key);\n if (existing) existing.push(link);\n else map.set(key, [link]);\n }\n }\n\n return map;\n}\n","import { logger } from \"@rebasepro/server\";\nimport type { RawSqlRunner } from \"../../security/rls-enforcement\";\n\n/**\n * Trigger-based Change Data Capture (CDC).\n *\n * The preferred CDC source is the write-ahead log (logical replication), which\n * — like Supabase Realtime — sees *every* commit regardless of how it was made.\n * When logical replication is unavailable (managed Postgres without\n * `wal_level=logical`, no replication privilege, no `REPLICA IDENTITY`), this\n * trigger-based fallback provides the same guarantee at the row level:\n *\n * AFTER INSERT/UPDATE/DELETE trigger → pg_notify('rebase_cdc', payload)\n *\n * A single dedicated LISTEN client per backend instance consumes the channel\n * (see {@link CdcListener}) and feeds the change into the existing\n * `RealtimeService.notifyUpdate` pipeline, so subscribers see the change no\n * matter what wrote it — psql, a cron in another service, raw Drizzle/SQL, or\n * the Studio SQL editor.\n *\n * Provisioning runs from the framework's own bootstrap as the owner (server)\n * context, alongside the RLS role provisioning. It is idempotent.\n */\n\n/** Postgres NOTIFY channel carrying database-level change events. */\nexport const CDC_CHANNEL = \"rebase_cdc\";\n\n/** Schema-qualified name of the generic trigger function. */\nexport const CDC_TRIGGER_FUNCTION = \"rebase.rebase_cdc_notify\";\n\n/** Name of the per-table trigger (unqualified — triggers are namespaced by table). */\nexport const CDC_TRIGGER_NAME = \"rebase_cdc_trigger\";\n\n/**\n * pg_notify hard-caps payloads at 8000 bytes and *aborts the triggering\n * statement* if the limit is exceeded. We stay comfortably under it and, for\n * wide rows, fall back to an identity-only payload so CDC can never break a\n * write. 7900 leaves headroom for the JSON envelope keys.\n */\nconst MAX_NOTIFY_BYTES = 7900;\n\nconst quoteIdent = (name: string): string => `\"${name.replace(/\"/g, \"\\\"\\\"\")}\"`;\nconst quoteLiteral = (value: string): string => `'${value.replace(/'/g, \"''\")}'`;\n\n/**\n * SQL that (re)creates the generic CDC trigger function. Safe to run repeatedly:\n * `CREATE OR REPLACE` updates in place without dropping dependent triggers.\n *\n * The function emits `{ schema, table, op, row }`. The `row` is the full changed\n * tuple (NEW for insert/update, OLD for delete) so the consumer can route it to\n * a collection and extract the primary key. It is *not* trusted for delivery:\n * the consumer marks the row invalidated and each subscriber re-reads it under\n * its own RLS context, so a subscriber never receives a row it cannot read.\n */\nexport function buildCdcFunctionSql(): string {\n return `\nCREATE SCHEMA IF NOT EXISTS rebase;\n\nCREATE OR REPLACE FUNCTION ${CDC_TRIGGER_FUNCTION}() RETURNS trigger\nLANGUAGE plpgsql AS $rebase_cdc$\nDECLARE\n rec jsonb;\n payload text;\nBEGIN\n IF (TG_OP = 'DELETE') THEN\n rec := to_jsonb(OLD);\n ELSE\n rec := to_jsonb(NEW);\n END IF;\n\n payload := json_build_object(\n 'schema', TG_TABLE_SCHEMA,\n 'table', TG_TABLE_NAME,\n 'op', TG_OP,\n 'row', rec\n )::text;\n\n -- Never let CDC abort the write: if the full row overflows the pg_notify\n -- 8000-byte cap, emit an identity-only payload the consumer can still route\n -- (and refetch the authoritative row from).\n IF (octet_length(payload) > ${MAX_NOTIFY_BYTES}) THEN\n payload := json_build_object(\n 'schema', TG_TABLE_SCHEMA,\n 'table', TG_TABLE_NAME,\n 'op', TG_OP,\n 'row', CASE WHEN rec ? 'id' THEN jsonb_build_object('id', rec->'id') ELSE '{}'::jsonb END,\n 'truncated', true\n )::text;\n END IF;\n\n PERFORM pg_notify(${quoteLiteral(CDC_CHANNEL)}, payload);\n RETURN NULL;\nEND;\n$rebase_cdc$;\n`.trim();\n}\n\n/**\n * SQL that (re)attaches the CDC trigger to a single table. `DROP ... IF EXISTS`\n * before `CREATE` keeps it idempotent and picks up any function signature change.\n */\nexport function buildCdcTriggerSql(schema: string, table: string): string {\n const qualified = `${quoteIdent(schema)}.${quoteIdent(table)}`;\n return (\n `DROP TRIGGER IF EXISTS ${quoteIdent(CDC_TRIGGER_NAME)} ON ${qualified};\\n` +\n `CREATE TRIGGER ${quoteIdent(CDC_TRIGGER_NAME)} ` +\n `AFTER INSERT OR UPDATE OR DELETE ON ${qualified} ` +\n `FOR EACH ROW EXECUTE FUNCTION ${CDC_TRIGGER_FUNCTION}();`\n );\n}\n\nexport interface CdcTableRef {\n schema: string;\n table: string;\n}\n\nexport interface ProvisionResult {\n /** Tables the trigger was successfully attached to. */\n installed: CdcTableRef[];\n /** Tables that could not be provisioned (e.g. not yet migrated), with the error. */\n skipped: Array<CdcTableRef & { reason: string }>;\n}\n\n/**\n * Idempotently install the CDC trigger function and per-table triggers.\n *\n * Runs as the owner (server) connection at bootstrap. A table that does not yet\n * exist in the database (schema drift) is skipped with a warning rather than\n * aborting the whole install, so one un-migrated collection cannot disable CDC\n * for the rest.\n */\nexport async function provisionTriggerCdc(\n run: RawSqlRunner,\n tables: CdcTableRef[]\n): Promise<ProvisionResult> {\n // 1. The shared trigger function (once).\n await run(buildCdcFunctionSql());\n\n // 2. One trigger per managed table. De-duplicate identical refs.\n const seen = new Set<string>();\n const installed: CdcTableRef[] = [];\n const skipped: ProvisionResult[\"skipped\"] = [];\n\n for (const ref of tables) {\n const key = `${ref.schema}.${ref.table}`;\n if (seen.has(key)) continue;\n seen.add(key);\n\n try {\n await run(buildCdcTriggerSql(ref.schema, ref.table));\n installed.push(ref);\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n skipped.push({ ...ref, reason });\n logger.warn(\n `⚠️ [CDC] Could not attach change-capture trigger to \"${key}\" — ` +\n `is the table migrated? Writes to it won't emit database-level events.`,\n { detail: reason }\n );\n }\n }\n\n // Wiring detail. The single `Realtime source = …` line in the\n // bootstrapper is the fact a developer acts on; how many triggers it\n // took is for a diagnosis, and the skipped-table warning above still\n // fires on its own.\n logger.debug(\n `📡 [CDC] Trigger-based change capture provisioned on ${installed.length} table(s)` +\n (skipped.length ? ` (${skipped.length} skipped)` : \"\") + \".\"\n );\n\n return { installed, skipped };\n}\n","/**\n * A dedicated, self-healing Postgres `LISTEN` connection.\n *\n * Every cross-instance feature in the backend needs the same thing: one\n * connection *outside* the Drizzle pool that stays open, holds a `LISTEN`, and\n * comes back on its own after the database or the network drops it. CDC needed\n * it first; the channel bus needs it too. This is that connection, with the one\n * behaviour that matters to callers preserved: the **first** connect is\n * validated and rethrown, so a caller can fall back to a different strategy,\n * while every later drop is repaired quietly in the background.\n *\n * `LISTEN` is session state, so this connection must not go through a\n * transaction-mode pooler (PgBouncer): give it the direct database URL.\n */\n\nimport { Client as PgClient } from \"pg\";\nimport { logger } from \"@rebasepro/server\";\n\nexport interface PgNotifyListenerOptions {\n /** Direct Postgres connection string (must bypass a transaction-mode pooler). */\n connectionString: string;\n /** NOTIFY channel to LISTEN on. Must be a plain identifier — it is interpolated. */\n channel: string;\n /** Called for every notification payload received. */\n onPayload: (payload: string) => void | Promise<void>;\n /** Prefix for log lines, e.g. `\"[CDC]\"`. */\n logLabel: string;\n /** Delay before a reconnect attempt. */\n reconnectDelayMs?: number;\n}\n\nconst DEFAULT_RECONNECT_DELAY_MS = 3000;\n/** Guards the identifier interpolated into `LISTEN`. */\nconst SAFE_CHANNEL = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\nexport class PgNotifyListener {\n private client?: PgClient;\n private running = false;\n private reconnectTimer?: ReturnType<typeof setTimeout>;\n\n constructor(private readonly options: PgNotifyListenerOptions) {\n if (!SAFE_CHANNEL.test(options.channel)) {\n throw new Error(`Unsafe NOTIFY channel name \"${options.channel}\" — expected a plain SQL identifier.`);\n }\n }\n\n /** Whether the listener is meant to be connected right now. */\n get active(): boolean {\n return this.running;\n }\n\n /**\n * Connect and begin listening. Idempotent.\n *\n * Rejects if the *initial* connection or `LISTEN` fails, leaving the\n * listener stopped — callers use that to degrade deliberately instead of\n * running blind against a channel nothing is delivering.\n */\n async start(): Promise<void> {\n if (this.running) return;\n this.running = true;\n try {\n await this.connect({ initial: true });\n } catch (err) {\n this.running = false;\n throw err;\n }\n }\n\n /** Stop listening and release the connection. Idempotent. */\n async stop(): Promise<void> {\n this.running = false;\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = undefined;\n }\n if (this.client) {\n try {\n await this.client.end();\n } catch { /* ignore close errors */ }\n this.client = undefined;\n }\n }\n\n private async connect({ initial = false }: { initial?: boolean } = {}): Promise<void> {\n const { connectionString, channel, onPayload, logLabel } = this.options;\n // Held here rather than only inside the `try` so the failure path can\n // still reach it: everything below `connect()` can throw, and until\n // `this.client` is assigned nothing else in this class knows the\n // connection exists. Left unreleased it stays open on the server while\n // `scheduleReconnect` opens another — one leaked backend per attempt,\n // every few seconds, for as long as the failure lasts.\n let pending: PgClient | undefined;\n try {\n const client = new PgClient({ connectionString });\n pending = client;\n\n client.on(\"error\", (err) => {\n logger.error(`❌ ${logLabel} LISTEN client error`, { detail: err.message });\n this.scheduleReconnect();\n });\n\n client.on(\"end\", () => {\n if (this.running) {\n logger.warn(`⚠️ ${logLabel} LISTEN client disconnected unexpectedly.`);\n this.scheduleReconnect();\n }\n });\n\n client.on(\"notification\", (msg) => {\n if (!msg.payload) return;\n // A handler rejection must never surface as an unhandled\n // rejection inside the pg client's event emitter.\n Promise.resolve(onPayload(msg.payload)).catch((err) =>\n logger.error(`❌ ${logLabel} Error handling notification`, { error: err })\n );\n });\n\n await client.connect();\n await client.query(`LISTEN ${channel}`);\n this.client = client;\n // Adopted: `stop()` and `scheduleReconnect` will close it now.\n pending = undefined;\n logger.debug(`📡 ${logLabel} Listening on channel \"${channel}\".`);\n } catch (err) {\n // Never adopted, so nothing else will ever close it.\n if (pending) {\n try { await pending.end(); } catch { /* already dead */ }\n }\n // Surface the initial failure so callers can choose to fall back;\n // for reconnects, keep retrying quietly in the background.\n if (initial) throw err;\n logger.error(`❌ ${logLabel} Failed to connect LISTEN client`, { error: err });\n this.scheduleReconnect();\n }\n }\n\n private scheduleReconnect(): void {\n if (!this.running || this.reconnectTimer) return;\n\n this.reconnectTimer = setTimeout(async () => {\n this.reconnectTimer = undefined;\n if (!this.running) return;\n if (this.client) {\n try { await this.client.end(); } catch { /* ignore */ }\n this.client = undefined;\n }\n await this.connect();\n }, this.options.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS);\n }\n}\n","import { logger } from \"@rebasepro/server\";\nimport { CDC_CHANNEL } from \"./trigger-cdc\";\nimport { PgNotifyListener } from \"../pg-notify-listener\";\n\n/**\n * A single database change captured by the CDC triggers and delivered over the\n * `rebase_cdc` NOTIFY channel.\n */\nexport interface CdcChangeEvent {\n schema: string;\n table: string;\n op: \"INSERT\" | \"UPDATE\" | \"DELETE\";\n /**\n * The changed tuple (NEW for insert/update, OLD for delete). May be a\n * partial identity-only object when the full row overflowed the pg_notify\n * size cap — see {@link truncated}.\n */\n row: Record<string, unknown>;\n /** True when the row was reduced to its identity because it was too large to notify. */\n truncated?: boolean;\n}\n\n/**\n * Parse a `rebase_cdc` NOTIFY payload. Returns `null` for anything malformed so\n * a single bad message can never crash the listener.\n */\nexport function parseCdcPayload(payload: string): CdcChangeEvent | null {\n let parsed: unknown;\n try {\n parsed = JSON.parse(payload);\n } catch {\n return null;\n }\n if (!parsed || typeof parsed !== \"object\") return null;\n\n const obj = parsed as Record<string, unknown>;\n const schema = typeof obj.schema === \"string\" ? obj.schema : undefined;\n const table = typeof obj.table === \"string\" ? obj.table : undefined;\n const op = obj.op;\n if (!schema || !table) return null;\n if (op !== \"INSERT\" && op !== \"UPDATE\" && op !== \"DELETE\") return null;\n\n const row = obj.row && typeof obj.row === \"object\" ? (obj.row as Record<string, unknown>) : {};\n\n return {\n schema,\n table,\n op,\n row,\n truncated: obj.truncated === true\n };\n}\n\n/**\n * Dedicated Postgres LISTEN client for database-level CDC.\n *\n * A {@link PgNotifyListener} — a connection outside the Drizzle pool that stays\n * open and repairs itself — plus the parsing that turns a `rebase_cdc` payload\n * into a change event. Each backend instance runs one, so every instance\n * observes every committed change regardless of which instance (or external\n * process) made the write.\n */\nexport class CdcListener {\n private readonly listener: PgNotifyListener;\n\n constructor(connectionString: string, onEvent: (event: CdcChangeEvent) => void | Promise<void>) {\n this.listener = new PgNotifyListener({\n connectionString,\n channel: CDC_CHANNEL,\n logLabel: \"[CDC]\",\n onPayload: (payload) => {\n const event = parseCdcPayload(payload);\n if (!event) {\n logger.warn(\"⚠️ [CDC] Dropping unparseable change notification.\");\n return;\n }\n return onEvent(event);\n }\n });\n }\n\n /**\n * Connect and begin listening. Idempotent.\n *\n * The **initial** connection is validated synchronously: if it cannot be\n * established (or `LISTEN` is refused), this rejects so callers — notably\n * `REALTIME_CDC=auto` — can detect an unusable connection and fall back to\n * app-level realtime. Once the initial connection succeeds, later drops\n * self-heal in the background.\n */\n async start(): Promise<void> {\n if (this.listener.active) {\n logger.warn(\"⚠️ [CDC] CdcListener.start() called but already running. Ignoring.\");\n return;\n }\n await this.listener.start();\n }\n\n /** Stop listening and release the connection. */\n async stop(): Promise<void> {\n await this.listener.stop();\n }\n}\n","/**\n * The server's DDL bootstrapper, over a Drizzle handle.\n *\n * `createDdlBootstrapper` in `@rebasepro/server` wants a plain\n * `(sql: string) => Promise<rows>`; the driver's internal stores hold a Drizzle\n * database. This is the adapter between them, and it exists so the retry policy\n * has exactly one definition. A second copy of the SQLSTATE list living in the\n * driver is how the two drift apart, and the drift is invisible: both versions\n * work perfectly on every single-instance deployment.\n */\nimport { sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { createDdlBootstrapper, type DdlBootstrapper } from \"@rebasepro/server\";\n\n/**\n * A {@link DdlBootstrapper} that runs its statements through `db.execute`.\n *\n * @param db the Drizzle handle the calling store already holds\n * @param scope log prefix identifying the caller, e.g. `\"channel-presence\"`\n */\nexport function drizzleDdlBootstrapper(\n db: NodePgDatabase<Record<string, unknown>>,\n scope: string\n): DdlBootstrapper {\n return createDdlBootstrapper(async (statement: string) => {\n // `sql.raw`, because everything reaching this path is DDL assembled from\n // identifiers that were validated before they got here — there is no\n // parameter to bind, and Drizzle's tagged template would treat the whole\n // statement as one.\n const result = await db.execute(sql.raw(statement));\n return (result as unknown as { rows?: Record<string, unknown>[] }).rows ?? [];\n }, scope);\n}\n","/**\n * Ordered, replayable per-channel message history.\n *\n * Broadcast on its own is fire-and-forget to whoever is connected at the\n * instant it is sent: fine for presence and for \"someone saved\" notifications,\n * not enough for op-based collaborative editing, where a client that blinks\n * out for two seconds has to resync a whole document rather than catch up on\n * the four operations it missed. This adds the missing half — every retained\n * broadcast gets a per-channel sequence number, and a client can ask for\n * everything after the last one it saw.\n *\n * Three decisions worth stating, because each rules out a simpler-looking one:\n *\n * - **Retention is server-side and opt-in.** A channel is created by whoever\n * names it, so a client-supplied history depth would let any visitor commit\n * the backend to unbounded storage. And presence channels — the common case\n * — must not pay for this: with no rules configured nothing is written, no\n * table is created, and `broadcast` runs exactly the code it ran before.\n *\n * - **Sequence numbers come from the database, not from a counter in this\n * process.** They have to survive a restart and be shared across instances;\n * an in-memory counter would restart at 1 after a deploy and hand a\n * reconnecting client a replay from the wrong era, silently.\n *\n * - **The cursor row outlives the messages it numbered.** Pruning is what\n * makes retention affordable, but pruning the cursor along with the messages\n * would restart the sequence and make `sinceSeq` mean something different\n * before and after — the worst kind of bug, because replay would still\n * return rows and they would look plausible. Cursors are tiny and are kept\n * forever; see {@link prune}, which touches only `channel_messages`.\n */\n\nimport { sql } from \"drizzle-orm\";\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type { ChannelHistoryEntry, ChannelRetentionRule } from \"@rebasepro/types\";\nimport { logger } from \"@rebasepro/server\";\nimport { revokeInternalTableSql } from \"@rebasepro/common\";\nimport { drizzleDdlBootstrapper } from \"../schema/drizzle-ddl\";\n\n/** How many messages a replay returns when the caller does not say. */\nconst DEFAULT_REPLAY_LIMIT = 200;\n\n/**\n * Hard ceiling on one replay, whatever the caller asks for.\n *\n * A reconnecting client names its own `limit`, so this is the only thing\n * standing between a stale `sinceSeq` and a single frame carrying a channel's\n * entire retained history. A client that is further behind than this is told so\n * via `latestSeq` and can decide to resync wholesale instead of paging.\n */\nconst MAX_REPLAY_LIMIT = 1000;\n\n/** Minimum gap between two prunes of the same channel. */\nconst PRUNE_THROTTLE_MS = 30_000;\n\n/**\n * Parse a retention TTL into milliseconds.\n *\n * Accepts a raw millisecond count or a short duration string (`\"30s\"`, `\"15m\"`,\n * `\"24h\"`, `\"7d\"`). Returns undefined for anything unparseable, which the\n * caller treats as \"no TTL\" — a misspelt duration must not silently become an\n * aggressive one.\n */\nexport function parseTtlMs(ttl: number | string | undefined): number | undefined {\n if (ttl === undefined || ttl === null) return undefined;\n if (typeof ttl === \"number\") return Number.isFinite(ttl) && ttl > 0 ? ttl : undefined;\n\n const match = /^\\s*(\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h|d)\\s*$/i.exec(ttl);\n if (!match) {\n logger.warn(`⚠️ [ChannelHistory] Ignoring unparseable retention ttl \"${ttl}\" — expected e.g. \"30s\", \"15m\", \"24h\", \"7d\".`);\n return undefined;\n }\n const value = parseFloat(match[1]);\n const unit = match[2].toLowerCase();\n const multiplier = unit === \"ms\" ? 1\n : unit === \"s\" ? 1_000\n : unit === \"m\" ? 60_000\n : unit === \"h\" ? 3_600_000\n : 86_400_000;\n const ms = value * multiplier;\n return ms > 0 ? ms : undefined;\n}\n\n/**\n * Whether `channel` is covered by `rule`.\n *\n * Exact match, or a trailing `*` acting as a prefix. Not a general glob: this\n * decides what reaches disk, and a pattern language whose reach is not obvious\n * at a glance is the wrong tool for that job.\n */\nexport function channelMatchesRule(channel: string, rule: ChannelRetentionRule): boolean {\n const pattern = rule.match;\n if (pattern === \"*\") return true;\n if (pattern.endsWith(\"*\")) return channel.startsWith(pattern.slice(0, -1));\n return channel === pattern;\n}\n\n/** A rule with its TTL already resolved to milliseconds. */\nexport interface ResolvedRetention {\n limit?: number;\n ttlMs?: number;\n}\n\n/**\n * Persistence and replay for retained channels.\n *\n * Inert unless constructed with at least one rule: {@link enabled} is false,\n * {@link ensureTables} does nothing, and {@link retentionFor} answers undefined\n * for every channel, so the realtime service never reaches the SQL below.\n */\nexport class ChannelHistoryStore {\n private rules: ChannelRetentionRule[];\n /** Resolved rule per channel name, so the match runs once per channel. */\n private resolved = new Map<string, ResolvedRetention | null>();\n /** Channel → timestamp of its last prune, for {@link PRUNE_THROTTLE_MS}. */\n private lastPruned = new Map<string, number>();\n private tablesReady = false;\n\n constructor(private db: NodePgDatabase<Record<string, unknown>>, rules: ChannelRetentionRule[] = []) {\n this.rules = rules.filter(rule => {\n if (!rule?.match) {\n logger.warn(\"⚠️ [ChannelHistory] Ignoring a retention rule with no `match`.\");\n return false;\n }\n const hasBound = rule.limit !== undefined || rule.ttl !== undefined;\n if (!hasBound) {\n // Unbounded retention is almost never intended and cannot be\n // walked back once the table has grown, so it is refused rather\n // than honoured.\n logger.warn(`⚠️ [ChannelHistory] Retention rule \"${rule.match}\" sets neither \\`limit\\` nor \\`ttl\\` — ignoring it, as it would retain forever.`);\n return false;\n }\n return true;\n });\n }\n\n /** Whether any channel retains anything at all. */\n get enabled(): boolean {\n return this.rules.length > 0;\n }\n\n /**\n * The retention that applies to `channel`, or undefined when none does.\n *\n * First matching rule wins, so callers order them most-specific first.\n */\n retentionFor(channel: string): ResolvedRetention | undefined {\n if (!this.enabled || !channel) return undefined;\n\n const cached = this.resolved.get(channel);\n if (cached !== undefined) return cached ?? undefined;\n\n const rule = this.rules.find(r => channelMatchesRule(channel, r));\n const resolved: ResolvedRetention | null = rule\n ? { limit: rule.limit, ttlMs: parseTtlMs(rule.ttl) }\n : null;\n\n // Bounded by the number of distinct channel names seen, which is the\n // same thing the in-memory channel and presence maps are bounded by.\n this.resolved.set(channel, resolved);\n return resolved ?? undefined;\n }\n\n /**\n * Create the history tables. Idempotent, and a no-op when no rule is set —\n * a deployment that never retains anything gets no schema for it.\n */\n async ensureTables(): Promise<void> {\n if (!this.enabled || this.tablesReady) return;\n\n // Contained, retrying steps rather than one straight sequence — see the\n // note on `ChannelPresenceStore.ensureTables`. The failure mode here is\n // the same and the stakes are the same: the two `REVOKE`s at the end are\n // what keep retained broadcasts off the end-user role, and a lost create\n // race used to skip them.\n const ddl = drizzleDdlBootstrapper(this.db, \"channel-history\");\n\n await ddl.ensureObject(\"rebase schema\", \"CREATE SCHEMA IF NOT EXISTS rebase\");\n\n // The primary key is exactly the replay query's access path\n // (`channel = $1 AND seq > $2 ORDER BY seq`), so it needs no further\n // index of its own.\n await ddl.ensureObject(\"channel_messages table\", `\n CREATE TABLE IF NOT EXISTS rebase.channel_messages (\n channel TEXT NOT NULL,\n seq BIGINT NOT NULL,\n event TEXT NOT NULL,\n payload JSONB,\n sender_id TEXT,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n PRIMARY KEY (channel, seq)\n )\n `);\n\n // Only for the TTL arm of pruning; the limit arm rides the primary key.\n await ddl.ensureObject(\"channel_messages created_at index\", `\n CREATE INDEX IF NOT EXISTS idx_channel_messages_created\n ON rebase.channel_messages (created_at)\n `);\n\n // Never pruned — see the note at the top of this file. One row per\n // channel that has ever retained a message.\n await ddl.ensureObject(\"channel_cursors table\", `\n CREATE TABLE IF NOT EXISTS rebase.channel_cursors (\n channel TEXT PRIMARY KEY,\n last_seq BIGINT NOT NULL\n )\n `);\n\n // Retained broadcasts for every channel in one table, with no RLS: who\n // may replay a channel is decided before the read, by the channel gate\n // in `realtimeService.authorizeChannelAction` — a replay is answered\n // only for a client that has joined the channel, plus whatever an\n // installed `ChannelAuthorizer` adds. That gate is the entire reason\n // this table can sit outside the RLS model, so it fails closed; the\n // rule language it does not yet have is written up in\n // `docs/channel-authorization.md`. The driver's schema-wide\n // grant reaches these (created here, after it ran), so take the\n // privilege back.\n //\n // Driven off a probe of what exists rather than off who won each create.\n const [messagesReady, cursorsReady] = await Promise.all([\n ddl.isReadable(\"rebase.channel_messages\"),\n ddl.isReadable(\"rebase.channel_cursors\")\n ]);\n if (messagesReady) {\n await ddl.step(\"channel_messages revoke\", () =>\n this.db.execute(sql.raw(revokeInternalTableSql(\"rebase\", \"channel_messages\")))\n );\n }\n if (cursorsReady) {\n await ddl.step(\"channel_cursors revoke\", () =>\n this.db.execute(sql.raw(revokeInternalTableSql(\"rebase\", \"channel_cursors\")))\n );\n }\n\n if (!messagesReady || !cursorsReady) {\n // Left un-ready on purpose so the next call retries. Announcing\n // \"ready\" here is what would turn a half-created schema into replays\n // that answer empty forever.\n logger.warn(\n \"[ChannelHistory] Retained-channel tables are not both present; history is not ready yet.\"\n );\n return;\n }\n\n this.tablesReady = true;\n logger.info(`✅ [ChannelHistory] Retained channels ready (${this.rules.length} rule(s)).`);\n }\n\n /**\n * Append a broadcast and return the sequence number it was given.\n *\n * The sequence is allocated by the same statement that stores the message,\n * so a crash between the two is not a possibility. `ON CONFLICT DO UPDATE`\n * takes a row lock on the channel's cursor, which is what makes concurrent\n * broadcasts to one channel line up in a single order — and what keeps\n * different channels from contending with each other at all.\n */\n async append(\n channel: string,\n event: string,\n payload: unknown,\n senderId?: string\n ): Promise<{ seq: number; at: string }> {\n const result = await this.db.execute(sql`\n WITH next AS (\n INSERT INTO rebase.channel_cursors (channel, last_seq)\n VALUES (${channel}, 1)\n ON CONFLICT (channel)\n DO UPDATE SET last_seq = rebase.channel_cursors.last_seq + 1\n RETURNING last_seq\n )\n INSERT INTO rebase.channel_messages (channel, seq, event, payload, sender_id)\n SELECT ${channel}, next.last_seq, ${event}, ${JSON.stringify(payload ?? null)}::jsonb, ${senderId ?? null}\n FROM next\n RETURNING seq, created_at\n `);\n\n const row = result.rows[0] as { seq: string | number; created_at: Date | string } | undefined;\n if (!row) throw new Error(`Failed to append to channel history for \"${channel}\"`);\n\n return {\n // BIGINT comes back as a string from node-postgres; the wire type is\n // a number, and a channel would need 2^53 messages to notice.\n seq: Number(row.seq),\n at: row.created_at instanceof Date ? row.created_at.toISOString() : String(row.created_at)\n };\n }\n\n /**\n * Everything retained for `channel` after `sinceSeq`, oldest first.\n *\n * `latestSeq` is reported whether or not the messages were capped, so a\n * client that is further behind than one page can tell.\n */\n async replay(\n channel: string,\n sinceSeq = 0,\n limit = DEFAULT_REPLAY_LIMIT\n ): Promise<{ messages: ChannelHistoryEntry[]; latestSeq: number }> {\n const capped = Math.max(1, Math.min(Math.floor(limit) || DEFAULT_REPLAY_LIMIT, MAX_REPLAY_LIMIT));\n const after = Number.isFinite(sinceSeq) && sinceSeq > 0 ? Math.floor(sinceSeq) : 0;\n\n const result = await this.db.execute(sql`\n SELECT seq, event, payload, sender_id, created_at\n FROM rebase.channel_messages\n WHERE channel = ${channel} AND seq > ${after}\n ORDER BY seq ASC\n LIMIT ${capped}\n `);\n\n const messages = (result.rows as Array<{\n seq: string | number;\n event: string;\n payload: unknown;\n sender_id: string | null;\n created_at: Date | string;\n }>).map(row => ({\n seq: Number(row.seq),\n event: row.event,\n payload: row.payload,\n senderId: row.sender_id ?? undefined,\n at: row.created_at instanceof Date ? row.created_at.toISOString() : String(row.created_at)\n }));\n\n // Read from the cursor rather than from the messages: the cursor is the\n // authority on how far the channel has got, and still says so after\n // pruning has removed the messages it counted.\n const cursor = await this.db.execute(sql`\n SELECT last_seq FROM rebase.channel_cursors WHERE channel = ${channel}\n `);\n const cursorRow = cursor.rows[0] as { last_seq: string | number } | undefined;\n const latestSeq = cursorRow ? Number(cursorRow.last_seq) : 0;\n\n return { messages, latestSeq };\n }\n\n /**\n * One retained message by its address.\n *\n * This is what makes the cross-instance pointer path work: a broadcast too\n * large to travel inside a `pg_notify` payload is already stored here, so\n * the notification carries `(channel, seq)` and each receiving instance\n * reads the body back. Returns null when the message has since been pruned\n * — a receiver that is that far behind has nothing useful to deliver, and\n * the client's own `channel_history` replay is the repair path.\n */\n async getBySeq(channel: string, seq: number): Promise<ChannelHistoryEntry | null> {\n const result = await this.db.execute(sql`\n SELECT seq, event, payload, sender_id, created_at\n FROM rebase.channel_messages\n WHERE channel = ${channel} AND seq = ${seq}\n `);\n\n const row = result.rows[0] as {\n seq: string | number;\n event: string;\n payload: unknown;\n sender_id: string | null;\n created_at: Date | string;\n } | undefined;\n if (!row) return null;\n\n return {\n seq: Number(row.seq),\n event: row.event,\n payload: row.payload,\n senderId: row.sender_id ?? undefined,\n at: row.created_at instanceof Date ? row.created_at.toISOString() : String(row.created_at)\n };\n }\n\n /**\n * Enforce a channel's retention bounds.\n *\n * Throttled per channel, so a burst of operations prunes once rather than\n * once per message — the cost then tracks elapsed time instead of write\n * volume, which is what makes retention affordable on a hot channel.\n */\n async prune(channel: string, retention: ResolvedRetention): Promise<number> {\n const now = Date.now();\n const last = this.lastPruned.get(channel) ?? 0;\n if (now - last < PRUNE_THROTTLE_MS) return 0;\n this.lastPruned.set(channel, now);\n\n let deleted = 0;\n\n if (retention.ttlMs !== undefined) {\n const result = await this.db.execute(sql`\n DELETE FROM rebase.channel_messages\n WHERE channel = ${channel}\n AND created_at < NOW() - MAKE_INTERVAL(secs => ${retention.ttlMs / 1000})\n `);\n deleted += result.rowCount ?? 0;\n }\n\n if (retention.limit !== undefined && retention.limit > 0) {\n // OFFSET past the newest `limit` rows to find the highest seq that\n // is no longer wanted, then delete everything at or below it. Fewer\n // rows than the limit leaves the subquery empty, and the comparison\n // with NULL deletes nothing.\n const result = await this.db.execute(sql`\n DELETE FROM rebase.channel_messages\n WHERE channel = ${channel}\n AND seq <= (\n SELECT seq FROM rebase.channel_messages\n WHERE channel = ${channel}\n ORDER BY seq DESC\n OFFSET ${Math.floor(retention.limit)} LIMIT 1\n )\n `);\n deleted += result.rowCount ?? 0;\n }\n\n return deleted;\n }\n\n /** Forget throttle and match caches. Called on shutdown. */\n clear(): void {\n this.resolved.clear();\n this.lastPruned.clear();\n }\n}\n","/**\n * The shared presence roster.\n *\n * Broadcast only ever needed *fan-out* to work across instances — a frame goes\n * out, whoever is connected receives it. Presence needs more than that, because\n * `presence_state` is a question (\"who is in this document?\") and a per-process\n * `Map` can only answer for the clients that happen to share a replica with the\n * asker. Two people editing the same scene through different pods would each\n * see an empty room while broadcasting cursors at each other perfectly.\n *\n * So presence gets one row per tracked client, in Postgres, readable by every\n * instance. Three consequences worth stating:\n *\n * - **The table is the roster; the in-process map is a cache of our own\n * clients.** Reads answer from the table when this store is active, so the\n * answer is the same whichever instance is asked.\n *\n * - **`last_seen` is the liveness signal, and it is already there.** The client\n * heartbeats presence every ~20 s against a 30 s window; the sweep that has\n * always reaped local stale entries now also reaps rows belonging to\n * instances that stopped writing — which is exactly what a crashed pod looks\n * like. Crash recovery is a property of the TTL, not a separate mechanism.\n *\n * - **The sweep deletes with `RETURNING`.** Whichever instance wins the delete\n * is the one that announces the departures, so a stale client produces one\n * `presence_diff` for the cluster rather than one per replica.\n */\n\nimport { sql } from \"drizzle-orm\";\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { revokeInternalTableSql } from \"@rebasepro/common\";\nimport { drizzleDdlBootstrapper } from \"../schema/drizzle-ddl\";\n\n/** A tracked client, as any instance sees it. */\nexport interface PresenceRow {\n channel: string;\n clientId: string;\n state: Record<string, unknown>;\n}\n\nexport class ChannelPresenceStore {\n private tablesReady = false;\n\n constructor(\n private readonly db: NodePgDatabase<Record<string, unknown>>,\n private readonly instanceId: string\n ) {}\n\n /**\n * Create the roster table. Idempotent, and safe to run on every instance at\n * once.\n *\n * Written as separate contained steps rather than one straight sequence for\n * a reason that only bites with more than one replica, which is exactly the\n * deployment shape this table exists to serve: `CREATE … IF NOT EXISTS`\n * reads the catalog and then writes to it non-atomically, so peers booting\n * together collide, and the loser used to abandon everything after it —\n * including the trailing `REVOKE`. That revoke is the only thing keeping the\n * roster off the end-user role, so losing a boot race silently left the\n * whole channel roster readable by every signed-in user.\n *\n * `tablesReady` is now set from a probe of what exists, not from having been\n * the instance that created it.\n */\n async ensureTables(): Promise<void> {\n if (this.tablesReady) return;\n\n const ddl = drizzleDdlBootstrapper(this.db, \"channel-presence\");\n\n await ddl.ensureObject(\"rebase schema\", \"CREATE SCHEMA IF NOT EXISTS rebase\");\n\n // Keyed by (channel, client_id): a client id is globally unique, so the\n // instance is a column rather than part of the identity — a client that\n // reconnects onto another replica replaces its own row instead of\n // appearing twice in the roster.\n await ddl.ensureObject(\"channel_presence table\", `\n CREATE TABLE IF NOT EXISTS rebase.channel_presence (\n channel TEXT NOT NULL,\n client_id TEXT NOT NULL,\n instance_id TEXT NOT NULL,\n state JSONB NOT NULL DEFAULT '{}'::jsonb,\n last_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n PRIMARY KEY (channel, client_id)\n )\n `);\n\n // The sweep's access path; the roster read rides the primary key.\n await ddl.ensureObject(\"channel_presence last_seen index\", `\n CREATE INDEX IF NOT EXISTS idx_channel_presence_last_seen\n ON rebase.channel_presence (last_seen)\n `);\n\n // The roster of every client on every channel, with no RLS — a row\n // policy has nothing to match on here, since a channel name is a string\n // a client invents rather than a row anyone owns. What guards it is the\n // channel gate in `realtimeService.authorizeChannelAction`: presence is\n // readable only to a client that has joined the channel, plus whatever\n // an installed `ChannelAuthorizer` adds. That gate is the entire reason\n // this table can sit outside the RLS model, so it fails closed —\n // see `docs/channel-authorization.md` for what it does *not*\n // yet decide. Revoke the schema-wide grant the driver handed out\n // before this table existed.\n //\n // Driven off the probe, not off who won the create: the privilege has to\n // come off whether this instance created the table or found it.\n if (await ddl.isReadable(\"rebase.channel_presence\")) {\n await ddl.step(\"channel_presence revoke\", () =>\n this.db.execute(sql.raw(revokeInternalTableSql(\"rebase\", \"channel_presence\")))\n );\n this.tablesReady = true;\n }\n }\n\n /** Record (or refresh) a client's presence. */\n async track(channel: string, clientId: string, state: Record<string, unknown>): Promise<void> {\n await this.db.execute(sql`\n INSERT INTO rebase.channel_presence (channel, client_id, instance_id, state, last_seen)\n VALUES (${channel}, ${clientId}, ${this.instanceId}, ${JSON.stringify(state ?? {})}::jsonb, NOW())\n ON CONFLICT (channel, client_id) DO UPDATE\n SET state = EXCLUDED.state,\n instance_id = EXCLUDED.instance_id,\n last_seen = NOW()\n `);\n }\n\n /** Drop one client's presence in one channel. */\n async remove(channel: string, clientId: string): Promise<void> {\n await this.db.execute(sql`\n DELETE FROM rebase.channel_presence\n WHERE channel = ${channel} AND client_id = ${clientId}\n `);\n }\n\n /** Drop a client from every channel — used when its socket closes. */\n async removeClient(clientId: string): Promise<void> {\n await this.db.execute(sql`\n DELETE FROM rebase.channel_presence WHERE client_id = ${clientId}\n `);\n }\n\n /** The global roster for a channel. */\n async roster(channel: string): Promise<Record<string, Record<string, unknown>>> {\n const result = await this.db.execute(sql`\n SELECT client_id, state FROM rebase.channel_presence WHERE channel = ${channel}\n `);\n\n const presences: Record<string, Record<string, unknown>> = {};\n for (const row of result.rows as Array<{ client_id: string; state: Record<string, unknown> | null }>) {\n presences[row.client_id] = row.state ?? {};\n }\n return presences;\n }\n\n /**\n * Reap rows this instance is not responsible for and that have gone quiet.\n *\n * Own rows are excluded because the in-process sweep already handles them —\n * and handles them better, since it can tell \"the socket is gone\" from \"the\n * heartbeat is late\". What is left is precisely the interesting case: rows\n * written by an instance that is no longer writing.\n *\n * Returns what was removed, so the caller can announce it.\n */\n async sweepStale(ttlMs: number): Promise<PresenceRow[]> {\n const result = await this.db.execute(sql`\n DELETE FROM rebase.channel_presence\n WHERE instance_id <> ${this.instanceId}\n AND last_seen < NOW() - MAKE_INTERVAL(secs => ${ttlMs / 1000})\n RETURNING channel, client_id, state\n `);\n\n return (result.rows as Array<{ channel: string; client_id: string; state: Record<string, unknown> | null }>)\n .map(row => ({ channel: row.channel, clientId: row.client_id, state: row.state ?? {} }));\n }\n\n /**\n * Remove every row this instance owns. Called on graceful shutdown so a\n * rolling deploy does not leave a TTL window of ghosts in every roster.\n */\n async removeInstance(): Promise<void> {\n await this.db.execute(sql`\n DELETE FROM rebase.channel_presence WHERE instance_id = ${this.instanceId}\n `);\n }\n}\n","/**\n * Runtime pieces of the channel bus that are not the contract itself.\n *\n * The interface, the frame shape and the implementer's contract live in\n * `@rebasepro/types` (`types/channel_bus.ts`), so a transport shipped as its own\n * package — a Redis one, say — depends on the contract and not on this database\n * adapter. They are re-exported here for convenience: code already importing\n * from the adapter should not have to know where the types are declared.\n */\n\nimport type { ChannelBusFrame } from \"@rebasepro/types\";\n\nexport type {\n ChannelBus,\n ChannelBusFrame,\n ChannelBusHandler,\n ChannelBusConfig,\n ChannelBusSetting\n} from \"@rebasepro/types\";\nexport { isChannelBusInstance } from \"@rebasepro/types\";\n\n/**\n * The default: no cross-instance delivery at all.\n *\n * This is what every deployment ran before the bus existed, and what a\n * single-instance deployment should keep running — `publish` resolves without\n * touching the network, so the broadcast path is the same handful of `ws.send`\n * calls it always was.\n */\nexport class MemoryChannelBus {\n readonly kind = \"memory\" as const;\n readonly maxFrameBytes = Infinity;\n\n async start(): Promise<void> { /* nothing to connect */ }\n\n async publish(): Promise<void> { /* nowhere to publish to */ }\n\n async stop(): Promise<void> { /* nothing to release */ }\n}\n\n/** Encoded size of a frame, for a transport's size check. */\nexport function frameByteLength(frame: ChannelBusFrame): number {\n return Buffer.byteLength(JSON.stringify(frame), \"utf8\");\n}\n","/**\n * Channel bus over Postgres LISTEN/NOTIFY.\n *\n * Chosen because it needs nothing that a Rebase deployment does not already\n * have — the same database, the same direct URL the CDC listener uses. Three\n * properties of `NOTIFY` shape everything below:\n *\n * - **8000 bytes per payload.** Presence and cursors fit with room to spare; a\n * scene snapshot does not. Rather than truncate or drop, an oversized frame\n * on a *retained* channel is published as a pointer — the body is already in\n * `rebase.channel_messages` with a sequence number, so the receiver reads it\n * back. That is the same trick the entity path uses (notify an address,\n * refetch the row), applied to a different table. On an ephemeral channel\n * there is nothing to point at, so the publish is refused loudly instead of\n * reaching some instances and not others.\n *\n * - **A notify is a query on the primary database.** Not a slow one, but it\n * competes with the application's real queries, and that — not throughput —\n * is what actually limits this transport. Measured, it carried ~10k\n * cross-instance messages/second and stayed flat out to eight instances; what\n * it should not do is spend 10k queries/second of the database's budget on\n * cursor movement. Hence the batching below.\n *\n * - **Delivery is best-effort.** Retained channels repair themselves through\n * the client's history replay, so a lost frame costs a live update rather\n * than correctness. That is what makes coalescing safe.\n */\n\nimport { sql } from \"drizzle-orm\";\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { logger } from \"@rebasepro/server\";\nimport { PgNotifyListener } from \"../pg-notify-listener\";\nimport { ChannelBus, ChannelBusFrame, ChannelBusHandler, frameByteLength } from \"./ChannelBus\";\n\n/** NOTIFY channel carrying channel-bus frames. */\nexport const CHANNEL_BUS_NOTIFY_CHANNEL = \"rebase_channel_bus\";\n\n/**\n * Postgres refuses a NOTIFY payload of 8000 bytes or more. The margin below it\n * is for nothing in particular — it is there so that a payload which passes this\n * check cannot fail at the server for being a few bytes over.\n */\nexport const PG_NOTIFY_MAX_PAYLOAD_BYTES = 7500;\n\n/**\n * How long a batching window stays open.\n *\n * Ten milliseconds is below the threshold where a human notices a cursor lag,\n * and it is the difference between one query per message and one query per\n * window under load. Set to 0 to disable coalescing entirely.\n */\nexport const DEFAULT_BATCH_WINDOW_MS = 10;\n\n/** JSON overhead per frame inside a batch: the wrapping array's comma. */\nconst BATCH_SEPARATOR_BYTES = 1;\n/** JSON overhead of the batch envelope itself: `{\"batch\":[]}`. */\nconst BATCH_ENVELOPE_BYTES = 12;\n\ninterface PendingFrame {\n frame: ChannelBusFrame;\n bytes: number;\n resolve: () => void;\n reject: (error: unknown) => void;\n}\n\nexport class PostgresChannelBus implements ChannelBus {\n readonly kind = \"postgres\" as const;\n readonly maxFrameBytes = PG_NOTIFY_MAX_PAYLOAD_BYTES;\n\n private listener?: PgNotifyListener;\n private readonly batchWindowMs: number;\n\n /**\n * Frames waiting for the current window to close.\n *\n * The window is opened by a publish that found none open, and that publish\n * is sent *immediately* rather than joining a batch — see {@link publish}.\n */\n private pending: PendingFrame[] = [];\n private pendingBytes = BATCH_ENVELOPE_BYTES;\n private windowTimer?: ReturnType<typeof setTimeout>;\n private stopped = false;\n\n constructor(\n private readonly db: NodePgDatabase<Record<string, unknown>>,\n private readonly connectionString: string,\n options: { batchWindowMs?: number } = {}\n ) {\n const configured = options.batchWindowMs;\n this.batchWindowMs = typeof configured === \"number\" && configured >= 0\n ? configured\n : DEFAULT_BATCH_WINDOW_MS;\n }\n\n async start(handler: ChannelBusHandler): Promise<void> {\n this.stopped = false;\n this.listener = new PgNotifyListener({\n connectionString: this.connectionString,\n channel: CHANNEL_BUS_NOTIFY_CHANNEL,\n logLabel: \"[ChannelBus]\",\n onPayload: async (payload) => {\n const frames = parseChannelBusPayload(payload);\n if (!frames.length) {\n logger.warn(\"⚠️ [ChannelBus] Dropping unparseable payload.\");\n return;\n }\n // In order: a batch preserves the sender's publish order, and a\n // retained channel's consumers rely on it.\n for (const frame of frames) await handler(frame);\n }\n });\n await this.listener.start();\n }\n\n /**\n * Publish, coalescing under load.\n *\n * The window is *leading edge*: a publish arriving when no window is open is\n * sent straight away and opens one, so an idle channel pays no added latency\n * at all. Frames arriving while it is open are collected and leave together\n * when it closes. The effect is that cost tracks elapsed time rather than\n * message count — one query per window instead of one per message — which is\n * the same shape as the retention pruning throttle, for the same reason.\n *\n * The returned promise settles when the frame has actually left, not when it\n * was queued, so the contract (\"reaches the other instances, or rejects\")\n * still holds.\n */\n async publish(frame: ChannelBusFrame): Promise<void> {\n if (this.batchWindowMs === 0 || this.stopped) {\n await this.send([frame]);\n return;\n }\n\n if (!this.windowTimer) {\n this.openWindow();\n await this.send([frame]);\n return;\n }\n\n const bytes = frameByteLength(frame) + BATCH_SEPARATOR_BYTES;\n\n // A batch is one NOTIFY payload, so the 8 KB ceiling applies to the\n // whole batch. Send what we have rather than let the frame push it over.\n if (this.pending.length && this.pendingBytes + bytes > this.maxFrameBytes) {\n this.flush();\n }\n\n return new Promise<void>((resolve, reject) => {\n this.pending.push({ frame, bytes, resolve, reject });\n this.pendingBytes += bytes;\n });\n }\n\n async stop(): Promise<void> {\n this.stopped = true;\n if (this.windowTimer) {\n clearTimeout(this.windowTimer);\n this.windowTimer = undefined;\n }\n // Anything still queued belongs to clients that are already waiting on\n // it; dropping it on shutdown would be a silent loss where a flush costs\n // one more query.\n this.flush();\n await this.listener?.stop();\n this.listener = undefined;\n }\n\n private openWindow(): void {\n this.windowTimer = setTimeout(() => {\n this.windowTimer = undefined;\n if (this.pending.length) {\n // Still busy: send this window's frames and open the next one,\n // so a sustained stream keeps costing one query per window.\n this.flush();\n this.openWindow();\n }\n // Otherwise leave it closed, so the next publish after a quiet\n // moment goes out immediately.\n }, this.batchWindowMs);\n\n // Housekeeping must never hold the process open.\n (this.windowTimer as unknown as { unref?: () => void }).unref?.();\n }\n\n /** Send everything queued and settle the promises waiting on it. */\n private flush(): void {\n if (!this.pending.length) return;\n\n const batch = this.pending;\n this.pending = [];\n this.pendingBytes = BATCH_ENVELOPE_BYTES;\n\n this.send(batch.map(p => p.frame))\n .then(() => { for (const p of batch) p.resolve(); })\n .catch((error) => { for (const p of batch) p.reject(error); });\n }\n\n /**\n * One NOTIFY.\n *\n * A single frame goes out in the plain, unwrapped shape. That is not just\n * economy: during a rolling deploy an instance running the previous build\n * understands only that shape, and low-rate traffic — presence, the tail of\n * a session — is exactly what is flowing while pods restart. Batching only\n * appears under load, which shrinks the mixed-version window to almost\n * nothing.\n */\n private async send(frames: ChannelBusFrame[]): Promise<void> {\n if (!frames.length) return;\n const payload = frames.length === 1\n ? JSON.stringify(frames[0])\n : JSON.stringify({ batch: frames });\n\n await this.db.execute(sql`SELECT pg_notify(${CHANNEL_BUS_NOTIFY_CHANNEL}, ${payload})`);\n }\n}\n\n/**\n * Parse a bus payload into the frames it carries.\n *\n * Accepts both wire shapes — a bare frame and a `{ batch: [...] }` envelope —\n * so an instance on the new build understands one on the old. Returns an empty\n * array for anything unrecognisable: a malformed or future-versioned message\n * must never take the listener down.\n */\nexport function parseChannelBusPayload(payload: string): ChannelBusFrame[] {\n let parsed: unknown;\n try {\n parsed = JSON.parse(payload);\n } catch {\n return [];\n }\n if (!parsed || typeof parsed !== \"object\") return [];\n\n const batch = (parsed as { batch?: unknown }).batch;\n if (Array.isArray(batch)) {\n return batch\n .map(entry => coerceFrame(entry))\n .filter((frame): frame is ChannelBusFrame => frame !== null);\n }\n\n const single = coerceFrame(parsed);\n return single ? [single] : [];\n}\n\n/**\n * Parse a single bus frame, returning null for anything that is not a frame we\n * understand.\n */\nexport function parseChannelBusFrame(payload: string): ChannelBusFrame | null {\n try {\n return coerceFrame(JSON.parse(payload));\n } catch {\n return null;\n }\n}\n\nfunction coerceFrame(value: unknown): ChannelBusFrame | null {\n if (!value || typeof value !== \"object\") return null;\n\n const obj = value as Record<string, unknown>;\n const sid = typeof obj.sid === \"string\" ? obj.sid : undefined;\n const channel = typeof obj.channel === \"string\" ? obj.channel : undefined;\n if (!sid || !channel) return null;\n\n switch (obj.kind) {\n case \"broadcast\":\n if (typeof obj.event !== \"string\") return null;\n return {\n kind: \"broadcast\",\n sid,\n channel,\n event: obj.event,\n from: typeof obj.from === \"string\" ? obj.from : undefined,\n seq: typeof obj.seq === \"number\" ? obj.seq : undefined,\n payload: obj.payload\n };\n case \"broadcast_ref\":\n if (typeof obj.seq !== \"number\") return null;\n return {\n kind: \"broadcast_ref\",\n sid,\n channel,\n from: typeof obj.from === \"string\" ? obj.from : undefined,\n seq: obj.seq\n };\n case \"presence_diff\":\n return {\n kind: \"presence_diff\",\n sid,\n channel,\n joins: (obj.joins ?? {}) as Record<string, Record<string, unknown>>,\n leaves: (obj.leaves ?? {}) as Record<string, Record<string, unknown>>\n };\n default:\n return null;\n }\n}\n","/**\n * Resolution of the channel bus from config, environment, or a supplied instance.\n *\n * Opt-in, like every other cross-cutting realtime switch here: with nothing\n * configured a deployment gets the memory bus and behaves exactly as it did\n * before this existed. Unlike `REALTIME_CDC=auto`, there is no \"try it and see\"\n * default — a bus changes where messages go, and quietly turning on a Postgres\n * NOTIFY per broadcast because a direct URL happened to be set is not a\n * decision to make on the user's behalf.\n *\n * Two transports ship, and neither adds a service to a deployment. A third is\n * not a code change here: `realtime.bus` also accepts an already-constructed\n * {@link ChannelBus}, so a transport published as its own package plugs in\n * without this file learning about it. See `@rebasepro/types` →\n * `types/channel_bus.ts` for the contract such a package implements.\n */\n\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { isChannelBusInstance, type ChannelBus, type ChannelBusConfig, type ChannelBusSetting } from \"@rebasepro/types\";\nimport { logger } from \"@rebasepro/server\";\nimport { MemoryChannelBus } from \"./ChannelBus\";\nimport { PostgresChannelBus } from \"./PostgresChannelBus\";\n\nexport * from \"./ChannelBus\";\nexport {\n PostgresChannelBus,\n CHANNEL_BUS_NOTIFY_CHANNEL,\n PG_NOTIFY_MAX_PAYLOAD_BYTES,\n DEFAULT_BATCH_WINDOW_MS,\n parseChannelBusFrame,\n parseChannelBusPayload\n} from \"./PostgresChannelBus\";\n\nexport interface ChannelBusDeps {\n db: NodePgDatabase<Record<string, unknown>>;\n /**\n * Direct (non-pooled) Postgres URL for the LISTEN client. `LISTEN` is\n * session state, so behind PgBouncer in transaction mode this must be the\n * database itself and not the pooler.\n */\n directUrl?: string;\n}\n\n/**\n * Merge `REALTIME_CHANNEL_BUS` into the configured bus.\n *\n * The environment wins over a *named* built-in, so the transport can be changed\n * per deployment without a rebuild — the same reason `REALTIME_CDC` is an env\n * var. It does **not** win over a supplied instance: the env var can only name\n * transports this package knows how to construct, so honouring it there would\n * mean silently discarding the object the application handed us.\n */\nexport function resolveChannelBusSetting(configured?: ChannelBusSetting): ChannelBusSetting {\n const raw = (process.env.REALTIME_CHANNEL_BUS || \"\").trim().toLowerCase();\n\n if (isChannelBusInstance(configured)) {\n if (raw && raw !== configured.kind) {\n logger.warn(\n `⚠️ [ChannelBus] REALTIME_CHANNEL_BUS=\"${raw}\" is ignored because realtime.bus was given a ` +\n `\"${configured.kind}\" transport instance directly. Remove one of the two to make the intent clear.`\n );\n }\n return configured;\n }\n\n if (!raw) return configured ?? { type: \"memory\" };\n\n if (raw !== \"memory\" && raw !== \"postgres\") {\n logger.warn(\n `⚠️ [ChannelBus] Unknown REALTIME_CHANNEL_BUS value \"${raw}\" — expected memory|postgres, or pass a ` +\n \"ChannelBus instance as realtime.bus for a transport that ships separately. Falling back to the \" +\n \"configured bus.\"\n );\n return configured ?? { type: \"memory\" };\n }\n\n // Keep the configured options (an explicit connection string) when the env\n // var only restates the type it was already set to.\n if (configured?.type === raw) return configured;\n return raw === \"memory\" ? { type: \"memory\" } : { type: \"postgres\" };\n}\n\n/**\n * Produce the bus a setting asks for.\n *\n * An instance is handed straight back — constructing it was the application's\n * job, and this function has nothing to add. A named built-in that turns out to\n * be unusable degrades to the memory bus, with the reason logged, rather than\n * throwing: a misconfigured bus should cost a deployment its cross-instance\n * fan-out, not its ability to boot.\n */\nexport function createChannelBus(setting: ChannelBusSetting, deps: ChannelBusDeps): ChannelBus {\n if (isChannelBusInstance(setting)) return setting;\n\n switch (setting.type) {\n case \"postgres\": {\n const connectionString = setting.connectionString || deps.directUrl;\n if (!connectionString) {\n logger.warn(\n \"⚠️ [ChannelBus] realtime.bus is \\\"postgres\\\" but no direct database URL is available \" +\n \"(set DATABASE_DIRECT_URL or realtime.bus.connectionString) — channel broadcast and presence \" +\n \"stay per-instance.\"\n );\n return new MemoryChannelBus();\n }\n return new PostgresChannelBus(deps.db, connectionString, {\n batchWindowMs: setting.batchWindowMs\n });\n }\n case \"memory\":\n default:\n return new MemoryChannelBus();\n }\n}\n","import { WebSocket } from \"ws\";\nimport { EventEmitter } from \"events\";\nimport { Client as PgClient } from \"pg\";\nimport { randomUUID } from \"crypto\";\nimport { DataService } from \"./dataService\";\n\nimport { ANONYMOUS_USER_ID, FetchCollectionProps, ListenCollectionProps, ListenOneProps, DataDriver, CollectionUpdateMessage, SingleUpdateMessage, CollectionPatchMessage, WebSocketMessage, FilterValues, LogicalCondition, OrderByTuple, CollectionConfig, RebaseCallContext, resolveClientListLimit, ListLimitError } from \"@rebasepro/types\";\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { sql as drizzleSql } from \"drizzle-orm\";\nimport { RealtimeProvider, CollectionSubscriptionConfig, SingleSubscriptionConfig } from \"../interfaces\";\nimport { PostgresCollectionRegistry } from \"../collections/PostgresCollectionRegistry\";\nimport { buildPropertyCallbacks, getTableName, OrderBySpecError, parseOrderBySpecStrict } from \"@rebasepro/common\";\nimport { applyAuthContext } from \"../security/rls-enforcement\";\nimport { buildJunctionLinkMap, type JunctionLink } from \"./cdc/junction-tables\";\nimport { logger } from \"@rebasepro/server\";\nimport { sanitizeErrorForClient } from \"../utils/pg-error-utils\";\nimport { CdcListener, type CdcChangeEvent } from \"./cdc/CdcListener\";\nimport { deriveRowAddress, getPrimaryKeys, type PrimaryKeyInfo } from \"./collection-helpers\";\nimport { ChannelHistoryStore, type ResolvedRetention } from \"./channel-history\";\nimport { ChannelPresenceStore } from \"./channel-presence\";\nimport { ChannelBus, ChannelBusFrame, MemoryChannelBus, frameByteLength } from \"./channel-bus\";\nimport type { ChannelHistoryEntry, ChannelRetentionRule } from \"@rebasepro/types\";\n\n/** Channel name used for Postgres LISTEN/NOTIFY cross-instance realtime. */\nconst PG_NOTIFY_CHANNEL = \"rebase_entity_changes\";\n\n/**\n * Auth context stored per-subscription so real-time refetches respect RLS.\n * Mirrors the session variables set by PostgresBackendDriver.withAuth().\n */\nexport interface SubscriptionAuthContext {\n uid: string;\n roles: string[];\n}\n\n/** What a channel frame is asking to do. */\nexport type ChannelAction = \"join\" | \"broadcast\" | \"presence\" | \"history\";\n\n/** Everything an authorizer is told about the frame it is asked to allow. */\nexport interface ChannelAuthorizationRequest {\n /** The channel the frame names, exactly as the client wrote it. */\n channel: string;\n action: ChannelAction;\n /** The socket, not the principal — one user may hold several. */\n clientId: string;\n /** The socket's authenticated principal, or the anonymous one. */\n user?: SubscriptionAuthContext;\n}\n\n/**\n * The extension point for channel access rules.\n *\n * **This is deliberately not a product API yet.** The rule *language* — a\n * config key, a per-pattern DSL, how it composes with `securityRules` — is an\n * open design question (see `docs/channel-authorization.md`), and\n * inventing one here would be inventing the answer. What exists is the single\n * place every channel frame passes through, so that whatever shape the rules\n * eventually take has exactly one seam to plug into and no arm of the switch\n * can be forgotten.\n *\n * Returning `false` — or throwing — refuses the frame. It is consulted *after*\n * the membership floor below, so an authorizer can only ever narrow access,\n * never widen it.\n */\nexport type ChannelAuthorizer = (request: ChannelAuthorizationRequest) => boolean | Promise<boolean>;\n\ninterface DataDriverWithData extends DataDriver {\n data: unknown;\n}\n\ntype RealTimeListenCollectionProps = ListenCollectionProps & {\n subscriptionId: string\n};\n\n/**\n * The narrowing a collection subscription was created with, kept so that every\n * refetch answers the same query the initial fetch did.\n *\n * Named once because it used to be written out inline in five places, and a\n * field missing from one of them is accepted over the wire and then silently\n * ignored: `offset` was declared on the incoming props and never stored, so a\n * live list on page three served page one, and `logical` was never stored\n * either, so an `or(...)` subscription was pushed every row in the table.\n */\ntype StoredCollectionRequest = {\n filter?: Record<string, unknown>;\n logical?: LogicalCondition;\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n offset?: number;\n startAfter?: Record<string, unknown>;\n databaseId?: string;\n searchString?: string;\n /** Ask each row which declared search field matched — populates `_matches`. */\n searchExplain?: boolean;\n};\n\ntype RealTimeListenEntityProps = ListenOneProps & { subscriptionId: string };\n\n/**\n * A registered subscription, plus the two counters that order its deliveries.\n *\n * Every update a subscription delivers is a full re-fetch, and more than one\n * thing starts one for the same subscription without coordinating: the initial\n * fetch at subscribe time, and a debounced refetch per notification (app\n * mutation, cross-instance NOTIFY, or CDC). A fetch that started earlier can\n * finish later, and the delivery replaces everything the subscriber has — so\n * the subscriber goes back to the state before the change and stays there,\n * silently, until the next write to that collection.\n *\n * The debounce is not a fix for this. It collapses a burst into one refetch and\n * does nothing about two refetches that overlap: notification A fires its timer\n * and starts fetch A, notification B arrives while A is still in flight, and B's\n * timer fires and starts fetch B regardless. See class 44 in\n * `docs/bug-classes.md`.\n *\n * `started` is taken before the work, `delivered` after it — which makes the\n * last delivery *started* the last one *delivered*.\n */\ntype Subscription = {\n clientId: string;\n type: \"collection\" | \"single\";\n path: string;\n id?: string | number;\n // Store full collection request parameters for proper refetching\n collectionRequest?: StoredCollectionRequest;\n // Auth context for RLS — when set, refetches run in a transaction\n // with set_config('app.uid', ...) / set_config('app.user_roles', ...)\n authContext?: SubscriptionAuthContext;\n /** How many deliveries have been started for this subscription. */\n started: number;\n /** The highest started-sequence that has already reached the subscriber. */\n delivered: number;\n};\n\n/**\n * PostgreSQL-specific realtime service.\n * Handles WebSocket connections and subscriptions for real-time row updates.\n *\n * Implements the RealtimeProvider interface for database abstraction.\n */\nexport class RealtimeService extends EventEmitter implements RealtimeProvider {\n /**\n * Declares to the multi-engine router that channel frames can be handled\n * here. Read by `createRoutedRealtimeService`, which otherwise would have to\n * guess — and guessed \"the default provider\", whichever engine that is.\n */\n public readonly supportsChannels = true;\n\n private clients = new Map<string, WebSocket>();\n\n // Broadcast channels: channel name → set of client IDs\n private channels = new Map<string, Set<string>>();\n\n // Presence: channel → Map<clientId, { state, lastSeen }>\n private presence = new Map<string, Map<string, { state: Record<string, unknown>; lastSeen: number }>>();\n\n /**\n * Ordered, replayable history for channels that opt into it.\n *\n * Undefined until {@link configureChannelHistory} is called, and inert even\n * then unless retention rules were supplied — so presence and ephemeral\n * notification channels never touch it. See `channel-history.ts`.\n */\n private channelHistory?: ChannelHistoryStore;\n\n /**\n * One promise chain per retained channel, so that assigning a sequence\n * number and fanning the message out happen in the same order for every\n * message on that channel.\n *\n * Without it, two concurrent broadcasts can be numbered 4 and 5 by the\n * database and still reach subscribers as 5 then 4 — live order and replay\n * order would disagree, which is exactly the divergence sequence numbers\n * are supposed to rule out. Keyed by channel, so unrelated channels never\n * wait on each other.\n */\n private channelSendQueues = new Map<string, Promise<void>>();\n\n /**\n * Cross-instance transport for channel frames and presence.\n *\n * Defaults to the memory bus, which publishes nowhere — so a single-instance\n * deployment runs the same fan-out it always did, with one resolved promise\n * per broadcast for company. See `channel-bus/ChannelBus.ts`.\n */\n private bus: ChannelBus = new MemoryChannelBus();\n\n /**\n * The shared presence roster, present only when a real bus is active.\n *\n * Fan-out alone is not enough for presence: `presence_state` has to answer\n * with everyone in the channel, and per-process maps can only answer for\n * this replica's clients. See `channel-presence.ts`.\n */\n private presenceStore?: ChannelPresenceStore;\n\n /** Sweeps roster rows left behind by instances that stopped heartbeating. */\n private presenceSweepInterval?: ReturnType<typeof setInterval>;\n\n /**\n * Channels whose oversized ephemeral broadcasts have already been reported,\n * so a hot channel logs the problem once rather than once per message.\n */\n private oversizedBroadcastWarned = new Set<string>();\n\n /**\n * Optional narrowing on top of the membership floor — see\n * {@link ChannelAuthorizer}. Unset by default, which leaves membership as\n * the whole of the rule.\n */\n private channelAuthorizer?: ChannelAuthorizer;\n\n /**\n * Whether a notification from another instance has ever arrived.\n *\n * The entity LISTEN handler sees a foreign `sid` on every cross-instance\n * change, which is proof that this deployment runs more than one pod — the\n * one fact needed to tell \"the memory bus is fine here\" from \"broadcast and\n * presence silently reach a fraction of your users\".\n */\n private foreignInstanceSeen = false;\n\n /** So the multi-pod memory-bus warning is emitted once, not once per join. */\n private memoryBusWarned = false;\n\n private presenceInterval?: ReturnType<typeof setInterval>;\n private static readonly PRESENCE_TIMEOUT_MS = 30000; // 30s\n /** How often stale roster rows from other instances are reaped. */\n private static readonly PRESENCE_SWEEP_INTERVAL_MS = 10000; // 10s\n private dataService: DataService;\n // Enhanced subscriptions storage with full request parameters\n private _subscriptions = new Map<string, Subscription>();\n\n // Add callback storage for DataDriver subscriptions\n private subscriptionCallbacks = new Map<string, (data: Record<string, unknown>[] | Record<string, unknown> | null) => void>();\n\n private driver?: DataDriver;\n\n // ── Cross-instance LISTEN/NOTIFY ──\n /** Unique identifier for this process instance, used to skip own notifications. */\n private readonly instanceId = `inst_${randomUUID().slice(0, 8)}`;\n /** Dedicated pg.Client for LISTEN (outside the Drizzle pool). */\n private listenClient?: PgClient;\n /** Connection string used for reconnecting the LISTEN client. */\n private listenConnectionString?: string;\n /** Whether cross-instance broadcasting is active. */\n private broadcasting = false;\n /** Reconnection timer handle. */\n private reconnectTimer?: ReturnType<typeof setTimeout>;\n /** Debounce timers for collection refetches to prevent refetch storms. */\n private refetchTimers = new Map<string, ReturnType<typeof setTimeout>>();\n /** Debounce window (ms) for coalescing rapid row updates into a single correctness refetch. */\n private static readonly REFETCH_DEBOUNCE_MS = 300;\n\n // ── Database-level Change Data Capture (CDC) ──\n /** Dedicated LISTEN client for DB-level change events (undefined unless CDC is enabled). */\n private cdcListener?: CdcListener;\n /** Whether database-level CDC is the active cross-instance change source. */\n private cdcActive = false;\n /** Junction table → the child lists its rows belong to, built when CDC starts. */\n private junctionLinkMap?: Map<string, JunctionLink[]>;\n\n /** Reverse lookup: `schema.table` (and bare `table`) → collection, built when CDC starts. */\n private cdcTableMap?: Map<string, CollectionConfig>;\n /**\n * Short-lived record of `path/id` keys this instance just fanned out via the\n * app path (a Rebase-API mutation). When CDC echoes the same committed change\n * back to *this* instance, we suppress the duplicate — the change was already\n * delivered locally. Other instances have no such record, so they still\n * deliver the CDC event. External writes (psql, cron, SQL editor) never match\n * and always flow through. Keyed → expiry timestamp (ms).\n */\n private recentAppEmits = new Map<string, number>();\n /** How long an app-emit key suppresses its own CDC echo. Covers NOTIFY round-trip latency. */\n private static readonly CDC_DEDUP_WINDOW_MS = 5000;\n\n constructor(private db: NodePgDatabase<any>, private registry: PostgresCollectionRegistry) {\n super();\n this.dataService = new DataService(db, registry);\n }\n\n /**\n * Restricted role that auth-scoped refetches run as (via `SET LOCAL ROLE`)\n * so RLS `select` policies bind. Set by the bootstrapper alongside\n * `PostgresBackendDriver.rlsUserRole`; undefined when the connection\n * is already subject to RLS natively. Without this, realtime refetches\n * would leak rows the initial (isolated) fetch correctly hid.\n */\n public rlsUserRole?: string;\n\n /** Whether to emit verbose debug logs (disabled in production). */\n private static readonly DEBUG = process.env.NODE_ENV !== \"production\";\n private debugLog(...args: unknown[]) {\n if (RealtimeService.DEBUG) console.debug(...args);\n }\n\n setDataDriver(driver: DataDriver) {\n this.driver = driver;\n }\n\n // Make subscriptions accessible for DataDriver\n get subscriptions() {\n return this._subscriptions;\n }\n\n /**\n * Claim a delivery slot for a subscription, before doing the work.\n *\n * Returns the check to run immediately before delivering. It refuses in\n * three cases, all of which used to deliver:\n *\n * - **Out of order.** A newer refetch has already delivered, so this one is\n * stale — the subscriber would go back to the state before the change.\n * - **Unsubscribed.** The subscription was cancelled while the fetch was in\n * flight. The `has(subscriptionId)` check the debounced refetches ran\n * *before* the await cannot answer this; only a check after it can.\n * - **Replaced.** The same id can name a *different* subscription by the\n * time a fetch lands — a re-subscribe overwrites the map entry, and the\n * old filter's rows would be delivered to the new subscriber.\n *\n * The last two are identity, not presence: the map has to still hold *this\n * exact object*, not merely something under this id.\n */\n private beginDelivery(subscriptionId: string, subscription: Subscription): () => boolean {\n const seq = ++subscription.started;\n return () => {\n if (this._subscriptions.get(subscriptionId) !== subscription) return false;\n if (seq <= subscription.delivered) return false;\n subscription.delivered = seq;\n return true;\n };\n }\n\n // Add public method to register DataDriver subscriptions\n registerDataDriverSubscription(subscriptionId: string, subscription: {\n clientId: string;\n type: \"collection\" | \"single\";\n path: string;\n id?: string | number;\n collectionRequest?: StoredCollectionRequest;\n authContext?: SubscriptionAuthContext;\n }) {\n this.debugLog(\"📋 [RealtimeService] Registering DataDriver subscription:\", subscriptionId, subscription.authContext ? \"(with auth)\" : \"(no auth)\");\n this._subscriptions.set(subscriptionId, { ...subscription, started: 0, delivered: 0 });\n }\n\n // Add callback management methods\n addSubscriptionCallback(subscriptionId: string, callback: (data: Record<string, unknown>[] | Record<string, unknown> | null) => void) {\n this.debugLog(\"📋 [RealtimeService] Adding callback for subscription:\", subscriptionId);\n this.subscriptionCallbacks.set(subscriptionId, callback);\n }\n\n removeSubscriptionCallback(subscriptionId: string) {\n this.debugLog(\"📋 [RealtimeService] Removing callback for subscription:\", subscriptionId);\n this.subscriptionCallbacks.delete(subscriptionId);\n }\n\n // =============================================================================\n // RealtimeProvider Interface Methods\n // =============================================================================\n\n /**\n * Subscribe to collection changes (RealtimeProvider interface)\n */\n subscribeToCollection(\n subscriptionId: string,\n config: CollectionSubscriptionConfig,\n callback?: (rows: Record<string, unknown>[]) => void\n ): void {\n this._subscriptions.set(subscriptionId, {\n clientId: config.clientId,\n type: \"collection\",\n path: config.path,\n collectionRequest: {\n filter: config.filter as Record<string, unknown> | undefined,\n orderBy: config.orderBy,\n order: config.order,\n limit: config.limit,\n startAfter: config.startAfter as Record<string, unknown> | undefined,\n databaseId: config.databaseId,\n searchString: config.searchString,\n searchExplain: config.searchExplain\n },\n started: 0,\n delivered: 0\n });\n\n if (callback) {\n this.subscriptionCallbacks.set(subscriptionId, callback as (data: Record<string, unknown>[] | Record<string, unknown> | null) => void);\n }\n }\n\n /**\n * Subscribe to single row changes (RealtimeProvider interface)\n */\n subscribeToOne(\n subscriptionId: string,\n config: SingleSubscriptionConfig,\n callback?: (row: Record<string, unknown> | null) => void\n ): void {\n this._subscriptions.set(subscriptionId, {\n clientId: config.clientId,\n type: \"single\",\n path: config.path,\n id: config.id,\n started: 0,\n delivered: 0\n });\n\n if (callback) {\n this.subscriptionCallbacks.set(subscriptionId, callback as (data: Record<string, unknown>[] | Record<string, unknown> | null) => void);\n }\n }\n\n /**\n * Unsubscribe from a subscription (RealtimeProvider interface)\n */\n unsubscribe(subscriptionId: string): void {\n this._subscriptions.delete(subscriptionId);\n this.subscriptionCallbacks.delete(subscriptionId);\n }\n\n // =============================================================================\n // WebSocket Client Management\n // =============================================================================\n\n addClient(clientId: string, ws: WebSocket) {\n this.clients.set(clientId, ws);\n\n ws.on(\"close\", () => {\n this.removeClient(clientId);\n });\n\n ws.on(\"error\", (error) => {\n logger.error(\"WebSocket error for client\", { detail: clientId, error });\n this.removeClient(clientId);\n });\n }\n\n // Public method to handle messages from external sources (like main WebSocket handler)\n async handleClientMessage(clientId: string, message: WebSocketMessage, authContext?: SubscriptionAuthContext) {\n await this.handleMessage(clientId, message, authContext);\n }\n\n async removeClient(clientId: string) {\n this.clients.delete(clientId);\n\n // Remove all subscriptions, callbacks, and pending refetch timers for this client\n for (const [subscriptionId, subscription] of this._subscriptions.entries()) {\n if (subscription.clientId === clientId) {\n this._subscriptions.delete(subscriptionId);\n this.subscriptionCallbacks.delete(subscriptionId);\n\n // Cancel any pending debounced refetch timers\n for (const prefix of [\"ws_\", \"drv_\", \"wse_\", \"drve_\"]) {\n const key = `${prefix}${subscriptionId}`;\n const timer = this.refetchTimers.get(key);\n if (timer) { clearTimeout(timer); this.refetchTimers.delete(key); }\n }\n }\n }\n\n // The shared rows go before the announcement, not after it. Every\n // `removePresence` below publishes a departure — to local members and\n // over the bus — while `sendPresenceState` answers a *newly arriving*\n // subscriber from this table. Announcing first leaves a window where the\n // table still lists someone who has left: a client hydrating inside it is\n // handed the ghost, and the diff that would have corrected it was\n // broadcast before that client existed, so it holds the ghost until the\n // TTL sweep rather than for the length of one statement.\n //\n // One statement for every channel the client was in, rather than one per\n // channel below — a disconnect is the common case, not a rare one. The\n // subscription and timer cleanup above stays synchronous on purpose: it\n // is what leaks if the database is slow, and it owes nothing to the\n // shared table. With no bus configured this is a no-op that never awaits\n // a query.\n await this.presenceStoreOp(() => this.presenceStore!.removeClient(clientId), \"client removal\");\n\n // Remove from all broadcast channels\n for (const [channel, members] of this.channels.entries()) {\n if (members.has(clientId)) {\n members.delete(clientId);\n this.removePresence(clientId, channel, { skipStore: true });\n if (members.size === 0) this.channels.delete(channel);\n }\n }\n\n // Remove from all presence channels\n for (const [channel] of this.presence) {\n this.removePresence(clientId, channel, { skipStore: true });\n }\n }\n\n private async handleMessage(clientId: string, message: WebSocketMessage, authContext?: SubscriptionAuthContext) {\n const payload = message.payload as Record<string, unknown> | undefined;\n switch (message.type) {\n case \"subscribe_collection\":\n await this.handleCollectionSubscription(clientId, message.payload as RealTimeListenCollectionProps, authContext);\n break;\n case \"subscribe_one\":\n await this.handleEntitySubscription(clientId, message.payload as RealTimeListenEntityProps, authContext);\n break;\n case \"unsubscribe\":\n await this.handleUnsubscribe(clientId, message.subscriptionId!);\n break;\n\n // ── Broadcast Channels & Presence ──\n //\n // One arm for all of them, because every one has to pass the same\n // gate and a switch with seven arms is a place to forget it once.\n // See `handleChannelMessage`.\n case \"join_channel\":\n case \"leave_channel\":\n case \"broadcast\":\n case \"channel_history\":\n case \"presence_track\":\n case \"presence_untrack\":\n case \"presence_state\":\n await this.handleChannelMessage(clientId, message.type, payload, authContext);\n break;\n\n default:\n this.sendError(clientId, \"Unknown message type \" + message.type, message.subscriptionId);\n }\n }\n\n private async handleCollectionSubscription(clientId: string, request: RealTimeListenCollectionProps, authContext?: SubscriptionAuthContext) {\n const subscriptionId = request.subscriptionId;\n\n try {\n // Early validation: ensure the requested collection exists in the registry\n const collection = this.registry.getCollectionByPath(request.path);\n if (!collection) {\n const registered = this.registry.getCollections().map(c => c.slug).join(\", \");\n const msg = `Collection not found: '${request.path}'. Registered: [${registered}]`;\n logger.error(`[RealtimeService] ${msg}`);\n this.sendError(clientId, msg, subscriptionId);\n return;\n }\n\n // A vector search cannot be served here, and the parameter used to\n // be read for one thing only — the limit default below — and then\n // dropped: the stored request carries no `vectorSearch` and the\n // refetch has no branch for one. So `.vectorSearch(…).listen()`\n // delivered an ordinary `id DESC` listing, with no `_distance` and\n // no error, forever. Refusing says what the silence did not.\n if (request.vectorSearch) {\n const msg =\n \"Realtime subscriptions do not support vector search: a subscription is re-run on every \" +\n \"matching write, and nothing here computes distances. Use `.vectorSearch(...).find()` for \" +\n \"the query, and subscribe without it if you need live updates.\";\n logger.warn(`[RealtimeService] ${msg}`);\n this.sendError(clientId, msg, subscriptionId, \"VECTOR_SEARCH_NOT_LIVE\");\n return;\n }\n\n // Bound the client-supplied limit with the SAME guarantee the REST\n // ingress applies (`resolveClientListLimit`): default an absent\n // limit by mode, refuse one above the ceiling. A subscription is\n // re-fetched on every matching write, so an unbounded one is a DoS\n // amplified per write — resolve it once and reuse for the stored\n // request and the initial fetch.\n //\n // Refusing matters more here than on the REST route: a\n // `collection_update` frame carries rows and nothing else — no\n // `total`, no `hasMore` — so a subscriber handed a quietly smaller\n // page has no way at all to learn it is not seeing the collection.\n let boundedLimit: number;\n try {\n boundedLimit = resolveClientListLimit(request.limit);\n } catch (e) {\n if (!(e instanceof ListLimitError)) throw e;\n logger.warn(`[RealtimeService] Refused subscription to '${request.path}': ${e.message}`);\n this.sendError(clientId, e.message, subscriptionId, \"INVALID_LIMIT\");\n return;\n }\n\n // The sort arrives as whatever JSON the client put in the frame, so\n // its *shape* is checked here the way the REST ingress checks the\n // query parameter. Unchecked, a malformed entry reads as a field\n // name that resolves to no column, and under lenient unknown-field\n // handling the subscription then streams rows in no order at all\n // while reporting nothing wrong.\n let orderBy: OrderByTuple[] | undefined;\n try {\n orderBy = parseOrderBySpecStrict(request.orderBy, request.order);\n } catch (e) {\n if (!(e instanceof OrderBySpecError)) throw e;\n logger.warn(`[RealtimeService] Refused subscription to '${request.path}': ${e.message}`);\n this.sendError(clientId, e.message, subscriptionId, e.code);\n return;\n }\n\n // Store subscription with full request parameters and auth context for RLS\n const subscription: Subscription = {\n clientId,\n type: \"collection\",\n path: request.path,\n collectionRequest: {\n filter: request.filter,\n logical: request.logical,\n orderBy,\n order: request.order,\n limit: boundedLimit,\n offset: request.offset,\n startAfter: request.startAfter as Record<string, unknown> | undefined,\n databaseId: request.collection?.databaseId,\n searchString: request.searchString,\n searchExplain: request.searchExplain\n },\n authContext,\n started: 0,\n delivered: 0\n };\n this._subscriptions.set(subscriptionId, subscription);\n\n // The subscription is registered before this fetch runs, so a write\n // arriving in that window starts a refetch of its own — with nothing\n // ordering the two. Claim a slot first: this fetch is the oldest, so\n // if the refetch answers first, this one no longer delivers.\n const canDeliver = this.beginDelivery(subscriptionId, subscription);\n\n // Send initial data. Built from the request the subscription just\n // stored, so the first answer and every refetch after it cannot\n // describe different queries.\n const rows = await this.fetchCollectionWithAuth(\n request.path,\n subscription.collectionRequest!,\n authContext\n );\n\n if (canDeliver()) {\n this.sendCollectionUpdate(clientId, subscriptionId, rows, request.path);\n }\n\n } catch (error) {\n const sanitized = sanitizeErrorForClient(error, request.path);\n this.sendError(clientId, sanitized.message, subscriptionId, sanitized.code);\n }\n }\n\n private async handleEntitySubscription(clientId: string, request: RealTimeListenEntityProps, authContext?: SubscriptionAuthContext) {\n const subscriptionId = request.subscriptionId;\n\n try {\n // Early validation: ensure the requested collection exists in the registry\n const collection = this.registry.getCollectionByPath(request.path);\n if (!collection) {\n const registered = this.registry.getCollections().map(c => c.slug).join(\", \");\n const msg = `Collection not found: '${request.path}'. Registered: [${registered}]`;\n logger.error(`[RealtimeService] ${msg}`);\n this.sendError(clientId, msg, subscriptionId);\n return;\n }\n\n // Store subscription in memory with auth context for RLS\n const subscription: Subscription = {\n clientId,\n type: \"single\",\n path: request.path,\n id: request.id,\n authContext,\n started: 0,\n delivered: 0\n };\n this._subscriptions.set(subscriptionId, subscription);\n\n // Same race as the collection case: a write landing between the\n // registration above and this fetch starts a refetch that can answer\n // first, and this one must not overwrite it afterwards.\n const canDeliver = this.beginDelivery(subscriptionId, subscription);\n\n // Send initial data\n const row = await this.fetchEntityWithAuth(\n request.path,\n String(request.id),\n authContext\n );\n\n if (canDeliver()) {\n this.sendSingleUpdate(clientId, subscriptionId, row || null);\n }\n\n } catch (error) {\n const sanitized = sanitizeErrorForClient(error, request.path);\n this.sendError(clientId, sanitized.message, subscriptionId, sanitized.code);\n }\n }\n\n private async handleUnsubscribe(_clientId: string, subscriptionId: string) {\n this._subscriptions.delete(subscriptionId);\n this.subscriptionCallbacks.delete(subscriptionId);\n // Cancel any pending debounced refetch\n for (const prefix of [\"ws_\", \"drv_\", \"wse_\", \"drve_\"]) {\n const key = `${prefix}${subscriptionId}`;\n const timer = this.refetchTimers.get(key);\n if (timer) { clearTimeout(timer); this.refetchTimers.delete(key); }\n }\n }\n\n /**\n * Enhanced notification method that handles nested relation updates.\n * @param broadcast When true (default), also sends a pg_notify so other instances\n * pick up the change. Set to false when handling an incoming\n * cross-instance notification to avoid infinite loops.\n * @param origin `\"app\"` (default) — a Rebase-API mutation on this instance;\n * `\"cdc\"` — a database-level change observed via CDC (any writer,\n * any instance). The origin drives de-duplication: an app emit\n * records the change so this instance can suppress the matching\n * CDC echo, while an unmatched CDC event is delivered normally.\n */\n async notifyUpdate(path: string, id: string, row: Record<string, unknown> | null, databaseId?: string, broadcast = true, origin: \"app\" | \"cdc\" = \"app\") {\n this.debugLog(\"🔔 [RealtimeService] notifyUpdate called for path:\", path, \"id:\", id, \"isDelete:\", row === null, \"origin:\", origin);\n\n // De-duplicate against database-level CDC. The app path (a mutation made\n // through the Rebase API) fans out locally AND, once CDC is active, the\n // same committed change is echoed back to this instance via the WAL /\n // trigger stream. Record app emits so we can drop that echo here; deliver\n // any CDC event we did not originate (external writes, other instances).\n if (this.cdcActive) {\n const key = this.dedupKey(path, id, databaseId);\n if (origin === \"cdc\") {\n if (this.consumeAppEmit(key)) {\n this.debugLog(\"🔁 [RealtimeService] Suppressing CDC echo of local app mutation:\", key);\n return;\n }\n } else {\n this.markAppEmit(key);\n }\n }\n\n // Get all paths that need to be notified - the direct path plus any parent paths\n const pathsToNotify = [path];\n\n // If this is a nested relation path (like \"posts/70/tags\"), also notify parent paths\n if (path.includes(\"/\") && path.split(\"/\").length > 1) {\n const parentPaths = this.getParentPaths(path);\n pathsToNotify.push(...parentPaths);\n this.debugLog(`🔗 [RealtimeService] Nested path detected. Will notify paths: ${pathsToNotify.join(\", \")}`);\n }\n\n // Process each path that needs notification\n for (const notifyPath of pathsToNotify) {\n await this.notifyPathUpdate(notifyPath, path, id, row, databaseId);\n }\n\n // Broadcast to other instances via pg_notify (only for local mutations).\n // When CDC is active it IS the cross-instance channel — every instance\n // observes every commit through the change stream — so the legacy\n // per-mutation broadcast is redundant (and would double-deliver). Skip it.\n if (broadcast && this.broadcasting && !this.cdcActive) {\n try {\n await this.broadcastChange(path, id, databaseId);\n } catch (err) {\n logger.error(\"❌ [RealtimeService] Failed to broadcast change via pg_notify\", { error: err });\n }\n }\n\n this.debugLog(\"🔔 [RealtimeService] notifyUpdate completed for path:\", path);\n }\n\n /**\n * Notify subscriptions for a specific path.\n *\n * **A subscriber only ever receives rows re-read under its own scope.**\n * `row` is used to decide *that* something changed, never to say *what* —\n * every delivery below goes through a refetch that binds the subscription's\n * own auth context.\n *\n * It used to be conditional. The CDC path already did the right thing: it\n * discards the captured tuple and emits `{_rebase_invalidated: true}`, and\n * that marker selected the refetch branch. But the marker is produced in\n * exactly two places, and the *other* side of each branch here shipped the\n * row it was handed straight to the socket. Two of the three entry paths\n * took that side — every API mutation (`PostgresBackendDriver.save` passes\n * the row it just wrote, read under the **writer's** scope) and the legacy\n * cross-instance LISTEN handler (which re-reads on the owner connection,\n * bypassing RLS altogether). Path matching was the only filter applied: the\n * subscription's own `filter`/`logical` was never evaluated, and any\n * `afterRead` redaction was the writer's rather than the reader's.\n *\n * A single-row subscription was the sharpest case. `subscribe_one` on a row\n * RLS denies is accepted and answered `null`; the next update then pushed\n * the full row with no later correction. The collection variant was merely\n * papered over ~300 ms later by the debounced refetch — after the bytes had\n * already reached the browser.\n *\n * The same defect was found and fixed on the Mongo driver in `065e2b615`\n * (see `packages/server-mongo/test/realtime-authorization.test.ts`); this is\n * the Postgres half, stated as one rule rather than three patched branches.\n *\n * The cost is the instant row-level patch that used to precede the refetch:\n * cross-tab feedback now waits for the debounce. That is the price of not\n * being able to know, without asking the database as this subscriber,\n * whether this subscriber may see the row at all.\n */\n private async notifyPathUpdate(notifyPath: string, originalPath: string, id: string, row: Record<string, unknown> | null, _databaseId?: string) {\n this.debugLog(`📡 [RealtimeService] Notifying path: ${notifyPath} (original: ${originalPath})`);\n\n // Find all relevant subscriptions for this specific path\n const allSubscriptions = Array.from(this._subscriptions.entries()).filter(([, sub]) => {\n const isPathMatch = sub.path === notifyPath;\n\n // For row subscriptions, check if the id matches (only for exact path matches)\n if (sub.type === \"single\") {\n return isPathMatch && (notifyPath === originalPath ? sub.id === id : true);\n }\n // For collection subscriptions, it's always relevant if the path matches\n if (sub.type === \"collection\") {\n return isPathMatch;\n }\n return false;\n });\n\n this.debugLog(`📡 [RealtimeService] Found ${allSubscriptions.length} subscriptions for path: ${notifyPath}`);\n\n // Separate WebSocket subscriptions from DataDriver callback subscriptions\n const webSocketSubscriptions = allSubscriptions.filter(([, sub]) =>\n sub.clientId !== \"driver\" && this.clients.has(sub.clientId)\n );\n\n const driverSubscriptions = allSubscriptions.filter(([subscriptionId, sub]) =>\n sub.clientId === \"driver\" && this.subscriptionCallbacks.has(subscriptionId)\n );\n\n // Handle WebSocket subscriptions\n for (const [subscriptionId, subscription] of webSocketSubscriptions) {\n try {\n if (subscription.type === \"single\" && notifyPath === originalPath) {\n this.debouncedSingleRefetch(subscriptionId, notifyPath, id, subscription);\n } else if (subscription.type === \"collection\" && subscription.collectionRequest) {\n this.debouncedCollectionRefetch(subscriptionId, notifyPath, subscription);\n }\n } catch (error) {\n const sanitized = sanitizeErrorForClient(error, notifyPath);\n this.sendError(subscription.clientId, sanitized.message, subscriptionId, sanitized.code);\n }\n }\n\n // Handle DataDriver callback subscriptions\n for (const [subscriptionId, subscription] of driverSubscriptions) {\n try {\n const callback = this.subscriptionCallbacks.get(subscriptionId);\n if (!callback) continue;\n\n if (subscription.type === \"single\" && notifyPath === originalPath) {\n this.debouncedSingleDriverRefetch(subscriptionId, notifyPath, id, subscription, callback);\n } else if (subscription.type === \"collection\" && subscription.collectionRequest) {\n // Debounce collection refetches for DataDriver subscriptions too\n this.debouncedDriverRefetch(subscriptionId, notifyPath, subscription, callback);\n }\n } catch (error) {\n logger.error(`❌ [RealtimeService] Error processing DataDriver subscription ${subscriptionId}`, { error: error });\n }\n }\n }\n\n /**\n * Debounce a collection refetch for a WebSocket subscription.\n * Coalesces rapid row mutations into a single database query.\n */\n private debouncedCollectionRefetch(\n subscriptionId: string,\n notifyPath: string,\n subscription: Subscription\n ) {\n const timerKey = `ws_${subscriptionId}`;\n const existing = this.refetchTimers.get(timerKey);\n if (existing) clearTimeout(existing);\n\n this.refetchTimers.set(timerKey, setTimeout(async () => {\n this.refetchTimers.delete(timerKey);\n // Cheap bail before spending a query: the client may have\n // disconnected, or re-subscribed under the same id. It is only an\n // optimisation — `canDeliver()` after the await is what makes the\n // delivery safe, because the same things can happen *during* it.\n if (this._subscriptions.get(subscriptionId) !== subscription) return;\n // Claimed here rather than when the timer was scheduled: the\n // debounce coalesces, and no work exists to order until it fires.\n const canDeliver = this.beginDelivery(subscriptionId, subscription);\n try {\n const rows = await this.fetchCollectionWithAuth(notifyPath, subscription.collectionRequest!, subscription.authContext);\n if (canDeliver()) {\n this.sendCollectionUpdate(subscription.clientId, subscriptionId, rows, notifyPath);\n }\n } catch (error) {\n const sanitized = sanitizeErrorForClient(error, notifyPath);\n this.sendError(subscription.clientId, sanitized.message, subscriptionId, sanitized.code);\n }\n }, RealtimeService.REFETCH_DEBOUNCE_MS));\n }\n\n /**\n * Debounce a collection refetch for a DataDriver callback subscription.\n */\n private debouncedDriverRefetch(\n subscriptionId: string,\n notifyPath: string,\n subscription: Subscription,\n callback: (data: Record<string, unknown>[] | Record<string, unknown> | null) => void\n ) {\n const timerKey = `drv_${subscriptionId}`;\n const existing = this.refetchTimers.get(timerKey);\n if (existing) clearTimeout(existing);\n\n this.refetchTimers.set(timerKey, setTimeout(async () => {\n this.refetchTimers.delete(timerKey);\n if (this._subscriptions.get(subscriptionId) !== subscription) return;\n const canDeliver = this.beginDelivery(subscriptionId, subscription);\n try {\n const rows = await this.fetchCollectionWithAuth(notifyPath, subscription.collectionRequest!, subscription.authContext);\n if (canDeliver()) callback(rows);\n } catch (error) {\n logger.error(`❌ [RealtimeService] Error in debounced driver refetch for ${subscriptionId}`, { error: error });\n }\n }, RealtimeService.REFETCH_DEBOUNCE_MS));\n }\n\n /**\n * Fetch a collection with optional RLS auth context.\n * When authContext is provided, the fetch runs inside a transaction\n * with set_config calls so PostgreSQL RLS policies are enforced.\n */\n private async fetchCollectionWithAuth(\n notifyPath: string,\n collectionRequest: StoredCollectionRequest,\n authContext?: SubscriptionAuthContext\n ): Promise<Record<string, unknown>[]> {\n if (this.driver) {\n const collection = this.registry.getCollectionByPath(notifyPath);\n const fetchFn = async () => this.driver!.fetchCollection({\n path: notifyPath,\n collection: collection,\n filter: collectionRequest.filter as FetchCollectionProps[\"filter\"],\n logical: collectionRequest.logical,\n orderBy: collectionRequest.orderBy,\n order: collectionRequest.order,\n limit: collectionRequest.limit,\n offset: collectionRequest.offset,\n startAfter: collectionRequest.startAfter,\n searchString: collectionRequest.searchString,\n searchExplain: collectionRequest.searchExplain\n });\n\n // Always wrap in a transaction with session vars, defaulting to anonymous context if missing.\n // Refetches are reads: apply the same GUCs + reader-role downgrade as the\n // driver's read path, so realtime cannot leak rows the initial fetch hid.\n const activeAuth = authContext || { uid: ANONYMOUS_USER_ID,\nroles: [\"anon\"] };\n return await this.db.transaction(async (tx) => {\n await applyAuthContext(tx, { uid: activeAuth.uid, roles: activeAuth.roles }, this.rlsUserRole);\n const txEntityService = new DataService(tx, this.registry);\n let fetchedEntities;\n if (collectionRequest.searchString) {\n fetchedEntities = await txEntityService.searchRows(\n notifyPath,\n collectionRequest.searchString,\n {\n filter: collectionRequest.filter as FilterValues<string>,\n // The subscription stored a group; the search branch\n // did not pass it on, so a filtered live search\n // widened to every row matching the text.\n logical: collectionRequest.logical,\n orderBy: collectionRequest.orderBy,\n order: collectionRequest.order,\n limit: collectionRequest.limit,\n databaseId: collectionRequest.databaseId,\n searchExplain: collectionRequest.searchExplain\n }\n );\n } else {\n fetchedEntities = await txEntityService.fetchCollection(notifyPath, {\n filter: collectionRequest.filter as FilterValues<string>,\n logical: collectionRequest.logical,\n orderBy: collectionRequest.orderBy,\n order: collectionRequest.order,\n limit: collectionRequest.limit,\n offset: collectionRequest.offset,\n startAfter: collectionRequest.startAfter,\n databaseId: collectionRequest.databaseId\n });\n }\n\n // Re-apply `afterRead` lifecycle hooks to ensure consistent data structures\n // between the initial driver fetch and this RLS-bound refetch.\n const registryCollection = this.registry.getCollectionByPath(notifyPath);\n const resolvedCollection = collection ? { ...collection,\n...registryCollection } as CollectionConfig : registryCollection as CollectionConfig;\n\n const callbacks = resolvedCollection?.callbacks;\n const globalCallbacks = this.registry?.getGlobalCallbacks();\n const propertyCallbacks = resolvedCollection?.properties ? buildPropertyCallbacks(resolvedCollection.properties) : undefined;\n\n if (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead) {\n const contextForCallback = {\n user: { uid: activeAuth.uid,\nroles: activeAuth.roles },\n driver: this.driver,\n data: (this.driver && \"data\" in this.driver) ? (this.driver as DataDriverWithData).data : undefined\n } as unknown as RebaseCallContext;\n\n return await Promise.all(fetchedEntities.map(async (fetchedRow) => {\n let processedEntity = fetchedRow;\n // 1. Global callbacks first\n if (globalCallbacks?.afterRead) {\n processedEntity = await globalCallbacks.afterRead({\n collection: resolvedCollection,\n path: notifyPath,\n row: processedEntity,\n context: contextForCallback\n }) ?? processedEntity;\n }\n // 2. Collection callbacks second\n if (callbacks?.afterRead) {\n processedEntity = await callbacks.afterRead({\n collection: resolvedCollection,\n path: notifyPath,\n row: processedEntity,\n context: contextForCallback\n }) ?? processedEntity;\n }\n // 3. Property callbacks third\n if (propertyCallbacks?.afterRead) {\n processedEntity = await propertyCallbacks.afterRead({\n collection: resolvedCollection,\n path: notifyPath,\n row: processedEntity,\n context: contextForCallback\n }) ?? processedEntity;\n }\n return processedEntity;\n }));\n }\n\n return fetchedEntities;\n });\n }\n\n // No driver — use dataService directly (no auth wrapping possible).\n // The `logical` group is carried here as well: this branch answers the\n // same subscription as the one above, and a fallback that drops a\n // condition returns *more* rows than the path it stands in for.\n if (collectionRequest.searchString) {\n return await this.dataService.searchRows(\n notifyPath,\n collectionRequest.searchString,\n {\n filter: collectionRequest.filter as FilterValues<string>,\n logical: collectionRequest.logical,\n orderBy: collectionRequest.orderBy,\n order: collectionRequest.order,\n limit: collectionRequest.limit,\n databaseId: collectionRequest.databaseId,\n searchExplain: collectionRequest.searchExplain\n }\n );\n }\n return await this.dataService.fetchCollection(notifyPath, {\n filter: collectionRequest.filter as FilterValues<string>,\n logical: collectionRequest.logical,\n orderBy: collectionRequest.orderBy,\n order: collectionRequest.order,\n limit: collectionRequest.limit,\n offset: collectionRequest.offset,\n startAfter: collectionRequest.startAfter,\n databaseId: collectionRequest.databaseId\n });\n }\n\n /**\n * Debounce an row refetch for a WebSocket subscription.\n */\n private debouncedSingleRefetch(\n subscriptionId: string,\n notifyPath: string,\n id: string,\n subscription: Subscription\n ) {\n const timerKey = `wse_${subscriptionId}`;\n const existing = this.refetchTimers.get(timerKey);\n if (existing) clearTimeout(existing);\n\n this.refetchTimers.set(timerKey, setTimeout(async () => {\n this.refetchTimers.delete(timerKey);\n if (this._subscriptions.get(subscriptionId) !== subscription) return;\n const canDeliver = this.beginDelivery(subscriptionId, subscription);\n try {\n const row = await this.fetchEntityWithAuth(notifyPath, id, subscription.authContext);\n if (canDeliver()) {\n this.sendSingleUpdate(subscription.clientId, subscriptionId, row || null);\n }\n } catch (error) {\n const sanitized = sanitizeErrorForClient(error, notifyPath);\n this.sendError(subscription.clientId, sanitized.message, subscriptionId, sanitized.code);\n }\n }, RealtimeService.REFETCH_DEBOUNCE_MS));\n }\n\n /**\n * Debounce an row refetch for a Driver callback subscription.\n */\n private debouncedSingleDriverRefetch(\n subscriptionId: string,\n notifyPath: string,\n id: string,\n subscription: Subscription,\n callback: (data: Record<string, unknown>[] | Record<string, unknown> | null) => void\n ) {\n const timerKey = `drve_${subscriptionId}`;\n const existing = this.refetchTimers.get(timerKey);\n if (existing) clearTimeout(existing);\n\n this.refetchTimers.set(timerKey, setTimeout(async () => {\n this.refetchTimers.delete(timerKey);\n if (this._subscriptions.get(subscriptionId) !== subscription) return;\n const canDeliver = this.beginDelivery(subscriptionId, subscription);\n try {\n const row = await this.fetchEntityWithAuth(notifyPath, id, subscription.authContext);\n if (canDeliver()) callback(row || null);\n } catch (error) {\n logger.error(`❌ [RealtimeService] Error in debounced row driver refetch for ${subscriptionId}`, { error: error });\n }\n }, RealtimeService.REFETCH_DEBOUNCE_MS));\n }\n\n /**\n * Fetch a single row with optional RLS auth context.\n */\n private async fetchEntityWithAuth(\n notifyPath: string,\n id: string | number,\n authContext?: SubscriptionAuthContext\n ): Promise<Record<string, unknown> | undefined> {\n if (this.driver) {\n const collection = this.registry.getCollectionByPath(notifyPath);\n const fetchFn = async () => this.driver!.fetchOne({\n path: notifyPath,\n id,\n collection\n });\n\n // Always wrap in a transaction with session vars, defaulting to anonymous context if missing.\n // Same read isolation as collection refetches: GUCs + reader-role downgrade.\n const activeAuth = authContext || { uid: ANONYMOUS_USER_ID,\nroles: [\"anon\"] };\n return await this.db.transaction(async (tx) => {\n await applyAuthContext(tx, { uid: activeAuth.uid, roles: activeAuth.roles }, this.rlsUserRole);\n const txEntityService = new DataService(tx, this.registry);\n let processedEntity = await txEntityService.fetchOne(notifyPath, id, collection?.databaseId);\n\n if (processedEntity) {\n const registryCollection = this.registry.getCollectionByPath(notifyPath);\n const resolvedCollection = collection ? { ...collection,\n...registryCollection } as CollectionConfig : registryCollection as CollectionConfig;\n\n const callbacks = resolvedCollection?.callbacks;\n const globalCallbacks = this.registry?.getGlobalCallbacks();\n const propertyCallbacks = resolvedCollection?.properties ? buildPropertyCallbacks(resolvedCollection.properties) : undefined;\n\n if (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead) {\n const contextForCallback = {\n user: { uid: activeAuth.uid,\nroles: activeAuth.roles },\n driver: this.driver,\n data: (this.driver && \"data\" in this.driver) ? (this.driver as DataDriverWithData).data : undefined\n } as unknown as RebaseCallContext;\n\n // 1. Global callbacks first\n if (globalCallbacks?.afterRead) {\n processedEntity = await globalCallbacks.afterRead({\n collection: resolvedCollection,\n path: notifyPath,\n row: processedEntity,\n context: contextForCallback\n }) ?? processedEntity;\n }\n // 2. Collection callbacks second\n if (callbacks?.afterRead) {\n processedEntity = await callbacks.afterRead({\n collection: resolvedCollection,\n path: notifyPath,\n row: processedEntity,\n context: contextForCallback\n }) ?? processedEntity;\n }\n // 3. Property callbacks third\n if (propertyCallbacks?.afterRead) {\n processedEntity = await propertyCallbacks.afterRead({\n collection: resolvedCollection,\n path: notifyPath,\n row: processedEntity,\n context: contextForCallback\n }) ?? processedEntity;\n }\n }\n }\n\n return processedEntity;\n });\n }\n\n return await this.dataService.fetchOne(notifyPath, id);\n }\n\n private sendCollectionUpdate(clientId: string, subscriptionId: string, rows: Record<string, unknown>[], path: string) {\n const message: CollectionUpdateMessage = {\n type: \"collection_update\",\n subscriptionId,\n rows: rows,\n pks: this.primaryKeysForPath(path)\n };\n this.sendMessage(clientId, message);\n }\n\n private sendSingleUpdate(clientId: string, subscriptionId: string, row: Record<string, unknown> | null) {\n const message: SingleUpdateMessage = {\n type: \"single_update\",\n subscriptionId,\n row: row\n };\n this.sendMessage(clientId, message);\n }\n\n /**\n * Send a lightweight row-level patch to a collection subscriber.\n * The client can merge this into its cached data for instant feedback.\n *\n * The key columns ride along: the patch names a row by address, and the\n * client has to find that row among the ones it cached — which carry\n * columns and no address. The SDK holds no collection config to derive one\n * from, so this is the only place the mapping can come from.\n */\n /** The key columns of the collection at `path`, if they can be resolved. */\n private primaryKeysForPath(path: string): PrimaryKeyInfo[] | undefined {\n try {\n const collection = this.registry.getCollectionByPath(path);\n if (!collection) return undefined;\n const keys = getPrimaryKeys(collection, this.registry);\n return keys.length > 0 ? keys : undefined;\n } catch {\n // `getCollectionByPath` throws on a path it cannot walk — and this\n // is called for parent paths too, which include entity paths like\n // `posts/1` that name no collection. Telling the subscriber nothing\n // is right here; letting it throw would drop the notification.\n return undefined;\n }\n }\n\n private sendError(clientId: string, error: string, subscriptionId?: string, code?: string) {\n const message = {\n type: \"error\" as const,\n subscriptionId,\n payload: {\n error: code ? { message: error, code } : error\n },\n error\n };\n this.sendMessage(clientId, message);\n }\n\n private sendMessage(clientId: string, message: CollectionUpdateMessage | SingleUpdateMessage | CollectionPatchMessage | { type: string; subscriptionId?: string; error?: string; payload?: unknown }) {\n const client = this.clients.get(clientId);\n if (client && client.readyState === WebSocket.OPEN) {\n client.send(JSON.stringify(message));\n }\n }\n\n /**\n * Extract parent paths from a nested path like \"posts/70/tags\"\n * Returns [\"posts\", \"posts/70\"] for the example above\n */\n private getParentPaths(path: string): string[] {\n const segments = path.split(\"/\").filter(s => s.length > 0);\n const parentPaths: string[] = [];\n\n // Build parent paths progressively\n for (let i = 1; i < segments.length; i += 2) {\n const parentPath = segments.slice(0, i).join(\"/\");\n if (parentPath) {\n parentPaths.push(parentPath);\n }\n\n // If there's an row ID, add the path including the row\n if (i + 1 < segments.length) {\n const pathWithEntity = segments.slice(0, i + 1).join(\"/\");\n parentPaths.push(pathWithEntity);\n }\n }\n\n return parentPaths;\n }\n\n // =============================================================================\n // Broadcast Channels\n // =============================================================================\n\n /**\n * Install a channel authorizer — see {@link ChannelAuthorizer}.\n *\n * Nothing in the framework calls this yet: it is the seam a rules API will\n * be built on, kept deliberately separate from the membership floor so the\n * floor holds whether or not anyone uses it.\n */\n setChannelAuthorizer(authorizer: ChannelAuthorizer | undefined): void {\n this.channelAuthorizer = authorizer;\n }\n\n /** Which action each channel frame is asking to perform. */\n private static readonly CHANNEL_ACTIONS: Record<string, ChannelAction> = {\n join_channel: \"join\",\n broadcast: \"broadcast\",\n channel_history: \"history\",\n presence_track: \"join\",\n presence_state: \"presence\"\n };\n\n /**\n * The one door every channel frame comes through.\n *\n * Returns synchronously — and so dispatches synchronously — unless an\n * authorizer is installed. That matters: a client sends `join_channel`,\n * `presence_state` and `channel_history` back to back on connect, and the\n * socket's message handler processes each frame up to its first `await`,\n * so a gate that always yielded would let the reads overtake the join that\n * is about to authorize them.\n */\n private handleChannelMessage(\n clientId: string,\n type: string,\n payload: Record<string, unknown> | undefined,\n authContext?: SubscriptionAuthContext\n ): void | Promise<void> {\n const channel = payload?.channel as string;\n\n // Leaving and untracking only ever remove the caller's own state, so\n // they need no permission — refusing them could only strand a client.\n if (type === \"leave_channel\") {\n this.leaveChannel(clientId, channel);\n return;\n }\n if (type === \"presence_untrack\") {\n this.removePresence(clientId, channel);\n return;\n }\n\n const action = RealtimeService.CHANNEL_ACTIONS[type];\n const allowed = this.authorizeChannelAction(clientId, channel, action, authContext);\n if (allowed === false) return;\n if (allowed === true) return this.dispatchChannelMessage(clientId, type, channel, payload);\n return allowed.then((ok) => {\n if (ok) return this.dispatchChannelMessage(clientId, type, channel, payload);\n });\n }\n\n /** Perform an already-authorized channel frame. */\n private dispatchChannelMessage(\n clientId: string,\n type: string,\n channel: string,\n payload: Record<string, unknown> | undefined\n ): void | Promise<void> {\n switch (type) {\n case \"join_channel\":\n this.joinChannel(clientId, channel);\n return;\n case \"broadcast\":\n this.broadcastToChannel(clientId, channel, payload?.event as string, payload?.payload);\n return;\n case \"channel_history\":\n return this.handleChannelHistoryRequest(\n clientId,\n channel,\n payload?.sinceSeq as number | undefined,\n payload?.limit as number | undefined\n );\n case \"presence_track\":\n // Auto-join the channel so presence works without a separate join\n this.joinChannel(clientId, channel);\n this.trackPresence(clientId, channel, payload?.state as Record<string, unknown> ?? {});\n return;\n case \"presence_state\":\n this.sendPresenceState(clientId, channel);\n return;\n }\n }\n\n /**\n * Decide whether a client may perform an action on a channel.\n *\n * **Membership is the floor.** Reading a channel's presence roster, replaying\n * its retained history and broadcasting into it all require that this client\n * has joined it. That is a low bar — joining is open to anyone who can name\n * the channel — but it is not the bar that was there before, which was none\n * at all: `channel_history` and `presence_state` answered any socket about\n * any channel, and a broadcast fanned out to members the sender had never\n * joined. Two internal tables (`rebase.channel_presence`,\n * `rebase.channel_messages`) are held outside RLS on the strength of this\n * check, so it fails closed: an authorizer that throws refuses the frame.\n *\n * Anything richer than membership belongs in a {@link ChannelAuthorizer};\n * this method is where it is consulted, and the only place.\n */\n private authorizeChannelAction(\n clientId: string,\n channel: string,\n action: ChannelAction,\n authContext?: SubscriptionAuthContext\n ): boolean | Promise<boolean> {\n // Joining is what establishes membership, so it cannot require it.\n if (action !== \"join\" && !this.channels.get(channel)?.has(clientId)) {\n this.denyChannelAction(clientId, channel, action, \"not a member of the channel\");\n return false;\n }\n\n const authorizer = this.channelAuthorizer;\n if (!authorizer) return true;\n\n let verdict: boolean | Promise<boolean>;\n try {\n verdict = authorizer({ channel, action, clientId, user: authContext });\n } catch (error) {\n logger.error(`❌ [Channels] Authorizer threw for ${action} on \"${channel}\" — refusing`, { error });\n this.denyChannelAction(clientId, channel, action, \"channel authorization failed\");\n return false;\n }\n\n if (typeof verdict === \"boolean\") {\n if (!verdict) this.denyChannelAction(clientId, channel, action, \"refused by the channel authorizer\");\n return verdict;\n }\n\n return verdict.then(\n (ok) => {\n if (!ok) this.denyChannelAction(clientId, channel, action, \"refused by the channel authorizer\");\n return ok;\n },\n (error) => {\n logger.error(`❌ [Channels] Authorizer rejected for ${action} on \"${channel}\" — refusing`, { error });\n this.denyChannelAction(clientId, channel, action, \"channel authorization failed\");\n return false;\n }\n );\n }\n\n /** Tell the client why its channel frame went nowhere, and say so in the log. */\n private denyChannelAction(clientId: string, channel: string, action: ChannelAction, reason: string): void {\n this.debugLog(`🚫 [Channels] Refused ${action} on \"${channel}\" for ${clientId}: ${reason}`);\n this.sendError(\n clientId,\n `Refused ${action} on channel \"${channel}\": ${reason}`,\n undefined,\n \"CHANNEL_FORBIDDEN\"\n );\n }\n\n /** Join a broadcast channel */\n joinChannel(clientId: string, channel: string): void {\n if (!this.channels.has(channel)) {\n this.channels.set(channel, new Set());\n }\n this.channels.get(channel)!.add(clientId);\n this.warnIfMemoryBusOnMultiplePods();\n this.debugLog(`📡 [Broadcast] Client ${clientId} joined channel: ${channel}`);\n }\n\n /**\n * Say something the first time channels are used on a deployment that is\n * demonstrably multi-pod while the bus is still the in-memory default.\n *\n * Every other warning in this subsystem covers a *configured* bus failing —\n * the case where the operator already knew a bus mattered. The common\n * misconfiguration is the opposite one: scaled to two replicas, never\n * touched `realtime.bus`, and broadcast and presence quietly serve a\n * fraction of the room. The evidence is already in the process, so use it.\n */\n private warnIfMemoryBusOnMultiplePods(): void {\n if (this.memoryBusWarned) return;\n if (this.bus.kind !== \"memory\" || !this.foreignInstanceSeen) return;\n this.memoryBusWarned = true;\n logger.warn(\n \"⚠️ [ChannelBus] Channels are in use with the in-memory bus, but notifications from another \" +\n \"instance have been seen — this deployment runs more than one process. Broadcast and presence \" +\n \"reach only the clients connected to this one. Set `realtime.bus` (or REALTIME_CHANNEL_BUS=postgres) \" +\n \"to make channels cross-instance.\"\n );\n }\n\n /** Leave a broadcast channel */\n leaveChannel(clientId: string, channel: string): void {\n const members = this.channels.get(channel);\n if (members) {\n members.delete(clientId);\n if (members.size === 0) this.channels.delete(channel);\n }\n // Also remove presence\n this.removePresence(clientId, channel);\n }\n\n /**\n * Broadcast a message to all clients in a channel except the sender.\n *\n * On a channel with no retention rule this is what it always was: a\n * synchronous fan-out to whoever is connected, with no sequence number, no\n * SQL and no await — the body below runs to completion before returning.\n *\n * On a retained channel the message is durably numbered first and only then\n * delivered, through a per-channel queue so that delivery order matches\n * sequence order. That ordering is the whole point: a client that catches up\n * with `sinceSeq` has to arrive at the same state as one that never\n * disconnected.\n */\n broadcastToChannel(clientId: string, channel: string, event: string, payload: unknown): void {\n const retention = this.channelHistory?.retentionFor(channel);\n if (!retention) {\n this.fanOutBroadcast(clientId, channel, event, payload);\n // Other instances get the same frame, but never before the clients\n // on this one: the local fan-out above is synchronous and the\n // publish is not, which is also what keeps the ephemeral path free\n // of any await for a single-instance deployment.\n this.publishBroadcast(clientId, channel, event, payload);\n return;\n }\n\n const previous = this.channelSendQueues.get(channel) ?? Promise.resolve();\n const next = previous\n // A failed predecessor must not poison the chain — the next message\n // on this channel is independent and still deserves to be sent.\n .catch(() => { /* already reported below */ })\n .then(() => this.persistAndFanOut(clientId, channel, event, payload, retention));\n\n this.channelSendQueues.set(channel, next);\n void next.finally(() => {\n // Only clear if nothing has queued behind us in the meantime.\n if (this.channelSendQueues.get(channel) === next) this.channelSendQueues.delete(channel);\n });\n }\n\n /**\n * Number a broadcast, store it, then deliver it.\n *\n * A message that cannot be stored is **not** delivered. Delivering it would\n * put it in front of live subscribers while leaving it absent from every\n * future replay — the two views of the channel would disagree permanently,\n * and no later message could repair the gap. Failing loudly to the sender\n * instead lets it retry, which for an operation stream is the only outcome\n * that keeps clients convergent.\n */\n private async persistAndFanOut(\n clientId: string,\n channel: string,\n event: string,\n payload: unknown,\n retention: ResolvedRetention\n ): Promise<void> {\n let seq: number;\n try {\n ({ seq } = await this.channelHistory!.append(channel, event, payload, clientId));\n } catch (error) {\n logger.error(`❌ [ChannelHistory] Could not persist broadcast on \"${channel}\" — message dropped`, { error });\n this.sendError(\n clientId,\n `Could not persist broadcast on retained channel \"${channel}\"`,\n undefined,\n \"CHANNEL_HISTORY_WRITE_FAILED\"\n );\n return;\n }\n\n this.fanOutBroadcast(clientId, channel, event, payload, seq);\n this.publishBroadcast(clientId, channel, event, payload, seq);\n\n try {\n await this.channelHistory!.prune(channel, retention);\n } catch (error) {\n // Retention is a housekeeping concern; the message is already\n // delivered and durable, so a failed prune must not surface as a\n // broadcast failure. It will be retried on the next message.\n logger.warn(`⚠️ [ChannelHistory] Prune failed for \"${channel}\"`, { error });\n }\n }\n\n /** Deliver a broadcast frame to every member of a channel but the sender. */\n private fanOutBroadcast(clientId: string, channel: string, event: string, payload: unknown, seq?: number): void {\n const members = this.channels.get(channel);\n if (!members) return;\n\n const message = JSON.stringify({\n type: \"broadcast\",\n channel,\n event,\n payload,\n ...(seq !== undefined ? { seq } : {})\n });\n\n for (const memberId of members) {\n if (memberId === clientId) continue; // Don't echo back to sender\n const ws = this.clients.get(memberId);\n if (ws && ws.readyState === WebSocket.OPEN) {\n ws.send(message);\n }\n }\n }\n\n // =============================================================================\n // Cross-Instance Channel Bus\n // =============================================================================\n\n /**\n * Install the transport that carries channel frames between instances.\n *\n * Called once at boot. A bus that cannot start is reported and replaced with\n * the memory bus: losing cross-instance fan-out degrades collaboration to\n * what it was before this existed, whereas refusing to boot takes the whole\n * backend down for it.\n */\n async configureChannelBus(bus: ChannelBus): Promise<void> {\n if (bus.kind === \"memory\") {\n this.bus = bus;\n return;\n }\n\n try {\n await bus.start((frame) => this.handleBusFrame(frame));\n } catch (error) {\n logger.warn(\n `⚠️ [ChannelBus] Could not start the \"${bus.kind}\" channel bus — channel broadcast and presence ` +\n \"stay per-instance. Clients served by different replicas will not see each other.\",\n { error }\n );\n await bus.stop().catch(() => { /* best effort */ });\n this.bus = new MemoryChannelBus();\n return;\n }\n\n this.bus = bus;\n\n // Presence needs shared *state*, not just shared fan-out — see\n // `channel-presence.ts`. It comes up with the bus and only with it.\n try {\n const store = new ChannelPresenceStore(this.db, this.instanceId);\n await store.ensureTables();\n this.presenceStore = store;\n this.ensurePresenceSweep();\n } catch (error) {\n logger.warn(\n \"⚠️ [ChannelBus] Could not create the shared presence table — presence rosters will only list \" +\n \"clients connected to this instance (broadcast is unaffected).\",\n { error }\n );\n this.presenceStore = undefined;\n }\n\n logger.info(\n `📡 [ChannelBus] Cross-instance channels active via ${bus.kind} (instanceId: ${this.instanceId}).`\n );\n }\n\n /** Which transport is in use — `\"memory\"` means per-instance only. */\n public getChannelBusKind(): ChannelBus[\"kind\"] {\n return this.bus.kind;\n }\n\n /**\n * Send a broadcast to the other instances.\n *\n * Fire-and-forget by design: the clients on this instance have already been\n * served, and a bus that is briefly unreachable must not turn a broadcast\n * into an error for the sender.\n */\n private publishBroadcast(clientId: string, channel: string, event: string, payload: unknown, seq?: number): void {\n if (this.bus.kind === \"memory\") return;\n\n const frame: ChannelBusFrame = {\n kind: \"broadcast\",\n sid: this.instanceId,\n channel,\n event,\n from: clientId,\n ...(seq !== undefined ? { seq } : {}),\n payload\n };\n\n // Postgres caps a NOTIFY payload at 8 KB. A retained message is already\n // durable and addressable, so it travels as a pointer and each receiver\n // reads the body back — the same shape as the entity path, which\n // notifies an address and refetches the row.\n if (frameByteLength(frame) > this.bus.maxFrameBytes) {\n if (seq === undefined) {\n this.reportOversizedBroadcast(clientId, channel);\n return;\n }\n void this.publishFrame({\n kind: \"broadcast_ref\",\n sid: this.instanceId,\n channel,\n from: clientId,\n seq\n });\n return;\n }\n\n void this.publishFrame(frame);\n }\n\n private async publishFrame(frame: ChannelBusFrame): Promise<void> {\n try {\n await this.bus.publish(frame);\n } catch (error) {\n logger.error(\"❌ [ChannelBus] Failed to publish frame — other instances did not receive it\", {\n detail: `${frame.kind} on \"${frame.channel}\"`,\n error\n });\n }\n }\n\n /**\n * Tell the sender that a message was delivered locally but nowhere else.\n *\n * Staying quiet here would be the worst option available: on one instance\n * the app works, on two it works for half the users, and nothing in the\n * logs connects the two. The fix is a one-liner in config — give the\n * channel a retention rule and the message travels as a pointer instead —\n * so the message says exactly that.\n */\n private reportOversizedBroadcast(clientId: string, channel: string): void {\n const remedy =\n `Add a retention rule for \"${channel}\" (realtime.channels) — retained messages travel by reference ` +\n \"and have no size limit.\";\n\n if (!this.oversizedBroadcastWarned.has(channel)) {\n this.oversizedBroadcastWarned.add(channel);\n logger.warn(\n `⚠️ [ChannelBus] A broadcast on ephemeral channel \"${channel}\" exceeds the ` +\n `${this.bus.maxFrameBytes}-byte limit of the ${this.bus.kind} bus and reached only this instance. ` +\n remedy\n );\n }\n this.sendError(\n clientId,\n `Broadcast on \"${channel}\" was too large to reach other instances. ${remedy}`,\n undefined,\n \"CHANNEL_BUS_PAYLOAD_TOO_LARGE\"\n );\n }\n\n /**\n * Deliver a frame published by another instance to this one's clients.\n *\n * Frames we published ourselves are dropped on arrival — the local fan-out\n * happened before the publish — exactly as the entity-change handler skips\n * its own `sid`.\n */\n private async handleBusFrame(frame: ChannelBusFrame): Promise<void> {\n if (frame.sid === this.instanceId) return;\n\n switch (frame.kind) {\n case \"broadcast\":\n this.fanOutBroadcast(frame.from ?? \"\", frame.channel, frame.event, frame.payload, frame.seq);\n return;\n\n case \"broadcast_ref\": {\n // Nothing to read back for: skip the query rather than pay for\n // a message no client here is waiting for.\n if (!this.channels.get(frame.channel)?.size) return;\n\n const entry = await this.channelHistory?.getBySeq(frame.channel, frame.seq);\n if (!entry) {\n logger.warn(\n `⚠️ [ChannelBus] Message ${frame.seq} on \"${frame.channel}\" is no longer retained — ` +\n \"clients on this instance will need to replay (channel_history) to catch up.\"\n );\n return;\n }\n this.fanOutBroadcast(frame.from ?? \"\", frame.channel, entry.event, entry.payload, entry.seq);\n return;\n }\n\n case \"presence_diff\":\n this.deliverPresenceDiff(frame.channel, frame.joins, frame.leaves);\n return;\n }\n }\n\n // =============================================================================\n // Channel History\n // =============================================================================\n\n /**\n * Install retention rules and create the tables they need.\n *\n * Safe to call with no rules (and safe not to call at all): the store stays\n * inert, no schema is created, and broadcast keeps its original\n * fire-and-forget path.\n */\n async configureChannelHistory(\n rules: ChannelRetentionRule[] | undefined,\n options?: { provision?: boolean }\n ): Promise<void> {\n // The store is built in every process, whether or not this one creates\n // the tables: retaining a message is what a process does when it\n // *publishes* to a retained channel, and a function handler publishes as\n // readily as a websocket client does. Only the DDL is owned.\n this.channelHistory = new ChannelHistoryStore(this.db, rules ?? []);\n if (!this.channelHistory.enabled) return;\n if (options?.provision === false) return;\n await this.channelHistory.ensureTables();\n }\n\n /** Whether any channel is configured to retain messages. */\n public isChannelHistoryEnabled(): boolean {\n return this.channelHistory?.enabled ?? false;\n }\n\n /**\n * Answer a client's catch-up request.\n *\n * A channel with no retention rule is answered with `retained: false`\n * rather than an empty list, so the client can tell \"you missed nothing\"\n * apart from \"this channel never keeps anything\" — the second means its\n * reconnect strategy has to be a full resync, and silence would leave it\n * guessing.\n */\n private async handleChannelHistoryRequest(\n clientId: string,\n channel: string,\n sinceSeq?: number,\n limit?: number\n ): Promise<void> {\n if (!channel) return;\n\n const retention = this.channelHistory?.retentionFor(channel);\n if (!retention) {\n this.sendChannelHistory(clientId, channel, [], false);\n return;\n }\n\n try {\n const { messages, latestSeq } = await this.channelHistory!.replay(channel, sinceSeq, limit);\n this.sendChannelHistory(clientId, channel, messages, true, latestSeq);\n } catch (error) {\n logger.error(`❌ [ChannelHistory] Replay failed for \"${channel}\"`, { error });\n this.sendError(clientId, `Could not replay history for channel \"${channel}\"`, undefined, \"CHANNEL_HISTORY_READ_FAILED\");\n }\n }\n\n private sendChannelHistory(\n clientId: string,\n channel: string,\n messages: ChannelHistoryEntry[],\n retained: boolean,\n latestSeq?: number\n ): void {\n const ws = this.clients.get(clientId);\n if (ws && ws.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({\n type: \"channel_history\",\n channel,\n messages,\n retained,\n ...(latestSeq !== undefined ? { latestSeq } : {})\n }));\n }\n }\n\n // =============================================================================\n // Presence\n // =============================================================================\n\n /**\n * Track presence in a channel.\n *\n * The client re-sends this every ~20s as a heartbeat against the 30s\n * timeout, so most calls carry the state that is already recorded. Those\n * refresh `last_seen` and stop there: re-announcing an unchanged state to\n * every instance would put a bus message per client per heartbeat on the\n * wire to tell everyone nothing happened.\n */\n trackPresence(clientId: string, channel: string, state: Record<string, unknown>): void {\n if (!this.presence.has(channel)) {\n this.presence.set(channel, new Map());\n }\n\n const channelPresence = this.presence.get(channel)!;\n const previous = channelPresence.get(clientId);\n const changed = !previous || JSON.stringify(previous.state) !== JSON.stringify(state);\n channelPresence.set(clientId, { state,\nlastSeen: Date.now() });\n\n // Refresh the shared roster on every heartbeat — that timestamp is what\n // tells other instances this client is still here.\n void this.presenceStoreOp(() => this.presenceStore!.track(channel, clientId, state), \"track\");\n\n // Broadcast join / state update to channel\n this.deliverPresenceDiff(channel, { [clientId]: state }, {});\n if (changed) {\n this.publishPresenceDiff(channel, { [clientId]: state }, {});\n }\n\n // Start cleanup interval if not running\n this.ensurePresenceCleanup();\n }\n\n /**\n * Remove presence from a channel.\n *\n * `skipStore` is for the socket-close path, which clears every channel at\n * once and then deletes the client's rows in a single statement instead of\n * one per channel.\n */\n removePresence(clientId: string, channel: string, options?: { skipStore?: boolean }): void {\n const channelPresence = this.presence.get(channel);\n if (!channelPresence) return;\n\n const entry = channelPresence.get(clientId);\n if (entry) {\n channelPresence.delete(clientId);\n this.deliverPresenceDiff(channel, {}, { [clientId]: entry.state });\n this.publishPresenceDiff(channel, {}, { [clientId]: entry.state });\n if (!options?.skipStore) {\n void this.presenceStoreOp(() => this.presenceStore!.remove(channel, clientId), \"remove\");\n }\n }\n\n if (channelPresence.size === 0) {\n this.presence.delete(channel);\n }\n }\n\n /**\n * Send the full roster for a channel to one client.\n *\n * Answered from the shared table when there is one, because \"who is in this\n * document?\" has a single answer that must not depend on which replica the\n * asker happens to be connected to. Without a bus there is nothing to share\n * and the local map *is* the roster — that path stays synchronous, which is\n * what it always was.\n */\n sendPresenceState(clientId: string, channel: string): void {\n if (!this.presenceStore) {\n this.sendPresenceStateMessage(clientId, channel, this.localPresences(channel));\n return;\n }\n\n void this.presenceStore.roster(channel)\n .then((presences) => {\n this.sendPresenceStateMessage(clientId, channel, presences);\n })\n .catch((error) => {\n // A roster the asker can act on beats none: fall back to the\n // clients we can see rather than leaving the request unanswered.\n logger.warn(`⚠️ [Presence] Could not read the shared roster for \"${channel}\" — answering with this instance's clients only.`, { error });\n this.sendPresenceStateMessage(clientId, channel, this.localPresences(channel));\n });\n }\n\n /** Presence of the clients connected to this instance. */\n private localPresences(channel: string): Record<string, Record<string, unknown>> {\n const channelPresence = this.presence.get(channel);\n const presences: Record<string, Record<string, unknown>> = {};\n if (channelPresence) {\n for (const [id, { state }] of channelPresence) {\n presences[id] = state;\n }\n }\n return presences;\n }\n\n private sendPresenceStateMessage(\n clientId: string,\n channel: string,\n presences: Record<string, Record<string, unknown>>\n ): void {\n const ws = this.clients.get(clientId);\n if (ws && ws.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({\n type: \"presence_state\",\n channel,\n presences\n }));\n }\n }\n\n /** Deliver a presence diff to this instance's members of the channel. */\n private deliverPresenceDiff(\n channel: string,\n joins: Record<string, Record<string, unknown>>,\n leaves: Record<string, Record<string, unknown>>\n ): void {\n const members = this.channels.get(channel);\n if (!members) return;\n\n const message = JSON.stringify({\n type: \"presence_diff\",\n channel,\n joins,\n leaves\n });\n\n for (const memberId of members) {\n const ws = this.clients.get(memberId);\n if (ws && ws.readyState === WebSocket.OPEN) {\n ws.send(message);\n }\n }\n }\n\n /** Tell the other instances about a presence change. */\n private publishPresenceDiff(\n channel: string,\n joins: Record<string, Record<string, unknown>>,\n leaves: Record<string, Record<string, unknown>>\n ): void {\n if (this.bus.kind === \"memory\") return;\n void this.publishFrame({ kind: \"presence_diff\", sid: this.instanceId, channel, joins, leaves });\n }\n\n /** Run a roster write when there is a roster, and never let it throw. */\n private async presenceStoreOp(op: () => Promise<void>, label: string): Promise<void> {\n if (!this.presenceStore) return;\n try {\n await op();\n } catch (error) {\n logger.warn(`⚠️ [Presence] Shared roster ${label} failed`, { error });\n }\n }\n\n /** Periodic cleanup for stale presences */\n private ensurePresenceCleanup(): void {\n if (this.presenceInterval) return;\n this.presenceInterval = setInterval(() => {\n const now = Date.now();\n for (const [channel, channelPresence] of this.presence) {\n for (const [clientId, entry] of channelPresence) {\n if (now - entry.lastSeen > RealtimeService.PRESENCE_TIMEOUT_MS) {\n this.removePresence(clientId, channel);\n }\n }\n }\n // Stop interval if no presences tracked\n if (this.presence.size === 0 && this.presenceInterval) {\n clearInterval(this.presenceInterval);\n this.presenceInterval = undefined;\n }\n }, 10000); // Check every 10s\n }\n\n /**\n * Reap roster rows whose owning instance stopped heartbeating.\n *\n * This is the cross-instance half of the sweep above, and it doubles as\n * crash recovery: a pod that dies takes its clients with it but leaves\n * their rows behind, and after one TTL window they look exactly like any\n * other client that went quiet. The delete returns what it removed, so\n * whichever instance wins the race is the one that announces the\n * departures — once for the cluster, not once per replica.\n */\n private ensurePresenceSweep(): void {\n if (this.presenceSweepInterval || !this.presenceStore) return;\n\n this.presenceSweepInterval = setInterval(\n () => void this.sweepStalePresence(),\n RealtimeService.PRESENCE_SWEEP_INTERVAL_MS\n );\n\n // Never hold the process open for housekeeping.\n (this.presenceSweepInterval as unknown as { unref?: () => void }).unref?.();\n }\n\n /** One pass of the stale-roster sweep. See {@link ensurePresenceSweep}. */\n private async sweepStalePresence(): Promise<void> {\n if (!this.presenceStore) return;\n try {\n const removed = await this.presenceStore.sweepStale(RealtimeService.PRESENCE_TIMEOUT_MS);\n for (const row of removed) {\n this.debugLog(`👻 [Presence] Reaped stale presence ${row.clientId} on \"${row.channel}\"`);\n this.deliverPresenceDiff(row.channel, {}, { [row.clientId]: row.state });\n this.publishPresenceDiff(row.channel, {}, { [row.clientId]: row.state });\n }\n } catch (error) {\n logger.warn(\"⚠️ [Presence] Stale-roster sweep failed\", { error });\n }\n }\n\n // =============================================================================\n // Lifecycle / Cleanup\n // =============================================================================\n\n /**\n * Gracefully tear down all realtime resources.\n *\n * This MUST be called during process shutdown, **before** `pool.end()`.\n * It ensures:\n * 1. All debounced refetch timers are cancelled (prevents queries after pool closes).\n * 2. All subscription state and callbacks are cleared.\n * 3. The dedicated LISTEN client (outside the pool) is disconnected.\n * 4. All WebSocket clients are removed (but not forcefully closed — the\n * HTTP server close will handle that).\n */\n async destroy(): Promise<void> {\n // 1. Cancel every pending debounced refetch timer\n for (const [key, timer] of this.refetchTimers) {\n clearTimeout(timer);\n this.refetchTimers.delete(key);\n }\n\n // 2. Clear subscriptions and callbacks\n this._subscriptions.clear();\n this.subscriptionCallbacks.clear();\n\n // 3. Clear broadcast channels and presence\n this.channels.clear();\n this.presence.clear();\n // Pending history writes hold the pool open; let them settle before the\n // caller closes it, but never let a rejected one break shutdown.\n await Promise.allSettled([...this.channelSendQueues.values()]);\n this.channelSendQueues.clear();\n this.channelHistory?.clear();\n if (this.presenceInterval) {\n clearInterval(this.presenceInterval);\n this.presenceInterval = undefined;\n }\n if (this.presenceSweepInterval) {\n clearInterval(this.presenceSweepInterval);\n this.presenceSweepInterval = undefined;\n }\n this.oversizedBroadcastWarned.clear();\n\n // Drop this instance's roster rows now rather than leaving every other\n // replica to wait out a TTL window on ghosts — a rolling deploy would\n // otherwise show 30s of departed users on every restart.\n if (this.presenceStore) {\n try {\n await this.presenceStore.removeInstance();\n } catch (error) {\n logger.warn(\"⚠️ [Presence] Could not clear this instance's roster rows on shutdown\", { error });\n }\n this.presenceStore = undefined;\n }\n\n // 4. Disconnect the dedicated LISTEN client(s)\n await this.stopListening();\n await this.stopCdc();\n await this.bus.stop().catch((error) =>\n logger.warn(\"⚠️ [ChannelBus] Error while stopping the channel bus\", { error }));\n this.bus = new MemoryChannelBus();\n\n // 5. Drop client references (don't close — server.close drains them)\n this.clients.clear();\n\n this.debugLog(\"🧹 [RealtimeService] destroy() complete — all resources released.\");\n }\n\n // =============================================================================\n // Database-level Change Data Capture (CDC)\n // =============================================================================\n\n /** Whether database-level change capture is currently the active source. */\n public isCdcActive(): boolean {\n return this.cdcActive;\n }\n\n /**\n * Enable database-level change capture as the realtime source.\n *\n * A dedicated LISTEN client consumes committed changes from the `rebase_cdc`\n * channel (fed by CDC triggers — see {@link provisionTriggerCdc}) and routes\n * them into the same {@link notifyUpdate} pipeline used by API mutations. The\n * effect: subscribers see a change no matter how it was written — psql, a\n * cron in another service, raw SQL, or the Studio SQL editor — exactly like\n * Supabase Realtime tailing the WAL.\n *\n * Because CDC observes every commit on every instance, it also *replaces* the\n * legacy per-mutation cross-instance broadcast (see the guard in\n * {@link notifyUpdate}); callers should not also call {@link startListening}.\n *\n * @param connectionString Direct Postgres connection for the LISTEN client\n * (bypass PgBouncer — LISTEN needs a session connection).\n */\n async enableCdc(connectionString: string): Promise<void> {\n if (this.cdcActive) {\n logger.warn(\"⚠️ [CDC] enableCdc called but CDC is already active. Ignoring.\");\n return;\n }\n this.cdcTableMap = this.buildCdcTableMap();\n this.junctionLinkMap = buildJunctionLinkMap(this.registry);\n this.cdcListener = new CdcListener(connectionString, (event) => this.handleCdcEvent(event));\n try {\n // start() validates the initial connection; if it can't be established\n // it rejects here, and we leave CDC inactive so the caller can fall\n // back to app-level realtime rather than silently dropping events.\n await this.cdcListener.start();\n } catch (err) {\n await this.cdcListener.stop().catch(() => { /* best effort */ });\n this.cdcListener = undefined;\n this.cdcTableMap = undefined;\n this.junctionLinkMap = undefined;\n throw err;\n }\n this.cdcActive = true;\n // The bootstrapper says the same thing one line later, in the\n // vocabulary of the setting that produced it (REALTIME_CDC).\n logger.debug(\n `📡 [RealtimeService] Database-level change capture ACTIVE — writes from ANY source now emit realtime events ` +\n `(${this.cdcTableMap.size} mapped table key(s)).`\n );\n }\n\n /** Stop the CDC listener and clear its state. */\n async stopCdc(): Promise<void> {\n this.cdcActive = false;\n if (this.cdcListener) {\n await this.cdcListener.stop();\n this.cdcListener = undefined;\n }\n this.cdcTableMap = undefined;\n this.junctionLinkMap = undefined;\n this.recentAppEmits.clear();\n }\n\n /**\n * Build the reverse map from database table → collection. A change event\n * carries `schema` + `table`; realtime subscriptions are keyed by collection\n * path (slug). We index by both `schema.table` and bare `table` so the lookup\n * works whether or not the collection declares an explicit schema.\n */\n private buildCdcTableMap(): Map<string, CollectionConfig> {\n const map = new Map<string, CollectionConfig>();\n for (const collection of this.registry.getCollections()) {\n const table = getTableName(collection);\n if (!table) continue;\n const schema = (collection as { schema?: string }).schema ?? \"public\";\n map.set(`${schema}.${table}`, collection);\n // Bare-table fallback; first registration wins to keep it deterministic.\n if (!map.has(table)) map.set(table, collection);\n }\n return map;\n }\n\n private resolveCollectionForTable(schema: string, table: string): CollectionConfig | undefined {\n if (!this.cdcTableMap) return undefined;\n return this.cdcTableMap.get(`${schema}.${table}`) ?? this.cdcTableMap.get(table);\n }\n\n /**\n * Route a captured database change into the realtime pipeline.\n *\n * Delivery is RLS-safe by construction: the raw tuple from the WAL/trigger is\n * NOT forwarded to subscribers. Instead the change is marked invalidated, so\n * every matching subscription re-reads the row under its own auth context via\n * {@link fetchCollectionWithAuth} / {@link fetchEntityWithAuth}. A subscriber\n * therefore only ever receives rows its RLS policies permit — filtering is per\n * subscriber, never per publisher.\n */\n private async handleCdcEvent(event: CdcChangeEvent): Promise<void> {\n const collection = this.resolveCollectionForTable(event.schema, event.table);\n if (!collection) {\n // A junction table backs no collection, but its rows *are* a child\n // list. Route the change to the lists it changes before giving up.\n if (await this.handleJunctionCdcEvent(event)) return;\n\n // Unmapped table (not backed by a collection) — nothing to deliver.\n this.debugLog(`📡 [CDC] Ignoring change on unmapped table ${event.schema}.${event.table}`);\n return;\n }\n\n const path = collection.slug;\n const databaseId = (collection as { databaseId?: string }).databaseId;\n const id = this.extractIdFromCdcRow(collection, event.row);\n\n // Deletes carry a null row (subscribers drop the id); inserts/updates carry\n // an invalidation marker that forces a per-subscriber RLS-bound refetch.\n const row = event.op === \"DELETE\" ? null : { _rebase_invalidated: true };\n\n await this.notifyUpdate(path, id, row, databaseId, /* broadcast */ false, /* origin */ \"cdc\");\n }\n\n /**\n * Deliver a change on a many-to-many junction table as a change to the child\n * lists it belongs to.\n *\n * Linking a tag to a post writes only `posts_tags`. That table backs no\n * collection, so change capture dropped the event as unmapped and the\n * subscribers of `posts/1/tags` never heard about it — every other write in\n * the system was realtime, and this one silently was not. The junction row\n * carries both ids, so it names its own paths exactly.\n *\n * Notifies the nested path rather than either endpoint collection, because\n * invalidation walks *parent* paths and never child ones: telling `tags` it\n * changed would not reach a subscription on `posts/1/tags`.\n *\n * Returns whether the table was recognised as a junction.\n */\n private async handleJunctionCdcEvent(event: CdcChangeEvent): Promise<boolean> {\n const links = this.junctionLinkMap?.get(`${event.schema}.${event.table}`)\n ?? this.junctionLinkMap?.get(event.table);\n if (!links?.length) return false;\n\n for (const link of links) {\n const sourceId = event.row?.[link.sourceColumn];\n const targetId = event.row?.[link.targetColumn];\n if (sourceId === undefined || sourceId === null || targetId === undefined || targetId === null) {\n this.debugLog(\n `📡 [CDC] Junction row on ${event.table} is missing '${link.sourceColumn}'/'${link.targetColumn}' — skipping.`\n );\n continue;\n }\n\n const path = `${link.parentCollection.slug}/${String(sourceId)}/${link.relationKey}`;\n // An unlink removes the target from this list; a link invalidates it\n // so each subscriber refetches under its own RLS context.\n const row = event.op === \"DELETE\" ? null : { _rebase_invalidated: true };\n\n await this.notifyUpdate(\n path,\n String(targetId),\n row,\n (link.parentCollection as { databaseId?: string }).databaseId,\n /* broadcast */ false,\n /* origin */ \"cdc\"\n );\n }\n\n return true;\n }\n\n /** Compute the canonical (possibly composite) id string from a captured row. */\n private extractIdFromCdcRow(collection: CollectionConfig, row: Record<string, unknown>): string {\n // Unaddressable falls back to a collection-level invalidation: single-row\n // subs won't match, but collection subs still refetch.\n return deriveRowAddress(row, collection, this.registry) || \"*\";\n }\n\n // ── App/CDC de-duplication ──\n\n private dedupKey(path: string, id: string, databaseId?: string): string {\n return `${databaseId ?? \"\"}::${path}::${id}`;\n }\n\n /** Record that this instance just delivered `key` via the app path. */\n private markAppEmit(key: string): void {\n const now = Date.now();\n this.recentAppEmits.set(key, now + RealtimeService.CDC_DEDUP_WINDOW_MS);\n // Opportunistic purge so the map cannot grow unbounded under write load.\n if (this.recentAppEmits.size > 1000) {\n for (const [k, expiry] of this.recentAppEmits) {\n if (expiry <= now) this.recentAppEmits.delete(k);\n }\n }\n }\n\n /** Consume a matching app-emit record if present and unexpired; true ⇒ suppress the CDC echo. */\n private consumeAppEmit(key: string): boolean {\n const expiry = this.recentAppEmits.get(key);\n if (expiry === undefined) return false;\n this.recentAppEmits.delete(key);\n return expiry > Date.now();\n }\n\n // =============================================================================\n // Cross-Instance LISTEN/NOTIFY\n // =============================================================================\n\n /**\n * Enable cross-instance realtime broadcasting via Postgres LISTEN/NOTIFY.\n * Creates a dedicated pg.Client (outside the Drizzle pool) that stays\n * connected and listens for change notifications from other instances.\n *\n * This is an **optional** feature — if never called, the backend operates\n * in single-instance mode (the default, perfectly fine for most setups).\n *\n * @param connectionString Raw Postgres connection string for the LISTEN client.\n */\n async startListening(connectionString: string): Promise<void> {\n if (this.broadcasting) {\n logger.warn(\"⚠️ [RealtimeService] startListening called but already listening. Ignoring.\");\n return;\n }\n\n this.listenConnectionString = connectionString;\n // Set broadcasting BEFORE connecting so that scheduleReconnect()\n // works correctly if the initial connection attempt fails.\n this.broadcasting = true;\n await this.connectListenClient();\n logger.info(`📡 [RealtimeService] Cross-instance realtime enabled (instanceId: ${this.instanceId})`);\n }\n\n /**\n * Stop listening and clean up the dedicated LISTEN connection.\n */\n async stopListening(): Promise<void> {\n this.broadcasting = false;\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = undefined;\n }\n if (this.listenClient) {\n try {\n await this.listenClient.end();\n } catch { /* ignore close errors */ }\n this.listenClient = undefined;\n }\n logger.info(\"📡 [RealtimeService] Cross-instance realtime disabled.\");\n }\n\n /**\n * Broadcast a change notification to other instances via pg_notify.\n * Uses the main Drizzle connection (pooled) — NOT the LISTEN client.\n */\n private async broadcastChange(path: string, id: string, databaseId?: string): Promise<void> {\n const payload = JSON.stringify({\n sid: this.instanceId,\n p: path,\n eid: id,\n db: databaseId ?? null\n });\n await this.db.execute(drizzleSql`SELECT pg_notify(${PG_NOTIFY_CHANNEL}, ${payload})`);\n }\n\n /**\n * Create and connect the dedicated LISTEN client with auto-reconnect.\n */\n private async connectListenClient(): Promise<void> {\n if (!this.listenConnectionString) return;\n\n let pending: PgClient | undefined;\n try {\n // See `PgNotifyListener.connect` — same shape, same reason. Until\n // `this.listenClient` is assigned, nothing else in this class knows\n // the connection exists, so a throw between `connect()` and that\n // assignment leaks a live backend and `scheduleReconnect` opens\n // another one three seconds later.\n const client = new PgClient({ connectionString: this.listenConnectionString });\n pending = client;\n\n client.on(\"error\", (err) => {\n logger.error(\"❌ [RealtimeService] LISTEN client error\", { detail: err.message });\n this.scheduleReconnect();\n });\n\n client.on(\"end\", () => {\n if (this.broadcasting) {\n logger.warn(\"⚠️ [RealtimeService] LISTEN client disconnected unexpectedly.\");\n this.scheduleReconnect();\n }\n });\n\n client.on(\"notification\", async (msg) => {\n if (!msg.payload) return;\n try {\n const { sid, p, eid, db } = JSON.parse(msg.payload) as {\n sid: string;\n p: string;\n eid: string;\n db: string | null;\n };\n\n // Skip our own notifications — already processed locally\n if (sid === this.instanceId) return;\n\n // A foreign sid is proof of a second process. Nothing here\n // needs that fact, but the channel path does — see\n // `warnIfMemoryBusOnMultiplePods`.\n this.foreignInstanceSeen = true;\n\n this.debugLog(`📡 [RealtimeService] Received cross-instance notification: path=${p}, id=${eid}, from=${sid}`);\n\n // Refetch the row from the DB so row subscriptions\n // receive the actual data instead of null (which the client\n // would interpret as \"deleted\").\n let refetchedRow: Record<string, unknown> | null = null;\n try {\n if (this.driver) {\n const collection = this.registry.getCollectionByPath(p);\n const fetched = await this.driver.fetchOne({\n path: p,\n id: eid,\n collection: collection\n });\n refetchedRow = fetched ?? null;\n } else {\n const fetched = await this.dataService.fetchOne(\n p, eid, db ?? undefined\n );\n refetchedRow = fetched ?? null;\n }\n } catch (fetchErr) {\n // If the fetch fails (e.g. row was deleted), refetchedRow stays null\n this.debugLog(`📡 [RealtimeService] Could not refetch row ${eid} from ${p} — treating as deleted`, fetchErr);\n }\n\n // Trigger local fan-out with broadcast=false to avoid re-broadcasting\n await this.notifyUpdate(p, eid, refetchedRow, db ?? undefined, false);\n } catch (err) {\n logger.error(\"❌ [RealtimeService] Error processing cross-instance notification\", { error: err });\n }\n });\n\n await client.connect();\n await client.query(`LISTEN ${PG_NOTIFY_CHANNEL}`);\n this.listenClient = client;\n // Adopted: `destroy()` and `scheduleReconnect` close it now.\n pending = undefined;\n\n this.debugLog(`📡 [RealtimeService] LISTEN client connected on channel \"${PG_NOTIFY_CHANNEL}\"`);\n } catch (err) {\n if (pending) {\n try { await pending.end(); } catch { /* already dead */ }\n }\n logger.error(\"❌ [RealtimeService] Failed to connect LISTEN client\", { error: err });\n this.scheduleReconnect();\n }\n }\n\n /**\n * Schedule a reconnection attempt with a fixed 3s delay.\n */\n private scheduleReconnect(): void {\n if (!this.broadcasting || this.reconnectTimer) return;\n\n const delay = 3000; // Fixed 3s delay; simple and predictable\n this.debugLog(`📡 [RealtimeService] Scheduling LISTEN reconnect in ${delay}ms...`);\n\n this.reconnectTimer = setTimeout(async () => {\n this.reconnectTimer = undefined;\n if (!this.broadcasting) return;\n\n // Clean up old client\n if (this.listenClient) {\n try { await this.listenClient.end(); } catch { /* ignore */ }\n this.listenClient = undefined;\n }\n\n await this.connectListenClient();\n }, delay);\n }\n}\n\n/**\n * Alias for RealtimeService for consistent naming with other database implementations.\n * This allows code to use PostgresRealtimeProvider alongside future MongoRealtimeProvider, etc.\n */\nexport const PostgresRealtimeProvider = RealtimeService;\n","import { CollectionRegistry, resolveCollectionRelations } from \"@rebasepro/common\";\nimport { type CollectionConfig } from \"@rebasepro/types\";\nimport { PgEnum, PgTable } from \"drizzle-orm/pg-core\";\nimport { Relations } from \"drizzle-orm\";\nimport { CollectionRegistryInterface } from \"../interfaces\";\nimport { getTableName } from \"@rebasepro/common\";\n\n/**\n * PostgreSQL-specific collection registry.\n * Extends the base CollectionRegistry with support for Drizzle ORM tables, enums, and relations.\n *\n * Satisfies CollectionRegistryInterface through inheritance from CollectionRegistry.\n */\nexport class PostgresCollectionRegistry extends CollectionRegistry implements CollectionRegistryInterface {\n\n private tables = new Map<string, PgTable>();\n private enums = new Map<string, PgEnum<[string, ...string[]]>>();\n private relations = new Map<string, Relations>();\n\n registerTable(table: PgTable, tableName: string) {\n this.tables.set(tableName, table);\n }\n\n getTable(tableName: string): PgTable | undefined {\n return this.tables.get(tableName);\n }\n\n /**\n * Checks if a specific collection has a registered table\n */\n hasTableForCollection(tableName: string): boolean {\n return this.tables.has(tableName);\n }\n\n /**\n * Returns all registered table names.\n */\n getTableNames(): string[] {\n return Array.from(this.tables.keys());\n }\n\n /**\n * Finds collections assigned to a specific data source that do not have a registered table.\n */\n getCollectionsWithoutTables(dataSourceKey = \"(default)\"): CollectionConfig[] {\n const collections = this.getCollections().filter(\n c => c.dataSource === dataSourceKey || (!c.dataSource && dataSourceKey === \"(default)\")\n );\n return collections.filter(c => !this.tables.has(getTableName(c)));\n }\n\n registerEnums(enums: Record<string, PgEnum<[string, ...string[]]>>) {\n Object.entries(enums).forEach(([name, value]) => this.enums.set(name, value));\n }\n\n registerRelations(relations: Record<string, Relations>) {\n Object.entries(relations).forEach(([name, value]) => this.relations.set(name, value));\n }\n\n getEnum(name: string): PgEnum<[string, ...string[]]> | undefined {\n return this.enums.get(name);\n }\n\n getRelation(name: string): Relations | undefined {\n return this.relations.get(name);\n }\n\n getAllEnums(): Record<string, PgEnum<[string, ...string[]]>> {\n return Object.fromEntries(this.enums.entries());\n }\n\n getAllRelations(): Record<string, Relations> {\n return Object.fromEntries(this.relations.entries());\n }\n\n /**\n * Get the merged schema object (tables + relations) for use with Drizzle's\n * relational query API (`db.query`).\n */\n getMergedSchema(): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const [name, table] of this.tables.entries()) {\n result[name] = table;\n }\n for (const [name, relation] of this.relations.entries()) {\n result[name] = relation;\n }\n return result;\n }\n\n /**\n * Get the available Drizzle relation keys for a given collection path.\n * Maps from the collection's relation property names to the Drizzle relation names\n * defined in the schema.\n */\n getRelationKeysForCollection(collectionPath: string): string[] {\n const collection = this.getCollectionByPath(collectionPath);\n if (!collection) return [];\n // Resolved, not authored. `relationName` is optional at the authoring\n // surface and defaults to the property key or the target's slug, so\n // reading the raw field dropped every relation that relied on the\n // default — and never saw relations declared inline on a property at\n // all, since those are not in the `relations` array.\n return Object.keys(resolveCollectionRelations(collection));\n }\n\n}\n\n","/**\n * Scheduled backups, wired into the server cron system.\n *\n * A backend enables nightly (or custom-schedule) backups by dropping a cron\n * file that default-exports {@link createBackupCron}. The heavy lifting —\n * dump, upload, prune — reuses the same primitives as the CLI so behaviour\n * is identical between a manual `rebase db backup` and an automated run.\n */\nimport fs from \"fs\";\nimport type { CronJobDefinition } from \"@rebasepro/types\";\nimport type { StorageController } from \"@rebasepro/server\";\nimport {\n BackupDestination,\n parseBackupDestination,\n parseDbNameFromUrl,\n resolveConnectionString\n} from \"./pg-tools\";\n// NOTE: `./backup-service` pulls in `execa` (ESM-only). It is imported\n// lazily inside the handler so the pure `backupCronConfigFromEnv` parser can\n// be unit-tested under CommonJS without loading it.\n\nexport interface BackupCronConfig {\n /** Cron expression, e.g. `\"0 3 * * *\"` for 03:00 daily. */\n schedule: string;\n /** Postgres connection string. Defaults to `DATABASE_URL`. */\n connectionString: string;\n /** Where backups are written: a local path or an `s3://` / `gs://` URL. */\n destination: BackupDestination;\n /**\n * Storage controller used for `s3`/`gcs` destinations. Reuse the one the\n * backend already configured (S3/GCS/Local StorageController). Not needed\n * for local destinations.\n */\n storage?: StorageController;\n /** Delete backups older than this many days. `0` disables pruning. */\n retentionDays?: number;\n /** Always keep at least this many recent backups regardless of age. */\n keepMinimum?: number;\n /** Schemas to exclude from the dump (defaults to Atlas revision schema). */\n excludeSchemas?: string[];\n /** Cron job display name. */\n name?: string;\n /** Whether the job is enabled. */\n enabled?: boolean;\n}\n\nexport interface EnvResolution {\n /** Resolved config, present when a schedule is configured. */\n config?: Omit<BackupCronConfig, \"storage\">;\n /** True when `BACKUP_SCHEDULE` is unset — the cron should be a no-op. */\n disabled?: boolean;\n /** Human-readable reason the config could not be built. */\n error?: string;\n}\n\n/**\n * Build backup-cron config purely from environment variables.\n * Recognised keys:\n * - `BACKUP_SCHEDULE` cron expression (required to enable)\n * - `BACKUP_DESTINATION` local path or `s3://` / `gs://` URL (required)\n * - `BACKUP_RETENTION_DAYS` integer days (optional)\n * - `BACKUP_KEEP_MINIMUM` integer count (optional)\n * - `DATABASE_URL` connection string\n *\n * Pure and side-effect free so it can be unit-tested without a database.\n */\nexport function backupCronConfigFromEnv(env: Record<string, string | undefined>): EnvResolution {\n const schedule = env.BACKUP_SCHEDULE?.trim();\n if (!schedule) {\n return { disabled: true };\n }\n\n const connectionString = resolveConnectionString(env);\n if (!connectionString) {\n return { error: \"BACKUP_SCHEDULE is set but DATABASE_URL is not configured.\" };\n }\n\n const destinationRaw = env.BACKUP_DESTINATION?.trim();\n if (!destinationRaw) {\n return { error: \"BACKUP_SCHEDULE is set but BACKUP_DESTINATION is not configured.\" };\n }\n const destination = parseBackupDestination(destinationRaw);\n\n const retentionDays = parseOptionalInt(env.BACKUP_RETENTION_DAYS);\n if (retentionDays === \"invalid\") {\n return { error: `BACKUP_RETENTION_DAYS must be an integer, got \"${env.BACKUP_RETENTION_DAYS}\".` };\n }\n const keepMinimum = parseOptionalInt(env.BACKUP_KEEP_MINIMUM);\n if (keepMinimum === \"invalid\") {\n return { error: `BACKUP_KEEP_MINIMUM must be an integer, got \"${env.BACKUP_KEEP_MINIMUM}\".` };\n }\n\n return {\n config: {\n schedule,\n connectionString,\n destination,\n retentionDays: retentionDays ?? undefined,\n keepMinimum: keepMinimum ?? undefined\n }\n };\n}\n\nfunction parseOptionalInt(value: string | undefined): number | null | \"invalid\" {\n if (value === undefined || value.trim() === \"\") return null;\n const n = Number(value);\n if (!Number.isInteger(n) || n < 0) return \"invalid\";\n return n;\n}\n\n/**\n * Create a {@link CronJobDefinition} that dumps the database, uploads the\n * result to the configured destination, and prunes old backups. Object\n * destinations require {@link BackupCronConfig.storage}.\n */\nexport function createBackupCron(config: BackupCronConfig): CronJobDefinition {\n const dbName = parseDbNameFromUrl(config.connectionString) ?? \"database\";\n const excludeSchemas = config.excludeSchemas ?? [\"rebase\"];\n\n return {\n name: config.name ?? \"Scheduled database backup\",\n schedule: config.schedule,\n description: \"Dumps the Postgres database and uploads it to the configured backup destination.\",\n enabled: config.enabled ?? true,\n // Backups of a large database can take a while; allow up to an hour.\n timeoutSeconds: 3600,\n async handler({ log }) {\n const { createDump, pruneBackups, uploadBackup, validateDump } = await import(\"./backup-service\");\n const { destination } = config;\n\n if (destination.kind !== \"local\" && !config.storage) {\n throw new Error(\n `Backup destination is ${destination.kind} but no storage controller was provided. ` +\n \"Pass the backend's configured StorageController to createBackupCron({ storage }).\"\n );\n }\n\n log(`Starting backup of \"${dbName}\"…`);\n const outDir = destination.kind === \"local\" ? destination.path : undefined;\n const dump = await createDump({\n connectionString: config.connectionString,\n dbName,\n outDir,\n excludeSchemas\n });\n log(`Dump created: ${dump.fileName} (${formatBytes(dump.sizeBytes)})`);\n\n // Validate BEFORE pruning: a corrupt-but-exit-0 dump must never be\n // the reason the last good backup gets deleted.\n const check = await validateDump(dump.localFile);\n if (!check.ok) {\n // Clean up the bad temp file for object destinations.\n if (destination.kind !== \"local\" && fs.existsSync(dump.localFile)) {\n fs.unlinkSync(dump.localFile);\n }\n if (dump.globalsFile && destination.kind !== \"local\" && fs.existsSync(dump.globalsFile)) {\n fs.unlinkSync(dump.globalsFile);\n }\n throw new Error(`New backup failed validation — skipping upload and pruning to protect existing backups. ${check.reason}`);\n }\n\n let storedKey = dump.localFile;\n try {\n if (destination.kind !== \"local\") {\n const uploaded = await uploadBackup(config.storage!, dump.localFile, destination);\n storedKey = uploaded.storageUrl;\n log(`Uploaded to ${uploaded.storageUrl}`);\n // Upload the roles sidecar so a restore can recreate the\n // roles the dump's GRANT/RLS statements depend on.\n if (dump.globalsFile && fs.existsSync(dump.globalsFile)) {\n const g = await uploadBackup(config.storage!, dump.globalsFile, destination);\n log(`Uploaded roles sidecar to ${g.storageUrl}`);\n }\n }\n } finally {\n // For object-storage destinations the local dump was a temp\n // file — remove it once uploaded (or on failure).\n if (destination.kind !== \"local\" && fs.existsSync(dump.localFile)) {\n fs.unlinkSync(dump.localFile);\n }\n if (dump.globalsFile && destination.kind !== \"local\" && fs.existsSync(dump.globalsFile)) {\n fs.unlinkSync(dump.globalsFile);\n }\n }\n\n let pruned: string[] = [];\n if (config.retentionDays && config.retentionDays > 0) {\n pruned = await pruneBackups(\n destination,\n { retentionDays: config.retentionDays, keepMinimum: config.keepMinimum },\n config.storage\n );\n if (pruned.length > 0) {\n log(`Pruned ${pruned.length} backup(s) older than ${config.retentionDays} day(s).`);\n }\n }\n\n return {\n backup: storedKey,\n sizeBytes: dump.sizeBytes,\n pruned: pruned.length\n };\n }\n };\n}\n\nfunction formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;\n}\n","import { getTableColumns } from \"drizzle-orm\";\nimport { PgTable } from \"drizzle-orm/pg-core\";\nimport { CollectionConfig, ResolvedRelation } from \"@rebasepro/types\";\nimport { getTableName, resolveCollectionRelations } from \"@rebasepro/common\";\nimport { generateForeignKeyName, legacyForeignKeyName } from \"@rebasepro/utils\";\n\nimport { PostgresCollectionRegistry } from \"./PostgresCollectionRegistry\";\n\n/**\n * Check every relation against the schema it actually runs on, at boot.\n *\n * The tagged union made the *shape* of a relation impossible to get wrong: a\n * `manyToMany` cannot carry a `foreignKeyOnTarget`, a to-many cannot carry a\n * `localKey`. What it cannot know is whether any of the names are real —\n * whether `posts_tags` is a table, whether `author_id` is a column, whether a\n * `joinPath` connects the tables it claims to. Those are facts about the\n * database, and the type system never sees them.\n *\n * Until now nothing checked them until a query ran, and the failures were the\n * quiet kind. A missing junction table logged a warning and returned no rows,\n * so `posts/1/tags` answered `[]` — indistinguishable from a post with no tags.\n * The relation looked configured, the admin drew the tab, the tab was empty,\n * and nothing anywhere said why.\n *\n * The junction default is the sharp edge this exists for. `through.table`\n * defaults to the two table names sorted and joined, so renaming a table\n * silently re-points the relation at a name that was never created. It is the\n * one default whose output changes when you edit something that looks\n * unrelated.\n */\nexport interface RelationDefect {\n /** Slug of the collection declaring the relation. */\n collection: string;\n relationName: string;\n kind: ResolvedRelation[\"kind\"];\n /** What is wrong, in terms of the schema. */\n problem: string;\n /** The edit that fixes it. */\n fix: string;\n}\n\n/**\n * Every name a column answers to: the key in the drizzle schema and the real\n * column name in Postgres. A relation may legitimately be written with either,\n * and reporting a working relation as broken is worse than not checking.\n */\nfunction columnNames(table: PgTable): Set<string> {\n const names = new Set<string>();\n for (const [key, col] of Object.entries(getTableColumns(table))) {\n names.add(key);\n const dbName = (col as { name?: string })?.name;\n if (dbName) names.add(dbName);\n }\n return names;\n}\n\nconst quote = (xs: Iterable<string>) => Array.from(xs).map(s => `\\`${s}\\``).join(\", \");\n\n/** `on.from` / `on.to` accept a single column or a composite tuple. */\nconst asColumns = (value: string | string[]): string[] => Array.isArray(value) ? value : [value];\n\n/**\n * Distinguish \"this column name is wrong\" from \"the generated schema is old\".\n *\n * They present identically here — a relation asks for a column the registered\n * table does not have — but they are opposite problems with opposite fixes, and\n * getting them the wrong way round is how the 0.12 → 0.13 upgrade bricked\n * projects.\n *\n * The registered table is not the database. It comes from the project's\n * checked-in `backend/src/schema.generated.ts`, and 0.13 changed the rule that\n * derives foreign-key names: `categories` yields `category_id` where it used to\n * yield `categorie_id`. Boot-ensure renames the database column to match, so by\n * the time this runs the *database* is correct and the *generated module* is the\n * stale one. Reporting \"not a column\" then points at the wrong artifact, and the\n * generic fix — \"set `through.targetColumn` to one of: …\", listing the legacy\n * name because that is what the stale module still has — talks the reader into\n * pinning a column that no longer exists.\n *\n * So when the wanted name is what the current rule derives, and the table\n * carries what the *previous* rule would have derived from the same source, say\n * that instead.\n *\n * @param wanted the column the relation asks for\n * @param available every column the registered table has\n * @param sources names the default could have been derived from (a slug, a\n * relation name) — checking against these rather than guessing\n * backwards from `wanted` keeps the match exact\n */\nfunction staleCodegenRename(\n wanted: string,\n available: Set<string>,\n sources: string[]\n): { legacy: string; current: string } | null {\n for (const source of sources) {\n if (!source) continue;\n const current = generateForeignKeyName(source);\n const legacy = legacyForeignKeyName(source);\n // Only a name that actually moved, and only when the table still has the\n // old spelling and not the new one.\n if (current !== wanted || legacy === current) continue;\n if (available.has(legacy) && !available.has(current)) return { legacy, current };\n }\n return null;\n}\n\n/** The shared explanation, so every relation kind reports it identically. */\nfunction staleCodegenDefect(\n table: string,\n { legacy, current }: { legacy: string; current: string }\n): Pick<RelationDefect, \"problem\" | \"fix\"> {\n return {\n problem:\n `the generated Drizzle schema still declares \\`${legacy}\\` on \\`${table}\\`, but this ` +\n `release derives \\`${current}\\` — the generated schema predates the foreign-key ` +\n \"naming fix and no longer describes the database\",\n fix:\n \"regenerate it with `rebase schema generate` (or `pnpm run schema:generate`). The \" +\n \"database column has already been renamed for you at boot, so nothing else is needed. \" +\n `To keep \\`${legacy}\\` instead, name it explicitly on the relation and regenerate.`\n };\n}\n\n/**\n * Relations whose names do not resolve against the registered schema.\n *\n * Fails open wherever it cannot see enough to be sure — an unregistered source\n * table, a target belonging to another backend — because a false alarm here\n * costs more than a missed one: it would block boot on a working app.\n */\nexport function findRelationDefects(\n collections: CollectionConfig[],\n registry: PostgresCollectionRegistry\n): RelationDefect[] {\n const defects: RelationDefect[] = [];\n const registeredSlugs = new Set(registry.getCollections().map(c => c.slug));\n\n for (const collection of collections) {\n const sourceTableName = getTableName(collection);\n const sourceTable = registry.getTable(sourceTableName);\n // Nothing to check against. Another boot warning already covers this.\n if (!sourceTable) continue;\n\n const sourceColumns = columnNames(sourceTable);\n const relations = resolveCollectionRelations(collection);\n\n for (const relation of Object.values(relations)) {\n const at = { collection: collection.slug,\nrelationName: relation.relationName,\nkind: relation.kind };\n\n let targetCollection: CollectionConfig;\n try {\n targetCollection = relation.target();\n } catch (e) {\n defects.push({\n ...at,\n problem: `its \\`target()\\` threw: ${e instanceof Error ? e.message : String(e)}`,\n fix: \"a target thunk usually throws because of a circular import — make sure it is `() => otherCollection` and not evaluated at module load\"\n });\n continue;\n }\n\n // A target this registry has never heard of belongs to another\n // backend; its tables are not ours to check.\n if (!registeredSlugs.has(targetCollection.slug)) continue;\n\n const targetTableName = getTableName(targetCollection);\n const targetTable = registry.getTable(targetTableName);\n if (!targetTable) {\n defects.push({\n ...at,\n problem: `it points at collection \\`${targetCollection.slug}\\`, which has no table \\`${targetTableName}\\` in the schema`,\n fix: `create the \\`${targetTableName}\\` table, or correct \\`table\\` on the \\`${targetCollection.slug}\\` collection`\n });\n continue;\n }\n const targetColumns = columnNames(targetTable);\n\n switch (relation.kind) {\n case \"belongsTo\": {\n if (!sourceColumns.has(relation.localKey)) {\n // `localKey` defaults to the relation name run through\n // the foreign-key rule, so it moves with that rule.\n const stale = staleCodegenRename(\n relation.localKey,\n sourceColumns,\n [relation.relationName, targetCollection.slug]\n );\n defects.push(stale\n ? { ...at, ...staleCodegenDefect(sourceTableName, stale) }\n : {\n ...at,\n problem: `\\`localKey: \"${relation.localKey}\"\\` is not a column on \\`${sourceTableName}\\``,\n fix: `add the column, or set \\`localKey\\` to one of: ${quote(sourceColumns)}`\n });\n }\n break;\n }\n\n case \"hasOne\":\n case \"hasMany\": {\n if (!targetColumns.has(relation.foreignKeyOnTarget)) {\n // The default is derived from *this* collection's slug —\n // the column on the target that points back here.\n const stale = staleCodegenRename(\n relation.foreignKeyOnTarget,\n targetColumns,\n [collection.slug]\n );\n defects.push(stale\n ? { ...at, ...staleCodegenDefect(targetTableName, stale) }\n : {\n ...at,\n problem: `\\`foreignKeyOnTarget: \"${relation.foreignKeyOnTarget}\"\\` is not a column on the target table \\`${targetTableName}\\``,\n fix: `add the column, or set \\`foreignKeyOnTarget\\` to one of: ${quote(targetColumns)}`\n });\n }\n // `sourceKey` is the easiest of the two to put on the wrong\n // side — it is the only column in a `hasMany` that lives\n // here rather than on the target, and naming a target column\n // reads perfectly well right next to `foreignKeyOnTarget`.\n if (relation.sourceKey && !sourceColumns.has(relation.sourceKey)) {\n defects.push({\n ...at,\n problem: `\\`sourceKey: \"${relation.sourceKey}\"\\` is not a column on \\`${sourceTableName}\\``,\n fix: targetColumns.has(relation.sourceKey)\n ? `it is a column on the *target* table \\`${targetTableName}\\` — \\`sourceKey\\` names ` +\n \"the column on this collection that the target's foreign key points at, so it \" +\n `must be one of: ${quote(sourceColumns)}`\n : `add the column, or set \\`sourceKey\\` to one of: ${quote(sourceColumns)}`\n });\n }\n break;\n }\n\n case \"manyToMany\": {\n const { table, sourceColumn, targetColumn } = relation.through;\n const junction = registry.getTable(table);\n if (!junction) {\n defects.push({\n ...at,\n problem: `its junction table \\`${table}\\` does not exist`,\n fix: `create \\`${table}\\`, or name the real one with \\`through: { table: \"...\" }\\`. ` +\n \"Note that an omitted `through.table` is derived from the two table names sorted \" +\n \"and joined, so renaming a table changes it\"\n });\n break;\n }\n const junctionColumns = columnNames(junction);\n // Junction columns are the ones that actually moved in 0.13:\n // each defaults to its endpoint collection's *slug* run\n // through the foreign-key rule, and slugs are plural.\n const derivedFrom = {\n sourceColumn: [collection.slug],\n targetColumn: [targetCollection.slug]\n } as const;\n for (const [label, column] of [[\"sourceColumn\", sourceColumn], [\"targetColumn\", targetColumn]] as const) {\n if (!junctionColumns.has(column)) {\n const stale = staleCodegenRename(column, junctionColumns, [...derivedFrom[label]]);\n defects.push(stale\n ? { ...at, ...staleCodegenDefect(table, stale) }\n : {\n ...at,\n problem: `\\`through.${label}: \"${column}\"\\` is not a column on the junction table \\`${table}\\``,\n fix: `set \\`through.${label}\\` to one of: ${quote(junctionColumns)}` +\n (label === \"sourceColumn\" ? \" — it is the column naming *this* collection\" : \"\")\n });\n }\n }\n break;\n }\n\n case \"via\": {\n if (relation.joinPath.length === 0) {\n defects.push({\n ...at,\n problem: \"its `joinPath` is empty, so it joins nothing\",\n fix: \"add at least one step, ending at the target's table\"\n });\n break;\n }\n\n // Walk the chain: each step's `from` names columns on the\n // previous table, its `to` names columns on its own.\n let prevName = sourceTableName;\n let prevColumns = sourceColumns;\n let broken = false;\n\n for (const [i, step] of relation.joinPath.entries()) {\n const stepTable = registry.getTable(step.table);\n if (!stepTable) {\n defects.push({\n ...at,\n problem: `step ${i + 1} of its \\`joinPath\\` joins \\`${step.table}\\`, which is not a table in the schema`,\n fix: `correct \\`joinPath[${i}].table\\``\n });\n broken = true;\n break;\n }\n const stepColumns = columnNames(stepTable);\n\n for (const column of asColumns(step.on.from)) {\n if (!prevColumns.has(column)) {\n defects.push({\n ...at,\n problem: `step ${i + 1} joins \\`${prevName}.${column}\\` → \\`${step.table}\\`, but \\`${column}\\` is not a column on \\`${prevName}\\``,\n fix: `\\`joinPath[${i}].on.from\\` names columns on ${i === 0 ? \"this collection's table\" : `the previous step's table (\\`${prevName}\\`)`}: ${quote(prevColumns)}`\n });\n }\n }\n for (const column of asColumns(step.on.to)) {\n if (!stepColumns.has(column)) {\n defects.push({\n ...at,\n problem: `step ${i + 1} joins into \\`${step.table}.${column}\\`, but \\`${column}\\` is not a column on \\`${step.table}\\``,\n fix: `\\`joinPath[${i}].on.to\\` names columns on \\`${step.table}\\`: ${quote(stepColumns)}`\n });\n }\n }\n\n if (asColumns(step.on.from).length !== asColumns(step.on.to).length) {\n defects.push({\n ...at,\n problem: `step ${i + 1} compares ${asColumns(step.on.from).length} column(s) against ${asColumns(step.on.to).length}`,\n fix: `\\`from\\` and \\`to\\` must name the same number of columns in \\`joinPath[${i}]\\``\n });\n }\n\n prevName = step.table;\n prevColumns = stepColumns;\n }\n\n // The chain has to end where the relation says it points,\n // or the rows it returns are not the target's rows.\n if (!broken && prevName !== targetTableName) {\n defects.push({\n ...at,\n problem: `its \\`joinPath\\` ends at \\`${prevName}\\`, but it targets \\`${targetCollection.slug}\\` (table \\`${targetTableName}\\`)`,\n fix: `make the last step join \\`${targetTableName}\\`, or point \\`target\\` at the collection backed by \\`${prevName}\\``\n });\n }\n break;\n }\n\n default: {\n const exhaustive: never = relation;\n throw new Error(`Unhandled relation kind: ${JSON.stringify(exhaustive)}`);\n }\n }\n }\n }\n\n return defects;\n}\n\n/**\n * Fail boot on any relation that cannot resolve, listing all of them at once.\n *\n * Deliberately fatal rather than a warning. Every one of these produces an\n * empty result at query time and nothing else — an empty tab, an empty\n * `include`, a subcollection that looks like it has no rows. A server that\n * refuses to start is recoverable in a minute; a relation that quietly answers\n * \"nothing\" is the kind of bug found in production, weeks later, by a user\n * asking where their data went.\n */\nexport function assertRelationsResolve(\n collections: CollectionConfig[],\n registry: PostgresCollectionRegistry\n): void {\n const defects = findRelationDefects(collections, registry);\n if (defects.length === 0) return;\n\n const lines = defects.map(d =>\n ` • ${d.collection}.${d.relationName} (${d.kind})\\n` +\n ` ${d.problem}\\n` +\n ` fix: ${d.fix}`\n );\n\n throw new Error(\n `${defects.length} relation${defects.length === 1 ? \"\" : \"s\"} cannot resolve against ` +\n \"`backend/src/schema.generated.ts`.\\n\\n\" +\n \"Each of these would return no rows at query time rather than reporting an error, \" +\n \"so they are fatal at boot instead.\\n\\n\" +\n // This reads the *generated file*, not the database, and the difference\n // is the whole diagnosis after an upgrade. Boot-ensure renames columns\n // in the database — a 0.12 → 0.13 upgrade singularises a junction key,\n // `categorie_id` → `category_id` — and the checked-in file still\n // declares the old name. The config is then correct and the file is\n // stale, so the per-defect advice below, which lists the columns this\n // file has, names a column that no longer exists in the database.\n // Following it turns a recoverable state into a broken config.\n //\n // Hence the ordering: regenerate first, and only then consider that the\n // collection might be the thing that is wrong.\n \"If the database was migrated recently — an upgrade, a `db push`, a restore — this file is\\n\" +\n \"probably older than the schema it describes. Regenerate it before changing anything else:\\n\\n\" +\n \" rebase schema generate\\n\\n\" +\n \"If it is already current, then the collection is what disagrees with it:\\n\\n\" +\n lines.join(\"\\n\\n\") + \"\\n\"\n );\n}\n","import { isTable, getTableName, Relations } from \"drizzle-orm\";\nimport { PgEnum, PgTable } from \"drizzle-orm/pg-core\";\nimport { CollectionConfig } from \"@rebasepro/types\";\nimport { logger } from \"@rebasepro/server\";\nimport { PostgresCollectionRegistry } from \"./PostgresCollectionRegistry\";\nimport { warnOnKeysTheAdminCannotResolve } from \"../services/collection-helpers\";\nimport { assertRelationsResolve } from \"./validate-relations\";\n\n/**\n * Everything a registry is built from: the collections, and the drizzle schema\n * they are backed by. In BaaS mode all of it is introspected from the live\n * database; when collections are declared it comes from the config and the generated schema.\n */\nexport interface RegistrySchema {\n collections?: CollectionConfig[];\n tables?: Record<string, unknown>;\n enums?: Record<string, PgEnum<[string, ...string[]]>>;\n relations?: Record<string, Relations>;\n}\n\n/**\n * Build the collection registry for a driver.\n *\n * The order matters and is the reason this is one function rather than a run of\n * statements in the bootstrapper. Keys are resolved from the drizzle schema, so\n * anything that inspects them has to run *after* the tables are registered —\n * and `warnOnKeysTheAdminCannotResolve` fails open if it does not, because a\n * collection whose table it cannot look up is one it has nothing to say about.\n * Warned too early, it would skip every collection and report nothing, which\n * reads exactly like having nothing to report.\n */\nexport function buildCollectionRegistry(schema: RegistrySchema): PostgresCollectionRegistry {\n const registry = new PostgresCollectionRegistry();\n\n if (schema.collections) {\n registry.registerMultiple(schema.collections);\n // `Auto-discovered collections` already reports the count and the\n // directory they came from; this is the same fact with the names.\n logger.debug(\n `📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: ` +\n `[${registry.getCollections().map(c => c.slug).join(\", \")}]`\n );\n }\n\n if (schema.tables) {\n Object.values(schema.tables).forEach((table) => {\n if (isTable(table)) {\n registry.registerTable(table as PgTable, getTableName(table));\n }\n });\n }\n\n if (schema.enums) registry.registerEnums(schema.enums);\n if (schema.relations) registry.registerRelations(schema.relations);\n\n // Now that the keys resolve: say which of them the admin cannot see. It\n // compiles the same collection files into its bundle but never the drizzle\n // schema, and nothing serves it one, so only an edit to the config fixes it.\n warnOnKeysTheAdminCannotResolve(registry.getCollections(), registry);\n\n // And now that the tables resolve: refuse to start on a relation whose\n // names do not exist. The union checks a relation's shape at compile time;\n // only here is there a schema to check its *names* against. Every one of\n // these used to surface as an empty result at query time and nothing else.\n assertRelationsResolve(registry.getCollections(), registry);\n\n return registry;\n}\n","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","import { eq, getTableName, sql } from \"drizzle-orm\";\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { getTableConfig } from \"drizzle-orm/pg-core\";\nimport type { RebasePgTable } from \"../types\";\nimport { users, refreshTokens, passwordResetTokens, userIdentities, magicLinkTokens } from \"../schema/auth-schema\";\nimport {\n UserRepository,\n RoleRepository,\n TokenRepository,\n MfaRepository,\n AuthRepository,\n UserData,\n CreateUserData,\n RoleData,\n CreateRoleData,\n RefreshTokenInfo,\n RefreshTokenSession,\n PasswordResetTokenInfo,\n MagicLinkTokenInfo,\n UserIdentityData,\n ListUsersOptions,\n PaginatedUsersResult,\n MfaFactor,\n MfaChallengeInfo,\n RoleData as Role,\n ApiError\n} from \"@rebasepro/server\";\nimport { toSnakeCase, camelCase } from \"@rebasepro/utils\";\nimport { escapeLikePattern } from \"../utils/drizzle-conditions\";\nimport { extractPgError } from \"../utils/pg-error-utils\";\n\nexport type { Role };\n\nexport interface AuthSchemaTables {\n users: RebasePgTable;\n refreshTokens: RebasePgTable;\n passwordResetTokens: RebasePgTable;\n appConfig: RebasePgTable;\n userIdentities: RebasePgTable;\n}\n\nfunction getColumnKey(table: RebasePgTable | undefined, ...keys: string[]): string | undefined {\n if (!table) return undefined;\n for (const key of keys) {\n if (key in table) return key;\n const snake = toSnakeCase(key);\n if (snake in table) return snake;\n const camel = camelCase(key);\n if (camel in table) return camel;\n }\n return undefined;\n}\n\nfunction getColumn(table: RebasePgTable | undefined, ...keys: string[]): RebasePgTable[string] | undefined {\n if (!table) return undefined;\n const key = getColumnKey(table, ...keys);\n return key ? table[key] : undefined;\n}\n\n/**\n * The single definition of what an email address looks like in storage.\n *\n * Reads have always folded case; writes did not, and normalising was left to\n * each caller. That asymmetry is only ever one forgotten `.toLowerCase()` away\n * from a row no lookup can find — the account exists, every sign-in path\n * reports no such user, and the byte-exact UNIQUE on the column does not stop a\n * duplicate differing only in case. Applied on both sides here so the guarantee\n * belongs to the repository rather than to its callers' discipline; the\n * `lower(email)` unique index added in `ensureAuthTablesExist` is the database\n * half of the same rule.\n *\n * Whitespace goes too: a trailing space survives the fold and reproduces the\n * problem exactly.\n *\n * Re-exported rather than defined here: `@rebasepro/server` and\n * `@rebasepro/server-mongo` write this column too, and a second copy of this\n * rule is the defect it exists to prevent.\n */\nimport { normalizeEmail } from \"@rebasepro/common\";\nexport { normalizeEmail };\n\n/**\n * PostgreSQL implementation of UserRepository.\n * Handles all user-related database operations using Drizzle ORM.\n */\nexport class UserService implements UserRepository {\n private usersTable: RebasePgTable;\n private userIdentitiesTable: RebasePgTable;\n\n constructor(\n private db: NodePgDatabase,\n tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>\n ) {\n if (tableOrTables && ((tableOrTables as Partial<AuthSchemaTables>).users)) {\n const tables = tableOrTables as Partial<AuthSchemaTables>;\n this.usersTable = (tables.users || users) as RebasePgTable;\n this.userIdentitiesTable = (tables.userIdentities || userIdentities) as RebasePgTable;\n } else {\n const table = tableOrTables as RebasePgTable | undefined;\n this.usersTable = table || (users as unknown as RebasePgTable);\n this.userIdentitiesTable = userIdentities as unknown as RebasePgTable;\n }\n }\n\n private getQualifiedUsersTableName(): string {\n const name = getTableName(this.usersTable);\n const schema = getTableConfig(this.usersTable).schema || \"public\";\n return `\"${schema}\".\"${name}\"`;\n }\n\n /**\n * Run a privileged auth write with an explicitly cleared RLS context.\n *\n * The auth services run on the base/owner connection, which by design\n * carries a NULL `app.uid` so the `rebase.uid() IS NULL` server-escape\n * in the default policies applies. That NULL is normally guaranteed by\n * `set_config(..., is_local = true)` resetting at transaction end — but a\n * GUC that survives on a pooled connection (or a connection role that\n * doesn't bypass RLS: FORCE ROW LEVEL SECURITY, or a non-owner role)\n * turns the trusted write into an RLS-scoped one and denies it with\n * SQLSTATE 42501. Clearing the GUCs here, transaction-locally at the\n * single chokepoint, makes the server context deterministic instead of\n * trusting whatever state the pool hands us. `rebase.uid()` reads '' as\n * NULL via NULLIF, so '' is the server context.\n */\n private async withServerContext<T>(fn: (db: NodePgDatabase) => Promise<T>): Promise<T> {\n return await this.db.transaction(async (tx) => {\n await tx.execute(sql`\n SELECT set_config('app.uid', '', true),\n set_config('app.user_id', '', true),\n set_config('app.user_roles', '', true),\n set_config('app.jwt', '', true)\n `);\n return await fn(tx as unknown as NodePgDatabase);\n });\n }\n\n private mapRowToUser(row: Record<string, unknown>): UserData {\n if (!row) return row as UserData;\n\n const id = (row.id ?? row.uid) as string;\n const email = row.email as string;\n const passwordHash = (row.password_hash ?? row.passwordHash ?? null) as string | null | undefined;\n const displayName = (row.display_name ?? row.displayName ?? null) as string | null | undefined;\n const photoUrl = (row.photo_url ?? row.photoUrl ?? row.photoURL ?? null) as string | null | undefined;\n const emailVerified = (row.email_verified ?? row.emailVerified ?? false) as boolean;\n const emailVerificationToken = (row.email_verification_token ?? row.emailVerificationToken ?? null) as string | null | undefined;\n const emailVerificationSentAt = (row.email_verification_sent_at ?? row.emailVerificationSentAt ?? null) as string | number | Date | null;\n const isAnonymous = (row.is_anonymous ?? row.isAnonymous ?? false) as boolean;\n const createdAt = (row.created_at ?? row.createdAt) as string | number | Date | undefined;\n const updatedAt = (row.updated_at ?? row.updatedAt) as string | number | Date | undefined;\n\n const metadata: Record<string, any> = { ...((row.metadata as Record<string, any> | undefined) || {}) };\n\n const knownKeys = new Set([\n \"id\", \"uid\", \"email\",\n \"password_hash\", \"passwordHash\",\n \"display_name\", \"displayName\",\n \"photo_url\", \"photoUrl\", \"photoURL\",\n \"email_verified\", \"emailVerified\",\n \"email_verification_token\", \"emailVerificationToken\",\n \"email_verification_sent_at\", \"emailVerificationSentAt\",\n \"is_anonymous\", \"isAnonymous\",\n \"roles\",\n \"created_at\", \"createdAt\",\n \"updated_at\", \"updatedAt\",\n \"metadata\"\n ]);\n\n for (const [key, val] of Object.entries(row)) {\n if (!knownKeys.has(key)) {\n const camelKey = camelCase(key);\n metadata[camelKey] = val;\n }\n }\n\n return {\n id,\n email,\n passwordHash,\n displayName,\n photoUrl,\n emailVerified,\n emailVerificationToken,\n emailVerificationSentAt: emailVerificationSentAt ? new Date(emailVerificationSentAt) : null,\n isAnonymous,\n createdAt: createdAt ? new Date(createdAt) : new Date(),\n updatedAt: updatedAt ? new Date(updatedAt) : new Date(),\n metadata\n };\n }\n\n private mapPayload(data: Partial<CreateUserData>): Record<string, unknown> {\n if (!data) return {};\n\n const payload: Record<string, unknown> = {};\n\n const idKey = getColumnKey(this.usersTable, \"id\") || \"id\";\n const emailKey = getColumnKey(this.usersTable, \"email\") || \"email\";\n const passwordHashKey = getColumnKey(this.usersTable, \"passwordHash\", \"password_hash\") || \"passwordHash\";\n const displayNameKey = getColumnKey(this.usersTable, \"displayName\", \"display_name\") || \"displayName\";\n const photoUrlKey = getColumnKey(this.usersTable, \"photoUrl\", \"photo_url\") || \"photoUrl\";\n const emailVerifiedKey = getColumnKey(this.usersTable, \"emailVerified\", \"email_verified\") || \"emailVerified\";\n const emailVerificationTokenKey = getColumnKey(this.usersTable, \"emailVerificationToken\", \"email_verification_token\") || \"emailVerificationToken\";\n const emailVerificationSentAtKey = getColumnKey(this.usersTable, \"emailVerificationSentAt\", \"email_verification_sent_at\") || \"emailVerificationSentAt\";\n const isAnonymousKey = getColumnKey(this.usersTable, \"isAnonymous\", \"is_anonymous\") || \"isAnonymous\";\n const createdAtKey = getColumnKey(this.usersTable, \"createdAt\", \"created_at\") || \"createdAt\";\n const updatedAtKey = getColumnKey(this.usersTable, \"updatedAt\", \"updated_at\") || \"updatedAt\";\n const metadataKey = getColumnKey(this.usersTable, \"metadata\") || \"metadata\";\n\n if (\"id\" in data) payload[idKey] = data.id;\n if (\"email\" in data) payload[emailKey] = normalizeEmail(data.email);\n if (\"passwordHash\" in data) payload[passwordHashKey] = data.passwordHash;\n if (\"displayName\" in data) payload[displayNameKey] = data.displayName;\n if (\"photoUrl\" in data) payload[photoUrlKey] = data.photoUrl;\n if (\"emailVerified\" in data) payload[emailVerifiedKey] = data.emailVerified;\n if (\"emailVerificationToken\" in data) payload[emailVerificationTokenKey] = data.emailVerificationToken;\n if (\"emailVerificationSentAt\" in data) payload[emailVerificationSentAtKey] = data.emailVerificationSentAt;\n if (\"isAnonymous\" in data) payload[isAnonymousKey] = data.isAnonymous;\n if (\"createdAt\" in data) payload[createdAtKey] = data.createdAt;\n if (\"updatedAt\" in data) payload[updatedAtKey] = data.updatedAt;\n\n const metadata: Record<string, any> = { ...(data.metadata || {}) };\n const remainingMetadata: Record<string, any> = {};\n\n for (const [key, val] of Object.entries(metadata)) {\n const tableColKey = getColumnKey(this.usersTable, key);\n if (tableColKey &&\n tableColKey !== idKey &&\n tableColKey !== emailKey &&\n tableColKey !== passwordHashKey &&\n tableColKey !== displayNameKey &&\n tableColKey !== photoUrlKey &&\n tableColKey !== emailVerifiedKey &&\n tableColKey !== emailVerificationTokenKey &&\n tableColKey !== emailVerificationSentAtKey &&\n tableColKey !== isAnonymousKey &&\n tableColKey !== createdAtKey &&\n tableColKey !== updatedAtKey &&\n tableColKey !== metadataKey) {\n payload[tableColKey] = val;\n } else {\n remainingMetadata[key] = val;\n }\n }\n\n if (metadataKey in this.usersTable) {\n payload[metadataKey] = remainingMetadata;\n }\n\n return payload;\n }\n\n /**\n * @see UserRepository.createUser — an email already in use is a 409.\n *\n * The route checks first and answers 409; this is the same answer for the\n * requests that get past the check, which two clicks on a signup button\n * are enough to produce. `PersistService` has mapped `23505` to a conflict\n * for collection writes since the layer that holds the SQLSTATE was made\n * responsible for saying whose fault a failure is; the auth writes never\n * got the same treatment and reached the client as \"Internal Server Error\".\n */\n async createUser(data: CreateUserData): Promise<UserData> {\n const payload = this.mapPayload(data);\n try {\n const [row] = await this.withServerContext(async (db) =>\n (await db.insert(this.usersTable).values(payload).returning()) as Record<string, unknown>[]\n );\n return this.mapRowToUser(row);\n } catch (error) {\n // Drizzle wraps the pg error, so the SQLSTATE is down the `cause`\n // chain rather than on the error itself.\n if (extractPgError(error)?.code === \"23505\") {\n throw ApiError.conflict(\"Email already registered\", \"EMAIL_EXISTS\");\n }\n throw error;\n }\n }\n\n async getUserById(id: string): Promise<UserData | null> {\n const idCol = getColumn(this.usersTable, \"id\");\n if (!idCol) return null;\n const [row] = await this.db.select().from(this.usersTable).where(eq(idCol, id));\n return row ? this.mapRowToUser(row as Record<string, unknown>) : null;\n }\n\n async getUserByEmail(email: string): Promise<UserData | null> {\n const emailCol = getColumn(this.usersTable, \"email\");\n if (!emailCol) return null;\n const [row] = await this.db.select().from(this.usersTable).where(eq(emailCol, normalizeEmail(email)));\n return row ? this.mapRowToUser(row as Record<string, unknown>) : null;\n }\n\n async getUserByIdentity(provider: string, providerId: string): Promise<UserData | null> {\n const userIdCol = getColumn(this.usersTable, \"id\");\n if (!userIdCol) return null;\n\n const result = await this.db\n .select({ user: this.usersTable })\n .from(this.usersTable)\n .innerJoin(this.userIdentitiesTable, eq(userIdCol, this.userIdentitiesTable.uid))\n .where(\n sql`${this.userIdentitiesTable.provider} = ${provider} AND ${this.userIdentitiesTable.providerId} = ${providerId}`\n )\n .limit(1);\n\n if (result.length === 0) return null;\n return this.mapRowToUser(result[0].user as Record<string, unknown>);\n }\n\n async getUserIdentities(uid: string): Promise<UserIdentityData[]> {\n const schema = getTableConfig(this.userIdentitiesTable).schema || \"public\";\n const result = await this.db.execute(sql`\n SELECT id, uid, provider, provider_id, profile_data, created_at, updated_at\n FROM ${sql.raw(`\"${schema}\".\"user_identities\"`)}\n WHERE uid = ${uid}\n `);\n\n return result.rows.map((row: Record<string, unknown>) => ({\n id: row.id as string,\n uid: row.uid as string,\n provider: row.provider as string,\n providerId: row.provider_id as string,\n profileData: (row.profile_data as Record<string, unknown> | null) ?? null,\n createdAt: row.created_at as Date,\n updatedAt: row.updated_at as Date\n }));\n }\n\n async linkUserIdentity(uid: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void> {\n await this.withServerContext(async (db) => db.insert(this.userIdentitiesTable).values({\n uid,\n provider,\n providerId,\n profileData: profileData || null\n }).onConflictDoNothing({ target: [this.userIdentitiesTable.provider, this.userIdentitiesTable.providerId] }));\n }\n\n async updateUser(id: string, data: Partial<Omit<CreateUserData, \"id\">>): Promise<UserData | null> {\n const idCol = getColumn(this.usersTable, \"id\");\n if (!idCol) return null;\n const payload = this.mapPayload(data);\n const updatedAtKey = getColumnKey(this.usersTable, \"updatedAt\", \"updated_at\") || \"updatedAt\";\n payload[updatedAtKey] = new Date();\n\n const [row] = await this.withServerContext(async (db) =>\n (await db\n .update(this.usersTable)\n .set(payload)\n .where(eq(idCol, id))\n .returning()) as Record<string, unknown>[]\n );\n return row ? this.mapRowToUser(row) : null;\n }\n\n async deleteUser(id: string): Promise<void> {\n const idCol = getColumn(this.usersTable, \"id\");\n if (!idCol) return;\n await this.withServerContext(async (db) => db.delete(this.usersTable).where(eq(idCol, id)));\n }\n\n async listUsers(): Promise<UserData[]> {\n const rows = await this.db.select().from(this.usersTable);\n return (rows as Record<string, unknown>[]).map(row => this.mapRowToUser(row));\n }\n\n async listUsersPaginated(options?: ListUsersOptions): Promise<PaginatedUsersResult> {\n const limit = options?.limit ?? 25;\n const offset = options?.offset ?? 0;\n const search = options?.search?.trim() || \"\";\n const orderBy = options?.orderBy || \"createdAt\";\n const orderDir = options?.orderDir || \"desc\";\n const roleId = options?.roleId;\n\n const orderCol = getColumn(this.usersTable, orderBy);\n const orderColumn = orderCol ? orderCol.name : \"created_at\";\n const direction = orderDir === \"asc\" ? sql`ASC` : sql`DESC`;\n\n const emailCol = getColumn(this.usersTable, \"email\");\n const emailColumn = emailCol ? emailCol.name : \"email\";\n const displayNameCol = getColumn(this.usersTable, \"displayName\", \"display_name\");\n const displayNameColumn = displayNameCol ? displayNameCol.name : \"display_name\";\n const idCol = getColumn(this.usersTable, \"id\");\n const idColumn = idCol ? idCol.name : \"id\";\n\n const usersTableName = this.getQualifiedUsersTableName();\n const conditions = [];\n if (roleId) {\n conditions.push(sql`${roleId} = ANY(${sql.raw(usersTableName)}.roles)`);\n }\n if (search) {\n // `search` is a substring search over the admin user list, not a\n // pattern the caller writes: the same reasoning as the collection\n // search path, so it shares that path's helper rather than growing\n // a second copy that can drift. See `escapeLikePattern`.\n const pattern = `%${escapeLikePattern(search)}%`;\n conditions.push(sql`(${sql.raw(usersTableName)}.${sql.raw(emailColumn)} ILIKE ${pattern} OR ${sql.raw(usersTableName)}.${sql.raw(displayNameColumn)} ILIKE ${pattern})`);\n }\n\n const whereClause = conditions.length > 0 ? sql`WHERE ${sql.join(conditions, sql` AND `)}` : sql``;\n\n // Sorting: users with roles first if no role filter, then by requested order\n const orderByClause = roleId\n ? sql`ORDER BY ${sql.raw(usersTableName)}.${sql.raw(orderColumn)} ${direction}`\n : sql`ORDER BY array_length(${sql.raw(usersTableName)}.roles, 1) DESC NULLS LAST, ${sql.raw(usersTableName)}.${sql.raw(orderColumn)} ${direction}`;\n\n const countResult = await this.db.execute(sql`\n SELECT count(*)::int as total FROM ${sql.raw(usersTableName)}\n ${whereClause}\n `);\n const total = (countResult.rows[0] as { total: number }).total;\n\n const dataResult = await this.db.execute(sql`\n SELECT * FROM ${sql.raw(usersTableName)}\n ${whereClause}\n ${orderByClause}\n LIMIT ${limit} OFFSET ${offset}\n `);\n const rows = dataResult.rows;\n\n // Map rows to camelCase UserData\n const mappedUsers: UserData[] = (rows as Record<string, unknown>[]).map((row) => this.mapRowToUser(row));\n\n return { users: mappedUsers,\n total,\n limit,\n offset };\n }\n\n /**\n * Update user's password hash\n */\n async updatePassword(id: string, passwordHash: string): Promise<void> {\n const idCol = getColumn(this.usersTable, \"id\");\n if (!idCol) return;\n const passwordHashColKey = getColumnKey(this.usersTable, \"passwordHash\", \"password_hash\") || \"passwordHash\";\n const updatedAtColKey = getColumnKey(this.usersTable, \"updatedAt\", \"updated_at\") || \"updatedAt\";\n\n await this.withServerContext(async (db) => db\n .update(this.usersTable)\n .set({\n [passwordHashColKey]: passwordHash,\n [updatedAtColKey]: new Date()\n })\n .where(eq(idCol, id)));\n }\n\n /**\n * Set email verification status\n */\n async setEmailVerified(id: string, verified: boolean): Promise<void> {\n const idCol = getColumn(this.usersTable, \"id\");\n if (!idCol) return;\n const emailVerifiedColKey = getColumnKey(this.usersTable, \"emailVerified\", \"email_verified\") || \"emailVerified\";\n const emailVerificationTokenColKey = getColumnKey(this.usersTable, \"emailVerificationToken\", \"email_verification_token\") || \"emailVerificationToken\";\n const updatedAtColKey = getColumnKey(this.usersTable, \"updatedAt\", \"updated_at\") || \"updatedAt\";\n\n await this.withServerContext(async (db) => db\n .update(this.usersTable)\n .set({\n [emailVerifiedColKey]: verified,\n [emailVerificationTokenColKey]: null,\n [updatedAtColKey]: new Date()\n })\n .where(eq(idCol, id)));\n }\n\n /**\n * Set email verification token\n */\n async setVerificationToken(id: string, token: string | null): Promise<void> {\n const idCol = getColumn(this.usersTable, \"id\");\n if (!idCol) return;\n const emailVerificationTokenColKey = getColumnKey(this.usersTable, \"emailVerificationToken\", \"email_verification_token\") || \"emailVerificationToken\";\n const emailVerificationSentAtColKey = getColumnKey(this.usersTable, \"emailVerificationSentAt\", \"email_verification_sent_at\") || \"emailVerificationSentAt\";\n const updatedAtColKey = getColumnKey(this.usersTable, \"updatedAt\", \"updated_at\") || \"updatedAt\";\n\n await this.withServerContext(async (db) => db\n .update(this.usersTable)\n .set({\n [emailVerificationTokenColKey]: token,\n [emailVerificationSentAtColKey]: token ? new Date() : null,\n [updatedAtColKey]: new Date()\n })\n .where(eq(idCol, id)));\n }\n\n /**\n * Find user by email verification token\n */\n async getUserByVerificationToken(token: string): Promise<UserData | null> {\n const tokenCol = getColumn(this.usersTable, \"emailVerificationToken\", \"email_verification_token\");\n if (!tokenCol) return null;\n const [row] = await this.db\n .select()\n .from(this.usersTable)\n .where(eq(tokenCol, token));\n return row ? this.mapRowToUser(row as Record<string, unknown>) : null;\n }\n\n /**\n * Get roles for a user from database (inline TEXT[] column)\n */\n async getUserRoles(uid: string): Promise<Role[]> {\n const usersTableName = this.getQualifiedUsersTableName();\n const result = await this.db.execute(sql`\n SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${uid}\n `);\n\n if (result.rows.length === 0) return [];\n\n const row = result.rows[0] as { roles: string[] | null };\n const roleIds = row.roles ?? [];\n\n return roleIds.map(id => ({\n id,\n name: id,\n isAdmin: id === \"admin\",\n defaultPermissions: null,\n collectionPermissions: null\n }));\n }\n\n /**\n * Get role IDs for a user\n */\n async getUserRoleIds(uid: string): Promise<string[]> {\n const usersTableName = this.getQualifiedUsersTableName();\n const result = await this.db.execute(sql`\n SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${uid}\n `);\n\n if (result.rows.length === 0) return [];\n\n const row = result.rows[0] as { roles: string[] | null };\n return row.roles ?? [];\n }\n\n /**\n * Set roles for a user (replaces existing roles)\n */\n async setUserRoles(uid: string, roleIds: string[]): Promise<void> {\n const usersTableName = this.getQualifiedUsersTableName();\n const rolesArray = `{${roleIds.join(\",\")}}`;\n await this.withServerContext(async (db) => db.execute(sql`\n UPDATE ${sql.raw(usersTableName)}\n SET roles = ${rolesArray}::text[], updated_at = NOW()\n WHERE id = ${uid}\n `));\n }\n\n /**\n * Assign a specific role to new user (appends if not present)\n */\n async assignDefaultRole(uid: string, roleId: string): Promise<void> {\n const usersTableName = this.getQualifiedUsersTableName();\n await this.withServerContext(async (db) => db.execute(sql`\n UPDATE ${sql.raw(usersTableName)}\n SET roles = array_append(roles, ${roleId}), updated_at = NOW()\n WHERE id = ${uid} AND NOT (${roleId} = ANY(roles))\n `));\n }\n\n /**\n * Get user with their roles\n */\n async getUserWithRoles(uid: string): Promise<{ user: UserData; roles: Role[] } | null> {\n const user = await this.getUserById(uid);\n if (!user) return null;\n\n const roles = await this.getUserRoles(uid);\n return { user,\n roles };\n }\n}\n\n\nexport class RefreshTokenService {\n private refreshTokensTable: RebasePgTable;\n private usersTable: RebasePgTable | null;\n\n constructor(\n private db: NodePgDatabase,\n tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>\n ) {\n if (tableOrTables && ((tableOrTables as Partial<AuthSchemaTables>).refreshTokens || (tableOrTables as Partial<AuthSchemaTables>).users)) {\n this.refreshTokensTable = ((tableOrTables as Partial<AuthSchemaTables>).refreshTokens || refreshTokens) as RebasePgTable;\n this.usersTable = ((tableOrTables as Partial<AuthSchemaTables>).users || users) as RebasePgTable;\n } else {\n this.refreshTokensTable = (tableOrTables as RebasePgTable) || (refreshTokens as unknown as RebasePgTable);\n this.usersTable = users as unknown as RebasePgTable;\n }\n }\n\n /**\n * Whether the table actually carries a column, so a host application that\n * supplied its own `refresh_tokens` table — one that predates session\n * grouping — degrades instead of throwing on every sign-in.\n */\n private has(column: string): boolean {\n return Boolean((this.refreshTokensTable as unknown as Record<string, unknown>)[column]);\n }\n\n private col(column: string) {\n return (this.refreshTokensTable as unknown as Record<string, never>)[column];\n }\n\n /** The columns to read back, narrowed to the ones this table has. */\n private selection() {\n const selection: Record<string, never> = {\n id: this.refreshTokensTable.id,\n uid: this.refreshTokensTable.uid,\n tokenHash: this.refreshTokensTable.tokenHash,\n expiresAt: this.refreshTokensTable.expiresAt,\n createdAt: this.refreshTokensTable.createdAt,\n userAgent: this.refreshTokensTable.userAgent,\n ipAddress: this.refreshTokensTable.ipAddress\n } as unknown as Record<string, never>;\n for (const optional of [\"sessionId\", \"rotatedAt\", \"revoked\", \"sessionStartedAt\", \"aal\"]) {\n if (this.has(optional)) selection[optional] = this.col(optional);\n }\n return selection;\n }\n\n async createToken(\n uid: string,\n tokenHash: string,\n expiresAt: Date,\n userAgent?: string,\n ipAddress?: string,\n session?: RefreshTokenSession\n ): Promise<void> {\n // Empty strings rather than NULLs: the device-session UNIQUE constraint\n // that needed them is gone, but sessions-list UIs already render \"\" as\n // \"unknown device\" and would start showing blanks otherwise.\n const safeUserAgent = userAgent || \"\";\n const safeIpAddress = ipAddress || \"\";\n\n // A plain INSERT. Rotation ADDS a token; it does not replace a device's\n // row. Two refreshes racing on the same session therefore both succeed\n // and both end holding a usable token, where the previous upsert had\n // them overwrite each other and logged one of the two tabs out.\n const values: Record<string, unknown> = {\n uid,\n tokenHash,\n expiresAt,\n userAgent: safeUserAgent,\n ipAddress: safeIpAddress\n };\n if (session && this.has(\"sessionId\")) values.sessionId = session.id;\n if (session && this.has(\"sessionStartedAt\")) values.sessionStartedAt = session.startedAt;\n // Written on every token of the session, including the ones rotation\n // mints, because refresh reads the level off whichever row was\n // presented. A table without the column degrades to `aal1` on read,\n // which is the restrictive answer rather than a bypass.\n if (session?.aal && this.has(\"aal\")) values.aal = session.aal;\n\n await this.db.insert(this.refreshTokensTable).values(values);\n }\n\n async findByHash(tokenHash: string): Promise<RefreshTokenInfo | null> {\n const [token] = await this.db\n .select(this.selection())\n .from(this.refreshTokensTable)\n .where(eq(this.refreshTokensTable.tokenHash, tokenHash));\n\n return (token as unknown as RefreshTokenInfo) || null;\n }\n\n /**\n * Record that a token was rotated away, keeping the row.\n *\n * The row is what lets `/auth/refresh` distinguish \"you already used this,\n * here is a fresh one\" from \"no idea what this is\". Deleting it — which is\n * what this used to do — collapsed both into a 401 and signed the user out\n * for the crime of losing a response.\n */\n async markRotated(tokenHash: string): Promise<void> {\n if (!this.has(\"rotatedAt\")) {\n await this.deleteByHash(tokenHash);\n return;\n }\n await this.db\n .update(this.refreshTokensTable)\n .set({ rotatedAt: new Date() })\n .where(eq(this.refreshTokensTable.tokenHash, tokenHash));\n }\n\n /** Final kill of one sign-in: logout, or revoking a device remotely. */\n async revokeSession(sessionId: string): Promise<void> {\n if (!this.has(\"sessionId\")) return;\n if (this.has(\"revoked\")) {\n await this.db\n .update(this.refreshTokensTable)\n .set({ revoked: true, ...(this.has(\"rotatedAt\") ? { rotatedAt: new Date() } : {}) })\n .where(eq(this.col(\"sessionId\"), sessionId));\n return;\n }\n await this.db.delete(this.refreshTokensTable).where(eq(this.col(\"sessionId\"), sessionId));\n }\n\n /**\n * Housekeeping: rotation would otherwise leave a row per refresh forever.\n * Superseded rows are only needed for as long as a straggler might still\n * present them, and expired ones are dead weight everywhere.\n */\n async prune(uid: string, sessionId: string, supersededBefore: Date): Promise<void> {\n const uidCol = this.refreshTokensTable.uid;\n const expiresCol = this.refreshTokensTable.expiresAt;\n if (!this.has(\"rotatedAt\") || !this.has(\"sessionId\")) {\n await this.db.delete(this.refreshTokensTable)\n .where(sql`${uidCol} = ${uid} AND ${expiresCol} < NOW()`);\n return;\n }\n const rotatedCol = this.col(\"rotatedAt\");\n const sessionCol = this.col(\"sessionId\");\n await this.db.delete(this.refreshTokensTable).where(sql`\n ${uidCol} = ${uid}\n AND (\n ${expiresCol} < NOW()\n OR (\n ${sessionCol} = ${sessionId}\n AND ${rotatedCol} IS NOT NULL\n AND ${rotatedCol} < ${supersededBefore}\n )\n )\n `);\n }\n\n async getTokensValidAfter(uid: string): Promise<Date | null> {\n if (!this.usersTable || !(this.usersTable as unknown as Record<string, unknown>).tokensValidAfter) return null;\n const [row] = await this.db\n .select({ tokensValidAfter: (this.usersTable as unknown as Record<string, never>).tokensValidAfter })\n .from(this.usersTable)\n .where(eq(this.usersTable.id, uid));\n const value = (row as { tokensValidAfter?: Date | string | null } | undefined)?.tokensValidAfter;\n return value ? new Date(value) : null;\n }\n\n async setTokensValidAfter(uid: string, at: Date): Promise<void> {\n if (!this.usersTable || !(this.usersTable as unknown as Record<string, unknown>).tokensValidAfter) return;\n await this.db\n .update(this.usersTable)\n .set({ tokensValidAfter: at })\n .where(eq(this.usersTable.id, uid));\n }\n\n async deleteByHash(tokenHash: string): Promise<void> {\n await this.db.delete(this.refreshTokensTable).where(eq(this.refreshTokensTable.tokenHash, tokenHash));\n }\n\n async deleteAllForUser(uid: string): Promise<void> {\n await this.db.delete(this.refreshTokensTable).where(eq(this.refreshTokensTable.uid, uid));\n }\n\n async listForUser(uid: string): Promise<RefreshTokenInfo[]> {\n const tokens = await this.db\n .select(this.selection())\n .from(this.refreshTokensTable)\n .where(eq(this.refreshTokensTable.uid, uid))\n .orderBy(this.refreshTokensTable.createdAt);\n\n return tokens as unknown as RefreshTokenInfo[];\n }\n\n async deleteById(id: string, uid: string): Promise<void> {\n await this.db.delete(this.refreshTokensTable)\n .where(sql`${this.refreshTokensTable.id} = ${id} AND ${this.refreshTokensTable.uid} = ${uid}`);\n }\n}\n\n/**\n * Password reset token service\n */\nexport class PasswordResetTokenService {\n private passwordResetTokensTable: RebasePgTable;\n\n constructor(\n private db: NodePgDatabase,\n tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>\n ) {\n if (tableOrTables && ((tableOrTables as Partial<AuthSchemaTables>).passwordResetTokens || (tableOrTables as Partial<AuthSchemaTables>).users)) {\n this.passwordResetTokensTable = ((tableOrTables as Partial<AuthSchemaTables>).passwordResetTokens || passwordResetTokens) as RebasePgTable;\n } else {\n this.passwordResetTokensTable = (tableOrTables as RebasePgTable) || (passwordResetTokens as unknown as RebasePgTable);\n }\n }\n\n private getQualifiedPasswordResetTokensTableName(): string {\n const name = getTableName(this.passwordResetTokensTable);\n const schema = getTableConfig(this.passwordResetTokensTable).schema || \"public\";\n return `\"${schema}\".\"${name}\"`;\n }\n\n /**\n * Create a password reset token\n */\n async createToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {\n // Delete any existing unused tokens for this user\n const tableName = this.getQualifiedPasswordResetTokensTableName();\n await this.db.execute(sql`\n DELETE FROM ${sql.raw(tableName)} \n WHERE uid = ${uid} AND used_at IS NULL\n `);\n\n await this.db.insert(this.passwordResetTokensTable).values({\n uid,\n tokenHash,\n expiresAt\n });\n }\n\n /**\n * Find a valid (not expired, not used) token by hash\n */\n async findValidByHash(tokenHash: string): Promise<{ uid: string; expiresAt: Date } | null> {\n const [token] = await this.db\n .select({\n uid: this.passwordResetTokensTable.uid,\n expiresAt: this.passwordResetTokensTable.expiresAt\n })\n .from(this.passwordResetTokensTable)\n .where(eq(this.passwordResetTokensTable.tokenHash, tokenHash)) as unknown as Array<{ uid: string; expiresAt: Date }>;\n\n if (!token) return null;\n\n // Check if expired or used\n const tableName = this.getQualifiedPasswordResetTokensTableName();\n const result = await this.db.execute(sql`\n SELECT uid, expires_at \n FROM ${sql.raw(tableName)} \n WHERE token_hash = ${tokenHash} \n AND used_at IS NULL \n AND expires_at > NOW()\n `);\n\n if (result.rows.length === 0) return null;\n\n const row = result.rows[0] as { uid: string; expires_at: string | number | Date };\n return {\n uid: row.uid,\n expiresAt: new Date(row.expires_at)\n };\n }\n\n /**\n * Mark token as used\n */\n async markAsUsed(tokenHash: string): Promise<void> {\n await this.db\n .update(this.passwordResetTokensTable)\n .set({ usedAt: new Date() })\n .where(eq(this.passwordResetTokensTable.tokenHash, tokenHash));\n }\n\n /**\n * Delete all tokens for a user\n */\n async deleteAllForUser(uid: string): Promise<void> {\n await this.db.delete(this.passwordResetTokensTable).where(eq(this.passwordResetTokensTable.uid, uid));\n }\n\n /**\n * Clean up expired tokens\n */\n async deleteExpired(): Promise<void> {\n const tableName = this.getQualifiedPasswordResetTokensTableName();\n await this.db.execute(sql`\n DELETE FROM ${sql.raw(tableName)} \n WHERE expires_at < NOW()\n `);\n }\n}\n\n/**\n * Magic link token service.\n * Handles magic link token storage for passwordless email login.\n */\nexport class MagicLinkTokenService {\n private magicLinkTokensTable: RebasePgTable;\n\n constructor(\n private db: NodePgDatabase,\n tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>\n ) {\n this.magicLinkTokensTable = (magicLinkTokens as unknown as RebasePgTable);\n }\n\n private getQualifiedTableName(): string {\n const name = getTableName(this.magicLinkTokensTable);\n const schema = getTableConfig(this.magicLinkTokensTable).schema || \"public\";\n return `\"${schema}\".\"${name}\"`;\n }\n\n async createToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {\n // Delete any existing unused tokens for this user\n const tableName = this.getQualifiedTableName();\n await this.db.execute(sql`\n DELETE FROM ${sql.raw(tableName)} \n WHERE uid = ${uid} AND used_at IS NULL\n `);\n\n await this.db.insert(this.magicLinkTokensTable).values({\n uid,\n tokenHash,\n expiresAt\n });\n }\n\n async findValidByHash(tokenHash: string): Promise<MagicLinkTokenInfo | null> {\n const tableName = this.getQualifiedTableName();\n const result = await this.db.execute(sql`\n SELECT uid, expires_at \n FROM ${sql.raw(tableName)} \n WHERE token_hash = ${tokenHash} \n AND used_at IS NULL \n AND expires_at > NOW()\n `);\n\n if (result.rows.length === 0) return null;\n\n const row = result.rows[0] as { uid: string; expires_at: string | number | Date };\n return {\n uid: row.uid,\n expiresAt: new Date(row.expires_at)\n };\n }\n\n async markAsUsed(tokenHash: string): Promise<void> {\n await this.db\n .update(this.magicLinkTokensTable)\n .set({ usedAt: new Date() })\n .where(eq(this.magicLinkTokensTable.tokenHash, tokenHash));\n }\n}\n\n/**\n * PostgreSQL implementation of TokenRepository.\n * Combines refresh token and password reset token operations.\n */\nexport class PostgresTokenRepository implements TokenRepository {\n private refreshTokenService: RefreshTokenService;\n private passwordResetTokenService: PasswordResetTokenService;\n private magicLinkTokenService: MagicLinkTokenService;\n\n constructor(\n private db: NodePgDatabase,\n tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>\n ) {\n this.refreshTokenService = new RefreshTokenService(db, tableOrTables);\n this.passwordResetTokenService = new PasswordResetTokenService(db, tableOrTables);\n this.magicLinkTokenService = new MagicLinkTokenService(db, tableOrTables);\n }\n\n // Refresh token operations\n\n async createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise<void> {\n await this.refreshTokenService.createToken(uid, tokenHash, expiresAt, userAgent, ipAddress, session);\n }\n\n async markRefreshTokenRotated(tokenHash: string): Promise<void> {\n await this.refreshTokenService.markRotated(tokenHash);\n }\n\n async revokeRefreshTokenSession(sessionId: string): Promise<void> {\n await this.refreshTokenService.revokeSession(sessionId);\n }\n\n async pruneRefreshTokens(uid: string, sessionId: string, supersededBefore: Date): Promise<void> {\n await this.refreshTokenService.prune(uid, sessionId, supersededBefore);\n }\n\n async getTokensValidAfter(uid: string): Promise<Date | null> {\n return this.refreshTokenService.getTokensValidAfter(uid);\n }\n\n async setTokensValidAfter(uid: string, at: Date): Promise<void> {\n await this.refreshTokenService.setTokensValidAfter(uid, at);\n }\n\n async findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null> {\n return this.refreshTokenService.findByHash(tokenHash);\n }\n\n async deleteRefreshToken(tokenHash: string): Promise<void> {\n await this.refreshTokenService.deleteByHash(tokenHash);\n }\n\n async deleteAllRefreshTokensForUser(uid: string): Promise<void> {\n await this.refreshTokenService.deleteAllForUser(uid);\n }\n\n async listRefreshTokensForUser(uid: string): Promise<RefreshTokenInfo[]> {\n return this.refreshTokenService.listForUser(uid);\n }\n\n async deleteRefreshTokenById(id: string, uid: string): Promise<void> {\n await this.refreshTokenService.deleteById(id, uid);\n }\n\n // Password reset token operations\n\n async createPasswordResetToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {\n await this.passwordResetTokenService.createToken(uid, tokenHash, expiresAt);\n }\n\n async findValidPasswordResetToken(tokenHash: string): Promise<PasswordResetTokenInfo | null> {\n return this.passwordResetTokenService.findValidByHash(tokenHash);\n }\n\n async markPasswordResetTokenUsed(tokenHash: string): Promise<void> {\n await this.passwordResetTokenService.markAsUsed(tokenHash);\n }\n\n async deleteAllPasswordResetTokensForUser(uid: string): Promise<void> {\n await this.passwordResetTokenService.deleteAllForUser(uid);\n }\n\n async deleteExpiredTokens(): Promise<void> {\n await this.passwordResetTokenService.deleteExpired();\n }\n\n // Magic link token operations\n\n async createMagicLinkToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {\n await this.magicLinkTokenService.createToken(uid, tokenHash, expiresAt);\n }\n\n async findValidMagicLinkToken(tokenHash: string): Promise<MagicLinkTokenInfo | null> {\n return this.magicLinkTokenService.findValidByHash(tokenHash);\n }\n\n async markMagicLinkTokenUsed(tokenHash: string): Promise<void> {\n await this.magicLinkTokenService.markAsUsed(tokenHash);\n }\n}\n\n/**\n * PostgreSQL implementation of AuthRepository.\n * Combines user, role, and token repository operations.\n * This provides a convenient single-class interface for all auth operations.\n */\nexport class PostgresAuthRepository implements AuthRepository {\n private userService: UserService;\n private tokenRepository: PostgresTokenRepository;\n\n constructor(\n private db: NodePgDatabase,\n tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>\n ) {\n this.userService = new UserService(db, tableOrTables);\n this.tokenRepository = new PostgresTokenRepository(db, tableOrTables);\n }\n\n // User operations (delegate to UserService)\n\n async createUser(data: CreateUserData): Promise<UserData> {\n return this.userService.createUser(data);\n }\n\n async getUserById(id: string): Promise<UserData | null> {\n return this.userService.getUserById(id);\n }\n\n async getUserByEmail(email: string): Promise<UserData | null> {\n return this.userService.getUserByEmail(email);\n }\n\n async getUserByIdentity(provider: string, providerId: string): Promise<UserData | null> {\n return this.userService.getUserByIdentity(provider, providerId);\n }\n\n async getUserIdentities(uid: string): Promise<UserIdentityData[]> {\n return this.userService.getUserIdentities(uid);\n }\n\n async linkUserIdentity(uid: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void> {\n return this.userService.linkUserIdentity(uid, provider, providerId, profileData);\n }\n\n async updateUser(id: string, data: Partial<Omit<CreateUserData, \"id\">>): Promise<UserData | null> {\n return this.userService.updateUser(id, data);\n }\n\n async deleteUser(id: string): Promise<void> {\n await this.userService.deleteUser(id);\n }\n\n async listUsers(): Promise<UserData[]> {\n return this.userService.listUsers();\n }\n\n async listUsersPaginated(options?: ListUsersOptions): Promise<PaginatedUsersResult> {\n return this.userService.listUsersPaginated(options);\n }\n\n async updatePassword(id: string, passwordHash: string): Promise<void> {\n await this.userService.updatePassword(id, passwordHash);\n }\n\n async setEmailVerified(id: string, verified: boolean): Promise<void> {\n await this.userService.setEmailVerified(id, verified);\n }\n\n async setVerificationToken(id: string, token: string | null): Promise<void> {\n await this.userService.setVerificationToken(id, token);\n }\n\n async getUserByVerificationToken(token: string): Promise<UserData | null> {\n return this.userService.getUserByVerificationToken(token);\n }\n\n async getUserRoles(uid: string): Promise<RoleData[]> {\n return this.userService.getUserRoles(uid);\n }\n\n async getUserRoleIds(uid: string): Promise<string[]> {\n return this.userService.getUserRoleIds(uid);\n }\n\n async setUserRoles(uid: string, roleIds: string[]): Promise<void> {\n await this.userService.setUserRoles(uid, roleIds);\n }\n\n async assignDefaultRole(uid: string, roleId: string): Promise<void> {\n await this.userService.assignDefaultRole(uid, roleId);\n }\n\n async getUserWithRoles(uid: string): Promise<{ user: UserData; roles: RoleData[] } | null> {\n return this.userService.getUserWithRoles(uid);\n }\n\n // Role operations (roles are inline on users, synthesized from string IDs)\n\n async getRoleById(id: string): Promise<RoleData | null> {\n return {\n id,\n name: id,\n isAdmin: id === \"admin\",\n defaultPermissions: null,\n collectionPermissions: null\n };\n }\n\n async listRoles(): Promise<RoleData[]> {\n return [\n { id: \"admin\",\nname: \"Admin\",\nisAdmin: true,\ndefaultPermissions: null,\ncollectionPermissions: null },\n { id: \"editor\",\nname: \"Editor\",\nisAdmin: false,\ndefaultPermissions: null,\ncollectionPermissions: null },\n { id: \"viewer\",\nname: \"Viewer\",\nisAdmin: false,\ndefaultPermissions: null,\ncollectionPermissions: null }\n ];\n }\n\n async createRole(_data: CreateRoleData): Promise<RoleData> {\n return {\n id: _data.id,\n name: _data.name,\n isAdmin: _data.isAdmin ?? false,\n defaultPermissions: _data.defaultPermissions ?? null,\n collectionPermissions: _data.collectionPermissions ?? null\n };\n }\n\n async updateRole(id: string, data: Partial<Omit<RoleData, \"id\">>): Promise<RoleData | null> {\n return {\n id,\n name: data.name ?? id,\n isAdmin: data.isAdmin ?? (id === \"admin\"),\n defaultPermissions: data.defaultPermissions ?? null,\n collectionPermissions: data.collectionPermissions ?? null\n };\n }\n\n async deleteRole(_id: string): Promise<void> {\n // No-op: roles are inline strings on users\n }\n\n // Token operations (delegate to PostgresTokenRepository)\n\n async createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise<void> {\n await this.tokenRepository.createRefreshToken(uid, tokenHash, expiresAt, userAgent, ipAddress, session);\n }\n\n async markRefreshTokenRotated(tokenHash: string): Promise<void> {\n await this.tokenRepository.markRefreshTokenRotated(tokenHash);\n }\n\n async revokeRefreshTokenSession(sessionId: string): Promise<void> {\n await this.tokenRepository.revokeRefreshTokenSession(sessionId);\n }\n\n async pruneRefreshTokens(uid: string, sessionId: string, supersededBefore: Date): Promise<void> {\n await this.tokenRepository.pruneRefreshTokens(uid, sessionId, supersededBefore);\n }\n\n async getTokensValidAfter(uid: string): Promise<Date | null> {\n return this.tokenRepository.getTokensValidAfter(uid);\n }\n\n async setTokensValidAfter(uid: string, at: Date): Promise<void> {\n await this.tokenRepository.setTokensValidAfter(uid, at);\n }\n\n async findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null> {\n return this.tokenRepository.findRefreshTokenByHash(tokenHash);\n }\n\n async deleteRefreshToken(tokenHash: string): Promise<void> {\n await this.tokenRepository.deleteRefreshToken(tokenHash);\n }\n\n async deleteAllRefreshTokensForUser(uid: string): Promise<void> {\n await this.tokenRepository.deleteAllRefreshTokensForUser(uid);\n }\n\n async listRefreshTokensForUser(uid: string): Promise<RefreshTokenInfo[]> {\n return this.tokenRepository.listRefreshTokensForUser(uid);\n }\n\n async deleteRefreshTokenById(id: string, uid: string): Promise<void> {\n await this.tokenRepository.deleteRefreshTokenById(id, uid);\n }\n\n async createPasswordResetToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {\n await this.tokenRepository.createPasswordResetToken(uid, tokenHash, expiresAt);\n }\n\n async findValidPasswordResetToken(tokenHash: string): Promise<PasswordResetTokenInfo | null> {\n return this.tokenRepository.findValidPasswordResetToken(tokenHash);\n }\n\n async markPasswordResetTokenUsed(tokenHash: string): Promise<void> {\n await this.tokenRepository.markPasswordResetTokenUsed(tokenHash);\n }\n\n async deleteAllPasswordResetTokensForUser(uid: string): Promise<void> {\n await this.tokenRepository.deleteAllPasswordResetTokensForUser(uid);\n }\n\n async deleteExpiredTokens(): Promise<void> {\n await this.tokenRepository.deleteExpiredTokens();\n }\n\n // Magic link token operations\n\n async createMagicLinkToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void> {\n await this.tokenRepository.createMagicLinkToken(uid, tokenHash, expiresAt);\n }\n\n async findValidMagicLinkToken(tokenHash: string): Promise<MagicLinkTokenInfo | null> {\n return this.tokenRepository.findValidMagicLinkToken(tokenHash);\n }\n\n async markMagicLinkTokenUsed(tokenHash: string): Promise<void> {\n await this.tokenRepository.markMagicLinkTokenUsed(tokenHash);\n }\n\n // MFA operations (delegate to MfaService)\n\n private _mfaService: MfaService | null = null;\n private getMfaService(): MfaService {\n if (!this._mfaService) {\n this._mfaService = new MfaService(this.db);\n }\n return this._mfaService;\n }\n\n async createMfaFactor(uid: string, factorType: \"totp\", secretEncrypted: string, friendlyName?: string): Promise<MfaFactor> {\n return this.getMfaService().createMfaFactor(uid, factorType, secretEncrypted, friendlyName);\n }\n\n async getMfaFactors(uid: string): Promise<MfaFactor[]> {\n return this.getMfaService().getMfaFactors(uid);\n }\n\n async getMfaFactorById(factorId: string): Promise<(MfaFactor & { secretEncrypted: string }) | null> {\n return this.getMfaService().getMfaFactorById(factorId);\n }\n\n async verifyMfaFactor(factorId: string): Promise<void> {\n return this.getMfaService().verifyMfaFactor(factorId);\n }\n\n async updateMfaFactorSecret(factorId: string, secretEncrypted: string): Promise<void> {\n return this.getMfaService().updateMfaFactorSecret(factorId, secretEncrypted);\n }\n\n async deleteMfaFactor(factorId: string, uid: string): Promise<void> {\n return this.getMfaService().deleteMfaFactor(factorId, uid);\n }\n\n async createMfaChallenge(factorId: string, ipAddress?: string): Promise<MfaChallengeInfo> {\n return this.getMfaService().createMfaChallenge(factorId, ipAddress);\n }\n\n async getMfaChallengeById(challengeId: string): Promise<MfaChallengeInfo | null> {\n return this.getMfaService().getMfaChallengeById(challengeId);\n }\n\n async verifyMfaChallenge(challengeId: string): Promise<void> {\n return this.getMfaService().verifyMfaChallenge(challengeId);\n }\n\n async createRecoveryCodes(uid: string, codeHashes: string[]): Promise<void> {\n return this.getMfaService().createRecoveryCodes(uid, codeHashes);\n }\n\n async useRecoveryCode(uid: string, codeHash: string): Promise<boolean> {\n return this.getMfaService().useRecoveryCode(uid, codeHash);\n }\n\n async getUnusedRecoveryCodeCount(uid: string): Promise<number> {\n return this.getMfaService().getUnusedRecoveryCodeCount(uid);\n }\n\n async deleteAllRecoveryCodes(uid: string): Promise<void> {\n return this.getMfaService().deleteAllRecoveryCodes(uid);\n }\n\n async hasVerifiedMfaFactors(uid: string): Promise<boolean> {\n return this.getMfaService().hasVerifiedMfaFactors(uid);\n }\n\n async claimMfaFactorCounter(factorId: string, counter: number): Promise<boolean> {\n return this.getMfaService().claimMfaFactorCounter(factorId, counter);\n }\n\n async recordMfaChallengeAttempt(challengeId: string): Promise<number> {\n return this.getMfaService().recordMfaChallengeAttempt(challengeId);\n }\n}\n\n// =============================================================================\n// MFA SERVICE\n// =============================================================================\n\n/**\n * PostgreSQL implementation of MfaRepository.\n * Handles all MFA-related database operations.\n */\nexport class MfaService implements MfaRepository {\n constructor(private db: NodePgDatabase, private schemaName = \"rebase\") {}\n\n private qualify(tableName: string): string {\n return `\"${this.schemaName}\".\"${tableName}\"`;\n }\n\n async createMfaFactor(\n uid: string,\n factorType: \"totp\",\n secretEncrypted: string,\n friendlyName?: string\n ): Promise<MfaFactor> {\n const tableName = this.qualify(\"mfa_factors\");\n const result = await this.db.execute(sql`\n INSERT INTO ${sql.raw(tableName)} (uid, factor_type, secret_encrypted, friendly_name)\n VALUES (${uid}, ${factorType}, ${secretEncrypted}, ${friendlyName ?? null})\n RETURNING id, uid, factor_type, friendly_name, verified, created_at, updated_at\n `);\n\n const row = result.rows[0] as Record<string, unknown>;\n return {\n id: row.id as string,\n uid: row.uid as string,\n factorType: row.factor_type as \"totp\",\n friendlyName: (row.friendly_name as string | null) ?? undefined,\n verified: row.verified as boolean,\n createdAt: new Date(row.created_at as string),\n updatedAt: new Date(row.updated_at as string)\n };\n }\n\n async getMfaFactors(uid: string): Promise<MfaFactor[]> {\n const tableName = this.qualify(\"mfa_factors\");\n const result = await this.db.execute(sql`\n SELECT id, uid, factor_type, friendly_name, verified, created_at, updated_at\n FROM ${sql.raw(tableName)}\n WHERE uid = ${uid}\n ORDER BY created_at\n `);\n\n return (result.rows as Array<Record<string, unknown>>).map(row => ({\n id: row.id as string,\n uid: row.uid as string,\n factorType: row.factor_type as \"totp\",\n friendlyName: (row.friendly_name as string | null) ?? undefined,\n verified: row.verified as boolean,\n createdAt: new Date(row.created_at as string),\n updatedAt: new Date(row.updated_at as string)\n }));\n }\n\n async getMfaFactorById(factorId: string): Promise<(MfaFactor & { secretEncrypted: string }) | null> {\n const tableName = this.qualify(\"mfa_factors\");\n const result = await this.db.execute(sql`\n SELECT id, uid, factor_type, secret_encrypted, friendly_name, verified, last_used_counter, created_at, updated_at\n FROM ${sql.raw(tableName)}\n WHERE id = ${factorId}\n `);\n\n if (result.rows.length === 0) return null;\n\n const row = result.rows[0] as Record<string, unknown>;\n return {\n id: row.id as string,\n uid: row.uid as string,\n factorType: row.factor_type as \"totp\",\n secretEncrypted: row.secret_encrypted as string,\n friendlyName: (row.friendly_name as string | null) ?? undefined,\n verified: row.verified as boolean,\n // BIGINT comes back as a string from node-postgres.\n lastUsedCounter: row.last_used_counter === null || row.last_used_counter === undefined\n ? null\n : Number(row.last_used_counter),\n createdAt: new Date(row.created_at as string),\n updatedAt: new Date(row.updated_at as string)\n };\n }\n\n /**\n * Spend a TOTP time step, once and only once.\n *\n * One statement: the `WHERE` is the check, the `UPDATE` is the act, and\n * `RETURNING` reports which of two concurrent requests carrying the same\n * six digits won. Reading the counter and then writing it would let both\n * pass — the exact replay this closes.\n */\n async claimMfaFactorCounter(factorId: string, counter: number): Promise<boolean> {\n const tableName = this.qualify(\"mfa_factors\");\n const result = await this.db.execute(sql`\n UPDATE ${sql.raw(tableName)}\n SET last_used_counter = ${counter}, updated_at = NOW()\n WHERE id = ${factorId}\n AND (last_used_counter IS NULL OR last_used_counter < ${counter})\n RETURNING id\n `);\n\n return result.rows.length > 0;\n }\n\n async verifyMfaFactor(factorId: string): Promise<void> {\n const tableName = this.qualify(\"mfa_factors\");\n await this.db.execute(sql`\n UPDATE ${sql.raw(tableName)}\n SET verified = TRUE, updated_at = NOW()\n WHERE id = ${factorId}\n `);\n }\n\n async updateMfaFactorSecret(factorId: string, secretEncrypted: string): Promise<void> {\n const tableName = this.qualify(\"mfa_factors\");\n await this.db.execute(sql`\n UPDATE ${sql.raw(tableName)}\n SET secret_encrypted = ${secretEncrypted}, updated_at = NOW()\n WHERE id = ${factorId}\n `);\n }\n\n async deleteMfaFactor(factorId: string, uid: string): Promise<void> {\n const tableName = this.qualify(\"mfa_factors\");\n await this.db.execute(sql`\n DELETE FROM ${sql.raw(tableName)}\n WHERE id = ${factorId} AND uid = ${uid}\n `);\n }\n\n async createMfaChallenge(factorId: string, ipAddress?: string): Promise<MfaChallengeInfo> {\n const tableName = this.qualify(\"mfa_challenges\");\n // Challenges expire in 5 minutes\n const expiresAt = new Date(Date.now() + 5 * 60 * 1000);\n const result = await this.db.execute(sql`\n INSERT INTO ${sql.raw(tableName)} (factor_id, ip_address, expires_at)\n VALUES (${factorId}, ${ipAddress ?? null}, ${expiresAt})\n RETURNING id, factor_id, created_at, verified_at, ip_address\n `);\n\n const row = result.rows[0] as Record<string, unknown>;\n return {\n id: row.id as string,\n factorId: row.factor_id as string,\n createdAt: new Date(row.created_at as string),\n verifiedAt: row.verified_at ? new Date(row.verified_at as string) : undefined,\n ipAddress: (row.ip_address as string | null) ?? undefined\n };\n }\n\n async getMfaChallengeById(challengeId: string): Promise<MfaChallengeInfo | null> {\n const tableName = this.qualify(\"mfa_challenges\");\n const result = await this.db.execute(sql`\n SELECT id, factor_id, created_at, verified_at, ip_address, attempts, expires_at\n FROM ${sql.raw(tableName)}\n WHERE id = ${challengeId} AND expires_at > NOW() AND verified_at IS NULL\n `);\n\n if (result.rows.length === 0) return null;\n\n const row = result.rows[0] as Record<string, unknown>;\n return {\n id: row.id as string,\n factorId: row.factor_id as string,\n createdAt: new Date(row.created_at as string),\n verifiedAt: row.verified_at ? new Date(row.verified_at as string) : undefined,\n ipAddress: (row.ip_address as string | null) ?? undefined,\n attempts: Number(row.attempts ?? 0)\n };\n }\n\n /**\n * Count one failed guess against a challenge and report the new total.\n *\n * Incremented in the database rather than in the route so that guesses\n * arriving in parallel — the shape any real brute-force takes — cannot\n * share a single increment.\n */\n async recordMfaChallengeAttempt(challengeId: string): Promise<number> {\n const tableName = this.qualify(\"mfa_challenges\");\n const result = await this.db.execute(sql`\n UPDATE ${sql.raw(tableName)}\n SET attempts = attempts + 1\n WHERE id = ${challengeId}\n RETURNING attempts\n `);\n\n if (result.rows.length === 0) return 0;\n return Number((result.rows[0] as { attempts: number | string }).attempts);\n }\n\n async verifyMfaChallenge(challengeId: string): Promise<void> {\n const tableName = this.qualify(\"mfa_challenges\");\n await this.db.execute(sql`\n UPDATE ${sql.raw(tableName)}\n SET verified_at = NOW()\n WHERE id = ${challengeId}\n `);\n }\n\n async createRecoveryCodes(uid: string, codeHashes: string[]): Promise<void> {\n const tableName = this.qualify(\"recovery_codes\");\n // Delete existing codes first\n await this.db.execute(sql`\n DELETE FROM ${sql.raw(tableName)} WHERE uid = ${uid}\n `);\n\n // Insert new codes\n for (const hash of codeHashes) {\n await this.db.execute(sql`\n INSERT INTO ${sql.raw(tableName)} (uid, code_hash)\n VALUES (${uid}, ${hash})\n `);\n }\n }\n\n async useRecoveryCode(uid: string, codeHash: string): Promise<boolean> {\n const tableName = this.qualify(\"recovery_codes\");\n const result = await this.db.execute(sql`\n UPDATE ${sql.raw(tableName)}\n SET used_at = NOW()\n WHERE uid = ${uid} AND code_hash = ${codeHash} AND used_at IS NULL\n RETURNING id\n `);\n\n return result.rows.length > 0;\n }\n\n async getUnusedRecoveryCodeCount(uid: string): Promise<number> {\n const tableName = this.qualify(\"recovery_codes\");\n const result = await this.db.execute(sql`\n SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}\n WHERE uid = ${uid} AND used_at IS NULL\n `);\n\n return (result.rows[0] as { count: number }).count;\n }\n\n async deleteAllRecoveryCodes(uid: string): Promise<void> {\n const tableName = this.qualify(\"recovery_codes\");\n await this.db.execute(sql`\n DELETE FROM ${sql.raw(tableName)} WHERE uid = ${uid}\n `);\n }\n\n async hasVerifiedMfaFactors(uid: string): Promise<boolean> {\n const tableName = this.qualify(\"mfa_factors\");\n const result = await this.db.execute(sql`\n SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}\n WHERE uid = ${uid} AND verified = TRUE\n `);\n\n return (result.rows[0] as { count: number }).count > 0;\n }\n}\n\n// =============================================================================\n// PostgreSQL Type Aliases (for consistent naming with other implementations)\n// =============================================================================\n\n/** PostgreSQL user repository implementation */\nexport type PostgresUserRepository = UserService;\n","import { sql } from \"drizzle-orm\";\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { logger } from \"@rebasepro/server\";\nimport type { EntityHistoryEntry } from \"@rebasepro/types\";\n\nexport type {\n RecordHistoryParams,\n FetchHistoryOptions,\n HistoryRetentionConfig\n} from \"@rebasepro/types\";\nimport type { RecordHistoryParams, FetchHistoryOptions, HistoryRetentionConfig } from \"@rebasepro/types\";\n\n/**\n * A Postgres history row is already the wire shape — `updated_at` comes back\n * from the driver as a string. Kept as an alias because the name is used\n * throughout this package and in `PostgresBackendDriver`.\n */\nexport type HistoryEntry = EntityHistoryEntry;\n\nconst DEFAULT_RETENTION: HistoryRetentionConfig = {\n maxEntries: 200,\n ttlDays: 90\n};\n\n/**\n * Service for recording and querying row change history.\n * Stores history entries in the `rebase.entity_history` table.\n */\nexport class HistoryService {\n public retention: HistoryRetentionConfig;\n\n constructor(\n private db: NodePgDatabase,\n retention?: Partial<HistoryRetentionConfig>\n ) {\n this.retention = { ...DEFAULT_RETENTION,\n...retention };\n }\n\n /**\n * Record a history entry for an row change.\n * This is intentionally fire-and-forget safe — errors are logged but never\n * bubble up to block the main save/delete operation.\n *\n * After inserting, kicks off a non-blocking pruning pass for this row.\n */\n async recordHistory(params: RecordHistoryParams): Promise<void> {\n const {\n tableName,\n id,\n action,\n values,\n previousValues,\n updatedBy\n } = params;\n\n const changedFields = previousValues && values\n ? findChangedFields(previousValues, values)\n : null;\n\n\n // Skip recording if this is an update with zero actual changes\n\n if (action === \"update\" && (!changedFields || changedFields.length === 0)) {\n return;\n }\n\n try {\n await this.db.execute(sql`\n INSERT INTO rebase.entity_history \n (table_name, entity_id, action, changed_fields, \"values\", previous_values, updated_by)\n VALUES (\n ${tableName},\n ${String(id)},\n ${action},\n ${changedFields ? sql`ARRAY[${sql.join(changedFields.map(f => sql`${f}`), sql`, `)}]::text[]` : sql`NULL`},\n ${values ? sql`${JSON.stringify(values)}::jsonb` : sql`NULL`},\n ${previousValues ? sql`${JSON.stringify(previousValues)}::jsonb` : sql`NULL`},\n ${updatedBy ?? null}\n )\n `);\n\n // Non-blocking prune for this specific row\n this.pruneEntity(tableName, id).catch(err =>\n logger.error(\"History prune failed\", { error: err })\n );\n } catch (error) {\n logger.error(\"Failed to record row history\", { error: error });\n }\n }\n\n /**\n * Fetch history entries for an row, ordered by most recent first.\n */\n async fetchHistory(\n tableName: string,\n id: string,\n options: FetchHistoryOptions = {}\n ): Promise<{ data: HistoryEntry[]; total: number }> {\n const limit = options.limit ?? 20;\n const offset = options.offset ?? 0;\n\n const [countResult, dataResult] = await Promise.all([\n this.db.execute(sql`\n SELECT COUNT(*) as count\n FROM rebase.entity_history\n WHERE table_name = ${tableName}\n AND entity_id = ${String(id)}\n `),\n this.db.execute(sql`\n SELECT id, table_name, entity_id, action, changed_fields,\n \"values\", previous_values, updated_by, updated_at\n FROM rebase.entity_history\n WHERE table_name = ${tableName}\n AND entity_id = ${String(id)}\n ORDER BY updated_at DESC\n LIMIT ${limit}\n OFFSET ${offset}\n `)\n ]);\n\n const total = parseInt(\n (countResult.rows[0] as Record<string, string>)?.count ?? \"0\",\n 10\n );\n\n return {\n data: dataResult.rows as unknown as HistoryEntry[],\n total\n };\n }\n\n /**\n * Fetch a single history entry by ID.\n */\n async fetchHistoryEntry(historyId: string): Promise<HistoryEntry | null> {\n const result = await this.db.execute(sql`\n SELECT id, table_name, entity_id, action, changed_fields,\n \"values\", previous_values, updated_by, updated_at\n FROM rebase.entity_history\n WHERE id = ${historyId}\n `);\n\n if (result.rows.length === 0) return null;\n return result.rows[0] as unknown as HistoryEntry;\n }\n\n // ───────── Retention / Pruning ─────────\n\n /**\n * Prune history for a single row: enforce maxEntries and TTL.\n */\n async pruneEntity(tableName: string, id: string): Promise<number> {\n let deleted = 0;\n\n // 1. TTL — delete entries older than ttlDays\n const ttlResult = await this.db.execute(sql`\n DELETE FROM rebase.entity_history\n WHERE table_name = ${tableName}\n AND entity_id = ${String(id)}\n AND updated_at < NOW() - MAKE_INTERVAL(days => ${this.retention.ttlDays})\n `);\n deleted += ttlResult.rowCount ?? 0;\n\n // 2. Max entries — keep the newest maxEntries, delete the rest\n const maxResult = await this.db.execute(sql`\n DELETE FROM rebase.entity_history\n WHERE id IN (\n SELECT id FROM rebase.entity_history\n WHERE table_name = ${tableName}\n AND entity_id = ${String(id)}\n ORDER BY updated_at DESC\n OFFSET ${this.retention.maxEntries}\n )\n `);\n deleted += maxResult.rowCount ?? 0;\n\n return deleted;\n }\n\n /**\n * Global prune: enforce TTL across ALL rows in a single sweep.\n * Intended to be called periodically (e.g. once per hour or daily).\n */\n async pruneExpired(): Promise<number> {\n const result = await this.db.execute(sql`\n DELETE FROM rebase.entity_history\n WHERE updated_at < NOW() - MAKE_INTERVAL(days => ${this.retention.ttlDays})\n `);\n return result.rowCount ?? 0;\n }\n}\n\n\n/**\n * Deep equality without JSON.stringify.\n * Handles primitives, arrays, Dates, and plain objects recursively.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a == null || b == null) return false;\n if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n return a.every((v, i) => deepEqual(v, b[i]));\n }\n if (typeof a === \"object\" && typeof b === \"object\") {\n const aObj = a as Record<string, unknown>;\n const bObj = b as Record<string, unknown>;\n const aKeys = Object.keys(aObj);\n const bKeys = Object.keys(bObj);\n if (aKeys.length !== bKeys.length) return false;\n return aKeys.every(k => deepEqual(aObj[k], bObj[k]));\n }\n return false;\n}\n\n/**\n * Shallow comparison to find top-level keys that changed between two objects.\n */\nexport function findChangedFields(\n oldValues: Record<string, unknown>,\n newValues: Record<string, unknown>\n): string[] | null {\n const changed: string[] = [];\n const allKeys = new Set([\n ...Object.keys(oldValues),\n ...Object.keys(newValues)\n ]);\n\n for (const key of allKeys) {\n const oldVal = oldValues[key];\n const newVal = newValues[key];\n\n // Skip internal metadata\n if (key.startsWith(\"__\")) continue;\n\n if (oldVal !== newVal) {\n // For objects/arrays, use structural comparison\n if (\n typeof oldVal === \"object\" && oldVal !== null &&\n typeof newVal === \"object\" && newVal !== null\n ) {\n if (!deepEqual(oldVal, newVal)) {\n changed.push(key);\n }\n } else {\n changed.push(key);\n }\n }\n }\n\n return changed.length > 0 ? changed : null;\n}\n","import { sql } from \"drizzle-orm\";\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { logger } from \"@rebasepro/server\";\nimport { revokeInternalTableSql } from \"@rebasepro/common\";\n\n/**\n * Auto-create the row history table if it doesn't exist.\n * This runs on startup when history is enabled, following the same\n * pattern as `ensureAuthTablesExist`.\n */\nexport async function ensureHistoryTableExists(db: NodePgDatabase): Promise<void> {\n logger.debug(\"🔍 Checking row history table...\");\n\n try {\n // Create the rebase schema (idempotent — may already exist from auth init)\n await db.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);\n\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS rebase.entity_history (\n id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,\n table_name TEXT NOT NULL,\n entity_id TEXT NOT NULL,\n action TEXT NOT NULL,\n changed_fields TEXT[],\n \"values\" JSONB,\n previous_values JSONB,\n updated_by TEXT,\n updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n )\n `);\n\n await db.execute(sql`\n CREATE INDEX IF NOT EXISTS idx_history_entity\n ON rebase.entity_history(table_name, entity_id)\n `);\n\n await db.execute(sql`\n CREATE INDEX IF NOT EXISTS idx_history_time\n ON rebase.entity_history(table_name, entity_id, updated_at DESC)\n `);\n\n // Every previous value of every audited row, in one table with no RLS\n // and no tenant scoping — so a readable copy defeats the row policies on\n // the tables it shadows. The driver's schema-wide grant reaches it\n // (created here, after that grant ran), so take it back.\n await db.execute(sql.raw(revokeInternalTableSql(\"rebase\", \"entity_history\")));\n\n logger.debug(\"✅ Entity history table ready\");\n } catch (error) {\n logger.error(\"❌ Failed to create row history table\", { error: error });\n logger.warn(\"⚠️ Continuing without creating history table.\");\n }\n}\n","import { getTableColumns } from \"drizzle-orm\";\nimport { PgArray, PgTable } from \"drizzle-orm/pg-core\";\nimport { logger } from \"@rebasepro/server\";\n\n/**\n * Patches all PgArray columns on the given tables to handle NULL values safely.\n *\n * Drizzle ORM's `PgArray.mapFromDriverValue` calls `value.map(...)` without\n * guarding against `null`. When a PostgreSQL native array column (`text[]`,\n * `integer[]`, etc.) contains NULL, the pg driver returns `null` in JavaScript,\n * and `null.map(...)` throws `TypeError: value.map is not a function`.\n *\n * This function walks every column of every registered table and, for any\n * `PgArray` column, wraps its `mapFromDriverValue` to return `null` when the\n * database value is nullish.\n *\n * This is a workaround for a known Drizzle ORM issue. Should be removed once\n * Drizzle handles nullable arrays natively.\n */\nexport function patchPgArrayNullSafety(tables: Record<string, unknown>): void {\n let patchedCount = 0;\n\n for (const tableOrRelation of Object.values(tables)) {\n if (!(tableOrRelation instanceof PgTable)) continue;\n\n const columns = getTableColumns(tableOrRelation);\n for (const column of Object.values(columns)) {\n if (column instanceof PgArray) {\n const original = column.mapFromDriverValue.bind(column);\n column.mapFromDriverValue = function (value: unknown) {\n if (value == null) return null;\n return original(value as string | unknown[]);\n };\n patchedCount++;\n }\n }\n }\n\n if (patchedCount > 0) {\n logger.debug(`[PgArray] Patched ${patchedCount} array column(s) for null-safety`);\n }\n}\n","/**\n * Naming helpers shared by the introspection modules. These live apart from\n * `introspect-db-logic.ts` because the inference pass needs them too, and\n * importing them from there would close a cycle back through this module.\n */\n\n/**\n * Convert a snake_case name to a human-readable Title Case label.\n * e.g. \"created_at\" -> \"Created At\", \"customer_id\" -> \"Customer Id\"\n */\nexport function humanize(snakeName: string): string {\n return snakeName\n .replace(/_/g, \" \")\n .replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n","/**\n * The PostgreSQL type → Rebase property type mapping.\n *\n * Split out of `introspect-db-logic` so that the structural analysis can use it\n * without importing the generator, which imports the analysis. Re-exported from\n * `introspect-db-logic` so existing callers keep their import path.\n */\n\n/**\n * Map a PostgreSQL data type to a Rebase property type.\n */\nexport function mapPgType(dataType: string): string {\n const dt = dataType.toLowerCase();\n\n // Interval MUST be checked before numeric (\"interval\" contains \"int\")\n if (dt === \"interval\") return \"string\";\n\n // Array types MUST be checked before numeric (\"_int4\" contains \"int\")\n if (dt === \"array\" || dt.startsWith(\"_\")) return \"array\";\n\n // Numeric types\n if (\n dt.includes(\"int\") || // integer, smallint, bigint\n dt.includes(\"numeric\") ||\n dt.includes(\"decimal\") ||\n dt.includes(\"serial\") || // serial, bigserial\n dt === \"real\" ||\n dt === \"float4\" ||\n dt === \"float8\" ||\n dt === \"double precision\" ||\n dt === \"money\"\n ) {\n return \"number\";\n }\n\n // Boolean\n if (dt.includes(\"bool\")) return \"boolean\";\n\n // Date / Time\n if (dt.includes(\"time\") || dt.includes(\"date\")) return \"date\";\n\n // JSON\n if (dt === \"json\" || dt === \"jsonb\") return \"map\";\n\n // Binary\n if (dt === \"bytea\") return \"binary\";\n\n // Network types\n if (dt === \"inet\" || dt === \"cidr\" || dt === \"macaddr\" || dt === \"macaddr8\") return \"string\";\n\n // UUID\n if (dt === \"uuid\") return \"string\";\n\n // Text/varchar/char — default to string\n return \"string\";\n}\n","/**\n * Introspection logic — pure functions and the pipeline that transforms\n * raw PostgreSQL metadata into Rebase collection definition files.\n *\n * This module contains NO side-effects: no fs writes, no pg.Client creation,\n * no process.exit. It is imported by introspect-db.ts (the CLI entry-point)\n * and consumed directly by tests.\n */\nimport { firstFreeKey, toWireKey } from \"@rebasepro/utils\";\nimport { inferPropertyFromData } from \"./introspect-db-inference\";\nimport { humanize } from \"./introspect-db-naming\";\nimport { mapPgType } from \"./introspect-db-types\";\nimport type { CheckFactsByTable } from \"./introspect-db-constraints\";\nimport type { TableClassification } from \"./introspect-db-structure\";\nimport {\n buildColumnFacts,\n deriveKanbanProperty,\n deriveListProperties,\n deriveSort,\n deriveTitleProperty,\n isDerivedIndexColumn,\n isReadOnlyColumn\n} from \"./introspect-db-structure\";\n\n// ── Typed interfaces for SQL query results ────────────────────────────\n\nexport interface TableRow {\n table_name: string;\n /** True for the parent of a partitioned table (`relkind = 'p'`). */\n is_partitioned?: boolean;\n}\n\nexport interface TableColumn {\n table_name: string;\n column_name: string;\n data_type: string;\n udt_name: string;\n is_nullable: string;\n column_default: string | null;\n atttypmod: number | null;\n /** 1-based position in the table, as declared. */\n ordinal_position?: number;\n /** `\"ALWAYS\"` for a generated column, `\"NEVER\"` otherwise. */\n is_generated?: string;\n /** `\"YES\"` for an identity column. */\n is_identity?: string;\n /** `\"ALWAYS\"` or `\"BY DEFAULT\"` on an identity column. */\n identity_generation?: string | null;\n /** The declared `varchar(n)` / `char(n)` bound, if any. */\n character_maximum_length?: number | null;\n numeric_precision?: number | null;\n numeric_scale?: number | null;\n}\n\nexport interface EnumValue {\n enum_name: string;\n enum_value: string;\n sort_order: number;\n}\n\nexport interface PrimaryKeyRow {\n table_name: string;\n column_name: string;\n}\n\nexport interface ForeignKeyRow {\n table_name: string;\n column_name: string;\n foreign_table_name: string;\n foreign_column_name: string;\n /** Name of the FK constraint — the only way to tell composite keys apart. */\n constraint_name?: string;\n /** 1-based position of this column within its constraint. */\n ordinal?: number;\n /** `\"CASCADE\"`, `\"RESTRICT\"`, `\"SET NULL\"`, `\"SET DEFAULT\"`, `\"NO ACTION\"`. */\n delete_rule?: string;\n}\n\n/** A unique constraint or unique index, as an ordered column list. */\nexport interface UniqueConstraintRow {\n table_name: string;\n constraint_name: string;\n column_names: string[];\n}\n\n/** A CHECK constraint, as `pg_get_constraintdef` renders it. */\nexport interface CheckConstraintRow {\n table_name: string;\n constraint_name: string;\n definition: string;\n}\n\n/** A `COMMENT ON TABLE` (null `column_name`) or `COMMENT ON COLUMN`. */\nexport interface CommentRow {\n table_name: string;\n column_name: string | null;\n comment: string;\n}\n\n/**\n * Everything one introspection run reads from the database.\n *\n * Passed around as one value so a new signal means a new field here rather than\n * a new parameter on every function between the query and the generator — the\n * shape `generateCollectionFile` had grown to seven positional arguments by.\n */\nexport interface SchemaMetadata {\n schema: string;\n tables: TableRow[];\n columns: TableColumn[];\n enumValues: EnumValue[];\n pks: PrimaryKeyRow[];\n fks: ForeignKeyRow[];\n uniques: UniqueConstraintRow[];\n checks: CheckConstraintRow[];\n comments: CommentRow[];\n /**\n * Row counts for the tables that needed one, capped — see `countRowsUpTo`.\n * Absent for every table introspection never had a reason to count.\n */\n rowCounts: Record<string, number>;\n}\n\nexport interface TableMeta {\n name: string;\n columns: TableColumn[];\n pks: string[];\n fks: ForeignKeyRow[];\n}\n\n// ── Irregular plurals that naive rules can't handle ───────────────────\n\nconst IRREGULAR_SINGULARS: Record<string, string> = {\n people: \"person\",\n children: \"child\",\n men: \"man\",\n women: \"woman\",\n mice: \"mouse\",\n geese: \"goose\",\n teeth: \"tooth\",\n feet: \"foot\",\n data: \"datum\",\n media: \"medium\",\n criteria: \"criterion\",\n phenomena: \"phenomenon\"\n};\n\n/**\n * Plurals in \"-ves\" whose singular really ends in f/fe, and which of the two.\n *\n * A blanket \"-ves\" -> \"-f\" rule gets `knives` -> `knif`, and mangles every\n * ordinary \"-ive\" noun that happens to be plural along with it: `archives` ->\n * `archif`, `objectives` -> `objectif`. The set of English words that genuinely\n * swap f/fe for ves is small and closed, so it is listed rather than guessed —\n * anything else ending in \"ves\" drops the trailing 's' like any other plural.\n *\n * Matched on the whole word, not on the suffix: `olives` ends in `lives`.\n */\nconst VES_SINGULAR_ENDINGS: Record<string, \"f\" | \"fe\"> = {\n calves: \"f\", dwarves: \"f\", elves: \"f\", halves: \"f\", hooves: \"f\",\n leaves: \"f\", loaves: \"f\", scarves: \"f\", selves: \"f\", sheaves: \"f\",\n shelves: \"f\", thieves: \"f\", wharves: \"f\", wolves: \"f\",\n knives: \"fe\", lives: \"fe\", wives: \"fe\"\n};\n\n/** Words ending in 's' that are already singular. */\nconst UNCOUNTABLE = new Set([\n \"status\", \"campus\", \"virus\", \"bus\", \"plus\", \"census\",\n \"diagnosis\", \"analysis\", \"basis\", \"crisis\", \"thesis\",\n \"synopsis\", \"parenthesis\", \"hypothesis\", \"emphasis\",\n \"news\", \"series\", \"species\", \"means\", \"athletics\",\n \"economics\", \"electronics\", \"mathematics\", \"physics\",\n \"politics\", \"statistics\"\n]);\n\nexport function singularize(word: string): string {\n const lower = word.toLowerCase();\n\n // Check irregular forms\n if (IRREGULAR_SINGULARS[lower]) {\n // Preserve the original casing of the first character\n const singular = IRREGULAR_SINGULARS[lower];\n return word[0] === word[0].toUpperCase()\n ? singular.charAt(0).toUpperCase() + singular.slice(1)\n : singular;\n }\n\n // Check uncountable\n if (UNCOUNTABLE.has(lower)) return word;\n\n // Latin/Greek -es endings (diagnosis -> diagnosis is uncountable, but \"addresses\" -> \"address\")\n if (lower.endsWith(\"ices\") && lower.length > 5) {\n // e.g. \"indices\" -> \"index\", \"vertices\" -> \"vertex\"\n return word.slice(0, -4) + \"ex\";\n }\n if (lower.endsWith(\"ies\") && lower.length > 3) {\n return word.slice(0, -3) + \"y\";\n }\n if (VES_SINGULAR_ENDINGS[lower]) {\n // e.g. \"wolves\" -> \"wolf\", \"knives\" -> \"knife\"\n return word.slice(0, -3) + VES_SINGULAR_ENDINGS[lower];\n }\n if (lower.endsWith(\"ches\") || lower.endsWith(\"shes\") || lower.endsWith(\"sses\") || lower.endsWith(\"xes\") || lower.endsWith(\"zes\")) {\n return word.slice(0, -2);\n }\n if (lower.endsWith(\"ses\") && !lower.endsWith(\"sses\")) {\n // e.g. \"responses\" -> \"response\", \"databases\" -> \"database\"\n return word.slice(0, -1);\n }\n if (lower.endsWith(\"s\") && !lower.endsWith(\"ss\") && !lower.endsWith(\"us\") && !lower.endsWith(\"is\")) {\n return word.slice(0, -1);\n }\n\n return word;\n}\n\n/**\n * Convert a snake_case table name to a camelCase + \"Collection\" variable name.\n * e.g. \"company_token\" -> \"companyTokenCollection\"\n */\nexport function toCollectionVarName(tableName: string): string {\n const camel = tableName.replace(/_([a-z])/g, (_g, letter: string) => letter.toUpperCase()) + \"Collection\";\n // Only reshapes names that are not identifiers already, so every table that\n // generated a working file keeps the exact variable name it had. A table\n // called `2024 archive` used to emit `const 2024 archiveCollection`, which\n // is three syntax errors rather than a declaration.\n if (JS_IDENTIFIER.test(camel)) return camel;\n const sanitized = camel.replace(/[^A-Za-z0-9_$]/g, \"_\");\n return /^[0-9]/.test(sanitized) ? `_${sanitized}` : sanitized;\n}\n\nexport function getIconForTable(tableName: string): string {\n const table = tableName.toLowerCase();\n if (table.includes(\"user\") || table.includes(\"account\") || table.includes(\"member\") || table.includes(\"customer\") || table.includes(\"client\") || table.includes(\"patient\")) return \"Users\";\n if (table.includes(\"post\") || table.includes(\"article\") || table.includes(\"blog\") || table.includes(\"page\")) return \"FileText\";\n if (table.includes(\"product\") || table.includes(\"item\")) return \"Package\";\n if (table.includes(\"order\") || table.includes(\"cart\") || table.includes(\"purchase\") || table.includes(\"invoice\")) return \"ShoppingCart\";\n if (table.includes(\"setting\") || table.includes(\"config\")) return \"Settings\";\n if (table.includes(\"tag\") || table.includes(\"categor\")) return \"Tag\";\n if (table.includes(\"image\") || table.includes(\"photo\") || table.includes(\"media\") || table.includes(\"asset\")) return \"Image\";\n if (table.includes(\"notification\") || table.includes(\"message\") || table.includes(\"email\")) return \"Mail\";\n if (table.includes(\"log\") || table.includes(\"audit\") || table.includes(\"event\")) return \"Activity\";\n if (table.includes(\"subscription\") || table.includes(\"plan\") || table.includes(\"billing\")) return \"CreditCard\";\n if (table.includes(\"comment\") || table.includes(\"review\") || table.includes(\"feedback\")) return \"MessageCircle\";\n return \"Database\";\n}\n\nexport { mapPgType };\n\n// ── Build the enum map from query results ─────────────────────────────\n\nexport function buildEnumMap(enumValues: EnumValue[]): Map<string, string[]> {\n const enumMap = new Map<string, string[]>();\n for (const ev of enumValues) {\n const existing = enumMap.get(ev.enum_name);\n if (existing) {\n existing.push(ev.enum_value);\n } else {\n enumMap.set(ev.enum_name, [ev.enum_value]);\n }\n }\n return enumMap;\n}\n\n// ── Build the tables map from raw query results ───────────────────────\n\nexport function buildTablesMap(\n tables: TableRow[],\n columns: TableColumn[],\n pks: PrimaryKeyRow[],\n fks: ForeignKeyRow[]\n): Map<string, TableMeta> {\n const tablesMap = new Map<string, TableMeta>();\n for (const t of tables) {\n tablesMap.set(t.table_name, {\n name: t.table_name,\n columns: columns.filter((c) => c.table_name === t.table_name),\n pks: pks.filter((pk) => pk.table_name === t.table_name).map((pk) => pk.column_name),\n fks: fks.filter((fk) => fk.table_name === t.table_name)\n });\n }\n return tablesMap;\n}\n\n// ── Identify join tables ──────────────────────────────────────────────\n\n/**\n * Join tables, identified by column name.\n *\n * Superseded for the CLI by `classifyTables` in `./introspect-db-structure`,\n * which asks the database instead: two single-column keys, unique together, no\n * payload column, nothing referencing the table. This rule folds away\n * `northwind.order_details` — which has the key shape and carries unit price,\n * quantity and discount — because it recognises `id`, `created_at` and\n * `updated_at` by name and calls everything else a foreign key.\n *\n * Still used by `./introspect-runtime`, which builds collections in memory from\n * a narrower set of catalog queries and has no unique-constraint or row-count\n * data to reason with.\n */\nexport function identifyJoinTables(tablesMap: Map<string, TableMeta>): Set<string> {\n const joinTables = new Set<string>();\n for (const [tableName, meta] of tablesMap.entries()) {\n if (meta.fks.length === 2) {\n const isLikelyJoinTable = meta.columns.every((c) =>\n meta.fks.some((fk) => fk.column_name === c.column_name) ||\n c.column_name === \"id\" ||\n c.column_name === \"created_at\" ||\n c.column_name === \"updated_at\"\n );\n\n if (isLikelyJoinTable) {\n joinTables.add(tableName);\n }\n }\n }\n return joinTables;\n}\n\n// ── Property ordering heuristics ──────────────────────────────────────\n\n/**\n * Property metadata used to compute display priority.\n * Keeps computePropertyPriority free of any TableMeta coupling.\n */\nexport interface PropertyOrderingContext {\n /** The resolved Rebase property type (e.g. \"string\", \"number\", \"date\", \"relation\"). */\n propType: string;\n /** Whether this column is a primary key. */\n isPk: boolean;\n /** Whether this column is an enum (USER-DEFINED with matching values). */\n isEnum: boolean;\n /** Whether this is a storage/file-upload field (detected from column name). */\n isStorage: boolean;\n /** The PostgreSQL data_type (e.g. \"text\", \"character varying\", \"jsonb\"). */\n pgDataType: string;\n /** The original column index in PostgreSQL (for stable tiebreaking). */\n originalIndex: number;\n}\n\n// — Tier 0: Identity (0–9) ————————————————————————————————————————————\nconst IDENTITY_EXACT: Record<string, number> = {\n id: 0,\n uuid: 1,\n _id: 2\n};\n\n// — Tier 1: Title / Name — the \"display column\" (10–19) ———————————————\nconst TITLE_EXACT: Record<string, number> = {\n name: 10,\n title: 11,\n label: 12,\n display_name: 13,\n displayname: 13,\n headline: 14,\n subject: 15,\n heading: 16\n};\n\n// — Tier 2: Human identity fields (20–29) —————————————————————————————\nconst HUMAN_IDENTITY_EXACT: Record<string, number> = {\n first_name: 20,\n firstname: 20,\n last_name: 21,\n lastname: 21,\n full_name: 22,\n fullname: 22,\n given_name: 22,\n family_name: 23,\n middle_name: 24,\n username: 25,\n user_name: 25,\n email: 26,\n email_address: 26,\n phone: 27,\n phone_number: 27,\n mobile: 27\n};\n\n// — Tier 3: Core descriptors (30–39) ——————————————————————————————————\nconst DESCRIPTOR_EXACT: Record<string, number> = {\n slug: 30,\n code: 31,\n sku: 32,\n reference: 33,\n ref: 33,\n type: 34,\n kind: 34,\n status: 35,\n state: 35,\n role: 36,\n category: 37,\n group: 38,\n priority: 39,\n order: 39,\n sort_order: 39,\n position: 39\n};\n\n// — Tier 12: System timestamps (120–129) ——————————————————————————————\nconst SYSTEM_TIMESTAMP_EXACT: Record<string, number> = {\n created_at: 120,\n createdat: 120,\n creation_date: 120,\n inserted_at: 121,\n updated_at: 122,\n updatedat: 122,\n modified_at: 122,\n last_modified: 122,\n deleted_at: 123,\n deletedat: 123,\n archived_at: 124\n};\n\n// — Pattern-based rules for partial matches ———————————————————————————\nconst TITLE_PATTERNS = [\"name\", \"title\", \"label\"];\nconst LONG_TEXT_NAMES = new Set([\"description\", \"summary\", \"excerpt\", \"abstract\", \"overview\", \"bio\", \"biography\", \"about\"]);\nconst RICH_CONTENT_NAMES = new Set([\"content\", \"body\", \"html\", \"markup\", \"text\", \"article_body\", \"post_body\"]);\nconst MEDIA_PATTERNS = [\"image\", \"avatar\", \"photo\", \"logo\", \"cover\", \"thumbnail\", \"banner\", \"icon\", \"picture\", \"poster\"];\nconst JSON_MAP_NAMES = new Set([\"metadata\", \"meta\", \"config\", \"configuration\", \"settings\", \"options\", \"preferences\", \"data\", \"payload\", \"attributes\", \"extra\", \"additional_info\"]);\n\n/**\n * Compute a numeric priority score for a property.\n * Lower scores appear first in the generated `propertiesOrder` array.\n *\n * The system uses 14 tiers (0–139), with the original column index\n * added as a fractional tiebreaker (originalIndex / 10000) to\n * guarantee stable ordering within the same tier.\n *\n * Pure function — no side effects.\n */\nexport function computePropertyPriority(\n columnName: string,\n ctx: PropertyOrderingContext\n): number {\n // Normalize camelCase/PascalCase to snake_case, then lowercase\n const col = columnName.replace(/([a-z0-9])([A-Z])/g, \"$1_$2\").toLowerCase();\n const tiebreaker = ctx.originalIndex / 10000;\n\n // ── Tier 0: Primary key identity fields\n if (ctx.isPk) {\n const exactScore = IDENTITY_EXACT[col];\n return (exactScore ?? 5) + tiebreaker;\n }\n\n // ── Tier 12: System timestamps (check early to prevent false matches)\n const systemTs = SYSTEM_TIMESTAMP_EXACT[col];\n if (systemTs !== undefined) {\n return systemTs + tiebreaker;\n }\n\n // ── Tier 1: Title / Name exact matches\n const titleExact = TITLE_EXACT[col];\n if (titleExact !== undefined) {\n return titleExact + tiebreaker;\n }\n\n // ── Tier 2: Human identity exact matches\n const humanExact = HUMAN_IDENTITY_EXACT[col];\n if (humanExact !== undefined) {\n return humanExact + tiebreaker;\n }\n\n // ── Tier 3: Core descriptor exact matches\n const descriptorExact = DESCRIPTOR_EXACT[col];\n if (descriptorExact !== undefined) {\n return descriptorExact + tiebreaker;\n }\n\n // ── Tier 1b: Title-like partial matches (e.g. \"product_name\", \"page_title\")\n // Score 17–19 so they rank after exact matches but still in tier 1.\n for (const pattern of TITLE_PATTERNS) {\n if (col.includes(pattern) && col !== pattern) {\n return 17 + tiebreaker;\n }\n }\n\n // ── Tier 9: Media / file upload fields (check before general strings)\n if (ctx.isStorage) {\n return 90 + tiebreaker;\n }\n for (const pattern of MEDIA_PATTERNS) {\n if (col.includes(pattern)) {\n return 91 + tiebreaker;\n }\n }\n if (col.endsWith(\"_url\") || col.endsWith(\"_uri\") || col.endsWith(\"_link\")) {\n return 92 + tiebreaker;\n }\n\n // ── Tier 7: Long text fields\n if (LONG_TEXT_NAMES.has(col)) {\n return 70 + tiebreaker;\n }\n\n // ── Tier 8: Rich content fields\n if (RICH_CONTENT_NAMES.has(col)) {\n return 80 + tiebreaker;\n }\n\n // ── Tier 10: JSON / Map types\n if (ctx.propType === \"map\") {\n return JSON_MAP_NAMES.has(col) ? 100 + tiebreaker : 105 + tiebreaker;\n }\n\n // ── Tier 11: Array types\n if (ctx.propType === \"array\") {\n return 110 + tiebreaker;\n }\n\n // ── Tier 6: Owning relations\n if (ctx.propType === \"relation\") {\n return 60 + tiebreaker;\n }\n\n // ── Tier 4: Short text, enums, booleans — \"quick glance\" fields\n if (ctx.isEnum) {\n return 40 + tiebreaker;\n }\n if (ctx.propType === \"boolean\") {\n return 45 + tiebreaker;\n }\n if (ctx.propType === \"string\" && ctx.pgDataType !== \"text\") {\n // Short string (varchar, char, uuid that's not a PK)\n return 42 + tiebreaker;\n }\n\n // ── Tier 5: Numbers & user-facing dates\n if (ctx.propType === \"number\") {\n return 50 + tiebreaker;\n }\n if (ctx.propType === \"date\") {\n // A date that isn't a system timestamp (already handled above)\n return 55 + tiebreaker;\n }\n\n // ── Tier 7b: text data_type that didn't match long-text names\n if (ctx.propType === \"string\" && ctx.pgDataType === \"text\") {\n return 75 + tiebreaker;\n }\n\n // ── Tier 13: Fallback / unknown\n return 130 + tiebreaker;\n}\n\n/**\n * Sort a `propertiesOrder` array using the priority heuristic.\n * Returns a new sorted array; does not mutate the input.\n *\n * @param entries - Array of { key, columnName, propType, ... } objects\n * carrying the information needed to compute priority.\n */\nexport interface PropertyOrderEntry {\n /** The property key in the generated collection (may differ from columnName for relations). */\n key: string;\n /** The ordering context for this property. */\n ctx: PropertyOrderingContext;\n}\n\nexport function sortPropertiesOrder(entries: PropertyOrderEntry[]): string[] {\n return [...entries]\n .sort((a, b) => computePropertyPriority(a.key, a.ctx) - computePropertyPriority(b.key, b.ctx))\n .map((e) => e.key);\n}\n\n// ── Generate collection file content ──────────────────────────────────\n\nexport interface GeneratedFile {\n tableName: string;\n fileName: string;\n content: string;\n}\n\n/**\n * The structural analysis a run can hand the generator.\n *\n * Optional in full, and the generator degrades to exactly its previous output\n * without it. That is not politeness towards old callers: three existing test\n * suites and the `rebase init` scaffold path build a `TableMeta` by hand and\n * have no database to read constraints or row counts from, and they must keep\n * producing a valid collection.\n */\n/**\n * Which `defineCollection` — if any — the project being generated into can import.\n *\n * A bare `const x: PostgresCollectionConfig = { … }` annotation widens `properties`\n * to `Record<string, …>`, and every key-shaped field in the admin block —\n * `titleProperty`, `sort`, `propertiesOrder`, `listProperties`, `fixedFilter` — is\n * derived from those keys. Annotated, they accept any string: introspection was\n * emitting a `propertiesOrder` array that nothing checked, so renaming a column and\n * re-introspecting left a stale key that compiled silently. `defineCollection` is\n * the identity function whose `const P` type parameter keeps the keys literal, which\n * is what turns that checking on.\n *\n * There are two of them and they are not interchangeable:\n *\n * - `admin-types` — `@rebasepro/cms-types`. Its index side-effect-imports\n * `augment.ts`, so importing it is also what *declares* the `admin` block. Only a\n * project that depends on the package can resolve it.\n * - `common` — `@rebasepro/common`. Same key inference, no admin surface, no React\n * anywhere in its graph (`scripts/headless-guard` lists it as core). This is the\n * headless flavour.\n * - `annotation` — neither package is declared, so neither import would resolve and\n * the old annotation is the only honest thing to emit. Projects scaffolded before\n * `@rebasepro/common` joined the headless config package land here.\n *\n * The last two emit **no admin block, on the collection or on any property**. That is\n * not a downgrade: `@rebasepro/types` declares no `admin` field at all, so the block\n * introspection used to emit was a type error in every headless project it was\n * written into. See `packages/cms-types/src/augment.ts`.\n */\nexport type CollectionBuilder = \"admin-types\" | \"common\" | \"annotation\";\n\n/**\n * The package specifiers the generated files name, spelled once.\n *\n * Written as constants rather than inline in the import templates below because\n * `scripts/headless-guard/check-types.mjs` scans core sources for `from\n * \"@rebasepro/cms-types\"` and cannot tell a real import from one this module\n * *writes*. It is right to be that blunt — the guard's whole value is that it\n * cannot be reasoned around — so the string simply never appears in that shape\n * here. Inlining them back into the templates re-breaks `check:types-headless`.\n */\nexport const ADMIN_TYPES_PACKAGE = \"@rebasepro/cms-types\";\nexport const COMMON_PACKAGE = \"@rebasepro/common\";\nexport const TYPES_PACKAGE = \"@rebasepro/types\";\n\nexport interface GenerationContext {\n metadata?: SchemaMetadata;\n classifications?: Map<string, TableClassification>;\n checkFacts?: CheckFactsByTable;\n /**\n * Defaults to `admin-types`, which is what the generator has always emitted.\n * The CLI never relies on the default — `introspect-db.ts` detects the flavour\n * from the target project and passes it. See `detectCollectionBuilder`.\n */\n builder?: CollectionBuilder;\n}\n\n/** Adds entries to a property's `validation` block, creating it if absent. */\nfunction withValidation(extra: string, entries: string[]): string {\n if (entries.length === 0) return extra;\n const block = entries.map((e) => ` ${e}`).join(\",\\n\");\n if (extra.includes(\"validation: {\")) {\n return extra.replace(\"validation: {\", `validation: {\\n${block},`);\n }\n return `${extra}\\n validation: {\\n${block}\\n },`;\n}\n\n/**\n * Whether a property's generated text already sets `key:` as an object key.\n *\n * A bare `extra.includes(\"min:\")` looks like it answers this and does not:\n * `admin:` ends in `min:`, so every property with an admin block claimed to\n * have a minimum already and silently lost the one the database declared. The\n * leading-delimiter requirement is the whole point — a key is preceded by a\n * newline, a brace or a comma, never by another identifier character.\n */\nfunction hasGeneratedKey(extra: string, key: string): boolean {\n return new RegExp(`(^|[\\\\s{,])${key}\\\\s*:`).test(extra);\n}\n\n/**\n * Adds entries to a property's `admin` block, creating it if absent.\n *\n * `emitAdmin` is false for the headless flavours, where `BaseProperty` has no\n * `admin` field to put them in — see {@link CollectionBuilder}. The options are\n * dropped rather than relocated: every one of them (`readOnly`, `multiline`,\n * `hideFromCollection`, `urlPreview`) describes a form widget, and there is no\n * form.\n */\nfunction withAdminOptions(extra: string, entries: string[], emitAdmin = true): string {\n if (!emitAdmin) return extra;\n if (entries.length === 0) return extra;\n const block = entries.map((e) => ` ${e}`).join(\",\\n\");\n if (extra.includes(\"admin: {\")) {\n return extra.replace(\"admin: {\", `admin: {\\n${block},`);\n }\n return `${extra}\\n admin: {\\n${block}\\n },`;\n}\n\n/** A TypeScript string literal, escaped. */\nfunction quote(value: string): string {\n return JSON.stringify(value);\n}\n\nconst JS_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n\n/**\n * An object key for the generated file: verbatim when the name is a JavaScript\n * identifier, quoted otherwise.\n *\n * Postgres constrains an identifier only by quoting, so `order`, `full name`\n * and `2fa_enabled` are all ordinary column names — and all three produced a\n * file that did not parse when written as a bare key.\n *\n * Still needed now that keys are camel-cased rather than copied from the\n * column: `toWireKey` splits on separators and joins, which fixes `full name`\n * but cannot fix a name that is not an identifier for some other reason —\n * `2fa_enabled` becomes `2faEnabled`, still leading with a digit, and `order`\n * was never a separator problem at all.\n */\nfunction propKey(name: string): string {\n return JS_IDENTIFIER.test(name) ? name : quote(name);\n}\n\n/**\n * Text safe to put after `//`.\n *\n * A table comment or a classification reason is free text out of the database.\n * A newline in one ended the comment and let the rest of the value continue as\n * code.\n */\nfunction commentText(value: string): string {\n return value.replace(/\\s*[\\r\\n]+\\s*/g, \" \");\n}\n\n/** A property-key array, one key per line, indented for the admin block. */\nfunction formatKeyList(keys: string[]): string {\n if (keys.length === 0) return \"[]\";\n return `[\\n${keys.map((k) => ` ${quote(k)}`).join(\",\\n\")}\\n ]`;\n}\n\n/**\n * Generate the full TypeScript file content for a single collection.\n * Pure function — no I/O.\n */\nexport function generateCollectionFile(\n tableName: string,\n meta: TableMeta,\n allFks: ForeignKeyRow[],\n joinTables: Set<string>,\n tablesMap: Map<string, TableMeta>,\n enumMap: Map<string, string[]>,\n sampleData?: Record<string, unknown>[],\n context: GenerationContext = {}\n): string {\n const collectionName = humanize(tableName);\n const singular = singularize(collectionName);\n const icon = getIconForTable(tableName);\n\n const classification = context.classifications?.get(tableName);\n const checkFacts: CheckFactsByTable = context.checkFacts ?? new Map();\n const tableChecks = checkFacts.get(tableName);\n const columnComments = new Map<string, string>();\n let tableComment: string | undefined;\n for (const comment of context.metadata?.comments ?? []) {\n if (comment.table_name !== tableName) continue;\n if (comment.column_name === null) tableComment = comment.comment;\n else columnComments.set(comment.column_name, comment.comment);\n }\n const singleColumnUniques = new Set(\n (context.metadata?.uniques ?? [])\n .filter((u) => u.table_name === tableName && u.column_names.length === 1)\n .map((u) => u.column_names[0])\n );\n\n const builder: CollectionBuilder = context.builder ?? \"admin-types\";\n /** Whether the target project has an `admin` field to write into at all. */\n const emitAdmin = builder === \"admin-types\";\n\n const BUILDER_IMPORT: Record<CollectionBuilder, string> = {\n \"admin-types\": `import { defineCollection } from ${quote(ADMIN_TYPES_PACKAGE)};`,\n common: `import { defineCollection } from ${quote(COMMON_PACKAGE)};`,\n annotation: `import { PostgresCollectionConfig } from ${quote(TYPES_PACKAGE)};`\n };\n const imports = new Set<string>([BUILDER_IMPORT[builder]]);\n\n /**\n * Imports the collection a relation points at — unless it is this one.\n *\n * A self-referencing key (`employees.reports_to -> employees`, which both\n * northwind and chinook have) otherwise made the file import its own default\n * export under the name it declares three lines later: `TS2440: Import\n * declaration conflicts with local declaration`. The relation target is a\n * thunk, so referring to the local const directly is fine — it is only\n * dereferenced after the module has finished evaluating.\n */\n const importCollection = (otherTable: string): string => {\n const varName = toCollectionVarName(otherTable);\n if (otherTable !== tableName) imports.add(`import ${varName} from ${quote(`./${otherTable}`)};`);\n return varName;\n };\n\n /**\n * A relation's `target` thunk, with its return type spelled out.\n *\n * The annotation is what makes `defineCollection` survive a relational schema.\n * Without an explicit type on the const, the collection's type is *inferred*,\n * and a relation cycle — `posts` belongs to `authors`, `authors` has many\n * `posts`; or `employees.reports_to -> employees`, which northwind, chinook and\n * musicbrainz all have — makes that inference circular: `TS7022: implicitly has\n * type 'any' because it is referenced directly or indirectly in its own\n * initializer`, plus `TS7023` on the thunk and `TS2303` on the import alias.\n * Naming the return type lets the checker type the thunk without resolving the\n * collection it points at, which breaks the cycle. Nothing else is given up:\n * the inference that matters runs over `properties`, not over `relations`.\n *\n * The annotated flavour has no cycle to break — its const is already typed — so\n * it keeps the plainer thunk it has always emitted.\n */\n const relationTarget = (targetVarName: string): string => {\n if (builder === \"annotation\") return `() => ${targetVarName}`;\n imports.add(`import type { AnyCollectionConfig } from ${quote(TYPES_PACKAGE)};`);\n return `(): AnyCollectionConfig => ${targetVarName}`;\n };\n\n let propsOutput = \"\";\n let relationsOutput = \"\";\n const orderEntries: PropertyOrderEntry[] = [];\n const propertyBlocks = new Map<string, string>();\n /**\n * Column → the property key it was generated under.\n *\n * Needed because the structural helpers below (`deriveTitleProperty`,\n * `deriveKanbanProperty`, `deriveSort`) answer in *columns* — they read\n * `pg_attribute` — while `display.title`, `kanban.columnProperty` and\n * `sort` name **properties**. The two used to be the same string, so\n * nothing carried the translation; now `full_name` is generated as\n * `fullName` and a title pointing at `full_name` points at nothing.\n */\n const keyByColumn = new Map<string, string>();\n /** Properties the list view will not render, so `listProperties` skips them. */\n const hiddenFromCollection = new Set<string>();\n let columnIndex = 0;\n\n // Detect composite primary keys\n const isCompositePk = meta.pks.length > 1;\n\n // Map columns\n for (const col of meta.columns) {\n // Skip foreign keys since we handle them as relations\n // Exception: Do not skip if it's part of the primary key!\n if (meta.fks.some((fk) => fk.column_name === col.column_name) && !meta.pks.includes(col.column_name)) continue;\n\n const currentIndex = columnIndex++;\n\n // The key this column is generated under — its *wire* name, which is\n // not its column name. `columnName` below carries the column, so the\n // two never have to agree and the API stops carrying `user_id` next to\n // `displayName`.\n //\n // Camel-casing makes collisions possible where none existed: `user_id`\n // and `userId` are two columns and one key, which is a duplicate key in\n // an object literal — a TypeScript error that stops the whole generated\n // collection compiling. Resolved the same way the foreign-key loop\n // below resolves its own: first free candidate, then a numbered tail,\n // so no column is ever dropped. The raw column name is the second\n // candidate, so the loser of a collision still gets a name that means\n // something. Deterministic for a given database: the columns arrive in\n // ordinal order, so the same schema always yields the same keys.\n const propertyKey = firstFreeKey([toWireKey(col.column_name), col.column_name], propertyBlocks);\n keyByColumn.set(col.column_name, propertyKey);\n\n // Check if this column uses a PostgreSQL enum type\n const colEnumValues = enumMap.get(col.udt_name);\n const isEnumColumn = col.data_type === \"USER-DEFINED\" && colEnumValues !== undefined;\n const isVectorColumn = col.udt_name === \"vector\";\n\n const propType = isEnumColumn ? \"string\" : (isVectorColumn ? \"vector\" : mapPgType(col.data_type));\n let extra = \"\";\n\n const colNameLower = col.column_name.toLowerCase();\n\n // ── Data Inference Engine ────────────────────────────────────────────\n let finalPropType = propType;\n let inferenceExtra = \"\";\n\n if (!isEnumColumn && sampleData && sampleData.length > 0) {\n const values = sampleData.map(r => r[col.column_name]);\n const inferred = inferPropertyFromData(col.column_name, col.data_type, propType, values, meta.pks.includes(col.column_name), emitAdmin);\n if (inferred.propType) finalPropType = inferred.propType;\n if (inferred.extra) inferenceExtra = inferred.extra;\n }\n\n const columnChecks = tableChecks?.get(col.column_name);\n\n // Enum values — generate real enum from the PG enum\n if (isEnumColumn && colEnumValues) {\n const enumEntries = colEnumValues\n .map((v) => `{ id: ${quote(v)}, label: ${quote(humanize(v))} }`)\n .join(\", \");\n extra += `\\n enum: [${enumEntries}],`;\n } else if (columnChecks?.enumValues && !inferenceExtra.includes(\"enum:\") && propType === \"string\") {\n // `CHECK (col IN (…))` is the other way a schema declares a closed\n // set. It is the same statement as a Postgres enum type, made by an\n // author who did not want a type — and until now the form offered a\n // free-text box for it and let the database reject the write.\n const enumEntries = columnChecks.enumValues\n .map((v) => `{ id: ${quote(v)}, label: ${quote(humanize(v))} }`)\n .join(\", \");\n extra += `\\n enum: [${enumEntries}],`;\n }\n\n // Date auto-value heuristics\n if (finalPropType === \"date\") {\n if (colNameLower === \"created_at\" || colNameLower === \"createdat\") {\n extra += \"\\n autoValue: \\\"on_create\\\",\";\n extra = withAdminOptions(extra, [\"readOnly: true\", \"hideFromCollection: true\"], emitAdmin);\n hiddenFromCollection.add(propertyKey);\n } else if (colNameLower === \"updated_at\" || colNameLower === \"updatedat\") {\n extra += \"\\n autoValue: \\\"on_update\\\",\";\n extra = withAdminOptions(extra, [\"readOnly: true\", \"hideFromCollection: true\"], emitAdmin);\n hiddenFromCollection.add(propertyKey);\n } else if (col.column_default && (col.column_default.includes(\"now()\") || col.column_default.includes(\"CURRENT_TIMESTAMP\"))) {\n extra += \"\\n autoValue: \\\"on_create\\\",\";\n extra = withAdminOptions(extra, [\"readOnly: true\"], emitAdmin);\n }\n }\n\n // Array/Map heuristics (Fallback if not inferred)\n if (finalPropType === \"array\" && !inferenceExtra.includes(\"of: {\")) {\n let innerType = \"string\";\n let colType = \"\";\n if (col.udt_name.startsWith(\"_\")) {\n const baseType = col.udt_name.substring(1);\n innerType = mapPgType(baseType);\n if (innerType === \"string\") colType = \"text[]\";\n else if (innerType === \"number\") colType = col.udt_name === \"_numeric\" ? \"numeric[]\" : \"integer[]\";\n else if (innerType === \"boolean\") colType = \"boolean[]\";\n }\n if (colType) {\n extra += `\\n columnType: ${quote(colType)},`;\n }\n extra += `\\n of: { name: ${quote(`${humanize(col.column_name)} Item`)}, type: ${quote(innerType)} },`;\n } else if (finalPropType === \"map\" && !inferenceExtra.includes(\"keyValue: true\") && !inferenceExtra.includes(\"properties: {\")) {\n extra += \"\\n keyValue: true,\";\n }\n\n // String sub-type heuristics (Fallback if not handled by inference or enum)\n if (finalPropType === \"string\" && !isEnumColumn && !inferenceExtra) {\n const isUrl = colNameLower.endsWith(\"_url\") || colNameLower.endsWith(\"_uri\") || colNameLower.endsWith(\"_link\");\n const isMedia = colNameLower.includes(\"image\") || colNameLower.includes(\"avatar\") || colNameLower.includes(\"photo\") || colNameLower.includes(\"logo\") || colNameLower.includes(\"cover\");\n\n if (isMedia) {\n extra += `\\n storage: {\\n storagePath: ${quote(`${tableName}/${col.column_name}`)}\\n },`;\n } else if (isUrl) {\n extra += \"\\n url: true,\";\n } else if (colNameLower === \"description\" || colNameLower === \"summary\" || colNameLower === \"excerpt\") {\n extra = withAdminOptions(extra, [\"multiline: true\"], emitAdmin);\n } else if (colNameLower === \"content\" || colNameLower === \"body\") {\n // Inside `admin`, because that is where both options live. At the\n // top of the property — where these were — the generated file\n // does not compile: `StringProperty` declares neither. Six of\n // OpenStreetMap's tables have a `body` column, which is how this\n // surfaced.\n extra = withAdminOptions(extra, [\"multiline: true\", \"markdown: true\"], emitAdmin);\n } else if (col.data_type === \"text\") {\n extra = withAdminOptions(extra, [\"multiline: true\"], emitAdmin);\n }\n }\n\n // Append inference results\n if (inferenceExtra) {\n extra += inferenceExtra;\n if (!extra.endsWith(\",\")) extra += \",\";\n }\n\n // ── Rules the database already enforces ──────────────────────────────\n // Everything below is read from the catalog, not guessed from the data\n // or the column's name. Each one is a constraint a write would hit\n // anyway; surfacing it means the form says no before the database does.\n const declaredValidation: string[] = [];\n\n // `varchar(n)` — a bound the author wrote down and nothing has read.\n if (finalPropType === \"string\" &&\n typeof col.character_maximum_length === \"number\" &&\n col.character_maximum_length > 0 &&\n !hasGeneratedKey(extra, \"max\")) {\n declaredValidation.push(`max: ${col.character_maximum_length}`);\n }\n\n if (columnChecks) {\n if (finalPropType === \"number\") {\n if (columnChecks.min !== undefined && !hasGeneratedKey(extra, \"min\")) declaredValidation.push(`min: ${columnChecks.min}`);\n if (columnChecks.max !== undefined && !hasGeneratedKey(extra, \"max\")) declaredValidation.push(`max: ${columnChecks.max}`);\n if (columnChecks.moreThan !== undefined) declaredValidation.push(`moreThan: ${columnChecks.moreThan}`);\n if (columnChecks.lessThan !== undefined) declaredValidation.push(`lessThan: ${columnChecks.lessThan}`);\n }\n if (finalPropType === \"string\") {\n if (columnChecks.lengthMin !== undefined && !hasGeneratedKey(extra, \"min\")) declaredValidation.push(`min: ${columnChecks.lengthMin}`);\n if (columnChecks.lengthMax !== undefined && !declaredValidation.some((v) => v.startsWith(\"max:\")) && !hasGeneratedKey(extra, \"max\")) {\n declaredValidation.push(`max: ${columnChecks.lengthMax}`);\n }\n }\n }\n\n // A single-column unique index is the same promise `validation.unique`\n // makes. Composite uniqueness is not: it constrains the combination, and\n // marking either column unique on its own would reject valid rows.\n if (singleColumnUniques.has(col.column_name) && !meta.pks.includes(col.column_name)) {\n declaredValidation.push(\"unique: true\");\n }\n\n extra = withValidation(extra, declaredValidation);\n\n // A generated column rejects every write, and a tsvector holds lexeme\n // positions rather than text, so an editable field for either is a field\n // that can only ever produce an error.\n if (isReadOnlyColumn(col) && !hasGeneratedKey(extra, \"readOnly\")) {\n const options = [\"readOnly: true\"];\n if (isDerivedIndexColumn(col) && !hasGeneratedKey(extra, \"hideFromCollection\")) {\n options.push(\"hideFromCollection: true\");\n hiddenFromCollection.add(propertyKey);\n }\n extra = withAdminOptions(extra, options, emitAdmin);\n }\n\n // `COMMENT ON COLUMN` — documentation the author already wrote, which\n // introspection has never carried across.\n const columnComment = columnComments.get(col.column_name);\n if (columnComment) {\n extra = `\\n description: ${quote(columnComment)},${extra}`;\n }\n\n // Identify IDs (unless already inferred as UUID/CUID by inferenceEngine)\n if (meta.pks.includes(col.column_name)) {\n if (isCompositePk) {\n extra += `\\n // Part of composite primary key (${commentText(meta.pks.join(\", \"))})`;\n } else if (finalPropType === \"number\" && !inferenceExtra.includes(\"isId:\")) {\n extra += \"\\n isId: \\\"increment\\\",\";\n } else if (col.data_type.toLowerCase() === \"uuid\" && !inferenceExtra.includes(\"isId:\")) {\n extra += \"\\n isId: \\\"uuid\\\",\";\n } else if (!inferenceExtra.includes(\"isId:\")) {\n extra += \"\\n isId: \\\"uuid\\\", // Verify if this is a UUID or CUID\";\n }\n }\n\n if (finalPropType === \"vector\") {\n const dims = col.atttypmod && col.atttypmod > 0 ? col.atttypmod : 1536;\n extra += `\\n dimensions: ${dims},`;\n }\n\n // `required` on a column the user cannot write is a form that cannot be\n // submitted: pagila's `film.fulltext` is NOT NULL and maintained by a\n // trigger, so demanding it of the user blocks every create.\n if (col.is_nullable === \"NO\" && !meta.pks.includes(col.column_name) && !col.column_default && !isReadOnlyColumn(col)) {\n if (extra.includes(\"validation: {\")) {\n extra = extra.replace(\"validation: {\", \"validation: {\\n required: true,\");\n } else {\n extra += \"\\n validation: {\\n required: true\\n },\";\n }\n }\n\n const humanName = humanize(col.column_name);\n\n orderEntries.push({\n key: propertyKey,\n ctx: {\n propType: finalPropType,\n isPk: meta.pks.includes(col.column_name),\n isEnum: isEnumColumn,\n isStorage: extra.includes(\"storage: {\") || inferenceExtra.includes(\"storage: {\"),\n pgDataType: col.data_type,\n originalIndex: currentIndex\n }\n });\n\n propertyBlocks.set(propertyKey, `\n ${propKey(propertyKey)}: {\n name: ${quote(humanName)},\n columnName: ${quote(col.column_name)},\n type: ${quote(finalPropType)},${extra}\n },`);\n }\n\n // Map Owning Relations (from this table's FKs to other tables)\n for (const fk of meta.fks) {\n const targetTableName = fk.foreign_table_name;\n if (!joinTables.has(targetTableName)) {\n // The relation gets its own property key, and it must not be one this\n // file has already used — a duplicate key in an object literal is a\n // TypeScript error, so the whole collection stops compiling.\n //\n // The collision needs three things at once and is invisible without\n // all three: a foreign key column that does *not* end in `_id`, that\n // column also being part of the primary key (which is what keeps it\n // as a property of its own rather than folding it into the relation),\n // and the stripped name matching the target table. MusicBrainz names\n // every foreign key after the table it points at — `area_tag (area,\n // tag)` — so 67 of its 339 collections came out with a property\n // declared twice.\n //\n // Camel-cased for the same reason the columns above are: this is a\n // property key, it sits in the same object literal, and a\n // `blog_author` beside a `publishedAt` is the two-conventions\n // defect reproduced inside a single collection.\n const stripped = toWireKey(fk.column_name.replace(/_id$/, \"\"));\n const relName = firstFreeKey(\n [\n stripped,\n toWireKey(targetTableName),\n `${stripped}Relation`\n ],\n propertyBlocks\n );\n // Push the relation property key, not the FK column name\n orderEntries.push({\n key: relName,\n ctx: {\n propType: \"relation\",\n isPk: false,\n isEnum: false,\n isStorage: false,\n pgDataType: \"\",\n originalIndex: columnIndex++\n }\n });\n\n const targetCollectionCamel = importCollection(targetTableName);\n\n const relHumanName = humanize(relName);\n\n propertyBlocks.set(relName, `\n ${propKey(relName)}: {\n name: ${quote(relHumanName)},\n type: \"relation\",\n // mapped from foreign key: ${commentText(fk.column_name)} -> ${commentText(targetTableName)}(${commentText(fk.foreign_column_name)})\n relation: {\n kind: \"belongsTo\",\n target: ${relationTarget(targetCollectionCamel)},\n localKey: ${quote(fk.column_name)}\n }\n },`);\n }\n }\n\n // Map Inverse Relations (1-to-many where OTHER table points to THIS table)\n // These go into the `relations` array so they render as subcollection tabs.\n const inverseFks = allFks.filter((fk) => fk.foreign_table_name === tableName && !joinTables.has(fk.table_name));\n for (const fk of inverseFks) {\n const sourceTableName = fk.table_name;\n\n const targetCollectionCamel = importCollection(sourceTableName);\n\n relationsOutput += `\n {\n kind: \"hasMany\",\n relationName: ${quote(sourceTableName)},\n target: ${relationTarget(targetCollectionCamel)},\n // the ${commentText(sourceTableName)}.${commentText(fk.column_name)} FK points back here\n foreignKeyOnTarget: ${quote(fk.column_name)}\n },`;\n }\n\n // Map Many-to-Many Relations (Join Tables)\n // These also go into the `relations` array so they render as subcollection tabs.\n const relatedJoinTables = Array.from(joinTables).filter((jt) => {\n const jtMeta = tablesMap.get(jt);\n return jtMeta ? jtMeta.fks.some((fk) => fk.foreign_table_name === tableName) : false;\n });\n\n for (const jt of relatedJoinTables) {\n const jtMeta = tablesMap.get(jt);\n if (!jtMeta) continue;\n\n const joinFks = jtMeta.fks;\n\n // Handle self-referencing M2M: both FKs point to the same table\n const selfRefFks = joinFks.filter((fk) => fk.foreign_table_name === tableName);\n if (selfRefFks.length === 2) {\n // Self-referencing M2M — generate a single owning relation\n const thisFk = selfRefFks[0];\n const otherFk = selfRefFks[1];\n\n const relPropName = `${tableName}_via_${otherFk.column_name.replace(/_id$/, \"\")}`;\n\n relationsOutput += `\n {\n kind: \"manyToMany\",\n relationName: ${quote(relPropName)},\n target: ${relationTarget(toCollectionVarName(tableName))},\n through: {\n table: ${quote(jt)},\n sourceColumn: ${quote(thisFk.column_name)},\n targetColumn: ${quote(otherFk.column_name)}\n }\n },`;\n continue;\n }\n\n const otherFk = joinFks.find((fk) => fk.foreign_table_name !== tableName);\n\n if (otherFk) {\n const targetTableName = otherFk.foreign_table_name;\n\n const targetCollectionCamel = importCollection(targetTableName);\n\n // Both sides of a many-to-many are `manyToMany`. There is no owning\n // and inverse side to pick between any more, so this no longer\n // guesses one from table-name ordering and no longer emits a\n // half-configured relation on the losing side with a comment asking\n // the reader to finish it by hand. Introspection already knows both\n // junction columns; each side just names them from its own end.\n const thisFk = joinFks.find((fk) => fk.foreign_table_name === tableName);\n\n const throughCode = thisFk\n ? `\\n through: {\\n table: ${quote(jt)},\\n sourceColumn: ${quote(thisFk.column_name)},\\n targetColumn: ${quote(otherFk.column_name)}\\n }`\n : \"\";\n\n relationsOutput += `\n {\n kind: \"manyToMany\",\n relationName: ${quote(targetTableName)},\n target: ${relationTarget(targetCollectionCamel)},${throughCode}\n },`;\n }\n }\n\n const relationsBlock = relationsOutput\n ? `\\n relations: [${relationsOutput}\\n ],`\n : \"\";\n\n const sortedPropertiesOrder = sortPropertiesOrder(orderEntries);\n for (const key of sortedPropertiesOrder) {\n propsOutput += propertyBlocks.get(key) || \"\";\n }\n\n // ── The admin block ──────────────────────────────────────────────────\n // `icon` and `propertiesOrder` used to be emitted at the *top level* of the\n // config, where they have not belonged since the admin block was split out:\n // `PostgresCollectionConfig` does not declare them, so every generated file\n // was a type error, and the panel — which reads the block — never saw them.\n const adminEntries: string[] = [`icon: ${quote(icon)}`];\n\n if (classification) {\n const derivedFacts = context.metadata\n ? buildColumnFacts(meta, context.metadata, enumMap, checkFacts)\n : undefined;\n\n if (classification.role === \"owned-child\") {\n // The rows are already reachable: every inbound foreign key renders\n // as a tab on the parent. A second, top-level entry for them is what\n // turns a navigation of eight nouns into a list of thirty tables.\n adminEntries.push(\"hideFromNavigation: true\");\n } else if (classification.role === \"lookup\") {\n adminEntries.push('group: \"Reference\"');\n }\n\n if (derivedFacts) {\n // Each of these comes back as a column and is emitted as a property.\n const asProperty = (column: string): string => keyByColumn.get(column) ?? toWireKey(column);\n\n const titleProperty = deriveTitleProperty(derivedFacts);\n if (titleProperty) adminEntries.push(`display: { title: ${quote(asProperty(titleProperty))} }`);\n\n const kanbanProperty = deriveKanbanProperty(derivedFacts);\n if (kanbanProperty) adminEntries.push(`kanban: {\\n columnProperty: ${quote(asProperty(kanbanProperty))}\\n }`);\n\n const sort = deriveSort(derivedFacts);\n if (sort) adminEntries.push(`sort: [${quote(asProperty(sort[0]))}, \"desc\"]`);\n }\n\n const listProperties = deriveListProperties(sortedPropertiesOrder, hiddenFromCollection);\n if (listProperties) {\n adminEntries.push(`listProperties: ${formatKeyList(listProperties)}`);\n }\n }\n\n adminEntries.push(`propertiesOrder: ${formatKeyList(sortedPropertiesOrder)}`);\n\n // Every entry above names a property key, and with `defineCollection` those\n // keys are now checked against `properties` — a stale `propertiesOrder` entry\n // left behind by a renamed column is a compile error rather than a silent\n // no-op. Which is also why the block cannot be emitted where the field is not\n // declared: see {@link CollectionBuilder}.\n const adminBlock = emitAdmin\n ? `\\n admin: {\\n ${adminEntries.join(\",\\n \")}\\n }`\n : \"\";\n\n const descriptionBlock = tableComment\n ? `\\n description: ${quote(tableComment)},`\n : \"\";\n\n // The classification is stated in the file because it is a *decision*, and a\n // decision the reader may disagree with. Naming the evidence tells them\n // which line to delete when they do.\n const classificationNote = classification && classification.role !== \"entity\"\n ? `\\n// Introspected as a ${commentText(classification.role)}: ${commentText(classification.reason)}.\\n`\n : \"\";\n\n const collectionVarName = toCollectionVarName(tableName);\n // `const x = defineCollection({ … })` is also the shape the ts-morph schema\n // editor in `@rebasepro/server` expects — `COLLECTION_FACTORIES` — so an\n // introspected collection is now editable from the panel the way a scaffolded\n // one is.\n const [open, close] = builder === \"annotation\"\n ? [`const ${collectionVarName}: PostgresCollectionConfig = {`, \"};\"]\n : [`const ${collectionVarName} = defineCollection({`, \"});\"];\n // Package imports first, then siblings. `AnyCollectionConfig` is added the\n // moment the first relation needs it — which is after the sibling collections\n // it points at have already been added — and only when a relation needs it, so\n // a project with `noUnusedLocals` never sees an import it does not use.\n const importLines = Array.from(imports);\n const orderedImports = [\n ...importLines.filter((line) => !line.includes('from \"./')),\n ...importLines.filter((line) => line.includes('from \"./'))\n ];\n const fileContent = `${orderedImports.join(\"\\n\")}\n${classificationNote}\n${open}\n name: ${quote(collectionName)},\n singularName: ${quote(singular)},\n slug: ${quote(tableName)},\n table: ${quote(tableName)},${descriptionBlock}\n properties: {${propsOutput}\n },${relationsBlock}${adminBlock}\n${close}\n\nexport default ${collectionVarName};\n`;\n\n return fileContent;\n}\n\n/**\n * Generate the content for an index.ts file that re-exports all collections.\n */\nexport function generateIndexContent(fileNames: string[]): string {\n const sorted = [...fileNames].sort();\n let imports = \"\";\n let arrayElements = \"\";\n for (const f of sorted) {\n const varName = toCollectionVarName(f);\n imports += `import ${varName} from ${quote(`./${f}`)};\\n`;\n arrayElements += ` ${varName},\\n`;\n }\n return `${imports}\\nexport const collections = [\\n${arrayElements}];\\n`;\n}\n\n/**\n * Merge new exports into existing index.ts content.\n * Returns the merged content string.\n */\nexport function mergeIndexContent(existingContent: string, newFileNames: string[]): string {\n const existingImports = new Set(\n [...existingContent.matchAll(/import\\s+([a-zA-Z0-9_]+)\\s+from\\s+\"\\.\\/([^\"]+)\"/g)].map((m) => m[2])\n );\n const sorted = [...newFileNames].sort();\n\n let newImports = \"\";\n let newElements = \"\";\n\n for (const f of sorted) {\n if (!existingImports.has(f)) {\n const varName = toCollectionVarName(f);\n newImports += `import ${varName} from ${quote(`./${f}`)};\\n`;\n newElements += ` ${varName},\\n`;\n }\n }\n\n if (!newImports) return existingContent;\n\n // Simple injection logic:\n // Add new imports below the last import or at the top\n const importRegex = /import\\s+.*?;/g;\n let lastImportMatch;\n let match;\n while ((match = importRegex.exec(existingContent)) !== null) {\n lastImportMatch = match;\n }\n\n let contentWithImports = existingContent;\n if (lastImportMatch) {\n const pos = lastImportMatch.index + lastImportMatch[0].length;\n contentWithImports = existingContent.slice(0, pos) + \"\\n\" + newImports.trimEnd() + existingContent.slice(pos);\n } else {\n contentWithImports = newImports + \"\\n\" + existingContent;\n }\n\n // Inject into the `collections = [...]` array\n const arrayRegex = /export\\s+const\\s+collections\\s*=\\s*\\[([\\s\\S]*?)\\];/;\n return contentWithImports.replace(arrayRegex, (fullMatch, arrayContent) => {\n let mergedArray = arrayContent.trimEnd();\n if (mergedArray && !mergedArray.endsWith(\",\")) mergedArray += \",\";\n if (mergedArray) mergedArray += \"\\n\";\n mergedArray += newElements.trimEnd();\n return `export const collections = [\\n ${mergedArray.trim()}\\n];`;\n });\n}\n\n/**\n * Safely extract the host portion of a database URL for logging.\n */\nexport function safeHostFromUrl(url: string): string {\n return url.includes(\"@\") ? url.split(\"@\")[1] : \"(local connection)\";\n}\n","/**\n * Runtime introspection — builds collections in memory from the live database.\n *\n * This is what makes BaaS mode work with zero configuration: instead of loading\n * collection files from disk, the server reads `information_schema` at boot and\n * derives a collection per table, so any database is served over REST without a\n * single config file.\n *\n * Distinct from `introspect-db.ts`, which runs the same queries but emits\n * TypeScript *source* for a developer to edit and commit (declared collections). The two\n * share the mapping helpers in `introspect-db-logic.ts` so a table is described\n * the same way whether it was generated or introspected.\n */\nimport type { PostgresCollectionConfig } from \"@rebasepro/types\";\n\nimport {\n TableRow,\n TableColumn,\n EnumValue,\n PrimaryKeyRow,\n ForeignKeyRow,\n TableMeta,\n buildTablesMap,\n buildEnumMap,\n identifyJoinTables,\n singularize,\n mapPgType,\n getIconForTable\n} from \"./introspect-db-logic\";\nimport { humanize } from \"./introspect-db-naming\";\nimport { firstFreeKey, toWireKey } from \"@rebasepro/utils\";\n\nexport interface IntrospectedSchema {\n tablesMap: Map<string, TableMeta>;\n enumMap: Map<string, string[]>;\n joinTables: Set<string>;\n}\n\n/** Whether a table carries an authorization model of its own. */\nexport interface TableRlsStatus {\n table: string;\n /** ALTER TABLE … ENABLE ROW LEVEL SECURITY has been run. */\n rlsEnabled: boolean;\n /** Policies attached to it. RLS enabled with none = nothing is visible. */\n policyCount: number;\n}\n\n/**\n * Read the RLS posture of each table in a schema.\n *\n * This is what decides whether baas mode may serve a table. A table with RLS\n * disabled has no authorization model: since every authenticated request runs\n * as `rebase_user`, and that role is granted DML on the schema, serving such a\n * table hands every row to every logged-in user.\n */\nexport async function readRlsStatus(client: Queryable, pgSchema: string): Promise<Map<string, TableRlsStatus>> {\n const { rows } = await client.query<{ table: string; rls_enabled: boolean; policy_count: string | number }>(\n `SELECT c.relname AS table,\n c.relrowsecurity AS rls_enabled,\n (SELECT count(*) FROM pg_policy p WHERE p.polrelid = c.oid) AS policy_count\n FROM pg_class c\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE n.nspname = $1 AND c.relkind = 'r'`,\n [pgSchema]\n );\n\n return new Map(\n rows.map((r) => [\n r.table,\n { table: r.table, rlsEnabled: r.rls_enabled === true, policyCount: Number(r.policy_count ?? 0) }\n ])\n );\n}\n\n/** Minimal query surface — satisfied by pg.Client and pg.Pool alike. */\nexport interface Queryable {\n query<R>(text: string, values?: unknown[]): Promise<{ rows: R[] }>;\n}\n\n/**\n * Read tables, columns, enums, primary keys and foreign keys for a schema.\n * Mirrors the queries in introspect-db.ts.\n */\nexport async function introspectSchema(client: Queryable, pgSchema: string): Promise<IntrospectedSchema> {\n const { rows: tables } = await client.query<TableRow>(\n `SELECT table_name\n FROM information_schema.tables\n WHERE table_schema = $1 AND table_type = 'BASE TABLE'\n AND table_name NOT LIKE 'drizzle_%'\n AND table_name NOT LIKE 'rebase_%'\n ORDER BY table_name`,\n [pgSchema]\n );\n\n const { rows: columns } = await client.query<TableColumn>(\n `SELECT\n c.table_name,\n c.column_name,\n c.data_type,\n c.udt_name,\n c.is_nullable,\n c.column_default,\n (SELECT a.atttypmod FROM pg_attribute a\n JOIN pg_class pc ON a.attrelid = pc.oid\n WHERE pc.relname = c.table_name\n AND a.attname = c.column_name\n AND pc.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = c.table_schema)) as atttypmod\n FROM information_schema.columns c\n WHERE c.table_schema = $1`,\n [pgSchema]\n );\n\n const { rows: enumValues } = await client.query<EnumValue>(\n `SELECT t.typname AS enum_name,\n e.enumlabel AS enum_value,\n e.enumsortorder AS sort_order\n FROM pg_type t\n JOIN pg_enum e ON t.oid = e.enumtypid\n JOIN pg_namespace n ON t.typnamespace = n.oid\n WHERE n.nspname = $1\n ORDER BY t.typname, e.enumsortorder`,\n [pgSchema]\n );\n\n const { rows: pks } = await client.query<PrimaryKeyRow>(\n `SELECT t.relname as table_name, a.attname as column_name\n FROM pg_index i\n JOIN pg_attribute a ON a.attrelid = i.indrelid\n AND a.attnum = ANY(i.indkey)\n JOIN pg_class t ON t.oid = i.indrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n WHERE i.indisprimary AND n.nspname = $1`,\n [pgSchema]\n );\n\n const { rows: fks } = await client.query<ForeignKeyRow>(\n `SELECT\n tc.table_name,\n kcu.column_name,\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\n AND tc.table_schema = kcu.table_schema\n JOIN information_schema.constraint_column_usage AS ccu\n ON ccu.constraint_name = tc.constraint_name\n AND ccu.table_schema = tc.table_schema\n WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = $1`,\n [pgSchema]\n );\n\n const tablesMap = buildTablesMap(tables, columns, pks, fks);\n return {\n tablesMap,\n enumMap: buildEnumMap(enumValues),\n joinTables: identifyJoinTables(tablesMap)\n };\n}\n\n/** Derive the `isId` flavour for a primary-key column. */\nfunction idKindFor(col: TableColumn, propType: string): \"uuid\" | \"increment\" | true {\n if (col.data_type.toLowerCase() === \"uuid\") return \"uuid\";\n if (propType === \"number\") return \"increment\";\n return true;\n}\n\nfunction buildProperties(\n meta: TableMeta,\n enumMap: Map<string, string[]>\n): Record<string, Record<string, unknown>> {\n const properties: Record<string, Record<string, unknown>> = {};\n const takenKeys = new Set<string>();\n\n for (const col of meta.columns) {\n const isPk = meta.pks.includes(col.column_name);\n // Foreign keys surface as relations below, unless they are also part of\n // the primary key, in which case the column itself must stay.\n const isFk = meta.fks.some((fk) => fk.column_name === col.column_name);\n if (isFk && !isPk) continue;\n\n const enumValues = enumMap.get(col.udt_name);\n const isEnum = col.data_type === \"USER-DEFINED\" && enumValues !== undefined;\n const isVector = col.udt_name === \"vector\";\n const propType = isEnum ? \"string\" : isVector ? \"vector\" : mapPgType(col.data_type);\n\n const property: Record<string, unknown> = {\n name: humanize(col.column_name),\n columnName: col.column_name,\n type: propType\n };\n\n // The wire name, with `columnName` above carrying the column. The two\n // are different names for different things — this used to serve the\n // column, so a runtime-introspected collection answered `user_id` while\n // every authored one beside it answered `displayName`.\n //\n // `user_id` and `userId` as two real columns camel-case to one key, so\n // the first free candidate wins and the second falls back to its own\n // column name, then to a numbered tail. Never dropped, and stable for a\n // given database: columns arrive in ordinal order.\n const key = firstFreeKey([toWireKey(col.column_name), col.column_name], takenKeys);\n takenKeys.add(key);\n\n if (isPk) {\n property.isId = idKindFor(col, propType);\n } else if (col.is_nullable === \"NO\" && col.column_default === null) {\n property.validation = { required: true };\n }\n\n if (isEnum && enumValues) {\n property.enum = enumValues.map((value) => ({ id: value, label: humanize(value) }));\n }\n\n properties[key] = property;\n }\n\n return properties;\n}\n\n/**\n * Owning relations, derived from this table's foreign keys — the same shape\n * `generateCollectionFile` writes into a collection file: a `relation` property\n * whose nested descriptor carries `kind`, a `target` thunk and the `localKey`.\n *\n * The shape is load-bearing, not cosmetic. `resolveCollectionRelations` reads\n * relations from `property.relation` and `resolveRelation` requires `target` to\n * be a thunk; this used to emit `target`/`cardinality`/`localKey` flat on the\n * property with the slug as a bare string, which satisfies neither. Nothing\n * threw — the resolver simply skipped every such property and reported that the\n * collection had no relations. So an introspected BaaS collection had its FK\n * columns removed from `properties` (they \"surface as relations\") and then no\n * resolvable relation to surface as, which is why writing the FK column\n * directly came back as `has no field 'product_id'`: `assertKnownWriteFields`\n * learns that column from the resolved relation's `localKey`.\n *\n * The thunk closes over the collections being built in this same pass rather\n * than importing a module, which is what a runtime introspection has instead of\n * generated files. It is called lazily, after the map is fully populated, so a\n * table may reference one introspected later.\n */\nfunction buildRelations(\n meta: TableMeta,\n slugByTable: Map<string, string>,\n collectionBySlug: Map<string, PostgresCollectionConfig>\n): Record<string, Record<string, unknown>> {\n const relations: Record<string, Record<string, unknown>> = {};\n\n for (const fk of meta.fks) {\n const targetSlug = slugByTable.get(fk.foreign_table_name);\n if (!targetSlug) continue;\n\n // Strip the conventional _id suffix: author_id -> author\n let key = toWireKey(fk.column_name.replace(/_id$/, \"\"));\n if (meta.pks.includes(fk.column_name) && key === fk.column_name) {\n // The FK is also the PK and its name doesn't imply a relation (e.g.\n // \"id\"), so naming the relation after the column would collide with\n // the primary-key property.\n key = fk.foreign_table_name;\n }\n\n relations[key] = {\n name: humanize(key),\n type: \"relation\",\n relation: {\n kind: \"belongsTo\",\n target: () => collectionBySlug.get(targetSlug),\n localKey: fk.column_name\n }\n };\n }\n\n return relations;\n}\n\n/**\n * Turn an introspected schema into collections.\n *\n * Join tables are skipped: they carry no identity of their own and exist to\n * express a many-to-many edge between two other tables.\n */\nexport function buildCollectionsFromSchema(\n { tablesMap, enumMap, joinTables }: IntrospectedSchema,\n pgSchema: string\n): PostgresCollectionConfig[] {\n const slugByTable = new Map<string, string>();\n for (const tableName of tablesMap.keys()) {\n if (!joinTables.has(tableName)) slugByTable.set(tableName, tableName);\n }\n\n const collections: PostgresCollectionConfig[] = [];\n // Filled as we go; the relation thunks read it lazily, so a table may point\n // at one that has not been built yet at the moment its relation is created.\n const collectionBySlug = new Map<string, PostgresCollectionConfig>();\n\n for (const [tableName, meta] of tablesMap) {\n if (joinTables.has(tableName)) continue;\n\n const collectionName = humanize(tableName);\n const collection = {\n name: collectionName,\n singularName: singularize(collectionName),\n slug: tableName,\n table: tableName,\n schema: pgSchema,\n icon: getIconForTable(tableName),\n properties: {\n ...buildProperties(meta, enumMap),\n ...buildRelations(meta, slugByTable, collectionBySlug)\n }\n } as unknown as PostgresCollectionConfig;\n\n collections.push(collection);\n collectionBySlug.set(tableName, collection);\n }\n\n return collections;\n}\n\n/** Introspect the database and return ready-to-serve collections. */\nexport async function introspectCollections(\n client: Queryable,\n pgSchema: string\n): Promise<PostgresCollectionConfig[]> {\n const schema = await introspectSchema(client, pgSchema);\n return buildCollectionsFromSchema(schema, pgSchema);\n}\n","/**\n * Build drizzle tables at runtime from an introspected schema.\n *\n * A project with declared collections gets its drizzle tables from a generated `schema.generated.ts` that\n * the developer commits. BaaS mode has no such file — it points at a database\n * and serves it — so the equivalent table objects are constructed here from\n * `information_schema` metadata.\n *\n * These are handed to drizzle as its schema, which keeps the relational query\n * path (`db.query.*`) working; without them FetchService would fall back to\n * plain selects and lose relation loading.\n */\nimport {\n bigint,\n boolean,\n char,\n cidr,\n customType,\n date,\n doublePrecision,\n geometry,\n inet,\n integer,\n interval,\n json,\n jsonb,\n line,\n macaddr,\n macaddr8,\n numeric,\n pgSchema,\n pgTable,\n point,\n primaryKey,\n real,\n smallint,\n text,\n time,\n timestamp,\n uuid,\n varchar,\n vector,\n type PgColumnBuilderBase,\n type PgTable\n} from \"drizzle-orm/pg-core\";\n\nimport { relations, type Relations } from \"drizzle-orm\";\n\nimport type { TableColumn, TableMeta } from \"./introspect-db-logic\";\n\n/** drizzle ships no bytea builder; binary must round-trip as a Buffer. */\nconst bytea = customType<{ data: Buffer; driverData: Buffer }>({\n dataType: () => \"bytea\"\n});\n\n/**\n * Postgres stores a column's type modifier (varchar length, vector dimensions)\n * in `atttypmod`. For length-carrying string types it is length + VARHDRSZ(4);\n * for pgvector it is the dimension count as-is. -1 means unspecified.\n */\nfunction varlenLength(col: TableColumn): number | undefined {\n return col.atttypmod && col.atttypmod > 4 ? col.atttypmod - 4 : undefined;\n}\n\n/**\n * Map a Postgres type to a drizzle column builder, keyed on `udt_name` — the\n * concrete underlying type, which is exact where `data_type` reports umbrella\n * values like \"ARRAY\" or \"USER-DEFINED\".\n *\n * Unknown types fall back to `text`: the driver still reads and writes them,\n * with the value passing through as a string, which beats dropping the column.\n */\nfunction scalarBuilder(udtName: string, name: string, col: TableColumn): PgColumnBuilderBase {\n switch (udtName) {\n case \"uuid\":\n return uuid(name);\n case \"bool\":\n return boolean(name);\n case \"int2\":\n return smallint(name);\n case \"int4\":\n return integer(name);\n case \"int8\":\n return bigint(name, { mode: \"number\" });\n case \"float4\":\n return real(name);\n case \"float8\":\n return doublePrecision(name);\n case \"numeric\":\n case \"money\":\n return numeric(name);\n case \"json\":\n return json(name);\n case \"jsonb\":\n return jsonb(name);\n case \"date\":\n return date(name);\n case \"time\":\n return time(name);\n case \"timetz\":\n return time(name, { withTimezone: true });\n case \"timestamp\":\n return timestamp(name);\n case \"timestamptz\":\n return timestamp(name, { withTimezone: true });\n case \"interval\":\n return interval(name);\n case \"bytea\":\n return bytea(name);\n case \"inet\":\n return inet(name);\n case \"cidr\":\n return cidr(name);\n case \"macaddr\":\n return macaddr(name);\n case \"macaddr8\":\n return macaddr8(name);\n case \"point\":\n return point(name);\n case \"line\":\n return line(name);\n case \"geometry\":\n return geometry(name);\n case \"vector\": {\n // drizzle requires the dimension count; without it fall back to text\n // rather than declaring a vector of unknown width.\n const dimensions = col.atttypmod && col.atttypmod > 0 ? col.atttypmod : undefined;\n return dimensions ? vector(name, { dimensions }) : text(name);\n }\n case \"bpchar\": {\n const length = varlenLength(col);\n return length ? char(name, { length }) : char(name);\n }\n case \"varchar\": {\n const length = varlenLength(col);\n return length ? varchar(name, { length }) : text(name);\n }\n default:\n // Includes text, enums (pg enums are strings on the wire), citext,\n // geography, and anything else this driver hasn't met yet.\n return text(name);\n }\n}\n\nfunction columnBuilderFor(col: TableColumn): PgColumnBuilderBase {\n // Array types are named after their element with a leading underscore\n // (_int4 = int4[]), so the element mapping is reused verbatim.\n if (col.udt_name.startsWith(\"_\")) {\n const element = scalarBuilder(col.udt_name.slice(1), col.column_name, col);\n return (element as unknown as { array(): PgColumnBuilderBase }).array();\n }\n return scalarBuilder(col.udt_name, col.column_name, col);\n}\n\n/**\n * Build one drizzle table per introspected table, keyed by table name.\n */\nexport function buildDrizzleTablesFromSchema(\n tablesMap: Map<string, TableMeta>,\n pgSchemaName = \"public\"\n): Record<string, PgTable> {\n const schema = pgSchemaName === \"public\" ? null : pgSchema(pgSchemaName);\n // The column set is only known at runtime, so drizzle's generic table\n // signature can't be satisfied statically; call it through a loose type.\n const createTable = (schema ? schema.table.bind(schema) : pgTable) as unknown as (\n name: string,\n columns: Record<string, PgColumnBuilderBase>,\n extras?: (self: Record<string, unknown>) => unknown[]\n ) => PgTable;\n\n const tables: Record<string, PgTable> = {};\n\n for (const [tableName, meta] of tablesMap) {\n const columns: Record<string, PgColumnBuilderBase> = {};\n\n for (const col of meta.columns) {\n let builder = columnBuilderFor(col);\n\n if (col.is_nullable === \"NO\") {\n builder = (builder as unknown as { notNull(): PgColumnBuilderBase }).notNull();\n }\n // Single-column primary keys are marked inline; composite keys are\n // declared in the table extras below.\n if (meta.pks.length === 1 && meta.pks[0] === col.column_name) {\n builder = (builder as unknown as { primaryKey(): PgColumnBuilderBase }).primaryKey();\n }\n\n columns[col.column_name] = builder;\n }\n\n const isComposite = meta.pks.length > 1;\n tables[tableName] = createTable(\n tableName,\n columns,\n isComposite\n ? (t) => [primaryKey({ columns: meta.pks.map((pk) => t[pk]) as never })]\n : undefined\n );\n }\n\n return tables;\n}\n\n/**\n * Build drizzle `relations()` for the foreign keys, so the relational query\n * path can actually load them.\n *\n * FetchService asks drizzle for `with: { <key>: true }`, keyed by the relation\n * property on the collection. Tables alone don't satisfy that — without these,\n * `?include=author` silently returns the raw `author_id` and no author.\n *\n * The keys here must match `buildRelations` in introspect-runtime.ts, which is\n * what names the collection's relation properties.\n */\nexport function buildDrizzleRelationsFromSchema(\n tablesMap: Map<string, TableMeta>,\n tables: Record<string, PgTable>\n): Record<string, Relations> {\n /** Owning side, per table: the `one()` relations from its foreign keys. */\n const owning = new Map<string, { key: string; targetTable: string; fkColumn: string; targetColumn: string; relationName: string }[]>();\n /** Inverse side, per referenced table: the matching `many()` back-references. */\n const inverse = new Map<string, { key: string; sourceTable: string; relationName: string }[]>();\n\n for (const [tableName, meta] of tablesMap) {\n if (!tables[tableName]) continue;\n const columnNames = new Set(meta.columns.map((c) => c.column_name));\n\n for (const fk of meta.fks) {\n if (!tables[fk.foreign_table_name]) continue;\n\n // Mirrors buildRelations in introspect-runtime: author_id -> author,\n // falling back to the target table when the column name carries no hint.\n let key = fk.column_name.replace(/_id$/, \"\");\n if (meta.pks.includes(fk.column_name) && key === fk.column_name) {\n key = fk.foreign_table_name;\n }\n // A relation key must not shadow a real column.\n if (columnNames.has(key)) continue;\n\n // Pairs the two sides. Drizzle matches a named one() to the many()\n // carrying the same name, and disambiguates multiple foreign keys\n // into the same table.\n const relationName = `${tableName}_${fk.column_name}`;\n\n owning.set(tableName, [\n ...(owning.get(tableName) ?? []),\n { key, targetTable: fk.foreign_table_name, fkColumn: fk.column_name, targetColumn: fk.foreign_column_name, relationName }\n ]);\n\n const backKey = tableName;\n const targetColumns = new Set((tablesMap.get(fk.foreign_table_name)?.columns ?? []).map((c) => c.column_name));\n if (targetColumns.has(backKey)) continue;\n\n inverse.set(fk.foreign_table_name, [\n ...(inverse.get(fk.foreign_table_name) ?? []),\n { key: backKey, sourceTable: tableName, relationName }\n ]);\n }\n }\n\n const built: Record<string, Relations> = {};\n\n for (const tableName of tablesMap.keys()) {\n const table = tables[tableName];\n const ones = owning.get(tableName) ?? [];\n const manys = inverse.get(tableName) ?? [];\n if (!table || (ones.length === 0 && manys.length === 0)) continue;\n\n built[`${tableName}Relations`] = relations(table, ({ one, many }) => {\n const map: Record<string, unknown> = {};\n\n for (const rel of ones) {\n map[rel.key] = one(tables[rel.targetTable], {\n fields: [(table as unknown as Record<string, never>)[rel.fkColumn]],\n references: [(tables[rel.targetTable] as unknown as Record<string, never>)[rel.targetColumn]],\n relationName: rel.relationName\n });\n }\n\n // Drizzle needs the inverse of every named one(); without it,\n // normalizeRelation throws and the relational path is dead.\n for (const rel of manys) {\n // An inverse must not collide with an owning key on this table.\n if (map[rel.key]) continue;\n map[rel.key] = many(tables[rel.sourceTable], { relationName: rel.relationName });\n }\n\n return map as never;\n });\n }\n\n return built;\n}\n","import chalk from \"chalk\";\nimport { outWarn, outError } from \"./cli-output\";\n\n/**\n * Detect whether an error (or AggregateError wrapping multiple attempts)\n * represents an ECONNREFUSED — i.e. the database is simply not running.\n *\n * Handles:\n * - Direct `{ code: \"ECONNREFUSED\" }` errors from Node `net`\n * - `AggregateError` from dual-stack IPv4+IPv6 connection attempts\n * - Drizzle's `cause`-wrapped pg errors\n */\nexport function isEconnrefused(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as { code?: string; cause?: unknown; errors?: unknown[] };\n if (e.code === \"ECONNREFUSED\") return true;\n // AggregateError from Node net (dual-stack IPv4 + IPv6)\n if (Array.isArray(e.errors)) {\n return e.errors.some(inner =>\n inner && typeof inner === \"object\" && (inner as { code?: string }).code === \"ECONNREFUSED\"\n );\n }\n // Drizzle wraps the pg error in `cause`\n if (e.cause && typeof e.cause === \"object\") {\n return isEconnrefused(e.cause);\n }\n return false;\n}\n\n/**\n * Detect PostgreSQL authentication failures.\n * PG error codes: 28P01 (invalid_password), 28000 (invalid_authorization_specification)\n */\nexport function isAuthFailure(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as { code?: string; cause?: unknown };\n if (e.code === \"28P01\" || e.code === \"28000\") return true;\n if (e.cause && typeof e.cause === \"object\") {\n return isAuthFailure(e.cause);\n }\n // Also check the message for common pg auth failure text\n if (\"message\" in e && typeof (e as { message?: string }).message === \"string\") {\n const msg = (e as { message: string }).message.toLowerCase();\n if (msg.includes(\"password authentication failed\") || msg.includes(\"no pg_hba.conf entry\")) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Detect the \"SSL is not enabled on the server\" failure — the client attempted\n * an SSL handshake against a Postgres server that doesn't support it (common\n * with a plain local dev database). The fix is `?sslmode=disable` on the URL.\n */\nexport function isSslNotEnabled(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as { message?: string; cause?: unknown };\n if (typeof e.message === \"string\" && e.message.toLowerCase().includes(\"ssl is not enabled on the server\")) {\n return true;\n }\n if (e.cause && typeof e.cause === \"object\") {\n return isSslNotEnabled(e.cause);\n }\n return false;\n}\n\n/**\n * Detect PostgreSQL \"cannot drop ... because other objects depend on it\"\n * (error code 2BP01, dependent_objects_still_exist). This is the failure that\n * strands a declarative `db push` half-applied when a collection is removed but\n * an enum type it defined is still referenced by another object.\n */\nexport function isDependencyDropError(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as { code?: string; message?: string; cause?: unknown };\n if (e.code === \"2BP01\") return true;\n if (typeof e.message === \"string\") {\n const msg = e.message.toLowerCase();\n if (msg.includes(\"other objects depend on it\") || msg.includes(\"cannot drop type\")) {\n return true;\n }\n }\n if (e.cause && typeof e.cause === \"object\") {\n return isDependencyDropError(e.cause);\n }\n return false;\n}\n\n/**\n * Parse host:port from a DATABASE_URL for display purposes.\n */\nfunction parseHostInfo(databaseUrl: string): string {\n try {\n const parsed = new URL(databaseUrl);\n return `${parsed.hostname}:${parsed.port || 5432}`;\n } catch {\n return \"unknown\";\n }\n}\n\n/**\n * Format a diagnostic banner for ECONNREFUSED errors.\n */\nfunction formatConnectionRefusedBanner(databaseUrl: string): string {\n const hostInfo = parseHostInfo(databaseUrl);\n return (\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ Cannot connect to PostgreSQL at ${hostInfo}\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n ` The database server is not running or is not accepting\\n` +\n ` connections. Common fixes:\\n` +\n `\\n` +\n ` • docker compose up -d db (the service a Rebase scaffold ships)\\n` +\n ` • brew services start postgresql@18\\n` +\n ` • Verify DATABASE_URL in your .env file\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`\n );\n}\n\n/**\n * Format a diagnostic banner for authentication failures.\n */\nfunction formatAuthFailureBanner(databaseUrl: string): string {\n const hostInfo = parseHostInfo(databaseUrl);\n let username = \"unknown\";\n try {\n username = new URL(databaseUrl).username || \"unknown\";\n } catch { /* ignore */ }\n\n return (\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ Authentication failed for user \"${username}\" at ${hostInfo}\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n ` PostgreSQL rejected the credentials. Common fixes:\\n` +\n `\\n` +\n ` • Check the username and password in DATABASE_URL\\n` +\n ` • Verify the user exists: psql -c \"\\\\du\"\\n` +\n ` • Reset the password: ALTER USER ${username} PASSWORD 'new_password';\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`\n );\n}\n\n/**\n * Format a diagnostic banner for \"SSL is not enabled on the server\".\n */\nfunction formatSslNotEnabledBanner(databaseUrl: string): string {\n const hostInfo = parseHostInfo(databaseUrl);\n const suggestion = databaseUrl.includes(\"?\") ? \"&sslmode=disable\" : \"?sslmode=disable\";\n return (\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ SSL is not enabled on the PostgreSQL server at ${hostInfo}\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n ` The client tried to connect over SSL, but the server does not\\n` +\n ` support it. This is normal for a plain local dev database.\\n` +\n `\\n` +\n ` Fix: append ${chalk.bold(\"sslmode=disable\")} to DATABASE_URL, e.g.\\n` +\n `\\n` +\n ` DATABASE_URL=...${suggestion}\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`\n );\n}\n\n/**\n * Format a diagnostic banner for a dependency-drop failure during `db push`.\n * Explains that the database may be left partially migrated and how to recover.\n */\nfunction formatDependencyDropBanner(): string {\n return (\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ Schema push failed: a type/table could not be dropped\\n` +\n ` because other objects still depend on it.\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n ` ${chalk.yellow(\"The database may now be partially migrated.\")} Atlas applies\\n` +\n ` statements individually, so earlier changes in this push may\\n` +\n ` already be committed while later ones failed.\\n` +\n `\\n` +\n ` This commonly happens when a collection is removed but an enum\\n` +\n ` type it defined is still referenced. To recover:\\n` +\n `\\n` +\n ` 1. Inspect the leftover object named in the error above.\\n` +\n ` 2. Drop it with CASCADE, e.g.:\\n` +\n ` psql \"$DATABASE_URL\" -c 'DROP TYPE \"<name>\" CASCADE;'\\n` +\n ` 3. Re-run: ${chalk.bold.green(\"rebase db push\")}\\n` +\n `\\n` +\n ` Prefer a safe, versioned workflow? Use ${chalk.bold(\"rebase db generate\")}\\n` +\n ` + ${chalk.bold(\"rebase db migrate\")} instead of push for destructive changes.\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`\n );\n}\n\n/**\n * Pre-flight check: verify that the database is reachable before running\n * a heavy subprocess (Atlas, migrations, etc.).\n *\n * Exits with code 1 and a friendly banner on known failure modes.\n * On unknown errors, logs a warning and allows the caller to proceed.\n */\nexport async function checkDatabaseConnectivity(databaseUrl: string): Promise<void> {\n let client: import(\"pg\").Client | undefined;\n try {\n const { Client } = await import(\"pg\");\n client = new Client({\n connectionString: databaseUrl,\n connectionTimeoutMillis: 5000\n });\n await client.connect();\n await client.query(\"SELECT 1\");\n } catch (err: unknown) {\n if (isEconnrefused(err)) {\n outError(formatConnectionRefusedBanner(databaseUrl));\n process.exit(1);\n }\n if (isAuthFailure(err)) {\n outError(formatAuthFailureBanner(databaseUrl));\n process.exit(1);\n }\n if (isSslNotEnabled(err)) {\n outError(formatSslNotEnabledBanner(databaseUrl));\n process.exit(1);\n }\n // Unknown error — warn but don't block; let the downstream tool surface details\n outWarn(chalk.yellow(` ⚠ Could not verify database connectivity: ${err instanceof Error ? err.message : String(err)}`));\n outWarn(chalk.gray(\" Proceeding anyway — the command may fail if the database is unreachable.\"));\n } finally {\n try {\n await client?.end();\n } catch {\n // ignore cleanup errors\n }\n }\n}\n\n/**\n * Post-hoc error diagnosis for direct database operations (e.g. applyPolicies).\n * Returns a formatted diagnostic string if the error matches a known pattern,\n * or null if unrecognized.\n */\nexport function diagnoseDbError(err: unknown, databaseUrl?: string): string | null {\n if (isEconnrefused(err)) {\n return formatConnectionRefusedBanner(databaseUrl || \"\");\n }\n if (isAuthFailure(err)) {\n return formatAuthFailureBanner(databaseUrl || \"\");\n }\n if (isSslNotEnabled(err)) {\n return formatSslNotEnabledBanner(databaseUrl || \"\");\n }\n if (isDependencyDropError(err)) {\n return formatDependencyDropBanner();\n }\n return null;\n}\n","/**\n * PostgresBootstrapper\n *\n * Implements the `BackendBootstrapper` interface for PostgreSQL.\n */\n\nimport { Relations, sql } from \"drizzle-orm\";\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { PgEnum, PgTable } from \"drizzle-orm/pg-core\";\nimport type { RebasePgTable } from \"./types\";\nimport {\n type AuthAdapter,\n BackendBootstrapper,\n BootstrappedAuth,\n DatabaseAdmin,\n type DataDriver,\n CollectionConfig,\n isRelationalCollectionConfig,\n type HistoryConfig,\n InitializedDriver,\n RealtimeProvider,\n type RealtimeChannelsConfig\n} from \"@rebasepro/types\";\nimport { PostgresBackendDriver } from \"./PostgresBackendDriver\";\nimport { RealtimeService } from \"./services/realtimeService\";\nimport { buildCollectionRegistry } from \"./collections/buildRegistry\";\nimport { DatabasePoolManager } from \"./databasePoolManager\";\nimport { PostgresCollectionRegistry } from \"./collections/PostgresCollectionRegistry\";\nimport { createEmailService, type EmailConfig, type EmailService, logger } from \"@rebasepro/server\";\nimport { getTableName as getCollectionTableName } from \"@rebasepro/common\";\nimport { ensureAuthTablesExist } from \"./auth/ensure-tables\";\nimport { probeAuthSchema, resolveAuthSchema } from \"./auth/schema-version\";\nimport { AuthSchemaTables, PostgresAuthRepository, UserService } from \"./auth/services\";\nimport { createAuthSchema } from \"./schema/auth-schema\";\nimport { HistoryService } from \"./history/HistoryService\";\nimport { ensureHistoryTableExists } from \"./history/ensure-history-table\";\nimport { patchPgArrayNullSafety } from \"./utils/pg-array-null-patch\";\nimport { buildCollectionsFromSchema, introspectSchema, readRlsStatus } from \"./schema/introspect-runtime\";\nimport { buildDrizzleTablesFromSchema, buildDrizzleRelationsFromSchema } from \"./schema/dynamic-tables\";\nimport { detectConnectionPosture, ensureAppRole, validatePolicyPgRoles, warnOnAnonymousGrants, warnOnLegacyRlsFunctions, warnOnRoleSchemaCollision, REBASE_USER_ROLE, type RawSqlRunner } from \"./security/rls-enforcement\";\nimport { provisionTriggerCdc, type CdcTableRef } from \"./services/cdc/trigger-cdc\";\nimport { collectJunctionLinks } from \"./services/cdc/junction-tables\";\nimport { createChannelBus, resolveChannelBusSetting } from \"./services/channel-bus\";\nimport { isChannelBusInstance } from \"@rebasepro/types\";\nimport { configureUnknownFilterFields, type UnknownFilterFieldsMode } from \"./utils/drizzle-conditions\";\n\nexport interface PostgresDriverConfig {\n connectionString?: string;\n adminConnectionString?: string;\n readConnectionString?: string;\n connection?: unknown;\n schema?: {\n tables?: Record<string, unknown>;\n enums?: Record<string, unknown>;\n relations?: Record<string, unknown>;\n };\n /**\n * PostgreSQL schema to read when deriving collections from the database\n * (BaaS mode). Defaults to `public`.\n */\n introspectionSchema?: string;\n /**\n * Realtime options, both opt-in:\n *\n * - `channels` — retention. Without rules no channel keeps any history and\n * broadcast stays fire-and-forget. See {@link ChannelRetentionRule}.\n * - `bus` — the cross-instance transport for channel broadcast and\n * presence. Defaults to in-process only, which is correct for a single\n * instance and wrong for two. See {@link ChannelBusConfig}.\n */\n realtime?: RealtimeChannelsConfig;\n /**\n * What to do with a filter field that resolves to no column at all.\n * Defaults to `\"error\"` — a filter that cannot be compiled would otherwise\n * be dropped, and a dropped condition can only widen the result set.\n * Set to `\"warn\"` to restore the pre-fix behaviour of dropping it silently.\n */\n unknownFilterFields?: UnknownFilterFieldsMode;\n}\n\n/**\n * Opaque internals bag that PostgresBootstrapper stores during `initializeDriver()`\n * and re-uses in subsequent lifecycle hooks.\n */\nexport interface PostgresDriverInternals {\n db: NodePgDatabase<any>;\n readDb?: NodePgDatabase<any>;\n registry: PostgresCollectionRegistry;\n realtimeService: RealtimeService;\n driver: PostgresBackendDriver;\n poolManager?: DatabasePoolManager;\n /**\n * Attach CDC triggers to tables that did not exist when the driver\n * bootstrapped. Only set when database-level capture is actually active.\n *\n * Auth owns its own tables and creates them later in boot, so at driver\n * bootstrap they are legitimately missing and get skipped; without this\n * they would stay uninstrumented until the next restart.\n */\n provisionCdcForTables?: (tables: CdcTableRef[]) => Promise<void>;\n}\n\n// Re-export from shared CLI error utilities\nimport { isEconnrefused } from \"./cli-errors\";\nimport { classifyConnectFailure } from \"./utils/pg-error-utils\";\n\n/**\n * Which table name the boot-time drift check should look for, for one collection.\n *\n * A declared `table` IS the table name, not a hint to be second-guessed. This\n * used to ask the registry whether it had indexed the declared name and fall\n * back to the SLUG when it had not — but \"the registry does not know this table\"\n * is exactly the condition the drift check exists to report, so the fallback\n * fired precisely when it was most harmful.\n *\n * A collection with `slug: \"usage-daily\", table: \"usage_daily\"` was reported as\n * missing table `usage-daily`: a name that does not exist, should never exist,\n * and that nobody can find by looking. Worse, the remediation the caller prints\n * says to run `rebase db push` — which would then CREATE that invented table\n * beside the correct one, the same \"second copy\" hazard the misplaced-schema\n * branch further down exists to prevent. Seen in production, where a correctly\n * migrated database reported drift on every boot.\n *\n * The slug is used only when nothing was declared, which is the config shape\n * where the slug genuinely is the table name.\n *\n * Exported for its own test: the caller needs a live pool and a real database,\n * and this is the part that was wrong.\n */\nexport function resolveDriftCheckName(\n col: CollectionConfig,\n registeredTableNames: string[]\n): string {\n const declaredTable = isRelationalCollectionConfig(col) ? col.table : undefined;\n return declaredTable\n ?? registeredTableNames.find((k) => k === col.slug)\n ?? col.slug;\n}\n\n/**\n * Why the tables this backend serves are not in the database — the part of the\n * drift warning that has to be true rather than merely plausible.\n *\n * The three answers need three different actions, and only the caller knows\n * which one applies. This warning used to assert the first (\"this runtime\n * applies the collection schema at boot unless REBASE_MIGRATE_ON_BOOT=none\")\n * and then point at that variable and at driver-version skew. For an app whose\n * boot path contained no provisioning step at all, every word of that was a\n * dead end: nothing read the variable, and the driver was current. The advice\n * cost an investigation, which is a strictly worse outcome than saying less.\n *\n * Exported for its own test: the surrounding check needs a live pool and a real\n * database, and this is the part that was wrong.\n */\nexport function describeSchemaDriftCause(\n provisioning: { attempted: boolean; reason?: string } | undefined\n): string[] {\n // A caller too old to send the signal gets no claim either way — just where\n // to look. Guessing is what got this wrong the first time.\n if (provisioning === undefined) {\n return [\n \" This runtime could not determine whether a schema-creation step ran\",\n \" before this check (the caller predates that signal).\",\n \" • Look for a \\\"Collection schema:\\\" line above. No such line at all\",\n \" means nothing tried to create these tables in this process.\"\n ];\n }\n if (provisioning.attempted) {\n return [\n \" A schema-creation step DID run this boot and these tables are still\",\n \" missing, so it did not create them — check the \\\"schema:\\\" lines above\",\n \" for what it did instead, and for DDL errors.\",\n \" • A collection routed to another engine or data source is not\",\n \" created here; that is reported separately at boot.\",\n \" • Otherwise this is a bug worth reporting, with those lines.\"\n ];\n }\n return [\n \" No schema-creation step ran this boot:\",\n ` ${provisioning.reason ?? \"no reason was given.\"}`,\n \" Resolve that reason — the drift is its consequence, not a separate\",\n \" problem, and re-running a migration tool will not change it.\"\n ];\n}\n\n/**\n * Is this the local database `rebase init` scaffolds — i.e. the one case where\n * \"you are connected as a superuser\" is not news?\n *\n * The scaffold's own `docker-compose.yml` sets `POSTGRES_USER: rebase_app`,\n * which makes that role the cluster superuser, so the superuser advisory below\n * was the only WARN a brand-new project ever saw and it was about a decision\n * the tool had made for the developer.\n *\n * Of the two available fixes — provision a non-superuser table-owner role in\n * the scaffold, or recognise the local shape and stay quiet — this is the\n * second, because the first breaks the scaffold it is meant to improve: a\n * non-superuser owner cannot `CREATE EXTENSION` (search collections need\n * `pg_trgm`/`unaccent`, applied by `rebase db push` and again by the boot\n * schema-ensure), so the very first `pnpm run db:push` on a scaffolded project\n * with a search block would fail. Trading a working first run for a quieter log\n * line is the wrong trade.\n *\n * The condition is deliberately narrow — a *non-production* process talking to\n * a database on the loopback interface. A genuine production superuser\n * connection still warns, and so does a non-production process pointed at a\n * remote database (the usual \"my dev machine writes to staging\" mistake, where\n * the advisory is exactly right). NODE_ENV alone would not do: the scaffold\n * ships `NODE_ENV=development` and some deployments inherit it.\n */\nexport function isScaffoldedLocalDatabase(connectionString: string | undefined): boolean {\n if (process.env.NODE_ENV === \"production\") return false;\n if (!connectionString) return false;\n let host: string;\n try {\n host = new URL(connectionString).hostname;\n } catch {\n return false;\n }\n // `new URL` keeps IPv6 literals in brackets.\n const bare = host.replace(/^\\[|\\]$/g, \"\").toLowerCase();\n return bare === \"localhost\"\n || bare === \"::1\"\n || bare === \"0.0.0.0\"\n || bare === \"\"\n || /^127\\./.test(bare)\n || bare.endsWith(\".localhost\");\n}\n\n/**\n * Default PostgreSQL bootstrapper.\n *\n * Use it to register Postgres with `initializeRebaseBackend()`:\n * ```typescript\n * initializeRebaseBackend({\n * ...config,\n * bootstrappers: [postgresBootstrapper()]\n * });\n * ```\n */\n/**\n * Where the collections schema stamp lives.\n *\n * The runtime's own internal schema, always — unlike the auth stamp, which\n * follows the users collection. `rebase` and `auth` sit outside\n * `introspectionSchema` by construction, so nothing here is ever served as a\n * collection.\n */\nconst SCHEMA_META_SCHEMA = \"rebase\";\n\nexport function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): BackendBootstrapper {\n // Applied at construction rather than threaded through every read: the\n // condition builder's static methods are reached from call sites that\n // carry no config. See `UnknownFilterFieldsMode`.\n if (pgConfig.unknownFilterFields) {\n configureUnknownFilterFields(pgConfig.unknownFilterFields);\n }\n\n /**\n * The handle the schema/policy hooks issue their DDL through.\n *\n * Both hooks run BEFORE `initializeDriver`, so `driverResult` is a stand-in\n * the caller may not have: the bundle path can synthesize one from the\n * connection its coordinator opened, but an application that built this\n * adapter itself never handed the framework a connection — it handed it to\n * *us*, as `pgConfig.connection`. Falling back to that is what lets a\n * self-built adapter provision at all; requiring the argument is what left\n * those apps with no tables and a 500 on every data route.\n *\n * Either handle is equivalent here. The driver's `schemaAwareDb` differs\n * only by the drizzle schema object registered on it — relevant to the query\n * builder, not to `execute(sql.raw(...))` — and every statement these hooks\n * emit is schema-qualified DDL, so neither depends on `search_path`.\n */\n /**\n * The drizzle handle itself, for the statements that want parameters.\n *\n * `provisioningQueryable` below hands back a `query(text)` shim because the\n * DDL it serves is built as text. The schema stamp writes a *value*, so it\n * wants the parameterised form — building that string by hand would be one\n * more place a quoted literal has to be got right for no benefit.\n */\n const provisioningDb = (driverResult?: InitializedDriver) => {\n const internals = driverResult?.internals as PostgresDriverInternals | undefined;\n return internals?.db ?? (pgConfig.connection as PostgresDriverInternals[\"db\"] | undefined);\n };\n\n const provisioningQueryable = (driverResult?: InitializedDriver) => {\n const internals = driverResult?.internals as PostgresDriverInternals | undefined;\n const db = internals?.db ?? (pgConfig.connection as PostgresDriverInternals[\"db\"] | undefined);\n if (!db) {\n throw new Error(\n \"Cannot provision the collection schema: this Postgres adapter was created without a \" +\n \"`connection`, and no initialized driver was supplied to fall back on. Pass `connection` \" +\n \"to `createPostgresAdapter` (see `createPostgresDatabaseConnection`).\"\n );\n }\n return {\n async query<T>(text: string): Promise<{ rows: T[] }> {\n const result = await db.execute(sql.raw(text));\n const rows = (result as unknown as { rows?: T[] }).rows;\n return { rows: rows ?? (Array.isArray(result) ? (result as T[]) : []) };\n }\n };\n };\n\n return {\n type: \"postgres\",\n\n async initializeDriver(config: unknown): Promise<InitializedDriver> {\n // config is passed from coordinator, we merge it with our internal pgConfig if needed\n // Currently config from init.ts is `{ collections, collectionRegistry, mode }`\n const { collections, collectionRegistry, introspectCollections, baas, schemaProvisioning, realtime } = config as {\n collections?: CollectionConfig[];\n collectionRegistry?: unknown;\n introspectCollections?: boolean;\n baas?: { unprotectedTables?: \"exclude\" | \"serve\" };\n schemaProvisioning?: { attempted: boolean; reason?: string };\n realtime?: { subscribe: boolean; provision: boolean };\n };\n // Absent means a caller that predates the field, and every one of\n // those is a single process that both serves websockets and owns the\n // schema. Defaulting to false here would silently disable realtime\n // for them — the failure this whole area is prone to.\n const realtimeSubscribes = realtime?.subscribe ?? true;\n const realtimeProvisions = realtime?.provision ?? true;\n // Secure by default: a table with no RLS is not served.\n const unprotectedTables = baas?.unprotectedTables ?? \"exclude\";\n\n const connection = pgConfig.connection;\n const rawClient = (connection && typeof connection === \"object\" && \"$client\" in connection\n ? (connection as Record<string, unknown>).$client\n : connection) as import(\"pg\").Pool;\n\n // ── No declared collections: derive the schema from the database ──\n // No collection files and no generated drizzle schema exist, so read\n // the live database and build both from what is actually there.\n let introspectedCollections: CollectionConfig[] | undefined;\n let introspectedTables: Record<string, PgTable> | undefined;\n let introspectedRelations: Record<string, Relations> | undefined;\n if (introspectCollections && (!collections || collections.length === 0)) {\n const pgSchemaName = pgConfig.introspectionSchema ?? \"public\";\n const schema = await introspectSchema(rawClient, pgSchemaName);\n\n // ── Only serve what the database protects ────────────────\n // Requests run as rebase_user, which is granted DML on the\n // schema. A table with RLS disabled therefore has no\n // authorization model at all: serving it hands every row to\n // every authenticated user. baas never runs `db push`, so\n // nothing here would have enabled RLS on the user's behalf.\n const rlsStatus = await readRlsStatus(rawClient, pgSchemaName);\n const unprotected = [...schema.tablesMap.keys()].filter(\n (t) => !schema.joinTables.has(t) && !rlsStatus.get(t)?.rlsEnabled\n );\n const policyless = [...schema.tablesMap.keys()].filter(\n (t) => !schema.joinTables.has(t) && rlsStatus.get(t)?.rlsEnabled && rlsStatus.get(t)?.policyCount === 0\n );\n\n if (unprotected.length > 0) {\n if (unprotectedTables === \"serve\") {\n logger.warn(\n `🔓 [rls] Serving ${unprotected.length} table(s) with row-level security DISABLED: ${unprotected.join(\", \")}. ` +\n \"Every authenticated request can read and write every row of these. \" +\n \"This is baas.unprotectedTables: \\\"serve\\\".\"\n );\n } else {\n logger.warn(\n `🔒 [rls] Not serving ${unprotected.length} table(s) — row-level security is disabled, so they have no ` +\n `authorization model: ${unprotected.join(\", \")}\\n` +\n unprotected.map((t) => ` ALTER TABLE \"${pgSchemaName}\".\"${t}\" ENABLE ROW LEVEL SECURITY; -- then add a policy`).join(\"\\n\") +\n \"\\n Set baas.unprotectedTables: \\\"serve\\\" to expose them regardless.\"\n );\n for (const t of unprotected) schema.tablesMap.delete(t);\n }\n }\n if (policyless.length > 0) {\n // Legal, and silently returns nothing — worth saying out loud,\n // since an empty table reads exactly like one with no rows.\n logger.warn(\n `🔒 [rls] ${policyless.length} table(s) have RLS enabled but no policies, so they will return no rows: ${policyless.join(\", \")}`\n );\n }\n\n introspectedCollections = buildCollectionsFromSchema(schema, pgSchemaName);\n introspectedTables = buildDrizzleTablesFromSchema(schema.tablesMap, pgSchemaName);\n // Without these, drizzle's relational path can't resolve the\n // relations the collections above advertise.\n introspectedRelations = buildDrizzleRelationsFromSchema(schema.tablesMap, introspectedTables);\n logger.info(\n `🔍 [PostgresRegistry] BaaS mode: derived ${introspectedCollections.length} collections from schema \"${pgSchemaName}\" [${introspectedCollections.map(c => c.slug).join(\", \")}]`\n );\n }\n\n const activeCollections = introspectedCollections ?? collections;\n const schemaTables = introspectedTables ?? pgConfig.schema?.tables;\n const schemaRelations = introspectedRelations ?? (pgConfig.schema?.relations as Record<string, Relations> | undefined);\n\n // Create a fresh registry for this driver. Registration order is\n // load-bearing, so it lives in one place — see `buildCollectionRegistry`.\n const registry = buildCollectionRegistry({\n collections: activeCollections,\n tables: schemaTables,\n enums: pgConfig.schema?.enums as Record<string, PgEnum<[string, ...string[]]>> | undefined,\n relations: schemaRelations\n });\n\n // Patch Drizzle's PgArray columns to handle NULL values safely.\n // Drizzle's mapFromDriverValue crashes with \"value.map is not a function\"\n // when a native array column (text[], integer[], etc.) contains NULL.\n if (schemaTables) {\n patchPgArrayNullSafety(schemaTables as Record<string, unknown>);\n }\n\n // Build schema-aware Drizzle connection\n const mergedSchema: Record<string, unknown> = {\n ...schemaTables,\n ...(schemaRelations || {})\n };\n const { drizzle: createDrizzle } = await import(\"drizzle-orm/node-postgres\");\n const schemaAwareDb = createDrizzle(rawClient, { schema: mergedSchema });\n\n // Verify connection — fail fast if the database is unreachable\n try {\n await schemaAwareDb.execute(sql`SELECT 1`);\n } catch (err: unknown) {\n const isConnectionRefused = isEconnrefused(err);\n if (isConnectionRefused) {\n // Parse host/port from connection string for a helpful message\n let hostInfo = pgConfig.connectionString || \"unknown\";\n try {\n const parsed = new URL(pgConfig.connectionString || \"\");\n hostInfo = `${parsed.hostname}:${parsed.port || 5432}`;\n } catch { /* use raw string */ }\n\n const message =\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ Cannot connect to PostgreSQL at ${hostInfo}\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n ` The database server is not running or is not accepting\\n` +\n ` connections. Common fixes:\\n` +\n `\\n` +\n ` • docker compose up -d db (the service a Rebase scaffold ships)\\n` +\n ` • brew services start postgresql@18\\n` +\n ` • Verify DATABASE_URL in your .env file\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`;\n logger.error(message);\n throw new Error(`Cannot connect to PostgreSQL at ${hostInfo}: connection refused. Is the database running?`);\n }\n\n /*\n * Everything else.\n *\n * Two problems with what used to happen here. First, the logged\n * error was Drizzle's wrapper — `Failed query: SELECT 1` and a\n * stack through drizzle internals — while the sentence that\n * actually says what is wrong (\"password authentication failed\n * for user …\", \"database … does not exist\") sits in `.cause`\n * and was never printed. A developer with a typo in their\n * DATABASE_URL got two walls of stack trace and no cause.\n *\n * Second, \"continuing… the pool may recover\" is only true for\n * a transient fault. A wrong password or a missing database is\n * settled: the next query fails the same way, so the process\n * died seconds later anyway — after printing a reassurance.\n * Those now fail here, where the message can be about the\n * cause rather than about whichever query ran next.\n */\n const { fatal, reason, code } = classifyConnectFailure(err);\n const detail = code ? ` [${code}]` : \"\";\n\n if (fatal) {\n logger.error(\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ PostgreSQL refused the connection\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n ` ${reason}${detail}\\n` +\n `\\n` +\n ` The server is reachable, so this is the credentials or the\\n` +\n ` database name in DATABASE_URL — check them in your .env.\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`\n );\n throw new Error(`PostgreSQL refused the connection: ${reason}${detail}`);\n }\n\n logger.error(`❌ Failed to connect to PostgreSQL: ${reason}${detail}`, { error: err });\n logger.warn(\"⚠️ Continuing without initial database verification. Drizzle/PG will attempt to connect on subsequent queries.\");\n }\n\n // Create services\n const realtimeService = new RealtimeService(schemaAwareDb, registry);\n\n // Initialize read replica connection if configured\n let readDb: import(\"drizzle-orm/node-postgres\").NodePgDatabase<any> | undefined;\n const readUrl = process.env.DATABASE_READ_URL;\n if (readUrl && readUrl !== pgConfig.connectionString) {\n try {\n const { createReadReplicaConnection } = await import(\"./connection\");\n const readResources = createReadReplicaConnection(readUrl, mergedSchema);\n readDb = readResources.db;\n logger.info(\"📖 [PostgresBootstrapper] Read replica connection established\");\n } catch (err) {\n logger.warn(\"⚠️ Could not connect to read replica, falling back to primary for all queries\", { error: err });\n }\n }\n const poolManager = pgConfig.adminConnectionString\n ? new DatabasePoolManager(pgConfig.adminConnectionString)\n : undefined;\n const driver = new PostgresBackendDriver(schemaAwareDb, realtimeService, registry, undefined, poolManager);\n realtimeService.setDataDriver(driver);\n\n // ── RLS enforcement (user context) ───────────────────────────────\n // Authenticated requests are authorized entirely by RLS policies,\n // but a privileged connection (superuser / BYPASSRLS / table owner)\n // bypasses RLS. Detect the posture and, when privileged, provision\n // the restricted `rebase_user` role and route authenticated\n // requests (reads AND writes) through it. The server context (base\n // driver / auth flows / `rebase.sql`) stays on the owner connection\n // and bypasses; `rebase.dataAsAdmin` does NOT — it is scoped with\n // `withAuth({ uid: \"service\", roles: [\"admin\"] })` at boot, so it\n // runs as `rebase_user` and its policies are evaluated.\n // Default-on: a privileged connection that cannot be\n // isolated fails the boot rather than serving unenforced requests.\n {\n const runSql: RawSqlRunner = async (text) => {\n const res = await schemaAwareDb.execute(sql.raw(text));\n return (res.rows ?? []) as Record<string, unknown>[];\n };\n // Said before anything else touches the schema: if the role and\n // a schema share a name, unqualified SQL from any tool that does\n // not pin `search_path` has been landing in the wrong place, and\n // that is worth knowing before reading the drift report below.\n await warnOnRoleSchemaCollision(runSql);\n\n const posture = await detectConnectionPosture(runSql);\n if (posture.privileged) {\n const collectionSchemas = registry.getCollections()\n .map((c) => (c as { schema?: string }).schema)\n .filter((s): s is string => typeof s === \"string\");\n // `auth` is deliberately absent: the RLS helpers moved into\n // `rebase`, and granting USAGE on a schema Rebase does not\n // own would, on a Supabase database, hand the end-user role\n // access to theirs.\n await ensureAppRole(runSql, [\"public\", \"rebase\", ...collectionSchemas]);\n driver.rlsUserRole = REBASE_USER_ROLE;\n realtimeService.rlsUserRole = REBASE_USER_ROLE;\n logger.info(`🔐 RLS enforcement active: authenticated requests run as \"${REBASE_USER_ROLE}\" (connection \"${posture.role}\" bypasses RLS: ${posture.superuser ? \"superuser\" : posture.bypassRLS ? \"BYPASSRLS\" : \"table owner\"})`);\n if (posture.superuser || posture.bypassRLS) {\n const message =\n `The database connection runs as ${posture.superuser ? \"a superuser\" : \"a BYPASSRLS role\"} (\"${posture.role}\"). ` +\n `User requests are isolated via SET LOCAL ROLE, but connect as a non-superuser ` +\n `table-owner role in production so the server/owner context is least-privilege.`;\n if (isScaffoldedLocalDatabase(pgConfig.connectionString)) {\n logger.debug(`🔐 ${message}`);\n } else {\n logger.warn(`⚠️ ${message}`);\n }\n }\n } else {\n logger.info(`🔐 RLS enforcement: connection role \"${posture.role}\" is subject to RLS natively; no role switch needed.`);\n }\n\n // Independent of posture: a policy targeting a role the request\n // never runs as filters every row, so the collection reads as\n // empty rather than erroring. Applies to both branches — the\n // role that matters is whichever one requests actually use.\n await validatePolicyPgRoles(\n runSql,\n registry.getCollections() as never,\n driver.rlsUserRole ?? posture.role\n );\n\n // The same habit one surface over, and the dangerous direction:\n // a rule that reads as \"signed in only\" but is true for every\n // caller grants the data away rather than hiding it.\n warnOnAnonymousGrants(registry.getCollections() as never);\n\n // Raw policy SQL written against the pre-1.0 helper schema. It\n // is rewritten on compile, so this is the only place the project\n // is ever told the spelling moved.\n warnOnLegacyRlsFunctions(registry.getCollections() as never);\n }\n\n // Ensure branch metadata table exists when branching is available\n if (driver.branchService) {\n try {\n await driver.branchService.ensureBranchMetadataTable();\n } catch (err) {\n logger.warn(\"⚠️ Could not initialize branch metadata table\", { error: err });\n }\n }\n\n // ── Channel history ──────────────────────────────────────────────\n // Opt-in per channel pattern. With no rules this creates no tables\n // and leaves broadcast on its original fire-and-forget path, so\n // presence-only apps pay nothing for it.\n try {\n await realtimeService.configureChannelHistory(\n pgConfig.realtime?.channels,\n { provision: realtimeProvisions }\n );\n } catch (err) {\n logger.warn(\"⚠️ Could not initialize channel history tables — retained channels will not replay\", { error: err });\n }\n\n // ── Realtime change source ───────────────────────────────────────\n // Prefer DATABASE_DIRECT_URL to bypass PgBouncer for LISTEN/NOTIFY.\n const directUrl = process.env.DATABASE_DIRECT_URL || pgConfig.connectionString;\n\n // ── Cross-instance channel bus ───────────────────────────────────\n // Entity changes already span instances (CDC / LISTEN below).\n // Channel broadcast and presence did not — they lived in per-process\n // maps, so behind two replicas the clients of one were invisible to\n // the other. Opt-in, and a no-op when left at \"memory\".\n try {\n const busSetting = resolveChannelBusSetting(pgConfig.realtime?.bus);\n // A supplied instance is always installed; a named built-in only\n // when it is not the in-process default, so the common case\n // touches none of this machinery.\n const wantsBus = isChannelBusInstance(busSetting) || busSetting.type !== \"memory\";\n if (wantsBus) {\n await realtimeService.configureChannelBus(\n createChannelBus(busSetting, {\n db: schemaAwareDb as unknown as NodePgDatabase<Record<string, unknown>>,\n directUrl\n })\n );\n }\n } catch (err) {\n logger.warn(\"⚠️ [ChannelBus] Could not configure the channel bus — channel broadcast and presence stay per-instance\", { error: err });\n }\n\n // Database-level Change Data Capture. When active, realtime events are\n // emitted for EVERY committed write — including ones that bypass the\n // Rebase API (psql, another service's cron, raw SQL, the Studio SQL\n // editor) — matching Supabase Realtime's WAL-tailing model. CDC also\n // becomes the cross-instance channel, so the legacy per-mutation\n // LISTEN/NOTIFY is not started alongside it.\n // REALTIME_CDC=auto → default: enable where the connection supports\n // it; silently fall back to app-level otherwise\n // REALTIME_CDC=trigger → force trigger-based capture (warns if it can't)\n // REALTIME_CDC=wal → prefer WAL logical replication (degrades to trigger)\n // REALTIME_CDC=off → app-level realtime only\n const validModes = new Set([\"auto\", \"wal\", \"trigger\", \"off\"]);\n let cdcMode = (process.env.REALTIME_CDC || \"auto\").trim().toLowerCase();\n if (!validModes.has(cdcMode)) {\n logger.warn(`⚠️ [CDC] Unknown REALTIME_CDC value \"${cdcMode}\" — expected auto|wal|trigger|off. Defaulting to \"auto\".`);\n cdcMode = \"auto\";\n }\n\n // `auto` tries CDC but treats \"can't\" as a normal outcome (info log);\n // explicit trigger/wal was asked for, so a failure is worth a warning.\n // A process that consumes nothing needs no capture *started*, and a\n // process that does not own the schema installs no triggers. They\n // come apart: the `api` in a split with an external migration Job\n // subscribes without provisioning, and both answers are correct.\n const wantsCdc = cdcMode !== \"off\" && (realtimeSubscribes || realtimeProvisions);\n const explicitCdc = cdcMode === \"trigger\" || cdcMode === \"wal\";\n let cdcEnabled = false;\n let provisionCdcForTables: PostgresDriverInternals[\"provisionCdcForTables\"];\n\n if (wantsCdc && !directUrl) {\n const reason = \"no direct database connection is available for the realtime LISTEN client (set DATABASE_DIRECT_URL)\";\n if (explicitCdc) logger.warn(`⚠️ [CDC] REALTIME_CDC=${cdcMode} but ${reason} — using app-level realtime.`);\n else logger.info(`ℹ️ [CDC] Using app-level realtime — ${reason}.`);\n } else if (wantsCdc && directUrl) {\n if (cdcMode === \"wal\") {\n // Native WAL logical-replication streaming requires wal_level=logical,\n // a replication-privileged role and a replication slot, none of which\n // are bundled with this adapter yet. Degrade to trigger-based capture,\n // which provides equivalent database-level coverage.\n logger.warn(\n \"⚠️ [CDC] REALTIME_CDC=wal: native WAL streaming is not bundled in this build; \" +\n \"using trigger-based change capture instead (equivalent database-level coverage).\"\n );\n }\n try {\n const cdcRunSql: RawSqlRunner = async (text) => {\n const res = await schemaAwareDb.execute(sql.raw(text));\n return (res.rows ?? []) as Record<string, unknown>[];\n };\n const cdcTables: CdcTableRef[] = registry.getCollections()\n .map((c) => ({\n schema: (c as { schema?: string }).schema ?? \"public\",\n table: getCollectionTableName(c)\n }))\n .filter((t) => Boolean(t.table) && registry.hasTableForCollection(t.table));\n // Junction tables back no collection, so the list above misses\n // them — and a link or unlink is a write nobody would hear\n // about. Their rows are the contents of a child list, which is\n // as much a change as a write to the rows themselves.\n for (const link of collectJunctionLinks(registry)) {\n cdcTables.push({ schema: link.schema,\ntable: link.table });\n }\n // Provisioning throws only when the connection can't create the\n // trigger function (insufficient privilege); enableCdc throws when\n // the LISTEN connection can't be established. Either → fall back.\n if (realtimeProvisions) await provisionTriggerCdc(cdcRunSql, cdcTables);\n if (realtimeSubscribes) await realtimeService.enableCdc(directUrl);\n cdcEnabled = true;\n // Boot steps that create their own tables (auth) run after\n // this one and use it to instrument what they just created.\n // Left undefined where this process installs nothing, so a\n // later boot step cannot re-enter the DDL path by the side\n // door — the callers already treat it as optional, because a\n // driver without CDC never sets it either.\n if (realtimeProvisions) {\n provisionCdcForTables = async (tables) => {\n await provisionTriggerCdc(cdcRunSql, tables);\n };\n }\n // Say which half ran. \"All writes now emit realtime events\"\n // is a claim about the database and stays true for a process\n // that only installed the triggers; what changes is whether\n // *this* process is listening, and an operator reading one\n // pod's log should not have to infer that from its role.\n logger.info(\n `📡 [CDC] Realtime source = database-level change capture (mode: ${cdcMode === \"wal\" ? \"wal→trigger\" : \"trigger\"}). ` +\n `All writes now emit realtime events regardless of origin.` +\n (realtimeSubscribes ? \"\" : \" This process installs the capture but does not consume it.\")\n );\n } catch (err) {\n if (explicitCdc) {\n logger.warn(\"⚠️ [CDC] Could not enable database-level change capture — falling back to app-level realtime.\", { error: err });\n } else {\n logger.info(\n \"ℹ️ [CDC] Database-level change capture unavailable (likely insufficient privileges to create triggers, \" +\n \"or the LISTEN connection was refused) — using app-level realtime. Set REALTIME_CDC=off to silence this.\",\n { detail: err instanceof Error ? err.message : String(err) }\n );\n }\n }\n }\n\n // Legacy cross-instance realtime (app-level). Skipped when CDC is\n // active because CDC already spans instances.\n if (!cdcEnabled && directUrl && realtimeSubscribes) {\n try {\n await realtimeService.startListening(directUrl);\n } catch (err) {\n logger.warn(\"⚠️ Cross-instance realtime could not be started\", { error: err });\n }\n }\n\n // ── Startup Schema Validation ────────────────────────────────────\n // One-directional: only checks collections → DB (extra DB tables\n // that aren't mapped to collections are perfectly fine).\n try {\n const registeredCollections = registry.getCollections();\n if (registeredCollections.length > 0) {\n // Deliberately unfiltered by schema: a table that is missing\n // from where the collection says it lives is very often\n // sitting in another schema entirely (see the misplaced-table\n // report below), and knowing *which* is the whole answer.\n const result = await schemaAwareDb.execute(sql.raw(`\n SELECT table_name, table_schema\n FROM information_schema.tables\n WHERE table_schema NOT IN ('pg_catalog', 'information_schema')\n AND table_type = 'BASE TABLE'\n `));\n const tablesByName = new Map<string, string[]>();\n for (const row of result.rows as Array<{ table_name: string; table_schema: string }>) {\n const schemas = tablesByName.get(row.table_name) ?? [];\n schemas.push(row.table_schema);\n tablesByName.set(row.table_name, schemas);\n }\n const dbTables = new Set(\n (result.rows as Array<{ table_name: string; table_schema: string }>).map(r =>\n r.table_schema === \"public\" ? r.table_name : `${r.table_schema}.${r.table_name}`\n )\n );\n const missing: Array<{ slug: string; table: string; foundIn: string[] }> = [];\n for (const col of registeredCollections) {\n // Auth owns its table and creates it later in this same\n // boot (initializeAuth → ensureAuthTablesExist), so it is\n // legitimately absent right now. Reporting it as drift\n // tells the user to `db:push` a table that is about to\n // exist — and on an introspected database, one that the\n // database was never supposed to hold.\n if ((col as { auth?: { enabled?: boolean } }).auth?.enabled) continue;\n\n const schemaName = \"schema\" in col && col.schema ? col.schema : \"public\";\n const checkName = resolveDriftCheckName(col, registry.getTableNames());\n const fullCheckName = schemaName === \"public\" ? checkName : `${schemaName}.${checkName}`;\n if (!dbTables.has(fullCheckName)) {\n // Report what was actually looked up: an unqualified\n // \"users\" sends people hunting for public.users.\n missing.push({ slug: col.slug,\ntable: fullCheckName,\nfoundIn: (tablesByName.get(checkName) ?? []).filter(s => s !== schemaName) });\n }\n }\n if (missing.length > 0) {\n const lines = missing.map(\n m => ` • collection \"${m.slug}\" → table \"${m.table}\"` +\n (m.foundIn.length > 0\n ? ` — but a table of that name exists in ${m.foundIn.map(s => `\"${s}\"`).join(\", \")}`\n : \"\")\n );\n // A table that exists under the same name in another\n // schema is not ordinary drift: it is almost always this\n // framework's own `search_path` hazard. Postgres resolves\n // unqualified SQL through `\"$user\", public`, and this\n // project creates a schema named `rebase` while every\n // template names the database role `rebase` too — so an\n // unqualified CREATE TABLE (a hand-written migration, the\n // SQL editor, drizzle-kit) lands in `rebase`, and the\n // runtime, which now pins `search_path=public`, cannot see\n // it. Say so, because \"missing table\" sends people to\n // re-run a push that will create a *second* copy.\n const misplaced = missing.filter(m => m.foundIn.length > 0);\n const misplacedHelp = misplaced.length === 0 ? [] : [\n \" Those tables exist — in the wrong schema. Unqualified SQL run by a\",\n \" role whose name matches a schema (`rebase` is both, by default)\",\n \" resolves through search_path's \\\"$user\\\" and creates there. Move them:\",\n ...misplaced.map(m =>\n ` ALTER TABLE \"${m.foundIn[0]}\".\"${m.table.split(\".\").pop()}\" SET SCHEMA \"${m.table.includes(\".\") ? m.table.split(\".\")[0] : \"public\"}\";`\n ),\n \" and qualify the SQL that created them, or pin search_path in DATABASE_URL\",\n \" (`?options=-c%20search_path%3Dpublic`).\",\n \"\"\n ];\n // What to tell the operator depends entirely on whether a\n // create step ran in this process, and the caller is the\n // only thing that knows. This warning used to assert that\n // it had (\"this runtime applies the collection schema at\n // boot unless REBASE_MIGRATE_ON_BOOT=none\") and send\n // people to that variable and to driver-version skew. For\n // an app whose boot path contained no provisioning step,\n // both were dead ends: nothing read that variable, and the\n // driver was current. Say which case this is instead of\n // guessing, and say nothing when the caller is too old to\n // tell us.\n const cause = describeSchemaDriftCause(schemaProvisioning);\n logger.warn([\n \"\",\n \"⚠️ SCHEMA DRIFT — the database is missing tables this backend serves:\",\n ...lines,\n \"\",\n ...misplacedHelp,\n ...cause,\n \"\",\n \" To apply this project's schema:\",\n \" • Managed cloud: the runtime creates tables and RLS at boot. `rebase db\",\n \" push` cannot reach a tenant's in-cluster database — redeploy instead.\",\n \" • Self-host: run `rebase db push` (dev) or `rebase db migrate` (prod)\",\n \" against DATABASE_URL.\",\n \"\"\n ].join(\"\\n\"));\n }\n }\n } catch (err) {\n logger.warn(\"⚠️ Startup schema validation could not run\", {\n error: err instanceof Error ? err.message : String(err)\n });\n }\n\n const internals: PostgresDriverInternals = {\n db: schemaAwareDb,\n readDb,\n registry,\n realtimeService,\n driver,\n poolManager,\n provisionCdcForTables\n };\n\n return {\n driver,\n realtimeProvider: realtimeService,\n collectionRegistry: registry,\n // Only set in baas mode — tells the server which collections the\n // database turned out to have.\n collections: introspectedCollections,\n internals\n };\n },\n\n async initializeAuth(config: unknown, driverResult: InitializedDriver): Promise<BootstrappedAuth | undefined> {\n const authConfig = config as Record<string, unknown> | undefined;\n if (!authConfig) return undefined;\n\n const internals = driverResult.internals as PostgresDriverInternals;\n const db = internals.db;\n const registry = internals.registry;\n\n // Resolve the auth collection from the explicit config.\n // This replaces the old `registry.getTable(\"users\")` magic string lookup.\n const authCollection = authConfig.collection as CollectionConfig | undefined;\n\n // ensureAuthTablesExist works with the collection abstraction — no Drizzle leakage.\n await ensureAuthTablesExist(db, authCollection);\n\n // The driver bootstrapped before these tables existed, so CDC skipped\n // them. Instrument them now, or writes to the user table emit no\n // realtime events until the next restart.\n if (authCollection && internals.provisionCdcForTables) {\n const authSchema = \"schema\" in authCollection && typeof authCollection.schema === \"string\"\n ? authCollection.schema\n : \"rebase\";\n const authTable = \"table\" in authCollection && typeof authCollection.table === \"string\"\n ? authCollection.table\n : authCollection.slug;\n if (authTable) {\n try {\n await internals.provisionCdcForTables([{ schema: authSchema, table: authTable }]);\n } catch (err) {\n logger.warn(\n `⚠️ [CDC] Could not attach change-capture to the auth table \"${authSchema}.${authTable}\" — ` +\n \"writes to it won't emit database-level events.\",\n { detail: err instanceof Error ? err.message : String(err) }\n );\n }\n }\n }\n\n let emailService: EmailService | undefined;\n if (authConfig.email) {\n emailService = createEmailService(authConfig.email as EmailConfig);\n }\n\n // Resolve the Drizzle table for the internal UserService/AuthRepository.\n // These are internal Postgres-specific services that need the Drizzle table reference.\n const tableName = authCollection\n ? (\"table\" in authCollection && typeof authCollection.table === \"string\"\n ? authCollection.table\n : authCollection.slug)\n : undefined;\n const usersTable = tableName\n ? registry.getTable(tableName) as RebasePgTable | undefined\n : undefined;\n\n let usersSchemaName = \"rebase\";\n if (authCollection && \"schema\" in authCollection && typeof authCollection.schema === \"string\") {\n usersSchemaName = authCollection.schema;\n }\n\n const authTables = createAuthSchema(usersSchemaName) as unknown as AuthSchemaTables;\n if (usersTable) {\n authTables.users = usersTable as RebasePgTable;\n }\n\n const userService = new UserService(db, authTables);\n const authRepository = new PostgresAuthRepository(db, authTables);\n\n return { userService,\nroleService: userService,\nemailService,\nauthRepository,\n// Bound to the same schema `ensureAuthTablesExist` just migrated, so the\n// health endpoint reports on the tables auth actually reads.\nschemaHealthCheck: () => probeAuthSchema(db, resolveAuthSchema(authCollection)) };\n },\n\n async initializeHistory(config: HistoryConfig, driverResult: InitializedDriver): Promise<{ historyService: HistoryService } | undefined> {\n if (!config) return undefined;\n\n const internals = driverResult.internals as PostgresDriverInternals;\n const db = internals.db;\n\n await ensureHistoryTableExists(db);\n\n const retention = typeof config === \"object\" ? config.retention : undefined;\n const historyService = new HistoryService(db, retention ? { ttlDays: retention } : undefined);\n\n return { historyService };\n },\n\n async initializeRealtime(_config: unknown, driverResult: InitializedDriver): Promise<RealtimeProvider | undefined> {\n const internals = driverResult.internals as PostgresDriverInternals;\n return internals.realtimeService;\n },\n\n /**\n * Create any collection tables, columns and enum types the database is\n * missing — additively, never destructively.\n *\n * This is what lets the managed runtime boot a project against a fresh\n * database and actually serve it. Before this, only auth tables were\n * ensured, so a managed tenant came up with working sign-in and a 500 on\n * every data route.\n *\n * Runs through the drizzle handle's underlying session so it uses the\n * same connection (and therefore the same privileges) the driver already\n * proved it can bootstrap with.\n */\n async ensureCollectionSchema(\n collections: unknown[],\n driverResult?: InitializedDriver,\n log?: (message: string) => void\n ): Promise<{ applied: number }> {\n const { ensureCollectionTables } = await import(\"./schema/ensure-collection-tables\");\n // Runs through the drizzle handle the driver already bootstrapped\n // with, so it uses exactly the connection and privileges that were\n // proven to work. Every statement is DDL or a catalogue read with no\n // bindable values (schema names are identifiers), and the module\n // validates them before they reach a string.\n const queryable = provisioningQueryable(driverResult);\n const plan = await ensureCollectionTables(\n queryable,\n collections as Parameters<typeof ensureCollectionTables>[1],\n log\n );\n for (const failure of plan.failures) {\n logger.warn(\n failure.kind === \"comment-column\"\n // The stamp records which `search` block the generated\n // column was built from. Without it the next boot cannot\n // tell a changed block from an unchanged one and adopts\n // the column again instead of refusing.\n ? `🔍 [schema] Could not record the search fingerprint on \"${failure.target}\" — search works, ` +\n `but a later change to the \\`search\\` block will not be detected: ${failure.error}`\n : `🔗 [schema] Could not add foreign key \"${failure.target}\" — the column exists and the ` +\n `collection still serves, but rows are not policed by this constraint: ${failure.error}`\n );\n }\n return { applied: plan.actions.length - plan.failures.length };\n },\n\n /**\n * Apply the collections' RLS policies — ENABLE ROW LEVEL SECURITY and the\n * `securityRules` compiled to `CREATE POLICY` — so a freshly provisioned\n * database serves data instead of denying every user-context read.\n *\n * The companion to {@link ensureCollectionSchema}: that creates the\n * tables, this makes them servable. Boot runs it *after* auth\n * initialization, because the generated policies call `rebase.uid()` /\n * `rebase.roles()`, and `CREATE POLICY` validates those functions exist.\n *\n * Runs through the same drizzle handle, one statement at a time (that\n * handle speaks the extended query protocol, which rejects multi-command\n * strings). Failures are per-table and non-fatal: a table left un-policed\n * stays RLS-enabled, so it denies rather than leaks.\n */\n async ensureCollectionPolicies(\n collections: unknown[],\n driverResult?: InitializedDriver,\n log?: (message: string) => void\n ): Promise<{ applied: number }> {\n const { ensureCollectionPolicies } = await import(\"./schema/ensure-collection-policies\");\n const queryable = provisioningQueryable(driverResult);\n const outcome = await ensureCollectionPolicies(\n queryable,\n collections as CollectionConfig[],\n log\n );\n\n for (const skip of outcome.skipped) {\n logger.warn(`🔐 [rls] Policies not applied to \"${skip.table}\": ${skip.reason}`);\n }\n for (const failure of outcome.failures) {\n logger.warn(\n `🔐 [rls] Could not fully apply policies to \"${failure.table}\" — RLS is on, so it denies until this is resolved: ${failure.error}`\n );\n }\n\n // RLS could not be turned on at all. The schema-wide grant to the\n // user role has already run by this point, so without the revoke\n // below the table is readable and writable by every authenticated\n // request with no row filtering — the one state this boot path must\n // never leave behind, and the one it used to describe as \"locked\".\n const unrevoked = outcome.unsecured.filter(u => !u.grantWithdrawn);\n for (const u of outcome.unsecured.filter(u => u.grantWithdrawn)) {\n logger.error(\n `🔐 [rls] Could not enable row-level security on \"${u.table}\": ${u.error}. ` +\n `Its privileges have been revoked from ${REBASE_USER_ROLE}, so the table is ` +\n \"unreachable rather than unprotected. Reads and writes to that collection will \" +\n \"fail until RLS can be enabled.\"\n );\n }\n if (unrevoked.length > 0) {\n // Neither securable nor closable. Serving here would mean\n // handing every authenticated caller unfiltered access, so the\n // boot fails instead — this is the one failure that is not\n // survivable per-table.\n throw new Error(\n \"Refusing to start: row-level security could not be enabled on \" +\n unrevoked.map(u => `\"${u.table}\" (${u.error})`).join(\", \") +\n `, and the privileges granted to ${REBASE_USER_ROLE} could not be revoked either. ` +\n \"The table would be served with no row filtering. Fix the database permissions \" +\n \"(the connection role must own these tables, or be able to ALTER them) and boot again.\"\n );\n }\n\n // Retire the pre-1.0 `auth` schema now that the policies above no\n // longer call into it. Deliberately after, and deliberately quiet:\n // Postgres refuses to drop a function an RLS policy still\n // references, so on a database where some table has not been\n // recompiled yet this is expected to fail and succeed on a later\n // boot. See DROP_LEGACY_AUTH_SCHEMA_SQL for the guards that keep it\n // off a Supabase `auth` schema.\n try {\n const { dropLegacyAuthSchema } = await import(\"./schema/rls-bootstrap-sql\");\n await dropLegacyAuthSchema(\n async (text) => (await queryable.query<Record<string, unknown>>(text)).rows,\n { info: (m) => logger.info(m), warn: (m) => logger.warn(m) }\n );\n } catch (err) {\n logger.info(\n \"Left the legacy `auth` schema in place: \" +\n (err instanceof Error ? err.message : String(err))\n );\n }\n\n return { applied: outcome.policiesApplied };\n },\n\n /**\n * Read what the last provisioning boot recorded, or `null`.\n *\n * The meta schema is `rebase` rather than the auth stamp's — see\n * `schema/collections-schema-version.ts` for why the two can differ.\n */\n async readCollectionsSchemaVersion(driverResult?: InitializedDriver): Promise<string | null> {\n const db = provisioningDb(driverResult);\n if (!db) return null;\n const { readCollectionsSchemaVersion } = await import(\"./schema/collections-schema-version\");\n return readCollectionsSchemaVersion(db as never, SCHEMA_META_SCHEMA);\n },\n\n /** Record what this process just applied. Only the provisioning process calls this. */\n async stampCollectionsSchemaVersion(version: string, driverResult?: InitializedDriver): Promise<void> {\n const db = provisioningDb(driverResult);\n if (!db) return;\n const { stampCollectionsSchemaVersion } = await import(\"./schema/collections-schema-version\");\n await stampCollectionsSchemaVersion(db as never, SCHEMA_META_SCHEMA, version);\n },\n\n getAdmin(driverResult: InitializedDriver): DatabaseAdmin | undefined {\n const internals = driverResult.internals as PostgresDriverInternals;\n return internals.driver.admin;\n },\n\n mountRoutes(app: unknown, basePath: string, driverResult: InitializedDriver): void {\n // The coordinator handles auth/storage/data routes.\n // This hook is for driver-specific extensions only.\n // Currently Postgres doesn't need additional routes beyond what the coordinator mounts.\n },\n\n async initializeWebsockets(server: unknown, realtimeService: RealtimeProvider, driver: DataDriver, config?: unknown, adapter?: unknown): Promise<void> {\n const { createPostgresWebSocket } = await import(\"./websocket\");\n createPostgresWebSocket(\n server as import(\"http\").Server,\n realtimeService as RealtimeService,\n driver as PostgresBackendDriver,\n config as { requireAuth?: boolean; jwtSecret?: string; serviceKey?: string },\n adapter as AuthAdapter | undefined\n );\n }\n };\n}\n","import { DatabaseAdapter, InitializedDriver, RealtimeProvider, DataDriver, DatabaseAdmin, BootstrappedAuth } from \"@rebasepro/types\";\nimport { createPostgresBootstrapper } from \"./PostgresBootstrapper\";\nimport type { PostgresDriverConfig } from \"./PostgresBootstrapper\";\n\n/**\n * Creates a Postgres database adapter for Rebase.\n */\nexport function createPostgresAdapter(pgConfig: PostgresDriverConfig): DatabaseAdapter {\n const bootstrapper = createPostgresBootstrapper(pgConfig);\n\n return {\n type: bootstrapper.type,\n\n async initializeDriver(config) {\n return bootstrapper.initializeDriver(config);\n },\n\n async initializeRealtime(driverResult) {\n if (bootstrapper.initializeRealtime) {\n return bootstrapper.initializeRealtime({}, driverResult);\n }\n return undefined;\n },\n\n async initializeAuth(config, driverResult) {\n if (bootstrapper.initializeAuth) {\n return bootstrapper.initializeAuth(config, driverResult);\n }\n return undefined;\n },\n\n async initializeHistory(config, driverResult) {\n if (bootstrapper.initializeHistory) {\n return bootstrapper.initializeHistory(config, driverResult);\n }\n return undefined;\n },\n\n // `adapter` is forwarded for the same reason the schema hooks below are:\n // dropping an argument here is invisible at the call site and silent at\n // runtime. This one decided whether the realtime socket authenticates at\n // all — without it, a server whose auth comes from an AuthAdapter fell\n // back to \"is a jwtSecret set?\", answered no, and admitted every client.\n initializeWebsockets(server, realtimeService, driver, config, adapter) {\n if (bootstrapper.initializeWebsockets) {\n return bootstrapper.initializeWebsockets(server, realtimeService, driver, config, adapter);\n }\n },\n\n // Forwarded so the boot-time schema/RLS provisioning is reachable when\n // this adapter is wrapped back into a bootstrapper. Omitting either left\n // a managed tenant with no tables (500 on every data route) or tables\n // but no policies (401 on every read) — the create step never ran.\n ensureCollectionSchema: bootstrapper.ensureCollectionSchema\n ? (collections, driverResult, log) =>\n bootstrapper.ensureCollectionSchema!(collections, driverResult, log)\n : undefined,\n\n ensureCollectionPolicies: bootstrapper.ensureCollectionPolicies\n ? (collections, driverResult, log) =>\n bootstrapper.ensureCollectionPolicies!(collections, driverResult, log)\n : undefined,\n\n // Same forwarding rule, third instance. Dropping these is not a type\n // error and not a runtime error: the stamp is simply never written and\n // never read, so a split deployment loses the only thing that would tell\n // it a unit is serving against a schema it was not built for — and a\n // check that is off looks exactly like a check that passed. This one was\n // in fact dropped on the first attempt, and the e2e caught it.\n readCollectionsSchemaVersion: bootstrapper.readCollectionsSchemaVersion\n ? (driverResult) => bootstrapper.readCollectionsSchemaVersion!(driverResult)\n : undefined,\n\n stampCollectionsSchemaVersion: bootstrapper.stampCollectionsSchemaVersion\n ? (version, driverResult) => bootstrapper.stampCollectionsSchemaVersion!(version, driverResult)\n : undefined,\n\n getAdmin(driverResult) {\n if (bootstrapper.getAdmin) {\n return bootstrapper.getAdmin(driverResult);\n }\n return undefined;\n },\n\n mountRoutes(app, basePath, driverResult) {\n if (bootstrapper.mountRoutes) {\n bootstrapper.mountRoutes(app, basePath, driverResult);\n }\n }\n };\n}\n"],"mappings":"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"}