@telorun/run 0.1.3 → 0.2.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,87 @@
1
1
  # @telorun/run
2
2
 
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [3c4ac58]
8
+ - @telorun/sdk@0.3.2
9
+
10
+ ## 0.2.0
11
+
12
+ ### Minor Changes
13
+
14
+ - 353d7e5: feat: invocable errors — structured error channel end-to-end
15
+
16
+ Invocables and runnables now have a first-class structured-error channel for domain failures (`InvokeError`), distinct from operational failures (plain `Error` / `RuntimeError`). Route handlers branch on named codes via `catches:`; sequences catch with `error.code` / `error.message` / `error.data` / `error.step` context.
17
+
18
+ **SDK** (`@telorun/sdk`)
19
+
20
+ - New `InvokeError` class + `isInvokeError` guard. Symbol-based discrimination (`Symbol.for("telo.InvokeError")`) is dual-realm-safe across pnpm hoist splits, registry modules, and future sandbox isolation.
21
+ - `ResourceDefinition.throws`: declared-throw contract (`codes` map, `inherit: true`, `passthrough: true`).
22
+ - `ResourceContext` / `EvaluationContext` gain `invokeResolved(kind, name, instance, inputs)` for callers that already hold a resolved instance.
23
+
24
+ **Kernel** (`@telorun/kernel`)
25
+
26
+ - Single emission point for invoke-level events: `Invoked` / `InvokeRejected` / `InvokeFailed` / `InvokeRejected.Undeclared`. All call paths (direct invoke, sequence scope path, HTTP route handler) route through the same wrapper.
27
+ - `Telo.Definition.throws:` schema with per-capability restrictions (rule 8: only on Invocable / Runnable).
28
+ - `resolveChildren` now auto-registers bare-kind inline refs when a resource name is supplied without an explicit name on the ref — lets stateless invocables like `Run.Throw` be used inline via `invoke: {kind: Run.Throw}`.
29
+
30
+ **Analyzer** (`@telorun/analyzer`)
31
+
32
+ - New dataflow resolver (`resolve-throws-union.ts`) for `inherit: true` / `passthrough: true` declarations. Walks `x-telo-step-context` arrays generically, applies `try`/`catch` subtraction, detects cycles, memoises per manifest.
33
+ - New coverage validator (`validate-throws-coverage.ts`) — rules 1/2/4/7 for `catches:` lists. Coverage-proving CEL parser recognises `error.code == 'X'`, disjunctions, and `error.code in [...]`. Typed `error.data.<field>` access against per-code `data:` schemas, with intersection narrowing for disjunctive `when:` clauses.
34
+ - New error codes: `UNDECLARED_THROW_CODE`, `UNCOVERED_THROW_CODE`, `UNBOUNDED_UNION_NEEDS_CATCHALL`, `CATCHALL_NOT_LAST`, `INHERIT_WITHOUT_STEP_CONTEXT`.
35
+
36
+ **Run module** (`@telorun/run`)
37
+
38
+ - `Run.Sequence` declares `throws: { inherit: true }`. Its effective union is resolved from step invocables at analysis time.
39
+ - New `Run.Throw` invocable: takes `{code, message, data?}` and throws `InvokeError`. Declared with `throws: { passthrough: true }`; the analyzer resolves constant / `error.code`-inside-catch forms at each call site.
40
+ - Sequence `try`/`catch` `error` context gains `data?: unknown` and now branches on `isInvokeError`.
41
+
42
+ **HTTP server module** (`@telorun/http-server`) — **breaking**
43
+
44
+ - Route-level `response:` is replaced by two channel lists: `returns:` (how to render handler results) and `catches:` (how to render `InvokeError` throws). Applies to both `Http.Api` routes and `Http.Server.notFoundHandler`.
45
+ - Plain `Error` / `RuntimeError` throws skip `catches:` and fall through to Fastify's default 5xx renderer — operational vs. domain failures are now distinct on the wire.
46
+ - `catches:` entries reject `mode: stream` at schema validation (structured errors always render as JSON).
47
+ - Unmatched `returns:` dispatch now throws (surfaces via Fastify's error handler) instead of rendering a silent 500.
48
+ - Every `response:` occurrence across the repo (apps, benchmarks, examples, tests) migrated to `returns:` — no manifest carries the old shape.
49
+
50
+ See `sdk/nodejs/plans/invocable-errors.md` for the full design and rollout phasing.
51
+
52
+ - 31d721e: feat: bearer-token auth for the Telo module registry publish endpoint
53
+
54
+ The registry's `PUT /{namespace}/{name}/{version}` now requires an `Authorization: Bearer <token>` header. Reads stay anonymous. Tokens are provisioned declaratively at boot via `TELO_PUBLISH_TOKEN` and stored as SHA-256 hashes in a `tokens` table joined to `users` and `namespaces`.
55
+
56
+ **Analyzer** (`@telorun/analyzer`) — **breaking for direct API consumers**
57
+
58
+ - `StaticAnalyzer` and `Loader` now accept an optional `{ celHandlers }` in their constructors. Analyzer-only callers (VS Code extension, Docusaurus preview, CLI `check`/`publish`) can omit it and get throwing stubs. Runtime callers (kernel) must supply real handlers.
59
+ - The module-level `celEnvironment` singleton is removed — `precompile.ts` now takes the `Environment` as a parameter.
60
+ - New CEL stdlib function: `sha256(string): string`. Always registered with the correct signature so `env.check()` type-checks; behaviour depends on the supplied handler.
61
+ - The throws-union resolver recognises the new `throw:` step shape (see Run module) and resolves its code at the call site using the same rules as passthrough invocables (literal / `${{ 'LIT' }}` / `${{ error.code }}` in catch).
62
+ - CEL type-check failures now surface as diagnostics. Previously the analyzer only reported schema/type mismatches on valid expressions; `env.check(...)` returning `{ valid: false }` (wrong method, wrong operand types, wrong overload — e.g. `s.slice(7)` on a dyn) was silently dropped. Now surfaces as `SCHEMA_VIOLATION` with a `CEL type error:` message.
63
+
64
+ **Kernel** (`@telorun/kernel`)
65
+
66
+ - Constructs `StaticAnalyzer` and `Loader` with a `node:crypto`-backed `sha256` handler, so CEL templates invoking `sha256()` evaluate at runtime.
67
+
68
+ **Run module** (`@telorun/run`) — **breaking**
69
+
70
+ - `Run.Sequence` gains a first-class `throw:` step variant: `- name: X; throw: { code, message?, data? }` — throws `InvokeError` directly from inside the sequence. Works inside `catch:` blocks via `code: "${{ error.code }}"` for re-raise. A malformed `throw.code` (non-string or empty after expansion) is itself reported as `InvokeError("INVALID_THROW_STEP", …)` rather than a plain Error, so the failure stays in the structured-error channel and a surrounding `catches:` can map it.
71
+ - The `Run.Throw` invocable is removed. Existing `invoke: { kind: Run.Throw }` call sites must migrate to `throw:` steps. The separate kind was redundant with the new step form, and the `throw:` step expresses the intent more directly inside sequences.
72
+ - **Event-stream change:** `throw:` steps do **not** emit a scoped `<Kind>.<name>.InvokeRejected` event the way `Run.Throw` did. The error is thrown from inside the sequence's own `invoke()`, so the enclosing kind's event is what fires (e.g. `Run.Sequence.<handlerName>.InvokeRejected` — or nothing, when an enclosing `try` absorbs the throw). Downstream observers that filtered on `Run.Throw.*.InvokeRejected` must switch filters.
73
+
74
+ **CLI** (`@telorun/cli`)
75
+
76
+ - `telo publish` reads `TELO_REGISTRY_TOKEN` and sends it as `Authorization: Bearer <token>`. Without the env var, publishes to auth-gated registries fail with 401.
77
+
78
+ See `apps/registry/plans/registry-auth.md` for the full plan.
79
+
80
+ ### Patch Changes
81
+
82
+ - Updated dependencies [353d7e5]
83
+ - @telorun/sdk@0.3.0
84
+
3
85
  ## 0.1.3
4
86
 
5
87
  ### Patch Changes
package/README.md CHANGED
@@ -34,7 +34,7 @@ Manifests also support directives for dynamic generation: `$let`, `$if`, `$for`,
34
34
  Here is an example Telo application that defines a simple HTTP API:
35
35
 
36
36
  ```yaml
37
- kind: Kernel.Module
37
+ kind: Telo.Application
38
38
  metadata:
39
39
  name: feedback
40
40
  version: 1.0.0
@@ -45,12 +45,12 @@ targets:
45
45
  - Migrations
46
46
  - Server
47
47
  ---
48
- kind: Kernel.Import
48
+ kind: Telo.Import
49
49
  metadata:
50
50
  name: Http
51
51
  source: ../modules/http-server
52
52
  ---
53
- kind: Kernel.Import
53
+ kind: Telo.Import
54
54
  metadata:
55
55
  name: Sql
56
56
  source: ../modules/sql
@@ -1,4 +1,4 @@
1
- import type { Invocable, KindRef, ResourceContext, ScopeHandle } from "@telorun/sdk";
1
+ import { type Invocable, type KindRef, type ResourceContext, type ScopeHandle } from "@telorun/sdk";
2
2
  interface RetryPolicy {
3
3
  attempts?: number;
4
4
  delay?: string;
@@ -38,7 +38,15 @@ interface TryStep {
38
38
  catch?: Step[];
39
39
  finally?: Step[];
40
40
  }
41
- type Step = InvokeStep | IfStep | WhileStep | SwitchStep | TryStep;
41
+ interface ThrowStep {
42
+ name: string;
43
+ throw: {
44
+ code: string;
45
+ message?: string;
46
+ data?: unknown;
47
+ };
48
+ }
49
+ type Step = InvokeStep | IfStep | WhileStep | SwitchStep | TryStep | ThrowStep;
42
50
  interface RunSequenceManifest {
43
51
  metadata: Record<string, string | number | boolean>;
44
52
  with?: ScopeHandle;
@@ -64,6 +72,7 @@ declare class RunSequence {
64
72
  private executeIfStep;
65
73
  private executeWhileStep;
66
74
  private executeSwitchStep;
75
+ private executeThrowStep;
67
76
  private executeTryStep;
68
77
  }
69
78
  export declare function register(): void;
package/dist/sequence.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { InvokeError, isInvokeError, } from "@telorun/sdk";
1
2
  function isInvokeStep(step) {
2
3
  return "invoke" in step;
3
4
  }
@@ -13,6 +14,9 @@ function isSwitchStep(step) {
13
14
  function isTryStep(step) {
14
15
  return "try" in step;
15
16
  }
17
+ function isThrowStep(step) {
18
+ return "throw" in step;
19
+ }
16
20
  class RunSequence {
17
21
  ctx;
18
22
  resource;
@@ -120,6 +124,8 @@ class RunSequence {
120
124
  await this.executeSwitchStep(step, steps, scope, extraCtx);
121
125
  else if (isTryStep(step))
122
126
  await this.executeTryStep(step, steps, scope, extraCtx);
127
+ else if (isThrowStep(step))
128
+ this.executeThrowStep(step, steps, extraCtx);
123
129
  else
124
130
  throw new Error(`Step "${step.name}" has no recognized type key`);
125
131
  }
@@ -136,7 +142,8 @@ class RunSequence {
136
142
  else {
137
143
  const ref = raw;
138
144
  if (scope) {
139
- result = await scope.getInstance(ref.name).invoke(inputs);
145
+ const instance = scope.getInstance(ref.name);
146
+ result = await this.ctx.invokeResolved(ref.kind, ref.name, instance, inputs);
140
147
  }
141
148
  else {
142
149
  result = await this.ctx.invoke(ref.kind, ref.name, inputs, { retry: step.retry });
@@ -178,6 +185,19 @@ class RunSequence {
178
185
  throw new Error(`Switch step "${step.name}": no matching case for "${key}" and no default`);
179
186
  }
180
187
  }
188
+ executeThrowStep(step, steps, extraCtx) {
189
+ const cel = { steps, ...extraCtx };
190
+ const expanded = this.ctx.expandValue(step.throw, cel);
191
+ const code = expanded?.code;
192
+ if (typeof code !== "string" || code.length === 0) {
193
+ // Structured error (not plain Error) so the failure stays in the InvokeError
194
+ // channel and a route's `catches:` list can still map it. The alternative —
195
+ // a plain Error — would skip catches: entirely and fall through to a 500.
196
+ throw new InvokeError("INVALID_THROW_STEP", `Run.Sequence step "${step.name}": throw.code is required and must resolve to a non-empty string`, { step: step.name, code });
197
+ }
198
+ const message = typeof expanded.message === "string" ? expanded.message : code;
199
+ throw new InvokeError(code, message, expanded.data);
200
+ }
181
201
  async executeTryStep(step, steps, scope, extraCtx) {
182
202
  if (step.when !== undefined && !this.ctx.expandValue(step.when, { steps, ...extraCtx }))
183
203
  return;
@@ -225,11 +245,11 @@ class RunSequence {
225
245
  }
226
246
  }
227
247
  function toSequenceError(err, stepName) {
248
+ if (isInvokeError(err)) {
249
+ return { message: err.message, code: err.code, data: err.data, step: stepName };
250
+ }
228
251
  const message = err instanceof Error ? err.message : String(err);
229
- const code = err instanceof Error && err.code != null
230
- ? err.code
231
- : null;
232
- return { message, code, step: stepName };
252
+ return { message, code: null, data: undefined, step: stepName };
233
253
  }
234
254
  export function register() { }
235
255
  export async function create(resource, ctx) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/run",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "description": "Telo Run module - Sequence execution for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -29,7 +29,7 @@
29
29
  }
30
30
  },
31
31
  "dependencies": {
32
- "@telorun/sdk": "0.2.8"
32
+ "@telorun/sdk": "0.3.2"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/node": "^20.0.0",
package/src/sequence.ts CHANGED
@@ -1,4 +1,13 @@
1
- import type { Invocable, KindRef, ResourceContext, ScopeContext, ScopeHandle } from "@telorun/sdk";
1
+ import {
2
+ InvokeError,
3
+ isInvokeError,
4
+ type Invocable,
5
+ type KindRef,
6
+ type ResourceContext,
7
+ type ResourceInstance,
8
+ type ScopeContext,
9
+ type ScopeHandle,
10
+ } from "@telorun/sdk";
2
11
 
3
12
  interface RetryPolicy {
4
13
  attempts?: number;
@@ -45,7 +54,16 @@ interface TryStep {
45
54
  finally?: Step[];
46
55
  }
47
56
 
48
- type Step = InvokeStep | IfStep | WhileStep | SwitchStep | TryStep;
57
+ interface ThrowStep {
58
+ name: string;
59
+ throw: {
60
+ code: string;
61
+ message?: string;
62
+ data?: unknown;
63
+ };
64
+ }
65
+
66
+ type Step = InvokeStep | IfStep | WhileStep | SwitchStep | TryStep | ThrowStep;
49
67
 
50
68
  interface RunSequenceManifest {
51
69
  metadata: Record<string, string | number | boolean>;
@@ -59,6 +77,7 @@ interface RunSequenceManifest {
59
77
  interface SequenceError {
60
78
  message: string;
61
79
  code: string | null;
80
+ data?: unknown;
62
81
  step: string;
63
82
  }
64
83
 
@@ -77,6 +96,9 @@ function isSwitchStep(step: Step): step is SwitchStep {
77
96
  function isTryStep(step: Step): step is TryStep {
78
97
  return "try" in step;
79
98
  }
99
+ function isThrowStep(step: Step): step is ThrowStep {
100
+ return "throw" in step;
101
+ }
80
102
 
81
103
  class RunSequence {
82
104
  constructor(
@@ -196,6 +218,7 @@ class RunSequence {
196
218
  else if (isWhileStep(step)) await this.executeWhileStep(step, steps, scope, extraCtx);
197
219
  else if (isSwitchStep(step)) await this.executeSwitchStep(step, steps, scope, extraCtx);
198
220
  else if (isTryStep(step)) await this.executeTryStep(step, steps, scope, extraCtx);
221
+ else if (isThrowStep(step)) this.executeThrowStep(step, steps, extraCtx);
199
222
  else throw new Error(`Step "${(step as Step).name}" has no recognized type key`);
200
223
  }
201
224
 
@@ -217,7 +240,8 @@ class RunSequence {
217
240
  } else {
218
241
  const ref = raw as KindRef<Invocable>;
219
242
  if (scope) {
220
- result = await (scope.getInstance(ref.name) as unknown as Invocable).invoke(inputs);
243
+ const instance = scope.getInstance(ref.name) as unknown as ResourceInstance;
244
+ result = await this.ctx.invokeResolved(ref.kind, ref.name, instance, inputs);
221
245
  } else {
222
246
  result = await this.ctx.invoke(ref.kind, ref.name, inputs, { retry: step.retry });
223
247
  }
@@ -278,6 +302,32 @@ class RunSequence {
278
302
  }
279
303
  }
280
304
 
305
+ private executeThrowStep(
306
+ step: ThrowStep,
307
+ steps: Record<string, unknown>,
308
+ extraCtx: Record<string, unknown>,
309
+ ): never {
310
+ const cel = { steps, ...extraCtx };
311
+ const expanded = this.ctx.expandValue(step.throw, cel) as {
312
+ code: unknown;
313
+ message?: unknown;
314
+ data?: unknown;
315
+ };
316
+ const code = expanded?.code;
317
+ if (typeof code !== "string" || code.length === 0) {
318
+ // Structured error (not plain Error) so the failure stays in the InvokeError
319
+ // channel and a route's `catches:` list can still map it. The alternative —
320
+ // a plain Error — would skip catches: entirely and fall through to a 500.
321
+ throw new InvokeError(
322
+ "INVALID_THROW_STEP",
323
+ `Run.Sequence step "${step.name}": throw.code is required and must resolve to a non-empty string`,
324
+ { step: step.name, code },
325
+ );
326
+ }
327
+ const message = typeof expanded.message === "string" ? expanded.message : code;
328
+ throw new InvokeError(code, message, expanded.data);
329
+ }
330
+
281
331
  private async executeTryStep(
282
332
  step: TryStep,
283
333
  steps: Record<string, unknown>,
@@ -329,12 +379,11 @@ class RunSequence {
329
379
  }
330
380
 
331
381
  function toSequenceError(err: unknown, stepName: string): SequenceError {
382
+ if (isInvokeError(err)) {
383
+ return { message: err.message, code: err.code, data: err.data, step: stepName };
384
+ }
332
385
  const message = err instanceof Error ? err.message : String(err);
333
- const code =
334
- err instanceof Error && (err as Error & { code?: string }).code != null
335
- ? (err as Error & { code?: string }).code!
336
- : null;
337
- return { message, code, step: stepName };
386
+ return { message, code: null, data: undefined, step: stepName };
338
387
  }
339
388
 
340
389
  export function register(): void {}