@rebasepro/server 0.14.0 → 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 +37 -1
- package/dist/api/rest/write-validation.d.ts +26 -0
- package/dist/auth/index.d.ts +3 -1
- package/dist/auth/interfaces.d.ts +14 -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 +62 -2
- package/dist/{auth-CYoPVf-E.js → auth-BobZVd0j.js} +142 -167
- 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-Dvr4Y1sZ.js → cron-store-CB1x-Ken.js} +3 -3
- package/dist/{cron-store-Dvr4Y1sZ.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-Cywoj8Ta.js.map +1 -0
- 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 +8 -3
- package/dist/index.es.js +1113 -661
- 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-_IFqfTOg.js → jwt-VJyXTdQQ.js} +447 -11
- package/dist/jwt-VJyXTdQQ.js.map +1 -0
- package/dist/{openapi-generator-DPKtUC9X.js → openapi-generator-DQeQ_q2f.js} +68 -3
- 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/{request-timeout-RivJsME0.js → request-timeout-BuFoEKwT.js} +6 -3
- package/dist/request-timeout-BuFoEKwT.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-C7rkDGxA.js → src-8XDWyDfR.js} +84 -13
- package/dist/src-8XDWyDfR.js.map +1 -0
- package/dist/src-Cz9nMgUR.js.map +1 -1
- package/dist/storage/keys.d.ts +17 -0
- package/dist/storage/routes.d.ts +1 -1
- package/dist/storage/storage-registry.d.ts +46 -4
- package/dist/storage/tus-handler.d.ts +1 -1
- package/package.json +5 -5
- package/dist/auth-CYoPVf-E.js.map +0 -1
- package/dist/ddl-bootstrap-BhXbTnBl.js.map +0 -1
- package/dist/jwt-_IFqfTOg.js.map +0 -1
- package/dist/openapi-generator-DPKtUC9X.js.map +0 -1
- package/dist/request-timeout-RivJsME0.js.map +0 -1
- package/dist/src-C7rkDGxA.js.map +0 -1
package/dist/index.es.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
import process from "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
|
-
import { A as RebaseApiError,
|
|
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
|
|
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";
|
|
8
9
|
import { n as createContractRoutes } from "./contract-routes-Bet-eCNJ.js";
|
|
9
|
-
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";
|
|
10
11
|
import { t as logger } from "./logger-DfvF_8r-.js";
|
|
11
12
|
import { n as errorHandler, t as ApiError } from "./errors-EBYiaJ2E.js";
|
|
12
|
-
import { i as generateDownloadToken, n as configureJwt } from "./jwt-_IFqfTOg.js";
|
|
13
13
|
import { t as logMiddleware } from "./logs-routes-BYA72C_C.js";
|
|
14
14
|
import { n as DOCUMENT_MIME_TYPES, r as IMAGE_MIME_TYPES, t as DEFAULT_MAX_FILE_SIZE } from "./types-DSnOC4mF.js";
|
|
15
15
|
import { t as S3StorageController } from "./S3StorageController-B2EDXXMH.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";
|
|
@@ -49,153 +51,6 @@ import { secureHeaders } from "hono/secure-headers";
|
|
|
49
51
|
import { getRequestListener } from "@hono/node-server";
|
|
50
52
|
import { execFile } from "node:child_process";
|
|
51
53
|
import { promisify as promisify$1 } from "node:util";
|
|
52
|
-
//#region ../types/src/types/storage_source.ts
|
|
53
|
-
/**
|
|
54
|
-
* Describes a named storage backend — a place files live.
|
|
55
|
-
*
|
|
56
|
-
* Declared once and shared front + back: the frontend uses it to decide
|
|
57
|
-
* transport (HTTP proxy vs direct SDK), the backend uses the same `key`
|
|
58
|
-
* to resolve a StorageController, and collection properties reference
|
|
59
|
-
* a definition by its `key` via `StorageConfig.storageSource`.
|
|
60
|
-
*
|
|
61
|
-
* This mirrors the {@link DataSourceDefinition} pattern used for databases.
|
|
62
|
-
*
|
|
63
|
-
* @group Models
|
|
64
|
-
*/
|
|
65
|
-
/**
|
|
66
|
-
* The default storage source key, used when a property does not specify
|
|
67
|
-
* a `storageSource`. Shared by the frontend and backend registries so
|
|
68
|
-
* both agree on "the default storage backend".
|
|
69
|
-
* @group Models
|
|
70
|
-
*/
|
|
71
|
-
var DEFAULT_STORAGE_SOURCE_KEY = "(default)";
|
|
72
|
-
/**
|
|
73
|
-
* The environment-variable suffix for a storage or data source key.
|
|
74
|
-
*
|
|
75
|
-
* `""` for the default source — so a single-bucket project keeps configuring
|
|
76
|
-
* plain `S3_BUCKET` — and `__<KEY>` for every named one, uppercased with
|
|
77
|
-
* non-alphanumerics collapsed to underscores: `media-cdn` → `S3_BUCKET__MEDIA_CDN`.
|
|
78
|
-
*
|
|
79
|
-
* The rule derives the variable name from the declared key rather than
|
|
80
|
-
* discovering keys by scanning the environment. Scanning would have to guess how
|
|
81
|
-
* `S3_BUCKET__MEDIA_CDN` splits into a key; deriving cannot be ambiguous, and a
|
|
82
|
-
* typo surfaces as a missing source at boot instead of a silently ignored
|
|
83
|
-
* variable.
|
|
84
|
-
*
|
|
85
|
-
* It lives in this package, with no dependencies, because four things must agree
|
|
86
|
-
* on it exactly: the CLI (validating a build), the runtime (reading its own
|
|
87
|
-
* environment), the control plane (writing a tenant's Secret), and the docs. A
|
|
88
|
-
* second implementation of a naming convention is a second chance to disagree.
|
|
89
|
-
*
|
|
90
|
-
* @group Models
|
|
91
|
-
*/
|
|
92
|
-
function storageEnvSuffix(key, defaultKey = DEFAULT_STORAGE_SOURCE_KEY) {
|
|
93
|
-
if (!key || key === defaultKey) return "";
|
|
94
|
-
const normalized = key.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
|
|
95
|
-
if (!normalized) throw new Error(`Source key "${key}" cannot be turned into an environment variable name. Use a key containing at least one letter or digit.`);
|
|
96
|
-
return `__${normalized}`;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Two distinct keys that collapse onto the same variable name, or `null`.
|
|
100
|
-
*
|
|
101
|
-
* `media-cdn` and `media_cdn` are different source keys but the same suffix, so
|
|
102
|
-
* without this one of them silently reads the other's configuration. Returns the
|
|
103
|
-
* offending pair rather than throwing, so each caller can raise it in its own
|
|
104
|
-
* idiom — a `BundleError` at boot, a build failure in the CLI, a rejected deploy
|
|
105
|
-
* in a control plane.
|
|
106
|
-
*
|
|
107
|
-
* @group Models
|
|
108
|
-
*/
|
|
109
|
-
function findStorageSuffixCollision(keys, defaultKey = DEFAULT_STORAGE_SOURCE_KEY) {
|
|
110
|
-
const seen = /* @__PURE__ */ new Map();
|
|
111
|
-
for (const key of keys) {
|
|
112
|
-
const suffix = storageEnvSuffix(key, defaultKey);
|
|
113
|
-
const existing = seen.get(suffix);
|
|
114
|
-
if (existing !== void 0 && existing !== key) return {
|
|
115
|
-
a: existing,
|
|
116
|
-
b: key,
|
|
117
|
-
suffix
|
|
118
|
-
};
|
|
119
|
-
seen.set(suffix, key);
|
|
120
|
-
}
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Merge the two places a project may declare storage sources into one list.
|
|
125
|
-
*
|
|
126
|
-
* `rebase.json` is authoritative for every field it states. Config code may add
|
|
127
|
-
* sources it does not mention and fill in fields it left out, but may not
|
|
128
|
-
* contradict it: the manifest is what a host reads to decide which buckets need
|
|
129
|
-
* configuring, and a runtime that quietly disagreed with it would put the
|
|
130
|
-
* console back to describing a topology the tenant does not have — the exact
|
|
131
|
-
* failure this whole mechanism exists to end.
|
|
132
|
-
*
|
|
133
|
-
* Note what is *not* here: no default source is invented when both inputs are
|
|
134
|
-
* empty. That decision belongs to the resolver, which knows whether declaring
|
|
135
|
-
* nothing means "one plain bucket" (it does) or "no storage at all".
|
|
136
|
-
*
|
|
137
|
-
* @group Models
|
|
138
|
-
*/
|
|
139
|
-
function normalizeStorageSources(declared, exported) {
|
|
140
|
-
const merged = /* @__PURE__ */ new Map();
|
|
141
|
-
const declaredEntries = Array.isArray(declared) ? declared.filter((d) => d?.key).map((d) => [d.key, d]) : Object.entries(declared ?? {});
|
|
142
|
-
for (const [key, config] of declaredEntries) merged.set(key, {
|
|
143
|
-
key,
|
|
144
|
-
engine: config.engine,
|
|
145
|
-
transport: config.transport ?? "server",
|
|
146
|
-
...config.label !== void 0 ? { label: config.label } : {}
|
|
147
|
-
});
|
|
148
|
-
for (const definition of exported ?? []) {
|
|
149
|
-
if (!definition?.key) continue;
|
|
150
|
-
const existing = merged.get(definition.key);
|
|
151
|
-
if (!existing) {
|
|
152
|
-
merged.set(definition.key, {
|
|
153
|
-
key: definition.key,
|
|
154
|
-
engine: definition.engine,
|
|
155
|
-
transport: definition.transport ?? "server",
|
|
156
|
-
...definition.label !== void 0 ? { label: definition.label } : {}
|
|
157
|
-
});
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
if (existing.label === void 0 && definition.label !== void 0) existing.label = definition.label;
|
|
161
|
-
}
|
|
162
|
-
return Array.from(merged.values());
|
|
163
|
-
}
|
|
164
|
-
//#endregion
|
|
165
|
-
//#region ../common/src/data/sort-dialect.ts
|
|
166
|
-
/**
|
|
167
|
-
* Sort-order wire codec.
|
|
168
|
-
*
|
|
169
|
-
* This is the ONLY module that knows about the colon-delimited wire format
|
|
170
|
-
* (`"field:direction"`) used in HTTP query parameters.
|
|
171
|
-
* Everything else speaks {@link OrderByTuple} exclusively.
|
|
172
|
-
*
|
|
173
|
-
* Mirrors the filter architecture in `filter-dialect.ts`.
|
|
174
|
-
*
|
|
175
|
-
* @module
|
|
176
|
-
*/
|
|
177
|
-
/**
|
|
178
|
-
* Serialize an {@link OrderByTuple} to the wire format `"field:direction"`.
|
|
179
|
-
*
|
|
180
|
-
* **Runtime tolerance:** if the input is already a well-formed wire string
|
|
181
|
-
* (from an untyped JS caller), it is returned unchanged.
|
|
182
|
-
* This is undocumented tolerance, not public API — don't rely on it.
|
|
183
|
-
*
|
|
184
|
-
* @param orderBy - A canonical `[field, direction]` tuple, or at runtime
|
|
185
|
-
* possibly a pre-serialized string (undocumented tolerance).
|
|
186
|
-
* @returns The wire-format string, or `undefined` if the input is falsy.
|
|
187
|
-
*
|
|
188
|
-
* @remarks
|
|
189
|
-
* Field names containing `:` are representable in the tuple form but
|
|
190
|
-
* **not** on the wire — this is an inherent limitation of the colon-delimited
|
|
191
|
-
* encoding and is not resolved here.
|
|
192
|
-
*/
|
|
193
|
-
function serializeOrderBy(orderBy) {
|
|
194
|
-
if (!orderBy) return void 0;
|
|
195
|
-
if (typeof orderBy === "string") return orderBy;
|
|
196
|
-
return `${orderBy[0]}:${orderBy[1]}`;
|
|
197
|
-
}
|
|
198
|
-
//#endregion
|
|
199
54
|
//#region src/collections/BackendCollectionRegistry.ts
|
|
200
55
|
/**
|
|
201
56
|
* Backend-agnostic collection registry.
|
|
@@ -673,6 +528,344 @@ async function loadCollectionsFromDirectory(source, options = {}) {
|
|
|
673
528
|
return validate(applyCollectionDefaults(collections, await readDefaults(resolved)));
|
|
674
529
|
}
|
|
675
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
|
|
676
869
|
//#region src/services/driver-registry.ts
|
|
677
870
|
/**
|
|
678
871
|
* The default driver identifier used when:
|
|
@@ -1083,6 +1276,35 @@ function projectResponseFields(rows, fields, collection, options) {
|
|
|
1083
1276
|
return projected;
|
|
1084
1277
|
});
|
|
1085
1278
|
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Both write checks, as one call, for a transport that is not the REST router.
|
|
1281
|
+
*
|
|
1282
|
+
* The REST routes run `assertKnownWriteFields` and `assertWriteValuesValid`
|
|
1283
|
+
* back to back on the caller's body, seven times over — and they were the only
|
|
1284
|
+
* place either ran. The WebSocket `SAVE` handler took a client payload straight
|
|
1285
|
+
* to `driver.save`, so the same write arrived validated through one door and
|
|
1286
|
+
* unvalidated through the other: `PATCH /api/data/users/1 { age: 999 }` was a
|
|
1287
|
+
* 400, and the socket wrote it.
|
|
1288
|
+
*
|
|
1289
|
+
* Exported for the sockets in `@rebasepro/server-postgres` and
|
|
1290
|
+
* `@rebasepro/server-mongo`, which are the other request boundaries. Not the
|
|
1291
|
+
* route builder — `index.ts` keeps that internal, and this is a rule rather
|
|
1292
|
+
* than wiring.
|
|
1293
|
+
*
|
|
1294
|
+
* It stays at the boundary rather than moving into the driver deliberately: it
|
|
1295
|
+
* validates what a *caller sent*, before `beforeSave` gets a chance to fill in
|
|
1296
|
+
* or rewrite anything. In-process writes through `rebase.data` are trusted
|
|
1297
|
+
* server code and are not run through it, which is the placement the REST layer
|
|
1298
|
+
* already chose.
|
|
1299
|
+
*
|
|
1300
|
+
* @param values the caller's payload, exactly as it arrived
|
|
1301
|
+
* @param collection resolved from the registry by path — never the copy the
|
|
1302
|
+
* client sent, or the rules would be the caller's to pick
|
|
1303
|
+
*/
|
|
1304
|
+
function assertWriteRequestValid(values, collection) {
|
|
1305
|
+
assertKnownWriteFields(values, collection);
|
|
1306
|
+
assertWriteValuesValid(values, collection);
|
|
1307
|
+
}
|
|
1086
1308
|
//#endregion
|
|
1087
1309
|
//#region src/api/rest/idempotency.ts
|
|
1088
1310
|
/**
|
|
@@ -1482,6 +1704,25 @@ var RestApiGenerator = class {
|
|
|
1482
1704
|
const total = await this.countRawEntities(driver, resolvedCollection, queryOptions, searchString);
|
|
1483
1705
|
return c.json({ count: total });
|
|
1484
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
|
+
});
|
|
1485
1726
|
this.router.get(basePath, async (c) => {
|
|
1486
1727
|
this.enforceApiKeyPermission(c, collection.slug);
|
|
1487
1728
|
const queryDict = c.req.queries();
|
|
@@ -1495,12 +1736,11 @@ var RestApiGenerator = class {
|
|
|
1495
1736
|
logical: queryOptions.logical,
|
|
1496
1737
|
limit: queryOptions.limit,
|
|
1497
1738
|
offset: queryOptions.offset,
|
|
1498
|
-
orderBy: queryOptions.orderBy
|
|
1499
|
-
order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
|
|
1739
|
+
orderBy: orderByEntriesToTuples(queryOptions.orderBy),
|
|
1500
1740
|
searchString,
|
|
1501
1741
|
searchExplain,
|
|
1502
1742
|
vectorSearch: queryOptions.vectorSearch
|
|
1503
|
-
}, queryOptions.include) : await this.fetchRawCollection(driver, resolvedCollection, queryOptions, searchString);
|
|
1743
|
+
}, queryOptions.include) : await this.fetchRawCollection(driver, resolvedCollection, queryOptions, searchString, searchExplain);
|
|
1504
1744
|
const total = await this.countRawEntities(driver, resolvedCollection, queryOptions, searchString);
|
|
1505
1745
|
return c.json({
|
|
1506
1746
|
data: projectResponseFields(entities, queryOptions.fields, resolvedCollection, { include: queryOptions.include }),
|
|
@@ -1829,7 +2069,8 @@ var RestApiGenerator = class {
|
|
|
1829
2069
|
path: parsed.collectionPath,
|
|
1830
2070
|
filter: queryOptions.where,
|
|
1831
2071
|
logical: queryOptions.logical,
|
|
1832
|
-
searchString
|
|
2072
|
+
searchString,
|
|
2073
|
+
vectorSearch: queryOptions.vectorSearch
|
|
1833
2074
|
}) : 0;
|
|
1834
2075
|
return c.json({ count: total });
|
|
1835
2076
|
} else if (parsed.id) {
|
|
@@ -1853,9 +2094,9 @@ var RestApiGenerator = class {
|
|
|
1853
2094
|
logical: queryOptions.logical,
|
|
1854
2095
|
limit: queryOptions.limit,
|
|
1855
2096
|
offset: queryOptions.offset,
|
|
1856
|
-
orderBy: queryOptions.orderBy
|
|
1857
|
-
|
|
1858
|
-
|
|
2097
|
+
orderBy: orderByEntriesToTuples(queryOptions.orderBy),
|
|
2098
|
+
searchString,
|
|
2099
|
+
vectorSearch: queryOptions.vectorSearch
|
|
1859
2100
|
};
|
|
1860
2101
|
const entities = fetchService ? await fetchService.fetchCollectionForRest(parsed.collectionPath, listOptions, queryOptions.include) : await driver.fetchCollection({
|
|
1861
2102
|
path: parsed.collectionPath,
|
|
@@ -1865,7 +2106,8 @@ var RestApiGenerator = class {
|
|
|
1865
2106
|
path: parsed.collectionPath,
|
|
1866
2107
|
filter: queryOptions.where,
|
|
1867
2108
|
logical: queryOptions.logical,
|
|
1868
|
-
searchString
|
|
2109
|
+
searchString,
|
|
2110
|
+
vectorSearch: queryOptions.vectorSearch
|
|
1869
2111
|
}) : entities.length;
|
|
1870
2112
|
const listCollection = this.resolveNestedWriteCollection(parsed.collectionPath);
|
|
1871
2113
|
return c.json({
|
|
@@ -1960,17 +2202,17 @@ var RestApiGenerator = class {
|
|
|
1960
2202
|
/**
|
|
1961
2203
|
* Fetch raw collection data without Entity wrapper (fallback for non-Postgres)
|
|
1962
2204
|
*/
|
|
1963
|
-
async fetchRawCollection(driver, collection, queryOptions, searchString) {
|
|
2205
|
+
async fetchRawCollection(driver, collection, queryOptions, searchString, searchExplain) {
|
|
1964
2206
|
return await driver.fetchCollection({
|
|
1965
2207
|
path: getCollectionDataPath(collection),
|
|
1966
2208
|
collection,
|
|
1967
2209
|
filter: queryOptions.where,
|
|
1968
2210
|
logical: queryOptions.logical,
|
|
1969
2211
|
limit: queryOptions.limit,
|
|
1970
|
-
orderBy: queryOptions.orderBy
|
|
1971
|
-
|
|
1972
|
-
startAfter: queryOptions.offset ? String(queryOptions.offset) : void 0,
|
|
2212
|
+
orderBy: orderByEntriesToTuples(queryOptions.orderBy),
|
|
2213
|
+
offset: queryOptions.offset,
|
|
1973
2214
|
searchString,
|
|
2215
|
+
searchExplain,
|
|
1974
2216
|
vectorSearch: queryOptions.vectorSearch
|
|
1975
2217
|
});
|
|
1976
2218
|
}
|
|
@@ -1983,7 +2225,8 @@ var RestApiGenerator = class {
|
|
|
1983
2225
|
collection,
|
|
1984
2226
|
filter: queryOptions.where,
|
|
1985
2227
|
logical: queryOptions.logical,
|
|
1986
|
-
searchString
|
|
2228
|
+
searchString,
|
|
2229
|
+
vectorSearch: queryOptions.vectorSearch
|
|
1987
2230
|
}) : 0;
|
|
1988
2231
|
}
|
|
1989
2232
|
/**
|
|
@@ -2410,34 +2653,157 @@ var LocalStorageController = class {
|
|
|
2410
2653
|
else items.push(ref);
|
|
2411
2654
|
count++;
|
|
2412
2655
|
}
|
|
2413
|
-
return {
|
|
2414
|
-
items,
|
|
2415
|
-
prefixes,
|
|
2416
|
-
nextPageToken: scanned < entries.length ? String(scanned) : void 0
|
|
2417
|
-
};
|
|
2418
|
-
} catch (error) {
|
|
2419
|
-
const code = error?.code;
|
|
2420
|
-
if (code === "ENOENT" || code === "ENOTDIR") return {
|
|
2421
|
-
items: [],
|
|
2422
|
-
prefixes: []
|
|
2423
|
-
};
|
|
2424
|
-
throw error;
|
|
2656
|
+
return {
|
|
2657
|
+
items,
|
|
2658
|
+
prefixes,
|
|
2659
|
+
nextPageToken: scanned < entries.length ? String(scanned) : void 0
|
|
2660
|
+
};
|
|
2661
|
+
} catch (error) {
|
|
2662
|
+
const code = error?.code;
|
|
2663
|
+
if (code === "ENOENT" || code === "ENOTDIR") return {
|
|
2664
|
+
items: [],
|
|
2665
|
+
prefixes: []
|
|
2666
|
+
};
|
|
2667
|
+
throw error;
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
/**
|
|
2671
|
+
* Get the absolute filesystem path for serving files
|
|
2672
|
+
* Used by the storage routes to serve files directly
|
|
2673
|
+
*/
|
|
2674
|
+
getAbsolutePath(key, bucket) {
|
|
2675
|
+
return this.getFullPath(key, bucket);
|
|
2676
|
+
}
|
|
2677
|
+
/**
|
|
2678
|
+
* Get the base path for the storage
|
|
2679
|
+
*/
|
|
2680
|
+
getBasePath() {
|
|
2681
|
+
return this.basePath;
|
|
2682
|
+
}
|
|
2683
|
+
};
|
|
2684
|
+
//#endregion
|
|
2685
|
+
//#region src/storage/storage-registry.ts
|
|
2686
|
+
/**
|
|
2687
|
+
* Storage Registry
|
|
2688
|
+
*
|
|
2689
|
+
* Manages multiple storage controllers for Rebase backend.
|
|
2690
|
+
* Allows different storage backends for different use cases.
|
|
2691
|
+
*
|
|
2692
|
+
* Usage:
|
|
2693
|
+
* - Single storage: Pass a single StorageController → maps to "(default)"
|
|
2694
|
+
* - Multiple storages: Pass a map of { storageId: StorageController }
|
|
2695
|
+
* - String properties use `storageId` in their config to specify which storage to use
|
|
2696
|
+
* - Properties without `storageId` fallback to "(default)"
|
|
2697
|
+
*/
|
|
2698
|
+
/**
|
|
2699
|
+
* The default storage identifier used when:
|
|
2700
|
+
* - A single storage controller is provided (not a map)
|
|
2701
|
+
* - A property doesn't specify a storageId
|
|
2702
|
+
*
|
|
2703
|
+
* An alias rather than a second `"(default)"` literal: the same string is the
|
|
2704
|
+
* registry key here, the source key a project declares in `rebase.json`, and
|
|
2705
|
+
* the scope baked into a download token. Two literals that must stay equal are
|
|
2706
|
+
* a divergence waiting to happen — and this one would silently unscope tokens.
|
|
2707
|
+
*/
|
|
2708
|
+
var DEFAULT_STORAGE_ID = DEFAULT_STORAGE_SOURCE_KEY;
|
|
2709
|
+
/**
|
|
2710
|
+
* A request named a storage source that is not registered.
|
|
2711
|
+
*
|
|
2712
|
+
* This used to be a `logger.warn` and a fallback to the default source, which
|
|
2713
|
+
* is the wrong answer in a way that is hard to notice: the caller asks for
|
|
2714
|
+
* `media`, the `storageAuthorize` hook is asked about `media`, and the bytes
|
|
2715
|
+
* are read from — or written to — `(default)`. A hook that widens access for
|
|
2716
|
+
* one named source therefore widens it for the default one, and the object the
|
|
2717
|
+
* hook approved is not the object the request touched. That is the same shape
|
|
2718
|
+
* as the non-canonical-key bypass: the check and the read disagree about which
|
|
2719
|
+
* object is in play.
|
|
2720
|
+
*
|
|
2721
|
+
* The failure is silent by construction. Both sources hold the *same key*,
|
|
2722
|
+
* because that is what makes a caller reach for a second source in the first
|
|
2723
|
+
* place, so the fallback returns plausible bytes rather than an error.
|
|
2724
|
+
*
|
|
2725
|
+
* It is also reachable without an attacker: a source declared in `rebase.json`
|
|
2726
|
+
* but not configured by the environment is skipped at boot and never
|
|
2727
|
+
* registered, while `GET /sources` still advertises it. The client asks for a
|
|
2728
|
+
* source it was told exists and silently gets a different bucket's contents.
|
|
2729
|
+
*
|
|
2730
|
+
* Carries `knownKeys` so the route layer can tell a typo from a source that was
|
|
2731
|
+
* declared but never configured — two different answers to the caller. The
|
|
2732
|
+
* registry itself does not know what was *declared*, only what was registered,
|
|
2733
|
+
* so it does not make that decision.
|
|
2734
|
+
*/
|
|
2735
|
+
var UnknownStorageSourceError = class extends Error {
|
|
2736
|
+
storageId;
|
|
2737
|
+
knownKeys;
|
|
2738
|
+
constructor(storageId, knownKeys) {
|
|
2739
|
+
super(`[StorageRegistry] Storage source "${storageId}" is not registered. Registered sources: ${knownKeys.length > 0 ? knownKeys.map((k) => `"${k}"`).join(", ") : "(none)"}.`);
|
|
2740
|
+
this.storageId = storageId;
|
|
2741
|
+
this.knownKeys = knownKeys;
|
|
2742
|
+
this.name = "UnknownStorageSourceError";
|
|
2743
|
+
}
|
|
2744
|
+
};
|
|
2745
|
+
/**
|
|
2746
|
+
* Default implementation of StorageRegistry
|
|
2747
|
+
*/
|
|
2748
|
+
var DefaultStorageRegistry = class DefaultStorageRegistry {
|
|
2749
|
+
controllers = /* @__PURE__ */ new Map();
|
|
2750
|
+
/**
|
|
2751
|
+
* Create a StorageRegistry from either a single controller or a map
|
|
2752
|
+
* @param input - Single StorageController (maps to "(default)") or Record<string, StorageController>
|
|
2753
|
+
*/
|
|
2754
|
+
static create(input) {
|
|
2755
|
+
const registry = new DefaultStorageRegistry();
|
|
2756
|
+
if (isStorageController(input)) registry.register(DEFAULT_STORAGE_ID, input);
|
|
2757
|
+
else {
|
|
2758
|
+
for (const [id, controller] of Object.entries(input)) if (isStorageController(controller)) registry.register(id, controller);
|
|
2759
|
+
if (!registry.has(DEFAULT_STORAGE_ID) && registry.size() > 0) {
|
|
2760
|
+
const firstId = Object.keys(input).find((k) => isStorageController(input[k]));
|
|
2761
|
+
if (firstId) {
|
|
2762
|
+
logger.warn(`[StorageRegistry] No "${DEFAULT_STORAGE_ID}" storage provided. Using "${firstId}" as the default.`);
|
|
2763
|
+
registry.register(DEFAULT_STORAGE_ID, input[firstId]);
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2425
2766
|
}
|
|
2767
|
+
return registry;
|
|
2426
2768
|
}
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
*/
|
|
2431
|
-
getAbsolutePath(key, bucket) {
|
|
2432
|
-
return this.getFullPath(key, bucket);
|
|
2769
|
+
register(id, controller) {
|
|
2770
|
+
if (this.controllers.has(id)) logger.warn(`[StorageRegistry] Overwriting storage with id "${id}"`);
|
|
2771
|
+
this.controllers.set(id, controller);
|
|
2433
2772
|
}
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2773
|
+
getDefault() {
|
|
2774
|
+
const controller = this.controllers.get(DEFAULT_STORAGE_ID);
|
|
2775
|
+
if (!controller) throw new Error(`[StorageRegistry] No default storage registered. Register one with id "${DEFAULT_STORAGE_ID}" or pass a single StorageController.`);
|
|
2776
|
+
return controller;
|
|
2777
|
+
}
|
|
2778
|
+
get(id) {
|
|
2779
|
+
return this.controllers.get(canonicalStorageId(id));
|
|
2780
|
+
}
|
|
2781
|
+
getOrDefault(id) {
|
|
2782
|
+
const key = canonicalStorageId(id);
|
|
2783
|
+
if (key === DEFAULT_STORAGE_ID) return this.getDefault();
|
|
2784
|
+
const controller = this.controllers.get(key);
|
|
2785
|
+
if (controller) return controller;
|
|
2786
|
+
throw new UnknownStorageSourceError(key, this.list());
|
|
2787
|
+
}
|
|
2788
|
+
has(id) {
|
|
2789
|
+
return this.controllers.has(canonicalStorageId(id));
|
|
2790
|
+
}
|
|
2791
|
+
list() {
|
|
2792
|
+
return Array.from(this.controllers.keys());
|
|
2793
|
+
}
|
|
2794
|
+
size() {
|
|
2795
|
+
return this.controllers.size;
|
|
2439
2796
|
}
|
|
2440
2797
|
};
|
|
2798
|
+
/**
|
|
2799
|
+
* Type guard to check if an object is a StorageController
|
|
2800
|
+
* vs a Record<string, StorageController> (multiple storages)
|
|
2801
|
+
*/
|
|
2802
|
+
function isStorageController(obj) {
|
|
2803
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
2804
|
+
const controller = obj;
|
|
2805
|
+
return typeof controller.putObject === "function" && typeof controller.getSignedUrl === "function" && typeof controller.deleteObject === "function" && typeof controller.listObjects === "function" && typeof controller.getType === "function";
|
|
2806
|
+
}
|
|
2441
2807
|
//#endregion
|
|
2442
2808
|
//#region src/storage/image-transform.ts
|
|
2443
2809
|
/**
|
|
@@ -2810,6 +3176,7 @@ var TusHandler = class {
|
|
|
2810
3176
|
throw new ApiError(400, "INVALID_STORAGE_BUCKET", err instanceof InvalidStorageBucketError ? err.message : "Invalid storage bucket");
|
|
2811
3177
|
}
|
|
2812
3178
|
const storageId = metadata.storageId || c.req.query("storageId") || void 0;
|
|
3179
|
+
if (storageId !== void 0 && this.storageRegistry && !this.storageRegistry.has(canonicalStorageId(storageId))) throw new ApiError(400, "UNKNOWN_STORAGE_SOURCE", `Unknown storage source "${storageId}". Available: ${this.storageRegistry.list().map((k) => `"${k}"`).join(", ") || "(none)"}.`, void 0, true);
|
|
2813
3180
|
if (this.authorizeUpload) await this.authorizeUpload(c, key, bucket || "default", storageId);
|
|
2814
3181
|
const filePath = join(this.tusDir, id);
|
|
2815
3182
|
await writeFile(filePath, Buffer.alloc(0));
|
|
@@ -2912,7 +3279,12 @@ var TusHandler = class {
|
|
|
2912
3279
|
async finalize(upload) {
|
|
2913
3280
|
const storageId = upload.storageId;
|
|
2914
3281
|
let targetController = this.storageController;
|
|
2915
|
-
if (this.storageRegistry)
|
|
3282
|
+
if (this.storageRegistry) try {
|
|
3283
|
+
targetController = storageId ? this.storageRegistry.getOrDefault(storageId) : this.storageRegistry.getDefault();
|
|
3284
|
+
} catch (err) {
|
|
3285
|
+
if (!(err instanceof UnknownStorageSourceError)) throw err;
|
|
3286
|
+
targetController = void 0;
|
|
3287
|
+
}
|
|
2916
3288
|
if (!targetController) {
|
|
2917
3289
|
logger.error("[TUS] Upload received but no StorageController is configured — nothing can be written", {
|
|
2918
3290
|
uploadId: upload.id,
|
|
@@ -3074,8 +3446,8 @@ function canonicalKeyOrBadRequest(key) {
|
|
|
3074
3446
|
* The bucket's counterpart to {@link canonicalKeyOrBadRequest}, and it exists
|
|
3075
3447
|
* for the same reason: the value routes a write, so it has to be checked where
|
|
3076
3448
|
* it enters rather than where it is used. Applied at every entry point a bucket
|
|
3077
|
-
* has — this route's multipart body, the
|
|
3078
|
-
* `Upload-Metadata` header.
|
|
3449
|
+
* has — this route's multipart body, the folder route's JSON body, the
|
|
3450
|
+
* `?bucket=` query, and the TUS `Upload-Metadata` header.
|
|
3079
3451
|
*/
|
|
3080
3452
|
function canonicalBucketOrBadRequest(bucket) {
|
|
3081
3453
|
try {
|
|
@@ -3175,13 +3547,46 @@ function createStorageRoutes(config) {
|
|
|
3175
3547
|
if (!allowed) throw ApiError.forbidden("Not authorized for this object");
|
|
3176
3548
|
};
|
|
3177
3549
|
/**
|
|
3178
|
-
* Resolve the storage controller for a request.
|
|
3179
|
-
*
|
|
3180
|
-
*
|
|
3550
|
+
* Resolve the storage controller for a request, or refuse the request.
|
|
3551
|
+
*
|
|
3552
|
+
* A `storageId` that names no registered source used to fall back to the
|
|
3553
|
+
* default one. That is silently wrong: the `storageAuthorize` hook is asked
|
|
3554
|
+
* about the source the caller named while the bytes come from `(default)`,
|
|
3555
|
+
* so the object the hook approved is not the object the request touched —
|
|
3556
|
+
* and because the two sources hold the same key, the fallback returns
|
|
3557
|
+
* plausible bytes instead of an error.
|
|
3558
|
+
*
|
|
3559
|
+
* The caller gets one of two answers, because they are two different
|
|
3560
|
+
* problems with two different fixes:
|
|
3561
|
+
*
|
|
3562
|
+
* - **501** if the source *is* declared in `rebase.json` but has no
|
|
3563
|
+
* credentials in this environment. It was skipped at boot rather than
|
|
3564
|
+
* crash-looping the backend, but `GET /sources` still advertises it, so a
|
|
3565
|
+
* client asking for it is not confused — the deployment is incomplete.
|
|
3566
|
+
* 501 (not 503) for the same reason the whole-storage stub uses it: this
|
|
3567
|
+
* is permanent until someone configures it, and the client's offline
|
|
3568
|
+
* queue retries 503 forever.
|
|
3569
|
+
* - **400** otherwise. The id names nothing this deployment has ever heard
|
|
3570
|
+
* of, which is a caller mistake. `expected`, so one client holding a
|
|
3571
|
+
* stale source name does not write a warning per request forever.
|
|
3181
3572
|
*/
|
|
3573
|
+
const declaredKeys = new Set((declaredSources ?? []).map((s) => s.key));
|
|
3574
|
+
const refuseUnknownSource = (storageId, knownKeys) => {
|
|
3575
|
+
if (declaredKeys.has(storageId)) throw new ApiError(501, "STORAGE_SOURCE_NOT_CONFIGURED", `Storage source "${storageId}" is declared but not configured on this deployment, so it cannot be read from or written to. Set its credentials (the ${storageId} suffixed environment variables) and redeploy.`);
|
|
3576
|
+
throw new ApiError(400, "UNKNOWN_STORAGE_SOURCE", `Unknown storage source "${storageId}". Available: ${knownKeys.length > 0 ? knownKeys.map((k) => `"${k}"`).join(", ") : "(none)"}.`, void 0, true);
|
|
3577
|
+
};
|
|
3182
3578
|
const resolveController = (storageId) => {
|
|
3183
|
-
if (registry)
|
|
3184
|
-
|
|
3579
|
+
if (registry) try {
|
|
3580
|
+
return registry.getOrDefault(storageId);
|
|
3581
|
+
} catch (err) {
|
|
3582
|
+
if (err instanceof UnknownStorageSourceError) return refuseUnknownSource(err.storageId, err.knownKeys);
|
|
3583
|
+
throw err;
|
|
3584
|
+
}
|
|
3585
|
+
if (controller) {
|
|
3586
|
+
const requested = canonicalStorageId(storageId);
|
|
3587
|
+
if (requested !== "(default)") return refuseUnknownSource(requested, [DEFAULT_STORAGE_SOURCE_KEY]);
|
|
3588
|
+
return controller;
|
|
3589
|
+
}
|
|
3185
3590
|
throw new Error("No storage controller or registry available");
|
|
3186
3591
|
};
|
|
3187
3592
|
/** Get the default controller (used for TUS and base-path derivation). */
|
|
@@ -3263,7 +3668,7 @@ function createStorageRoutes(config) {
|
|
|
3263
3668
|
const transformKeyPrefix = `${storageId || "(default)"}/${bucket}/${resolvedPath}`;
|
|
3264
3669
|
const transformOpts = transformOptionsOrBadRequest(c.req.query());
|
|
3265
3670
|
if (resolved.getType() === "local") {
|
|
3266
|
-
const absolutePath = resolved.getAbsolutePath(resolvedPath, bucket);
|
|
3671
|
+
const absolutePath = resolved.getAbsolutePath(resolvedPath, bucket ?? "default");
|
|
3267
3672
|
try {
|
|
3268
3673
|
await fsp.access(absolutePath);
|
|
3269
3674
|
} catch {
|
|
@@ -3324,7 +3729,7 @@ function createStorageRoutes(config) {
|
|
|
3324
3729
|
const scopedPath = `${bucket}/${resolvedPath}`;
|
|
3325
3730
|
if (isPublicStoragePath(scopedPath)) downloadConfig.metadata.public = true;
|
|
3326
3731
|
else {
|
|
3327
|
-
downloadConfig.metadata.token = generateDownloadToken(scopedPath, 300);
|
|
3732
|
+
downloadConfig.metadata.token = generateDownloadToken(scopedPath, 300, storageId);
|
|
3328
3733
|
downloadConfig.metadata.tokenExpiresIn = 300;
|
|
3329
3734
|
}
|
|
3330
3735
|
}
|
|
@@ -3384,9 +3789,10 @@ function createStorageRoutes(config) {
|
|
|
3384
3789
|
const storageId = typeof body.storageId === "string" ? body.storageId : c.req.query("storageId");
|
|
3385
3790
|
if (!folderPath || typeof folderPath !== "string") throw ApiError.badRequest("Folder path is required");
|
|
3386
3791
|
const resolved = resolveController(storageId);
|
|
3387
|
-
const {
|
|
3792
|
+
const { resolvedPath } = parseBucketAndPath(folderPath);
|
|
3793
|
+
const bucket = canonicalBucketOrBadRequest(typeof body.bucket === "string" ? body.bucket : void 0);
|
|
3388
3794
|
if (!resolvedPath || resolvedPath.trim() === "") throw ApiError.badRequest("Invalid folder path");
|
|
3389
|
-
await checkAuthorized(c, "write", resolvedPath, bucket, storageId);
|
|
3795
|
+
await checkAuthorized(c, "write", resolvedPath, bucket ?? "default", storageId);
|
|
3390
3796
|
if (resolved.getType() === "local") {
|
|
3391
3797
|
const absolutePath = resolved.getAbsolutePath(resolvedPath, bucket);
|
|
3392
3798
|
fs$1.mkdirSync(absolutePath, { recursive: true });
|
|
@@ -3395,7 +3801,8 @@ function createStorageRoutes(config) {
|
|
|
3395
3801
|
const emptyFile = new File([], key, { type: "application/x-directory" });
|
|
3396
3802
|
await resolved.putObject({
|
|
3397
3803
|
file: emptyFile,
|
|
3398
|
-
key
|
|
3804
|
+
key,
|
|
3805
|
+
bucket
|
|
3399
3806
|
});
|
|
3400
3807
|
}
|
|
3401
3808
|
return c.json({
|
|
@@ -3446,77 +3853,6 @@ function createStorageRoutes(config) {
|
|
|
3446
3853
|
return router;
|
|
3447
3854
|
}
|
|
3448
3855
|
//#endregion
|
|
3449
|
-
//#region src/storage/storage-registry.ts
|
|
3450
|
-
/**
|
|
3451
|
-
* The default storage identifier used when:
|
|
3452
|
-
* - A single storage controller is provided (not a map)
|
|
3453
|
-
* - A property doesn't specify a storageId
|
|
3454
|
-
*/
|
|
3455
|
-
var DEFAULT_STORAGE_ID = "(default)";
|
|
3456
|
-
/**
|
|
3457
|
-
* Default implementation of StorageRegistry
|
|
3458
|
-
*/
|
|
3459
|
-
var DefaultStorageRegistry = class DefaultStorageRegistry {
|
|
3460
|
-
controllers = /* @__PURE__ */ new Map();
|
|
3461
|
-
/**
|
|
3462
|
-
* Create a StorageRegistry from either a single controller or a map
|
|
3463
|
-
* @param input - Single StorageController (maps to "(default)") or Record<string, StorageController>
|
|
3464
|
-
*/
|
|
3465
|
-
static create(input) {
|
|
3466
|
-
const registry = new DefaultStorageRegistry();
|
|
3467
|
-
if (isStorageController(input)) registry.register(DEFAULT_STORAGE_ID, input);
|
|
3468
|
-
else {
|
|
3469
|
-
for (const [id, controller] of Object.entries(input)) if (isStorageController(controller)) registry.register(id, controller);
|
|
3470
|
-
if (!registry.has("(default)") && registry.size() > 0) {
|
|
3471
|
-
const firstId = Object.keys(input).find((k) => isStorageController(input[k]));
|
|
3472
|
-
if (firstId) {
|
|
3473
|
-
logger.warn(`[StorageRegistry] No "${DEFAULT_STORAGE_ID}" storage provided. Using "${firstId}" as the default.`);
|
|
3474
|
-
registry.register(DEFAULT_STORAGE_ID, input[firstId]);
|
|
3475
|
-
}
|
|
3476
|
-
}
|
|
3477
|
-
}
|
|
3478
|
-
return registry;
|
|
3479
|
-
}
|
|
3480
|
-
register(id, controller) {
|
|
3481
|
-
if (this.controllers.has(id)) logger.warn(`[StorageRegistry] Overwriting storage with id "${id}"`);
|
|
3482
|
-
this.controllers.set(id, controller);
|
|
3483
|
-
}
|
|
3484
|
-
getDefault() {
|
|
3485
|
-
const controller = this.controllers.get(DEFAULT_STORAGE_ID);
|
|
3486
|
-
if (!controller) throw new Error(`[StorageRegistry] No default storage registered. Register one with id "${DEFAULT_STORAGE_ID}" or pass a single StorageController.`);
|
|
3487
|
-
return controller;
|
|
3488
|
-
}
|
|
3489
|
-
get(id) {
|
|
3490
|
-
if (id === void 0 || id === null) return this.controllers.get(DEFAULT_STORAGE_ID);
|
|
3491
|
-
return this.controllers.get(id);
|
|
3492
|
-
}
|
|
3493
|
-
getOrDefault(id) {
|
|
3494
|
-
if (id === void 0 || id === null) return this.getDefault();
|
|
3495
|
-
const controller = this.controllers.get(id);
|
|
3496
|
-
if (controller) return controller;
|
|
3497
|
-
logger.warn(`[StorageRegistry] Storage "${id}" not found, falling back to "${DEFAULT_STORAGE_ID}"`);
|
|
3498
|
-
return this.getDefault();
|
|
3499
|
-
}
|
|
3500
|
-
has(id) {
|
|
3501
|
-
return this.controllers.has(id);
|
|
3502
|
-
}
|
|
3503
|
-
list() {
|
|
3504
|
-
return Array.from(this.controllers.keys());
|
|
3505
|
-
}
|
|
3506
|
-
size() {
|
|
3507
|
-
return this.controllers.size;
|
|
3508
|
-
}
|
|
3509
|
-
};
|
|
3510
|
-
/**
|
|
3511
|
-
* Type guard to check if an object is a StorageController
|
|
3512
|
-
* vs a Record<string, StorageController> (multiple storages)
|
|
3513
|
-
*/
|
|
3514
|
-
function isStorageController(obj) {
|
|
3515
|
-
if (typeof obj !== "object" || obj === null) return false;
|
|
3516
|
-
const controller = obj;
|
|
3517
|
-
return typeof controller.putObject === "function" && typeof controller.getSignedUrl === "function" && typeof controller.deleteObject === "function" && typeof controller.listObjects === "function" && typeof controller.getType === "function";
|
|
3518
|
-
}
|
|
3519
|
-
//#endregion
|
|
3520
3856
|
//#region src/storage/index.ts
|
|
3521
3857
|
/**
|
|
3522
3858
|
* Create a storage controller from a config object.
|
|
@@ -3610,7 +3946,7 @@ function assertStorageAccessControlConfigured(state, isProduction) {
|
|
|
3610
3946
|
*/
|
|
3611
3947
|
async function mountOpenApiDocs(app, basePath, enableSwagger, serverCollections, requireAuth) {
|
|
3612
3948
|
if (enableSwagger === false || serverCollections.length === 0) return;
|
|
3613
|
-
const { generateOpenApiSpec } = await import("./openapi-generator-
|
|
3949
|
+
const { generateOpenApiSpec } = await import("./openapi-generator-DQeQ_q2f.js");
|
|
3614
3950
|
app.get(`${basePath}/docs`, (c) => {
|
|
3615
3951
|
const spec = generateOpenApiSpec(serverCollections, {
|
|
3616
3952
|
basePath,
|
|
@@ -3763,6 +4099,10 @@ function createShutdown(config) {
|
|
|
3763
4099
|
config.cronScheduler.stop();
|
|
3764
4100
|
logger.info("Cron scheduler stopped");
|
|
3765
4101
|
}
|
|
4102
|
+
if (config.jobQueue) {
|
|
4103
|
+
await config.jobQueue.stop();
|
|
4104
|
+
logger.info("Job queue stopped");
|
|
4105
|
+
}
|
|
3766
4106
|
for (const [key, rt] of Object.entries(config.realtimeServices)) try {
|
|
3767
4107
|
if (typeof rt.destroy === "function") {
|
|
3768
4108
|
await rt.destroy();
|
|
@@ -3787,6 +4127,42 @@ function createShutdown(config) {
|
|
|
3787
4127
|
};
|
|
3788
4128
|
}
|
|
3789
4129
|
//#endregion
|
|
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
|
|
3790
4166
|
//#region src/init/process-safety.ts
|
|
3791
4167
|
/**
|
|
3792
4168
|
* Installed once per process. The singleton is process-global for the same
|
|
@@ -5092,9 +5468,15 @@ var SDKQueryBuilder = class {
|
|
|
5092
5468
|
}
|
|
5093
5469
|
/**
|
|
5094
5470
|
* Order the results by a specific column.
|
|
5471
|
+
*
|
|
5472
|
+
* Call it again to add a tie-breaker rather than replace the sort: keys
|
|
5473
|
+
* apply in the order they were added, so
|
|
5474
|
+
* `.orderBy("roles").orderBy("created_at", "desc")` sorts by role and
|
|
5475
|
+
* shows the newest first within each one.
|
|
5095
5476
|
*/
|
|
5096
5477
|
orderBy(column, direction = "asc") {
|
|
5097
|
-
this.params.orderBy
|
|
5478
|
+
const existing = normalizeOrderBy(this.params.orderBy) ?? [];
|
|
5479
|
+
this.params.orderBy = [...existing, [column, direction]];
|
|
5098
5480
|
return this;
|
|
5099
5481
|
}
|
|
5100
5482
|
/**
|
|
@@ -5409,8 +5791,7 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
5409
5791
|
logical: params?.logical,
|
|
5410
5792
|
limit: params?.limit,
|
|
5411
5793
|
offset: window.driverOffset,
|
|
5412
|
-
orderBy: params?.orderBy
|
|
5413
|
-
order: params?.orderBy?.[1],
|
|
5794
|
+
orderBy: normalizeOrderBy(params?.orderBy),
|
|
5414
5795
|
searchString: params?.searchString,
|
|
5415
5796
|
searchExplain: params?.searchExplain
|
|
5416
5797
|
}, (incomingRows) => {
|
|
@@ -7894,27 +8275,43 @@ function matchesParams(row, params) {
|
|
|
7894
8275
|
* Sort in place, Postgres-style: nulls last ascending, first descending, with
|
|
7895
8276
|
* the row id as a tiebreak so paging through an unsorted-but-equal run does
|
|
7896
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.
|
|
7897
8285
|
*/
|
|
7898
8286
|
function sortRows(rows, orderBy) {
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
const sign = direction === "desc" ? -1 : 1;
|
|
8287
|
+
const keys = normalizeOrderBy(orderBy);
|
|
8288
|
+
if (!keys) return rows;
|
|
7902
8289
|
return rows.sort((a, b) => {
|
|
7903
|
-
const
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
if (aNull && bNull) return tiebreak(a, b);
|
|
7909
|
-
return (aNull ? 1 : -1) * (direction === "desc" ? -1 : 1);
|
|
7910
|
-
}
|
|
7911
|
-
const cmp = compareValues(av, bv);
|
|
7912
|
-
if (cmp === void 0 || cmp === 0) return tiebreak(a, b);
|
|
7913
|
-
return cmp * sign;
|
|
8290
|
+
for (const [field, direction = "asc"] of keys) {
|
|
8291
|
+
const cmp = compareOnKey(a, b, field, direction);
|
|
8292
|
+
if (cmp !== 0) return cmp;
|
|
8293
|
+
}
|
|
8294
|
+
return tiebreak(a, b);
|
|
7914
8295
|
});
|
|
7915
8296
|
}
|
|
8297
|
+
/** One key's verdict: negative, positive, or 0 for "these two are equal here". */
|
|
8298
|
+
function compareOnKey(a, b, field, direction) {
|
|
8299
|
+
const av = a[field];
|
|
8300
|
+
const bv = b[field];
|
|
8301
|
+
const aNull = isNullish(toComparable(av));
|
|
8302
|
+
const bNull = isNullish(toComparable(bv));
|
|
8303
|
+
if (aNull || bNull) {
|
|
8304
|
+
if (aNull && bNull) return 0;
|
|
8305
|
+
return (aNull ? 1 : -1) * (direction === "desc" ? -1 : 1);
|
|
8306
|
+
}
|
|
8307
|
+
const cmp = compareValues(av, bv);
|
|
8308
|
+
if (cmp === void 0 || cmp === 0) return 0;
|
|
8309
|
+
return cmp * (direction === "desc" ? -1 : 1);
|
|
8310
|
+
}
|
|
8311
|
+
/** The last word, and the server's: `id DESC`. */
|
|
7916
8312
|
function tiebreak(a, b) {
|
|
7917
|
-
|
|
8313
|
+
const cmp = compareValues(a.id, b.id);
|
|
8314
|
+
return cmp === void 0 ? 0 : -cmp;
|
|
7918
8315
|
}
|
|
7919
8316
|
/**
|
|
7920
8317
|
* Resolve `page`/`offset`/`limit` the way the server does.
|
|
@@ -8009,17 +8406,16 @@ function isExactlyEvaluable(params) {
|
|
|
8009
8406
|
* comparator.
|
|
8010
8407
|
*/
|
|
8011
8408
|
function isLocallySortable(rows, orderBy) {
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8409
|
+
const keys = normalizeOrderBy(orderBy);
|
|
8410
|
+
if (!keys) return true;
|
|
8411
|
+
return keys.every(([field]) => rows.every((row) => {
|
|
8015
8412
|
const value = toComparable(row[field]);
|
|
8016
|
-
if (isNullish(value))
|
|
8017
|
-
if (typeof value === "number" || typeof value === "boolean")
|
|
8018
|
-
if (typeof value === "bigint")
|
|
8019
|
-
if (typeof value === "string" && value.trim() !== "" && !Number.isNaN(Number(value)))
|
|
8413
|
+
if (isNullish(value)) return true;
|
|
8414
|
+
if (typeof value === "number" || typeof value === "boolean") return true;
|
|
8415
|
+
if (typeof value === "bigint") return true;
|
|
8416
|
+
if (typeof value === "string" && value.trim() !== "" && !Number.isNaN(Number(value))) return true;
|
|
8020
8417
|
return false;
|
|
8021
|
-
}
|
|
8022
|
-
return true;
|
|
8418
|
+
}));
|
|
8023
8419
|
}
|
|
8024
8420
|
/** Run a full query — filter, sort, paginate — over a set of rows. */
|
|
8025
8421
|
function runLocalQuery(rows, params) {
|
|
@@ -9946,6 +10342,13 @@ async function _initializeRebaseBackend(config) {
|
|
|
9946
10342
|
installUnhandledRejectionHandler();
|
|
9947
10343
|
const basePath = config.basePath || "/api";
|
|
9948
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
|
+
});
|
|
9949
10352
|
configureMiddlewares(config.app, basePath, isProduction, config);
|
|
9950
10353
|
const collectionRegistry = new BackendCollectionRegistry();
|
|
9951
10354
|
const dataSourceRegistry = createDataSourceRegistry(config.dataSources);
|
|
@@ -9974,6 +10377,16 @@ async function _initializeRebaseBackend(config) {
|
|
|
9974
10377
|
if (bootstrappers.length === 0) throw new Error("No bootstrappers or database adapter provided. Cannot initialize database drivers.");
|
|
9975
10378
|
let defaultDriverId = DEFAULT_DRIVER_ID;
|
|
9976
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
|
+
});
|
|
9977
10390
|
for (const bootstrapper of bootstrappers) {
|
|
9978
10391
|
const b = bootstrapper;
|
|
9979
10392
|
logger.debug("Running bootstrapper for driver", { driverId: b.id || bootstrapper.type });
|
|
@@ -9982,7 +10395,11 @@ async function _initializeRebaseBackend(config) {
|
|
|
9982
10395
|
collections: activeCollections,
|
|
9983
10396
|
collectionRegistry,
|
|
9984
10397
|
introspectCollections,
|
|
9985
|
-
baas: config.baas
|
|
10398
|
+
baas: config.baas,
|
|
10399
|
+
schemaProvisioning: {
|
|
10400
|
+
attempted: schemaOutcome.status === "applied",
|
|
10401
|
+
reason: schemaOutcome.status === "skipped" ? schemaOutcome.reason : void 0
|
|
10402
|
+
}
|
|
9986
10403
|
});
|
|
9987
10404
|
delegates[b.id || bootstrapper.type] = driverResult.driver;
|
|
9988
10405
|
if (introspectCollections) {
|
|
@@ -10057,7 +10474,9 @@ async function _initializeRebaseBackend(config) {
|
|
|
10057
10474
|
if (safeAuthConfig.jwtSecret) configureJwt({
|
|
10058
10475
|
secret: safeAuthConfig.jwtSecret,
|
|
10059
10476
|
accessExpiresIn: safeAuthConfig.accessExpiresIn || "1h",
|
|
10060
|
-
refreshExpiresIn: safeAuthConfig.refreshExpiresIn || "30d"
|
|
10477
|
+
refreshExpiresIn: safeAuthConfig.refreshExpiresIn || "30d",
|
|
10478
|
+
signingKeys: safeAuthConfig.signingKeys,
|
|
10479
|
+
activeKid: safeAuthConfig.activeKid
|
|
10061
10480
|
});
|
|
10062
10481
|
if (safeAuthConfig.serviceKey) {
|
|
10063
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'))\"");
|
|
@@ -10070,6 +10489,10 @@ async function _initializeRebaseBackend(config) {
|
|
|
10070
10489
|
logger.info("Authentication initialized");
|
|
10071
10490
|
} else logger.warn("Auth requested but default bootstrapper does not support initializeAuth");
|
|
10072
10491
|
}
|
|
10492
|
+
await provisionCollectionPolicies(provisionable, provisionTarget, {
|
|
10493
|
+
introspecting: introspectCollections,
|
|
10494
|
+
provision: config.provisionSchema ?? true
|
|
10495
|
+
});
|
|
10073
10496
|
let historyConfigResult = void 0;
|
|
10074
10497
|
if (config.history) if (defaultBootstrapper.initializeHistory) {
|
|
10075
10498
|
logger.debug("Bootstrapping entity history via driver protocol");
|
|
@@ -10094,8 +10517,8 @@ async function _initializeRebaseBackend(config) {
|
|
|
10094
10517
|
store: apiKeyStore,
|
|
10095
10518
|
driver: defaultDriver
|
|
10096
10519
|
}) : void 0;
|
|
10097
|
-
if (apiKeyPreAuth) config.app.use(`${basePath}/admin/*`, apiKeyPreAuth);
|
|
10098
|
-
if (apiKeyStore) {
|
|
10520
|
+
if (apiKeyPreAuth && surfaces.admin) config.app.use(`${basePath}/admin/*`, apiKeyPreAuth);
|
|
10521
|
+
if (apiKeyStore && surfaces.admin) {
|
|
10099
10522
|
const apiKeyRoutes = createApiKeyRoutes({
|
|
10100
10523
|
store: apiKeyStore,
|
|
10101
10524
|
serviceKey: internalServiceKey
|
|
@@ -10185,7 +10608,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
10185
10608
|
]) {
|
|
10186
10609
|
const providerConfig = safeAuthConfig[key];
|
|
10187
10610
|
if (providerConfig && requiredFields.every((f) => Boolean(providerConfig[f]))) {
|
|
10188
|
-
const createFn = (await import("./auth-
|
|
10611
|
+
const createFn = (await import("./auth-BobZVd0j.js").then((n) => n.t))[factory];
|
|
10189
10612
|
oauthProviders.push(createFn(providerConfig));
|
|
10190
10613
|
}
|
|
10191
10614
|
}
|
|
@@ -10212,14 +10635,14 @@ async function _initializeRebaseBackend(config) {
|
|
|
10212
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.");
|
|
10213
10636
|
}
|
|
10214
10637
|
}
|
|
10215
|
-
if (authAdapter && authAdapter.createAuthRoutes) {
|
|
10638
|
+
if (authAdapter && authAdapter.createAuthRoutes && surfaces.auth) {
|
|
10216
10639
|
const authRoutes = authAdapter.createAuthRoutes();
|
|
10217
10640
|
if (authRoutes) {
|
|
10218
10641
|
config.app.route(`${basePath}/auth`, authRoutes);
|
|
10219
10642
|
logger.debug("Auth routes mounted via adapter", { adapter: authAdapter.id });
|
|
10220
10643
|
}
|
|
10221
10644
|
}
|
|
10222
|
-
if (authAdapter && authAdapter.createAdminRoutes) {
|
|
10645
|
+
if (authAdapter && authAdapter.createAdminRoutes && surfaces.admin) {
|
|
10223
10646
|
const adminRoutes = authAdapter.createAdminRoutes();
|
|
10224
10647
|
if (adminRoutes) {
|
|
10225
10648
|
config.app.route(`${basePath}/admin`, adminRoutes);
|
|
@@ -10227,6 +10650,10 @@ async function _initializeRebaseBackend(config) {
|
|
|
10227
10650
|
}
|
|
10228
10651
|
}
|
|
10229
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
|
+
}
|
|
10230
10657
|
const adminSurfacesGated = !!authAdapter && (isAuthAdapter(config.auth) || !!config.auth.jwtSecret);
|
|
10231
10658
|
const applyAdminGate = (router, surface) => {
|
|
10232
10659
|
if (!adminSurfacesGated) {
|
|
@@ -10291,7 +10718,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
10291
10718
|
logger.warn(`Schema Editor disabled: ${schemaEditorOff.message}`);
|
|
10292
10719
|
} else throw err;
|
|
10293
10720
|
}
|
|
10294
|
-
{
|
|
10721
|
+
if (surfaces.admin) {
|
|
10295
10722
|
const schemaEditorRouter = new Hono();
|
|
10296
10723
|
applyAdminGate(schemaEditorRouter, "Schema editor");
|
|
10297
10724
|
schemaEditorRouter.get("/status", (c) => c.json(schemaEditorOff ? {
|
|
@@ -10342,15 +10769,17 @@ async function _initializeRebaseBackend(config) {
|
|
|
10342
10769
|
}
|
|
10343
10770
|
}));
|
|
10344
10771
|
storageRouter.route("/", storageRoutes);
|
|
10345
|
-
config.app.route(`${basePath}/storage`, storageRouter);
|
|
10772
|
+
if (surfaces.storage) config.app.route(`${basePath}/storage`, storageRouter);
|
|
10346
10773
|
} else {
|
|
10347
10774
|
const storageStub = new Hono();
|
|
10348
10775
|
storageStub.all("/*", (c) => c.json({ error: {
|
|
10349
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.",
|
|
10350
10777
|
code: "STORAGE_NOT_CONFIGURED"
|
|
10351
10778
|
} }, 501));
|
|
10352
|
-
|
|
10353
|
-
|
|
10779
|
+
if (surfaces.storage) {
|
|
10780
|
+
config.app.route(`${basePath}/storage`, storageStub);
|
|
10781
|
+
logger.info("Storage not configured — /storage returns 501 STORAGE_NOT_CONFIGURED");
|
|
10782
|
+
}
|
|
10354
10783
|
}
|
|
10355
10784
|
const serverCollections = activeCollections.filter((collection) => resolveDataSource(collection, dataSourceRegistry).transport === "server");
|
|
10356
10785
|
if (activeCollections.length > 0) {
|
|
@@ -10399,9 +10828,9 @@ async function _initializeRebaseBackend(config) {
|
|
|
10399
10828
|
}
|
|
10400
10829
|
const restGenerator = new RestApiGenerator(serverCollections, defaultDriver, authAdapter);
|
|
10401
10830
|
dataRouter.route("/", restGenerator.generateRoutes());
|
|
10402
|
-
config.app.route(`${basePath}/data`, dataRouter);
|
|
10831
|
+
if (surfaces.data) config.app.route(`${basePath}/data`, dataRouter);
|
|
10403
10832
|
}
|
|
10404
|
-
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));
|
|
10405
10834
|
const serverClient = createRebaseClient({
|
|
10406
10835
|
baseUrl: "http://localhost",
|
|
10407
10836
|
apiPath: basePath,
|
|
@@ -10460,11 +10889,17 @@ async function _initializeRebaseBackend(config) {
|
|
|
10460
10889
|
const driver = defaultDriverResult.internals.driver;
|
|
10461
10890
|
if (driver && "client" in driver) driver.client = serverClient;
|
|
10462
10891
|
}
|
|
10463
|
-
if (config.functionsDir) {
|
|
10892
|
+
if (config.functionsDir && surfaces.functions) {
|
|
10464
10893
|
const { loadFunctionsWithDiagnostics } = await import("./function-loader-DDS1v7YX.js").then((n) => n.t);
|
|
10465
10894
|
const { createFunctionRoutes } = await import("./function-routes-Btcez1T-.js").then((n) => n.n);
|
|
10466
|
-
const { createFunctionsRequestTimeout, resolveFunctionsTimeoutMs } = await import("./request-timeout-
|
|
10467
|
-
const { functions:
|
|
10895
|
+
const { createFunctionsRequestTimeout, resolveFunctionsTimeoutMs } = await import("./request-timeout-BuFoEKwT.js");
|
|
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
|
+
});
|
|
10468
10903
|
const functionsRouter = new Hono();
|
|
10469
10904
|
functionsRouter.onError(errorHandler);
|
|
10470
10905
|
const functionsTimeoutMs = resolveFunctionsTimeoutMs(config.functionsTimeoutMs);
|
|
@@ -10497,12 +10932,23 @@ async function _initializeRebaseBackend(config) {
|
|
|
10497
10932
|
});
|
|
10498
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."));
|
|
10499
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
|
+
}
|
|
10500
10946
|
let cronScheduler;
|
|
10501
|
-
if (config.cronsDir) {
|
|
10947
|
+
if (config.cronsDir && (surfaces.cron || ownership.cronScheduler)) {
|
|
10502
10948
|
const { loadCronJobsWithDiagnostics } = await import("./cron-loader-YhhQeVBM.js").then((n) => n.t);
|
|
10503
10949
|
const { CronScheduler } = await import("./cron-scheduler-DIpYBmZP.js").then((n) => n.n);
|
|
10504
10950
|
const { createCronRoutes } = await import("./cron-routes-maM_RlUu.js").then((n) => n.n);
|
|
10505
|
-
const { createCronStore } = await import("./cron-store-
|
|
10951
|
+
const { createCronStore } = await import("./cron-store-CB1x-Ken.js").then((n) => n.n);
|
|
10506
10952
|
const { jobs: loadedCronJobs, problems: cronProblems } = await loadCronJobsWithDiagnostics(config.cronsDir);
|
|
10507
10953
|
cronScheduler = new CronScheduler();
|
|
10508
10954
|
cronScheduler.setClient(serverSingleton);
|
|
@@ -10517,8 +10963,12 @@ async function _initializeRebaseBackend(config) {
|
|
|
10517
10963
|
const cronRouter = new Hono();
|
|
10518
10964
|
applyAdminGate(cronRouter, "Cron");
|
|
10519
10965
|
cronRouter.route("/", createCronRoutes(cronScheduler, cronProblems.length));
|
|
10520
|
-
config.app.route(`${basePath}/cron`, cronRouter);
|
|
10521
|
-
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) {
|
|
10522
10972
|
cronScheduler.start();
|
|
10523
10973
|
logger.info("Mounted cron jobs", {
|
|
10524
10974
|
count: loadedCronJobs.length,
|
|
@@ -10526,7 +10976,20 @@ async function _initializeRebaseBackend(config) {
|
|
|
10526
10976
|
});
|
|
10527
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."));
|
|
10528
10978
|
}
|
|
10529
|
-
|
|
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) {
|
|
10530
10993
|
const { createBackupRoutes, parseBackupDestination } = await import("./backup-C6ljYVTp.js").then((n) => n.t);
|
|
10531
10994
|
const backupRouter = new Hono();
|
|
10532
10995
|
applyAdminGate(backupRouter, "Backup");
|
|
@@ -10540,7 +11003,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
10540
11003
|
config.app.route(`${basePath}/admin/backups`, backupRouter);
|
|
10541
11004
|
logger.debug("Backup admin routes mounted", { path: `${basePath}/admin/backups` });
|
|
10542
11005
|
}
|
|
10543
|
-
{
|
|
11006
|
+
if (surfaces.admin) {
|
|
10544
11007
|
const { default: logsRoutes } = await import("./logs-routes-BYA72C_C.js").then((n) => n.n);
|
|
10545
11008
|
const logsRouter = new Hono();
|
|
10546
11009
|
applyAdminGate(logsRouter, "Logs");
|
|
@@ -10548,7 +11011,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
10548
11011
|
config.app.route(`${basePath}/logs`, logsRouter);
|
|
10549
11012
|
logger.debug("Logs routes mounted", { path: `${basePath}/logs` });
|
|
10550
11013
|
}
|
|
10551
|
-
{
|
|
11014
|
+
if (surfaces.meta) {
|
|
10552
11015
|
const { createContractRoutes } = await import("./contract-routes-Bet-eCNJ.js").then((n) => n.t);
|
|
10553
11016
|
const contractRouter = new Hono();
|
|
10554
11017
|
if (adminSurfacesGated) {
|
|
@@ -10581,6 +11044,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
10581
11044
|
const shutdown = createShutdown({
|
|
10582
11045
|
server: config.server,
|
|
10583
11046
|
cronScheduler,
|
|
11047
|
+
jobQueue,
|
|
10584
11048
|
realtimeServices
|
|
10585
11049
|
});
|
|
10586
11050
|
/**
|
|
@@ -10619,6 +11083,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
10619
11083
|
storageController,
|
|
10620
11084
|
collectionRegistry,
|
|
10621
11085
|
cronScheduler,
|
|
11086
|
+
jobQueue,
|
|
10622
11087
|
healthCheck,
|
|
10623
11088
|
shutdown
|
|
10624
11089
|
};
|
|
@@ -10811,6 +11276,22 @@ var rebaseEnvSchema = object({
|
|
|
10811
11276
|
DATABASE_URL: string().url("DATABASE_URL must be a valid URL"),
|
|
10812
11277
|
ADMIN_CONNECTION_STRING: string().url().optional(),
|
|
10813
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"),
|
|
10814
11295
|
JWT_ACCESS_EXPIRES_IN: string().default("1h"),
|
|
10815
11296
|
JWT_REFRESH_EXPIRES_IN: string().default("400d"),
|
|
10816
11297
|
GOOGLE_CLIENT_ID: string().optional(),
|
|
@@ -11032,6 +11513,14 @@ async function assertAllowedOutboundUrl(rawUrl, options = {}) {
|
|
|
11032
11513
|
}
|
|
11033
11514
|
//#endregion
|
|
11034
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";
|
|
11035
11524
|
/** Read at most this much of a response before cancelling the rest. */
|
|
11036
11525
|
var MAX_RESPONSE_BYTES = 64 * 1024;
|
|
11037
11526
|
var WebhookDispatcher = class {
|
|
@@ -11085,21 +11574,64 @@ var WebhookDispatcher = class {
|
|
|
11085
11574
|
* for a third party. It also means a receiver that is down can no longer
|
|
11086
11575
|
* fail the customer's write.
|
|
11087
11576
|
*
|
|
11088
|
-
* The cost,
|
|
11089
|
-
* or a deploy between the enqueue and the delivery drops
|
|
11090
|
-
* receiver may see the notification a few milliseconds
|
|
11091
|
-
* describes is committed. Use {@link flush} on shutdown,
|
|
11092
|
-
* `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.
|
|
11093
11586
|
*/
|
|
11094
11587
|
enqueueEntityChange(table, event, id, entity, previousEntity) {
|
|
11095
11588
|
const jobs = this.buildDeliveries(table, event, entity, previousEntity);
|
|
11096
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
|
+
}
|
|
11097
11602
|
this.queue.push(...jobs);
|
|
11098
11603
|
this.startDraining();
|
|
11099
11604
|
}
|
|
11100
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
|
+
/**
|
|
11101
11630
|
* Wait for every queued delivery to finish. For graceful shutdown, and for
|
|
11102
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.
|
|
11103
11635
|
*/
|
|
11104
11636
|
async flush() {
|
|
11105
11637
|
while (this.draining) await this.draining;
|
|
@@ -11299,7 +11831,7 @@ function listenWithPortRetry(server, startPort, options) {
|
|
|
11299
11831
|
server.once("error", onError);
|
|
11300
11832
|
server.listen(startPort, host, () => {
|
|
11301
11833
|
server.removeListener("error", onError);
|
|
11302
|
-
resolve(startPort);
|
|
11834
|
+
resolve(boundPort(server) ?? startPort);
|
|
11303
11835
|
});
|
|
11304
11836
|
});
|
|
11305
11837
|
let affinityPort = null;
|
|
@@ -11329,14 +11861,15 @@ function listenWithPortRetry(server, startPort, options) {
|
|
|
11329
11861
|
attempt++;
|
|
11330
11862
|
const onListening = () => {
|
|
11331
11863
|
cleanup();
|
|
11864
|
+
const bound = boundPort(server) ?? port;
|
|
11332
11865
|
if (portFileDir) {
|
|
11333
11866
|
try {
|
|
11334
11867
|
const portFile = path.join(portFileDir, DEV_PORT_FILENAME);
|
|
11335
|
-
fs.writeFileSync(portFile, `${
|
|
11868
|
+
fs.writeFileSync(portFile, `${bound} ${startPort}`, "utf-8");
|
|
11336
11869
|
} catch {}
|
|
11337
|
-
writeStateFile(portFileDir,
|
|
11870
|
+
writeStateFile(portFileDir, bound, options?.serviceKey);
|
|
11338
11871
|
}
|
|
11339
|
-
resolve(
|
|
11872
|
+
resolve(bound);
|
|
11340
11873
|
};
|
|
11341
11874
|
const onError = (err) => {
|
|
11342
11875
|
cleanup();
|
|
@@ -11404,6 +11937,18 @@ function writeStateFile(projectRoot, port, serviceKey) {
|
|
|
11404
11937
|
fs.chmodSync(stateFile, 384);
|
|
11405
11938
|
} catch {}
|
|
11406
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
|
+
}
|
|
11407
11952
|
//#endregion
|
|
11408
11953
|
//#region src/serve-spa.ts
|
|
11409
11954
|
/**
|
|
@@ -11895,7 +12440,64 @@ var bootEnvExtension = object({
|
|
|
11895
12440
|
""
|
|
11896
12441
|
]).default("true").transform((v) => v !== "false"),
|
|
11897
12442
|
/** Comma-separated origins allowed to make credentialed cross-origin calls. */
|
|
11898
|
-
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()
|
|
11899
12501
|
});
|
|
11900
12502
|
/**
|
|
11901
12503
|
* Load and validate the environment for a bundle boot.
|
|
@@ -11952,24 +12554,152 @@ function resolveEnableSwagger(env) {
|
|
|
11952
12554
|
return env.NODE_ENV === "production" ? false : void 0;
|
|
11953
12555
|
}
|
|
11954
12556
|
/**
|
|
11955
|
-
* Build the CORS origin policy.
|
|
12557
|
+
* Build the CORS origin policy.
|
|
12558
|
+
*
|
|
12559
|
+
* Production serves an explicit allow-list and nothing else. `loadEnv` already
|
|
12560
|
+
* refuses to start a production process with neither `CORS_ORIGINS` nor
|
|
12561
|
+
* `FRONTEND_URL`, so an empty list here can only mean the values were blank
|
|
12562
|
+
* strings — still worth failing on, because the alternative is an API that
|
|
12563
|
+
* quietly rejects its own frontend.
|
|
12564
|
+
*/
|
|
12565
|
+
function resolveCorsOrigin(env) {
|
|
12566
|
+
if (!(env.NODE_ENV === "production")) return (origin) => {
|
|
12567
|
+
if (!origin) return "*";
|
|
12568
|
+
return isLocalhostOrigin(origin) ? origin : null;
|
|
12569
|
+
};
|
|
12570
|
+
const allowed = (env.CORS_ORIGINS || env.FRONTEND_URL || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
12571
|
+
if (allowed.length === 0) throw new Error("CORS_ORIGINS or FRONTEND_URL must be set in production. Example: CORS_ORIGINS=https://yourdomain.com");
|
|
12572
|
+
if (allowed.includes("*")) throw new Error("CORS_ORIGINS cannot be \"*\" — the API sends credentials, and browsers refuse a wildcard origin on credentialed requests. List the exact origins.");
|
|
12573
|
+
return (origin) => allowed.includes(origin) ? origin : null;
|
|
12574
|
+
}
|
|
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
|
|
11956
12682
|
*
|
|
11957
|
-
*
|
|
11958
|
-
*
|
|
11959
|
-
*
|
|
11960
|
-
*
|
|
11961
|
-
*
|
|
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.
|
|
11962
12702
|
*/
|
|
11963
|
-
function resolveCorsOrigin(env) {
|
|
11964
|
-
if (!(env.NODE_ENV === "production")) return (origin) => {
|
|
11965
|
-
if (!origin) return "*";
|
|
11966
|
-
return isLocalhostOrigin(origin) ? origin : null;
|
|
11967
|
-
};
|
|
11968
|
-
const allowed = (env.CORS_ORIGINS || env.FRONTEND_URL || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
11969
|
-
if (allowed.length === 0) throw new Error("CORS_ORIGINS or FRONTEND_URL must be set in production. Example: CORS_ORIGINS=https://yourdomain.com");
|
|
11970
|
-
if (allowed.includes("*")) throw new Error("CORS_ORIGINS cannot be \"*\" — the API sends credentials, and browsers refuse a wildcard origin on credentialed requests. List the exact origins.");
|
|
11971
|
-
return (origin) => allowed.includes(origin) ? origin : null;
|
|
11972
|
-
}
|
|
11973
12703
|
//#endregion
|
|
11974
12704
|
//#region src/boot/sources.ts
|
|
11975
12705
|
/**
|
|
@@ -12190,168 +12920,6 @@ function loadDeclaredStorageSources(startDir, levels = 5) {
|
|
|
12190
12920
|
return [];
|
|
12191
12921
|
}
|
|
12192
12922
|
//#endregion
|
|
12193
|
-
//#region src/boot/version-skew.ts
|
|
12194
|
-
/**
|
|
12195
|
-
* Telling a *stale* driver apart from a driver that never had the feature.
|
|
12196
|
-
*
|
|
12197
|
-
* The managed runtime is two halves that version independently. The image
|
|
12198
|
-
* supplies `@rebasepro/server` — `docker/entrypoint.mjs` symlinks it over
|
|
12199
|
-
* whatever the bundle installed, so the boot harness is always the image's. The
|
|
12200
|
-
* **driver** is not redirected: `@rebasepro/server-postgres` comes from the
|
|
12201
|
-
* bundle's `deps.declared`, pinned to whatever the project's `package.json`
|
|
12202
|
-
* asked npm for. Shipping a new image therefore does not ship a new driver, and
|
|
12203
|
-
* a fix that spans both packages reaches a tenant only after an npm publish AND
|
|
12204
|
-
* a rebuild.
|
|
12205
|
-
*
|
|
12206
|
-
* That split has a nasty failure mode, and it has already burned a day. Boot
|
|
12207
|
-
* looks for an optional capability, does not find it, and reports the honest
|
|
12208
|
-
* local fact — "this driver does not implement collection-table creation" —
|
|
12209
|
-
* which is exactly what a schemaless driver looks like. But a driver that is
|
|
12210
|
-
* merely *older than the runtime asking* looks identical, and the two want
|
|
12211
|
-
* opposite responses: one is "this database is not managed by Rebase, carry on",
|
|
12212
|
-
* the other is "your tables were never created and every data route is about to
|
|
12213
|
-
* 500". Naming the versions is what separates them, so these helpers exist to
|
|
12214
|
-
* put both numbers in front of whoever reads the log.
|
|
12215
|
-
*/
|
|
12216
|
-
/** The package whose version defines "the runtime" for skew purposes. */
|
|
12217
|
-
var RUNTIME_PACKAGE = "@rebasepro/server";
|
|
12218
|
-
/**
|
|
12219
|
-
* Locate a package by walking `node_modules` up from a directory.
|
|
12220
|
-
*
|
|
12221
|
-
* Lives here rather than in `driver.ts` because both the driver loader and the
|
|
12222
|
-
* version check need it, and `driver.ts` already imports this module.
|
|
12223
|
-
*/
|
|
12224
|
-
function findPackageDir(fromDir, packageName) {
|
|
12225
|
-
let dir = path.resolve(fromDir);
|
|
12226
|
-
for (;;) {
|
|
12227
|
-
const candidate = path.join(dir, "node_modules", ...packageName.split("/"));
|
|
12228
|
-
if (fs.existsSync(path.join(candidate, "package.json"))) return candidate;
|
|
12229
|
-
const parent = path.dirname(dir);
|
|
12230
|
-
if (parent === dir) return void 0;
|
|
12231
|
-
dir = parent;
|
|
12232
|
-
}
|
|
12233
|
-
}
|
|
12234
|
-
/**
|
|
12235
|
-
* Parse the subset of semver the workspace actually publishes.
|
|
12236
|
-
*
|
|
12237
|
-
* Deliberately lenient: an unparseable version yields `undefined` and every
|
|
12238
|
-
* comparison then declines to judge, because a wrong "your driver is old"
|
|
12239
|
-
* warning on a fork's custom version string is worse than no warning at all.
|
|
12240
|
-
*/
|
|
12241
|
-
function parseVersion(version) {
|
|
12242
|
-
const match = /^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/.exec(version.trim());
|
|
12243
|
-
if (!match) return void 0;
|
|
12244
|
-
return {
|
|
12245
|
-
parts: [
|
|
12246
|
-
Number(match[1]),
|
|
12247
|
-
Number(match[2]),
|
|
12248
|
-
Number(match[3])
|
|
12249
|
-
],
|
|
12250
|
-
prerelease: match[4]
|
|
12251
|
-
};
|
|
12252
|
-
}
|
|
12253
|
-
/**
|
|
12254
|
-
* Compare two versions, or `undefined` when either cannot be parsed.
|
|
12255
|
-
*
|
|
12256
|
-
* Follows semver on the one rule that matters here: a prerelease sorts *below*
|
|
12257
|
-
* the release it leads to, so `0.12.1-canary.g7f1150f` < `0.12.1`. Without that
|
|
12258
|
-
* rule every canary would read as newer than the stable it precedes, and the
|
|
12259
|
-
* canaries are precisely where fixes land first.
|
|
12260
|
-
*/
|
|
12261
|
-
function compareVersions(a, b) {
|
|
12262
|
-
const left = parseVersion(a);
|
|
12263
|
-
const right = parseVersion(b);
|
|
12264
|
-
if (!left || !right) return void 0;
|
|
12265
|
-
for (let i = 0; i < 3; i++) if (left.parts[i] !== right.parts[i]) return left.parts[i] < right.parts[i] ? -1 : 1;
|
|
12266
|
-
if (left.prerelease === right.prerelease) return 0;
|
|
12267
|
-
if (left.prerelease === void 0) return 1;
|
|
12268
|
-
if (right.prerelease === void 0) return -1;
|
|
12269
|
-
return left.prerelease < right.prerelease ? -1 : 1;
|
|
12270
|
-
}
|
|
12271
|
-
/**
|
|
12272
|
-
* Describe how a driver's version relates to the runtime asking it for work.
|
|
12273
|
-
*
|
|
12274
|
-
* Returns `stale: false` whenever it cannot tell — an unknown version, an
|
|
12275
|
-
* unparseable one, a driver that is newer. Silence beats a confident wrong
|
|
12276
|
-
* diagnosis, and the caller still prints its own local fact either way.
|
|
12277
|
-
*/
|
|
12278
|
-
function describeDriverSkew(driverVersion, runtimeVersion) {
|
|
12279
|
-
if (!driverVersion || !runtimeVersion) return { stale: false };
|
|
12280
|
-
const order = compareVersions(driverVersion, runtimeVersion);
|
|
12281
|
-
if (order === void 0) return { stale: false };
|
|
12282
|
-
if (order < 0) return {
|
|
12283
|
-
stale: true,
|
|
12284
|
-
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.`
|
|
12285
|
-
};
|
|
12286
|
-
return {
|
|
12287
|
-
stale: false,
|
|
12288
|
-
detail: `Driver ${driverVersion}, runtime ${runtimeVersion}.`
|
|
12289
|
-
};
|
|
12290
|
-
}
|
|
12291
|
-
/**
|
|
12292
|
-
* How to get a project's schema applied, written once and owned by the runtime.
|
|
12293
|
-
*
|
|
12294
|
-
* This text lives in `@rebasepro/server` on purpose. The equivalent guidance in
|
|
12295
|
-
* the Postgres driver's schema-drift warning can only ever be as current as the
|
|
12296
|
-
* driver a tenant pinned — so the tenants who most need to be told "your driver
|
|
12297
|
-
* is too old" are exactly the ones whose driver still prints the old advice.
|
|
12298
|
-
* The image's copy of this package is never stale, so guidance printed from
|
|
12299
|
-
* here always reflects the platform as it is today.
|
|
12300
|
-
*
|
|
12301
|
-
* Both audiences get a line because the runtime cannot reliably tell which one
|
|
12302
|
-
* it is serving, and naming only the pnpm scripts — as this once did everywhere
|
|
12303
|
-
* — tells a managed operator to run a command that structurally cannot reach
|
|
12304
|
-
* their in-cluster database.
|
|
12305
|
-
*/
|
|
12306
|
-
function schemaRecoveryGuidance(options = {}) {
|
|
12307
|
-
const lines = [
|
|
12308
|
-
" To apply this project's schema:",
|
|
12309
|
-
" • Managed cloud: the runtime applies tables and RLS at boot (unless",
|
|
12310
|
-
" REBASE_MIGRATE_ON_BOOT=none). `rebase db push` cannot reach a tenant's",
|
|
12311
|
-
" in-cluster database — redeploy instead."
|
|
12312
|
-
];
|
|
12313
|
-
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.");
|
|
12314
|
-
lines.push(" • Self-host: run `rebase db push` (dev) or `rebase db migrate` (prod).");
|
|
12315
|
-
return lines.join("\n");
|
|
12316
|
-
}
|
|
12317
|
-
/**
|
|
12318
|
-
* The installed version of a package, read from its `package.json`.
|
|
12319
|
-
*
|
|
12320
|
-
* Returns `undefined` rather than throwing for every failure — a missing or
|
|
12321
|
-
* malformed manifest degrades the diagnosis, and must never take down a boot
|
|
12322
|
-
* that would otherwise have served.
|
|
12323
|
-
*/
|
|
12324
|
-
function readPackageVersion(packageDir) {
|
|
12325
|
-
try {
|
|
12326
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(packageDir, "package.json"), "utf8"));
|
|
12327
|
-
return typeof pkg.version === "string" ? pkg.version : void 0;
|
|
12328
|
-
} catch {
|
|
12329
|
-
return;
|
|
12330
|
-
}
|
|
12331
|
-
}
|
|
12332
|
-
/**
|
|
12333
|
-
* The version of `@rebasepro/server` this deployment is actually running.
|
|
12334
|
-
*
|
|
12335
|
-
* Resolved from the same roots the drivers are, and that is the point rather
|
|
12336
|
-
* than a convenience. In a managed container `docker/entrypoint.mjs` replaces
|
|
12337
|
-
* `/bundle/node_modules/@rebasepro/server` with a link to the image's copy, so
|
|
12338
|
-
* reading the manifest *through the bundle* reports the version that will
|
|
12339
|
-
* really execute — which is exactly the number the driver has to be compared
|
|
12340
|
-
* against. Reading this package's own manifest by module path would report the
|
|
12341
|
-
* same thing in the happy case and quietly lie in the case that matters: a
|
|
12342
|
-
* bundle whose link did not happen.
|
|
12343
|
-
*
|
|
12344
|
-
* Returns `undefined` when the package cannot be found; every caller treats
|
|
12345
|
-
* that as "cannot judge" and stays quiet.
|
|
12346
|
-
*/
|
|
12347
|
-
function readRuntimeVersion(roots) {
|
|
12348
|
-
for (const root of roots) {
|
|
12349
|
-
const dir = findPackageDir(root, RUNTIME_PACKAGE);
|
|
12350
|
-
const version = dir ? readPackageVersion(dir) : void 0;
|
|
12351
|
-
if (version) return version;
|
|
12352
|
-
}
|
|
12353
|
-
}
|
|
12354
|
-
//#endregion
|
|
12355
12923
|
//#region src/boot/driver.ts
|
|
12356
12924
|
/**
|
|
12357
12925
|
* Run a trivial query against a source, to see whether it answers.
|
|
@@ -12567,6 +13135,10 @@ function resolveAuthOptions(env, usersCollection) {
|
|
|
12567
13135
|
const auth = {
|
|
12568
13136
|
collection: usersCollection,
|
|
12569
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,
|
|
12570
13142
|
accessExpiresIn: env.JWT_ACCESS_EXPIRES_IN,
|
|
12571
13143
|
refreshExpiresIn: env.JWT_REFRESH_EXPIRES_IN,
|
|
12572
13144
|
serviceKey: env.REBASE_SERVICE_KEY,
|
|
@@ -12990,6 +13562,11 @@ async function bootFromBundle(options = {}) {
|
|
|
12990
13562
|
if (bundle.manifest.kind === "static") return bootStaticApp(bundle, devRoot, options);
|
|
12991
13563
|
const env = loadBootEnv();
|
|
12992
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
|
+
});
|
|
12993
13570
|
const configExports = await loadBundleConfigExports(bundle);
|
|
12994
13571
|
const dataSourceDefs = configExports.dataSources;
|
|
12995
13572
|
const declaredStorage = normalizeStorageSources(bundle.manifest.storage?.sources, configExports.storageSources);
|
|
@@ -13013,14 +13590,20 @@ async function bootFromBundle(options = {}) {
|
|
|
13013
13590
|
const server = createServer(getRequestListener(app.fetch));
|
|
13014
13591
|
const usersCollection = await loadUsersCollection(bundle);
|
|
13015
13592
|
const storage = resolveStorageSources(process.env, storageSourceDefs, path.join(bundle.dir, "uploads"));
|
|
13016
|
-
|
|
13593
|
+
warnOnUnusableBundleShape(bundle);
|
|
13017
13594
|
const backend = await initializeRebaseBackend({
|
|
13018
13595
|
server,
|
|
13019
13596
|
app,
|
|
13020
13597
|
basePath: env.REBASE_BASE_PATH,
|
|
13598
|
+
provisioningDriverResult: dataSources[0] ? { internals: dataSources[0].connection } : void 0,
|
|
13021
13599
|
collectionsDir: bundle.collectionsDir,
|
|
13022
13600
|
functionsDir: bundle.functionsDir,
|
|
13023
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,
|
|
13024
13607
|
bootstrappers: dataSources.map((s) => s.bootstrapper),
|
|
13025
13608
|
dataSources: dataSourceDefs,
|
|
13026
13609
|
storage,
|
|
@@ -13040,7 +13623,6 @@ async function bootFromBundle(options = {}) {
|
|
|
13040
13623
|
runtimeVersion: bundle.manifest.runtime?.builtAgainst,
|
|
13041
13624
|
schemaEditor: false
|
|
13042
13625
|
});
|
|
13043
|
-
await ensureCollectionPolicies(bundle, dataSources, env);
|
|
13044
13626
|
metrics?.setKnownCollections(backend.collectionRegistry.getCollections().map((collection) => collection.slug).filter((slug) => Boolean(slug)));
|
|
13045
13627
|
const healthPaths = ["/health", `${(env.REBASE_BASE_PATH || "/api").replace(/\/$/, "")}/health`];
|
|
13046
13628
|
for (const healthPath of [...new Set(healthPaths)]) app.get(healthPath, async (c) => {
|
|
@@ -13079,6 +13661,7 @@ async function bootFromBundle(options = {}) {
|
|
|
13079
13661
|
"/health",
|
|
13080
13662
|
"/livez",
|
|
13081
13663
|
"/metrics",
|
|
13664
|
+
"/.well-known",
|
|
13082
13665
|
...siblings
|
|
13083
13666
|
],
|
|
13084
13667
|
spa: staticApp.spa
|
|
@@ -13090,10 +13673,12 @@ async function bootFromBundle(options = {}) {
|
|
|
13090
13673
|
server.once("error", reject);
|
|
13091
13674
|
server.listen(env.PORT, () => {
|
|
13092
13675
|
server.removeListener("error", reject);
|
|
13676
|
+
const address = server.address();
|
|
13677
|
+
if (address && typeof address === "object") port = address.port;
|
|
13093
13678
|
resolve();
|
|
13094
13679
|
});
|
|
13095
13680
|
});
|
|
13096
|
-
logger.info(`Rebase runtime listening on port ${
|
|
13681
|
+
logger.info(`Rebase runtime listening on port ${port}`);
|
|
13097
13682
|
} else {
|
|
13098
13683
|
port = await listenWithPortRetry(server, env.PORT, {
|
|
13099
13684
|
portFileDir: devRoot,
|
|
@@ -13227,7 +13812,7 @@ async function runFromBundle(options = {}) {
|
|
|
13227
13812
|
try {
|
|
13228
13813
|
return await bootFromBundle(options);
|
|
13229
13814
|
} catch (err) {
|
|
13230
|
-
if (err instanceof BundleError) {
|
|
13815
|
+
if (err instanceof BundleError || err instanceof RoleConfigurationError || err instanceof FunctionSelectionError) {
|
|
13231
13816
|
logger.error(err.message);
|
|
13232
13817
|
if (err.hint) logger.error(err.hint);
|
|
13233
13818
|
} else logger.error("Failed to start the Rebase runtime", { error: err instanceof Error ? err : new Error(String(err)) });
|
|
@@ -13259,158 +13844,25 @@ function warnOnDriverSkew(dataSources, runtimeVersion) {
|
|
|
13259
13844
|
}
|
|
13260
13845
|
}
|
|
13261
13846
|
/**
|
|
13262
|
-
*
|
|
13263
|
-
*
|
|
13264
|
-
* A bundle's collections directory holds *every* collection the project
|
|
13265
|
-
* declares, whatever engine serves it — that is the point of `dataSource`
|
|
13266
|
-
* routing. What it is not is a list of tables to create: handing the whole
|
|
13267
|
-
* directory to the primary source's bootstrapper made a Firestore collection
|
|
13268
|
-
* declared alongside the Postgres ones arrive as an empty Postgres table, with
|
|
13269
|
-
* RLS policies, while the app went on reading its documents from Firestore.
|
|
13270
|
-
*
|
|
13271
|
-
* Excluding is deliberately conservative. A collection that names neither an
|
|
13272
|
-
* `engine` nor a `dataSource` belongs to whichever source is primary — the
|
|
13273
|
-
* "postgres" that `resolveDataSource` falls back to there is a default, not a
|
|
13274
|
-
* declaration, and must not exclude anything on its own. Only a collection that
|
|
13275
|
-
* explicitly routes to a *different* engine is dropped, so a project running two
|
|
13276
|
-
* sources on the same engine is unaffected.
|
|
13277
|
-
*/
|
|
13278
|
-
function collectionsStoredBy(collections, primary, dataSources) {
|
|
13279
|
-
const registry = createDataSourceRegistry(dataSources.map((source) => ({
|
|
13280
|
-
key: source.key,
|
|
13281
|
-
engine: source.engine
|
|
13282
|
-
})));
|
|
13283
|
-
return collections.filter((collection) => {
|
|
13284
|
-
const declared = collection.engine ?? (collection.dataSource ? resolveDataSource(collection, registry).engine : void 0);
|
|
13285
|
-
if (!declared) return true;
|
|
13286
|
-
return declared === primary.engine;
|
|
13287
|
-
});
|
|
13288
|
-
}
|
|
13289
|
-
/** Say what was routed elsewhere, so a missing table is never a silent one. */
|
|
13290
|
-
function logForeignCollections(all, stored, primary) {
|
|
13291
|
-
if (stored.length === all.length) return;
|
|
13292
|
-
const foreign = all.filter((c) => !stored.includes(c)).map((c) => c.slug);
|
|
13293
|
-
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.`);
|
|
13294
|
-
}
|
|
13295
|
-
/**
|
|
13296
|
-
* Bring the database's collection tables up to date before serving.
|
|
13297
|
-
*
|
|
13298
|
-
* Delegates to whichever driver bootstrapped the default data source; a driver
|
|
13299
|
-
* without `ensureCollectionSchema` (a schemaless one, or an older build) skips
|
|
13300
|
-
* rather than failing, which is why this cannot break an existing deployment.
|
|
13301
|
-
*
|
|
13302
|
-
* Every path out of here says why, at info or louder. Guaranteeing the tables
|
|
13303
|
-
* exist is this function's entire job, so "it declined, and said nothing" is the
|
|
13304
|
-
* one outcome it must never produce: a deployment that skips comes up answering
|
|
13305
|
-
* sign-in and 500ing every `/api/data/*` route, and the operator's only evidence
|
|
13306
|
-
* is what these lines print. Silence here has already sent one investigation
|
|
13307
|
-
* chasing a stale runtime image that was not stale.
|
|
13308
|
-
*
|
|
13309
|
-
* Failure is fatal on purpose. Booting anyway would produce exactly the state
|
|
13310
|
-
* this exists to prevent — an app that answers sign-in and 500s every data
|
|
13311
|
-
* request — and a crash-looping pod with the DDL error in its logs is a far
|
|
13312
|
-
* better signal than a running one that silently cannot serve.
|
|
13313
|
-
*/
|
|
13314
|
-
async function ensureCollectionSchema(bundle, dataSources, env) {
|
|
13315
|
-
const skip = (reason, level = "info") => {
|
|
13316
|
-
logger[level](`Collection schema: skipped — ${reason}`);
|
|
13317
|
-
};
|
|
13318
|
-
if ((env.REBASE_MIGRATE_ON_BOOT || "ensure") === "none") {
|
|
13319
|
-
skip("REBASE_MIGRATE_ON_BOOT=none, leaving the database schema untouched.");
|
|
13320
|
-
return;
|
|
13321
|
-
}
|
|
13322
|
-
if (bundle.manifest.kind !== "backend") {
|
|
13323
|
-
skip(`this bundle's kind is "${bundle.manifest.kind}", which serves no database.`);
|
|
13324
|
-
return;
|
|
13325
|
-
}
|
|
13326
|
-
if (!bundle.manifest.entry?.config) {
|
|
13327
|
-
skip("this bundle declares no config package, so its collections are read from the database rather than from code.");
|
|
13328
|
-
return;
|
|
13329
|
-
}
|
|
13330
|
-
if (!bundle.collectionsDir) {
|
|
13331
|
-
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");
|
|
13332
|
-
return;
|
|
13333
|
-
}
|
|
13334
|
-
const primary = dataSources[0];
|
|
13335
|
-
if (!primary) {
|
|
13336
|
-
skip("no data source was initialized for this runtime.", "warn");
|
|
13337
|
-
return;
|
|
13338
|
-
}
|
|
13339
|
-
if (!primary.bootstrapper.ensureCollectionSchema) {
|
|
13340
|
-
const skew = describeDriverSkew(primary.driverVersion, readRuntimeVersion(bundleResolutionRoots(bundle.dir)));
|
|
13341
|
-
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");
|
|
13342
|
-
return;
|
|
13343
|
-
}
|
|
13344
|
-
const loaded = await loadCollectionsFromDirectory(bundle.collectionsDir);
|
|
13345
|
-
if (loaded.length === 0) {
|
|
13346
|
-
skip(`no collections were loaded from "${bundle.collectionsDir}".`, "warn");
|
|
13347
|
-
return;
|
|
13348
|
-
}
|
|
13349
|
-
const collections = collectionsStoredBy(loaded, primary, dataSources);
|
|
13350
|
-
logForeignCollections(loaded, collections, primary);
|
|
13351
|
-
if (collections.length === 0) {
|
|
13352
|
-
skip(`none of the ${loaded.length} declared collection(s) are stored by the "${primary.engine}" data source.`);
|
|
13353
|
-
return;
|
|
13354
|
-
}
|
|
13355
|
-
const { applied } = await primary.bootstrapper.ensureCollectionSchema(collections, preInitDriverResult(primary), (message) => logger.debug(`schema: ${message}`));
|
|
13356
|
-
if (applied > 0) logger.info(`Applied ${applied} additive schema change(s) before boot.`);
|
|
13357
|
-
else logger.debug("Collection schema is up to date.");
|
|
13358
|
-
}
|
|
13359
|
-
/**
|
|
13360
|
-
* The `InitializedDriver` to hand a bootstrapper before any driver exists.
|
|
13847
|
+
* Warn about a bundle whose shape makes provisioning impossible.
|
|
13361
13848
|
*
|
|
13362
|
-
*
|
|
13363
|
-
*
|
|
13364
|
-
*
|
|
13365
|
-
*
|
|
13366
|
-
*
|
|
13367
|
-
*
|
|
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.
|
|
13368
13855
|
*
|
|
13369
|
-
*
|
|
13370
|
-
*
|
|
13371
|
-
* crash — `TypeError: Cannot read properties of undefined (reading 'db')` — on
|
|
13372
|
-
* every project whose driver implements these hooks. The cast is narrowed to the
|
|
13373
|
-
* one field a pre-init result cannot honestly supply, rather than `as never`
|
|
13374
|
-
* blanketing the whole argument.
|
|
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.
|
|
13375
13858
|
*/
|
|
13376
|
-
function
|
|
13377
|
-
return { internals: source.connection };
|
|
13378
|
-
}
|
|
13379
|
-
/**
|
|
13380
|
-
* Apply the project's RLS policies before serving — the companion to
|
|
13381
|
-
* {@link ensureCollectionSchema}, which creates the tables this makes servable.
|
|
13382
|
-
*
|
|
13383
|
-
* Runs after `initializeRebaseBackend`, not alongside table creation: the
|
|
13384
|
-
* generated policies call the `auth.*` helper functions, and `CREATE POLICY`
|
|
13385
|
-
* validates those exist, so this cannot run before auth is initialized. The
|
|
13386
|
-
* gate conditions mirror `ensureCollectionSchema` (mode, bundle shape, driver
|
|
13387
|
-
* support) — and because that function already ran and explained any skip on
|
|
13388
|
-
* this same boot, the benign gates here return quietly rather than logging the
|
|
13389
|
-
* same reason twice. The one thing it does say out loud is a driver that
|
|
13390
|
-
* created tables but cannot apply policies: that is the difference between a
|
|
13391
|
-
* served collection and a 401, and it must not pass in silence.
|
|
13392
|
-
*/
|
|
13393
|
-
async function ensureCollectionPolicies(bundle, dataSources, env) {
|
|
13394
|
-
if ((env.REBASE_MIGRATE_ON_BOOT || "ensure") === "none") return;
|
|
13859
|
+
function warnOnUnusableBundleShape(bundle) {
|
|
13395
13860
|
if (bundle.manifest.kind !== "backend") return;
|
|
13396
13861
|
if (!bundle.manifest.entry?.config) return;
|
|
13397
|
-
if (
|
|
13398
|
-
|
|
13399
|
-
if (!primary) return;
|
|
13400
|
-
if (!primary.bootstrapper.ensureCollectionPolicies) {
|
|
13401
|
-
const skew = describeDriverSkew(primary.driverVersion, readRuntimeVersion(bundleResolutionRoots(bundle.dir)));
|
|
13402
|
-
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 }));
|
|
13403
|
-
return;
|
|
13404
|
-
}
|
|
13405
|
-
const loaded = await loadCollectionsFromDirectory(bundle.collectionsDir);
|
|
13406
|
-
if (loaded.length === 0) return;
|
|
13407
|
-
const collections = collectionsStoredBy(loaded, primary, dataSources);
|
|
13408
|
-
if (collections.length === 0) return;
|
|
13409
|
-
const { applied } = await primary.bootstrapper.ensureCollectionPolicies(collections, preInitDriverResult(primary), (message) => logger.debug(`policies: ${message}`));
|
|
13410
|
-
if (applied > 0) logger.info(`Applied ${applied} RLS policy statement(s) before serving.`);
|
|
13411
|
-
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\`.`);
|
|
13412
13864
|
}
|
|
13413
13865
|
//#endregion
|
|
13414
|
-
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, WebhookDispatcher, _resetRebaseMock, _setRebaseMock, applyCollectionDefaults, assertAllowedOutboundUrl, assertCollectionConfigs, assertDistinctSuffixes, assertEmailLinkBases, 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 };
|
|
13415
13867
|
|
|
13416
13868
|
//# sourceMappingURL=index.es.js.map
|