@rebasepro/server 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.
Files changed (55) hide show
  1. package/dist/GCSStorageController-BEmDYFKc.js +216 -0
  2. package/dist/GCSStorageController-BEmDYFKc.js.map +1 -0
  3. package/dist/S3StorageController-CK7r5yZb.js +243 -0
  4. package/dist/S3StorageController-CK7r5yZb.js.map +1 -0
  5. package/dist/admin_block-BGQFSAuV.js +123 -0
  6. package/dist/admin_block-BGQFSAuV.js.map +1 -0
  7. package/dist/auth-Bb7XFCKN.js +8898 -0
  8. package/dist/auth-Bb7XFCKN.js.map +1 -0
  9. package/dist/backend-CIxN4FVm.js +15 -0
  10. package/dist/backend-CIxN4FVm.js.map +1 -0
  11. package/dist/backup-C3Bah7XZ.js +163 -0
  12. package/dist/backup-C3Bah7XZ.js.map +1 -0
  13. package/dist/boot/boot.d.ts +20 -0
  14. package/dist/boot/fetch-bundle.d.ts +43 -0
  15. package/dist/contract-routes-Dj8i5AiM.js +264 -0
  16. package/dist/contract-routes-Dj8i5AiM.js.map +1 -0
  17. package/dist/cron/cron-scheduler.d.ts +34 -0
  18. package/dist/cron-loader-B1S2MCSl.js +63 -0
  19. package/dist/cron-loader-B1S2MCSl.js.map +1 -0
  20. package/dist/cron-routes-Do325hDt.js +62 -0
  21. package/dist/cron-routes-Do325hDt.js.map +1 -0
  22. package/dist/cron-scheduler-B3RFt0HS.js +647 -0
  23. package/dist/cron-scheduler-B3RFt0HS.js.map +1 -0
  24. package/dist/cron-store-BywZsyfZ.js +164 -0
  25. package/dist/cron-store-BywZsyfZ.js.map +1 -0
  26. package/dist/dynamic-import-Dvh-K5fl.js.map +1 -1
  27. package/dist/errors-BYAQztMf.js +222 -0
  28. package/dist/errors-BYAQztMf.js.map +1 -0
  29. package/dist/function-loader-B_1fYfUY.js +86 -0
  30. package/dist/function-loader-B_1fYfUY.js.map +1 -0
  31. package/dist/function-routes-C0cLIy3N.js +28 -0
  32. package/dist/function-routes-C0cLIy3N.js.map +1 -0
  33. package/dist/index.es.js +7480 -17861
  34. package/dist/index.es.js.map +1 -1
  35. package/dist/{jwt-D-eI6TTu.js → jwt-DD6EtpGj.js} +32 -13
  36. package/dist/{jwt-D-eI6TTu.js.map → jwt-DD6EtpGj.js.map} +1 -1
  37. package/dist/logger-BYU66ENZ.js.map +1 -1
  38. package/dist/logs-routes-BYA72C_C.js +100 -0
  39. package/dist/logs-routes-BYA72C_C.js.map +1 -0
  40. package/dist/{openapi-generator-Bjzmb5cn.js → openapi-generator-DFDS7kVk.js} +9 -3
  41. package/dist/openapi-generator-DFDS7kVk.js.map +1 -0
  42. package/dist/{schema-editor-routes-DDxfOIid.js → schema-editor-routes-CZVW2iBr.js} +4 -3
  43. package/dist/{schema-editor-routes-DDxfOIid.js.map → schema-editor-routes-CZVW2iBr.js.map} +1 -1
  44. package/dist/{src-CoOAMnBh.js → src-DymRyxdb.js} +196 -65
  45. package/dist/src-DymRyxdb.js.map +1 -0
  46. package/dist/src-_qQ3RNCK.js +279 -0
  47. package/dist/src-_qQ3RNCK.js.map +1 -0
  48. package/dist/types-DSnOC4mF.js +38 -0
  49. package/dist/types-DSnOC4mF.js.map +1 -0
  50. package/package.json +10 -10
  51. package/dist/openapi-generator-Bjzmb5cn.js.map +0 -1
  52. package/dist/src-CoOAMnBh.js.map +0 -1
  53. package/dist/src-Ivjud8jD.js +0 -957
  54. package/dist/src-Ivjud8jD.js.map +0 -1
  55. /package/dist/{chunk-DSJWtz9O.js → rolldown-runtime-DSJWtz9O.js} +0 -0
