@jarenjs/contract 0.43.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/README.md +508 -0
- package/dist/types/adapters/fetch.d.ts +27 -0
- package/dist/types/adapters/node.d.ts +47 -0
- package/dist/types/app/binding.d.ts +122 -0
- package/dist/types/app/effect.d.ts +77 -0
- package/dist/types/app/index.d.ts +31 -0
- package/dist/types/app/subscription.d.ts +82 -0
- package/dist/types/bundle.d.ts +43 -0
- package/dist/types/cli.d.ts +15 -0
- package/dist/types/client/http.d.ts +242 -0
- package/dist/types/client/outcome.d.ts +289 -0
- package/dist/types/compat.d.ts +36 -0
- package/dist/types/compile.d.ts +196 -0
- package/dist/types/describe.d.ts +115 -0
- package/dist/types/diff.d.ts +91 -0
- package/dist/types/errors.d.ts +205 -0
- package/dist/types/http/dispatch.d.ts +148 -0
- package/dist/types/http/serve.d.ts +154 -0
- package/dist/types/http/wire.d.ts +334 -0
- package/dist/types/index.d.ts +39 -0
- package/dist/types/ledger.d.ts +207 -0
- package/dist/types/local/index.d.ts +127 -0
- package/dist/types/messages.d.ts +63 -0
- package/dist/types/path.d.ts +119 -0
- package/dist/types/pipeline.d.ts +157 -0
- package/dist/types/port/client.d.ts +142 -0
- package/dist/types/port/frame.d.ts +195 -0
- package/dist/types/port/serve.d.ts +102 -0
- package/dist/types/project/index.d.ts +34 -0
- package/dist/types/project/markdown.d.ts +28 -0
- package/dist/types/project/openapi.d.ts +102 -0
- package/dist/types/project/tools.d.ts +57 -0
- package/dist/types/project/typescript.d.ts +59 -0
- package/dist/types/public.d.ts +73 -0
- package/dist/types/revision.d.ts +36 -0
- package/dist/types/stream/client.d.ts +104 -0
- package/dist/types/stream/server.d.ts +106 -0
- package/dist/types/stream/sse.d.ts +62 -0
- package/docs/APP-INTEGRATION.md +301 -0
- package/docs/CONTRACT-FORMAT.md +1923 -0
- package/package.json +110 -0
- package/schemas/jaren-contract-port.draft-07.schema.json +241 -0
- package/schemas/jaren-contract-port.schema.json +241 -0
- package/schemas/jaren-contract.draft-07.schema.json +287 -0
- package/schemas/jaren-contract.schema.json +287 -0
- package/src/adapters/fetch.js +109 -0
- package/src/adapters/node.js +238 -0
- package/src/app/binding.js +426 -0
- package/src/app/effect.js +190 -0
- package/src/app/index.js +26 -0
- package/src/app/subscription.js +130 -0
- package/src/bundle.js +168 -0
- package/src/cli.js +264 -0
- package/src/client/http.js +1150 -0
- package/src/client/outcome.js +364 -0
- package/src/compat.js +62 -0
- package/src/compile.js +1162 -0
- package/src/describe.js +109 -0
- package/src/diff.js +610 -0
- package/src/errors.js +236 -0
- package/src/http/dispatch.js +1054 -0
- package/src/http/serve.js +301 -0
- package/src/http/wire.js +469 -0
- package/src/index.js +33 -0
- package/src/ledger.js +225 -0
- package/src/local/index.js +363 -0
- package/src/messages.js +68 -0
- package/src/path.js +471 -0
- package/src/pipeline.js +241 -0
- package/src/port/client.js +518 -0
- package/src/port/frame.js +196 -0
- package/src/port/serve.js +442 -0
- package/src/project/index.js +29 -0
- package/src/project/markdown.js +244 -0
- package/src/project/openapi.js +564 -0
- package/src/project/openapi.jslt.json +149 -0
- package/src/project/tools.js +139 -0
- package/src/project/typescript.js +152 -0
- package/src/project/typescript.jtlt.json +72 -0
- package/src/public.js +206 -0
- package/src/revision.js +90 -0
- package/src/stream/client.js +212 -0
- package/src/stream/server.js +306 -0
- package/src/stream/sse.js +67 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file `diffContracts(a, b)`: what changed from contract `a` to contract
|
|
3
|
+
* `b`, classified by the published rule table (docs/CONTRACT-FORMAT.md
|
|
4
|
+
* §13, rows R1–R15) into `breaking`, `additive`, `neutral` and `unknown`
|
|
5
|
+
* — where `unknown` is the honest fourth class: a schema construct the
|
|
6
|
+
* checker does not model (`anyOf`, `if`, a changed `pattern`, an external
|
|
7
|
+
* `$ref` that moved) is REPORTED, never silently classed.
|
|
8
|
+
*
|
|
9
|
+
* The schema comparison walks the two sides in parallel over the resolved
|
|
10
|
+
* same-document structure — a bare `{ "$ref": "#/$defs/X" }` hop is
|
|
11
|
+
* followed with the same rules the compiler used — and models exactly the
|
|
12
|
+
* R6 keyword set (`type`, `const`, `enum`, `maximum`, `minimum`,
|
|
13
|
+
* `maxLength`, `minLength`, `pattern`) plus object structure
|
|
14
|
+
* (`properties`, `required`, `additionalProperties`) and `items`. Pure
|
|
15
|
+
* annotations (`title`, `description`, `examples`, `$comment`,
|
|
16
|
+
* `deprecated`) never move a wire byte and are ignored; every other
|
|
17
|
+
* keyword that differs between the two sides lands in `unknown` (R15).
|
|
18
|
+
*
|
|
19
|
+
* A `Change`'s `docPath` points into the document that carries it — a
|
|
20
|
+
* removal into `a`, everything else into `b` — composed over the RESOLVED
|
|
21
|
+
* structure, so a constraint reached through a `$ref` reports the path a
|
|
22
|
+
* validator error would name, not the `$defs` entry's.
|
|
23
|
+
*/
|
|
24
|
+
export { isCompatible, compatReason } from './compat.js';
|
|
25
|
+
export type Contract = import('./compile.js').Contract;
|
|
26
|
+
export type CompiledOperation = import('./compile.js').CompiledOperation;
|
|
27
|
+
export type Change = {
|
|
28
|
+
/**
|
|
29
|
+
* - a stable slug naming what changed (`'operation-removed'`, `'input-narrowed'`, …)
|
|
30
|
+
*/
|
|
31
|
+
kind: string;
|
|
32
|
+
/**
|
|
33
|
+
* - the operation id
|
|
34
|
+
*/
|
|
35
|
+
op: string;
|
|
36
|
+
/**
|
|
37
|
+
* - RFC 6901 pointer to the change (into `a` for a removal, into `b` otherwise)
|
|
38
|
+
*/
|
|
39
|
+
docPath: string;
|
|
40
|
+
/**
|
|
41
|
+
* - the old value, where one exists
|
|
42
|
+
*/
|
|
43
|
+
from?: unknown;
|
|
44
|
+
/**
|
|
45
|
+
* - the new value, where one exists
|
|
46
|
+
*/
|
|
47
|
+
to?: unknown;
|
|
48
|
+
/**
|
|
49
|
+
* - the §13 row: `'R1'`–`'R15'`
|
|
50
|
+
*/
|
|
51
|
+
rule: string;
|
|
52
|
+
/**
|
|
53
|
+
* - the honesty rider some rows carry (R5's "now ignored, not validated")
|
|
54
|
+
*/
|
|
55
|
+
note?: string;
|
|
56
|
+
};
|
|
57
|
+
export type ContractDiff = {
|
|
58
|
+
breaking: Change[];
|
|
59
|
+
additive: Change[];
|
|
60
|
+
neutral: Change[];
|
|
61
|
+
unknown: Change[];
|
|
62
|
+
};
|
|
63
|
+
export type Side = {
|
|
64
|
+
doc: any;
|
|
65
|
+
anchors: Map<string, object>;
|
|
66
|
+
};
|
|
67
|
+
export type Sink = {
|
|
68
|
+
constraint: (direction: 'narrowed' | 'widened', keyword: string, path: string, from: unknown, to: unknown) => void;
|
|
69
|
+
/**
|
|
70
|
+
* `direction` is the AP-aware narrowing/widening reading of the member
|
|
71
|
+
* event (`null` when it is a no-op, e.g. an unconstrained optional
|
|
72
|
+
* member added to an open object)
|
|
73
|
+
*/
|
|
74
|
+
member: (event: 'removed' | 'added-required' | 'added-optional' | 'made-required' | 'made-optional', direction: 'narrowed' | 'widened' | null, path: string, member: string) => void;
|
|
75
|
+
unknown: (keyword: string, path: string, from: unknown, to: unknown) => void;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Classify every change from contract `a` to contract `b` by the §13
|
|
79
|
+
* rule table. Takes compiled contracts or raw documents (documents are
|
|
80
|
+
* compiled, so a malformed one refuses with its compile error before any
|
|
81
|
+
* comparison). Operations whose `policy.audience` is `server` on BOTH
|
|
82
|
+
* sides are outside the compatibility surface and are skipped; an
|
|
83
|
+
* audience flip is R14 and subsumes the operation's other changes.
|
|
84
|
+
* @param {Contract | Record<string, unknown>} a - the contract consumers hold today
|
|
85
|
+
* @param {Contract | Record<string, unknown>} b - the contract they would meet
|
|
86
|
+
* @returns {ContractDiff}
|
|
87
|
+
* @example
|
|
88
|
+
* const { breaking } = diffContracts(v1Doc, v2Doc);
|
|
89
|
+
* if (breaking.length > 0) throw new Error(breaking.map((c) => `${c.rule} ${c.op}: ${c.kind}`).join('\n'));
|
|
90
|
+
*/
|
|
91
|
+
export declare function diffContracts(a: Contract | Record<string, unknown>, b: Contract | Record<string, unknown>): ContractDiff;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Error types for @jarenjs/contract, built on `@jarenjs/core`'s
|
|
3
|
+
* coded contract: every failure carries a stable `code`, a bare
|
|
4
|
+
* `reason`, a composed `message`, and — for compile errors — the
|
|
5
|
+
* `docPath` of the offending member of the contract document. The
|
|
6
|
+
* normative table lives in docs/CONTRACT-FORMAT.md §6, proven in sync
|
|
7
|
+
* with `CONTRACT_CODES` below by a test.
|
|
8
|
+
*
|
|
9
|
+
* The `JC` code space is partitioned by range so the layers of this
|
|
10
|
+
* package never collide:
|
|
11
|
+
*
|
|
12
|
+
* - `JC0001–JC0049` document compile (`ContractCompileError`)
|
|
13
|
+
* - `JC0050–JC0069` binding declaration and projection compile
|
|
14
|
+
* (`JC0060`: the OpenAPI keyword policy)
|
|
15
|
+
* - `JC1001–JC1049` host programming errors (thrown `TypeError`s)
|
|
16
|
+
* - `JC2001–JC2049` http request-time (`ContractRuntimeError`)
|
|
17
|
+
* - `JC2050–JC2069` client-side
|
|
18
|
+
* - `JC2070–JC2089` port/local bindings
|
|
19
|
+
* - `JC2090–JC2109` stream binding
|
|
20
|
+
*
|
|
21
|
+
* The document-compile range, the projection code, the host range, the
|
|
22
|
+
* http request-time range, the client range and the port/local range are
|
|
23
|
+
* populated; stream is reserved for its binding and is listed here so a
|
|
24
|
+
* later addition lands in its range rather than at the next free number.
|
|
25
|
+
*/
|
|
26
|
+
import { CodedError } from '@jarenjs/core/errors';
|
|
27
|
+
/**
|
|
28
|
+
* The runtime code table: one entry per code this package can raise,
|
|
29
|
+
* proven in sync with CONTRACT-FORMAT.md §6's normative table by a test.
|
|
30
|
+
*/
|
|
31
|
+
export declare const CONTRACT_CODES: Readonly<{
|
|
32
|
+
JC0001: "the document is not a well-formed contract object: not an object, $contract is not \"0.1\", $defs is not a map of schemas, a member is not a JSON value, or a member threw when read";
|
|
33
|
+
JC0002: "operations is not an object with at least one member";
|
|
34
|
+
JC0003: "an operation id is not a dotted lowercase identifier";
|
|
35
|
+
JC0004: "kind is none of read, command, subscribe";
|
|
36
|
+
JC0005: "input is not a schema whose effective type is object";
|
|
37
|
+
JC0006: "output is absent or not a schema";
|
|
38
|
+
JC0007: "a $ref resolves neither within the document nor against the registered schemas";
|
|
39
|
+
JC0008: "http.path is not a valid path template";
|
|
40
|
+
JC0009: "a path variable, http.in key or http.body names no input member, or a member is mapped to a location it cannot travel in";
|
|
41
|
+
JC0010: "two operations share method and canonical path shape";
|
|
42
|
+
JC0011: "errors is malformed: not an object, a code is not a lowercase hyphenated word, a status is not a 100–599 integer, or a schema is not a schema";
|
|
43
|
+
JC0012: "http.method is not an uppercase token of the supported set, http.status is not a 200–299 integer, or http.media is not a media type";
|
|
44
|
+
JC0013: "an unknown member in a closed object (the document root, an operation, policy, http, limits, retry, or an error declaration)";
|
|
45
|
+
JC0014: "a policy member is mistyped or outside its declared set";
|
|
46
|
+
JC0015: "id, version, compat or an operation doc is mistyped";
|
|
47
|
+
JC0016: "an operation bound to GET or HEAD carries a body-located member (a GET body)";
|
|
48
|
+
JC0017: "an opaque operation (a non-JSON http.media) declares a body-located member — its body is bytes the contract never decodes, so the member could never be validated";
|
|
49
|
+
JC0018: "a subscribe operation declares a policy.task other than switch — a subscription slot is replaced, never queued";
|
|
50
|
+
JC0019: "a subscribe operation is bound to a method other than GET — a stream is fetched, not sent";
|
|
51
|
+
JC0020: "a subscribe operation declares a policy.idempotency other than none — a subscription registers, it does not commit";
|
|
52
|
+
JC0060: "the OpenAPI projection met a schema keyword it cannot map honestly: a boolean required (draft-04 style) or a same-document $ref that lands outside $defs (both dropped and reported under lenient), or a components member inside a schema";
|
|
53
|
+
JC0061: "the public projection is not canonicalizable, so no revision exists — a string with an unpaired surrogate, say; docPath points at the offending value inside the projection";
|
|
54
|
+
JC1001: "serveHttp, serveLocal or servePort: handlers is not an object, a key names no operation of the contract, a value is not a function, or an option (a channel without postMessage, say) is malformed";
|
|
55
|
+
JC1002: "serveHttp, serveLocal or servePort: an operation has no handler (and, on serveHttp, options.partial is not set; an opaque operation needs none on the status-less bindings)";
|
|
56
|
+
JC1003: "a binding cannot carry a declared feature: an operation declares idempotency and serveHttp was given no ledger";
|
|
57
|
+
JC1004: "dispatch received a malformed request object (method or url not a string, headers not an object, body not a string, Uint8Array or null)";
|
|
58
|
+
JC1005: "a client or the contract effect was asked for an operation the contract does not declare, or invoke was asked for an opaque operation (use client.url on http; the status-less bindings cannot carry it at all)";
|
|
59
|
+
JC1006: "ctx.status(n) was called with a status that is not an integer in 200–299";
|
|
60
|
+
JC1007: "contractAppBinding: ops names an operation the contract does not declare, or namespace/statePath is malformed";
|
|
61
|
+
JC1008: "openHttpClient, openPortClient, client.url, createContractEffect, createContractSubscription or a projection (publicProjection, toOpenApi, toTypeScript, toMarkdown, contractTools): an argument or option is malformed (not a compiled contract, fetch/keys/sleep/createTaskEffect/projectError not a function, storage without read/write, a non-object input to url, an ops entry naming no or an opaque operation, a tool name outside ^[a-zA-Z0-9_-]{1,64}$ or shared by two operations)";
|
|
62
|
+
JC1009: "encodeSseEvent (the stream wire): an event, id or data string the SSE frame cannot carry — a bare carriage return inside data, a line terminator inside event or id";
|
|
63
|
+
JC1010: "client.subscribe was asked for an operation that is not a subscribe operation (invoke carries reads and commands; subscribe carries streams)";
|
|
64
|
+
JC2001: "no operation matches the request method and path (404)";
|
|
65
|
+
JC2002: "the path shape is served under other methods (405, Allow lists them)";
|
|
66
|
+
JC2003: "the request body exceeds policy.limits.maxBodyBytes, by content-length or by read length (413)";
|
|
67
|
+
JC2004: "a body-carrying operation received a content-type that is not its declared media (415)";
|
|
68
|
+
JC2005: "the request body is present and is not valid JSON, or its bytes are not valid UTF-8 (400)";
|
|
69
|
+
JC2006: "the reassembled input fails the operation's input validator (400)";
|
|
70
|
+
JC2007: "the operation requires an Idempotency-Key header and none was sent (400)";
|
|
71
|
+
JC2008: "the handler threw a non-declared error, rejected, or returned a hostile value (500; onError sees it)";
|
|
72
|
+
JC2009: "the idempotency ledger reports the key in progress (retryable) or bound to a different request (mismatch) (409)";
|
|
73
|
+
JC2010: "the handler value fails the output validator or a declared error's details fail its schema — the server broke the contract (500)";
|
|
74
|
+
JC2011: "the request path carries a malformed percent-escape (400)";
|
|
75
|
+
JC2012: "the query string is not decodable (400)";
|
|
76
|
+
JC2013: "the operation has no handler on this partial server (501)";
|
|
77
|
+
JC2014: "the If-Match precondition does not match the entity tag the handler armed (412)";
|
|
78
|
+
JC2015: "a declared header member is repeated when its schema is scalar, or fails transport decoding (400)";
|
|
79
|
+
JC2050: "the input fails the operation's input validator before anything was sent (kind contract)";
|
|
80
|
+
JC2051: "the request did not complete: the transport rejected or timed out (kind network, retryable)";
|
|
81
|
+
JC2052: "the request was cancelled through the caller's signal or client.close() (kind cancelled)";
|
|
82
|
+
JC2053: "a success response is not JSON or fails the operation's output validator (kind contract)";
|
|
83
|
+
JC2054: "the durable idempotency-key storage threw before the request was sent (kind contract)";
|
|
84
|
+
JC2055: "the server answered a status with a body that is neither a declared error nor a taxonomy error (kind contract; retryable for 5xx and 429)";
|
|
85
|
+
JC2056: "negotiation: the server does not answer a jaren-contract description at the well-known path, or describes another contract id";
|
|
86
|
+
JC2057: "negotiation: the server speaks a version neither end declares compatible";
|
|
87
|
+
JC2058: "the host threw while invoking an operation through the contract effect — projected into an outcome, never a string";
|
|
88
|
+
JC2070: "the handler failed on a local or port serving host: it threw a non-declared error, rejected, answered an undeclared code, or broke its output or error-details schema (kind contract; onError sees the cause)";
|
|
89
|
+
JC2071: "a request frame names no operation this channel serves (unknown, or opaque — a port carries JSON only)";
|
|
90
|
+
JC2072: "a port request got no answer within timeoutMs (kind network, retryable)";
|
|
91
|
+
JC2073: "a response frame addressed to this client does not match the frame grammar (kind contract)";
|
|
92
|
+
JC2074: "the channel refused the request frame — closed or detached (kind network)";
|
|
93
|
+
JC2090: "the server answered a subscribe request with a non-stream response (kind contract)";
|
|
94
|
+
JC2091: "a snapshot fails the operation's output validator — the server broke the contract; the stream ends with an error event carrying this code";
|
|
95
|
+
JC2092: "a stream event's seq is not strictly greater than the last one delivered (kind contract, client-side)";
|
|
96
|
+
JC2093: "the stream ended with a server error event whose code the operation does not declare (kind contract; a declared code is a failure outcome under its own code)";
|
|
97
|
+
JC2094: "the stream went silent for twice policy.stream.heartbeatMs (kind network, client-side)";
|
|
98
|
+
JC2095: "a requested resume was refused — informational, carried as resumed:false in the fresh snapshot's event data, never an outcome";
|
|
99
|
+
}>;
|
|
100
|
+
/**
|
|
101
|
+
* A defect in the contract document itself, raised while
|
|
102
|
+
* `compileContract` compiles it. Every instance carries the JSON
|
|
103
|
+
* Pointer of the offending member as `docPath` (`''` is the document
|
|
104
|
+
* root; an operation id is one reference token, so a dotted id such as
|
|
105
|
+
* `product.save` appears unescaped and only `~` and `/` are escaped per
|
|
106
|
+
* RFC 6901). The codes are `JC0001–JC0049` (docs/CONTRACT-FORMAT.md §6).
|
|
107
|
+
*/
|
|
108
|
+
export declare class ContractCompileError extends CodedError {
|
|
109
|
+
/**
|
|
110
|
+
* @param {string} code
|
|
111
|
+
* @param {string} reason - The bare reason; `message` is composed per
|
|
112
|
+
* the coded contract.
|
|
113
|
+
* @param {string} [docPath] - JSON Pointer into the contract document;
|
|
114
|
+
* `''` is the document root, `undefined` means no location.
|
|
115
|
+
* @param {Error} [cause]
|
|
116
|
+
*/
|
|
117
|
+
constructor(code: string, reason: string, docPath?: string, cause?: Error);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* A request-time failure (`JC2xxx`) as it exists in-process before a
|
|
121
|
+
* binding maps it onto its wire: a stable `code`, a `msgid`
|
|
122
|
+
* (`contract/<slug>`) with `params` for the locale catalogs, and the
|
|
123
|
+
* transport hints a binding may carry (`status`, `retryable`). It has no
|
|
124
|
+
* document location — a request is not a document — so `docPath` is
|
|
125
|
+
* always `undefined`. Never thrown across a binding: a binding settles
|
|
126
|
+
* it into a wire error.
|
|
127
|
+
*/
|
|
128
|
+
export declare class ContractRuntimeError extends CodedError {
|
|
129
|
+
/** @type {string} */
|
|
130
|
+
msgid: string;
|
|
131
|
+
/** @type {Record<string, unknown>} */
|
|
132
|
+
params: Record<string, unknown>;
|
|
133
|
+
/** @type {number | undefined} */
|
|
134
|
+
status: number | undefined;
|
|
135
|
+
/** @type {boolean | undefined} */
|
|
136
|
+
retryable: boolean | undefined;
|
|
137
|
+
/**
|
|
138
|
+
* @param {string} code
|
|
139
|
+
* @param {string} reason - The bare English reason; `message` is
|
|
140
|
+
* composed per the coded contract.
|
|
141
|
+
* @param {{ msgid: string, params?: Record<string, unknown>, status?: number, retryable?: boolean, cause?: unknown }} options
|
|
142
|
+
* `msgid` is the catalog key; `cause` is installed as an own property
|
|
143
|
+
* exactly when the key is present (`hasOwn` form).
|
|
144
|
+
*/
|
|
145
|
+
constructor(code: string, reason: string, options: {
|
|
146
|
+
msgid: string;
|
|
147
|
+
params?: Record<string, unknown>;
|
|
148
|
+
status?: number;
|
|
149
|
+
retryable?: boolean;
|
|
150
|
+
cause?: unknown;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* A host programming error at a binding's construction or use — a
|
|
155
|
+
* handler table that names no operation, a missing ledger for a declared
|
|
156
|
+
* idempotency policy, a malformed request object handed to `dispatch`, a
|
|
157
|
+
* `ctx.status` outside 2xx. `TypeError`, thrown, never a wire response:
|
|
158
|
+
* the mistake is the host's, not the request's. The codes are
|
|
159
|
+
* `JC1001–JC1049` (docs/CONTRACT-FORMAT.md §7).
|
|
160
|
+
*/
|
|
161
|
+
export declare class ContractHostError extends TypeError {
|
|
162
|
+
/** @type {string} */
|
|
163
|
+
code: string;
|
|
164
|
+
/** @type {string} */
|
|
165
|
+
reason: string;
|
|
166
|
+
/**
|
|
167
|
+
* @param {string} code
|
|
168
|
+
* @param {string} reason - The bare reason; `message` is `${code}: ${reason}`.
|
|
169
|
+
*/
|
|
170
|
+
constructor(code: string, reason: string);
|
|
171
|
+
}
|
|
172
|
+
export type ContractFailureValue = {
|
|
173
|
+
code: string;
|
|
174
|
+
params: Readonly<Record<string, unknown>>;
|
|
175
|
+
/**
|
|
176
|
+
* - `undefined` when the failure carries none
|
|
177
|
+
*/
|
|
178
|
+
details: unknown;
|
|
179
|
+
/**
|
|
180
|
+
* - `null` defers to the operation's retry policy
|
|
181
|
+
*/
|
|
182
|
+
retryable: boolean | null;
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* Make a declared failure value: what a handler returns (or `ctx.fail`
|
|
186
|
+
* returns for it) to answer with one of the operation's declared error
|
|
187
|
+
* codes. A branded plain-object factory, not a class: the value crosses
|
|
188
|
+
* no binding as an `Error` and carries only JSON.
|
|
189
|
+
* @param {string} code - A code the operation declares in `errors`
|
|
190
|
+
* @param {Record<string, unknown>} [params] - Message parameters for the catalog
|
|
191
|
+
* @param {unknown} [details] - The wire `details` member
|
|
192
|
+
* @param {{ retryable?: boolean }} [options] - `retryable` overrides the
|
|
193
|
+
* default taken from the operation's `policy.retry.on`
|
|
194
|
+
* @returns {ContractFailureValue}
|
|
195
|
+
*/
|
|
196
|
+
export declare function ContractFailure(code: string, params?: Record<string, unknown>, details?: unknown, options?: {
|
|
197
|
+
retryable?: boolean;
|
|
198
|
+
}): ContractFailureValue;
|
|
199
|
+
/**
|
|
200
|
+
* True exactly for a value `ContractFailure` produced. Reads nothing
|
|
201
|
+
* from the value, so it is total for a hostile object.
|
|
202
|
+
* @param {unknown} value
|
|
203
|
+
* @returns {value is ContractFailureValue}
|
|
204
|
+
*/
|
|
205
|
+
export declare function isContractFailure(value: unknown): value is ContractFailureValue;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The request pipeline of the HTTP server binding: one plain
|
|
3
|
+
* request object in, one plain response object out — route, decode,
|
|
4
|
+
* assemble, normalize, validate, claim idempotency, call the handler
|
|
5
|
+
* through one uniform promise boundary, validate the output, apply the
|
|
6
|
+
* entity-tag conditionals, serialize, commit. Every failure a request
|
|
7
|
+
* can cause is a coded response (docs/CONTRACT-FORMAT.md §7); the
|
|
8
|
+
* function rejects only for a malformed request OBJECT (`JC1004`, an
|
|
9
|
+
* adapter author's mistake) — never for request content and never for
|
|
10
|
+
* what a handler returns or throws (the `tasks.js` posture: a hostile
|
|
11
|
+
* value settles into `JC2008`/`JC2010`, it does not escape).
|
|
12
|
+
*
|
|
13
|
+
* The trust boundary: the contract, the handlers and the ledger are the
|
|
14
|
+
* host's; the request is hostile. Only declared header members and the
|
|
15
|
+
* protocol headers the binding itself needs are read; the input object
|
|
16
|
+
* is assembled through `setObjectMember` from declared names only; a
|
|
17
|
+
* body member that names a path/query/header member is ignored; nothing
|
|
18
|
+
* a request sent is echoed into a message.
|
|
19
|
+
*/
|
|
20
|
+
export type HttpRequest = import('./wire.js').HttpRequest;
|
|
21
|
+
export type HttpResponse = import('./wire.js').HttpResponse;
|
|
22
|
+
export type Catalog = import('./wire.js').Catalog;
|
|
23
|
+
export type CompiledOperation = import('../compile.js').CompiledOperation;
|
|
24
|
+
export type CompiledErrorDecl = import('../compile.js').CompiledErrorDecl;
|
|
25
|
+
export type ContractFailureValue = import('../errors.js').ContractFailureValue;
|
|
26
|
+
export type OperationResult = import('../pipeline.js').OperationResult;
|
|
27
|
+
export type Ledger = import('../ledger.js').Ledger;
|
|
28
|
+
export type RequestContext = {
|
|
29
|
+
op: CompiledOperation;
|
|
30
|
+
trace: string;
|
|
31
|
+
method: string;
|
|
32
|
+
path: string;
|
|
33
|
+
params: Readonly<Record<string, string>>;
|
|
34
|
+
headers: Readonly<Record<string, string>>;
|
|
35
|
+
body: string | Uint8Array | null;
|
|
36
|
+
signal: AbortSignal | null;
|
|
37
|
+
idempotency: Readonly<{
|
|
38
|
+
key: string;
|
|
39
|
+
scope: string;
|
|
40
|
+
}> | null;
|
|
41
|
+
fail: (code: string, params?: Record<string, unknown>, details?: unknown, options?: {
|
|
42
|
+
retryable?: boolean;
|
|
43
|
+
}) => ContractFailureValue;
|
|
44
|
+
etag: (tag: string, options?: {
|
|
45
|
+
strong?: boolean;
|
|
46
|
+
}) => void;
|
|
47
|
+
status: (status: number) => void;
|
|
48
|
+
};
|
|
49
|
+
export type Handler = (input: any, ctx: RequestContext) => unknown;
|
|
50
|
+
export type RawResponse = {
|
|
51
|
+
status: number;
|
|
52
|
+
headers?: Record<string, string>;
|
|
53
|
+
body?: string | Uint8Array | null;
|
|
54
|
+
};
|
|
55
|
+
export type Route = {
|
|
56
|
+
op: CompiledOperation;
|
|
57
|
+
/**
|
|
58
|
+
* - `null` on a partial server
|
|
59
|
+
*/
|
|
60
|
+
handler: Handler | null;
|
|
61
|
+
/**
|
|
62
|
+
* - opaque: the handler is raw
|
|
63
|
+
*/
|
|
64
|
+
raw: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* - a subscribe operation: the handler answers a subscription
|
|
67
|
+
*/
|
|
68
|
+
stream: boolean;
|
|
69
|
+
maxBody: number;
|
|
70
|
+
media: string;
|
|
71
|
+
/**
|
|
72
|
+
* - any body-located member, or a whole-body member
|
|
73
|
+
*/
|
|
74
|
+
hasBody: boolean;
|
|
75
|
+
wholeBody: string | null;
|
|
76
|
+
/**
|
|
77
|
+
* - path/query/header member names, never taken from the body
|
|
78
|
+
*/
|
|
79
|
+
nonBody: ReadonlySet<string>;
|
|
80
|
+
pathMembers: readonly string[];
|
|
81
|
+
queryMembers: ReadonlySet<string>;
|
|
82
|
+
/**
|
|
83
|
+
* - array-typed query members
|
|
84
|
+
*/
|
|
85
|
+
repeated: ReadonlySet<string>;
|
|
86
|
+
/**
|
|
87
|
+
* - member names
|
|
88
|
+
*/
|
|
89
|
+
headerMembers: readonly string[];
|
|
90
|
+
/**
|
|
91
|
+
* - the lowercase header of each
|
|
92
|
+
*/
|
|
93
|
+
headerNames: readonly string[];
|
|
94
|
+
/**
|
|
95
|
+
* - array-typed, per header member
|
|
96
|
+
*/
|
|
97
|
+
headerArray: readonly boolean[];
|
|
98
|
+
normalize: ((value: any) => any) | null;
|
|
99
|
+
validateInput: ((value: unknown) => any) | null;
|
|
100
|
+
validateOutput: (value: unknown) => any;
|
|
101
|
+
details: 'none' | 'paths' | 'full';
|
|
102
|
+
idempotency: 'none' | 'optional' | 'required';
|
|
103
|
+
retryOn: ReadonlySet<string>;
|
|
104
|
+
errors: Readonly<Record<string, CompiledErrorDecl>>;
|
|
105
|
+
status: number;
|
|
106
|
+
};
|
|
107
|
+
export type Server = {
|
|
108
|
+
contract: import('../compile.js').Contract;
|
|
109
|
+
routes: ReadonlyMap<string, Route>;
|
|
110
|
+
trace: () => string;
|
|
111
|
+
ledger: Ledger | null;
|
|
112
|
+
scope: (ctx: RequestContext) => string;
|
|
113
|
+
head: boolean;
|
|
114
|
+
validateOutput: boolean;
|
|
115
|
+
wellKnown: string | false;
|
|
116
|
+
errorBody: ((wire: any, ctx: RequestContext | null) => unknown) | null;
|
|
117
|
+
onError: ((error: unknown, ctx: RequestContext | null) => void) | null;
|
|
118
|
+
catalog: Catalog | null;
|
|
119
|
+
now: () => number;
|
|
120
|
+
/**
|
|
121
|
+
* - the memoized well-known body
|
|
122
|
+
*/
|
|
123
|
+
described: {
|
|
124
|
+
text: string | null;
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* - the live SSE
|
|
128
|
+
* streams' stoppers; the dispatcher's `close()` ends them all
|
|
129
|
+
*/
|
|
130
|
+
streams: Set<(reason: string | null) => void>;
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* The one entry point. Validates the request object (a malformed one is
|
|
134
|
+
* `JC1004`, rejected — the adapter's mistake), then runs the pipeline and
|
|
135
|
+
* lifts its result into a promise. Total for request content: every
|
|
136
|
+
* request-caused failure resolves to a response.
|
|
137
|
+
* @param {Server} server
|
|
138
|
+
* @param {HttpRequest} request
|
|
139
|
+
* @returns {Promise<HttpResponse>}
|
|
140
|
+
*/
|
|
141
|
+
export declare function dispatch(server: Server, request: HttpRequest): Promise<HttpResponse>;
|
|
142
|
+
export type Armed = {
|
|
143
|
+
etag: string | null;
|
|
144
|
+
strong: boolean;
|
|
145
|
+
status: number;
|
|
146
|
+
outcome: number;
|
|
147
|
+
retryable: boolean;
|
|
148
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file `serveHttp(contract, handlers, options)`: the HTTP server binding
|
|
3
|
+
* of a compiled contract — a driver in the `@jarenjs/db` sense: a
|
|
4
|
+
* `name`, a frozen `capabilities` table that says what this binding
|
|
5
|
+
* cannot carry (never a silent downgrade), and the one method a host
|
|
6
|
+
* calls per request, `dispatch(request) → Promise<response>` over plain
|
|
7
|
+
* request/response objects (docs/CONTRACT-FORMAT.md §7). The adapters
|
|
8
|
+
* (`@jarenjs/contract/fetch`, `/node`) put that function behind the
|
|
9
|
+
* platform's request and response types.
|
|
10
|
+
*
|
|
11
|
+
* Construction refuses host mistakes with thrown `ContractHostError`s
|
|
12
|
+
* (`JC1001–JC1003`): a handler table that names no operation, a missing
|
|
13
|
+
* handler on a non-partial server, a declared idempotency policy with no
|
|
14
|
+
* ledger to carry it. Everything the pipeline reads per request is
|
|
15
|
+
* decided here, once, into a `Route` per operation.
|
|
16
|
+
*/
|
|
17
|
+
import { HTTP_ERRORS, WELL_KNOWN_PATH } from './wire.js';
|
|
18
|
+
export { HTTP_ERRORS, WELL_KNOWN_PATH };
|
|
19
|
+
export type HttpRequest = import('./wire.js').HttpRequest;
|
|
20
|
+
export type HttpResponse = import('./wire.js').HttpResponse;
|
|
21
|
+
export type WireErrorBody = import('./wire.js').WireErrorBody;
|
|
22
|
+
export type RequestContext = import('./dispatch.js').RequestContext;
|
|
23
|
+
export type Handler = import('./dispatch.js').Handler;
|
|
24
|
+
export type RawResponse = import('./dispatch.js').RawResponse;
|
|
25
|
+
export type Route = import('./dispatch.js').Route;
|
|
26
|
+
export type Server = import('./dispatch.js').Server;
|
|
27
|
+
export type Ledger = import('../ledger.js').Ledger;
|
|
28
|
+
export type Contract = import('../compile.js').Contract;
|
|
29
|
+
export type CompiledOperation = import('../compile.js').CompiledOperation;
|
|
30
|
+
export type ServeHttpOptions = {
|
|
31
|
+
/**
|
|
32
|
+
* - the server trace generator; default `crypto.randomUUID`
|
|
33
|
+
*/
|
|
34
|
+
trace?: () => string;
|
|
35
|
+
/**
|
|
36
|
+
* - the idempotency ledger; `null` refuses
|
|
37
|
+
* (`JC1003`) any operation whose `policy.idempotency` is not `none`
|
|
38
|
+
*/
|
|
39
|
+
ledger?: Ledger | null;
|
|
40
|
+
/**
|
|
41
|
+
* - the idempotency scope
|
|
42
|
+
* of a request (an installation, a principal — never a rotating token);
|
|
43
|
+
* default `''`; called before `ctx.idempotency` is set
|
|
44
|
+
*/
|
|
45
|
+
scope?: (ctx: RequestContext) => string;
|
|
46
|
+
/**
|
|
47
|
+
* - allow missing handlers; a missing one answers 501 `JC2013`
|
|
48
|
+
*/
|
|
49
|
+
partial?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* - answer HEAD for GET operations by running the handler and dropping the body; default true
|
|
52
|
+
*/
|
|
53
|
+
head?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* - `'never'` is a declared downgrade, reported in `capabilities.validatedOutput`
|
|
56
|
+
*/
|
|
57
|
+
validateOutput?: 'always' | 'never';
|
|
58
|
+
/**
|
|
59
|
+
* - the path answering `describe()`; default `/.well-known/jaren-contract`; `false` disables
|
|
60
|
+
*/
|
|
61
|
+
wellKnown?: string | false;
|
|
62
|
+
/**
|
|
63
|
+
* - projects the wire error record into the response body (a legacy
|
|
64
|
+
* shape, an extra `error` string); a throw or a non-JSON result falls
|
|
65
|
+
* back to the D7 shape
|
|
66
|
+
*/
|
|
67
|
+
errorBody?: (wire: WireErrorBody & {
|
|
68
|
+
status: number;
|
|
69
|
+
}, ctx: RequestContext | null) => unknown;
|
|
70
|
+
/**
|
|
71
|
+
* - observes `JC2008`/`JC2010` causes and ledger faults; the response never carries them
|
|
72
|
+
*/
|
|
73
|
+
onError?: (error: unknown, ctx: RequestContext | null) => void;
|
|
74
|
+
/**
|
|
75
|
+
* - a message catalog (templates or compiled renderers) consulted before the English one
|
|
76
|
+
*/
|
|
77
|
+
catalog?: Record<string, string | ((params: object) => string)>;
|
|
78
|
+
/**
|
|
79
|
+
* - the clock stamped into ledger claims; default `Date.now`
|
|
80
|
+
*/
|
|
81
|
+
now?: () => number;
|
|
82
|
+
};
|
|
83
|
+
export type HttpCapabilities = {
|
|
84
|
+
name: 'http';
|
|
85
|
+
/**
|
|
86
|
+
* - statuses are carried
|
|
87
|
+
*/
|
|
88
|
+
status: true;
|
|
89
|
+
/**
|
|
90
|
+
* - headers are carried
|
|
91
|
+
*/
|
|
92
|
+
headers: true;
|
|
93
|
+
/**
|
|
94
|
+
* - non-JSON media is carried (opaque operations)
|
|
95
|
+
*/
|
|
96
|
+
media: true;
|
|
97
|
+
/**
|
|
98
|
+
* - HEAD is answered for GET operations
|
|
99
|
+
*/
|
|
100
|
+
head: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* - entity tags and conditionals are honored
|
|
103
|
+
*/
|
|
104
|
+
etag: true;
|
|
105
|
+
/**
|
|
106
|
+
* - a ledger is present
|
|
107
|
+
*/
|
|
108
|
+
idempotency: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* - the output validator runs
|
|
111
|
+
*/
|
|
112
|
+
validatedOutput: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* - subscribe operations stream as SSE (docs/CONTRACT-FORMAT.md §18.1)
|
|
115
|
+
*/
|
|
116
|
+
stream: true;
|
|
117
|
+
/**
|
|
118
|
+
* - cancellation reaches the handler as `ctx.signal`
|
|
119
|
+
*/
|
|
120
|
+
cancel: 'signal';
|
|
121
|
+
};
|
|
122
|
+
export type HttpDispatcher = {
|
|
123
|
+
dispatch: (request: HttpRequest) => Promise<HttpResponse>;
|
|
124
|
+
capabilities: HttpCapabilities;
|
|
125
|
+
contract: Contract;
|
|
126
|
+
describe: () => any;
|
|
127
|
+
/**
|
|
128
|
+
* - ends every live SSE stream with an `end`
|
|
129
|
+
* event (`server-shutdown`) and releases its subscription; requests in
|
|
130
|
+
* flight are unaffected
|
|
131
|
+
*/
|
|
132
|
+
close: () => void;
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Serve a compiled contract over HTTP: a dispatcher whose `dispatch`
|
|
136
|
+
* routes, decodes, normalizes, validates, calls the handler, validates
|
|
137
|
+
* the output, applies idempotency and entity-tag policy and answers with
|
|
138
|
+
* the declared statuses and the D7 error body — a pure function over
|
|
139
|
+
* plain request/response objects. Refuses host mistakes at construction
|
|
140
|
+
* (`JC1001` handler table, `JC1002` missing handler, `JC1003` idempotency
|
|
141
|
+
* without a ledger).
|
|
142
|
+
*
|
|
143
|
+
* @param {Contract} contract
|
|
144
|
+
* @param {Record<string, Handler>} handlers - operation id → handler
|
|
145
|
+
* @param {ServeHttpOptions} [options]
|
|
146
|
+
* @returns {HttpDispatcher}
|
|
147
|
+
* @example
|
|
148
|
+
* const server = serveHttp(contract, {
|
|
149
|
+
* 'catalog.load': async (input, ctx) => { ctx.etag('r42'); return catalog; },
|
|
150
|
+
* 'product.save': (input, ctx) => saved ? product : ctx.fail('conflict', {}, { current }),
|
|
151
|
+
* }, { ledger: createMemoryLedger() });
|
|
152
|
+
* const response = await server.dispatch({ method: 'GET', url: '/api/catalog', headers: {}, body: null });
|
|
153
|
+
*/
|
|
154
|
+
export declare function serveHttp(contract: Contract, handlers: Record<string, Handler>, options?: ServeHttpOptions): HttpDispatcher;
|