@aithos/sdk 0.1.0-alpha.5 → 0.1.0-alpha.51
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 +245 -7
- package/dist/src/apps.d.ts +224 -0
- package/dist/src/apps.js +432 -0
- package/dist/src/assets.d.ts +209 -0
- package/dist/src/assets.js +534 -0
- package/dist/src/auth-api.d.ts +219 -0
- package/dist/src/auth-api.js +248 -0
- package/dist/src/auth.d.ts +543 -0
- package/dist/src/auth.js +937 -31
- package/dist/src/compute.d.ts +464 -6
- package/dist/src/compute.js +746 -20
- package/dist/src/data-schema-contacts-v1.d.ts +14 -0
- package/dist/src/data-schema-contacts-v1.js +28 -0
- package/dist/src/data.d.ts +342 -0
- package/dist/src/data.js +1002 -0
- package/dist/src/endpoints.d.ts +25 -0
- package/dist/src/endpoints.js +7 -0
- package/dist/src/ethos.d.ts +85 -0
- package/dist/src/ethos.js +463 -7
- package/dist/src/index.d.ts +17 -6
- package/dist/src/index.js +25 -3
- package/dist/src/internal/delegate-bundle.js +7 -2
- package/dist/src/internal/envelope.d.ts +93 -0
- package/dist/src/internal/envelope.js +59 -0
- package/dist/src/mandates.d.ts +111 -2
- package/dist/src/mandates.js +150 -7
- package/dist/src/react/AithosAsset.d.ts +66 -0
- package/dist/src/react/AithosAsset.js +67 -0
- package/dist/src/react/context.d.ts +29 -0
- package/dist/src/react/context.js +31 -0
- package/dist/src/react/index.d.ts +29 -0
- package/dist/src/react/index.js +31 -0
- package/dist/src/react/use-aithos-asset.d.ts +39 -0
- package/dist/src/react/use-aithos-asset.js +118 -0
- package/dist/src/react/use-transcribe-pending.d.ts +21 -0
- package/dist/src/react/use-transcribe-pending.js +47 -0
- package/dist/src/sdk.d.ts +10 -0
- package/dist/src/sdk.js +22 -0
- package/dist/src/transcribe-resilience.d.ts +57 -0
- package/dist/src/transcribe-resilience.js +203 -0
- package/dist/src/web.d.ts +279 -0
- package/dist/src/web.js +186 -0
- package/dist/test/auth-j3.test.js +32 -1
- package/dist/test/canonical-conformance.test.d.ts +2 -0
- package/dist/test/canonical-conformance.test.js +86 -0
- package/dist/test/compute-delegate-path.test.d.ts +2 -0
- package/dist/test/compute-delegate-path.test.js +183 -0
- package/dist/test/compute.test.js +4 -0
- package/dist/test/endpoints.test.js +30 -1
- package/dist/test/envelope-core-conformance.test.d.ts +2 -0
- package/dist/test/envelope-core-conformance.test.js +75 -0
- package/dist/test/envelope.test.d.ts +2 -0
- package/dist/test/envelope.test.js +318 -0
- package/dist/test/ethos-first-edition.test.d.ts +2 -0
- package/dist/test/ethos-first-edition.test.js +371 -0
- package/dist/test/mandates-compute.test.d.ts +2 -0
- package/dist/test/mandates-compute.test.js +256 -0
- package/dist/test/sdk.test.js +11 -2
- package/dist/test/signup-bootstrap.test.d.ts +2 -0
- package/dist/test/signup-bootstrap.test.js +311 -0
- package/dist/test/transcribe-invoke.test.d.ts +2 -0
- package/dist/test/transcribe-invoke.test.js +204 -0
- package/dist/test/transcribe.test.d.ts +2 -0
- package/dist/test/transcribe.test.js +186 -0
- package/dist/test/web.test.d.ts +2 -0
- package/dist/test/web.test.js +270 -0
- package/package.json +20 -3
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
// Tests for the alpha.7 first-edition path in EthosClient — the case
|
|
4
|
+
// where an Ethos identity exists on api.aithos.be (provisioned by
|
|
5
|
+
// auth.signUp() since alpha.6) but no edition has been published yet.
|
|
6
|
+
//
|
|
7
|
+
// Two flows must work:
|
|
8
|
+
// 1. Reading any zone returns an empty list (instead of throwing
|
|
9
|
+
// "not found: edition").
|
|
10
|
+
// 2. Publishing for the first time builds height=1 from staged
|
|
11
|
+
// mutations and POSTs publish_ethos_edition directly, instead
|
|
12
|
+
// of going through publishZoneEdit (which requires a previous
|
|
13
|
+
// manifest).
|
|
14
|
+
//
|
|
15
|
+
// We mock global fetch end-to-end so the tests run offline.
|
|
16
|
+
import { strict as assert } from "node:assert";
|
|
17
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
18
|
+
import { createBrowserIdentity } from "@aithos/protocol-client";
|
|
19
|
+
import { AithosAuth, AithosSDKError, EthosNamespace, memoryKeyStore, noopStore, } from "../src/index.js";
|
|
20
|
+
import { serializeRecoveryFile } from "../src/internal/recovery-file.js";
|
|
21
|
+
import { DEFAULT_SDK_ENDPOINTS } from "../src/endpoints.js";
|
|
22
|
+
let fetchCalls = [];
|
|
23
|
+
let savedFetch;
|
|
24
|
+
function installFetchMock(handlers) {
|
|
25
|
+
savedFetch = globalThis.fetch;
|
|
26
|
+
fetchCalls = [];
|
|
27
|
+
globalThis.fetch = (async (input, init) => {
|
|
28
|
+
const url = String(input);
|
|
29
|
+
const method = init?.method ?? "GET";
|
|
30
|
+
const bodyText = typeof init?.body === "string"
|
|
31
|
+
? init.body
|
|
32
|
+
: init?.body == null
|
|
33
|
+
? null
|
|
34
|
+
: String(init.body);
|
|
35
|
+
const body = bodyText ? JSON.parse(bodyText) : null;
|
|
36
|
+
const call = { url, method, body };
|
|
37
|
+
fetchCalls.push(call);
|
|
38
|
+
for (const h of handlers) {
|
|
39
|
+
if (!url.includes(h.url))
|
|
40
|
+
continue;
|
|
41
|
+
if (h.rpcMethod && body?.method !== h.rpcMethod)
|
|
42
|
+
continue;
|
|
43
|
+
const out = h.respond(call);
|
|
44
|
+
const status = out.status ?? 200;
|
|
45
|
+
return new Response(JSON.stringify(out.json), {
|
|
46
|
+
status,
|
|
47
|
+
headers: { "content-type": "application/json" },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`unhandled fetch: ${method} ${url} (rpc: ${body?.method ?? "n/a"})`);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function uninstallFetchMock() {
|
|
54
|
+
if (savedFetch) {
|
|
55
|
+
globalThis.fetch = savedFetch;
|
|
56
|
+
savedFetch = undefined;
|
|
57
|
+
}
|
|
58
|
+
fetchCalls = [];
|
|
59
|
+
}
|
|
60
|
+
function makeAuth() {
|
|
61
|
+
return new AithosAuth({
|
|
62
|
+
authBaseUrl: "https://auth.test",
|
|
63
|
+
apiBaseUrl: "https://api.test",
|
|
64
|
+
fetch: (() => {
|
|
65
|
+
throw new Error("AithosAuth.fetch must not be called in these tests");
|
|
66
|
+
}),
|
|
67
|
+
sessionStore: noopStore(),
|
|
68
|
+
keyStore: memoryKeyStore(),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function makeNamespace(auth) {
|
|
72
|
+
return new EthosNamespace({
|
|
73
|
+
auth,
|
|
74
|
+
endpoints: DEFAULT_SDK_ENDPOINTS,
|
|
75
|
+
// EthosNamespace itself doesn't read fetch from this slot today;
|
|
76
|
+
// protocol-client uses the global fetch we mock above.
|
|
77
|
+
fetch: globalThis.fetch.bind(globalThis),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
async function signInAsAlice(auth) {
|
|
81
|
+
const id = createBrowserIdentity("alice", "Alice");
|
|
82
|
+
const { text } = serializeRecoveryFile(id);
|
|
83
|
+
const info = await auth.signInWithRecovery({ file: text });
|
|
84
|
+
return { did: info.did };
|
|
85
|
+
}
|
|
86
|
+
function noEditionYetResponse() {
|
|
87
|
+
// Mirrors the server's primitives-read `notFound("edition for <did>")`:
|
|
88
|
+
// JSON-RPC error code -32020, message starts with "not found: edition for ".
|
|
89
|
+
return {
|
|
90
|
+
json: {
|
|
91
|
+
jsonrpc: "2.0",
|
|
92
|
+
id: "aithos.get_ethos_manifest",
|
|
93
|
+
error: {
|
|
94
|
+
code: -32020,
|
|
95
|
+
message: "not found: edition for did:aithos:zSomething",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function publishOkResponse() {
|
|
101
|
+
return {
|
|
102
|
+
json: {
|
|
103
|
+
jsonrpc: "2.0",
|
|
104
|
+
id: "publish_ethos_edition",
|
|
105
|
+
result: { ok: true, height: 1, manifest_uri: "s3://aithos/.../manifest.json" },
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/* -------------------------------------------------------------------------- */
|
|
110
|
+
/* Tests */
|
|
111
|
+
/* -------------------------------------------------------------------------- */
|
|
112
|
+
describe("EthosClient — fresh Ethos (no edition published yet)", () => {
|
|
113
|
+
beforeEach(() => {
|
|
114
|
+
fetchCalls = [];
|
|
115
|
+
});
|
|
116
|
+
afterEach(() => {
|
|
117
|
+
uninstallFetchMock();
|
|
118
|
+
});
|
|
119
|
+
it("zone(public).sections() returns [] when server says 'not found: edition'", async () => {
|
|
120
|
+
installFetchMock([
|
|
121
|
+
{
|
|
122
|
+
url: "/mcp/primitives/read",
|
|
123
|
+
rpcMethod: "aithos.get_ethos_manifest",
|
|
124
|
+
respond: noEditionYetResponse,
|
|
125
|
+
},
|
|
126
|
+
]);
|
|
127
|
+
const auth = makeAuth();
|
|
128
|
+
await signInAsAlice(auth);
|
|
129
|
+
const me = makeNamespace(auth).me();
|
|
130
|
+
const sections = await me.zone("public").sections();
|
|
131
|
+
assert.deepEqual(sections, []);
|
|
132
|
+
});
|
|
133
|
+
it("zone(public).sections() reflects locally staged adds even when no edition exists", async () => {
|
|
134
|
+
installFetchMock([
|
|
135
|
+
{
|
|
136
|
+
url: "/mcp/primitives/read",
|
|
137
|
+
rpcMethod: "aithos.get_ethos_manifest",
|
|
138
|
+
respond: noEditionYetResponse,
|
|
139
|
+
},
|
|
140
|
+
]);
|
|
141
|
+
const auth = makeAuth();
|
|
142
|
+
await signInAsAlice(auth);
|
|
143
|
+
const me = makeNamespace(auth).me();
|
|
144
|
+
me.zone("public").addSection({ title: "Hello", body: "World" });
|
|
145
|
+
const sections = await me.zone("public").sections();
|
|
146
|
+
assert.equal(sections.length, 1);
|
|
147
|
+
assert.equal(sections[0].title, "Hello");
|
|
148
|
+
});
|
|
149
|
+
it("publish() routes to publish_ethos_edition with height=1 on first publish", async () => {
|
|
150
|
+
let publishBody = null;
|
|
151
|
+
installFetchMock([
|
|
152
|
+
{
|
|
153
|
+
url: "/mcp/primitives/read",
|
|
154
|
+
rpcMethod: "aithos.get_ethos_manifest",
|
|
155
|
+
respond: noEditionYetResponse,
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
url: "/mcp/primitives/write",
|
|
159
|
+
rpcMethod: "aithos.publish_ethos_edition",
|
|
160
|
+
respond: (call) => {
|
|
161
|
+
publishBody = call.body;
|
|
162
|
+
return publishOkResponse();
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
]);
|
|
166
|
+
const auth = makeAuth();
|
|
167
|
+
const alice = await signInAsAlice(auth);
|
|
168
|
+
const me = makeNamespace(auth).me();
|
|
169
|
+
me.zone("public").addSection({ title: "First", body: "Hello." });
|
|
170
|
+
me.zone("public").addSection({ title: "Second", body: "World." });
|
|
171
|
+
const r = await me.publish();
|
|
172
|
+
assert.equal(r.editionHeight, 1);
|
|
173
|
+
assert.equal(r.subjectDid, alice.did);
|
|
174
|
+
assert.deepEqual(r.zonesPublished, ["public"]);
|
|
175
|
+
// Verify the wire shape: JSON-RPC publish_ethos_edition with a height=1
|
|
176
|
+
// manifest containing both staged sections.
|
|
177
|
+
assert.equal(publishBody.method, "aithos.publish_ethos_edition");
|
|
178
|
+
const manifest = publishBody.params.manifest;
|
|
179
|
+
assert.equal(manifest.edition.height, 1);
|
|
180
|
+
assert.equal(manifest.edition.prev_hash, null);
|
|
181
|
+
assert.equal(manifest.edition.supersedes, null);
|
|
182
|
+
assert.deepEqual(manifest.zones.public.section_titles, ["First", "Second"]);
|
|
183
|
+
// Envelope is signed under #public.
|
|
184
|
+
const env = publishBody.params._envelope;
|
|
185
|
+
assert.equal(env.method, "aithos.publish_ethos_edition");
|
|
186
|
+
assert.match(env.proof.verificationMethod, /#public$/);
|
|
187
|
+
});
|
|
188
|
+
it("publish() accepts circle mutations on first edition with auto-injected public sentinel", async () => {
|
|
189
|
+
// Regression target: this used to throw `ethos_first_edition_public_only`.
|
|
190
|
+
// Since aithos-sdk@0.1.0-alpha.43 + protocol-client@0.1.0-alpha.14, the
|
|
191
|
+
// SDK auto-injects an `aithos-init` public section and seals the circle
|
|
192
|
+
// sections in the same height=1 manifest.
|
|
193
|
+
let publishBody = null;
|
|
194
|
+
installFetchMock([
|
|
195
|
+
{
|
|
196
|
+
url: "/mcp/primitives/read",
|
|
197
|
+
rpcMethod: "aithos.get_ethos_manifest",
|
|
198
|
+
respond: noEditionYetResponse,
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
url: "/mcp/primitives/write",
|
|
202
|
+
rpcMethod: "aithos.publish_ethos_edition",
|
|
203
|
+
respond: (call) => {
|
|
204
|
+
publishBody = call.body;
|
|
205
|
+
return publishOkResponse();
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
]);
|
|
209
|
+
const auth = makeAuth();
|
|
210
|
+
await signInAsAlice(auth);
|
|
211
|
+
const me = makeNamespace(auth).me();
|
|
212
|
+
me.zone("circle").addSection({ title: "Private", body: "..." });
|
|
213
|
+
const r = await me.publish();
|
|
214
|
+
assert.equal(r.editionHeight, 1);
|
|
215
|
+
assert.deepEqual(r.zonesPublished, ["public", "circle"]);
|
|
216
|
+
const manifest = publishBody.params.manifest;
|
|
217
|
+
assert.equal(manifest.edition.height, 1);
|
|
218
|
+
assert.equal(manifest.edition.prev_hash, null);
|
|
219
|
+
// Auto-injected sentinel.
|
|
220
|
+
assert.deepEqual(manifest.zones.public.section_titles, ["aithos-init"]);
|
|
221
|
+
assert.equal(manifest.zones.public.encrypted, false);
|
|
222
|
+
// Sealed circle zone.
|
|
223
|
+
assert.deepEqual(manifest.zones.circle.section_titles, ["Private"]);
|
|
224
|
+
assert.equal(manifest.zones.circle.encrypted, true);
|
|
225
|
+
assert.ok(manifest.zones.circle.cipher, "circle cipher must be present");
|
|
226
|
+
// Both zones uploaded.
|
|
227
|
+
assert.ok(publishBody.params.zones.public?.bytes_base64);
|
|
228
|
+
assert.ok(publishBody.params.zones.circle?.bytes_base64);
|
|
229
|
+
assert.equal(publishBody.params.zones.self, undefined);
|
|
230
|
+
});
|
|
231
|
+
it("publish() preserves the caller's explicit public section when mixed with circle", async () => {
|
|
232
|
+
// When the caller stages BOTH a public and a circle add, the SDK
|
|
233
|
+
// must NOT inject a sentinel — the user's public section is enough.
|
|
234
|
+
let publishBody = null;
|
|
235
|
+
installFetchMock([
|
|
236
|
+
{
|
|
237
|
+
url: "/mcp/primitives/read",
|
|
238
|
+
rpcMethod: "aithos.get_ethos_manifest",
|
|
239
|
+
respond: noEditionYetResponse,
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
url: "/mcp/primitives/write",
|
|
243
|
+
rpcMethod: "aithos.publish_ethos_edition",
|
|
244
|
+
respond: (call) => {
|
|
245
|
+
publishBody = call.body;
|
|
246
|
+
return publishOkResponse();
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
]);
|
|
250
|
+
const auth = makeAuth();
|
|
251
|
+
await signInAsAlice(auth);
|
|
252
|
+
const me = makeNamespace(auth).me();
|
|
253
|
+
me.zone("public").addSection({ title: "About", body: "Public bio." });
|
|
254
|
+
me.zone("circle").addSection({ title: "Notes", body: "Private notes." });
|
|
255
|
+
const r = await me.publish();
|
|
256
|
+
assert.deepEqual(r.zonesPublished, ["public", "circle"]);
|
|
257
|
+
const manifest = publishBody.params.manifest;
|
|
258
|
+
// No sentinel — the user's section is the public titles entry.
|
|
259
|
+
assert.deepEqual(manifest.zones.public.section_titles, ["About"]);
|
|
260
|
+
assert.deepEqual(manifest.zones.circle.section_titles, ["Notes"]);
|
|
261
|
+
});
|
|
262
|
+
it("publish() auto-injects public sentinel when only self mutations are staged", async () => {
|
|
263
|
+
let publishBody = null;
|
|
264
|
+
installFetchMock([
|
|
265
|
+
{
|
|
266
|
+
url: "/mcp/primitives/read",
|
|
267
|
+
rpcMethod: "aithos.get_ethos_manifest",
|
|
268
|
+
respond: noEditionYetResponse,
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
url: "/mcp/primitives/write",
|
|
272
|
+
rpcMethod: "aithos.publish_ethos_edition",
|
|
273
|
+
respond: (call) => {
|
|
274
|
+
publishBody = call.body;
|
|
275
|
+
return publishOkResponse();
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
]);
|
|
279
|
+
const auth = makeAuth();
|
|
280
|
+
await signInAsAlice(auth);
|
|
281
|
+
const me = makeNamespace(auth).me();
|
|
282
|
+
me.zone("self").addSection({ title: "Journal", body: "Private." });
|
|
283
|
+
const r = await me.publish();
|
|
284
|
+
assert.deepEqual(r.zonesPublished, ["public", "self"]);
|
|
285
|
+
const manifest = publishBody.params.manifest;
|
|
286
|
+
assert.deepEqual(manifest.zones.public.section_titles, ["aithos-init"]);
|
|
287
|
+
assert.deepEqual(manifest.zones.self.section_titles, ["Journal"]);
|
|
288
|
+
assert.equal(manifest.zones.self.encrypted, true);
|
|
289
|
+
assert.equal(manifest.zones.circle, undefined);
|
|
290
|
+
});
|
|
291
|
+
it("publish() lands public + circle + self in a single height=1 edition", async () => {
|
|
292
|
+
let publishBody = null;
|
|
293
|
+
installFetchMock([
|
|
294
|
+
{
|
|
295
|
+
url: "/mcp/primitives/read",
|
|
296
|
+
rpcMethod: "aithos.get_ethos_manifest",
|
|
297
|
+
respond: noEditionYetResponse,
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
url: "/mcp/primitives/write",
|
|
301
|
+
rpcMethod: "aithos.publish_ethos_edition",
|
|
302
|
+
respond: (call) => {
|
|
303
|
+
publishBody = call.body;
|
|
304
|
+
return publishOkResponse();
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
]);
|
|
308
|
+
const auth = makeAuth();
|
|
309
|
+
await signInAsAlice(auth);
|
|
310
|
+
const me = makeNamespace(auth).me();
|
|
311
|
+
me.zone("public").addSection({ title: "Bio", body: "Bio body." });
|
|
312
|
+
me.zone("circle").addSection({ title: "Circle", body: "Circle body." });
|
|
313
|
+
me.zone("self").addSection({ title: "Self", body: "Self body." });
|
|
314
|
+
const r = await me.publish();
|
|
315
|
+
assert.equal(r.editionHeight, 1);
|
|
316
|
+
assert.deepEqual(r.zonesPublished, ["public", "circle", "self"]);
|
|
317
|
+
const manifest = publishBody.params.manifest;
|
|
318
|
+
assert.deepEqual(manifest.zones.public.section_titles, ["Bio"]);
|
|
319
|
+
assert.deepEqual(manifest.zones.circle.section_titles, ["Circle"]);
|
|
320
|
+
assert.deepEqual(manifest.zones.self.section_titles, ["Self"]);
|
|
321
|
+
assert.ok(publishBody.params.zones.public?.bytes_base64);
|
|
322
|
+
assert.ok(publishBody.params.zones.circle?.bytes_base64);
|
|
323
|
+
assert.ok(publishBody.params.zones.self?.bytes_base64);
|
|
324
|
+
});
|
|
325
|
+
it("publish() rejects update/delete operations on a fresh Ethos", async () => {
|
|
326
|
+
installFetchMock([
|
|
327
|
+
{
|
|
328
|
+
url: "/mcp/primitives/read",
|
|
329
|
+
rpcMethod: "aithos.get_ethos_manifest",
|
|
330
|
+
respond: noEditionYetResponse,
|
|
331
|
+
},
|
|
332
|
+
]);
|
|
333
|
+
const auth = makeAuth();
|
|
334
|
+
await signInAsAlice(auth);
|
|
335
|
+
const me = makeNamespace(auth).me();
|
|
336
|
+
// Stage a delete for a section that doesn't exist (no edition exists at all).
|
|
337
|
+
me.zone("public")["_parent"]; // type-safety placeholder; we use the public API
|
|
338
|
+
// EthosZone exposes deleteSection — go via that.
|
|
339
|
+
me.zone("public").deleteSection("sec_doesnotexist000");
|
|
340
|
+
await assert.rejects(() => me.publish(), (e) => e instanceof AithosSDKError && e.code === "ethos_first_edition_invalid_op");
|
|
341
|
+
});
|
|
342
|
+
it("publish() surfaces server JSON-RPC errors as ethos_first_edition_rejected", async () => {
|
|
343
|
+
installFetchMock([
|
|
344
|
+
{
|
|
345
|
+
url: "/mcp/primitives/read",
|
|
346
|
+
rpcMethod: "aithos.get_ethos_manifest",
|
|
347
|
+
respond: noEditionYetResponse,
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
url: "/mcp/primitives/write",
|
|
351
|
+
rpcMethod: "aithos.publish_ethos_edition",
|
|
352
|
+
respond: () => ({
|
|
353
|
+
json: {
|
|
354
|
+
jsonrpc: "2.0",
|
|
355
|
+
id: "publish_ethos_edition",
|
|
356
|
+
error: {
|
|
357
|
+
code: -32020,
|
|
358
|
+
message: "subject identity not published (call publish_identity first)",
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
}),
|
|
362
|
+
},
|
|
363
|
+
]);
|
|
364
|
+
const auth = makeAuth();
|
|
365
|
+
await signInAsAlice(auth);
|
|
366
|
+
const me = makeNamespace(auth).me();
|
|
367
|
+
me.zone("public").addSection({ title: "Hi", body: "There." });
|
|
368
|
+
await assert.rejects(() => me.publish(), (e) => e instanceof AithosSDKError && e.code === "ethos_first_edition_rejected");
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
//# sourceMappingURL=ethos-first-edition.test.js.map
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
/**
|
|
4
|
+
* Tests for the `compute` namespace on `MandatesNamespace.create`.
|
|
5
|
+
*
|
|
6
|
+
* Invariants under test (mirror of the protocol-core v0.4.0 invariants,
|
|
7
|
+
* enforced at the SDK boundary so callers fail fast with a precise
|
|
8
|
+
* error rather than only blowing up server-side):
|
|
9
|
+
*
|
|
10
|
+
* 1. Passing `compute.invoke` directly in `scopes[]` is rejected.
|
|
11
|
+
* The opt-in must go through the typed `compute` namespace, which
|
|
12
|
+
* is what a consent UI can review.
|
|
13
|
+
* 2. The `compute` namespace requires at least one of
|
|
14
|
+
* `dailyCapMicrocredits` or `totalCapMicrocredits` — an unbounded
|
|
15
|
+
* compute mandate is the bearer-token footgun this whole namespace
|
|
16
|
+
* exists to prevent.
|
|
17
|
+
* 3. Numeric caps must be positive integers.
|
|
18
|
+
* 4. `allowedModels`, when present, must be an array of non-empty
|
|
19
|
+
* strings.
|
|
20
|
+
* 5. A compute-only mandate (`scopes: []` + `compute: {…}`) is
|
|
21
|
+
* legitimate and accepted — the grantee gets no ethos data
|
|
22
|
+
* access, only bounded compute spending. Empty scopes WITHOUT
|
|
23
|
+
* compute is still rejected.
|
|
24
|
+
*
|
|
25
|
+
* Network-dependent paths (real mint, list, revoke) ride on the same
|
|
26
|
+
* integration suite as the rest of the namespace — see the note in
|
|
27
|
+
* `mandates.test.ts`. We intentionally don't go through `mintDelegateBundle`
|
|
28
|
+
* here; failures fire BEFORE that boundary.
|
|
29
|
+
*/
|
|
30
|
+
import { strict as assert } from "node:assert";
|
|
31
|
+
import { describe, it } from "node:test";
|
|
32
|
+
import { createBrowserIdentity } from "@aithos/protocol-client";
|
|
33
|
+
import { AithosAuth, AithosSDKError, COMPUTE_INVOKE_SCOPE, MandatesNamespace, memoryKeyStore, noopStore, } from "../src/index.js";
|
|
34
|
+
import { DEFAULT_SDK_ENDPOINTS } from "../src/endpoints.js";
|
|
35
|
+
import { serializeRecoveryFile } from "../src/internal/recovery-file.js";
|
|
36
|
+
function makeAuth() {
|
|
37
|
+
return new AithosAuth({
|
|
38
|
+
authBaseUrl: "https://auth.test",
|
|
39
|
+
fetch: (() => {
|
|
40
|
+
throw new Error("network not expected");
|
|
41
|
+
}),
|
|
42
|
+
sessionStore: noopStore(),
|
|
43
|
+
keyStore: memoryKeyStore(),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function makeMandates(auth) {
|
|
47
|
+
return new MandatesNamespace({
|
|
48
|
+
auth,
|
|
49
|
+
endpoints: DEFAULT_SDK_ENDPOINTS,
|
|
50
|
+
fetch: globalThis.fetch.bind(globalThis),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async function signInAsAlice(auth) {
|
|
54
|
+
const id = createBrowserIdentity("alice", "Alice");
|
|
55
|
+
const { text } = serializeRecoveryFile(id);
|
|
56
|
+
const info = await auth.signInWithRecovery({ file: text });
|
|
57
|
+
return info.did;
|
|
58
|
+
}
|
|
59
|
+
/* -------------------------------------------------------------------------- */
|
|
60
|
+
/* Constant export sanity */
|
|
61
|
+
/* -------------------------------------------------------------------------- */
|
|
62
|
+
describe("COMPUTE_INVOKE_SCOPE constant", () => {
|
|
63
|
+
it("is the canonical 'compute.invoke' string", () => {
|
|
64
|
+
assert.equal(COMPUTE_INVOKE_SCOPE, "compute.invoke");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
/* -------------------------------------------------------------------------- */
|
|
68
|
+
/* Smuggling guard: compute.invoke in scopes[] is rejected */
|
|
69
|
+
/* -------------------------------------------------------------------------- */
|
|
70
|
+
describe("MandatesNamespace.create — compute.invoke smuggling guard", () => {
|
|
71
|
+
it("rejects a mandate that smuggles compute.invoke into scopes[]", async () => {
|
|
72
|
+
const auth = makeAuth();
|
|
73
|
+
await signInAsAlice(auth);
|
|
74
|
+
const m = makeMandates(auth);
|
|
75
|
+
await assert.rejects(() => m.create({
|
|
76
|
+
granteeId: "urn:agent:bob",
|
|
77
|
+
// Up-cast through string[] — this is exactly the path the
|
|
78
|
+
// runtime check has to catch. Type-checking can't (the union
|
|
79
|
+
// doesn't include "compute.invoke", but cast slips through).
|
|
80
|
+
scopes: ["ethos.read.public", "compute.invoke"],
|
|
81
|
+
ttlSeconds: 3600,
|
|
82
|
+
}), (e) => e instanceof AithosSDKError &&
|
|
83
|
+
e.code === "mandates_invalid_scopes" &&
|
|
84
|
+
/compute' namespace/.test(e.message));
|
|
85
|
+
});
|
|
86
|
+
it("rejects even when 'compute.invoke' is the ONLY scope passed", async () => {
|
|
87
|
+
const auth = makeAuth();
|
|
88
|
+
await signInAsAlice(auth);
|
|
89
|
+
const m = makeMandates(auth);
|
|
90
|
+
await assert.rejects(() => m.create({
|
|
91
|
+
granteeId: "urn:agent:bob",
|
|
92
|
+
scopes: ["compute.invoke"],
|
|
93
|
+
ttlSeconds: 3600,
|
|
94
|
+
}), (e) => e instanceof AithosSDKError &&
|
|
95
|
+
e.code === "mandates_invalid_scopes");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
/* -------------------------------------------------------------------------- */
|
|
99
|
+
/* Compute namespace — input validation */
|
|
100
|
+
/* -------------------------------------------------------------------------- */
|
|
101
|
+
describe("MandatesNamespace.create — compute namespace validation", () => {
|
|
102
|
+
it("rejects compute namespace with no caps at all", async () => {
|
|
103
|
+
const auth = makeAuth();
|
|
104
|
+
await signInAsAlice(auth);
|
|
105
|
+
const m = makeMandates(auth);
|
|
106
|
+
await assert.rejects(() => m.create({
|
|
107
|
+
granteeId: "urn:agent:bob",
|
|
108
|
+
scopes: ["ethos.read.public"],
|
|
109
|
+
ttlSeconds: 3600,
|
|
110
|
+
compute: {}, // empty — neither daily nor total cap
|
|
111
|
+
}), (e) => e instanceof AithosSDKError &&
|
|
112
|
+
e.code === "mandates_invalid_compute" &&
|
|
113
|
+
/at least one of dailyCapMicrocredits or totalCapMicrocredits/.test(e.message));
|
|
114
|
+
});
|
|
115
|
+
it("rejects compute namespace with only allowedModels (no cap)", async () => {
|
|
116
|
+
const auth = makeAuth();
|
|
117
|
+
await signInAsAlice(auth);
|
|
118
|
+
const m = makeMandates(auth);
|
|
119
|
+
await assert.rejects(() => m.create({
|
|
120
|
+
granteeId: "urn:agent:bob",
|
|
121
|
+
scopes: ["ethos.read.public"],
|
|
122
|
+
ttlSeconds: 3600,
|
|
123
|
+
compute: { allowedModels: ["claude-haiku-4-5"] },
|
|
124
|
+
}), (e) => e instanceof AithosSDKError &&
|
|
125
|
+
e.code === "mandates_invalid_compute");
|
|
126
|
+
});
|
|
127
|
+
it("rejects non-positive caps", async () => {
|
|
128
|
+
const auth = makeAuth();
|
|
129
|
+
await signInAsAlice(auth);
|
|
130
|
+
const m = makeMandates(auth);
|
|
131
|
+
for (const bad of [0, -1, 3.14]) {
|
|
132
|
+
await assert.rejects(() => m.create({
|
|
133
|
+
granteeId: "urn:agent:bob",
|
|
134
|
+
scopes: ["ethos.read.public"],
|
|
135
|
+
ttlSeconds: 3600,
|
|
136
|
+
compute: { dailyCapMicrocredits: bad },
|
|
137
|
+
}), (e) => e instanceof AithosSDKError &&
|
|
138
|
+
e.code === "mandates_invalid_compute" &&
|
|
139
|
+
/must be a positive integer/.test(e.message), `expected rejection for dailyCapMicrocredits=${bad}`);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
it("rejects malformed allowedModels (empty string entry)", async () => {
|
|
143
|
+
const auth = makeAuth();
|
|
144
|
+
await signInAsAlice(auth);
|
|
145
|
+
const m = makeMandates(auth);
|
|
146
|
+
await assert.rejects(() => m.create({
|
|
147
|
+
granteeId: "urn:agent:bob",
|
|
148
|
+
scopes: ["ethos.read.public"],
|
|
149
|
+
ttlSeconds: 3600,
|
|
150
|
+
compute: {
|
|
151
|
+
dailyCapMicrocredits: 5_000,
|
|
152
|
+
allowedModels: ["claude-haiku-4-5", ""],
|
|
153
|
+
},
|
|
154
|
+
}), (e) => e instanceof AithosSDKError &&
|
|
155
|
+
e.code === "mandates_invalid_compute" &&
|
|
156
|
+
/allowedModels entries must be non-empty strings/.test(e.message));
|
|
157
|
+
});
|
|
158
|
+
it("rejects allowedModels that is not an array", async () => {
|
|
159
|
+
const auth = makeAuth();
|
|
160
|
+
await signInAsAlice(auth);
|
|
161
|
+
const m = makeMandates(auth);
|
|
162
|
+
await assert.rejects(() => m.create({
|
|
163
|
+
granteeId: "urn:agent:bob",
|
|
164
|
+
scopes: ["ethos.read.public"],
|
|
165
|
+
ttlSeconds: 3600,
|
|
166
|
+
compute: {
|
|
167
|
+
dailyCapMicrocredits: 5_000,
|
|
168
|
+
// @ts-expect-error — runtime check guards against bad casts
|
|
169
|
+
allowedModels: "claude-haiku-4-5",
|
|
170
|
+
},
|
|
171
|
+
}), (e) => e instanceof AithosSDKError &&
|
|
172
|
+
e.code === "mandates_invalid_compute");
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
/* -------------------------------------------------------------------------- */
|
|
176
|
+
/* Read-only mandate is unaffected */
|
|
177
|
+
/* -------------------------------------------------------------------------- */
|
|
178
|
+
describe("MandatesNamespace.create — read-only mandates unaffected", () => {
|
|
179
|
+
it("does NOT reject a clean read-only mandate (no compute, no smuggling)", async () => {
|
|
180
|
+
// We can't run the full mint here because the installed
|
|
181
|
+
// @aithos/protocol-core (0.5.1, pre-0.4.0-mandate) doesn't yet
|
|
182
|
+
// accept compute.invoke on the public sphere whitelist — the real
|
|
183
|
+
// mint would still succeed for a read-only mandate, but it goes
|
|
184
|
+
// out to S3 etc. Instead we check the synchronous validation
|
|
185
|
+
// surface: passing a clean read-only input must not throw before
|
|
186
|
+
// the mint call.
|
|
187
|
+
//
|
|
188
|
+
// (The async `m.create(...)` would still error later on the
|
|
189
|
+
// network path — that's the integration suite's job.)
|
|
190
|
+
//
|
|
191
|
+
// What we ARE asserting here: no SDK-side validation rejection.
|
|
192
|
+
const auth = makeAuth();
|
|
193
|
+
await signInAsAlice(auth);
|
|
194
|
+
const m = makeMandates(auth);
|
|
195
|
+
// The mint will fail with a network error (fetch throws), but
|
|
196
|
+
// crucially NOT with a validation error — that proves our compute
|
|
197
|
+
// guard is precisely scoped to compute-related inputs.
|
|
198
|
+
await assert.rejects(() => m.create({
|
|
199
|
+
granteeId: "urn:viewer:bob",
|
|
200
|
+
scopes: ["ethos.read.public"],
|
|
201
|
+
ttlSeconds: 3600,
|
|
202
|
+
}), (e) => {
|
|
203
|
+
if (!(e instanceof AithosSDKError))
|
|
204
|
+
return true; // any non-validation error fine
|
|
205
|
+
const code = e.code;
|
|
206
|
+
// Anything except a validation rejection is fine — the network
|
|
207
|
+
// is not wired up in this fake auth.
|
|
208
|
+
return (code !== "mandates_invalid_scopes" &&
|
|
209
|
+
code !== "mandates_invalid_compute");
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
/* -------------------------------------------------------------------------- */
|
|
214
|
+
/* Compute-only mandate (scopes: [] + compute namespace) */
|
|
215
|
+
/* -------------------------------------------------------------------------- */
|
|
216
|
+
describe("MandatesNamespace.create — compute-only mandate", () => {
|
|
217
|
+
it("accepts an empty scopes[] when compute is provided (no ethos data access, just bounded spending)", async () => {
|
|
218
|
+
// Same caveat as the "read-only unaffected" suite: the installed
|
|
219
|
+
// @aithos/protocol-core does not yet whitelist compute.invoke on
|
|
220
|
+
// the public sphere, so the real `mintDelegateBundle` call would
|
|
221
|
+
// still reject downstream. What this test proves is that the
|
|
222
|
+
// SDK validation layer (a) does NOT throw `mandates_invalid_scopes`
|
|
223
|
+
// for the empty list when compute is set, and (b) does NOT throw
|
|
224
|
+
// `mandates_invalid_compute` for a properly-capped budget. The
|
|
225
|
+
// request is allowed to proceed past validation.
|
|
226
|
+
const auth = makeAuth();
|
|
227
|
+
await signInAsAlice(auth);
|
|
228
|
+
const m = makeMandates(auth);
|
|
229
|
+
await assert.rejects(() => m.create({
|
|
230
|
+
granteeId: "urn:agent:creative",
|
|
231
|
+
scopes: [], // no ethos data access
|
|
232
|
+
ttlSeconds: 3600,
|
|
233
|
+
compute: { dailyCapMicrocredits: 1_000 },
|
|
234
|
+
}), (e) => {
|
|
235
|
+
if (!(e instanceof AithosSDKError))
|
|
236
|
+
return true;
|
|
237
|
+
const code = e.code;
|
|
238
|
+
// Validation must NOT have rejected this input.
|
|
239
|
+
return (code !== "mandates_invalid_scopes" &&
|
|
240
|
+
code !== "mandates_invalid_compute");
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
it("STILL rejects empty scopes[] when compute is NOT provided", async () => {
|
|
244
|
+
const auth = makeAuth();
|
|
245
|
+
await signInAsAlice(auth);
|
|
246
|
+
const m = makeMandates(auth);
|
|
247
|
+
await assert.rejects(() => m.create({
|
|
248
|
+
granteeId: "urn:agent:bob",
|
|
249
|
+
scopes: [],
|
|
250
|
+
ttlSeconds: 3600,
|
|
251
|
+
}), (e) => e instanceof AithosSDKError &&
|
|
252
|
+
e.code === "mandates_invalid_scopes" &&
|
|
253
|
+
/compute-only mandate/.test(e.message));
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
//# sourceMappingURL=mandates-compute.test.js.map
|
package/dist/test/sdk.test.js
CHANGED
|
@@ -33,6 +33,9 @@ describe("DEFAULT_SDK_ENDPOINTS", () => {
|
|
|
33
33
|
it("points at the production Aithos hosts", () => {
|
|
34
34
|
assert.equal(DEFAULT_SDK_ENDPOINTS.compute, "https://compute.aithos.be");
|
|
35
35
|
assert.equal(DEFAULT_SDK_ENDPOINTS.wallet, "https://wallet.aithos.be");
|
|
36
|
+
assert.equal(DEFAULT_SDK_ENDPOINTS.web, "https://extract.aithos.be");
|
|
37
|
+
assert.equal(DEFAULT_SDK_ENDPOINTS.pds, "https://pds.aithos.be");
|
|
38
|
+
assert.equal(DEFAULT_SDK_ENDPOINTS.assets, "https://assets.aithos.be");
|
|
36
39
|
});
|
|
37
40
|
});
|
|
38
41
|
describe("AithosSDK constructor", () => {
|
|
@@ -101,7 +104,12 @@ describe("AithosSDK constructor", () => {
|
|
|
101
104
|
assert.equal(typeof sdk.ethos.me, "function");
|
|
102
105
|
assert.equal(typeof sdk.mandates.create, "function");
|
|
103
106
|
});
|
|
104
|
-
it("compute.invokeBedrock rejects when no owner
|
|
107
|
+
it("compute.invokeBedrock rejects when no owner AND no delegate matches the mandate", async () => {
|
|
108
|
+
// Since alpha.9, invokeBedrock supports a delegate signing path. The
|
|
109
|
+
// failure mode here (no owner loaded AND no imported mandate matching
|
|
110
|
+
// the requested id) returns code `sdk_no_delegate_for_mandate`.
|
|
111
|
+
// The previous `sdk_no_owner` code now applies only to other entry
|
|
112
|
+
// points where there's no delegate fallback.
|
|
105
113
|
const auth = makeAuth();
|
|
106
114
|
const sdk = new AithosSDK({
|
|
107
115
|
auth,
|
|
@@ -114,7 +122,8 @@ describe("AithosSDK constructor", () => {
|
|
|
114
122
|
mandateId: "mandate:x",
|
|
115
123
|
model: "claude-sonnet-4-6",
|
|
116
124
|
messages: [{ role: "user", content: "hi" }],
|
|
117
|
-
}), (e) => e instanceof AithosSDKError &&
|
|
125
|
+
}), (e) => e instanceof AithosSDKError &&
|
|
126
|
+
e.code === "sdk_no_delegate_for_mandate");
|
|
118
127
|
});
|
|
119
128
|
});
|
|
120
129
|
//# sourceMappingURL=sdk.test.js.map
|