@aexhq/sdk 0.58.0 → 0.59.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/README.md +25 -22
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +5 -6
- package/dist/resources.d.ts +57 -12
- package/dist/resources.js +124 -17
- package/dist/session.d.ts +31 -31
- package/dist/session.js +178 -56
- package/dist/tools.d.ts +8 -60
- package/dist/tools.js +18 -323
- package/dist/transport.d.ts +2 -3
- package/dist/transport.js +6 -38
- package/package.json +3 -7
- package/dist/internal.d.ts +0 -2
- package/dist/internal.js +0 -1
package/dist/tools.js
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { callback as callbackComponent } from "@aexhq/env-app";
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
import { jcsSha256 } from "./json.js";
|
|
4
4
|
const EMPTY_INPUT = z.object({});
|
|
5
5
|
const TOOL_NAME = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/u;
|
|
6
|
-
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
7
|
-
const SHA256 = /^[0-9a-f]{64}$/u;
|
|
8
6
|
export function tool(inputOrHandler, maybeHandler) {
|
|
9
7
|
if (typeof inputOrHandler === "function") {
|
|
10
8
|
return makeTool({
|
|
11
9
|
...(inputOrHandler.name === "" ? {} : { name: inputOrHandler.name }),
|
|
12
10
|
input: EMPTY_INPUT,
|
|
13
11
|
handler: inputOrHandler,
|
|
14
|
-
requirements: Object.freeze({}),
|
|
15
12
|
});
|
|
16
13
|
}
|
|
17
14
|
if (typeof maybeHandler !== "function")
|
|
@@ -20,7 +17,6 @@ export function tool(inputOrHandler, maybeHandler) {
|
|
|
20
17
|
...(maybeHandler.name === "" ? {} : { name: maybeHandler.name }),
|
|
21
18
|
input: inputOrHandler,
|
|
22
19
|
handler: maybeHandler,
|
|
23
|
-
requirements: Object.freeze({}),
|
|
24
20
|
});
|
|
25
21
|
}
|
|
26
22
|
function makeTool(draft) {
|
|
@@ -30,10 +26,7 @@ function makeTool(draft) {
|
|
|
30
26
|
...(draft.description === undefined ? {} : { description: draft.description }),
|
|
31
27
|
input: draft.input,
|
|
32
28
|
...(draft.output === undefined ? {} : { output: draft.output }),
|
|
33
|
-
requirements: draft.requirements,
|
|
34
29
|
handler: draft.handler,
|
|
35
|
-
...(draft.setupHandler === undefined ? {} : { setupHandler: draft.setupHandler }),
|
|
36
|
-
...(draft.artifact === undefined ? {} : { artifact: draft.artifact }),
|
|
37
30
|
describe(description) {
|
|
38
31
|
if (description.trim() === "" || description.length > 4096) {
|
|
39
32
|
throw new TypeError("Tool description must contain 1 through 4096 characters");
|
|
@@ -48,65 +41,19 @@ function makeTool(draft) {
|
|
|
48
41
|
schemaOf(output, `${draft.name ?? "Tool"} output`);
|
|
49
42
|
return makeTool({ ...draft, output });
|
|
50
43
|
},
|
|
51
|
-
needs(requirements) {
|
|
52
|
-
return makeTool({ ...draft, requirements: normalizeRequirements(requirements) });
|
|
53
|
-
},
|
|
54
|
-
setup(handler) {
|
|
55
|
-
if (typeof handler !== "function")
|
|
56
|
-
throw new TypeError("Tool setup requires a function");
|
|
57
|
-
return makeTool({ ...draft, setupHandler: handler });
|
|
58
|
-
},
|
|
59
|
-
bind(environment) {
|
|
60
|
-
if (!isEnvironmentRef(environment))
|
|
61
|
-
throw new TypeError("Tool binding requires an EnvironmentRef");
|
|
62
|
-
return Object.freeze({ kind: "aex.bound-tool", tool: makeTool(draft), environment });
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
export function withPreparedArtifact(value, artifact) {
|
|
67
|
-
assertTool(value);
|
|
68
|
-
assertArtifact(artifact);
|
|
69
|
-
return makeTool({
|
|
70
|
-
...(value.name === undefined ? {} : { name: value.name }),
|
|
71
|
-
...(value.description === undefined ? {} : { description: value.description }),
|
|
72
|
-
input: value.input,
|
|
73
|
-
...(value.output === undefined ? {} : { output: value.output }),
|
|
74
|
-
requirements: value.requirements,
|
|
75
|
-
handler: value.handler,
|
|
76
|
-
...(value.setupHandler === undefined ? {} : { setupHandler: value.setupHandler }),
|
|
77
|
-
artifact: Object.freeze({
|
|
78
|
-
...artifact,
|
|
79
|
-
layers: Object.freeze(artifact.layers.map((layer) => Object.freeze({ ...layer }))),
|
|
80
|
-
}),
|
|
81
44
|
});
|
|
82
45
|
}
|
|
83
|
-
export async function
|
|
84
|
-
const
|
|
85
|
-
const selected = [...(selections ?? [])];
|
|
86
|
-
if (selected.length > 0 && environments.byName.size === 0) {
|
|
87
|
-
throw new TypeError("Tools require at least one declared environment");
|
|
88
|
-
}
|
|
89
|
-
const items = [];
|
|
90
|
-
const bundles = new Map();
|
|
91
|
-
const layers = new Map();
|
|
46
|
+
export async function compileCallbacks(selections) {
|
|
47
|
+
const components = [];
|
|
92
48
|
const registrations = [];
|
|
93
49
|
const names = new Set();
|
|
94
|
-
|
|
95
|
-
for (const selection of selected) {
|
|
96
|
-
const value = selection.kind === "aex.bound-tool" ? selection.tool : selection;
|
|
50
|
+
for (const value of selections) {
|
|
97
51
|
assertTool(value);
|
|
98
52
|
const contract = await compileContract(value);
|
|
99
|
-
if (names.has(contract.name))
|
|
53
|
+
if (names.has(contract.name)) {
|
|
100
54
|
throw new TypeError(`Tool ${JSON.stringify(contract.name)} was selected twice`);
|
|
101
|
-
names.add(contract.name);
|
|
102
|
-
const explicit = selection.kind === "aex.bound-tool" ? selection.environment : undefined;
|
|
103
|
-
const environment = chooseEnvironment(contract.name, value, explicit, environments);
|
|
104
|
-
const environmentName = environments.byRef.get(environment);
|
|
105
|
-
const environmentProfile = inspectEnvironment(environment).serialized.profile;
|
|
106
|
-
for (const key of value.requirements.env ?? []) {
|
|
107
|
-
if (!(key in secrets))
|
|
108
|
-
throw new TypeError(`Tool ${JSON.stringify(contract.name)} requires missing secret ${JSON.stringify(key)}`);
|
|
109
55
|
}
|
|
56
|
+
names.add(contract.name);
|
|
110
57
|
const definition = {
|
|
111
58
|
name: contract.name,
|
|
112
59
|
...(contract.description === undefined ? {} : { description: contract.description }),
|
|
@@ -114,180 +61,22 @@ export async function compileTools(selections, declared, secrets = {}) {
|
|
|
114
61
|
output_schema: contract.outputSchema ?? {},
|
|
115
62
|
contract_digest: contract.contractDigest,
|
|
116
63
|
};
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const registration = `tool:${contract.contractDigest}`;
|
|
128
|
-
registrations.push(Object.freeze({
|
|
129
|
-
registration,
|
|
130
|
-
name: contract.name,
|
|
131
|
-
contractDigest: contract.contractDigest,
|
|
132
|
-
input: value.input,
|
|
133
|
-
...(value.output === undefined ? {} : { output: value.output }),
|
|
134
|
-
handler: value.handler,
|
|
135
|
-
}));
|
|
136
|
-
items.push({
|
|
137
|
-
definition,
|
|
138
|
-
executor: {
|
|
139
|
-
kind: "environment",
|
|
140
|
-
environment: environmentName,
|
|
141
|
-
callback_registration: registration,
|
|
142
|
-
requirements: wireRequirements(value.requirements),
|
|
143
|
-
},
|
|
144
|
-
});
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
if (value.artifact === undefined) {
|
|
148
|
-
throw new TypeError(`Tool ${JSON.stringify(contract.name)} has no prepared computer artifact`);
|
|
149
|
-
}
|
|
150
|
-
assertArtifact(value.artifact);
|
|
151
|
-
const layerRefs = [];
|
|
152
|
-
for (const layer of value.artifact.layers) {
|
|
153
|
-
const bytes = await readArtifactLayer(layer);
|
|
154
|
-
const existing = layers.get(layer.digest);
|
|
155
|
-
const payload = {
|
|
156
|
-
checksum: layer.digest,
|
|
157
|
-
bytes: layer.bytes,
|
|
158
|
-
media_type: layer.mediaType,
|
|
159
|
-
content: bytes,
|
|
160
|
-
};
|
|
161
|
-
if (existing !== undefined &&
|
|
162
|
-
(existing.bytes !== payload.bytes || existing.media_type !== payload.media_type)) {
|
|
163
|
-
throw new TypeError(`Prepared artifact layer ${layer.digest} conflicts with an earlier layer`);
|
|
164
|
-
}
|
|
165
|
-
layers.set(layer.digest, payload);
|
|
166
|
-
layerRefs.push({
|
|
167
|
-
checksum: layer.digest,
|
|
168
|
-
bytes: layer.bytes,
|
|
169
|
-
media_type: layer.mediaType,
|
|
170
|
-
mount_path: layer.mountPath,
|
|
171
|
-
unpack: layer.unpack,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
const manifest = {
|
|
175
|
-
checksum: value.artifact.digest,
|
|
176
|
-
bytes: value.artifact.bytes,
|
|
177
|
-
target: value.artifact.target,
|
|
178
|
-
execute_path: value.artifact.execute,
|
|
179
|
-
...(value.artifact.setup === undefined ? {} : { setup_path: value.artifact.setup }),
|
|
180
|
-
layers: layerRefs,
|
|
181
|
-
};
|
|
182
|
-
const manifestIdentity = {
|
|
183
|
-
profile: "computer/v1",
|
|
184
|
-
target: value.artifact.target,
|
|
185
|
-
execute_path: value.artifact.execute,
|
|
186
|
-
setup_path: value.artifact.setup ?? null,
|
|
187
|
-
layers: layerRefs,
|
|
188
|
-
};
|
|
189
|
-
if (await jcsSha256(manifestIdentity) !== value.artifact.digest) {
|
|
190
|
-
throw new TypeError(`Prepared artifact ${value.artifact.digest} manifest digest changed`);
|
|
191
|
-
}
|
|
192
|
-
const existingManifest = bundles.get(value.artifact.digest);
|
|
193
|
-
if (existingManifest !== undefined && JSON.stringify(existingManifest) !== JSON.stringify(manifest)) {
|
|
194
|
-
throw new TypeError(`Prepared artifact ${value.artifact.digest} conflicts with an earlier manifest`);
|
|
195
|
-
}
|
|
196
|
-
bundles.set(value.artifact.digest, manifest);
|
|
197
|
-
items.push({
|
|
198
|
-
definition,
|
|
199
|
-
executor: {
|
|
200
|
-
kind: "environment",
|
|
201
|
-
environment: environmentName,
|
|
202
|
-
artifact_digest: value.artifact.digest,
|
|
203
|
-
requirements: wireRequirements(value.requirements),
|
|
204
|
-
},
|
|
205
|
-
});
|
|
64
|
+
const registration = `tool:${contract.contractDigest}`;
|
|
65
|
+
components.push(callbackComponent(definition, registration));
|
|
66
|
+
registrations.push(Object.freeze({
|
|
67
|
+
registration,
|
|
68
|
+
name: contract.name,
|
|
69
|
+
contractDigest: contract.contractDigest,
|
|
70
|
+
input: value.input,
|
|
71
|
+
...(value.output === undefined ? {} : { output: value.output }),
|
|
72
|
+
handler: value.handler,
|
|
73
|
+
}));
|
|
206
74
|
}
|
|
207
75
|
return {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
layers: Object.freeze([...layers.values()]),
|
|
211
|
-
clientRegistrations: Object.freeze(registrations),
|
|
212
|
-
environments: environments.serialized,
|
|
213
|
-
environmentNames: environments.byRef,
|
|
214
|
-
...(callbackClientId === undefined ? {} : { callbackClientId }),
|
|
76
|
+
components: Object.freeze(components),
|
|
77
|
+
registrations: Object.freeze(registrations),
|
|
215
78
|
};
|
|
216
79
|
}
|
|
217
|
-
function normalizeEnvironments(declared) {
|
|
218
|
-
const byName = new Map();
|
|
219
|
-
const byRef = new Map();
|
|
220
|
-
const serialized = Object.create(null);
|
|
221
|
-
for (const [name, environment] of Object.entries(declared ?? {})) {
|
|
222
|
-
if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/u.test(name)) {
|
|
223
|
-
throw new TypeError(`Invalid environment name ${JSON.stringify(name)}`);
|
|
224
|
-
}
|
|
225
|
-
if (!isEnvironmentRef(environment))
|
|
226
|
-
throw new TypeError(`Environment ${JSON.stringify(name)} is not an EnvironmentRef`);
|
|
227
|
-
const previous = byRef.get(environment);
|
|
228
|
-
if (previous !== undefined) {
|
|
229
|
-
throw new TypeError(`The same EnvironmentRef was declared as ${JSON.stringify(previous)} and ${JSON.stringify(name)}`);
|
|
230
|
-
}
|
|
231
|
-
byName.set(name, environment);
|
|
232
|
-
byRef.set(environment, name);
|
|
233
|
-
serialized[name] = inspectEnvironment(environment).serialized;
|
|
234
|
-
}
|
|
235
|
-
return { byName, byRef, serialized: Object.freeze(serialized) };
|
|
236
|
-
}
|
|
237
|
-
function chooseEnvironment(toolName, value, explicit, environments) {
|
|
238
|
-
if (explicit !== undefined) {
|
|
239
|
-
const name = environments.byRef.get(explicit);
|
|
240
|
-
if (name === undefined)
|
|
241
|
-
throw new TypeError(`Tool ${JSON.stringify(toolName)} is bound to an environment absent from environments`);
|
|
242
|
-
const reasons = incompatibilities(value, inspectEnvironment(explicit).serialized.profile);
|
|
243
|
-
if (reasons.length > 0) {
|
|
244
|
-
throw new TypeError(`Tool ${JSON.stringify(toolName)} is bound to ${JSON.stringify(name)}, which ${reasons.join(" and ")}`);
|
|
245
|
-
}
|
|
246
|
-
return explicit;
|
|
247
|
-
}
|
|
248
|
-
const matches = [];
|
|
249
|
-
const failures = [];
|
|
250
|
-
for (const [name, environment] of environments.byName) {
|
|
251
|
-
const reasons = incompatibilities(value, inspectEnvironment(environment).serialized.profile);
|
|
252
|
-
if (reasons.length === 0)
|
|
253
|
-
matches.push(environment);
|
|
254
|
-
else
|
|
255
|
-
failures.push(`- ${JSON.stringify(name)} ${reasons.join(" and ")}`);
|
|
256
|
-
}
|
|
257
|
-
if (matches.length === 1)
|
|
258
|
-
return matches[0];
|
|
259
|
-
if (matches.length === 0) {
|
|
260
|
-
throw new TypeError(`Tool ${JSON.stringify(toolName)} has no compatible environment:\n${failures.join("\n")}`);
|
|
261
|
-
}
|
|
262
|
-
const names = matches.map((environment) => JSON.stringify(environments.byRef.get(environment))).join(", ");
|
|
263
|
-
throw new TypeError(`Tool ${JSON.stringify(toolName)} matches more than one environment: ${names}. Bind it explicitly.`);
|
|
264
|
-
}
|
|
265
|
-
function incompatibilities(value, profile) {
|
|
266
|
-
const reasons = [];
|
|
267
|
-
if (profile.kind === "computer") {
|
|
268
|
-
if (value.artifact === undefined || profile.platform !== value.artifact.target) {
|
|
269
|
-
reasons.push(`cannot launch target ${JSON.stringify(value.artifact?.target ?? "application-callback")}`);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
else {
|
|
273
|
-
if (value.artifact !== undefined)
|
|
274
|
-
reasons.push("does not run prepared artifacts");
|
|
275
|
-
if (value.setupHandler !== undefined)
|
|
276
|
-
reasons.push("does not provide tool setup");
|
|
277
|
-
}
|
|
278
|
-
const needs = value.requirements;
|
|
279
|
-
if (needs.workspace === true && profile.kind !== "computer")
|
|
280
|
-
reasons.push("is missing workspace");
|
|
281
|
-
if (needs.processes === true && profile.kind !== "computer")
|
|
282
|
-
reasons.push("is missing processes");
|
|
283
|
-
if (needs.network !== undefined && needs.network.length > 0 && profile.network !== "allowlist") {
|
|
284
|
-
reasons.push("cannot enforce network allowlists");
|
|
285
|
-
}
|
|
286
|
-
if (needs.recovery !== undefined && needs.recovery !== profile.recovery) {
|
|
287
|
-
reasons.push(`provides ${profile.recovery} recovery instead of ${needs.recovery}`);
|
|
288
|
-
}
|
|
289
|
-
return reasons;
|
|
290
|
-
}
|
|
291
80
|
async function compileContract(value) {
|
|
292
81
|
const name = value.name;
|
|
293
82
|
if (name === undefined)
|
|
@@ -309,46 +98,6 @@ async function compileContract(value) {
|
|
|
309
98
|
contractDigest: await jcsSha256(canonical),
|
|
310
99
|
};
|
|
311
100
|
}
|
|
312
|
-
function wireRequirements(requirements) {
|
|
313
|
-
return {
|
|
314
|
-
...(requirements.env === undefined ? {} : { env: [...requirements.env] }),
|
|
315
|
-
...(requirements.workspace === undefined ? {} : { workspace: requirements.workspace }),
|
|
316
|
-
...(requirements.processes === undefined ? {} : { processes: requirements.processes }),
|
|
317
|
-
...(requirements.network === undefined
|
|
318
|
-
? {}
|
|
319
|
-
: { network: requirements.network.map(({ host, port }) => ({ host, ports: [port], protocol: "tls" })) }),
|
|
320
|
-
...(requirements.streaming === undefined ? {} : { streaming: requirements.streaming }),
|
|
321
|
-
...(requirements.recovery === undefined
|
|
322
|
-
? {}
|
|
323
|
-
: { recovery: requirements.recovery === "replay-safe" ? "replay_safe" : requirements.recovery }),
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
function normalizeRequirements(requirements) {
|
|
327
|
-
const env = [...(requirements.env ?? [])];
|
|
328
|
-
if (new Set(env).size !== env.length)
|
|
329
|
-
throw new TypeError("Tool environment requirements contain duplicates");
|
|
330
|
-
for (const key of env)
|
|
331
|
-
if (!ENV_NAME.test(key))
|
|
332
|
-
throw new TypeError(`Invalid required environment name ${JSON.stringify(key)}`);
|
|
333
|
-
const network = [...(requirements.network ?? [])];
|
|
334
|
-
if (network.length > 32)
|
|
335
|
-
throw new TypeError("Tool network requirements exceed 32 destinations");
|
|
336
|
-
for (const destination of network) {
|
|
337
|
-
if (destination.host.trim() === "" || destination.host.length > 253)
|
|
338
|
-
throw new TypeError("Invalid tool network host");
|
|
339
|
-
if (!Number.isInteger(destination.port) || destination.port < 1 || destination.port > 65535) {
|
|
340
|
-
throw new TypeError("Invalid tool network port");
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
return Object.freeze({
|
|
344
|
-
...(env.length === 0 ? {} : { env: Object.freeze(env) }),
|
|
345
|
-
...(requirements.workspace === undefined ? {} : { workspace: requirements.workspace }),
|
|
346
|
-
...(requirements.processes === undefined ? {} : { processes: requirements.processes }),
|
|
347
|
-
...(network.length === 0 ? {} : { network: Object.freeze(network.map((item) => Object.freeze({ ...item }))) }),
|
|
348
|
-
...(requirements.streaming === undefined ? {} : { streaming: requirements.streaming }),
|
|
349
|
-
...(requirements.recovery === undefined ? {} : { recovery: requirements.recovery }),
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
101
|
function schemaOf(schema, label) {
|
|
353
102
|
try {
|
|
354
103
|
const value = z.toJSONSchema(schema, { target: "draft-2020-12", unrepresentable: "throw" });
|
|
@@ -368,57 +117,3 @@ function assertToolName(name) {
|
|
|
368
117
|
if (!TOOL_NAME.test(name))
|
|
369
118
|
throw new TypeError(`Invalid Tool name ${JSON.stringify(name)}`);
|
|
370
119
|
}
|
|
371
|
-
function assertArtifact(artifact) {
|
|
372
|
-
if (!SHA256.test(artifact.digest))
|
|
373
|
-
throw new TypeError("Prepared artifact digest must be lower-case SHA-256 hex");
|
|
374
|
-
if (!Number.isSafeInteger(artifact.bytes) || artifact.bytes < 1)
|
|
375
|
-
throw new TypeError("Prepared artifact bytes are invalid");
|
|
376
|
-
if (!artifact.execute.startsWith("/"))
|
|
377
|
-
throw new TypeError("Prepared artifact execute entrypoint must be absolute");
|
|
378
|
-
if (artifact.setup !== undefined && !artifact.setup.startsWith("/")) {
|
|
379
|
-
throw new TypeError("Prepared artifact setup entrypoint must be absolute");
|
|
380
|
-
}
|
|
381
|
-
if (!Array.isArray(artifact.layers) || artifact.layers.length === 0) {
|
|
382
|
-
throw new TypeError("Prepared artifact must contain at least one immutable layer");
|
|
383
|
-
}
|
|
384
|
-
let bytes = 0;
|
|
385
|
-
for (const layer of artifact.layers) {
|
|
386
|
-
if (!SHA256.test(layer.digest))
|
|
387
|
-
throw new TypeError("Prepared artifact layer digest must be lower-case SHA-256 hex");
|
|
388
|
-
if (!Number.isSafeInteger(layer.bytes) || layer.bytes < 1)
|
|
389
|
-
throw new TypeError("Prepared artifact layer bytes are invalid");
|
|
390
|
-
if (!(layer.source instanceof URL))
|
|
391
|
-
throw new TypeError("Prepared artifact layer source must be a URL");
|
|
392
|
-
if (!layer.mountPath.startsWith("/"))
|
|
393
|
-
throw new TypeError("Prepared artifact layer mount path must be absolute");
|
|
394
|
-
bytes += layer.bytes;
|
|
395
|
-
}
|
|
396
|
-
if (bytes !== artifact.bytes)
|
|
397
|
-
throw new TypeError("Prepared artifact bytes do not equal its layers");
|
|
398
|
-
}
|
|
399
|
-
async function readArtifactLayer(layer) {
|
|
400
|
-
let bytes;
|
|
401
|
-
if (layer.source.protocol === "file:") {
|
|
402
|
-
const { readFile } = await import("node:fs/promises");
|
|
403
|
-
bytes = await readFile(layer.source);
|
|
404
|
-
}
|
|
405
|
-
else if (layer.source.protocol === "https:" || layer.source.protocol === "data:") {
|
|
406
|
-
const response = await fetch(layer.source);
|
|
407
|
-
if (!response.ok)
|
|
408
|
-
throw new TypeError(`Could not read artifact layer ${layer.digest}: HTTP ${response.status}`);
|
|
409
|
-
bytes = new Uint8Array(await response.arrayBuffer());
|
|
410
|
-
}
|
|
411
|
-
else {
|
|
412
|
-
throw new TypeError(`Unsupported artifact layer source protocol ${layer.source.protocol}`);
|
|
413
|
-
}
|
|
414
|
-
if (bytes.byteLength !== layer.bytes)
|
|
415
|
-
throw new TypeError(`Artifact layer ${layer.digest} byte length changed`);
|
|
416
|
-
const actual = await sha256(bytes);
|
|
417
|
-
if (actual !== layer.digest)
|
|
418
|
-
throw new TypeError(`Artifact layer ${layer.digest} content digest changed`);
|
|
419
|
-
return bytes;
|
|
420
|
-
}
|
|
421
|
-
async function sha256(bytes) {
|
|
422
|
-
const digest = await globalThis.crypto.subtle.digest("SHA-256", Uint8Array.from(bytes).buffer);
|
|
423
|
-
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
424
|
-
}
|
package/dist/transport.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Event } from "@aexhq/
|
|
2
|
-
import type { JsonRequestOptions, TransferTicket } from "@aexhq/
|
|
1
|
+
import type { Event } from "@aexhq/brain/session";
|
|
2
|
+
import type { JsonRequestOptions, TransferTicket } from "@aexhq/brain";
|
|
3
3
|
export type Fetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
4
4
|
export interface EventOptions {
|
|
5
5
|
after?: number;
|
|
@@ -21,7 +21,6 @@ export declare class Transport {
|
|
|
21
21
|
downloadTransfer(ticket: TransferTicket, signal?: AbortSignal, expectedBytes?: number): Promise<Uint8Array>;
|
|
22
22
|
downloadTransferStream(ticket: TransferTicket, signal?: AbortSignal, expectedBytes?: number): Promise<ReadableStream<Uint8Array>>;
|
|
23
23
|
uploadTransfer(ticket: TransferTicket, content: Uint8Array | (() => ReadableStream<Uint8Array>), bytes: number, signal?: AbortSignal): Promise<void>;
|
|
24
|
-
ensureArtifactLayer(digest: string, mediaType: string, content: Uint8Array, signal?: AbortSignal): Promise<void>;
|
|
25
24
|
json<T>(method: "GET" | "POST" | "DELETE", path: string, options?: JsonRequestOptions): Promise<T>;
|
|
26
25
|
/** Accept one durable deletion job; strict callers poll its short status resource client-side. */
|
|
27
26
|
deleteSession(sessionId: string, waitForCompletion: boolean, signal?: AbortSignal): Promise<void>;
|
package/dist/transport.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MAX_CREATE_SESSION_REQUEST_BYTES, MAX_CUSTOMER_OBSERVATION_BYTES, MAX_MESSAGE_REQUEST_BYTES, MAX_PUBLIC_EVENT_BYTES, } from "@aexhq/
|
|
1
|
+
import { MAX_CREATE_SESSION_REQUEST_BYTES, MAX_CUSTOMER_OBSERVATION_BYTES, MAX_MESSAGE_REQUEST_BYTES, MAX_PUBLIC_EVENT_BYTES, } from "@aexhq/brain";
|
|
2
2
|
import { AbortError, AexError, SessionError, abortError, errorFromApi } from "./errors.js";
|
|
3
3
|
const MAX_ORDINARY_JSON_BYTES = 2 * 1024 * 1024;
|
|
4
4
|
const MAX_ERROR_RESPONSE_BYTES = 64 * 1024;
|
|
@@ -127,38 +127,6 @@ export class Transport {
|
|
|
127
127
|
});
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
|
-
async ensureArtifactLayer(digest, mediaType, content, signal) {
|
|
131
|
-
const path = `/v1/artifacts/${encodeURIComponent(digest)}`;
|
|
132
|
-
const authorization = `Bearer ${this.#apiKey}`;
|
|
133
|
-
const existing = await this.#fetch(`${this.baseUrl}${path}`, {
|
|
134
|
-
method: "HEAD",
|
|
135
|
-
redirect: "error",
|
|
136
|
-
headers: { Authorization: authorization },
|
|
137
|
-
...(signal === undefined ? {} : { signal }),
|
|
138
|
-
});
|
|
139
|
-
if (existing.ok) {
|
|
140
|
-
const bytes = Number(existing.headers.get("content-length"));
|
|
141
|
-
const storedMediaType = existing.headers.get("content-type");
|
|
142
|
-
if (bytes !== content.byteLength || storedMediaType !== mediaType) {
|
|
143
|
-
throw new SessionError(`Uploaded Tool artifact ${digest} has conflicting metadata`);
|
|
144
|
-
}
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
if (existing.status !== 404)
|
|
148
|
-
throw await this.responseError(existing);
|
|
149
|
-
const uploaded = await this.#fetch(`${this.baseUrl}${path}`, {
|
|
150
|
-
method: "PUT",
|
|
151
|
-
redirect: "error",
|
|
152
|
-
headers: {
|
|
153
|
-
Authorization: authorization,
|
|
154
|
-
"Content-Type": mediaType,
|
|
155
|
-
},
|
|
156
|
-
body: arrayBufferBody(content),
|
|
157
|
-
...(signal === undefined ? {} : { signal }),
|
|
158
|
-
});
|
|
159
|
-
if (!uploaded.ok)
|
|
160
|
-
throw await this.responseError(uploaded);
|
|
161
|
-
}
|
|
162
130
|
async json(method, path, options = {}) {
|
|
163
131
|
const attempts = options.retry === true ? 2 : 1;
|
|
164
132
|
// Serialize once before the first await. A caller mutating its source object after a lost
|
|
@@ -453,13 +421,13 @@ function assertTransferTicket(ticket, method) {
|
|
|
453
421
|
}
|
|
454
422
|
function validateCustomerEnvironmentGrant(baseUrl, grant) {
|
|
455
423
|
if (!/^[A-Za-z0-9_.-]{1,128}$/u.test(grant.grant_id)) {
|
|
456
|
-
throw new SessionError("Aex returned an invalid customer
|
|
424
|
+
throw new SessionError("Aex returned an invalid customer Environment grant id");
|
|
457
425
|
}
|
|
458
426
|
if (!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]{1,2048}$/u.test(grant.protocol)) {
|
|
459
|
-
throw new SessionError("Aex returned an invalid customer
|
|
427
|
+
throw new SessionError("Aex returned an invalid customer Environment WebSocket protocol");
|
|
460
428
|
}
|
|
461
429
|
if (!/^[\x21-\x7e]{1,2048}$/u.test(grant.observation_token)) {
|
|
462
|
-
throw new SessionError("Aex returned an invalid customer
|
|
430
|
+
throw new SessionError("Aex returned an invalid customer Environment observation token");
|
|
463
431
|
}
|
|
464
432
|
let socket;
|
|
465
433
|
let observation;
|
|
@@ -470,13 +438,13 @@ function validateCustomerEnvironmentGrant(baseUrl, grant) {
|
|
|
470
438
|
expectedObservation = new URL(`${baseUrl}/v1/customer-environment/observations/${encodeURIComponent(grant.grant_id)}`);
|
|
471
439
|
}
|
|
472
440
|
catch (cause) {
|
|
473
|
-
throw new SessionError("Aex returned an invalid customer
|
|
441
|
+
throw new SessionError("Aex returned an invalid customer Environment URL", { cause });
|
|
474
442
|
}
|
|
475
443
|
const secureSocket = socket.protocol === "wss:" ||
|
|
476
444
|
(socket.protocol === "ws:" && isLoopback(socket.hostname));
|
|
477
445
|
if (!secureSocket || socket.username !== "" || socket.password !== "" ||
|
|
478
446
|
socket.search !== "" || socket.hash !== "") {
|
|
479
|
-
throw new SessionError("Aex customer
|
|
447
|
+
throw new SessionError("Aex customer Environment sockets require credential-free WSS (or loopback WS for development)");
|
|
480
448
|
}
|
|
481
449
|
if (observation.href !== expectedObservation.href) {
|
|
482
450
|
throw new SessionError("Aex returned an unsafe customer Environment observation URL");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aexhq/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.59.0",
|
|
4
4
|
"description": "TypeScript SDK for Aex sessions",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://aex.dev",
|
|
@@ -23,10 +23,6 @@
|
|
|
23
23
|
"types": "./dist/index.d.ts",
|
|
24
24
|
"import": "./dist/index.js"
|
|
25
25
|
},
|
|
26
|
-
"./internal": {
|
|
27
|
-
"types": "./dist/internal.d.ts",
|
|
28
|
-
"import": "./dist/internal.js"
|
|
29
|
-
},
|
|
30
26
|
"./package.json": "./package.json"
|
|
31
27
|
},
|
|
32
28
|
"files": [
|
|
@@ -38,8 +34,8 @@
|
|
|
38
34
|
"test": "tsc -p tsconfig.json && node --test"
|
|
39
35
|
},
|
|
40
36
|
"dependencies": {
|
|
41
|
-
"@aexhq/
|
|
42
|
-
"@aexhq/
|
|
37
|
+
"@aexhq/brain": "0.3.1",
|
|
38
|
+
"@aexhq/env-app": "0.2.0"
|
|
43
39
|
},
|
|
44
40
|
"peerDependencies": {
|
|
45
41
|
"zod": "4.4.3"
|
package/dist/internal.d.ts
DELETED
package/dist/internal.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { withPreparedArtifact } from "./tools.js";
|