@rebasepro/server-postgres 0.12.0 → 0.12.1-canary.gdfba2a1

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket-BKcGvILX.js","names":[],"sources":["../../types/src/types/backend.ts","../src/websocket.ts"],"sourcesContent":["import type { CollectionConfig, FilterValues, WhereFilterOp } from \"./collections\";\nimport type { AuthAdapter } from \"./auth_adapter\";\nimport type { HistoryConfig } from \"../controllers/client\";\nimport type { ChannelBusSetting } from \"./channel_bus\";\n\n// =============================================================================\n// DATABASE CONNECTION INTERFACES\n// =============================================================================\n\n/**\n * Abstract database connection interface.\n * Represents a connection to any database system.\n */\nexport interface DatabaseConnection {\n /**\n * Type identifier for this database (e.g., 'postgres', 'mongodb', 'mysql')\n */\n readonly type: string;\n\n /**\n * Whether the connection is currently active\n */\n readonly isConnected?: boolean;\n\n /**\n * Close the database connection and release resources.\n */\n close?(): Promise<void>;\n}\n\n// =============================================================================\n// QUERY BUILDING INTERFACES\n// =============================================================================\n\n/**\n * A single filter condition for database queries\n */\nexport interface QueryFilter {\n field: string;\n operator: WhereFilterOp;\n value: unknown;\n}\n\n/**\n * Options for fetching a collection of entities\n */\nexport interface FetchCollectionOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n filter?: FilterValues<Extract<keyof M, string>>;\n orderBy?: string;\n order?: \"desc\" | \"asc\";\n limit?: number;\n offset?: number;\n startAfter?: unknown;\n searchString?: string;\n databaseId?: string;\n collection?: CollectionConfig;\n}\n\n/**\n * Options for searching entities\n */\nexport interface SearchOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n filter?: FilterValues<Extract<keyof M, string>>;\n orderBy?: string;\n order?: \"desc\" | \"asc\";\n limit?: number;\n databaseId?: string;\n collection?: CollectionConfig;\n}\n\n/**\n * Options for counting entities\n */\nexport interface CountOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n filter?: FilterValues<Extract<keyof M, string>>;\n searchString?: string;\n databaseId?: string;\n}\n\n/**\n * Abstract condition builder interface.\n * Implementations translate Rebase filter conditions to database-specific queries.\n *\n * Note: This interface can be implemented as instance methods or as a class with static methods.\n * For static implementations (like DrizzleConditionBuilder), use the ConditionBuilderStatic type.\n *\n * @template T The type of condition returned by the builder (e.g., SQL for PostgreSQL, Filter<Document> for MongoDB)\n */\nexport interface ConditionBuilder<T = unknown> {\n /**\n * Build filter conditions from Rebase FilterValues\n */\n buildFilterConditions<M extends Record<string, unknown>>(\n filter: FilterValues<Extract<keyof M, string>>,\n collectionPath: string,\n ...args: unknown[]\n ): T[];\n\n /**\n * Build search conditions for text search\n */\n buildSearchConditions(\n searchString: string,\n properties: Record<string, unknown>,\n ...args: unknown[]\n ): T[];\n\n /**\n * Combine multiple conditions with AND operator\n */\n combineConditionsWithAnd(conditions: T[]): T | undefined;\n\n /**\n * Combine multiple conditions with OR operator\n */\n combineConditionsWithOr(conditions: T[]): T | undefined;\n}\n\n/**\n * Static condition builder type for implementations using static methods.\n * Use this type when the class provides static methods rather than instance methods.\n *\n * @example\n * // DrizzleConditionBuilder satisfies this type\n * const builder: ConditionBuilderStatic<SQL> = DrizzleConditionBuilder;\n */\nexport type ConditionBuilderStatic<T = unknown> = {\n buildFilterConditions<M extends Record<string, unknown>>(\n filter: FilterValues<Extract<keyof M, string>>,\n ...args: unknown[]\n ): T[];\n buildSearchConditions(\n searchString: string,\n properties: Record<string, unknown>,\n ...args: unknown[]\n ): T[];\n combineConditionsWithAnd(conditions: T[]): T | undefined;\n combineConditionsWithOr(conditions: T[]): T | undefined;\n};\n\n// =============================================================================\n// ENTITY REPOSITORY INTERFACES\n// =============================================================================\n\n/**\n * Abstract entity repository interface.\n * Handles all CRUD operations for entities in the database.\n *\n * Implementations should handle:\n * - Entity serialization/deserialization\n * - Relation resolution\n * - ID generation and conversion\n */\nexport interface DataRepository {\n /**\n * Fetch a single entity by ID\n */\n fetchOne<M extends Record<string, unknown>>(\n collectionPath: string,\n id: string | number,\n databaseId?: string\n ): Promise<Record<string, unknown> | undefined>;\n\n /**\n * Fetch a collection of entities with optional filtering, ordering, and pagination\n */\n fetchCollection<M extends Record<string, unknown>>(\n collectionPath: string,\n options?: FetchCollectionOptions<M>\n ): Promise<Record<string, unknown>[]>;\n\n /**\n * Search entities by text\n */\n searchRows<M extends Record<string, unknown>>(\n collectionPath: string,\n searchString: string,\n options?: SearchOptions<M>\n ): Promise<Record<string, unknown>[]>;\n\n /**\n * Count entities in a collection\n */\n count<M extends Record<string, unknown>>(\n collectionPath: string,\n options?: CountOptions<M>\n ): Promise<number>;\n\n /**\n * Save a entity (create or update)\n */\n save<M extends Record<string, unknown>>(\n collectionPath: string,\n values: Partial<M>,\n id?: string | number,\n databaseId?: string\n ): Promise<Record<string, unknown>>;\n\n /**\n * Delete a entity by ID\n */\n delete(\n collectionPath: string,\n id: string | number,\n databaseId?: string\n ): Promise<void>;\n\n /**\n * Check if a field value is unique in a collection\n */\n checkUniqueField(\n collectionPath: string,\n fieldName: string,\n value: unknown,\n excludeEntityId?: string,\n databaseId?: string\n ): Promise<boolean>;\n\n}\n\n// =============================================================================\n// REALTIME INTERFACES\n// =============================================================================\n\n/**\n * Configuration for subscribing to a collection\n */\nexport interface CollectionSubscriptionConfig {\n clientId: string;\n path: string;\n filter?: unknown;\n orderBy?: string;\n order?: \"desc\" | \"asc\";\n limit?: number;\n startAfter?: unknown;\n databaseId?: string;\n searchString?: string;\n}\n\n/**\n * Configuration for subscribing to a single entity\n */\nexport interface SingleSubscriptionConfig {\n clientId: string;\n path: string;\n id: string | number;\n}\n\n/**\n * Opt-in retention for one set of broadcast channels.\n *\n * Retention is configured on the server and nowhere else. A channel is created\n * by whoever names it, so letting a client ask for its own history depth would\n * let any visitor commit the backend to unbounded storage; and presence-only or\n * notification-only channels — the overwhelming majority — must not pay for a\n * feature they never use. With no rules configured nothing is written, no table\n * is created, and broadcast behaves exactly as it did before history existed.\n */\nexport interface ChannelRetentionRule {\n /**\n * Channel name to match. Either exact (`\"doc:42\"`) or a trailing-`*` prefix\n * (`\"doc:*\"`). Deliberately not a full glob or RegExp: this decides what\n * gets written to disk, and a rule whose blast radius is not obvious at a\n * glance is the wrong shape for that.\n */\n match: string;\n /** Keep at most this many of the most recent messages per channel. */\n limit?: number;\n /**\n * Keep messages for at most this long. Accepts a millisecond count or a\n * short duration string (`\"30s\"`, `\"15m\"`, `\"24h\"`, `\"7d\"`).\n */\n ttl?: number | string;\n}\n\n/**\n * Server-side realtime options.\n *\n * The channel bus contract and its config live in `./channel_bus` so that a\n * transport shipped as its own package depends on the contract alone.\n */\nexport interface RealtimeChannelsConfig {\n /**\n * Retention rules, most specific first — the first match wins. Omitted or\n * empty means no channel retains anything.\n */\n channels?: ChannelRetentionRule[];\n /**\n * How channel broadcast and presence reach other backend instances.\n * Defaults to `{ type: \"memory\" }` — i.e. they don't.\n */\n bus?: ChannelBusSetting;\n}\n\n/**\n * Abstract realtime provider interface.\n * Handles real-time subscriptions and notifications for entity changes.\n */\nexport interface RealtimeProvider {\n /**\n * Subscribe to collection changes\n */\n subscribeToCollection(\n subscriptionId: string,\n config: CollectionSubscriptionConfig,\n callback?: (rows: Record<string, unknown>[]) => void\n ): void;\n\n /**\n * Subscribe to single entity changes\n */\n subscribeToOne(\n subscriptionId: string,\n config: SingleSubscriptionConfig,\n callback?: (row: Record<string, unknown> | null) => void\n ): void;\n\n /**\n * Unsubscribe from a subscription\n */\n unsubscribe(subscriptionId: string): void;\n\n /**\n * Notify all relevant subscribers of a entity update\n */\n notifyUpdate(\n path: string,\n id: string,\n row: Record<string, unknown> | null,\n databaseId?: string\n ): Promise<void>;\n\n /**\n * Called when the HTTP server is ready and listening.\n * Useful for providers that need the server address for callbacks.\n */\n onServerReady?(serverInfo: { port: number; hostname?: string }): void;\n\n /**\n * Gracefully shut down the realtime provider.\n * Called during server shutdown to clean up resources.\n */\n destroy?(): Promise<void>;\n\n /**\n * Stop the internal LISTEN client (e.g., PostgreSQL LISTEN/NOTIFY).\n * Called during graceful shutdown before closing database connections.\n */\n stopListening?(): Promise<void>;\n}\n\n// =============================================================================\n// COLLECTION REGISTRY INTERFACES\n// =============================================================================\n\n/**\n * Abstract collection registry interface.\n * Manages registration and lookup of entity collections.\n */\nexport interface CollectionRegistryInterface {\n /**\n * Register a collection\n */\n register(collection: CollectionConfig): void;\n\n /**\n * Get a collection by its path\n */\n getCollectionByPath(path: string): CollectionConfig | undefined;\n\n /**\n * Get all registered collections\n */\n getCollections(): CollectionConfig[];\n\n /**\n * Get the currently registered global callbacks, if any.\n */\n getGlobalCallbacks(): any | undefined;\n}\n\n// =============================================================================\n// DATA TRANSFORMER INTERFACES\n// =============================================================================\n\n/**\n * Abstract data transformer interface.\n * Handles serialization/deserialization between frontend and database formats.\n */\nexport interface DataTransformer {\n /**\n * Transform entity data for storage in the database\n */\n serializeToDatabase<M extends Record<string, unknown>>(\n entity: M,\n collection: CollectionConfig\n ): Record<string, unknown>;\n\n /**\n * Transform database data back to entity format\n */\n deserializeFromDatabase<M extends Record<string, unknown>>(\n data: Record<string, unknown>,\n collection: CollectionConfig\n ): Promise<M>;\n}\n\n// =============================================================================\n// DATABASE ADMIN — CAPABILITY-SPECIFIC INTERFACES (1.3)\n// =============================================================================\n\n/**\n * Administrative operations for SQL-based databases (PostgreSQL, MySQL, etc.).\n * Used by the SQL Editor, RLS Editor, and schema browser.\n *\n * @group Admin\n */\nexport interface SQLAdmin {\n /**\n * Execute raw SQL against the database.\n */\n executeSql(sql: string, options?: { database?: string; role?: string; params?: unknown[] }): Promise<Record<string, unknown>[]>;\n\n /**\n * Fetch the available databases on the server.\n */\n fetchAvailableDatabases?(): Promise<string[]>;\n\n /**\n * Fetch the available *native PostgreSQL* database roles (from `pg_roles`).\n *\n * These are connection-level roles — what the SQL editor can `SET ROLE` to,\n * and what `SecurityRule.pgRoles` targets. They are NOT application roles;\n * for those use {@link fetchApplicationRoles}.\n */\n fetchAvailableRoles?(): Promise<string[]>;\n\n /**\n * Fetch the *application-level* roles in use in this project.\n *\n * These are the strings stored on the users table's `roles` column and\n * exposed to policies as `auth.roles()` — what `SecurityRule.roles`\n * matches against. Distinct from {@link fetchAvailableRoles}; the two are\n * not interchangeable.\n */\n fetchApplicationRoles?(): Promise<string[]>;\n\n /**\n * Fetch the current database name.\n */\n fetchCurrentDatabase?(): Promise<string | undefined>;\n}\n\n/**\n * Administrative operations for document-based databases (MongoDB, Firestore, etc.).\n * Used by future document administration tools.\n *\n * @group Admin\n */\nexport interface DocumentAdmin {\n /**\n * Execute an aggregation pipeline or equivalent query.\n */\n executeAggregate?(pipeline: Record<string, unknown>[]): Promise<Record<string, unknown>[]>;\n\n /**\n * Fetch statistics for a collection (document count, size, etc.).\n */\n fetchCollectionStats?(collectionName: string): Promise<{ count: number; sizeBytes?: number }>;\n}\n\n/**\n * Administrative operations for schema management.\n * Shared across SQL and document databases.\n *\n * @group Admin\n */\nexport interface SchemaAdmin {\n /**\n * Fetch database tables/collections not yet mapped to a Rebase collection.\n */\n fetchUnmappedTables?(mappedPaths?: string[]): Promise<string[]>;\n\n /**\n * Fetch column/field metadata for a single table/collection.\n * The return type is generic — SQL backends return TableMetadata,\n * document backends may return a different shape.\n */\n fetchTableMetadata?(tableName: string): Promise<unknown>;\n}\n\n/**\n * Metadata for a database branch.\n * @group Admin\n */\nexport interface BranchInfo {\n /** Branch name (without prefix). */\n name: string;\n /** The database this branch was created from. */\n parentDatabase: string;\n /** When the branch was created. */\n createdAt: Date;\n /** Size in bytes, if available from the server. */\n sizeBytes?: number;\n}\n\n/**\n * Administrative operations for database branching.\n * Allows creating isolated database copies for development/preview workflows.\n *\n * @group Admin\n */\nexport interface BranchAdmin {\n /** Create a new branch (database copy) from the current or specified source database. */\n createBranch(name: string, options?: { source?: string }): Promise<BranchInfo>;\n\n /** Delete a branch database. Cannot delete the main/default database. */\n deleteBranch(name: string): Promise<void>;\n\n /** List all branches (databases that were created via branching). */\n listBranches(): Promise<BranchInfo[]>;\n\n /** Get info about a specific branch. */\n getBranchInfo(name: string): Promise<BranchInfo | undefined>;\n}\n\n/**\n * Union type for all admin capabilities.\n * A backend may implement any combination of these interfaces.\n *\n * Use type guards (`isSQLAdmin`, `isDocumentAdmin`, `isSchemaAdmin`, `isBranchAdmin`)\n * to safely narrow the type before calling methods.\n *\n * @group Admin\n */\nexport type DatabaseAdmin = Partial<SQLAdmin> & Partial<DocumentAdmin> & Partial<SchemaAdmin> & Partial<BranchAdmin>;\n\n/**\n * Type guard: does this admin support SQL operations?\n * @group Admin\n */\nexport function isSQLAdmin(admin: DatabaseAdmin | undefined): admin is SQLAdmin {\n return !!admin && typeof (admin as SQLAdmin).executeSql === \"function\";\n}\n\n/**\n * Type guard: does this admin support document operations?\n * @group Admin\n */\nexport function isDocumentAdmin(admin: DatabaseAdmin | undefined): admin is DocumentAdmin {\n return !!admin && (\n typeof (admin as DocumentAdmin).executeAggregate === \"function\" ||\n typeof (admin as DocumentAdmin).fetchCollectionStats === \"function\"\n );\n}\n\n/**\n * Type guard: does this admin support schema management?\n * @group Admin\n */\nexport function isSchemaAdmin(admin: DatabaseAdmin | undefined): admin is SchemaAdmin {\n return !!admin && (\n typeof (admin as SchemaAdmin).fetchUnmappedTables === \"function\" ||\n typeof (admin as SchemaAdmin).fetchTableMetadata === \"function\"\n );\n}\n\n/**\n * Type guard: does this admin support database branching?\n * @group Admin\n */\nexport function isBranchAdmin(admin: DatabaseAdmin | undefined): admin is BranchAdmin {\n return !!admin && typeof (admin as BranchAdmin).createBranch === \"function\";\n}\n\n// =============================================================================\n// LIFECYCLE INTERFACES (1.4)\n// =============================================================================\n\n/**\n * Health check result returned by `healthCheck()`.\n * @group Lifecycle\n */\nexport interface HealthCheckResult {\n /** Whether the backend is healthy and able to serve requests. */\n healthy: boolean;\n /** Round-trip latency to the database in milliseconds. */\n latencyMs: number;\n /** Optional details (e.g., pool stats, replication lag). */\n details?: Record<string, unknown>;\n}\n\n/**\n * Lifecycle contract for backend components that hold resources\n * (database connections, WebSocket pools, timers, etc.).\n *\n * All methods are optional — simple backends (e.g., in-memory) can skip them.\n * @group Lifecycle\n */\nexport interface BackendLifecycle {\n /**\n * Initialize the backend: open connections, run migrations, seed data.\n * Called once during startup. Idempotent.\n */\n initialize?(): Promise<void>;\n\n /**\n * Check whether the backend is healthy and reachable.\n * Should be fast (< 1 s) and safe to call frequently.\n */\n healthCheck?(): Promise<HealthCheckResult>;\n\n /**\n * Gracefully shut down: close connections, flush buffers, cancel timers.\n * After calling `destroy()`, no other methods should be called.\n */\n destroy?(): Promise<void>;\n}\n\n// =============================================================================\n// BACKEND FACTORY INTERFACES\n// =============================================================================\n\n/**\n * Configuration for creating a database backend\n */\nexport interface BackendConfig {\n /**\n * Type of database backend\n */\n type: string;\n\n /**\n * Database connection (implementation-specific)\n */\n connection: unknown;\n\n /**\n * Schema definition (implementation-specific, e.g., Drizzle schema for PostgreSQL)\n */\n schema?: unknown;\n}\n\n/**\n * A complete backend instance with all required services.\n *\n * Now includes optional lifecycle management and admin capabilities.\n */\nexport interface BackendInstance extends BackendLifecycle {\n /**\n * Entity repository for CRUD operations\n */\n entityRepository: DataRepository;\n\n /**\n * Realtime provider for subscriptions\n */\n realtimeProvider: RealtimeProvider;\n\n /**\n * Collection registry\n */\n collectionRegistry: CollectionRegistryInterface;\n\n /**\n * The underlying database connection\n */\n connection: DatabaseConnection;\n\n /**\n * Administrative operations (SQL, schema, documents).\n * What's available depends on the backend type — use type guards\n * (`isSQLAdmin`, `isSchemaAdmin`, etc.) to narrow.\n */\n admin?: DatabaseAdmin;\n}\n\n/**\n * Factory function type for creating backend instances\n */\nexport type BackendFactory<TConfig extends BackendConfig = BackendConfig> =\n (config: TConfig) => BackendInstance;\n\n// =============================================================================\n// BACKEND BOOTSTRAPPER (1.2)\n// =============================================================================\n\n/**\n * A `BackendBootstrapper` encapsulates all driver-specific initialization logic.\n *\n * Instead of hard-coding Postgres setup into `initializeRebaseBackend()`,\n * each database backend provides its own bootstrapper that knows how to:\n * - Create the DataDriver from a config object\n * - Optionally initialize auth tables\n * - Optionally create a realtime service\n * - Mount driver-specific API routes\n *\n * The main `initializeRebaseBackend()` becomes a **coordinator** that iterates\n * registered bootstrappers, calls their hooks, and wires the results together.\n *\n * @group Backend\n *\n * @example\n * ```typescript\n * // Third-party MySQL bootstrapper\n * const mysqlBootstrapper: BackendBootstrapper = {\n * type: \"mysql\",\n * initializeDriver: async (config) => new MySQLDataDriver(config.connection),\n * initializeRealtime: async (config) => new MySQLChangeStreamRealtime(config.connection),\n * };\n *\n * initializeRebaseBackend({\n * ...config,\n * bootstrappers: [postgresBootstrapper, mysqlBootstrapper]\n * });\n * ```\n */\nexport interface BackendBootstrapper {\n /**\n * Which driver type this bootstrapper handles.\n * Must match the `type` field on the driver config object\n * (e.g., `\"postgres\"`, `\"mongodb\"`, `\"mysql\"`).\n */\n type: string;\n\n /**\n * Unique identifier for this bootstrapper instance.\n * Used to register the driver in the driver registry.\n * Defaults to `type` if not set.\n */\n id?: string;\n\n /**\n * Whether this bootstrapper provides the default driver.\n * When true, the coordinator uses this driver as the primary one.\n */\n isDefault?: boolean;\n\n /**\n * Run database migrations for this driver.\n * Called by the coordinator after all drivers are initialized.\n */\n runMigrations?(config: unknown, driverResult: InitializedDriver): Promise<void>;\n\n /**\n * Create a DataDriver from the given config.\n * This is the only **required** method.\n */\n initializeDriver(config: unknown): Promise<InitializedDriver>;\n\n /**\n * Initialize auth tables / services if this driver supports them.\n * Return undefined if auth is not supported by this backend.\n */\n initializeAuth?(config: unknown, driverResult: InitializedDriver): Promise<BootstrappedAuth | undefined>;\n\n /**\n * Initialize history tables / services if this driver supports them.\n * Return undefined if history is not supported by this backend.\n */\n initializeHistory?(config: HistoryConfig, driverResult: InitializedDriver): Promise<{ historyService: unknown } | undefined>;\n\n /**\n * Create a realtime provider for this driver.\n * Return undefined if the driver does not support realtime.\n */\n initializeRealtime?(config: unknown, driverResult: InitializedDriver): Promise<RealtimeProvider | undefined>;\n\n /**\n * Mount any driver-specific HTTP routes (e.g., custom admin endpoints).\n * Called after all drivers are initialized.\n */\n mountRoutes?(app: unknown, basePath: string, driverResult: InitializedDriver): void;\n\n /**\n * Return admin capabilities for this driver.\n */\n getAdmin?(driverResult: InitializedDriver): DatabaseAdmin | undefined;\n\n /**\n * Bring the database's collection tables up to date, additively.\n *\n * Optional because it is only meaningful for schema-ful drivers. A managed\n * runtime boots a compiled project against a database it has never seen; auth\n * tables are ensured on boot but collection tables were created by nothing,\n * so every data request answered 500 on a missing relation. The CLI's `db\n * push` cannot fill the gap — it needs Atlas, and the runtime image ships no\n * CLI.\n *\n * Implementations MUST be additive-only: create missing tables, columns and\n * enum types, and never drop, narrow or rewrite anything. This runs\n * unattended against live customer data with nobody reading a diff, so the\n * destructive half stays a deliberate migration.\n */\n ensureCollectionSchema?(\n collections: unknown[],\n driverResult: InitializedDriver,\n log?: (message: string) => void\n ): Promise<{ applied: number }>;\n\n /**\n * Initialize WebSocket server for realtime operations.\n */\n initializeWebsockets?(server: unknown, realtimeService: RealtimeProvider, driver: import(\"../controllers/data_driver\").DataDriver, config?: unknown, authAdapter?: AuthAdapter): Promise<void> | void;\n}\n\n/**\n * Result of `BackendBootstrapper.initializeDriver()`.\n * @group Backend\n */\nexport interface InitializedDriver {\n /** The DataDriver instance, ready for use. */\n driver: import(\"../controllers/data_driver\").DataDriver;\n\n /** The realtime service, if the driver created one during init. */\n realtimeProvider?: RealtimeProvider;\n\n /** A collection registry to register schema / tables into. */\n collectionRegistry?: CollectionRegistryInterface;\n\n /**\n * Collections the driver derived from the live database schema.\n *\n * Set by drivers that introspect in `baas` mode; the server serves these\n * instead of collections loaded from config files.\n */\n collections?: import(\"./collections\").CollectionConfig[];\n\n /** The underlying database connection (for lifecycle management). */\n connection?: DatabaseConnection;\n\n /**\n * Opaque handle that the bootstrapper can use in subsequent hooks\n * (e.g., `initializeAuth`, `mountRoutes`) to access driver internals.\n * Not used by the coordinator.\n */\n internals?: unknown;\n}\n\n/**\n * Result of `BackendBootstrapper.initializeAuth()`.\n * @group Backend\n */\nexport interface BootstrappedAuth {\n /** User management service. */\n userService: unknown;\n /** Role management service (optional, roles are now simple strings). */\n roleService?: unknown;\n /** Email service (optional). */\n emailService?: unknown;\n /** Combined Auth Repository for unified token and user management. */\n authRepository?: unknown;\n /**\n * Whether the auth schema in the database is one this runtime can serve.\n *\n * Folded into `healthCheck()` so a schema mismatch shows up as a degraded\n * health response. Without it, a server whose auth is entirely broken still\n * reports healthy — the database connection it probes is fine, and the\n * mismatch is only discovered one failed login at a time.\n */\n schemaHealthCheck?(): Promise<AuthSchemaHealth>;\n}\n\n/**\n * Result of {@link BootstrappedAuth.schemaHealthCheck}.\n * @group Lifecycle\n */\nexport interface AuthSchemaHealth {\n /** False when this runtime cannot be trusted to serve auth against this database. */\n healthy: boolean;\n /** Human-readable descriptions of each mismatch found. Empty when healthy. */\n problems: string[];\n /** Auth schema version recorded in the database, when it records one. */\n databaseVersion?: number | null;\n /** Auth schema version this runtime expects. */\n runtimeVersion?: number;\n}\n","import { RealtimeService } from \"./services/realtimeService\";\nimport { PostgresBackendDriver } from \"./PostgresBackendDriver\";\nimport type { DataDriver, DeleteProps, FetchCollectionProps, FetchOneProps, SaveProps, TableMetadata, BranchInfo, AuthAdapter } from \"@rebasepro/types\";\nimport { isSQLAdmin, isSchemaAdmin } from \"@rebasepro/types\";\nimport type { User } from \"@rebasepro/types\";\n\nimport { WebSocketServer, WebSocket } from \"ws\";\nimport { Server } from \"http\";\nimport { inspect } from \"util\";\nimport { extractUserFromToken, AccessTokenPayload, safeCompare } from \"@rebasepro/server\";\nimport { logger } from \"@rebasepro/server\";\n\n/** Minimal subset of RebaseAuthConfig used by the WebSocket layer. */\ninterface WsAuthConfig {\n requireAuth?: boolean;\n jwtSecret?: string;\n /**\n * Same static server-to-server secret the HTTP middleware accepts. Without\n * it here, a service key authenticates over HTTP but not over the socket —\n * so any SDK client using one (scripts, cron, server-to-server) connects,\n * fails realtime auth with \"jwt malformed\", and silently gets no events.\n */\n serviceKey?: string;\n}\n\n/**\n * Normalized user identity for WebSocket sessions.\n */\ninterface WsUserIdentity {\n uid: string;\n roles: string[];\n isAdmin: boolean;\n}\n\ninterface ClientSession {\n ws: WebSocket;\n user?: WsUserIdentity;\n authenticated: boolean;\n /** Sliding window message counter for rate limiting */\n messageCount: number;\n messageWindowStart: number;\n}\n\n\n/** Maximum messages per client per window */\nconst WS_RATE_LIMIT = 2000;\n/** Rate limit window in milliseconds (60 seconds) */\nconst WS_RATE_WINDOW_MS = 60_000;\n\n/** Admin-only WebSocket message types */\nconst ADMIN_ONLY_TYPES = new Set([\n \"EXECUTE_SQL\",\n \"FETCH_DATABASES\",\n \"FETCH_ROLES\",\n \"FETCH_UNMAPPED_TABLES\",\n \"FETCH_TABLE_METADATA\",\n \"FETCH_CURRENT_DATABASE\",\n \"CREATE_BRANCH\",\n \"DELETE_BRANCH\",\n \"LIST_BRANCHES\"\n]);\n\n/**\n * Recursively extract the deepest error message from an error's cause chain (e.g., Drizzle wrapping a PG error).\n */\nfunction extractErrorMessage(error: unknown): string {\n if (!error) return \"Unknown error\";\n if (error instanceof Error) {\n if (\"cause\" in error && error.cause) {\n return extractErrorMessage(error.cause);\n }\n return error.message;\n }\n if (typeof error === \"object\" && \"message\" in error && typeof (error as { message: unknown }).message === \"string\") {\n return (error as { message: string }).message;\n }\n return String(error);\n}\n\n/**\n * Check if the current session belongs to an admin user.\n */\nfunction isAdminSession(session: ClientSession | undefined): boolean {\n if (!session?.user) return false;\n // Fast path: new adapter-aware sessions set isAdmin directly\n if (session.user.isAdmin) return true;\n if (!session.user.roles) return false;\n return session.user.roles.some((r) => r === \"admin\");\n}\n\nexport function createPostgresWebSocket(\n server: Server,\n realtimeService: RealtimeService,\n driver: PostgresBackendDriver,\n authConfig?: WsAuthConfig,\n authAdapter?: AuthAdapter\n) {\n // Session map scoped to this factory invocation — prevents stale sessions\n // leaking across hot reloads or multiple factory calls.\n const clientSessions = new Map<string, ClientSession>();\n\n const isProduction = process.env.NODE_ENV === \"production\";\n /** Debug logger that is suppressed in production to prevent PII/data leaks */\n const wsDebug = (...args: unknown[]) => { if (!isProduction) console.debug(...args); };\n const wss = new WebSocketServer({ server });\n\n // Handle errors on the WSS so that EADDRINUSE from the underlying HTTP\n // server doesn't surface as an unhandled 'error' event and crash the\n // process. The dev-mode `listenWithPortRetry` utility handles retry\n // logic on the HTTP server side — we just need the WSS not to throw.\n wss.on(\"error\", (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\") {\n // Silently absorbed — listenWithPortRetry will retry the next port\n return;\n }\n logger.error(\"❌ [WebSocket Server] Error\", { error: err });\n });\n\n // Auth is required when either: an adapter is present (secure by default),\n // OR the config has a jwtSecret and requireAuth !== false.\n const requireAuth = authAdapter\n ? true\n : (authConfig?.requireAuth !== false && !!authConfig?.jwtSecret);\n\n wss.on(\"connection\", (ws) => {\n const clientId = `client_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;\n wsDebug(`WebSocket client connected: ${clientId}`);\n\n // Initialize client session\n clientSessions.set(clientId, { ws,\nauthenticated: !requireAuth,\nmessageCount: 0,\nmessageWindowStart: Date.now() });\n realtimeService.addClient(clientId, ws);\n\n ws.on(\"close\", () => {\n wsDebug(`WebSocket client disconnected: ${clientId}`);\n clientSessions.delete(clientId);\n });\n\n // Route all messages through RealtimeService for unified handling\n ws.on(\"message\", async (message) => {\n let requestId: string | undefined;\n try {\n const {\n type,\n payload,\n requestId: reqId\n } = JSON.parse(message.toString());\n requestId = reqId; // Capture requestId for use in catch block\n\n wsDebug(`[WS] ${clientId} → ${type}`, requestId ? `(${requestId})` : \"\");\n\n // Handle authentication first\n // Helper: send a canonical error frame\n const sendError = (errType: \"ERROR\" | \"AUTH_ERROR\", code: string, msg: string) => {\n ws.send(JSON.stringify({\n type: errType,\n requestId,\n payload: { error: { message: msg,\ncode } }\n }));\n };\n\n if (type === \"AUTHENTICATE\") {\n const { token } = payload || {};\n if (!token) {\n sendError(\"AUTH_ERROR\", \"INVALID_INPUT\", \"Token is required\");\n return;\n }\n\n // Use the auth adapter when available (custom auth, Clerk, etc.)\n // Fall back to JWT extraction otherwise.\n let verifiedUser: WsUserIdentity | null = null;\n\n if (authAdapter) {\n try {\n const adapterUser = authAdapter.verifyToken\n ? await authAdapter.verifyToken(token)\n : await authAdapter.verifyRequest(new Request(\"http://localhost/_ws_auth\", {\n headers: { Authorization: `Bearer ${token}` }\n }));\n\n if (adapterUser) {\n verifiedUser = {\n uid: adapterUser.uid,\n roles: adapterUser.roles,\n isAdmin: adapterUser.isAdmin\n };\n }\n } catch {\n // Adapter threw — treat as invalid token\n }\n } else if (authConfig?.serviceKey && safeCompare(token, authConfig.serviceKey)) {\n // Service key: a static secret, not a JWT. Checked\n // before verification, mirroring the HTTP middleware —\n // verifying it as a JWT can only ever fail.\n verifiedUser = { uid: \"service\", roles: [\"admin\"], isAdmin: true };\n } else {\n // Standard JWT path\n const jwtPayload = extractUserFromToken(token);\n if (jwtPayload) {\n verifiedUser = {\n uid: jwtPayload.uid,\n roles: jwtPayload.roles ?? [],\n isAdmin: (jwtPayload.roles ?? []).some((r: string) => r === \"admin\")\n };\n }\n }\n\n if (verifiedUser) {\n const session = clientSessions.get(clientId);\n if (session) {\n session.user = verifiedUser;\n session.authenticated = true;\n }\n wsDebug(`[WS] replying AUTH_SUCCESS for requestId ${requestId}`);\n ws.send(JSON.stringify({\n type: \"AUTH_SUCCESS\",\n requestId,\n payload: { uid: verifiedUser.uid,\nroles: verifiedUser.roles }\n }));\n wsDebug(`🔐 [WebSocket Server] Client ${clientId} authenticated as ${verifiedUser.uid}`);\n } else {\n wsDebug(`[WS] replying AUTH_ERROR for requestId ${requestId} (invalid token)`);\n sendError(\"AUTH_ERROR\", \"INVALID_TOKEN\", \"Invalid or expired token\");\n }\n return;\n }\n\n // Check authentication for protected operations\n if (requireAuth) {\n const session = clientSessions.get(clientId);\n if (!session?.authenticated) {\n sendError(\"ERROR\", \"UNAUTHORIZED\", \"Authentication required\");\n return;\n }\n }\n\n // Rate limiting: reject if client exceeds message limit\n {\n const session = clientSessions.get(clientId);\n if (session) {\n const now = Date.now();\n if (now - session.messageWindowStart > WS_RATE_WINDOW_MS) {\n session.messageCount = 0;\n session.messageWindowStart = now;\n }\n session.messageCount++;\n if (session.messageCount > WS_RATE_LIMIT) {\n sendError(\"ERROR\", \"RATE_LIMITED\", \"Too many requests. Please slow down.\");\n return;\n }\n }\n }\n\n // Admin-only operations require admin role\n if (ADMIN_ONLY_TYPES.has(type)) {\n const session = clientSessions.get(clientId);\n if (!isAdminSession(session)) {\n sendError(\"ERROR\", \"FORBIDDEN\", \"Admin access required for this operation\");\n return;\n }\n }\n\n // Helper to get correctly scoped delegate for the current request\n const getScopedDelegate = async (): Promise<DataDriver> => {\n const session = clientSessions.get(clientId);\n // Check if the driver supports RLS-scoped delegates\n if (typeof driver.withAuth === \"function\") {\n try {\n const userForAuth: User = session?.user\n ? {\n uid: session.user.uid,\n displayName: null,\n email: null,\n photoURL: null,\n providerId: \"websocket\",\n isAnonymous: false,\n roles: session.user.roles ?? []\n }\n : {\n uid: \"anon\",\n displayName: null,\n email: null,\n photoURL: null,\n providerId: \"websocket\",\n isAnonymous: true,\n roles: [\"anon\"]\n };\n return await driver.withAuth(userForAuth);\n } catch (e) {\n logger.error(\"Failed to create RLS scoped delegate for WS request\", { error: e });\n throw new Error(\"Internal authentication error\");\n }\n }\n return driver;\n };\n\n switch (type) {\n case \"FETCH_COLLECTION\": {\n wsDebug(\"📋 [WebSocket Server] Processing FETCH_COLLECTION request\");\n const request: FetchCollectionProps = payload;\n const delegate = await getScopedDelegate();\n const rows = await delegate.fetchCollection(request);\n wsDebug(\"📋 [WebSocket Server] FETCH_COLLECTION result - rows count:\", rows.length);\n const response = {\n type: \"FETCH_COLLECTION_SUCCESS\",\n payload: { rows },\n requestId\n };\n wsDebug(\"📋 [WebSocket Server] Sending FETCH_COLLECTION_SUCCESS response\");\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"FETCH_ONE\": {\n wsDebug(\"📄 [WebSocket Server] Processing FETCH_ENTITY request\");\n const request: FetchOneProps = payload;\n const delegate = await getScopedDelegate();\n const row = await delegate.fetchOne(request);\n wsDebug(\"📄 [WebSocket Server] FETCH_ENTITY result:\", row);\n const response = {\n type: \"FETCH_ONE_SUCCESS\",\n payload: { row: row ?? null },\n requestId\n };\n wsDebug(\"📄 [WebSocket Server] Sending FETCH_ENTITY_SUCCESS response\");\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"SAVE\": {\n wsDebug(\"💾 [WebSocket Server] Processing SAVE_ENTITY request\");\n const request: SaveProps = payload;\n wsDebug(\"💾 [WebSocket Server] Saving row with request:\", inspect(request, { depth: null,\ncolors: true }));\n const delegate = await getScopedDelegate();\n const row = await delegate.save(request);\n wsDebug(\"💾 [WebSocket Server] SAVE_ENTITY result:\", inspect(row, { depth: null,\ncolors: true }));\n const response = {\n type: \"SAVE_SUCCESS\",\n payload: { row },\n requestId\n };\n wsDebug(\"💾 [WebSocket Server] Sending SAVE_ENTITY_SUCCESS response\");\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"DELETE\": {\n wsDebug(\"🗑️ [WebSocket Server] Processing DELETE_ENTITY request\");\n const request: DeleteProps = payload;\n wsDebug(\"🗑️ [WebSocket Server] Deleting row:\", request.row);\n const delegate = await getScopedDelegate();\n await delegate.delete(request);\n wsDebug(\"🗑️ [WebSocket Server] DELETE_ENTITY completed successfully\");\n const response = {\n type: \"DELETE_SUCCESS\",\n payload: { success: true },\n requestId\n };\n wsDebug(\"🗑️ [WebSocket Server] Sending DELETE_ENTITY_SUCCESS response\");\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"CHECK_UNIQUE_FIELD\": {\n wsDebug(\"🔍 [WebSocket Server] Processing CHECK_UNIQUE_FIELD request\");\n const {\n path,\n name,\n value,\n id,\n collection\n } = payload;\n const delegate = await getScopedDelegate();\n const isUnique = await delegate.checkUniqueField(path, name, value, id, collection);\n wsDebug(\"🔍 [WebSocket Server] CHECK_UNIQUE_FIELD result:\", isUnique);\n const response = {\n type: \"CHECK_UNIQUE_FIELD_SUCCESS\",\n payload: { isUnique },\n requestId\n };\n wsDebug(\"🔍 [WebSocket Server] Sending CHECK_UNIQUE_FIELD_SUCCESS response\");\n ws.send(JSON.stringify(response));\n }\n break;\n\n\n case \"COUNT\": {\n const request: FetchCollectionProps = payload;\n const delegate = await getScopedDelegate();\n const count = await delegate.count!(request);\n const response = {\n type: \"COUNT_SUCCESS\",\n payload: { count },\n requestId\n };\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"EXECUTE_SQL\": {\n const { sql, options } = payload;\n try {\n const delegate = await getScopedDelegate();\n const admin = delegate.admin;\n if (!isSQLAdmin(admin)) {\n sendError(\"ERROR\", \"NOT_SUPPORTED\", \"SQL execution is not available for this driver.\");\n break;\n }\n const result = await admin.executeSql(sql, options);\n if (process.env.NODE_ENV !== \"production\") {\n wsDebug(`⚡ [WebSocket Server] SQL executed. Returned ${Array.isArray(result) ? result.length : \"non-array\"} rows.`);\n }\n const auditSession = clientSessions.get(clientId);\n console.log(\"[SQL Audit] WebSocket SQL execution\", JSON.stringify({\n sql: typeof sql === \"string\" ? sql.substring(0, 500) : sql,\n options,\n resultRows: Array.isArray(result) ? result.length : \"unknown\",\n uid: auditSession?.user?.uid ?? \"unknown\",\n roles: auditSession?.user?.roles ?? [],\n isAdmin: auditSession?.user?.isAdmin ?? false,\n }));\n const response = {\n type: \"EXECUTE_SQL_SUCCESS\",\n payload: { result },\n requestId\n };\n ws.send(JSON.stringify(response));\n } catch (sqlError: unknown) {\n // This is a query execution error (e.g., syntax error, permission denied).\n // We return it cleanly to the client without logging a server stack trace.\n const errMsg = extractErrorMessage(sqlError);\n sendError(\"ERROR\", \"SQL_ERROR\", errMsg);\n }\n }\n break;\n\n case \"FETCH_DATABASES\": {\n wsDebug(\"📚 [WebSocket Server] Processing FETCH_DATABASES request\");\n const delegate = await getScopedDelegate();\n const admin = delegate.admin;\n let databases: string[] = [];\n if (isSQLAdmin(admin) && admin.fetchAvailableDatabases) {\n databases = await admin.fetchAvailableDatabases();\n }\n wsDebug(`📚 [WebSocket Server] Fetched ${databases.length} databases.`);\n const response = {\n type: \"FETCH_DATABASES_SUCCESS\",\n payload: { databases },\n requestId\n };\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"FETCH_ROLES\": {\n wsDebug(\"👤 [WebSocket Server] Processing FETCH_ROLES request\");\n const delegate = await getScopedDelegate();\n const admin = delegate.admin;\n let roles: string[] = [];\n if (isSQLAdmin(admin) && admin.fetchAvailableRoles) {\n roles = await admin.fetchAvailableRoles();\n }\n wsDebug(`👤 [WebSocket Server] Fetched ${roles.length} roles.`);\n const response = {\n type: \"FETCH_ROLES_SUCCESS\",\n payload: { roles },\n requestId\n };\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"FETCH_APPLICATION_ROLES\": {\n wsDebug(\"👤 [WebSocket Server] Processing FETCH_APPLICATION_ROLES request\");\n const delegate = await getScopedDelegate();\n const admin = delegate.admin;\n let roles: string[] = [];\n if (isSQLAdmin(admin) && admin.fetchApplicationRoles) {\n roles = await admin.fetchApplicationRoles();\n }\n wsDebug(`👤 [WebSocket Server] Fetched ${roles.length} application roles.`);\n const response = {\n type: \"FETCH_APPLICATION_ROLES_SUCCESS\",\n payload: { roles },\n requestId\n };\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"FETCH_CURRENT_DATABASE\": {\n wsDebug(\"📚 [WebSocket Server] Processing FETCH_CURRENT_DATABASE request\");\n const delegate = await getScopedDelegate();\n const admin = delegate.admin;\n let database: string | undefined = undefined;\n if (isSQLAdmin(admin) && admin.fetchCurrentDatabase) {\n database = await admin.fetchCurrentDatabase();\n }\n const response = {\n type: \"FETCH_CURRENT_DATABASE_SUCCESS\",\n payload: { database },\n requestId\n };\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"FETCH_UNMAPPED_TABLES\": {\n wsDebug(\"📋 [WebSocket Server] Processing FETCH_UNMAPPED_TABLES request\");\n const delegate = await getScopedDelegate();\n const admin = delegate.admin;\n let tables: string[] = [];\n if (isSchemaAdmin(admin) && admin.fetchUnmappedTables) {\n tables = await admin.fetchUnmappedTables(payload?.mappedPaths);\n }\n wsDebug(`📋 [WebSocket Server] Fetched ${tables.length} unmapped tables.`);\n const response = {\n type: \"FETCH_UNMAPPED_TABLES_SUCCESS\",\n payload: { tables },\n requestId\n };\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"FETCH_TABLE_METADATA\": {\n wsDebug(\"📋 [WebSocket Server] Processing FETCH_TABLE_METADATA request\");\n const { tableName } = payload;\n const delegate = await getScopedDelegate();\n const admin = delegate.admin;\n let metadata: TableMetadata | undefined;\n if (isSchemaAdmin(admin) && admin.fetchTableMetadata) {\n metadata = await admin.fetchTableMetadata(tableName) as TableMetadata;\n }\n wsDebug(`📋 [WebSocket Server] Fetched metadata for table '${tableName}'. (${metadata?.columns?.length ?? 0} columns)`);\n const response = {\n type: \"FETCH_TABLE_METADATA_SUCCESS\",\n payload: { metadata },\n requestId\n };\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"CREATE_BRANCH\": {\n wsDebug(\"🌿 [WebSocket Server] Processing CREATE_BRANCH request\");\n const { name, options } = payload;\n const delegate = await getScopedDelegate();\n if (!delegate.admin?.createBranch) {\n sendError(\"ERROR\", \"NOT_SUPPORTED\", \"Database branching is not available. Configure adminConnectionString.\");\n break;\n }\n const branch: BranchInfo = await delegate.admin.createBranch(name, options);\n wsDebug(`🌿 [WebSocket Server] Branch created: ${branch.name}`);\n const response = {\n type: \"CREATE_BRANCH_SUCCESS\",\n payload: { branch },\n requestId\n };\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"DELETE_BRANCH\": {\n wsDebug(\"🗑️ [WebSocket Server] Processing DELETE_BRANCH request\");\n const { name: branchName } = payload;\n const delegate = await getScopedDelegate();\n if (!delegate.admin?.deleteBranch) {\n sendError(\"ERROR\", \"NOT_SUPPORTED\", \"Database branching is not available.\");\n break;\n }\n await delegate.admin.deleteBranch(branchName);\n wsDebug(`🗑️ [WebSocket Server] Branch deleted: ${branchName}`);\n const response = {\n type: \"DELETE_BRANCH_SUCCESS\",\n payload: { success: true },\n requestId\n };\n ws.send(JSON.stringify(response));\n }\n break;\n\n case \"LIST_BRANCHES\": {\n wsDebug(\"🌿 [WebSocket Server] Processing LIST_BRANCHES request\");\n const delegate = await getScopedDelegate();\n let branches: BranchInfo[] = [];\n if (delegate.admin?.listBranches) {\n branches = await delegate.admin.listBranches();\n }\n wsDebug(`🌿 [WebSocket Server] Listed ${branches.length} branches.`);\n const response = {\n type: \"LIST_BRANCHES_SUCCESS\",\n payload: { branches },\n requestId\n };\n ws.send(JSON.stringify(response));\n }\n break;\n\n // Route subscription messages, broadcast channels, and presence to RealtimeService\n case \"subscribe_collection\":\n case \"subscribe_one\":\n case \"unsubscribe\":\n case \"join_channel\":\n case \"leave_channel\":\n case \"broadcast\":\n case \"presence_track\":\n case \"presence_untrack\":\n case \"presence_state\":\n case \"channel_history\": {\n wsDebug(\"🔄 [WebSocket Server] Routing realtime message to RealtimeService:\", type);\n // Attach auth context from the WS session so RLS-aware refetches work\n const session = clientSessions.get(clientId);\n const authContext = session?.user\n ? { uid: session.user.uid,\nroles: session.user.roles ?? [] }\n : { uid: \"anon\",\nroles: [\"anon\"] };\n // Let RealtimeService handle these messages\n await realtimeService.handleClientMessage(clientId, {\n type,\n payload,\n subscriptionId: payload?.subscriptionId\n }, authContext);\n break;\n }\n\n default:\n logger.error(\"❌ [WebSocket Server] Unknown message type\", { detail: type });\n }\n } catch (error: unknown) {\n logger.error(\"💥 [WebSocket Server] Error handling message\", { error: error });\n if (error instanceof Error) {\n logger.error(\"Stack trace\", { detail: error.stack });\n }\n // Unwrap the cause chain: a Drizzle failure reports itself as\n // \"Failed query: <sql> params:\", which tells the user nothing and\n // echoes the statement back at them. The reason is in the cause.\n const errorMessage = process.env.NODE_ENV === \"production\"\n ? \"An unexpected error occurred\"\n : extractErrorMessage(error);\n const errorResponse = {\n type: \"ERROR\",\n requestId,\n payload: {\n error: {\n message: errorMessage,\n code: \"INTERNAL_ERROR\"\n }\n }\n };\n ws.send(JSON.stringify(errorResponse));\n }\n });\n });\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/server-postgres",
3
3
  "type": "module",
