@rebasepro/server 0.14.1-canary.g7e666eb → 0.14.1

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 (45) hide show
  1. package/dist/api/rest/query-parser.d.ts +22 -0
  2. package/dist/auth/index.d.ts +3 -1
  3. package/dist/auth/jwks-routes.d.ts +17 -0
  4. package/dist/auth/jwt-keys.d.ts +108 -0
  5. package/dist/auth/jwt.d.ts +32 -0
  6. package/dist/{auth-BQcdhMBL.js → auth-BobZVd0j.js} +82 -6
  7. package/dist/auth-BobZVd0j.js.map +1 -0
  8. package/dist/boot/boot.d.ts +36 -50
  9. package/dist/boot/ddl-bootstrap.d.ts +15 -0
  10. package/dist/boot/env.d.ts +20 -0
  11. package/dist/boot/provision.d.ts +182 -0
  12. package/dist/boot/role.d.ts +88 -0
  13. package/dist/{cron-store-D5dUNviq.js → cron-store-CB1x-Ken.js} +2 -2
  14. package/dist/{cron-store-D5dUNviq.js.map → cron-store-CB1x-Ken.js.map} +1 -1
  15. package/dist/{ddl-bootstrap-BhXbTnBl.js → ddl-bootstrap-Cywoj8Ta.js} +40 -2
  16. package/dist/{ddl-bootstrap-BhXbTnBl.js.map → ddl-bootstrap-Cywoj8Ta.js.map} +1 -1
  17. package/dist/env.d.ts +2 -0
  18. package/dist/functions/proxy.d.ts +41 -0
  19. package/dist/functions/selection.d.ts +45 -0
  20. package/dist/index.d.ts +7 -3
  21. package/dist/index.es.js +815 -351
  22. package/dist/index.es.js.map +1 -1
  23. package/dist/init/shutdown.d.ts +4 -0
  24. package/dist/init/surfaces.d.ts +79 -0
  25. package/dist/init.d.ts +121 -1
  26. package/dist/jobs/index.d.ts +5 -0
  27. package/dist/jobs/job-queue.d.ts +14 -0
  28. package/dist/jobs/job-store.d.ts +22 -0
  29. package/dist/jobs/types.d.ts +125 -0
  30. package/dist/jobs-DR4SjGrD.js +326 -0
  31. package/dist/jobs-DR4SjGrD.js.map +1 -0
  32. package/dist/{jwt-CYGFT0ih.js → jwt-VJyXTdQQ.js} +152 -6
  33. package/dist/jwt-VJyXTdQQ.js.map +1 -0
  34. package/dist/{openapi-generator-BWL8F2La.js → openapi-generator-DQeQ_q2f.js} +45 -1
  35. package/dist/openapi-generator-DQeQ_q2f.js.map +1 -0
  36. package/dist/proxy-Bj5DVllb.js +139 -0
  37. package/dist/proxy-Bj5DVllb.js.map +1 -0
  38. package/dist/selection-_z6TM1DB.js +64 -0
  39. package/dist/selection-_z6TM1DB.js.map +1 -0
  40. package/dist/services/webhook-service.d.ts +43 -5
  41. package/dist/src-8XDWyDfR.js.map +1 -1
  42. package/package.json +5 -5
  43. package/dist/auth-BQcdhMBL.js.map +0 -1
  44. package/dist/jwt-CYGFT0ih.js.map +0 -1
  45. package/dist/openapi-generator-BWL8F2La.js.map +0 -1
package/dist/index.es.js CHANGED
@@ -4,10 +4,10 @@ __createRequire(import.meta.url);
4
4
  import { A as GeoPoint, M as RebaseApiError, N as RebaseClientError, O as EntityReference, S as toSnakeCase, _ as fieldKeyForColumn, b as buildCompositeId, c as collectAllPages, d as normalizeOrderBy, f as serializeOrderBy, g as getEffectiveSecurityRules, h as resolveDataSource, j as Vector, k as EntityRelation, l as paginateFind, m as createDataSourceRegistry, n as buildSdkData, o as serializeFilter, p as CollectionRegistry, s as serializeLogicalCondition, t as buildRoutedRebaseData, u as resolveFindWindow, v as findRelation, x as resolvePrimaryKeys, y as resolveCollectionRelations } from "./src-8XDWyDfR.js";
5
5
  import { c as DEFAULT_DATA_SOURCE_KEY, i as getCollectionDataPath, m as toCanonicalOp, n as RLS_ROLES_SQL, o as isPostgresCollectionConfig, r as RLS_UID_SQL, t as RLS_JWT_SQL } from "./src-Cz9nMgUR.js";
6
6
  import { n as ADMIN_PROPERTY_KEYS, t as ADMIN_COLLECTION_KEYS } from "./admin_block-H86dCPsj.js";
7
- import { i as isSQLAdmin, r as revokeInternalTableSql } from "./ddl-bootstrap-BhXbTnBl.js";
8
- import { C as DEFAULT_STORAGE_SOURCE_KEY, E as storageEnvSuffix, T as normalizeStorageSources, _ as InvalidStorageKeyError, b as canonicalStorageKey, g as InvalidStorageBucketError, i as generateDownloadToken, n as configureJwt, v as canonicalStorageBucket, w as findStorageSuffixCollision, y as canonicalStorageId } from "./jwt-CYGFT0ih.js";
7
+ import { a as isDuplicateObjectRace, i as isConcurrentDdlRace, n as createDdlBootstrapper, o as revokeInternalTableSql, s as isSQLAdmin, t as CONCURRENT_DDL_SQLSTATES } from "./ddl-bootstrap-Cywoj8Ta.js";
8
+ import { C as canonicalStorageKey, D as findStorageSuffixCollision, E as DEFAULT_STORAGE_SOURCE_KEY, O as normalizeStorageSources, S as canonicalStorageId, b as InvalidStorageKeyError, c as getJwks, d as hasAsymmetricSigningKey, i as generateDownloadToken, k as storageEnvSuffix, n as configureJwt, p as isJwtConfigured, v as normalizePemFromEnv, x as canonicalStorageBucket, y as InvalidStorageBucketError } from "./jwt-VJyXTdQQ.js";
9
9
  import { n as createContractRoutes } from "./contract-routes-Bet-eCNJ.js";
10
- import { $ as fileTokenAuth, A as MemoryRateLimitStore, B as getEmailVerificationTemplate, C as ZodNumber, D as _coercedNumber, E as string, F as resolveAuthHooks, G as RawHtml, H as getPasswordResetTemplate, I as hashPassword, J as raw, K as escapeHtml, L as validatePasswordStrength, M as createEmailService, N as assertEmailLinkBases, O as DEFAULT_FUNCTIONS_ANONYMOUS_LIMIT, P as resolveEmailLinkBase, Q as extractUserFromToken, R as verifyPassword, S as createBuiltinAuthAdapter, T as object, U as getUserInvitationTemplate, V as getMagicLinkTemplate, W as getWelcomeEmailTemplate, X as createAuthMiddleware, Y as createAdapterAuthMiddleware, Z as createRequireAuth, _ as createLinkedinProvider, _t as parseQueryOptions, a as createSpotifyProvider, at as createApiKeyPreAuth, b as pkceTokenParams, bt as isPublicStoragePath, c as createGitLabProvider, ct as isApiKeyToken, d as createFacebookProvider, dt as safeCompare, et as optionalAuth, f as createAppleProvider, ft as SERVICE_IDENTITY, g as createGitHubProvider, gt as orderByEntriesToTuples, h as verifyOidcIdToken, ht as isOperationAllowed, i as createApiKeyStore, it as requireAuth, j as SMTPEmailService, k as createDataRateLimiter, l as createDiscordProvider, lt as validateApiKey, m as tryVerifyOidcIdToken, mt as httpMethodToOperation, n as createCustomAuthAdapter, nt as queryTokenAuth, o as createSlackProvider, ot as createFunctionApiKeyGuard, p as createMicrosoftProvider, pt as scopeDataDriver, q as html, r as createApiKeyRoutes, rt as requireAdmin, s as createBitbucketProvider, st as createStorageApiKeyGuard, tt as publicObjectAuth, u as createTwitterProvider, ut as extractBearerToken, v as createGoogleProvider, vt as resolveListLimitParam, w as _enum, x as providerVerifiedEmail, y as oauthCodeFlowSchema, yt as PUBLIC_STORAGE_PREFIX, z as generateSecurePassword } from "./auth-BQcdhMBL.js";
10
+ import { $ as extractUserFromToken, A as createDataRateLimiter, B as generateSecurePassword, C as createBuiltinAuthAdapter, Ct as isPublicStoragePath, D as string, E as object, F as resolveEmailLinkBase, G as getWelcomeEmailTemplate, H as getMagicLinkTemplate, I as resolveAuthHooks, J as html, K as RawHtml, L as hashPassword, M as SMTPEmailService, N as createEmailService, O as _coercedNumber, P as assertEmailLinkBases, Q as createRequireAuth, R as validatePasswordStrength, S as createJwksRoutes, St as PUBLIC_STORAGE_PREFIX, T as _enum, U as getPasswordResetTemplate, V as getEmailVerificationTemplate, W as getUserInvitationTemplate, X as createAdapterAuthMiddleware, Y as raw, Z as createAuthMiddleware, _ as createLinkedinProvider, _t as orderByEntriesToTuples, a as createSpotifyProvider, at as requireAuth, b as pkceTokenParams, bt as parseQueryOptions, c as createGitLabProvider, ct as createStorageApiKeyGuard, d as createFacebookProvider, dt as extractBearerToken, et as fileTokenAuth, f as createAppleProvider, ft as safeCompare, g as createGitHubProvider, gt as isOperationAllowed, h as verifyOidcIdToken, ht as httpMethodToOperation, i as createApiKeyStore, it as requireAdmin, j as MemoryRateLimitStore, k as DEFAULT_FUNCTIONS_ANONYMOUS_LIMIT, l as createDiscordProvider, lt as isApiKeyToken, m as tryVerifyOidcIdToken, mt as scopeDataDriver, n as createCustomAuthAdapter, nt as publicObjectAuth, o as createSlackProvider, ot as createApiKeyPreAuth, p as createMicrosoftProvider, pt as SERVICE_IDENTITY, q as escapeHtml, r as createApiKeyRoutes, rt as queryTokenAuth, s as createBitbucketProvider, st as createFunctionApiKeyGuard, tt as optionalAuth, u as createTwitterProvider, ut as validateApiKey, v as createGoogleProvider, vt as parseAggregateSelect, w as ZodNumber, x as providerVerifiedEmail, xt as resolveListLimitParam, y as oauthCodeFlowSchema, yt as parseGroupBy, z as verifyPassword } from "./auth-BobZVd0j.js";
11
11
  import { t as logger } from "./logger-DfvF_8r-.js";
12
12
  import { n as errorHandler, t as ApiError } from "./errors-EBYiaJ2E.js";
13
13
  import { t as logMiddleware } from "./logs-routes-BYA72C_C.js";
@@ -19,8 +19,10 @@ import { t as createFunctionRoutes } from "./function-routes-Btcez1T-.js";
19
19
  import { n as loadCronJobsFromDirectory, r as loadCronJobsWithDiagnostics } from "./cron-loader-YhhQeVBM.js";
20
20
  import { r as validateCronExpression, t as CronScheduler } from "./cron-scheduler-DIpYBmZP.js";
21
21
  import { t as createCronRoutes } from "./cron-routes-maM_RlUu.js";
22
- import { t as createCronStore } from "./cron-store-D5dUNviq.js";
22
+ import { t as createCronStore } from "./cron-store-CB1x-Ken.js";
23
23
  import { a as parseBackupTimestamp, i as parseBackupDestination, n as createBackupRoutes, o as readBackupBytes, r as listBackupObjects } from "./backup-C6ljYVTp.js";
24
+ import { i as createJobStore, n as createJobQueue, r as defaultBackoff } from "./jobs-DR4SjGrD.js";
25
+ import { t as FunctionSelectionError } from "./selection-_z6TM1DB.js";
24
26
  import { createHash, randomBytes, randomUUID } from "node:crypto";
25
27
  import * as fs$2 from "fs";
26
28
  import fs, { existsSync } from "fs";
@@ -526,6 +528,344 @@ async function loadCollectionsFromDirectory(source, options = {}) {
526
528
  return validate(applyCollectionDefaults(collections, await readDefaults(resolved)));
527
529
  }
528
530
  //#endregion
