@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,469 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The wire shapes of the HTTP server binding: the request-time
4
+ * error taxonomy as data (code → status, msgid, retryable), the error
5
+ * body every non-2xx JSON response carries, header reading, media
6
+ * matching, entity-tag comparison and the query decoder. Nothing here
7
+ * calls a handler or touches a ledger; `dispatch.js` composes these.
8
+ *
9
+ * Every response header name is lowercase; every error response carries
10
+ * `x-jaren-trace` (the server trace, `requestId` in the body) and
11
+ * `cache-control: no-store`. A message is rendered from the msgid
12
+ * through the catalog and never interpolates a request value — the
13
+ * parameters are the operation id, a declared limit, a media type, a
14
+ * method list, a declared header name or a declared error code
15
+ * (docs/CONTRACT-FORMAT.md §7).
16
+ */
17
+
18
+ import { isJsonValue, setObjectMember } from '@jarenjs/core/object';
19
+
20
+ import { contractCatalogEn } from '../messages.js';
21
+
22
+ /**
23
+ * A request as the binding sees it — what an adapter builds and what a
24
+ * test hands to `dispatch` directly. `url` is origin-less: the path plus
25
+ * an optional `?query`; header names are lowercase; a header value is a
26
+ * string, or an array of strings when the adapter can see repeated field
27
+ * lines; `body` is the bytes/text as received (`null` for none);
28
+ * `signal` is the request's abort signal when the host has one.
29
+ * @typedef {Object} HttpRequest
30
+ * @property {string} method
31
+ * @property {string} url
32
+ * @property {Readonly<Record<string, string | readonly string[]>>} headers
33
+ * @property {string | Uint8Array | null} body
34
+ * @property {AbortSignal | null} [signal]
35
+ */
36
+
37
+ /**
38
+ * A response as the binding answers it: a status, lowercase header names,
39
+ * and a body that is a string (JSON text), bytes (an opaque operation) or
40
+ * `null` (HEAD, 204, 304). A streaming response (a subscribe operation
41
+ * under `accept: text/event-stream`) carries `body: null` plus `stream`:
42
+ * the adapter writes the headers, then MUST call `stream` exactly once
43
+ * with its sink — the pump writes SSE text through `sink.write` and
44
+ * calls `sink.end()` when the stream terminates; the returned function
45
+ * stops the stream when the consumer cancels.
46
+ * @typedef {Object} HttpResponse
47
+ * @property {number} status
48
+ * @property {Readonly<Record<string, string>>} headers
49
+ * @property {string | Uint8Array | null} body
50
+ * @property {(sink: { write: (chunk: string) => void, end: () => void }) => (() => void)} [stream]
51
+ */
52
+
53
+ /**
54
+ * The D7 error body of every non-2xx JSON response.
55
+ * @typedef {Object} WireErrorBody
56
+ * @property {string} code - a `JC2xxx` code, or the declared error code
57
+ * @property {string} message
58
+ * @property {string} requestId - the server trace (`x-jaren-trace`)
59
+ * @property {unknown} [details]
60
+ * @property {boolean} retryable
61
+ */
62
+
63
+ /**
64
+ * The taxonomy row of one request-time code.
65
+ * @typedef {{ status: number, msgid: string, retryable: boolean }} WireErrorRow
66
+ */
67
+
68
+ /**
69
+ * The request-time taxonomy as data: code → `{ status, msgid, retryable }`.
70
+ * The normative table is docs/CONTRACT-FORMAT.md §7; a test holds the
71
+ * two equal, and equal to `CONTRACT_CODES` and the English catalog. The
72
+ * client binding assembles outcomes from exactly these statuses.
73
+ */
74
+ export const HTTP_ERRORS = Object.freeze({
75
+ JC2001: Object.freeze({ status: 404, msgid: 'contract/not-found', retryable: false }),
76
+ JC2002: Object.freeze({ status: 405, msgid: 'contract/method-not-allowed', retryable: false }),
77
+ JC2003: Object.freeze({ status: 413, msgid: 'contract/body-too-large', retryable: false }),
78
+ JC2004: Object.freeze({ status: 415, msgid: 'contract/unsupported-media', retryable: false }),
79
+ JC2005: Object.freeze({ status: 400, msgid: 'contract/malformed-json', retryable: false }),
80
+ JC2006: Object.freeze({ status: 400, msgid: 'contract/invalid-input', retryable: false }),
81
+ JC2007: Object.freeze({ status: 400, msgid: 'contract/idempotency-key-required', retryable: false }),
82
+ JC2008: Object.freeze({ status: 500, msgid: 'contract/handler-failed', retryable: false }),
83
+ JC2009: Object.freeze({ status: 409, msgid: 'contract/idempotency-conflict', retryable: false }),
84
+ JC2010: Object.freeze({ status: 500, msgid: 'contract/invalid-output', retryable: false }),
85
+ JC2011: Object.freeze({ status: 400, msgid: 'contract/malformed-path', retryable: false }),
86
+ JC2012: Object.freeze({ status: 400, msgid: 'contract/malformed-query', retryable: false }),
87
+ JC2013: Object.freeze({ status: 501, msgid: 'contract/not-implemented', retryable: false }),
88
+ JC2014: Object.freeze({ status: 412, msgid: 'contract/precondition-failed', retryable: false }),
89
+ JC2015: Object.freeze({ status: 400, msgid: 'contract/invalid-header', retryable: false }),
90
+ });
91
+
92
+ /** The msgid of a declared operation error that has no message of its own. */
93
+ export const HANDLER_ERROR_MSGID = 'contract/handler-error';
94
+
95
+ /** The default well-known negotiation path, served by `serveHttp` and asked by the client. */
96
+ export const WELL_KNOWN_PATH = '/.well-known/jaren-contract';
97
+
98
+ /** The default JSON media of the binding. */
99
+ export const JSON_MEDIA = 'application/json';
100
+
101
+ /** The response `content-type` of a JSON body. */
102
+ export const JSON_CONTENT_TYPE = 'application/json; charset=utf-8';
103
+
104
+ /** The catalog fallback for a code that renders in no catalog. */
105
+ const NO_MESSAGE = 'request failed';
106
+
107
+ //#region messages
108
+
109
+ /**
110
+ * A compiled catalog: msgid → render.
111
+ * @typedef {Readonly<Record<string, (params: object, error?: object) => string>>} Catalog
112
+ */
113
+
114
+ /**
115
+ * Render a message: the host catalog first, the English catalog second,
116
+ * a fixed fallback last. TOTAL: a rendering closure that throws yields
117
+ * the fallback rather than escaping into the response path.
118
+ * @param {Catalog | null} catalog - the host catalog, or null for English only
119
+ * @param {string} msgid
120
+ * @param {Record<string, unknown>} params
121
+ * @returns {string}
122
+ */
123
+ export function renderMessage(catalog, msgid, params) {
124
+ let render = catalog !== null ? catalog[msgid] : undefined;
125
+ if (render === undefined) render = contractCatalogEn[msgid];
126
+ if (render === undefined) return NO_MESSAGE;
127
+ try {
128
+ const text = render(params);
129
+ return typeof text === 'string' ? text : NO_MESSAGE;
130
+ }
131
+ catch {
132
+ return NO_MESSAGE;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * The message of a declared operation error, as every server-side
138
+ * binding renders it: `contract/error/<code>` from the host catalog when
139
+ * it defines one, else the generic `contract/handler-error` — with the
140
+ * failure's own params plus `op` and `code`.
141
+ * @param {Catalog | null} catalog
142
+ * @param {string} op - the operation id
143
+ * @param {string} code - the declared error code
144
+ * @param {Readonly<Record<string, unknown>>} params
145
+ * @returns {string}
146
+ */
147
+ export function declaredMessage(catalog, op, code, params) {
148
+ const messageParams = { ...params, op, code };
149
+ const own = catalog !== null ? catalog[`contract/error/${code}`] : undefined;
150
+ return own !== undefined
151
+ ? renderMessage(catalog, `contract/error/${code}`, messageParams)
152
+ : renderMessage(catalog, HANDLER_ERROR_MSGID, messageParams);
153
+ }
154
+
155
+ //#endregion
156
+
157
+ //#region headers
158
+
159
+ /**
160
+ * The single string value of a header: a repeated field is combined with
161
+ * `, ` (RFC 9110 §5.3), an absent one is `undefined`. For the protocol
162
+ * headers the binding itself reads (`content-type`, `content-length`,
163
+ * `idempotency-key`, `if-match`, `if-none-match`).
164
+ * @param {Readonly<Record<string, string | readonly string[]>>} headers
165
+ * @param {string} name - lowercase
166
+ * @returns {string | undefined}
167
+ */
168
+ export function headerValue(headers, name) {
169
+ const v = headers[name];
170
+ if (v === undefined || typeof v === 'string') return v;
171
+ if (Array.isArray(v)) {
172
+ if (v.length === 0) return undefined;
173
+ if (v.length === 1) return typeof v[0] === 'string' ? v[0] : undefined;
174
+ let out = '';
175
+ for (let i = 0; i < v.length; i++) {
176
+ if (typeof v[i] !== 'string') return undefined;
177
+ out += i === 0 ? v[i] : ', ' + v[i];
178
+ }
179
+ return out;
180
+ }
181
+ return undefined;
182
+ }
183
+
184
+ /**
185
+ * The declared `content-length` as a non-negative integer, or `-1` when
186
+ * absent or not a plain decimal number (a malformed value never blocks
187
+ * the read-length check that follows).
188
+ * @param {Readonly<Record<string, string | readonly string[]>>} headers
189
+ * @returns {number}
190
+ */
191
+ export function contentLength(headers) {
192
+ const raw = headerValue(headers, 'content-length');
193
+ if (raw === undefined || raw.length === 0 || raw.length > 15) return -1;
194
+ let n = 0;
195
+ for (let i = 0; i < raw.length; i++) {
196
+ const c = raw.charCodeAt(i) - 48;
197
+ if (c < 0 || c > 9) return -1;
198
+ n = n * 10 + c;
199
+ }
200
+ return n;
201
+ }
202
+
203
+ /**
204
+ * Whether a request `content-type` names the operation's media:
205
+ * type/subtype compared case-insensitively, parameters ignored, and a
206
+ * `+json` structured-syntax suffix accepted for `application/json`.
207
+ * @param {string | undefined} contentType
208
+ * @param {string} media - the operation's declared media
209
+ * @returns {boolean}
210
+ */
211
+ export function mediaMatches(contentType, media) {
212
+ if (contentType === undefined) return false;
213
+ const bare = bareMedia(contentType);
214
+ const want = bareMedia(media);
215
+ if (bare === want) return true;
216
+ return want === JSON_MEDIA && bare.endsWith('+json') && bare.indexOf('/') !== -1;
217
+ }
218
+
219
+ /**
220
+ * `type/subtype` of a media type, lowercased, parameters and whitespace
221
+ * dropped.
222
+ * @param {string} value
223
+ * @returns {string}
224
+ */
225
+ function bareMedia(value) {
226
+ const semi = value.indexOf(';');
227
+ return (semi === -1 ? value : value.slice(0, semi)).trim().toLowerCase();
228
+ }
229
+
230
+ /**
231
+ * The byte length of a body — text measured as UTF-8. Cheap on the
232
+ * common path: a string shorter than the limit in code units cannot
233
+ * exceed it, and one longer than a third of the limit in code units is
234
+ * measured exactly only when it could.
235
+ * @param {string | Uint8Array} body
236
+ * @param {number} limit
237
+ * @returns {boolean} true when the body exceeds `limit` bytes
238
+ */
239
+ export function exceedsBytes(body, limit) {
240
+ if (typeof body !== 'string') return body.byteLength > limit;
241
+ const units = body.length;
242
+ if (units * 3 <= limit) return false;
243
+ if (units > limit) return true;
244
+ return new TextEncoder().encode(body).byteLength > limit;
245
+ }
246
+
247
+ //#endregion
248
+
249
+ //#region entity tags
250
+
251
+ /**
252
+ * Parse an `If-Match`/`If-None-Match` field into its opaque tags. `*` is
253
+ * reported as `null` in the list; a weak indicator is dropped
254
+ * (`W/"x"` → `x`) — the caller decides weak/strong comparison because a
255
+ * strong comparison must reject weak tags, which `weak[i]` records.
256
+ * @param {string} value
257
+ * @returns {{ any: boolean, tags: string[], weak: boolean[] }}
258
+ */
259
+ export function parseEntityTags(value) {
260
+ /** @type {string[]} */
261
+ const tags = [];
262
+ /** @type {boolean[]} */
263
+ const weak = [];
264
+ let any = false;
265
+ const parts = value.split(',');
266
+ for (let i = 0; i < parts.length; i++) {
267
+ let part = parts[i].trim();
268
+ if (part === '*') {
269
+ any = true;
270
+ continue;
271
+ }
272
+ let isWeak = false;
273
+ if (part.startsWith('W/') || part.startsWith('w/')) {
274
+ isWeak = true;
275
+ part = part.slice(2);
276
+ }
277
+ if (part.length >= 2 && part.charCodeAt(0) === 0x22 && part.charCodeAt(part.length - 1) === 0x22) {
278
+ part = part.slice(1, -1);
279
+ }
280
+ else if (part.length === 0) continue;
281
+ tags.push(part);
282
+ weak.push(isWeak);
283
+ }
284
+ return { any, tags, weak };
285
+ }
286
+
287
+ /**
288
+ * Whether a conditional header matches the tag the handler armed.
289
+ * Weak comparison ignores the weak indicators (RFC 9110 §8.8.3.2);
290
+ * strong comparison requires both tags strong.
291
+ * @param {string} header - the raw `if-match`/`if-none-match` value
292
+ * @param {string} tag - the armed opaque tag
293
+ * @param {boolean} tagStrong - whether the armed tag is strong
294
+ * @param {boolean} strong - strong comparison
295
+ * @returns {boolean}
296
+ */
297
+ export function entityTagMatches(header, tag, tagStrong, strong) {
298
+ const parsed = parseEntityTags(header);
299
+ if (parsed.any) return true;
300
+ for (let i = 0; i < parsed.tags.length; i++) {
301
+ if (parsed.tags[i] !== tag) continue;
302
+ if (!strong || (tagStrong && !parsed.weak[i])) return true;
303
+ }
304
+ return false;
305
+ }
306
+
307
+ /**
308
+ * The `etag` header value of an armed tag.
309
+ * @param {string} tag
310
+ * @param {boolean} strong
311
+ * @returns {string}
312
+ */
313
+ export function formatEntityTag(tag, strong) {
314
+ return (strong ? '"' : 'W/"') + tag + '"';
315
+ }
316
+
317
+ //#endregion
318
+
319
+ //#region query
320
+
321
+ /**
322
+ * Decode a query string into the declared members of an input object:
323
+ * only declared names are set (an undeclared key is never merged, so no
324
+ * request can smuggle a member); a `repeated` member collects every
325
+ * occurrence into an array, every other member is last-wins; a `+` is a
326
+ * space and escapes decode as `application/x-www-form-urlencoded`
327
+ * (`URLSearchParams`). Returns `false` when the query is not decodable
328
+ * (a malformed percent-escape or invalid UTF-8) — the `JC2012` case.
329
+ * @param {string} query - the part after `?`, possibly empty
330
+ * @param {ReadonlySet<string>} declared - the query member names
331
+ * @param {ReadonlySet<string>} repeated - the array-typed ones
332
+ * @param {Record<string, unknown>} out - the input object under assembly
333
+ * @returns {boolean} false when not decodable
334
+ */
335
+ export function decodeQuery(query, declared, repeated, out) {
336
+ if (query.length === 0) return true;
337
+ // URLSearchParams never throws: it keeps a malformed escape as its
338
+ // literal text and replaces invalid UTF-8; both are "not decodable"
339
+ // for a contract, so they are detected first (only when an escape is
340
+ // present) with the strict decoder
341
+ if (query.indexOf('%') !== -1) {
342
+ try {
343
+ decodeURIComponent(query);
344
+ }
345
+ catch {
346
+ return false;
347
+ }
348
+ }
349
+ const params = new URLSearchParams(query);
350
+ for (const [name, value] of params) {
351
+ if (!declared.has(name)) continue;
352
+ if (repeated.has(name)) {
353
+ const list = out[name];
354
+ if (Array.isArray(list)) list.push(value);
355
+ else setObjectMember(out, name, [value]);
356
+ }
357
+ else setObjectMember(out, name, value);
358
+ }
359
+ return true;
360
+ }
361
+
362
+ //#endregion
363
+
364
+ //#region validation
365
+
366
+ /**
367
+ * The verdict of a compiled validator under either contract: the
368
+ * default `{ valid, errors }` or a host-injected boolean validator.
369
+ * TOTAL: a validator that throws is a failed verdict carrying the throw.
370
+ * Shared by the server pipeline and the client.
371
+ * @param {(value: unknown) => any} validate
372
+ * @param {unknown} value
373
+ * @returns {{ valid: boolean, errors: any[], thrown: unknown }}
374
+ */
375
+ export function verdict(validate, value) {
376
+ try {
377
+ const r = validate(value);
378
+ if (typeof r === 'boolean') return { valid: r, errors: [], thrown: undefined };
379
+ if (r !== null && typeof r === 'object' && typeof r.valid === 'boolean') {
380
+ return { valid: r.valid, errors: Array.isArray(r.errors) ? r.errors : [], thrown: undefined };
381
+ }
382
+ return { valid: false, errors: [], thrown: undefined };
383
+ }
384
+ catch (err) {
385
+ return { valid: false, errors: [], thrown: err };
386
+ }
387
+ }
388
+
389
+ //#endregion
390
+
391
+ //#region the error response
392
+
393
+ /**
394
+ * The details member of a validation failure by policy: `none` → absent,
395
+ * `paths` → `[{ path, keyword }]`, `full` → the validator's own error
396
+ * records (`toJSON()` when it has one) with their `params`.
397
+ * @param {'none' | 'paths' | 'full'} policy
398
+ * @param {any[]} errors - the validator's error list
399
+ * @returns {unknown}
400
+ */
401
+ export function projectValidationDetails(policy, errors) {
402
+ if (policy === 'none' || !Array.isArray(errors)) return undefined;
403
+ const out = new Array(errors.length);
404
+ for (let i = 0; i < errors.length; i++) {
405
+ const e = errors[i];
406
+ if (policy === 'paths') {
407
+ out[i] = {
408
+ path: e !== null && typeof e === 'object' && typeof e.instancePath === 'string' ? e.instancePath : '',
409
+ keyword: e !== null && typeof e === 'object' && typeof e.keyword === 'string' ? e.keyword : 'unknown',
410
+ };
411
+ }
412
+ else {
413
+ out[i] = e !== null && typeof e === 'object' && typeof e.toJSON === 'function' ? e.toJSON() : e;
414
+ }
415
+ }
416
+ return out;
417
+ }
418
+
419
+ /**
420
+ * Build the D7 error body and the response around it. `override` is the
421
+ * host's `errorBody` option: called with the wire record (the body plus
422
+ * `status`) and the request context; TOTAL — a projector that throws or
423
+ * returns a non-JSON value falls back to the D7 shape.
424
+ * @param {number} status
425
+ * @param {string} code
426
+ * @param {string} message
427
+ * @param {string} trace
428
+ * @param {unknown} details - `undefined` for none
429
+ * @param {boolean} retryable
430
+ * @param {Readonly<Record<string, string>> | null} extraHeaders - `allow`, `retry-after`, `etag`
431
+ * @param {((wire: WireErrorBody & { status: number }, ctx: any) => unknown) | null} override
432
+ * @param {any} ctx - the request context, or null before an operation matched
433
+ * @returns {HttpResponse}
434
+ */
435
+ export function errorResponse(status, code, message, trace, details, retryable, extraHeaders, override, ctx) {
436
+ /** @type {WireErrorBody & { status: number }} */
437
+ const wire = details === undefined
438
+ ? { code, message, requestId: trace, retryable, status }
439
+ : { code, message, requestId: trace, details, retryable, status };
440
+ let body;
441
+ if (override !== null) {
442
+ try {
443
+ const projected = override(wire, ctx);
444
+ body = projected !== undefined && isJsonValue(projected) ? projected : null;
445
+ }
446
+ catch {
447
+ body = null;
448
+ }
449
+ }
450
+ else body = null;
451
+ if (body === null) {
452
+ body = details === undefined
453
+ ? { code, message, requestId: trace, retryable }
454
+ : { code, message, requestId: trace, details, retryable };
455
+ }
456
+ /** @type {Record<string, string>} */
457
+ const headers = {
458
+ 'content-type': JSON_CONTENT_TYPE,
459
+ 'x-jaren-trace': trace,
460
+ 'cache-control': 'no-store',
461
+ };
462
+ if (extraHeaders !== null) {
463
+ const names = Object.keys(extraHeaders);
464
+ for (let i = 0; i < names.length; i++) headers[names[i]] = extraHeaders[names[i]];
465
+ }
466
+ return { status, headers, body: JSON.stringify(body) };
467
+ }
468
+
469
+ //#endregion
package/src/index.js ADDED
@@ -0,0 +1,33 @@
1
+ //@ts-check
2
+ /**
3
+ * @file Public surface of @jarenjs/contract: `compileContract` turns a
4
+ * `$contract` document into a frozen `Contract` (docs/CONTRACT-FORMAT.md);
5
+ * the error classes, the `JC` code table and the `ContractFailure`
6
+ * factory are what a host catches, reads and returns. The bindings live
7
+ * behind their own subpaths (`./http`, `./fetch`, `./node`, `./ledger`)
8
+ * so a consumer that only compiles never loads them. The path matcher is
9
+ * deliberately NOT exported — it is reached only through `contract.match`
10
+ * and `contract.allowed`.
11
+ */
12
+
13
+ export { compileContract } from './compile.js';
14
+ export {
15
+ ContractCompileError, ContractRuntimeError, ContractHostError,
16
+ ContractFailure, isContractFailure, CONTRACT_CODES,
17
+ } from './errors.js';
18
+ export { contractMessagesEn, contractCatalogEn } from './messages.js';
19
+
20
+ /**
21
+ * @typedef {import('./compile.js').Contract} Contract
22
+ * @typedef {import('./compile.js').CompiledOperation} CompiledOperation
23
+ * @typedef {import('./compile.js').CompiledHttp} CompiledHttp
24
+ * @typedef {import('./compile.js').CompiledPolicy} CompiledPolicy
25
+ * @typedef {import('./compile.js').CompiledInput} CompiledInput
26
+ * @typedef {import('./compile.js').CompiledOutput} CompiledOutput
27
+ * @typedef {import('./compile.js').CompiledErrorDecl} CompiledErrorDecl
28
+ * @typedef {import('./compile.js').InputTransport} InputTransport
29
+ * @typedef {import('./compile.js').CompileContractOptions} CompileContractOptions
30
+ * @typedef {import('./describe.js').ContractDescription} ContractDescription
31
+ * @typedef {import('./describe.js').OperationDescription} OperationDescription
32
+ * @typedef {import('./errors.js').ContractFailureValue} ContractFailureValue
33
+ */