4
- "version": "0.12.0",
4
+ "version": "0.12.1-canary.gdfba2a1",
5
5
  "description": "PostgreSQL data source backend implementation for Rebase with Drizzle ORM",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -38,34 +38,34 @@
38
38
  "./package.json": "./package.json"
39
39
  },
40
40
  "dependencies": {
41
- "@ariga/atlas": "^1.2.2",
41
+ "@ariga/atlas": "^1.2.3",
42
42
  "arg": "^5.0.2",
43
43
  "chalk": "^4.1.2",
44
44
  "chokidar": "5.0.0",
45
45
  "dotenv": "^17.4.2",
46
46
  "drizzle-orm": "^0.45.2",
47
47
  "execa": "^9.6.1",
48
- "pg": "^8.21.0",
49
- "ws": "^8.21.0",
50
- "@rebasepro/codegen": "0.12.0",
51
- "@rebasepro/server": "0.12.0",
52
- "@rebasepro/common": "0.12.0",
53
- "@rebasepro/utils": "0.12.0",
54
- "@rebasepro/types": "0.12.0"
48
+ "pg": "^8.22.0",
49
+ "ws": "^8.21.1",
50
+ "@rebasepro/codegen": "0.12.1-canary.gdfba2a1",
51
+ "@rebasepro/common": "0.12.1-canary.gdfba2a1",
52
+ "@rebasepro/server": "0.12.1-canary.gdfba2a1",
53
+ "@rebasepro/types": "0.12.1-canary.gdfba2a1",
54
+ "@rebasepro/utils": "0.12.1-canary.gdfba2a1"
55
55
  },
