@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.cjs
CHANGED
|
@@ -72,6 +72,7 @@ __export(index_exports, {
|
|
|
72
72
|
ScheduleAlreadyExistsError: () => ScheduleAlreadyExistsError,
|
|
73
73
|
ScheduleClient: () => ScheduleClient,
|
|
74
74
|
ScheduleNotFoundError: () => ScheduleNotFoundError,
|
|
75
|
+
SchemaValidationError: () => SchemaValidationError,
|
|
75
76
|
SignalTimeoutError: () => SignalTimeoutError,
|
|
76
77
|
SingletonQueueFullError: () => SingletonQueueFullError,
|
|
77
78
|
TERMINAL_RUN_STATUSES: () => TERMINAL_RUN_STATUSES,
|
|
@@ -134,6 +135,7 @@ __export(index_exports, {
|
|
|
134
135
|
handoffName: () => handoffName,
|
|
135
136
|
hasDurableExecutionWrappers: () => hasDurableExecutionWrappers,
|
|
136
137
|
heartbeatStatus: () => heartbeatStatus,
|
|
138
|
+
hmacTenantVerifier: () => hmacTenantVerifier,
|
|
137
139
|
indexWaitersByRun: () => indexWaitersByRun,
|
|
138
140
|
info: () => info,
|
|
139
141
|
isEngineMintedTag: () => isEngineMintedTag,
|
|
@@ -171,6 +173,8 @@ __export(index_exports, {
|
|
|
171
173
|
resolveConcurrency: () => resolveConcurrency,
|
|
172
174
|
resolveRouting: () => resolveRouting,
|
|
173
175
|
resolveRunWaiting: () => resolveRunWaiting,
|
|
176
|
+
resolveTenantToken: () => resolveTenantToken,
|
|
177
|
+
resolveWireTenant: () => resolveWireTenant,
|
|
174
178
|
retiredHandoffName: () => retiredHandoffName,
|
|
175
179
|
routeFor: () => routeFor,
|
|
176
180
|
runDetailEtag: () => runDetailEtag,
|
|
@@ -183,7 +187,9 @@ __export(index_exports, {
|
|
|
183
187
|
runStepHandler: () => runStepHandler,
|
|
184
188
|
runValueFacetsFromRuns: () => runValueFacetsFromRuns,
|
|
185
189
|
sanitizeQueueToken: () => sanitizeQueueToken,
|
|
190
|
+
scheduleRunIdAt: () => scheduleRunIdAt,
|
|
186
191
|
scheduledRunId: () => scheduledRunId,
|
|
192
|
+
signTenantToken: () => signTenantToken,
|
|
187
193
|
singletonAdmissionDenialPatch: () => singletonAdmissionDenialPatch,
|
|
188
194
|
singletonAdmissionTags: () => singletonAdmissionTags,
|
|
189
195
|
sqlComparator: () => sqlComparator,
|
|
@@ -481,6 +487,31 @@ var FatalError = class extends Error {
|
|
|
481
487
|
this.details = details;
|
|
482
488
|
}
|
|
483
489
|
};
|
|
490
|
+
var SchemaValidationError = class extends FatalError {
|
|
491
|
+
static {
|
|
492
|
+
__name(this, "SchemaValidationError");
|
|
493
|
+
}
|
|
494
|
+
static code = "schema_validation";
|
|
495
|
+
issues;
|
|
496
|
+
/**
|
|
497
|
+
* @param label What was validated, e.g. `workflow "checkout" input`; the message is
|
|
498
|
+
* `invalid <label>: <path>: <message>; …`.
|
|
499
|
+
* @param summary Overrides the `<path>: <message>; …` part, for issues whose messages already
|
|
500
|
+
* name their field (class-validator's `orderId must be a string`).
|
|
501
|
+
*/
|
|
502
|
+
constructor(label, issues, code = "schema_validation", summary) {
|
|
503
|
+
const normalized = issues.map((issue) => ({
|
|
504
|
+
message: issue.message,
|
|
505
|
+
path: (issue.path ?? []).map((segment) => String(typeof segment === "object" && segment !== null ? segment.key : segment))
|
|
506
|
+
}));
|
|
507
|
+
const text = summary ?? normalized.map((issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message).join("; ");
|
|
508
|
+
super(`invalid ${label}: ${text}`, code, {
|
|
509
|
+
issues: normalized
|
|
510
|
+
});
|
|
511
|
+
this.name = "SchemaValidationError";
|
|
512
|
+
this.issues = normalized;
|
|
513
|
+
}
|
|
514
|
+
};
|
|
484
515
|
var ConcurrencyLimitError = class extends Error {
|
|
485
516
|
static {
|
|
486
517
|
__name(this, "ConcurrencyLimitError");
|
|
@@ -2212,9 +2243,14 @@ function liveHandoff(runId, checkpoints) {
|
|
|
2212
2243
|
return live;
|
|
2213
2244
|
}
|
|
2214
2245
|
__name(liveHandoff, "liveHandoff");
|
|
2246
|
+
var SCHEMA_VALIDATION_CODES = /* @__PURE__ */ new Set([
|
|
2247
|
+
"invalid_input",
|
|
2248
|
+
"invalid_output"
|
|
2249
|
+
]);
|
|
2215
2250
|
function routeFor(routes, error2) {
|
|
2216
2251
|
if (!routes?.length || error2.code && UNROUTABLE_CODES.has(error2.code)) return void 0;
|
|
2217
|
-
|
|
2252
|
+
const { code } = error2;
|
|
2253
|
+
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);
|
|
2218
2254
|
}
|
|
2219
2255
|
__name(routeFor, "routeFor");
|
|
2220
2256
|
function assertRoutes(workflow, routes) {
|
|
@@ -2486,9 +2522,13 @@ __name(scheduleRunIdsAt, "scheduleRunIdsAt");
|
|
|
2486
2522
|
async function runSchedules(engine, schedules, nowMs, opts) {
|
|
2487
2523
|
const random = opts?.random ?? Math.random;
|
|
2488
2524
|
const sleep = opts?.sleep ?? defaultSleep;
|
|
2525
|
+
const settled = settledIdsFor(engine);
|
|
2489
2526
|
const ids = [];
|
|
2490
2527
|
for (const s of schedules) {
|
|
2528
|
+
if (engine.isDraining?.()) break;
|
|
2491
2529
|
if (s.paused) continue;
|
|
2530
|
+
const candidates = scheduleRunIdsAt(s, nowMs).filter((id) => !settled.get(s.key)?.has(id));
|
|
2531
|
+
if (candidates.length === 0) continue;
|
|
2492
2532
|
if (s.overlap === "skip" && s.everyMs) {
|
|
2493
2533
|
const prevBucket = Math.floor(nowMs / s.everyMs) - 1;
|
|
2494
2534
|
if (prevBucket >= 0) {
|
|
@@ -2497,14 +2537,44 @@ async function runSchedules(engine, schedules, nowMs, opts) {
|
|
|
2497
2537
|
}
|
|
2498
2538
|
}
|
|
2499
2539
|
if (s.jitter && s.jitter > 0) await sleep(Math.floor(random() * s.jitter));
|
|
2500
|
-
for (const runId of
|
|
2501
|
-
|
|
2540
|
+
for (const runId of candidates) {
|
|
2541
|
+
if (engine.isDraining?.()) break;
|
|
2542
|
+
const existing = await engine.getRun(runId);
|
|
2543
|
+
remember(settled, s.key, runId);
|
|
2544
|
+
if (existing) continue;
|
|
2545
|
+
await engine.start(workflowName(s.workflow), s.input, runId, {
|
|
2546
|
+
namespace: s.namespace
|
|
2547
|
+
});
|
|
2502
2548
|
ids.push(runId);
|
|
2503
2549
|
}
|
|
2504
2550
|
}
|
|
2505
2551
|
return ids;
|
|
2506
2552
|
}
|
|
2507
2553
|
__name(runSchedules, "runSchedules");
|
|
2554
|
+
var settledRunIds = /* @__PURE__ */ new WeakMap();
|
|
2555
|
+
function settledIdsFor(engine) {
|
|
2556
|
+
let memo = settledRunIds.get(engine);
|
|
2557
|
+
if (memo === void 0) {
|
|
2558
|
+
memo = /* @__PURE__ */ new Map();
|
|
2559
|
+
settledRunIds.set(engine, memo);
|
|
2560
|
+
}
|
|
2561
|
+
return memo;
|
|
2562
|
+
}
|
|
2563
|
+
__name(settledIdsFor, "settledIdsFor");
|
|
2564
|
+
function remember(memo, key, runId) {
|
|
2565
|
+
const seen = memo.get(key);
|
|
2566
|
+
if (seen === void 0) {
|
|
2567
|
+
memo.set(key, /* @__PURE__ */ new Set([
|
|
2568
|
+
runId
|
|
2569
|
+
]));
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
seen.add(runId);
|
|
2573
|
+
if (seen.size > 64) memo.set(key, /* @__PURE__ */ new Set([
|
|
2574
|
+
runId
|
|
2575
|
+
]));
|
|
2576
|
+
}
|
|
2577
|
+
__name(remember, "remember");
|
|
2508
2578
|
|
|
2509
2579
|
// src/schedules.ts
|
|
2510
2580
|
var ScheduleAlreadyExistsError = class extends Error {
|
|
@@ -3080,6 +3150,73 @@ async function stalledInfo(store, run, now, thresholdMs, ageMs) {
|
|
|
3080
3150
|
}
|
|
3081
3151
|
__name(stalledInfo, "stalledInfo");
|
|
3082
3152
|
|
|
3153
|
+
// src/tenant-auth.ts
|
|
3154
|
+
var import_node_crypto = require("crypto");
|
|
3155
|
+
var TOKEN_SEP = ".";
|
|
3156
|
+
var EXP_SUFFIX = /\.exp(\d+)$/;
|
|
3157
|
+
function sign(claim, secret) {
|
|
3158
|
+
return (0, import_node_crypto.createHmac)("sha256", secret).update(claim).digest("base64url");
|
|
3159
|
+
}
|
|
3160
|
+
__name(sign, "sign");
|
|
3161
|
+
function signTenantToken(tenant, secret, opts) {
|
|
3162
|
+
const claim = opts?.ttlMs !== void 0 ? `${tenant}${TOKEN_SEP}exp${(opts.now ?? Date.now()) + opts.ttlMs}` : tenant;
|
|
3163
|
+
return `${claim}${TOKEN_SEP}${sign(claim, secret)}`;
|
|
3164
|
+
}
|
|
3165
|
+
__name(signTenantToken, "signTenantToken");
|
|
3166
|
+
function hmacTenantVerifier(secret, capabilities) {
|
|
3167
|
+
const secrets = Array.isArray(secret) ? secret : [
|
|
3168
|
+
secret
|
|
3169
|
+
];
|
|
3170
|
+
return ({ token }) => {
|
|
3171
|
+
if (typeof token !== "string" || token.length === 0) return null;
|
|
3172
|
+
const sep = token.lastIndexOf(TOKEN_SEP);
|
|
3173
|
+
if (sep <= 0 || sep === token.length - 1) return null;
|
|
3174
|
+
const claim = token.slice(0, sep);
|
|
3175
|
+
const presented = token.slice(sep + 1);
|
|
3176
|
+
if (!secrets.some((s) => constantTimeEquals(presented, sign(claim, s)))) return null;
|
|
3177
|
+
const exp = EXP_SUFFIX.exec(claim);
|
|
3178
|
+
if (exp) {
|
|
3179
|
+
const expiresAt = Number(exp[1]);
|
|
3180
|
+
if (!Number.isFinite(expiresAt) || Date.now() > expiresAt) return null;
|
|
3181
|
+
const tenant = claim.slice(0, claim.length - (exp[0]?.length ?? 0));
|
|
3182
|
+
if (tenant.length === 0) return null;
|
|
3183
|
+
return capabilities !== void 0 ? {
|
|
3184
|
+
tenant,
|
|
3185
|
+
capabilities
|
|
3186
|
+
} : {
|
|
3187
|
+
tenant
|
|
3188
|
+
};
|
|
3189
|
+
}
|
|
3190
|
+
return capabilities !== void 0 ? {
|
|
3191
|
+
tenant: claim,
|
|
3192
|
+
capabilities
|
|
3193
|
+
} : {
|
|
3194
|
+
tenant: claim
|
|
3195
|
+
};
|
|
3196
|
+
};
|
|
3197
|
+
}
|
|
3198
|
+
__name(hmacTenantVerifier, "hmacTenantVerifier");
|
|
3199
|
+
async function resolveWireTenant(wireTenant, verifyTenant) {
|
|
3200
|
+
if (!verifyTenant) return wireTenant;
|
|
3201
|
+
const verified = await verifyTenant({
|
|
3202
|
+
token: wireTenant,
|
|
3203
|
+
tenant: wireTenant
|
|
3204
|
+
});
|
|
3205
|
+
return verified ? verified.tenant : null;
|
|
3206
|
+
}
|
|
3207
|
+
__name(resolveWireTenant, "resolveWireTenant");
|
|
3208
|
+
async function resolveTenantToken(source) {
|
|
3209
|
+
return typeof source === "function" ? source() : source;
|
|
3210
|
+
}
|
|
3211
|
+
__name(resolveTenantToken, "resolveTenantToken");
|
|
3212
|
+
function constantTimeEquals(a, b) {
|
|
3213
|
+
const ab = Buffer.from(a);
|
|
3214
|
+
const bb = Buffer.from(b);
|
|
3215
|
+
if (ab.length !== bb.length) return false;
|
|
3216
|
+
return (0, import_node_crypto.timingSafeEqual)(ab, bb);
|
|
3217
|
+
}
|
|
3218
|
+
__name(constantTimeEquals, "constantTimeEquals");
|
|
3219
|
+
|
|
3083
3220
|
// src/transport-pool.ts
|
|
3084
3221
|
var TransportPool = class {
|
|
3085
3222
|
static {
|
|
@@ -3303,11 +3440,6 @@ function normalizeChildOptions(options) {
|
|
|
3303
3440
|
} : options ?? {};
|
|
3304
3441
|
}
|
|
3305
3442
|
__name(normalizeChildOptions, "normalizeChildOptions");
|
|
3306
|
-
function formatIssuePath(path) {
|
|
3307
|
-
if (!path || path.length === 0) return "(root)";
|
|
3308
|
-
return path.map((segment) => String(typeof segment === "object" ? segment.key : segment)).join(".");
|
|
3309
|
-
}
|
|
3310
|
-
__name(formatIssuePath, "formatIssuePath");
|
|
3311
3443
|
function resolveStepPolicy(config, opts) {
|
|
3312
3444
|
const policy = {};
|
|
3313
3445
|
const retries = opts?.retries ?? config?.retries;
|
|
@@ -4139,11 +4271,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
4139
4271
|
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 }).`);
|
|
4140
4272
|
}
|
|
4141
4273
|
if (result.issues) {
|
|
4142
|
-
|
|
4143
|
-
...new Set(result.issues.map((issue) => formatIssuePath(issue.path)))
|
|
4144
|
-
];
|
|
4145
|
-
const details = result.issues.map((issue) => `${formatIssuePath(issue.path)}: ${issue.message}`).join("; ");
|
|
4146
|
-
throw new Error(`workflow "${workflow}": ctx.upsertSearchAttributes rejected by its searchAttributes schema \u2014 offending key(s) ${offendingKeys.join(", ")} \u2014 ${details}`);
|
|
4274
|
+
throw new SchemaValidationError(`workflow "${workflow}" ctx.upsertSearchAttributes`, result.issues);
|
|
4147
4275
|
}
|
|
4148
4276
|
}
|
|
4149
4277
|
await host.upsertSearchAttributes(runId, attrs);
|
|
@@ -4565,9 +4693,20 @@ var WorkflowEngine = class {
|
|
|
4565
4693
|
await this.completeRemoteDecision(decision);
|
|
4566
4694
|
}
|
|
4567
4695
|
);
|
|
4696
|
+
const verifyTenant = deps.verifyTenant;
|
|
4568
4697
|
this.pool.onStartRun(async (message) => {
|
|
4698
|
+
let tenant;
|
|
4699
|
+
try {
|
|
4700
|
+
tenant = await resolveWireTenant(message.tenant, verifyTenant);
|
|
4701
|
+
} catch {
|
|
4702
|
+
tenant = null;
|
|
4703
|
+
}
|
|
4704
|
+
if (tenant === null) {
|
|
4705
|
+
console.warn(`[nestjs-durable] dropped a start-run for "${message.workflow}": invalid or unauthorized tenant token`);
|
|
4706
|
+
return;
|
|
4707
|
+
}
|
|
4569
4708
|
await this.start(message.workflow, message.input, message.runId ?? globalThis.crypto.randomUUID(), {
|
|
4570
|
-
namespace:
|
|
4709
|
+
namespace: tenant,
|
|
4571
4710
|
tags: message.tags,
|
|
4572
4711
|
searchAttributes: message.searchAttributes
|
|
4573
4712
|
});
|
|
@@ -5349,6 +5488,10 @@ var WorkflowEngine = class {
|
|
|
5349
5488
|
for (const rearm of this.drainRearms) rearm();
|
|
5350
5489
|
}
|
|
5351
5490
|
}
|
|
5491
|
+
/** True once {@link beginDrain} (or {@link drain}) has been called. */
|
|
5492
|
+
isDraining() {
|
|
5493
|
+
return this.draining;
|
|
5494
|
+
}
|
|
5352
5495
|
/**
|
|
5353
5496
|
* Graceful shutdown: stop picking up new runs (recovery/timer become no-ops) and wait for
|
|
5354
5497
|
* in-flight executions to settle, up to `timeoutMs`. Call from your app's shutdown hook so a
|
|
@@ -10940,6 +11083,7 @@ var InMemoryTransport = class {
|
|
|
10940
11083
|
ScheduleAlreadyExistsError,
|
|
10941
11084
|
ScheduleClient,
|
|
10942
11085
|
ScheduleNotFoundError,
|
|
11086
|
+
SchemaValidationError,
|
|
10943
11087
|
SignalTimeoutError,
|
|
10944
11088
|
SingletonQueueFullError,
|
|
10945
11089
|
TERMINAL_RUN_STATUSES,
|
|
@@ -11002,6 +11146,7 @@ var InMemoryTransport = class {
|
|
|
11002
11146
|
handoffName,
|
|
11003
11147
|
hasDurableExecutionWrappers,
|
|
11004
11148
|
heartbeatStatus,
|
|
11149
|
+
hmacTenantVerifier,
|
|
11005
11150
|
indexWaitersByRun,
|
|
11006
11151
|
info,
|
|
11007
11152
|
isEngineMintedTag,
|
|
@@ -11039,6 +11184,8 @@ var InMemoryTransport = class {
|
|
|
11039
11184
|
resolveConcurrency,
|
|
11040
11185
|
resolveRouting,
|
|
11041
11186
|
resolveRunWaiting,
|
|
11187
|
+
resolveTenantToken,
|
|
11188
|
+
resolveWireTenant,
|
|
11042
11189
|
retiredHandoffName,
|
|
11043
11190
|
routeFor,
|
|
11044
11191
|
runDetailEtag,
|
|
@@ -11051,7 +11198,9 @@ var InMemoryTransport = class {
|
|
|
11051
11198
|
runStepHandler,
|
|
11052
11199
|
runValueFacetsFromRuns,
|
|
11053
11200
|
sanitizeQueueToken,
|
|
11201
|
+
scheduleRunIdAt,
|
|
11054
11202
|
scheduledRunId,
|
|
11203
|
+
signTenantToken,
|
|
11055
11204
|
singletonAdmissionDenialPatch,
|
|
11056
11205
|
singletonAdmissionTags,
|
|
11057
11206
|
sqlComparator,
|