@aithos/sdk 0.1.0-alpha.4 → 0.1.0-alpha.5
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/dist/src/auth.d.ts +94 -136
- package/dist/src/auth.js +440 -159
- package/dist/src/compute.d.ts +8 -6
- package/dist/src/compute.js +19 -11
- package/dist/src/ethos.d.ts +117 -1
- package/dist/src/ethos.js +417 -16
- package/dist/src/index.d.ts +7 -4
- package/dist/src/index.js +17 -5
- package/dist/src/internal/delegate-bundle.d.ts +18 -0
- package/dist/src/internal/delegate-bundle.js +89 -0
- package/dist/src/internal/delegate-state.d.ts +45 -0
- package/dist/src/internal/delegate-state.js +120 -0
- package/dist/src/internal/owner-signers.d.ts +78 -0
- package/dist/src/internal/owner-signers.js +179 -0
- package/dist/src/internal/protocol-client-bridge.d.ts +8 -0
- package/dist/src/internal/protocol-client-bridge.js +20 -0
- package/dist/src/internal/recovery-file.d.ts +29 -0
- package/dist/src/internal/recovery-file.js +98 -0
- package/dist/src/internal/signer.d.ts +59 -0
- package/dist/src/internal/signer.js +86 -0
- package/dist/src/key-store.d.ts +128 -0
- package/dist/src/key-store.js +244 -0
- package/dist/src/mandates.d.ts +88 -1
- package/dist/src/mandates.js +185 -8
- package/dist/src/sdk.d.ts +36 -3
- package/dist/src/sdk.js +27 -23
- package/dist/src/wallet.d.ts +4 -6
- package/dist/src/wallet.js +18 -8
- package/dist/test/auth-j3.test.d.ts +2 -0
- package/dist/test/auth-j3.test.js +360 -0
- package/dist/test/compute.test.js +22 -11
- package/dist/test/ethos.test.d.ts +2 -0
- package/dist/test/ethos.test.js +219 -0
- package/dist/test/key-store.test.d.ts +2 -0
- package/dist/test/key-store.test.js +161 -0
- package/dist/test/mandates.test.d.ts +2 -0
- package/dist/test/mandates.test.js +93 -0
- package/dist/test/sdk.test.js +64 -30
- package/dist/test/signer.test.d.ts +2 -0
- package/dist/test/signer.test.js +117 -0
- package/dist/test/wallet.test.js +20 -9
- package/package.json +2 -1
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
// Tests for J3 — KeyStore integration, signInWithRecovery,
|
|
4
|
+
// importMandate, resume(), signOut(), state accessors.
|
|
5
|
+
import { strict as assert } from "node:assert";
|
|
6
|
+
import { describe, it } from "node:test";
|
|
7
|
+
import { createBrowserIdentity } from "@aithos/protocol-client";
|
|
8
|
+
import { AithosAuth, AithosSDKError, memoryKeyStore, noopStore, } from "../src/index.js";
|
|
9
|
+
import { parseDelegateBundle, readDelegateBundleText, } from "../src/internal/delegate-bundle.js";
|
|
10
|
+
import { parseRecoveryFile, serializeRecoveryFile, } from "../src/internal/recovery-file.js";
|
|
11
|
+
/* -------------------------------------------------------------------------- */
|
|
12
|
+
/* Test helpers */
|
|
13
|
+
/* -------------------------------------------------------------------------- */
|
|
14
|
+
function makeAuth(opts = {}) {
|
|
15
|
+
return new AithosAuth({
|
|
16
|
+
authBaseUrl: "https://auth.test",
|
|
17
|
+
fetch: (() => {
|
|
18
|
+
throw new Error("network not expected in this test");
|
|
19
|
+
}),
|
|
20
|
+
sessionStore: opts.sessionStore ?? noopStore(),
|
|
21
|
+
keyStore: opts.keyStore ?? memoryKeyStore(),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** Build a recovery-file JSON string from a fresh BrowserIdentity. */
|
|
25
|
+
function recoveryTextFor(handle, displayName) {
|
|
26
|
+
const id = createBrowserIdentity(handle, displayName);
|
|
27
|
+
const { text } = serializeRecoveryFile(id);
|
|
28
|
+
return { text, did: id.did };
|
|
29
|
+
}
|
|
30
|
+
function delegateBundleText(args) {
|
|
31
|
+
return JSON.stringify({
|
|
32
|
+
aithos_delegate_version: "0.1.0",
|
|
33
|
+
mandate: {
|
|
34
|
+
id: args.mandateId,
|
|
35
|
+
subject_did: args.subjectDid,
|
|
36
|
+
grantee: {
|
|
37
|
+
id: args.granteeId,
|
|
38
|
+
pubkey: args.granteePubkeyMultibase ?? "z6MkqGenericPubKey",
|
|
39
|
+
},
|
|
40
|
+
scopes: args.scopes ?? ["ethos.read.public"],
|
|
41
|
+
},
|
|
42
|
+
delegate_seed_hex: "11".repeat(32),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/* -------------------------------------------------------------------------- */
|
|
46
|
+
/* parseRecoveryFile / serializeRecoveryFile */
|
|
47
|
+
/* -------------------------------------------------------------------------- */
|
|
48
|
+
describe("recovery file: parse + serialize", () => {
|
|
49
|
+
it("round-trips a fresh identity", () => {
|
|
50
|
+
const id = createBrowserIdentity("alice", "Alice");
|
|
51
|
+
const { text } = serializeRecoveryFile(id);
|
|
52
|
+
const parsed = parseRecoveryFile(text);
|
|
53
|
+
assert.equal(parsed.did, id.did);
|
|
54
|
+
assert.equal(parsed.handle, id.handle);
|
|
55
|
+
assert.equal(parsed.displayName, id.displayName);
|
|
56
|
+
assert.equal(parsed.seedsHex.root.length, 64);
|
|
57
|
+
});
|
|
58
|
+
it("accepts the runOnboarding shape (warning + created_at)", () => {
|
|
59
|
+
const id = createBrowserIdentity("bob", "Bob");
|
|
60
|
+
const text = JSON.stringify({
|
|
61
|
+
aithos_recovery_version: "0.1.0-plaintext",
|
|
62
|
+
warning: "this is plaintext, store offline",
|
|
63
|
+
handle: id.handle,
|
|
64
|
+
display_name: id.displayName,
|
|
65
|
+
did: id.did,
|
|
66
|
+
created_at: new Date().toISOString(),
|
|
67
|
+
seeds_hex: {
|
|
68
|
+
root: bytesToHex(id.root.seed),
|
|
69
|
+
public: bytesToHex(id.public.seed),
|
|
70
|
+
circle: bytesToHex(id.circle.seed),
|
|
71
|
+
self: bytesToHex(id.self.seed),
|
|
72
|
+
},
|
|
73
|
+
public_keys_multibase: {},
|
|
74
|
+
});
|
|
75
|
+
const parsed = parseRecoveryFile(text);
|
|
76
|
+
assert.equal(parsed.did, id.did);
|
|
77
|
+
});
|
|
78
|
+
it("rejects unsupported versions", () => {
|
|
79
|
+
assert.throws(() => parseRecoveryFile(JSON.stringify({ aithos_recovery_version: "9.9.9" })), (e) => e instanceof AithosSDKError && e.code === "auth_invalid_recovery_file");
|
|
80
|
+
});
|
|
81
|
+
it("rejects malformed seeds", () => {
|
|
82
|
+
const id = createBrowserIdentity("alice", "Alice");
|
|
83
|
+
const { text } = serializeRecoveryFile(id);
|
|
84
|
+
const obj = JSON.parse(text);
|
|
85
|
+
obj.seeds_hex.root = "not hex";
|
|
86
|
+
assert.throws(() => parseRecoveryFile(JSON.stringify(obj)), AithosSDKError);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
/* -------------------------------------------------------------------------- */
|
|
90
|
+
/* parseDelegateBundle */
|
|
91
|
+
/* -------------------------------------------------------------------------- */
|
|
92
|
+
describe("delegate bundle: parse", () => {
|
|
93
|
+
it("parses a well-formed bundle", () => {
|
|
94
|
+
const text = delegateBundleText({
|
|
95
|
+
mandateId: "mandate:01H8XYZ",
|
|
96
|
+
subjectDid: "did:aithos:zCarol",
|
|
97
|
+
granteeId: "urn:aithos:agent:bob1",
|
|
98
|
+
scopes: ["ethos.read.public", "ethos.write.public"],
|
|
99
|
+
});
|
|
100
|
+
const parsed = parseDelegateBundle(text);
|
|
101
|
+
assert.equal(parsed.mandateId, "mandate:01H8XYZ");
|
|
102
|
+
assert.equal(parsed.subjectDid, "did:aithos:zCarol");
|
|
103
|
+
assert.equal(parsed.granteeId, "urn:aithos:agent:bob1");
|
|
104
|
+
assert.equal(parsed.delegateSeedHex.length, 64);
|
|
105
|
+
});
|
|
106
|
+
it("readDelegateBundleText accepts string passthrough", async () => {
|
|
107
|
+
const text = delegateBundleText({
|
|
108
|
+
mandateId: "m",
|
|
109
|
+
subjectDid: "did:aithos:z",
|
|
110
|
+
granteeId: "urn:x",
|
|
111
|
+
});
|
|
112
|
+
assert.equal(await readDelegateBundleText(text), text);
|
|
113
|
+
});
|
|
114
|
+
it("rejects missing mandate", () => {
|
|
115
|
+
assert.throws(() => parseDelegateBundle(JSON.stringify({
|
|
116
|
+
aithos_delegate_version: "0.1.0",
|
|
117
|
+
delegate_seed_hex: "11".repeat(32),
|
|
118
|
+
})), AithosSDKError);
|
|
119
|
+
});
|
|
120
|
+
it("rejects malformed delegate seed", () => {
|
|
121
|
+
assert.throws(() => parseDelegateBundle(JSON.stringify({
|
|
122
|
+
aithos_delegate_version: "0.1.0",
|
|
123
|
+
mandate: {
|
|
124
|
+
id: "m",
|
|
125
|
+
subject_did: "did:aithos:z",
|
|
126
|
+
grantee: { id: "u", pubkey: "z6MkXYZ" },
|
|
127
|
+
},
|
|
128
|
+
delegate_seed_hex: "not hex",
|
|
129
|
+
})), AithosSDKError);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
/* -------------------------------------------------------------------------- */
|
|
133
|
+
/* AithosAuth — recovery sign-in */
|
|
134
|
+
/* -------------------------------------------------------------------------- */
|
|
135
|
+
describe("AithosAuth.signInWithRecovery", () => {
|
|
136
|
+
it("hydrates owner signers + persists to keyStore (no JWT)", async () => {
|
|
137
|
+
const keyStore = memoryKeyStore();
|
|
138
|
+
const sessionStore = noopStore();
|
|
139
|
+
const auth = makeAuth({ sessionStore, keyStore });
|
|
140
|
+
assert.equal(auth.canSignAsOwner(), false);
|
|
141
|
+
assert.equal(auth.getOwnerInfo(), null);
|
|
142
|
+
const { text } = recoveryTextFor("alice", "Alice");
|
|
143
|
+
const info = await auth.signInWithRecovery({ file: text });
|
|
144
|
+
assert.equal(info.handle, "alice");
|
|
145
|
+
assert.equal(auth.canSignAsOwner(), true);
|
|
146
|
+
assert.equal(auth.getOwnerInfo()?.did, info.did);
|
|
147
|
+
assert.equal(auth.getCurrentSession(), null, "no JWT for recovery flow");
|
|
148
|
+
const persisted = await keyStore.loadOwner();
|
|
149
|
+
assert.equal(persisted?.did, info.did);
|
|
150
|
+
});
|
|
151
|
+
it("rejects loading a different owner without signOut first", async () => {
|
|
152
|
+
const auth = makeAuth();
|
|
153
|
+
const a = recoveryTextFor("alice", "Alice");
|
|
154
|
+
await auth.signInWithRecovery({ file: a.text });
|
|
155
|
+
const b = recoveryTextFor("bob", "Bob");
|
|
156
|
+
await assert.rejects(() => auth.signInWithRecovery({ file: b.text }), (e) => e instanceof AithosSDKError && e.code === "auth_owner_already_loaded");
|
|
157
|
+
});
|
|
158
|
+
it("clears any stale JWT to keep stores in sync", async () => {
|
|
159
|
+
const keyStore = memoryKeyStore();
|
|
160
|
+
let stored = {
|
|
161
|
+
session: "stale-jwt",
|
|
162
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
163
|
+
did: "did:aithos:zStale",
|
|
164
|
+
handle: "stale",
|
|
165
|
+
blob_b64: "",
|
|
166
|
+
blob_nonce_b64: "",
|
|
167
|
+
blob_version: 0,
|
|
168
|
+
enc_key_b64: "",
|
|
169
|
+
is_first_login: false,
|
|
170
|
+
};
|
|
171
|
+
const sessionStore = {
|
|
172
|
+
get: () => stored,
|
|
173
|
+
set: (s) => {
|
|
174
|
+
stored = s;
|
|
175
|
+
},
|
|
176
|
+
clear: () => {
|
|
177
|
+
stored = null;
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
const auth = makeAuth({ sessionStore, keyStore });
|
|
181
|
+
const { text } = recoveryTextFor("alice", "Alice");
|
|
182
|
+
await auth.signInWithRecovery({ file: text });
|
|
183
|
+
assert.equal(stored, null, "stale JWT must be wiped");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
/* -------------------------------------------------------------------------- */
|
|
187
|
+
/* AithosAuth — importMandate */
|
|
188
|
+
/* -------------------------------------------------------------------------- */
|
|
189
|
+
describe("AithosAuth.importMandate", () => {
|
|
190
|
+
it("registers a delegate, lists it, removes it", async () => {
|
|
191
|
+
const keyStore = memoryKeyStore();
|
|
192
|
+
const auth = makeAuth({ keyStore });
|
|
193
|
+
const text = delegateBundleText({
|
|
194
|
+
mandateId: "mandate:A",
|
|
195
|
+
subjectDid: "did:aithos:zCarol",
|
|
196
|
+
granteeId: "urn:aithos:agent:bob1",
|
|
197
|
+
scopes: ["ethos.read.circle"],
|
|
198
|
+
});
|
|
199
|
+
const info = await auth.importMandate({ bundle: text });
|
|
200
|
+
assert.equal(info.mandateId, "mandate:A");
|
|
201
|
+
assert.equal(info.subjectDid, "did:aithos:zCarol");
|
|
202
|
+
assert.deepEqual(info.scopes, ["ethos.read.circle"]);
|
|
203
|
+
const list = auth.getDelegates();
|
|
204
|
+
assert.equal(list.length, 1);
|
|
205
|
+
assert.equal(list[0]?.mandateId, "mandate:A");
|
|
206
|
+
assert.equal(auth.canSignAsDelegateFor("did:aithos:zCarol"), true);
|
|
207
|
+
assert.equal(auth.canSignAsDelegateFor("did:aithos:zNobody"), false);
|
|
208
|
+
await auth.removeMandate("mandate:A");
|
|
209
|
+
assert.equal(auth.getDelegates().length, 0);
|
|
210
|
+
assert.equal((await keyStore.listDelegates()).length, 0);
|
|
211
|
+
});
|
|
212
|
+
it("works without an owner loaded (delegate-only session)", async () => {
|
|
213
|
+
const auth = makeAuth();
|
|
214
|
+
assert.equal(auth.canSignAsOwner(), false);
|
|
215
|
+
const text = delegateBundleText({
|
|
216
|
+
mandateId: "mandate:Solo",
|
|
217
|
+
subjectDid: "did:aithos:zCarol",
|
|
218
|
+
granteeId: "urn:aithos:agent:solo1",
|
|
219
|
+
});
|
|
220
|
+
await auth.importMandate({ bundle: text });
|
|
221
|
+
assert.equal(auth.canSignAsOwner(), false);
|
|
222
|
+
assert.equal(auth.canSignAsDelegateFor("did:aithos:zCarol"), true);
|
|
223
|
+
});
|
|
224
|
+
it("re-importing the same mandate replaces the prior actor", async () => {
|
|
225
|
+
const auth = makeAuth();
|
|
226
|
+
const text = delegateBundleText({
|
|
227
|
+
mandateId: "mandate:R",
|
|
228
|
+
subjectDid: "did:aithos:zCarol",
|
|
229
|
+
granteeId: "urn:aithos:agent:r",
|
|
230
|
+
});
|
|
231
|
+
await auth.importMandate({ bundle: text });
|
|
232
|
+
await auth.importMandate({ bundle: text });
|
|
233
|
+
assert.equal(auth.getDelegates().length, 1);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
/* -------------------------------------------------------------------------- */
|
|
237
|
+
/* AithosAuth — resume() */
|
|
238
|
+
/* -------------------------------------------------------------------------- */
|
|
239
|
+
describe("AithosAuth.resume", () => {
|
|
240
|
+
it("rehydrates owner + delegates from keyStore on a fresh instance", async () => {
|
|
241
|
+
const keyStore = memoryKeyStore();
|
|
242
|
+
const sessionStore = noopStore();
|
|
243
|
+
// Seed the stores via a first auth instance.
|
|
244
|
+
{
|
|
245
|
+
const auth1 = makeAuth({ keyStore, sessionStore });
|
|
246
|
+
const { text: rt } = recoveryTextFor("alice", "Alice");
|
|
247
|
+
await auth1.signInWithRecovery({ file: rt });
|
|
248
|
+
const dt = delegateBundleText({
|
|
249
|
+
mandateId: "mandate:M1",
|
|
250
|
+
subjectDid: "did:aithos:zCarol",
|
|
251
|
+
granteeId: "urn:x",
|
|
252
|
+
});
|
|
253
|
+
await auth1.importMandate({ bundle: dt });
|
|
254
|
+
}
|
|
255
|
+
// Fresh instance, same stores → resume() must reload everything.
|
|
256
|
+
const auth2 = makeAuth({ keyStore, sessionStore });
|
|
257
|
+
assert.equal(auth2.canSignAsOwner(), false, "before resume, in-memory only");
|
|
258
|
+
await auth2.resume();
|
|
259
|
+
assert.equal(auth2.canSignAsOwner(), true);
|
|
260
|
+
assert.equal(auth2.getOwnerInfo()?.handle, "alice");
|
|
261
|
+
assert.equal(auth2.getDelegates().length, 1);
|
|
262
|
+
assert.equal(auth2.canSignAsDelegateFor("did:aithos:zCarol"), true);
|
|
263
|
+
});
|
|
264
|
+
it("strict mode: JWT in sessionStore but no owner in keyStore → wipes JWT", async () => {
|
|
265
|
+
let jwt = {
|
|
266
|
+
session: "j",
|
|
267
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
268
|
+
did: "did:aithos:zGhost",
|
|
269
|
+
handle: "ghost",
|
|
270
|
+
blob_b64: "",
|
|
271
|
+
blob_nonce_b64: "",
|
|
272
|
+
blob_version: 0,
|
|
273
|
+
enc_key_b64: "",
|
|
274
|
+
is_first_login: false,
|
|
275
|
+
};
|
|
276
|
+
const sessionStore = {
|
|
277
|
+
get: () => jwt,
|
|
278
|
+
set: (s) => {
|
|
279
|
+
jwt = s;
|
|
280
|
+
},
|
|
281
|
+
clear: () => {
|
|
282
|
+
jwt = null;
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
const keyStore = memoryKeyStore();
|
|
286
|
+
const auth = makeAuth({ keyStore, sessionStore });
|
|
287
|
+
await auth.resume();
|
|
288
|
+
assert.equal(jwt, null, "out-of-sync JWT must be cleared");
|
|
289
|
+
assert.equal(auth.canSignAsOwner(), false);
|
|
290
|
+
});
|
|
291
|
+
it("strict mode: JWT and owner disagree on DID → wipes JWT only", async () => {
|
|
292
|
+
const keyStore = memoryKeyStore();
|
|
293
|
+
const { text } = recoveryTextFor("alice", "Alice");
|
|
294
|
+
// Seed the keystore with alice via one instance.
|
|
295
|
+
{
|
|
296
|
+
const auth1 = makeAuth({ keyStore });
|
|
297
|
+
await auth1.signInWithRecovery({ file: text });
|
|
298
|
+
}
|
|
299
|
+
// Now plant a JWT for a DIFFERENT DID in the session store.
|
|
300
|
+
let jwt = {
|
|
301
|
+
session: "j",
|
|
302
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
303
|
+
did: "did:aithos:zSomeoneElse",
|
|
304
|
+
handle: "someone",
|
|
305
|
+
blob_b64: "",
|
|
306
|
+
blob_nonce_b64: "",
|
|
307
|
+
blob_version: 0,
|
|
308
|
+
enc_key_b64: "",
|
|
309
|
+
is_first_login: false,
|
|
310
|
+
};
|
|
311
|
+
const sessionStore = {
|
|
312
|
+
get: () => jwt,
|
|
313
|
+
set: (s) => {
|
|
314
|
+
jwt = s;
|
|
315
|
+
},
|
|
316
|
+
clear: () => {
|
|
317
|
+
jwt = null;
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
const auth2 = makeAuth({ keyStore, sessionStore });
|
|
321
|
+
await auth2.resume();
|
|
322
|
+
assert.equal(jwt, null, "mismatched JWT must be wiped");
|
|
323
|
+
// Owner is preserved (it's the source of truth in strict mode).
|
|
324
|
+
assert.equal(auth2.canSignAsOwner(), true);
|
|
325
|
+
assert.equal(auth2.getOwnerInfo()?.handle, "alice");
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
/* -------------------------------------------------------------------------- */
|
|
329
|
+
/* AithosAuth — signOut */
|
|
330
|
+
/* -------------------------------------------------------------------------- */
|
|
331
|
+
describe("AithosAuth.signOut", () => {
|
|
332
|
+
it("wipes both stores and in-memory state", async () => {
|
|
333
|
+
const keyStore = memoryKeyStore();
|
|
334
|
+
const auth = makeAuth({ keyStore });
|
|
335
|
+
const { text } = recoveryTextFor("alice", "Alice");
|
|
336
|
+
await auth.signInWithRecovery({ file: text });
|
|
337
|
+
const dt = delegateBundleText({
|
|
338
|
+
mandateId: "mandate:X",
|
|
339
|
+
subjectDid: "did:aithos:zCarol",
|
|
340
|
+
granteeId: "urn:x",
|
|
341
|
+
});
|
|
342
|
+
await auth.importMandate({ bundle: dt });
|
|
343
|
+
await auth.signOut();
|
|
344
|
+
assert.equal(auth.canSignAsOwner(), false);
|
|
345
|
+
assert.equal(auth.getOwnerInfo(), null);
|
|
346
|
+
assert.equal(auth.getDelegates().length, 0);
|
|
347
|
+
assert.equal(await keyStore.loadOwner(), null);
|
|
348
|
+
assert.equal((await keyStore.listDelegates()).length, 0);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
/* -------------------------------------------------------------------------- */
|
|
352
|
+
/* Helpers */
|
|
353
|
+
/* -------------------------------------------------------------------------- */
|
|
354
|
+
function bytesToHex(b) {
|
|
355
|
+
let out = "";
|
|
356
|
+
for (let i = 0; i < b.length; i++)
|
|
357
|
+
out += b[i].toString(16).padStart(2, "0");
|
|
358
|
+
return out;
|
|
359
|
+
}
|
|
360
|
+
//# sourceMappingURL=auth-j3.test.js.map
|
|
@@ -9,12 +9,23 @@
|
|
|
9
9
|
import { strict as assert } from "node:assert";
|
|
10
10
|
import { describe, it } from "node:test";
|
|
11
11
|
import { createBrowserIdentity } from "@aithos/protocol-client";
|
|
12
|
-
import { AithosSDK, AithosSDKError } from "../src/index.js";
|
|
12
|
+
import { AithosAuth, AithosSDK, AithosSDKError, memoryKeyStore, noopStore, } from "../src/index.js";
|
|
13
|
+
import { serializeRecoveryFile } from "../src/internal/recovery-file.js";
|
|
13
14
|
const APP_DID = "did:aithos:app:test";
|
|
14
|
-
function makeSdk(fetchImpl) {
|
|
15
|
-
const
|
|
15
|
+
async function makeSdk(fetchImpl) {
|
|
16
|
+
const id = createBrowserIdentity("test-handle", "Test User");
|
|
17
|
+
const auth = new AithosAuth({
|
|
18
|
+
authBaseUrl: "https://auth.test",
|
|
19
|
+
fetch: (() => {
|
|
20
|
+
throw new Error("auth not used in compute tests");
|
|
21
|
+
}),
|
|
22
|
+
sessionStore: noopStore(),
|
|
23
|
+
keyStore: memoryKeyStore(),
|
|
24
|
+
});
|
|
25
|
+
const { text } = serializeRecoveryFile(id);
|
|
26
|
+
await auth.signInWithRecovery({ file: text });
|
|
16
27
|
return new AithosSDK({
|
|
17
|
-
|
|
28
|
+
auth,
|
|
18
29
|
appDid: APP_DID,
|
|
19
30
|
endpoints: { compute: "https://compute.example.test" },
|
|
20
31
|
fetch: fetchImpl,
|
|
@@ -40,7 +51,7 @@ describe("compute.invokeBedrock — happy path", () => {
|
|
|
40
51
|
headers: { "content-type": "application/json" },
|
|
41
52
|
});
|
|
42
53
|
};
|
|
43
|
-
const sdk = makeSdk(fakeFetch);
|
|
54
|
+
const sdk = await makeSdk(fakeFetch);
|
|
44
55
|
const out = await sdk.compute.invokeBedrock({
|
|
45
56
|
mandateId: "mandate:abc",
|
|
46
57
|
model: "claude-sonnet-4-6",
|
|
@@ -71,7 +82,7 @@ describe("compute.invokeBedrock — happy path", () => {
|
|
|
71
82
|
headers: { "content-type": "application/json" },
|
|
72
83
|
});
|
|
73
84
|
};
|
|
74
|
-
const sdk = makeSdk(fakeFetch);
|
|
85
|
+
const sdk = await makeSdk(fakeFetch);
|
|
75
86
|
await sdk.compute.invokeBedrock({
|
|
76
87
|
mandateId: "mandate:abc",
|
|
77
88
|
model: "claude-sonnet-4-6",
|
|
@@ -92,7 +103,7 @@ describe("compute.invokeBedrock — errors", () => {
|
|
|
92
103
|
const fakeFetch = async () => {
|
|
93
104
|
throw new Error("fetch failed: ECONNREFUSED");
|
|
94
105
|
};
|
|
95
|
-
const sdk = makeSdk(fakeFetch);
|
|
106
|
+
const sdk = await makeSdk(fakeFetch);
|
|
96
107
|
await assert.rejects(sdk.compute.invokeBedrock({
|
|
97
108
|
mandateId: "mandate:abc",
|
|
98
109
|
model: "claude-sonnet-4-6",
|
|
@@ -106,7 +117,7 @@ describe("compute.invokeBedrock — errors", () => {
|
|
|
106
117
|
});
|
|
107
118
|
it("wraps an HTTP non-2xx as AithosSDKError(code='http')", async () => {
|
|
108
119
|
const fakeFetch = async () => new Response("nope", { status: 503, statusText: "Service Unavailable" });
|
|
109
|
-
const sdk = makeSdk(fakeFetch);
|
|
120
|
+
const sdk = await makeSdk(fakeFetch);
|
|
110
121
|
await assert.rejects(sdk.compute.invokeBedrock({
|
|
111
122
|
mandateId: "mandate:abc",
|
|
112
123
|
model: "claude-sonnet-4-6",
|
|
@@ -126,7 +137,7 @@ describe("compute.invokeBedrock — errors", () => {
|
|
|
126
137
|
data: { balance: 0, required: 1 },
|
|
127
138
|
},
|
|
128
139
|
}), { status: 200, headers: { "content-type": "application/json" } });
|
|
129
|
-
const sdk = makeSdk(fakeFetch);
|
|
140
|
+
const sdk = await makeSdk(fakeFetch);
|
|
130
141
|
await assert.rejects(sdk.compute.invokeBedrock({
|
|
131
142
|
mandateId: "mandate:abc",
|
|
132
143
|
model: "claude-sonnet-4-6",
|
|
@@ -143,7 +154,7 @@ describe("compute.invokeBedrock — errors", () => {
|
|
|
143
154
|
status: 200,
|
|
144
155
|
headers: { "content-type": "application/json" },
|
|
145
156
|
});
|
|
146
|
-
const sdk = makeSdk(fakeFetch);
|
|
157
|
+
const sdk = await makeSdk(fakeFetch);
|
|
147
158
|
await assert.rejects(sdk.compute.invokeBedrock({
|
|
148
159
|
mandateId: "mandate:abc",
|
|
149
160
|
model: "claude-sonnet-4-6",
|
|
@@ -165,7 +176,7 @@ describe("compute.invokeBedrock — abort", () => {
|
|
|
165
176
|
headers: { "content-type": "application/json" },
|
|
166
177
|
});
|
|
167
178
|
};
|
|
168
|
-
const sdk = makeSdk(fakeFetch);
|
|
179
|
+
const sdk = await makeSdk(fakeFetch);
|
|
169
180
|
const ac = new AbortController();
|
|
170
181
|
await sdk.compute.invokeBedrock({
|
|
171
182
|
mandateId: "mandate:abc",
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
// Tests for J4 — sdk.ethos namespace: actor resolution, staging
|
|
4
|
+
// semantics, anonymous-mode guard rails. Tests requiring real
|
|
5
|
+
// protocol-client network calls (sections() / publish()) are not
|
|
6
|
+
// covered here — they ride on an integration suite against a real
|
|
7
|
+
// backend.
|
|
8
|
+
import { strict as assert } from "node:assert";
|
|
9
|
+
import { describe, it } from "node:test";
|
|
10
|
+
import { createBrowserIdentity } from "@aithos/protocol-client";
|
|
11
|
+
import { AithosAuth, AithosSDKError, EthosNamespace, ZONE_NAMES, memoryKeyStore, noopStore, } from "../src/index.js";
|
|
12
|
+
import { serializeRecoveryFile } from "../src/internal/recovery-file.js";
|
|
13
|
+
import { DEFAULT_SDK_ENDPOINTS } from "../src/endpoints.js";
|
|
14
|
+
/* -------------------------------------------------------------------------- */
|
|
15
|
+
/* Fixtures */
|
|
16
|
+
/* -------------------------------------------------------------------------- */
|
|
17
|
+
function makeAuth(opts = {}) {
|
|
18
|
+
return new AithosAuth({
|
|
19
|
+
authBaseUrl: "https://auth.test",
|
|
20
|
+
fetch: (() => {
|
|
21
|
+
throw new Error("network not expected in unit tests");
|
|
22
|
+
}),
|
|
23
|
+
sessionStore: opts.sessionStore ?? noopStore(),
|
|
24
|
+
keyStore: opts.keyStore ?? memoryKeyStore(),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function makeNamespace(auth) {
|
|
28
|
+
return new EthosNamespace({
|
|
29
|
+
auth,
|
|
30
|
+
endpoints: DEFAULT_SDK_ENDPOINTS,
|
|
31
|
+
fetch: globalThis.fetch.bind(globalThis),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async function signInAsAlice(auth) {
|
|
35
|
+
const id = createBrowserIdentity("alice", "Alice");
|
|
36
|
+
const { text } = serializeRecoveryFile(id);
|
|
37
|
+
const info = await auth.signInWithRecovery({ file: text });
|
|
38
|
+
return { did: info.did, handle: info.handle };
|
|
39
|
+
}
|
|
40
|
+
function delegateBundleText(args) {
|
|
41
|
+
return JSON.stringify({
|
|
42
|
+
aithos_delegate_version: "0.1.0",
|
|
43
|
+
mandate: {
|
|
44
|
+
id: args.mandateId,
|
|
45
|
+
subject_did: args.subjectDid,
|
|
46
|
+
grantee: {
|
|
47
|
+
id: args.granteeId,
|
|
48
|
+
pubkey: "z6MkqGenericPubKey",
|
|
49
|
+
},
|
|
50
|
+
scopes: args.scopes ?? ["ethos.read.public", "ethos.write.public"],
|
|
51
|
+
},
|
|
52
|
+
delegate_seed_hex: "11".repeat(32),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/* -------------------------------------------------------------------------- */
|
|
56
|
+
/* EthosNamespace — actor resolution */
|
|
57
|
+
/* -------------------------------------------------------------------------- */
|
|
58
|
+
describe("EthosNamespace.me()", () => {
|
|
59
|
+
it("returns an owner-mode EthosClient when signed in", async () => {
|
|
60
|
+
const auth = makeAuth();
|
|
61
|
+
const alice = await signInAsAlice(auth);
|
|
62
|
+
const ns = makeNamespace(auth);
|
|
63
|
+
const me = ns.me();
|
|
64
|
+
assert.equal(me.mode, "owner");
|
|
65
|
+
assert.equal(me.subjectDid, alice.did);
|
|
66
|
+
// Three zones, all return Zone proxies.
|
|
67
|
+
for (const z of ZONE_NAMES) {
|
|
68
|
+
const zone = me.zone(z);
|
|
69
|
+
assert.equal(zone.name, z);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
it("throws ethos_no_owner when no owner is signed in", () => {
|
|
73
|
+
const auth = makeAuth();
|
|
74
|
+
const ns = makeNamespace(auth);
|
|
75
|
+
assert.throws(() => ns.me(), (e) => e instanceof AithosSDKError && e.code === "ethos_no_owner");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe("EthosNamespace.of(did)", () => {
|
|
79
|
+
it("returns owner-mode for the user's own DID", async () => {
|
|
80
|
+
const auth = makeAuth();
|
|
81
|
+
const alice = await signInAsAlice(auth);
|
|
82
|
+
const ns = makeNamespace(auth);
|
|
83
|
+
const me = await ns.of(alice.did);
|
|
84
|
+
assert.equal(me.mode, "owner");
|
|
85
|
+
});
|
|
86
|
+
it("returns delegate-mode when a mandate covers the subject", async () => {
|
|
87
|
+
const auth = makeAuth();
|
|
88
|
+
await auth.importMandate({
|
|
89
|
+
bundle: delegateBundleText({
|
|
90
|
+
mandateId: "mandate:A",
|
|
91
|
+
subjectDid: "did:aithos:zCarol",
|
|
92
|
+
granteeId: "urn:aithos:agent:bob1",
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
const ns = makeNamespace(auth);
|
|
96
|
+
const carol = await ns.of("did:aithos:zCarol");
|
|
97
|
+
assert.equal(carol.mode, "delegate");
|
|
98
|
+
assert.equal(carol.subjectDid, "did:aithos:zCarol");
|
|
99
|
+
});
|
|
100
|
+
it("returns anonymous-mode when no actor matches", async () => {
|
|
101
|
+
const auth = makeAuth();
|
|
102
|
+
const ns = makeNamespace(auth);
|
|
103
|
+
const stranger = await ns.of("did:aithos:zStranger");
|
|
104
|
+
assert.equal(stranger.mode, "anonymous");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
/* -------------------------------------------------------------------------- */
|
|
108
|
+
/* Mutation staging (no network) */
|
|
109
|
+
/* -------------------------------------------------------------------------- */
|
|
110
|
+
describe("EthosClient mutation staging", () => {
|
|
111
|
+
it("addSection / updateSection / deleteSection accumulate", async () => {
|
|
112
|
+
const auth = makeAuth();
|
|
113
|
+
await signInAsAlice(auth);
|
|
114
|
+
const ns = makeNamespace(auth);
|
|
115
|
+
const me = ns.me();
|
|
116
|
+
assert.equal(me.hasPendingChanges(), false);
|
|
117
|
+
me.zone("public").addSection({ title: "Hello", body: "world" });
|
|
118
|
+
me.zone("public").addSection({ title: "Two", body: "sections" });
|
|
119
|
+
me.zone("circle").deleteSection("sec_old");
|
|
120
|
+
me.zone("self").updateSection("sec_x", { body: "new" });
|
|
121
|
+
assert.equal(me.hasPendingChanges(), true);
|
|
122
|
+
const pending = me.pendingChanges();
|
|
123
|
+
assert.equal(pending.length, 4);
|
|
124
|
+
const kinds = pending.map((c) => `${c.zone}:${c.kind}`);
|
|
125
|
+
assert.deepEqual(kinds, [
|
|
126
|
+
"public:add",
|
|
127
|
+
"public:add",
|
|
128
|
+
"circle:delete",
|
|
129
|
+
"self:update",
|
|
130
|
+
]);
|
|
131
|
+
});
|
|
132
|
+
it("each addSection generates a fresh sec_-prefixed id", async () => {
|
|
133
|
+
const auth = makeAuth();
|
|
134
|
+
await signInAsAlice(auth);
|
|
135
|
+
const ns = makeNamespace(auth);
|
|
136
|
+
const me = ns.me();
|
|
137
|
+
me.zone("public").addSection({ title: "a", body: "1" });
|
|
138
|
+
me.zone("public").addSection({ title: "b", body: "2" });
|
|
139
|
+
const ids = me
|
|
140
|
+
.pendingChanges()
|
|
141
|
+
.filter((c) => c.kind === "add")
|
|
142
|
+
.map((c) => c.section.id);
|
|
143
|
+
assert.equal(ids.length, 2);
|
|
144
|
+
for (const id of ids) {
|
|
145
|
+
assert.match(id, /^sec_[0-9a-f]{12}$/);
|
|
146
|
+
}
|
|
147
|
+
assert.notEqual(ids[0], ids[1]);
|
|
148
|
+
});
|
|
149
|
+
it("discard() clears pending changes", async () => {
|
|
150
|
+
const auth = makeAuth();
|
|
151
|
+
await signInAsAlice(auth);
|
|
152
|
+
const ns = makeNamespace(auth);
|
|
153
|
+
const me = ns.me();
|
|
154
|
+
me.zone("public").addSection({ title: "h", body: "w" });
|
|
155
|
+
assert.equal(me.hasPendingChanges(), true);
|
|
156
|
+
me.discard();
|
|
157
|
+
assert.equal(me.hasPendingChanges(), false);
|
|
158
|
+
assert.equal(me.pendingChanges().length, 0);
|
|
159
|
+
});
|
|
160
|
+
it("publish() rejects when nothing is staged", async () => {
|
|
161
|
+
const auth = makeAuth();
|
|
162
|
+
await signInAsAlice(auth);
|
|
163
|
+
const ns = makeNamespace(auth);
|
|
164
|
+
const me = ns.me();
|
|
165
|
+
await assert.rejects(() => me.publish(), (e) => e instanceof AithosSDKError && e.code === "ethos_nothing_to_publish");
|
|
166
|
+
});
|
|
167
|
+
it("zone(invalid) throws", async () => {
|
|
168
|
+
const auth = makeAuth();
|
|
169
|
+
await signInAsAlice(auth);
|
|
170
|
+
const ns = makeNamespace(auth);
|
|
171
|
+
const me = ns.me();
|
|
172
|
+
assert.throws(() => me.zone("nope"), AithosSDKError);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
/* -------------------------------------------------------------------------- */
|
|
176
|
+
/* Anonymous mode guard rails */
|
|
177
|
+
/* -------------------------------------------------------------------------- */
|
|
178
|
+
describe("EthosClient anonymous mode", () => {
|
|
179
|
+
it("rejects every write op", async () => {
|
|
180
|
+
const auth = makeAuth();
|
|
181
|
+
const ns = makeNamespace(auth);
|
|
182
|
+
const stranger = await ns.of("did:aithos:zStranger");
|
|
183
|
+
assert.equal(stranger.mode, "anonymous");
|
|
184
|
+
assert.throws(() => stranger
|
|
185
|
+
.zone("public")
|
|
186
|
+
.addSection({ title: "x", body: "y" }), AithosSDKError);
|
|
187
|
+
assert.throws(() => stranger.zone("public").updateSection("sec_x", { body: "y" }), AithosSDKError);
|
|
188
|
+
assert.throws(() => stranger.zone("public").deleteSection("sec_x"), AithosSDKError);
|
|
189
|
+
});
|
|
190
|
+
it("rejects publish()", async () => {
|
|
191
|
+
const auth = makeAuth();
|
|
192
|
+
const ns = makeNamespace(auth);
|
|
193
|
+
const stranger = await ns.of("did:aithos:zStranger");
|
|
194
|
+
await assert.rejects(() => stranger.publish(), (e) => e instanceof AithosSDKError && e.code === "ethos_anonymous_cannot_publish");
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
/* -------------------------------------------------------------------------- */
|
|
198
|
+
/* Delegate mode scope checks */
|
|
199
|
+
/* -------------------------------------------------------------------------- */
|
|
200
|
+
describe("EthosClient delegate mode", () => {
|
|
201
|
+
it("publish rejects writes outside the mandate scopes", async () => {
|
|
202
|
+
const auth = makeAuth();
|
|
203
|
+
// Mandate only grants ethos.read.public — no write scope at all.
|
|
204
|
+
await auth.importMandate({
|
|
205
|
+
bundle: delegateBundleText({
|
|
206
|
+
mandateId: "mandate:RO",
|
|
207
|
+
subjectDid: "did:aithos:zCarol",
|
|
208
|
+
granteeId: "urn:x",
|
|
209
|
+
scopes: ["ethos.read.public"],
|
|
210
|
+
}),
|
|
211
|
+
});
|
|
212
|
+
const ns = makeNamespace(auth);
|
|
213
|
+
const carol = await ns.of("did:aithos:zCarol");
|
|
214
|
+
carol.zone("public").addSection({ title: "x", body: "y" });
|
|
215
|
+
await assert.rejects(() => carol.publish(), (e) => e instanceof AithosSDKError &&
|
|
216
|
+
e.code === "ethos_delegate_scope_missing");
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
//# sourceMappingURL=ethos.test.js.map
|