@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 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
- return (error2.code ? routes.find((r) => r.code === error2.code) : void 0) ?? routes.find((r) => r.code === void 0);
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 scheduleRunIdsAt(s, nowMs)) {
2501
- await engine.start(s.workflow, s.input, runId);
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
- const offendingKeys = [
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: message.tenant,
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,