@aventara/client 0.0.0-stage → 0.1.0-pilot.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/LICENSE +91 -0
  2. package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
  3. package/README.md +234 -2
  4. package/dist/avclient.bin.d.ts +2 -0
  5. package/dist/avclient.bin.js +15 -0
  6. package/dist/cli/command.parser.d.ts +37 -0
  7. package/dist/cli/command.parser.js +177 -0
  8. package/dist/cli/generate.command.d.ts +24 -0
  9. package/dist/cli/generate.command.js +41 -0
  10. package/dist/cli/generation-failure.renderer.d.ts +6 -0
  11. package/dist/cli/generation-failure.renderer.js +52 -0
  12. package/dist/cli/generation-success.renderer.d.ts +32 -0
  13. package/dist/cli/generation-success.renderer.js +47 -0
  14. package/dist/cli/terminal.prompter.d.ts +13 -0
  15. package/dist/cli/terminal.prompter.js +53 -0
  16. package/dist/cli/warning.renderer.d.ts +10 -0
  17. package/dist/cli/warning.renderer.js +14 -0
  18. package/dist/cli.d.ts +29 -0
  19. package/dist/cli.js +75 -0
  20. package/dist/config/client-config.interface.d.ts +62 -0
  21. package/dist/config/client-config.interface.js +14 -0
  22. package/dist/config/config.loader.d.ts +41 -0
  23. package/dist/config/config.loader.js +95 -0
  24. package/dist/config/config.resolver.d.ts +50 -0
  25. package/dist/config/config.resolver.js +126 -0
  26. package/dist/config/env.cascade.d.ts +84 -0
  27. package/dist/config/env.cascade.js +126 -0
  28. package/dist/contract/contract.acceptance.d.ts +77 -0
  29. package/dist/contract/contract.acceptance.js +124 -0
  30. package/dist/contract/contract.fetcher.d.ts +64 -0
  31. package/dist/contract/contract.fetcher.js +85 -0
  32. package/dist/contract/contract.loader.d.ts +32 -0
  33. package/dist/contract/contract.loader.js +32 -0
  34. package/dist/emit/banner.emitter.d.ts +31 -0
  35. package/dist/emit/banner.emitter.js +42 -0
  36. package/dist/emit/client-surface.emitter.d.ts +32 -0
  37. package/dist/emit/client-surface.emitter.js +236 -0
  38. package/dist/emit/client-tree.emitter.d.ts +37 -0
  39. package/dist/emit/client-tree.emitter.js +103 -0
  40. package/dist/emit/contract-carrier.emitter.d.ts +13 -0
  41. package/dist/emit/contract-carrier.emitter.js +60 -0
  42. package/dist/emit/derivation.emitter.d.ts +45 -0
  43. package/dist/emit/derivation.emitter.js +233 -0
  44. package/dist/emit/descriptor.emitter.d.ts +4 -0
  45. package/dist/emit/descriptor.emitter.js +97 -0
  46. package/dist/emit/emitted-tree.interface.d.ts +61 -0
  47. package/dist/emit/emitted-tree.interface.js +18 -0
  48. package/dist/emit/enum.emitter.d.ts +24 -0
  49. package/dist/emit/enum.emitter.js +42 -0
  50. package/dist/emit/name.deriver.d.ts +153 -0
  51. package/dist/emit/name.deriver.js +411 -0
  52. package/dist/emit/named-type.emitter.d.ts +32 -0
  53. package/dist/emit/named-type.emitter.js +50 -0
  54. package/dist/emit/runtime.emitter.d.ts +87 -0
  55. package/dist/emit/runtime.emitter.js +707 -0
  56. package/dist/emit/scalar.codec.d.ts +63 -0
  57. package/dist/emit/scalar.codec.js +498 -0
  58. package/dist/emit/transaction.emitter.d.ts +17 -0
  59. package/dist/emit/transaction.emitter.js +438 -0
  60. package/dist/generate.d.ts +123 -0
  61. package/dist/generate.js +98 -0
  62. package/dist/index.d.ts +8 -0
  63. package/dist/index.js +8 -0
  64. package/dist/init/client-config.template.d.ts +11 -0
  65. package/dist/init/client-config.template.js +27 -0
  66. package/dist/init/client-init.errors.d.ts +9 -0
  67. package/dist/init/client-init.errors.js +9 -0
  68. package/dist/init/client-init.orchestrator.d.ts +3 -0
  69. package/dist/init/client-init.orchestrator.js +86 -0
  70. package/dist/init/client-init.planner.d.ts +27 -0
  71. package/dist/init/client-init.planner.js +99 -0
  72. package/dist/init/client-init.questions.d.ts +52 -0
  73. package/dist/init/client-init.questions.js +124 -0
  74. package/dist/init/client-project.inspector.d.ts +15 -0
  75. package/dist/init/client-project.inspector.js +32 -0
  76. package/dist/init/command.runner.d.ts +8 -0
  77. package/dist/init/command.runner.js +17 -0
  78. package/dist/node-version.guard.d.ts +8 -0
  79. package/dist/node-version.guard.js +59 -0
  80. package/dist/output/output.validator.d.ts +75 -0
  81. package/dist/output/output.validator.js +262 -0
  82. package/dist/output/output.writer.d.ts +162 -0
  83. package/dist/output/output.writer.js +499 -0
  84. package/package.json +47 -3
