@blokjs/shared 2.0.1 → 2.1.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.
@@ -204,7 +204,9 @@ export default class BlokError extends GlobalError {
204
204
  export declare const WORKFLOW_INPUT_VALIDATION = "WORKFLOW_INPUT_VALIDATION";
205
205
  /**
206
206
  * True when `err` is the ADR-0015 **input-validation gate's** deterministic
207
- * failure — a `GlobalError` carrying the {@link WORKFLOW_INPUT_VALIDATION} tag.
207
+ * failure — a `GlobalError` carrying the {@link WORKFLOW_INPUT_VALIDATION} tag
208
+ * (in practice a {@link WorkflowInputValidationError}; the tag is what's matched
209
+ * so an error that crossed a serialization boundary still classifies).
208
210
  *
209
211
  * Triggers consult this to route the error to DLQ / drop / a 4xx response
210
212
  * instead of a poison-message loop (worker burning its retry budget, pub/sub
@@ -217,3 +219,32 @@ export declare const WORKFLOW_INPUT_VALIDATION = "WORKFLOW_INPUT_VALIDATION";
217
219
  * never surfaced by the webhook 4xx path.
218
220
  */
219
221
  export declare function isNonRetryableValidationError(err: unknown): boolean;
222
+ /** One Zod issue, flattened to plain data for the wire. */
223
+ export interface WorkflowInputValidationIssue {
224
+ path: (string | number)[];
225
+ message: string;
226
+ code: string;
227
+ }
228
+ /** What the ADR-0015 gate knows about the rejection. */
229
+ export interface WorkflowInputValidationInfo {
230
+ /** The workflow whose declared `input` schema rejected the payload. */
231
+ workflowName: string;
232
+ /** Every Zod issue, in schema order. */
233
+ issues: WorkflowInputValidationIssue[];
234
+ }
235
+ /**
236
+ * ADR 0015 — thrown by the input gate in `TriggerBase.run()` when the invoking
237
+ * trigger's payload fails the workflow's declared `input` Zod.
238
+ *
239
+ * Named + exported so callers can `instanceof` it, matching the vocabulary of
240
+ * the other gate errors (`ConcurrencyLimitError`, `QueueExpiredError`,
241
+ * `MapperResolutionError`). It extends `GlobalError` — carrying code 400, the
242
+ * {@link WORKFLOW_INPUT_VALIDATION} tag on `context.name`, and the structured
243
+ * `validation_errors` json — so every existing transport translation
244
+ * (HTTP 400, MCP `isError`, gRPC error status, worker/pubsub/webhook DLQ
245
+ * routing via {@link isNonRetryableValidationError}) keeps working untouched.
246
+ */
247
+ export declare class WorkflowInputValidationError extends GlobalError {
248
+ readonly info: WorkflowInputValidationInfo;
249
+ constructor(info: WorkflowInputValidationInfo);
250
+ }
package/dist/BlokError.js CHANGED
@@ -335,7 +335,9 @@ export default class BlokError extends GlobalError {
335
335
  export const WORKFLOW_INPUT_VALIDATION = "WORKFLOW_INPUT_VALIDATION";
336
336
  /**
337
337
  * True when `err` is the ADR-0015 **input-validation gate's** deterministic
338
- * failure — a `GlobalError` carrying the {@link WORKFLOW_INPUT_VALIDATION} tag.
338
+ * failure — a `GlobalError` carrying the {@link WORKFLOW_INPUT_VALIDATION} tag
339
+ * (in practice a {@link WorkflowInputValidationError}; the tag is what's matched
340
+ * so an error that crossed a serialization boundary still classifies).
339
341
  *
340
342
  * Triggers consult this to route the error to DLQ / drop / a 4xx response
341
343
  * instead of a poison-message loop (worker burning its retry budget, pub/sub
@@ -350,3 +352,34 @@ export const WORKFLOW_INPUT_VALIDATION = "WORKFLOW_INPUT_VALIDATION";
350
352
  export function isNonRetryableValidationError(err) {
351
353
  return err instanceof GlobalError && err.context.name === WORKFLOW_INPUT_VALIDATION;
352
354
  }
355
+ /**
356
+ * ADR 0015 — thrown by the input gate in `TriggerBase.run()` when the invoking
357
+ * trigger's payload fails the workflow's declared `input` Zod.
358
+ *
359
+ * Named + exported so callers can `instanceof` it, matching the vocabulary of
360
+ * the other gate errors (`ConcurrencyLimitError`, `QueueExpiredError`,
361
+ * `MapperResolutionError`). It extends `GlobalError` — carrying code 400, the
362
+ * {@link WORKFLOW_INPUT_VALIDATION} tag on `context.name`, and the structured
363
+ * `validation_errors` json — so every existing transport translation
364
+ * (HTTP 400, MCP `isError`, gRPC error status, worker/pubsub/webhook DLQ
365
+ * routing via {@link isNonRetryableValidationError}) keeps working untouched.
366
+ */
367
+ export class WorkflowInputValidationError extends GlobalError {
368
+ info;
369
+ constructor(info) {
370
+ const summary = info.issues.map((i) => `${i.path.join(".") || "(root)"} (${i.message})`).join(", ");
371
+ super(`Input validation failed for workflow '${info.workflowName}': ${summary}`);
372
+ // `GlobalError`'s own constructor pins the prototype to GlobalError.prototype,
373
+ // so a subclass MUST re-pin or `instanceof WorkflowInputValidationError` is false.
374
+ Object.setPrototypeOf(this, WorkflowInputValidationError.prototype);
375
+ this.name = "WorkflowInputValidationError";
376
+ this.info = info;
377
+ this.setCode(400);
378
+ this.setName(WORKFLOW_INPUT_VALIDATION);
379
+ this.setJson({
380
+ error: "Input validation failed",
381
+ workflowName: info.workflowName,
382
+ validation_errors: info.issues,
383
+ });
384
+ }
385
+ }
@@ -2,7 +2,11 @@ export default class GlobalError extends Error {
2
2
  context = { message: "" };
3
3
  constructor(msg) {
4
4
  super(msg);
5
- Object.setPrototypeOf(this, GlobalError.prototype);
5
+ // Standard Error-subclass pattern: pin to new.target's prototype (the
6
+ // class actually being constructed), not GlobalError's own — otherwise
7
+ // this clobbers a subclass's correct prototype and `instanceof Subclass`
8
+ // silently fails for any subclass that doesn't re-pin itself (#736).
9
+ Object.setPrototypeOf(this, new.target.prototype);
6
10
  this.context.message = msg;
7
11
  }
8
12
  setCode(code) {
package/dist/index.d.ts CHANGED
@@ -6,30 +6,30 @@
6
6
  * published as a back-compat alias — existing imports keep working unchanged —
7
7
  * but new code should import from `@blokjs/core`.
8
8
  */
9
- import BlokError, { type BlokErrorOpts, DEFAULT_HTTP_STATUS, DEFAULT_RETRYABLE, ErrorCategory, ErrorSeverity, isNonRetryableValidationError, type NodeErrorPayload, WORKFLOW_INPUT_VALIDATION } from "./BlokError.js";
9
+ import BlokError, { type BlokErrorOpts, DEFAULT_HTTP_STATUS, DEFAULT_RETRYABLE, ErrorCategory, ErrorSeverity, isNonRetryableValidationError, type NodeErrorPayload, WORKFLOW_INPUT_VALIDATION, WorkflowInputValidationError, type WorkflowInputValidationInfo, type WorkflowInputValidationIssue } from "./BlokError.js";
10
10
  import GlobalError from "./GlobalError.js";
11
11
  import GlobalLogger from "./GlobalLogger.js";
12
12
  import { Metrics, type MetricsType } from "./Metrics.js";
13
13
  import NodeBase from "./NodeBase.js";
14
14
  import Trigger from "./Trigger.js";
15
- import ConfigContext from "./types/ConfigContext.js";
15
+ import type ConfigContext from "./types/ConfigContext.js";
16
16
  import type ConnectionContext from "./types/ConnectionContext.js";
17
- import Context from "./types/Context.js";
18
- import EnvContext from "./types/EnvContext.js";
19
- import ErrorContext from "./types/ErrorContext.js";
20
- import FunctionContext from "./types/FunctionContext.js";
21
- import LoggerContext from "./types/LoggerContext.js";
22
- import NodeConfigContext from "./types/NodeConfigContext.js";
23
- import RequestContext from "./types/RequestContext.js";
17
+ import type Context from "./types/Context.js";
18
+ import type EnvContext from "./types/EnvContext.js";
19
+ import type ErrorContext from "./types/ErrorContext.js";
20
+ import type FunctionContext from "./types/FunctionContext.js";
21
+ import type LoggerContext from "./types/LoggerContext.js";
22
+ import type NodeConfigContext from "./types/NodeConfigContext.js";
23
+ import type RequestContext from "./types/RequestContext.js";
24
24
  import { RESPOND_BRAND, type RespondEnvelope, isRespondEnvelope } from "./types/RespondEnvelope.js";
25
- import ResponseContext from "./types/ResponseContext.js";
26
- import StateContext from "./types/StateContext.js";
27
- import Step from "./types/Step.js";
25
+ import type ResponseContext from "./types/ResponseContext.js";
26
+ import type StateContext from "./types/StateContext.js";
27
+ import type Step from "./types/Step.js";
28
28
  import type StreamContext from "./types/StreamContext.js";
29
- import VarsContext from "./types/VarsContext.js";
29
+ import type VarsContext from "./types/VarsContext.js";
30
30
  import mapper from "./utils/Mapper.js";
31
31
  import { MapperResolutionError } from "./utils/MapperResolutionError.js";
32
32
  import MemoryUsage from "./utils/MemoryUsage.js";
33
33
  import { NamedMissingStateError } from "./utils/NamedMissingStateError.js";
34
- import { type StructuralRef, lowerRefs } from "./utils/lowerRefs.js";
35
- export { NodeBase, Context, RequestContext, ResponseContext, RESPOND_BRAND, type RespondEnvelope, isRespondEnvelope, EnvContext, ErrorContext, LoggerContext, ConfigContext, type ConnectionContext, type StreamContext, Trigger, NodeConfigContext, FunctionContext, StateContext, VarsContext, Step, GlobalLogger, GlobalError, BlokError, type BlokErrorOpts, type NodeErrorPayload, ErrorCategory, ErrorSeverity, DEFAULT_HTTP_STATUS, DEFAULT_RETRYABLE, isNonRetryableValidationError, WORKFLOW_INPUT_VALIDATION, Metrics, MemoryUsage, type MetricsType, mapper, MapperResolutionError, NamedMissingStateError, lowerRefs, type StructuralRef, };
34
+ import { type StructuralRef, type StructuralTpl, isStructuralRef, isStructuralTpl, lowerRefs } from "./utils/lowerRefs.js";
35
+ export { NodeBase, type Context, type RequestContext, type ResponseContext, RESPOND_BRAND, type RespondEnvelope, isRespondEnvelope, type EnvContext, type ErrorContext, type LoggerContext, type ConfigContext, type ConnectionContext, type StreamContext, Trigger, type NodeConfigContext, type FunctionContext, type StateContext, type VarsContext, type Step, GlobalLogger, GlobalError, BlokError, type BlokErrorOpts, type NodeErrorPayload, ErrorCategory, ErrorSeverity, DEFAULT_HTTP_STATUS, DEFAULT_RETRYABLE, isNonRetryableValidationError, WORKFLOW_INPUT_VALIDATION, WorkflowInputValidationError, type WorkflowInputValidationInfo, type WorkflowInputValidationIssue, Metrics, MemoryUsage, type MetricsType, mapper, MapperResolutionError, NamedMissingStateError, lowerRefs, isStructuralRef, isStructuralTpl, type StructuralRef, type StructuralTpl, };
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * published as a back-compat alias — existing imports keep working unchanged —
7
7
  * but new code should import from `@blokjs/core`.
8
8
  */
9
- import BlokError, { DEFAULT_HTTP_STATUS, DEFAULT_RETRYABLE, ErrorCategory, ErrorSeverity, isNonRetryableValidationError, WORKFLOW_INPUT_VALIDATION, } from "./BlokError.js";
9
+ import BlokError, { DEFAULT_HTTP_STATUS, DEFAULT_RETRYABLE, ErrorCategory, ErrorSeverity, isNonRetryableValidationError, WORKFLOW_INPUT_VALIDATION, WorkflowInputValidationError, } from "./BlokError.js";
10
10
  import GlobalError from "./GlobalError.js";
11
11
  import GlobalLogger from "./GlobalLogger.js";
12
12
  import { Metrics } from "./Metrics.js";
@@ -17,5 +17,9 @@ import mapper from "./utils/Mapper.js";
17
17
  import { MapperResolutionError } from "./utils/MapperResolutionError.js";
18
18
  import MemoryUsage from "./utils/MemoryUsage.js";
19
19
  import { NamedMissingStateError } from "./utils/NamedMissingStateError.js";
20
- import { lowerRefs } from "./utils/lowerRefs.js";
21
- export { NodeBase, RESPOND_BRAND, isRespondEnvelope, Trigger, GlobalLogger, GlobalError, BlokError, ErrorCategory, ErrorSeverity, DEFAULT_HTTP_STATUS, DEFAULT_RETRYABLE, isNonRetryableValidationError, WORKFLOW_INPUT_VALIDATION, Metrics, MemoryUsage, mapper, MapperResolutionError, NamedMissingStateError, lowerRefs, };
20
+ import { isStructuralRef, isStructuralTpl, lowerRefs } from "./utils/lowerRefs.js";
21
+ export { NodeBase, RESPOND_BRAND, isRespondEnvelope, Trigger, GlobalLogger, GlobalError, BlokError, ErrorCategory, ErrorSeverity, DEFAULT_HTTP_STATUS, DEFAULT_RETRYABLE, isNonRetryableValidationError, WORKFLOW_INPUT_VALIDATION, WorkflowInputValidationError, Metrics, MemoryUsage, mapper, MapperResolutionError, NamedMissingStateError, lowerRefs,
22
+ // The lowering pass's OWN predicates — exported so the normalizer's
23
+ // post-lowering total invariant (#707) tests the identical rule instead of
24
+ // a second hand-written copy that could drift from what `lowerRefs` lowers.
25
+ isStructuralRef, isStructuralTpl, };
@@ -14,7 +14,12 @@
14
14
  * BEFORE the Mapper, and compiles every structural `{$ref}` into EXACTLY the
15
15
  * wire string today's engine already resolves. The Mapper stays untouched.
16
16
  *
17
- * Scope: the STEP-INPUTS, TRIGGER-ROOT, and TPL positions.
17
+ * Scope: the STEP-INPUTS, TRIGGER-ROOT, and TPL positions — plus, since #707,
18
+ * the three RESOLVED-KEY positions (step `idempotencyKey`, trigger
19
+ * `concurrencyKey`, trigger `debounce.key`), which the normalizer feeds through
20
+ * this same encoder. The caller picks the positions; this pass is unaware of
21
+ * them, which is why control positions (`branch.when`, `forEach.in`,
22
+ * `switch.on`) are simply never handed to it.
18
23
  * - step input value: `"js/ctx.state.<root>" + path`
19
24
  * - `path` mapping: string segment → `.seg`, numeric → `[n]`,
20
25
  * empty `path: []` (whole-output ref) → `"js/ctx.state.<root>"`.
@@ -36,6 +41,25 @@ export interface StructuralRef {
36
41
  path?: (string | number)[];
37
42
  };
38
43
  }
44
+ /**
45
+ * Is `value` the reserved `{$ref}` sentinel — a single-key object whose only
46
+ * key is `$ref` and whose `$ref.step` is a string?
47
+ *
48
+ * The single-key + string-step guard is what makes `$ref` safe to reserve:
49
+ * a step-inputs object that legitimately carries unrelated data (even data
50
+ * with the literal keys `step`/`path`) is NOT a ref and passes through
51
+ * untouched. ADR 0001 confirmed no current workflow uses `$ref` as user data.
52
+ */
53
+ export declare function isStructuralRef(value: object): value is StructuralRef;
54
+ /**
55
+ * The structural template sentinel (#425) — a single-key `{$tpl: [...]}` whose
56
+ * segments alternate raw strings and `{$ref}` nodes (plus the occasional literal
57
+ * non-string interpolation). Mirrors `tpl\`...\`` in `core/runner/src/stepBuilder.ts`.
58
+ */
59
+ export interface StructuralTpl {
60
+ $tpl: unknown[];
61
+ }
62
+ export declare function isStructuralTpl(value: object): value is StructuralTpl;
39
63
  /**
40
64
  * Recursively lower every structural `{$ref}` inside `value` to its wire
41
65
  * string. Pure — returns a NEW value, never mutates the input. Plain
@@ -14,7 +14,12 @@
14
14
  * BEFORE the Mapper, and compiles every structural `{$ref}` into EXACTLY the
15
15
  * wire string today's engine already resolves. The Mapper stays untouched.
16
16
  *
17
- * Scope: the STEP-INPUTS, TRIGGER-ROOT, and TPL positions.
17
+ * Scope: the STEP-INPUTS, TRIGGER-ROOT, and TPL positions — plus, since #707,
18
+ * the three RESOLVED-KEY positions (step `idempotencyKey`, trigger
19
+ * `concurrencyKey`, trigger `debounce.key`), which the normalizer feeds through
20
+ * this same encoder. The caller picks the positions; this pass is unaware of
21
+ * them, which is why control positions (`branch.when`, `forEach.in`,
22
+ * `switch.on`) are simply never handed to it.
18
23
  * - step input value: `"js/ctx.state.<root>" + path`
19
24
  * - `path` mapping: string segment → `.seg`, numeric → `[n]`,
20
25
  * empty `path: []` (whole-output ref) → `"js/ctx.state.<root>"`.
@@ -38,7 +43,7 @@
38
43
  * with the literal keys `step`/`path`) is NOT a ref and passes through
39
44
  * untouched. ADR 0001 confirmed no current workflow uses `$ref` as user data.
40
45
  */
41
- function isStructuralRef(value) {
46
+ export function isStructuralRef(value) {
42
47
  const keys = Object.keys(value);
43
48
  if (keys.length !== 1 || keys[0] !== "$ref")
44
49
  return false;
@@ -126,7 +131,7 @@ function refExpr(ref) {
126
131
  function lowerRef(ref) {
127
132
  return `js/${refExpr(ref)}`;
128
133
  }
129
- function isStructuralTpl(value) {
134
+ export function isStructuralTpl(value) {
130
135
  const keys = Object.keys(value);
131
136
  return keys.length === 1 && keys[0] === "$tpl" && Array.isArray(value.$tpl);
132
137
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blokjs/shared",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "files": ["dist"],
5
5
  "description": "Shared class, interfaces and types",
6
6
  "type": "module",