@aithos/sdk 0.1.0-alpha.3 → 0.1.0-alpha.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +159 -0
- package/dist/src/auth-api.d.ts +105 -0
- package/dist/src/auth-api.js +178 -0
- package/dist/src/auth.d.ts +359 -68
- package/dist/src/auth.js +1035 -69
- package/dist/src/compute.d.ts +221 -9
- package/dist/src/compute.js +293 -16
- package/dist/src/data-schema-contacts-v1.d.ts +14 -0
- package/dist/src/data-schema-contacts-v1.js +28 -0
- package/dist/src/data.d.ts +97 -0
- package/dist/src/data.js +634 -0
- package/dist/src/endpoints.d.ts +9 -0
- package/dist/src/endpoints.js +5 -0
- package/dist/src/ethos.d.ts +202 -1
- package/dist/src/ethos.js +821 -16
- package/dist/src/index.d.ts +15 -6
- package/dist/src/index.js +36 -9
- package/dist/src/internal/delegate-bundle.d.ts +18 -0
- package/dist/src/internal/delegate-bundle.js +94 -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 +163 -1
- package/dist/src/mandates.js +286 -8
- package/dist/src/sdk.d.ts +39 -3
- package/dist/src/sdk.js +36 -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/src/web.d.ts +279 -0
- package/dist/src/web.js +186 -0
- package/dist/test/auth-j3.test.d.ts +2 -0
- package/dist/test/auth-j3.test.js +391 -0
- package/dist/test/compute-delegate-path.test.d.ts +2 -0
- package/dist/test/compute-delegate-path.test.js +183 -0
- package/dist/test/compute.test.js +26 -11
- package/dist/test/endpoints.test.js +20 -1
- package/dist/test/ethos-first-edition.test.d.ts +2 -0
- package/dist/test/ethos-first-edition.test.js +248 -0
- 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-compute.test.d.ts +2 -0
- package/dist/test/mandates-compute.test.js +256 -0
- package/dist/test/mandates.test.d.ts +2 -0
- package/dist/test/mandates.test.js +93 -0
- package/dist/test/sdk.test.js +70 -30
- package/dist/test/signer.test.d.ts +2 -0
- package/dist/test/signer.test.js +117 -0
- package/dist/test/signup-bootstrap.test.d.ts +2 -0
- package/dist/test/signup-bootstrap.test.js +222 -0
- package/dist/test/wallet.test.js +20 -9
- package/dist/test/web.test.d.ts +2 -0
- package/dist/test/web.test.js +270 -0
- package/package.json +5 -4
|
@@ -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,35 @@ 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 AND no delegate matches the mandate", async () => {
|
|
105
|
+
// Since alpha.9, invokeBedrock supports a delegate signing path. The
|
|
106
|
+
// failure mode here (no owner loaded AND no imported mandate matching
|
|
107
|
+
// the requested id) returns code `sdk_no_delegate_for_mandate`.
|
|
108
|
+
// The previous `sdk_no_owner` code now applies only to other entry
|
|
109
|
+
// points where there's no delegate fallback.
|
|
110
|
+
const auth = makeAuth();
|
|
111
|
+
const sdk = new AithosSDK({
|
|
112
|
+
auth,
|
|
113
|
+
appDid: APP_DID,
|
|
114
|
+
fetch: (() => {
|
|
115
|
+
throw new Error("not expected");
|
|
116
|
+
}),
|
|
117
|
+
});
|
|
118
|
+
await assert.rejects(sdk.compute.invokeBedrock({
|
|
119
|
+
mandateId: "mandate:x",
|
|
120
|
+
model: "claude-sonnet-4-6",
|
|
121
|
+
messages: [{ role: "user", content: "hi" }],
|
|
122
|
+
}), (e) => e instanceof AithosSDKError &&
|
|
123
|
+
e.code === "sdk_no_delegate_for_mandate");
|
|
84
124
|
});
|
|
85
125
|
});
|
|
86
126
|
//# sourceMappingURL=sdk.test.js.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
// Unit tests for the internal Signer abstraction.
|
|
4
|
+
import { strict as assert } from "node:assert";
|
|
5
|
+
import { describe, it } from "node:test";
|
|
6
|
+
import { createBrowserIdentity, verify as ed25519Verify, } from "@aithos/protocol-client";
|
|
7
|
+
import { RawSeedSigner } from "../src/internal/signer.js";
|
|
8
|
+
import { OwnerSigners } from "../src/internal/owner-signers.js";
|
|
9
|
+
/* -------------------------------------------------------------------------- */
|
|
10
|
+
/* RawSeedSigner */
|
|
11
|
+
/* -------------------------------------------------------------------------- */
|
|
12
|
+
describe("RawSeedSigner", () => {
|
|
13
|
+
it("signs a message that verifies under its public key", async () => {
|
|
14
|
+
const id = createBrowserIdentity("alice", "Alice");
|
|
15
|
+
const signer = new RawSeedSigner(id.public.seed, id.public.publicKey);
|
|
16
|
+
const msg = new TextEncoder().encode("hello aithos");
|
|
17
|
+
const sig = await signer.sign(msg);
|
|
18
|
+
assert.equal(sig.length, 64, "Ed25519 signatures are 64 bytes");
|
|
19
|
+
assert.ok(ed25519Verify(sig, msg, signer.publicKey), "signature should verify under the signer's public key");
|
|
20
|
+
});
|
|
21
|
+
it("defensively copies the seed — mutating the input has no effect", async () => {
|
|
22
|
+
const id = createBrowserIdentity("alice", "Alice");
|
|
23
|
+
const seedCopy = new Uint8Array(id.public.seed);
|
|
24
|
+
const signer = new RawSeedSigner(id.public.seed, id.public.publicKey);
|
|
25
|
+
// Tamper with the original seed buffer the caller passed in.
|
|
26
|
+
id.public.seed.fill(0xff);
|
|
27
|
+
const msg = new TextEncoder().encode("hello");
|
|
28
|
+
const sig = await signer.sign(msg);
|
|
29
|
+
// Recompute with the pristine seed → must match.
|
|
30
|
+
const expected = await new RawSeedSigner(seedCopy, id.public.publicKey).sign(msg);
|
|
31
|
+
assert.deepEqual(sig, expected, "signer must use its own copy of the seed");
|
|
32
|
+
});
|
|
33
|
+
it("destroy zeroizes and blocks further use", async () => {
|
|
34
|
+
const id = createBrowserIdentity("alice", "Alice");
|
|
35
|
+
const signer = new RawSeedSigner(id.public.seed, id.public.publicKey);
|
|
36
|
+
signer.destroy();
|
|
37
|
+
await assert.rejects(() => signer.sign(new Uint8Array(1)), /destroyed/, "sign() must reject after destroy()");
|
|
38
|
+
assert.throws(() => signer._unsafeKeyPair(), /destroyed/);
|
|
39
|
+
// Idempotent — second destroy() is a no-op.
|
|
40
|
+
assert.doesNotThrow(() => signer.destroy());
|
|
41
|
+
});
|
|
42
|
+
it("rejects malformed seed/public-key sizes", () => {
|
|
43
|
+
const id = createBrowserIdentity("alice", "Alice");
|
|
44
|
+
assert.throws(() => new RawSeedSigner(new Uint8Array(31), id.public.publicKey), /seed must be 32 bytes/);
|
|
45
|
+
assert.throws(() => new RawSeedSigner(id.public.seed, new Uint8Array(33)), /publicKey must be 32 bytes/);
|
|
46
|
+
});
|
|
47
|
+
it("_unsafeKeyPair returns a KeyPair compatible with protocol-client", async () => {
|
|
48
|
+
const id = createBrowserIdentity("alice", "Alice");
|
|
49
|
+
const signer = new RawSeedSigner(id.public.seed, id.public.publicKey);
|
|
50
|
+
const kp = signer._unsafeKeyPair();
|
|
51
|
+
assert.equal(kp.seed.length, 32);
|
|
52
|
+
assert.equal(kp.publicKey.length, 32);
|
|
53
|
+
assert.deepEqual(kp.publicKey, signer.publicKey);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
/* -------------------------------------------------------------------------- */
|
|
57
|
+
/* OwnerSigners */
|
|
58
|
+
/* -------------------------------------------------------------------------- */
|
|
59
|
+
describe("OwnerSigners", () => {
|
|
60
|
+
it("fromBrowserIdentity yields four working signers", async () => {
|
|
61
|
+
const id = createBrowserIdentity("alice", "Alice");
|
|
62
|
+
const signers = OwnerSigners.fromBrowserIdentity(id);
|
|
63
|
+
assert.equal(signers.did, id.did);
|
|
64
|
+
assert.equal(signers.handle, id.handle);
|
|
65
|
+
assert.equal(signers.displayName, id.displayName);
|
|
66
|
+
for (const sphere of ["root", "public", "circle", "self"]) {
|
|
67
|
+
const sig = await signers[sphere].sign(new TextEncoder().encode(sphere));
|
|
68
|
+
assert.equal(sig.length, 64, `${sphere}: sig length`);
|
|
69
|
+
assert.ok(ed25519Verify(sig, new TextEncoder().encode(sphere), signers[sphere].publicKey), `${sphere}: signature must verify`);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
it("signerForSphere maps strings to the right signer", () => {
|
|
73
|
+
const id = createBrowserIdentity("alice", "Alice");
|
|
74
|
+
const signers = OwnerSigners.fromBrowserIdentity(id);
|
|
75
|
+
assert.strictEqual(signers.signerForSphere("root"), signers.root);
|
|
76
|
+
assert.strictEqual(signers.signerForSphere("public"), signers.public);
|
|
77
|
+
assert.strictEqual(signers.signerForSphere("circle"), signers.circle);
|
|
78
|
+
assert.strictEqual(signers.signerForSphere("self"), signers.self);
|
|
79
|
+
});
|
|
80
|
+
it("destroy zeroizes all signers", async () => {
|
|
81
|
+
const id = createBrowserIdentity("alice", "Alice");
|
|
82
|
+
const signers = OwnerSigners.fromBrowserIdentity(id);
|
|
83
|
+
signers.destroy();
|
|
84
|
+
assert.equal(signers.destroyed, true);
|
|
85
|
+
await assert.rejects(() => signers.public.sign(new Uint8Array(1)), /destroyed/);
|
|
86
|
+
// Idempotent.
|
|
87
|
+
assert.doesNotThrow(() => signers.destroy());
|
|
88
|
+
});
|
|
89
|
+
it("fromStoredIdentity round-trips through hex", async () => {
|
|
90
|
+
const id = createBrowserIdentity("alice", "Alice");
|
|
91
|
+
const stored = {
|
|
92
|
+
version: "0.1.0",
|
|
93
|
+
handle: id.handle,
|
|
94
|
+
displayName: id.displayName,
|
|
95
|
+
did: id.did,
|
|
96
|
+
seeds: {
|
|
97
|
+
root: bytesToHex(id.root.seed),
|
|
98
|
+
public: bytesToHex(id.public.seed),
|
|
99
|
+
circle: bytesToHex(id.circle.seed),
|
|
100
|
+
self: bytesToHex(id.self.seed),
|
|
101
|
+
},
|
|
102
|
+
savedAt: new Date().toISOString(),
|
|
103
|
+
};
|
|
104
|
+
const signers = OwnerSigners.fromStoredIdentity(stored);
|
|
105
|
+
const sig = await signers.public.sign(new TextEncoder().encode("test"));
|
|
106
|
+
assert.ok(ed25519Verify(sig, new TextEncoder().encode("test"), signers.public.publicKey));
|
|
107
|
+
// The pubkey must match what we'd derive from the original BrowserIdentity.
|
|
108
|
+
assert.deepEqual(signers.public.publicKey, id.public.publicKey);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
function bytesToHex(b) {
|
|
112
|
+
let out = "";
|
|
113
|
+
for (let i = 0; i < b.length; i++)
|
|
114
|
+
out += b[i].toString(16).padStart(2, "0");
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=signer.test.js.map
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
// Tests for the Ethos bootstrap step inside AithosAuth.signUp().
|
|
4
|
+
//
|
|
5
|
+
// The contract:
|
|
6
|
+
// 1. POST /auth/register → creates the auth user (auth.aithos.be).
|
|
7
|
+
// 2. POST /mcp/primitives/write with method=aithos.publish_identity →
|
|
8
|
+
// provisions the user's Ethos on api.aithos.be.
|
|
9
|
+
// 3. Hydrate local state ONLY after both steps succeed.
|
|
10
|
+
//
|
|
11
|
+
// Failure modes:
|
|
12
|
+
// - register fails → throw, no publish_identity attempted, no hydrate.
|
|
13
|
+
// - publish_identity rejected (JSON-RPC error) → throw immediately,
|
|
14
|
+
// no retry, no hydrate.
|
|
15
|
+
// - publish_identity 5xx / network error → 2 retries with backoff,
|
|
16
|
+
// then throw `ethos_bootstrap_failed` if all fail.
|
|
17
|
+
//
|
|
18
|
+
// We mock fetch end-to-end so the tests run offline. The protocol-client
|
|
19
|
+
// crypto runs for real — we want to assert that the envelope shape coming
|
|
20
|
+
// out of the SDK matches what api.aithos.be will accept.
|
|
21
|
+
import { strict as assert } from "node:assert";
|
|
22
|
+
import { describe, it } from "node:test";
|
|
23
|
+
import { AithosAuth, AithosSDKError, memoryKeyStore, noopStore, } from "../src/index.js";
|
|
24
|
+
function makeMockFetch(handlers) {
|
|
25
|
+
const calls = [];
|
|
26
|
+
const fetchImpl = (async (input, init) => {
|
|
27
|
+
const url = String(input);
|
|
28
|
+
const method = init?.method ?? "GET";
|
|
29
|
+
const bodyText = typeof init?.body === "string"
|
|
30
|
+
? init.body
|
|
31
|
+
: init?.body == null
|
|
32
|
+
? null
|
|
33
|
+
: String(init.body);
|
|
34
|
+
const body = bodyText ? JSON.parse(bodyText) : null;
|
|
35
|
+
const call = { url, method, body };
|
|
36
|
+
calls.push(call);
|
|
37
|
+
for (const h of handlers) {
|
|
38
|
+
if (!url.includes(h.url))
|
|
39
|
+
continue;
|
|
40
|
+
if (h.method && h.method !== method)
|
|
41
|
+
continue;
|
|
42
|
+
if (h.remaining !== undefined && h.remaining <= 0)
|
|
43
|
+
continue;
|
|
44
|
+
if (h.remaining !== undefined)
|
|
45
|
+
h.remaining--;
|
|
46
|
+
const out = await h.respond(call);
|
|
47
|
+
const status = out.status ?? 200;
|
|
48
|
+
return new Response(JSON.stringify(out.json), {
|
|
49
|
+
status,
|
|
50
|
+
headers: { "content-type": "application/json" },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`unhandled fetch: ${method} ${url}`);
|
|
54
|
+
});
|
|
55
|
+
return { fetch: fetchImpl, calls };
|
|
56
|
+
}
|
|
57
|
+
function fakeRegisterOk() {
|
|
58
|
+
return {
|
|
59
|
+
json: {
|
|
60
|
+
session: "jwt-token-here",
|
|
61
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function fakePublishIdentityOk() {
|
|
66
|
+
return {
|
|
67
|
+
json: {
|
|
68
|
+
jsonrpc: "2.0",
|
|
69
|
+
id: "publish_identity",
|
|
70
|
+
result: { ok: true, did_document_url: "https://cdn.aithos.be/ethos/zABC/did.json" },
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function makeAuth(fetchImpl) {
|
|
75
|
+
return new AithosAuth({
|
|
76
|
+
authBaseUrl: "https://auth.test",
|
|
77
|
+
apiBaseUrl: "https://api.test",
|
|
78
|
+
fetch: fetchImpl,
|
|
79
|
+
sessionStore: noopStore(),
|
|
80
|
+
keyStore: memoryKeyStore(),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const validInput = {
|
|
84
|
+
email: "alice@test.example",
|
|
85
|
+
password: "correct horse battery staple",
|
|
86
|
+
handle: "alice",
|
|
87
|
+
};
|
|
88
|
+
/* -------------------------------------------------------------------------- */
|
|
89
|
+
/* Happy path */
|
|
90
|
+
/* -------------------------------------------------------------------------- */
|
|
91
|
+
describe("AithosAuth.signUp — Ethos bootstrap", () => {
|
|
92
|
+
it("calls /auth/register THEN /mcp/primitives/write with publish_identity", async () => {
|
|
93
|
+
const { fetch: f, calls } = makeMockFetch([
|
|
94
|
+
{ url: "/auth/register", method: "POST", respond: fakeRegisterOk },
|
|
95
|
+
{
|
|
96
|
+
url: "/mcp/primitives/write",
|
|
97
|
+
method: "POST",
|
|
98
|
+
respond: fakePublishIdentityOk,
|
|
99
|
+
},
|
|
100
|
+
]);
|
|
101
|
+
const auth = makeAuth(f);
|
|
102
|
+
const r = await auth.signUp(validInput);
|
|
103
|
+
assert.equal(calls.length, 2, "should make exactly 2 calls");
|
|
104
|
+
assert.match(calls[0].url, /\/auth\/register$/);
|
|
105
|
+
assert.match(calls[1].url, /\/mcp\/primitives\/write$/);
|
|
106
|
+
assert.ok(r.session.session === "jwt-token-here");
|
|
107
|
+
assert.ok(auth.canSignAsOwner(), "must be hydrated as owner");
|
|
108
|
+
});
|
|
109
|
+
it("envelope is JSON-RPC publish_identity, signed by #root, with valid params", async () => {
|
|
110
|
+
let publishBody = null;
|
|
111
|
+
const { fetch: f } = makeMockFetch([
|
|
112
|
+
{ url: "/auth/register", method: "POST", respond: fakeRegisterOk },
|
|
113
|
+
{
|
|
114
|
+
url: "/mcp/primitives/write",
|
|
115
|
+
method: "POST",
|
|
116
|
+
respond: (call) => {
|
|
117
|
+
publishBody = call.body;
|
|
118
|
+
return fakePublishIdentityOk();
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
]);
|
|
122
|
+
await makeAuth(f).signUp(validInput);
|
|
123
|
+
// JSON-RPC envelope shape
|
|
124
|
+
assert.equal(publishBody.jsonrpc, "2.0");
|
|
125
|
+
assert.equal(publishBody.method, "aithos.publish_identity");
|
|
126
|
+
// params include the inline _envelope and the publish_identity payload
|
|
127
|
+
const params = publishBody.params;
|
|
128
|
+
assert.equal(typeof params.handle, "string");
|
|
129
|
+
assert.equal(params.handle, "alice");
|
|
130
|
+
assert.equal(typeof params.display_name, "string");
|
|
131
|
+
assert.ok(params.did_document, "did_document must be present");
|
|
132
|
+
assert.ok(params._envelope, "_envelope must be present");
|
|
133
|
+
// envelope is signed by #root
|
|
134
|
+
const env = params._envelope;
|
|
135
|
+
assert.equal(env["aithos-envelope"], "0.1.0");
|
|
136
|
+
assert.match(env.iss, /^did:aithos:/);
|
|
137
|
+
assert.equal(env.method, "aithos.publish_identity");
|
|
138
|
+
assert.equal(env.aud, "https://api.test/mcp/primitives/write");
|
|
139
|
+
assert.equal(env.proof.type, "Ed25519Signature2020");
|
|
140
|
+
assert.match(env.proof.verificationMethod, /#root$/);
|
|
141
|
+
assert.equal(typeof env.proof.proofValue, "string");
|
|
142
|
+
assert.ok(env.proof.proofValue.length > 0);
|
|
143
|
+
});
|
|
144
|
+
it("does NOT hydrate state when /auth/register fails", async () => {
|
|
145
|
+
const { fetch: f, calls } = makeMockFetch([
|
|
146
|
+
{
|
|
147
|
+
url: "/auth/register",
|
|
148
|
+
method: "POST",
|
|
149
|
+
respond: () => ({
|
|
150
|
+
status: 409,
|
|
151
|
+
json: { error: "email_taken" },
|
|
152
|
+
}),
|
|
153
|
+
},
|
|
154
|
+
]);
|
|
155
|
+
const auth = makeAuth(f);
|
|
156
|
+
await assert.rejects(() => auth.signUp(validInput), AithosSDKError);
|
|
157
|
+
assert.equal(calls.length, 1, "publish_identity must NOT be called");
|
|
158
|
+
assert.equal(auth.canSignAsOwner(), false);
|
|
159
|
+
});
|
|
160
|
+
it("does NOT hydrate state when publish_identity returns a JSON-RPC error", async () => {
|
|
161
|
+
const { fetch: f, calls } = makeMockFetch([
|
|
162
|
+
{ url: "/auth/register", method: "POST", respond: fakeRegisterOk },
|
|
163
|
+
{
|
|
164
|
+
url: "/mcp/primitives/write",
|
|
165
|
+
method: "POST",
|
|
166
|
+
respond: () => ({
|
|
167
|
+
json: {
|
|
168
|
+
jsonrpc: "2.0",
|
|
169
|
+
id: "publish_identity",
|
|
170
|
+
error: { code: -32600, message: "invalid envelope signature" },
|
|
171
|
+
},
|
|
172
|
+
}),
|
|
173
|
+
},
|
|
174
|
+
]);
|
|
175
|
+
const auth = makeAuth(f);
|
|
176
|
+
await assert.rejects(() => auth.signUp(validInput), (e) => e instanceof AithosSDKError && e.code === "ethos_bootstrap_failed");
|
|
177
|
+
// No retry on JSON-RPC error: 1 register + 1 publish.
|
|
178
|
+
assert.equal(calls.length, 2);
|
|
179
|
+
assert.equal(auth.canSignAsOwner(), false);
|
|
180
|
+
});
|
|
181
|
+
it("retries publish_identity on 5xx, then succeeds", async () => {
|
|
182
|
+
let publishCalls = 0;
|
|
183
|
+
const { fetch: f } = makeMockFetch([
|
|
184
|
+
{ url: "/auth/register", method: "POST", respond: fakeRegisterOk },
|
|
185
|
+
{
|
|
186
|
+
url: "/mcp/primitives/write",
|
|
187
|
+
method: "POST",
|
|
188
|
+
respond: () => {
|
|
189
|
+
publishCalls++;
|
|
190
|
+
if (publishCalls < 2) {
|
|
191
|
+
return { status: 503, json: { error: "transient" } };
|
|
192
|
+
}
|
|
193
|
+
return fakePublishIdentityOk();
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
]);
|
|
197
|
+
const auth = makeAuth(f);
|
|
198
|
+
await auth.signUp(validInput);
|
|
199
|
+
assert.equal(publishCalls, 2);
|
|
200
|
+
assert.ok(auth.canSignAsOwner());
|
|
201
|
+
});
|
|
202
|
+
it("throws ethos_bootstrap_failed after all retries fail with 5xx", async () => {
|
|
203
|
+
let publishCalls = 0;
|
|
204
|
+
const { fetch: f } = makeMockFetch([
|
|
205
|
+
{ url: "/auth/register", method: "POST", respond: fakeRegisterOk },
|
|
206
|
+
{
|
|
207
|
+
url: "/mcp/primitives/write",
|
|
208
|
+
method: "POST",
|
|
209
|
+
respond: () => {
|
|
210
|
+
publishCalls++;
|
|
211
|
+
return { status: 503, json: { error: "transient" } };
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
]);
|
|
215
|
+
const auth = makeAuth(f);
|
|
216
|
+
await assert.rejects(() => auth.signUp(validInput), (e) => e instanceof AithosSDKError && e.code === "ethos_bootstrap_failed");
|
|
217
|
+
// 3 attempts total (initial + 2 retries).
|
|
218
|
+
assert.equal(publishCalls, 3);
|
|
219
|
+
assert.equal(auth.canSignAsOwner(), false);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
//# sourceMappingURL=signup-bootstrap.test.js.map
|
package/dist/test/wallet.test.js
CHANGED
|
@@ -4,12 +4,23 @@
|
|
|
4
4
|
import { strict as assert } from "node:assert";
|
|
5
5
|
import { describe, it } from "node:test";
|
|
6
6
|
import { createBrowserIdentity } from "@aithos/protocol-client";
|
|
7
|
-
import { AithosSDK, AithosSDKError } from "../src/index.js";
|
|
7
|
+
import { AithosAuth, AithosSDK, AithosSDKError, memoryKeyStore, noopStore, } from "../src/index.js";
|
|
8
|
+
import { serializeRecoveryFile } from "../src/internal/recovery-file.js";
|
|
8
9
|
const APP_DID = "did:aithos:app:test";
|
|
9
|
-
function makeSdk(fetchImpl) {
|
|
10
|
-
const
|
|
10
|
+
async function makeSdk(fetchImpl) {
|
|
11
|
+
const id = createBrowserIdentity("test-handle", "Test User");
|
|
12
|
+
const auth = new AithosAuth({
|
|
13
|
+
authBaseUrl: "https://auth.test",
|
|
14
|
+
fetch: (() => {
|
|
15
|
+
throw new Error("auth not used in wallet tests");
|
|
16
|
+
}),
|
|
17
|
+
sessionStore: noopStore(),
|
|
18
|
+
keyStore: memoryKeyStore(),
|
|
19
|
+
});
|
|
20
|
+
const { text } = serializeRecoveryFile(id);
|
|
21
|
+
await auth.signInWithRecovery({ file: text });
|
|
11
22
|
return new AithosSDK({
|
|
12
|
-
|
|
23
|
+
auth,
|
|
13
24
|
appDid: APP_DID,
|
|
14
25
|
endpoints: { wallet: "https://wallet.example.test" },
|
|
15
26
|
fetch: fetchImpl,
|
|
@@ -27,7 +38,7 @@ describe("wallet.createTopupSession — happy path", () => {
|
|
|
27
38
|
session_id: "cs_test_xyz",
|
|
28
39
|
}), { status: 200, headers: { "content-type": "application/json" } });
|
|
29
40
|
};
|
|
30
|
-
const sdk = makeSdk(fakeFetch);
|
|
41
|
+
const sdk = await makeSdk(fakeFetch);
|
|
31
42
|
const out = await sdk.wallet.createTopupSession({
|
|
32
43
|
packId: "credits-1m",
|
|
33
44
|
successUrl: "https://app.example.com/?topup=success",
|
|
@@ -47,7 +58,7 @@ describe("wallet.createTopupSession — errors", () => {
|
|
|
47
58
|
const fakeFetch = async () => {
|
|
48
59
|
throw new TypeError("Failed to fetch");
|
|
49
60
|
};
|
|
50
|
-
const sdk = makeSdk(fakeFetch);
|
|
61
|
+
const sdk = await makeSdk(fakeFetch);
|
|
51
62
|
await assert.rejects(sdk.wallet.createTopupSession({
|
|
52
63
|
packId: "credits-100k",
|
|
53
64
|
successUrl: "https://app.example.com/?ok",
|
|
@@ -60,7 +71,7 @@ describe("wallet.createTopupSession — errors", () => {
|
|
|
60
71
|
});
|
|
61
72
|
it("surfaces the proxy's structured error (error/detail) on a 4xx", async () => {
|
|
62
73
|
const fakeFetch = async () => new Response(JSON.stringify({ error: "unknown_pack", pack_id: "credits-9999" }), { status: 400, headers: { "content-type": "application/json" } });
|
|
63
|
-
const sdk = makeSdk(fakeFetch);
|
|
74
|
+
const sdk = await makeSdk(fakeFetch);
|
|
64
75
|
await assert.rejects(sdk.wallet.createTopupSession({
|
|
65
76
|
// @ts-expect-error: deliberately invalid pack id
|
|
66
77
|
packId: "credits-9999",
|
|
@@ -78,7 +89,7 @@ describe("wallet.createTopupSession — errors", () => {
|
|
|
78
89
|
status: 500,
|
|
79
90
|
headers: { "content-type": "text/html" },
|
|
80
91
|
});
|
|
81
|
-
const sdk = makeSdk(fakeFetch);
|
|
92
|
+
const sdk = await makeSdk(fakeFetch);
|
|
82
93
|
await assert.rejects(sdk.wallet.createTopupSession({
|
|
83
94
|
packId: "credits-100k",
|
|
84
95
|
successUrl: "https://app.example.com/?ok",
|
|
@@ -95,7 +106,7 @@ describe("wallet.createTopupSession — errors", () => {
|
|
|
95
106
|
status: 200,
|
|
96
107
|
headers: { "content-type": "application/json" },
|
|
97
108
|
});
|
|
98
|
-
const sdk = makeSdk(fakeFetch);
|
|
109
|
+
const sdk = await makeSdk(fakeFetch);
|
|
99
110
|
await assert.rejects(sdk.wallet.createTopupSession({
|
|
100
111
|
packId: "credits-100k",
|
|
101
112
|
successUrl: "https://app.example.com/?ok",
|