@dudousxd/nestjs-durable-core 0.82.0 → 0.84.0
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/index.cjs +163 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +167 -10
- package/dist/index.d.ts +167 -10
- package/dist/index.js +157 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -277,6 +277,31 @@ var FatalError = class extends Error {
|
|
|
277
277
|
this.details = details;
|
|
278
278
|
}
|
|
279
279
|
};
|
|
280
|
+
var SchemaValidationError = class extends FatalError {
|
|
281
|
+
static {
|
|
282
|
+
__name(this, "SchemaValidationError");
|
|
283
|
+
}
|
|
284
|
+
static code = "schema_validation";
|
|
285
|
+
issues;
|
|
286
|
+
/**
|
|
287
|
+
* @param label What was validated, e.g. `workflow "checkout" input`; the message is
|
|
288
|
+
* `invalid <label>: <path>: <message>; …`.
|
|
289
|
+
* @param summary Overrides the `<path>: <message>; …` part, for issues whose messages already
|
|
290
|
+
* name their field (class-validator's `orderId must be a string`).
|
|
291
|
+
*/
|
|
292
|
+
constructor(label, issues, code = "schema_validation", summary) {
|
|
293
|
+
const normalized = issues.map((issue) => ({
|
|
294
|
+
message: issue.message,
|
|
295
|
+
path: (issue.path ?? []).map((segment) => String(typeof segment === "object" && segment !== null ? segment.key : segment))
|
|
296
|
+
}));
|
|
297
|
+
const text = summary ?? normalized.map((issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message).join("; ");
|
|
298
|
+
super(`invalid ${label}: ${text}`, code, {
|
|
299
|
+
issues: normalized
|
|
300
|
+
});
|
|
301
|
+
this.name = "SchemaValidationError";
|
|
302
|
+
this.issues = normalized;
|
|
303
|
+
}
|
|
304
|
+
};
|
|
280
305
|
var ConcurrencyLimitError = class extends Error {
|
|
281
306
|
static {
|
|
282
307
|
__name(this, "ConcurrencyLimitError");
|
|
@@ -2008,9 +2033,14 @@ function liveHandoff(runId, checkpoints) {
|
|
|
2008
2033
|
return live;
|
|
2009
2034
|
}
|
|
2010
2035
|
__name(liveHandoff, "liveHandoff");
|
|
2036
|
+
var SCHEMA_VALIDATION_CODES = /* @__PURE__ */ new Set([
|
|
2037
|
+
"invalid_input",
|
|
2038
|
+
"invalid_output"
|
|
2039
|
+
]);
|
|
2011
2040
|
function routeFor(routes, error2) {
|
|
2012
2041
|
if (!routes?.length || error2.code && UNROUTABLE_CODES.has(error2.code)) return void 0;
|
|
2013
|
-
|
|
2042
|
+
const { code } = error2;
|
|
2043
|
+
return (code ? routes.find((r) => r.code === code) : void 0) ?? (code && SCHEMA_VALIDATION_CODES.has(code) ? routes.find((r) => r.code === "schema_validation") : void 0) ?? routes.find((r) => r.code === void 0);
|
|
2014
2044
|
}
|
|
2015
2045
|
__name(routeFor, "routeFor");
|
|
2016
2046
|
function assertRoutes(workflow, routes) {
|
|
@@ -2282,9 +2312,13 @@ __name(scheduleRunIdsAt, "scheduleRunIdsAt");
|
|
|
2282
2312
|
async function runSchedules(engine, schedules, nowMs, opts) {
|
|
2283
2313
|
const random = opts?.random ?? Math.random;
|
|
2284
2314
|
const sleep = opts?.sleep ?? defaultSleep;
|
|
2315
|
+
const settled = settledIdsFor(engine);
|
|
2285
2316
|
const ids = [];
|
|
2286
2317
|
for (const s of schedules) {
|
|
2318
|
+
if (engine.isDraining?.()) break;
|
|
2287
2319
|
if (s.paused) continue;
|
|
2320
|
+
const candidates = scheduleRunIdsAt(s, nowMs).filter((id) => !settled.get(s.key)?.has(id));
|
|
2321
|
+
if (candidates.length === 0) continue;
|
|
2288
2322
|
if (s.overlap === "skip" && s.everyMs) {
|
|
2289
2323
|
const prevBucket = Math.floor(nowMs / s.everyMs) - 1;
|
|
2290
2324
|
if (prevBucket >= 0) {
|
|
@@ -2293,14 +2327,44 @@ async function runSchedules(engine, schedules, nowMs, opts) {
|
|
|
2293
2327
|
}
|
|
2294
2328
|
}
|
|
2295
2329
|
if (s.jitter && s.jitter > 0) await sleep(Math.floor(random() * s.jitter));
|
|
2296
|
-
for (const runId of
|
|
2297
|
-
|
|
2330
|
+
for (const runId of candidates) {
|
|
2331
|
+
if (engine.isDraining?.()) break;
|
|
2332
|
+
const existing = await engine.getRun(runId);
|
|
2333
|
+
remember(settled, s.key, runId);
|
|
2334
|
+
if (existing) continue;
|
|
2335
|
+
await engine.start(workflowName(s.workflow), s.input, runId, {
|
|
2336
|
+
namespace: s.namespace
|
|
2337
|
+
});
|
|
2298
2338
|
ids.push(runId);
|
|
2299
2339
|
}
|
|
2300
2340
|
}
|
|
2301
2341
|
return ids;
|
|
2302
2342
|
}
|
|
2303
2343
|
__name(runSchedules, "runSchedules");
|
|
2344
|
+
var settledRunIds = /* @__PURE__ */ new WeakMap();
|
|
2345
|
+
function settledIdsFor(engine) {
|
|
2346
|
+
let memo = settledRunIds.get(engine);
|
|
2347
|
+
if (memo === void 0) {
|
|
2348
|
+
memo = /* @__PURE__ */ new Map();
|
|
2349
|
+
settledRunIds.set(engine, memo);
|
|
2350
|
+
}
|
|
2351
|
+
return memo;
|
|
2352
|
+
}
|
|
2353
|
+
__name(settledIdsFor, "settledIdsFor");
|
|
2354
|
+
function remember(memo, key, runId) {
|
|
2355
|
+
const seen = memo.get(key);
|
|
2356
|
+
if (seen === void 0) {
|
|
2357
|
+
memo.set(key, /* @__PURE__ */ new Set([
|
|
2358
|
+
runId
|
|
2359
|
+
]));
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2362
|
+
seen.add(runId);
|
|
2363
|
+
if (seen.size > 64) memo.set(key, /* @__PURE__ */ new Set([
|
|
2364
|
+
runId
|
|
2365
|
+
]));
|
|
2366
|
+
}
|
|
2367
|
+
__name(remember, "remember");
|
|
2304
2368
|
|
|
2305
2369
|
// src/schedules.ts
|
|
2306
2370
|
var ScheduleAlreadyExistsError = class extends Error {
|
|
@@ -2876,6 +2940,73 @@ async function stalledInfo(store, run, now, thresholdMs, ageMs) {
|
|
|
2876
2940
|
}
|
|
2877
2941
|
__name(stalledInfo, "stalledInfo");
|
|
2878
2942
|
|
|
2943
|
+
// src/tenant-auth.ts
|
|
2944
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2945
|
+
var TOKEN_SEP = ".";
|
|
2946
|
+
var EXP_SUFFIX = /\.exp(\d+)$/;
|
|
2947
|
+
function sign(claim, secret) {
|
|
2948
|
+
return createHmac("sha256", secret).update(claim).digest("base64url");
|
|
2949
|
+
}
|
|
2950
|
+
__name(sign, "sign");
|
|
2951
|
+
function signTenantToken(tenant, secret, opts) {
|
|
2952
|
+
const claim = opts?.ttlMs !== void 0 ? `${tenant}${TOKEN_SEP}exp${(opts.now ?? Date.now()) + opts.ttlMs}` : tenant;
|
|
2953
|
+
return `${claim}${TOKEN_SEP}${sign(claim, secret)}`;
|
|
2954
|
+
}
|
|
2955
|
+
__name(signTenantToken, "signTenantToken");
|
|
2956
|
+
function hmacTenantVerifier(secret, capabilities) {
|
|
2957
|
+
const secrets = Array.isArray(secret) ? secret : [
|
|
2958
|
+
secret
|
|
2959
|
+
];
|
|
2960
|
+
return ({ token }) => {
|
|
2961
|
+
if (typeof token !== "string" || token.length === 0) return null;
|
|
2962
|
+
const sep = token.lastIndexOf(TOKEN_SEP);
|
|
2963
|
+
if (sep <= 0 || sep === token.length - 1) return null;
|
|
2964
|
+
const claim = token.slice(0, sep);
|
|
2965
|
+
const presented = token.slice(sep + 1);
|
|
2966
|
+
if (!secrets.some((s) => constantTimeEquals(presented, sign(claim, s)))) return null;
|
|
2967
|
+
const exp = EXP_SUFFIX.exec(claim);
|
|
2968
|
+
if (exp) {
|
|
2969
|
+
const expiresAt = Number(exp[1]);
|
|
2970
|
+
if (!Number.isFinite(expiresAt) || Date.now() > expiresAt) return null;
|
|
2971
|
+
const tenant = claim.slice(0, claim.length - (exp[0]?.length ?? 0));
|
|
2972
|
+
if (tenant.length === 0) return null;
|
|
2973
|
+
return capabilities !== void 0 ? {
|
|
2974
|
+
tenant,
|
|
2975
|
+
capabilities
|
|
2976
|
+
} : {
|
|
2977
|
+
tenant
|
|
2978
|
+
};
|
|
2979
|
+
}
|
|
2980
|
+
return capabilities !== void 0 ? {
|
|
2981
|
+
tenant: claim,
|
|
2982
|
+
capabilities
|
|
2983
|
+
} : {
|
|
2984
|
+
tenant: claim
|
|
2985
|
+
};
|
|
2986
|
+
};
|
|
2987
|
+
}
|
|
2988
|
+
__name(hmacTenantVerifier, "hmacTenantVerifier");
|
|
2989
|
+
async function resolveWireTenant(wireTenant, verifyTenant) {
|
|
2990
|
+
if (!verifyTenant) return wireTenant;
|
|
2991
|
+
const verified = await verifyTenant({
|
|
2992
|
+
token: wireTenant,
|
|
2993
|
+
tenant: wireTenant
|
|
2994
|
+
});
|
|
2995
|
+
return verified ? verified.tenant : null;
|
|
2996
|
+
}
|
|
2997
|
+
__name(resolveWireTenant, "resolveWireTenant");
|
|
2998
|
+
async function resolveTenantToken(source) {
|
|
2999
|
+
return typeof source === "function" ? source() : source;
|
|
3000
|
+
}
|
|
3001
|
+
__name(resolveTenantToken, "resolveTenantToken");
|
|
3002
|
+
function constantTimeEquals(a, b) {
|
|
3003
|
+
const ab = Buffer.from(a);
|
|
3004
|
+
const bb = Buffer.from(b);
|
|
3005
|
+
if (ab.length !== bb.length) return false;
|
|
3006
|
+
return timingSafeEqual(ab, bb);
|
|
3007
|
+
}
|
|
3008
|
+
__name(constantTimeEquals, "constantTimeEquals");
|
|
3009
|
+
|
|
2879
3010
|
// src/transport-pool.ts
|
|
2880
3011
|
var TransportPool = class {
|
|
2881
3012
|
static {
|
|
@@ -3099,11 +3230,6 @@ function normalizeChildOptions(options) {
|
|
|
3099
3230
|
} : options ?? {};
|
|
3100
3231
|
}
|
|
3101
3232
|
__name(normalizeChildOptions, "normalizeChildOptions");
|
|
3102
|
-
function formatIssuePath(path) {
|
|
3103
|
-
if (!path || path.length === 0) return "(root)";
|
|
3104
|
-
return path.map((segment) => String(typeof segment === "object" ? segment.key : segment)).join(".");
|
|
3105
|
-
}
|
|
3106
|
-
__name(formatIssuePath, "formatIssuePath");
|
|
3107
3233
|
function resolveStepPolicy(config, opts) {
|
|
3108
3234
|
const policy = {};
|
|
3109
3235
|
const retries = opts?.retries ?? config?.retries;
|
|
@@ -3935,11 +4061,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
3935
4061
|
throw new Error(`workflow "${workflow}": ctx.upsertSearchAttributes schema validation must be synchronous (the searchAttributes schema's \`~standard.validate\` returned a Promise) \u2014 replay-determinism requires a sync validator; drop any async refinement from the schema passed to @Workflow({ searchAttributes }).`);
|
|
3936
4062
|
}
|
|
3937
4063
|
if (result.issues) {
|
|
3938
|
-
|
|
3939
|
-
...new Set(result.issues.map((issue) => formatIssuePath(issue.path)))
|
|
3940
|
-
];
|
|
3941
|
-
const details = result.issues.map((issue) => `${formatIssuePath(issue.path)}: ${issue.message}`).join("; ");
|
|
3942
|
-
throw new Error(`workflow "${workflow}": ctx.upsertSearchAttributes rejected by its searchAttributes schema \u2014 offending key(s) ${offendingKeys.join(", ")} \u2014 ${details}`);
|
|
4064
|
+
throw new SchemaValidationError(`workflow "${workflow}" ctx.upsertSearchAttributes`, result.issues);
|
|
3943
4065
|
}
|
|
3944
4066
|
}
|
|
3945
4067
|
await host.upsertSearchAttributes(runId, attrs);
|
|
@@ -4361,9 +4483,20 @@ var WorkflowEngine = class {
|
|
|
4361
4483
|
await this.completeRemoteDecision(decision);
|
|
4362
4484
|
}
|
|
4363
4485
|
);
|
|
4486
|
+
const verifyTenant = deps.verifyTenant;
|
|
4364
4487
|
this.pool.onStartRun(async (message) => {
|
|
4488
|
+
let tenant;
|
|
4489
|
+
try {
|
|
4490
|
+
tenant = await resolveWireTenant(message.tenant, verifyTenant);
|
|
4491
|
+
} catch {
|
|
4492
|
+
tenant = null;
|
|
4493
|
+
}
|
|
4494
|
+
if (tenant === null) {
|
|
4495
|
+
console.warn(`[nestjs-durable] dropped a start-run for "${message.workflow}": invalid or unauthorized tenant token`);
|
|
4496
|
+
return;
|
|
4497
|
+
}
|
|
4365
4498
|
await this.start(message.workflow, message.input, message.runId ?? globalThis.crypto.randomUUID(), {
|
|
4366
|
-
namespace:
|
|
4499
|
+
namespace: tenant,
|
|
4367
4500
|
tags: message.tags,
|
|
4368
4501
|
searchAttributes: message.searchAttributes
|
|
4369
4502
|
});
|
|
@@ -5145,6 +5278,10 @@ var WorkflowEngine = class {
|
|
|
5145
5278
|
for (const rearm of this.drainRearms) rearm();
|
|
5146
5279
|
}
|
|
5147
5280
|
}
|
|
5281
|
+
/** True once {@link beginDrain} (or {@link drain}) has been called. */
|
|
5282
|
+
isDraining() {
|
|
5283
|
+
return this.draining;
|
|
5284
|
+
}
|
|
5148
5285
|
/**
|
|
5149
5286
|
* Graceful shutdown: stop picking up new runs (recovery/timer become no-ops) and wait for
|
|
5150
5287
|
* in-flight executions to settle, up to `timeoutMs`. Call from your app's shutdown hook so a
|
|
@@ -10735,6 +10872,7 @@ export {
|
|
|
10735
10872
|
ScheduleAlreadyExistsError,
|
|
10736
10873
|
ScheduleClient,
|
|
10737
10874
|
ScheduleNotFoundError,
|
|
10875
|
+
SchemaValidationError,
|
|
10738
10876
|
SignalTimeoutError,
|
|
10739
10877
|
SingletonQueueFullError,
|
|
10740
10878
|
TERMINAL_RUN_STATUSES,
|
|
@@ -10797,6 +10935,7 @@ export {
|
|
|
10797
10935
|
handoffName,
|
|
10798
10936
|
hasDurableExecutionWrappers,
|
|
10799
10937
|
heartbeatStatus,
|
|
10938
|
+
hmacTenantVerifier,
|
|
10800
10939
|
indexWaitersByRun,
|
|
10801
10940
|
info,
|
|
10802
10941
|
isEngineMintedTag,
|
|
@@ -10834,6 +10973,8 @@ export {
|
|
|
10834
10973
|
resolveConcurrency,
|
|
10835
10974
|
resolveRouting,
|
|
10836
10975
|
resolveRunWaiting,
|
|
10976
|
+
resolveTenantToken,
|
|
10977
|
+
resolveWireTenant,
|
|
10837
10978
|
retiredHandoffName,
|
|
10838
10979
|
routeFor,
|
|
10839
10980
|
runDetailEtag,
|
|
@@ -10846,7 +10987,9 @@ export {
|
|
|
10846
10987
|
runStepHandler,
|
|
10847
10988
|
runValueFacetsFromRuns,
|
|
10848
10989
|
sanitizeQueueToken,
|
|
10990
|
+
scheduleRunIdAt,
|
|
10849
10991
|
scheduledRunId,
|
|
10992
|
+
signTenantToken,
|
|
10850
10993
|
singletonAdmissionDenialPatch,
|
|
10851
10994
|
singletonAdmissionTags,
|
|
10852
10995
|
sqlComparator,
|