@@ -0,0 +1,15 @@
1
+ import { createRequire as __createRequire } from "module";
2
+ import "process";
3
+ __createRequire(import.meta.url);
4
+ //#region ../types/src/types/backend.ts
5
+ /**
6
+ * Type guard: does this admin support SQL operations?
7
+ * @group Admin
8
+ */
9
+ function isSQLAdmin(admin) {
10
+ return !!admin && typeof admin.executeSql === "function";
11
+ }
12
+ //#endregion
13
+ export { isSQLAdmin as t };
14
+
15
+ //# sourceMappingURL=backend-CIxN4FVm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend-CIxN4FVm.js","names":[],"sources":["../../types/src/types/backend.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"],"mappings":";;;;;;;;AA6hBA,SAAgB,WAAW,OAAqD;CAC5E,OAAO,CAAC,CAAC,SAAS,OAAQ,MAAmB,eAAe;AAChE"}
@@ -0,0 +1,163 @@
1
+ import { createRequire as __createRequire } from "module";
2
+ import "process";
3
+ __createRequire(import.meta.url);
4
+ import { n as __exportAll } from "./rolldown-runtime-DSJWtz9O.js";
5
+ import { n as errorHandler, t as ApiError } from "./errors-BYAQztMf.js";
6
+ import fs from "fs";
7
+ import path from "path";
8
+ import { Hono } from "hono";
9
+ //#region src/backup/backup-common.ts
10
+ /**
11
+ * Storage-generic backup helpers used by the admin backup routes.
12
+ *
13
+ * These live in server (which owns {@link StorageController}) so the
14
+ * admin API can list backups without depending on the Postgres driver —
15
+ * `server` must not import from `server-postgres`. The `pg_dump`-side
16
+ * copies of the tiny pure parsers live in `@rebasepro/server-postgres`;
17
+ * keep the two in sync if the naming scheme ever changes.
18
+ */
19
+ /** Parse a destination string (`s3://…`, `gs://…`, or a local path). */
20
+ function parseBackupDestination(out) {
21
+ const s3 = out.match(/^s3:\/\/([^/]+)\/?(.*)$/);
22
+ if (s3) return {
23
+ kind: "s3",
24
+ bucket: s3[1],
25
+ prefix: s3[2].replace(/\/+$/, "")
26
+ };
27
+ const gcs = out.match(/^gs:\/\/([^/]+)\/?(.*)$/);
28
+ if (gcs) return {
29
+ kind: "gcs",
30
+ bucket: gcs[1],
31
+ prefix: gcs[2].replace(/\/+$/, "")
32
+ };
33
+ return {
34
+ kind: "local",
35
+ path: out
36
+ };
37
+ }
38
+ /**
39
+ * Recover the UTC creation time encoded in a `rebase-<db>-<ts>.dump` name.
40
+ * Returns `null` for names that don't match, so foreign objects are ignored.
41
+ */
42
+ function parseBackupTimestamp(fileName) {
43
+ const match = (fileName.split("/").pop() ?? fileName).match(/-(\d{8})T(\d{6})Z\.dump$/);
44
+ if (!match) return null;
45
+ const [, ymd, hms] = match;
46
+ const iso = `${ymd.slice(0, 4)}-${ymd.slice(4, 6)}-${ymd.slice(6, 8)}T${hms.slice(0, 2)}:${hms.slice(2, 4)}:${hms.slice(4, 6)}Z`;
47
+ const date = new Date(iso);
48
+ return Number.isNaN(date.getTime()) ? null : date;
49
+ }
50
+ /**
51
+ * List the backups at a destination as {@link BackupInfo}, newest first.
52
+ * Only `.dump` files are considered.
53
+ */
54
+ async function listBackupObjects(dest, storage) {
55
+ if (dest.kind === "local") {
56
+ if (!fs.existsSync(dest.path)) return [];
57
+ const dir = fs.statSync(dest.path).isDirectory() ? dest.path : path.dirname(dest.path);
58
+ if (!fs.existsSync(dir)) return [];
59
+ return fs.readdirSync(dir).filter((f) => f.endsWith(".dump")).map((f) => {
60
+ const full = path.join(dir, f);
61
+ const st = fs.statSync(full);
62
+ const createdAt = parseBackupTimestamp(f) ?? st.mtime;
63
+ return {
64
+ key: full,
65
+ name: f,
66
+ sizeBytes: st.size,
67
+ createdAt: createdAt.toISOString(),
68
+ destinationKind: "local"
69
+ };
70
+ }).sort(byNewest);
71
+ }
72
+ if (!storage) return [];
73
+ return (await storage.listObjects(dest.prefix ? `${dest.prefix}/` : "", {
74
+ bucket: dest.bucket,
75
+ maxResults: 1e3
76
+ })).items.map((item) => item.fullPath).filter((key) => key.endsWith(".dump")).map((key) => {
77
+ const createdAt = parseBackupTimestamp(key);
78
+ return {
79
+ key,
80
+ name: key.split("/").pop() || key,
81
+ createdAt: createdAt ? createdAt.toISOString() : void 0,
82
+ destinationKind: dest.kind
83
+ };
84
+ }).sort(byNewest);
85
+ }
86
+ function byNewest(a, b) {
87
+ const ta = a.createdAt ? Date.parse(a.createdAt) : 0;
88
+ return (b.createdAt ? Date.parse(b.createdAt) : 0) - ta;
89
+ }
90
+ /** Read a single backup's bytes. Guards against path traversal for local. */
91
+ async function readBackupBytes(dest, key, storage) {
92
+ if (dest.kind === "local") {
93
+ const dir = (fs.existsSync(dest.path) ? fs.statSync(dest.path) : null)?.isDirectory() ? dest.path : path.dirname(dest.path);
94
+ const resolvedDir = path.resolve(dir);
95
+ const resolved = path.resolve(key);
96
+ if (!resolved.startsWith(resolvedDir + path.sep) || !resolved.endsWith(".dump")) return null;
97
+ if (!fs.existsSync(resolved)) return null;
98
+ return {
99
+ bytes: new Uint8Array(fs.readFileSync(resolved)),
100
+ name: path.basename(resolved)
101
+ };
102
+ }
103
+ if (!storage) return null;
104
+ if (!key.endsWith(".dump")) return null;
105
+ const file = await storage.getObject(key, dest.bucket);
106
+ if (!file) return null;
107
+ return {
108
+ bytes: new Uint8Array(await file.arrayBuffer()),
109
+ name: key.split("/").pop() || key
110
+ };
111
+ }
112
+ //#endregion
113
+ //#region src/backup/backup-routes.ts
114
+ /**
115
+ * Admin REST routes for the Backups panel.
116
+ *
117
+ * Routes (mounted under `/admin/backups`, admin-guarded by the caller):
118
+ * GET / → list available backups
119
+ * GET /download → download a backup's bytes (?key=…)
120
+ */
121
+ function createBackupRoutes(config) {
122
+ const router = new Hono();
123
+ router.onError(errorHandler);
124
+ router.get("/", async (c) => {
125
+ const dest = config.getDestination();
126
+ if (!dest) return c.json({
127
+ backups: [],
128
+ destinationKind: "local",
129
+ configured: false
130
+ });
131
+ const backups = await listBackupObjects(dest, config.storage);
132
+ return c.json({
133
+ backups,
134
+ destinationKind: dest.kind,
135
+ configured: true
136
+ });
137
+ });
138
+ router.get("/download", async (c) => {
139
+ const dest = config.getDestination();
140
+ if (!dest) throw ApiError.badRequest("Backups are not configured (set BACKUP_DESTINATION).");
141
+ const key = c.req.query("key");
142
+ if (!key) throw ApiError.badRequest("Missing 'key' query parameter.");
143
+ const result = await readBackupBytes(dest, key, config.storage);
144
+ if (!result) throw ApiError.notFound(`Backup not found: ${key}`);
145
+ c.header("Content-Type", "application/octet-stream");
146
+ c.header("Content-Disposition", `attachment; filename="${result.name}"`);
147
+ return c.body(result.bytes);
148
+ });
149
+ return router;
150
+ }
151
+ //#endregion
152
+ //#region src/backup/index.ts
153
+ var backup_exports = /* @__PURE__ */ __exportAll({
154
+ createBackupRoutes: () => createBackupRoutes,
155
+ listBackupObjects: () => listBackupObjects,
156
+ parseBackupDestination: () => parseBackupDestination,
157
+ parseBackupTimestamp: () => parseBackupTimestamp,
158
+ readBackupBytes: () => readBackupBytes
159
+ });
160
+ //#endregion
161
+ export { parseBackupTimestamp as a, parseBackupDestination as i, createBackupRoutes as n, readBackupBytes as o, listBackupObjects as r, backup_exports as t };
162
+
163
+ //# sourceMappingURL=backup-C3Bah7XZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backup-C3Bah7XZ.js","names":[],"sources":["../src/backup/backup-common.ts","../src/backup/backup-routes.ts","../src/backup/index.ts"],"sourcesContent":["/**\n * Storage-generic backup helpers used by the admin backup routes.\n *\n * These live in server (which owns {@link StorageController}) so the\n * admin API can list backups without depending on the Postgres driver —\n * `server` must not import from `server-postgres`. The `pg_dump`-side\n * copies of the tiny pure parsers live in `@rebasepro/server-postgres`;\n * keep the two in sync if the naming scheme ever changes.\n */\nimport fs from \"fs\";\nimport path from \"path\";\nimport type { BackupInfo, BackupDestinationKind } from \"@rebasepro/types\";\nimport type { StorageController } from \"../storage\";\n\nexport type BackupDestination =\n | { kind: \"local\"; path: string }\n | { kind: \"s3\"; bucket: string; prefix: string }\n | { kind: \"gcs\"; bucket: string; prefix: string };\n\n/** Parse a destination string (`s3://…`, `gs://…`, or a local path). */\nexport function parseBackupDestination(out: string): BackupDestination {\n const s3 = out.match(/^s3:\\/\\/([^/]+)\\/?(.*)$/);\n if (s3) return { kind: \"s3\", bucket: s3[1], prefix: s3[2].replace(/\\/+$/, \"\") };\n const gcs = out.match(/^gs:\\/\\/([^/]+)\\/?(.*)$/);\n if (gcs) return { kind: \"gcs\", bucket: gcs[1], prefix: gcs[2].replace(/\\/+$/, \"\") };\n return { kind: \"local\", path: out };\n}\n\n/**\n * Recover the UTC creation time encoded in a `rebase-<db>-<ts>.dump` name.\n * Returns `null` for names that don't match, so foreign objects are ignored.\n */\nexport function parseBackupTimestamp(fileName: string): Date | null {\n const base = fileName.split(\"/\").pop() ?? fileName;\n const match = base.match(/-(\\d{8})T(\\d{6})Z\\.dump$/);\n if (!match) return null;\n const [, ymd, hms] = match;\n const iso =\n `${ymd.slice(0, 4)}-${ymd.slice(4, 6)}-${ymd.slice(6, 8)}` +\n `T${hms.slice(0, 2)}:${hms.slice(2, 4)}:${hms.slice(4, 6)}Z`;\n const date = new Date(iso);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\n/**\n * List the backups at a destination as {@link BackupInfo}, newest first.\n * Only `.dump` files are considered.\n */\nexport async function listBackupObjects(\n dest: BackupDestination,\n storage?: StorageController\n): Promise<BackupInfo[]> {\n if (dest.kind === \"local\") {\n if (!fs.existsSync(dest.path)) return [];\n const stat = fs.statSync(dest.path);\n const dir = stat.isDirectory() ? dest.path : path.dirname(dest.path);\n if (!fs.existsSync(dir)) return [];\n return fs\n .readdirSync(dir)\n .filter((f) => f.endsWith(\".dump\"))\n .map((f): BackupInfo => {\n const full = path.join(dir, f);\n const st = fs.statSync(full);\n const createdAt = parseBackupTimestamp(f) ?? st.mtime;\n return {\n key: full,\n name: f,\n sizeBytes: st.size,\n createdAt: createdAt.toISOString(),\n destinationKind: \"local\"\n };\n })\n .sort(byNewest);\n }\n\n if (!storage) return [];\n const result = await storage.listObjects(dest.prefix ? `${dest.prefix}/` : \"\", {\n bucket: dest.bucket,\n maxResults: 1000\n });\n return result.items\n .map((item) => item.fullPath)\n .filter((key) => key.endsWith(\".dump\"))\n .map((key): BackupInfo => {\n const createdAt = parseBackupTimestamp(key);\n return {\n key,\n name: key.split(\"/\").pop() || key,\n createdAt: createdAt ? createdAt.toISOString() : undefined,\n destinationKind: dest.kind as BackupDestinationKind\n };\n })\n .sort(byNewest);\n}\n\nfunction byNewest(a: BackupInfo, b: BackupInfo): number {\n const ta = a.createdAt ? Date.parse(a.createdAt) : 0;\n const tb = b.createdAt ? Date.parse(b.createdAt) : 0;\n return tb - ta;\n}\n\n/** Read a single backup's bytes. Guards against path traversal for local. */\nexport async function readBackupBytes(\n dest: BackupDestination,\n key: string,\n storage?: StorageController\n): Promise<{ bytes: Uint8Array<ArrayBuffer>; name: string } | null> {\n if (dest.kind === \"local\") {\n const stat = fs.existsSync(dest.path) ? fs.statSync(dest.path) : null;\n const dir = stat?.isDirectory() ? dest.path : path.dirname(dest.path);\n const resolvedDir = path.resolve(dir);\n const resolved = path.resolve(key);\n // Only allow reads inside the backup directory, and only .dump files.\n if (!resolved.startsWith(resolvedDir + path.sep) || !resolved.endsWith(\".dump\")) {\n return null;\n }\n if (!fs.existsSync(resolved)) return null;\n return { bytes: new Uint8Array(fs.readFileSync(resolved)), name: path.basename(resolved) };\n }\n\n if (!storage) return null;\n if (!key.endsWith(\".dump\")) return null;\n const file = await storage.getObject(key, dest.bucket);\n if (!file) return null;\n return { bytes: new Uint8Array(await file.arrayBuffer()), name: key.split(\"/\").pop() || key };\n}\n","import { Hono } from \"hono\";\nimport type { HonoEnv } from \"../api/types\";\nimport { ApiError, errorHandler } from \"../api/errors\";\nimport type { StorageController } from \"../storage\";\nimport { BackupDestination, listBackupObjects, readBackupBytes } from \"./backup-common\";\n\nexport interface BackupRoutesConfig {\n /**\n * Resolve the current backup destination, or `null` when backups are not\n * configured (`BACKUP_DESTINATION` unset). Read lazily so a restart isn't\n * required to pick up config.\n */\n getDestination: () => BackupDestination | null;\n /** Storage controller for object-storage destinations. */\n storage?: StorageController;\n}\n\n/**\n * Admin REST routes for the Backups panel.\n *\n * Routes (mounted under `/admin/backups`, admin-guarded by the caller):\n * GET / → list available backups\n * GET /download → download a backup's bytes (?key=…)\n */\nexport function createBackupRoutes(config: BackupRoutesConfig): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n router.onError(errorHandler);\n\n router.get(\"/\", async (c) => {\n const dest = config.getDestination();\n if (!dest) {\n return c.json({ backups: [], destinationKind: \"local\" as const, configured: false });\n }\n const backups = await listBackupObjects(dest, config.storage);\n return c.json({ backups, destinationKind: dest.kind, configured: true });\n });\n\n router.get(\"/download\", async (c) => {\n const dest = config.getDestination();\n if (!dest) {\n throw ApiError.badRequest(\"Backups are not configured (set BACKUP_DESTINATION).\");\n }\n const key = c.req.query(\"key\");\n if (!key) {\n throw ApiError.badRequest(\"Missing 'key' query parameter.\");\n }\n const result = await readBackupBytes(dest, key, config.storage);\n if (!result) {\n throw ApiError.notFound(`Backup not found: ${key}`);\n }\n c.header(\"Content-Type\", \"application/octet-stream\");\n c.header(\"Content-Disposition\", `attachment; filename=\"${result.name}\"`);\n return c.body(result.bytes);\n });\n\n return router;\n}\n","/**\n * Storage-generic backup helpers and admin routes for the Backups panel.\n * The `pg_dump`/`pg_restore` machinery lives in `@rebasepro/server-postgres`.\n */\nexport * from \"./backup-common\";\nexport { createBackupRoutes } from \"./backup-routes\";\nexport type { BackupRoutesConfig } from \"./backup-routes\";\n"],"mappings":"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"}
@@ -57,3 +57,23 @@ export declare function bootFromBundle(options?: BootOptions): Promise<BootedRun
57
57
  * bug and the trace is the point.