531
+ //#region src/boot/version-skew.ts
532
+ /**
533
+ * Telling a *stale* driver apart from a driver that never had the feature.
534
+ *
535
+ * The managed runtime is two halves that version independently. The image
536
+ * supplies `@rebasepro/server` — `docker/entrypoint.mjs` symlinks it over
537
+ * whatever the bundle installed, so the boot harness is always the image's. The
538
+ * **driver** is not redirected: `@rebasepro/server-postgres` comes from the
539
+ * bundle's `deps.declared`, pinned to whatever the project's `package.json`
540
+ * asked npm for. Shipping a new image therefore does not ship a new driver, and
541
+ * a fix that spans both packages reaches a tenant only after an npm publish AND
542
+ * a rebuild.
543
+ *
544
+ * That split has a nasty failure mode, and it has already burned a day. Boot
545
+ * looks for an optional capability, does not find it, and reports the honest
546
+ * local fact — "this driver does not implement collection-table creation" —
547
+ * which is exactly what a schemaless driver looks like. But a driver that is
548
+ * merely *older than the runtime asking* looks identical, and the two want
549
+ * opposite responses: one is "this database is not managed by Rebase, carry on",
550
+ * the other is "your tables were never created and every data route is about to
551
+ * 500". Naming the versions is what separates them, so these helpers exist to
552
+ * put both numbers in front of whoever reads the log.
553
+ */
554
+ /** The package whose version defines "the runtime" for skew purposes. */
555
+ var RUNTIME_PACKAGE = "@rebasepro/server";
556
+ /**
557
+ * Locate a package by walking `node_modules` up from a directory.
558
+ *
559
+ * Lives here rather than in `driver.ts` because both the driver loader and the
560
+ * version check need it, and `driver.ts` already imports this module.
561
+ */
562
+ function findPackageDir(fromDir, packageName) {
563
+ let dir = path.resolve(fromDir);
564
+ for (;;) {
565
+ const candidate = path.join(dir, "node_modules", ...packageName.split("/"));
566
+ if (fs.existsSync(path.join(candidate, "package.json"))) return candidate;
567
+ const parent = path.dirname(dir);
568
+ if (parent === dir) return void 0;
569
+ dir = parent;
570
+ }
571
+ }
572
+ /**
573
+ * Parse the subset of semver the workspace actually publishes.
574
+ *
575
+ * Deliberately lenient: an unparseable version yields `undefined` and every
576
+ * comparison then declines to judge, because a wrong "your driver is old"
577
+ * warning on a fork's custom version string is worse than no warning at all.
578
+ */
579
+ function parseVersion(version) {
580
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/.exec(version.trim());
581
+ if (!match) return void 0;
582
+ return {
583
+ parts: [
584
+ Number(match[1]),
585
+ Number(match[2]),
586
+ Number(match[3])
587
+ ],
588
+ prerelease: match[4]
589
+ };
590
+ }
591
+ /**
592
+ * Compare two versions, or `undefined` when either cannot be parsed.
593
+ *
594
+ * Follows semver on the one rule that matters here: a prerelease sorts *below*
595
+ * the release it leads to, so `0.12.1-canary.g7f1150f` < `0.12.1`. Without that
596
+ * rule every canary would read as newer than the stable it precedes, and the
597
+ * canaries are precisely where fixes land first.
598
+ */
599
+ function compareVersions(a, b) {
600
+ const left = parseVersion(a);
601
+ const right = parseVersion(b);
602
+ if (!left || !right) return void 0;
603
+ for (let i = 0; i < 3; i++) if (left.parts[i] !== right.parts[i]) return left.parts[i] < right.parts[i] ? -1 : 1;
604
+ if (left.prerelease === right.prerelease) return 0;
605
+ if (left.prerelease === void 0) return 1;
606
+ if (right.prerelease === void 0) return -1;
607
+ return left.prerelease < right.prerelease ? -1 : 1;
608
+ }
609
+ /**
610
+ * Describe how a driver's version relates to the runtime asking it for work.
611
+ *
612
+ * Returns `stale: false` whenever it cannot tell — an unknown version, an
613
+ * unparseable one, a driver that is newer. Silence beats a confident wrong
614
+ * diagnosis, and the caller still prints its own local fact either way.
615
+ */
616
+ function describeDriverSkew(driverVersion, runtimeVersion) {
617
+ if (!driverVersion || !runtimeVersion) return { stale: false };
618
+ const order = compareVersions(driverVersion, runtimeVersion);
619
+ if (order === void 0) return { stale: false };
620
+ if (order < 0) return {
621
+ stale: true,
622
+ detail: `The driver is at ${driverVersion} while this runtime is ${runtimeVersion} — the driver is OLDER, so this is very likely a stale pin rather than a driver that never had the capability.`
623
+ };
624
+ return {
625
+ stale: false,
626
+ detail: `Driver ${driverVersion}, runtime ${runtimeVersion}.`
627
+ };
628
+ }
629
+ /**
630
+ * How to get a project's schema applied, written once and owned by the runtime.
631
+ *
632
+ * This text lives in `@rebasepro/server` on purpose. The equivalent guidance in
633
+ * the Postgres driver's schema-drift warning can only ever be as current as the
634
+ * driver a tenant pinned — so the tenants who most need to be told "your driver
635
+ * is too old" are exactly the ones whose driver still prints the old advice.
636
+ * The image's copy of this package is never stale, so guidance printed from
637
+ * here always reflects the platform as it is today.
638
+ *
639
+ * Both audiences get a line because the runtime cannot reliably tell which one
640
+ * it is serving, and naming only the pnpm scripts — as this once did everywhere
641
+ * — tells a managed operator to run a command that structurally cannot reach
642
+ * their in-cluster database.
643
+ */
644
+ function schemaRecoveryGuidance(options = {}) {
645
+ const lines = [
646
+ " To apply this project's schema:",
647
+ " • Managed cloud: the runtime applies tables and RLS at boot (unless",
648
+ " REBASE_MIGRATE_ON_BOOT=none). `rebase db push` cannot reach a tenant's",
649
+ " in-cluster database — redeploy instead."
650
+ ];
651
+ if (options.staleDriver) lines.push(" Because the driver is pinned by your bundle, bump the", " `@rebasepro/server-postgres` version in your project's package.json", " and redeploy — a newer platform image alone will NOT update it.");
652
+ lines.push(" • Self-host: run `rebase db push` (dev) or `rebase db migrate` (prod).");
653
+ return lines.join("\n");
654
+ }
655
+ /**
656
+ * The installed version of a package, read from its `package.json`.
657
+ *
658
+ * Returns `undefined` rather than throwing for every failure — a missing or
659
+ * malformed manifest degrades the diagnosis, and must never take down a boot
660
+ * that would otherwise have served.
661
+ */
662
+ function readPackageVersion(packageDir) {
663
+ try {
664
+ const pkg = JSON.parse(fs.readFileSync(path.join(packageDir, "package.json"), "utf8"));
665
+ return typeof pkg.version === "string" ? pkg.version : void 0;
666
+ } catch {
667
+ return;
668
+ }
669
+ }
670
+ /**
671
+ * The version of `@rebasepro/server` this deployment is actually running.
672
+ *
673
+ * Resolved from the same roots the drivers are, and that is the point rather
674
+ * than a convenience. In a managed container `docker/entrypoint.mjs` replaces
675
+ * `/bundle/node_modules/@rebasepro/server` with a link to the image's copy, so
676
+ * reading the manifest *through the bundle* reports the version that will
677
+ * really execute — which is exactly the number the driver has to be compared
678
+ * against. Reading this package's own manifest by module path would report the
679
+ * same thing in the happy case and quietly lie in the case that matters: a
680
+ * bundle whose link did not happen.
681
+ *
682
+ * Returns `undefined` when the package cannot be found; every caller treats
683
+ * that as "cannot judge" and stays quiet.
684
+ */
685
+ function readRuntimeVersion(roots) {
686
+ for (const root of roots) {
687
+ const dir = findPackageDir(root, RUNTIME_PACKAGE);
688
+ const version = dir ? readPackageVersion(dir) : void 0;
689
+ if (version) return version;
690
+ }
691
+ }
692
+ //#endregion
693
+ //#region src/boot/provision.ts
694
+ /**
695
+ * Name the object that is missing a method, in a way that points somewhere.
696
+ *
697
+ * What is missing is the method on the ADAPTER — the only object this module
698
+ * ever sees — which is not the same as the driver package lacking the code. So
699
+ * a resolved package name is quoted as the adapter's *origin*, and an adapter
700
+ * the application built itself is described as exactly that, with no package
701
+ * name attached for the reader to go and bump.
702
+ */
703
+ function describeAdapter(target) {
704
+ return target.driverPackage ? `the adapter from "${target.driverPackage}" (engine "${target.engine}")` : `this project's "${target.engine}" adapter`;
705
+ }
706
+ function provisioningDisabled(env = process.env) {
707
+ return (env.REBASE_MIGRATE_ON_BOOT || "ensure") === "none";
708
+ }
709
+ /**
710
+ * Create any collection tables the database is missing, before serving.
711
+ *
712
+ * Additive only: the driver may create missing tables, columns and enum types,
713
+ * and may never drop or rewrite. Destructive changes stay a deliberate
714
+ * migration, because this runs unattended with nobody reading a diff.
715
+ *
716
+ * Every path out of here says why, at info or louder. Guaranteeing the tables
717
+ * exist is this function's entire job, so "it declined, and said nothing" is the
718
+ * one outcome it must never produce: a deployment that skips comes up answering
719
+ * sign-in and 500ing every `/api/data/*` route, and the operator's only evidence
720
+ * is what these lines print.
721
+ *
722
+ * Note that silence is still possible one level up — if nothing CALLS this, no
723
+ * line is printed and the absence is the only signal. That is precisely the bug
724
+ * this module was extracted to make structurally impossible, and it is why the
725
+ * outcome is reported back to the driver rather than only logged.
726
+ *
727
+ * Failure is fatal on purpose. Booting anyway would produce exactly the state
728
+ * this exists to prevent, and a crash-looping pod with the DDL error in its logs
729
+ * is a far better signal than a running one that silently cannot serve.
730
+ */
731
+ async function provisionCollectionTables(collections, target, options = {}) {
732
+ const skip = (reason, level = "info") => {
733
+ logger[level](`Collection schema: skipped — ${reason}`);
734
+ return {
735
+ status: "skipped",
736
+ reason
737
+ };
738
+ };
739
+ if (options.provision === false) return skip("this process does not provision the schema — another process in this deployment owns it.");
740
+ if (provisioningDisabled(options.env ?? process.env)) return skip("REBASE_MIGRATE_ON_BOOT=none, leaving the database schema untouched.");
741
+ if (options.introspecting) return skip("this project declares no collections, so they are read from the database rather than from code.");
742
+ if (collections.length === 0) return skip("no collections resolved for this data source.");
743
+ if (!target.bootstrapper.ensureCollectionSchema) return skip(`${describeAdapter(target)} does not expose collection-table creation. The driver package may well implement it on a class the adapter does not forward, so check the adapter's shape before blaming its version. Collection tables will NOT be created, so every /api/data route will fail on a missing relation.
744
+ ` + schemaRecoveryGuidance(), "warn");
745
+ const { applied } = await target.bootstrapper.ensureCollectionSchema(collections, target.driverResult, (message) => logger.debug(`schema: ${message}`));
746
+ if (applied > 0) logger.info(`Applied ${applied} additive schema change(s) before boot.`);
747
+ else logger.debug("Collection schema is up to date.");
748
+ return {
749
+ status: "applied",
750
+ applied
751
+ };
752
+ }
753
+ /**
754
+ * Apply the collections' RLS policies — the companion to
755
+ * {@link provisionCollectionTables}, which creates the tables this makes
756
+ * servable.
757
+ *
758
+ * Runs after auth is initialized, not alongside table creation: the generated
759
+ * policies call the `auth.*` helper functions and `CREATE POLICY` validates
760
+ * those exist. Tables without policies are not servable either — authenticated
761
+ * requests run as a restricted role, so a read with no policy returns nothing.
762
+ *
763
+ * The benign gates return quietly here rather than logging the same reason
764
+ * twice: table provisioning already ran on this same boot and explained itself.
765
+ * The one thing this does say out loud is a driver that created tables but
766
+ * cannot apply policies — that is the difference between a served collection and
767
+ * a 401, and it must not pass in silence.
768
+ */
769
+ async function provisionCollectionPolicies(collections, target, options = {}) {
770
+ if (options.provision === false) {
771
+ logger.info("Collection RLS policies: not applied by this process — another process in this deployment owns them.");
772
+ return {
773
+ status: "skipped",
774
+ reason: "another process provisions"
775
+ };
776
+ }
777
+ if (provisioningDisabled(options.env ?? process.env)) return {
778
+ status: "skipped",
779
+ reason: "REBASE_MIGRATE_ON_BOOT=none"
780
+ };
781
+ if (options.introspecting) return {
782
+ status: "skipped",
783
+ reason: "collections are introspected"
784
+ };
785
+ if (collections.length === 0) return {
786
+ status: "skipped",
787
+ reason: "no collections"
788
+ };
789
+ if (!target.bootstrapper.ensureCollectionPolicies) {
790
+ const reason = `${describeAdapter(target)} does not apply RLS policies at boot`;
791
+ logger.warn(`Collection policies: skipped — ${reason}. Collections are not row-secured until policies are applied — depending on the driver's grants that means reads are denied or that they are unfiltered. Do not serve traffic until this is resolved.
792
+ ` + schemaRecoveryGuidance());
793
+ return {
794
+ status: "skipped",
795
+ reason
796
+ };
797
+ }
798
+ const { applied } = await target.bootstrapper.ensureCollectionPolicies(collections, target.driverResult, (message) => logger.debug(`policies: ${message}`));
799
+ if (applied > 0) logger.info(`Applied ${applied} RLS policy statement(s) before serving.`);
800
+ else logger.debug("RLS policies are up to date.");
801
+ return {
802
+ status: "applied",
803
+ applied
804
+ };
805
+ }
806
+ /**
807
+ * The collections a data source's engine is the store for.
808
+ *
809
+ * A project's collections directory holds *every* collection it declares,
810
+ * whatever engine serves it — that is the point of `dataSource` routing. What it
811
+ * is not is a list of tables to create: handing the whole directory to the
812
+ * primary source's bootstrapper made a Firestore collection declared alongside
813
+ * the Postgres ones arrive as an empty Postgres table, with RLS policies, while
814
+ * the app went on reading its documents from Firestore.
815
+ *
816
+ * Excluding is deliberately conservative. A collection that names neither an
817
+ * `engine` nor a `dataSource` belongs to whichever source is primary — the
818
+ * "postgres" that `resolveDataSource` falls back to there is a default, not a
819
+ * declaration, and must not exclude anything on its own. Only a collection that
820
+ * explicitly routes to a *different* engine is dropped, so a project running two
821
+ * sources on the same engine is unaffected.
822
+ *
823
+ * Takes the shapes it actually reads rather than an `InitializedDataSource`, so
824
+ * the path that has data sources and the path that has only an adapter can share
825
+ * one implementation instead of growing a second one that drifts.
826
+ */
827
+ function collectionsStoredBy(collections, primary, dataSources) {
828
+ const registry = createDataSourceRegistry(dataSources.map((source) => ({
829
+ key: source.key,
830
+ engine: source.engine
831
+ })));
832
+ return collections.filter((collection) => {
833
+ const declared = collection.engine ?? (collection.dataSource ? resolveDataSource(collection, registry).engine : void 0);
834
+ if (!declared) return true;
835
+ return declared === primary.engine;
836
+ });
837
+ }
838
+ /** Say what was routed elsewhere, so a missing table is never a silent one. */
839
+ function logForeignCollections(all, stored, primary) {
840
+ if (stored.length === all.length) return;
841
+ const foreign = all.filter((c) => !stored.includes(c)).map((c) => c.slug);
842
+ logger.info(`Skipping ${foreign.length} collection(s) served by another engine, not "${primary.engine}": ${foreign.join(", ")}. Their storage is not managed by this data source.`);
843
+ }
844
+ /**
845
+ * Which bootstrapper provisions, and what to call it in a message.
846
+ *
847
+ * The default source is the one that stores this project's collections, so it
848
+ * is the one asked to create them; `dataSources[0]` is the bundle path's
849
+ * equivalent of the same choice.
850
+ *
851
+ * `driverResult` is deliberately left undefined here. The hooks run BEFORE
852
+ * `initializeDriver`, so there is no driver result to pass, and unlike the
853
+ * bundle path — where the coordinator opens the connection itself — an app that
854
+ * built its own adapter never handed the framework a connection handle. The
855
+ * adapter has one: it was constructed with it. So the contract is that an
856
+ * adapter falls back to its own connection when no result is supplied, which is
857
+ * the only shape that works for both paths.
858
+ */
859
+ function provisionTargetFor(bootstrappers, adapter, driverResult) {
860
+ const primary = bootstrappers.find((b) => b.isDefault) ?? bootstrappers[0];
861
+ return {
862
+ driverPackage: void 0,
863
+ engine: primary?.type ?? adapter?.type ?? "unknown",
864
+ bootstrapper: primary ?? {},
865
+ driverResult
866
+ };
867
+ }
868
+ //#endregion
529
869
  //#region src/services/driver-registry.ts