@@ -0,0 +1,707 @@
1
+ import { OPERATION_CODES, VALIDATION_CODES, } from "@aventara/core";
2
+ import { AvProtocol } from "@aventara/core/protocol";
3
+ import { entrypointHref } from "../config/config.resolver.js";
4
+ import { wireGrammarLiteral } from "./scalar.codec.js";
5
+ /**
6
+ * The runtime modules the generated client carries: `metadata.ts`, the
7
+ * framework `Decimal`, the outcome codes and error classes, and the transport
8
+ * here; the scalar codec in `scalar.codec.ts`.
9
+ */
10
+ /**
11
+ * `runtime/decimal.ts` — the framework-owned `Decimal` of §6.2, the generated
12
+ * runtime value of the `decimal` scalar. Bundled with the client and imported
13
+ * from nothing: its public type is its own, never an ORM's (§6.2, C-801).
14
+ *
15
+ * It holds the decimal string it was made from, digit for digit, and does no
16
+ * arithmetic — the surface is core's `Decimal` (a string constructor and
17
+ * `toString`) plus `toJSON`, so a value means the same on both sides of the wire.
18
+ * A proven decimal library may later sit behind it (§6.2); none is needed to
19
+ * carry digits.
20
+ *
21
+ * `toJSON` is the architect's (2026-10-04, S6 answers): it returns the decimal
22
+ * string, so a Decimal in a consumer's own JSON — a log line, a cache entry — is
23
+ * its digits rather than `{}`. No `equals`, no arithmetic. The request body never
24
+ * relies on it: `serializeWireBody` refuses an unencoded Decimal regardless, so a
25
+ * value the codec did not encode cannot slip onto the wire through `toJSON`.
26
+ *
27
+ * The grammar is core's `Decimal` grammar, read from `AvProtocol.scalarFormats`
28
+ * and emitted by value (C2, R7; `protocol-parity.spec.ts`). Unlike
29
+ * core's, the constructor refuses a non-string outright: a pattern test coerces
30
+ * its argument, so `5` would otherwise pass as `"5"` and be kept as a number.
31
+ */
32
+ export function emitDecimalModule() {
33
+ return {
34
+ path: "runtime/decimal.ts",
35
+ source: `const DECIMAL_STRING = ${wireGrammarLiteral("decimal")};
36
+
37
+ /**
38
+ * An exact decimal: the runtime value of the "decimal" scalar (§6.2).
39
+ *
40
+ * It holds the decimal string it was made from, digit for digit, and is sent as
41
+ * that string. It does no arithmetic.
42
+ */
43
+ export class Decimal {
44
+ readonly #value: string;
45
+
46
+ /** @throws TypeError when the value is not a decimal string. */
47
+ constructor(value: string) {
48
+ if (typeof value !== "string" || !DECIMAL_STRING.test(value)) {
49
+ throw new TypeError("Decimal value must be a valid decimal string.");
50
+ }
51
+ this.#value = value;
52
+ }
53
+
54
+ /** The decimal string, exactly as given. */
55
+ toString(): string {
56
+ return this.#value;
57
+ }
58
+
59
+ /** The decimal string, exactly as given: what JSON.stringify writes for a Decimal. */
60
+ toJSON(): string {
61
+ return this.#value;
62
+ }
63
+ }
64
+ `,
65
+ };
66
+ }
67
+ /**
68
+ * `metadata.ts` — what this client was generated against: the ClientContract
69
+ * hash and the protocol version (Phase 12-partial Q4 = A), and the resolved
70
+ * entrypoint as the client's default (§15.2; Phase 12-rest Q5 = A). Nothing else:
71
+ * no driver, no timestamp, no generator version, no host path — anything a re-run
72
+ * over the same inputs could change breaks byte-equality (§19.3's spirit). The
73
+ * entrypoint is an input like the contract: changing it regenerates different
74
+ * bytes and leaves the hash alone (§15.2), and it never carries credentials
75
+ * (refused at resolution, Q5).
76
+ *
77
+ * The hash and the version are what every operation request sends (§12.4), from
78
+ * `runtime/transport.ts`; all three are internal — `AvClient.ts` exports none.
79
+ */
80
+ export function emitMetadataModule(protocol, entrypoint) {
81
+ return {
82
+ path: "metadata.ts",
83
+ source: "/** The hash of the ClientContract this client was generated against (§19.2). */\n" +
84
+ `export const CLIENT_CONTRACT_HASH = ${JSON.stringify(protocol.hash)};\n` +
85
+ "\n" +
86
+ "/** The Aventara protocol version that ClientContract speaks (§19.1). */\n" +
87
+ `export const PROTOCOL_VERSION = ${JSON.stringify(protocol.version)};\n` +
88
+ "\n" +
89
+ "/** The entrypoint the client was generated from: its default deployment (§15.2). */\n" +
90
+ `export const DEFAULT_ENTRYPOINT = ${JSON.stringify(entrypointHref(entrypoint))};\n`,
91
+ };
92
+ }
93
+ /**
94
+ * # The FrameworkError subclass set (Q2 = B)
95
+ *
96
+ * One subclass per code CLASS, plus the two the specification names by
97
+ * behaviour: `NotFoundError` (§13.4, A2003) and the contract-mismatch error
98
+ * (invariant 15, A2005). Each class's doc line is emitted above it.
99
+ *
100
+ * The keys are the emitted class names, in UTF-16 code-unit order.
101
+ */
102
+ const FRAMEWORK_ERROR_CLASSES = {
103
+ AuthError: "The caller is not authenticated, or is not permitted to run the operation.",
104
+ ConflictError: "The operation conflicted with stored state, and nothing was written. A2008 fails the same way if sent again; A2013 and A2014 may succeed if sent again, and no retry is automatic (§13.5).",
105
+ ContractMismatchError: "This client was generated against a ClientContract other than the one the server serves (invariant 15). Regenerate it with `avclient generate`.",
106
+ InternalError: "The server failed. Nothing in its cause is actionable; the diagnostics are in the server's logs (§13.5).",
107
+ NotFoundError: "A strict unique target does not exist (§13.4).",
108
+ ProtocolError: "The request was not one the server could route or interpret: an unknown Resource or operation, a protocol version it does not speak, or a body, media type, size or method it refuses.",
109
+ ValidationError: "The request broke a Contract rule: a field, type, capability, projection, reference or limit. The cause's issues say which, by code and path.",
110
+ };
111
+ /**
112
+ * The class every error code throws. A mapped type over core's
113
+ * `OperationErrorCode`, so a code core adds is a compile error here until it is
114
+ * classified — the classification is total by construction, never by care.
115
+ */
116
+ const FRAMEWORK_ERROR_CLASS_OF = {
117
+ A2000: "ProtocolError",
118
+ A2001: "ProtocolError",
119
+ A2002: "ProtocolError",
120
+ A2003: "NotFoundError",
121
+ A2004: "ValidationError",
122
+ A2005: "ContractMismatchError",
123
+ A2006: "ProtocolError",
124
+ A2007: "ValidationError",
125
+ A2008: "ConflictError",
126
+ A2009: "ValidationError",
127
+ A2010: "ProtocolError",
128
+ A2011: "ProtocolError",
129
+ A2012: "ProtocolError",
130
+ A2013: "ConflictError",
131
+ A2014: "ConflictError",
132
+ A3000: "InternalError",
133
+ A3001: "InternalError",
134
+ A3002: "InternalError",
135
+ A3003: "InternalError",
136
+ A4000: "AuthError",
137
+ A4001: "AuthError",
138
+ A4002: "AuthError",
139
+ };
140
+ /**
141
+ * The `FrameworkError` subclasses the generated runtime declares, in UTF-16
142
+ * code-unit order — names the root namespace reserves (`name.deriver.ts`).
143
+ */
144
+ export const FRAMEWORK_ERROR_CLASS_NAMES = Object.keys(FRAMEWORK_ERROR_CLASSES);
145
+ /** The error codes, in core's allocation order. */
146
+ const ERROR_CODES = OPERATION_CODES.filter((code) => !code.startsWith("A1"));
147
+ /**
148
+ * The names `AvClient.ts` re-exports from `generated/runtime/errors.ts`: the classes and the
149
+ * two code unions §15.4 requires, and `Cause` and `ValidationIssue` — the types a
150
+ * thrown FrameworkError carries, public by the architect's decision of
151
+ * 2026-10-04 — in UTF-16 code-unit order.
152
+ */
153
+ export function errorsModuleExports() {
154
+ return [
155
+ ...FRAMEWORK_ERROR_CLASS_NAMES,
156
+ "FrameworkError",
157
+ "TransportError",
158
+ "type Cause",
159
+ "type OperationCode",
160
+ "type ValidationCode",
161
+ "type ValidationIssue",
162
+ ].sort((left, right) => {
163
+ const a = left.replace(/^type /, "");
164
+ const b = right.replace(/^type /, "");
165
+ return a < b ? -1 : a > b ? 1 : 0;
166
+ });
167
+ }
168
+ /** One code array as emitted source: a `const` tuple, one code per line. */
169
+ function codeArray(codes) {
170
+ return `[\n${codes.map((code) => `\t${JSON.stringify(code)},\n`).join("")}] as const`;
171
+ }
172
+ const ERRORS_MODULE_HEAD = `/**
173
+ * The outcome codes and error classes of Aventara protocol version 1 (§13).
174
+ *
175
+ * A1xxx resolves; every other code throws the FrameworkError subclass of its
176
+ * class (§13.5); a response that is not a framework envelope throws
177
+ * TransportError. Branch on the class or on \`code\`, never on the message.
178
+ */
179
+ `;
180
+ const ERRORS_MODULE_TYPES = `
181
+ /** One actionable validation issue (§13.1); \`path\` locates it in the request. */
182
+ export interface ValidationIssue {
183
+ readonly path?: readonly (string | number)[];
184
+ readonly code: ValidationCode;
185
+ readonly message: string;
186
+ }
187
+
188
+ /**
189
+ * Why an operation failed (§13.1). Rebuilt member by member from the envelope:
190
+ * a member the protocol does not define is never carried, so no stack, SQL or
191
+ * server path the server should not have sent reaches it (§13.5).
192
+ */
193
+ export interface Cause {
194
+ readonly message: string;
195
+ readonly issues?: readonly ValidationIssue[];
196
+ /** The index of the failing operation in a transaction plan. */
197
+ readonly operation?: number;
198
+ }
199
+
200
+ /**
201
+ * A framework outcome that throws (§13.5): every code outside A1xxx. It is
202
+ * always thrown as the subclass of its code's class; catch FrameworkError to
203
+ * catch them all.
204
+ */
205
+ export class FrameworkError<C extends OperationErrorCode = OperationErrorCode> extends Error {
206
+ override readonly name: string = "FrameworkError";
207
+ /** The outcome code. Branch on this, never on the message. */
208
+ readonly code: C;
209
+ /** The server's cause: its message, and its issues or failing operation when it gave them. */
210
+ override readonly cause: Cause;
211
+
212
+ constructor(code: C, cause: Cause) {
213
+ super(cause.message);
214
+ this.code = code;
215
+ this.cause = cause;
216
+ }
217
+ }
218
+ `;
219
+ const ERRORS_MODULE_TAIL = `
220
+ /**
221
+ * A failure that never produced a framework envelope (§13.5): no response
222
+ * arrived, or the one that did is not an Aventara response envelope — a proxy's
223
+ * HTML error page, a truncated body, JSON of another shape. It carries no
224
+ * cause: nothing in such a response is the framework's to report.
225
+ */
226
+ export class TransportError extends Error {
227
+ override readonly name: string = "TransportError";
228
+ /** The HTTP status that arrived, or null when no response arrived at all. */
229
+ readonly status: number | null;
230
+
231
+ constructor(message: string, status: number | null) {
232
+ super(message);
233
+ this.status = status;
234
+ }
235
+ }
236
+ `;
237
+ /**
238
+ * `runtime/errors.ts` — §13's vocabulary as the generated client carries it.
239
+ *
240
+ * Both code unions are emitted from core's exported arrays (Q3), so the emitted
241
+ * tree states each code exactly once and core stays their one source; the
242
+ * subclass set and the code each one throws are Q2's (above), accepted as-is by
243
+ * the architect on 2026-10-04. Exported beyond what `AvClient.ts` re-exports: the
244
+ * two arrays, `OperationErrorCode` and `frameworkErrorOf`, which
245
+ * `runtime/transport.ts` reads.
246
+ */
247
+ export function emitErrorsModule() {
248
+ const classes = Object.entries(FRAMEWORK_ERROR_CLASSES).map(([name, summary]) => {
249
+ const codes = ERROR_CODES.filter((code) => FRAMEWORK_ERROR_CLASS_OF[code] === name);
250
+ return (`\n/** ${summary} Thrown for ${codes.join(", ")}. */\n` +
251
+ `export class ${name} extends FrameworkError<${codes.map((code) => JSON.stringify(code)).join(" | ")}> {\n` +
252
+ `\toverride readonly name: string = ${JSON.stringify(name)};\n}\n`);
253
+ });
254
+ const table = ERROR_CODES.map((code) => `\t${code}: ${FRAMEWORK_ERROR_CLASS_OF[code]},\n`).join("");
255
+ return {
256
+ path: "runtime/errors.ts",
257
+ source: ERRORS_MODULE_HEAD +
258
+ "\n/** The outcome codes of the protocol, in allocation order (§13.2). */\n" +
259
+ `export const OPERATION_CODES = ${codeArray(OPERATION_CODES)};\n` +
260
+ "\n/** An outcome code (§13.2). */\n" +
261
+ "export type OperationCode = (typeof OPERATION_CODES)[number];\n" +
262
+ "\n/** An outcome code that throws: every code outside A1xxx (§13.5). */\n" +
263
+ `export type OperationErrorCode = Exclude<OperationCode, \`A1\${string}\`>;\n` +
264
+ "\n/** The validation codes of the protocol, in allocation order (§13.3). */\n" +
265
+ `export const VALIDATION_CODES = ${codeArray(VALIDATION_CODES)};\n` +
266
+ "\n/** A validation code (§13.3): what one issue in a cause is about. */\n" +
267
+ "export type ValidationCode = (typeof VALIDATION_CODES)[number];\n" +
268
+ ERRORS_MODULE_TYPES +
269
+ classes.join("") +
270
+ ERRORS_MODULE_TAIL +
271
+ "\nconst ERROR_CLASS_OF: {\n" +
272
+ "\treadonly [C in OperationErrorCode]: new (code: C, cause: Cause) => FrameworkError;\n" +
273
+ `} = {\n${table}};\n` +
274
+ "\n/** The FrameworkError subclass a failure envelope's code throws. */\n" +
275
+ "export function frameworkErrorOf<C extends OperationErrorCode>(code: C, cause: Cause): FrameworkError {\n" +
276
+ "\tconst ErrorClass = ERROR_CLASS_OF[code];\n" +
277
+ "\treturn new ErrorClass(code, cause);\n" +
278
+ "}\n",
279
+ };
280
+ }
281
+ /**
282
+ * `runtime/transport.ts` — the happy path of §12 and §13.5, above Phase
283
+ * 12-partial's boundary: one POST per operation to
284
+ * `<entrypoint>/_resources/<resource>/<family>/<variant>` (§12.1–§12.3), the
285
+ * identity headers on every request (§12.4), per-request options that are never
286
+ * serialized (§15.7), and the envelope read back by §13.5's rule.
287
+ *
288
+ * `execute` is Q1 = B's primitive: exported from this module for the
289
+ * per-variant methods to wrap, and re-exported from nowhere — it is not public
290
+ * surface. It reads no capability and no operation descriptor; the caller names
291
+ * the operation.
292
+ *
293
+ * Fetch and AbortSignal are the platform's, reached through the structural types
294
+ * below so the module compiles with neither DOM nor Node types (§15.7, U5).
295
+ *
296
+ * Stale-route recovery is not here: it is F-716, Phase 10's, and the emitted
297
+ * module documents it as a hole in its own doc comment (S9).
298
+ */
299
+ export function emitTransportModule() {
300
+ return { path: "runtime/transport.ts", source: TRANSPORT_MODULE };
301
+ }
302
+ /**
303
+ * Core's identity header names (`AvProtocol.headers`, §12.4), split back into
304
+ * the one wire prefix and each header's own part, so the emitted transport
305
+ * spells both from one constant — its freeze stays a one-line change in the
306
+ * generated code — and from core's values rather than restated text.
307
+ *
308
+ * @throws Error at load when core's names stop sharing one `<prefix>-` head:
309
+ * the emitted spelling could no longer be the names core sends.
310
+ */
311
+ const IDENTITY_HEADERS = (() => {
312
+ const { protocolVersion, contractHash, requestId } = AvProtocol.headers;
313
+ const prefix = protocolVersion.slice(0, protocolVersion.indexOf("-"));
314
+ for (const name of [protocolVersion, contractHash, requestId]) {
315
+ if (prefix === "" || !name.startsWith(`${prefix}-`)) {
316
+ throw new Error(`AvProtocol.headers no longer share one wire prefix: ${name}`);
317
+ }
318
+ }
319
+ return {
320
+ prefix,
321
+ protocolVersion: protocolVersion.slice(prefix.length),
322
+ contractHash: contractHash.slice(prefix.length),
323
+ requestId: requestId.slice(prefix.length),
324
+ };
325
+ })();
326
+ const TRANSPORT_MODULE = `import { CLIENT_CONTRACT_HASH, PROTOCOL_VERSION } from "../metadata.js";
327
+ import { decodeResult, encodeArguments, serializeWireBody } from "./codec.js";
328
+ import { DECODE_TABLE } from "./descriptor.js";
329
+ import {
330
+ type Cause,
331
+ frameworkErrorOf,
332
+ OPERATION_CODES,
333
+ type OperationCode,
334
+ type OperationErrorCode,
335
+ TransportError,
336
+ VALIDATION_CODES,
337
+ type ValidationCode,
338
+ type ValidationIssue,
339
+ } from "./errors.js";
340
+
341
+ /**
342
+ * The transport of Aventara protocol version 1: one POST per operation (§12.2),
343
+ * sent with the protocol version and ClientContract hash (§12.4), its response
344
+ * read by §13.5's rule — A1xxx resolves with the envelope's data, every other
345
+ * code throws its FrameworkError subclass, and anything that is not a framework
346
+ * envelope throws TransportError. No request is ever retried (§13.5).
347
+ *
348
+ * # A stale client (F-716)
349
+ *
350
+ * A client generated against an older ClientContract must fail hard and say to
351
+ * regenerate (cross-phase invariant 15). The server is what knows: it checks the
352
+ * identity this transport sends BEFORE it routes (Phase 10, Q3), so a request
353
+ * under a stale hash — to an operation the deployment still advertises, or to
354
+ * one it no longer does — is answered 409 A2005, which throws
355
+ * ContractMismatchError naming the remedy. Nothing here guesses: a response that
356
+ * is not a framework envelope — a host's own 404 for a path it does not mount —
357
+ * is a TransportError, and names no remedy, because nothing in it says this
358
+ * client is stale.
359
+ */
360
+
361
+ /** The wire prefix of the identity headers, in one place: its freeze (§12.4) is a one-line change. */
362
+ const WIRE_PREFIX = ${JSON.stringify(IDENTITY_HEADERS.prefix)};
363
+
364
+ /** The headers the framework owns on every operation request (§12.3, §12.4). */
365
+ const FRAMEWORK_HEADERS: Readonly<Record<string, string>> = {
366
+ "Content-Type": "application/json",
367
+ [WIRE_PREFIX + ${JSON.stringify(IDENTITY_HEADERS.protocolVersion)}]: String(PROTOCOL_VERSION),
368
+ [WIRE_PREFIX + ${JSON.stringify(IDENTITY_HEADERS.contractHash)}]: CLIENT_CONTRACT_HASH,
369
+ };
370
+
371
+ /** The request id header (§12.4, Phase 10 Q13): CallOptions.requestId sends it. */
372
+ const REQUEST_ID_HEADER = WIRE_PREFIX + ${JSON.stringify(IDENTITY_HEADERS.requestId)};
373
+
374
+ /** What the transport reads of an AbortSignal, where the consumer's lib declares none. */
375
+ export interface StructuralSignal {
376
+ readonly aborted: boolean;
377
+ }
378
+
379
+ /** The request the transport hands to fetch. */
380
+ export interface FetchInit {
381
+ readonly method: "POST";
382
+ readonly headers: Readonly<Record<string, string>>;
383
+ readonly body: string;
384
+ readonly signal?: PlatformSignal;
385
+ }
386
+
387
+ /** What the transport reads of a fetch Response. */
388
+ export interface FetchResponse {
389
+ readonly status: number;
390
+ text(): Promise<string>;
391
+ }
392
+
393
+ /** The Fetch API as far as the transport calls it, where the consumer's lib declares none. */
394
+ export type StructuralFetch = (url: string, init: FetchInit) => Promise<FetchResponse>;
395
+
396
+ /**
397
+ * The fetch this client calls (§15.7): the consumer's own platform fetch type when
398
+ * their lib declares one — DOM's, Node's — so the platform fetch is accepted
399
+ * without a cast, and the structural fetch above where it declares none. Read
400
+ * through typeof globalThis, so this module names neither lib.
401
+ */
402
+ export type Fetch = typeof globalThis extends { readonly fetch: infer F } ? F : StructuralFetch;
403
+
404
+ /** The consumer's AbortSignal type when their lib declares one, else the structural signal. */
405
+ export type PlatformSignal = typeof globalThis extends {
406
+ readonly AbortSignal: { readonly prototype: infer S };
407
+ }
408
+ ? S
409
+ : StructuralSignal;
410
+
411
+ /** Where operations go: the framework entrypoint, and the fetch that reaches it. */
412
+ export interface TransportConnection {
413
+ /** The framework entrypoint, an absolute URL without a trailing slash (§12.1). */
414
+ readonly entrypoint: string;
415
+ readonly fetch: Fetch;
416
+ }
417
+
418
+ /** Per-call options (§15.7). Never serialized into the operation body. */
419
+ export interface CallOptions {
420
+ /** Aborts the request; the call rejects with what fetch rejected with. */
421
+ readonly signal?: PlatformSignal;
422
+ /** Extra request headers. The framework's own headers cannot be set here. */
423
+ readonly headers?: Readonly<Record<string, string>>;
424
+ /**
425
+ * Sent as the request id header (Aventara-Request-Id); the empty string is
426
+ * absent. It wins over that header in headers, which it overwrites.
427
+ */
428
+ readonly requestId?: string;
429
+ }
430
+
431
+ type SuccessCode = Exclude<OperationCode, OperationErrorCode>;
432
+
433
+ /** A framework envelope (§13.1), read and checked. */
434
+ type Envelope =
435
+ | { readonly code: SuccessCode; readonly data: unknown; readonly cause: null }
436
+ | { readonly code: OperationErrorCode; readonly data: null; readonly cause: Cause };
437
+
438
+ /**
439
+ * Runs one operation: POSTs its arguments and settles its envelope.
440
+ *
441
+ * @returns the envelope's data when the code is A1xxx — null for A1001.
442
+ * @throws the FrameworkError subclass of any other code; TransportError when no
443
+ * framework envelope arrived; TypeError, before anything is sent, when the
444
+ * arguments are not a JSON-safe object or a request header is the framework's.
445
+ * A caller's abort rejects with what fetch rejected with.
446
+ */
447
+ export async function execute(
448
+ connection: TransportConnection,
449
+ resource: string,
450
+ family: string,
451
+ variant: string,
452
+ args: Readonly<Record<string, unknown>>,
453
+ options: CallOptions = {},
454
+ ): Promise<unknown> {
455
+ const operation = JSON.stringify(resource) + " " + family + "." + variant;
456
+ const headers = requestHeaders(options);
457
+ const body = serializeWireBody(encodeArguments(argumentObject(args)));
458
+ const path = "/_resources/" + [resource, family, variant].map(encodeURIComponent).join("/");
459
+ const response = await post(connection, path, operation, headers, body, options);
460
+ return decoded(operation, response.status, () => decodeResult(DECODE_TABLE, resource, response.data));
461
+ }
462
+
463
+ /**
464
+ * Runs one transaction plan (§14.2): POSTs it to \`/_transactions\` and settles
465
+ * its envelope; a committed plan's data is one result per operation, each decoded
466
+ * by its own operation's Resource (\`resources\`, in plan order).
467
+ *
468
+ * @throws as {@link execute} does; a rolled-back plan throws its failing
469
+ * operation's FrameworkError subclass, its cause naming the operation.
470
+ */
471
+ export async function executePlan(
472
+ connection: TransportConnection,
473
+ plan: { readonly operations: readonly unknown[] },
474
+ resources: readonly string[],
475
+ options: CallOptions = {},
476
+ ): Promise<unknown[]> {
477
+ const operation = "the transaction";
478
+ const headers = requestHeaders(options);
479
+ const body = serializeWireBody(plan);
480
+ const response = await post(connection, "/_transactions", operation, headers, body, options);
481
+ return decoded(operation, response.status, () => {
482
+ if (!Array.isArray(response.data) || response.data.length !== resources.length) {
483
+ throw new TypeError("A committed plan's data must hold one result per operation.");
484
+ }
485
+ return response.data.map((slot, index) => decodeResult(DECODE_TABLE, resources[index] ?? "", slot));
486
+ });
487
+ }
488
+
489
+ /** One POST and its settled envelope data, still in wire form. */
490
+ async function post(
491
+ connection: TransportConnection,
492
+ path: string,
493
+ operation: string,
494
+ headers: Readonly<Record<string, string>>,
495
+ body: string,
496
+ options: CallOptions,
497
+ ): Promise<{ readonly status: number; readonly data: unknown }> {
498
+ const url = connection.entrypoint + path;
499
+ const init: FetchInit =
500
+ options.signal === undefined
501
+ ? { method: "POST", headers, body }
502
+ : { method: "POST", headers, body, signal: options.signal };
503
+
504
+ // Called unbound: a browser's fetch refuses a this that is not the global.
505
+ const fetch = connection.fetch as StructuralFetch;
506
+ let response: FetchResponse;
507
+ try {
508
+ response = await fetch(url, init);
509
+ } catch (error) {
510
+ if (options.signal?.aborted === true) {
511
+ throw error;
512
+ }
513
+ throw new TransportError("The request for " + operation + " failed before any response arrived.", null);
514
+ }
515
+ let text: string;
516
+ try {
517
+ text = await response.text();
518
+ } catch (error) {
519
+ if (options.signal?.aborted === true) {
520
+ throw error;
521
+ }
522
+ throw new TransportError(
523
+ "The response to " + operation + " (HTTP " + response.status + ") could not be read.",
524
+ response.status,
525
+ );
526
+ }
527
+ return { status: response.status, data: settle(operation, response.status, text) };
528
+ }
529
+
530
+ /**
531
+ * The decoded data, or — a scalar in \`data\` that does not decode, inside an
532
+ * otherwise valid envelope — a TransportError (architect, 2026-10-04).
533
+ */
534
+ function decoded<T>(operation: string, status: number, decode: () => T): T {
535
+ try {
536
+ return decode();
537
+ } catch (error) {
538
+ throw new TransportError(
539
+ "The response to " + operation + " (HTTP " + status + ") holds a value this client cannot read: " +
540
+ (error instanceof Error ? error.message : String(error)),
541
+ status,
542
+ );
543
+ }
544
+ }
545
+
546
+ /**
547
+ * Settles one response by §13.5's rule: the envelope's data, still in wire form,
548
+ * which the caller decodes by the decode table (P1). A decode failure's mode is
549
+ * decided (architect, 2026-10-04):
550
+ * A scalar in \`data\` that fails to decode, inside an otherwise valid envelope, throws TransportError
551
+ * — not a FrameworkError, because the server reported no failure; the response is
552
+ * one this client cannot read, which is what TransportError means.
553
+ */
554
+ function settle(operation: string, status: number, text: string): unknown {
555
+ const envelope = envelopeOf(parseJson(text));
556
+ if (envelope === undefined) {
557
+ throw new TransportError(
558
+ "The response to " + operation + " (HTTP " + status + ") is not an Aventara response envelope.",
559
+ status,
560
+ );
561
+ }
562
+ if (envelope.cause === null) {
563
+ return envelope.data;
564
+ }
565
+ throw frameworkErrorOf(envelope.code, envelope.cause);
566
+ }
567
+
568
+ /** The parsed body, or undefined — which no envelope is — when it is not JSON. */
569
+ function parseJson(text: string): unknown {
570
+ try {
571
+ return JSON.parse(text);
572
+ } catch {
573
+ return undefined;
574
+ }
575
+ }
576
+
577
+ function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
578
+ return typeof value === "object" && value !== null && !Array.isArray(value);
579
+ }
580
+
581
+ function isOperationCode(value: unknown): value is OperationCode {
582
+ return OPERATION_CODES.some((code) => code === value);
583
+ }
584
+
585
+ function isValidationCode(value: unknown): value is ValidationCode {
586
+ return VALIDATION_CODES.some((code) => code === value);
587
+ }
588
+
589
+ function isSuccessCode(code: OperationCode): code is SuccessCode {
590
+ return code.startsWith("A1");
591
+ }
592
+
593
+ /** The envelope in a parsed body, or undefined when the body is not one (§13.1). */
594
+ function envelopeOf(body: unknown): Envelope | undefined {
595
+ if (!isRecord(body) || !Object.hasOwn(body, "data")) {
596
+ return undefined;
597
+ }
598
+ const code = body["code"];
599
+ const data = body["data"];
600
+ if (!isOperationCode(code)) {
601
+ return undefined;
602
+ }
603
+ if (isSuccessCode(code)) {
604
+ return body["cause"] === null && (code !== "A1001" || data === null) ? { code, data, cause: null } : undefined;
605
+ }
606
+ const cause = causeOf(body["cause"]);
607
+ return data === null && cause !== undefined ? { code, data, cause } : undefined;
608
+ }
609
+
610
+ /**
611
+ * The cause rebuilt from the members §13.1 defines, or undefined when one of them
612
+ * is malformed. Every other member is dropped, so whatever else a server put in
613
+ * its cause — a stack, SQL, a file path — is never carried into a thrown error.
614
+ */
615
+ function causeOf(value: unknown): Cause | undefined {
616
+ if (!isRecord(value)) {
617
+ return undefined;
618
+ }
619
+ const message = value["message"];
620
+ const issues = value["issues"];
621
+ const operation = value["operation"];
622
+ if (typeof message !== "string") {
623
+ return undefined;
624
+ }
625
+ let cause: Cause = { message };
626
+ if (issues !== undefined) {
627
+ if (!Array.isArray(issues)) {
628
+ return undefined;
629
+ }
630
+ const rebuilt: ValidationIssue[] = [];
631
+ for (const issue of issues) {
632
+ const copy = issueOf(issue);
633
+ if (copy === undefined) {
634
+ return undefined;
635
+ }
636
+ rebuilt.push(copy);
637
+ }
638
+ cause = { ...cause, issues: Object.freeze(rebuilt) };
639
+ }
640
+ if (operation !== undefined) {
641
+ if (typeof operation !== "number" || !Number.isInteger(operation) || operation < 0) {
642
+ return undefined;
643
+ }
644
+ cause = { ...cause, operation };
645
+ }
646
+ return Object.freeze(cause);
647
+ }
648
+
649
+ function issueOf(value: unknown): ValidationIssue | undefined {
650
+ if (!isRecord(value)) {
651
+ return undefined;
652
+ }
653
+ const path = value["path"];
654
+ const code = value["code"];
655
+ const message = value["message"];
656
+ if (!isValidationCode(code) || typeof message !== "string") {
657
+ return undefined;
658
+ }
659
+ if (path === undefined) {
660
+ return Object.freeze({ code, message });
661
+ }
662
+ if (!Array.isArray(path)) {
663
+ return undefined;
664
+ }
665
+ const segments: (string | number)[] = [];
666
+ for (const segment of path) {
667
+ if (typeof segment !== "string" && !Number.isInteger(segment)) {
668
+ return undefined;
669
+ }
670
+ segments.push(segment);
671
+ }
672
+ return Object.freeze({ path: Object.freeze(segments), code, message });
673
+ }
674
+
675
+ /** The arguments, refused unless they are an object: the body is always one (§12.6). */
676
+ function argumentObject(args: unknown): unknown {
677
+ if (!isRecord(args)) {
678
+ throw new TypeError("An operation's arguments must be an object; send {} when there are none (§12.3).");
679
+ }
680
+ return args;
681
+ }
682
+
683
+ /**
684
+ * The framework's headers plus the caller's, refusing a caller's that names one of
685
+ * the framework's; then requestId, which overwrites any request id header the
686
+ * caller set (architect, 2026-10-05).
687
+ */
688
+ function requestHeaders(options: CallOptions): Record<string, string> {
689
+ const headers: Record<string, string> = { ...FRAMEWORK_HEADERS };
690
+ const owned = new Set(Object.keys(FRAMEWORK_HEADERS).map((name) => name.toLowerCase()));
691
+ for (const [name, value] of Object.entries(options.headers ?? {})) {
692
+ if (owned.has(name.toLowerCase())) {
693
+ throw new TypeError('The "' + name + '" header is the framework\\'s own; a request cannot set it (§12.4).');
694
+ }
695
+ headers[name] = value;
696
+ }
697
+ if (options.requestId !== undefined && options.requestId !== "") {
698
+ for (const name of Object.keys(headers)) {
699
+ if (name.toLowerCase() === REQUEST_ID_HEADER.toLowerCase()) {
700
+ delete headers[name];
701
+ }
702
+ }
703
+ headers[REQUEST_ID_HEADER] = options.requestId;
704
+ }
705
+ return headers;
706
+ }
707
+ `;