@telorun/http-server 0.19.0 → 0.26.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
CHANGED
|
@@ -1,5 +1,92 @@
|
|
|
1
1
|
# @telorun/http-server
|
|
2
2
|
|
|
3
|
+
## 0.20.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- c8d457b: One value-type annotation: `x-telo-type` says what a value IS, and the
|
|
8
|
+
vocabulary is data both runtimes read.
|
|
9
|
+
|
|
10
|
+
Three annotations answered one question — _what is the value at this slot, beyond
|
|
11
|
+
what JSON Schema's `type` vocabulary can say?_ — and each answered it
|
|
12
|
+
differently: `x-telo-type: TcpPort` (a nominal brand from a closed kernel table),
|
|
13
|
+
`x-telo-binary: true` (raw bytes, the one annotation that emitted validation
|
|
14
|
+
code), `x-telo-stream: true` (a live handle, exempt from schema walks). They
|
|
15
|
+
differ in _posture_ toward the JSON Schema layer — refine, replace, exempt — not
|
|
16
|
+
in kind, so a fourth cost eleven files across four packages, and three defects
|
|
17
|
+
followed from the spread: an unrecognized brand degraded silently, a byte slot's
|
|
18
|
+
expression typed as `dyn` because nothing consulted `x-telo-binary`, and a module
|
|
19
|
+
string-matched the keyword because a module may import `@telorun/sdk` and there
|
|
20
|
+
was nothing there to read.
|
|
21
|
+
|
|
22
|
+
- **The vocabulary is DATA; the binding to a language is not.** A type is one
|
|
23
|
+
JSON file under `sdk/value-types/`, copied into the SDK by its `prepare` and
|
|
24
|
+
embedded by Rust with `include_str!` — the `analyzer/migrations/` arrangement,
|
|
25
|
+
for the same reason. An entry declares `name`, `representation` (`json` + a
|
|
26
|
+
`base`, or `instance` + a symbolic `binding`), `live`, `parameters` and a
|
|
27
|
+
`description`, and nothing about any runtime. Each runtime carries its own
|
|
28
|
+
table mapping `binding` to its own identity; a binding with **no row is a hard
|
|
29
|
+
startup error**, never a skipped assertion, because a type that cannot be
|
|
30
|
+
asserted would silently exempt every slot declaring it.
|
|
31
|
+
- **`registerTeloKeywords`** replaces five drifted AJV registration sites — the
|
|
32
|
+
analyzer's `createAjv` and the kernel's `schema-validator`, `resource-context`,
|
|
33
|
+
`observed-state` and `manifest-schemas`, which registered overlapping lists of
|
|
34
|
+
twelve, four, one and one. Drift there is not cosmetic: a keyword that emits
|
|
35
|
+
code was simply missing from any instance that forgot it, so one schema
|
|
36
|
+
validated two ways depending on which AJV saw it.
|
|
37
|
+
- **Exemption is a property of the TYPE, not of a position.** The old walk
|
|
38
|
+
neutralized only a key it found in a `properties` map, so an array-of-streams
|
|
39
|
+
element was reached and left constrained even though it descended into `items`.
|
|
40
|
+
Reading liveness off the declared type makes an item, a union branch and a
|
|
41
|
+
property one case. It is exemption from **validation**, never from **typing**:
|
|
42
|
+
a live type's arguments still travel through every schema-typing walk.
|
|
43
|
+
- **Value types are generic.** An entry declares named type parameters and the
|
|
44
|
+
annotation's object form supplies arguments — `{ name: Telo.Stream, of:
|
|
45
|
+
Telo.Bytes }`. An argument is a schema node, so it nests with no new grammar,
|
|
46
|
+
and a bare name is sugar for a node carrying only the annotation, normalized in
|
|
47
|
+
the single reader. Comparison is **covariant and gradual**: an omitted argument
|
|
48
|
+
is _any_ in both directions, so every producer and consumer that has not
|
|
49
|
+
declared an element keeps checking exactly as it did. A definite conflict is
|
|
50
|
+
`CEL_TYPE_ARGUMENT_MISMATCH`, reported where a produced value's schema meets a
|
|
51
|
+
consuming slot's — a step's `inputs:` against the invoked target's contract,
|
|
52
|
+
which is the one place both halves are in hand.
|
|
53
|
+
- **A shape is named with `!ref`**, Telo's one reference grammar, and `use:
|
|
54
|
+
schema` has been in the `x-telo-ref` vocabulary for exactly this relation all
|
|
55
|
+
along. The loader normalizes it to the canonical `telo:<module>/<Type>` `$ref`
|
|
56
|
+
— authoring surface and internal form, the split `resolveRefSentinels` and
|
|
57
|
+
`resolveSchemaRefKinds` already have. Normalizing rather than inlining is what
|
|
58
|
+
preserves schema identity (the compiled-validator cache is keyed on it) and
|
|
59
|
+
leaves a recursive shape expressible; carrying the owning module is what makes
|
|
60
|
+
resolution alias-aware, where matching a bare `metadata.name` across a
|
|
61
|
+
flattened list silently dropped the alias.
|
|
62
|
+
- **A tag's produced type is declared by its ENGINE.** `TemplatingEngine` gains
|
|
63
|
+
`producedType()`; `!include-bytes` declares `Telo.Bytes` and `!include-text`
|
|
64
|
+
declares `type: string`, and `substituteCelFields` loses its tag-name branch —
|
|
65
|
+
the only place a tag's produced type was written down, written in the consumer.
|
|
66
|
+
- **`X_TELO_TYPE_UNKNOWN`** (Levenshtein-suggested) and
|
|
67
|
+
**`X_TELO_TYPE_ARGUMENT_UNKNOWN`** replace the silent degrade, on every
|
|
68
|
+
schema-bearing field of every manifest — not only definition docs, since an
|
|
69
|
+
inline `inputType:` on an ordinary resource carries a schema too.
|
|
70
|
+
- **The migration selector gains a schema region.** `inSchema: true` bounds a
|
|
71
|
+
rule to the kernel's own schema-valued keys, and only with it may `inKind` /
|
|
72
|
+
`under` be `["*"]`, and only for a rule keyed on an `x-telo-*` annotation. That
|
|
73
|
+
pairing is the containment: an annotation keyword occurs in author-written
|
|
74
|
+
schema fragments inside ordinary resource documents, and that set of kinds is
|
|
75
|
+
open, so enumerating it would be both incomplete and a violation of the
|
|
76
|
+
topology-driven constraint.
|
|
77
|
+
|
|
78
|
+
**Breaking:** `x-telo-binary` and `x-telo-stream` are rewritten to `x-telo-type`
|
|
79
|
+
at load by the `normalize-value-types` migration, so every published manifest
|
|
80
|
+
keeps working and `telo migrate` repairs a file in place. `binaryKeyword` /
|
|
81
|
+
`isBinarySlot` / `X_TELO_BINARY` are removed from `@telorun/analyzer` in favour
|
|
82
|
+
of `registerTeloKeywords` and the SDK accessors; `withStreamPropertiesSkipped` is
|
|
83
|
+
now `withLiveValuesSkipped`. Value brands are `Telo.`-qualified (`TcpPort` →
|
|
84
|
+
`Telo.TcpPort`), which the same migration rewrites.
|
|
85
|
+
|
|
86
|
+
### Patch Changes
|
|
87
|
+
|
|
88
|
+
- @telorun/http-dispatch@0.4.2
|
|
89
|
+
|
|
3
90
|
## 0.19.0
|
|
4
91
|
|
|
5
92
|
### Minor Changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { CatchEntry, dispatchCatches, dispatchReturns, ReturnEntry, validateNoContentTypeHeader, validateStreamWhenDoesNotReferenceResult, } from "@telorun/http-dispatch";
|
|
3
|
-
import { InvokeError, isCancellationError, isInvokeError, Ref, Stream, } from "@telorun/sdk";
|
|
3
|
+
import { InvokeError, isCancellationError, isInvokeError, isLiveSlot, Ref, Stream, } from "@telorun/sdk";
|
|
4
4
|
import { fastifyReplySink } from "./fastify-reply-sink.js";
|
|
5
5
|
const HttpApiRouteManifest = Type.Object({
|
|
6
6
|
request: Type.Object({
|
|
@@ -61,10 +61,16 @@ export class HttpServerApi {
|
|
|
61
61
|
const handlerName = handlerRef?.name ?? "";
|
|
62
62
|
const translatedPath = joinMountPath(prefix, translateOpenApiPath(route.request.path));
|
|
63
63
|
const schema = { response: {} };
|
|
64
|
-
// A
|
|
64
|
+
// A live-typed body is delivered as a raw `Stream<Uint8Array>` (see the
|
|
65
65
|
// server's `contentTypeParsers[].stream`); it is opaque to AJV, so skip
|
|
66
66
|
// body-schema registration and wrap the raw request stream in the handler.
|
|
67
|
-
|
|
67
|
+
//
|
|
68
|
+
// Read through the SDK accessor rather than by string-matching the keyword.
|
|
69
|
+
// A module may import `@telorun/sdk` and nothing else, and before the value
|
|
70
|
+
// types were unified there was nothing on that surface to read — so this was
|
|
71
|
+
// a literal key comparison, the same shape as the four surfaces that
|
|
72
|
+
// string-matched `x-telo-ref` before `ref-slot.ts`.
|
|
73
|
+
const streamBody = isLiveSlot(route.request.schema?.body);
|
|
68
74
|
if (route.request.schema?.query)
|
|
69
75
|
schema.querystring = route.request.schema.query;
|
|
70
76
|
if (route.request.schema?.params)
|
|
@@ -164,8 +164,35 @@ class HttpServer {
|
|
|
164
164
|
}, { eventName: "http.server.request" });
|
|
165
165
|
});
|
|
166
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Accept a multipart body out of the box.
|
|
169
|
+
*
|
|
170
|
+
* Fastify ships parsers for JSON and urlencoded and nothing else, so a route
|
|
171
|
+
* receiving a file upload answered 415 before any handler ran — a failure that
|
|
172
|
+
* names a media type the author DID send and points at no fix. Every server
|
|
173
|
+
* taking an upload had to discover `contentTypeParsers` first.
|
|
174
|
+
*
|
|
175
|
+
* Registered as RAW BYTES rather than a string, because that is what a
|
|
176
|
+
* multipart body is: decoding it as text corrupts every binary part, and the
|
|
177
|
+
* parts are the point. The handler receives the undrained request stream, which
|
|
178
|
+
* `Multipart.Decoder` consumes.
|
|
179
|
+
*
|
|
180
|
+
* Registered UNCONDITIONALLY, as a regex. Fastify keys a duplicate on the exact
|
|
181
|
+
* string (or the regex's `toString()`) and consults its string parsers before
|
|
182
|
+
* its regex ones, so a declared `multipart/form-data` neither collides with this
|
|
183
|
+
* nor is shadowed by it — it simply wins for its own type. Skipping the default
|
|
184
|
+
* whenever any multipart parser was declared would instead disable it for the
|
|
185
|
+
* SIBLING subtypes the author did not customize, so declaring a parser for
|
|
186
|
+
* `form-data` would silently restore the 415 for `related` and `mixed`.
|
|
187
|
+
*/
|
|
188
|
+
installDefaultMultipartParser() {
|
|
189
|
+
this.app.addContentTypeParser(/^multipart\//, (_req, payload, done) => {
|
|
190
|
+
done(null, payload);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
167
193
|
async setupPlugins() {
|
|
168
194
|
this.installRequestLogging();
|
|
195
|
+
this.installDefaultMultipartParser();
|
|
169
196
|
for (const { contentType, parser, stream } of this.resource.contentTypeParsers ?? []) {
|
|
170
197
|
if (stream) {
|
|
171
198
|
// Raw passthrough: omit `parseAs` so Fastify hands the handler the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/http-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Telo HTTP Server module - HTTP server and API resource kinds for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -49,13 +49,13 @@
|
|
|
49
49
|
"ajv": "^8.17.1",
|
|
50
50
|
"ajv-formats": "^3.0.1",
|
|
51
51
|
"fastify": "^5.7.2",
|
|
52
|
-
"@telorun/http-dispatch": "0.
|
|
52
|
+
"@telorun/http-dispatch": "0.11.1"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/node": "^20.0.0",
|
|
56
56
|
"typescript": "^5.0.0",
|
|
57
57
|
"vitest": "^2.1.8",
|
|
58
|
-
"@telorun/sdk": "0.
|
|
58
|
+
"@telorun/sdk": "0.75.0"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@telorun/sdk": "*"
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
InvokeError,
|
|
15
15
|
isCancellationError,
|
|
16
16
|
isInvokeError,
|
|
17
|
+
isLiveSlot,
|
|
17
18
|
KindRef,
|
|
18
19
|
Ref,
|
|
19
20
|
ResourceContext,
|
|
@@ -95,10 +96,16 @@ export class HttpServerApi implements ResourceInstance {
|
|
|
95
96
|
|
|
96
97
|
const schema: any = { response: {} };
|
|
97
98
|
|
|
98
|
-
// A
|
|
99
|
+
// A live-typed body is delivered as a raw `Stream<Uint8Array>` (see the
|
|
99
100
|
// server's `contentTypeParsers[].stream`); it is opaque to AJV, so skip
|
|
100
101
|
// body-schema registration and wrap the raw request stream in the handler.
|
|
101
|
-
|
|
102
|
+
//
|
|
103
|
+
// Read through the SDK accessor rather than by string-matching the keyword.
|
|
104
|
+
// A module may import `@telorun/sdk` and nothing else, and before the value
|
|
105
|
+
// types were unified there was nothing on that surface to read — so this was
|
|
106
|
+
// a literal key comparison, the same shape as the four surfaces that
|
|
107
|
+
// string-matched `x-telo-ref` before `ref-slot.ts`.
|
|
108
|
+
const streamBody = isLiveSlot(route.request.schema?.body);
|
|
102
109
|
|
|
103
110
|
if (route.request.schema?.query) schema.querystring = route.request.schema.query;
|
|
104
111
|
if (route.request.schema?.params) schema.params = route.request.schema.params;
|
|
@@ -261,8 +261,36 @@ class HttpServer implements ResourceInstance {
|
|
|
261
261
|
});
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Accept a multipart body out of the box.
|
|
266
|
+
*
|
|
267
|
+
* Fastify ships parsers for JSON and urlencoded and nothing else, so a route
|
|
268
|
+
* receiving a file upload answered 415 before any handler ran — a failure that
|
|
269
|
+
* names a media type the author DID send and points at no fix. Every server
|
|
270
|
+
* taking an upload had to discover `contentTypeParsers` first.
|
|
271
|
+
*
|
|
272
|
+
* Registered as RAW BYTES rather than a string, because that is what a
|
|
273
|
+
* multipart body is: decoding it as text corrupts every binary part, and the
|
|
274
|
+
* parts are the point. The handler receives the undrained request stream, which
|
|
275
|
+
* `Multipart.Decoder` consumes.
|
|
276
|
+
*
|
|
277
|
+
* Registered UNCONDITIONALLY, as a regex. Fastify keys a duplicate on the exact
|
|
278
|
+
* string (or the regex's `toString()`) and consults its string parsers before
|
|
279
|
+
* its regex ones, so a declared `multipart/form-data` neither collides with this
|
|
280
|
+
* nor is shadowed by it — it simply wins for its own type. Skipping the default
|
|
281
|
+
* whenever any multipart parser was declared would instead disable it for the
|
|
282
|
+
* SIBLING subtypes the author did not customize, so declaring a parser for
|
|
283
|
+
* `form-data` would silently restore the 415 for `related` and `mixed`.
|
|
284
|
+
*/
|
|
285
|
+
private installDefaultMultipartParser(): void {
|
|
286
|
+
this.app.addContentTypeParser(/^multipart\//, (_req, payload, done) => {
|
|
287
|
+
done(null, payload);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
264
291
|
private async setupPlugins() {
|
|
265
292
|
this.installRequestLogging();
|
|
293
|
+
this.installDefaultMultipartParser();
|
|
266
294
|
for (const { contentType, parser, stream } of this.resource.contentTypeParsers ?? []) {
|
|
267
295
|
if (stream) {
|
|
268
296
|
// Raw passthrough: omit `parseAs` so Fastify hands the handler the
|