@rebasepro/server 0.11.0 → 0.11.1-canary.g8caabf3
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/contract-routes.d.ts +0 -1
- package/dist/boot/bundle.d.ts +17 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +266 -78
- package/dist/index.es.js.map +1 -1
- package/dist/init.d.ts +1 -17
- package/dist/{openapi-generator-CeOnlAJ3.js → openapi-generator-Bi3kcf6D.js} +2 -2
- package/dist/{openapi-generator-CeOnlAJ3.js.map → openapi-generator-Bi3kcf6D.js.map} +1 -1
- package/dist/serve-spa.d.ts +31 -1
- package/dist/src-BYbxB4PR.js.map +1 -1
- package/dist/{src-q6_elgGZ.js → src-DjtcIpz3.js} +2 -2
- package/dist/src-DjtcIpz3.js.map +1 -0
- package/package.json +5 -5
- package/dist/src-q6_elgGZ.js.map +0 -1
|
@@ -29,7 +29,6 @@ export interface ContractRoutesConfig {
|
|
|
29
29
|
* at boot, so there was nothing to hash when it was built.
|
|
30
30
|
*/
|
|
31
31
|
schemaVersion?: string;
|
|
32
|
-
mode: "cms" | "baas";
|
|
33
32
|
/** Runtime package version, surfaced so a client can report what it built against. */
|
|
34
33
|
runtimeVersion?: string;
|
|
35
34
|
}
|
package/dist/boot/bundle.d.ts
CHANGED
|
@@ -13,10 +13,23 @@ export interface LoadedBundle {
|
|
|
13
13
|
collectionsDir?: string;
|
|
14
14
|
functionsDir?: string;
|
|
15
15
|
cronsDir?: string;
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Built static apps to serve from this process, in mount order.
|
|
18
|
+
*
|
|
19
|
+
* A list, not a single directory: one process serves a site at `/` and an
|
|
20
|
+
* admin at `/admin`. Entries whose directory is missing are dropped with a
|
|
21
|
+
* warning, so a partially-built bundle still boots its API.
|
|
22
|
+
*/
|
|
23
|
+
staticApps: LoadedStaticApp[];
|
|
24
|
+
}
|
|
25
|
+
/** One built static app inside a loaded bundle, with an absolute directory. */
|
|
26
|
+
export interface LoadedStaticApp {
|
|
27
|
+
/** Public base path, e.g. `/` or `/admin`. */
|
|
28
|
+
path: string;
|
|
29
|
+
/** Absolute path to the built assets. */
|
|
30
|
+
dir: string;
|
|
31
|
+
/** Serve `index.html` for unmatched paths under `path`. */
|
|
32
|
+
spa: boolean;
|
|
20
33
|
}
|
|
21
34
|
/**
|
|
22
35
|
* Read and validate a bundle's manifest.
|
|
@@ -83,7 +96,6 @@ export declare function createSourceBundle(options: {
|
|
|
83
96
|
functions?: string;
|
|
84
97
|
crons?: string;
|
|
85
98
|
schema?: string;
|
|
86
|
-
mode?: "cms" | "baas" | "static";
|
|
87
99
|
app?: string;
|
|
88
100
|
}): LoadedBundle;
|
|
89
101
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ export { loadEnv } from "./env";
|
|
|
32
32
|
export type { RebaseEnv } from "./env";
|
|
33
33
|
export * from "./types";
|
|
34
34
|
export * from "./services/driver-registry";
|
|
35
|
+
export * from "./services/webhook-service";
|
|
35
36
|
export { cleanupDevPortFile, listenWithPortRetry } from "./utils/dev-port";
|
|
36
37
|
export { serveSPA } from "./serve-spa";
|
|
37
38
|
export { installShutdownHandlers } from "./init/shutdown";
|
package/dist/index.es.js
CHANGED
|
@@ -3,7 +3,7 @@ import process from "process";
|
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
4
|
import { i as __toESM, n as __exportAll } from "./chunk-DSJWtz9O.js";
|
|
5
5
|
import { C as RebaseClientError, S as RebaseApiError, _ as toCanonicalOp, a as DEFAULT_DATA_SOURCE_KEY, b as GeoPoint, f as isPostgresCollectionConfig, i as DEFAULT_STORAGE_SOURCE_KEY, n as serializeCollections, r as SCHEMA_VERSION_HEADER, s as isSQLAdmin, t as computeSchemaVersion, u as getCollectionDataPath, v as EntityReference, x as Vector, y as EntityRelation } from "./src-BYbxB4PR.js";
|
|
6
|
-
import { a as deserializeFilter, c as serializeLogicalCondition, d as resolveDataSource, f as findRelation, h as toSnakeCase, i as buildSdkData, l as CollectionRegistry, m as buildCompositeId, n as serializeOrderBy, o as deserializeLogicalCondition, p as resolveCollectionRelations, r as buildRoutedRebaseData, s as serializeFilter, t as deserializeOrderBy, u as createDataSourceRegistry } from "./src-
|
|
6
|
+
import { a as deserializeFilter, c as serializeLogicalCondition, d as resolveDataSource, f as findRelation, h as toSnakeCase, i as buildSdkData, l as CollectionRegistry, m as buildCompositeId, n as serializeOrderBy, o as deserializeLogicalCondition, p as resolveCollectionRelations, r as buildRoutedRebaseData, s as serializeFilter, t as deserializeOrderBy, u as createDataSourceRegistry } from "./src-DjtcIpz3.js";
|
|
7
7
|
import { t as logger } from "./logger-BYU66ENZ.js";
|
|
8
8
|
import { a as generateRefreshToken, c as getRefreshTokenTtlMs, d as verifyAccessToken, f as verifyDownloadToken, i as generateDownloadToken, l as hashRefreshToken, n as configureJwt, o as getAccessTokenExpiry, p as require_jsonwebtoken, r as generateAccessToken, s as getRefreshTokenExpiry, t as MAX_COOKIE_AGE_MS } from "./jwt-D-eI6TTu.js";
|
|
9
9
|
import { t as nativeDynamicImport } from "./dynamic-import-Dvh-K5fl.js";
|
|
@@ -765,7 +765,14 @@ var TABLE$2 = "\"rebase\".\"idempotency_keys\"";
|
|
|
765
765
|
* by a scheduled job — there is no cron guaranteed to be running.
|
|
766
766
|
*/
|
|
767
767
|
var TTL_HOURS = 24;
|
|
768
|
-
/**
|
|
768
|
+
/**
|
|
769
|
+
* The principal a key belongs to; anonymous and service writes share a sentinel.
|
|
770
|
+
*
|
|
771
|
+
* The NUL is written as an escape, not as a raw byte in the source. The
|
|
772
|
+
* sentinel itself is deliberate — a uid can never contain one — but written
|
|
773
|
+
* literally it makes this file test as binary, and every repo-wide grep then
|
|
774
|
+
* skips all 124 lines of it silently. Identical at runtime.
|
|
775
|
+
*/
|
|
769
776
|
function principal(uid) {
|
|
770
777
|
return uid && uid.length > 0 ? uid : "\0anon";
|
|
771
778
|
}
|
|
@@ -11970,7 +11977,7 @@ function assertStorageAccessControlConfigured(state, isProduction) {
|
|
|
11970
11977
|
//#region src/init/docs.ts
|
|
11971
11978
|
async function mountOpenApiDocs(app, basePath, enableSwagger, activeCollections, requireAuth) {
|
|
11972
11979
|
if (enableSwagger === false || activeCollections.length === 0) return;
|
|
11973
|
-
const { generateOpenApiSpec } = await import("./openapi-generator-
|
|
11980
|
+
const { generateOpenApiSpec } = await import("./openapi-generator-Bi3kcf6D.js");
|
|
11974
11981
|
app.get(`${basePath}/docs`, (c) => {
|
|
11975
11982
|
const spec = generateOpenApiSpec(activeCollections, {
|
|
11976
11983
|
basePath,
|
|
@@ -17553,13 +17560,37 @@ function createEmailService(config) {
|
|
|
17553
17560
|
}
|
|
17554
17561
|
//#endregion
|
|
17555
17562
|
//#region src/singleton.ts
|
|
17556
|
-
|
|
17563
|
+
/**
|
|
17564
|
+
* The backing instance lives on a process-global slot, NOT in a module-local
|
|
17565
|
+
* variable — because more than one copy of this module can be loaded into one
|
|
17566
|
+
* process, and a module-local would leave every copy but the booting one dead.
|
|
17567
|
+
*
|
|
17568
|
+
* That is the normal layout under the managed runtime, not an edge case: the
|
|
17569
|
+
* image ships the framework at `/app/node_modules`, while a project's bundle
|
|
17570
|
+
* installs its own dependencies into `/bundle/node_modules` — and every custom
|
|
17571
|
+
* function imports `defineFunction` from `@rebasepro/server`, which resolves to
|
|
17572
|
+
* the bundle's transitively-installed copy. `initializeRebaseBackend()` then ran
|
|
17573
|
+
* against `/app`'s copy while every function held `/bundle`'s, so `rebase.data`,
|
|
17574
|
+
* `rebase.storage` and `rebase.dataAsAdmin` threw "server not initialized yet"
|
|
17575
|
+
* on EVERY request, forever, in an otherwise healthy process.
|
|
17576
|
+
*
|
|
17577
|
+
* `Symbol.for` is the fix because its registry is per-process rather than
|
|
17578
|
+
* per-module: whichever copy boots publishes here, and every other copy — same
|
|
17579
|
+
* version or not — reads the same live client.
|
|
17580
|
+
*/
|
|
17581
|
+
var INSTANCE_SLOT = Symbol.for("@rebasepro/server:singleton-instance");
|
|
17582
|
+
function getInstance() {
|
|
17583
|
+
return globalThis[INSTANCE_SLOT] ?? null;
|
|
17584
|
+
}
|
|
17585
|
+
function setInstance(client) {
|
|
17586
|
+
globalThis[INSTANCE_SLOT] = client;
|
|
17587
|
+
}
|
|
17557
17588
|
/**
|
|
17558
17589
|
* @internal Called once during server initialization to set the backing instance.
|
|
17559
17590
|
* This is invoked by `initializeRebaseBackend()` — never call it manually.
|
|
17560
17591
|
*/
|
|
17561
17592
|
function _initRebase(client) {
|
|
17562
|
-
|
|
17593
|
+
setInstance(client);
|
|
17563
17594
|
}
|
|
17564
17595
|
/**
|
|
17565
17596
|
* @internal Allows overriding the underlying instance for unit testing.
|
|
@@ -17567,17 +17598,17 @@ function _initRebase(client) {
|
|
|
17567
17598
|
*/
|
|
17568
17599
|
function _setRebaseMock(mockInstance) {
|
|
17569
17600
|
if (process.env.NODE_ENV !== "test") throw new Error("_setRebaseMock can only be called in a test environment (NODE_ENV=test).");
|
|
17570
|
-
|
|
17571
|
-
...
|
|
17601
|
+
setInstance({
|
|
17602
|
+
...getInstance() || {},
|
|
17572
17603
|
...mockInstance
|
|
17573
|
-
};
|
|
17604
|
+
});
|
|
17574
17605
|
}
|
|
17575
17606
|
/**
|
|
17576
17607
|
* @internal Resets the singleton instance, useful for afterEach() in test suites.
|
|
17577
17608
|
*/
|
|
17578
17609
|
function _resetRebaseMock() {
|
|
17579
17610
|
if (process.env.NODE_ENV !== "test") throw new Error("_resetRebaseMock can only be called in a test environment.");
|
|
17580
|
-
|
|
17611
|
+
setInstance(null);
|
|
17581
17612
|
}
|
|
17582
17613
|
/**
|
|
17583
17614
|
* The server-side Rebase singleton.
|
|
@@ -17616,8 +17647,9 @@ function _resetRebaseMock() {
|
|
|
17616
17647
|
*/
|
|
17617
17648
|
var rebase = new Proxy({}, {
|
|
17618
17649
|
get(_, prop) {
|
|
17619
|
-
|
|
17620
|
-
|
|
17650
|
+
const instance = getInstance();
|
|
17651
|
+
if (!instance) throw new Error(`rebase.${String(prop)}: server not initialized yet. The singleton is available after Rebase starts — don't call it at import time.`);
|
|
17652
|
+
return instance[prop];
|
|
17621
17653
|
},
|
|
17622
17654
|
set(_, prop) {
|
|
17623
17655
|
throw new Error(`Cannot set rebase.${String(prop)} directly. The singleton is read-only. Use _initRebase() during server startup.`);
|
|
@@ -17664,21 +17696,16 @@ async function _initializeRebaseBackend(config) {
|
|
|
17664
17696
|
const dataSourceRegistry = createDataSourceRegistry(config.dataSources);
|
|
17665
17697
|
collectionRegistry.setDataSources(dataSourceRegistry);
|
|
17666
17698
|
if (config.callbacks) collectionRegistry.setGlobalCallbacks(config.callbacks);
|
|
17667
|
-
const mode = config.mode ?? "cms";
|
|
17668
|
-
logger.info(mode === "baas" ? "Starting in baas mode — collections derived from the database schema" : "Starting in cms mode — collections from config");
|
|
17669
17699
|
let activeCollections = config.collections || [];
|
|
17670
|
-
if (
|
|
17671
|
-
if (activeCollections.length > 0 || config.collectionsDir) {
|
|
17672
|
-
logger.warn("Ignoring configured collections: baas mode derives them from the database schema. Remove `collections`/`collectionsDir`, or use mode: \"cms\" to serve them.");
|
|
17673
|
-
activeCollections = [];
|
|
17674
|
-
}
|
|
17675
|
-
} else if (config.collectionsDir && activeCollections.length === 0) {
|
|
17700
|
+
if (config.collectionsDir && activeCollections.length === 0) {
|
|
17676
17701
|
activeCollections = await loadCollectionsFromDirectory(config.collectionsDir);
|
|
17677
17702
|
logger.info("Auto-discovered collections", {
|
|
17678
17703
|
count: activeCollections.length,
|
|
17679
17704
|
dir: config.collectionsDir
|
|
17680
17705
|
});
|
|
17681
17706
|
}
|
|
17707
|
+
const introspectCollections = activeCollections.length === 0;
|
|
17708
|
+
logger.info(introspectCollections ? "No collections declared — deriving them from the database schema" : "Serving declared collections");
|
|
17682
17709
|
const realtimeServices = {};
|
|
17683
17710
|
const delegates = {};
|
|
17684
17711
|
let bootstrappers = config.bootstrappers || [];
|
|
@@ -17706,16 +17733,16 @@ async function _initializeRebaseBackend(config) {
|
|
|
17706
17733
|
const driverResult = await bootstrapper.initializeDriver({
|
|
17707
17734
|
collections: activeCollections,
|
|
17708
17735
|
collectionRegistry,
|
|
17709
|
-
|
|
17736
|
+
introspectCollections,
|
|
17710
17737
|
baas: config.baas
|
|
17711
17738
|
});
|
|
17712
17739
|
delegates[b.id || bootstrapper.type] = driverResult.driver;
|
|
17713
|
-
if (
|
|
17740
|
+
if (introspectCollections) {
|
|
17714
17741
|
const driverName = b.id || bootstrapper.type;
|
|
17715
|
-
if (!driverResult.collections) throw new Error(`Driver "${driverName}"
|
|
17742
|
+
if (!driverResult.collections) throw new Error(`Driver "${driverName}" cannot derive collections from the database schema, and this project declared none. Declare collections, or use a driver that implements introspection (e.g. @rebasepro/server-postgres).`);
|
|
17716
17743
|
if (driverResult.collections.length === 0) logger.warn(`Driver "${driverName}" found no tables to serve. The data API will not be mounted. Create tables (migrations, SQL, any tool) and restart.`);
|
|
17717
17744
|
}
|
|
17718
|
-
if (driverResult.collections?.length) activeCollections = [...activeCollections, ...driverResult.collections];
|
|
17745
|
+
if (introspectCollections && driverResult.collections?.length) activeCollections = [...activeCollections, ...driverResult.collections];
|
|
17719
17746
|
if ((b.id || bootstrapper.type) === defaultDriverId || !defaultDriverResult) defaultDriverResult = driverResult;
|
|
17720
17747
|
if (bootstrapper.initializeRealtime) {
|
|
17721
17748
|
const realtime = await bootstrapper.initializeRealtime({}, driverResult);
|
|
@@ -17958,7 +17985,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
17958
17985
|
if (apiKeyPreAuth) router.use("/*", apiKeyPreAuth);
|
|
17959
17986
|
router.use("/*", createRequireAuth({ serviceKey: internalServiceKey }), requireAdmin);
|
|
17960
17987
|
};
|
|
17961
|
-
const schemaEditorEnabled = config.schemaEditor ?? (!!config.collectionsDir && process.env.NODE_ENV !== "production"
|
|
17988
|
+
const schemaEditorEnabled = config.schemaEditor ?? (!!config.collectionsDir && !introspectCollections && process.env.NODE_ENV !== "production");
|
|
17962
17989
|
if (schemaEditorEnabled && !config.collectionsDir) logger.warn("schemaEditor is enabled but no collectionsDir is set — the schema editor has nowhere to write. Skipping.");
|
|
17963
17990
|
if (schemaEditorEnabled && config.collectionsDir) {
|
|
17964
17991
|
let editorModule;
|
|
@@ -18215,7 +18242,6 @@ async function _initializeRebaseBackend(config) {
|
|
|
18215
18242
|
contractRouter.route("/", createContractRoutes({
|
|
18216
18243
|
collectionRegistry,
|
|
18217
18244
|
schemaVersion: config.schemaVersion,
|
|
18218
|
-
mode,
|
|
18219
18245
|
runtimeVersion: config.runtimeVersion
|
|
18220
18246
|
}));
|
|
18221
18247
|
config.app.route(`${basePath}/meta`, contractRouter);
|
|
@@ -19465,6 +19491,102 @@ function loadEnv(options) {
|
|
|
19465
19491
|
return env;
|
|
19466
19492
|
}
|
|
19467
19493
|
//#endregion
|
|
19494
|
+
//#region src/services/webhook-service.ts
|
|
19495
|
+
var WebhookDispatcher = class {
|
|
19496
|
+
webhooks = [];
|
|
19497
|
+
maxRetries = 3;
|
|
19498
|
+
retryDelays = [
|
|
19499
|
+
1e3,
|
|
19500
|
+
5e3,
|
|
19501
|
+
15e3
|
|
19502
|
+
];
|
|
19503
|
+
/** Register webhooks to watch */
|
|
19504
|
+
setWebhooks(webhooks) {
|
|
19505
|
+
this.webhooks = webhooks.filter((w) => w.enabled);
|
|
19506
|
+
}
|
|
19507
|
+
/** Called when a entity changes — checks if any webhook matches */
|
|
19508
|
+
async onEntityChange(table, event, id, entity, previousEntity) {
|
|
19509
|
+
const matchingWebhooks = this.webhooks.filter((w) => w.table === table && w.events.includes(event));
|
|
19510
|
+
if (matchingWebhooks.length === 0) return [];
|
|
19511
|
+
const results = [];
|
|
19512
|
+
for (const webhook of matchingWebhooks) {
|
|
19513
|
+
const payload = {
|
|
19514
|
+
type: event,
|
|
19515
|
+
table,
|
|
19516
|
+
record: entity,
|
|
19517
|
+
old_record: event === "UPDATE" ? previousEntity : void 0,
|
|
19518
|
+
schema: "public",
|
|
19519
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
19520
|
+
};
|
|
19521
|
+
const result = await this.deliverWithRetry(webhook, event, payload);
|
|
19522
|
+
results.push(result);
|
|
19523
|
+
}
|
|
19524
|
+
return results;
|
|
19525
|
+
}
|
|
19526
|
+
async deliverWithRetry(webhook, event, payload) {
|
|
19527
|
+
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
|
|
19528
|
+
const result = await this.deliver(webhook, event, payload, attempt);
|
|
19529
|
+
if (result.success) return result;
|
|
19530
|
+
if (attempt < this.maxRetries) await new Promise((r) => setTimeout(r, this.retryDelays[attempt - 1]));
|
|
19531
|
+
else return result;
|
|
19532
|
+
}
|
|
19533
|
+
return {
|
|
19534
|
+
webhookId: webhook.id,
|
|
19535
|
+
event,
|
|
19536
|
+
payload,
|
|
19537
|
+
statusCode: 0,
|
|
19538
|
+
responseBody: "Max retries exceeded",
|
|
19539
|
+
success: false,
|
|
19540
|
+
attemptNumber: this.maxRetries
|
|
19541
|
+
};
|
|
19542
|
+
}
|
|
19543
|
+
async deliver(webhook, event, payload, attemptNumber) {
|
|
19544
|
+
const body = JSON.stringify(payload);
|
|
19545
|
+
const headers = {
|
|
19546
|
+
"Content-Type": "application/json",
|
|
19547
|
+
"X-Webhook-Id": webhook.id,
|
|
19548
|
+
"X-Webhook-Event": event,
|
|
19549
|
+
"X-Webhook-Delivery": randomUUID$1(),
|
|
19550
|
+
"X-Webhook-Attempt": String(attemptNumber),
|
|
19551
|
+
...webhook.headers || {}
|
|
19552
|
+
};
|
|
19553
|
+
if (webhook.secret) headers["X-Webhook-Signature"] = `sha256=${createHmac("sha256", webhook.secret).update(body).digest("hex")}`;
|
|
19554
|
+
try {
|
|
19555
|
+
const controller = new AbortController();
|
|
19556
|
+
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
19557
|
+
const response = await fetch(webhook.url, {
|
|
19558
|
+
method: "POST",
|
|
19559
|
+
headers,
|
|
19560
|
+
body,
|
|
19561
|
+
signal: controller.signal
|
|
19562
|
+
});
|
|
19563
|
+
clearTimeout(timeout);
|
|
19564
|
+
const responseBody = await response.text().catch(() => "");
|
|
19565
|
+
const success = response.status >= 200 && response.status < 300;
|
|
19566
|
+
return {
|
|
19567
|
+
webhookId: webhook.id,
|
|
19568
|
+
event,
|
|
19569
|
+
payload,
|
|
19570
|
+
statusCode: response.status,
|
|
19571
|
+
responseBody: responseBody.slice(0, 1e3),
|
|
19572
|
+
success,
|
|
19573
|
+
attemptNumber
|
|
19574
|
+
};
|
|
19575
|
+
} catch (error) {
|
|
19576
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
19577
|
+
return {
|
|
19578
|
+
webhookId: webhook.id,
|
|
19579
|
+
event,
|
|
19580
|
+
payload,
|
|
19581
|
+
statusCode: 0,
|
|
19582
|
+
responseBody: message.slice(0, 1e3),
|
|
19583
|
+
success: false,
|
|
19584
|
+
attemptNumber
|
|
19585
|
+
};
|
|
19586
|
+
}
|
|
19587
|
+
}
|
|
19588
|
+
};
|
|
19589
|
+
//#endregion
|
|
19468
19590
|
//#region src/utils/dev-port.ts
|
|
19469
19591
|
var MAX_PORT_ATTEMPTS = 20;
|
|
19470
19592
|
/** Filename written next to the project `.env` so the CLI can read it. */
|
|
@@ -19606,6 +19728,21 @@ function writeStateFile(projectRoot, port, serviceKey) {
|
|
|
19606
19728
|
//#endregion
|
|
19607
19729
|
//#region src/serve-spa.ts
|
|
19608
19730
|
/**
|
|
19731
|
+
* Is `requestPath` the excluded path `prefix`, or something beneath it?
|
|
19732
|
+
*
|
|
19733
|
+
* Segment-aware on purpose. A plain `startsWith` reads "/api" as excluding
|
|
19734
|
+
* "/apidocs", and "/admin" as excluding "/administrators" — both ordinary
|
|
19735
|
+
* client-side routes of the app rooted at "/", both then answered with a 404
|
|
19736
|
+
* because the SPA fallback declined them and nothing else claims the path.
|
|
19737
|
+
* `apiBasePath` is always in the exclusion list, so this reached single-app
|
|
19738
|
+
* setups too, not just the multi-app ones the list was added for.
|
|
19739
|
+
*/
|
|
19740
|
+
function isUnderPath(requestPath, prefix) {
|
|
19741
|
+
const trimmed = prefix.replace(/\/+$/, "");
|
|
19742
|
+
if (trimmed === "") return true;
|
|
19743
|
+
return requestPath === trimmed || requestPath.startsWith(`${trimmed}/`);
|
|
19744
|
+
}
|
|
19745
|
+
/**
|
|
19609
19746
|
* Serve a Single Page Application from an Hono app.
|
|
19610
19747
|
*
|
|
19611
19748
|
* @internal Not part of the stable public API. Exported only because the
|
|
@@ -19615,21 +19752,30 @@ function writeStateFile(projectRoot, port, serviceKey) {
|
|
|
19615
19752
|
* may change without a major version bump.
|
|
19616
19753
|
*/
|
|
19617
19754
|
function serveSPA(app, config) {
|
|
19618
|
-
const { frontendPath, apiBasePath = "/api", excludePaths = [], indexFile = "index.html" } = config;
|
|
19755
|
+
const { frontendPath, apiBasePath = "/api", excludePaths = [], indexFile = "index.html", spa = true } = config;
|
|
19756
|
+
const rawBase = config.basePath ?? "/";
|
|
19757
|
+
const basePath = rawBase !== "/" ? rawBase.replace(/\/+$/, "") : "/";
|
|
19758
|
+
const isRoot = basePath === "/";
|
|
19619
19759
|
if (!fs$2.existsSync(frontendPath)) {
|
|
19620
19760
|
logger.warn(`⚠️ Frontend build path does not exist: ${frontendPath}`);
|
|
19621
19761
|
logger.warn(" SPA serving is disabled. Build your frontend first.");
|
|
19622
19762
|
return;
|
|
19623
19763
|
}
|
|
19624
|
-
|
|
19625
|
-
app.use(
|
|
19764
|
+
const scope = isRoot ? "/*" : `${basePath}/*`;
|
|
19765
|
+
app.use(scope, responseCompression());
|
|
19766
|
+
app.use(scope, serveStatic({
|
|
19626
19767
|
root: path$1.relative(process.cwd(), frontendPath),
|
|
19627
|
-
precompressed: true
|
|
19768
|
+
precompressed: true,
|
|
19769
|
+
...isRoot ? {} : { rewriteRequestPath: (p) => p.slice(basePath.length) || "/" }
|
|
19628
19770
|
}));
|
|
19771
|
+
if (!spa) {
|
|
19772
|
+
logger.info(`✅ Static serving enabled at ${basePath} from: ${frontendPath}`);
|
|
19773
|
+
return;
|
|
19774
|
+
}
|
|
19629
19775
|
const allExcludePaths = [apiBasePath, ...excludePaths];
|
|
19630
19776
|
let cachedHtml = null;
|
|
19631
|
-
app.get(
|
|
19632
|
-
if (allExcludePaths.some((p) => c.req.path
|
|
19777
|
+
app.get(scope, async (c, next) => {
|
|
19778
|
+
if (allExcludePaths.some((p) => isUnderPath(c.req.path, p))) return next();
|
|
19633
19779
|
const indexPath = path$1.join(frontendPath, indexFile);
|
|
19634
19780
|
if (!cachedHtml) try {
|
|
19635
19781
|
cachedHtml = await fsp.readFile(indexPath, "utf-8");
|
|
@@ -19639,7 +19785,7 @@ function serveSPA(app, config) {
|
|
|
19639
19785
|
}
|
|
19640
19786
|
return c.html(cachedHtml);
|
|
19641
19787
|
});
|
|
19642
|
-
logger.info(`✅ SPA serving enabled from: ${frontendPath}`);
|
|
19788
|
+
logger.info(`✅ SPA serving enabled at ${basePath} from: ${frontendPath}`);
|
|
19643
19789
|
}
|
|
19644
19790
|
//#endregion
|
|
19645
19791
|
//#region src/boot/bundle.ts
|
|
@@ -19654,6 +19800,36 @@ var BundleError = class extends Error {
|
|
|
19654
19800
|
};
|
|
19655
19801
|
var MANIFEST_FILENAME = "manifest.json";
|
|
19656
19802
|
/**
|
|
19803
|
+
* Bring a format-1 manifest up to the shape the rest of this runtime expects.
|
|
19804
|
+
*
|
|
19805
|
+
* Old bundles booting on a new runtime is the case the format version exists to
|
|
19806
|
+
* protect, so this is not a courtesy — it is the contract. A project built
|
|
19807
|
+
* before the rename ships `mode` and a single `entry.static` directory string,
|
|
19808
|
+
* and without this it would boot with no `kind` (so every gate keyed on
|
|
19809
|
+
* `kind === "backend"` would skip) and an `entry.static` the loader would try to
|
|
19810
|
+
* iterate as a list.
|
|
19811
|
+
*
|
|
19812
|
+
* In place, and only ever filling in what is absent, so a format-2 manifest
|
|
19813
|
+
* passes through untouched.
|
|
19814
|
+
*/
|
|
19815
|
+
function upgradeLegacyManifest(manifest) {
|
|
19816
|
+
const legacy = manifest;
|
|
19817
|
+
if (!legacy.kind) legacy.kind = legacy.mode === "static" ? "static" : "backend";
|
|
19818
|
+
const entry = legacy.entry;
|
|
19819
|
+
if (!entry) return;
|
|
19820
|
+
if (typeof entry.static === "string") entry.static = [{
|
|
19821
|
+
path: "/",
|
|
19822
|
+
dir: entry.static,
|
|
19823
|
+
spa: true
|
|
19824
|
+
}];
|
|
19825
|
+
else if (!entry.static && typeof entry.admin === "string") entry.static = [{
|
|
19826
|
+
path: "/",
|
|
19827
|
+
dir: entry.admin,
|
|
19828
|
+
spa: true
|
|
19829
|
+
}];
|
|
19830
|
+
delete entry.admin;
|
|
19831
|
+
}
|
|
19832
|
+
/**
|
|
19657
19833
|
* Read and validate a bundle's manifest.
|
|
19658
19834
|
*
|
|
19659
19835
|
* The checks here are the runtime half of the compatibility contract, and they
|
|
@@ -19671,7 +19847,8 @@ function readBundleManifest(bundleDir) {
|
|
|
19671
19847
|
throw new BundleError(`${manifestPath} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
19672
19848
|
}
|
|
19673
19849
|
if (typeof manifest.bundleFormat !== "number") throw new BundleError(`${manifestPath} is missing "bundleFormat".`);
|
|
19674
|
-
if (manifest.bundleFormat >
|
|
19850
|
+
if (manifest.bundleFormat > 2) throw new BundleError(`This bundle uses format ${manifest.bundleFormat}, but this runtime understands up to 2.`, "Upgrade the runtime image, or rebuild the bundle with a matching CLI.");
|
|
19851
|
+
upgradeLegacyManifest(manifest);
|
|
19675
19852
|
const contract = manifest.runtime?.contract;
|
|
19676
19853
|
if (typeof contract === "number" && contract !== 1) throw new BundleError(`This bundle targets runtime contract v${contract}, but this runtime implements v1.`, contract > 1 ? "Upgrade the runtime image to a version that implements the newer contract." : "Rebuild the bundle against the current runtime (`rebase build`), or run a runtime image from the previous major.");
|
|
19677
19854
|
return manifest;
|
|
@@ -19720,8 +19897,14 @@ function loadBundle(bundleDir) {
|
|
|
19720
19897
|
collectionsDir: entry.collections ? resolveEntry(entry.collections, "collections") : entry.config ? resolveEntry(path.join(entry.config, "collections"), "collections") : void 0,
|
|
19721
19898
|
functionsDir: resolveEntry(entry.functions, "functions"),
|
|
19722
19899
|
cronsDir: resolveEntry(entry.crons, "crons"),
|
|
19723
|
-
|
|
19724
|
-
|
|
19900
|
+
staticApps: (entry.static ?? []).map((item) => {
|
|
19901
|
+
const resolved = resolveEntry(item.dir, `static app "${item.path}"`);
|
|
19902
|
+
return resolved ? {
|
|
19903
|
+
path: item.path,
|
|
19904
|
+
dir: resolved,
|
|
19905
|
+
spa: item.spa !== false
|
|
19906
|
+
} : void 0;
|
|
19907
|
+
}).filter((item) => item !== void 0).sort((a, b) => b.path.length - a.path.length)
|
|
19725
19908
|
};
|
|
19726
19909
|
}
|
|
19727
19910
|
/**
|
|
@@ -19770,7 +19953,7 @@ function createSourceBundle(options) {
|
|
|
19770
19953
|
return {
|
|
19771
19954
|
dir,
|
|
19772
19955
|
manifest: {
|
|
19773
|
-
bundleFormat:
|
|
19956
|
+
bundleFormat: 2,
|
|
19774
19957
|
runtime: {
|
|
19775
19958
|
range: `^1`,
|
|
19776
19959
|
builtAgainst: "source",
|
|
@@ -19778,7 +19961,7 @@ function createSourceBundle(options) {
|
|
|
19778
19961
|
},
|
|
19779
19962
|
schemaVersion: "",
|
|
19780
19963
|
app: options.app ?? "backend",
|
|
19781
|
-
|
|
19964
|
+
kind: "backend",
|
|
19782
19965
|
entry: {
|
|
19783
19966
|
config: options.config ?? configDir,
|
|
19784
19967
|
collections: collectionsDir,
|
|
@@ -19797,8 +19980,7 @@ function createSourceBundle(options) {
|
|
|
19797
19980
|
collectionsDir: resolve(collectionsDir),
|
|
19798
19981
|
functionsDir: resolve(options.functions),
|
|
19799
19982
|
cronsDir: resolve(options.crons),
|
|
19800
|
-
|
|
19801
|
-
staticDir: void 0
|
|
19983
|
+
staticApps: []
|
|
19802
19984
|
};
|
|
19803
19985
|
}
|
|
19804
19986
|
/**
|
|
@@ -20741,11 +20923,11 @@ async function bootFromBundle(options = {}) {
|
|
|
20741
20923
|
const devRoot = process.env.REBASE_DEV_PROJECT_ROOT || process.cwd();
|
|
20742
20924
|
logger.info("Loaded bundle", {
|
|
20743
20925
|
app: bundle.manifest.app,
|
|
20744
|
-
|
|
20926
|
+
kind: bundle.manifest.kind,
|
|
20745
20927
|
schemaVersion: bundle.manifest.schemaVersion,
|
|
20746
20928
|
builtAgainst: bundle.manifest.runtime?.builtAgainst
|
|
20747
20929
|
});
|
|
20748
|
-
if (bundle.manifest.
|
|
20930
|
+
if (bundle.manifest.kind === "static") return bootStaticApp(bundle, devRoot, options);
|
|
20749
20931
|
const env = loadBootEnv();
|
|
20750
20932
|
const isProduction = env.NODE_ENV === "production";
|
|
20751
20933
|
const configExports = await loadBundleConfigExports(bundle);
|
|
@@ -20775,7 +20957,6 @@ async function bootFromBundle(options = {}) {
|
|
|
20775
20957
|
server,
|
|
20776
20958
|
app,
|
|
20777
20959
|
basePath: env.REBASE_BASE_PATH,
|
|
20778
|
-
mode: bundle.manifest.mode ?? "cms",
|
|
20779
20960
|
collectionsDir: bundle.collectionsDir,
|
|
20780
20961
|
functionsDir: bundle.functionsDir,
|
|
20781
20962
|
cronsDir: bundle.cronsDir,
|
|
@@ -20821,20 +21002,24 @@ async function bootFromBundle(options = {}) {
|
|
|
20821
21002
|
if (!env.REBASE_METRICS_TOKEN) logger.warn("Metrics are enabled without REBASE_METRICS_TOKEN — /metrics is readable by anyone who can reach this port. Set a token, or keep the port on a private network.");
|
|
20822
21003
|
app.route("/metrics", createMetricsRoutes(metrics.registry, env.REBASE_METRICS_TOKEN));
|
|
20823
21004
|
}
|
|
20824
|
-
if (env.REBASE_SERVE_STATIC) {
|
|
20825
|
-
const
|
|
20826
|
-
|
|
20827
|
-
|
|
20828
|
-
|
|
20829
|
-
|
|
20830
|
-
|
|
20831
|
-
|
|
20832
|
-
|
|
20833
|
-
|
|
20834
|
-
|
|
20835
|
-
|
|
20836
|
-
|
|
20837
|
-
|
|
21005
|
+
if (env.REBASE_SERVE_STATIC) for (const staticApp of bundle.staticApps) {
|
|
21006
|
+
const siblings = bundle.staticApps.filter((other) => other !== staticApp).map((other) => other.path).filter((other) => other !== "/");
|
|
21007
|
+
logger.info("Serving static assets", {
|
|
21008
|
+
path: staticApp.dir,
|
|
21009
|
+
at: staticApp.path
|
|
21010
|
+
});
|
|
21011
|
+
serveSPA(app, {
|
|
21012
|
+
frontendPath: staticApp.dir,
|
|
21013
|
+
basePath: staticApp.path,
|
|
21014
|
+
apiBasePath: env.REBASE_BASE_PATH,
|
|
21015
|
+
excludePaths: [
|
|
21016
|
+
"/health",
|
|
21017
|
+
"/livez",
|
|
21018
|
+
"/metrics",
|
|
21019
|
+
...siblings
|
|
21020
|
+
],
|
|
21021
|
+
spa: staticApp.spa
|
|
21022
|
+
});
|
|
20838
21023
|
}
|
|
20839
21024
|
let port = env.PORT;
|
|
20840
21025
|
if (options.listen !== false) if (isProduction) {
|
|
@@ -20886,8 +21071,7 @@ async function bootFromBundle(options = {}) {
|
|
|
20886
21071
|
* backend — the only difference is what the bundle contains.
|
|
20887
21072
|
*/
|
|
20888
21073
|
async function bootStaticApp(bundle, devRoot, options) {
|
|
20889
|
-
|
|
20890
|
-
if (!staticRoot) throw new BundleError("A static bundle declares no assets to serve.", "Its manifest has `mode: \"static\"` but no `entry.static` — rebuild the app with `rebase build`.");
|
|
21074
|
+
if (bundle.staticApps.length === 0) throw new BundleError("A static bundle declares no assets to serve.", "Its manifest has `kind: \"static\"` but no `entry.static` — rebuild the app with `rebase build`.");
|
|
20891
21075
|
const isProduction = process.env.NODE_ENV === "production";
|
|
20892
21076
|
const requestedPort = Number(process.env.PORT ?? "3001") || 3001;
|
|
20893
21077
|
const basePath = process.env.REBASE_BASE_PATH || "/api";
|
|
@@ -20907,19 +21091,26 @@ async function bootStaticApp(bundle, devRoot, options) {
|
|
|
20907
21091
|
latencyMs: 0
|
|
20908
21092
|
}));
|
|
20909
21093
|
if (metrics) app.route("/metrics", createMetricsRoutes(metrics.registry, metricsToken));
|
|
20910
|
-
|
|
20911
|
-
|
|
20912
|
-
|
|
20913
|
-
|
|
20914
|
-
|
|
20915
|
-
|
|
20916
|
-
|
|
20917
|
-
|
|
20918
|
-
|
|
20919
|
-
|
|
20920
|
-
|
|
20921
|
-
|
|
20922
|
-
|
|
21094
|
+
for (const staticApp of bundle.staticApps) {
|
|
21095
|
+
const siblings = bundle.staticApps.filter((other) => other !== staticApp).map((other) => other.path).filter((other) => other !== "/");
|
|
21096
|
+
logger.info("Serving static app", {
|
|
21097
|
+
app: bundle.manifest.app,
|
|
21098
|
+
path: staticApp.dir,
|
|
21099
|
+
at: staticApp.path
|
|
21100
|
+
});
|
|
21101
|
+
serveSPA(app, {
|
|
21102
|
+
frontendPath: staticApp.dir,
|
|
21103
|
+
basePath: staticApp.path,
|
|
21104
|
+
apiBasePath: basePath,
|
|
21105
|
+
excludePaths: [
|
|
21106
|
+
"/health",
|
|
21107
|
+
"/livez",
|
|
21108
|
+
"/metrics",
|
|
21109
|
+
...siblings
|
|
21110
|
+
],
|
|
21111
|
+
spa: staticApp.spa
|
|
21112
|
+
});
|
|
21113
|
+
}
|
|
20923
21114
|
let port = requestedPort;
|
|
20924
21115
|
if (options.listen !== false) if (isProduction) {
|
|
20925
21116
|
await new Promise((resolve, reject) => {
|
|
@@ -20997,7 +21188,8 @@ async function ensureCollectionSchema(bundle, dataSources, env) {
|
|
|
20997
21188
|
logger.info("REBASE_MIGRATE_ON_BOOT=none — leaving the database schema untouched.");
|
|
20998
21189
|
return;
|
|
20999
21190
|
}
|
|
21000
|
-
if (
|
|
21191
|
+
if (bundle.manifest.kind !== "backend") return;
|
|
21192
|
+
if (!bundle.manifest.entry?.config) return;
|
|
21001
21193
|
if (!bundle.collectionsDir) return;
|
|
21002
21194
|
const primary = dataSources[0];
|
|
21003
21195
|
if (!primary?.bootstrapper.ensureCollectionSchema) return;
|
|
@@ -21043,7 +21235,6 @@ function createContractRoutes(config) {
|
|
|
21043
21235
|
version: config.runtimeVersion ?? "unknown",
|
|
21044
21236
|
contract: 1
|
|
21045
21237
|
},
|
|
21046
|
-
mode: config.mode,
|
|
21047
21238
|
collections: serialized,
|
|
21048
21239
|
collectionSlugs: collections.map((collection) => collection.slug).filter((slug) => Boolean(slug)).sort(),
|
|
21049
21240
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -21062,15 +21253,12 @@ function createContractRoutes(config) {
|
|
|
21062
21253
|
router.get("/schema-version", (c) => {
|
|
21063
21254
|
const schemaVersion = schemaVersionOf(config.collectionRegistry.getRawCollections());
|
|
21064
21255
|
c.header(SCHEMA_VERSION_HEADER, schemaVersion);
|
|
21065
|
-
return c.json({
|
|
21066
|
-
schemaVersion,
|
|
21067
|
-
mode: config.mode
|
|
21068
|
-
});
|
|
21256
|
+
return c.json({ schemaVersion });
|
|
21069
21257
|
});
|
|
21070
21258
|
logger.debug("Contract routes mounted");
|
|
21071
21259
|
return router;
|
|
21072
21260
|
}
|
|
21073
21261
|
//#endregion
|
|
21074
|
-
export { ApiError, BundleError, CronScheduler, DEFAULT_DRIVER_ID, DEFAULT_MAX_FILE_SIZE, DEFAULT_STORAGE_ID, DOCUMENT_MIME_TYPES, DefaultDriverRegistry, DefaultStorageRegistry, GCSStorageController, IMAGE_MIME_TYPES, LocalStorageController, MetricsRegistry, S3StorageController, SMTPEmailService, TransformCache, TusHandler, _resetRebaseMock, _setRebaseMock, applyCollectionDefaults, authJwt, authRoles, authUid, 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, extractUserFromToken, fileTokenAuth, generateSecurePassword, getEmailVerificationTemplate, getMagicLinkTemplate, getPasswordResetTemplate, getUserInvitationTemplate, getWelcomeEmailTemplate, hashPassword, httpMethodToOperation, initializeDataSource, initializeDataSources, initializeRebaseBackend, installShutdownHandlers, isApiKeyToken, isAuthAdapter, isDatabaseAdapter, isLocalhostOrigin, isOperationAllowed, isRebaseApiError, isTransformableImage, listBackupObjects, listenWithPortRetry, loadBootEnv, loadBundle, loadBundleConfigExports, loadBundleSchema, loadCollectionsFromDirectory, loadCronJobsFromDirectory, loadEnv, loadFunctionsFromDirectory, loadUsersCollection, logger, optionalAuth, parseBackupDestination, parseBackupTimestamp, parseTransformOptions, queryTokenAuth, readBackupBytes, readBundleManifest, rebase, requireAdmin, requireAuth, resolveAuthHooks, resolveAuthOptions, resolveCorsOrigin, resolveDataSources, resolveEmailOptions, resolveStorageBackend, resolveStorageSources, runFromBundle, safeCompare, serveSPA, transformImage, validateApiKey, validateCronExpression, validatePasswordStrength, verifyPassword };
|
|
21262
|
+
export { ApiError, BundleError, CronScheduler, DEFAULT_DRIVER_ID, DEFAULT_MAX_FILE_SIZE, DEFAULT_STORAGE_ID, DOCUMENT_MIME_TYPES, DefaultDriverRegistry, DefaultStorageRegistry, GCSStorageController, IMAGE_MIME_TYPES, LocalStorageController, MetricsRegistry, S3StorageController, SMTPEmailService, TransformCache, TusHandler, WebhookDispatcher, _resetRebaseMock, _setRebaseMock, applyCollectionDefaults, authJwt, authRoles, authUid, 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, extractUserFromToken, fileTokenAuth, generateSecurePassword, getEmailVerificationTemplate, getMagicLinkTemplate, getPasswordResetTemplate, getUserInvitationTemplate, getWelcomeEmailTemplate, hashPassword, httpMethodToOperation, initializeDataSource, initializeDataSources, initializeRebaseBackend, installShutdownHandlers, isApiKeyToken, isAuthAdapter, isDatabaseAdapter, isLocalhostOrigin, isOperationAllowed, isRebaseApiError, isTransformableImage, listBackupObjects, listenWithPortRetry, loadBootEnv, loadBundle, loadBundleConfigExports, loadBundleSchema, loadCollectionsFromDirectory, loadCronJobsFromDirectory, loadEnv, loadFunctionsFromDirectory, loadUsersCollection, logger, optionalAuth, parseBackupDestination, parseBackupTimestamp, parseTransformOptions, queryTokenAuth, readBackupBytes, readBundleManifest, rebase, requireAdmin, requireAuth, resolveAuthHooks, resolveAuthOptions, resolveCorsOrigin, resolveDataSources, resolveEmailOptions, resolveStorageBackend, resolveStorageSources, runFromBundle, safeCompare, serveSPA, transformImage, validateApiKey, validateCronExpression, validatePasswordStrength, verifyPassword };
|
|
21075
21263
|
|
|
21076
21264
|
//# sourceMappingURL=index.es.js.map
|