56
56
  "devDependencies": {
57
- "@hono/node-server": "^2.0.11",
57
+ "@hono/node-server": "^2.0.12",
58
58
  "@jest/globals": "^30.4.1",
59
59
  "@types/jest": "^30.0.0",
60
- "@types/node": "^25.9.3",
60
+ "@types/node": "^26.1.2",
61
61
  "@types/pg": "^8.20.0",
62
62
  "@types/ws": "^8.18.1",
63
63
  "hono": "^4.12.27",
64
64
  "jest": "^30.4.2",
65
- "ts-jest": "^29.4.11",
65
+ "ts-jest": "^29.4.12",
66
66
  "typescript": "^6.0.3",
67
- "vite": "^8.0.16",
68
- "vitest": "^4.1.9"
67
+ "vite": "^8.1.5",
68
+ "vitest": "^4.1.10"
69
69
  },
70
70
  "gitHead": "d935eefa5aa8d1009a2398cfac2c1e4ee9aeb6b6",
71
71
  "publishConfig": {
@@ -107,16 +107,38 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: Col
107
107
  ? "GENERATED ALWAYS AS IDENTITY"
108
108
  : "DEFAULT gen_random_uuid()::text";
109
109
 
110
+ // Identifiers for the constraint and indexes reconciled further down.
111
+ // Derived from the resolved table name so two auth tables in different
112
+ // schemas cannot collide, and truncated to Postgres's 63-byte identifier
113
+ // limit here rather than letting the server truncate silently — the
114
+ // `IF NOT EXISTS` guards below have to compare against the same name
115
+ // Postgres actually stored, or they re-run forever.
116
+ const authIdentifier = (suffix: string) => `${resolvedTable}_${suffix}`.slice(0, 63);
117
+ const emailLengthConstraint = `"${authIdentifier("email_length_check")}"`;
118
+ const emailLowerUniqueIndex = authIdentifier("email_lower_key");
119
+ const verificationTokenIndex = authIdentifier("email_verification_token_idx");
120
+
121
+ // Every string column here is TEXT, deliberately. In Postgres VARCHAR(n)
122
+ // and TEXT are the same type with the same storage and the same
123
+ // performance; the only difference is a length check, and none of these
124
+ // columns wants one. The widths this table used to carry were inherited
125
+ // MySQL habit (255) and they were all wrong in the same direction —
126
+ // `password_hash VARCHAR(255)` against a 193-char scrypt string left 62
127
+ // characters of headroom in front of a KEY_LENGTH constant living in
128
+ // another package, and `photo_url VARCHAR(500)` rejected the `data:` URIs
129
+ // and long signed URLs that OAuth providers hand back. A limit worth
130
+ // having is a CHECK — alterable without a table rewrite, unlike a type
131
+ // modifier — which is why `email` has one and nothing else does.
110
132
  await db.execute(sql`
111
133
  CREATE TABLE IF NOT EXISTS ${sql.raw(usersTableName)} (
112
134
  id ${sql.raw(userIdType)} PRIMARY KEY ${sql.raw(idDefault)},
113
- email VARCHAR(255) UNIQUE NOT NULL,
114
- display_name VARCHAR(255),
115
- photo_url VARCHAR(500),
135
+ email TEXT NOT NULL CONSTRAINT ${sql.raw(emailLengthConstraint)} CHECK (length(email) <= 320),
136
+ display_name TEXT,
137
+ photo_url TEXT,
116
138
  roles TEXT[] DEFAULT '{}' NOT NULL,
117
- password_hash VARCHAR(255),
139
+ password_hash TEXT,
118
140
  email_verified BOOLEAN DEFAULT FALSE NOT NULL,
119
- email_verification_token VARCHAR(255),
141
+ email_verification_token TEXT,
120
142
  email_verification_sent_at TIMESTAMP WITH TIME ZONE,
121
143
  is_anonymous BOOLEAN DEFAULT FALSE NOT NULL,
122
144
  metadata JSONB DEFAULT '{}' NOT NULL,
@@ -359,12 +381,12 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: Col
359
381
  // statement that references it. `email` is deliberately absent: it has
360
382
  // existed since the first era and cannot be added NOT NULL safely.
361
383
  const userColumnBackfills = [
362
- "display_name VARCHAR(255)",
363
- "photo_url VARCHAR(500)",
384
+ "display_name TEXT",
385
+ "photo_url TEXT",
364
386
  "roles TEXT[] DEFAULT '{}' NOT NULL",
365
- "password_hash VARCHAR(255)",
387
+ "password_hash TEXT",
366
388
  "email_verified BOOLEAN DEFAULT FALSE NOT NULL",
367
- "email_verification_token VARCHAR(255)",
389
+ "email_verification_token TEXT",
368
390
  "email_verification_sent_at TIMESTAMP WITH TIME ZONE",
369
391
  "is_anonymous BOOLEAN DEFAULT FALSE NOT NULL",
370
392
  "metadata JSONB DEFAULT '{}' NOT NULL",
@@ -379,6 +401,139 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: Col
379
401
  `);
380
402
  }
