@telorun/http-server 0.3.4 → 0.5.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/CHANGELOG.md +107 -54
- package/LICENSE +2 -2
- package/README.md +79 -36
- package/dist/http-server-controller.d.ts +1 -0
- package/dist/http-server-controller.js +15 -2
- package/package.json +5 -3
- package/src/http-server-controller.ts +18 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,112 @@
|
|
|
1
1
|
# @telorun/http-server
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- b62e535: Streaming-Invocable convention, format-codec packages, and `Http.Api` `content:` map rewrite.
|
|
8
|
+
|
|
9
|
+
**Breaking** (`@telorun/http-server`, `@telorun/ai`):
|
|
10
|
+
|
|
11
|
+
- `Http.Api.routes[].returns[]` and `routes[].catches[]` (and the equivalent `Http.Server.notFoundHandler` lists) drop top-level `body` / `schema` in favour of a per-MIME `content:` map. Buffer-mode entries use `content[<mime>].body` / `content[<mime>].schema`; stream-mode entries use `content[<mime>].encoder` (ref to any `Codec.Encoder`). The map key is the canonical `Content-Type` — declaring `Content-Type` in `headers:` is rejected at load time. Multi-key `content:` maps are negotiated against the request's `Accept` header (RFC 9110 §12.5.1). Mismatch → `406 Not Acceptable`.
|
|
12
|
+
- `mode: stream` is forbidden in `catches:` (catches fire pre-stream; no upstream iterable to feed an encoder).
|
|
13
|
+
- Migration: every existing `returns: [..., body: ..., schema: ..., headers: { Content-Type: ... }]` rewrites mechanically to `returns: [..., content: { <mime>: { body, schema } }]`. In-tree manifests (`apps/registry`, `examples/*`, `tests/*`, `benchmarks/*`) migrated.
|
|
14
|
+
- `Ai.TextStream`: `format` field removed; controller no longer encodes the wire — it returns `{ output: Stream<StreamPart> }`. Pair with a format-codec encoder (`Ndjson.Encoder`, `Sse.Encoder`, `PlainText.Encoder`) for HTTP responses or other byte transports. `text-stream-drain-controller.ts` removed (replaced by inline source → encoder → decoder steps).
|
|
15
|
+
- `StreamPart.error` shape changed from native `Error` to `{ message, code?, data? }` so generic encoders can JSON-serialize error frames without bespoke translation.
|
|
16
|
+
|
|
17
|
+
**New** (`@telorun/codec`, `@telorun/plain-text-codec`, `@telorun/ndjson-codec`, `@telorun/sse-codec`, `@telorun/octet-codec`):
|
|
18
|
+
|
|
19
|
+
- `@telorun/codec` ships the `Encoder` and `Decoder` abstracts (no controllers — pure contracts).
|
|
20
|
+
- Format-codec packages each carry one or both directions: `PlainText.Encoder/.Decoder` (UTF-8 collect + emit), `Ndjson.Encoder` (one JSON record per line), `Sse.Encoder` (Server-Sent Events frames), `Octet.Encoder/.Decoder` (raw bytes pass-through and collect).
|
|
21
|
+
- All encoders implement `invoke({input}): Promise<{output: Stream<Uint8Array>}>` per the streaming-Invocable convention.
|
|
22
|
+
|
|
23
|
+
**New** (`@telorun/sdk`):
|
|
24
|
+
|
|
25
|
+
- `Stream<T>` class wrapping `AsyncIterable<T>`. Producers wrap their iterables in `new Stream(...)` so the value's constructor is recognized by CEL's runtime type-checker (which rejects unrecognized constructors like `AsyncGenerator` and Node `Readable`). The analyzer registers `Stream` as a CEL object type.
|
|
26
|
+
|
|
27
|
+
**Annotation** (`@telorun/kernel`, `@telorun/analyzer`):
|
|
28
|
+
|
|
29
|
+
- `x-telo-stream: true` schema annotation on input/output properties marks them as carrying a `Stream<T>`. CEL passes the value through by reference; analyzer's chain validator rejects `.field` / `[index]` access past a stream-marked property. Convention: streaming Invocables put the stream on `input` (inputs) and `output` (result).
|
|
30
|
+
- `Self.<Abstract>` magic alias auto-registered for every Telo.Library/Application — lets concrete kinds in the same library use `extends: Self.<Abstract>` without a self-import that would loop the loader.
|
|
31
|
+
- Analyzer's `buildReferenceFieldMap`, `resolveFieldValues`, `extractInlinesAtPath`, and `injectAtPath` (Phase 5) now recurse into `additionalProperties` via a `{}` path-segment marker. Required for refs nested inside open-keyed maps like `content[<mime>].encoder`.
|
|
32
|
+
- `isInlineResource` widened: bare-kind refs (`{kind: X}` with no `name` and no extra config) are now treated as inline-singleton definitions and Phase 2 extracts them as fresh stateless resources. Previously `{kind: X}` raised `INVALID_REFERENCE` (treated as a malformed named ref). This matches the runtime-side `resolveChildren` semantics already documented for `Run.Throw`-style stateless inlines, and lets `encoder: {kind: Ndjson.Encoder}` work without boilerplate. Manifests that had `{kind: X}` with the (broken) intent of resolving to an existing named resource will now silently extract a fresh resource — extremely unlikely in practice (those refs were already failing analysis), but worth flagging for downstream consumers.
|
|
33
|
+
|
|
34
|
+
**Behaviour changes worth flagging** (`@telorun/http-server`):
|
|
35
|
+
|
|
36
|
+
- **Single-key `content:` maps now do `Accept` negotiation.** A route declaring only `content: { application/json: ... }` returns `406 Not Acceptable` for `Accept: image/png` — RFC 9110 §15.5.7 compliant. Pre-PR, the legacy top-level `body:` shape ignored `Accept` entirely. To preserve "always send" behaviour, declare `*/*` as an explicit key.
|
|
37
|
+
- **Accept matching ignores media-type parameters** beyond the first `;`. `Accept: text/plain; charset=ascii` matches `content: { 'text/plain; charset=utf-8': ... }`. Q-values are still parsed for ranking; only the matching predicate ignores params. Authors needing parameter-level preference must declare distinct keys per parameter combo.
|
|
38
|
+
- **Load-time validators reject misconfigured `content:` shapes.** `validateContentEntryShape` rejects `body+encoder` together (mutually exclusive), missing `encoder` under `mode: stream`, `body` under `mode: stream`, and `encoder` under `mode: buffer`. Previously some of these slipped through to runtime where they manifested as 500-on-negotiation.
|
|
39
|
+
- **Mid-stream `pipeline()` failures emit `Http.Api.streamFailed` events.** Once `reply.hijack()` runs, mid-stream errors (encoder throws, broken pipe) bypass `catches:` by design (response is committed). They now emit a structured event with `path`, `method`, `status`, `mime`, and the error so operators can observe failures that would otherwise be silent.
|
|
40
|
+
|
|
41
|
+
**Other** (`@telorun/http-client`, `@telorun/javascript`):
|
|
42
|
+
|
|
43
|
+
- `HttpClient.Request` `mode: stream` returns `{ output: Stream<Uint8Array> }` instead of a bare `Readable` — fits the streaming-Invocable convention, pairs with `Octet.Encoder` for HTTP pass-through.
|
|
44
|
+
- `JS.Script` injects `Stream` into every script's scope (via the second function argument, destructured at the top of the wrapper). User code can `new Stream(asyncGen)` directly.
|
|
45
|
+
|
|
46
|
+
**Tests**:
|
|
47
|
+
|
|
48
|
+
- New Layer 1 hermetic streaming-contract test (`modules/ai/tests/text-stream-streaming-contract.yaml`) — three sub-targets, byte-exact NDJSON / SSE / PlainText.
|
|
49
|
+
- New Layer 2 live OpenAI streaming smoke (`modules/ai-openai/tests/openai-live-text-stream.yaml`) — env-gated; exercises `Ai.TextStream → Ndjson.Encoder → PlainText.Decoder` against the real provider.
|
|
50
|
+
- New http-server integration test (`modules/http-server/tests/text-stream-via-http.yaml`) — exercises three single-format routes plus a four-format negotiated route with five Accept variants.
|
|
51
|
+
|
|
52
|
+
### Minor Changes
|
|
53
|
+
|
|
54
|
+
- 0331069: Widen every "handler-shaped" `x-telo-ref` slot to accept both `telo#Invocable` and `telo#Runnable`, so dual-mode kinds — most commonly `Run.Sequence`, whose controller implements both `run()` and `invoke()` — pass static reference validation without each kind declaring secondary capabilities on its own definition.
|
|
55
|
+
|
|
56
|
+
Affected slots:
|
|
57
|
+
|
|
58
|
+
- `@telorun/http-server`: `Http.Server.parsers[].parser`, `Http.Server.notFoundHandler.invoke`, `Http.Api.routes[].handler`.
|
|
59
|
+
- `@telorun/mcp-server`: `Mcp.Tools.entries[].handler`, `Mcp.Resources.entries[].handler`, `Mcp.Prompts.entries[].handler`.
|
|
60
|
+
- `@telorun/lambda`: `Lambda.HttpApi.routes[].handler`, `Lambda.Sqs.handler`, `Lambda.Direct.handler`.
|
|
61
|
+
|
|
62
|
+
Mechanism: each slot's single `x-telo-ref: "telo#Invocable"` is replaced by an `anyOf:` block carrying both refs. The analyzer's reference-field-map walker already collects refs from `anyOf` branches and `checkKind` early-returns on the first match — so the union semantics are honoured without any analyzer change. AJV value-shape validation continues through the slot's existing `oneOf:` (string vs. object form), unchanged.
|
|
63
|
+
|
|
64
|
+
Runtime behaviour is unchanged: the kernel calls whichever method the handler's controller exposes (`.invoke()` or `.run()`). This release just lets the schema admit what the kernel already accepts.
|
|
65
|
+
|
|
66
|
+
### Patch Changes
|
|
67
|
+
|
|
68
|
+
- c0129c0: Align local `@telorun/http-server` version with the published `std/http-server@2.0.0` on the registry. The local manifest had diverged onto a parallel `1.x` line; this realigns the version stamp so the next publish bumps from `2.0.0` rather than backwards from `1.1.0`.
|
|
69
|
+
|
|
70
|
+
- be79957: Move `@telorun/sdk` to `peerDependencies` across the kernel, analyzer, templating, and every module.
|
|
71
|
+
|
|
72
|
+
The SDK carries the `Stream` class registered with `@marcbachmann/cel-js` for stream-typed CEL values. cel-js identifies object types by constructor identity, so a second copy of `@telorun/sdk` in the install tree silently breaks streaming-typed evaluations with `Unsupported type: Stream`. The contract was previously enforced with three layered mechanisms (a generated `dist/generated/runtime-deps.json` driving install-root `dependencies`, `overrides` + `pnpm.overrides` blocks, and a `globalThis`-keyed singleton in `stream.ts`); the build artifact silently degraded when the kernel was run without a build step, defeating the layering.
|
|
73
|
+
|
|
74
|
+
The new shape:
|
|
75
|
+
|
|
76
|
+
- Every package that imports `@telorun/sdk` declares it as a `peerDependency`. Consumers (the kernel's install root, the CLI, apps) provide a single copy and `peerDependencies` cause npm/pnpm to resolve every transitive import to it.
|
|
77
|
+
- The kernel's `NpmControllerLoader` no longer reads `runtime-deps.json`; the realm-collapse name list is a hardcoded constant (`REALM_COLLAPSE_NAMES = ["@telorun/sdk"]`) in `npm-loader.ts`. The install-root `package.json` it writes drops the `overrides` and `pnpm.overrides` blocks — peer-dep resolution makes them redundant.
|
|
78
|
+
- `scripts/generate-runtime-deps.mjs` and the generated artifact are removed; `scripts/prepack-bake-overrides.mjs` no longer chains the runtime-deps regeneration.
|
|
79
|
+
- The `globalThis` singleton in `sdk/nodejs/src/stream.ts` is **kept** as a safety net for environments that still end up with mismatched SDK copies (e.g. a controller install from a tarball that predates this change).
|
|
80
|
+
|
|
81
|
+
Consumers installing `@telorun/kernel` or any module directly must now ensure `@telorun/sdk` is present in their dependency tree. The kernel already lists it via the install root for any manifest it boots, so kernel-driven usage is unaffected.
|
|
82
|
+
|
|
83
|
+
- Updated dependencies [849f57a]
|
|
84
|
+
- Updated dependencies [be79957]
|
|
85
|
+
- @telorun/sdk@0.12.0
|
|
86
|
+
- @telorun/http-dispatch@0.3.0
|
|
87
|
+
|
|
88
|
+
- Updated dependencies [b62e535]
|
|
89
|
+
- @telorun/sdk@0.12.0
|
|
90
|
+
|
|
91
|
+
## 0.4.0
|
|
92
|
+
|
|
93
|
+
### Minor Changes
|
|
94
|
+
|
|
95
|
+
- 0f80fc5: `Bench.Suite.scenarios[*]` and `Http.Server.notFoundHandler` follow the canonical sibling shape: `invoke:` describes the dispatch target only; `inputs:` carries the call-time arguments as a sibling. The previously-accepted nested `invoke.inputs` form is gone — the benchmark runtime now reads `scenario.inputs` and the http-server runtime now reads `notFoundHandler.inputs`. Five benchmark manifests, one example, and `apps/registry/telo.yaml` migrated to the sibling form.
|
|
96
|
+
|
|
97
|
+
Statically validate CEL expressions inside `Telo.Definition` template bodies. The analyzer now registers `self` (typed from the definition's `schema:`) and `inputs` (typed from `inputType:`, falling back to the `extends:`-declared abstract's `inputType:`) as available variables in `resources:` / `invoke:` / `run:` / `provide:` / top-level `inputs:` / top-level `result:` fields, catching typos at load time instead of first invocation.
|
|
98
|
+
|
|
99
|
+
Aligns Telo.Definition's template-body shape with how Run.Sequence steps factor dispatch from data: `invoke:` / `provide:` / `run:` describe the dispatch target only; `inputs:` (values passed to the target) and `result:` (provide-only post-call mapping) live as top-level siblings on the definition. The previous nested `invoke.inputs` shape is gone — the kernel template controller now reads `definition.inputs`, and `modules/sql-repository/Read` migrates to the sibling form.
|
|
100
|
+
|
|
101
|
+
Inside top-level `result:`, the `result` CEL variable is typed from the dispatch target's `outputType:`. The produced top-level `result` value is also AJV-checked against the abstract this definition `extends` (`outputType`); top-level `inputs` is AJV-checked against the dispatch target's `inputType` when declared. Mismatches surface as a new `TEMPLATE_TARGET_MISMATCH` diagnostic.
|
|
102
|
+
|
|
103
|
+
Adds two reusable context-annotation forms used by the `Telo.Definition` builtin schema and available to any module that needs the same capabilities:
|
|
104
|
+
|
|
105
|
+
- `x-telo-context-from-root: "<path>"` — root-anchored navigation (replace semantics), used to type variables sourced from a top-level field regardless of where the CEL appears.
|
|
106
|
+
- `x-telo-context-from-ref-kind: "<refPath>#<field>"` — reads a kind name from `manifestRoot.<refPath>`, resolves it via the definition registry, and returns that kind's `<field>` schema.
|
|
107
|
+
|
|
108
|
+
Schema-extracted contexts are now sorted by scope specificity (longest first) so the first-match-wins resolver picks the most-specific context. No existing module relied on the previous ordering (no overlapping scopes), so this change is observably backward-compatible.
|
|
109
|
+
|
|
3
110
|
## 0.3.4
|
|
4
111
|
|
|
5
112
|
### Patch Changes
|
|
@@ -66,60 +173,6 @@
|
|
|
66
173
|
|
|
67
174
|
- d3ed5a5: Tighten `Http.Api.routes[].request.headers` to declare `additionalProperties: { type: "string" }`. Header values are matched as strings against the incoming request, so the schema now reflects what the runtime actually accepts. The telo editor renders this field as a key/value map editor instead of the JSON Schema designer.
|
|
68
175
|
|
|
69
|
-
## 1.0.0
|
|
70
|
-
|
|
71
|
-
### Major Changes
|
|
72
|
-
|
|
73
|
-
- b62e535: Streaming-Invocable convention, format-codec packages, and `Http.Api` `content:` map rewrite.
|
|
74
|
-
|
|
75
|
-
**Breaking** (`@telorun/http-server`, `@telorun/ai`):
|
|
76
|
-
|
|
77
|
-
- `Http.Api.routes[].returns[]` and `routes[].catches[]` (and the equivalent `Http.Server.notFoundHandler` lists) drop top-level `body` / `schema` in favour of a per-MIME `content:` map. Buffer-mode entries use `content[<mime>].body` / `content[<mime>].schema`; stream-mode entries use `content[<mime>].encoder` (ref to any `Codec.Encoder`). The map key is the canonical `Content-Type` — declaring `Content-Type` in `headers:` is rejected at load time. Multi-key `content:` maps are negotiated against the request's `Accept` header (RFC 9110 §12.5.1). Mismatch → `406 Not Acceptable`.
|
|
78
|
-
- `mode: stream` is forbidden in `catches:` (catches fire pre-stream; no upstream iterable to feed an encoder).
|
|
79
|
-
- Migration: every existing `returns: [..., body: ..., schema: ..., headers: { Content-Type: ... }]` rewrites mechanically to `returns: [..., content: { <mime>: { body, schema } }]`. In-tree manifests (`apps/registry`, `examples/*`, `tests/*`, `benchmarks/*`) migrated.
|
|
80
|
-
- `Ai.TextStream`: `format` field removed; controller no longer encodes the wire — it returns `{ output: Stream<StreamPart> }`. Pair with a format-codec encoder (`Ndjson.Encoder`, `Sse.Encoder`, `PlainText.Encoder`) for HTTP responses or other byte transports. `text-stream-drain-controller.ts` removed (replaced by inline source → encoder → decoder steps).
|
|
81
|
-
- `StreamPart.error` shape changed from native `Error` to `{ message, code?, data? }` so generic encoders can JSON-serialize error frames without bespoke translation.
|
|
82
|
-
|
|
83
|
-
**New** (`@telorun/codec`, `@telorun/plain-text-codec`, `@telorun/ndjson-codec`, `@telorun/sse-codec`, `@telorun/octet-codec`):
|
|
84
|
-
|
|
85
|
-
- `@telorun/codec` ships the `Encoder` and `Decoder` abstracts (no controllers — pure contracts).
|
|
86
|
-
- Format-codec packages each carry one or both directions: `PlainText.Encoder/.Decoder` (UTF-8 collect + emit), `Ndjson.Encoder` (one JSON record per line), `Sse.Encoder` (Server-Sent Events frames), `Octet.Encoder/.Decoder` (raw bytes pass-through and collect).
|
|
87
|
-
- All encoders implement `invoke({input}): Promise<{output: Stream<Uint8Array>}>` per the streaming-Invocable convention.
|
|
88
|
-
|
|
89
|
-
**New** (`@telorun/sdk`):
|
|
90
|
-
|
|
91
|
-
- `Stream<T>` class wrapping `AsyncIterable<T>`. Producers wrap their iterables in `new Stream(...)` so the value's constructor is recognized by CEL's runtime type-checker (which rejects unrecognized constructors like `AsyncGenerator` and Node `Readable`). The analyzer registers `Stream` as a CEL object type.
|
|
92
|
-
|
|
93
|
-
**Annotation** (`@telorun/kernel`, `@telorun/analyzer`):
|
|
94
|
-
|
|
95
|
-
- `x-telo-stream: true` schema annotation on input/output properties marks them as carrying a `Stream<T>`. CEL passes the value through by reference; analyzer's chain validator rejects `.field` / `[index]` access past a stream-marked property. Convention: streaming Invocables put the stream on `input` (inputs) and `output` (result).
|
|
96
|
-
- `Self.<Abstract>` magic alias auto-registered for every Telo.Library/Application — lets concrete kinds in the same library use `extends: Self.<Abstract>` without a self-import that would loop the loader.
|
|
97
|
-
- Analyzer's `buildReferenceFieldMap`, `resolveFieldValues`, `extractInlinesAtPath`, and `injectAtPath` (Phase 5) now recurse into `additionalProperties` via a `{}` path-segment marker. Required for refs nested inside open-keyed maps like `content[<mime>].encoder`.
|
|
98
|
-
- `isInlineResource` widened: bare-kind refs (`{kind: X}` with no `name` and no extra config) are now treated as inline-singleton definitions and Phase 2 extracts them as fresh stateless resources. Previously `{kind: X}` raised `INVALID_REFERENCE` (treated as a malformed named ref). This matches the runtime-side `resolveChildren` semantics already documented for `Run.Throw`-style stateless inlines, and lets `encoder: {kind: Ndjson.Encoder}` work without boilerplate. Manifests that had `{kind: X}` with the (broken) intent of resolving to an existing named resource will now silently extract a fresh resource — extremely unlikely in practice (those refs were already failing analysis), but worth flagging for downstream consumers.
|
|
99
|
-
|
|
100
|
-
**Behaviour changes worth flagging** (`@telorun/http-server`):
|
|
101
|
-
|
|
102
|
-
- **Single-key `content:` maps now do `Accept` negotiation.** A route declaring only `content: { application/json: ... }` returns `406 Not Acceptable` for `Accept: image/png` — RFC 9110 §15.5.7 compliant. Pre-PR, the legacy top-level `body:` shape ignored `Accept` entirely. To preserve "always send" behaviour, declare `*/*` as an explicit key.
|
|
103
|
-
- **Accept matching ignores media-type parameters** beyond the first `;`. `Accept: text/plain; charset=ascii` matches `content: { 'text/plain; charset=utf-8': ... }`. Q-values are still parsed for ranking; only the matching predicate ignores params. Authors needing parameter-level preference must declare distinct keys per parameter combo.
|
|
104
|
-
- **Load-time validators reject misconfigured `content:` shapes.** `validateContentEntryShape` rejects `body+encoder` together (mutually exclusive), missing `encoder` under `mode: stream`, `body` under `mode: stream`, and `encoder` under `mode: buffer`. Previously some of these slipped through to runtime where they manifested as 500-on-negotiation.
|
|
105
|
-
- **Mid-stream `pipeline()` failures emit `Http.Api.streamFailed` events.** Once `reply.hijack()` runs, mid-stream errors (encoder throws, broken pipe) bypass `catches:` by design (response is committed). They now emit a structured event with `path`, `method`, `status`, `mime`, and the error so operators can observe failures that would otherwise be silent.
|
|
106
|
-
|
|
107
|
-
**Other** (`@telorun/http-client`, `@telorun/javascript`):
|
|
108
|
-
|
|
109
|
-
- `HttpClient.Request` `mode: stream` returns `{ output: Stream<Uint8Array> }` instead of a bare `Readable` — fits the streaming-Invocable convention, pairs with `Octet.Encoder` for HTTP pass-through.
|
|
110
|
-
- `JS.Script` injects `Stream` into every script's scope (via the second function argument, destructured at the top of the wrapper). User code can `new Stream(asyncGen)` directly.
|
|
111
|
-
|
|
112
|
-
**Tests**:
|
|
113
|
-
|
|
114
|
-
- New Layer 1 hermetic streaming-contract test (`modules/ai/tests/text-stream-streaming-contract.yaml`) — three sub-targets, byte-exact NDJSON / SSE / PlainText.
|
|
115
|
-
- New Layer 2 live OpenAI streaming smoke (`modules/ai-openai/tests/openai-live-text-stream.yaml`) — env-gated; exercises `Ai.TextStream → Ndjson.Encoder → PlainText.Decoder` against the real provider.
|
|
116
|
-
- New http-server integration test (`modules/http-server/tests/text-stream-via-http.yaml`) — exercises three single-format routes plus a four-format negotiated route with five Accept variants.
|
|
117
|
-
|
|
118
|
-
### Patch Changes
|
|
119
|
-
|
|
120
|
-
- Updated dependencies [b62e535]
|
|
121
|
-
- @telorun/sdk@0.7.0
|
|
122
|
-
|
|
123
176
|
## 0.2.4
|
|
124
177
|
|
|
125
178
|
### Patch Changes
|
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SUSTAINABLE USE LICENSE (Fair-code)
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026
|
|
3
|
+
Copyright (c) 2026 CodeNet Sp. z o.o.
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, and distribute the Software for any purpose—including commercial purposes—subject to the following conditions:
|
|
6
6
|
|
|
@@ -14,4 +14,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
|
14
14
|
|
|
15
15
|
5. DISCLAIMER: The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the Software or the use or other dealings in the Software.
|
|
16
16
|
|
|
17
|
-
For commercial licensing, managed hosting exemptions, or enterprise inquiries, please contact
|
|
17
|
+
For commercial licensing, managed hosting exemptions, or enterprise inquiries, please contact <contact@codenet.pl>.
|
package/README.md
CHANGED
|
@@ -1,36 +1,89 @@
|
|
|
1
|
-
#
|
|
1
|
+
# HTTP Server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Language- and framework-agnostic HTTP server for Telo. Declarative routes, schema-validated requests, and a typed return/catch rendering pipeline.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Why use this
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- **Framework-neutral** — the underlying engine (Fastify, Actix, …) is an implementation detail; the same manifest runs on any compliant adapter.
|
|
8
|
+
- **OpenAPI-style paths** — `/users/{id}` syntax everywhere; the adapter translates to its native router.
|
|
9
|
+
- **Schema-driven validation** — `request.schema` (`body`, `query`, `params`, `headers`) yields a standardized HTTP 400 with `details[]` on failure.
|
|
10
|
+
- **Typed returns and catches** — render successful values and structured `InvokeError`s into status + headers + per-MIME bodies via CEL.
|
|
11
|
+
- **Composable mounts** — attach `Telo.Mount` resources (HTTP APIs, MCP endpoints, custom mounts) under any path prefix.
|
|
12
|
+
- **CORS and content-type parsers** — first-class manifest fields; no controller code needed.
|
|
8
13
|
|
|
14
|
+
## Kinds
|
|
15
|
+
|
|
16
|
+
| Kind | Purpose |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| `Http.Server` | Long-lived HTTP listener that hosts mounts on configured paths and ports. |
|
|
19
|
+
| `Http.Api` | Mountable router exposing route definitions with returns/catches rendering. |
|
|
20
|
+
|
|
21
|
+
## Example
|
|
22
|
+
|
|
23
|
+
```yaml
|
|
24
|
+
kind: Telo.Application
|
|
25
|
+
metadata: { name: hello-http, version: 1.0.0 }
|
|
26
|
+
targets: [Server]
|
|
27
|
+
---
|
|
28
|
+
kind: Telo.Import
|
|
29
|
+
metadata: { name: Http }
|
|
30
|
+
source: pkg:npm/@telorun/http-server@^1.0.0
|
|
9
31
|
---
|
|
32
|
+
kind: Telo.Import
|
|
33
|
+
metadata: { name: JS }
|
|
34
|
+
source: pkg:npm/@telorun/javascript@^1.0.0
|
|
35
|
+
---
|
|
36
|
+
kind: Http.Server
|
|
37
|
+
metadata: { name: Server }
|
|
38
|
+
port: 8080
|
|
39
|
+
mounts:
|
|
40
|
+
- path: /api
|
|
41
|
+
type: Api
|
|
42
|
+
---
|
|
43
|
+
kind: Http.Api
|
|
44
|
+
metadata: { name: Api }
|
|
45
|
+
routes:
|
|
46
|
+
- request: { method: GET, path: /hello/{name} }
|
|
47
|
+
inputs:
|
|
48
|
+
name: "${{ request.params.name }}"
|
|
49
|
+
handler: { kind: JS.Script, name: Greet }
|
|
50
|
+
returns:
|
|
51
|
+
- status: 200
|
|
52
|
+
content:
|
|
53
|
+
application/json:
|
|
54
|
+
body: { message: "${{ result.message }}" }
|
|
55
|
+
---
|
|
56
|
+
kind: JS.Script
|
|
57
|
+
metadata: { name: Greet }
|
|
58
|
+
code: |
|
|
59
|
+
return { message: `Hello, ${inputs.name}!` };
|
|
60
|
+
```
|
|
10
61
|
|
|
11
|
-
##
|
|
62
|
+
## Reference
|
|
12
63
|
|
|
13
|
-
|
|
64
|
+
- [`Http.Server` / `Http.Api` returns & catches](docs/returns-and-catches.md) — outcome lists, MIME negotiation, stream mode.
|
|
14
65
|
|
|
15
|
-
|
|
66
|
+
## Implementation Contract
|
|
16
67
|
|
|
17
|
-
|
|
18
|
-
- **Module Responsibility:** The underlying HTTP module must parse the Telo path and translate it into its framework's native routing syntax at startup.
|
|
68
|
+
The `Http.Server` and `Http.Api` manifests in Telo are designed to be strictly language-agnostic and framework-agnostic. To maintain the "Zero Lock-in" promise, the underlying HTTP engine (e.g. Fastify in Node.js, Actix in Rust) is treated purely as an implementation detail. All HTTP modules integrated into the Telo kernel MUST adhere to this behavioural contract.
|
|
19
69
|
|
|
20
|
-
|
|
70
|
+
### 1. Routing (path definitions)
|
|
21
71
|
|
|
22
|
-
|
|
23
|
-
- _Rust (Actix) Adapter translates to:_ `/api/v1/users/{userId}`
|
|
72
|
+
Telo standardizes on the OpenAPI specification format for paths.
|
|
24
73
|
|
|
25
|
-
|
|
74
|
+
- **Standard:** path parameters MUST be enclosed in curly braces: `{parameterName}`.
|
|
75
|
+
- **Module responsibility:** the underlying HTTP module must parse the Telo path and translate it into its framework's native routing syntax at startup.
|
|
76
|
+
|
|
77
|
+
**Example manifest path:** `/api/v1/users/{userId}`
|
|
26
78
|
|
|
27
|
-
|
|
79
|
+
- Node.js (Fastify) adapter translates to: `/api/v1/users/:userId`
|
|
80
|
+
- Rust (Actix) adapter translates to: `/api/v1/users/{userId}`
|
|
28
81
|
|
|
29
|
-
|
|
82
|
+
### 2. I/O context contract
|
|
30
83
|
|
|
31
|
-
|
|
84
|
+
When an incoming HTTP request is received, the underlying framework must normalize it into a standard Telo Request Object before passing it to the handler/CEL engine. Conversely, it must accept a standard Telo Response Object to send back to the client.
|
|
32
85
|
|
|
33
|
-
|
|
86
|
+
#### 2.1 Standardized Telo Request Object (input)
|
|
34
87
|
|
|
35
88
|
```json
|
|
36
89
|
{
|
|
@@ -51,12 +104,12 @@ The HTTP module must construct and pass the following exact payload to the execu
|
|
|
51
104
|
}
|
|
52
105
|
```
|
|
53
106
|
|
|
54
|
-
-
|
|
55
|
-
-
|
|
107
|
+
- All `headers` keys MUST be normalized to lowercase.
|
|
108
|
+
- If the `content-type` is `application/json`, the `body` MUST be parsed into a native object/dictionary before evaluation.
|
|
56
109
|
|
|
57
|
-
|
|
110
|
+
#### 2.2 Standardized Telo Response Object (output)
|
|
58
111
|
|
|
59
|
-
After the
|
|
112
|
+
After the handler executes and the `response.mapping` evaluates, the engine returns an object to the HTTP module. The module must map this directly to the native HTTP response.
|
|
60
113
|
|
|
61
114
|
```json
|
|
62
115
|
{
|
|
@@ -72,17 +125,9 @@ After the Handler executes and the `response.mapping` evaluates, the engine will
|
|
|
72
125
|
}
|
|
73
126
|
```
|
|
74
127
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
## 3. Validation & Error Handling Contract
|
|
78
|
-
|
|
79
|
-
When a request fails schema validation (defined in the `request.schema` of the manifest), the underlying engine (e.g., AJV in Fastify) will generate native errors. **These internal errors must not leak to the client.**
|
|
128
|
+
### 3. Validation and error handling
|
|
80
129
|
|
|
81
|
-
All Telo HTTP modules MUST intercept framework-specific validation errors and return a standardized HTTP 400 Bad Request payload.
|
|
82
|
-
|
|
83
|
-
### Standardized Validation Error Format
|
|
84
|
-
|
|
85
|
-
The response body must strictly follow this JSON structure:
|
|
130
|
+
When a request fails schema validation (defined in the `request.schema` of the manifest), the underlying engine (e.g. AJV in Fastify) will generate native errors. These internal errors must not leak to the client. All Telo HTTP modules MUST intercept framework-specific validation errors and return a standardized HTTP 400 Bad Request payload.
|
|
86
131
|
|
|
87
132
|
```json
|
|
88
133
|
{
|
|
@@ -104,12 +149,10 @@ The response body must strictly follow this JSON structure:
|
|
|
104
149
|
}
|
|
105
150
|
```
|
|
106
151
|
|
|
107
|
-
- **`location` enum:** `body` | `query` | `params` | `headers
|
|
108
|
-
- **Module
|
|
109
|
-
|
|
110
|
-
---
|
|
152
|
+
- **`location` enum:** `body` | `query` | `params` | `headers`.
|
|
153
|
+
- **Module responsibility:** the module author must write an error handler/mapper that transforms the native framework's validation output into the Telo `details` array.
|
|
111
154
|
|
|
112
|
-
|
|
155
|
+
### 4. Manifest schema upgrades
|
|
113
156
|
|
|
114
157
|
To fully support this contract, the `Http.Api` JSON Schema definition includes the following structural definitions for the `request` block:
|
|
115
158
|
|
|
@@ -138,9 +138,22 @@ class HttpServer {
|
|
|
138
138
|
};
|
|
139
139
|
const acceptHeader = request.headers["accept"]?.toString();
|
|
140
140
|
const sink = fastifyReplySink(reply);
|
|
141
|
+
// Expand the `inputs:` sibling template against the request context,
|
|
142
|
+
// then pass the merged shape (spread for convenience + `inputs:` field
|
|
143
|
+
// for handlers that read it explicitly) to the dispatch target. Same
|
|
144
|
+
// contract Api.routes[*] uses. When no `inputs:` is declared, the
|
|
145
|
+
// request context itself is forwarded so existing manifests that read
|
|
146
|
+
// `request.*` directly continue to work.
|
|
147
|
+
const resolvedInputs = handler.inputs && Object.keys(handler.inputs).length > 0
|
|
148
|
+
? (this.ctx.moduleContext.expandWith(handler.inputs, requestContext) ?? {})
|
|
149
|
+
: requestContext;
|
|
150
|
+
const invokeInput = {
|
|
151
|
+
...resolvedInputs,
|
|
152
|
+
inputs: resolvedInputs,
|
|
153
|
+
};
|
|
141
154
|
let result;
|
|
142
155
|
try {
|
|
143
|
-
result = await this.ctx.invoke(handler.kind, handler.name,
|
|
156
|
+
result = await this.ctx.invoke(handler.kind, handler.name, invokeInput);
|
|
144
157
|
}
|
|
145
158
|
catch (err) {
|
|
146
159
|
if (!isInvokeError(err))
|
|
@@ -206,7 +219,7 @@ export async function create(resource, ctx) {
|
|
|
206
219
|
resolvedNotFoundHandler = {
|
|
207
220
|
kind,
|
|
208
221
|
name,
|
|
209
|
-
inputs:
|
|
222
|
+
inputs: resource.notFoundHandler.inputs ?? {},
|
|
210
223
|
returns: resource.notFoundHandler.returns,
|
|
211
224
|
catches: resource.notFoundHandler.catches,
|
|
212
225
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/http-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Telo HTTP Server module - HTTP server and API resource kinds for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -43,14 +43,16 @@
|
|
|
43
43
|
"ajv": "^8.17.1",
|
|
44
44
|
"ajv-formats": "^3.0.1",
|
|
45
45
|
"fastify": "^5.7.2",
|
|
46
|
-
"@telorun/http-dispatch": "0.
|
|
47
|
-
"@telorun/sdk": "0.11.1"
|
|
46
|
+
"@telorun/http-dispatch": "0.3.0"
|
|
48
47
|
},
|
|
49
48
|
"devDependencies": {
|
|
50
49
|
"@types/node": "^20.0.0",
|
|
51
50
|
"typescript": "^5.0.0",
|
|
52
51
|
"vitest": "^2.1.8"
|
|
53
52
|
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@telorun/sdk": "0.12.0"
|
|
55
|
+
},
|
|
54
56
|
"scripts": {
|
|
55
57
|
"build": "tsc -p tsconfig.lib.json",
|
|
56
58
|
"test": "vitest run",
|
|
@@ -54,6 +54,7 @@ type HttpServerResource = RuntimeResource & {
|
|
|
54
54
|
}>;
|
|
55
55
|
notFoundHandler?: {
|
|
56
56
|
invoke: KindRef<Invocable>;
|
|
57
|
+
inputs?: Record<string, unknown>;
|
|
57
58
|
returns?: ReturnEntry[];
|
|
58
59
|
catches?: CatchEntry[];
|
|
59
60
|
};
|
|
@@ -221,9 +222,24 @@ class HttpServer implements ResourceInstance {
|
|
|
221
222
|
|
|
222
223
|
const sink = fastifyReplySink(reply);
|
|
223
224
|
|
|
225
|
+
// Expand the `inputs:` sibling template against the request context,
|
|
226
|
+
// then pass the merged shape (spread for convenience + `inputs:` field
|
|
227
|
+
// for handlers that read it explicitly) to the dispatch target. Same
|
|
228
|
+
// contract Api.routes[*] uses. When no `inputs:` is declared, the
|
|
229
|
+
// request context itself is forwarded so existing manifests that read
|
|
230
|
+
// `request.*` directly continue to work.
|
|
231
|
+
const resolvedInputs: Record<string, any> =
|
|
232
|
+
handler.inputs && Object.keys(handler.inputs).length > 0
|
|
233
|
+
? ((this.ctx.moduleContext.expandWith(handler.inputs, requestContext) as any) ?? {})
|
|
234
|
+
: requestContext;
|
|
235
|
+
const invokeInput: Record<string, any> = {
|
|
236
|
+
...resolvedInputs,
|
|
237
|
+
inputs: resolvedInputs,
|
|
238
|
+
};
|
|
239
|
+
|
|
224
240
|
let result: any;
|
|
225
241
|
try {
|
|
226
|
-
result = await this.ctx.invoke(handler.kind, handler.name,
|
|
242
|
+
result = await this.ctx.invoke(handler.kind, handler.name, invokeInput);
|
|
227
243
|
} catch (err) {
|
|
228
244
|
if (!isInvokeError(err)) throw err;
|
|
229
245
|
return dispatchCatches(
|
|
@@ -310,7 +326,7 @@ export async function create(
|
|
|
310
326
|
resolvedNotFoundHandler = {
|
|
311
327
|
kind,
|
|
312
328
|
name,
|
|
313
|
-
inputs:
|
|
329
|
+
inputs: resource.notFoundHandler.inputs ?? {},
|
|
314
330
|
returns: resource.notFoundHandler.returns,
|
|
315
331
|
catches: resource.notFoundHandler.catches,
|
|
316
332
|
};
|