58
58
  */
59
59
  export declare function runFromBundle(options?: BootOptions): Promise<BootedRuntime>;
60
+ /**
61
+ * Bring the database's collection tables up to date before serving.
62
+ *
63
+ * Delegates to whichever driver bootstrapped the default data source; a driver
64
+ * without `ensureCollectionSchema` (a schemaless one, or an older build) skips
65
+ * rather than failing, which is why this cannot break an existing deployment.
66
+ *
67
+ * Every path out of here says why, at info or louder. Guaranteeing the tables
68
+ * exist is this function's entire job, so "it declined, and said nothing" is the
69
+ * one outcome it must never produce: a deployment that skips comes up answering
70
+ * sign-in and 500ing every `/api/data/*` route, and the operator's only evidence
71
+ * is what these lines print. Silence here has already sent one investigation
72
+ * chasing a stale runtime image that was not stale.
73
+ *
74
+ * Failure is fatal on purpose. Booting anyway would produce exactly the state
75
+ * this exists to prevent — an app that answers sign-in and 500s every data
76
+ * request — and a crash-looping pod with the DDL error in its logs is a far
77
+ * better signal than a running one that silently cannot serve.
78
+ */
79
+ export declare function ensureCollectionSchema(bundle: LoadedBundle, dataSources: InitializedDataSource[], env: RebaseBootEnv): Promise<void>;
@@ -0,0 +1,43 @@
1
+ /** Where the runtime is told to fetch its bundle from. */
2
+ export declare const BUNDLE_URL_ENV = "REBASE_BUNDLE_URL";
3
+ /** Bearer token for that fetch. Does not expire — see the module note. */
4
+ export declare const BUNDLE_TOKEN_ENV = "REBASE_BUNDLE_TOKEN";
5
+ export interface FetchBundleOptions {
6
+ url: string;
7
+ token?: string;
8
+ /** Where to unpack. Defaults to a fresh directory under the OS temp dir. */
9
+ destination?: string;
10
+ /** Injected for tests. */
11
+ fetchImpl?: typeof fetch;
12
+ /** Injected for tests. */
13
+ extract?: (tarball: string, destination: string) => Promise<void>;
14
+ /** How long the download may take before it is abandoned. */
15
+ timeoutMs?: number;
16
+ }
17
+ /**
18
+ * Whether this process should fetch its bundle rather than read one from disk.
19
+ *
20
+ * An explicit `REBASE_BUNDLE` — a path — always wins. A platform that mounted a
21
+ * bundle AND set a URL means somebody is mid-migration between the two, and the
22
+ * local copy is the one that is definitely there.
23
+ */
24
+ export declare function shouldFetchBundle(env?: NodeJS.ProcessEnv): boolean;
25
+ /**
26
+ * Download and unpack a bundle, returning the directory it landed in.
27
+ *
28
+ * Downloads to a file rather than streaming into `tar`, deliberately. A stream
29
+ * that dies mid-transfer leaves `tar` having successfully extracted a prefix of
30
+ * the archive and exiting 0 — the half-unpacked bundle this module exists to
31
+ * refuse. Writing the whole tarball first means a truncated download is caught
32
+ * by `tar` as a corrupt archive, which is an error.
33
+ */
34
+ export declare function fetchBundle(options: FetchBundleOptions): Promise<string>;
35
+ /**
36
+ * Find the bundle root inside an unpacked directory.
37
+ *
38
+ * Tolerates one level of nesting, because whether a tarball has a top-level
39
+ * directory depends on how it was created — `tar czf x.tgz dist-bundle` and
40
+ * `tar czf x.tgz -C dist-bundle .` produce different shapes from the same
41
+ * files, and both are things a build script does.
42
+ */
43
+ export declare function bundleRootIn(directory: string): string | null;
@@ -0,0 +1,264 @@
1
+ import { createRequire as __createRequire } from "module";
2
+ import "process";
3
+ __createRequire(import.meta.url);
4
+ import { n as __exportAll } from "./rolldown-runtime-DSJWtz9O.js";
5
+ import "./src-_qQ3RNCK.js";
6
+ import { t as logger } from "./logger-BYU66ENZ.js";
7
+ import { Hono } from "hono";
8
+ //#region ../types/src/types/project_manifest.ts
9
+ /** Header carrying the schema version an SDK was generated from. */
10
+ var SCHEMA_VERSION_HEADER = "x-rebase-schema";
11
+ //#endregion
12
+ //#region ../types/src/types/collection_contract.ts
13
+ /** Depth limit for the walk — deep enough for real configs, finite for cyclic ones. */
14
+ var MAX_DEPTH = 64;
15
+ /**
16
+ * Resolve whatever a `target` thunk returns down to a collection.
17
+ *
18
+ * A target may be the collection, a module namespace (when the authoring file
19
+ * used `import * as`), or a default-export wrapper. All three appear in real
20
+ * projects, and the SDK generator already unwraps them the same way.
21
+ */
22
+ function unwrapTarget(value) {
23
+ if (!value || typeof value !== "object") return void 0;
24
+ const candidate = value;
25
+ if (candidate.default || candidate.__esModule) {
26
+ const inner = candidate.default;
27
+ if (inner && typeof inner === "object") return inner;
28
+ }
29
+ if (candidate.properties) return value;
30
+ }
31
+ /** The identity a serialized reference uses. Slug first — it is the routing key. */
32
+ function refFor(collection) {
33
+ if (!collection) return void 0;
34
+ const withPath = collection;
35
+ return collection.slug || withPath.path || collection.name;
36
+ }
37
+ function toSerializable(value, seen, depth, state, key) {
38
+ if (depth > MAX_DEPTH) {
39
+ state.truncations++;
40
+ return;
41
+ }
42
+ if (typeof value === "function") {
43
+ if (key === "target") try {
44
+ const ref = refFor(unwrapTarget(value()));
45
+ return ref ? { __collectionRef: ref } : void 0;
46
+ } catch {
47
+ return;
48
+ }
49
+ return;
50
+ }
51
+ if (value === null || typeof value !== "object") return value;
52
+ if (value instanceof Date) return value.toISOString();
53
+ if (value instanceof RegExp) return value.source;
54
+ if (seen.has(value)) {
55
+ state.truncations++;
56
+ return;
57
+ }
58
+ const cached = state.memo.get(value);
59
+ if (cached !== void 0) return cached;
60
+ seen.add(value);
61
+ const truncationsBefore = state.truncations;
62
+ const memoize = (result) => {
63
+ if (result !== void 0 && state.truncations === truncationsBefore) state.memo.set(value, result);
64
+ return result;
65
+ };
66
+ try {
67
+ if (Array.isArray(value)) {
68
+ const items = value.map((item) => toSerializable(item, seen, depth + 1, state)).filter((item) => item !== void 0);
69
+ return memoize(value.length > 0 && items.length === 0 ? void 0 : items);
70
+ }
71
+ if ("$$typeof" in value) return void 0;
72
+ const entries = Object.entries(value);
73
+ const out = {};
74
+ for (const [k, v] of entries) {
75
+ const converted = toSerializable(v, seen, depth + 1, state, k);
76
+ if (converted !== void 0) out[k] = converted;
77
+ }
78
+ if (entries.length > 0 && Object.keys(out).length === 0) return void 0;
79
+ return memoize(out);
80
+ } finally {
81
+ seen.delete(value);
82
+ }
83
+ }
84
+ /**
85
+ * Serialize collections for transport over the contract endpoint.
86
+ *
87
+ * Sorted by slug so the output — and therefore the schema hash computed from it
88
+ * — does not depend on filesystem ordering.
89
+ */
90
+ function serializeCollections(collections) {
91
+ return [...collections].sort((a, b) => String(a.slug ?? "").localeCompare(String(b.slug ?? ""))).map((collection) => toSerializable(withoutAdminBlock(collection), /* @__PURE__ */ new WeakSet(), 0, {
92
+ memo: /* @__PURE__ */ new WeakMap(),
93
+ truncations: 0
94
+ })).filter((c) => c !== void 0);
95
+ }
96
+ /**
97
+ * Drop the admin block before the walk.
98
+ *
99
+ * Nothing downstream of serialization is an admin panel. The contract endpoint
100
+ * feeds remote SDK generation, and `rebase build` writes the result into a bundle
101
+ * manifest that only the backend runtime reads. The block would survive the walk
102
+ * as a husk anyway — its React elements and component functions are dropped
103
+ * individually — and that husk has two costs worth avoiding: it puts every custom
104
+ * component's *file path* on an endpoint whose job is to describe data shapes, and
105
+ * it grows a payload that is fetched and cached per project.
106
+ *
107
+ * Removing it here rather than at each call site means one chokepoint, so a future
108
+ * consumer of `serializeCollections` cannot forget.
109
+ *
110
+ * Child collections carry their own block, so this recurses — stripping only the
111
+ * top level was the mistake `stripNonClientFields` in the contract routes already
112
+ * had to fix once for security rules.
113
+ */
114
+ function withoutAdminBlock(collection) {
115
+ const { admin: _admin, ...rest } = collection;
116
+ const nested = rest;
117
+ if (Array.isArray(nested.subcollections)) nested.subcollections = nested.subcollections.map((child) => withoutAdminBlock(child));
118
+ return rest;
119
+ }
120
+ //#endregion
121
+ //#region ../types/src/types/schema_version.ts
122
+ /**
123
+ * The schema version stamp.
124
+ *
125
+ * One function, used in three places that must agree or the whole drift-detection
126
+ * story is noise: `rebase build` writes it into a bundle manifest, the runtime
127
+ * serves it from the contract endpoint, and a generated SDK records the value it
128
+ * was built from. If any two of those computed it differently, every client would
129
+ * look permanently out of date.
130
+ *
131
+ * It covers **collections only** — the client's contract is the shape of the
132
+ * data, so editing a hook or a server function must not invalidate every SDK in
133
+ * every repository. That is a deliberate narrowing, not an oversight.
134
+ */
135
+ /** Stable stringify: object keys sorted at every level, so key order cannot alter the hash. */
136
+ function canonicalize(value) {
137
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
138
+ if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`;
139
+ return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonicalize(v)}`).join(",")}}`;
140
+ }
141
+ /**
142
+ * Reduce a collection to the parts a generated client is actually built from.
143
+ *
144
+ * The version answers one question — "is this SDK stale?" — so it must change
145
+ * exactly when the generated types could change, and never otherwise. Hashing a
146
+ * whole collection fails both halves of that:
147
+ *
148
+ * - Security rules, callbacks, icons, groups and UI settings do not appear in a
149
+ * generated client, so including them reports perfectly current SDKs as stale.
150
+ * - Worse, they are not stable *inputs*. The runtime applies default security
151
+ * rules when it loads collections, so the same source hashed before and after
152
+ * loading produced two different answers — a build-time stamp that could never
153
+ * match the server that served it.
154
+ *
155
+ * Codegen reads the slug (for the `Database` key and type names), the properties,
156
+ * and the relations. That is the projection.
157
+ */
158
+ function projectForCodegen(collection) {
159
+ const source = collection;
160
+ return {
161
+ slug: collection.slug ?? source.path,
162
+ properties: collection.properties,
163
+ relations: source.relations,
164
+ engine: source.engine,
165
+ dataSource: source.dataSource,
166
+ subcollections: source.subcollections?.map(projectForCodegen)
167
+ };
168
+ }
169
+ /**
170
+ * Compute the canonical string a schema version hashes.
171
+ *
172
+ * Exposed separately so the hashing itself can differ by environment: Node has
173
+ * `crypto`, and callers without it can still compare canonical forms directly.
174
+ */
175
+ function canonicalSchemaPayload(collections) {
176
+ return canonicalize(serializeCollections(collections).map((collection) => projectForCodegen(collection)));
177
+ }
178
+ /**
179
+ * A short, non-cryptographic digest of the canonical payload.
180
+ *
181
+ * FNV-1a style, 64 bits, as two 32-bit halves. This is an identity, not a
182
+ * security boundary: nothing trusts a schema version to prove anything, it only
183
+ * answers "is this the same schema as before". A hand-rolled hash keeps this
184
+ * module free of `node:crypto`, so the identical function runs in the browser,
185
+ * in the CLI, and in the runtime — which is the property that actually matters.
186
+ */
187
+ function computeSchemaVersion(collections) {
188
+ const payload = canonicalSchemaPayload(collections);
189
+ let h1 = 2166136261;
190
+ let h2 = 16777619;
191
+ for (let i = 0; i < payload.length; i++) {
192
+ const code = payload.charCodeAt(i);
193
+ h1 ^= code;
194
+ h1 = h1 + ((h1 << 1) + (h1 << 4) + (h1 << 7) + (h1 << 8) + (h1 << 24)) >>> 0;
195
+ h2 ^= code + i;
196
+ h2 = h2 + ((h2 << 1) + (h2 << 5) + (h2 << 9) + (h2 << 15) + (h2 << 24)) >>> 0;
197
+ }
198
+ const hex = (n) => n.toString(16).padStart(8, "0");
199
+ return `v1:${hex(h1)}${hex(h2)}`;
200
+ }
201
+ //#endregion
202
+ //#region src/api/contract-routes.ts
203
+ var contract_routes_exports = /* @__PURE__ */ __exportAll({ createContractRoutes: () => createContractRoutes });
204
+ /**
205
+ * Strip everything a client does not need from a serialized collection.
206
+ *
207
+ * The generator reads the slug, the properties and the relations. It never reads
208
+ * a security rule — but `securityRules` carries the raw SQL of every RLS
209
+ * predicate guarding the project, which is a description of the authorization
210
+ * model rather than of the data shape. Publishing it to anyone who can generate
211
+ * an SDK gives away more than the endpoint is for, so it is removed here rather
212
+ * than trusted not to matter.
213
+ */
214
+ function stripNonClientFields(collection) {
215
+ if (!collection || typeof collection !== "object") return collection;
216
+ const { securityRules: _securityRules, callbacks: _callbacks, ...rest } = collection;
217
+ if (Array.isArray(rest.subcollections)) rest.subcollections = rest.subcollections.map(stripNonClientFields);
218
+ return rest;
219
+ }
220
+ function createContractRoutes(config) {
221
+ const router = new Hono();
222
+ let cachedVersion;
223
+ const schemaVersionOf = (collections) => {
224
+ if (config.schemaVersion) return config.schemaVersion;
225
+ if (cachedVersion === void 0) cachedVersion = computeSchemaVersion(collections);
226
+ return cachedVersion;
227
+ };
228
+ router.get("/contract", (c) => {
229
+ const collections = config.collectionRegistry.getRawCollections();
230
+ const serialized = serializeCollections(collections).map(stripNonClientFields);
231
+ const schemaVersion = schemaVersionOf(collections);
232
+ const contract = {
233
+ schemaVersion,
234
+ runtime: {
235
+ version: config.runtimeVersion ?? "unknown",
236
+ contract: 1
237
+ },
238
+ collections: serialized,
239
+ collectionSlugs: collections.map((collection) => collection.slug).filter((slug) => Boolean(slug)).sort(),
240
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
241
+ };
242
+ c.header(SCHEMA_VERSION_HEADER, schemaVersion);
243
+ return c.json(contract);
244
+ });
245
+ /**
246
+ * Cheap drift check.
247
+ *
248
+ * Deliberately unauthenticated and deliberately tiny: it returns a version
249
+ * string and nothing else. A CI job that only wants to know whether its
250
+ * generated SDK is stale should not need admin credentials, and a version
251
+ * stamp reveals nothing about the schema it stands for.
252
+ */
253
+ router.get("/schema-version", (c) => {
254
+ const schemaVersion = schemaVersionOf(config.collectionRegistry.getRawCollections());
255
+ c.header(SCHEMA_VERSION_HEADER, schemaVersion);
256
+ return c.json({ schemaVersion });
257
+ });
258
+ logger.debug("Contract routes mounted");
259
+ return router;
260
+ }
261
+ //#endregion
262
+ export { createContractRoutes as n, contract_routes_exports as t };
263
+
264
+ //# sourceMappingURL=contract-routes-Dj8i5AiM.js.map