@rebasepro/server 0.14.1 → 0.14.2-canary.gca521e9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/logs-routes.d.ts +47 -8
- package/dist/{auth-BobZVd0j.js → auth-Cov94CTt.js} +2 -2
- package/dist/{auth-BobZVd0j.js.map → auth-Cov94CTt.js.map} +1 -1
- package/dist/{cron-store-CB1x-Ken.js → cron-store-DvabPKwA.js} +2 -2
- package/dist/{cron-store-CB1x-Ken.js.map → cron-store-DvabPKwA.js.map} +1 -1
- package/dist/index.es.js +70 -11
- package/dist/index.es.js.map +1 -1
- package/dist/{jobs-DR4SjGrD.js → jobs-DtxzEBEK.js} +2 -2
- package/dist/{jobs-DR4SjGrD.js.map → jobs-DtxzEBEK.js.map} +1 -1
- package/dist/logs-routes-CWBLQj2l.js +245 -0
- package/dist/logs-routes-CWBLQj2l.js.map +1 -0
- package/dist/{openapi-generator-DQeQ_q2f.js → openapi-generator-CkXIlxMM.js} +2 -2
- package/dist/{openapi-generator-DQeQ_q2f.js.map → openapi-generator-CkXIlxMM.js.map} +1 -1
- package/dist/{src-8XDWyDfR.js → src-Cx24hMDv.js} +44 -12
- package/dist/src-Cx24hMDv.js.map +1 -0
- package/package.json +5 -5
- package/dist/logs-routes-BYA72C_C.js +0 -100
- package/dist/logs-routes-BYA72C_C.js.map +0 -1
- package/dist/src-8XDWyDfR.js.map +0 -1
|
@@ -2,7 +2,7 @@ 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-
|
|
5
|
+
import "./src-Cx24hMDv.js";
|
|
6
6
|
import "./src-Cz9nMgUR.js";
|
|
7
7
|
import { n as createDdlBootstrapper, o as revokeInternalTableSql, r as hasInCauseChain, s as isSQLAdmin } from "./ddl-bootstrap-Cywoj8Ta.js";
|
|
8
8
|
import { t as logger } from "./logger-DfvF_8r-.js";
|
|
@@ -180,4 +180,4 @@ function rowToLogEntry(row) {
|
|
|
180
180
|
//#endregion
|
|
181
181
|
export { cron_store_exports as n, createCronStore as t };
|
|
182
182
|
|
|
183
|
-
//# sourceMappingURL=cron-store-
|
|
183
|
+
//# sourceMappingURL=cron-store-DvabPKwA.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cron-store-CB1x-Ken.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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"}
|
|
1
|
+
{"version":3,"file":"cron-store-DvabPKwA.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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"}
|
package/dist/index.es.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
import process from "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
|
-
import { A as GeoPoint, M as RebaseApiError, N as RebaseClientError, O as EntityReference, S as toSnakeCase, _ as fieldKeyForColumn, b as buildCompositeId, c as collectAllPages, d as normalizeOrderBy, f as serializeOrderBy, g as getEffectiveSecurityRules, h as resolveDataSource, j as Vector, k as EntityRelation, l as paginateFind, m as createDataSourceRegistry, n as buildSdkData, o as serializeFilter, p as CollectionRegistry, s as serializeLogicalCondition, t as buildRoutedRebaseData, u as resolveFindWindow, v as findRelation, x as resolvePrimaryKeys, y as resolveCollectionRelations } from "./src-
|
|
4
|
+
import { A as GeoPoint, M as RebaseApiError, N as RebaseClientError, O as EntityReference, S as toSnakeCase, _ as fieldKeyForColumn, b as buildCompositeId, c as collectAllPages, d as normalizeOrderBy, f as serializeOrderBy, g as getEffectiveSecurityRules, h as resolveDataSource, j as Vector, k as EntityRelation, l as paginateFind, m as createDataSourceRegistry, n as buildSdkData, o as serializeFilter, p as CollectionRegistry, s as serializeLogicalCondition, t as buildRoutedRebaseData, u as resolveFindWindow, v as findRelation, x as resolvePrimaryKeys, y as resolveCollectionRelations } from "./src-Cx24hMDv.js";
|
|
5
5
|
import { c as DEFAULT_DATA_SOURCE_KEY, i as getCollectionDataPath, m as toCanonicalOp, n as RLS_ROLES_SQL, o as isPostgresCollectionConfig, r as RLS_UID_SQL, t as RLS_JWT_SQL } from "./src-Cz9nMgUR.js";
|
|
6
6
|
import { n as ADMIN_PROPERTY_KEYS, t as ADMIN_COLLECTION_KEYS } from "./admin_block-H86dCPsj.js";
|
|
7
7
|
import { a as isDuplicateObjectRace, i as isConcurrentDdlRace, n as createDdlBootstrapper, o as revokeInternalTableSql, s as isSQLAdmin, t as CONCURRENT_DDL_SQLSTATES } from "./ddl-bootstrap-Cywoj8Ta.js";
|
|
8
8
|
import { C as canonicalStorageKey, D as findStorageSuffixCollision, E as DEFAULT_STORAGE_SOURCE_KEY, O as normalizeStorageSources, S as canonicalStorageId, b as InvalidStorageKeyError, c as getJwks, d as hasAsymmetricSigningKey, i as generateDownloadToken, k as storageEnvSuffix, n as configureJwt, p as isJwtConfigured, v as normalizePemFromEnv, x as canonicalStorageBucket, y as InvalidStorageBucketError } from "./jwt-VJyXTdQQ.js";
|
|
9
9
|
import { n as createContractRoutes } from "./contract-routes-Bet-eCNJ.js";
|
|
10
|
-
import { $ as extractUserFromToken, A as createDataRateLimiter, B as generateSecurePassword, C as createBuiltinAuthAdapter, Ct as isPublicStoragePath, D as string, E as object, F as resolveEmailLinkBase, G as getWelcomeEmailTemplate, H as getMagicLinkTemplate, I as resolveAuthHooks, J as html, K as RawHtml, L as hashPassword, M as SMTPEmailService, N as createEmailService, O as _coercedNumber, P as assertEmailLinkBases, Q as createRequireAuth, R as validatePasswordStrength, S as createJwksRoutes, St as PUBLIC_STORAGE_PREFIX, T as _enum, U as getPasswordResetTemplate, V as getEmailVerificationTemplate, W as getUserInvitationTemplate, X as createAdapterAuthMiddleware, Y as raw, Z as createAuthMiddleware, _ as createLinkedinProvider, _t as orderByEntriesToTuples, a as createSpotifyProvider, at as requireAuth, b as pkceTokenParams, bt as parseQueryOptions, c as createGitLabProvider, ct as createStorageApiKeyGuard, d as createFacebookProvider, dt as extractBearerToken, et as fileTokenAuth, f as createAppleProvider, ft as safeCompare, g as createGitHubProvider, gt as isOperationAllowed, h as verifyOidcIdToken, ht as httpMethodToOperation, i as createApiKeyStore, it as requireAdmin, j as MemoryRateLimitStore, k as DEFAULT_FUNCTIONS_ANONYMOUS_LIMIT, l as createDiscordProvider, lt as isApiKeyToken, m as tryVerifyOidcIdToken, mt as scopeDataDriver, n as createCustomAuthAdapter, nt as publicObjectAuth, o as createSlackProvider, ot as createApiKeyPreAuth, p as createMicrosoftProvider, pt as SERVICE_IDENTITY, q as escapeHtml, r as createApiKeyRoutes, rt as queryTokenAuth, s as createBitbucketProvider, st as createFunctionApiKeyGuard, tt as optionalAuth, u as createTwitterProvider, ut as validateApiKey, v as createGoogleProvider, vt as parseAggregateSelect, w as ZodNumber, x as providerVerifiedEmail, xt as resolveListLimitParam, y as oauthCodeFlowSchema, yt as parseGroupBy, z as verifyPassword } from "./auth-
|
|
10
|
+
import { $ as extractUserFromToken, A as createDataRateLimiter, B as generateSecurePassword, C as createBuiltinAuthAdapter, Ct as isPublicStoragePath, D as string, E as object, F as resolveEmailLinkBase, G as getWelcomeEmailTemplate, H as getMagicLinkTemplate, I as resolveAuthHooks, J as html, K as RawHtml, L as hashPassword, M as SMTPEmailService, N as createEmailService, O as _coercedNumber, P as assertEmailLinkBases, Q as createRequireAuth, R as validatePasswordStrength, S as createJwksRoutes, St as PUBLIC_STORAGE_PREFIX, T as _enum, U as getPasswordResetTemplate, V as getEmailVerificationTemplate, W as getUserInvitationTemplate, X as createAdapterAuthMiddleware, Y as raw, Z as createAuthMiddleware, _ as createLinkedinProvider, _t as orderByEntriesToTuples, a as createSpotifyProvider, at as requireAuth, b as pkceTokenParams, bt as parseQueryOptions, c as createGitLabProvider, ct as createStorageApiKeyGuard, d as createFacebookProvider, dt as extractBearerToken, et as fileTokenAuth, f as createAppleProvider, ft as safeCompare, g as createGitHubProvider, gt as isOperationAllowed, h as verifyOidcIdToken, ht as httpMethodToOperation, i as createApiKeyStore, it as requireAdmin, j as MemoryRateLimitStore, k as DEFAULT_FUNCTIONS_ANONYMOUS_LIMIT, l as createDiscordProvider, lt as isApiKeyToken, m as tryVerifyOidcIdToken, mt as scopeDataDriver, n as createCustomAuthAdapter, nt as publicObjectAuth, o as createSlackProvider, ot as createApiKeyPreAuth, p as createMicrosoftProvider, pt as SERVICE_IDENTITY, q as escapeHtml, r as createApiKeyRoutes, rt as queryTokenAuth, s as createBitbucketProvider, st as createFunctionApiKeyGuard, tt as optionalAuth, u as createTwitterProvider, ut as validateApiKey, v as createGoogleProvider, vt as parseAggregateSelect, w as ZodNumber, x as providerVerifiedEmail, xt as resolveListLimitParam, y as oauthCodeFlowSchema, yt as parseGroupBy, z as verifyPassword } from "./auth-Cov94CTt.js";
|
|
11
11
|
import { t as logger } from "./logger-DfvF_8r-.js";
|
|
12
12
|
import { n as errorHandler, t as ApiError } from "./errors-EBYiaJ2E.js";
|
|
13
|
-
import { t as logMiddleware } from "./logs-routes-
|
|
13
|
+
import { t as logMiddleware } from "./logs-routes-CWBLQj2l.js";
|
|
14
14
|
import { n as DOCUMENT_MIME_TYPES, r as IMAGE_MIME_TYPES, t as DEFAULT_MAX_FILE_SIZE } from "./types-DSnOC4mF.js";
|
|
15
15
|
import { t as S3StorageController } from "./S3StorageController-B2EDXXMH.js";
|
|
16
16
|
import { t as GCSStorageController } from "./GCSStorageController-BEmDYFKc.js";
|
|
@@ -19,9 +19,9 @@ import { t as createFunctionRoutes } from "./function-routes-Btcez1T-.js";
|
|
|
19
19
|
import { n as loadCronJobsFromDirectory, r as loadCronJobsWithDiagnostics } from "./cron-loader-YhhQeVBM.js";
|
|
20
20
|
import { r as validateCronExpression, t as CronScheduler } from "./cron-scheduler-DIpYBmZP.js";
|
|
21
21
|
import { t as createCronRoutes } from "./cron-routes-maM_RlUu.js";
|
|
22
|
-
import { t as createCronStore } from "./cron-store-
|
|
22
|
+
import { t as createCronStore } from "./cron-store-DvabPKwA.js";
|
|
23
23
|
import { a as parseBackupTimestamp, i as parseBackupDestination, n as createBackupRoutes, o as readBackupBytes, r as listBackupObjects } from "./backup-C6ljYVTp.js";
|
|
24
|
-
import { i as createJobStore, n as createJobQueue, r as defaultBackoff } from "./jobs-
|
|
24
|
+
import { i as createJobStore, n as createJobQueue, r as defaultBackoff } from "./jobs-DtxzEBEK.js";
|
|
25
25
|
import { t as FunctionSelectionError } from "./selection-_z6TM1DB.js";
|
|
26
26
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
27
27
|
import * as fs$2 from "fs";
|
|
@@ -2306,6 +2306,7 @@ function responseCompression() {
|
|
|
2306
2306
|
if (!vary) c.res.headers.set("Vary", "Accept-Encoding");
|
|
2307
2307
|
else if (!/\baccept-encoding\b/i.test(vary)) c.res.headers.append("Vary", "Accept-Encoding");
|
|
2308
2308
|
if (c.res.status === 206 || c.res.headers.has("Content-Range")) return;
|
|
2309
|
+
if (c.res.headers.get("Content-Type")?.startsWith("text/event-stream")) return;
|
|
2309
2310
|
await gzip(c, async () => {});
|
|
2310
2311
|
};
|
|
2311
2312
|
}
|
|
@@ -2398,7 +2399,7 @@ function configureMiddlewares(app, basePath, isProduction, config) {
|
|
|
2398
2399
|
}
|
|
2399
2400
|
if (!config.corsHandled && !process.env.CORS_ORIGINS && !process.env.FRONTEND_URL) logger.warn((isProduction ? "[PRODUCTION] " : "") + "No CORS configuration detected (CORS_ORIGINS / FRONTEND_URL not set). If your app does not install its own CORS middleware, the API may accept requests from any origin. Set CORS_ORIGINS to restrict access.");
|
|
2400
2401
|
app.use(`${basePath}/*`, requestLogger());
|
|
2401
|
-
app.use(`${basePath}/*`, logMiddleware());
|
|
2402
|
+
app.use(`${basePath}/*`, logMiddleware({ ignorePaths: [`${basePath}/logs/stream`] }));
|
|
2402
2403
|
}
|
|
2403
2404
|
//#endregion
|
|
2404
2405
|
//#region src/storage/LocalStorageController.ts
|
|
@@ -3946,7 +3947,7 @@ function assertStorageAccessControlConfigured(state, isProduction) {
|
|
|
3946
3947
|
*/
|
|
3947
3948
|
async function mountOpenApiDocs(app, basePath, enableSwagger, serverCollections, requireAuth) {
|
|
3948
3949
|
if (enableSwagger === false || serverCollections.length === 0) return;
|
|
3949
|
-
const { generateOpenApiSpec } = await import("./openapi-generator-
|
|
3950
|
+
const { generateOpenApiSpec } = await import("./openapi-generator-CkXIlxMM.js");
|
|
3950
3951
|
app.get(`${basePath}/docs`, (c) => {
|
|
3951
3952
|
const spec = generateOpenApiSpec(serverCollections, {
|
|
3952
3953
|
basePath,
|
|
@@ -10608,7 +10609,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
10608
10609
|
]) {
|
|
10609
10610
|
const providerConfig = safeAuthConfig[key];
|
|
10610
10611
|
if (providerConfig && requiredFields.every((f) => Boolean(providerConfig[f]))) {
|
|
10611
|
-
const createFn = (await import("./auth-
|
|
10612
|
+
const createFn = (await import("./auth-Cov94CTt.js").then((n) => n.t))[factory];
|
|
10612
10613
|
oauthProviders.push(createFn(providerConfig));
|
|
10613
10614
|
}
|
|
10614
10615
|
}
|
|
@@ -10948,7 +10949,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
10948
10949
|
const { loadCronJobsWithDiagnostics } = await import("./cron-loader-YhhQeVBM.js").then((n) => n.t);
|
|
10949
10950
|
const { CronScheduler } = await import("./cron-scheduler-DIpYBmZP.js").then((n) => n.n);
|
|
10950
10951
|
const { createCronRoutes } = await import("./cron-routes-maM_RlUu.js").then((n) => n.n);
|
|
10951
|
-
const { createCronStore } = await import("./cron-store-
|
|
10952
|
+
const { createCronStore } = await import("./cron-store-DvabPKwA.js").then((n) => n.n);
|
|
10952
10953
|
const { jobs: loadedCronJobs, problems: cronProblems } = await loadCronJobsWithDiagnostics(config.cronsDir);
|
|
10953
10954
|
cronScheduler = new CronScheduler();
|
|
10954
10955
|
cronScheduler.setClient(serverSingleton);
|
|
@@ -10978,7 +10979,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
10978
10979
|
}
|
|
10979
10980
|
let jobQueue;
|
|
10980
10981
|
if (config.jobs?.enabled) {
|
|
10981
|
-
const { createJobStore, createJobQueue } = await import("./jobs-
|
|
10982
|
+
const { createJobStore, createJobQueue } = await import("./jobs-DtxzEBEK.js").then((n) => n.t);
|
|
10982
10983
|
const store = createJobStore(defaultDriver);
|
|
10983
10984
|
if (store) {
|
|
10984
10985
|
await store.ensureTable();
|
|
@@ -11004,7 +11005,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
11004
11005
|
logger.debug("Backup admin routes mounted", { path: `${basePath}/admin/backups` });
|
|
11005
11006
|
}
|
|
11006
11007
|
if (surfaces.admin) {
|
|
11007
|
-
const { default: logsRoutes } = await import("./logs-routes-
|
|
11008
|
+
const { default: logsRoutes } = await import("./logs-routes-CWBLQj2l.js").then((n) => n.n);
|
|
11008
11009
|
const logsRouter = new Hono();
|
|
11009
11010
|
applyAdminGate(logsRouter, "Logs");
|
|
11010
11011
|
logsRouter.route("/", logsRoutes);
|
|
@@ -11967,6 +11968,63 @@ function isUnderPath(requestPath, prefix) {
|
|
|
11967
11968
|
return requestPath === trimmed || requestPath.startsWith(`${trimmed}/`);
|
|
11968
11969
|
}
|
|
11969
11970
|
/**
|
|
11971
|
+
* Extensions a build emits, never a client-side route.
|
|
11972
|
+
*
|
|
11973
|
+
* An allowlist rather than "does the last segment contain a dot": entity routes
|
|
11974
|
+
* carry ids, and an id is often an email or a dotted slug, so a generic
|
|
11975
|
+
* extension test answers `/c/users/ada@example.com` with a 404.
|
|
11976
|
+
*/
|
|
11977
|
+
var ASSET_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
11978
|
+
"js",
|
|
11979
|
+
"mjs",
|
|
11980
|
+
"cjs",
|
|
11981
|
+
"css",
|
|
11982
|
+
"map",
|
|
11983
|
+
"wasm",
|
|
11984
|
+
"png",
|
|
11985
|
+
"jpg",
|
|
11986
|
+
"jpeg",
|
|
11987
|
+
"gif",
|
|
11988
|
+
"svg",
|
|
11989
|
+
"webp",
|
|
11990
|
+
"avif",
|
|
11991
|
+
"ico",
|
|
11992
|
+
"bmp",
|
|
11993
|
+
"woff",
|
|
11994
|
+
"woff2",
|
|
11995
|
+
"ttf",
|
|
11996
|
+
"otf",
|
|
11997
|
+
"eot",
|
|
11998
|
+
"mp3",
|
|
11999
|
+
"mp4",
|
|
12000
|
+
"webm",
|
|
12001
|
+
"ogg",
|
|
12002
|
+
"wav"
|
|
12003
|
+
]);
|
|
12004
|
+
/**
|
|
12005
|
+
* Is this request for a build artifact rather than a client-side route?
|
|
12006
|
+
*
|
|
12007
|
+
* It matters because of what the fallback would otherwise do. A tab opened
|
|
12008
|
+
* before a deploy still holds the previous entry chunk, so the first lazy view
|
|
12009
|
+
* it opens fetches a content hash that no longer exists; answering that with
|
|
12010
|
+
* index.html hands the browser HTML where it asked for a module, and the error
|
|
12011
|
+
* it reports ("Failed to fetch dynamically imported module: …") names the chunk
|
|
12012
|
+
* and nothing else — it reads as a broken build, not as a stale tab, and a 404
|
|
12013
|
+
* would have said so. Serving HTML for a missing asset also poisons any cache
|
|
12014
|
+
* sitting in front of the origin with an HTML body under a .js URL.
|
|
12015
|
+
*
|
|
12016
|
+
* `Sec-Fetch-Dest` catches the extensionless cases browsers still label for us;
|
|
12017
|
+
* a missing header (curl, a health probe, an old browser) falls back to the
|
|
12018
|
+
* extension, and so keeps answering navigations with the app.
|
|
12019
|
+
*/
|
|
12020
|
+
function isAssetRequest(requestPath, destination) {
|
|
12021
|
+
if (destination && destination !== "document" && destination !== "iframe" && destination !== "frame") return true;
|
|
12022
|
+
const lastSegment = requestPath.slice(requestPath.lastIndexOf("/") + 1);
|
|
12023
|
+
const dot = lastSegment.lastIndexOf(".");
|
|
12024
|
+
if (dot <= 0) return false;
|
|
12025
|
+
return ASSET_EXTENSIONS.has(lastSegment.slice(dot + 1).toLowerCase());
|
|
12026
|
+
}
|
|
12027
|
+
/**
|
|
11970
12028
|
* Serve a Single Page Application from an Hono app.
|
|
11971
12029
|
*
|
|
11972
12030
|
* @internal Not part of the stable public API. Exported only because the
|
|
@@ -12001,6 +12059,7 @@ function serveSPA(app, config) {
|
|
|
12001
12059
|
let cachedHtml = null;
|
|
12002
12060
|
app.get(scope, async (c, next) => {
|
|
12003
12061
|
if (allExcludePaths.some((p) => isUnderPath(c.req.path, p))) return next();
|
|
12062
|
+
if (isAssetRequest(c.req.path, c.req.header("sec-fetch-dest"))) return c.text("Not found", 404);
|
|
12004
12063
|
const indexPath = path$2.join(frontendPath, indexFile);
|
|
12005
12064
|
if (!cachedHtml) try {
|
|
12006
12065
|
cachedHtml = await fsp.readFile(indexPath, "utf-8");
|