@aithos/sdk 0.1.0-alpha.2 → 0.1.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +45 -0
  2. package/dist/src/auth-api.d.ts +50 -0
  3. package/dist/src/auth-api.js +102 -0
  4. package/dist/src/auth.d.ts +253 -0
  5. package/dist/src/auth.js +940 -0
  6. package/dist/src/compute.d.ts +370 -9
  7. package/dist/src/compute.js +369 -16
  8. package/dist/src/ethos.d.ts +164 -1
  9. package/dist/src/ethos.js +729 -16
  10. package/dist/src/index.d.ts +11 -4
  11. package/dist/src/index.js +31 -5
  12. package/dist/src/internal/delegate-bundle.d.ts +18 -0
  13. package/dist/src/internal/delegate-bundle.js +94 -0
  14. package/dist/src/internal/delegate-state.d.ts +45 -0
  15. package/dist/src/internal/delegate-state.js +120 -0
  16. package/dist/src/internal/owner-signers.d.ts +78 -0
  17. package/dist/src/internal/owner-signers.js +179 -0
  18. package/dist/src/internal/protocol-client-bridge.d.ts +8 -0
  19. package/dist/src/internal/protocol-client-bridge.js +20 -0
  20. package/dist/src/internal/recovery-file.d.ts +29 -0
  21. package/dist/src/internal/recovery-file.js +98 -0
  22. package/dist/src/internal/signer.d.ts +59 -0
  23. package/dist/src/internal/signer.js +86 -0
  24. package/dist/src/key-store.d.ts +128 -0
  25. package/dist/src/key-store.js +244 -0
  26. package/dist/src/mandates.d.ts +163 -1
  27. package/dist/src/mandates.js +286 -8
  28. package/dist/src/sdk.d.ts +36 -3
  29. package/dist/src/sdk.js +27 -23
  30. package/dist/src/session-store.d.ts +58 -0
  31. package/dist/src/session-store.js +158 -0
  32. package/dist/src/wallet.d.ts +4 -6
  33. package/dist/src/wallet.js +18 -8
  34. package/dist/test/auth-j3.test.d.ts +2 -0
  35. package/dist/test/auth-j3.test.js +391 -0
  36. package/dist/test/auth.test.d.ts +2 -0
  37. package/dist/test/auth.test.js +175 -0
  38. package/dist/test/compute-delegate-path.test.d.ts +2 -0
  39. package/dist/test/compute-delegate-path.test.js +183 -0
  40. package/dist/test/compute.test.js +184 -11
  41. package/dist/test/ethos-first-edition.test.d.ts +2 -0
  42. package/dist/test/ethos-first-edition.test.js +248 -0
  43. package/dist/test/ethos.test.d.ts +2 -0
  44. package/dist/test/ethos.test.js +219 -0
  45. package/dist/test/key-store.test.d.ts +2 -0
  46. package/dist/test/key-store.test.js +161 -0
  47. package/dist/test/mandates-compute.test.d.ts +2 -0
  48. package/dist/test/mandates-compute.test.js +256 -0
  49. package/dist/test/mandates.test.d.ts +2 -0
  50. package/dist/test/mandates.test.js +93 -0
  51. package/dist/test/sdk.test.js +70 -30
  52. package/dist/test/signer.test.d.ts +2 -0
  53. package/dist/test/signer.test.js +117 -0
  54. package/dist/test/signup-bootstrap.test.d.ts +2 -0
  55. package/dist/test/signup-bootstrap.test.js +222 -0
  56. package/dist/test/wallet.test.js +20 -9
  57. package/package.json +4 -3