381
403
 
404
+ // Which of the columns below the table actually has. An adopted table —
405
+ // one this framework did not create, which the column-name resolution in
406
+ // `services.ts` exists to support — may be missing any of them, and every
407
+ // statement past this point has to tolerate that rather than abort the
408
+ // whole migration block.
409
+ const usersColumns = await db.execute(sql`
410
+ SELECT column_name, data_type
411
+ FROM information_schema.columns
412
+ WHERE table_schema = ${usersSchema} AND table_name = ${resolvedTable}
413
+ `);
414
+ const usersColumnTypes = new Map(
415
+ (usersColumns.rows as { column_name: string; data_type: string }[])
416
+ .map(row => [row.column_name, row.data_type])
417
+ );
418
+
419
+ // ── Migration: VARCHAR(n) → TEXT on the users string columns ────────
420
+ // Tables created before the widths came off still carry them. Postgres
421
+ // treats varchar(n) → text as binary-coercible with no stricter
422
+ // constraint, so this is a catalogue-only change: no table rewrite, no
423
+ // index rebuild, just a brief ACCESS EXCLUSIVE lock. Guarded on the
424
+ // current type so it runs once and is a pure catalogue read thereafter.
425
+ for (const column of ["email", "display_name", "photo_url", "password_hash", "email_verification_token"]) {
426
+ if (usersColumnTypes.get(column) !== "character varying") continue;
427
+ await db.execute(sql`
428
+ ALTER TABLE ${sql.raw(usersTableName)}
429
+ ALTER COLUMN ${sql.raw(`"${column}"`)} TYPE TEXT
430
+ `);
431
+ logger.info(`🔧 Widened ${usersTableName}.${column} from VARCHAR(n) to TEXT`);
432
+ }
433
+
434
+ // ── Migration: case-insensitive email identity ──────────────────────
435
+ // `getUserByEmail` has always searched `email.toLowerCase()` while the
436
+ // write path stored whatever it was handed, leaving normalisation to a
437
+ // convention every caller had to remember. A row that reached the table
438
+ // with mixed case is then invisible to every lookup — the account exists,
439
+ // login reports no such user, and the plain UNIQUE on `email` does not
440
+ // stop a second row differing only in case, because it compares bytes.
441
+ //
442
+ // Fixed on both sides: `mapPayload` now folds on write, and this index
443
+ // makes the database agree. A unique index on lower(email) is strictly
444
+ // stronger than the byte-exact UNIQUE that older tables carry, so the
445
+ // old constraint is left alone — it can no longer fire on anything the
446
+ // new one would allow.
447
+ //
448
+ // Deliberately no AUTH_SCHEMA_VERSION bump: a runtime that predates this
449
+ // migration keeps working against the migrated table (all of its own
450
+ // write paths already lower-cased), which is exactly the additive case
451
+ // the version stamp is documented not to cover.
452
+ if (usersColumnTypes.has("email")) {
453
+ const indexPresent = await db.execute(sql`
454
+ SELECT 1 FROM pg_class c
455
+ JOIN pg_namespace n ON n.oid = c.relnamespace
456
+ WHERE n.nspname = ${usersSchema} AND c.relname = ${emailLowerUniqueIndex} AND c.relkind = 'i'
457
+ `);
458
+ if (indexPresent.rows.length === 0) {
459
+ // Case-collisions already in the table would make the unique
460
+ // index impossible to build. Report them and leave the table
461
+ // alone: the next boot retries, so fixing the rows is all the
462
+ // operator has to do. Failing loudly beats folding the emails
463
+ // and letting CREATE INDEX pick which account survives.
464
+ const collisions = await db.execute(sql`
465
+ SELECT lower(email) AS normalized, count(*)::int AS occurrences
466
+ FROM ${sql.raw(usersTableName)}
467
+ WHERE email IS NOT NULL
468
+ GROUP BY lower(email)
469
+ HAVING count(*) > 1
470
+ LIMIT 10
471
+ `);
472
+ if (collisions.rows.length > 0) {
473
+ const sample = (collisions.rows as { normalized: string; occurrences: number }[])
474
+ .map(row => `${row.normalized} (×${row.occurrences})`)
475
+ .join(", ");
476
+ logger.error(
477
+ `❌ Cannot enforce case-insensitive email uniqueness on ${usersTableName}: ` +
478
+ `these addresses already exist more than once, differing only in case — ${sample}. ` +
479
+ "Merge or delete the duplicates and restart; until then two accounts can share " +
480
+ "one address and only the lower-cased one is reachable by login."
481
+ );
482
+ } else {
483
+ const folded = await db.execute(sql`
484
+ UPDATE ${sql.raw(usersTableName)}
485
+ SET email = lower(email)
486
+ WHERE email IS NOT NULL AND email <> lower(email)
487
+ `);
488
+ if (folded.rowCount) {
489
+ logger.info(`🔧 Lower-cased ${folded.rowCount} email address(es) in ${usersTableName}`);
490
+ }
491
+ await db.execute(sql`
492
+ CREATE UNIQUE INDEX IF NOT EXISTS ${sql.raw(`"${emailLowerUniqueIndex}"`)}
493
+ ON ${sql.raw(usersTableName)} (lower(email))
494
+ `);
495
+ logger.info(`✅ Email uniqueness on ${usersTableName} is now case-insensitive`);
496
+ }
497
+ }
498
+ }
499
+
500
+ // ── Migration: bound the email column's length ──────────────────────
501
+ // The only length limit on this table worth keeping. 320 is the RFC 5321
502
+ // maximum (64-char local part + @ + 255-char domain), and it matters here
503
+ // beyond tidiness: `email` carries a btree index, and a sufficiently long
504
+ // value fails index insertion with an error that says nothing about
505
+ // email. NOT VALID so an adopted table with a long row still migrates —
506
+ // it binds all new writes, which is the part that matters.
507
+ if (usersColumnTypes.has("email")) {
508
+ const checkPresent = await db.execute(sql`
509
+ SELECT 1 FROM pg_constraint c
510
+ JOIN pg_class t ON t.oid = c.conrelid
511
+ JOIN pg_namespace n ON n.oid = t.relnamespace
512
+ WHERE n.nspname = ${usersSchema}
513
+ AND t.relname = ${resolvedTable}
514
+ AND c.conname = ${authIdentifier("email_length_check")}
515
+ `);
516
+ if (checkPresent.rows.length === 0) {
517
+ await db.execute(sql`
518
+ ALTER TABLE ${sql.raw(usersTableName)}
519
+ ADD CONSTRAINT ${sql.raw(emailLengthConstraint)} CHECK (length(email) <= 320) NOT VALID
520
+ `);
521
+ }
522
+ }
523
+
524
+ // ── Index: email verification token lookups ─────────────────────────
525
+ // `getUserByVerificationToken` filters on this column, which had no
526
+ // index — every click of a verification link was a sequential scan of
527
+ // the whole users table. Partial, because the column is NULL for every
528
+ // user who is not mid-verification, which is nearly all of them.
529
+ if (usersColumnTypes.has("email_verification_token")) {
530
+ await db.execute(sql`
531
+ CREATE INDEX IF NOT EXISTS ${sql.raw(`"${verificationTokenIndex}"`)}
532
+ ON ${sql.raw(usersTableName)} (email_verification_token)
533
+ WHERE email_verification_token IS NOT NULL
534
+ `);
535
+ }
536
+
382
537
  // ── Migration: refresh_tokens become session-scoped, rotation-safe ──