530
870
  /**
531
871
  * The default driver identifier used when:
@@ -1364,6 +1704,25 @@ var RestApiGenerator = class {
1364
1704
  const total = await this.countRawEntities(driver, resolvedCollection, queryOptions, searchString);
1365
1705
  return c.json({ count: total });
1366
1706
  });
1707
+ this.router.get(`${basePath}/aggregate`, async (c) => {
1708
+ this.enforceApiKeyPermission(c, collection.slug);
1709
+ const queryDict = c.req.queries();
1710
+ const queryOptions = this.parseQuery(queryDict);
1711
+ const searchString = Array.isArray(queryDict.searchString) ? queryDict.searchString[queryDict.searchString.length - 1] : void 0;
1712
+ const aggregates = parseAggregateSelect(queryDict.select);
1713
+ if (!aggregates) throw ApiError.badRequest("`select` is required, e.g. `?select=count()` or `?select=sum(total)&groupBy=status`.", "MISSING_AGGREGATE_SELECT");
1714
+ const fetchService = this.getScopedDriver(c).restFetchService;
1715
+ if (!fetchService?.aggregate) throw new ApiError(501, "AGGREGATE_NOT_SUPPORTED", "Aggregates are not implemented for this backend's data driver.");
1716
+ const data = await fetchService.aggregate(collection.slug, {
1717
+ aggregates,
1718
+ groupBy: parseGroupBy(queryDict.groupBy),
1719
+ filter: queryOptions.where,
1720
+ logical: queryOptions.logical,
1721
+ searchString,
1722
+ limit: queryOptions.limit
1723
+ });
1724
+ return c.json({ data });
1725
+ });
1367
1726
  this.router.get(basePath, async (c) => {
1368
1727
  this.enforceApiKeyPermission(c, collection.slug);
1369
1728
  const queryDict = c.req.queries();
@@ -3587,7 +3946,7 @@ function assertStorageAccessControlConfigured(state, isProduction) {
3587
3946
  */
