@aithos/sdk 0.1.0-alpha.2 → 0.1.0-alpha.20
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 +45 -0
- package/dist/src/auth-api.d.ts +50 -0
- package/dist/src/auth-api.js +102 -0
- package/dist/src/auth.d.ts +253 -0
- package/dist/src/auth.js +940 -0
- package/dist/src/compute.d.ts +370 -9
- package/dist/src/compute.js +369 -16
- package/dist/src/ethos.d.ts +117 -1
- package/dist/src/ethos.js +646 -16
- package/dist/src/index.d.ts +11 -4
- package/dist/src/index.js +31 -5
- 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 +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 +391 -0
- package/dist/test/auth.test.d.ts +2 -0
- package/dist/test/auth.test.js +175 -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 +184 -11
- 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/package.json +4 -3
|
@@ -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",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aithos/sdk",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.20",
|
|
4
4
|
"description": "Aithos SDK — high-level TypeScript developer kit for building agentic apps on the Aithos protocol. Wraps @aithos/protocol-client and exposes the Aithos compute proxy and wallet (Stripe top-up) endpoints.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"aithos",
|
|
@@ -52,11 +52,12 @@
|
|
|
52
52
|
"node": ">=20"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"@aithos/protocol-client": ">=0.1.0-alpha.
|
|
55
|
+
"@aithos/protocol-client": ">=0.1.0-alpha.13 <0.2.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@aithos/protocol-client": "^0.1.0-alpha.
|
|
58
|
+
"@aithos/protocol-client": "^0.1.0-alpha.13",
|
|
59
59
|
"@types/node": "^24.12.2",
|
|
60
|
+
"fake-indexeddb": "^6.2.5",
|
|
60
61
|
"typescript": "^5.9.2"
|
|
61
62
|
},
|
|
62
63
|
"publishConfig": {
|