@aithos/sdk 0.1.0-alpha.6 → 0.1.0-alpha.60

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 (105) hide show
  1. package/README.md +202 -7
  2. package/dist/src/agent-dispatch.d.ts +18 -0
  3. package/dist/src/agent-dispatch.js +178 -0
  4. package/dist/src/agent-loop.d.ts +94 -0
  5. package/dist/src/agent-loop.js +95 -0
  6. package/dist/src/agent-tools.d.ts +24 -0
  7. package/dist/src/agent-tools.js +147 -0
  8. package/dist/src/apps.d.ts +224 -0
  9. package/dist/src/apps.js +432 -0
  10. package/dist/src/assets.d.ts +225 -0
  11. package/dist/src/assets.js +534 -0
  12. package/dist/src/auth-api.d.ts +219 -0
  13. package/dist/src/auth-api.js +248 -0
  14. package/dist/src/auth.d.ts +591 -0
  15. package/dist/src/auth.js +947 -31
  16. package/dist/src/compute.d.ts +674 -6
  17. package/dist/src/compute.js +968 -20
  18. package/dist/src/data-schema-contacts-v1.d.ts +14 -0
  19. package/dist/src/data-schema-contacts-v1.js +28 -0
  20. package/dist/src/data.d.ts +368 -0
  21. package/dist/src/data.js +1124 -0
  22. package/dist/src/endpoints.d.ts +43 -0
  23. package/dist/src/endpoints.js +23 -0
  24. package/dist/src/ethos.d.ts +85 -0
  25. package/dist/src/ethos.js +463 -7
  26. package/dist/src/index.d.ts +22 -4
  27. package/dist/src/index.js +47 -2
  28. package/dist/src/internal/cmk-wrap.d.ts +41 -0
  29. package/dist/src/internal/cmk-wrap.js +132 -0
  30. package/dist/src/internal/delegate-bundle.js +7 -2
  31. package/dist/src/internal/envelope.d.ts +93 -0
  32. package/dist/src/internal/envelope.js +59 -0
  33. package/dist/src/internal/owner-signers.d.ts +5 -2
  34. package/dist/src/internal/owner-signers.js +22 -1
  35. package/dist/src/internal/recovery-file.d.ts +2 -0
  36. package/dist/src/internal/recovery-file.js +7 -0
  37. package/dist/src/key-store.d.ts +10 -0
  38. package/dist/src/key-store.js +6 -0
  39. package/dist/src/mandates.d.ts +58 -1
  40. package/dist/src/mandates.js +46 -3
  41. package/dist/src/migrate.d.ts +105 -0
  42. package/dist/src/migrate.js +367 -0
  43. package/dist/src/react/AithosAsset.d.ts +66 -0
  44. package/dist/src/react/AithosAsset.js +67 -0
  45. package/dist/src/react/context.d.ts +29 -0
  46. package/dist/src/react/context.js +31 -0
  47. package/dist/src/react/index.d.ts +29 -0
  48. package/dist/src/react/index.js +31 -0
  49. package/dist/src/react/use-aithos-asset.d.ts +39 -0
  50. package/dist/src/react/use-aithos-asset.js +118 -0
  51. package/dist/src/react/use-transcribe-pending.d.ts +21 -0
  52. package/dist/src/react/use-transcribe-pending.js +47 -0
  53. package/dist/src/rotate.d.ts +94 -0
  54. package/dist/src/rotate.js +298 -0
  55. package/dist/src/sdk.d.ts +36 -2
  56. package/dist/src/sdk.js +72 -1
  57. package/dist/src/transcribe-resilience.d.ts +57 -0
  58. package/dist/src/transcribe-resilience.js +203 -0
  59. package/dist/src/web.d.ts +279 -0
  60. package/dist/src/web.js +186 -0
  61. package/dist/test/agent-dispatch.test.d.ts +2 -0
  62. package/dist/test/agent-dispatch.test.js +222 -0
  63. package/dist/test/agent-loop.test.d.ts +2 -0
  64. package/dist/test/agent-loop.test.js +117 -0
  65. package/dist/test/agent-tools.test.d.ts +2 -0
  66. package/dist/test/agent-tools.test.js +50 -0
  67. package/dist/test/auth-j3.test.js +32 -1
  68. package/dist/test/canonical-conformance.test.d.ts +2 -0
  69. package/dist/test/canonical-conformance.test.js +86 -0
  70. package/dist/test/compute-delegate-path.test.d.ts +2 -0
  71. package/dist/test/compute-delegate-path.test.js +183 -0
  72. package/dist/test/compute.test.js +4 -0
  73. package/dist/test/converse.test.d.ts +2 -0
  74. package/dist/test/converse.test.js +162 -0
  75. package/dist/test/data-sphere.test.d.ts +2 -0
  76. package/dist/test/data-sphere.test.js +57 -0
  77. package/dist/test/endpoints.test.js +40 -1
  78. package/dist/test/envelope-core-conformance.test.d.ts +2 -0
  79. package/dist/test/envelope-core-conformance.test.js +75 -0
  80. package/dist/test/envelope.test.d.ts +2 -0
  81. package/dist/test/envelope.test.js +318 -0
  82. package/dist/test/ethos-first-edition.test.d.ts +2 -0
  83. package/dist/test/ethos-first-edition.test.js +371 -0
  84. package/dist/test/invoke-turn-sdk.test.d.ts +2 -0
  85. package/dist/test/invoke-turn-sdk.test.js +177 -0
  86. package/dist/test/migrate.test.d.ts +2 -0
  87. package/dist/test/migrate.test.js +340 -0
  88. package/dist/test/owner-data-client.test.d.ts +2 -0
  89. package/dist/test/owner-data-client.test.js +88 -0
  90. package/dist/test/rotate-ethos.test.d.ts +2 -0
  91. package/dist/test/rotate-ethos.test.js +151 -0
  92. package/dist/test/rotate.test.d.ts +2 -0
  93. package/dist/test/rotate.test.js +63 -0
  94. package/dist/test/schema-autoresolve.test.d.ts +2 -0
  95. package/dist/test/schema-autoresolve.test.js +146 -0
  96. package/dist/test/sdk.test.js +11 -2
  97. package/dist/test/signup-bootstrap.test.d.ts +2 -0
  98. package/dist/test/signup-bootstrap.test.js +311 -0
  99. package/dist/test/transcribe-invoke.test.d.ts +2 -0
  100. package/dist/test/transcribe-invoke.test.js +204 -0
  101. package/dist/test/transcribe.test.d.ts +2 -0
  102. package/dist/test/transcribe.test.js +186 -0
  103. package/dist/test/web.test.d.ts +2 -0
  104. package/dist/test/web.test.js +270 -0
  105. package/package.json +20 -3
