@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,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
@@ -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 identity = createBrowserIdentity("test-handle", "Test User");
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
- identity,
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.2",
3
+ "version": "0.1.0-alpha.21",
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.7 <0.2.0"
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.7",
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": {