@aithos/sdk 0.1.0-alpha.3 → 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-api.d.ts +41 -0
- package/dist/src/auth-api.js +82 -0
- package/dist/src/auth.d.ts +114 -75
- package/dist/src/auth.js +553 -73
- 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 +8 -4
- package/dist/src/index.js +26 -8
- 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/session-store.d.ts +58 -0
- package/dist/src/session-store.js +158 -0
- 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 +4 -3
|
@@ -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,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,93 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
// Tests for J5 — sdk.mandates namespace. Covers input validation,
|
|
4
|
+
// owner-required guard rails, and the input → output projection.
|
|
5
|
+
// Network-dependent paths (real mint, list, revoke) ride on a future
|
|
6
|
+
// integration suite.
|
|
7
|
+
import { strict as assert } from "node:assert";
|
|
8
|
+
import { describe, it } from "node:test";
|
|
9
|
+
import { createBrowserIdentity } from "@aithos/protocol-client";
|
|
10
|
+
import { AithosAuth, AithosSDKError, MandatesNamespace, memoryKeyStore, noopStore, } from "../src/index.js";
|
|
11
|
+
import { DEFAULT_SDK_ENDPOINTS } from "../src/endpoints.js";
|
|
12
|
+
import { serializeRecoveryFile } from "../src/internal/recovery-file.js";
|
|
13
|
+
function makeAuth() {
|
|
14
|
+
return new AithosAuth({
|
|
15
|
+
authBaseUrl: "https://auth.test",
|
|
16
|
+
fetch: (() => {
|
|
17
|
+
throw new Error("network not expected");
|
|
18
|
+
}),
|
|
19
|
+
sessionStore: noopStore(),
|
|
20
|
+
keyStore: memoryKeyStore(),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function makeMandates(auth) {
|
|
24
|
+
return new MandatesNamespace({
|
|
25
|
+
auth,
|
|
26
|
+
endpoints: DEFAULT_SDK_ENDPOINTS,
|
|
27
|
+
fetch: globalThis.fetch.bind(globalThis),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async function signInAsAlice(auth) {
|
|
31
|
+
const id = createBrowserIdentity("alice", "Alice");
|
|
32
|
+
const { text } = serializeRecoveryFile(id);
|
|
33
|
+
const info = await auth.signInWithRecovery({ file: text });
|
|
34
|
+
return info.did;
|
|
35
|
+
}
|
|
36
|
+
/* -------------------------------------------------------------------------- */
|
|
37
|
+
/* Owner-required guard */
|
|
38
|
+
/* -------------------------------------------------------------------------- */
|
|
39
|
+
describe("MandatesNamespace owner guard", () => {
|
|
40
|
+
it("create rejects when no owner is signed in", async () => {
|
|
41
|
+
const m = makeMandates(makeAuth());
|
|
42
|
+
await assert.rejects(() => m.create({
|
|
43
|
+
granteeId: "urn:x",
|
|
44
|
+
scopes: ["ethos.read.public"],
|
|
45
|
+
ttlSeconds: 3600,
|
|
46
|
+
}), (e) => e instanceof AithosSDKError &&
|
|
47
|
+
e.code === "mandates_no_owner");
|
|
48
|
+
});
|
|
49
|
+
it("list rejects when no owner is signed in", async () => {
|
|
50
|
+
const m = makeMandates(makeAuth());
|
|
51
|
+
await assert.rejects(() => m.list(), (e) => e instanceof AithosSDKError &&
|
|
52
|
+
e.code === "mandates_no_owner");
|
|
53
|
+
});
|
|
54
|
+
it("revoke rejects when no owner is signed in", async () => {
|
|
55
|
+
const m = makeMandates(makeAuth());
|
|
56
|
+
await assert.rejects(() => m.revoke("mandate:x"), (e) => e instanceof AithosSDKError &&
|
|
57
|
+
e.code === "mandates_no_owner");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
/* -------------------------------------------------------------------------- */
|
|
61
|
+
/* Input validation */
|
|
62
|
+
/* -------------------------------------------------------------------------- */
|
|
63
|
+
describe("MandatesNamespace.create input validation", () => {
|
|
64
|
+
it("rejects empty scopes", async () => {
|
|
65
|
+
const auth = makeAuth();
|
|
66
|
+
await signInAsAlice(auth);
|
|
67
|
+
const m = makeMandates(auth);
|
|
68
|
+
await assert.rejects(() => m.create({
|
|
69
|
+
granteeId: "urn:x",
|
|
70
|
+
scopes: [],
|
|
71
|
+
ttlSeconds: 3600,
|
|
72
|
+
}), (e) => e instanceof AithosSDKError &&
|
|
73
|
+
e.code === "mandates_invalid_scopes");
|
|
74
|
+
});
|
|
75
|
+
it("rejects non-positive ttl", async () => {
|
|
76
|
+
const auth = makeAuth();
|
|
77
|
+
await signInAsAlice(auth);
|
|
78
|
+
const m = makeMandates(auth);
|
|
79
|
+
await assert.rejects(() => m.create({
|
|
80
|
+
granteeId: "urn:x",
|
|
81
|
+
scopes: ["ethos.read.public"],
|
|
82
|
+
ttlSeconds: 0,
|
|
83
|
+
}), (e) => e instanceof AithosSDKError &&
|
|
84
|
+
e.code === "mandates_invalid_ttl");
|
|
85
|
+
await assert.rejects(() => m.create({
|
|
86
|
+
granteeId: "urn:x",
|
|
87
|
+
scopes: ["ethos.read.public"],
|
|
88
|
+
ttlSeconds: -1,
|
|
89
|
+
}), (e) => e instanceof AithosSDKError &&
|
|
90
|
+
e.code === "mandates_invalid_ttl");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
//# sourceMappingURL=mandates.test.js.map
|
package/dist/test/sdk.test.js
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright 2026 Mathieu Colla
|
|
3
|
-
// Smoke tests for the AithosSDK construction surface.
|
|
4
|
-
//
|
|
5
|
-
// Runtime tests of `compute.invokeBedrock` and `wallet.createTopupSession`
|
|
6
|
-
// require either a running compute proxy or a more elaborate fetch mock
|
|
7
|
-
// pattern; we land them in a follow-up commit alongside the rest of Phase 4
|
|
8
|
-
// (mock-fetch unit tests for both namespaces).
|
|
3
|
+
// Smoke tests for the AithosSDK construction surface (post-J6 wiring).
|
|
9
4
|
import { strict as assert } from "node:assert";
|
|
10
5
|
import { describe, it } from "node:test";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
function
|
|
16
|
-
return {
|
|
6
|
+
import { createBrowserIdentity } from "@aithos/protocol-client";
|
|
7
|
+
import { AithosAuth, AithosSDK, AithosSDKError, DEFAULT_SDK_ENDPOINTS, memoryKeyStore, noopStore, VERSION, } from "../src/index.js";
|
|
8
|
+
import { serializeRecoveryFile } from "../src/internal/recovery-file.js";
|
|
9
|
+
const APP_DID = "did:aithos:app:smoke";
|
|
10
|
+
function makeAuth() {
|
|
11
|
+
return new AithosAuth({
|
|
12
|
+
authBaseUrl: "https://auth.test",
|
|
13
|
+
fetch: (() => {
|
|
14
|
+
throw new Error("network not expected");
|
|
15
|
+
}),
|
|
16
|
+
sessionStore: noopStore(),
|
|
17
|
+
keyStore: memoryKeyStore(),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async function signInAsAlice(auth) {
|
|
21
|
+
const id = createBrowserIdentity("alice", "Alice");
|
|
22
|
+
const { text } = serializeRecoveryFile(id);
|
|
23
|
+
const info = await auth.signInWithRecovery({ file: text });
|
|
24
|
+
return info.did;
|
|
17
25
|
}
|
|
18
26
|
describe("VERSION", () => {
|
|
19
27
|
it("is a non-empty semver-ish string", () => {
|
|
@@ -28,46 +36,53 @@ describe("DEFAULT_SDK_ENDPOINTS", () => {
|
|
|
28
36
|
});
|
|
29
37
|
});
|
|
30
38
|
describe("AithosSDK constructor", () => {
|
|
31
|
-
|
|
32
|
-
const appDid = "did:aithos:app:smoke";
|
|
33
|
-
it("requires an identity", () => {
|
|
39
|
+
it("requires an auth instance", () => {
|
|
34
40
|
assert.throws(() =>
|
|
35
41
|
// @ts-expect-error: deliberately passing an invalid config
|
|
36
|
-
new AithosSDK({ appDid }), /
|
|
42
|
+
new AithosSDK({ appDid: APP_DID }), /auth/);
|
|
37
43
|
});
|
|
38
44
|
it("requires an appDid", () => {
|
|
39
45
|
assert.throws(() =>
|
|
40
46
|
// @ts-expect-error: deliberately passing an invalid config
|
|
41
|
-
new AithosSDK({
|
|
47
|
+
new AithosSDK({ auth: makeAuth() }), /appDid/);
|
|
42
48
|
});
|
|
43
|
-
it("exposes endpoints, appDid, userDid", () => {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
it("exposes endpoints, appDid, auth, userDid", async () => {
|
|
50
|
+
const auth = makeAuth();
|
|
51
|
+
const did = await signInAsAlice(auth);
|
|
52
|
+
const sdk = new AithosSDK({ auth, appDid: APP_DID });
|
|
53
|
+
assert.equal(sdk.appDid, APP_DID);
|
|
54
|
+
assert.equal(sdk.userDid, did);
|
|
55
|
+
assert.equal(sdk.auth, auth);
|
|
47
56
|
assert.equal(sdk.endpoints.compute, DEFAULT_SDK_ENDPOINTS.compute);
|
|
48
57
|
assert.equal(sdk.endpoints.wallet, DEFAULT_SDK_ENDPOINTS.wallet);
|
|
49
58
|
});
|
|
59
|
+
it("userDid is null when no owner is signed in", () => {
|
|
60
|
+
const auth = makeAuth();
|
|
61
|
+
const sdk = new AithosSDK({ auth, appDid: APP_DID });
|
|
62
|
+
assert.equal(sdk.userDid, null);
|
|
63
|
+
});
|
|
50
64
|
it("merges endpoint overrides on top of defaults", () => {
|
|
65
|
+
const auth = makeAuth();
|
|
51
66
|
const sdk = new AithosSDK({
|
|
52
|
-
|
|
53
|
-
appDid,
|
|
67
|
+
auth,
|
|
68
|
+
appDid: APP_DID,
|
|
54
69
|
endpoints: { wallet: "https://staging-wallet.aithos.be" },
|
|
55
70
|
});
|
|
56
71
|
assert.equal(sdk.endpoints.compute, DEFAULT_SDK_ENDPOINTS.compute);
|
|
57
72
|
assert.equal(sdk.endpoints.wallet, "https://staging-wallet.aithos.be");
|
|
58
73
|
});
|
|
59
74
|
it("trims trailing slashes on endpoint URLs at compose time", async () => {
|
|
60
|
-
|
|
75
|
+
const auth = makeAuth();
|
|
76
|
+
await signInAsAlice(auth);
|
|
61
77
|
let capturedUrl;
|
|
62
78
|
const fakeFetch = async (input) => {
|
|
63
79
|
capturedUrl = typeof input === "string" ? input : input.toString();
|
|
64
|
-
// Reject so we don't have to construct a full happy-path response.
|
|
65
80
|
throw new Error("captured");
|
|
66
81
|
};
|
|
67
82
|
const sdk = new AithosSDK({
|
|
68
|
-
|
|
69
|
-
appDid,
|
|
70
|
-
endpoints: { wallet: "https://wallet.example.com/" },
|
|
83
|
+
auth,
|
|
84
|
+
appDid: APP_DID,
|
|
85
|
+
endpoints: { wallet: "https://wallet.example.com/" },
|
|
71
86
|
fetch: fakeFetch,
|
|
72
87
|
});
|
|
73
88
|
await assert.rejects(sdk.wallet.createTopupSession({
|
|
@@ -77,10 +92,29 @@ describe("AithosSDK constructor", () => {
|
|
|
77
92
|
}), AithosSDKError);
|
|
78
93
|
assert.equal(capturedUrl, "https://wallet.example.com/v1/wallet/topup/checkout");
|
|
79
94
|
});
|
|
80
|
-
it("exposes namespaces compute, wallet", () => {
|
|
81
|
-
const
|
|
95
|
+
it("exposes namespaces compute, wallet, ethos, mandates", async () => {
|
|
96
|
+
const auth = makeAuth();
|
|
97
|
+
await signInAsAlice(auth);
|
|
98
|
+
const sdk = new AithosSDK({ auth, appDid: APP_DID });
|
|
82
99
|
assert.equal(typeof sdk.compute.invokeBedrock, "function");
|
|
83
100
|
assert.equal(typeof sdk.wallet.createTopupSession, "function");
|
|
101
|
+
assert.equal(typeof sdk.ethos.me, "function");
|
|
102
|
+
assert.equal(typeof sdk.mandates.create, "function");
|
|
103
|
+
});
|
|
104
|
+
it("compute.invokeBedrock rejects when no owner is signed in", async () => {
|
|
105
|
+
const auth = makeAuth();
|
|
106
|
+
const sdk = new AithosSDK({
|
|
107
|
+
auth,
|
|
108
|
+
appDid: APP_DID,
|
|
109
|
+
fetch: (() => {
|
|
110
|
+
throw new Error("not expected");
|
|
111
|
+
}),
|
|
112
|
+
});
|
|
113
|
+
await assert.rejects(sdk.compute.invokeBedrock({
|
|
114
|
+
mandateId: "mandate:x",
|
|
115
|
+
model: "claude-sonnet-4-6",
|
|
116
|
+
messages: [{ role: "user", content: "hi" }],
|
|
117
|
+
}), (e) => e instanceof AithosSDKError && e.code === "sdk_no_owner");
|
|
84
118
|
});
|
|
85
119
|
});
|
|
86
120
|
//# sourceMappingURL=sdk.test.js.map
|