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