@@ -0,0 +1,146 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 Mathieu Colla
3
+ // A reader that did NOT bundle a vendor schema can still read a collection that
4
+ // uses it, by auto-resolving the PUBLISHED JSON Schema from the PDS and
5
+ // deriving the field split from its aithos:indexable / aithos:auto annotations.
6
+ // This is what lets the strangler reference app (and any latest-SDK client)
7
+ // read any collection without hardcoding VENDOR_SCHEMAS.
8
+ import { test } from "node:test";
9
+ import { strict as assert } from "node:assert";
10
+ import { randomBytes } from "node:crypto";
11
+ import { createDataClient, liteFromPublishedSchema } from "../src/data.js";
12
+ import * as ed from "@noble/ed25519";
13
+ import { sha512 } from "@noble/hashes/sha2.js";
14
+ ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
15
+ const MEMO_ID = "aithos.x.demo.memo.v1";
16
+ const memoLite = {
17
+ schema: MEMO_ID,
18
+ indexable: new Set(["title"]),
19
+ encrypted: new Set(["body"]),
20
+ auto: new Set(),
21
+ defaults: {},
22
+ };
23
+ const memoJson = {
24
+ "aithos:schema": MEMO_ID,
25
+ "aithos:version": "1.0.0",
26
+ type: "object",
27
+ additionalProperties: false,
28
+ required: ["title"],
29
+ properties: {
30
+ title: { type: "string", "aithos:indexable": true },
31
+ body: { type: "string" },
32
+ },
33
+ };
34
+ test("liteFromPublishedSchema derives the field split from annotations", () => {
35
+ const lite = liteFromPublishedSchema(memoJson);
36
+ assert.equal(lite.schema, MEMO_ID);
37
+ assert.deepEqual([...lite.indexable].sort(), ["title"]);
38
+ assert.deepEqual([...lite.encrypted].sort(), ["body"]);
39
+ assert.equal(lite.auto.size, 0);
40
+ });
41
+ function makePds() {
42
+ const collections = new Map();
43
+ const records = new Map();
44
+ const schemas = new Map();
45
+ const fetchImpl = (async (_url, init) => {
46
+ const body = JSON.parse(init.body);
47
+ const p = body.params ?? {};
48
+ const ok = (result) => new Response(JSON.stringify({ jsonrpc: "2.0", id: body.id, result }), { status: 200 });
49
+ const er = (code, m) => new Response(JSON.stringify({ jsonrpc: "2.0", id: body.id, error: { code, message: m } }), { status: 200 });
50
+ const urn = (did, n) => `urn:aithos:collection:${did}:${n}`;
51
+ switch (body.method) {
52
+ case "aithos.data.register_schema":
53
+ schemas.set(p.schema_doc["aithos:schema"], p.schema_doc);
54
+ return ok({ schema_id: p.schema_doc["aithos:schema"], doc_hash: "h", created: true });
55
+ case "aithos.data.get_schema": {
56
+ const d = schemas.get(p.schema);
57
+ return d ? ok({ schema: d }) : er(-32070, "unknown schema");
58
+ }
59
+ case "aithos.data.create_collection": {
60
+ const u = urn(p.subject_did, p.collection_name);
61
+ collections.set(u, { urn: u, name: p.collection_name, schema: p.schema, subject_did: p.subject_did, cmk_envelope: p.cmk_envelope, record_count: 0 });
62
+ records.set(u, new Map());
63
+ return ok({ urn: u, name: p.collection_name, schema: p.schema, cmk_envelope: p.cmk_envelope });
64
+ }
65
+ case "aithos.data.get_collection": {
66
+ const c = collections.get(urn(p.subject_did, p.collection_name));
67
+ return c ? ok({ urn: c.urn, name: c.name, schema: c.schema, cmk_envelope: c.cmk_envelope, record_count: c.record_count }) : er(-32020, "nf");
68
+ }
69
+ case "aithos.data.insert_record": {
70
+ records.get(p.collection_urn).set(p.record_id, { record_id: p.record_id, metadata: p.metadata, payload: p.payload });
71
+ return ok({ record_id: p.record_id });
72
+ }
73
+ case "aithos.data.get_record": {
74
+ const r = records.get(p.collection_urn)?.get(p.record_id);
75
+ return r ? ok(r) : er(-32020, "nf");
76
+ }
77
+ default:
78
+ return er(-32601, body.method);
79
+ }
80
+ });
81
+ return { fetchImpl, schemas };
82
+ }
83
+ test("a reader without the bundled vendor lite decodes via the published schema", async () => {
84
+ const seed = new Uint8Array(randomBytes(32));
85
+ const pub = ed.getPublicKey(seed);
86
+ // did:key style (single-key) — fine for the data PDS owner path in this mock.
87
+ let n = 0n;
88
+ const mc = new Uint8Array([0xed, 0x01, ...pub]);
89
+ for (const b of mc)
90
+ n = (n << 8n) | BigInt(b);
91
+ const alpha = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
92
+ let mb = "";
93
+ let x = n;
94
+ while (x > 0n) {
95
+ mb = alpha[Number(x % 58n)] + mb;
96
+ x /= 58n;
97
+ }
98
+ const did = `did:key:z${mb}`;
99
+ const vm = `${did}#z${mb}`;
100
+ const pds = makePds();
101
+ // Writer: bundles the lite, registers the JSON schema, writes a record.
102
+ const writer = createDataClient({ pdsUrl: "https://pds.test", did, sphereSeed: seed, verificationMethod: vm, schemas: [memoLite], fetch: pds.fetchImpl });
103
+ await writer.registerSchema(memoJson);
104
+ await writer.createCollection({ name: "memos", schema: MEMO_ID });
105
+ const recId = await writer.collection("memos").insert({ title: "Hello", body: "top secret body" });
106
+ // Reader: NO bundled schema. Must auto-resolve from the PDS-published schema.
107
+ const reader = createDataClient({ pdsUrl: "https://pds.test", did, sphereSeed: seed, verificationMethod: vm, fetch: pds.fetchImpl });
108
+ const got = await reader.collection("memos").get(recId);
109
+ assert.equal(got.title, "Hello", "indexable field present");
110
+ assert.equal(got.body, "top secret body", "encrypted field decoded via auto-resolved schema");
111
+ });
112
+ test("schema-less READ still works (decrypts); only WRITE requires the schema", async () => {
113
+ const seed = new Uint8Array(randomBytes(32));
114
+ const pub = ed.getPublicKey(seed);
115
+ let n = 0n;
116
+ const mc = new Uint8Array([0xed, 0x01, ...pub]);
117
+ for (const b of mc)
118
+ n = (n << 8n) | BigInt(b);
119
+ const alpha = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
120
+ let mb = "";
121
+ let x = n;
122
+ while (x > 0n) {
123
+ mb = alpha[Number(x % 58n)] + mb;
124
+ x /= 58n;
125
+ }
126
+ const did = `did:key:z${mb}`;
127
+ const vm = `${did}#z${mb}`;
128
+ const pds = makePds();
129
+ // Writer bundles the lite, creates the collection + a record, but does NOT
130
+ // register the schema on the PDS (simulates an app like delie that keeps its
131
+ // schema local). So no client can auto-resolve it.
132
+ const writer = createDataClient({ pdsUrl: "https://pds.test", did, sphereSeed: seed, verificationMethod: vm, schemas: [memoLite], fetch: pds.fetchImpl });
133
+ await writer.createCollection({ name: "memos", schema: MEMO_ID });
134
+ const recId = await writer.collection("memos").insert({ title: "Hi", body: "encrypted secret" });
135
+ // Reader has NEITHER the bundled lite NOR a published schema to fetch.
136
+ const reader = createDataClient({ pdsUrl: "https://pds.test", did, sphereSeed: seed, verificationMethod: vm, fetch: pds.fetchImpl });
137
+ // READ still works — records decrypt from the CMK + metadata/payload; the
138
+ // schema is only needed to SPLIT on write. This is what makes a migrated
139
+ // collection directly usable under #data by any client.
140
+ const got = await reader.collection("memos").get(recId);
141
+ assert.equal(got.title, "Hi", "indexable field (plaintext metadata) present");
142
+ assert.equal(got.body, "encrypted secret", "encrypted field decrypted WITHOUT the schema");
143
+ // WRITE fails cleanly (can't split/validate without the schema).
144
+ await assert.rejects(() => reader.collection("memos").insert({ title: "no", body: "go" }), /needs its schema/);
145
+ });
146
+ //# sourceMappingURL=schema-autoresolve.test.js.map
@@ -33,6 +33,9 @@ describe("DEFAULT_SDK_ENDPOINTS", () => {
33
33
  it("points at the production Aithos hosts", () => {
34
34
  assert.equal(DEFAULT_SDK_ENDPOINTS.compute, "https://compute.aithos.be");
35
35
  assert.equal(DEFAULT_SDK_ENDPOINTS.wallet, "https://wallet.aithos.be");
36
+ assert.equal(DEFAULT_SDK_ENDPOINTS.web, "https://extract.aithos.be");
37
+ assert.equal(DEFAULT_SDK_ENDPOINTS.pds, "https://pds.aithos.be");
38
+ assert.equal(DEFAULT_SDK_ENDPOINTS.assets, "https://assets.aithos.be");
36
39
  });
37
40
  });
