@rebasepro/server 0.14.1-canary.g7e666eb → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/rest/query-parser.d.ts +22 -0
- package/dist/auth/index.d.ts +3 -1
- package/dist/auth/jwks-routes.d.ts +17 -0
- package/dist/auth/jwt-keys.d.ts +108 -0
- package/dist/auth/jwt.d.ts +32 -0
- package/dist/{auth-BQcdhMBL.js → auth-BobZVd0j.js} +82 -6
- package/dist/auth-BobZVd0j.js.map +1 -0
- package/dist/boot/boot.d.ts +36 -50
- package/dist/boot/ddl-bootstrap.d.ts +15 -0
- package/dist/boot/env.d.ts +20 -0
- package/dist/boot/provision.d.ts +182 -0
- package/dist/boot/role.d.ts +88 -0
- package/dist/{cron-store-D5dUNviq.js → cron-store-CB1x-Ken.js} +2 -2
- package/dist/{cron-store-D5dUNviq.js.map → cron-store-CB1x-Ken.js.map} +1 -1
- package/dist/{ddl-bootstrap-BhXbTnBl.js → ddl-bootstrap-Cywoj8Ta.js} +40 -2
- package/dist/{ddl-bootstrap-BhXbTnBl.js.map → ddl-bootstrap-Cywoj8Ta.js.map} +1 -1
- package/dist/env.d.ts +2 -0
- package/dist/functions/proxy.d.ts +41 -0
- package/dist/functions/selection.d.ts +45 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.es.js +815 -351
- package/dist/index.es.js.map +1 -1
- package/dist/init/shutdown.d.ts +4 -0
- package/dist/init/surfaces.d.ts +79 -0
- package/dist/init.d.ts +121 -1
- package/dist/jobs/index.d.ts +5 -0
- package/dist/jobs/job-queue.d.ts +14 -0
- package/dist/jobs/job-store.d.ts +22 -0
- package/dist/jobs/types.d.ts +125 -0
- package/dist/jobs-DR4SjGrD.js +326 -0
- package/dist/jobs-DR4SjGrD.js.map +1 -0
- package/dist/{jwt-CYGFT0ih.js → jwt-VJyXTdQQ.js} +152 -6
- package/dist/jwt-VJyXTdQQ.js.map +1 -0
- package/dist/{openapi-generator-BWL8F2La.js → openapi-generator-DQeQ_q2f.js} +45 -1
- package/dist/openapi-generator-DQeQ_q2f.js.map +1 -0
- package/dist/proxy-Bj5DVllb.js +139 -0
- package/dist/proxy-Bj5DVllb.js.map +1 -0
- package/dist/selection-_z6TM1DB.js +64 -0
- package/dist/selection-_z6TM1DB.js.map +1 -0
- package/dist/services/webhook-service.d.ts +43 -5
- package/dist/src-8XDWyDfR.js.map +1 -1
- package/package.json +5 -5
- package/dist/auth-BQcdhMBL.js.map +0 -1
- package/dist/jwt-CYGFT0ih.js.map +0 -1
- package/dist/openapi-generator-BWL8F2La.js.map +0 -1
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from "module";
|
|
2
|
+
import process from "process";
|
|
3
|
+
__createRequire(import.meta.url);
|
|
4
|
+
import { n as __exportAll } from "./rolldown-runtime-DSJWtz9O.js";
|
|
5
|
+
import "./src-8XDWyDfR.js";
|
|
6
|
+
import "./src-Cz9nMgUR.js";
|
|
7
|
+
import { n as createDdlBootstrapper, o as revokeInternalTableSql, r as hasInCauseChain, s as isSQLAdmin } from "./ddl-bootstrap-Cywoj8Ta.js";
|
|
8
|
+
import { t as logger } from "./logger-DfvF_8r-.js";
|
|
9
|
+
import { randomUUID } from "crypto";
|
|
10
|
+
//#region src/jobs/job-store.ts
|
|
11
|
+
/**
|
|
12
|
+
* The queue's storage, as SQL against `rebase.jobs`.
|
|
13
|
+
*
|
|
14
|
+
* Split from the worker for the same reason `cron-store` is split from
|
|
15
|
+
* `cron-scheduler`: the interesting parts here are three statements that have
|
|
16
|
+
* to be exactly right under concurrency, and they are much easier to reason
|
|
17
|
+
* about — and to test — away from a polling loop.
|
|
18
|
+
*/
|
|
19
|
+
var TABLE = "rebase.jobs";
|
|
20
|
+
/** How long finished jobs are kept before the boot sweep removes them. */
|
|
21
|
+
var SUCCEEDED_RETENTION_DAYS = 3;
|
|
22
|
+
/**
|
|
23
|
+
* Failures outlive successes by a lot. A dead-lettered job is evidence, and the
|
|
24
|
+
* person who needs it is usually looking on Monday for something that happened
|
|
25
|
+
* on Friday night.
|
|
26
|
+
*/
|
|
27
|
+
var FAILED_RETENTION_DAYS = 30;
|
|
28
|
+
function toRecord(row) {
|
|
29
|
+
return {
|
|
30
|
+
id: row.id,
|
|
31
|
+
task: row.task,
|
|
32
|
+
payload: row.payload,
|
|
33
|
+
status: row.status,
|
|
34
|
+
runAt: new Date(row.run_at).toISOString(),
|
|
35
|
+
attempts: Number(row.attempts),
|
|
36
|
+
maxAttempts: Number(row.max_attempts),
|
|
37
|
+
lastError: row.last_error,
|
|
38
|
+
createdAt: new Date(row.created_at).toISOString(),
|
|
39
|
+
updatedAt: new Date(row.updated_at).toISOString()
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Same rule as `cron-store`: match the SQLSTATE, never the message. */
|
|
43
|
+
function isUniqueViolation(err) {
|
|
44
|
+
return hasInCauseChain(err, (e) => e.code === "23505");
|
|
45
|
+
}
|
|
46
|
+
function createJobStore(driver) {
|
|
47
|
+
const admin = driver.admin;
|
|
48
|
+
if (!isSQLAdmin(admin)) {
|
|
49
|
+
logger.warn("⚠️ [jobs] DataDriver does not support SQL admin — the durable job queue is unavailable. Work that would have been queued runs inline instead.");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const execRaw = (sqlText, options) => admin.executeSql(sqlText, options?.params ? { params: options.params } : void 0);
|
|
53
|
+
const exec = (sqlText, params) => execRaw(sqlText, params ? { params } : void 0);
|
|
54
|
+
const ddl = createDdlBootstrapper(execRaw, "jobs");
|
|
55
|
+
return {
|
|
56
|
+
async ensureTable() {
|
|
57
|
+
await ddl.ensureObject("Creating schema rebase", "CREATE SCHEMA IF NOT EXISTS rebase");
|
|
58
|
+
await ddl.ensureObject(`Creating ${TABLE}`, `
|
|
59
|
+
CREATE TABLE IF NOT EXISTS ${TABLE} (
|
|
60
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
61
|
+
task TEXT NOT NULL,
|
|
62
|
+
payload JSONB,
|
|
63
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
64
|
+
run_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
65
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
66
|
+
max_attempts INTEGER NOT NULL DEFAULT 3,
|
|
67
|
+
locked_at TIMESTAMPTZ,
|
|
68
|
+
locked_by TEXT,
|
|
69
|
+
idempotency_key TEXT,
|
|
70
|
+
last_error TEXT,
|
|
71
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
72
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
73
|
+
)
|
|
74
|
+
`);
|
|
75
|
+
await ddl.ensureObject("Creating idx_jobs_runnable", `
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_runnable
|
|
77
|
+
ON ${TABLE}(run_at, created_at)
|
|
78
|
+
WHERE status = 'pending'
|
|
79
|
+
`);
|
|
80
|
+
await ddl.ensureObject("Creating idx_jobs_running", `
|
|
81
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_running
|
|
82
|
+
ON ${TABLE}(locked_at)
|
|
83
|
+
WHERE status = 'running'
|
|
84
|
+
`);
|
|
85
|
+
await ddl.ensureObject("Creating idx_jobs_idempotency", `
|
|
86
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_jobs_idempotency
|
|
87
|
+
ON ${TABLE}(idempotency_key)
|
|
88
|
+
WHERE idempotency_key IS NOT NULL AND status IN ('pending', 'running')
|
|
89
|
+
`);
|
|
90
|
+
if (await ddl.isReadable(TABLE)) {
|
|
91
|
+
await ddl.step("Job retention sweep", async () => {
|
|
92
|
+
await exec(`DELETE FROM ${TABLE}
|
|
93
|
+
WHERE (status = 'succeeded' AND updated_at < now() - make_interval(days => $1))
|
|
94
|
+
OR (status = 'failed' AND updated_at < now() - make_interval(days => $2))`, [SUCCEEDED_RETENTION_DAYS, FAILED_RETENTION_DAYS]);
|
|
95
|
+
});
|
|
96
|
+
await ddl.step("Revoking end-user access to jobs", () => exec(revokeInternalTableSql("rebase", "jobs")));
|
|
97
|
+
logger.info("✅ Job queue table ready");
|
|
98
|
+
} else logger.error(`❌ [jobs] ${TABLE} is unavailable — nothing can be queued and nothing queued earlier will run. Callers fall back to running the work inline.`);
|
|
99
|
+
},
|
|
100
|
+
async insert(job) {
|
|
101
|
+
try {
|
|
102
|
+
return (await exec(`INSERT INTO ${TABLE} (task, payload, run_at, max_attempts, idempotency_key)
|
|
103
|
+
VALUES ($1, $2::jsonb, $3, $4, $5)
|
|
104
|
+
RETURNING id`, [
|
|
105
|
+
job.task,
|
|
106
|
+
JSON.stringify(job.payload ?? null),
|
|
107
|
+
job.runAt.toISOString(),
|
|
108
|
+
job.maxAttempts,
|
|
109
|
+
job.idempotencyKey ?? null
|
|
110
|
+
]))?.[0]?.id ?? null;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (isUniqueViolation(error)) return null;
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
async claim(limit, workerId) {
|
|
117
|
+
return (await exec(`UPDATE ${TABLE} SET
|
|
118
|
+
status = 'running',
|
|
119
|
+
attempts = attempts + 1,
|
|
120
|
+
locked_at = now(),
|
|
121
|
+
locked_by = $2,
|
|
122
|
+
updated_at = now()
|
|
123
|
+
WHERE id IN (
|
|
124
|
+
SELECT id FROM ${TABLE}
|
|
125
|
+
WHERE status = 'pending' AND run_at <= now()
|
|
126
|
+
ORDER BY run_at, created_at
|
|
127
|
+
LIMIT $1
|
|
128
|
+
FOR UPDATE SKIP LOCKED
|
|
129
|
+
)
|
|
130
|
+
RETURNING *`, [limit, workerId])).map(toRecord);
|
|
131
|
+
},
|
|
132
|
+
async complete(id) {
|
|
133
|
+
await exec(`UPDATE ${TABLE} SET status = 'succeeded', locked_at = NULL, locked_by = NULL,
|
|
134
|
+
last_error = NULL, updated_at = now()
|
|
135
|
+
WHERE id = $1`, [id]);
|
|
136
|
+
},
|
|
137
|
+
async fail(id, error, retryAt) {
|
|
138
|
+
if (retryAt) {
|
|
139
|
+
await exec(`UPDATE ${TABLE} SET status = 'pending', run_at = $2, locked_at = NULL,
|
|
140
|
+
locked_by = NULL, last_error = $3, updated_at = now()
|
|
141
|
+
WHERE id = $1`, [
|
|
142
|
+
id,
|
|
143
|
+
retryAt.toISOString(),
|
|
144
|
+
error
|
|
145
|
+
]);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
await exec(`UPDATE ${TABLE} SET status = 'failed', locked_at = NULL, locked_by = NULL,
|
|
149
|
+
last_error = $2, updated_at = now()
|
|
150
|
+
WHERE id = $1`, [id, error]);
|
|
151
|
+
},
|
|
152
|
+
async reapExpired(visibilityTimeoutMs) {
|
|
153
|
+
const seconds = Math.max(1, Math.round(visibilityTimeoutMs / 1e3));
|
|
154
|
+
const revived = await exec(`UPDATE ${TABLE} SET status = 'pending', locked_at = NULL, locked_by = NULL,
|
|
155
|
+
last_error = 'Worker stopped responding; the job was reclaimed', updated_at = now()
|
|
156
|
+
WHERE status = 'running'
|
|
157
|
+
AND locked_at < now() - make_interval(secs => $1)
|
|
158
|
+
AND attempts < max_attempts
|
|
159
|
+
RETURNING id`, [seconds]);
|
|
160
|
+
const buried = await exec(`UPDATE ${TABLE} SET status = 'failed', locked_at = NULL, locked_by = NULL,
|
|
161
|
+
last_error = 'Worker stopped responding on the final attempt', updated_at = now()
|
|
162
|
+
WHERE status = 'running'
|
|
163
|
+
AND locked_at < now() - make_interval(secs => $1)
|
|
164
|
+
AND attempts >= max_attempts
|
|
165
|
+
RETURNING id`, [seconds]);
|
|
166
|
+
const count = (revived?.length ?? 0) + (buried?.length ?? 0);
|
|
167
|
+
if (count > 0) logger.warn(`[jobs] Reclaimed ${count} job(s) from a worker that stopped responding (${revived?.length ?? 0} retryable, ${buried?.length ?? 0} dead-lettered)`);
|
|
168
|
+
return count;
|
|
169
|
+
},
|
|
170
|
+
async fetch(id) {
|
|
171
|
+
const row = (await exec(`SELECT * FROM ${TABLE} WHERE id = $1`, [id]))[0];
|
|
172
|
+
return row ? toRecord(row) : null;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/jobs/job-queue.ts
|
|
178
|
+
/**
|
|
179
|
+
* The worker: claim, run, record, repeat.
|
|
180
|
+
*
|
|
181
|
+
* Everything difficult about running jobs concurrently is in `job-store.ts`,
|
|
182
|
+
* where one `UPDATE … FOR UPDATE SKIP LOCKED` does the arbitration. What is
|
|
183
|
+
* left here is a loop and the decisions around a handler that throws.
|
|
184
|
+
*/
|
|
185
|
+
var DEFAULT_CONCURRENCY = 5;
|
|
186
|
+
var DEFAULT_POLL_INTERVAL_MS = 2e3;
|
|
187
|
+
var DEFAULT_VISIBILITY_TIMEOUT_MS = 5 * 6e4;
|
|
188
|
+
var DEFAULT_MAX_ATTEMPTS = 3;
|
|
189
|
+
/** 1s, 5s, 25s, 125s … capped at an hour. */
|
|
190
|
+
function defaultBackoff(attempt) {
|
|
191
|
+
return Math.min(1e3 * Math.pow(5, Math.max(0, attempt - 1)), 60 * 6e4);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* How often the reaper runs, relative to the visibility timeout.
|
|
195
|
+
*
|
|
196
|
+
* A quarter of it, so a stranded job waits at most 1.25× the timeout rather
|
|
197
|
+
* than 2× — and so the sweep is not itself a per-poll query against a table
|
|
198
|
+
* whose interesting rows are, almost always, none.
|
|
199
|
+
*/
|
|
200
|
+
var REAP_INTERVAL_FACTOR = .25;
|
|
201
|
+
function createJobQueue(store, options = {}) {
|
|
202
|
+
const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
|
|
203
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
204
|
+
const visibilityTimeoutMs = options.visibilityTimeoutMs ?? DEFAULT_VISIBILITY_TIMEOUT_MS;
|
|
205
|
+
const defaultMaxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
206
|
+
const backoff = options.backoff ?? defaultBackoff;
|
|
207
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
208
|
+
for (const [task, handler] of Object.entries(options.tasks ?? {})) handlers.set(task, handler);
|
|
209
|
+
const workerId = `${process.pid}-${randomUUID().slice(0, 8)}`;
|
|
210
|
+
let timer = null;
|
|
211
|
+
let running = false;
|
|
212
|
+
let draining = false;
|
|
213
|
+
/** Resolves when the in-flight poll finishes, so `stop()` can wait for it. */
|
|
214
|
+
let inFlight = Promise.resolve();
|
|
215
|
+
let lastReapAt = 0;
|
|
216
|
+
async function runJob(job) {
|
|
217
|
+
const handler = handlers.get(job.task);
|
|
218
|
+
if (!handler) {
|
|
219
|
+
logger.warn(`[jobs] No handler registered for task "${job.task}" — returning the job to the queue`);
|
|
220
|
+
await store.fail(job.id, `No handler registered for task "${job.task}"`, job.attempts < job.maxAttempts ? new Date(Date.now() + backoff(job.attempts)) : null);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
await handler({
|
|
225
|
+
id: job.id,
|
|
226
|
+
task: job.task,
|
|
227
|
+
payload: job.payload,
|
|
228
|
+
attempt: job.attempts,
|
|
229
|
+
maxAttempts: job.maxAttempts
|
|
230
|
+
});
|
|
231
|
+
await store.complete(job.id);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
234
|
+
const willRetry = job.attempts < job.maxAttempts;
|
|
235
|
+
await store.fail(job.id, message.slice(0, 4e3), willRetry ? new Date(Date.now() + backoff(job.attempts)) : null);
|
|
236
|
+
if (willRetry) logger.warn(`[jobs] "${job.task}" failed on attempt ${job.attempts}/${job.maxAttempts}; retrying`, { jobId: job.id });
|
|
237
|
+
else logger.error(`[jobs] "${job.task}" failed permanently after ${job.attempts} attempts`, {
|
|
238
|
+
jobId: job.id,
|
|
239
|
+
error: message
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async function poll() {
|
|
244
|
+
const now = Date.now();
|
|
245
|
+
if (now - lastReapAt > visibilityTimeoutMs * REAP_INTERVAL_FACTOR) {
|
|
246
|
+
lastReapAt = now;
|
|
247
|
+
try {
|
|
248
|
+
await store.reapExpired(visibilityTimeoutMs);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
logger.error("[jobs] Failed to reclaim expired jobs", { error });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const jobs = await store.claim(concurrency, workerId);
|
|
254
|
+
if (jobs.length === 0) return 0;
|
|
255
|
+
await Promise.allSettled(jobs.map(runJob));
|
|
256
|
+
return jobs.length;
|
|
257
|
+
}
|
|
258
|
+
function schedule(delayMs) {
|
|
259
|
+
if (!running) return;
|
|
260
|
+
timer = setTimeout(() => {
|
|
261
|
+
tick();
|
|
262
|
+
}, delayMs);
|
|
263
|
+
timer.unref?.();
|
|
264
|
+
}
|
|
265
|
+
async function tick() {
|
|
266
|
+
if (!running) return;
|
|
267
|
+
const work = (async () => {
|
|
268
|
+
try {
|
|
269
|
+
return await poll();
|
|
270
|
+
} catch (error) {
|
|
271
|
+
logger.error("[jobs] Poll failed", { error });
|
|
272
|
+
return 0;
|
|
273
|
+
}
|
|
274
|
+
})();
|
|
275
|
+
inFlight = work;
|
|
276
|
+
schedule(await work >= concurrency ? 0 : pollIntervalMs);
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
start() {
|
|
280
|
+
if (running) return;
|
|
281
|
+
running = true;
|
|
282
|
+
logger.info(`[jobs] Worker started (concurrency ${concurrency}, poll ${pollIntervalMs}ms)`);
|
|
283
|
+
schedule(0);
|
|
284
|
+
},
|
|
285
|
+
async stop() {
|
|
286
|
+
running = false;
|
|
287
|
+
if (timer) {
|
|
288
|
+
clearTimeout(timer);
|
|
289
|
+
timer = null;
|
|
290
|
+
}
|
|
291
|
+
if (draining) return;
|
|
292
|
+
draining = true;
|
|
293
|
+
await inFlight.catch(() => void 0);
|
|
294
|
+
draining = false;
|
|
295
|
+
},
|
|
296
|
+
runOnce() {
|
|
297
|
+
return poll();
|
|
298
|
+
},
|
|
299
|
+
register(task, handler) {
|
|
300
|
+
if (handlers.has(task)) logger.warn(`[jobs] Task "${task}" was already registered; the later handler wins`);
|
|
301
|
+
handlers.set(task, handler);
|
|
302
|
+
},
|
|
303
|
+
isRunning() {
|
|
304
|
+
return running;
|
|
305
|
+
},
|
|
306
|
+
async enqueue(task, payload, enqueueOptions = {}) {
|
|
307
|
+
return store.insert({
|
|
308
|
+
task,
|
|
309
|
+
payload: payload ?? null,
|
|
310
|
+
runAt: new Date(Date.now() + (enqueueOptions.delayMs ?? 0)),
|
|
311
|
+
maxAttempts: enqueueOptions.maxAttempts ?? defaultMaxAttempts,
|
|
312
|
+
idempotencyKey: enqueueOptions.idempotencyKey
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
//#endregion
|
|
318
|
+
//#region src/jobs/index.ts
|
|
319
|
+
var jobs_exports = /* @__PURE__ */ __exportAll({
|
|
320
|
+
createJobQueue: () => createJobQueue,
|
|
321
|
+
createJobStore: () => createJobStore
|
|
322
|
+
});
|
|
323
|
+
//#endregion
|
|
324
|
+
export { createJobStore as i, createJobQueue as n, defaultBackoff as r, jobs_exports as t };
|
|
325
|
+
|
|
326
|
+
//# sourceMappingURL=jobs-DR4SjGrD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jobs-DR4SjGrD.js","names":[],"sources":["../src/jobs/job-store.ts","../src/jobs/job-queue.ts","../src/jobs/index.ts"],"sourcesContent":["import type { DataDriver } from \"@rebasepro/types\";\nimport { isSQLAdmin } from \"@rebasepro/types\";\nimport { revokeInternalTableSql } from \"@rebasepro/common\";\nimport { logger } from \"../utils/logger.js\";\nimport { createDdlBootstrapper, hasInCauseChain, type SqlExec } from \"../boot/ddl-bootstrap.js\";\nimport type { JobRecord } from \"./types.js\";\n\n/**\n * The queue's storage, as SQL against `rebase.jobs`.\n *\n * Split from the worker for the same reason `cron-store` is split from\n * `cron-scheduler`: the interesting parts here are three statements that have\n * to be exactly right under concurrency, and they are much easier to reason\n * about — and to test — away from a polling loop.\n */\n\nconst TABLE = \"rebase.jobs\";\n\n/** How long finished jobs are kept before the boot sweep removes them. */\nconst SUCCEEDED_RETENTION_DAYS = 3;\n/**\n * Failures outlive successes by a lot. A dead-lettered job is evidence, and the\n * person who needs it is usually looking on Monday for something that happened\n * on Friday night.\n */\nconst FAILED_RETENTION_DAYS = 30;\n\n/** A row as Postgres returns it. */\ninterface JobRow {\n id: string;\n task: string;\n payload: unknown;\n status: string;\n run_at: string;\n attempts: number;\n max_attempts: number;\n last_error: string | null;\n created_at: string;\n updated_at: string;\n}\n\nfunction toRecord(row: JobRow): JobRecord {\n return {\n id: row.id,\n task: row.task,\n payload: row.payload,\n status: row.status as JobRecord[\"status\"],\n runAt: new Date(row.run_at).toISOString(),\n attempts: Number(row.attempts),\n maxAttempts: Number(row.max_attempts),\n lastError: row.last_error,\n createdAt: new Date(row.created_at).toISOString(),\n updatedAt: new Date(row.updated_at).toISOString()\n };\n}\n\n/** Same rule as `cron-store`: match the SQLSTATE, never the message. */\nfunction isUniqueViolation(err: unknown): boolean {\n return hasInCauseChain(err, (e) => e.code === \"23505\");\n}\n\nexport interface JobStore {\n ensureTable(): Promise<void>;\n /** Returns the new job's id, or `null` if an idempotency key matched unfinished work. */\n insert(job: {\n task: string;\n payload: unknown;\n runAt: Date;\n maxAttempts: number;\n idempotencyKey?: string;\n }): Promise<string | null>;\n /** Atomically take up to `limit` runnable jobs for this worker. */\n claim(limit: number, workerId: string): Promise<JobRecord[]>;\n complete(id: string): Promise<void>;\n /** Back to `pending` with a later `runAt`, or `failed` when out of attempts. */\n fail(id: string, error: string, retryAt: Date | null): Promise<void>;\n /** Return jobs stranded by a worker that died holding them. Resolves with how many. */\n reapExpired(visibilityTimeoutMs: number): Promise<number>;\n fetch(id: string): Promise<JobRecord | null>;\n}\n\nexport function createJobStore(driver: DataDriver): JobStore | undefined {\n const admin = driver.admin;\n if (!isSQLAdmin(admin)) {\n logger.warn(\n \"⚠️ [jobs] DataDriver does not support SQL admin — the durable job queue is unavailable. \" +\n \"Work that would have been queued runs inline instead.\"\n );\n return undefined;\n }\n\n // Two shapes of the same call: the bootstrapper's `SqlExec` takes an\n // options object, while everything below reads better with a positional\n // parameter array.\n const execRaw: SqlExec = (sqlText, options) =>\n admin.executeSql(sqlText, options?.params ? { params: options.params } : undefined);\n const exec = (sqlText: string, params?: unknown[]) =>\n execRaw(sqlText, params ? { params } : undefined);\n\n const ddl = createDdlBootstrapper(execRaw, \"jobs\");\n\n return {\n async ensureTable(): Promise<void> {\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 task TEXT NOT NULL,\n payload JSONB,\n status TEXT NOT NULL DEFAULT 'pending',\n run_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n attempts INTEGER NOT NULL DEFAULT 0,\n max_attempts INTEGER NOT NULL DEFAULT 3,\n locked_at TIMESTAMPTZ,\n locked_by TEXT,\n idempotency_key TEXT,\n last_error TEXT,\n created_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n updated_at TIMESTAMPTZ NOT NULL DEFAULT now()\n )\n `);\n\n // The claim query's index. Partial, because the rows it has to find\n // fast are a shrinking minority of a table that also holds every\n // success and every dead letter.\n await ddl.ensureObject(\"Creating idx_jobs_runnable\", `\n CREATE INDEX IF NOT EXISTS idx_jobs_runnable\n ON ${TABLE}(run_at, created_at)\n WHERE status = 'pending'\n `);\n\n // Finds jobs stranded by a dead worker.\n await ddl.ensureObject(\"Creating idx_jobs_running\", `\n CREATE INDEX IF NOT EXISTS idx_jobs_running\n ON ${TABLE}(locked_at)\n WHERE status = 'running'\n `);\n\n // What makes `idempotencyKey` a guarantee rather than a\n // check-then-act race: two instances reacting to one event both\n // reach the INSERT, and the index decides. Partial on unfinished\n // work — see `EnqueueOptions.idempotencyKey` for why a key must be\n // reusable once its job is done.\n await ddl.ensureObject(\"Creating idx_jobs_idempotency\", `\n CREATE UNIQUE INDEX IF NOT EXISTS idx_jobs_idempotency\n ON ${TABLE}(idempotency_key)\n WHERE idempotency_key IS NOT NULL AND status IN ('pending', 'running')\n `);\n\n const ready = await ddl.isReadable(TABLE);\n\n if (ready) {\n await ddl.step(\"Job retention sweep\", async () => {\n await exec(\n `DELETE FROM ${TABLE}\n WHERE (status = 'succeeded' AND updated_at < now() - make_interval(days => $1))\n OR (status = 'failed' AND updated_at < now() - make_interval(days => $2))`,\n [SUCCEEDED_RETENTION_DAYS, FAILED_RETENTION_DAYS]\n );\n });\n\n // Payloads are arbitrary application data — a webhook body, a\n // user id, whatever was passed — and a writable queue lets any\n // signed-in user schedule work of their choosing under the\n // server's own authority. Same reasoning, and the same\n // unconditional re-application, as `cron_claims`.\n await ddl.step(\"Revoking end-user access to jobs\", () =>\n exec(revokeInternalTableSql(\"rebase\", \"jobs\")));\n\n logger.info(\"✅ Job queue table ready\");\n } else {\n logger.error(\n `❌ [jobs] ${TABLE} is unavailable — nothing can be queued and nothing queued earlier ` +\n \"will run. Callers fall back to running the work inline.\"\n );\n }\n },\n\n async insert(job): Promise<string | null> {\n try {\n const rows = await exec(\n `INSERT INTO ${TABLE} (task, payload, run_at, max_attempts, idempotency_key)\n VALUES ($1, $2::jsonb, $3, $4, $5)\n RETURNING id`,\n [\n job.task,\n JSON.stringify(job.payload ?? null),\n job.runAt.toISOString(),\n job.maxAttempts,\n job.idempotencyKey ?? null\n ]\n );\n return (rows?.[0]?.id as string) ?? null;\n } catch (error) {\n // The losing side of an idempotency race. Not an error: the\n // work the caller wanted is already queued, which is the\n // outcome they asked for.\n if (isUniqueViolation(error)) return null;\n throw error;\n }\n },\n\n async claim(limit: number, workerId: string): Promise<JobRecord[]> {\n // `FOR UPDATE SKIP LOCKED` is the whole design. The inner select\n // takes row locks on the jobs it picks and *skips* any a concurrent\n // worker already holds, so N workers polling the same table divide\n // the work instead of contending for the head of it — and no job is\n // ever handed to two of them.\n //\n // `attempts` is incremented here, on claim, rather than on failure.\n // A worker that is killed mid-job never reports anything, so\n // counting on failure would let a job that crashes the process be\n // retried forever, once per restart, taking the process down each\n // time.\n const rows = await exec(\n `UPDATE ${TABLE} SET\n status = 'running',\n attempts = attempts + 1,\n locked_at = now(),\n locked_by = $2,\n updated_at = now()\n WHERE id IN (\n SELECT id FROM ${TABLE}\n WHERE status = 'pending' AND run_at <= now()\n ORDER BY run_at, created_at\n LIMIT $1\n FOR UPDATE SKIP LOCKED\n )\n RETURNING *`,\n [limit, workerId]\n );\n return (rows as unknown as JobRow[]).map(toRecord);\n },\n\n async complete(id: string): Promise<void> {\n await exec(\n `UPDATE ${TABLE} SET status = 'succeeded', locked_at = NULL, locked_by = NULL,\n last_error = NULL, updated_at = now()\n WHERE id = $1`,\n [id]\n );\n },\n\n async fail(id: string, error: string, retryAt: Date | null): Promise<void> {\n if (retryAt) {\n await exec(\n `UPDATE ${TABLE} SET status = 'pending', run_at = $2, locked_at = NULL,\n locked_by = NULL, last_error = $3, updated_at = now()\n WHERE id = $1`,\n [id, retryAt.toISOString(), error]\n );\n return;\n }\n await exec(\n `UPDATE ${TABLE} SET status = 'failed', locked_at = NULL, locked_by = NULL,\n last_error = $2, updated_at = now()\n WHERE id = $1`,\n [id, error]\n );\n },\n\n async reapExpired(visibilityTimeoutMs: number): Promise<number> {\n // A worker killed while holding a job cannot release it, so nothing\n // but a timeout will ever free the row. Jobs that still have\n // attempts left go back to `pending`; the rest are dead-lettered\n // with an error that says what happened, because \"attempts: 3,\n // lastError: null\" is otherwise a genuinely baffling row to find.\n const seconds = Math.max(1, Math.round(visibilityTimeoutMs / 1000));\n\n const revived = await exec(\n `UPDATE ${TABLE} SET status = 'pending', locked_at = NULL, locked_by = NULL,\n last_error = 'Worker stopped responding; the job was reclaimed', updated_at = now()\n WHERE status = 'running'\n AND locked_at < now() - make_interval(secs => $1)\n AND attempts < max_attempts\n RETURNING id`,\n [seconds]\n );\n\n const buried = await exec(\n `UPDATE ${TABLE} SET status = 'failed', locked_at = NULL, locked_by = NULL,\n last_error = 'Worker stopped responding on the final attempt', updated_at = now()\n WHERE status = 'running'\n AND locked_at < now() - make_interval(secs => $1)\n AND attempts >= max_attempts\n RETURNING id`,\n [seconds]\n );\n\n const count = (revived?.length ?? 0) + (buried?.length ?? 0);\n if (count > 0) {\n logger.warn(\n `[jobs] Reclaimed ${count} job(s) from a worker that stopped responding ` +\n `(${revived?.length ?? 0} retryable, ${buried?.length ?? 0} dead-lettered)`\n );\n }\n return count;\n },\n\n async fetch(id: string): Promise<JobRecord | null> {\n const rows = await exec(`SELECT * FROM ${TABLE} WHERE id = $1`, [id]);\n const row = (rows as unknown as JobRow[])[0];\n return row ? toRecord(row) : null;\n }\n };\n}\n","import { randomUUID } from \"crypto\";\nimport { logger } from \"../utils/logger.js\";\nimport type { JobStore } from \"./job-store.js\";\nimport type { EnqueueOptions, JobHandler, JobQueueClient, JobQueueOptions, JobRecord } from \"./types.js\";\n\n/**\n * The worker: claim, run, record, repeat.\n *\n * Everything difficult about running jobs concurrently is in `job-store.ts`,\n * where one `UPDATE … FOR UPDATE SKIP LOCKED` does the arbitration. What is\n * left here is a loop and the decisions around a handler that throws.\n */\n\nconst DEFAULT_CONCURRENCY = 5;\nconst DEFAULT_POLL_INTERVAL_MS = 2_000;\nconst DEFAULT_VISIBILITY_TIMEOUT_MS = 5 * 60_000;\nconst DEFAULT_MAX_ATTEMPTS = 3;\n\n/** 1s, 5s, 25s, 125s … capped at an hour. */\nexport function defaultBackoff(attempt: number): number {\n return Math.min(1_000 * Math.pow(5, Math.max(0, attempt - 1)), 60 * 60_000);\n}\n\n/**\n * How often the reaper runs, relative to the visibility timeout.\n *\n * A quarter of it, so a stranded job waits at most 1.25× the timeout rather\n * than 2× — and so the sweep is not itself a per-poll query against a table\n * whose interesting rows are, almost always, none.\n */\nconst REAP_INTERVAL_FACTOR = 0.25;\n\nexport interface JobQueue extends JobQueueClient {\n start(): void;\n stop(): Promise<void>;\n /** Run one poll's worth of work and return how many jobs ran. For tests and for `/jobs/drain`. */\n runOnce(): Promise<number>;\n /** Registered after construction — how `tasks` from config and internal producers meet. */\n register<P = unknown>(task: string, handler: JobHandler<P>): void;\n isRunning(): boolean;\n}\n\nexport function createJobQueue(store: JobStore, options: JobQueueOptions = {}): JobQueue {\n const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;\n const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n const visibilityTimeoutMs = options.visibilityTimeoutMs ?? DEFAULT_VISIBILITY_TIMEOUT_MS;\n const defaultMaxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const backoff = options.backoff ?? defaultBackoff;\n\n const handlers = new Map<string, JobHandler<never>>();\n for (const [task, handler] of Object.entries(options.tasks ?? {})) {\n handlers.set(task, handler);\n }\n\n // Identifies this process in `locked_by`. Purely diagnostic — the claim is\n // enforced by the row lock, not by this — but \"which pod had it when it\n // stopped\" is the first question anyone asks of a stuck job.\n const workerId = `${process.pid}-${randomUUID().slice(0, 8)}`;\n\n let timer: NodeJS.Timeout | null = null;\n let running = false;\n let draining = false;\n /** Resolves when the in-flight poll finishes, so `stop()` can wait for it. */\n let inFlight: Promise<unknown> = Promise.resolve();\n let lastReapAt = 0;\n\n async function runJob(job: JobRecord): Promise<void> {\n const handler = handlers.get(job.task);\n\n if (!handler) {\n // Not a failure. A rolling deploy runs old and new code at once, and\n // an instance that has not been updated yet must not burn the\n // attempts of a job belonging to one that has. Give the row back\n // and let a peer — or this process after its next deploy — take it.\n //\n // The attempt increment from the claim is deliberately not undone:\n // a task nobody in the fleet implements would otherwise cycle\n // forever, and this way it dead-letters after `maxAttempts` with an\n // error naming the task.\n logger.warn(`[jobs] No handler registered for task \"${job.task}\" — returning the job to the queue`);\n await store.fail(\n job.id,\n `No handler registered for task \"${job.task}\"`,\n job.attempts < job.maxAttempts ? new Date(Date.now() + backoff(job.attempts)) : null\n );\n return;\n }\n\n try {\n await handler({\n id: job.id,\n task: job.task,\n payload: job.payload as never,\n attempt: job.attempts,\n maxAttempts: job.maxAttempts\n } as never);\n await store.complete(job.id);\n } catch (error) {\n const message = error instanceof Error ? (error.stack ?? error.message) : String(error);\n const willRetry = job.attempts < job.maxAttempts;\n\n // Truncated, because `last_error` holds a stack and a queue that\n // accumulates megabytes of them is its own outage.\n await store.fail(job.id, message.slice(0, 4_000), willRetry ? new Date(Date.now() + backoff(job.attempts)) : null);\n\n if (willRetry) {\n logger.warn(`[jobs] \"${job.task}\" failed on attempt ${job.attempts}/${job.maxAttempts}; retrying`, { jobId: job.id });\n } else {\n // The last attempt is an error, not a warning: nothing else will\n // touch this job, and if nobody looks at the table it is simply\n // lost work.\n logger.error(`[jobs] \"${job.task}\" failed permanently after ${job.attempts} attempts`, { jobId: job.id, error: message });\n }\n }\n }\n\n async function poll(): Promise<number> {\n // The reaper, on its own cadence.\n const now = Date.now();\n if (now - lastReapAt > visibilityTimeoutMs * REAP_INTERVAL_FACTOR) {\n lastReapAt = now;\n try {\n await store.reapExpired(visibilityTimeoutMs);\n } catch (error) {\n logger.error(\"[jobs] Failed to reclaim expired jobs\", { error });\n }\n }\n\n const jobs = await store.claim(concurrency, workerId);\n if (jobs.length === 0) return 0;\n\n // Settled, not `all`: `runJob` handles its own errors, but a store\n // write failing inside it must not abandon this batch's siblings.\n await Promise.allSettled(jobs.map(runJob));\n return jobs.length;\n }\n\n function schedule(delayMs: number): void {\n if (!running) return;\n timer = setTimeout(() => {\n void tick();\n }, delayMs);\n // Never hold the process open. A queue with nothing to do should not be\n // the reason `rebase dev` will not exit.\n timer.unref?.();\n }\n\n async function tick(): Promise<void> {\n if (!running) return;\n const work = (async () => {\n try {\n return await poll();\n } catch (error) {\n logger.error(\"[jobs] Poll failed\", { error });\n return 0;\n }\n })();\n inFlight = work;\n const count = await work;\n\n // A full batch means there is probably more waiting, so go straight\n // back rather than sleeping through a backlog.\n schedule(count >= concurrency ? 0 : pollIntervalMs);\n }\n\n return {\n start(): void {\n if (running) return;\n running = true;\n logger.info(`[jobs] Worker started (concurrency ${concurrency}, poll ${pollIntervalMs}ms)`);\n schedule(0);\n },\n\n async stop(): Promise<void> {\n running = false;\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n if (draining) return;\n draining = true;\n // Jobs in flight keep their claim until they finish or the\n // visibility timeout expires, so waiting here is what turns a\n // graceful shutdown into \"no job runs twice\".\n await inFlight.catch(() => undefined);\n draining = false;\n },\n\n runOnce(): Promise<number> {\n return poll();\n },\n\n register<P = unknown>(task: string, handler: JobHandler<P>): void {\n if (handlers.has(task)) {\n logger.warn(`[jobs] Task \"${task}\" was already registered; the later handler wins`);\n }\n handlers.set(task, handler as JobHandler<never>);\n },\n\n isRunning(): boolean {\n return running;\n },\n\n async enqueue<P = unknown>(task: string, payload?: P, enqueueOptions: EnqueueOptions = {}): Promise<string | null> {\n return store.insert({\n task,\n payload: payload ?? null,\n runAt: new Date(Date.now() + (enqueueOptions.delayMs ?? 0)),\n maxAttempts: enqueueOptions.maxAttempts ?? defaultMaxAttempts,\n idempotencyKey: enqueueOptions.idempotencyKey\n });\n }\n };\n}\n","export { createJobStore } from \"./job-store\";\nexport type { JobStore } from \"./job-store\";\nexport { createJobQueue, defaultBackoff } from \"./job-queue\";\nexport type { JobQueue } from \"./job-queue\";\nexport type {\n EnqueueOptions,\n JobContext,\n JobHandler,\n JobQueueClient,\n JobQueueOptions,\n JobRecord,\n JobStatus\n} from \"./types\";\n"],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@ __createRequire(import.meta.url);
|
|
|
4
4
|
import { i as __toESM, n as __exportAll, r as __require, t as __commonJSMin } from "./rolldown-runtime-DSJWtz9O.js";
|
|
5
5
|
import "./src-Cz9nMgUR.js";
|
|
6
6
|
import { t as logger } from "./logger-DfvF_8r-.js";
|
|
7
|
-
import { createHash, randomBytes } from "crypto";
|
|
7
|
+
import { createHash, createPrivateKey, createPublicKey, randomBytes } from "crypto";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
//#region ../types/src/types/storage_source.ts
|
|
10
10
|
/**
|
|
@@ -4019,7 +4019,7 @@ var require_sign = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
4019
4019
|
var isPlainObject = require_lodash_isplainobject();
|
|
4020
4020
|
var isString = require_lodash_isstring();
|
|
4021
4021
|
var once = require_lodash_once();
|
|
4022
|
-
var { KeyObject, createSecretKey, createPrivateKey } = __require("crypto");
|
|
4022
|
+
var { KeyObject, createSecretKey, createPrivateKey: createPrivateKey$1 } = __require("crypto");
|
|
4023
4023
|
var SUPPORTED_ALGS = [
|
|
4024
4024
|
"RS256",
|
|
4025
4025
|
"RS384",
|
|
@@ -4160,7 +4160,7 @@ var require_sign = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
4160
4160
|
}
|
|
4161
4161
|
if (!secretOrPrivateKey && options.algorithm !== "none") return failure(/* @__PURE__ */ new Error("secretOrPrivateKey must have a value"));
|
|
4162
4162
|
if (secretOrPrivateKey != null && !(secretOrPrivateKey instanceof KeyObject)) try {
|
|
4163
|
-
secretOrPrivateKey = createPrivateKey(secretOrPrivateKey);
|
|
4163
|
+
secretOrPrivateKey = createPrivateKey$1(secretOrPrivateKey);
|
|
4164
4164
|
} catch (_) {
|
|
4165
4165
|
try {
|
|
4166
4166
|
secretOrPrivateKey = createSecretKey(typeof secretOrPrivateKey === "string" ? Buffer.from(secretOrPrivateKey) : secretOrPrivateKey);
|
|
@@ -4415,6 +4415,111 @@ function canonicalStorageBucket(rawBucket) {
|
|
|
4415
4415
|
return rawBucket;
|
|
4416
4416
|
}
|
|
4417
4417
|
//#endregion
|
|
4418
|
+
//#region src/auth/jwt-keys.ts
|
|
4419
|
+
/**
|
|
4420
|
+
* The algorithm a key type implies.
|
|
4421
|
+
*
|
|
4422
|
+
* Rejecting anything else here rather than defaulting is deliberate: an Ed25519
|
|
4423
|
+
* key configured by someone expecting it to work would otherwise be signed with
|
|
4424
|
+
* `RS256` in the header and fail verification everywhere, at runtime, on tokens
|
|
4425
|
+
* already handed to users.
|
|
4426
|
+
*/
|
|
4427
|
+
function algorithmForKey(key, kid) {
|
|
4428
|
+
switch (key.asymmetricKeyType) {
|
|
4429
|
+
case "rsa":
|
|
4430
|
+
case "rsa-pss": return "RS256";
|
|
4431
|
+
case "ec": return "ES256";
|
|
4432
|
+
default: throw new Error(`JWT signing key "${kid}" is a ${key.asymmetricKeyType ?? "non-asymmetric"} key. Supported: RSA (RS256) and EC P-256 (ES256). Generate one with: openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out jwt-key.pem`);
|
|
4433
|
+
}
|
|
4434
|
+
}
|
|
4435
|
+
/**
|
|
4436
|
+
* An EC key of the wrong curve is the other way a key parses cleanly and then
|
|
4437
|
+
* fails to verify: `ES256` means P-256 specifically, and a P-384 key signs a
|
|
4438
|
+
* token whose header says P-256's algorithm.
|
|
4439
|
+
*/
|
|
4440
|
+
function assertCurveMatches(publicKey, kid) {
|
|
4441
|
+
const jwk = publicKey.export({ format: "jwk" });
|
|
4442
|
+
if (jwk.crv && jwk.crv !== "P-256") throw new Error(`JWT signing key "${kid}" uses curve ${jwk.crv}, but ES256 requires P-256. Generate one with: openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out jwt-key.pem`);
|
|
4443
|
+
}
|
|
4444
|
+
/**
|
|
4445
|
+
* Parse the configured keys, deriving each public half from its private key.
|
|
4446
|
+
*
|
|
4447
|
+
* Throws on anything malformed. This runs at boot, from `configureJwt`, so a
|
|
4448
|
+
* key that cannot sign takes the process down at start rather than at the first
|
|
4449
|
+
* login — the same bargain every other credential in this file makes.
|
|
4450
|
+
*/
|
|
4451
|
+
function resolveSigningKeys(configs) {
|
|
4452
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4453
|
+
return configs.map((config) => {
|
|
4454
|
+
if (!config.kid) throw new Error("Every JWT signing key needs a `kid`; it is what the JWKS and the token header agree on.");
|
|
4455
|
+
if (seen.has(config.kid)) throw new Error(`Duplicate JWT signing key id "${config.kid}". A \`kid\` selects exactly one key at verification time, so two keys sharing one is a token that verifies or does not depending on order.`);
|
|
4456
|
+
seen.add(config.kid);
|
|
4457
|
+
let privateKey;
|
|
4458
|
+
try {
|
|
4459
|
+
privateKey = createPrivateKey(config.privateKey);
|
|
4460
|
+
} catch (error) {
|
|
4461
|
+
throw new Error(`JWT signing key "${config.kid}" is not a readable PEM private key: ${error instanceof Error ? error.message : String(error)}`);
|
|
4462
|
+
}
|
|
4463
|
+
const publicKey = createPublicKey(privateKey);
|
|
4464
|
+
const derived = algorithmForKey(privateKey, config.kid);
|
|
4465
|
+
const algorithm = config.algorithm ?? derived;
|
|
4466
|
+
if (algorithm !== derived) throw new Error(`JWT signing key "${config.kid}" is declared as ${algorithm} but is a ${privateKey.asymmetricKeyType} key, which signs ${derived}.`);
|
|
4467
|
+
if (algorithm === "ES256") assertCurveMatches(publicKey, config.kid);
|
|
4468
|
+
return {
|
|
4469
|
+
kid: config.kid,
|
|
4470
|
+
algorithm,
|
|
4471
|
+
privateKey,
|
|
4472
|
+
publicKey
|
|
4473
|
+
};
|
|
4474
|
+
});
|
|
4475
|
+
}
|
|
4476
|
+
/**
|
|
4477
|
+
* The key a token names, or `null` if it names none we hold.
|
|
4478
|
+
*
|
|
4479
|
+
* The returned algorithm is the *key's*, and the caller must verify with that
|
|
4480
|
+
* one alone. See the module docblock for what happens otherwise.
|
|
4481
|
+
*/
|
|
4482
|
+
function resolveVerificationKey(keys, kid) {
|
|
4483
|
+
if (!kid) return null;
|
|
4484
|
+
return keys.find((key) => key.kid === kid) ?? null;
|
|
4485
|
+
}
|
|
4486
|
+
/**
|
|
4487
|
+
* A PEM as an environment variable can actually carry it.
|
|
4488
|
+
*
|
|
4489
|
+
* A PEM is multi-line and environment variables are not, so every deployment
|
|
4490
|
+
* tool solves it differently: `.env` files and most secret managers escape the
|
|
4491
|
+
* newlines to `\n`, Kubernetes and Docker secrets pass the bytes through
|
|
4492
|
+
* intact, and CI systems that mangle both are usually fed base64. All three
|
|
4493
|
+
* arrive here, and guessing wrong produces "not a readable PEM private key" at
|
|
4494
|
+
* boot with a key the operator can see is perfectly valid.
|
|
4495
|
+
*
|
|
4496
|
+
* Detection is on content, not on a flag: a PEM says so on its first line, and
|
|
4497
|
+
* anything that does not is tried as base64.
|
|
4498
|
+
*/
|
|
4499
|
+
function normalizePemFromEnv(value) {
|
|
4500
|
+
const trimmed = value.trim();
|
|
4501
|
+
if (trimmed.includes("-----BEGIN")) return trimmed.replace(/\\n/g, "\n");
|
|
4502
|
+
return Buffer.from(trimmed, "base64").toString("utf8");
|
|
4503
|
+
}
|
|
4504
|
+
/**
|
|
4505
|
+
* The public halves, in JWKS form.
|
|
4506
|
+
*
|
|
4507
|
+
* Node exports a JWK containing only public parameters for a public
|
|
4508
|
+
* `KeyObject` — no `d`, no primes — so the private material cannot leak
|
|
4509
|
+
* through this path even if a private key were passed by mistake. The keys are
|
|
4510
|
+
* derived from `publicKey` regardless, and this is asserted in the tests,
|
|
4511
|
+
* because "cannot" is worth checking on the one endpoint whose entire job is to
|
|
4512
|
+
* be world-readable.
|
|
4513
|
+
*/
|
|
4514
|
+
function toJwks(keys) {
|
|
4515
|
+
return { keys: keys.map((key) => ({
|
|
4516
|
+
...key.publicKey.export({ format: "jwk" }),
|
|
4517
|
+
kid: key.kid,
|
|
4518
|
+
alg: key.algorithm,
|
|
4519
|
+
use: "sig"
|
|
4520
|
+
})) };
|
|
4521
|
+
}
|
|
4522
|
+
//#endregion
|
|
4418
4523
|
//#region src/auth/jwt.ts
|
|
4419
4524
|
var jwt_exports = /* @__PURE__ */ __exportAll({
|
|
4420
4525
|
MAX_COOKIE_AGE_MS: () => MAX_COOKIE_AGE_MS,
|
|
@@ -4426,8 +4531,10 @@ var jwt_exports = /* @__PURE__ */ __exportAll({
|
|
|
4426
4531
|
generateRefreshToken: () => generateRefreshToken,
|
|
4427
4532
|
getAccessTokenExpiry: () => getAccessTokenExpiry,
|
|
4428
4533
|
getAccessTokenExpiryMs: () => getAccessTokenExpiryMs,
|
|
4534
|
+
getJwks: () => getJwks,
|
|
4429
4535
|
getRefreshTokenExpiry: () => getRefreshTokenExpiry,
|
|
4430
4536
|
getRefreshTokenTtlMs: () => getRefreshTokenTtlMs,
|
|
4537
|
+
hasAsymmetricSigningKey: () => hasAsymmetricSigningKey,
|
|
4431
4538
|
hashRefreshToken: () => hashRefreshToken,
|
|
4432
4539
|
isJwtConfigured: () => isJwtConfigured,
|
|
4433
4540
|
verifyAccessToken: () => verifyAccessToken,
|
|
@@ -4440,6 +4547,15 @@ var jwtConfig = {
|
|
|
4440
4547
|
refreshExpiresIn: "400d"
|
|
4441
4548
|
};
|
|
4442
4549
|
/**
|
|
4550
|
+
* The parsed signing keys, and which one mints new access tokens.
|
|
4551
|
+
*
|
|
4552
|
+
* Both are module state beside `jwtConfig` for the same reason it is: every
|
|
4553
|
+
* signing and verifying path in the server reads them through this file, and
|
|
4554
|
+
* there is exactly one JWT configuration per process.
|
|
4555
|
+
*/
|
|
4556
|
+
var signingKeys = [];
|
|
4557
|
+
var activeSigningKey = null;
|
|
4558
|
+
/**
|
|
4443
4559
|
* Configure JWT settings - call this during initialization.
|
|
4444
4560
|
* Validates the secret strength to prevent deployment with default/weak secrets.
|
|
4445
4561
|
*/
|
|
@@ -4467,10 +4583,33 @@ function configureJwt(config) {
|
|
|
4467
4583
|
]);
|
|
4468
4584
|
if (!config.secret || config.secret.length < 32) throw new Error("JWT secret is too short. Must be at least 32 characters. Generate one with: node -e \"logger.info(require('crypto').randomBytes(48).toString('base64'))\"");
|
|
4469
4585
|
if (weakSecrets.has(config.secret.toLowerCase())) throw new Error("JWT secret is a known default/weak value. Please use a strong, randomly generated secret. Generate one with: node -e \"logger.info(require('crypto').randomBytes(48).toString('base64'))\"");
|
|
4586
|
+
const resolved = config.signingKeys ? resolveSigningKeys(config.signingKeys) : [];
|
|
4587
|
+
let active = null;
|
|
4588
|
+
if (resolved.length > 0) if (config.activeKid) {
|
|
4589
|
+
active = resolved.find((key) => key.kid === config.activeKid) ?? null;
|
|
4590
|
+
if (!active) throw new Error(`auth.activeKid is "${config.activeKid}", which is not among the configured signing keys (${resolved.map((k) => `"${k.kid}"`).join(", ")}). Signing with a key nobody published produces tokens no verifier can check.`);
|
|
4591
|
+
} else active = resolved[0];
|
|
4470
4592
|
jwtConfig = {
|
|
4471
4593
|
...jwtConfig,
|
|
4472
4594
|
...config
|
|
4473
4595
|
};
|
|
4596
|
+
signingKeys = resolved;
|
|
4597
|
+
activeSigningKey = active;
|
|
4598
|
+
}
|
|
4599
|
+
/**
|
|
4600
|
+
* The public keys, in JWKS form, for `/.well-known/jwks.json`.
|
|
4601
|
+
*
|
|
4602
|
+
* An empty `keys` array on a backend with no asymmetric keys configured is the
|
|
4603
|
+
* correct answer rather than a 404: it says "this issuer publishes none",
|
|
4604
|
+
* which a verifier can act on, where a 404 is indistinguishable from a
|
|
4605
|
+
* misconfigured URL.
|
|
4606
|
+
*/
|
|
4607
|
+
function getJwks() {
|
|
4608
|
+
return toJwks(signingKeys);
|
|
4609
|
+
}
|
|
4610
|
+
/** Is this backend signing access tokens asymmetrically? */
|
|
4611
|
+
function hasAsymmetricSigningKey() {
|
|
4612
|
+
return activeSigningKey !== null;
|
|
4474
4613
|
}
|
|
4475
4614
|
/**
|
|
4476
4615
|
* Has this server been given a JWT secret?
|
|
@@ -4497,6 +4636,11 @@ function generateAccessToken(uid, roles, aal = "aal1", customClaims) {
|
|
|
4497
4636
|
...customClaims,
|
|
4498
4637
|
aal
|
|
4499
4638
|
};
|
|
4639
|
+
if (activeSigningKey) return import_jsonwebtoken.default.sign(payload, activeSigningKey.privateKey, {
|
|
4640
|
+
expiresIn: jwtConfig.accessExpiresIn,
|
|
4641
|
+
algorithm: activeSigningKey.algorithm,
|
|
4642
|
+
keyid: activeSigningKey.kid
|
|
4643
|
+
});
|
|
4500
4644
|
return import_jsonwebtoken.default.sign(payload, jwtConfig.secret, {
|
|
4501
4645
|
expiresIn: jwtConfig.accessExpiresIn,
|
|
4502
4646
|
algorithm: "HS256"
|
|
@@ -4540,7 +4684,9 @@ function getAccessTokenExpiry() {
|
|
|
4540
4684
|
function verifyAccessToken(token) {
|
|
4541
4685
|
if (!jwtConfig.secret) throw new Error("JWT secret not configured. Call configureJwt() first.");
|
|
4542
4686
|
try {
|
|
4543
|
-
const
|
|
4687
|
+
const header = import_jsonwebtoken.default.decode(token, { complete: true })?.header;
|
|
4688
|
+
const namedKey = resolveVerificationKey(signingKeys, header?.kid);
|
|
4689
|
+
const decoded = namedKey ? import_jsonwebtoken.default.verify(token, namedKey.publicKey, { algorithms: [namedKey.algorithm] }) : import_jsonwebtoken.default.verify(token, jwtConfig.secret, { algorithms: ["HS256"] });
|
|
4544
4690
|
if (decoded.purpose) {
|
|
4545
4691
|
logger.error("[JWT] Verification failed: a purpose-scoped token is not an access token", { purpose: decoded.purpose });
|
|
4546
4692
|
return null;
|
|
@@ -4712,6 +4858,6 @@ function verifyDownloadToken(token) {
|
|
|
4712
4858
|
}
|
|
4713
4859
|
}
|
|
4714
4860
|
//#endregion
|
|
4715
|
-
export {
|
|
4861
|
+
export { canonicalStorageKey as C, findStorageSuffixCollision as D, DEFAULT_STORAGE_SOURCE_KEY as E, normalizeStorageSources as O, canonicalStorageId as S, require_jsonwebtoken as T, verifyMfaPendingToken as _, generateMfaPendingToken as a, InvalidStorageKeyError as b, getJwks as c, hasAsymmetricSigningKey as d, hashRefreshToken as f, verifyDownloadToken as g, verifyAccessToken as h, generateDownloadToken as i, storageEnvSuffix as k, getRefreshTokenExpiry as l, jwt_exports as m, configureJwt as n, generateRefreshToken as o, isJwtConfigured as p, generateAccessToken as r, getAccessTokenExpiry as s, MAX_COOKIE_AGE_MS as t, getRefreshTokenTtlMs as u, normalizePemFromEnv as v, tryCanonicalStorageKey as w, canonicalStorageBucket as x, InvalidStorageBucketError as y };
|
|
4716
4862
|
|
|
4717
|
-
//# sourceMappingURL=jwt-
|
|
4863
|
+
//# sourceMappingURL=jwt-VJyXTdQQ.js.map
|