@rebasepro/server 0.16.0 → 0.16.1-canary.g0d7af95
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/{GCSStorageController-BEmDYFKc.js → GCSStorageController-Bl7nmhdv.js} +7 -6
- package/dist/{GCSStorageController-BEmDYFKc.js.map → GCSStorageController-Bl7nmhdv.js.map} +1 -1
- package/dist/{S3StorageController-B2EDXXMH.js → S3StorageController-CWvvrRpF.js} +7 -6
- package/dist/{S3StorageController-B2EDXXMH.js.map → S3StorageController-CWvvrRpF.js.map} +1 -1
- package/dist/{admin_block-DLILvzle.js → admin_block-BeypnEfb.js} +6 -6
- package/dist/admin_block-BeypnEfb.js.map +1 -0
- package/dist/api/ast-schema-editor.d.ts +23 -0
- package/dist/api/contract-routes.d.ts +1 -1
- package/dist/api/errors.d.ts +1 -1
- package/dist/api/index.d.ts +3 -3
- package/dist/api/live-schema-routes.d.ts +122 -0
- package/dist/api/logs-routes.d.ts +1 -1
- package/dist/api/mount.d.ts +41 -0
- package/dist/api/rest/api-generator.d.ts +2 -2
- package/dist/api/rest/index.d.ts +1 -1
- package/dist/api/rest/query-parser.d.ts +1 -1
- package/dist/api/schema-editor-routes.d.ts +1 -1
- package/dist/api/types.d.ts +12 -4
- package/dist/{schema-editor-routes-CV9k0w3G.js → ast-schema-editor-BrarYZCq.js} +37 -45
- package/dist/ast-schema-editor-BrarYZCq.js.map +1 -0
- package/dist/auth/adapter-middleware.d.ts +2 -2
- package/dist/auth/admin-roles-route.d.ts +2 -2
- package/dist/auth/admin-user-ops.d.ts +3 -3
- package/dist/auth/admin-users-route.d.ts +4 -6
- package/dist/auth/api-keys/api-key-middleware.d.ts +2 -2
- package/dist/auth/api-keys/api-key-permission-guard.d.ts +1 -1
- package/dist/auth/api-keys/api-key-routes.d.ts +2 -2
- package/dist/auth/api-keys/api-key-store.d.ts +1 -1
- package/dist/auth/api-keys/index.d.ts +9 -9
- package/dist/auth/apple-oauth.d.ts +2 -2
- package/dist/auth/auth-hooks.d.ts +4 -4
- package/dist/auth/bitbucket-oauth.d.ts +2 -2
- package/dist/auth/builtin-auth-adapter.d.ts +8 -4
- package/dist/auth/capabilities.d.ts +51 -0
- package/dist/auth/captcha.d.ts +86 -0
- package/dist/auth/cookie-utils.d.ts +2 -2
- package/dist/auth/discord-oauth.d.ts +2 -2
- package/dist/auth/facebook-oauth.d.ts +2 -2
- package/dist/auth/github-oauth.d.ts +2 -2
- package/dist/auth/gitlab-oauth.d.ts +2 -2
- package/dist/auth/google-oauth.d.ts +1 -1
- package/dist/auth/index.d.ts +58 -54
- package/dist/auth/jwks-routes.d.ts +1 -1
- package/dist/auth/jwt.d.ts +1 -1
- package/dist/auth/linkedin-oauth.d.ts +2 -2
- package/dist/auth/magic-link-routes.d.ts +10 -3
- package/dist/auth/mfa-gate.d.ts +1 -1
- package/dist/auth/mfa-routes.d.ts +3 -3
- package/dist/auth/microsoft-oauth.d.ts +2 -2
- package/dist/auth/middleware.d.ts +6 -7
- package/dist/auth/otp-routes.d.ts +89 -0
- package/dist/auth/rate-limiter.d.ts +2 -2
- package/dist/auth/require-auth.d.ts +1 -1
- package/dist/auth/reset-password-admin.d.ts +4 -4
- package/dist/auth/routes.d.ts +18 -10
- package/dist/auth/session-routes.d.ts +3 -3
- package/dist/auth/slack-oauth.d.ts +2 -2
- package/dist/auth/spotify-oauth.d.ts +2 -2
- package/dist/auth/token-revocation.d.ts +2 -2
- package/dist/auth/twitter-oauth.d.ts +2 -2
- package/dist/{auth-5Et5mnUA.js → auth-DU-nUjPp.js} +604 -4376
- package/dist/auth-DU-nUjPp.js.map +1 -0
- package/dist/backup/backup-common.d.ts +1 -1
- package/dist/backup/backup-routes.d.ts +3 -3
- package/dist/backup/index.d.ts +3 -3
- package/dist/{backup-C6ljYVTp.js → backup-CRdZkA6c.js} +7 -6
- package/dist/{backup-C6ljYVTp.js.map → backup-CRdZkA6c.js.map} +1 -1
- package/dist/boot/boot.d.ts +5 -5
- package/dist/boot/bundle.d.ts +10 -1
- package/dist/boot/driver.d.ts +1 -1
- package/dist/boot/env.d.ts +28 -1
- package/dist/boot/fetch-bundle.d.ts +144 -1
- package/dist/boot/options.d.ts +30 -6
- package/dist/boot/resource-adapters.d.ts +31 -0
- package/dist/boot/resource-loading.d.ts +10 -0
- package/dist/boot/role.d.ts +3 -2
- package/dist/boot/sources.d.ts +8 -2
- package/dist/boot/version-skew.d.ts +1 -1
- package/dist/collections/BackendCollectionRegistry.d.ts +1 -1
- package/dist/collections/loader.d.ts +1 -1
- package/dist/collections/validate-config.d.ts +13 -0
- package/dist/{contract-routes-DZ-LBpSL.js → contract-routes-D8TvxPLo.js} +8 -7
- package/dist/contract-routes-D8TvxPLo.js.map +1 -0
- package/dist/cron/cron-routes.d.ts +2 -2
- package/dist/cron/cron-scheduler.d.ts +3 -3
- package/dist/cron/index.d.ts +8 -8
- package/dist/{cron-loader-YhhQeVBM.js → cron-loader-d9WMFENB.js} +8 -7
- package/dist/{cron-loader-YhhQeVBM.js.map → cron-loader-d9WMFENB.js.map} +1 -1
- package/dist/{cron-routes-maM_RlUu.js → cron-routes-BsukhMGm.js} +9 -8
- package/dist/cron-routes-BsukhMGm.js.map +1 -0
- package/dist/{cron-scheduler-DIpYBmZP.js → cron-scheduler-BB82dWuU.js} +9 -9
- package/dist/cron-scheduler-BB82dWuU.js.map +1 -0
- package/dist/{cron-store-DfH_4Cd9.js → cron-store-CbFfQhbg.js} +11 -9
- package/dist/{cron-store-DfH_4Cd9.js.map → cron-store-CbFfQhbg.js.map} +1 -1
- package/dist/ddl-bootstrap-5YZCZ8qk.js +146 -0
- package/dist/ddl-bootstrap-5YZCZ8qk.js.map +1 -0
- package/dist/deploy/pod-contract.d.ts +139 -0
- package/dist/dev-secrets.d.ts +52 -0
- package/dist/{dynamic-import-Dvh-K5fl.js → dynamic-import-X40pTZUQ.js} +5 -4
- package/dist/{dynamic-import-Dvh-K5fl.js.map → dynamic-import-X40pTZUQ.js.map} +1 -1
- package/dist/email/dev-sink.d.ts +88 -0
- package/dist/email/index.d.ts +9 -7
- package/dist/email/link-base.d.ts +1 -1
- package/dist/email/smtp-email-service.d.ts +2 -2
- package/dist/email/templates.d.ts +12 -0
- package/dist/email/types.d.ts +18 -6
- package/dist/env.d.ts +1 -1
- package/dist/{errors-EBYiaJ2E.js → errors-DBwpj9N8.js} +8 -7
- package/dist/errors-DBwpj9N8.js.map +1 -0
- package/dist/{function-loader-DDS1v7YX.js → function-loader-LLdmBFoL.js} +9 -8
- package/dist/{function-loader-DDS1v7YX.js.map → function-loader-LLdmBFoL.js.map} +1 -1
- package/dist/{function-routes-Btcez1T-.js → function-routes-ClT6UQpD.js} +8 -7
- package/dist/function-routes-ClT6UQpD.js.map +1 -0
- package/dist/functions/context.d.ts +141 -0
- package/dist/functions/define-function.d.ts +1 -1
- package/dist/functions/function-routes.d.ts +9 -3
- package/dist/functions/guards.d.ts +76 -0
- package/dist/functions/index.d.ts +96 -5
- package/dist/functions/index.js +919 -0
- package/dist/functions/index.js.map +1 -0
- package/dist/functions/internal.d.ts +26 -0
- package/dist/functions/proxy.d.ts +1 -1
- package/dist/functions/request-timeout.d.ts +9 -1
- package/dist/functions/runtime-env.d.ts +92 -0
- package/dist/functions/wait-until.d.ts +74 -0
- package/dist/history/history-routes.d.ts +2 -2
- package/dist/history/index.d.ts +1 -1
- package/dist/history-recorder-B67maiTi.js +76 -0
- package/dist/history-recorder-B67maiTi.js.map +1 -0
- package/dist/history-store-oiqhb3HU.js +210 -0
- package/dist/history-store-oiqhb3HU.js.map +1 -0
- package/dist/index.d.ts +65 -54
- package/dist/index.es.js +3667 -328
- package/dist/index.es.js.map +1 -1
- package/dist/init/docs.d.ts +1 -1
- package/dist/init/middlewares.d.ts +1 -1
- package/dist/init/shutdown.d.ts +4 -0
- package/dist/init/storage.d.ts +1 -1
- package/dist/init/surfaces.d.ts +10 -0
- package/dist/init.d.ts +179 -22
- package/dist/internal-tables-DpxfPaEB.js +97 -0
- package/dist/internal-tables-DpxfPaEB.js.map +1 -0
- package/dist/jobs/index.d.ts +5 -5
- package/dist/{jobs-CyOKXXlu.js → jobs-B2ELModo.js} +11 -9
- package/dist/{jobs-CyOKXXlu.js.map → jobs-B2ELModo.js.map} +1 -1
- package/dist/{jwt-DxH9fLPt.js → jwt-DoHkMMWF.js} +9 -8
- package/dist/jwt-DoHkMMWF.js.map +1 -0
- package/dist/{logger-DfvF_8r-.js → logger-DS03e908.js} +104 -10
- package/dist/logger-DS03e908.js.map +1 -0
- package/dist/{logs-routes-CWBLQj2l.js → logs-routes-DB72iQSr.js} +36 -9
- package/dist/logs-routes-DB72iQSr.js.map +1 -0
- package/dist/metrics/history-recorder.d.ts +18 -0
- package/dist/metrics/history-store.d.ts +123 -0
- package/dist/metrics/index.d.ts +10 -2
- package/dist/{openapi-generator-BCKJRUS4.js → openapi-generator-FFGT0H6O.js} +7 -11
- package/dist/{openapi-generator-BCKJRUS4.js.map → openapi-generator-FFGT0H6O.js.map} +1 -1
- package/dist/{proxy-Bj5DVllb.js → proxy-CMymhnwG.js} +9 -6
- package/dist/{proxy-Bj5DVllb.js.map → proxy-CMymhnwG.js.map} +1 -1
- package/dist/query-parser--nstjRCh.js +355 -0
- package/dist/query-parser--nstjRCh.js.map +1 -0
- package/dist/{request-timeout-BuFoEKwT.js → request-timeout-C8gkc-j7.js} +21 -6
- package/dist/request-timeout-C8gkc-j7.js.map +1 -0
- package/dist/rls-audit/index.d.ts +111 -0
- package/dist/{rolldown-runtime-DSJWtz9O.js → rolldown-runtime-dW7B1o5h.js} +4 -3
- package/dist/schema-edit/apply-schema-change.d.ts +141 -0
- package/dist/schema-edit/github-repository.d.ts +67 -0
- package/dist/schema-edit/local-git-repository.d.ts +28 -0
- package/dist/schema-edit/project-root.d.ts +43 -0
- package/dist/schema-edit/remote-source.d.ts +25 -0
- package/dist/schema-edit/schema-edit-permissions.d.ts +129 -0
- package/dist/schema-editor-routes-Bf5h5Emf.js +87 -0
- package/dist/schema-editor-routes-Bf5h5Emf.js.map +1 -0
- package/dist/schemas-DBxgjM9A.js +4153 -0
- package/dist/schemas-DBxgjM9A.js.map +1 -0
- package/dist/{selection-_z6TM1DB.js → selection-CRpqKUbt.js} +6 -5
- package/dist/{selection-_z6TM1DB.js.map → selection-CRpqKUbt.js.map} +1 -1
- package/dist/services/webhook-service.d.ts +1 -1
- package/dist/singleton.d.ts +7 -0
- package/dist/{src-BPfYOeN4.js → src-B-CmIFMr.js} +8 -6
- package/dist/src-B-CmIFMr.js.map +1 -0
- package/dist/{src-CrCxd8km.js → src-Cdsw7DqV.js} +126 -5
- package/dist/src-Cdsw7DqV.js.map +1 -0
- package/dist/storage/GCSStorageController.d.ts +1 -1
- package/dist/storage/LocalStorageController.d.ts +1 -1
- package/dist/storage/S3StorageController.d.ts +1 -1
- package/dist/storage/cache-headers.d.ts +87 -0
- package/dist/storage/index.d.ts +11 -11
- package/dist/storage/path-pattern.d.ts +58 -0
- package/dist/storage/policies.d.ts +88 -0
- package/dist/storage/range.d.ts +63 -0
- package/dist/storage/rendition-cache.d.ts +45 -0
- package/dist/storage/routes.d.ts +17 -5
- package/dist/storage/storage-registry.d.ts +1 -1
- package/dist/storage/triggers.d.ts +66 -0
- package/dist/storage/tus-handler.d.ts +34 -3
- package/dist/topics/runtime.d.ts +68 -0
- package/dist/{types-DSnOC4mF.js → types-BfKcm9do.js} +5 -4
- package/dist/{types-DSnOC4mF.js.map → types-BfKcm9do.js.map} +1 -1
- package/dist/utils/host.d.ts +58 -0
- package/dist/utils/logger.d.ts +0 -15
- package/dist/utils/request-id.d.ts +1 -1
- package/functions/package.json +24 -0
- package/package.json +13 -7
- package/dist/admin_block-DLILvzle.js.map +0 -1
- package/dist/auth-5Et5mnUA.js.map +0 -1
- package/dist/contract-routes-DZ-LBpSL.js.map +0 -1
- package/dist/cron-routes-maM_RlUu.js.map +0 -1
- package/dist/cron-scheduler-DIpYBmZP.js.map +0 -1
- package/dist/ddl-bootstrap-Cywoj8Ta.js +0 -221
- package/dist/ddl-bootstrap-Cywoj8Ta.js.map +0 -1
- package/dist/errors-EBYiaJ2E.js.map +0 -1
- package/dist/function-routes-Btcez1T-.js.map +0 -1
- package/dist/jwt-DxH9fLPt.js.map +0 -1
- package/dist/logger-DfvF_8r-.js.map +0 -1
- package/dist/logs-routes-CWBLQj2l.js.map +0 -1
- package/dist/request-timeout-BuFoEKwT.js.map +0 -1
- package/dist/schema-editor-routes-CV9k0w3G.js.map +0 -1
- package/dist/src-BPfYOeN4.js.map +0 -1
- package/dist/src-CrCxd8km.js.map +0 -1
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { createRequire as __rebaseCreateRequire } from "module";
|
|
2
|
+
import __rebaseProcess from "process";
|
|
3
|
+
globalThis.process ??= __rebaseProcess;
|
|
4
|
+
__rebaseCreateRequire(import.meta.url);
|
|
5
|
+
import { t as logger } from "./logger-DS03e908.js";
|
|
6
|
+
//#region ../types/src/types/backend.ts
|
|
7
|
+
/**
|
|
8
|
+
* Type guard: can this admin plan a live schema change?
|
|
9
|
+
*
|
|
10
|
+
* Planning is engine-specific — it renders DDL, a Drizzle schema and the
|
|
11
|
+
* declarative SQL artifacts — so the implementation lives in the driver
|
|
12
|
+
* package. The server detects the capability structurally, exactly as it does
|
|
13
|
+
* for SQL, rather than importing an engine it is supposed to know nothing
|
|
14
|
+
* about.
|
|
15
|
+
*
|
|
16
|
+
* @group Admin
|
|
17
|
+
*/
|
|
18
|
+
function isSchemaEditingAdmin(admin) {
|
|
19
|
+
return !!admin && typeof admin.planSchemaChange === "function";
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Type guard: does this admin support SQL operations?
|
|
23
|
+
* @group Admin
|
|
24
|
+
*/
|
|
25
|
+
function isSQLAdmin(admin) {
|
|
26
|
+
return !!admin && typeof admin.executeSql === "function";
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/boot/ddl-bootstrap.ts
|
|
30
|
+
/**
|
|
31
|
+
* SQLSTATEs a *simultaneous boot* can raise from statements that are otherwise
|
|
32
|
+
* idempotent. Retrying is always the right answer for these: the second attempt
|
|
33
|
+
* finds the object present and does nothing.
|
|
34
|
+
*/
|
|
35
|
+
var CONCURRENT_DDL_SQLSTATES = /* @__PURE__ */ new Set([
|
|
36
|
+
"23505",
|
|
37
|
+
"42P06",
|
|
38
|
+
"42P07",
|
|
39
|
+
"42710",
|
|
40
|
+
"40P01"
|
|
41
|
+
]);
|
|
42
|
+
var DDL_RETRY_BASE_MS = 40;
|
|
43
|
+
/**
|
|
44
|
+
* Walk an error's `cause` chain, stopping at the first link `visit` accepts.
|
|
45
|
+
* Drizzle wraps the driver error, so nothing useful is ever on the top level.
|
|
46
|
+
*/
|
|
47
|
+
function hasInCauseChain(err, visit) {
|
|
48
|
+
let current = err;
|
|
49
|
+
for (let depth = 0; depth < 10 && current; depth++) {
|
|
50
|
+
if (typeof current !== "object") break;
|
|
51
|
+
const e = current;
|
|
52
|
+
if (visit(e)) return true;
|
|
53
|
+
current = e.cause;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Is this the loser of a race to create something that already exists?
|
|
59
|
+
*
|
|
60
|
+
* Deliberately narrow. A permission failure, an unreachable database or a typo
|
|
61
|
+
* in the DDL must surface on the first attempt rather than being retried four
|
|
62
|
+
* times and then reported as a race that never was.
|
|
63
|
+
*/
|
|
64
|
+
function isConcurrentDdlRace(err) {
|
|
65
|
+
return hasInCauseChain(err, (e) => typeof e.code === "string" && CONCURRENT_DDL_SQLSTATES.has(e.code) || typeof e.message === "string" && /already exists/i.test(e.message));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* SQLSTATEs that mean, unambiguously, *the object is already there*.
|
|
69
|
+
*
|
|
70
|
+
* A subset of {@link CONCURRENT_DDL_SQLSTATES} and a stricter question. The
|
|
71
|
+
* broad set answers "should this be retried"; this one answers "is it safe to
|
|
72
|
+
* carry on as though the statement had succeeded", which is a claim about the
|
|
73
|
+
* end state rather than about the attempt. Deadlock is not in it — a deadlocked
|
|
74
|
+
* statement did nothing and must be retried, not skipped.
|
|
75
|
+
*/
|
|
76
|
+
var DUPLICATE_OBJECT_SQLSTATES = /* @__PURE__ */ new Set([
|
|
77
|
+
"42P06",
|
|
78
|
+
"42P07",
|
|
79
|
+
"42710"
|
|
80
|
+
]);
|
|
81
|
+
/**
|
|
82
|
+
* Did this statement fail *because a peer already created the same object*?
|
|
83
|
+
*
|
|
84
|
+
* The narrow companion to {@link isConcurrentDdlRace}, for the one caller that
|
|
85
|
+
* needs to tell "someone beat me to it" from "this genuinely failed": a loop
|
|
86
|
+
* applying a schema plan, where treating every `23505` as a harmless race would
|
|
87
|
+
* silently swallow the one that matters — a unique constraint that cannot be
|
|
88
|
+
* added because the customer's existing rows violate it.
|
|
89
|
+
*
|
|
90
|
+
* `23505` is therefore only accepted when it names a `pg_catalog` index. That is
|
|
91
|
+
* what a lost `CREATE TYPE`/`CREATE TABLE` race raises (`pg_type_typname_nsp_index`
|
|
92
|
+
* is the one seen in practice); a unique violation on user data names the user's
|
|
93
|
+
* own constraint and is left to the caller.
|
|
94
|
+
*/
|
|
95
|
+
function isDuplicateObjectRace(err) {
|
|
96
|
+
return hasInCauseChain(err, (e) => {
|
|
97
|
+
if (typeof e.code !== "string") return false;
|
|
98
|
+
if (DUPLICATE_OBJECT_SQLSTATES.has(e.code)) return true;
|
|
99
|
+
if (e.code !== "23505") return false;
|
|
100
|
+
const constraint = typeof e.constraint === "string" ? e.constraint : "";
|
|
101
|
+
const detail = typeof e.detail === "string" ? e.detail : "";
|
|
102
|
+
return constraint.startsWith("pg_") || /\bpg_[a-z_]+_index\b/.test(detail);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* @param exec the driver's SQL escape hatch
|
|
107
|
+
* @param scope log prefix identifying the caller, e.g. `"cron-store"`
|
|
108
|
+
*/
|
|
109
|
+
function createDdlBootstrapper(exec, scope) {
|
|
110
|
+
/** Jittered, so peers that collided once do not collide again in lockstep. */
|
|
111
|
+
const backoff = (attempt) => new Promise((resolve) => setTimeout(resolve, DDL_RETRY_BASE_MS * attempt * (1 + Math.random())));
|
|
112
|
+
const step = async (label, run) => {
|
|
113
|
+
try {
|
|
114
|
+
await run();
|
|
115
|
+
} catch (err) {
|
|
116
|
+
logger.error(`[${scope}] ${label} failed`, { error: err });
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
step,
|
|
121
|
+
ensureObject(label, sqlText) {
|
|
122
|
+
return step(label, async () => {
|
|
123
|
+
for (let attempt = 1;; attempt++) try {
|
|
124
|
+
await exec(sqlText);
|
|
125
|
+
return;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (!isConcurrentDdlRace(err) || attempt >= 4) throw err;
|
|
128
|
+
logger.debug(`[${scope}] Lost a create race for ${label} with another instance (attempt ${attempt}/4) — retrying`);
|
|
129
|
+
await backoff(attempt);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
async isReadable(table) {
|
|
134
|
+
try {
|
|
135
|
+
await exec(`SELECT 1 FROM ${table} WHERE false`);
|
|
136
|
+
return true;
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
//#endregion
|
|
144
|
+
export { isDuplicateObjectRace as a, isConcurrentDdlRace as i, createDdlBootstrapper as n, isSQLAdmin as o, hasInCauseChain as r, isSchemaEditingAdmin as s, CONCURRENT_DDL_SQLSTATES as t };
|
|
145
|
+
|
|
146
|
+
//# sourceMappingURL=ddl-bootstrap-5YZCZ8qk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ddl-bootstrap-5YZCZ8qk.js","names":[],"sources":["../../types/src/types/backend.ts","../src/boot/ddl-bootstrap.ts"],"sourcesContent":["import type { CollectionConfig, FilterValues, WhereFilterOp } from \"./collections\";\nimport type { OrderByTuple } from \"./filter-operators\";\nimport type { LogicalCondition } from \"../controllers/data\";\nimport type { AuthAdapter } from \"./auth_adapter\";\nimport type { HistoryConfig } from \"../controllers/client\";\nimport type { ChannelBusSetting } from \"./channel_bus\";\nimport type { SchemaEditingAdmin } from \"./schema_editing\";\n\n// =============================================================================\n// DATABASE CONNECTION INTERFACES\n// =============================================================================\n\n/**\n * Abstract database connection interface.\n * Represents a connection to any database system.\n */\nexport interface DatabaseConnection {\n /**\n * Type identifier for this database (e.g., 'postgres', 'mongodb', 'mysql')\n */\n readonly type: string;\n\n /**\n * Whether the connection is currently active\n */\n readonly isConnected?: boolean;\n\n /**\n * Close the database connection and release resources.\n */\n close?(): Promise<void>;\n}\n\n// =============================================================================\n// QUERY BUILDING INTERFACES\n// =============================================================================\n\n/**\n * A single filter condition for database queries\n */\nexport interface QueryFilter {\n field: string;\n operator: WhereFilterOp;\n value: unknown;\n}\n\n/**\n * Options for fetching a collection of entities\n */\nexport interface FetchCollectionOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n filter?: FilterValues<Extract<keyof M, string>>;\n /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n offset?: number;\n startAfter?: unknown;\n searchString?: string;\n databaseId?: string;\n collection?: CollectionConfig;\n}\n\n/**\n * Options for searching entities\n */\nexport interface SearchOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n filter?: FilterValues<Extract<keyof M, string>>;\n /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n databaseId?: string;\n collection?: CollectionConfig;\n}\n\n/**\n * Options for counting entities\n */\nexport interface CountOptions<M extends Record<string, unknown> = Record<string, unknown>> {\n filter?: FilterValues<Extract<keyof M, string>>;\n /**\n * An `or(...)`/`and(...)` group, alongside `filter`.\n *\n * Counted as well as fetched, or `total` describes a different set of rows\n * from the one that was served — the same reason `filter` is here.\n */\n logical?: LogicalCondition;\n searchString?: string;\n databaseId?: string;\n}\n\n/**\n * Abstract condition builder interface.\n * Implementations translate Rebase filter conditions to database-specific queries.\n *\n * Note: This interface can be implemented as instance methods or as a class with static methods.\n * For static implementations (like DrizzleConditionBuilder), use the ConditionBuilderStatic type.\n *\n * @template T The type of condition returned by the builder (e.g., SQL for PostgreSQL, Filter<Document> for MongoDB)\n */\nexport interface ConditionBuilder<T = unknown> {\n /**\n * Build filter conditions from Rebase FilterValues\n */\n buildFilterConditions<M extends Record<string, unknown>>(\n filter: FilterValues<Extract<keyof M, string>>,\n collectionPath: string,\n ...args: unknown[]\n ): T[];\n\n /**\n * Build search conditions for text search\n */\n buildSearchConditions(\n searchString: string,\n properties: Record<string, unknown>,\n ...args: unknown[]\n ): T[];\n\n /**\n * Combine multiple conditions with AND operator\n */\n combineConditionsWithAnd(conditions: T[]): T | undefined;\n\n /**\n * Combine multiple conditions with OR operator\n */\n combineConditionsWithOr(conditions: T[]): T | undefined;\n}\n\n/**\n * Static condition builder type for implementations using static methods.\n * Use this type when the class provides static methods rather than instance methods.\n *\n * @example\n * // DrizzleConditionBuilder satisfies this type\n * const builder: ConditionBuilderStatic<SQL> = DrizzleConditionBuilder;\n */\nexport type ConditionBuilderStatic<T = unknown> = {\n buildFilterConditions<M extends Record<string, unknown>>(\n filter: FilterValues<Extract<keyof M, string>>,\n ...args: unknown[]\n ): T[];\n buildSearchConditions(\n searchString: string,\n properties: Record<string, unknown>,\n ...args: unknown[]\n ): T[];\n combineConditionsWithAnd(conditions: T[]): T | undefined;\n combineConditionsWithOr(conditions: T[]): T | undefined;\n};\n\n// =============================================================================\n// ENTITY REPOSITORY INTERFACES\n// =============================================================================\n\n/**\n * Abstract entity repository interface.\n * Handles all CRUD operations for entities in the database.\n *\n * Implementations should handle:\n * - Entity serialization/deserialization\n * - Relation resolution\n * - ID generation and conversion\n */\nexport interface DataRepository {\n /**\n * Fetch a single entity by ID\n */\n fetchOne<M extends Record<string, unknown>>(\n collectionPath: string,\n id: string | number,\n databaseId?: string\n ): Promise<Record<string, unknown> | undefined>;\n\n /**\n * Fetch a collection of entities with optional filtering, ordering, and pagination\n */\n fetchCollection<M extends Record<string, unknown>>(\n collectionPath: string,\n options?: FetchCollectionOptions<M>\n ): Promise<Record<string, unknown>[]>;\n\n /**\n * Search entities by text\n */\n searchRows<M extends Record<string, unknown>>(\n collectionPath: string,\n searchString: string,\n options?: SearchOptions<M>\n ): Promise<Record<string, unknown>[]>;\n\n /**\n * Count entities in a collection\n */\n count<M extends Record<string, unknown>>(\n collectionPath: string,\n options?: CountOptions<M>\n ): Promise<number>;\n\n /**\n * Save a entity (create or update)\n */\n save<M extends Record<string, unknown>>(\n collectionPath: string,\n values: Partial<M>,\n id?: string | number,\n databaseId?: string\n ): Promise<Record<string, unknown>>;\n\n /**\n * Delete a entity by ID\n */\n delete(\n collectionPath: string,\n id: string | number,\n databaseId?: string\n ): Promise<void>;\n\n /**\n * Check if a field value is unique in a collection\n */\n checkUniqueField(\n collectionPath: string,\n fieldName: string,\n value: unknown,\n excludeEntityId?: string,\n databaseId?: string\n ): Promise<boolean>;\n\n}\n\n// =============================================================================\n// REALTIME INTERFACES\n// =============================================================================\n\n/**\n * Configuration for subscribing to a collection\n */\nexport interface CollectionSubscriptionConfig {\n clientId: string;\n path: string;\n filter?: unknown;\n /**\n * An `or(...)`/`and(...)` group, applied alongside `filter`.\n *\n * Declared here because a subscription is a query, and every field a query\n * has this one needs too. It was missing, so the type-checked boundary\n * dropped it: the client sent the group, nothing rejected it, and the\n * subscription re-fetched with the group gone — pushing every row the\n * caller's policies allowed rather than the ones they asked for. The same\n * defect `FetchCollectionProps.logical` documents, one layer up.\n */\n logical?: LogicalCondition;\n /**\n * Where the subscription's page starts. Missing for the same reason, with\n * a quieter symptom: a subscriber watching page two was pushed page one,\n * and a `collection_update` frame carries no window for it to notice with.\n */\n offset?: number;\n /** See `FetchCollectionProps.orderBy`: a field name plus `order`, or a list of tuples. */\n orderBy?: string | OrderByTuple[];\n order?: \"desc\" | \"asc\";\n limit?: number;\n startAfter?: unknown;\n databaseId?: string;\n searchString?: string;\n /** Ask each row which declared search field matched. */\n searchExplain?: boolean;\n}\n\n/**\n * Configuration for subscribing to a single entity\n */\nexport interface SingleSubscriptionConfig {\n clientId: string;\n path: string;\n id: string | number;\n}\n\n/**\n * Opt-in retention for one set of broadcast channels.\n *\n * Retention is configured on the server and nowhere else. A channel is created\n * by whoever names it, so letting a client ask for its own history depth would\n * let any visitor commit the backend to unbounded storage; and presence-only or\n * notification-only channels — the overwhelming majority — must not pay for a\n * feature they never use. With no rules configured nothing is written, no table\n * is created, and broadcast behaves exactly as it did before history existed.\n */\nexport interface ChannelRetentionRule {\n /**\n * Channel name to match. Either exact (`\"doc:42\"`) or a trailing-`*` prefix\n * (`\"doc:*\"`). Deliberately not a full glob or RegExp: this decides what\n * gets written to disk, and a rule whose blast radius is not obvious at a\n * glance is the wrong shape for that.\n */\n match: string;\n /** Keep at most this many of the most recent messages per channel. */\n limit?: number;\n /**\n * Keep messages for at most this long. Accepts a millisecond count or a\n * short duration string (`\"30s\"`, `\"15m\"`, `\"24h\"`, `\"7d\"`).\n */\n ttl?: number | string;\n}\n\n/**\n * Server-side realtime options.\n *\n * The channel bus contract and its config live in `./channel_bus` so that a\n * transport shipped as its own package depends on the contract alone.\n */\nexport interface RealtimeChannelsConfig {\n /**\n * Retention rules, most specific first — the first match wins. Omitted or\n * empty means no channel retains anything.\n */\n channels?: ChannelRetentionRule[];\n /**\n * How channel broadcast and presence reach other backend instances.\n * Defaults to `{ type: \"memory\" }` — i.e. they don't.\n */\n bus?: ChannelBusSetting;\n}\n\n/**\n * Abstract realtime provider interface.\n * Handles real-time subscriptions and notifications for entity changes.\n */\nexport interface RealtimeProvider {\n /**\n * Subscribe to collection changes\n */\n subscribeToCollection(\n subscriptionId: string,\n config: CollectionSubscriptionConfig,\n callback?: (rows: Record<string, unknown>[]) => void\n ): void;\n\n /**\n * Subscribe to single entity changes\n */\n subscribeToOne(\n subscriptionId: string,\n config: SingleSubscriptionConfig,\n callback?: (row: Record<string, unknown> | null) => void\n ): void;\n\n /**\n * Unsubscribe from a subscription\n */\n unsubscribe(subscriptionId: string): void;\n\n /**\n * Notify all relevant subscribers of a entity update\n */\n notifyUpdate(\n path: string,\n id: string,\n row: Record<string, unknown> | null,\n databaseId?: string\n ): Promise<void>;\n\n /**\n * Called when the HTTP server is ready and listening.\n * Useful for providers that need the server address for callbacks.\n */\n onServerReady?(serverInfo: { port: number; hostname?: string }): void;\n\n /**\n * Gracefully shut down the realtime provider.\n * Called during server shutdown to clean up resources.\n */\n destroy?(): Promise<void>;\n\n /**\n * Stop the internal LISTEN client (e.g., PostgreSQL LISTEN/NOTIFY).\n * Called during graceful shutdown before closing database connections.\n */\n stopListening?(): Promise<void>;\n}\n\n// =============================================================================\n// COLLECTION REGISTRY INTERFACES\n// =============================================================================\n\n/**\n * Abstract collection registry interface.\n * Manages registration and lookup of entity collections.\n */\nexport interface CollectionRegistryInterface {\n /**\n * Register a collection\n */\n register(collection: CollectionConfig): void;\n\n /**\n * Get a collection by its path\n */\n getCollectionByPath(path: string): CollectionConfig | undefined;\n\n /**\n * Get all registered collections\n */\n getCollections(): CollectionConfig[];\n\n /**\n * Get the currently registered global callbacks, if any.\n */\n getGlobalCallbacks(): any | undefined;\n}\n\n// =============================================================================\n// DATA TRANSFORMER INTERFACES\n// =============================================================================\n\n/**\n * Abstract data transformer interface.\n * Handles serialization/deserialization between frontend and database formats.\n */\nexport interface DataTransformer {\n /**\n * Transform entity data for storage in the database\n */\n serializeToDatabase<M extends Record<string, unknown>>(\n entity: M,\n collection: CollectionConfig\n ): Record<string, unknown>;\n\n /**\n * Transform database data back to entity format\n */\n deserializeFromDatabase<M extends Record<string, unknown>>(\n data: Record<string, unknown>,\n collection: CollectionConfig\n ): Promise<M>;\n}\n\n// =============================================================================\n// DATABASE ADMIN — CAPABILITY-SPECIFIC INTERFACES (1.3)\n// =============================================================================\n\n/**\n * Administrative operations for SQL-based databases (PostgreSQL, MySQL, etc.).\n * Used by the SQL Editor, RLS Editor, and schema browser.\n *\n * @group Admin\n */\nexport interface SQLAdmin {\n /**\n * Execute raw SQL against the database.\n */\n executeSql(sql: string, options?: { database?: string; role?: string; params?: unknown[] }): Promise<Record<string, unknown>[]>;\n\n /**\n * Fetch the available databases on the server.\n */\n fetchAvailableDatabases?(): Promise<string[]>;\n\n /**\n * Fetch the available *native PostgreSQL* database roles (from `pg_roles`).\n *\n * These are connection-level roles — what the SQL editor can `SET ROLE` to,\n * and what `SecurityRule.pgRoles` targets. They are NOT application roles;\n * for those use {@link fetchApplicationRoles}.\n */\n fetchAvailableRoles?(): Promise<string[]>;\n\n /**\n * Fetch the *application-level* roles in use in this project.\n *\n * These are the strings stored on the users table's `roles` column and\n * exposed to policies as `rebase.roles()` — what `SecurityRule.roles`\n * matches against. Distinct from {@link fetchAvailableRoles}; the two are\n * not interchangeable.\n */\n fetchApplicationRoles?(): Promise<string[]>;\n\n /**\n * Fetch the current database name.\n */\n fetchCurrentDatabase?(): Promise<string | undefined>;\n}\n\n/**\n * Administrative operations for document-based databases (MongoDB, Firestore, etc.).\n * Used by future document administration tools.\n *\n * @group Admin\n */\nexport interface DocumentAdmin {\n /**\n * Execute an aggregation pipeline or equivalent query.\n */\n executeAggregate?(pipeline: Record<string, unknown>[]): Promise<Record<string, unknown>[]>;\n\n /**\n * Fetch statistics for a collection (document count, size, etc.).\n */\n fetchCollectionStats?(collectionName: string): Promise<{ count: number; sizeBytes?: number }>;\n}\n\n/**\n * Administrative operations for schema management.\n * Shared across SQL and document databases.\n *\n * @group Admin\n */\nexport interface SchemaAdmin {\n /**\n * Fetch database tables/collections not yet mapped to a Rebase collection.\n */\n fetchUnmappedTables?(mappedPaths?: string[]): Promise<string[]>;\n\n /**\n * Fetch column/field metadata for a single table/collection.\n * The return type is generic — SQL backends return TableMetadata,\n * document backends may return a different shape.\n */\n fetchTableMetadata?(tableName: string): Promise<unknown>;\n}\n\n/**\n * Metadata for a database branch.\n * @group Admin\n */\nexport interface BranchInfo {\n /** Branch name (without prefix). */\n name: string;\n /** The database this branch was created from. */\n parentDatabase: string;\n /** When the branch was created. */\n createdAt: Date;\n /** Size in bytes, if available from the server. */\n sizeBytes?: number;\n}\n\n/**\n * Administrative operations for database branching.\n * Allows creating isolated database copies for development/preview workflows.\n *\n * @group Admin\n */\nexport interface BranchAdmin {\n /** Create a new branch (database copy) from the current or specified source database. */\n createBranch(name: string, options?: { source?: string }): Promise<BranchInfo>;\n\n /** Delete a branch database. Cannot delete the main/default database. */\n deleteBranch(name: string): Promise<void>;\n\n /** List all branches (databases that were created via branching). */\n listBranches(): Promise<BranchInfo[]>;\n\n /** Get info about a specific branch. */\n getBranchInfo(name: string): Promise<BranchInfo | undefined>;\n}\n\n/**\n * Union type for all admin capabilities.\n * A backend may implement any combination of these interfaces.\n *\n * Use type guards (`isSQLAdmin`, `isDocumentAdmin`, `isSchemaAdmin`, `isBranchAdmin`)\n * to safely narrow the type before calling methods.\n *\n * @group Admin\n */\nexport type DatabaseAdmin = Partial<SQLAdmin> & Partial<DocumentAdmin> & Partial<SchemaAdmin>\n & Partial<BranchAdmin> & Partial<SchemaEditingAdmin>;\n\n/**\n * Type guard: can this admin plan a live schema change?\n *\n * Planning is engine-specific — it renders DDL, a Drizzle schema and the\n * declarative SQL artifacts — so the implementation lives in the driver\n * package. The server detects the capability structurally, exactly as it does\n * for SQL, rather than importing an engine it is supposed to know nothing\n * about.\n *\n * @group Admin\n */\nexport function isSchemaEditingAdmin(admin: DatabaseAdmin | undefined): admin is SchemaEditingAdmin {\n return !!admin && typeof (admin as SchemaEditingAdmin).planSchemaChange === \"function\";\n}\n\n/**\n * Type guard: does this admin support SQL operations?\n * @group Admin\n */\nexport function isSQLAdmin(admin: DatabaseAdmin | undefined): admin is SQLAdmin {\n return !!admin && typeof (admin as SQLAdmin).executeSql === \"function\";\n}\n\n/**\n * Type guard: does this admin support document operations?\n * @group Admin\n */\nexport function isDocumentAdmin(admin: DatabaseAdmin | undefined): admin is DocumentAdmin {\n return !!admin && (\n typeof (admin as DocumentAdmin).executeAggregate === \"function\" ||\n typeof (admin as DocumentAdmin).fetchCollectionStats === \"function\"\n );\n}\n\n/**\n * Type guard: does this admin support schema management?\n * @group Admin\n */\nexport function isSchemaAdmin(admin: DatabaseAdmin | undefined): admin is SchemaAdmin {\n return !!admin && (\n typeof (admin as SchemaAdmin).fetchUnmappedTables === \"function\" ||\n typeof (admin as SchemaAdmin).fetchTableMetadata === \"function\"\n );\n}\n\n/**\n * Type guard: does this admin support database branching?\n * @group Admin\n */\nexport function isBranchAdmin(admin: DatabaseAdmin | undefined): admin is BranchAdmin {\n return !!admin && typeof (admin as BranchAdmin).createBranch === \"function\";\n}\n\n// =============================================================================\n// LIFECYCLE INTERFACES (1.4)\n// =============================================================================\n\n/**\n * Health check result returned by `healthCheck()`.\n * @group Lifecycle\n */\nexport interface HealthCheckResult {\n /** Whether the backend is healthy and able to serve requests. */\n healthy: boolean;\n /** Round-trip latency to the database in milliseconds. */\n latencyMs: number;\n /** Optional details (e.g., pool stats, replication lag). */\n details?: Record<string, unknown>;\n}\n\n/**\n * Lifecycle contract for backend components that hold resources\n * (database connections, WebSocket pools, timers, etc.).\n *\n * All methods are optional — simple backends (e.g., in-memory) can skip them.\n * @group Lifecycle\n */\nexport interface BackendLifecycle {\n /**\n * Initialize the backend: open connections, run migrations, seed data.\n * Called once during startup. Idempotent.\n */\n initialize?(): Promise<void>;\n\n /**\n * Check whether the backend is healthy and reachable.\n * Should be fast (< 1 s) and safe to call frequently.\n */\n healthCheck?(): Promise<HealthCheckResult>;\n\n /**\n * Gracefully shut down: close connections, flush buffers, cancel timers.\n * After calling `destroy()`, no other methods should be called.\n */\n destroy?(): Promise<void>;\n}\n\n// =============================================================================\n// BACKEND FACTORY INTERFACES\n// =============================================================================\n\n/**\n * Configuration for creating a database backend\n */\nexport interface BackendConfig {\n /**\n * Type of database backend\n */\n type: string;\n\n /**\n * Database connection (implementation-specific)\n */\n connection: unknown;\n\n /**\n * Schema definition (implementation-specific, e.g., Drizzle schema for PostgreSQL)\n */\n schema?: unknown;\n}\n\n/**\n * A complete backend instance with all required services.\n *\n * Now includes optional lifecycle management and admin capabilities.\n */\nexport interface BackendInstance extends BackendLifecycle {\n /**\n * Entity repository for CRUD operations\n */\n entityRepository: DataRepository;\n\n /**\n * Realtime provider for subscriptions\n */\n realtimeProvider: RealtimeProvider;\n\n /**\n * Collection registry\n */\n collectionRegistry: CollectionRegistryInterface;\n\n /**\n * The underlying database connection\n */\n connection: DatabaseConnection;\n\n /**\n * Administrative operations (SQL, schema, documents).\n * What's available depends on the backend type — use type guards\n * (`isSQLAdmin`, `isSchemaAdmin`, etc.) to narrow.\n */\n admin?: DatabaseAdmin;\n}\n\n/**\n * Factory function type for creating backend instances\n */\nexport type BackendFactory<TConfig extends BackendConfig = BackendConfig> =\n (config: TConfig) => BackendInstance;\n\n// =============================================================================\n// BACKEND BOOTSTRAPPER (1.2)\n// =============================================================================\n\n/**\n * A `BackendBootstrapper` encapsulates all driver-specific initialization logic.\n *\n * Instead of hard-coding Postgres setup into `initializeRebaseBackend()`,\n * each database backend provides its own bootstrapper that knows how to:\n * - Create the DataDriver from a config object\n * - Optionally initialize auth tables\n * - Optionally create a realtime service\n * - Mount driver-specific API routes\n *\n * The main `initializeRebaseBackend()` becomes a **coordinator** that iterates\n * registered bootstrappers, calls their hooks, and wires the results together.\n *\n * @group Backend\n *\n * @example\n * ```typescript\n * // Third-party MySQL bootstrapper\n * const mysqlBootstrapper: BackendBootstrapper = {\n * type: \"mysql\",\n * initializeDriver: async (config) => new MySQLDataDriver(config.connection),\n * initializeRealtime: async (config) => new MySQLChangeStreamRealtime(config.connection),\n * };\n *\n * initializeRebaseBackend({\n * ...config,\n * bootstrappers: [postgresBootstrapper, mysqlBootstrapper]\n * });\n * ```\n */\nexport interface BackendBootstrapper {\n /**\n * Which driver type this bootstrapper handles.\n * Must match the `type` field on the driver config object\n * (e.g., `\"postgres\"`, `\"mongodb\"`, `\"mysql\"`).\n */\n type: string;\n\n /**\n * Unique identifier for this bootstrapper instance.\n * Used to register the driver in the driver registry.\n * Defaults to `type` if not set.\n */\n id?: string;\n\n /**\n * Whether this bootstrapper provides the default driver.\n * When true, the coordinator uses this driver as the primary one.\n */\n isDefault?: boolean;\n\n /**\n * Run database migrations for this driver.\n * Called by the coordinator after all drivers are initialized.\n */\n runMigrations?(config: unknown, driverResult: InitializedDriver): Promise<void>;\n\n /**\n * Create a DataDriver from the given config.\n * This is the only **required** method.\n */\n initializeDriver(config: unknown): Promise<InitializedDriver>;\n\n /**\n * Initialize auth tables / services if this driver supports them.\n * Return undefined if auth is not supported by this backend.\n */\n initializeAuth?(config: unknown, driverResult: InitializedDriver): Promise<BootstrappedAuth | undefined>;\n\n /**\n * Initialize history tables / services if this driver supports them.\n * Return undefined if history is not supported by this backend.\n */\n initializeHistory?(config: HistoryConfig, driverResult: InitializedDriver): Promise<{ historyService: unknown } | undefined>;\n\n /**\n * Create a realtime provider for this driver.\n * Return undefined if the driver does not support realtime.\n */\n initializeRealtime?(config: unknown, driverResult: InitializedDriver): Promise<RealtimeProvider | undefined>;\n\n /**\n * Mount any driver-specific HTTP routes (e.g., custom admin endpoints).\n * Called after all drivers are initialized.\n */\n mountRoutes?(app: unknown, basePath: string, driverResult: InitializedDriver): void;\n\n /**\n * Return admin capabilities for this driver.\n */\n getAdmin?(driverResult: InitializedDriver): DatabaseAdmin | undefined;\n\n /**\n * Bring the database's collection tables up to date, additively.\n *\n * Optional because it is only meaningful for schema-ful drivers. A managed\n * runtime boots a compiled project against a database it has never seen; auth\n * tables are ensured on boot but collection tables were created by nothing,\n * so every data request answered 500 on a missing relation. The CLI's `db\n * push` cannot fill the gap — it needs Atlas, and the runtime image ships no\n * CLI.\n *\n * Implementations MUST be additive-only: create missing tables, columns and\n * enum types, and never drop, narrow or rewrite anything. This runs\n * unattended against live customer data with nobody reading a diff, so the\n * destructive half stays a deliberate migration.\n *\n * `driverResult` is optional: this runs before `initializeDriver`, and only\n * the bundle path has a pre-init stand-in to pass. An adapter built by an\n * application already holds its own connection and MUST use it when this is\n * `undefined` — dereferencing it unconditionally works for managed tenants\n * and breaks every app that builds its own adapter.\n */\n ensureCollectionSchema?(\n collections: unknown[],\n driverResult?: InitializedDriver,\n log?: (message: string) => void\n ): Promise<{ applied: number }>;\n\n /**\n * Apply the collections' row-level-security policies, additively and\n * idempotently — the companion to {@link ensureCollectionSchema}.\n *\n * That method creates the tables; a table with RLS disabled and no policies\n * is not servable, because authenticated requests run as a restricted role:\n * a read with no `SELECT` policy returns nothing (a public collection\n * answers 401) and a write with no `INSERT`/`UPDATE` policy is denied. The\n * `db push` CLI applies these from the same collections, but it cannot reach\n * a managed tenant's in-cluster database — the runtime, already connected,\n * is the only thing that can.\n *\n * MUST be idempotent (re-run on every boot) and MUST NOT be destructive.\n * Runs after auth initialization, because the generated policies call the\n * `auth.*` helper functions and `CREATE POLICY` validates they exist.\n */\n ensureCollectionPolicies?(\n collections: unknown[],\n driverResult?: InitializedDriver,\n log?: (message: string) => void\n ): Promise<{ applied: number }>;\n\n /**\n * Read the collections schema version this database was last provisioned\n * from, or `null` when nothing has ever stamped it.\n *\n * The companion to {@link stampCollectionsSchemaVersion}: one process writes\n * what it applied, every other process compares itself to it. This is what\n * lets a split deployment — several processes over one database, only one of\n * which provisions — notice that a unit is serving against a schema it was\n * not built for. That failure is otherwise silent in both directions: a\n * column that does not exist is a SQL error on one route, and a policy that\n * was never applied is a 200 with no rows.\n *\n * `null` is not an error and MUST NOT be treated as one — every database\n * provisioned before the stamp existed reads this way, and so does every\n * fresh one until its first provisioning boot finishes.\n */\n readCollectionsSchemaVersion?(\n driverResult?: InitializedDriver\n ): Promise<string | null>;\n\n /**\n * Record the collections schema version this process just applied.\n *\n * Called only by the process that provisions, and only after both\n * {@link ensureCollectionSchema} and {@link ensureCollectionPolicies} have\n * run — a stamp written before the policies would claim a schema that is\n * only half in place, and the half that is missing is the one that fails\n * without an error.\n */\n stampCollectionsSchemaVersion?(\n version: string,\n driverResult?: InitializedDriver\n ): Promise<void>;\n\n /**\n * Initialize WebSocket server for realtime operations.\n */\n initializeWebsockets?(server: unknown, realtimeService: RealtimeProvider, driver: import(\"../controllers/data_driver\").DataDriver, config?: unknown, authAdapter?: AuthAdapter): Promise<void> | void;\n}\n\n/**\n * Result of `BackendBootstrapper.initializeDriver()`.\n * @group Backend\n */\nexport interface InitializedDriver {\n /** The DataDriver instance, ready for use. */\n driver: import(\"../controllers/data_driver\").DataDriver;\n\n /** The realtime service, if the driver created one during init. */\n realtimeProvider?: RealtimeProvider;\n\n /** A collection registry to register schema / tables into. */\n collectionRegistry?: CollectionRegistryInterface;\n\n /**\n * Collections the driver derived from the live database schema.\n *\n * Set by drivers that introspect in `baas` mode; the server serves these\n * instead of collections loaded from config files.\n */\n collections?: import(\"./collections\").CollectionConfig[];\n\n /** The underlying database connection (for lifecycle management). */\n connection?: DatabaseConnection;\n\n /**\n * Opaque handle that the bootstrapper can use in subsequent hooks\n * (e.g., `initializeAuth`, `mountRoutes`) to access driver internals.\n * Not used by the coordinator.\n */\n internals?: unknown;\n}\n\n/**\n * Result of `BackendBootstrapper.initializeAuth()`.\n * @group Backend\n */\nexport interface BootstrappedAuth {\n /** User management service. */\n userService: unknown;\n /** Role management service (optional, roles are now simple strings). */\n roleService?: unknown;\n /** Email service (optional). */\n emailService?: unknown;\n /** Combined Auth Repository for unified token and user management. */\n authRepository?: unknown;\n /**\n * Whether the auth schema in the database is one this runtime can serve.\n *\n * Folded into `healthCheck()` so a schema mismatch shows up as a degraded\n * health response. Without it, a server whose auth is entirely broken still\n * reports healthy — the database connection it probes is fine, and the\n * mismatch is only discovered one failed login at a time.\n */\n schemaHealthCheck?(): Promise<AuthSchemaHealth>;\n}\n\n/**\n * Result of {@link BootstrappedAuth.schemaHealthCheck}.\n * @group Lifecycle\n */\nexport interface AuthSchemaHealth {\n /** False when this runtime cannot be trusted to serve auth against this database. */\n healthy: boolean;\n /** Human-readable descriptions of each mismatch found. Empty when healthy. */\n problems: string[];\n /** Auth schema version recorded in the database, when it records one. */\n databaseVersion?: number | null;\n /** Auth schema version this runtime expects. */\n runtimeVersion?: number;\n}\n","import { logger } from \"../utils/logger.js\";\n\n/**\n * Helpers for the \"create my internal table if it isn't there yet\" bootstrap\n * that several stores run at boot.\n *\n * These all look trivially safe — every statement is `IF NOT EXISTS` — and they\n * are not, for two reasons that only show up with more than one app instance:\n *\n * 1. `CREATE … IF NOT EXISTS` reads the catalog and then writes to it, and\n * those two steps are not one atomic operation. Instances starting together\n * — a rolling deploy, a replica count going from 1 to N, a crash loop\n * restarting the fleet — can both see \"absent\" and both try to create. The\n * loser gets a duplicate key on a *catalog* index instead of the silent\n * no-op the syntax appears to promise. Measured against Postgres 18: with\n * five instances booting at once, 8 of 10 `ensureTable` calls hit it.\n *\n * 2. A bootstrap written as one long `try` block therefore abandons everything\n * after the losing statement — including, in every store that has one, the\n * `REVOKE` that takes the table back off the end-user role. That revoke is\n * a security control and must not be collateral damage from a race.\n *\n * So: retry the race, contain each statement separately, and decide what to do\n * next from what actually exists rather than from who won.\n */\n\n/** A driver's `executeSql`, narrowed to what a bootstrap needs. */\nexport type SqlExec = (sqlText: string, options?: { params?: unknown[] }) => Promise<Record<string, unknown>[]>;\n\n/**\n * SQLSTATEs a *simultaneous boot* can raise from statements that are otherwise\n * idempotent. Retrying is always the right answer for these: the second attempt\n * finds the object present and does nothing.\n */\nexport const CONCURRENT_DDL_SQLSTATES = new Set([\n \"23505\", // unique_violation on pg_type / pg_class / pg_namespace\n \"42P06\", // duplicate_schema\n \"42P07\", // duplicate_table\n \"42710\", // duplicate_object — an index or a constraint\n \"40P01\" // deadlock_detected — two boots taking catalog locks in step\n]);\n\n/** Attempts per idempotent DDL statement, including the first. */\nexport const DDL_ATTEMPTS = 4;\nconst DDL_RETRY_BASE_MS = 40;\n\n/**\n * Walk an error's `cause` chain, stopping at the first link `visit` accepts.\n * Drizzle wraps the driver error, so nothing useful is ever on the top level.\n */\nexport function hasInCauseChain(err: unknown, visit: (e: Record<string, unknown>) => boolean): boolean {\n let current: unknown = err;\n for (let depth = 0; depth < 10 && current; depth++) {\n if (typeof current !== \"object\") break;\n const e = current as Record<string, unknown>;\n if (visit(e)) return true;\n current = e.cause;\n }\n return false;\n}\n\n/**\n * Is this the loser of a race to create something that already exists?\n *\n * Deliberately narrow. A permission failure, an unreachable database or a typo\n * in the DDL must surface on the first attempt rather than being retried four\n * times and then reported as a race that never was.\n */\nexport function isConcurrentDdlRace(err: unknown): boolean {\n return hasInCauseChain(err, (e) =>\n (typeof e.code === \"string\" && CONCURRENT_DDL_SQLSTATES.has(e.code)) ||\n // SQLite and MySQL say it in words rather than in a shared SQLSTATE.\n (typeof e.message === \"string\" && /already exists/i.test(e.message))\n );\n}\n\n/**\n * SQLSTATEs that mean, unambiguously, *the object is already there*.\n *\n * A subset of {@link CONCURRENT_DDL_SQLSTATES} and a stricter question. The\n * broad set answers \"should this be retried\"; this one answers \"is it safe to\n * carry on as though the statement had succeeded\", which is a claim about the\n * end state rather than about the attempt. Deadlock is not in it — a deadlocked\n * statement did nothing and must be retried, not skipped.\n */\nconst DUPLICATE_OBJECT_SQLSTATES = new Set([\n \"42P06\", // duplicate_schema\n \"42P07\", // duplicate_table\n \"42710\" // duplicate_object — a type, an index, a constraint\n]);\n\n/**\n * Did this statement fail *because a peer already created the same object*?\n *\n * The narrow companion to {@link isConcurrentDdlRace}, for the one caller that\n * needs to tell \"someone beat me to it\" from \"this genuinely failed\": a loop\n * applying a schema plan, where treating every `23505` as a harmless race would\n * silently swallow the one that matters — a unique constraint that cannot be\n * added because the customer's existing rows violate it.\n *\n * `23505` is therefore only accepted when it names a `pg_catalog` index. That is\n * what a lost `CREATE TYPE`/`CREATE TABLE` race raises (`pg_type_typname_nsp_index`\n * is the one seen in practice); a unique violation on user data names the user's\n * own constraint and is left to the caller.\n */\nexport function isDuplicateObjectRace(err: unknown): boolean {\n return hasInCauseChain(err, (e) => {\n if (typeof e.code !== \"string\") return false;\n if (DUPLICATE_OBJECT_SQLSTATES.has(e.code)) return true;\n if (e.code !== \"23505\") return false;\n // node-postgres puts the violated index in `constraint`; some paths only\n // carry it in the detail text, so check both rather than miss the race.\n const constraint = typeof e.constraint === \"string\" ? e.constraint : \"\";\n const detail = typeof e.detail === \"string\" ? e.detail : \"\";\n return constraint.startsWith(\"pg_\") || /\\bpg_[a-z_]+_index\\b/.test(detail);\n });\n}\n\nexport interface DdlBootstrapper {\n /**\n * Run one idempotent statement — `CREATE … IF NOT EXISTS`, `ALTER TABLE …\n * ADD COLUMN IF NOT EXISTS` — retrying the catalog race a simultaneous boot\n * produces. Never throws: a statement that cannot be made to work is logged\n * and the caller carries on to the next one.\n */\n ensureObject(label: string, sqlText: string): Promise<void>;\n\n /** Contain one step's failure so that the steps after it still run. */\n step(label: string, run: () => Promise<unknown>): Promise<void>;\n\n /**\n * Is this table there and readable? Asked with a query any SQL dialect\n * answers, rather than `to_regclass`, so a future non-Postgres SQL driver\n * gets a real answer instead of a syntax error read as \"missing\".\n */\n isReadable(table: string): Promise<boolean>;\n}\n\n/**\n * @param exec the driver's SQL escape hatch\n * @param scope log prefix identifying the caller, e.g. `\"cron-store\"`\n */\nexport function createDdlBootstrapper(exec: SqlExec, scope: string): DdlBootstrapper {\n /** Jittered, so peers that collided once do not collide again in lockstep. */\n const backoff = (attempt: number) =>\n new Promise(resolve => setTimeout(resolve, DDL_RETRY_BASE_MS * attempt * (1 + Math.random())));\n\n const step: DdlBootstrapper[\"step\"] = async (label, run) => {\n try {\n await run();\n } catch (err) {\n logger.error(`[${scope}] ${label} failed`, { error: err });\n }\n };\n\n return {\n step,\n\n ensureObject(label, sqlText) {\n return step(label, async () => {\n for (let attempt = 1; ; attempt++) {\n try {\n await exec(sqlText);\n return;\n } catch (err) {\n if (!isConcurrentDdlRace(err) || attempt >= DDL_ATTEMPTS) throw err;\n logger.debug(\n `[${scope}] Lost a create race for ${label} with another instance ` +\n `(attempt ${attempt}/${DDL_ATTEMPTS}) — retrying`\n );\n await backoff(attempt);\n }\n }\n });\n },\n\n async isReadable(table) {\n try {\n await exec(`SELECT 1 FROM ${table} WHERE false`);\n return true;\n } catch {\n return false;\n }\n }\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAqkBA,SAAgB,qBAAqB,OAA+D;CAChG,OAAO,CAAC,CAAC,SAAS,OAAQ,MAA6B,qBAAqB;AAChF;;;;;AAMA,SAAgB,WAAW,OAAqD;CAC5E,OAAO,CAAC,CAAC,SAAS,OAAQ,MAAmB,eAAe;AAChE;;;;;;;;AC7iBA,IAAa,2CAA2B,IAAI,IAAI;CAC5C;CACA;CACA;CACA;CACA;AACJ,CAAC;AAID,IAAM,oBAAoB;;;;;AAM1B,SAAgB,gBAAgB,KAAc,OAAyD;CACnG,IAAI,UAAmB;CACvB,KAAK,IAAI,QAAQ,GAAG,QAAQ,MAAM,SAAS,SAAS;EAChD,IAAI,OAAO,YAAY,UAAU;EACjC,MAAM,IAAI;EACV,IAAI,MAAM,CAAC,GAAG,OAAO;EACrB,UAAU,EAAE;CAChB;CACA,OAAO;AACX;;;;;;;;AASA,SAAgB,oBAAoB,KAAuB;CACvD,OAAO,gBAAgB,MAAM,MACxB,OAAO,EAAE,SAAS,YAAY,yBAAyB,IAAI,EAAE,IAAI,KAEjE,OAAO,EAAE,YAAY,YAAY,kBAAkB,KAAK,EAAE,OAAO,CACtE;AACJ;;;;;;;;;;AAWA,IAAM,6CAA6B,IAAI,IAAI;CACvC;CACA;CACA;AACJ,CAAC;;;;;;;;;;;;;;;AAgBD,SAAgB,sBAAsB,KAAuB;CACzD,OAAO,gBAAgB,MAAM,MAAM;EAC/B,IAAI,OAAO,EAAE,SAAS,UAAU,OAAO;EACvC,IAAI,2BAA2B,IAAI,EAAE,IAAI,GAAG,OAAO;EACnD,IAAI,EAAE,SAAS,SAAS,OAAO;EAG/B,MAAM,aAAa,OAAO,EAAE,eAAe,WAAW,EAAE,aAAa;EACrE,MAAM,SAAS,OAAO,EAAE,WAAW,WAAW,EAAE,SAAS;EACzD,OAAO,WAAW,WAAW,KAAK,KAAK,uBAAuB,KAAK,MAAM;CAC7E,CAAC;AACL;;;;;AA0BA,SAAgB,sBAAsB,MAAe,OAAgC;;CAEjF,MAAM,WAAW,YACb,IAAI,SAAQ,YAAW,WAAW,SAAS,oBAAoB,WAAW,IAAI,KAAK,OAAO,EAAE,CAAC;CAEjG,MAAM,OAAgC,OAAO,OAAO,QAAQ;EACxD,IAAI;GACA,MAAM,IAAI;EACd,SAAS,KAAK;GACV,OAAO,MAAM,IAAI,MAAM,IAAI,MAAM,UAAU,EAAE,OAAO,IAAI,CAAC;EAC7D;CACJ;CAEA,OAAO;EACH;EAEA,aAAa,OAAO,SAAS;GACzB,OAAO,KAAK,OAAO,YAAY;IAC3B,KAAK,IAAI,UAAU,IAAK,WACpB,IAAI;KACA,MAAM,KAAK,OAAO;KAClB;IACJ,SAAS,KAAK;KACV,IAAI,CAAC,oBAAoB,GAAG,KAAK,WAAA,GAAyB,MAAM;KAChE,OAAO,MACH,IAAI,MAAM,2BAA2B,MAAM,kCAC/B,QAAQ,eACxB;KACA,MAAM,QAAQ,OAAO;IACzB;GAER,CAAC;EACL;EAEA,MAAM,WAAW,OAAO;GACpB,IAAI;IACA,MAAM,KAAK,iBAAiB,MAAM,aAAa;IAC/C,OAAO;GACX,QAAQ;IACJ,OAAO;GACX;EACJ;CACJ;AACJ"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every deployment of this runtime must get right about the pod.
|
|
3
|
+
*
|
|
4
|
+
* There are two things that place a Rebase runtime in Kubernetes — the Helm
|
|
5
|
+
* chart in `infra/charts/rebase`, for self-hosting, and the control plane's
|
|
6
|
+
* `buildManagedContainer`, for cloud tenants — and they were written months
|
|
7
|
+
* apart by people solving different problems. Rendering both for the same unit
|
|
8
|
+
* and diffing them (2026-08-22) turned up four disagreements, and in three of
|
|
9
|
+
* them the chart was doing the thing this runtime's own source warns against.
|
|
10
|
+
*
|
|
11
|
+
* The disagreements were not in the parts that *should* differ. A managed pod
|
|
12
|
+
* takes its environment from a tenant Secret and a self-hosted one takes it from
|
|
13
|
+
* a values file; a managed pod is scheduled onto spot capacity its plan sold it
|
|
14
|
+
* and a self-hosted one goes where the operator says. Those are different jobs
|
|
15
|
+
* and they produce different manifests, correctly.
|
|
16
|
+
*
|
|
17
|
+
* What must not differ is anything that is really a statement about *this
|
|
18
|
+
* process*: which endpoint answers what, which variables decide the topology,
|
|
19
|
+
* where the bundle is mounted. Those are not deployment preferences. They are
|
|
20
|
+
* facts about the runtime, and a deployment that gets one wrong produces a
|
|
21
|
+
* cluster that looks healthy — which is why they are stated here, in the
|
|
22
|
+
* runtime, rather than twice in the things that deploy it.
|
|
23
|
+
*
|
|
24
|
+
* The chart cannot import this module. `tooling/scripts/check-chart.mjs` renders the
|
|
25
|
+
* chart and asserts it against these values instead, so the chart conforms by
|
|
26
|
+
* gate where the control plane conforms by construction.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Endpoints this runtime guarantees on every role, and what each one means.
|
|
30
|
+
*
|
|
31
|
+
* `/livez` is dependency-free — it answers "this process is running" and
|
|
32
|
+
* nothing else. `/health` opens the default driver and every configured
|
|
33
|
+
* secondary, and **answers 503 when any of them is unreachable**
|
|
34
|
+
* (`boot.ts`, the `healthPaths` handler).
|
|
35
|
+
*
|
|
36
|
+
* That difference is the whole reason both exist, and it is stated at the
|
|
37
|
+
* `/livez` registration in `boot.ts`: "`/health` touches the database, so a
|
|
38
|
+
* database blip would make an orchestrator kill an otherwise healthy process."
|
|
39
|
+
*/
|
|
40
|
+
export declare const RUNTIME_HEALTH_PATH = "/health";
|
|
41
|
+
export declare const RUNTIME_LIVENESS_PATH = "/livez";
|
|
42
|
+
/**
|
|
43
|
+
* Which endpoint each Kubernetes probe must target.
|
|
44
|
+
*
|
|
45
|
+
* The rule follows from what the probe *does* when it fails:
|
|
46
|
+
*
|
|
47
|
+
* - **liveness** restarts the container. A database outage must therefore never
|
|
48
|
+
* fail it, or an unreachable database turns into a cluster-wide restart loop
|
|
49
|
+
* that clears the moment the database returns and looks, in the logs, like
|
|
50
|
+
* the application crashed. `/livez`.
|
|
51
|
+
* - **readiness** removes the pod from its Service. That is exactly what should
|
|
52
|
+
* happen when the database is unreachable — the pod cannot serve. `/health`.
|
|
53
|
+
* - **startup** decides when the other two begin. Its job is "has this process
|
|
54
|
+
* finished booting", and booting may include fetching a bundle, installing
|
|
55
|
+
* dependencies and checking the schema. It must not also require the database
|
|
56
|
+
* to be *up*, or a cold database is indistinguishable from a broken image:
|
|
57
|
+
* the pod never passes startup, liveness never gets to run, and the pod
|
|
58
|
+
* CrashLoops with nothing in its output about a database. `/livez`.
|
|
59
|
+
*
|
|
60
|
+
* A probe targeting the wrong one of these fails silently in the direction that
|
|
61
|
+
* looks like an application bug, which is why this is a contract and not a
|
|
62
|
+
* default.
|
|
63
|
+
*/
|
|
64
|
+
export declare const RUNTIME_PROBE_PATHS: {
|
|
65
|
+
readonly liveness: "/livez";
|
|
66
|
+
readonly readiness: "/health";
|
|
67
|
+
readonly startup: "/livez";
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Environment variables that decide *topology* — which surfaces a process
|
|
71
|
+
* mounts, which singletons it owns, where it forwards.
|
|
72
|
+
*
|
|
73
|
+
* These are the platform's or the operator's decision expressed by the thing
|
|
74
|
+
* doing the deploying, and they must never be settable by whoever supplies the
|
|
75
|
+
* project's own environment. The cloud learned this the expensive way: a tenant
|
|
76
|
+
* who set `REBASE_ROLE=worker` in their project variables got a pod that served
|
|
77
|
+
* no HTTP at all, and because `/health` answers on **every** role the readiness
|
|
78
|
+
* probe passed, the rollout reported success, and every request 404'd.
|
|
79
|
+
*
|
|
80
|
+
* Kubernetes only lets `env` shadow `envFrom` for names it **lists**, so
|
|
81
|
+
* neutralising one of these means naming it explicitly. An empty value is read
|
|
82
|
+
* by the runtime as unset, which is why the cloud pins most of them to `""`
|
|
83
|
+
* rather than to a value: the entry exists to take the decision away from the
|
|
84
|
+
* project, not to make it here.
|
|
85
|
+
*/
|
|
86
|
+
export declare const TOPOLOGY_ENV_VARS: readonly ["REBASE_ROLE", "REBASE_FUNCTIONS_ONLY", "REBASE_FUNCTIONS_EXCLUDE", "REBASE_FUNCTIONS_UPSTREAM", "REBASE_CRON_SCHEDULER", "REBASE_JOB_WORKERS", "REBASE_RLS_AUDIT", "REBASE_MIGRATE_ON_BOOT", "TRUSTED_PROXY_HOPS", "REBASE_RATE_LIMIT_STORE", "REBASE_REQUIRE_SCHEMA_MATCH"];
|
|
87
|
+
export type TopologyEnvVar = (typeof TOPOLOGY_ENV_VARS)[number];
|
|
88
|
+
/** Whether a variable name is one the deployer owns rather than the project. */
|
|
89
|
+
export declare function isTopologyEnvVar(name: string): name is TopologyEnvVar;
|
|
90
|
+
/**
|
|
91
|
+
* Seconds to keep answering after Kubernetes decides to remove this pod.
|
|
92
|
+
*
|
|
93
|
+
* `installShutdownHandlers` drains on SIGTERM — it stops the scheduler, tears
|
|
94
|
+
* down realtime and closes the HTTP server, finishing what is in flight. What
|
|
95
|
+
* it cannot do is stop *new* requests arriving: kubelet sends SIGTERM and the
|
|
96
|
+
* endpoint controller removes the pod from its Service **concurrently**, so for
|
|
97
|
+
* as long as endpoint removal takes to propagate, the ingress is still routing
|
|
98
|
+
* to a process that has already stopped accepting connections.
|
|
99
|
+
*
|
|
100
|
+
* A `preStop` sleep is the only thing that orders those two. The pod keeps
|
|
101
|
+
* serving for this long *before* SIGTERM is sent, which is time the ingress
|
|
102
|
+
* uses to stop sending it anything.
|
|
103
|
+
*
|
|
104
|
+
* Anything greater than zero fixes the ordering; five seconds is what the
|
|
105
|
+
* control plane has run in production.
|
|
106
|
+
*/
|
|
107
|
+
export declare const RUNTIME_PRESTOP_DRAIN_SECONDS = 5;
|
|
108
|
+
/**
|
|
109
|
+
* The floor for `terminationGracePeriodSeconds`.
|
|
110
|
+
*
|
|
111
|
+
* The pod must outlive `preStop` *plus* the drain that follows it, or kubelet
|
|
112
|
+
* SIGKILLs mid-drain and the graceful shutdown was decoration. The drain's own
|
|
113
|
+
* default is 15s (`ShutdownHandlerOptions.timeoutMs`), and Kubernetes' default
|
|
114
|
+
* grace period is 30s — so the stock configuration has room, and this exists to
|
|
115
|
+
* say how much before someone raises the drain.
|
|
116
|
+
*/
|
|
117
|
+
export declare const RUNTIME_MIN_TERMINATION_GRACE_SECONDS: number;
|
|
118
|
+
/**
|
|
119
|
+
* How long a deployment must let this runtime take to answer for the first time.
|
|
120
|
+
*
|
|
121
|
+
* Nothing answers until boot is finished. `runFromBundle` binds the socket
|
|
122
|
+
* **last** — after the bundle is read, the drivers connect and, on the process
|
|
123
|
+
* that owns it, the schema DDL runs (`boot.ts`, "schema DDL happens during
|
|
124
|
+
* boot, above"). So the gap between the container starting and `/livez`
|
|
125
|
+
* answering is a whole provisioning run, not a framework warming up.
|
|
126
|
+
*
|
|
127
|
+
* A deployment with no startup probe measures that gap with its *liveness*
|
|
128
|
+
* probe instead, and liveness restarts the container. On the control plane's
|
|
129
|
+
* settings that is 20s of grace plus three 20s failures — 80 seconds — after
|
|
130
|
+
* which a slow first boot is killed and retried, and each retry starts from the
|
|
131
|
+
* beginning. It does not converge; it loops, and the logs show a pod that never
|
|
132
|
+
* finished starting rather than a database that was slow.
|
|
133
|
+
*
|
|
134
|
+
* So a startup probe is not optional here, and its budget is this. A healthy
|
|
135
|
+
* pod passes it on the first check and nothing is spent.
|
|
136
|
+
*/
|
|
137
|
+
export declare const RUNTIME_STARTUP_BUDGET_SECONDS = 300;
|
|
138
|
+
/** Where a bundle is mounted, and what the runtime is told to read. */
|
|
139
|
+
export declare const RUNTIME_BUNDLE_MOUNT = "/bundle";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Beside `.rebase-dev-port` and `.rebase-dev-url`, and gitignored with them. */
|
|
2
|
+
export declare const DEV_SECRETS_FILENAME = ".rebase-dev-secrets.json";
|
|
3
|
+
export interface DevSecretStore {
|
|
4
|
+
/** The cached value for `name`, or undefined. */
|
|
5
|
+
get(name: string): string | undefined;
|
|
6
|
+
/** Cache `value` under `name`. Failures are swallowed by design. */
|
|
7
|
+
set(name: string, value: string): void;
|
|
8
|
+
/** Where this store reads and writes, for logging. */
|
|
9
|
+
readonly file: string;
|
|
10
|
+
/** False when the file could not be read *or* written. */
|
|
11
|
+
readonly usable: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Whether these environment variables describe a test runner. */
|
|
14
|
+
export declare function detectTestRunner(env: NodeJS.ProcessEnv): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Whether a test runner started this process, decided **once at import**.
|
|
17
|
+
*
|
|
18
|
+
* Read at call time it would be wrong, and provably so: `env.test.ts` does
|
|
19
|
+
* `process.env = {}` in its `beforeEach`, which erases `NODE_ENV` and
|
|
20
|
+
* `JEST_WORKER_ID` alike. A predicate consulting the live environment therefore
|
|
21
|
+
* sees a bare object and concludes "development", which is how the first version
|
|
22
|
+
* of this dropped a `.rebase-dev-secrets.json` into `packages/server` on every
|
|
23
|
+
* test run.
|
|
24
|
+
*
|
|
25
|
+
* Whether a test runner is hosting the process is a fact about the process, not
|
|
26
|
+
* about whatever a test has since done to `process.env`, so it is captured
|
|
27
|
+
* before any test can rewrite it.
|
|
28
|
+
*/
|
|
29
|
+
export declare const TEST_RUNNER_DETECTED: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Whether generated secrets should be cached at all.
|
|
32
|
+
*
|
|
33
|
+
* Not in production — `loadEnv` refuses to boot there on a generated secret, so
|
|
34
|
+
* there is nothing to cache and no path by which a cached one becomes a
|
|
35
|
+
* production one.
|
|
36
|
+
*
|
|
37
|
+
* Not under a test runner: a test must not inherit a secret a previous run left
|
|
38
|
+
* behind, and a library call has no business writing into a suite's working
|
|
39
|
+
* directory. `inTestRunner` is a parameter only so this predicate can be tested
|
|
40
|
+
* on its own; callers pass nothing.
|
|
41
|
+
*/
|
|
42
|
+
export declare function shouldCacheDevSecrets(env?: NodeJS.ProcessEnv, inTestRunner?: boolean): boolean;
|
|
43
|
+
/** `REBASE_DEV_SECRETS_FILE`, or the conventional name in the working directory. */
|
|
44
|
+
export declare function resolveDevSecretsFile(env?: NodeJS.ProcessEnv): string;
|
|
45
|
+
/**
|
|
46
|
+
* Open the cache.
|
|
47
|
+
*
|
|
48
|
+
* Reading a malformed file is treated as an empty cache rather than an error:
|
|
49
|
+
* the file is ours, it holds regenerable values, and refusing to boot over a
|
|
50
|
+
* corrupted convenience cache would be worse than rewriting it.
|
|
51
|
+
*/
|
|
52
|
+
export declare function openDevSecretStore(file?: string): DevSecretStore;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { createRequire as
|
|
2
|
-
import "process";
|
|
3
|
-
|
|
1
|
+
import { createRequire as __rebaseCreateRequire } from "module";
|
|
2
|
+
import __rebaseProcess from "process";
|
|
3
|
+
globalThis.process ??= __rebaseProcess;
|
|
4
|
+
__rebaseCreateRequire(import.meta.url);
|
|
4
5
|
//#region src/utils/dynamic-import.ts
|
|
5
6
|
/**
|
|
6
7
|
* Native ESM dynamic import.
|
|
@@ -19,4 +20,4 @@ var nativeDynamicImport = (() => {
|
|
|
19
20
|
//#endregion
|
|
20
21
|
export { nativeDynamicImport as t };
|
|
21
22
|
|
|
22
|
-
//# sourceMappingURL=dynamic-import-
|
|
23
|
+
//# sourceMappingURL=dynamic-import-X40pTZUQ.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-import-
|
|
1
|
+
{"version":3,"file":"dynamic-import-X40pTZUQ.js","names":[],"sources":["../src/utils/dynamic-import.ts"],"sourcesContent":["/**\n * Shape returned by a dynamic module import: an object whose `default`\n * property holds the module's default export (CJS `module.exports` is exposed\n * as `default` by Node's ESM interop).\n */\nexport interface ImportedModule {\n default?: unknown;\n [key: string]: unknown;\n}\n\n/**\n * Imports a module by file URL. Injected into the directory loaders so tests\n * can supply a deterministic importer.\n */\nexport type ModuleImporter = (url: string) => Promise<ImportedModule>;\n\n/**\n * Native ESM dynamic import.\n *\n * Wrapped in `new Function` so TypeScript / bundlers don't down-compile the\n * `import()` to `require()` — the loaders must import user-authored `.ts`/`.js`\n * files at runtime using the host's real ESM loader. This is the production\n * default for {@link loadFunctionsFromDirectory} / {@link loadCronJobsFromDirectory}.\n *\n * @internal\n */\nexport const nativeDynamicImport: ModuleImporter = (() => {\n const doImport = new Function(\"url\", \"return import(url)\") as (url: string) => Promise<ImportedModule>;\n return (url: string) => doImport(url);\n})();\n"],"mappings":";;;;;;;;;;;;;;;AA0BA,IAAa,6BAA6C;CACtD,MAAM,WAAW,IAAI,SAAS,OAAO,oBAAoB;CACzD,QAAQ,QAAgB,SAAS,GAAG;AACxC,EAAA,CAAG"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The development mail sink: what happens to auth email when no SMTP is set.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* Three routes refused to work without a mail server — `POST /auth/magic-link`,
|
|
7
|
+
* `POST /auth/forgot-password` and the verification resend — each answering
|
|
8
|
+
* `503 EMAIL_NOT_CONFIGURED`. So the first thing a new project could not do was
|
|
9
|
+
* log in, and the fix was to go and find an SMTP host. The token was never the
|
|
10
|
+
* problem: it is minted, stored and valid. Only the delivery was missing.
|
|
11
|
+
*
|
|
12
|
+
* With this, delivery is the terminal. The message is captured and its links
|
|
13
|
+
* are printed, so a developer follows the link from the log and the flow
|
|
14
|
+
* completes end to end on minute one, with no account anywhere.
|
|
15
|
+
*
|
|
16
|
+
* ## Why it cannot reach production
|
|
17
|
+
*
|
|
18
|
+
* A captured password-reset mail contains a working reset token. Anything that
|
|
19
|
+
* writes those to a log or holds them in memory is a credential store, so the
|
|
20
|
+
* sink is wired only by {@link resolveEmailOptions}, only when `NODE_ENV` is not
|
|
21
|
+
* `production`, and it says loudly what it is on first use. There is no
|
|
22
|
+
* configuration that turns it on in production, deliberately: an operator who
|
|
23
|
+
* wants mail in production wants a mail server, not a ring buffer.
|
|
24
|
+
*
|
|
25
|
+
* The buffer is also capped and in-process. It is a development convenience, not
|
|
26
|
+
* a mailbox — a restart empties it, and nothing persists it.
|
|
27
|
+
*/
|
|
28
|
+
import type { EmailSendOptions, EmailSendResult } from "./types.js";
|
|
29
|
+
export interface CapturedEmail {
|
|
30
|
+
/** Monotonic within a process. Not stable across restarts. */
|
|
31
|
+
id: number;
|
|
32
|
+
/**
|
|
33
|
+
* The synthetic Message-ID this sink reported to the caller.
|
|
34
|
+
*
|
|
35
|
+
* Real, in the sense that it is what `send()` returned and what an
|
|
36
|
+
* application will have stored — so a flow that threads a reply against it
|
|
37
|
+
* behaves the same here as against a mail server, which is the entire point
|
|
38
|
+
* of the sink.
|
|
39
|
+
*/
|
|
40
|
+
messageId: string;
|
|
41
|
+
at: string;
|
|
42
|
+
to: string;
|
|
43
|
+
subject: string;
|
|
44
|
+
html?: string;
|
|
45
|
+
text?: string;
|
|
46
|
+
/**
|
|
47
|
+
* The absolute links found in the message, in document order.
|
|
48
|
+
*
|
|
49
|
+
* Extracted because this is the only part anyone needs: every auth mail
|
|
50
|
+
* exists to carry one URL, and reading it out of an HTML body in a terminal
|
|
51
|
+
* is miserable.
|
|
52
|
+
*/
|
|
53
|
+
links: string[];
|
|
54
|
+
}
|
|
55
|
+
export interface DevEmailSink {
|
|
56
|
+
/** Drop-in for `EmailConfig.sendEmail`. */
|
|
57
|
+
sendEmail: (options: EmailSendOptions) => Promise<EmailSendResult>;
|
|
58
|
+
/** Most recent first. */
|
|
59
|
+
list: () => CapturedEmail[];
|
|
60
|
+
clear: () => void;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Absolute http(s) URLs in a message body.
|
|
64
|
+
*
|
|
65
|
+
* Deliberately simple: it reads `href="…"` first, because that is where a real
|
|
66
|
+
* link lives, and falls back to bare URLs in the text part. Trailing markup and
|
|
67
|
+
* punctuation are trimmed so the result can be pasted straight into a browser.
|
|
68
|
+
*/
|
|
69
|
+
export declare function extractLinks(html: string | undefined, text: string | undefined): string[];
|
|
70
|
+
/**
|
|
71
|
+
* Create a sink. Each call is independent, which is what lets a test hold one
|
|
72
|
+
* without touching whatever the process is using.
|
|
73
|
+
*/
|
|
74
|
+
export declare function createDevEmailSink(options?: {
|
|
75
|
+
capacity?: number;
|
|
76
|
+
}): DevEmailSink;
|
|
77
|
+
/**
|
|
78
|
+
* Make `sink` the process's development mailbox.
|
|
79
|
+
*
|
|
80
|
+
* Returns the sink, so this can wrap the construction it replaces. In
|
|
81
|
+
* production it registers nothing and warns — the caller still gets its sink
|
|
82
|
+
* back and mail is still captured, it simply is not readable over HTTP.
|
|
83
|
+
*/
|
|
84
|
+
export declare function registerDevEmailSink(sink: DevEmailSink): DevEmailSink;
|
|
85
|
+
/** The registered mailbox, if there is one. */
|
|
86
|
+
export declare function activeDevEmailSink(): DevEmailSink | undefined;
|
|
87
|
+
/** Forget the registered mailbox. For tests, and for a server shutting down. */
|
|
88
|
+
export declare function clearActiveDevEmailSink(): void;
|
package/dist/email/index.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Email module exports
|
|
3
3
|
*/
|
|
4
|
-
export type { EmailService, EmailSendOptions, SMTPConfig, EmailConfig, PasswordResetTemplateFunction, EmailVerificationTemplateFunction, UserInvitationTemplateFunction, WelcomeEmailTemplateFunction, MagicLinkTemplateFunction } from "./types";
|
|
5
|
-
export { SMTPEmailService, createEmailService } from "./smtp-email-service";
|
|
6
|
-
export { html, raw, escapeHtml, RawHtml } from "./html";
|
|
7
|
-
export {
|
|
8
|
-
export type {
|
|
9
|
-
export {
|
|
10
|
-
export {
|
|
4
|
+
export type { EmailService, EmailSendOptions, EmailSendResult, SMTPConfig, EmailConfig, PasswordResetTemplateFunction, EmailVerificationTemplateFunction, UserInvitationTemplateFunction, WelcomeEmailTemplateFunction, MagicLinkTemplateFunction } from "./types.js";
|
|
5
|
+
export { SMTPEmailService, createEmailService } from "./smtp-email-service.js";
|
|
6
|
+
export { html, raw, escapeHtml, RawHtml } from "./html.js";
|
|
7
|
+
export { createDevEmailSink, extractLinks, registerDevEmailSink, activeDevEmailSink, clearActiveDevEmailSink } from "./dev-sink.js";
|
|
8
|
+
export type { DevEmailSink, CapturedEmail } from "./dev-sink.js";
|
|
9
|
+
export { resolveEmailLinkBase, assertEmailLinkBases } from "./link-base.js";
|
|
10
|
+
export type { EmailLinkKind } from "./link-base.js";
|
|
11
|
+
export { getPasswordResetTemplate, getEmailVerificationTemplate, getUserInvitationTemplate, getWelcomeEmailTemplate, getMagicLinkTemplate, getEmailOtpTemplate } from "./templates.js";
|
|
12
|
+
export { resolveEmailBranding } from "./templates.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EmailConfig, EmailSendOptions, EmailService } from "./types";
|
|
1
|
+
import { EmailConfig, EmailSendOptions, EmailSendResult, EmailService } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* SMTP Email Service implementation using Nodemailer
|
|
4
4
|
*/
|
|
@@ -18,7 +18,7 @@ export declare class SMTPEmailService implements EmailService {
|
|
|
18
18
|
/**
|
|
19
19
|
* Send an email using SMTP or custom send function
|
|
20
20
|
*/
|
|
21
|
-
send(options: EmailSendOptions): Promise<
|
|
21
|
+
send(options: EmailSendOptions): Promise<EmailSendResult>;
|
|
22
22
|
/**
|
|
23
23
|
* Verify SMTP connection (useful for startup checks)
|
|
24
24
|
*/
|
|
@@ -63,6 +63,18 @@ export declare function getWelcomeEmailTemplate(user: TemplateUser, appName?: st
|
|
|
63
63
|
html: string;
|
|
64
64
|
text: string;
|
|
65
65
|
};
|
|
66
|
+
/**
|
|
67
|
+
* Default one-time sign-in code email.
|
|
68
|
+
*
|
|
69
|
+
* The code is the entire content, so it is set large and repeated in the
|
|
70
|
+
* plain-text part: somebody is about to copy it by eye onto another device,
|
|
71
|
+
* which is the whole reason this flow exists rather than a link.
|
|
72
|
+
*/
|
|
73
|
+
export declare function getEmailOtpTemplate(code: string, user: TemplateUser, appName?: string, logoUrl?: string): {
|
|
74
|
+
subject: string;
|
|
75
|
+
html: string;
|
|
76
|
+
text: string;
|
|
77
|
+
};
|
|
66
78
|
/**
|
|
67
79
|
* Default magic link email template
|
|
68
80
|
*/
|