@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.
- package/dist/api/rest/query-parser.d.ts +22 -0
- package/dist/auth/index.d.ts +3 -1
- package/dist/auth/jwks-routes.d.ts +17 -0
- package/dist/auth/jwt-keys.d.ts +108 -0
- package/dist/auth/jwt.d.ts +32 -0
- package/dist/{auth-BQcdhMBL.js → auth-BobZVd0j.js} +82 -6
- package/dist/auth-BobZVd0j.js.map +1 -0
- package/dist/boot/boot.d.ts +36 -50
- package/dist/boot/ddl-bootstrap.d.ts +15 -0
- package/dist/boot/env.d.ts +20 -0
- package/dist/boot/provision.d.ts +182 -0
- package/dist/boot/role.d.ts +88 -0
- package/dist/{cron-store-D5dUNviq.js → cron-store-CB1x-Ken.js} +2 -2
- package/dist/{cron-store-D5dUNviq.js.map → cron-store-CB1x-Ken.js.map} +1 -1
- package/dist/{ddl-bootstrap-BhXbTnBl.js → ddl-bootstrap-Cywoj8Ta.js} +40 -2
- package/dist/{ddl-bootstrap-BhXbTnBl.js.map → ddl-bootstrap-Cywoj8Ta.js.map} +1 -1
- package/dist/env.d.ts +2 -0
- package/dist/functions/proxy.d.ts +41 -0
- package/dist/functions/selection.d.ts +45 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.es.js +815 -351
- package/dist/index.es.js.map +1 -1
- package/dist/init/shutdown.d.ts +4 -0
- package/dist/init/surfaces.d.ts +79 -0
- package/dist/init.d.ts +121 -1
- package/dist/jobs/index.d.ts +5 -0
- package/dist/jobs/job-queue.d.ts +14 -0
- package/dist/jobs/job-store.d.ts +22 -0
- package/dist/jobs/types.d.ts +125 -0
- package/dist/jobs-DR4SjGrD.js +326 -0
- package/dist/jobs-DR4SjGrD.js.map +1 -0
- package/dist/{jwt-CYGFT0ih.js → jwt-VJyXTdQQ.js} +152 -6
- package/dist/jwt-VJyXTdQQ.js.map +1 -0
- package/dist/{openapi-generator-BWL8F2La.js → openapi-generator-DQeQ_q2f.js} +45 -1
- package/dist/openapi-generator-DQeQ_q2f.js.map +1 -0
- package/dist/proxy-Bj5DVllb.js +139 -0
- package/dist/proxy-Bj5DVllb.js.map +1 -0
- package/dist/selection-_z6TM1DB.js +64 -0
- package/dist/selection-_z6TM1DB.js.map +1 -0
- package/dist/services/webhook-service.d.ts +43 -5
- package/dist/src-8XDWyDfR.js.map +1 -1
- package/package.json +5 -5
- package/dist/auth-BQcdhMBL.js.map +0 -1
- package/dist/jwt-CYGFT0ih.js.map +0 -1
- 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
|
|
8
|
-
import { C as
|
|
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
|
|
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-
|
|
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-
|
|
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/
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
10341
|
-
|
|
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:
|
|
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-
|
|
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,
|
|
11077
|
-
* or a deploy between the enqueue and the delivery drops
|
|
11078
|
-
* receiver may see the notification a few milliseconds
|
|
11079
|
-
* describes is committed. Use {@link flush} on shutdown,
|
|
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, `${
|
|
11868
|
+
fs.writeFileSync(portFile, `${bound} ${startPort}`, "utf-8");
|
|
11324
11869
|
} catch {}
|
|
11325
|
-
writeStateFile(portFileDir,
|
|
11870
|
+
writeStateFile(portFileDir, bound, options?.serviceKey);
|
|
11326
11871
|
}
|
|
11327
|
-
resolve(
|
|
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
|
-
|
|
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 ${
|
|
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
|
-
*
|
|
13847
|
+
* Warn about a bundle whose shape makes provisioning impossible.
|
|
13251
13848
|
*
|
|
13252
|
-
*
|
|
13253
|
-
*
|
|
13254
|
-
*
|
|
13255
|
-
*
|
|
13256
|
-
*
|
|
13257
|
-
*
|
|
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
|
-
*
|
|
13260
|
-
*
|
|
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
|
|
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 (
|
|
13386
|
-
|
|
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
|