@aventara/client 0.0.0-stage → 0.1.0-pilot.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +91 -0
- package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
- package/README.md +268 -2
- package/dist/avclient.bin.d.ts +2 -0
- package/dist/avclient.bin.js +15 -0
- package/dist/cli/command.parser.d.ts +30 -0
- package/dist/cli/command.parser.js +132 -0
- package/dist/cli/generate.command.d.ts +24 -0
- package/dist/cli/generate.command.js +41 -0
- package/dist/cli/generation-failure.renderer.d.ts +6 -0
- package/dist/cli/generation-failure.renderer.js +54 -0
- package/dist/cli/generation-success.renderer.d.ts +32 -0
- package/dist/cli/generation-success.renderer.js +47 -0
- package/dist/cli/terminal.prompter.d.ts +13 -0
- package/dist/cli/terminal.prompter.js +53 -0
- package/dist/cli/warning.renderer.d.ts +10 -0
- package/dist/cli/warning.renderer.js +14 -0
- package/dist/cli.d.ts +29 -0
- package/dist/cli.js +71 -0
- package/dist/config/client-config.interface.d.ts +62 -0
- package/dist/config/client-config.interface.js +14 -0
- package/dist/config/config.loader.d.ts +33 -0
- package/dist/config/config.loader.js +80 -0
- package/dist/config/config.resolver.d.ts +50 -0
- package/dist/config/config.resolver.js +126 -0
- package/dist/config/env.cascade.d.ts +84 -0
- package/dist/config/env.cascade.js +126 -0
- package/dist/contract/contract.acceptance.d.ts +77 -0
- package/dist/contract/contract.acceptance.js +124 -0
- package/dist/contract/contract.fetcher.d.ts +64 -0
- package/dist/contract/contract.fetcher.js +85 -0
- package/dist/contract/contract.loader.d.ts +32 -0
- package/dist/contract/contract.loader.js +32 -0
- package/dist/emit/banner.emitter.d.ts +31 -0
- package/dist/emit/banner.emitter.js +42 -0
- package/dist/emit/client-surface.emitter.d.ts +32 -0
- package/dist/emit/client-surface.emitter.js +236 -0
- package/dist/emit/client-tree.emitter.d.ts +37 -0
- package/dist/emit/client-tree.emitter.js +103 -0
- package/dist/emit/contract-carrier.emitter.d.ts +13 -0
- package/dist/emit/contract-carrier.emitter.js +60 -0
- package/dist/emit/derivation.emitter.d.ts +45 -0
- package/dist/emit/derivation.emitter.js +233 -0
- package/dist/emit/descriptor.emitter.d.ts +4 -0
- package/dist/emit/descriptor.emitter.js +97 -0
- package/dist/emit/emitted-tree.interface.d.ts +61 -0
- package/dist/emit/emitted-tree.interface.js +18 -0
- package/dist/emit/enum.emitter.d.ts +24 -0
- package/dist/emit/enum.emitter.js +42 -0
- package/dist/emit/name.deriver.d.ts +153 -0
- package/dist/emit/name.deriver.js +411 -0
- package/dist/emit/named-type.emitter.d.ts +32 -0
- package/dist/emit/named-type.emitter.js +50 -0
- package/dist/emit/runtime.emitter.d.ts +87 -0
- package/dist/emit/runtime.emitter.js +707 -0
- package/dist/emit/scalar.codec.d.ts +63 -0
- package/dist/emit/scalar.codec.js +498 -0
- package/dist/emit/transaction.emitter.d.ts +17 -0
- package/dist/emit/transaction.emitter.js +438 -0
- package/dist/generate.d.ts +123 -0
- package/dist/generate.js +98 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/init/client-config.template.d.ts +6 -0
- package/dist/init/client-config.template.js +22 -0
- package/dist/init/client-init.errors.d.ts +9 -0
- package/dist/init/client-init.errors.js +9 -0
- package/dist/init/client-init.orchestrator.d.ts +3 -0
- package/dist/init/client-init.orchestrator.js +82 -0
- package/dist/init/client-init.planner.d.ts +26 -0
- package/dist/init/client-init.planner.js +88 -0
- package/dist/init/client-init.questions.d.ts +52 -0
- package/dist/init/client-init.questions.js +124 -0
- package/dist/init/client-project.inspector.d.ts +15 -0
- package/dist/init/client-project.inspector.js +32 -0
- package/dist/init/command.runner.d.ts +8 -0
- package/dist/init/command.runner.js +17 -0
- package/dist/node-version.guard.d.ts +8 -0
- package/dist/node-version.guard.js +59 -0
- package/dist/output/output.validator.d.ts +76 -0
- package/dist/output/output.validator.js +254 -0
- package/dist/output/output.writer.d.ts +162 -0
- package/dist/output/output.writer.js +499 -0
- package/package.json +47 -3
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The transaction builder and runner (§14; Phase 12-rest S6, Q13, Q14, P3):
|
|
3
|
+
* `runtime/fingerprint.ts` and `runtime/transaction.ts`, emitted iff the
|
|
4
|
+
* ClientContract advertises `interactive` transactions (P3) — `client.ts` wires
|
|
5
|
+
* them to `avClient.tx` and `avClient.transaction`.
|
|
6
|
+
*
|
|
7
|
+
* Both are fixed by protocol version, not by the Contract, and mirror core's own
|
|
8
|
+
* seams: the plan is assembled as `transactions/transaction-plan-assembler.ts`
|
|
9
|
+
* assembles it — handles as pure data, a `$ref` bound lazily to its source
|
|
10
|
+
* handle's position in the list it runs with, the two refusals core answers in
|
|
11
|
+
* process (Q13) — and each node's fingerprint is core's
|
|
12
|
+
* `computeOperationFingerprint` over the node's WIRE arguments (§14.4), computed
|
|
13
|
+
* here by an emitted JCS and SHA-256 (Q14: `crypto.subtle` is absent from a
|
|
14
|
+
* browser page served over plain http from a non-localhost host).
|
|
15
|
+
*/
|
|
16
|
+
export function emitTransactionModules() {
|
|
17
|
+
return [
|
|
18
|
+
{ path: "runtime/fingerprint.ts", source: FINGERPRINT_MODULE },
|
|
19
|
+
{ path: "runtime/transaction.ts", source: TRANSACTION_MODULE },
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
const FINGERPRINT_MODULE = `/**
|
|
23
|
+
* The v1 operation fingerprint (§14.4): RFC 8785 canonical JSON of
|
|
24
|
+
* { resource, family, variant, args } over the WIRE-form arguments, its UTF-8
|
|
25
|
+
* bytes hashed with SHA-256, the first 16 bytes in base64url, prefixed "fp1:".
|
|
26
|
+
* Dependency-free (Q14), and pinned byte for byte to core's
|
|
27
|
+
* computeOperationFingerprint.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** One plan node, its arguments already in wire form. */
|
|
31
|
+
export interface FingerprintNode {
|
|
32
|
+
readonly resource: string;
|
|
33
|
+
readonly family: string;
|
|
34
|
+
readonly variant: string;
|
|
35
|
+
readonly args: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The node's fingerprint, or null when its arguments are not canonical JSON — a
|
|
40
|
+
* lone surrogate, a non-finite number — exactly when core cannot compute one
|
|
41
|
+
* either; the server then answers the operation itself.
|
|
42
|
+
*/
|
|
43
|
+
export function fingerprintOf(node: FingerprintNode): string | null {
|
|
44
|
+
let canonical: string;
|
|
45
|
+
try {
|
|
46
|
+
canonical = canonicalJson({
|
|
47
|
+
resource: node.resource,
|
|
48
|
+
family: node.family,
|
|
49
|
+
variant: node.variant,
|
|
50
|
+
args: node.args,
|
|
51
|
+
});
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return "fp1:" + base64Url(sha256(utf8(canonical)).subarray(0, 16));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** RFC 8785: JavaScript's own number and string spelling, keys in UTF-16 code-unit order. */
|
|
59
|
+
export function canonicalJson(value: unknown): string {
|
|
60
|
+
if (value === null) {
|
|
61
|
+
return "null";
|
|
62
|
+
}
|
|
63
|
+
if (typeof value === "boolean") {
|
|
64
|
+
return value ? "true" : "false";
|
|
65
|
+
}
|
|
66
|
+
if (typeof value === "number") {
|
|
67
|
+
if (!Number.isFinite(value)) {
|
|
68
|
+
throw new TypeError("A non-finite number is not canonical JSON.");
|
|
69
|
+
}
|
|
70
|
+
return JSON.stringify(value);
|
|
71
|
+
}
|
|
72
|
+
if (typeof value === "string") {
|
|
73
|
+
assertWellFormed(value);
|
|
74
|
+
return JSON.stringify(value);
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
return "[" + value.map((entry: unknown) => canonicalJson(entry)).join(",") + "]";
|
|
78
|
+
}
|
|
79
|
+
if (typeof value !== "object") {
|
|
80
|
+
throw new TypeError("A " + typeof value + " is not canonical JSON.");
|
|
81
|
+
}
|
|
82
|
+
const prototype: unknown = Object.getPrototypeOf(value);
|
|
83
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
84
|
+
throw new TypeError("A non-plain object is not canonical JSON.");
|
|
85
|
+
}
|
|
86
|
+
const record = value as Readonly<Record<string, unknown>>;
|
|
87
|
+
const keys = Object.keys(record).sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
|
|
88
|
+
return (
|
|
89
|
+
"{" +
|
|
90
|
+
keys
|
|
91
|
+
.map((key) => {
|
|
92
|
+
assertWellFormed(key);
|
|
93
|
+
return JSON.stringify(key) + ":" + canonicalJson(record[key]);
|
|
94
|
+
})
|
|
95
|
+
.join(",") +
|
|
96
|
+
"}"
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function assertWellFormed(text: string): void {
|
|
101
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
102
|
+
const unit = text.charCodeAt(index);
|
|
103
|
+
if (unit >= 0xd800 && unit <= 0xdbff) {
|
|
104
|
+
const next = text.charCodeAt(index + 1);
|
|
105
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) {
|
|
106
|
+
throw new TypeError("A lone surrogate is not canonical JSON.");
|
|
107
|
+
}
|
|
108
|
+
index += 1;
|
|
109
|
+
} else if (unit >= 0xdc00 && unit <= 0xdfff) {
|
|
110
|
+
throw new TypeError("A lone surrogate is not canonical JSON.");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** UTF-8 of well-formed text. */
|
|
116
|
+
function utf8(text: string): Uint8Array {
|
|
117
|
+
const bytes: number[] = [];
|
|
118
|
+
for (const character of text) {
|
|
119
|
+
const point = character.codePointAt(0) ?? 0;
|
|
120
|
+
if (point < 0x80) {
|
|
121
|
+
bytes.push(point);
|
|
122
|
+
} else if (point < 0x800) {
|
|
123
|
+
bytes.push(0xc0 | (point >> 6), 0x80 | (point & 63));
|
|
124
|
+
} else if (point < 0x10000) {
|
|
125
|
+
bytes.push(0xe0 | (point >> 12), 0x80 | ((point >> 6) & 63), 0x80 | (point & 63));
|
|
126
|
+
} else {
|
|
127
|
+
bytes.push(0xf0 | (point >> 18), 0x80 | ((point >> 12) & 63), 0x80 | ((point >> 6) & 63), 0x80 | (point & 63));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return new Uint8Array(bytes);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const ROUND = new Uint32Array([
|
|
134
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
135
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
136
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
137
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
138
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
139
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
140
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
141
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
function rotate(word: number, bits: number): number {
|
|
145
|
+
return (word >>> bits) | (word << (32 - bits));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** FIPS 180-4 SHA-256. */
|
|
149
|
+
export function sha256(message: Uint8Array): Uint8Array {
|
|
150
|
+
const blocks = Math.ceil((message.length + 9) / 64);
|
|
151
|
+
const padded = new Uint8Array(blocks * 64);
|
|
152
|
+
padded.set(message);
|
|
153
|
+
padded[message.length] = 0x80;
|
|
154
|
+
const bits = message.length * 8;
|
|
155
|
+
const view = new DataView(padded.buffer);
|
|
156
|
+
view.setUint32(padded.length - 8, Math.floor(bits / 0x100000000));
|
|
157
|
+
view.setUint32(padded.length - 4, bits >>> 0);
|
|
158
|
+
const state = new Uint32Array([
|
|
159
|
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
|
160
|
+
]);
|
|
161
|
+
const schedule = new Uint32Array(64);
|
|
162
|
+
for (let block = 0; block < blocks; block += 1) {
|
|
163
|
+
for (let index = 0; index < 64; index += 1) {
|
|
164
|
+
if (index < 16) {
|
|
165
|
+
schedule[index] = view.getUint32(block * 64 + index * 4);
|
|
166
|
+
} else {
|
|
167
|
+
const early = schedule[index - 15] ?? 0;
|
|
168
|
+
const late = schedule[index - 2] ?? 0;
|
|
169
|
+
const sigma0 = rotate(early, 7) ^ rotate(early, 18) ^ (early >>> 3);
|
|
170
|
+
const sigma1 = rotate(late, 17) ^ rotate(late, 19) ^ (late >>> 10);
|
|
171
|
+
schedule[index] = ((schedule[index - 16] ?? 0) + sigma0 + (schedule[index - 7] ?? 0) + sigma1) >>> 0;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
let [a, b, c, d, e, f, g, h] = state as unknown as [number, number, number, number, number, number, number, number];
|
|
175
|
+
for (let index = 0; index < 64; index += 1) {
|
|
176
|
+
const sum1 = rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25);
|
|
177
|
+
const choose = (e & f) ^ (~e & g);
|
|
178
|
+
const first = (h + sum1 + choose + (ROUND[index] ?? 0) + (schedule[index] ?? 0)) >>> 0;
|
|
179
|
+
const sum0 = rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22);
|
|
180
|
+
const majority = (a & b) ^ (a & c) ^ (b & c);
|
|
181
|
+
const second = (sum0 + majority) >>> 0;
|
|
182
|
+
h = g;
|
|
183
|
+
g = f;
|
|
184
|
+
f = e;
|
|
185
|
+
e = (d + first) >>> 0;
|
|
186
|
+
d = c;
|
|
187
|
+
c = b;
|
|
188
|
+
b = a;
|
|
189
|
+
a = (first + second) >>> 0;
|
|
190
|
+
}
|
|
191
|
+
const words = [a, b, c, d, e, f, g, h];
|
|
192
|
+
for (let index = 0; index < 8; index += 1) {
|
|
193
|
+
state[index] = ((state[index] ?? 0) + (words[index] ?? 0)) >>> 0;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const digest = new Uint8Array(32);
|
|
197
|
+
const output = new DataView(digest.buffer);
|
|
198
|
+
for (let index = 0; index < 8; index += 1) {
|
|
199
|
+
output.setUint32(index * 4, state[index] ?? 0);
|
|
200
|
+
}
|
|
201
|
+
return digest;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
205
|
+
|
|
206
|
+
/** RFC 4648 §5 base64url, unpadded. */
|
|
207
|
+
function base64Url(bytes: Uint8Array): string {
|
|
208
|
+
let text = "";
|
|
209
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
210
|
+
const remaining = bytes.length - index;
|
|
211
|
+
const group = ((bytes[index] ?? 0) << 16) | ((bytes[index + 1] ?? 0) << 8) | (bytes[index + 2] ?? 0);
|
|
212
|
+
text += BASE64URL_ALPHABET.charAt((group >> 18) & 63) + BASE64URL_ALPHABET.charAt((group >> 12) & 63);
|
|
213
|
+
if (remaining > 1) {
|
|
214
|
+
text += BASE64URL_ALPHABET.charAt((group >> 6) & 63);
|
|
215
|
+
}
|
|
216
|
+
if (remaining > 2) {
|
|
217
|
+
text += BASE64URL_ALPHABET.charAt(group & 63);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return text;
|
|
221
|
+
}
|
|
222
|
+
`;
|
|
223
|
+
const TRANSACTION_MODULE = `import { encodeArguments, serializeWireBody } from "./codec.js";
|
|
224
|
+
import { type Cause, type FrameworkError, frameworkErrorOf } from "./errors.js";
|
|
225
|
+
import { fingerprintOf } from "./fingerprint.js";
|
|
226
|
+
import { type CallOptions, executePlan, type TransportConnection } from "./transport.js";
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The deferred transaction builder and runner (§14), as core assembles a plan
|
|
230
|
+
* in process (transaction-plan-assembler.ts): \`avClient.tx\` defers an operation
|
|
231
|
+
* into a handle — pure data, no request — and \`avClient.transaction\` binds a
|
|
232
|
+
* list of handles into the one §14.2 plan, sends it once, and resolves one
|
|
233
|
+
* result per handle in list order. A handle has no connection, so any client of
|
|
234
|
+
* this generated tree may run it (Q13, a plan ruling).
|
|
235
|
+
*/
|
|
236
|
+
|
|
237
|
+
/** What a handle defers. */
|
|
238
|
+
interface DeferredRecord {
|
|
239
|
+
readonly resource: string;
|
|
240
|
+
readonly family: string;
|
|
241
|
+
readonly variant: string;
|
|
242
|
+
readonly args: unknown;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** What a \`$ref\` placeholder stands for until the plan is assembled. */
|
|
246
|
+
interface Placeholder {
|
|
247
|
+
readonly source: object;
|
|
248
|
+
readonly field: string;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
type MutableRecord = Record<string, unknown>;
|
|
252
|
+
|
|
253
|
+
/** One §14.2 wire node, its arguments in wire form. */
|
|
254
|
+
interface PlanNode {
|
|
255
|
+
readonly resource: string;
|
|
256
|
+
readonly family: string;
|
|
257
|
+
readonly variant: string;
|
|
258
|
+
readonly args: unknown;
|
|
259
|
+
readonly fingerprint: string | null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** One DA-3 container: a mutation-data record and its path inside the arguments. */
|
|
263
|
+
interface Container {
|
|
264
|
+
readonly path: readonly (string | number)[];
|
|
265
|
+
readonly record: Readonly<Record<string, unknown>>;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const HANDLES = new WeakMap<object, DeferredRecord>();
|
|
269
|
+
const PLACEHOLDERS = new WeakMap<object, Placeholder>();
|
|
270
|
+
|
|
271
|
+
function isPlainRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
272
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
const prototype: unknown = Object.getPrototypeOf(value);
|
|
276
|
+
return prototype === Object.prototype || prototype === null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** The argument keys holding DA-3 reference positions: the top-level mutation data. */
|
|
280
|
+
function referenceKeys(family: string): readonly string[] {
|
|
281
|
+
return family === "create" || family === "update" ? ["data"] : family === "upsert" ? ["create", "update"] : [];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function containers(family: string, args: unknown): readonly Container[] {
|
|
285
|
+
if (!isPlainRecord(args)) {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
return referenceKeys(family).flatMap((key): Container[] => {
|
|
289
|
+
const value = args[key];
|
|
290
|
+
if (Array.isArray(value)) {
|
|
291
|
+
return value.flatMap((entry: unknown, index) => (isPlainRecord(entry) ? [{ path: [key, index], record: entry }] : []));
|
|
292
|
+
}
|
|
293
|
+
return isPlainRecord(value) ? [{ path: [key], record: value }] : [];
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** \`args\` with each DA-3 container replaced by \`rebuild\`, cloning only the containing path. */
|
|
298
|
+
function replaceContainers(family: string, args: unknown, rebuild: (container: Container) => MutableRecord): unknown {
|
|
299
|
+
if (!isPlainRecord(args)) {
|
|
300
|
+
return args;
|
|
301
|
+
}
|
|
302
|
+
const result: MutableRecord = { ...args };
|
|
303
|
+
for (const key of referenceKeys(family)) {
|
|
304
|
+
const value = args[key];
|
|
305
|
+
if (Array.isArray(value)) {
|
|
306
|
+
result[key] = value.map((entry: unknown, index) => (isPlainRecord(entry) ? rebuild({ path: [key, index], record: entry }) : entry));
|
|
307
|
+
} else if (isPlainRecord(value)) {
|
|
308
|
+
result[key] = rebuild({ path: [key], record: value });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Defers one operation into a handle (§14.1): nothing is sent. Its DA-3
|
|
316
|
+
* containers are copied now, so a later change to the caller's data cannot move
|
|
317
|
+
* a reference — and a handle can reference only handles built before it.
|
|
318
|
+
*/
|
|
319
|
+
export function deferOperation(resource: string, family: string, variant: string, args: unknown): object {
|
|
320
|
+
const handle: object = Object.freeze({
|
|
321
|
+
$ref: (field: string): object => {
|
|
322
|
+
const placeholder = Object.freeze({ $ref: Object.freeze({ path: Object.freeze([field]) }) });
|
|
323
|
+
PLACEHOLDERS.set(placeholder, { source: handle, field });
|
|
324
|
+
return placeholder;
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
HANDLES.set(handle, {
|
|
328
|
+
resource,
|
|
329
|
+
family,
|
|
330
|
+
variant,
|
|
331
|
+
args: replaceContainers(family, args, ({ record }) => ({ ...record })),
|
|
332
|
+
});
|
|
333
|
+
return handle;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** What core answers in process for a plan entry it cannot run (A2004 / V1001). */
|
|
337
|
+
function entryRefusal(operation: number, message: string): FrameworkError {
|
|
338
|
+
const cause: Cause = {
|
|
339
|
+
message: "Transaction plan failed framework validation.",
|
|
340
|
+
issues: [{ path: ["operation"], code: "V1001", message }],
|
|
341
|
+
operation,
|
|
342
|
+
};
|
|
343
|
+
return frameworkErrorOf("A2004", cause);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Binds \`steps\` into the plan, sends it once, and resolves one decoded result per
|
|
348
|
+
* step (§14.1). Refused before anything is sent, with what core answers in
|
|
349
|
+
* process (Q13): a list that is not one, an entry that is not a handle of this
|
|
350
|
+
* generated client, one handle twice (ValidationError A2004 / V1001 at
|
|
351
|
+
* ["operation"]); a reference to a handle not in the list (ValidationError A2007 /
|
|
352
|
+
* V1010 at its \`$ref\`).
|
|
353
|
+
*/
|
|
354
|
+
export async function runTransaction(
|
|
355
|
+
connection: TransportConnection,
|
|
356
|
+
steps: unknown,
|
|
357
|
+
options: CallOptions = {},
|
|
358
|
+
): Promise<unknown[]> {
|
|
359
|
+
if (!Array.isArray(steps)) {
|
|
360
|
+
throw frameworkErrorOf("A2004", {
|
|
361
|
+
message: "Transaction plan failed framework validation.",
|
|
362
|
+
issues: [{ path: ["operations"], code: "V1001", message: "Transaction operations must be a list of deferred operations." }],
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
const records: DeferredRecord[] = [];
|
|
366
|
+
const positions = new Map<object, number>();
|
|
367
|
+
for (const [index, entry] of steps.entries()) {
|
|
368
|
+
const record = typeof entry === "object" && entry !== null ? HANDLES.get(entry) : undefined;
|
|
369
|
+
if (record === undefined) {
|
|
370
|
+
throw entryRefusal(index, "Operation must be a deferred operation built by this client's tx.");
|
|
371
|
+
}
|
|
372
|
+
if (positions.has(entry)) {
|
|
373
|
+
throw entryRefusal(index, "Deferred operation appears more than once in this transaction.");
|
|
374
|
+
}
|
|
375
|
+
positions.set(entry, index);
|
|
376
|
+
records.push(record);
|
|
377
|
+
}
|
|
378
|
+
for (const [index, record] of records.entries()) {
|
|
379
|
+
for (const { path, record: container } of containers(record.family, record.args)) {
|
|
380
|
+
for (const [field, value] of Object.entries(container)) {
|
|
381
|
+
const placeholder = typeof value === "object" && value !== null ? PLACEHOLDERS.get(value) : undefined;
|
|
382
|
+
if (placeholder !== undefined && !positions.has(placeholder.source)) {
|
|
383
|
+
throw frameworkErrorOf("A2007", {
|
|
384
|
+
message: "Operation arguments failed framework validation.",
|
|
385
|
+
issues: [
|
|
386
|
+
{
|
|
387
|
+
path: ["operations", index, "args", ...path, field, "$ref"],
|
|
388
|
+
code: "V1010",
|
|
389
|
+
message: "Transaction reference names an operation that is not in this transaction.",
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
operation: index,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Bound lazily, as core binds them: a node's references need their source's
|
|
400
|
+
// fingerprint, so each node is built once, on first need, wherever its source
|
|
401
|
+
// stands in the list — the server, not this client, refuses a later one.
|
|
402
|
+
const nodes = new Map<number, PlanNode>();
|
|
403
|
+
const nodeAt = (index: number): PlanNode => {
|
|
404
|
+
const built = nodes.get(index);
|
|
405
|
+
if (built !== undefined) {
|
|
406
|
+
return built;
|
|
407
|
+
}
|
|
408
|
+
const record = records[index] as DeferredRecord;
|
|
409
|
+
const bound = replaceContainers(record.family, record.args, ({ record: container }) => {
|
|
410
|
+
const copy: MutableRecord = { ...container };
|
|
411
|
+
for (const [field, value] of Object.entries(container)) {
|
|
412
|
+
const placeholder = typeof value === "object" && value !== null ? PLACEHOLDERS.get(value) : undefined;
|
|
413
|
+
if (placeholder !== undefined) {
|
|
414
|
+
const operation = positions.get(placeholder.source) as number;
|
|
415
|
+
copy[field] = { $ref: { operation, fingerprint: nodeAt(operation).fingerprint, path: [placeholder.field] } };
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return copy;
|
|
419
|
+
});
|
|
420
|
+
const args: unknown = JSON.parse(serializeWireBody(encodeArguments(bound)));
|
|
421
|
+
const node: PlanNode = {
|
|
422
|
+
resource: record.resource,
|
|
423
|
+
family: record.family,
|
|
424
|
+
variant: record.variant,
|
|
425
|
+
args,
|
|
426
|
+
fingerprint: fingerprintOf({ resource: record.resource, family: record.family, variant: record.variant, args }),
|
|
427
|
+
};
|
|
428
|
+
nodes.set(index, node);
|
|
429
|
+
return node;
|
|
430
|
+
};
|
|
431
|
+
return executePlan(
|
|
432
|
+
connection,
|
|
433
|
+
{ operations: records.map((_, index) => nodeAt(index)) },
|
|
434
|
+
records.map((record) => record.resource),
|
|
435
|
+
options,
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
`;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { AbsolutePath } from "./config/client-config.interface.js";
|
|
2
|
+
import { type ProcessEnvInput } from "./config/env.cascade.js";
|
|
3
|
+
import type { ContractFetch } from "./contract/contract.fetcher.js";
|
|
4
|
+
import { type EmittedFilePath } from "./emit/emitted-tree.interface.js";
|
|
5
|
+
import type { OutputCheck, TypeScriptResolver } from "./output/output.validator.js";
|
|
6
|
+
/**
|
|
7
|
+
* §15.3's generation pipeline, as one call (S7b):
|
|
8
|
+
*
|
|
9
|
+
* load env + config S2 — `.env` cascade, then `framework.client.ts`
|
|
10
|
+
* → GET <entrypoint>/_contract S3 — conditional on the output's carrier (Q6):
|
|
11
|
+
* `304` and identical bytes → "up to date", nothing written
|
|
12
|
+
* → validate protocol support, ClientContract structure, advertised hash S3
|
|
13
|
+
* → emit S4–S6 — enums, metadata, codecs, errors, transport
|
|
14
|
+
* → emit into a temporary directory, validate, atomically replace S7
|
|
15
|
+
*
|
|
16
|
+
* Only what exists above Phase 12-partial's boundary is emitted: no Resource
|
|
17
|
+
* method, no projection, no transaction builder — those read capabilities and
|
|
18
|
+
* operation descriptors (plan §1).
|
|
19
|
+
*
|
|
20
|
+
* The cascade is resolved BEFORE the config is evaluated, as §15.2 orders it, so
|
|
21
|
+
* an unreadable or malformed `.env` is reported even when the config would also
|
|
22
|
+
* fail. The cascade is a returned record, never written into `process.env` (S2):
|
|
23
|
+
* the config reaches it through `env("NAME")`.
|
|
24
|
+
*
|
|
25
|
+
* Output goes to `generateAt`: `AvClient.ts` and `generated/`, nothing else
|
|
26
|
+
* there touched (architect, 2026-10-04). Content in those two the generator did
|
|
27
|
+
* not produce is never overwritten unasked: the run returns
|
|
28
|
+
* `ForeignOutputContent` instead, and the caller decides.
|
|
29
|
+
*
|
|
30
|
+
* Every failure is thrown, every diagnostic that is not a failure is returned:
|
|
31
|
+
* this function prints nothing and asks nothing. `cli.ts` owns the terminal. A
|
|
32
|
+
* refusal raised after a warning (`OutputWriteError`) carries the warnings
|
|
33
|
+
* raised before it, and so does `ForeignOutputContent`; a defect is rethrown as
|
|
34
|
+
* itself, its warnings kept beside it (`warningsRaisedBeforeDefect`). No run's
|
|
35
|
+
* warnings are lost because it stopped.
|
|
36
|
+
*/
|
|
37
|
+
export interface ClientGenerationInput {
|
|
38
|
+
/** The project directory: where `framework.client.ts` and the `.env` files are. */
|
|
39
|
+
readonly directory: string;
|
|
40
|
+
/** A snapshot of the process environment. Never mutated. */
|
|
41
|
+
readonly processEnv: ProcessEnvInput;
|
|
42
|
+
/**
|
|
43
|
+
* Overwrite or remove content in `AvClient.ts` and `generated/` that the
|
|
44
|
+
* generator did not produce, without asking (the CLI's `--yes`). Otherwise
|
|
45
|
+
* finding any ends the run with `ForeignOutputContent` before anything is
|
|
46
|
+
* written.
|
|
47
|
+
*/
|
|
48
|
+
readonly overrideForeign?: boolean;
|
|
49
|
+
/** Injected for tests; the platform `fetch` otherwise. */
|
|
50
|
+
readonly fetch?: ContractFetch;
|
|
51
|
+
/** The `typescript` optional peer; the installed one by default (Q6). */
|
|
52
|
+
readonly resolveTypeScript?: TypeScriptResolver;
|
|
53
|
+
}
|
|
54
|
+
export interface ClientGenerated {
|
|
55
|
+
readonly kind: "generated";
|
|
56
|
+
/** The shared directory the client was generated into. */
|
|
57
|
+
readonly generateAt: AbsolutePath;
|
|
58
|
+
/** The files written, in the tree's order. */
|
|
59
|
+
readonly files: readonly EmittedFilePath[];
|
|
60
|
+
/** How deeply the output was validated before it replaced the previous one. */
|
|
61
|
+
readonly checked: OutputCheck;
|
|
62
|
+
/**
|
|
63
|
+
* The deployment answered `304` — the ClientContract is the one the previous
|
|
64
|
+
* output was generated against — and the output was still replaced, because
|
|
65
|
+
* this generator or this entrypoint emits other bytes (Q6).
|
|
66
|
+
*/
|
|
67
|
+
readonly contractUnchanged: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Everything the run has to say without failing, in a fixed order: the
|
|
70
|
+
* emission's rename notices, then the validator's degraded-check warning, then
|
|
71
|
+
* the writer's crash-recovery and cleanup notices.
|
|
72
|
+
*/
|
|
73
|
+
readonly warnings: readonly string[];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The run found content in `AvClient.ts` or `generated/` the generator did not
|
|
77
|
+
* produce, and stopped before writing anything (architect, 2026-10-04). Whoever
|
|
78
|
+
* owns the terminal asks; `proceed` writes the emission already made — no second
|
|
79
|
+
* fetch — overwriting or removing exactly `foreign`, through the same
|
|
80
|
+
* temp → validate → replace path.
|
|
81
|
+
*/
|
|
82
|
+
export interface ForeignOutputContent {
|
|
83
|
+
readonly kind: "foreign-content";
|
|
84
|
+
readonly generateAt: AbsolutePath;
|
|
85
|
+
/**
|
|
86
|
+
* Relative to `generateAt`, POSIX-separated, in code-unit order — including
|
|
87
|
+
* what a killed run left in the `generated/` it moved aside, named where its
|
|
88
|
+
* recovery puts it back.
|
|
89
|
+
*/
|
|
90
|
+
readonly foreign: readonly string[];
|
|
91
|
+
/**
|
|
92
|
+
* What the run said before it stopped here — the emission's rename notices —
|
|
93
|
+
* for whoever stops it for good to report. `proceed` returns them again, first
|
|
94
|
+
* among its own.
|
|
95
|
+
*/
|
|
96
|
+
readonly warnings: readonly string[];
|
|
97
|
+
proceed(): Promise<ClientGenerated>;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The deployment serves the ClientContract the output was generated against, and
|
|
101
|
+
* this generator, with this entrypoint, emits exactly the bytes already there
|
|
102
|
+
* (Phase 12-rest Q6): nothing was written.
|
|
103
|
+
*/
|
|
104
|
+
export interface ClientUpToDate {
|
|
105
|
+
readonly kind: "up-to-date";
|
|
106
|
+
readonly generateAt: AbsolutePath;
|
|
107
|
+
/** The files already there, in the tree's order. */
|
|
108
|
+
readonly files: readonly EmittedFilePath[];
|
|
109
|
+
/** The emission's rename notices: still true of the output. */
|
|
110
|
+
readonly warnings: readonly string[];
|
|
111
|
+
}
|
|
112
|
+
export type ClientGenerationOutcome = ClientGenerated | ClientUpToDate | ForeignOutputContent;
|
|
113
|
+
/**
|
|
114
|
+
* Generates the client the project's config describes — or, when content the
|
|
115
|
+
* generator did not produce stands in its way and `overrideForeign` is not set,
|
|
116
|
+
* says what it is and writes nothing.
|
|
117
|
+
*
|
|
118
|
+
* @throws a refusal (`EnvFileError`, `ClientConfigError`, `ContractTransportError`,
|
|
119
|
+
* `ContractProtocolError`, `GeneratedNameError`, `OutputWriteError`) whose
|
|
120
|
+
* message is the whole diagnosis; the previous output is intact. Anything else
|
|
121
|
+
* is a defect.
|
|
122
|
+
*/
|
|
123
|
+
export declare function generateClient(input: ClientGenerationInput): Promise<ClientGenerationOutcome>;
|
package/dist/generate.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { loadClientConfigFile } from "./config/config.loader.js";
|
|
4
|
+
import { resolveClientConfig } from "./config/config.resolver.js";
|
|
5
|
+
import { resolveEnvCascade, } from "./config/env.cascade.js";
|
|
6
|
+
import { loadClientContractSince } from "./contract/contract.loader.js";
|
|
7
|
+
import { emitClientTree } from "./emit/client-tree.emitter.js";
|
|
8
|
+
import { parseContractCarrier } from "./emit/contract-carrier.emitter.js";
|
|
9
|
+
import { GENERATED_DIRECTORY, } from "./emit/emitted-tree.interface.js";
|
|
10
|
+
import { findForeignOutputContent, OutputWriteError, ownedOutputMatches, precedeDefect, writeClientOutput, } from "./output/output.writer.js";
|
|
11
|
+
/**
|
|
12
|
+
* The ClientContract the output in `generateAt` holds, when its owned entries
|
|
13
|
+
* are intact — nothing in them the generator did not produce — and its carrier
|
|
14
|
+
* parses back and re-verifies (Q4, Q6); otherwise undefined, and the GET is
|
|
15
|
+
* unconditional. A failure to inspect is no carrier: the run's own inspection,
|
|
16
|
+
* later, reports it.
|
|
17
|
+
*/
|
|
18
|
+
async function storedContract(generateAt) {
|
|
19
|
+
try {
|
|
20
|
+
if ((await findForeignOutputContent(generateAt)).length > 0) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
return await parseContractCarrier(await readFile(path.join(generateAt, GENERATED_DIRECTORY, "contract.ts"), "utf8"));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Generates the client the project's config describes — or, when content the
|
|
31
|
+
* generator did not produce stands in its way and `overrideForeign` is not set,
|
|
32
|
+
* says what it is and writes nothing.
|
|
33
|
+
*
|
|
34
|
+
* @throws a refusal (`EnvFileError`, `ClientConfigError`, `ContractTransportError`,
|
|
35
|
+
* `ContractProtocolError`, `GeneratedNameError`, `OutputWriteError`) whose
|
|
36
|
+
* message is the whole diagnosis; the previous output is intact. Anything else
|
|
37
|
+
* is a defect.
|
|
38
|
+
*/
|
|
39
|
+
export async function generateClient(input) {
|
|
40
|
+
const cascade = resolveEnvCascade({
|
|
41
|
+
directory: input.directory,
|
|
42
|
+
processEnv: input.processEnv,
|
|
43
|
+
});
|
|
44
|
+
const loaded = await loadClientConfigFile(input.directory);
|
|
45
|
+
const config = resolveClientConfig({
|
|
46
|
+
config: loaded.config,
|
|
47
|
+
cascade,
|
|
48
|
+
configDirectory: path.dirname(loaded.file),
|
|
49
|
+
});
|
|
50
|
+
const { contract, notModified } = await loadClientContractSince(input.fetch === undefined
|
|
51
|
+
? { entrypoint: config.entrypoint }
|
|
52
|
+
: { entrypoint: config.entrypoint, fetch: input.fetch }, await storedContract(config.generateAt));
|
|
53
|
+
const emission = emitClientTree(contract, config.entrypoint);
|
|
54
|
+
if (notModified &&
|
|
55
|
+
(await ownedOutputMatches(config.generateAt, emission.tree))) {
|
|
56
|
+
return {
|
|
57
|
+
kind: "up-to-date",
|
|
58
|
+
generateAt: config.generateAt,
|
|
59
|
+
files: emission.tree.map((file) => file.path),
|
|
60
|
+
warnings: emission.warnings,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const write = async (overrideForeign) => {
|
|
64
|
+
const written = await writeClientOutput(input.resolveTypeScript === undefined
|
|
65
|
+
? { emission, generateAt: config.generateAt, overrideForeign }
|
|
66
|
+
: {
|
|
67
|
+
emission,
|
|
68
|
+
generateAt: config.generateAt,
|
|
69
|
+
overrideForeign,
|
|
70
|
+
resolveTypeScript: input.resolveTypeScript,
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
kind: "generated",
|
|
74
|
+
generateAt: written.generateAt,
|
|
75
|
+
files: emission.tree.map((file) => file.path),
|
|
76
|
+
checked: written.checked,
|
|
77
|
+
contractUnchanged: notModified,
|
|
78
|
+
warnings: written.warnings,
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
// A structural refusal or a defect here comes after the emission's renames
|
|
82
|
+
// were raised; it carries them, as the writer's own do.
|
|
83
|
+
const foreign = await findForeignOutputContent(config.generateAt).catch((error) => {
|
|
84
|
+
throw error instanceof OutputWriteError
|
|
85
|
+
? error.precededBy(emission.warnings)
|
|
86
|
+
: precedeDefect(error, emission.warnings);
|
|
87
|
+
});
|
|
88
|
+
if (foreign.length === 0 || input.overrideForeign === true) {
|
|
89
|
+
return write(foreign);
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
kind: "foreign-content",
|
|
93
|
+
generateAt: config.generateAt,
|
|
94
|
+
foreign,
|
|
95
|
+
warnings: emission.warnings,
|
|
96
|
+
proceed: () => write(foreign),
|
|
97
|
+
};
|
|
98
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This package's version, read from its own manifest — the `package.json` every
|
|
3
|
+
* tarball carries beside `dist/` — so a release's `changeset version` is the one
|
|
4
|
+
* statement of it.
|
|
5
|
+
*/
|
|
6
|
+
export declare const AVENTARA_CLIENT_GENERATOR_VERSION: string;
|
|
7
|
+
export type { ClientConfigInput, ConfigValue, EnvReference, } from "./config/client-config.interface.js";
|
|
8
|
+
export { defineClientConfig, env } from "./config/config.resolver.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
/**
|
|
3
|
+
* This package's version, read from its own manifest — the `package.json` every
|
|
4
|
+
* tarball carries beside `dist/` — so a release's `changeset version` is the one
|
|
5
|
+
* statement of it.
|
|
6
|
+
*/
|
|
7
|
+
export const AVENTARA_CLIENT_GENERATOR_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
8
|
+
export { defineClientConfig, env } from "./config/config.resolver.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** R4, §15.2 — the `framework.client.ts` `avclient init` writes: developer-owned, read by `avclient generate`. */
|
|
2
|
+
export declare function clientConfigSource(input: {
|
|
3
|
+
readonly entrypoint: string;
|
|
4
|
+
readonly envVar: string | undefined;
|
|
5
|
+
readonly generateAt: string;
|
|
6
|
+
}): string;
|