@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.
Files changed (84) hide show
  1. package/README.md +508 -0
  2. package/dist/types/adapters/fetch.d.ts +27 -0
  3. package/dist/types/adapters/node.d.ts +47 -0
  4. package/dist/types/app/binding.d.ts +122 -0
  5. package/dist/types/app/effect.d.ts +77 -0
  6. package/dist/types/app/index.d.ts +31 -0
  7. package/dist/types/app/subscription.d.ts +82 -0
  8. package/dist/types/bundle.d.ts +43 -0
  9. package/dist/types/cli.d.ts +15 -0
  10. package/dist/types/client/http.d.ts +242 -0
  11. package/dist/types/client/outcome.d.ts +289 -0
  12. package/dist/types/compat.d.ts +36 -0
  13. package/dist/types/compile.d.ts +196 -0
  14. package/dist/types/describe.d.ts +115 -0
  15. package/dist/types/diff.d.ts +91 -0
  16. package/dist/types/errors.d.ts +205 -0
  17. package/dist/types/http/dispatch.d.ts +148 -0
  18. package/dist/types/http/serve.d.ts +154 -0
  19. package/dist/types/http/wire.d.ts +334 -0
  20. package/dist/types/index.d.ts +39 -0
  21. package/dist/types/ledger.d.ts +207 -0
  22. package/dist/types/local/index.d.ts +127 -0
  23. package/dist/types/messages.d.ts +63 -0
  24. package/dist/types/path.d.ts +119 -0
  25. package/dist/types/pipeline.d.ts +157 -0
  26. package/dist/types/port/client.d.ts +142 -0
  27. package/dist/types/port/frame.d.ts +195 -0
  28. package/dist/types/port/serve.d.ts +102 -0
  29. package/dist/types/project/index.d.ts +34 -0
  30. package/dist/types/project/markdown.d.ts +28 -0
  31. package/dist/types/project/openapi.d.ts +102 -0
  32. package/dist/types/project/tools.d.ts +57 -0
  33. package/dist/types/project/typescript.d.ts +59 -0
  34. package/dist/types/public.d.ts +73 -0
  35. package/dist/types/revision.d.ts +36 -0
  36. package/dist/types/stream/client.d.ts +104 -0
  37. package/dist/types/stream/server.d.ts +106 -0
  38. package/dist/types/stream/sse.d.ts +62 -0
  39. package/docs/APP-INTEGRATION.md +301 -0
  40. package/docs/CONTRACT-FORMAT.md +1923 -0
  41. package/package.json +110 -0
  42. package/schemas/jaren-contract-port.draft-07.schema.json +241 -0
  43. package/schemas/jaren-contract-port.schema.json +241 -0
  44. package/schemas/jaren-contract.draft-07.schema.json +287 -0
  45. package/schemas/jaren-contract.schema.json +287 -0
  46. package/src/adapters/fetch.js +109 -0
  47. package/src/adapters/node.js +238 -0
  48. package/src/app/binding.js +426 -0
  49. package/src/app/effect.js +190 -0
  50. package/src/app/index.js +26 -0
  51. package/src/app/subscription.js +130 -0
  52. package/src/bundle.js +168 -0
  53. package/src/cli.js +264 -0
  54. package/src/client/http.js +1150 -0
  55. package/src/client/outcome.js +364 -0
  56. package/src/compat.js +62 -0
  57. package/src/compile.js +1162 -0
  58. package/src/describe.js +109 -0
  59. package/src/diff.js +610 -0
  60. package/src/errors.js +236 -0
  61. package/src/http/dispatch.js +1054 -0
  62. package/src/http/serve.js +301 -0
  63. package/src/http/wire.js +469 -0
  64. package/src/index.js +33 -0
  65. package/src/ledger.js +225 -0
  66. package/src/local/index.js +363 -0
  67. package/src/messages.js +68 -0
  68. package/src/path.js +471 -0
  69. package/src/pipeline.js +241 -0
  70. package/src/port/client.js +518 -0
  71. package/src/port/frame.js +196 -0
  72. package/src/port/serve.js +442 -0
  73. package/src/project/index.js +29 -0
  74. package/src/project/markdown.js +244 -0
  75. package/src/project/openapi.js +564 -0
  76. package/src/project/openapi.jslt.json +149 -0
  77. package/src/project/tools.js +139 -0
  78. package/src/project/typescript.js +152 -0
  79. package/src/project/typescript.jtlt.json +72 -0
  80. package/src/public.js +206 -0
  81. package/src/revision.js +90 -0
  82. package/src/stream/client.js +212 -0
  83. package/src/stream/server.js +306 -0
  84. 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;