38
41
  describe("AithosSDK constructor", () => {
@@ -101,7 +104,12 @@ describe("AithosSDK constructor", () => {
101
104
  assert.equal(typeof sdk.ethos.me, "function");
102
105
  assert.equal(typeof sdk.mandates.create, "function");
103
106
  });
104
- it("compute.invokeBedrock rejects when no owner is signed in", async () => {
107
+ it("compute.invokeBedrock rejects when no owner AND no delegate matches the mandate", async () => {
108
+ // Since alpha.9, invokeBedrock supports a delegate signing path. The
109
+ // failure mode here (no owner loaded AND no imported mandate matching
110
+ // the requested id) returns code `sdk_no_delegate_for_mandate`.
111
+ // The previous `sdk_no_owner` code now applies only to other entry
112
+ // points where there's no delegate fallback.
105
113
  const auth = makeAuth();
106
114
  const sdk = new AithosSDK({
107
115
  auth,
@@ -114,7 +122,8 @@ describe("AithosSDK constructor", () => {
114
122
  mandateId: "mandate:x",
115
123
  model: "claude-sonnet-4-6",
116
124
  messages: [{ role: "user", content: "hi" }],
117
- }), (e) => e instanceof AithosSDKError && e.code === "sdk_no_owner");
125
+ }), (e) => e instanceof AithosSDKError &&
126
+ e.code === "sdk_no_delegate_for_mandate");
118
127
  });
119
128
  });
