@rebasepro/server 0.13.0 → 0.13.1-canary.g06dbe5b
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/{admin_block-CcSGdH7M.js → admin_block-H86dCPsj.js} +62 -2
- package/dist/admin_block-H86dCPsj.js.map +1 -0
- package/dist/api/ast-schema-editor.d.ts +74 -6
- package/dist/api/errors.d.ts +0 -6
- package/dist/api/openapi-generator.d.ts +14 -0
- package/dist/api/rest/api-generator.d.ts +1 -1
- package/dist/api/rest/idempotency.d.ts +62 -4
- package/dist/api/rest/query-parser.d.ts +15 -1
- package/dist/api/rest/write-validation.d.ts +52 -9
- package/dist/auth/adapter-middleware.d.ts +3 -1
- package/dist/auth/apple-oauth.d.ts +16 -11
- package/dist/auth/bitbucket-oauth.d.ts +2 -4
- package/dist/auth/builtin-auth-adapter.d.ts +2 -0
- package/dist/auth/discord-oauth.d.ts +5 -6
- package/dist/auth/facebook-oauth.d.ts +2 -4
- package/dist/auth/github-oauth.d.ts +2 -4
- package/dist/auth/gitlab-oauth.d.ts +6 -4
- package/dist/auth/google-oauth.d.ts +1 -0
- package/dist/auth/index.d.ts +8 -1
- package/dist/auth/interfaces.d.ts +68 -1
- package/dist/auth/jwt.d.ts +40 -0
- package/dist/auth/linkedin-oauth.d.ts +2 -4
- package/dist/auth/mfa-gate.d.ts +42 -0
- package/dist/auth/mfa-routes.d.ts +26 -1
- package/dist/auth/mfa.d.ts +16 -0
- package/dist/auth/microsoft-oauth.d.ts +19 -4
- package/dist/auth/oauth-code-flow.d.ts +66 -0
- package/dist/auth/oauth-signin-policy.d.ts +61 -0
- package/dist/auth/oidc-id-token.d.ts +61 -0
- package/dist/auth/rate-limiter.d.ts +37 -2
- package/dist/auth/rls-scope.d.ts +25 -0
- package/dist/auth/routes.d.ts +8 -0
- package/dist/auth/slack-oauth.d.ts +2 -4
- package/dist/auth/spotify-oauth.d.ts +2 -4
- package/dist/auth/twitter-oauth.d.ts +2 -5
- package/dist/{auth-CuC9M2x6.js → auth-DkAHqvf5.js} +1604 -391
- package/dist/auth-DkAHqvf5.js.map +1 -0
- package/dist/{backup-CVggVhR2.js → backup-DLluVyuA.js} +2 -2
- package/dist/{backup-CVggVhR2.js.map → backup-DLluVyuA.js.map} +1 -1
- package/dist/boot/boot.d.ts +19 -0
- package/dist/boot/ddl-bootstrap.d.ts +70 -0
- package/dist/{contract-routes-Dj8i5AiM.js → contract-routes-BB1U05sS.js} +3 -3
- package/dist/{contract-routes-Dj8i5AiM.js.map → contract-routes-BB1U05sS.js.map} +1 -1
- package/dist/cron/cron-scheduler.d.ts +8 -3
- package/dist/cron/define-cron.d.ts +17 -3
- package/dist/{cron-loader-B1S2MCSl.js → cron-loader-3U5aGILy.js} +2 -2
- package/dist/{cron-loader-B1S2MCSl.js.map → cron-loader-3U5aGILy.js.map} +1 -1
- package/dist/{cron-routes-CrQ0tK-_.js → cron-routes-rZgwlOz4.js} +2 -2
- package/dist/{cron-routes-CrQ0tK-_.js.map → cron-routes-rZgwlOz4.js.map} +1 -1
- package/dist/{cron-scheduler-B3RFt0HS.js → cron-scheduler-FJAaCAXm.js} +30 -5
- package/dist/cron-scheduler-FJAaCAXm.js.map +1 -0
- package/dist/{cron-store-BywZsyfZ.js → cron-store-9NmUDfzL.js} +66 -47
- package/dist/cron-store-9NmUDfzL.js.map +1 -0
- package/dist/ddl-bootstrap-BhXbTnBl.js +183 -0
- package/dist/ddl-bootstrap-BhXbTnBl.js.map +1 -0
- package/dist/email/html.d.ts +54 -0
- package/dist/email/index.d.ts +3 -0
- package/dist/email/link-base.d.ts +39 -0
- package/dist/email/smtp-email-service.d.ts +7 -1
- package/dist/email/templates.d.ts +9 -1
- package/dist/email/types.d.ts +11 -1
- package/dist/{errors-CgkCzoj7.js → errors-B1WZEdsK.js} +3 -11
- package/dist/errors-B1WZEdsK.js.map +1 -0
- package/dist/function-loader-D1SwtCa5.js +139 -0
- package/dist/function-loader-D1SwtCa5.js.map +1 -0
- package/dist/{function-routes-C0cLIy3N.js → function-routes-Btcez1T-.js} +18 -6
- package/dist/function-routes-Btcez1T-.js.map +1 -0
- package/dist/functions/define-function.d.ts +25 -6
- package/dist/functions/function-loader.d.ts +23 -0
- package/dist/functions/function-routes.d.ts +7 -1
- package/dist/functions/request-timeout.d.ts +34 -0
- package/dist/history/history-routes.d.ts +6 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.es.js +1960 -794
- package/dist/index.es.js.map +1 -1
- package/dist/init/docs.d.ts +10 -1
- package/dist/init/process-safety.d.ts +25 -0
- package/dist/init.d.ts +27 -0
- package/dist/{jwt-DD6EtpGj.js → jwt-Cuq6MwNy.js} +70 -8
- package/dist/jwt-Cuq6MwNy.js.map +1 -0
- package/dist/logger-DfvF_8r-.js +190 -0
- package/dist/logger-DfvF_8r-.js.map +1 -0
- package/dist/openapi-generator-BQxxxDpb.js +865 -0
- package/dist/openapi-generator-BQxxxDpb.js.map +1 -0
- package/dist/request-timeout-RivJsME0.js +65 -0
- package/dist/request-timeout-RivJsME0.js.map +1 -0
- package/dist/schema-editor-routes-DZFKGXgq.js +437 -0
- package/dist/schema-editor-routes-DZFKGXgq.js.map +1 -0
- package/dist/serve-spa.d.ts +5 -4
- package/dist/services/outbound-url-guard.d.ts +53 -0
- package/dist/services/routed-realtime-service.d.ts +10 -2
- package/dist/services/webhook-service.d.ts +76 -1
- package/dist/singleton.d.ts +25 -8
- package/dist/{src-Cum9kox5.js → src-By48Ffg0.js} +322 -70
- package/dist/src-By48Ffg0.js.map +1 -0
- package/dist/{src-_qQ3RNCK.js → src-Ca6NhxKs.js} +79 -2
- package/dist/src-Ca6NhxKs.js.map +1 -0
- package/dist/storage/LocalStorageController.d.ts +12 -1
- package/dist/storage/image-transform.d.ts +57 -0
- package/dist/storage/keys.d.ts +98 -0
- package/dist/storage/routes.d.ts +11 -0
- package/dist/storage/tus-handler.d.ts +7 -1
- package/dist/utils/logger.d.ts +18 -0
- package/dist/utils/logging.d.ts +0 -4
- package/dist/utils/sql.d.ts +11 -6
- package/package.json +7 -6
- package/dist/admin_block-CcSGdH7M.js.map +0 -1
- package/dist/auth-CuC9M2x6.js.map +0 -1
- package/dist/backend-CIxN4FVm.js +0 -15
- package/dist/backend-CIxN4FVm.js.map +0 -1
- package/dist/cron-scheduler-B3RFt0HS.js.map +0 -1
- package/dist/cron-store-BywZsyfZ.js.map +0 -1
- package/dist/errors-CgkCzoj7.js.map +0 -1
- package/dist/function-loader-B_1fYfUY.js +0 -86
- package/dist/function-loader-B_1fYfUY.js.map +0 -1
- package/dist/function-routes-C0cLIy3N.js.map +0 -1
- package/dist/jwt-DD6EtpGj.js.map +0 -1
- package/dist/logger-BYU66ENZ.js +0 -94
- package/dist/logger-BYU66ENZ.js.map +0 -1
- package/dist/openapi-generator-BEwyiaAr.js +0 -597
- package/dist/openapi-generator-BEwyiaAr.js.map +0 -1
- package/dist/schema-editor-routes-CbF20Mf1.js +0 -248
- package/dist/schema-editor-routes-CbF20Mf1.js.map +0 -1
- package/dist/src-Cum9kox5.js.map +0 -1
- package/dist/src-_qQ3RNCK.js.map +0 -1
|
@@ -2,7 +2,7 @@ import { createRequire as __createRequire } from "module";
|
|
|
2
2
|
import process from "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
4
|
import { n as __exportAll } from "./rolldown-runtime-DSJWtz9O.js";
|
|
5
|
-
import { t as logger } from "./logger-
|
|
5
|
+
import { n as redactSensitiveText, t as logger } from "./logger-DfvF_8r-.js";
|
|
6
6
|
//#region src/cron/scale-to-zero.ts
|
|
7
7
|
/**
|
|
8
8
|
* Scale-to-zero detection for the cron scheduler.
|
|
@@ -261,13 +261,24 @@ var MAX_LOGS_PER_JOB = 50;
|
|
|
261
261
|
* Prevents tight re-execution loops caused by jitter or clock drift.
|
|
262
262
|
*/
|
|
263
263
|
var MIN_SCHEDULE_INTERVAL_MS = 5e3;
|
|
264
|
+
/**
|
|
265
|
+
* Largest delay setTimeout can hold. Node stores it in a 32-bit signed int;
|
|
266
|
+
* anything larger silently clamps to 1ms and fires immediately, so a slot
|
|
267
|
+
* further out than this must be reached in hops rather than one timer.
|
|
268
|
+
*/
|
|
269
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
264
270
|
var CronScheduler = class {
|
|
265
271
|
jobs = /* @__PURE__ */ new Map();
|
|
266
272
|
started = false;
|
|
267
273
|
store;
|
|
268
274
|
client;
|
|
269
275
|
/**
|
|
270
|
-
* Set the
|
|
276
|
+
* Set the server singleton to make it available to cron job handlers.
|
|
277
|
+
*
|
|
278
|
+
* `RebaseServerClient`, not `RebaseClient`: the object `init.ts` passes is
|
|
279
|
+
* the same one it registers as the singleton, so this was always the true
|
|
280
|
+
* type — and the wider annotation is what let `ctx.client.data` look like a
|
|
281
|
+
* user-scoped plane inside a cron, when it is the RLS-bypassing one.
|
|
271
282
|
*/
|
|
272
283
|
setClient(client) {
|
|
273
284
|
this.client = client;
|
|
@@ -464,6 +475,15 @@ var CronScheduler = class {
|
|
|
464
475
|
const nextRun = parseCronExpression(job.definition.schedule, now);
|
|
465
476
|
job.nextRunAt = nextRun;
|
|
466
477
|
const rawDelay = nextRun.getTime() - now.getTime();
|
|
478
|
+
const delay = Math.max(rawDelay, MIN_SCHEDULE_INTERVAL_MS);
|
|
479
|
+
if (delay > MAX_TIMER_DELAY_MS) {
|
|
480
|
+
const hop = setTimeout(() => {
|
|
481
|
+
if (this.started && job.enabled) this.scheduleNext(id);
|
|
482
|
+
}, MAX_TIMER_DELAY_MS);
|
|
483
|
+
if (hop && typeof hop === "object" && "unref" in hop) hop.unref();
|
|
484
|
+
job.timerId = hop;
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
467
487
|
const timer = setTimeout(async () => {
|
|
468
488
|
if (!job.enabled || !this.started) return;
|
|
469
489
|
if (job.executing) {
|
|
@@ -471,6 +491,10 @@ var CronScheduler = class {
|
|
|
471
491
|
this.scheduleNext(id);
|
|
472
492
|
return;
|
|
473
493
|
}
|
|
494
|
+
if (Date.now() < nextRun.getTime()) {
|
|
495
|
+
this.scheduleNext(id);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
474
498
|
if (this.store?.tryClaimRun) {
|
|
475
499
|
let claimed = true;
|
|
476
500
|
try {
|
|
@@ -486,7 +510,7 @@ var CronScheduler = class {
|
|
|
486
510
|
}
|
|
487
511
|
await this.executeJob(job, false);
|
|
488
512
|
if (this.started && job.enabled) this.scheduleNext(id);
|
|
489
|
-
},
|
|
513
|
+
}, delay);
|
|
490
514
|
if (timer && typeof timer === "object" && "unref" in timer) timer.unref();
|
|
491
515
|
job.timerId = timer;
|
|
492
516
|
} catch (err) {
|
|
@@ -572,6 +596,7 @@ var CronScheduler = class {
|
|
|
572
596
|
const line = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
|
|
573
597
|
capturedLogs.push(line);
|
|
574
598
|
},
|
|
599
|
+
rebase: this.client,
|
|
575
600
|
client: this.client
|
|
576
601
|
};
|
|
577
602
|
job.state = "running";
|
|
@@ -594,7 +619,7 @@ var CronScheduler = class {
|
|
|
594
619
|
}
|
|
595
620
|
} catch (err) {
|
|
596
621
|
success = false;
|
|
597
|
-
error = err instanceof Error ? err.message : String(err);
|
|
622
|
+
error = redactSensitiveText(err instanceof Error ? err.message : String(err));
|
|
598
623
|
job.totalFailures++;
|
|
599
624
|
} finally {
|
|
600
625
|
job.executing = false;
|
|
@@ -644,4 +669,4 @@ var CronScheduler = class {
|
|
|
644
669
|
//#endregion
|
|
645
670
|
export { cron_scheduler_exports as n, validateCronExpression as r, CronScheduler as t };
|
|
646
671
|
|
|
647
|
-
//# sourceMappingURL=cron-scheduler-
|
|
672
|
+
//# sourceMappingURL=cron-scheduler-FJAaCAXm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cron-scheduler-FJAaCAXm.js","names":[],"sources":["../src/cron/scale-to-zero.ts","../src/cron/cron-scheduler.ts"],"sourcesContent":["/**\n * Scale-to-zero detection for the cron scheduler.\n *\n * The scheduler drives jobs with in-process `setTimeout`. That works on any\n * always-running instance, but on a platform that freezes or evicts the\n * container between requests (Cloud Run with `--min-instances=0`, AWS Lambda,\n * Vercel functions) the timers simply never fire — the process boots, logs the\n * jobs as registered, and silently runs nothing.\n *\n * None of these platforms expose their scaling floor to the container, so this\n * detection is a heuristic: it identifies the *platform*, not the setting. It\n * is a warning only — it must never influence boot.\n *\n * Environment variables used here were verified against vendor documentation:\n * - `K_SERVICE` / `K_REVISION` / `K_CONFIGURATION` — Cloud Run services\n * (Cloud Run container contract; no variable exposes min-instances).\n * - `CLOUD_RUN_JOB` — Cloud Run jobs (same contract).\n * - `AWS_LAMBDA_FUNCTION_NAME` — reserved AWS Lambda runtime variable.\n * - `VERCEL=1` — Vercel system environment variable, available at runtime.\n * - `KUBERNETES_SERVICE_HOST` — injected into every pod by the kubelet. Used\n * as an *exclusion*: a Deployment pod runs continuously, and Knative on\n * Kubernetes also sets `K_SERVICE`, so a pod is never warned about.\n */\n\n/** Environment variable that permanently silences the scale-to-zero warning. */\nexport const CRON_ALWAYS_ON_ENV = \"REBASE_CRON_ALWAYS_ON\";\n\n/** The subset of `process.env` this module reads. */\nexport type EnvLike = Record<string, string | undefined>;\n\nexport interface FreezableRuntime {\n /** Human-readable platform name, used verbatim in the warning. */\n platform: string;\n /** Names of the environment variables that identified the platform. */\n signals: string[];\n}\n\n/** Minimal shape of a registered job needed to build the warning. */\nexport interface WarnableJob {\n id: string;\n enabled: boolean;\n}\n\nexport interface ScaleToZeroWarning {\n message: string;\n data: Record<string, unknown>;\n}\n\n/** Accepts the usual truthy spellings; anything else (including \"\") is false. */\nfunction isTruthy(value: string | undefined): boolean {\n if (!value) return false;\n const normalised = value.trim().toLowerCase();\n return normalised === \"1\" || normalised === \"true\" || normalised === \"yes\" || normalised === \"on\";\n}\n\n/**\n * Identify a runtime whose instances can be frozen or torn down between\n * requests. Returns `undefined` when the platform is unknown or known to run\n * continuously.\n */\nexport function detectFreezableRuntime(env: EnvLike = process.env): FreezableRuntime | undefined {\n // A Kubernetes pod (GKE, EKS, self-hosted) runs continuously. Knative and\n // Cloud Run for Anthos set K_SERVICE *inside* a pod, so this exclusion has\n // to come first or every Knative pod would be a false positive.\n if (env.KUBERNETES_SERVICE_HOST) return undefined;\n\n if (env.K_SERVICE) {\n const signals = [\"K_SERVICE\"];\n if (env.K_REVISION) signals.push(\"K_REVISION\");\n if (env.K_CONFIGURATION) signals.push(\"K_CONFIGURATION\");\n return { platform: \"Cloud Run\", signals };\n }\n\n if (env.CLOUD_RUN_JOB) {\n return { platform: \"Cloud Run Jobs\", signals: [\"CLOUD_RUN_JOB\"] };\n }\n\n if (env.AWS_LAMBDA_FUNCTION_NAME) {\n return { platform: \"AWS Lambda\", signals: [\"AWS_LAMBDA_FUNCTION_NAME\"] };\n }\n\n if (env.VERCEL === \"1\") {\n return { platform: \"Vercel\", signals: [\"VERCEL\"] };\n }\n\n return undefined;\n}\n\n/** How many job ids to name before collapsing the rest into \"+N more\". */\nconst MAX_NAMED_JOBS = 10;\n\n/**\n * Build the boot-time warning, or `undefined` when it does not apply.\n *\n * Fires only when all of the following hold:\n * 1. `NODE_ENV=production` — a laptop or CI run is not at risk.\n * 2. At least one *enabled* job is registered — nothing to lose otherwise.\n * 3. The environment looks like a freezable platform (see above).\n * 4. `REBASE_CRON_ALWAYS_ON` is not set to a truthy value.\n */\nexport function buildScaleToZeroWarning(\n jobs: WarnableJob[],\n env: EnvLike = process.env\n): ScaleToZeroWarning | undefined {\n if (env.NODE_ENV !== \"production\") return undefined;\n if (isTruthy(env[CRON_ALWAYS_ON_ENV])) return undefined;\n\n const enabled = jobs.filter((job) => job.enabled).map((job) => job.id);\n if (enabled.length === 0) return undefined;\n\n const runtime = detectFreezableRuntime(env);\n if (!runtime) return undefined;\n\n const named = enabled.slice(0, MAX_NAMED_JOBS);\n const list = enabled.length > named.length\n ? `${named.join(\", \")} (+${enabled.length - named.length} more)`\n : named.join(\", \");\n\n const message = `[cron] ${runtime.platform} detected — in-process timers do not fire while an instance is frozen or scaled to zero, so ${enabled.length} enabled job(s) may never run: ${list}; drive them from an external scheduler instead (POST /api/cron/:id/trigger, e.g. Cloud Scheduler). ${runtime.platform} does not expose its scaling floor to the container, so an always-warm deployment cannot be confirmed from inside the process — set ${CRON_ALWAYS_ON_ENV}=1 to silence this if at least one instance is pinned warm.`;\n\n return {\n message,\n data: {\n platform: runtime.platform,\n signals: runtime.signals,\n jobs: enabled\n }\n };\n}\n","import type {\n CronJobDefinition,\n CronJobStatus,\n CronJobLogEntry,\n CronJobRunState,\n CronJobContext\n} from \"@rebasepro/types\";\nimport type { RebaseServerClient } from \"@rebasepro/types\";\nimport type { LoadedCronJob } from \"./cron-loader\";\nimport type { CronStore } from \"./cron-store\";\nimport { logger, redactSensitiveText } from \"../utils/logger.js\";\nimport { buildScaleToZeroWarning } from \"./scale-to-zero.js\";\n\n// ─── Cron expression parser (minimal, no external dependency) ────────\n// Supports standard 5-field cron (minute hour dom month dow).\n// Returns the next Date after `after` that matches the expression.\n\n/**\n * Expand a single cron field into an ordered array of allowed values.\n * Supports: `*`, `N`, `N-M`, `N/S`, `N-M/S`, `*\\/S`, and comma-separated combinations.\n */\nfunction expandCronField(field: string, min: number, max: number): number[] {\n const results = new Set<number>();\n for (const segment of field.split(\",\")) {\n const trimmed = segment.trim();\n if (trimmed === \"*\") {\n for (let i = min; i <= max; i++) results.add(i);\n } else if (trimmed.includes(\"/\")) {\n const [rangeStr, stepStr] = trimmed.split(\"/\");\n const step = parseInt(stepStr, 10);\n if (isNaN(step) || step <= 0) {\n throw new Error(`Invalid step value \"${stepStr}\" in cron field \"${field}\"`);\n }\n let start = min;\n let end = max;\n if (rangeStr !== \"*\") {\n if (rangeStr.includes(\"-\")) {\n const [a, b] = rangeStr.split(\"-\").map(Number);\n start = a;\n end = b;\n } else {\n start = parseInt(rangeStr, 10);\n }\n }\n for (let i = start; i <= end; i += step) results.add(i);\n } else if (trimmed.includes(\"-\")) {\n const [a, b] = trimmed.split(\"-\").map(Number);\n for (let i = a; i <= b; i++) results.add(i);\n } else {\n const val = parseInt(trimmed, 10);\n if (isNaN(val)) {\n throw new Error(`Invalid value \"${trimmed}\" in cron field \"${field}\"`);\n }\n results.add(val);\n }\n }\n return [...results].sort((a, b) => a - b);\n}\n\n/**\n * Validates a standard 5-field cron expression structurally and semantically.\n * Returns `{ valid: true }` or `{ valid: false, reason: string }`.\n */\nexport function validateCronExpression(schedule: string): { valid: true } | { valid: false; reason: string } {\n if (!schedule || typeof schedule !== \"string\") {\n return { valid: false,\nreason: \"Schedule must be a non-empty string\" };\n }\n const parts = schedule.trim().split(/\\s+/);\n if (parts.length !== 5) {\n return { valid: false,\nreason: `Expected 5 fields, got ${parts.length}` };\n }\n const fieldRanges: [string, number, number][] = [\n [\"minute\", 0, 59],\n [\"hour\", 0, 23],\n [\"day of month\", 1, 31],\n [\"month\", 1, 12],\n [\"day of week\", 0, 6]\n ];\n for (let i = 0; i < 5; i++) {\n const [name, min, max] = fieldRanges[i];\n try {\n const values = expandCronField(parts[i], min, max);\n if (values.length === 0) {\n return { valid: false,\nreason: `${name} field \"${parts[i]}\" produces no values` };\n }\n for (const v of values) {\n if (v < min || v > max) {\n return { valid: false,\nreason: `${name} field value ${v} out of range [${min}–${max}]` };\n }\n }\n } catch (err) {\n return { valid: false,\nreason: `${name} field: ${err instanceof Error ? err.message : String(err)}` };\n }\n }\n return { valid: true };\n}\n\n/** The five cron fields, pre-expanded into the values each one allows. */\ninterface CronFields {\n minutes: number[];\n hours: number[];\n doms: number[];\n months: number[];\n dows: number[];\n}\n\n/** Expand all five fields of an expression. Throws on invalid expressions. */\nfunction parseCronFields(expression: string): CronFields {\n const parts = expression.trim().split(/\\s+/);\n if (parts.length < 5) {\n throw new Error(`Invalid cron expression: \"${expression}\". Expected 5 fields.`);\n }\n const [minField, hourField, domField, monField, dowField] = parts;\n return {\n minutes: expandCronField(minField, 0, 59),\n hours: expandCronField(hourField, 0, 23),\n doms: expandCronField(domField, 1, 31),\n months: expandCronField(monField, 1, 12),\n dows: expandCronField(dowField, 0, 6) // 0=Sunday\n };\n}\n\n/** Whether a minute-precision instant matches every field of the expression. */\nfunction matchesCronFields(candidate: Date, fields: CronFields): boolean {\n return fields.months.includes(candidate.getMonth() + 1) // getMonth is 0-11\n && fields.doms.includes(candidate.getDate())\n && fields.dows.includes(candidate.getDay())\n && fields.hours.includes(candidate.getHours())\n && fields.minutes.includes(candidate.getMinutes());\n}\n\n/** ~1 year in minutes — the walk bound for both search directions. */\nconst MAX_SLOT_SEARCH_MINUTES = 525960;\n\n/**\n * Calculate the next Date after `after` that matches the cron expression.\n * Throws on invalid expressions.\n */\nfunction parseCronExpression(expression: string, after: Date): Date {\n const fields = parseCronFields(expression);\n\n // Forward-search from `after + 1 minute`\n const candidate = new Date(after);\n candidate.setSeconds(0, 0);\n candidate.setMinutes(candidate.getMinutes() + 1);\n\n for (let i = 0; i < MAX_SLOT_SEARCH_MINUTES; i++) {\n if (matchesCronFields(candidate, fields)) {\n return candidate;\n }\n candidate.setMinutes(candidate.getMinutes() + 1);\n }\n\n // Fallback — should not happen with valid expressions\n const fallback = new Date(after);\n fallback.setMinutes(fallback.getMinutes() + 1);\n return fallback;\n}\n\n/**\n * The latest slot matching `expression` within the inclusive window\n * `[from, to]`, or `undefined` when the expression has no slot in it.\n *\n * Walks backwards a minute at a time from `to`, so the first hit is already\n * the answer — in the common case (a job that ran normally moments ago) that\n * is a handful of iterations, not a scan of the whole window.\n *\n * `to`'s own minute is included: an instance booting at 06:00:30 has *not* run\n * the 06:00 slot — `parseCronExpression` already skipped past it to tomorrow —\n * so that slot is genuinely missed and must be a candidate.\n *\n * Seconds and milliseconds are zeroed to match how `parseCronExpression`\n * builds a slot, so the same wall-clock slot serialises to a byte-identical\n * ISO string down either path. The claim key depends on that.\n */\nexport function findMostRecentSlot(expression: string, from: Date, to: Date): Date | undefined {\n const fields = parseCronFields(expression);\n\n const candidate = new Date(to);\n candidate.setSeconds(0, 0);\n\n for (let i = 0; i < MAX_SLOT_SEARCH_MINUTES && candidate.getTime() >= from.getTime(); i++) {\n if (matchesCronFields(candidate, fields)) {\n return candidate;\n }\n candidate.setMinutes(candidate.getMinutes() - 1);\n }\n\n return undefined;\n}\n\n// ─── In-memory ring buffer for logs ──────────────────────────────────\n\nconst MAX_LOGS_PER_JOB = 50;\n\n/**\n * Minimum milliseconds between scheduled executions of the same job.\n * Prevents tight re-execution loops caused by jitter or clock drift.\n */\nconst MIN_SCHEDULE_INTERVAL_MS = 5_000; // 5 seconds\n\n/**\n * Largest delay setTimeout can hold. Node stores it in a 32-bit signed int;\n * anything larger silently clamps to 1ms and fires immediately, so a slot\n * further out than this must be reached in hops rather than one timer.\n */\nconst MAX_TIMER_DELAY_MS = 2_147_483_647; // 2^31 - 1, ~24.8 days\n\n// ─── CronScheduler ───────────────────────────────────────────────────\n\ninterface RegisteredJob {\n id: string;\n definition: CronJobDefinition;\n enabled: boolean;\n state: CronJobRunState;\n lastRunAt?: Date;\n nextRunAt?: Date;\n lastDurationMs?: number;\n lastError?: string;\n totalRuns: number;\n totalFailures: number;\n timerId?: ReturnType<typeof setTimeout>;\n logs: CronJobLogEntry[];\n /** True while a handler is actively executing (prevents concurrent runs). */\n executing: boolean;\n}\n\nexport class CronScheduler {\n private jobs = new Map<string, RegisteredJob>();\n private started = false;\n private store?: CronStore;\n private client?: RebaseServerClient;\n\n /**\n * Set the server singleton to make it available to cron job handlers.\n *\n * `RebaseServerClient`, not `RebaseClient`: the object `init.ts` passes is\n * the same one it registers as the singleton, so this was always the true\n * type — and the wider annotation is what let `ctx.client.data` look like a\n * user-scoped plane inside a cron, when it is the RLS-bypassing one.\n */\n setClient(client: RebaseServerClient): void {\n this.client = client;\n }\n\n /**\n * Attach a persistence store for cron logs.\n * When set, execution logs are written to the database after each run,\n * and counters are seeded from the database on start.\n */\n setStore(store: CronStore): void {\n this.store = store;\n }\n\n /**\n * Register a batch of loaded cron jobs.\n *\n * If the scheduler is already started, newly registered jobs are\n * automatically scheduled (so late-registered jobs don't sit idle).\n *\n * Validates the cron schedule on registration — invalid schedules\n * are rejected with a warning and the job is NOT registered.\n */\n registerJobs(loadedJobs: LoadedCronJob[]): void {\n for (const loaded of loadedJobs) {\n // Validate schedule up-front — reject invalid schedules\n const validation = validateCronExpression(loaded.definition.schedule);\n if (!validation.valid) {\n logger.error(`[cron] Rejecting job \"${loaded.id}\": invalid schedule \"${loaded.definition.schedule}\" — ${validation.reason}`);\n continue;\n }\n\n const existing = this.jobs.get(loaded.id);\n if (existing) {\n logger.warn(`[cron] Duplicate cron job id: \"${loaded.id}\". Overwriting.`);\n this.stopJob(loaded.id);\n }\n\n const enabled = loaded.definition.enabled !== false;\n\n this.jobs.set(loaded.id, {\n id: loaded.id,\n definition: loaded.definition,\n enabled,\n state: enabled ? \"idle\" : \"disabled\",\n totalRuns: 0,\n totalFailures: 0,\n logs: [],\n executing: false\n });\n\n // If the scheduler is already running, auto-schedule new jobs\n if (this.started && enabled) {\n this.scheduleNext(loaded.id);\n }\n }\n }\n\n /**\n * Start the scheduler — begins ticking all enabled jobs.\n */\n start(): void {\n if (this.started) return;\n this.started = true;\n\n // Seed counters from DB (non-blocking — scheduler starts immediately)\n if (this.store) {\n this.store.fetchJobStats().then((stats) => {\n for (const [jobId, data] of stats) {\n const job = this.jobs.get(jobId);\n if (job) {\n job.totalRuns = data.totalRuns;\n job.totalFailures = data.totalFailures;\n if (data.lastRunAt) {\n job.lastRunAt = new Date(data.lastRunAt);\n }\n }\n }\n }).catch((err) => {\n logger.warn(\"[cron] Failed to seed job stats from database\", { error: err });\n });\n }\n\n for (const [id, job] of this.jobs) {\n if (job.enabled) {\n this.scheduleNext(id);\n }\n }\n if (!this.store) {\n logger.warn(\"[cron] No cron store attached — runs are uncoordinated; with multiple app instances every instance will execute every job\");\n }\n this.warnIfScaleToZero();\n\n // Recover slots that elapsed while nothing was ticking. Deliberately\n // not awaited: catch-up reaches the database and runs handlers, and\n // boot must not wait on either. Its own errors are contained inside.\n void this.catchUpMissedSlots();\n\n logger.info(`⏰ Cron scheduler started with ${this.jobs.size} job(s)`);\n }\n\n /**\n * Stop the scheduler and clear all timers.\n *\n * Currently-executing handlers run to completion (they are async),\n * but no further scheduling occurs after stop.\n */\n stop(): void {\n this.started = false;\n for (const [id] of this.jobs) {\n this.stopJob(id);\n }\n }\n\n /**\n * List all registered jobs with their current status.\n */\n listJobs(): CronJobStatus[] {\n return [...this.jobs.values()].map((job) => this.toStatus(job));\n }\n\n /**\n * Get a single job status by ID.\n */\n getJob(id: string): CronJobStatus | undefined {\n const job = this.jobs.get(id);\n return job ? this.toStatus(job) : undefined;\n }\n\n /**\n * Get log entries for a job.\n */\n getJobLogs(id: string, limit?: number): CronJobLogEntry[] {\n const job = this.jobs.get(id);\n if (!job) return [];\n const logs = [...job.logs].reverse(); // newest first\n return limit ? logs.slice(0, limit) : logs;\n }\n\n /**\n * Get log entries for a job from the database (if store is available).\n * Falls back to in-memory logs if no store is configured.\n */\n async getJobLogsFromDb(id: string, limit?: number): Promise<CronJobLogEntry[]> {\n if (this.store) {\n const dbLogs = await this.store.fetchLogs(id, limit);\n if (dbLogs.length > 0) return dbLogs;\n }\n // Fallback to in-memory\n return this.getJobLogs(id, limit);\n }\n\n /**\n * Enable or disable a job at runtime.\n */\n setJobEnabled(id: string, enabled: boolean): CronJobStatus | undefined {\n const job = this.jobs.get(id);\n if (!job) return undefined;\n\n job.enabled = enabled;\n\n if (enabled && this.started) {\n job.state = \"idle\";\n this.scheduleNext(id);\n } else if (!enabled) {\n this.stopJob(id);\n job.state = \"disabled\";\n }\n\n return this.toStatus(job);\n }\n\n /**\n * Manually trigger a job execution immediately.\n *\n * Returns `undefined` if the job doesn't exist.\n * If the job is currently executing, returns the log entry with\n * a `skipped: true` result rather than running concurrently.\n */\n async triggerJob(id: string): Promise<CronJobLogEntry | undefined> {\n const job = this.jobs.get(id);\n if (!job) return undefined;\n\n // Concurrency guard — don't run two instances simultaneously\n if (job.executing) {\n logger.warn(`[cron] Skipping manual trigger of \"${id}\" — already executing`);\n const logEntry: CronJobLogEntry = {\n jobId: id,\n startedAt: new Date().toISOString(),\n finishedAt: new Date().toISOString(),\n durationMs: 0,\n success: true,\n result: { skipped: true,\nreason: \"already_executing\" },\n logs: [\"Skipped: job is already running\"],\n manual: true\n };\n job.logs.push(logEntry);\n if (job.logs.length > MAX_LOGS_PER_JOB) job.logs.shift();\n return logEntry;\n }\n\n return this.executeJob(job, true);\n }\n\n // ─── Internal ────────────────────────────────────────────────────\n\n /**\n * Warn once at start when the process looks like it is running on a\n * platform that freezes or evicts instances between requests, where the\n * in-process timers this scheduler relies on never fire.\n *\n * Advisory only: any failure here is swallowed so a detection bug can\n * never take a production boot down.\n */\n private warnIfScaleToZero(): void {\n try {\n const warning = buildScaleToZeroWarning(\n [...this.jobs.values()].map((job) => ({ id: job.id, enabled: job.enabled }))\n );\n if (warning) {\n logger.warn(warning.message, warning.data);\n }\n } catch {\n // Never let the advisory check affect startup.\n }\n }\n\n /**\n * Schedule the next execution for a job.\n *\n * Safety guarantees:\n * 1. Clears any existing timer first (prevents leaked/duplicate timers)\n * 2. Enforces a minimum delay to prevent tight loops from jitter\n * 3. Unref's the timer so it doesn't prevent process exit\n * 4. Re-checks enabled & started state before executing\n * 5. Concurrency guard prevents overlapping handler executions\n */\n private scheduleNext(id: string): void {\n const job = this.jobs.get(id);\n if (!job || !job.enabled || !this.started) return;\n\n // Clear any previously scheduled timer to prevent double-firing\n this.stopJob(id);\n\n try {\n const now = new Date();\n const nextRun = parseCronExpression(job.definition.schedule, now);\n job.nextRunAt = nextRun;\n\n const rawDelay = nextRun.getTime() - now.getTime();\n // Enforce a minimum delay to prevent tight re-execution loops\n // from event loop jitter or near-zero setTimeout drift\n const delay = Math.max(rawDelay, MIN_SCHEDULE_INTERVAL_MS);\n\n // A slot past the 32-bit timer ceiling cannot be armed directly:\n // setTimeout would clamp it to 1ms and fire at once, and since the\n // slot is already claimed by then, every wake re-schedules the same\n // overflowing delay — a permanent hot loop, not a late job. Sleep\n // to the ceiling and re-derive the delay on waking instead; the\n // cron expression stays the source of truth across the hops.\n if (delay > MAX_TIMER_DELAY_MS) {\n const hop = setTimeout(() => {\n if (this.started && job.enabled) this.scheduleNext(id);\n }, MAX_TIMER_DELAY_MS);\n if (hop && typeof hop === \"object\" && \"unref\" in hop) {\n hop.unref();\n }\n job.timerId = hop;\n return;\n }\n\n const timer = setTimeout(async () => {\n // Re-check state: scheduler may have been stopped or job disabled\n // between when we scheduled and when we fire\n if (!job.enabled || !this.started) return;\n\n // Concurrency guard: if somehow we're already executing, skip\n if (job.executing) {\n logger.warn(`[cron] Skipping scheduled run of \"${id}\" — still executing from previous run`);\n // Re-schedule to try again later\n this.scheduleNext(id);\n return;\n }\n\n // A timer can wake before its slot: a delay past the 32-bit\n // ceiling, a clock stepped backwards by NTP, a VM resuming from\n // suspend. Claiming on an early wake is unrecoverable — claims\n // are permanent, so the slot would be burned and the real run\n // silently skipped when it came due. Re-derive from the wall\n // clock and re-arm instead; only the fire that is genuinely due\n // may claim.\n if (Date.now() < nextRun.getTime()) {\n this.scheduleNext(id);\n return;\n }\n\n // Cross-instance guard: claim the scheduled slot in the store.\n // The slot is the scheduled fire time — deterministic across\n // instances — so exactly one instance wins each (job, slot) pair.\n // A store without tryClaimRun (pre-claims custom implementation)\n // runs uncoordinated; a throwing store fails open — either way\n // this callback must never reject, or the job would silently\n // stop rescheduling.\n if (this.store?.tryClaimRun) {\n let claimed = true;\n try {\n claimed = await this.store.tryClaimRun(id, nextRun.toISOString());\n } catch (err) {\n logger.warn(`[cron] Claim check threw for \"${id}\" — running uncoordinated`, { error: err });\n }\n if (!claimed) {\n logger.info(`[cron] Slot ${nextRun.toISOString()} for \"${id}\" claimed by another instance — skipping`);\n if (this.started && job.enabled) {\n this.scheduleNext(id);\n }\n return;\n }\n }\n\n await this.executeJob(job, false);\n\n // Schedule the next tick (only if still started + enabled)\n if (this.started && job.enabled) {\n this.scheduleNext(id);\n }\n }, delay);\n\n // Unref the timer so it doesn't prevent Node.js from exiting\n // during graceful shutdown\n if (timer && typeof timer === \"object\" && \"unref\" in timer) {\n timer.unref();\n }\n\n job.timerId = timer;\n } catch (err: unknown) {\n logger.error(`[cron] Failed to schedule \"${id}\"`, { error: err });\n job.state = \"error\";\n job.lastError = err instanceof Error ? err.message : String(err);\n }\n }\n\n /**\n * Run any slot that elapsed while no instance was holding a timer for it.\n *\n * Only jobs that opted in via `catchUpWindowSeconds` are considered, and\n * only their single most recent missed slot — see the field's docs for why\n * both limits are deliberate.\n *\n * The claim is what makes this safe. In the ordinary case — an instance\n * restarting minutes after a slot ran normally — the most recent slot is\n * already claimed, so this costs one `tryClaimRun` per job per boot and\n * does nothing. A slot is only executed when no instance, past or present,\n * ever claimed it.\n *\n * Never throws: a failure here must not take down a scheduler that is\n * otherwise ticking correctly.\n */\n private async catchUpMissedSlots(): Promise<void> {\n const candidates = [...this.jobs.values()].filter(\n job => job.enabled && (job.definition.catchUpWindowSeconds ?? 0) > 0\n );\n if (candidates.length === 0) return;\n\n // A claims-capable store is the whole safety mechanism. Without one,\n // every boot would look like \"this slot never ran\" and an instance\n // recycled twice an hour would re-run the same hourly job twice an\n // hour. Refusing to catch up is the correct degradation.\n if (!this.store?.tryClaimRun) {\n logger.warn(\n `[cron] Catch-up is configured on ${candidates.length} job(s) but no claims-capable store is attached — skipping. ` +\n \"Without claims a restart cannot tell an unrun slot from one the previous instance already ran.\"\n );\n return;\n }\n\n const now = new Date();\n\n for (const job of candidates) {\n try {\n if (!this.started || !job.enabled || job.executing) continue;\n\n const windowSeconds = job.definition.catchUpWindowSeconds!;\n const from = new Date(now.getTime() - windowSeconds * 1000);\n const slot = findMostRecentSlot(job.definition.schedule, from, now);\n if (!slot) continue;\n\n const slotIso = slot.toISOString();\n\n // Same key the scheduled path claims with, so a slot that fired\n // normally is already taken and this is a no-op.\n let claimed: boolean;\n try {\n claimed = await this.store.tryClaimRun(job.id, slotIso);\n } catch (err) {\n // Fail closed, unlike the scheduled path. A missed slot is\n // a recovery, not an obligation — running it against a\n // store that cannot tell us whether it already ran risks a\n // duplicate on every boot.\n logger.warn(`[cron] Catch-up claim threw for \"${job.id}\" — skipping catch-up`, { error: err });\n continue;\n }\n\n if (!claimed) continue;\n\n const lateBy = Math.round((now.getTime() - slot.getTime()) / 1000);\n logger.info(`[cron] Catching up missed slot ${slotIso} for \"${job.id}\" (${lateBy}s late)`);\n\n await this.executeJob(job, false, `⏰ Catch-up run for missed slot ${slotIso} (${lateBy}s late)`);\n } catch (err) {\n logger.error(`[cron] Catch-up failed for \"${job.id}\"`, { error: err });\n }\n }\n }\n\n /**\n * Stop a single job's timer and clear its next run state.\n */\n private stopJob(id: string): void {\n const job = this.jobs.get(id);\n if (job?.timerId) {\n clearTimeout(job.timerId);\n job.timerId = undefined;\n job.nextRunAt = undefined;\n }\n }\n\n /**\n * Execute a job's handler with full isolation and safety.\n *\n * - Sets a concurrency flag to prevent overlapping runs\n * - Wraps handler in a timeout race\n * - Captures all logs, errors, and results\n * - Persists to store (non-blocking) if available\n * - Always restores state even on catastrophic errors\n */\n private async executeJob(\n job: RegisteredJob,\n manual: boolean,\n seedLog?: string\n ): Promise<CronJobLogEntry> {\n const startedAt = new Date();\n // A caller-supplied first line, stored with the run's own output. A\n // catch-up uses it to say so in the persisted log, where an operator\n // reading `cron_logs` will actually see it — `manual` is the only other\n // provenance the entry carries, and a catch-up is not manual.\n const capturedLogs: string[] = seedLog ? [seedLog] : [];\n\n // Set executing flag — prevents concurrent runs\n job.executing = true;\n\n const ctx: CronJobContext = {\n jobId: job.id,\n scheduledAt: startedAt,\n log: (...args: unknown[]) => {\n const line = args.map((a) =>\n typeof a === \"string\" ? a : JSON.stringify(a)\n ).join(\" \");\n capturedLogs.push(line);\n },\n // Both names, one object. `rebase` is canonical — it matches the\n // singleton import and `defineFunction`'s context — and `client` is\n // the deprecated alias kept so existing cron files keep running.\n //\n // The assertion covers only `client`'s extra `data`: the singleton\n // carries that alias at runtime (`init.ts` assigns `data` and\n // `dataAsAdmin` to the same plane) but `RebaseServerClient` omits it\n // by design, so the deprecated name has to be re-stated here rather\n // than leaking back into the canonical type.\n rebase: this.client!,\n client: this.client! as CronJobContext[\"client\"]\n };\n\n job.state = \"running\";\n job.lastRunAt = startedAt;\n job.totalRuns++;\n\n let success = true;\n let error: string | undefined;\n let result: unknown;\n\n try {\n // Race with timeout\n const timeout = (job.definition.timeoutSeconds ?? 300) * 1000;\n const handlerPromise = Promise.resolve(job.definition.handler(ctx));\n let timeoutHandle: ReturnType<typeof setTimeout>;\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutHandle = setTimeout(\n () => reject(new Error(`Cron job \"${job.id}\" timed out after ${timeout}ms`)),\n timeout\n );\n });\n\n try {\n result = await Promise.race([handlerPromise, timeoutPromise]);\n } finally {\n clearTimeout(timeoutHandle!);\n }\n } catch (err: unknown) {\n success = false;\n // Redacted at the point of capture, not just on the way to the log:\n // this string is persisted into `cron_logs` and rendered in the\n // Studio cron panel, and a job that fails on a query would\n // otherwise store `Failed query: <sql>\\nparams: <values>` — the\n // statement and every bound value — in a table, indefinitely.\n error = redactSensitiveText(err instanceof Error ? err.message : String(err));\n job.totalFailures++;\n } finally {\n // Always clear executing flag — even on catastrophic errors\n job.executing = false;\n }\n\n const finishedAt = new Date();\n const durationMs = finishedAt.getTime() - startedAt.getTime();\n\n job.state = success ? (job.enabled ? \"idle\" : \"disabled\") : \"error\";\n job.lastDurationMs = durationMs;\n job.lastError = error;\n\n const logEntry: CronJobLogEntry = {\n jobId: job.id,\n startedAt: startedAt.toISOString(),\n finishedAt: finishedAt.toISOString(),\n durationMs,\n success,\n error,\n result: result !== undefined ? result : undefined,\n logs: capturedLogs,\n manual\n };\n\n // Push to ring buffer\n job.logs.push(logEntry);\n if (job.logs.length > MAX_LOGS_PER_JOB) {\n job.logs.shift();\n }\n\n // Persist to database (non-blocking)\n if (this.store) {\n this.store.insertLog(logEntry).catch((persistErr) => {\n logger.error(`[cron] Failed to persist log for \"${job.id}\"`, { error: persistErr });\n });\n }\n\n if (success) {\n logger.info(`✅ [cron] \"${job.id}\" completed in ${durationMs}ms`);\n } else {\n logger.error(`❌ [cron] \"${job.id}\" failed in ${durationMs}ms: ${error}`);\n }\n\n return logEntry;\n }\n\n private toStatus(job: RegisteredJob): CronJobStatus {\n return {\n id: job.id,\n name: job.definition.name,\n description: job.definition.description,\n schedule: job.definition.schedule,\n enabled: job.enabled,\n state: job.state,\n lastRunAt: job.lastRunAt?.toISOString(),\n nextRunAt: job.nextRunAt?.toISOString(),\n lastDurationMs: job.lastDurationMs,\n lastError: job.lastError,\n totalRuns: job.totalRuns,\n totalFailures: job.totalFailures\n };\n }\n}\n"],"mappings":"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"}
|
|
@@ -2,9 +2,10 @@ import { createRequire as __createRequire } from "module";
|
|
|
2
2
|
import "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
4
|
import { n as __exportAll } from "./rolldown-runtime-DSJWtz9O.js";
|
|
5
|
-
import "./src-
|
|
6
|
-
import
|
|
7
|
-
import { t as
|
|
5
|
+
import "./src-By48Ffg0.js";
|
|
6
|
+
import "./src-Ca6NhxKs.js";
|
|
7
|
+
import { i as isSQLAdmin, n as hasInCauseChain, r as revokeInternalTableSql, t as createDdlBootstrapper } from "./ddl-bootstrap-BhXbTnBl.js";
|
|
8
|
+
import { t as logger } from "./logger-DfvF_8r-.js";
|
|
8
9
|
//#region src/cron/cron-store.ts
|
|
9
10
|
var cron_store_exports = /* @__PURE__ */ __exportAll({ createCronStore: () => createCronStore });
|
|
10
11
|
var TABLE = "rebase.cron_logs";
|
|
@@ -12,20 +13,24 @@ var CLAIMS_TABLE = "rebase.cron_claims";
|
|
|
12
13
|
/** Claims older than this are garbage-collected on startup. */
|
|
13
14
|
var CLAIM_RETENTION_DAYS = 7;
|
|
14
15
|
/**
|
|
16
|
+
* How far ahead a claim may legitimately sit. Slots are claimed as they fire,
|
|
17
|
+
* so anything beyond this is a clock-skewed peer at best and a stranded claim
|
|
18
|
+
* at worst; see the sweep in `ensureTable`.
|
|
19
|
+
*/
|
|
20
|
+
var FUTURE_CLAIM_SKEW_MINUTES = 2;
|
|
21
|
+
/**
|
|
15
22
|
* Detect a unique-constraint violation anywhere in an error's cause chain.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
23
|
+
* Match the SQLSTATE code, never message text. Also covers SQLite
|
|
24
|
+
* ("UNIQUE constraint failed") and MySQL (ER_DUP_ENTRY 1062) for future SQL
|
|
25
|
+
* drivers.
|
|
26
|
+
*
|
|
27
|
+
* Distinct from `isConcurrentDdlRace` in `boot/ddl-bootstrap.ts`, which shares
|
|
28
|
+
* the 23505 code but asks a different question — that one is about a losing
|
|
29
|
+
* `CREATE`, this one is about a losing claim, and only the latter means
|
|
30
|
+
* "another instance already has this slot".
|
|
19
31
|
*/
|
|
20
32
|
function isUniqueViolation(err) {
|
|
21
|
-
|
|
22
|
-
for (let depth = 0; depth < 10 && current; depth++) if (typeof current === "object") {
|
|
23
|
-
const e = current;
|
|
24
|
-
if (e.code === "23505" || e.errno === 1062) return true;
|
|
25
|
-
if (typeof e.message === "string" && e.message.includes("UNIQUE constraint failed")) return true;
|
|
26
|
-
current = e.cause;
|
|
27
|
-
} else break;
|
|
28
|
-
return false;
|
|
33
|
+
return hasInCauseChain(err, (e) => e.code === "23505" || e.errno === 1062 || typeof e.message === "string" && e.message.includes("UNIQUE constraint failed"));
|
|
29
34
|
}
|
|
30
35
|
function createCronStore(driver) {
|
|
31
36
|
const admin = driver.admin;
|
|
@@ -34,42 +39,56 @@ function createCronStore(driver) {
|
|
|
34
39
|
return;
|
|
35
40
|
}
|
|
36
41
|
const exec = (sqlText, options) => admin.executeSql(sqlText, options?.params ? { params: options.params } : void 0);
|
|
42
|
+
const ddl = createDdlBootstrapper(exec, "cron-store");
|
|
37
43
|
return {
|
|
38
44
|
async ensureTable() {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
45
|
+
await ddl.ensureObject("Creating schema rebase", "CREATE SCHEMA IF NOT EXISTS rebase");
|
|
46
|
+
await ddl.ensureObject(`Creating ${TABLE}`, `
|
|
47
|
+
CREATE TABLE IF NOT EXISTS ${TABLE} (
|
|
48
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
49
|
+
job_id TEXT NOT NULL,
|
|
50
|
+
started_at TIMESTAMPTZ NOT NULL,
|
|
51
|
+
finished_at TIMESTAMPTZ NOT NULL,
|
|
52
|
+
duration_ms INTEGER NOT NULL,
|
|
53
|
+
success BOOLEAN NOT NULL DEFAULT true,
|
|
54
|
+
error TEXT,
|
|
55
|
+
result JSONB,
|
|
56
|
+
logs JSONB,
|
|
57
|
+
manual BOOLEAN NOT NULL DEFAULT false
|
|
58
|
+
)
|
|
59
|
+
`);
|
|
60
|
+
await ddl.ensureObject("Creating idx_cron_logs_job", `
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_cron_logs_job
|
|
62
|
+
ON ${TABLE}(job_id, started_at DESC)
|
|
63
|
+
`);
|
|
64
|
+
await ddl.ensureObject(`Creating ${CLAIMS_TABLE}`, `
|
|
65
|
+
CREATE TABLE IF NOT EXISTS ${CLAIMS_TABLE} (
|
|
66
|
+
job_id TEXT NOT NULL,
|
|
67
|
+
slot TIMESTAMPTZ NOT NULL,
|
|
68
|
+
claimed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
69
|
+
PRIMARY KEY (job_id, slot)
|
|
70
|
+
)
|
|
71
|
+
`);
|
|
72
|
+
const [logsReady, claimsReady] = await Promise.all([ddl.isReadable(TABLE), ddl.isReadable(CLAIMS_TABLE)]);
|
|
73
|
+
if (claimsReady) {
|
|
74
|
+
await ddl.step("Claim retention sweep", async () => {
|
|
75
|
+
await exec(`DELETE FROM ${CLAIMS_TABLE} WHERE claimed_at < now() - make_interval(days => $1)`, { params: [CLAIM_RETENTION_DAYS] });
|
|
76
|
+
});
|
|
77
|
+
await ddl.step("Future-slot claim sweep", async () => {
|
|
78
|
+
const stranded = await exec(`DELETE FROM ${CLAIMS_TABLE}
|
|
79
|
+
WHERE slot > now() + make_interval(mins => $1)
|
|
80
|
+
RETURNING job_id, slot`, { params: [FUTURE_CLAIM_SKEW_MINUTES] });
|
|
81
|
+
for (const row of stranded ?? []) logger.warn(`[cron-store] Released a claim on the future slot ${new Date(row.slot).toISOString()} for "${row.job_id}" — it was claimed by a timer that fired early, and would otherwise have skipped that run`);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (logsReady) await ddl.step("Revoking end-user access to cron_logs", () => exec(revokeInternalTableSql("rebase", "cron_logs")));
|
|
85
|
+
if (claimsReady) await ddl.step("Revoking end-user access to cron_claims", () => exec(revokeInternalTableSql("rebase", "cron_claims")));
|
|
86
|
+
if (logsReady && claimsReady) {
|
|
68
87
|
logger.info("✅ Cron logs table ready");
|
|
69
|
-
|
|
70
|
-
logger.error("❌ Failed to create cron logs table", { error: err });
|
|
71
|
-
logger.warn("⚠️ Continuing without cron log persistence.");
|
|
88
|
+
return;
|
|
72
89
|
}
|
|
90
|
+
if (!claimsReady) logger.error(`❌ [cron-store] ${CLAIMS_TABLE} is unavailable — scheduled runs cannot be coordinated. With more than one app instance, every instance will now run every job on every tick.`);
|
|
91
|
+
if (!logsReady) logger.warn(`⚠️ [cron-store] ${TABLE} is unavailable — cron run history will not be persisted.`);
|
|
73
92
|
},
|
|
74
93
|
async insertLog(entry) {
|
|
75
94
|
try {
|
|
@@ -161,4 +180,4 @@ function rowToLogEntry(row) {
|
|
|
161
180
|
//#endregion
|
|
162
181
|
export { cron_store_exports as n, createCronStore as t };
|
|
163
182
|
|
|
164
|
-
//# sourceMappingURL=cron-store-
|
|
183
|
+
//# sourceMappingURL=cron-store-9NmUDfzL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cron-store-9NmUDfzL.js","names":[],"sources":["../src/cron/cron-store.ts"],"sourcesContent":["import type { CronJobLogEntry } from \"@rebasepro/types\";\nimport type { DataDriver } from \"@rebasepro/types\";\nimport { isSQLAdmin } from \"@rebasepro/types\";\nimport { revokeInternalTableSql } from \"@rebasepro/common\";\nimport { logger } from \"../utils/logger.js\";\nimport { createDdlBootstrapper, hasInCauseChain } from \"../boot/ddl-bootstrap.js\";\n\n/**\n * Persistence layer for cron job execution logs.\n *\n * Uses the DataDriver's `admin.executeSql` capability to store logs in a\n * `rebase.cron_logs` table. Falls back gracefully if the driver doesn't\n * support SQL (e.g. MongoDB) — in that case, no persistence occurs.\n */\nexport interface CronStore {\n /** Ensure the backing table exists. Called once on startup. */\n ensureTable(): Promise<void>;\n\n /** Persist a single log entry after execution. */\n insertLog(entry: CronJobLogEntry): Promise<void>;\n\n /**\n * Fetch the most recent logs for a job.\n * @param jobId The job identifier\n * @param limit Max entries to return (default 50)\n * @returns Logs sorted newest-first\n */\n fetchLogs(jobId: string, limit?: number): Promise<CronJobLogEntry[]>;\n\n /**\n * Fetch aggregate stats for all jobs (totalRuns, totalFailures, lastRunAt).\n * Used to seed in-memory counters on startup.\n */\n fetchJobStats(): Promise<Map<string, { totalRuns: number; totalFailures: number; lastRunAt?: string }>>;\n\n /**\n * Atomically claim a scheduled run slot for a job.\n *\n * `slot` is the *scheduled* fire time (ISO string) derived from the cron\n * expression — deterministic across instances regardless of timer drift,\n * so all instances contend on the same (jobId, slot) key. Exactly one\n * caller wins the insert against the unique constraint and executes;\n * the rest skip.\n *\n * Fails open (returns true) on unexpected store errors, so a broken\n * claims table degrades to uncoordinated execution rather than silently\n * never running jobs.\n *\n * Optional so custom stores written against the pre-claims interface\n * keep working — the scheduler treats a missing implementation as\n * uncoordinated (always run).\n */\n tryClaimRun?(jobId: string, slot: string): Promise<boolean>;\n}\n\n// ─── SQL-based implementation ────────────────────────────────────────\n\nconst TABLE = \"rebase.cron_logs\";\nconst CLAIMS_TABLE = \"rebase.cron_claims\";\n\n/** Claims older than this are garbage-collected on startup. */\nconst CLAIM_RETENTION_DAYS = 7;\n\n/**\n * How far ahead a claim may legitimately sit. Slots are claimed as they fire,\n * so anything beyond this is a clock-skewed peer at best and a stranded claim\n * at worst; see the sweep in `ensureTable`.\n */\nconst FUTURE_CLAIM_SKEW_MINUTES = 2;\n\n/**\n * Detect a unique-constraint violation anywhere in an error's cause chain.\n * Match the SQLSTATE code, never message text. Also covers SQLite\n * (\"UNIQUE constraint failed\") and MySQL (ER_DUP_ENTRY 1062) for future SQL\n * drivers.\n *\n * Distinct from `isConcurrentDdlRace` in `boot/ddl-bootstrap.ts`, which shares\n * the 23505 code but asks a different question — that one is about a losing\n * `CREATE`, this one is about a losing claim, and only the latter means\n * \"another instance already has this slot\".\n */\nfunction isUniqueViolation(err: unknown): boolean {\n return hasInCauseChain(err, (e) =>\n e.code === \"23505\" ||\n e.errno === 1062 ||\n (typeof e.message === \"string\" && e.message.includes(\"UNIQUE constraint failed\"))\n );\n}\n\nexport function createCronStore(driver: DataDriver): CronStore | undefined {\n const admin = driver.admin;\n if (!isSQLAdmin(admin)) {\n logger.warn(\"⚠️ [cron-store] DataDriver does not support SQL admin — cron logs will not be persisted.\");\n return undefined;\n }\n\n const exec = (sqlText: string, options?: { params?: unknown[] }) =>\n admin.executeSql(sqlText, options?.params ? { params: options.params } : undefined);\n\n const ddl = createDdlBootstrapper(exec, \"cron-store\");\n\n return {\n async ensureTable(): Promise<void> {\n // Creation. Every statement here is idempotent, so losing the race\n // to a peer that booted at the same moment is survivable — but only\n // if the loser retries rather than abandoning everything below it.\n // One step each, so a hard failure on any one of them does not take\n // the others with it. The claims table in particular must not be\n // lost because an index on the *logs* table could not be built.\n await ddl.ensureObject(\"Creating schema rebase\", \"CREATE SCHEMA IF NOT EXISTS rebase\");\n\n await ddl.ensureObject(`Creating ${TABLE}`, `\n CREATE TABLE IF NOT EXISTS ${TABLE} (\n id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,\n job_id TEXT NOT NULL,\n started_at TIMESTAMPTZ NOT NULL,\n finished_at TIMESTAMPTZ NOT NULL,\n duration_ms INTEGER NOT NULL,\n success BOOLEAN NOT NULL DEFAULT true,\n error TEXT,\n result JSONB,\n logs JSONB,\n manual BOOLEAN NOT NULL DEFAULT false\n )\n `);\n\n await ddl.ensureObject(\"Creating idx_cron_logs_job\", `\n CREATE INDEX IF NOT EXISTS idx_cron_logs_job\n ON ${TABLE}(job_id, started_at DESC)\n `);\n\n await ddl.ensureObject(`Creating ${CLAIMS_TABLE}`, `\n CREATE TABLE IF NOT EXISTS ${CLAIMS_TABLE} (\n job_id TEXT NOT NULL,\n slot TIMESTAMPTZ NOT NULL,\n claimed_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n PRIMARY KEY (job_id, slot)\n )\n `);\n\n // Everything from here is keyed on what actually exists, not on\n // whether *this* instance is the one that created it. A single\n // failure above used to abandon the rest of this method, which meant\n // the loser of a boot race skipped the sweeps and — far worse — the\n // privilege revocation, leaving the claims table writable by end\n // users on an instance that reported nothing but a warning about\n // log persistence.\n const [logsReady, claimsReady] = await Promise.all([\n ddl.isReadable(TABLE),\n ddl.isReadable(CLAIMS_TABLE)\n ]);\n\n if (claimsReady) {\n // Garbage-collect old claims — they are only needed while\n // instances could still contend on the same slot.\n await ddl.step(\"Claim retention sweep\", async () => {\n await exec(\n `DELETE FROM ${CLAIMS_TABLE} WHERE claimed_at < now() - make_interval(days => $1)`,\n { params: [CLAIM_RETENTION_DAYS] }\n );\n });\n\n // Drop claims for slots that have not happened yet. A slot is\n // claimed at the moment it fires, so a future one can only come\n // from a timer that woke early — and because claims are\n // permanent, that claim would silently skip the real run when it\n // finally came due. The margin keeps a legitimate claim made\n // moments early by a clock-skewed peer.\n await ddl.step(\"Future-slot claim sweep\", async () => {\n const stranded = await exec(\n `DELETE FROM ${CLAIMS_TABLE}\n WHERE slot > now() + make_interval(mins => $1)\n RETURNING job_id, slot`,\n { params: [FUTURE_CLAIM_SKEW_MINUTES] }\n );\n // A driver that does not honour RETURNING gives back\n // nothing; the rows are only used to report, so treat that\n // as \"none\".\n for (const row of (stranded ?? []) as { job_id: string; slot: string }[]) {\n logger.warn(\n `[cron-store] Released a claim on the future slot ${new Date(row.slot).toISOString()} ` +\n `for \"${row.job_id}\" — it was claimed by a timer that fired early, and would ` +\n \"otherwise have skipped that run\"\n );\n }\n });\n }\n\n // Neither table is a collection, so neither carries RLS, while the\n // Postgres driver's schema-wide grant reaches both. Cron logs hold\n // job output — arbitrary application data — and a writable\n // `cron_claims` lets any signed-in user suppress a scheduled run by\n // claiming its slot. This is a security control, so it is re-applied\n // by every instance on every boot, whatever else went wrong.\n if (logsReady) {\n await ddl.step(\"Revoking end-user access to cron_logs\", () =>\n exec(revokeInternalTableSql(\"rebase\", \"cron_logs\")));\n }\n if (claimsReady) {\n await ddl.step(\"Revoking end-user access to cron_claims\", () =>\n exec(revokeInternalTableSql(\"rebase\", \"cron_claims\")));\n }\n\n if (logsReady && claimsReady) {\n logger.info(\"✅ Cron logs table ready\");\n return;\n }\n // Say which capability is gone, and what it costs. \"Continuing\n // without cron log persistence\" undersold this: the claims table is\n // the only thing stopping every instance from running every job.\n if (!claimsReady) {\n logger.error(\n `❌ [cron-store] ${CLAIMS_TABLE} is unavailable — scheduled runs cannot be coordinated. ` +\n \"With more than one app instance, every instance will now run every job on every tick.\"\n );\n }\n if (!logsReady) {\n logger.warn(`⚠️ [cron-store] ${TABLE} is unavailable — cron run history will not be persisted.`);\n }\n },\n\n async insertLog(entry: CronJobLogEntry): Promise<void> {\n try {\n const resultJson = entry.result !== undefined ? JSON.stringify(entry.result) : null;\n const logsJson = entry.logs.length > 0 ? JSON.stringify(entry.logs) : null;\n\n await exec(\n `INSERT INTO ${TABLE} (job_id, started_at, finished_at, duration_ms, success, error, result, logs, manual)\n VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8::jsonb, $9)`,\n { params: [\n entry.jobId,\n entry.startedAt,\n entry.finishedAt,\n entry.durationMs,\n entry.success,\n entry.error || null,\n resultJson,\n logsJson,\n entry.manual\n ]}\n );\n } catch (err) {\n // Non-blocking — log persistence should never crash the scheduler\n logger.error(`[cron-store] Failed to persist log for \"${entry.jobId}\"`, { error: err });\n }\n },\n\n async fetchLogs(jobId: string, limit = 50): Promise<CronJobLogEntry[]> {\n try {\n const rows = await exec(\n `SELECT job_id, started_at, finished_at, duration_ms, success, error, result, logs, manual\n FROM ${TABLE}\n WHERE job_id = $1\n ORDER BY started_at DESC\n LIMIT $2`,\n { params: [jobId, limit] }\n );\n\n return rows.map(rowToLogEntry);\n } catch (err) {\n logger.error(`[cron-store] Failed to fetch logs for \"${jobId}\"`, { error: err });\n return [];\n }\n },\n\n async fetchJobStats(): Promise<Map<string, { totalRuns: number; totalFailures: number; lastRunAt?: string }>> {\n const stats = new Map<string, { totalRuns: number; totalFailures: number; lastRunAt?: string }>();\n try {\n const rows = await exec(`\n SELECT\n job_id,\n COUNT(*)::int AS total_runs,\n COUNT(*) FILTER (WHERE NOT success)::int AS total_failures,\n MAX(started_at) AS last_run_at\n FROM ${TABLE}\n GROUP BY job_id\n `);\n\n for (const row of rows) {\n stats.set(row.job_id as string, {\n totalRuns: row.total_runs as number,\n totalFailures: row.total_failures as number,\n lastRunAt: row.last_run_at ? new Date(row.last_run_at as string).toISOString() : undefined\n });\n }\n } catch (err) {\n logger.error(\"[cron-store] Failed to fetch job stats\", { error: err });\n }\n return stats;\n },\n\n async tryClaimRun(jobId: string, slot: string): Promise<boolean> {\n try {\n const rows = await exec(\n `INSERT INTO ${CLAIMS_TABLE} (job_id, slot)\n VALUES ($1, $2)\n ON CONFLICT (job_id, slot) DO NOTHING\n RETURNING job_id`,\n { params: [jobId, slot] }\n );\n return rows.length > 0;\n } catch (err) {\n if (isUniqueViolation(err)) {\n // Another instance won the race for this slot\n return false;\n }\n // Fail open: better to risk a duplicate run than to have a\n // broken claims table silently stop all cron execution.\n logger.warn(`[cron-store] Claim check failed for \"${jobId}\" — running uncoordinated`, { error: err });\n return true;\n }\n }\n };\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────\n\nfunction rowToLogEntry(row: Record<string, unknown>): CronJobLogEntry {\n return {\n jobId: row.job_id as string,\n startedAt: new Date(row.started_at as string).toISOString(),\n finishedAt: new Date(row.finished_at as string).toISOString(),\n durationMs: row.duration_ms as number,\n success: row.success as boolean,\n error: (row.error as string) ?? undefined,\n result: row.result ?? undefined,\n logs: Array.isArray(row.logs) ? row.logs : (row.logs ? (() => { try { return JSON.parse(row.logs as string); } catch { return []; } })() : []),\n manual: row.manual as boolean\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from "module";
|
|
2
|
+
import "process";
|
|
3
|
+
__createRequire(import.meta.url);
|
|
4
|
+
import { t as logger } from "./logger-DfvF_8r-.js";
|
|
5
|
+
//#region ../types/src/types/backend.ts
|
|
6
|
+
/**
|
|
7
|
+
* Type guard: does this admin support SQL operations?
|
|
8
|
+
* @group Admin
|
|
9
|
+
*/
|
|
10
|
+
function isSQLAdmin(admin) {
|
|
11
|
+
return !!admin && typeof admin.executeSql === "function";
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region ../common/src/util/internal-tables.ts
|
|
15
|
+
/**
|
|
16
|
+
* The tables Rebase creates for its own bookkeeping, and the SQL that keeps the
|
|
17
|
+
* end-user role away from them.
|
|
18
|
+
*
|
|
19
|
+
* ## Why this exists
|
|
20
|
+
*
|
|
21
|
+
* Authenticated requests run as {@link REBASE_USER_ROLE}, and the boot-time role
|
|
22
|
+
* provisioning grants that role `SELECT, INSERT, UPDATE, DELETE` on every table
|
|
23
|
+
* in the schemas a project uses — including `rebase`, because a project's own
|
|
24
|
+
* collections are allowed to live there (the scaffold puts `users` there). It
|
|
25
|
+
* also sets `ALTER DEFAULT PRIVILEGES`, so a table created *later* by the
|
|
26
|
+
* migrating role inherits the same grant.
|
|
27
|
+
*
|
|
28
|
+
* Every framework-internal table is created later: auth's tables come up during
|
|
29
|
+
* `initializeAuth`, `api_keys` during route mounting, `cron_logs` when the first
|
|
30
|
+
* job registers, `idempotency_keys` on the first request that carries a key. So
|
|
31
|
+
* they all inherited full DML for the end-user role — and none of them enables
|
|
32
|
+
* row-level security, because none of them is a collection with
|
|
33
|
+
* `securityRules`. Measured on a freshly provisioned database, `SET ROLE
|
|
34
|
+
* rebase_user` could read `rebase.refresh_tokens` (session token hashes),
|
|
35
|
+
* `rebase.mfa_factors` (`secret_encrypted`), `rebase.recovery_codes`, and
|
|
36
|
+
* `rebase.api_keys` (including its `admin` flag), and insert into
|
|
37
|
+
* `rebase.app_config`.
|
|
38
|
+
*
|
|
39
|
+
* Nothing routes a user-context query at those tables today, so this was not
|
|
40
|
+
* reachable over the API. That is the wrong thing to depend on: the documented
|
|
41
|
+
* model is that RLS is the authorization boundary, and these tables sat outside
|
|
42
|
+
* it. The boundary is now a privilege boundary instead — the role simply cannot
|
|
43
|
+
* address them.
|
|
44
|
+
*
|
|
45
|
+
* ## Why REVOKE rather than ENABLE ROW LEVEL SECURITY
|
|
46
|
+
*
|
|
47
|
+
* RLS with no policy denies every row, which is the same outcome, but it is the
|
|
48
|
+
* *weaker* statement: it leaves the grant in place, so a later policy — or a
|
|
49
|
+
* `FORCE` flag cleared by some future migration — reopens the table. There is no
|
|
50
|
+
* row of `refresh_tokens` any end user should ever reach, so the honest encoding
|
|
51
|
+
* is "this role has no privilege here at all". It also keeps the owner
|
|
52
|
+
* connection (which auth actually runs on) completely unaffected.
|
|
53
|
+
*
|
|
54
|
+
* ## Keeping it true
|
|
55
|
+
*
|
|
56
|
+
* `packages/rls-check` scans the `rebase` schema — it used to skip it as a
|
|
57
|
+
* "platform" schema — and its `rls-disabled` check fires on exactly the
|
|
58
|
+
* condition this module removes: RLS off *and* a DML grant to a reachable role.
|
|
59
|
+
* So a table added here without a revoke is caught by `pnpm rls:check`, not by
|
|
60
|
+
* someone re-reading this file.
|
|
61
|
+
*/
|
|
62
|
+
/**
|
|
63
|
+
* The Postgres role authenticated requests run as.
|
|
64
|
+
*
|
|
65
|
+
* Defined here rather than in the Postgres driver because both the driver (which
|
|
66
|
+
* provisions the role) and this module (which revokes on its behalf) need it,
|
|
67
|
+
* and a second spelling of a role name is a silent no-op waiting to happen.
|
|
68
|
+
*/
|
|
69
|
+
var REBASE_USER_ROLE = "rebase_user";
|
|
70
|
+
/** Postgres identifiers this module is willing to interpolate. */
|
|
71
|
+
var SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;
|
|
72
|
+
/**
|
|
73
|
+
* A single statement that takes every privilege on `schema.table` away from the
|
|
74
|
+
* end-user role.
|
|
75
|
+
*
|
|
76
|
+
* Wrapped in a `DO` block guarded on `pg_roles` for two reasons, both of which
|
|
77
|
+
* happen in practice:
|
|
78
|
+
*
|
|
79
|
+
* - the role does not exist when the connection is unprivileged (Rebase then
|
|
80
|
+
* relies on native RLS rather than a role switch), and a bare `REVOKE` on a
|
|
81
|
+
* missing role is an error, not a no-op;
|
|
82
|
+
* - the table may not exist yet — `cron_logs` never appears in a project with
|
|
83
|
+
* no cron jobs — and `to_regclass` returning NULL has to be tolerated too.
|
|
84
|
+
*
|
|
85
|
+
* One command, so it is safe on handles that speak the extended query protocol
|
|
86
|
+
* and reject multi-statement strings.
|
|
87
|
+
*/
|
|
88
|
+
function revokeInternalTableSql(schema, table) {
|
|
89
|
+
if (!SAFE_IDENTIFIER.test(schema)) throw new Error(`Refusing to build SQL with an unsafe schema name: ${JSON.stringify(schema)}`);
|
|
90
|
+
if (!SAFE_IDENTIFIER.test(table)) throw new Error(`Refusing to build SQL with an unsafe table name: ${JSON.stringify(table)}`);
|
|
91
|
+
const qualified = `"${schema}"."${table}"`;
|
|
92
|
+
return `
|
|
93
|
+
DO $rebase_revoke$
|
|
94
|
+
BEGIN
|
|
95
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}')
|
|
96
|
+
AND to_regclass('${qualified}') IS NOT NULL THEN
|
|
97
|
+
EXECUTE 'REVOKE ALL ON ${qualified} FROM ${REBASE_USER_ROLE}';
|
|
98
|
+
END IF;
|
|
99
|
+
END
|
|
100
|
+
$rebase_revoke$;
|
|
101
|
+
`.trim();
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/boot/ddl-bootstrap.ts
|
|
105
|
+
/**
|
|
106
|
+
* SQLSTATEs a *simultaneous boot* can raise from statements that are otherwise
|
|
107
|
+
* idempotent. Retrying is always the right answer for these: the second attempt
|
|
108
|
+
* finds the object present and does nothing.
|
|
109
|
+
*/
|
|
110
|
+
var CONCURRENT_DDL_SQLSTATES = /* @__PURE__ */ new Set([
|
|
111
|
+
"23505",
|
|
112
|
+
"42P06",
|
|
113
|
+
"42P07",
|
|
114
|
+
"42710",
|
|
115
|
+
"40P01"
|
|
116
|
+
]);
|
|
117
|
+
var DDL_RETRY_BASE_MS = 40;
|
|
118
|
+
/**
|
|
119
|
+
* Walk an error's `cause` chain, stopping at the first link `visit` accepts.
|
|
120
|
+
* Drizzle wraps the driver error, so nothing useful is ever on the top level.
|
|
121
|
+
*/
|
|
122
|
+
function hasInCauseChain(err, visit) {
|
|
123
|
+
let current = err;
|
|
124
|
+
for (let depth = 0; depth < 10 && current; depth++) {
|
|
125
|
+
if (typeof current !== "object") break;
|
|
126
|
+
const e = current;
|
|
127
|
+
if (visit(e)) return true;
|
|
128
|
+
current = e.cause;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Is this the loser of a race to create something that already exists?
|
|
134
|
+
*
|
|
135
|
+
* Deliberately narrow. A permission failure, an unreachable database or a typo
|
|
136
|
+
* in the DDL must surface on the first attempt rather than being retried four
|
|
137
|
+
* times and then reported as a race that never was.
|
|
138
|
+
*/
|
|
139
|
+
function isConcurrentDdlRace(err) {
|
|
140
|
+
return hasInCauseChain(err, (e) => typeof e.code === "string" && CONCURRENT_DDL_SQLSTATES.has(e.code) || typeof e.message === "string" && /already exists/i.test(e.message));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* @param exec the driver's SQL escape hatch
|
|
144
|
+
* @param scope log prefix identifying the caller, e.g. `"cron-store"`
|
|
145
|
+
*/
|
|
146
|
+
function createDdlBootstrapper(exec, scope) {
|
|
147
|
+
/** Jittered, so peers that collided once do not collide again in lockstep. */
|
|
148
|
+
const backoff = (attempt) => new Promise((resolve) => setTimeout(resolve, DDL_RETRY_BASE_MS * attempt * (1 + Math.random())));
|
|
149
|
+
const step = async (label, run) => {
|
|
150
|
+
try {
|
|
151
|
+
await run();
|
|
152
|
+
} catch (err) {
|
|
153
|
+
logger.error(`[${scope}] ${label} failed`, { error: err });
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
return {
|
|
157
|
+
step,
|
|
158
|
+
ensureObject(label, sqlText) {
|
|
159
|
+
return step(label, async () => {
|
|
160
|
+
for (let attempt = 1;; attempt++) try {
|
|
161
|
+
await exec(sqlText);
|
|
162
|
+
return;
|
|
163
|
+
} catch (err) {
|
|
164
|
+
if (!isConcurrentDdlRace(err) || attempt >= 4) throw err;
|
|
165
|
+
logger.debug(`[${scope}] Lost a create race for ${label} with another instance (attempt ${attempt}/4) — retrying`);
|
|
166
|
+
await backoff(attempt);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
async isReadable(table) {
|
|
171
|
+
try {
|
|
172
|
+
await exec(`SELECT 1 FROM ${table} WHERE false`);
|
|
173
|
+
return true;
|
|
174
|
+
} catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
//#endregion
|
|
181
|
+
export { isSQLAdmin as i, hasInCauseChain as n, revokeInternalTableSql as r, createDdlBootstrapper as t };
|
|
182
|
+
|
|
183
|
+
//# sourceMappingURL=ddl-bootstrap-BhXbTnBl.js.map
|