383
538
  // Two shapes are reconciled here, on EVERY table named refresh_tokens
384
539
  // in whatever schema it lives (a database provisioned by an older era
@@ -54,6 +54,25 @@ function getColumn(table: RebasePgTable | undefined, ...keys: string[]): RebaseP
54
54
  return key ? table[key] : undefined;
55
55
  }
56
56
 
57
+ /**
58
+ * The single definition of what an email address looks like in storage.
59
+ *
60
+ * Reads have always folded case; writes did not, and normalising was left to
61
+ * each caller. That asymmetry is only ever one forgotten `.toLowerCase()` away
62
+ * from a row no lookup can find — the account exists, every sign-in path
63
+ * reports no such user, and the byte-exact UNIQUE on the column does not stop a
64
+ * duplicate differing only in case. Applied on both sides here so the guarantee
65
+ * belongs to the repository rather than to its callers' discipline; the
66
+ * `lower(email)` unique index added in `ensureAuthTablesExist` is the database
67
+ * half of the same rule.
68
+ *
69
+ * Whitespace goes too: a trailing space survives the fold and reproduces the
70
+ * problem exactly.
71
+ */
72
+ export function normalizeEmail<T>(email: T): T | string {
73
+ return typeof email === "string" ? email.trim().toLowerCase() : email;
74
+ }
75
+
57
76
  /**
58
77
  * PostgreSQL implementation of UserRepository.
59
78
  * Handles all user-related database operations using Drizzle ORM.
@@ -184,7 +203,7 @@ export class UserService implements UserRepository {
184
203
  const metadataKey = getColumnKey(this.usersTable, "metadata") || "metadata";
185
204
 
186
205
  if ("id" in data) payload[idKey] = data.id;
187
- if ("email" in data) payload[emailKey] = data.email;
206
+ if ("email" in data) payload[emailKey] = normalizeEmail(data.email);
188
207
  if ("passwordHash" in data) payload[passwordHashKey] = data.passwordHash;
189
208
  if ("displayName" in data) payload[displayNameKey] = data.displayName;
190
209
  if ("photoUrl" in data) payload[photoUrlKey] = data.photoUrl;
@@ -244,7 +263,7 @@ export class UserService implements UserRepository {
244
263
  async getUserByEmail(email: string): Promise<UserData | null> {
245
264
  const emailCol = getColumn(this.usersTable, "email");
246
265
  if (!emailCol) return null;
247
- const [row] = await this.db.select().from(this.usersTable).where(eq(emailCol, email.toLowerCase()));
266
+ const [row] = await this.db.select().from(this.usersTable).where(eq(emailCol, normalizeEmail(email)));
248
267
  return row ? this.mapRowToUser(row as Record<string, unknown>) : null;
249
268
  }
250
269
 
@@ -1,8 +1,19 @@
1
- import { pgSchema, pgTable, varchar, uuid, timestamp, boolean, jsonb, text, unique, index } from "drizzle-orm/pg-core";
1
+ import { pgSchema, pgTable, uuid, timestamp, boolean, jsonb, text, unique, index } from "drizzle-orm/pg-core";
2
2
  import { relations } from "drizzle-orm";
3
3
 
4
4
  /**
5
5
  * Factory function to dynamically create the auth tables bound to the specified schema names.
6
+ *
7
+ * This module builds queries; it does not create tables. `ensureAuthTablesExist`
8
+ * owns the DDL, which makes everything here a *claim* about a database it cannot
9
+ * enforce — and the claims drifted. Every column below was declared
10
+ * `varchar(n)` while the DDL created it as `TEXT`: `user_agent` as varchar(500),
11
+ * `ip_address` as varchar(45), `secret_encrypted` as varchar(500), every
12
+ * `token_hash` as varchar(255). None of it was true of any database this
13
+ * framework ever provisioned. Harmless at runtime — drizzle does not enforce a
14
+ * length client-side, so the widths only ever misled the next reader — but a
15
+ * schema module that describes columns that do not exist is worse than no
16
+ * schema module. They are `text` here now because they are TEXT there.
6
17
  */