120
129
  //# sourceMappingURL=sdk.test.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=signup-bootstrap.test.d.ts.map
@@ -0,0 +1,311 @@
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
+ /* alpha.36 — defense-in-depth for legacy backends without semantic-equal */
223
+ /* ------------------------------------------------------------------------ */
224
+ it("treats -32022 'different did.json already published' as a no-op success", async () => {
225
+ // This is the regression introduced in alpha.33: republishing the same
226
+ // identity returns -32022 because `signedDidDocument()` regenerates
227
+ // `aithos.created_at` on every call. For an honest signer, this is
228
+ // semantically a no-op — the Ethos is published, crypto material matches.
229
+ // The SDK swallows this specific case so chatty publish_identity callers
230
+ // (signInCustodial, verifyEmail) don't break on every subsequent sign-in.
231
+ const { fetch: f, calls } = makeMockFetch([
232
+ { url: "/auth/register", method: "POST", respond: fakeRegisterOk },
233
+ {
234
+ url: "/mcp/primitives/write",
235
+ method: "POST",
236
+ respond: () => ({
237
+ json: {
238
+ jsonrpc: "2.0",
239
+ id: "publish_identity",
240
+ error: {
241
+ code: -32022,
242
+ message: "different did.json already published for this DID",
243
+ data: { existing_doc_url: "https://cdn.aithos.be/did.json" },
244
+ },
245
+ },
246
+ }),
247
+ },
248
+ ]);
249
+ const auth = makeAuth(f);
250
+ // Must succeed, not throw — the republish-conflict is masked.
251
+ await auth.signUp(validInput);
252
+ // Hydrate worked: owner is loaded.
253
+ assert.equal(auth.canSignAsOwner(), true);
254
+ // No retry on -32022 (deterministic) — exactly 1 publish call.
255
+ assert.equal(calls.filter((c) => c.url.includes("/mcp/primitives/write")).length, 1, "publish_identity must not retry on -32022");
256
+ });
257
+ it("still throws ethos_bootstrap_failed on -32022 with a DIFFERENT message", async () => {
258
+ // The shim is narrow: it ONLY swallows the specific
259
+ // "different did.json already published" message. Other -32022 cases
260
+ // (server might use the same code for different semantics) must still
261
+ // bubble up as ethos_bootstrap_failed.
262
+ const { fetch: f } = makeMockFetch([
263
+ { url: "/auth/register", method: "POST", respond: fakeRegisterOk },
264
+ {
265
+ url: "/mcp/primitives/write",
266
+ method: "POST",
267
+ respond: () => ({
268
+ json: {
269
+ jsonrpc: "2.0",
270
+ id: "publish_identity",
271
+ error: {
272
+ code: -32022,
273
+ message: "subject identity is tombstoned",
274
+ },
275
+ },
276
+ }),
277
+ },
278
+ ]);
279
+ const auth = makeAuth(f);
280
+ await assert.rejects(() => auth.signUp(validInput), (e) => e instanceof AithosSDKError &&
281
+ e.code === "ethos_bootstrap_failed" &&
282
+ /tombstoned/i.test(e.message));
283
+ assert.equal(auth.canSignAsOwner(), false);
284
+ });
285
+ it("still throws ethos_bootstrap_failed on the conflict message under a different code", async () => {
286
+ // Conversely, an error matching the message but with a code OTHER than
287
+ // -32022 is not swallowed — we anchor on both (code, message) to keep
288
+ // the shim narrow.
289
+ const { fetch: f } = makeMockFetch([
290
+ { url: "/auth/register", method: "POST", respond: fakeRegisterOk },
291
+ {
292
+ url: "/mcp/primitives/write",
293
+ method: "POST",
294
+ respond: () => ({
295
+ json: {
296
+ jsonrpc: "2.0",
297
+ id: "publish_identity",
298
+ error: {
299
+ code: -32000,
300
+ message: "different did.json already published for this DID",
301
+ },
302
+ },
303
+ }),
304
+ },
305
+ ]);
306
+ const auth = makeAuth(f);
307
+ await assert.rejects(() => auth.signUp(validInput), (e) => e instanceof AithosSDKError && e.code === "ethos_bootstrap_failed");
308
+ assert.equal(auth.canSignAsOwner(), false);
309
+ });
310
+ });
311
+ //# sourceMappingURL=signup-bootstrap.test.js.map
@@ -0,0 +1,2 @@
1
+ import "fake-indexeddb/auto";
2
+ //# sourceMappingURL=transcribe-invoke.test.d.ts.map