3588
3947
  async function mountOpenApiDocs(app, basePath, enableSwagger, serverCollections, requireAuth) {
3589
3948
  if (enableSwagger === false || serverCollections.length === 0) return;
3590
- const { generateOpenApiSpec } = await import("./openapi-generator-BWL8F2La.js");
3949
+ const { generateOpenApiSpec } = await import("./openapi-generator-DQeQ_q2f.js");
3591
3950
  app.get(`${basePath}/docs`, (c) => {
3592
3951
  const spec = generateOpenApiSpec(serverCollections, {
3593
3952
  basePath,
@@ -3740,6 +4099,10 @@ function createShutdown(config) {
3740
4099
  config.cronScheduler.stop();
3741
4100
  logger.info("Cron scheduler stopped");
3742
4101
  }
4102
+ if (config.jobQueue) {
4103
+ await config.jobQueue.stop();
4104
+ logger.info("Job queue stopped");
4105
+ }
3743
4106
  for (const [key, rt] of Object.entries(config.realtimeServices)) try {
3744
4107
  if (typeof rt.destroy === "function") {
3745
4108
  await rt.destroy();
@@ -3764,7 +4127,43 @@ function createShutdown(config) {
3764
4127
  };
3765
4128
  }
3766
4129
  //#endregion
3767
- //#region src/init/process-safety.ts
4130
+ //#region src/init/surfaces.ts
4131
+ /** Every surface, in a stable order. */
4132
+ var ALL_RUNTIME_SURFACES = [
4133
+ "auth",
4134
+ "data",
4135
+ "storage",
4136
+ "admin",
4137
+ "functions",
4138
+ "cron",
4139
+ "meta"
4140
+ ];
4141
+ /**
4142
+ * Resolve a partial surface set against the default, which is everything on.
4143
+ *
4144
+ * Defaulting to on rather than off is the whole compatibility story: a caller
4145
+ * that says nothing gets the process this server has always booted, and a
4146
+ * surface added later is mounted by every existing deployment without anyone
4147
+ * editing a list.
4148
+ */
4149
+ function resolveSurfaces(options) {
4150
+ const resolved = {};
4151
+ for (const surface of ALL_RUNTIME_SURFACES) resolved[surface] = options?.[surface] ?? true;
4152
+ return resolved;
4153
+ }
4154
+ /** Resolve partial ownership against the default, which is owning everything. */
4155
+ function resolveOwnership(options) {
4156
+ return {
4157
+ cronScheduler: options?.cronScheduler ?? true,
4158
+ jobWorkers: options?.jobWorkers ?? true
4159
+ };
4160
+ }
4161
+ /** The surfaces a process is not serving, for a boot log line. */
4162
+ function disabledSurfaces(resolved) {
4163
+ return ALL_RUNTIME_SURFACES.filter((surface) => !resolved[surface]);
4164
+ }
4165
+ //#endregion
4166
+ //#region src/init/process-safety.ts
3768
4167
  /**
3769
4168
  * Installed once per process. The singleton is process-global for the same
3770
4169
  * reason (see `singleton.ts`): more than one copy of this module can be loaded
@@ -7876,6 +8275,13 @@ function matchesParams(row, params) {
7876
8275
  * Sort in place, Postgres-style: nulls last ascending, first descending, with
7877
8276
  * the row id as a tiebreak so paging through an unsorted-but-equal run does
7878
8277
  * not shuffle rows between pages.
8278
+ *
8279
+ * The tiebreak runs *descending*, which is not a taste: every server-side sort
8280
+ * ends on `id DESC` — `FetchService.buildOrderExpressions` appends it to make
8281
+ * the ordering total, and the keyset cursor is built to match. This ran
8282
+ * ascending, so two rows sharing a sort value came back from the local overlay
8283
+ * in the opposite order to the server's, and {@link isLocallySortable} called
8284
+ * that page exactly reproducible while it was not.
7879
8285
  */
7880
8286
  function sortRows(rows, orderBy) {
7881
8287
  const keys = normalizeOrderBy(orderBy);
@@ -7902,8 +8308,10 @@ function compareOnKey(a, b, field, direction) {
7902
8308
  if (cmp === void 0 || cmp === 0) return 0;
7903
8309
  return cmp * (direction === "desc" ? -1 : 1);
7904
8310
  }
8311
+ /** The last word, and the server's: `id DESC`. */
7905
8312
  function tiebreak(a, b) {
7906
- return compareValues(a.id, b.id) ?? 0;
8313
+ const cmp = compareValues(a.id, b.id);
8314
+ return cmp === void 0 ? 0 : -cmp;
7907
8315
  }
7908
8316
  /**
7909
8317
  * Resolve `page`/`offset`/`limit` the way the server does.
@@ -9934,6 +10342,13 @@ async function _initializeRebaseBackend(config) {
9934
10342
  installUnhandledRejectionHandler();
9935
10343
  const basePath = config.basePath || "/api";
9936
10344
  const isProduction = process.env.NODE_ENV === "production";
10345
+ const surfaces = resolveSurfaces(config.surfaces);
10346
+ const ownership = resolveOwnership(config.ownership);
10347
+ const offSurfaces = disabledSurfaces(surfaces);
10348
+ if (offSurfaces.length > 0) logger.info("Partial runtime surface — some routes are not served by this process", {
10349
+ serving: ALL_RUNTIME_SURFACES.filter((surface) => surfaces[surface]),
10350
+ notServing: offSurfaces
10351
+ });
9937
10352
  configureMiddlewares(config.app, basePath, isProduction, config);
9938
10353
  const collectionRegistry = new BackendCollectionRegistry();
9939
10354
  const dataSourceRegistry = createDataSourceRegistry(config.dataSources);
@@ -9962,6 +10377,16 @@ async function _initializeRebaseBackend(config) {
9962
10377
  if (bootstrappers.length === 0) throw new Error("No bootstrappers or database adapter provided. Cannot initialize database drivers.");
9963
10378
  let defaultDriverId = DEFAULT_DRIVER_ID;
9964
10379
  let defaultDriverResult = void 0;
10380
+ const provisionTarget = provisionTargetFor(bootstrappers, config.database, config.provisioningDriverResult);
10381
+ const provisionable = collectionsStoredBy(activeCollections, { engine: provisionTarget.engine }, (config.dataSources ?? []).map((source) => ({
10382
+ key: source.key,
10383
+ engine: source.engine ?? provisionTarget.engine
10384
+ })));
10385
+ logForeignCollections(activeCollections, provisionable, { engine: provisionTarget.engine });
10386
+ const schemaOutcome = await provisionCollectionTables(provisionable, provisionTarget, {
10387
+ introspecting: introspectCollections,
10388
+ provision: config.provisionSchema ?? true
10389
+ });
9965
10390
  for (const bootstrapper of bootstrappers) {
9966
10391
  const b = bootstrapper;
9967
10392
  logger.debug("Running bootstrapper for driver", { driverId: b.id || bootstrapper.type });
@@ -9970,7 +10395,11 @@ async function _initializeRebaseBackend(config) {
9970
10395
  collections: activeCollections,
9971
10396
  collectionRegistry,
9972
10397
  introspectCollections,
9973
- baas: config.baas
10398
+ baas: config.baas,
10399
+ schemaProvisioning: {
10400
+ attempted: schemaOutcome.status === "applied",
10401
+ reason: schemaOutcome.status === "skipped" ? schemaOutcome.reason : void 0
10402
+ }
9974
10403
  });
9975
10404
  delegates[b.id || bootstrapper.type] = driverResult.driver;
9976
10405
  if (introspectCollections) {
@@ -10045,7 +10474,9 @@ async function _initializeRebaseBackend(config) {
10045
10474
  if (safeAuthConfig.jwtSecret) configureJwt({
10046
10475
  secret: safeAuthConfig.jwtSecret,
10047
10476
  accessExpiresIn: safeAuthConfig.accessExpiresIn || "1h",
10048
- refreshExpiresIn: safeAuthConfig.refreshExpiresIn || "30d"
10477
+ refreshExpiresIn: safeAuthConfig.refreshExpiresIn || "30d",
10478
+ signingKeys: safeAuthConfig.signingKeys,
10479
+ activeKid: safeAuthConfig.activeKid
10049
10480
  });
10050
10481
  if (safeAuthConfig.serviceKey) {
10051
10482
  if (safeAuthConfig.serviceKey.length < 32) throw new Error("REBASE_SERVICE_KEY is too short. Must be at least 32 characters. Generate one with: node -e \"logger.info(require('crypto').randomBytes(48).toString('base64'))\"");
@@ -10058,6 +10489,10 @@ async function _initializeRebaseBackend(config) {
10058
10489
  logger.info("Authentication initialized");
10059
10490
  } else logger.warn("Auth requested but default bootstrapper does not support initializeAuth");
10060
10491
  }
10492
+ await provisionCollectionPolicies(provisionable, provisionTarget, {
10493
+ introspecting: introspectCollections,
10494
+ provision: config.provisionSchema ?? true
10495
+ });
10061
10496
  let historyConfigResult = void 0;
10062
10497
  if (config.history) if (defaultBootstrapper.initializeHistory) {
10063
10498
  logger.debug("Bootstrapping entity history via driver protocol");
@@ -10082,8 +10517,8 @@ async function _initializeRebaseBackend(config) {
10082
10517
  store: apiKeyStore,
10083
10518
  driver: defaultDriver
10084
10519
  }) : void 0;
10085
- if (apiKeyPreAuth) config.app.use(`${basePath}/admin/*`, apiKeyPreAuth);
10086
- if (apiKeyStore) {
10520
+ if (apiKeyPreAuth && surfaces.admin) config.app.use(`${basePath}/admin/*`, apiKeyPreAuth);
10521
+ if (apiKeyStore && surfaces.admin) {
10087
10522
  const apiKeyRoutes = createApiKeyRoutes({
10088
10523
  store: apiKeyStore,
10089
10524
  serviceKey: internalServiceKey
@@ -10173,7 +10608,7 @@ async function _initializeRebaseBackend(config) {
10173
10608
  ]) {
10174
10609
  const providerConfig = safeAuthConfig[key];
10175
10610
  if (providerConfig && requiredFields.every((f) => Boolean(providerConfig[f]))) {
10176
- const createFn = (await import("./auth-BQcdhMBL.js").then((n) => n.t))[factory];
10611
+ const createFn = (await import("./auth-BobZVd0j.js").then((n) => n.t))[factory];
10177
10612
  oauthProviders.push(createFn(providerConfig));
10178
10613
  }
10179
10614
  }
@@ -10200,14 +10635,14 @@ async function _initializeRebaseBackend(config) {
10200
10635
  if (!isProduction && !process.env.CORS_ORIGINS && !process.env.FRONTEND_URL) logger.warn("[Auth] Cookie authentication (cookieAuth) is enabled, but no CORS restrictions are detected. Browser-based clients will require credentials: 'include' and the server MUST NOT use Access-Control-Allow-Origin: '*'. Ensure CORS_ORIGINS is set to your frontend URL.");
10201
10636
  }
10202
10637
  }
10203
- if (authAdapter && authAdapter.createAuthRoutes) {
10638
+ if (authAdapter && authAdapter.createAuthRoutes && surfaces.auth) {
10204
10639
  const authRoutes = authAdapter.createAuthRoutes();
10205
10640
  if (authRoutes) {
10206
10641
  config.app.route(`${basePath}/auth`, authRoutes);
10207
10642
  logger.debug("Auth routes mounted via adapter", { adapter: authAdapter.id });
10208
10643
  }
10209
10644
  }
10210
- if (authAdapter && authAdapter.createAdminRoutes) {
10645
+ if (authAdapter && authAdapter.createAdminRoutes && surfaces.admin) {
10211
10646
  const adminRoutes = authAdapter.createAdminRoutes();
10212
10647
  if (adminRoutes) {
10213
10648
  config.app.route(`${basePath}/admin`, adminRoutes);
@@ -10215,6 +10650,10 @@ async function _initializeRebaseBackend(config) {
10215
10650
  }
10216
10651
  }
10217
10652
  }
10653
+ if (isJwtConfigured() && surfaces.auth) {
10654
+ config.app.route("/.well-known", createJwksRoutes());
10655
+ if (hasAsymmetricSigningKey()) logger.info("Access tokens are signed asymmetrically; public keys at /.well-known/jwks.json");
10656
+ }
10218
10657
  const adminSurfacesGated = !!authAdapter && (isAuthAdapter(config.auth) || !!config.auth.jwtSecret);
10219
10658
  const applyAdminGate = (router, surface) => {
10220
10659
  if (!adminSurfacesGated) {
@@ -10279,7 +10718,7 @@ async function _initializeRebaseBackend(config) {
10279
10718
  logger.warn(`Schema Editor disabled: ${schemaEditorOff.message}`);
10280
10719
  } else throw err;
10281
10720
  }
10282
- {
10721
+ if (surfaces.admin) {
10283
10722
  const schemaEditorRouter = new Hono();
10284
10723
  applyAdminGate(schemaEditorRouter, "Schema editor");
10285
10724
  schemaEditorRouter.get("/status", (c) => c.json(schemaEditorOff ? {
@@ -10330,15 +10769,17 @@ async function _initializeRebaseBackend(config) {
10330
10769
  }
10331
10770
  }));
10332
10771
  storageRouter.route("/", storageRoutes);
10333
- config.app.route(`${basePath}/storage`, storageRouter);
10772
+ if (surfaces.storage) config.app.route(`${basePath}/storage`, storageRouter);
10334
10773
  } else {
10335
10774
  const storageStub = new Hono();
10336
10775
  storageStub.all("/*", (c) => c.json({ error: {
10337
10776
  message: "File storage is not configured on this deployment, so uploads and downloads are disabled. Configure a storage backend (STORAGE_TYPE=s3 or STORAGE_TYPE=gcs plus its bucket and credentials) and redeploy.",
10338
10777
  code: "STORAGE_NOT_CONFIGURED"
10339
10778
  } }, 501));
10340
- config.app.route(`${basePath}/storage`, storageStub);
10341
- logger.info("Storage not configured — /storage returns 501 STORAGE_NOT_CONFIGURED");
10779
+ if (surfaces.storage) {
10780
+ config.app.route(`${basePath}/storage`, storageStub);
10781
+ logger.info("Storage not configured — /storage returns 501 STORAGE_NOT_CONFIGURED");
10782
+ }
10342
10783
  }
10343
10784
  const serverCollections = activeCollections.filter((collection) => resolveDataSource(collection, dataSourceRegistry).transport === "server");
10344
10785
  if (activeCollections.length > 0) {
@@ -10387,9 +10828,9 @@ async function _initializeRebaseBackend(config) {
10387
10828
  }
10388
10829
  const restGenerator = new RestApiGenerator(serverCollections, defaultDriver, authAdapter);
10389
10830
  dataRouter.route("/", restGenerator.generateRoutes());
10390
- config.app.route(`${basePath}/data`, dataRouter);
10831
+ if (surfaces.data) config.app.route(`${basePath}/data`, dataRouter);
10391
10832
  }
10392
- await mountOpenApiDocs(config.app, basePath, config.enableSwagger, serverCollections, resolveRequireAuth(config.auth));
10833
+ if (surfaces.data) await mountOpenApiDocs(config.app, basePath, config.enableSwagger, serverCollections, resolveRequireAuth(config.auth));
10393
10834
  const serverClient = createRebaseClient({
10394
10835
  baseUrl: "http://localhost",
10395
10836
  apiPath: basePath,
@@ -10448,11 +10889,17 @@ async function _initializeRebaseBackend(config) {
10448
10889
  const driver = defaultDriverResult.internals.driver;
10449
10890
  if (driver && "client" in driver) driver.client = serverClient;
10450
10891
  }
10451
- if (config.functionsDir) {
10892
+ if (config.functionsDir && surfaces.functions) {
10452
10893
  const { loadFunctionsWithDiagnostics } = await import("./function-loader-DDS1v7YX.js").then((n) => n.t);
10453
10894
  const { createFunctionRoutes } = await import("./function-routes-Btcez1T-.js").then((n) => n.n);
10454
10895
  const { createFunctionsRequestTimeout, resolveFunctionsTimeoutMs } = await import("./request-timeout-BuFoEKwT.js");
10455
- const { functions: loadedFunctions, problems } = await loadFunctionsWithDiagnostics(config.functionsDir);
10896
+ const { functions: allFunctions, problems } = await loadFunctionsWithDiagnostics(config.functionsDir);
10897
+ const { selectFunctions } = await import("./selection-_z6TM1DB.js").then((n) => n.n);
10898
+ const loadedFunctions = selectFunctions(allFunctions, config.functionsSelection);
10899
+ if (loadedFunctions.length !== allFunctions.length) logger.info("Serving a subset of this bundle's functions", {
10900
+ serving: loadedFunctions.map((fn) => fn.name),
10901
+ notServing: allFunctions.filter((fn) => !loadedFunctions.includes(fn)).map((fn) => fn.name)
10902
+ });
10456
10903
  const functionsRouter = new Hono();
10457
10904
  functionsRouter.onError(errorHandler);
10458
10905
  const functionsTimeoutMs = resolveFunctionsTimeoutMs(config.functionsTimeoutMs);
@@ -10485,12 +10932,23 @@ async function _initializeRebaseBackend(config) {
10485
10932
  });
10486
10933
  else logger.warn(`Function routes mounted at ${basePath}/functions, but no functions loaded from ${config.functionsDir}. ` + (problems.length > 0 ? `${problems.length} file(s) were skipped — see the messages above.` : "The directory holds no .ts/.js function files."));
10487
10934
  }
10935
+ if (config.functionsUpstream && !surfaces.functions) {
10936
+ const { createFunctionsProxy } = await import("./proxy-Bj5DVllb.js");
10937
+ config.app.route(`${basePath}/functions`, createFunctionsProxy({
10938
+ upstream: config.functionsUpstream,
10939
+ basePath: `${basePath}/functions`
10940
+ }));
10941
+ logger.info("Forwarding custom functions to another process", {
10942
+ path: `${basePath}/functions`,
10943
+ upstream: config.functionsUpstream
10944
+ });
10945
+ }
10488
10946
  let cronScheduler;
10489
- if (config.cronsDir) {
10947
+ if (config.cronsDir && (surfaces.cron || ownership.cronScheduler)) {
10490
10948
  const { loadCronJobsWithDiagnostics } = await import("./cron-loader-YhhQeVBM.js").then((n) => n.t);
10491
10949
  const { CronScheduler } = await import("./cron-scheduler-DIpYBmZP.js").then((n) => n.n);
10492
10950
  const { createCronRoutes } = await import("./cron-routes-maM_RlUu.js").then((n) => n.n);
10493
- const { createCronStore } = await import("./cron-store-D5dUNviq.js").then((n) => n.n);
10951
+ const { createCronStore } = await import("./cron-store-CB1x-Ken.js").then((n) => n.n);
10494
10952
  const { jobs: loadedCronJobs, problems: cronProblems } = await loadCronJobsWithDiagnostics(config.cronsDir);
10495
10953
  cronScheduler = new CronScheduler();
10496
10954
  cronScheduler.setClient(serverSingleton);
@@ -10505,8 +10963,12 @@ async function _initializeRebaseBackend(config) {
10505
10963
  const cronRouter = new Hono();
10506
10964
  applyAdminGate(cronRouter, "Cron");
10507
10965
  cronRouter.route("/", createCronRoutes(cronScheduler, cronProblems.length));
10508
- config.app.route(`${basePath}/cron`, cronRouter);
10509
- if (loadedCronJobs.length > 0) {
10966
+ if (surfaces.cron) config.app.route(`${basePath}/cron`, cronRouter);
10967
+ if (loadedCronJobs.length > 0 && !ownership.cronScheduler) logger.info("Cron jobs registered but NOT scheduled in this process", {
10968
+ count: loadedCronJobs.length,
10969
+ reason: "this process does not own the cron scheduler"
10970
+ });
10971
+ else if (loadedCronJobs.length > 0) {
10510
10972
  cronScheduler.start();
10511
10973
  logger.info("Mounted cron jobs", {
10512
10974
  count: loadedCronJobs.length,
@@ -10514,7 +10976,20 @@ async function _initializeRebaseBackend(config) {
10514
10976
  });
10515
10977
  } else logger.warn(`Cron routes mounted at ${basePath}/cron, but no jobs loaded from ${config.cronsDir}. ` + (cronProblems.length > 0 ? `Nothing is scheduled — ${cronProblems.length} file(s) were skipped, see the messages above.` : "The directory holds no .ts/.js cron files, so nothing is scheduled."));
10516
10978
  }
10517
- {
10979
+ let jobQueue;
10980
+ if (config.jobs?.enabled) {
10981
+ const { createJobStore, createJobQueue } = await import("./jobs-DR4SjGrD.js").then((n) => n.t);
10982
+ const store = createJobStore(defaultDriver);
10983
+ if (store) {
10984
+ await store.ensureTable();
10985
+ jobQueue = createJobQueue(store, config.jobs);
10986
+ if (ownership.jobWorkers) {
10987
+ jobQueue.start();
10988
+ logger.info("Job queue started", { tasks: Object.keys(config.jobs.tasks ?? {}).length });
10989
+ } else logger.info("Job queue available for enqueue, workers NOT started in this process", { tasks: Object.keys(config.jobs.tasks ?? {}).length });
10990
+ } else logger.warn("Job queue requested but unavailable on this driver — `rebase.jobs.enqueue` will throw, and webhook deliveries fall back to the in-memory queue.");
10991
+ }
10992
+ if (surfaces.admin) {
10518
10993
  const { createBackupRoutes, parseBackupDestination } = await import("./backup-C6ljYVTp.js").then((n) => n.t);
10519
10994
  const backupRouter = new Hono();
10520
10995
  applyAdminGate(backupRouter, "Backup");
@@ -10528,7 +11003,7 @@ async function _initializeRebaseBackend(config) {
10528
11003
  config.app.route(`${basePath}/admin/backups`, backupRouter);
10529
11004
  logger.debug("Backup admin routes mounted", { path: `${basePath}/admin/backups` });
10530
11005
  }
10531
- {
11006
+ if (surfaces.admin) {
10532
11007
  const { default: logsRoutes } = await import("./logs-routes-BYA72C_C.js").then((n) => n.n);
10533
11008
  const logsRouter = new Hono();
10534
11009
  applyAdminGate(logsRouter, "Logs");
@@ -10536,7 +11011,7 @@ async function _initializeRebaseBackend(config) {
10536
11011
  config.app.route(`${basePath}/logs`, logsRouter);
10537
11012
  logger.debug("Logs routes mounted", { path: `${basePath}/logs` });
10538
11013
  }
10539
- {
11014
+ if (surfaces.meta) {
10540
11015
  const { createContractRoutes } = await import("./contract-routes-Bet-eCNJ.js").then((n) => n.t);
10541
11016
  const contractRouter = new Hono();
10542
11017
  if (adminSurfacesGated) {
@@ -10569,6 +11044,7 @@ async function _initializeRebaseBackend(config) {
10569
11044
  const shutdown = createShutdown({
10570
11045
  server: config.server,
10571
11046
  cronScheduler,
11047
+ jobQueue,
10572
11048
  realtimeServices
10573
11049
  });
10574
11050
  /**
@@ -10607,6 +11083,7 @@ async function _initializeRebaseBackend(config) {
10607
11083
  storageController,
10608
11084
  collectionRegistry,
10609
11085
  cronScheduler,
11086
+ jobQueue,
10610
11087
  healthCheck,
10611
11088
  shutdown
10612
11089
  };
@@ -10799,6 +11276,22 @@ var rebaseEnvSchema = object({
10799
11276
  DATABASE_URL: string().url("DATABASE_URL must be a valid URL"),
10800
11277
  ADMIN_CONNECTION_STRING: string().url().optional(),
10801
11278
  JWT_SECRET: string().min(32, "JWT_SECRET must be at least 32 characters long"),
11279
+ /**
11280
+ * PEM private key for signing access tokens asymmetrically, so anything
11281
+ * holding the JWKS can verify a session without being able to mint one.
11282
+ * Optional — without it, tokens stay HS256 and nothing changes.
11283
+ *
11284
+ * Accepts a PEM with real newlines, a PEM with `\n` escapes (how most
11285
+ * secret managers and `.env` files carry a multi-line value), or base64 of
11286
+ * the whole PEM.
11287
+ */
11288
+ JWT_PRIVATE_KEY: string().optional(),
11289
+ /**
11290
+ * Names {@link rebaseEnvSchema.JWT_PRIVATE_KEY} in the token header and in
11291
+ * the JWKS. Rotation depends on the old and new keys being
11292
+ * distinguishable, so change this whenever the key changes.
11293
+ */
11294
+ JWT_KEY_ID: string().default("default"),
10802
11295
  JWT_ACCESS_EXPIRES_IN: string().default("1h"),
10803
11296
  JWT_REFRESH_EXPIRES_IN: string().default("400d"),
10804
11297
  GOOGLE_CLIENT_ID: string().optional(),
@@ -11020,6 +11513,14 @@ async function assertAllowedOutboundUrl(rawUrl, options = {}) {
11020
11513
  }
11021
11514
  //#endregion
11022
11515
  //#region src/services/webhook-service.ts
11516
+ /**
11517
+ * The task name a queued webhook delivery is stored under.
11518
+ *
11519
+ * Namespaced, and frozen: it is written into `rebase.jobs` rows that outlive
11520
+ * the deploy that wrote them, so renaming it would strand every delivery
11521
+ * already queued under the old name until they dead-lettered.
11522
+ */
11523
+ var WEBHOOK_DELIVERY_TASK = "rebase.webhook.deliver";
11023
11524
  /** Read at most this much of a response before cancelling the rest. */
11024
11525
  var MAX_RESPONSE_BYTES = 64 * 1024;
11025
11526
  var WebhookDispatcher = class {
@@ -11073,21 +11574,64 @@ var WebhookDispatcher = class {
11073
11574
  * for a third party. It also means a receiver that is down can no longer
11074
11575
  * fail the customer's write.
11075
11576
  *
11076
- * The cost, stated plainly: the queue is in-process and in-memory. A crash
11077
- * or a deploy between the enqueue and the delivery drops the event, and a
11078
- * receiver may see the notification a few milliseconds before the row it
11079
- * describes is committed. Use {@link flush} on shutdown, and
11080
- * `onDelivery` to record failures.
11577
+ * The cost, when no `jobQueue` is configured: the queue is in-process and
11578
+ * in-memory. A crash or a deploy between the enqueue and the delivery drops
11579
+ * the event, and a receiver may see the notification a few milliseconds
11580
+ * before the row it describes is committed. Use {@link flush} on shutdown,
11581
+ * and `onDelivery` to record failures.
11582
+ *
11583
+ * With `jobQueue` set, only the second half of that remains: the delivery
11584
+ * becomes a row in `rebase.jobs` and survives the crash, the deploy and the
11585
+ * pod being rescheduled.
11081
11586
  */
11082
11587
  enqueueEntityChange(table, event, id, entity, previousEntity) {
11083
11588
  const jobs = this.buildDeliveries(table, event, entity, previousEntity);
11084
11589
  if (jobs.length === 0) return;
11590
+ if (this.options.jobQueue) {
11591
+ for (const job of jobs) this.options.jobQueue.enqueue(WEBHOOK_DELIVERY_TASK, {
11592
+ webhookId: job.webhook.id,
11593
+ event: job.event,
11594
+ payload: job.payload
11595
+ }).catch((error) => {
11596
+ logger.error("[webhooks] Could not queue a delivery; falling back to in-process", { error });
11597
+ this.queue.push(job);
11598
+ this.startDraining();
11599
+ });
11600
+ return;
11601
+ }
11085
11602
  this.queue.push(...jobs);
11086
11603
  this.startDraining();
11087
11604
  }
11088
11605
  /**
11606
+ * Deliver one queued job, as the worker runs it.
11607
+ *
11608
+ * One attempt, and it throws on failure. The retries belong to the queue
11609
+ * now: keeping both would multiply — three in-process attempts inside each
11610
+ * of three job attempts is nine deliveries and about two minutes of holding
11611
+ * a worker slot — and only the queue's retries survive a restart, which is
11612
+ * the whole reason for moving.
11613
+ */
11614
+ async deliverQueuedJob(job) {
11615
+ const webhook = this.webhooks.find((w) => w.id === job.webhookId);
11616
+ if (!webhook) {
11617
+ logger.warn(`[webhooks] Dropping a queued delivery for unknown or disabled webhook "${job.webhookId}"`);
11618
+ return;
11619
+ }
11620
+ const { result, terminal } = await this.deliver(webhook, job.event, job.payload, 1);
11621
+ this.options.onDelivery?.(result);
11622
+ if (result.success) return;
11623
+ if (terminal) {
11624
+ logger.error(`[webhooks] "${webhook.id}" failed permanently: ${result.responseBody.slice(0, 200)}`);
11625
+ return;
11626
+ }
11627
+ throw new Error(`Webhook "${webhook.id}" responded ${result.statusCode}: ${result.responseBody.slice(0, 200)}`);
11628
+ }
11629
+ /**
11089
11630
  * Wait for every queued delivery to finish. For graceful shutdown, and for
11090
11631
  * tests that need to observe what {@link enqueueEntityChange} sent.
11632
+ *
11633
+ * In-process deliveries only — with a `jobQueue` configured there is
11634
+ * nothing here to wait for, because the deliveries are rows.
11091
11635
  */
11092
11636
  async flush() {
11093
11637
  while (this.draining) await this.draining;
@@ -11287,7 +11831,7 @@ function listenWithPortRetry(server, startPort, options) {
11287
11831
  server.once("error", onError);
11288
11832
  server.listen(startPort, host, () => {
11289
11833
  server.removeListener("error", onError);
11290
- resolve(startPort);
11834
+ resolve(boundPort(server) ?? startPort);
11291
11835
  });
11292
11836
  });
11293
11837
  let affinityPort = null;
@@ -11317,14 +11861,15 @@ function listenWithPortRetry(server, startPort, options) {
11317
11861
  attempt++;
11318
11862
  const onListening = () => {
11319
11863
  cleanup();
11864
+ const bound = boundPort(server) ?? port;
11320
11865
  if (portFileDir) {
11321
11866
  try {
11322
11867
  const portFile = path.join(portFileDir, DEV_PORT_FILENAME);
11323
- fs.writeFileSync(portFile, `${port} ${startPort}`, "utf-8");
11868
+ fs.writeFileSync(portFile, `${bound} ${startPort}`, "utf-8");
11324
11869
  } catch {}
11325
- writeStateFile(portFileDir, port, options?.serviceKey);
11870
+ writeStateFile(portFileDir, bound, options?.serviceKey);
11326
11871
  }
11327
- resolve(port);
11872
+ resolve(bound);
11328
11873
  };
11329
11874
  const onError = (err) => {
11330
11875
  cleanup();
@@ -11392,6 +11937,18 @@ function writeStateFile(projectRoot, port, serviceKey) {
11392
11937
  fs.chmodSync(stateFile, 384);
11393
11938
  } catch {}
11394
11939
  }
11940
+ /**
11941
+ * The port a server is actually listening on, or `undefined` if it cannot say.
11942
+ *
11943
+ * `Server.address()` returns a string for a UNIX socket and `null` before the
11944
+ * socket is bound, so neither is a port and both fall back to the caller's own
11945
+ * answer rather than being coerced into a number.
11946
+ */
11947
+ function boundPort(server) {
11948
+ const address = server.address();
11949
+ if (!address || typeof address === "string") return void 0;
11950
+ return address.port;
11951
+ }
11395
11952
  //#endregion
11396
11953
  //#region src/serve-spa.ts
11397
11954
  /**
@@ -11883,7 +12440,64 @@ var bootEnvExtension = object({
11883
12440
  ""
11884
12441
  ]).default("true").transform((v) => v !== "false"),
11885
12442
  /** Comma-separated origins allowed to make credentialed cross-origin calls. */
11886
- CORS_ORIGINS: string().optional()
12443
+ CORS_ORIGINS: string().optional(),
12444
+ /**
12445
+ * Which part of the project this process serves.
12446
+ *
12447
+ * `all` — everything, in one process. The default, and what every existing
12448
+ * deployment gets by setting nothing.
12449
+ *
12450
+ * The other three exist so one image and one bundle can be booted several
12451
+ * times over, each serving a different part: `api` (data, auth, admin,
12452
+ * storage, meta), `functions` (custom functions only), `worker` (no HTTP
12453
+ * surface at all — cron and the job queue).
12454
+ *
12455
+ * See `resolveRole` in `boot/role.ts` for exactly what each one mounts and
12456
+ * owns, and for the combinations that refuse to boot.
12457
+ */
12458
+ REBASE_ROLE: _enum([
12459
+ "all",
12460
+ "api",
12461
+ "functions",
12462
+ "worker",
12463
+ ""
12464
+ ]).default("all").transform((v) => v === "" ? "all" : v),
12465
+ /**
12466
+ * Override whether this process runs the cron scheduler's timers.
12467
+ *
12468
+ * Separate from the role because "which URLs answer" and "which timers fire"
12469
+ * are independent questions: a three-way split wants cron off the `api`
12470
+ * process, a two-way split wants it on. Unset follows the role.
12471
+ */
12472
+ REBASE_CRON_SCHEDULER: _enum([
12473
+ "true",
12474
+ "false",
12475
+ ""
12476
+ ]).optional().transform((v) => v === void 0 || v === "" ? void 0 : v === "true"),
12477
+ /** Override whether this process runs job-queue workers. Unset follows the role. */
12478
+ REBASE_JOB_WORKERS: _enum([
12479
+ "true",
12480
+ "false",
12481
+ ""
12482
+ ]).optional().transform((v) => v === void 0 || v === "" ? void 0 : v === "true"),
12483
+ /**
12484
+ * Comma-separated function names this process serves. Unset means all.
12485
+ *
12486
+ * `functions` role only. A name that is not in the bundle is a boot failure,
12487
+ * not a warning — the deployment exists to serve that function, and a typo
12488
+ * that silently serves nothing is the failure this must not have.
12489
+ */
12490
+ REBASE_FUNCTIONS_ONLY: string().optional(),
12491
+ /** Comma-separated function names to skip. Applied after `_ONLY`. */
12492
+ REBASE_FUNCTIONS_EXCLUDE: string().optional(),
12493
+ /**
12494
+ * Base URL of the process serving `/api/functions/*`.
12495
+ *
12496
+ * `api` role only. Set it and this process forwards those requests there
12497
+ * instead of mounting them, so a split deployment presents the identical URL
12498
+ * surface and nobody has to run a reverse proxy to try one.
12499
+ */
12500
+ REBASE_FUNCTIONS_UPSTREAM: string().optional()
11887
12501
  });
11888
12502
  /**
11889
12503
  * Load and validate the environment for a bundle boot.
@@ -11959,6 +12573,134 @@ function resolveCorsOrigin(env) {
11959
12573
  return (origin) => allowed.includes(origin) ? origin : null;
11960
12574
  }
11961
12575
  //#endregion
12576
+ //#region src/boot/role.ts
12577
+ /**
12578
+ * A refusal to boot, phrased as the variable to change.
12579
+ *
12580
+ * Its own class so `bootFromBundle` can present it as a configuration problem
12581
+ * rather than a crash. Every message names the variable and the fix, because the
12582
+ * reader is looking at a container that will not start and has one line of log
12583
+ * to work from.
12584
+ */
12585
+ var RoleConfigurationError = class extends Error {
12586
+ hint;
12587
+ constructor(message, hint) {
12588
+ super(message);
12589
+ this.hint = hint;
12590
+ this.name = "RoleConfigurationError";
12591
+ }
12592
+ };
12593
+ /**
12594
+ * What each role serves and owns.
12595
+ *
12596
+ * Read as a table, deliberately. `api` keeps cron and the job workers so that
12597
+ * the common two-service split (`api` + `functions`) is complete without a third
12598
+ * container; a deployment that wants them off the request path adds a `worker`
12599
+ * and sets `REBASE_CRON_SCHEDULER=false` on the api. `functions` runs no timers
12600
+ * at all — a function process is scaled by request load and replaced at will, so
12601
+ * giving it scheduled work would make its replica count mean something.
12602
+ */
12603
+ var ROLES = {
12604
+ all: {
12605
+ surfaces: {},
12606
+ ownership: {},
12607
+ provisionSchema: true
12608
+ },
12609
+ api: {
12610
+ surfaces: { functions: false },
12611
+ ownership: {},
12612
+ provisionSchema: true
12613
+ },
12614
+ functions: {
12615
+ surfaces: {
12616
+ auth: false,
12617
+ data: false,
12618
+ storage: false,
12619
+ admin: false,
12620
+ cron: false,
12621
+ meta: false
12622
+ },
12623
+ ownership: {
12624
+ cronScheduler: false,
12625
+ jobWorkers: false
12626
+ },
12627
+ provisionSchema: false
12628
+ },
12629
+ worker: {
12630
+ surfaces: {
12631
+ auth: false,
12632
+ data: false,
12633
+ storage: false,
12634
+ admin: false,
12635
+ functions: false,
12636
+ cron: false,
12637
+ meta: false
12638
+ },
12639
+ ownership: {},
12640
+ provisionSchema: false
12641
+ }
12642
+ };
12643
+ /** Split a comma-separated list, dropping blanks. */
12644
+ function parseNameList(raw) {
12645
+ if (!raw) return [];
12646
+ return raw.split(",").map((name) => name.trim()).filter(Boolean);
12647
+ }
12648
+ /**
12649
+ * Resolve the process shape, or refuse.
12650
+ *
12651
+ * @throws RoleConfigurationError for a combination that would boot into a state
12652
+ * nobody meant — see the refusals inline. Each one is a refusal rather than a
12653
+ * warning because its failure mode is silent: a second process racing schema
12654
+ * DDL, or presence quietly not crossing instances, is not something an
12655
+ * operator finds by reading logs.
12656
+ */
12657
+ function resolveRole(env) {
12658
+ const role = env.REBASE_ROLE ?? "all";
12659
+ const base = ROLES[role];
12660
+ if (!base) throw new RoleConfigurationError(`REBASE_ROLE="${role}" is not a role.`, `Use one of: ${Object.keys(ROLES).join(", ")}.`);
12661
+ const migrate = env.REBASE_MIGRATE_ON_BOOT || "ensure";
12662
+ if (!base.provisionSchema && migrate !== "none") throw new RoleConfigurationError(`REBASE_ROLE="${role}" cannot also provision the database schema (REBASE_MIGRATE_ON_BOOT="${migrate}").`, "Set REBASE_MIGRATE_ON_BOOT=none on this process. Exactly one process in a split deployment provisions the schema, and that is the one running REBASE_ROLE=api (or all).");
12663
+ if (env.REBASE_FUNCTIONS_UPSTREAM && role !== "api") throw new RoleConfigurationError(`REBASE_FUNCTIONS_UPSTREAM is only read by REBASE_ROLE=api, and this process is "${role}".`, role === "functions" ? "A functions process serves those routes; it does not forward them. Set the upstream on the api process instead." : "Remove it, or set REBASE_ROLE=api on this process.");
12664
+ if (Boolean(env.REBASE_FUNCTIONS_ONLY || env.REBASE_FUNCTIONS_EXCLUDE) && role !== "functions") throw new RoleConfigurationError(`REBASE_FUNCTIONS_ONLY / REBASE_FUNCTIONS_EXCLUDE are only read by REBASE_ROLE=functions, and this process is "${role}".`, "Set them on the functions process, or set REBASE_ROLE=functions here.");
12665
+ const ownership = { ...base.ownership };
12666
+ if (env.REBASE_CRON_SCHEDULER !== void 0) ownership.cronScheduler = env.REBASE_CRON_SCHEDULER;
12667
+ if (env.REBASE_JOB_WORKERS !== void 0) ownership.jobWorkers = env.REBASE_JOB_WORKERS;
12668
+ return {
12669
+ role,
12670
+ surfaces: { ...base.surfaces },
12671
+ ownership,
12672
+ provisionSchema: base.provisionSchema,
12673
+ functionsSelection: {
12674
+ only: parseNameList(env.REBASE_FUNCTIONS_ONLY),
12675
+ exclude: parseNameList(env.REBASE_FUNCTIONS_EXCLUDE)
12676
+ },
12677
+ functionsUpstream: env.REBASE_FUNCTIONS_UPSTREAM?.trim() || void 0
12678
+ };
12679
+ }
12680
+ /**
12681
+ * ## Why there is no channel-bus refusal here
12682
+ *
12683
+ * Splitting `api` from `functions` was expected to need one: the in-memory
12684
+ * channel bus is the default, and broadcast and presence do not cross processes
12685
+ * on it. It turns out to be the wrong place to check, for two reasons.
12686
+ *
12687
+ * The first is that a role split does not create the problem. Only the roles
12688
+ * that mount the API serve websockets at all — a `functions` process has no
12689
+ * realtime clients to keep in sync, and a `worker` has no HTTP surface. What
12690
+ * makes the bus matter is the *replica count* of the websocket-serving process,
12691
+ * which is exactly as true for a single `all` deployment scaled to three as it
12692
+ * is for a split one, and which no process can read from its own environment.
12693
+ *
12694
+ * The second is that the runtime already answers it better than a guess from
12695
+ * configuration could: `RealtimeService.warnIfMemoryBusOnMultiplePods` fires on
12696
+ * *evidence* — the first notification seen from another instance while the bus
12697
+ * is still in-memory. That is a fact rather than an inference, and it catches
12698
+ * the scaled-`all` case this would have missed.
12699
+ *
12700
+ * So: no refusal, and the operator guidance belongs in the deployment docs
12701
+ * beside the replica count, not here.
12702
+ */
12703
+ //#endregion
11962
12704
  //#region src/boot/sources.ts
11963
12705
  /**
11964
12706
  * Convert a source key into the suffix used in environment variable names.
@@ -12178,168 +12920,6 @@ function loadDeclaredStorageSources(startDir, levels = 5) {
12178
12920
  return [];
12179
12921
  }
12180
12922
  //#endregion
12181
- //#region src/boot/version-skew.ts
12182
- /**
12183
- * Telling a *stale* driver apart from a driver that never had the feature.
12184
- *
12185
- * The managed runtime is two halves that version independently. The image
12186
- * supplies `@rebasepro/server` — `docker/entrypoint.mjs` symlinks it over
12187
- * whatever the bundle installed, so the boot harness is always the image's. The
12188
- * **driver** is not redirected: `@rebasepro/server-postgres` comes from the
12189
- * bundle's `deps.declared`, pinned to whatever the project's `package.json`
12190
- * asked npm for. Shipping a new image therefore does not ship a new driver, and
12191
- * a fix that spans both packages reaches a tenant only after an npm publish AND
12192
- * a rebuild.
12193
- *
12194
- * That split has a nasty failure mode, and it has already burned a day. Boot
12195
- * looks for an optional capability, does not find it, and reports the honest
12196
- * local fact — "this driver does not implement collection-table creation" —
12197
- * which is exactly what a schemaless driver looks like. But a driver that is
12198
- * merely *older than the runtime asking* looks identical, and the two want
12199
- * opposite responses: one is "this database is not managed by Rebase, carry on",
12200
- * the other is "your tables were never created and every data route is about to
12201
- * 500". Naming the versions is what separates them, so these helpers exist to
12202
- * put both numbers in front of whoever reads the log.
12203
- */
12204
- /** The package whose version defines "the runtime" for skew purposes. */
12205
- var RUNTIME_PACKAGE = "@rebasepro/server";
12206
- /**
12207
- * Locate a package by walking `node_modules` up from a directory.
12208
- *
12209
- * Lives here rather than in `driver.ts` because both the driver loader and the
12210
- * version check need it, and `driver.ts` already imports this module.
12211
- */
12212
- function findPackageDir(fromDir, packageName) {
12213
- let dir = path.resolve(fromDir);
12214
- for (;;) {
12215
- const candidate = path.join(dir, "node_modules", ...packageName.split("/"));
12216
- if (fs.existsSync(path.join(candidate, "package.json"))) return candidate;
12217
- const parent = path.dirname(dir);
12218
- if (parent === dir) return void 0;
12219
- dir = parent;
12220
- }
12221
- }
12222
- /**
12223
- * Parse the subset of semver the workspace actually publishes.
12224
- *
12225
- * Deliberately lenient: an unparseable version yields `undefined` and every
12226
- * comparison then declines to judge, because a wrong "your driver is old"
12227
- * warning on a fork's custom version string is worse than no warning at all.
12228
- */
12229
- function parseVersion(version) {
12230
- const match = /^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/.exec(version.trim());
12231
- if (!match) return void 0;
12232
- return {
12233
- parts: [
12234
- Number(match[1]),
12235
- Number(match[2]),
12236
- Number(match[3])
12237
- ],
12238
- prerelease: match[4]
12239
- };
12240
- }
12241
- /**
12242
- * Compare two versions, or `undefined` when either cannot be parsed.
12243
- *
12244
- * Follows semver on the one rule that matters here: a prerelease sorts *below*
12245
- * the release it leads to, so `0.12.1-canary.g7f1150f` < `0.12.1`. Without that
12246
- * rule every canary would read as newer than the stable it precedes, and the
12247
- * canaries are precisely where fixes land first.
12248
- */
12249
- function compareVersions(a, b) {
12250
- const left = parseVersion(a);
12251
- const right = parseVersion(b);
12252
- if (!left || !right) return void 0;
12253
- for (let i = 0; i < 3; i++) if (left.parts[i] !== right.parts[i]) return left.parts[i] < right.parts[i] ? -1 : 1;
12254
- if (left.prerelease === right.prerelease) return 0;
12255
- if (left.prerelease === void 0) return 1;
12256
- if (right.prerelease === void 0) return -1;
12257
- return left.prerelease < right.prerelease ? -1 : 1;
12258
- }
12259
- /**
12260
- * Describe how a driver's version relates to the runtime asking it for work.
12261
- *
12262
- * Returns `stale: false` whenever it cannot tell — an unknown version, an
12263
- * unparseable one, a driver that is newer. Silence beats a confident wrong
12264
- * diagnosis, and the caller still prints its own local fact either way.
12265
- */
12266
- function describeDriverSkew(driverVersion, runtimeVersion) {
12267
- if (!driverVersion || !runtimeVersion) return { stale: false };
12268
- const order = compareVersions(driverVersion, runtimeVersion);
12269
- if (order === void 0) return { stale: false };
12270
- if (order < 0) return {
12271
- stale: true,
12272
- detail: `The driver is at ${driverVersion} while this runtime is ${runtimeVersion} — the driver is OLDER, so this is very likely a stale pin rather than a driver that never had the capability.`
12273
- };
12274
- return {
12275
- stale: false,
12276
- detail: `Driver ${driverVersion}, runtime ${runtimeVersion}.`
12277
- };
12278
- }
12279
- /**
12280
- * How to get a project's schema applied, written once and owned by the runtime.
12281
- *
12282
- * This text lives in `@rebasepro/server` on purpose. The equivalent guidance in
12283
- * the Postgres driver's schema-drift warning can only ever be as current as the
12284
- * driver a tenant pinned — so the tenants who most need to be told "your driver
12285
- * is too old" are exactly the ones whose driver still prints the old advice.
12286
- * The image's copy of this package is never stale, so guidance printed from
12287
- * here always reflects the platform as it is today.
12288
- *
12289
- * Both audiences get a line because the runtime cannot reliably tell which one
12290
- * it is serving, and naming only the pnpm scripts — as this once did everywhere
12291
- * — tells a managed operator to run a command that structurally cannot reach
12292
- * their in-cluster database.
12293
- */
12294
- function schemaRecoveryGuidance(options = {}) {
12295
- const lines = [
12296
- " To apply this project's schema:",
12297
- " • Managed cloud: the runtime applies tables and RLS at boot (unless",
12298
- " REBASE_MIGRATE_ON_BOOT=none). `rebase db push` cannot reach a tenant's",
12299
- " in-cluster database — redeploy instead."
12300
- ];
12301
- if (options.staleDriver) lines.push(" Because the driver is pinned by your bundle, bump the", " `@rebasepro/server-postgres` version in your project's package.json", " and redeploy — a newer platform image alone will NOT update it.");
12302
- lines.push(" • Self-host: run `rebase db push` (dev) or `rebase db migrate` (prod).");
12303
- return lines.join("\n");
12304
- }
12305
- /**
12306
- * The installed version of a package, read from its `package.json`.
12307
- *
12308
- * Returns `undefined` rather than throwing for every failure — a missing or
12309
- * malformed manifest degrades the diagnosis, and must never take down a boot
12310
- * that would otherwise have served.
12311
- */
12312
- function readPackageVersion(packageDir) {
12313
- try {
12314
- const pkg = JSON.parse(fs.readFileSync(path.join(packageDir, "package.json"), "utf8"));
12315
- return typeof pkg.version === "string" ? pkg.version : void 0;
12316
- } catch {
12317
- return;
12318
- }
12319
- }
12320
- /**
12321
- * The version of `@rebasepro/server` this deployment is actually running.
12322
- *
12323
- * Resolved from the same roots the drivers are, and that is the point rather
12324
- * than a convenience. In a managed container `docker/entrypoint.mjs` replaces
12325
- * `/bundle/node_modules/@rebasepro/server` with a link to the image's copy, so
12326
- * reading the manifest *through the bundle* reports the version that will
12327
- * really execute — which is exactly the number the driver has to be compared
12328
- * against. Reading this package's own manifest by module path would report the
12329
- * same thing in the happy case and quietly lie in the case that matters: a
12330
- * bundle whose link did not happen.
12331
- *
12332
- * Returns `undefined` when the package cannot be found; every caller treats
12333
- * that as "cannot judge" and stays quiet.
12334
- */
12335
- function readRuntimeVersion(roots) {
12336
- for (const root of roots) {
12337
- const dir = findPackageDir(root, RUNTIME_PACKAGE);
12338
- const version = dir ? readPackageVersion(dir) : void 0;
12339
- if (version) return version;
12340
- }
12341
- }
12342
- //#endregion
12343
12923
  //#region src/boot/driver.ts
12344
12924
  /**
12345
12925
  * Run a trivial query against a source, to see whether it answers.
@@ -12555,6 +13135,10 @@ function resolveAuthOptions(env, usersCollection) {
12555
13135
  const auth = {
12556
13136
  collection: usersCollection,
12557
13137
  jwtSecret: env.JWT_SECRET,
13138
+ signingKeys: env.JWT_PRIVATE_KEY ? [{
13139
+ kid: env.JWT_KEY_ID,
13140
+ privateKey: normalizePemFromEnv(env.JWT_PRIVATE_KEY)
13141
+ }] : void 0,
12558
13142
  accessExpiresIn: env.JWT_ACCESS_EXPIRES_IN,
12559
13143
  refreshExpiresIn: env.JWT_REFRESH_EXPIRES_IN,
12560
13144
  serviceKey: env.REBASE_SERVICE_KEY,
@@ -12978,6 +13562,11 @@ async function bootFromBundle(options = {}) {
12978
13562
  if (bundle.manifest.kind === "static") return bootStaticApp(bundle, devRoot, options);
12979
13563
  const env = loadBootEnv();
12980
13564
  const isProduction = env.NODE_ENV === "production";
13565
+ const runtimeRole = resolveRole(env);
13566
+ if (runtimeRole.role !== "all") logger.info(`Runtime role: ${runtimeRole.role}`, {
13567
+ provisionsSchema: runtimeRole.provisionSchema,
13568
+ ...runtimeRole.ownership
13569
+ });
12981
13570
  const configExports = await loadBundleConfigExports(bundle);
12982
13571
  const dataSourceDefs = configExports.dataSources;
12983
13572
  const declaredStorage = normalizeStorageSources(bundle.manifest.storage?.sources, configExports.storageSources);
@@ -13001,14 +13590,20 @@ async function bootFromBundle(options = {}) {
13001
13590
  const server = createServer(getRequestListener(app.fetch));
13002
13591
  const usersCollection = await loadUsersCollection(bundle);
13003
13592
  const storage = resolveStorageSources(process.env, storageSourceDefs, path.join(bundle.dir, "uploads"));
13004
- await ensureCollectionSchema(bundle, dataSources, env);
13593
+ warnOnUnusableBundleShape(bundle);
13005
13594
  const backend = await initializeRebaseBackend({
13006
13595
  server,
13007
13596
  app,
13008
13597
  basePath: env.REBASE_BASE_PATH,
13598
+ provisioningDriverResult: dataSources[0] ? { internals: dataSources[0].connection } : void 0,
13009
13599
  collectionsDir: bundle.collectionsDir,
13010
13600
  functionsDir: bundle.functionsDir,
13011
13601
  cronsDir: bundle.cronsDir,
13602
+ provisionSchema: options.provisionSchema ?? runtimeRole.provisionSchema,
13603
+ surfaces: runtimeRole.surfaces,
13604
+ ownership: runtimeRole.ownership,
13605
+ functionsSelection: runtimeRole.functionsSelection,
13606
+ functionsUpstream: runtimeRole.functionsUpstream,
13012
13607
  bootstrappers: dataSources.map((s) => s.bootstrapper),
13013
13608
  dataSources: dataSourceDefs,
13014
13609
  storage,
@@ -13028,7 +13623,6 @@ async function bootFromBundle(options = {}) {
13028
13623
  runtimeVersion: bundle.manifest.runtime?.builtAgainst,
13029
13624
  schemaEditor: false
13030
13625
  });
13031
- await ensureCollectionPolicies(bundle, dataSources, env);
13032
13626
  metrics?.setKnownCollections(backend.collectionRegistry.getCollections().map((collection) => collection.slug).filter((slug) => Boolean(slug)));
13033
13627
  const healthPaths = ["/health", `${(env.REBASE_BASE_PATH || "/api").replace(/\/$/, "")}/health`];
13034
13628
  for (const healthPath of [...new Set(healthPaths)]) app.get(healthPath, async (c) => {
@@ -13067,6 +13661,7 @@ async function bootFromBundle(options = {}) {
13067
13661
  "/health",
13068
13662
  "/livez",
13069
13663
  "/metrics",
13664
+ "/.well-known",
13070
13665
  ...siblings
13071
13666
  ],
13072
13667
  spa: staticApp.spa
@@ -13078,10 +13673,12 @@ async function bootFromBundle(options = {}) {
13078
13673
  server.once("error", reject);
13079
13674
  server.listen(env.PORT, () => {
13080
13675
  server.removeListener("error", reject);
13676
+ const address = server.address();
13677
+ if (address && typeof address === "object") port = address.port;
13081
13678
  resolve();
13082
13679
  });
13083
13680
  });
13084
- logger.info(`Rebase runtime listening on port ${env.PORT}`);
13681
+ logger.info(`Rebase runtime listening on port ${port}`);
13085
13682
  } else {
13086
13683
  port = await listenWithPortRetry(server, env.PORT, {
13087
13684
  portFileDir: devRoot,
@@ -13215,7 +13812,7 @@ async function runFromBundle(options = {}) {
13215
13812
  try {
13216
13813
  return await bootFromBundle(options);
13217
13814
  } catch (err) {
13218
- if (err instanceof BundleError) {
13815
+ if (err instanceof BundleError || err instanceof RoleConfigurationError || err instanceof FunctionSelectionError) {
13219
13816
  logger.error(err.message);
13220
13817
  if (err.hint) logger.error(err.hint);
13221
13818
  } else logger.error("Failed to start the Rebase runtime", { error: err instanceof Error ? err : new Error(String(err)) });
@@ -13247,158 +13844,25 @@ function warnOnDriverSkew(dataSources, runtimeVersion) {
13247
13844
  }
13248
13845
  }
13249
13846
  /**
13250
- * The collections a data source's engine is the store for.
13847
+ * Warn about a bundle whose shape makes provisioning impossible.
13251
13848
  *
13252
- * A bundle's collections directory holds *every* collection the project
13253
- * declares, whatever engine serves it — that is the point of `dataSource`
13254
- * routing. What it is not is a list of tables to create: handing the whole
13255
- * directory to the primary source's bootstrapper made a Firestore collection
13256
- * declared alongside the Postgres ones arrive as an empty Postgres table, with
13257
- * RLS policies, while the app went on reading its documents from Firestore.
13849
+ * `initializeRebaseBackend` provisions the collection schema for every boot
13850
+ * path, and it decides from what actually resolved: a project with no
13851
+ * collections reads them from the database and creates nothing. That rule is
13852
+ * right, but from inside the runtime it cannot tell "this project declares no
13853
+ * collections" from "this build lost them" — and the second is a broken build
13854
+ * that silently serves an empty API.
13258
13855
  *
13259
- * Excluding is deliberately conservative. A collection that names neither an
13260
- * `engine` nor a `dataSource` belongs to whichever source is primary — the
13261
- * "postgres" that `resolveDataSource` falls back to there is a default, not a
13262
- * declaration, and must not exclude anything on its own. Only a collection that
13263
- * explicitly routes to a *different* engine is dropped, so a project running two
13264
- * sources on the same engine is unaffected.
13856
+ * Only the bundle knows the difference, so this is the one piece of the old
13857
+ * bundle-side provisioning worth keeping here. It warns; it never provisions.
13265
13858
  */
13266
- function collectionsStoredBy(collections, primary, dataSources) {
13267
- const registry = createDataSourceRegistry(dataSources.map((source) => ({
13268
- key: source.key,
13269
- engine: source.engine
13270
- })));
13271
- return collections.filter((collection) => {
13272
- const declared = collection.engine ?? (collection.dataSource ? resolveDataSource(collection, registry).engine : void 0);
13273
- if (!declared) return true;
13274
- return declared === primary.engine;
13275
- });
13276
- }
13277
- /** Say what was routed elsewhere, so a missing table is never a silent one. */
13278
- function logForeignCollections(all, stored, primary) {
13279
- if (stored.length === all.length) return;
13280
- const foreign = all.filter((c) => !stored.includes(c)).map((c) => c.slug);
13281
- logger.info(`Skipping ${foreign.length} collection(s) served by another engine, not "${primary.engine}": ${foreign.join(", ")}. Their storage is not managed by this data source.`);
13282
- }
13283
- /**
13284
- * Bring the database's collection tables up to date before serving.
13285
- *
13286
- * Delegates to whichever driver bootstrapped the default data source; a driver
13287
- * without `ensureCollectionSchema` (a schemaless one, or an older build) skips
13288
- * rather than failing, which is why this cannot break an existing deployment.
13289
- *
13290
- * Every path out of here says why, at info or louder. Guaranteeing the tables
13291
- * exist is this function's entire job, so "it declined, and said nothing" is the
13292
- * one outcome it must never produce: a deployment that skips comes up answering
13293
- * sign-in and 500ing every `/api/data/*` route, and the operator's only evidence
13294
- * is what these lines print. Silence here has already sent one investigation
13295
- * chasing a stale runtime image that was not stale.
13296
- *
13297
- * Failure is fatal on purpose. Booting anyway would produce exactly the state
13298
- * this exists to prevent — an app that answers sign-in and 500s every data
13299
- * request — and a crash-looping pod with the DDL error in its logs is a far
13300
- * better signal than a running one that silently cannot serve.
13301
- */
13302
- async function ensureCollectionSchema(bundle, dataSources, env) {
13303
- const skip = (reason, level = "info") => {
13304
- logger[level](`Collection schema: skipped — ${reason}`);
13305
- };
13306
- if ((env.REBASE_MIGRATE_ON_BOOT || "ensure") === "none") {
13307
- skip("REBASE_MIGRATE_ON_BOOT=none, leaving the database schema untouched.");
13308
- return;
13309
- }
13310
- if (bundle.manifest.kind !== "backend") {
13311
- skip(`this bundle's kind is "${bundle.manifest.kind}", which serves no database.`);
13312
- return;
13313
- }
13314
- if (!bundle.manifest.entry?.config) {
13315
- skip("this bundle declares no config package, so its collections are read from the database rather than from code.");
13316
- return;
13317
- }
13318
- if (!bundle.collectionsDir) {
13319
- skip(`this bundle declares a config package at "${bundle.manifest.entry.config}", but no collections directory resolved inside it. Rebuild with \`rebase build\` and check the manifest's \`entry.collections\`.`, "warn");
13320
- return;
13321
- }
13322
- const primary = dataSources[0];
13323
- if (!primary) {
13324
- skip("no data source was initialized for this runtime.", "warn");
13325
- return;
13326
- }
13327
- if (!primary.bootstrapper.ensureCollectionSchema) {
13328
- const skew = describeDriverSkew(primary.driverVersion, readRuntimeVersion(bundleResolutionRoots(bundle.dir)));
13329
- skip(`the adapter from "${primary.driverPackage}" (engine "${primary.engine}") does not expose collection-table creation. ` + (skew.detail ? `${skew.detail} ` : "") + "The driver package may well implement it on a class the adapter does not forward, so check the adapter's shape before blaming its version. Collection tables will NOT be created, so every /api/data route will fail on a missing relation.\n" + schemaRecoveryGuidance({ staleDriver: skew.stale }), "warn");
13330
- return;
13331
- }
13332
- const loaded = await loadCollectionsFromDirectory(bundle.collectionsDir);
13333
- if (loaded.length === 0) {
13334
- skip(`no collections were loaded from "${bundle.collectionsDir}".`, "warn");
13335
- return;
13336
- }
13337
- const collections = collectionsStoredBy(loaded, primary, dataSources);
13338
- logForeignCollections(loaded, collections, primary);
13339
- if (collections.length === 0) {
13340
- skip(`none of the ${loaded.length} declared collection(s) are stored by the "${primary.engine}" data source.`);
13341
- return;
13342
- }
13343
- const { applied } = await primary.bootstrapper.ensureCollectionSchema(collections, preInitDriverResult(primary), (message) => logger.debug(`schema: ${message}`));
13344
- if (applied > 0) logger.info(`Applied ${applied} additive schema change(s) before boot.`);
13345
- else logger.debug("Collection schema is up to date.");
13346
- }
13347
- /**
13348
- * The `InitializedDriver` to hand a bootstrapper before any driver exists.
13349
- *
13350
- * Both schema hooks are declared to take the result of `initializeDriver`, but
13351
- * they deliberately run *before* it: the tables have to exist before the driver
13352
- * introspects them and registers collections. So there is no real result to
13353
- * pass, and the field the hooks actually read is `internals` — the driver's own
13354
- * opaque handle, which at this point is exactly the connection the coordinator
13355
- * just opened (`{ db, pool }`, where `db` is the drizzle instance).
13356
- *
13357
- * Wrapping it matters: the connection passed *bare* type-checks through any cast
13358
- * and then reads `undefined.db` inside the driver, which surfaces as a boot
13359
- * crash — `TypeError: Cannot read properties of undefined (reading 'db')` — on
13360
- * every project whose driver implements these hooks. The cast is narrowed to the
13361
- * one field a pre-init result cannot honestly supply, rather than `as never`
13362
- * blanketing the whole argument.
13363
- */
13364
- function preInitDriverResult(source) {
13365
- return { internals: source.connection };
13366
- }
13367
- /**
13368
- * Apply the project's RLS policies before serving — the companion to
13369
- * {@link ensureCollectionSchema}, which creates the tables this makes servable.
13370
- *
13371
- * Runs after `initializeRebaseBackend`, not alongside table creation: the
13372
- * generated policies call the `auth.*` helper functions, and `CREATE POLICY`
13373
- * validates those exist, so this cannot run before auth is initialized. The
13374
- * gate conditions mirror `ensureCollectionSchema` (mode, bundle shape, driver
13375
- * support) — and because that function already ran and explained any skip on
13376
- * this same boot, the benign gates here return quietly rather than logging the
13377
- * same reason twice. The one thing it does say out loud is a driver that
13378
- * created tables but cannot apply policies: that is the difference between a
13379
- * served collection and a 401, and it must not pass in silence.
13380
- */
13381
- async function ensureCollectionPolicies(bundle, dataSources, env) {
13382
- if ((env.REBASE_MIGRATE_ON_BOOT || "ensure") === "none") return;
13859
+ function warnOnUnusableBundleShape(bundle) {
13383
13860
  if (bundle.manifest.kind !== "backend") return;
13384
13861
  if (!bundle.manifest.entry?.config) return;
13385
- if (!bundle.collectionsDir) return;
13386
- const primary = dataSources[0];
13387
- if (!primary) return;
13388
- if (!primary.bootstrapper.ensureCollectionPolicies) {
13389
- const skew = describeDriverSkew(primary.driverVersion, readRuntimeVersion(bundleResolutionRoots(bundle.dir)));
13390
- logger.warn(`Collection policies: skipped — the "${primary.driverPackage}" driver (engine "${primary.engine}") does not apply RLS policies at boot. ` + (skew.detail ? `${skew.detail} ` : "") + "Collections are not row-secured until policies are applied — depending on the driver's grants that means reads are denied or that they are unfiltered. Do not serve traffic until this is resolved.\n" + schemaRecoveryGuidance({ staleDriver: skew.stale }));
13391
- return;
13392
- }
13393
- const loaded = await loadCollectionsFromDirectory(bundle.collectionsDir);
13394
- if (loaded.length === 0) return;
13395
- const collections = collectionsStoredBy(loaded, primary, dataSources);
13396
- if (collections.length === 0) return;
13397
- const { applied } = await primary.bootstrapper.ensureCollectionPolicies(collections, preInitDriverResult(primary), (message) => logger.debug(`policies: ${message}`));
13398
- if (applied > 0) logger.info(`Applied ${applied} RLS policy statement(s) before serving.`);
13399
- else logger.debug("RLS policies are up to date.");
13862
+ if (bundle.collectionsDir) return;
13863
+ logger.warn(`This bundle declares a config package at "${bundle.manifest.entry.config}", but no collections directory resolved inside it, so no collections were loaded and no tables will be created. Rebuild with \`rebase build\` and check the manifest's \`entry.collections\`.`);
13400
13864
  }
13401
13865
  //#endregion
13402
- export { ApiError, BlockedUrlError, BundleError, CronScheduler, DEFAULT_DRIVER_ID, DEFAULT_MAX_FILE_SIZE, DEFAULT_STORAGE_ID, DOCUMENT_MIME_TYPES, DefaultDriverRegistry, DefaultStorageRegistry, GCSStorageController, IMAGE_MIME_TYPES, LocalStorageController, MetricsRegistry, RawHtml, S3StorageController, SMTPEmailService, TransformCache, TusHandler, UnknownStorageSourceError, WebhookDispatcher, _resetRebaseMock, _setRebaseMock, applyCollectionDefaults, assertAllowedOutboundUrl, assertCollectionConfigs, assertDistinctSuffixes, assertEmailLinkBases, assertWriteRequestValid, authJwt, authRoles, authUid, blockedAddressReason, bootFromBundle, classifySurface, cleanupDevPortFile, createAppleProvider, createBackupRoutes, createBitbucketProvider, createBuiltinAuthAdapter, createContractRoutes, createCronRoutes, createCronStore, createCustomAuthAdapter, createDiscordProvider, createEmailService, createFacebookProvider, createFunctionRoutes, createGitHubProvider, createGitLabProvider, createGoogleProvider, createHistoryRoutes, createLinkedinProvider, createMetricsMiddleware, createMetricsRoutes, createMicrosoftProvider, createSlackProvider, createSourceBundle, createSpotifyProvider, createStorageController, createStorageRoutes, createTwitterProvider, defineCron, defineFunction, envSuffixForKey, errorHandler, escapeHtml, extractUserFromToken, fileTokenAuth, findCollectionConfigProblems, generateSecurePassword, getEmailVerificationTemplate, getMagicLinkTemplate, getPasswordResetTemplate, getUserInvitationTemplate, getWelcomeEmailTemplate, hashPassword, html, httpMethodToOperation, initializeDataSource, initializeDataSources, initializeRebaseBackend, installShutdownHandlers, isApiKeyToken, isAuthAdapter, isDatabaseAdapter, isLocalhostOrigin, isOperationAllowed, isTransformableImage, listBackupObjects, listenWithPortRetry, loadBootEnv, loadBundle, loadBundleConfigExports, loadBundleSchema, loadCollectionsFromDirectory, loadCronJobsFromDirectory, loadCronJobsWithDiagnostics, loadDeclaredStorageSources, loadEnv, loadFunctionsFromDirectory, loadUsersCollection, logger, oauthCodeFlowSchema, optionalAuth, parseBackupDestination, parseBackupTimestamp, parseTransformOptions, pkceTokenParams, providerVerifiedEmail, queryTokenAuth, raw, readBackupBytes, readBundleManifest, rebase, requireAdmin, requireAuth, resolveAuthHooks, resolveAuthOptions, resolveCorsOrigin, resolveDataSources, resolveEmailLinkBase, resolveEmailOptions, resolveRequireAuth, resolveStorageBackend, resolveStorageSources, runFromBundle, safeCompare, serveSPA, transformImage, tryVerifyOidcIdToken, unknownKeyPolicyFromEnv, validateApiKey, validateCronExpression, validatePasswordStrength, verifyOidcIdToken, verifyPassword };
13866
+ export { ApiError, BlockedUrlError, BundleError, CONCURRENT_DDL_SQLSTATES, CronScheduler, DEFAULT_DRIVER_ID, DEFAULT_MAX_FILE_SIZE, DEFAULT_STORAGE_ID, DOCUMENT_MIME_TYPES, DefaultDriverRegistry, DefaultStorageRegistry, GCSStorageController, IMAGE_MIME_TYPES, LocalStorageController, MetricsRegistry, RawHtml, S3StorageController, SMTPEmailService, TransformCache, TusHandler, UnknownStorageSourceError, WEBHOOK_DELIVERY_TASK, WebhookDispatcher, _resetRebaseMock, _setRebaseMock, applyCollectionDefaults, assertAllowedOutboundUrl, assertCollectionConfigs, assertDistinctSuffixes, assertEmailLinkBases, assertWriteRequestValid, authJwt, authRoles, authUid, blockedAddressReason, bootFromBundle, classifySurface, cleanupDevPortFile, createAppleProvider, createBackupRoutes, createBitbucketProvider, createBuiltinAuthAdapter, createContractRoutes, createCronRoutes, createCronStore, createCustomAuthAdapter, createDdlBootstrapper, createDiscordProvider, createEmailService, createFacebookProvider, createFunctionRoutes, createGitHubProvider, createGitLabProvider, createGoogleProvider, createHistoryRoutes, createJobQueue, createJobStore, createJwksRoutes, createLinkedinProvider, createMetricsMiddleware, createMetricsRoutes, createMicrosoftProvider, createSlackProvider, createSourceBundle, createSpotifyProvider, createStorageController, createStorageRoutes, createTwitterProvider, defaultBackoff, defineCron, defineFunction, envSuffixForKey, errorHandler, escapeHtml, extractUserFromToken, fileTokenAuth, findCollectionConfigProblems, generateSecurePassword, getEmailVerificationTemplate, getJwks, getMagicLinkTemplate, getPasswordResetTemplate, getUserInvitationTemplate, getWelcomeEmailTemplate, hasAsymmetricSigningKey, hashPassword, html, httpMethodToOperation, initializeDataSource, initializeDataSources, initializeRebaseBackend, installShutdownHandlers, isApiKeyToken, isAuthAdapter, isConcurrentDdlRace, isDatabaseAdapter, isDuplicateObjectRace, isLocalhostOrigin, isOperationAllowed, isTransformableImage, listBackupObjects, listenWithPortRetry, loadBootEnv, loadBundle, loadBundleConfigExports, loadBundleSchema, loadCollectionsFromDirectory, loadCronJobsFromDirectory, loadCronJobsWithDiagnostics, loadDeclaredStorageSources, loadEnv, loadFunctionsFromDirectory, loadUsersCollection, logger, oauthCodeFlowSchema, optionalAuth, parseBackupDestination, parseBackupTimestamp, parseTransformOptions, pkceTokenParams, providerVerifiedEmail, queryTokenAuth, raw, readBackupBytes, readBundleManifest, rebase, requireAdmin, requireAuth, resolveAuthHooks, resolveAuthOptions, resolveCorsOrigin, resolveDataSources, resolveEmailLinkBase, resolveEmailOptions, resolveRequireAuth, resolveStorageBackend, resolveStorageSources, runFromBundle, safeCompare, serveSPA, transformImage, tryVerifyOidcIdToken, unknownKeyPolicyFromEnv, validateApiKey, validateCronExpression, validatePasswordStrength, verifyOidcIdToken, verifyPassword };
13403
13867
 
13404
13868
  //# sourceMappingURL=index.es.js.map