@@ -0,0 +1,248 @@
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() rejects circle/self mutations on first edition", async () => {
189
+ installFetchMock([
190
+ {
191
+ url: "/mcp/primitives/read",
192
+ rpcMethod: "aithos.get_ethos_manifest",
193
+ respond: noEditionYetResponse,
194
+ },
195
+ ]);
196
+ const auth = makeAuth();
197
+ await signInAsAlice(auth);
198
+ const me = makeNamespace(auth).me();
199
+ me.zone("circle").addSection({ title: "Private", body: "..." });
200
+ await assert.rejects(() => me.publish(), (e) => e instanceof AithosSDKError && e.code === "ethos_first_edition_public_only");
201
+ });
202
+ it("publish() rejects update/delete operations on a fresh Ethos", async () => {
203
+ installFetchMock([
204
+ {
205
+ url: "/mcp/primitives/read",
206
+ rpcMethod: "aithos.get_ethos_manifest",
207
+ respond: noEditionYetResponse,
208
+ },
209
+ ]);
210
+ const auth = makeAuth();
211
+ await signInAsAlice(auth);
212
+ const me = makeNamespace(auth).me();
213
+ // Stage a delete for a section that doesn't exist (no edition exists at all).
214
+ me.zone("public")["_parent"]; // type-safety placeholder; we use the public API
215
+ // EthosZone exposes deleteSection — go via that.
216
+ me.zone("public").deleteSection("sec_doesnotexist000");
217
+ await assert.rejects(() => me.publish(), (e) => e instanceof AithosSDKError && e.code === "ethos_first_edition_invalid_op");
218
+ });
219
+ it("publish() surfaces server JSON-RPC errors as ethos_first_edition_rejected", async () => {
220
+ installFetchMock([
221
+ {
222
+ url: "/mcp/primitives/read",
223
+ rpcMethod: "aithos.get_ethos_manifest",
224
+ respond: noEditionYetResponse,
225
+ },
226
+ {
227
+ url: "/mcp/primitives/write",
228
+ rpcMethod: "aithos.publish_ethos_edition",
229
+ respond: () => ({
230
+ json: {
231
+ jsonrpc: "2.0",
232
+ id: "publish_ethos_edition",
233
+ error: {
234
+ code: -32020,
235
+ message: "subject identity not published (call publish_identity first)",
236
+ },
237
+ },
238
+ }),
239
+ },
240
+ ]);
241
+ const auth = makeAuth();
242
+ await signInAsAlice(auth);
243
+ const me = makeNamespace(auth).me();
244
+ me.zone("public").addSection({ title: "Hi", body: "There." });
245
+ await assert.rejects(() => me.publish(), (e) => e instanceof AithosSDKError && e.code === "ethos_first_edition_rejected");
246
+ });
247
+ });
248
+ //# sourceMappingURL=ethos-first-edition.test.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ethos.test.d.ts.map
@@ -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
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=key-store.test.d.ts.map
@@ -0,0 +1,161 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 Mathieu Colla
3
+ // Unit tests for AithosKeyStore — both built-in implementations.
4
+ //
5
+ // memoryKeyStore is exercised directly. indexedDbKeyStore is driven by
6
+ // `fake-indexeddb` (a W3C-compatible in-memory IDBFactory), with a
7
+ // fresh factory + DB name per test so cases don't bleed into each
8
+ // other.
9
+ import { strict as assert } from "node:assert";
10
+ import { describe, it } from "node:test";
11
+ import { IDBFactory } from "fake-indexeddb";
12
+ import { indexedDbKeyStore, memoryKeyStore, } from "../src/key-store.js";
13
+ /* -------------------------------------------------------------------------- */
14
+ /* Fixtures */
15
+ /* -------------------------------------------------------------------------- */
16
+ const SEED_HEX = "00".repeat(32);
17
+ function ownerFixture(overrides = {}) {
18
+ return {
19
+ version: "0.1.0-hex",
20
+ did: "did:aithos:zAlice",
21
+ handle: "alice",
22
+ displayName: "Alice",
23
+ seedsHex: {
24
+ root: SEED_HEX,
25
+ public: SEED_HEX,
26
+ circle: SEED_HEX,
27
+ self: SEED_HEX,
28
+ },
29
+ savedAt: "2026-05-08T00:00:00.000Z",
30
+ ...overrides,
31
+ };
32
+ }
33
+ function delegateFixture(overrides = {}) {
34
+ return {
35
+ version: "0.1.0-hex",
36
+ subjectDid: "did:aithos:zCarol",
37
+ mandateId: "mandate:01H8XYZ",
38
+ mandate: { id: "mandate:01H8XYZ", scopes: ["ethos.read.public"] },
39
+ granteeId: "urn:aithos:agent:bob1",
40
+ granteePubkeyMultibase: "z6MkqBobPubKey",
41
+ delegateSeedHex: "11".repeat(32),
42
+ importedAt: "2026-05-08T01:00:00.000Z",
43
+ ...overrides,
44
+ };
45
+ }
46
+ /* -------------------------------------------------------------------------- */
47
+ /* Behaviour suite — runs against both implementations */
48
+ /* -------------------------------------------------------------------------- */
49
+ const implementations = [
50
+ { name: "memoryKeyStore", make: () => memoryKeyStore() },
51
+ {
52
+ name: "indexedDbKeyStore",
53
+ make: () => {
54
+ // Fresh in-memory IDBFactory + unique db name per call so cases stay
55
+ // isolated even across test runs.
56
+ const factory = new IDBFactory();
57
+ return indexedDbKeyStore({
58
+ dbName: `test-${Math.random().toString(36).slice(2)}`,
59
+ factory,
60
+ });
61
+ },
62
+ },
63
+ ];
64
+ for (const impl of implementations) {
65
+ describe(`AithosKeyStore: ${impl.name}`, () => {
66
+ it("loadOwner returns null on a fresh store", async () => {
67
+ const store = impl.make();
68
+ assert.equal(await store.loadOwner(), null);
69
+ });
70
+ it("save/load/clear owner round-trips", async () => {
71
+ const store = impl.make();
72
+ const o = ownerFixture();
73
+ await store.saveOwner(o);
74
+ const loaded = await store.loadOwner();
75
+ assert.deepEqual(loaded, o);
76
+ await store.clearOwner();
77
+ assert.equal(await store.loadOwner(), null);
78
+ });
79
+ it("saveOwner overwrites the prior owner", async () => {
80
+ const store = impl.make();
81
+ await store.saveOwner(ownerFixture({ handle: "alice" }));
82
+ await store.saveOwner(ownerFixture({ handle: "alice2" }));
83
+ const loaded = await store.loadOwner();
84
+ assert.equal(loaded?.handle, "alice2");
85
+ });
86
+ it("listDelegates is empty on a fresh store", async () => {
87
+ const store = impl.make();
88
+ assert.deepEqual(await store.listDelegates(), []);
89
+ });
90
+ it("save/load/list/remove delegates", async () => {
91
+ const store = impl.make();
92
+ const a = delegateFixture({ mandateId: "mandate:A" });
93
+ const b = delegateFixture({ mandateId: "mandate:B", subjectDid: "did:aithos:zDave" });
94
+ await store.saveDelegate(a);
95
+ await store.saveDelegate(b);
96
+ assert.deepEqual(await store.loadDelegate("mandate:A"), a);
97
+ assert.deepEqual(await store.loadDelegate("mandate:B"), b);
98
+ assert.equal(await store.loadDelegate("mandate:missing"), null);
99
+ const list = await store.listDelegates();
100
+ assert.equal(list.length, 2);
101
+ const ids = list.map((d) => d.mandateId).sort();
102
+ assert.deepEqual(ids, ["mandate:A", "mandate:B"]);
103
+ await store.removeDelegate("mandate:A");
104
+ const after = await store.listDelegates();
105
+ assert.equal(after.length, 1);
106
+ assert.equal(after[0]?.mandateId, "mandate:B");
107
+ });
108
+ it("clearAllDelegates wipes them all but leaves the owner intact", async () => {
109
+ const store = impl.make();
110
+ await store.saveOwner(ownerFixture());
111
+ await store.saveDelegate(delegateFixture({ mandateId: "mandate:A" }));
112
+ await store.saveDelegate(delegateFixture({ mandateId: "mandate:B" }));
113
+ await store.clearAllDelegates();
114
+ assert.deepEqual(await store.listDelegates(), []);
115
+ assert.notEqual(await store.loadOwner(), null);
116
+ });
117
+ });
118
+ }
119
+ /* -------------------------------------------------------------------------- */
120
+ /* Defensive validators — only relevant for IndexedDB which can hold */
121
+ /* arbitrary tampered records */
122
+ /* -------------------------------------------------------------------------- */
123
+ describe("AithosKeyStore: validators (indexedDbKeyStore)", () => {
124
+ it("treats a corrupted owner record as null", async () => {
125
+ const factory = new IDBFactory();
126
+ const dbName = `test-corrupt-${Math.random().toString(36).slice(2)}`;
127
+ const store = indexedDbKeyStore({ dbName, factory });
128
+ // Bypass the typed API to inject garbage at the OWNER_KEY slot.
129
+ const db = await new Promise((resolve, reject) => {
130
+ const req = factory.open(dbName, 1);
131
+ req.onupgradeneeded = () => {
132
+ const d = req.result;
133
+ if (!d.objectStoreNames.contains("owner"))
134
+ d.createObjectStore("owner");
135
+ if (!d.objectStoreNames.contains("delegates"))
136
+ d.createObjectStore("delegates", { keyPath: "mandateId" });
137
+ };
138
+ req.onsuccess = () => resolve(req.result);
139
+ req.onerror = () => reject(req.error);
140
+ });
141
+ await new Promise((resolve, reject) => {
142
+ const t = db.transaction(["owner"], "readwrite");
143
+ t.objectStore("owner").put({ not: "an owner" }, "self");
144
+ t.oncomplete = () => resolve();
145
+ t.onerror = () => reject(t.error);
146
+ });
147
+ db.close();
148
+ assert.equal(await store.loadOwner(), null);
149
+ });
150
+ it("treats a wrong-version owner record as null", async () => {
151
+ const factory = new IDBFactory();
152
+ const dbName = `test-version-${Math.random().toString(36).slice(2)}`;
153
+ const store = indexedDbKeyStore({ dbName, factory });
154
+ // Owner record with a future schema version → must not be returned.
155
+ const futureRecord = ownerFixture();
156
+ futureRecord.version = "0.2.0-cryptokey";
157
+ await store.saveOwner(futureRecord);
158
+ assert.equal(await store.loadOwner(), null);
159
+ });
160
+ });
161
+ //# sourceMappingURL=key-store.test.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=mandates-compute.test.d.ts.map