7
18
  export function createAuthSchema(usersSchemaName = "rebase") {
8
19
  const usersSchema = usersSchemaName === "public" ? null : pgSchema(usersSchemaName);
@@ -15,12 +26,12 @@ export function createAuthSchema(usersSchemaName = "rebase") {
15
26
  */
16
27
  const users = usersTableCreator("users", {
17
28
  id: uuid("id").defaultRandom().primaryKey(),
18
- email: varchar("email", { length: 255 }).notNull().unique(),
19
- passwordHash: varchar("password_hash", { length: 255 }), // NULL for OAuth-only users
20
- displayName: varchar("display_name", { length: 255 }),
21
- photoUrl: varchar("photo_url", { length: 500 }),
29
+ email: text("email").notNull().unique(),
30
+ passwordHash: text("password_hash"), // NULL for OAuth-only users
31
+ displayName: text("display_name"),
32
+ photoUrl: text("photo_url"),
22
33
  emailVerified: boolean("email_verified").default(false).notNull(),
23
- emailVerificationToken: varchar("email_verification_token", { length: 255 }),
34
+ emailVerificationToken: text("email_verification_token"),
24
35
  emailVerificationSentAt: timestamp("email_verification_sent_at"),
25
36
  isAnonymous: boolean("is_anonymous").default(false).notNull(),
26
37
  roles: text("roles").array().default([]).notNull(),
@@ -65,7 +76,7 @@ export function createAuthSchema(usersSchemaName = "rebase") {
65
76
  id: uuid("id").defaultRandom().primaryKey(),
66
77
  uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
67
78
  sessionId: uuid("session_id").defaultRandom().notNull(),
68
- tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
79
+ tokenHash: text("token_hash").notNull().unique(),
69
80
  expiresAt: timestamp("expires_at").notNull(),
70
81
  revoked: boolean("revoked").default(false).notNull(),
71
82
  rotatedAt: timestamp("rotated_at"),
@@ -76,8 +87,8 @@ export function createAuthSchema(usersSchemaName = "rebase") {
76
87
  * that rotates immediately after it.
77
88
  */
78
89
  sessionStartedAt: timestamp("session_started_at").defaultNow().notNull(),
79
- userAgent: varchar("user_agent", { length: 500 }),
80
- ipAddress: varchar("ip_address", { length: 45 }),
90
+ userAgent: text("user_agent"),
91
+ ipAddress: text("ip_address"),
81
92
  createdAt: timestamp("created_at").defaultNow().notNull()
82
93
  }, (table) => ({
83
94
  sessionIdx: index("idx_refresh_tokens_session").on(table.sessionId)
@@ -89,7 +100,7 @@ export function createAuthSchema(usersSchemaName = "rebase") {
89
100
  const passwordResetTokens = tableCreator("password_reset_tokens", {
90
101
  id: uuid("id").defaultRandom().primaryKey(),
91
102
  uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
92
- tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
103
+ tokenHash: text("token_hash").notNull().unique(),
93
104
  expiresAt: timestamp("expires_at").notNull(),
94
105
  usedAt: timestamp("used_at"),
95
106
  createdAt: timestamp("created_at").defaultNow().notNull()
@@ -99,7 +110,7 @@ export function createAuthSchema(usersSchemaName = "rebase") {
99
110
  * App config - key/value store for custom settings
100
111
  */
101
112
  const appConfig = tableCreator("app_config", {
102
- key: varchar("key", { length: 100 }).primaryKey(),
113
+ key: text("key").primaryKey(),
103
114
  value: jsonb("value").notNull(),
104
115
  updatedAt: timestamp("updated_at").defaultNow().notNull()
105
116
  });
@@ -110,8 +121,8 @@ export function createAuthSchema(usersSchemaName = "rebase") {
110
121
  const userIdentities = tableCreator("user_identities", {
111
122
  id: uuid("id").defaultRandom().primaryKey(),
112
123
  uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
113
- provider: varchar("provider", { length: 50 }).notNull(), // e.g. 'google', 'linkedin'
114
- providerId: varchar("provider_id", { length: 255 }).notNull(),
124
+ provider: text("provider").notNull(), // e.g. 'google', 'linkedin'
125
+ providerId: text("provider_id").notNull(),
115
126
  profileData: jsonb("profile_data"),
116
127
  createdAt: timestamp("created_at").defaultNow().notNull(),
117
128
  updatedAt: timestamp("updated_at").defaultNow().notNull()
@@ -125,9 +136,9 @@ export function createAuthSchema(usersSchemaName = "rebase") {
125
136
  const mfaFactors = tableCreator("mfa_factors", {
126
137
  id: uuid("id").defaultRandom().primaryKey(),
127
138
  uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
128
- factorType: varchar("factor_type", { length: 20 }).notNull(), // 'totp'
129
- secretEncrypted: varchar("secret_encrypted", { length: 500 }).notNull(),
130
- friendlyName: varchar("friendly_name", { length: 255 }),
139
+ factorType: text("factor_type").notNull(), // 'totp'
140
+ secretEncrypted: text("secret_encrypted").notNull(),
141
+ friendlyName: text("friendly_name"),
131
142
  verified: boolean("verified").default(false).notNull(),
132
143
  createdAt: timestamp("created_at").defaultNow().notNull(),
133
144
  updatedAt: timestamp("updated_at").defaultNow().notNull()
@@ -141,7 +152,7 @@ export function createAuthSchema(usersSchemaName = "rebase") {
141
152
  factorId: uuid("factor_id").notNull().references(() => mfaFactors.id, { onDelete: "cascade" }),
142
153
  createdAt: timestamp("created_at").defaultNow().notNull(),
143
154
  verifiedAt: timestamp("verified_at"),
144
- ipAddress: varchar("ip_address", { length: 45 }),
155
+ ipAddress: text("ip_address"),
145
156
  expiresAt: timestamp("expires_at").notNull()
146
157
  });
147
158
 
@@ -151,7 +162,7 @@ export function createAuthSchema(usersSchemaName = "rebase") {
151
162
  const recoveryCodes = tableCreator("recovery_codes", {
152
163
  id: uuid("id").defaultRandom().primaryKey(),
153
164
  uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
154
- codeHash: varchar("code_hash", { length: 255 }).notNull(),
165
+ codeHash: text("code_hash").notNull(),
155
166
  usedAt: timestamp("used_at"),
156
167
  createdAt: timestamp("created_at").defaultNow().notNull()
157
168
  });
@@ -162,7 +173,7 @@ export function createAuthSchema(usersSchemaName = "rebase") {
162
173
  const magicLinkTokens = tableCreator("magic_link_tokens", {
163
174
  id: uuid("id").defaultRandom().primaryKey(),
164
175
  uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
165
- tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
176
+ tokenHash: text("token_hash").notNull().unique(),
166
177
  expiresAt: timestamp("expires_at").notNull(),
167
178
  usedAt: timestamp("used_at"),
168
179
  createdAt: timestamp("created_at").defaultNow().notNull()
@@ -1,6 +1,6 @@
1
1
  import { CollectionConfig, NumberProperty, Property, ResolvedRelation, RelationProperty, SecurityOperation, SecurityRule, StringProperty, isPostgresCollectionConfig, DateProperty, ArrayProperty, MapProperty, ReferenceProperty, VectorProperty, BinaryProperty, isManyToMany, type ResolvedManyToMany, type ResolvedBelongsTo, type ResolvedForeignKeyOnTarget, hasForeignKeyOnTarget } from "@rebasepro/types";
2
2
  import { getPrimaryKeys } from "../services/collection-helpers";
3
- import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig } from "@rebasepro/common";
3
+ import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig, resolveStringColumnLength } from "@rebasepro/common";
4
4
  import { toSnakeCase, getPolicyNamesForRule } from "@rebasepro/utils";
5
5
  import { logger } from "@rebasepro/server";
6
6
  // --- Helper Functions ---
@@ -98,9 +98,13 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
98
98
  } else if (stringProp.columnType === "uuid") {
99
99
  columnDefinition = `uuid("${colName}")`;
100
100
  } else if (stringProp.columnType === "char") {
101
- columnDefinition = `char("${colName}")`;
101
+ columnDefinition = `char("${colName}", { length: ${resolveStringColumnLength(stringProp)} })`;
102
102
  } else if (stringProp.columnType === "varchar") {
103
- columnDefinition = `varchar("${colName}")`;
103
+ // The length is not optional decoration: `varchar("col")` with
104
+ // no length is an UNBOUNDED varchar in Postgres, which is what
105
+ // this emitted while the DDL generator emitted VARCHAR(255) for
106
+ // the very same property.
107
+ columnDefinition = `varchar("${colName}", { length: ${resolveStringColumnLength(stringProp)} })`;
104
108
  } else {
105
109
  // `text` is the default, and the only length-unbounded choice.
106
110
  // Ask for `varchar` explicitly if you want the length constraint.