@aithos/sdk 0.1.0-alpha.1 → 0.1.0-alpha.11

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 (55) hide show
  1. package/README.md +45 -0
  2. package/dist/src/auth-api.d.ts +41 -0
  3. package/dist/src/auth-api.js +82 -0
  4. package/dist/src/auth.d.ts +190 -0
  5. package/dist/src/auth.js +741 -0
  6. package/dist/src/compute.d.ts +27 -6
  7. package/dist/src/compute.js +67 -10
  8. package/dist/src/ethos.d.ts +117 -1
  9. package/dist/src/ethos.js +646 -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 +28 -22
  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 +42 -2
  33. package/dist/src/wallet.js +89 -14
  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 +22 -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/wallet.test.js +20 -9
  55. package/package.json +4 -3
@@ -0,0 +1,183 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 Mathieu Colla
3
+ // Tests for the delegate signing path on sdk.compute.invokeBedrock.
4
+ // Until alpha.9 the method ALWAYS required an owner — a session that
5
+ // only held a mandate (no owner signers) failed with sdk_no_owner
6
+ // before any network call. From alpha.9 onward, when no owner is
7
+ // loaded but a delegate matches the requested mandate id, the SDK
8
+ // signs the envelope with the delegate's keypair and attaches the
9
+ // SignedMandate.
10
+ import { strict as assert } from "node:assert";
11
+ import { afterEach, beforeEach, describe, it } from "node:test";
12
+ import { createBrowserIdentity } from "@aithos/protocol-client";
13
+ import { AithosAuth, AithosSDKError, ComputeNamespace, memoryKeyStore, noopStore, } from "../src/index.js";
14
+ import { DEFAULT_SDK_ENDPOINTS } from "../src/endpoints.js";
15
+ /* -------------------------------------------------------------------------- */
16
+ /* Test plumbing */
17
+ /* -------------------------------------------------------------------------- */
18
+ let savedFetch;
19
+ let lastRequestBody = null;
20
+ function installFetchMock(response) {
21
+ savedFetch = globalThis.fetch;
22
+ lastRequestBody = null;
23
+ globalThis.fetch = (async (_input, init) => {
24
+ lastRequestBody = JSON.parse(init?.body);
25
+ return new Response(JSON.stringify(response.json), {
26
+ status: response.status ?? 200,
27
+ headers: { "content-type": "application/json" },
28
+ });
29
+ });
30
+ }
31
+ function uninstallFetchMock() {
32
+ if (savedFetch)
33
+ globalThis.fetch = savedFetch;
34
+ savedFetch = undefined;
35
+ lastRequestBody = null;
36
+ }
37
+ const okResponse = {
38
+ json: {
39
+ jsonrpc: "2.0",
40
+ id: "x",
41
+ result: {
42
+ content: "ok",
43
+ stopReason: "end_turn",
44
+ usage: { inputTokens: 1, outputTokens: 1 },
45
+ creditsCharged: 1,
46
+ walletBalance: 999,
47
+ auditId: "audit-test",
48
+ },
49
+ },
50
+ };
51
+ function freshAuth() {
52
+ return new AithosAuth({
53
+ sessionStore: noopStore(),
54
+ keyStore: memoryKeyStore(),
55
+ });
56
+ }
57
+ function freshCompute(auth) {
58
+ return new ComputeNamespace({
59
+ auth,
60
+ appDid: "did:aithos:app:example-placeholder",
61
+ endpoints: DEFAULT_SDK_ENDPOINTS,
62
+ fetch: globalThis.fetch.bind(globalThis),
63
+ });
64
+ }
65
+ /**
66
+ * Build a delegate bundle JSON that matches the wire shape importMandate
67
+ * accepts. Uses a real SignedMandate-like object minted from a fresh
68
+ * issuer so signature verification on import succeeds.
69
+ */
70
+ function makeDelegateBundleText(args) {
71
+ const issuer = createBrowserIdentity("alice", "Alice");
72
+ return JSON.stringify({
73
+ aithos_delegate_version: "0.1.0",
74
+ mandate: {
75
+ "aithos-mandate": "0.4.0",
76
+ id: args.mandateId,
77
+ issuer: issuer.did,
78
+ issued_by_key: `${issuer.did}#self`,
79
+ grantee: {
80
+ id: args.granteeId ?? "urn:aithos:agent:test",
81
+ pubkey: "z6MkqGenericPubKey",
82
+ },
83
+ actor_sphere: "self",
84
+ scopes: args.scopes,
85
+ not_before: "2026-05-10T00:00:00Z",
86
+ not_after: "2026-05-11T00:00:00Z",
87
+ issued_at: "2026-05-10T00:00:00Z",
88
+ nonce: "abc",
89
+ signature: { alg: "ed25519", key: `${issuer.did}#self`, value: "..." },
90
+ },
91
+ delegate_seed_hex: "11".repeat(32),
92
+ });
93
+ }
94
+ /* -------------------------------------------------------------------------- */
95
+ /* Tests */
96
+ /* -------------------------------------------------------------------------- */
97
+ describe("ComputeNamespace.invokeBedrock — delegate path", () => {
98
+ beforeEach(() => {
99
+ installFetchMock(okResponse);
100
+ });
101
+ afterEach(() => {
102
+ uninstallFetchMock();
103
+ });
104
+ it("rejects with sdk_no_delegate_for_mandate when delegate-only and mandateId doesn't match", async () => {
105
+ const auth = freshAuth();
106
+ const compute = freshCompute(auth);
107
+ // Import a delegate with a DIFFERENT mandate id than the one the call
108
+ // requests. Should error with the new code (NOT sdk_no_owner anymore).
109
+ await auth.importMandate({
110
+ bundle: makeDelegateBundleText({
111
+ mandateId: "mandate:held",
112
+ scopes: ["compute.invoke"],
113
+ }),
114
+ });
115
+ await assert.rejects(() => compute.invokeBedrock({
116
+ mandateId: "mandate:other-not-held",
117
+ model: "claude-haiku-4-5",
118
+ messages: [{ role: "user", content: "Hi" }],
119
+ maxTokens: 1,
120
+ }), (e) => e instanceof AithosSDKError &&
121
+ e.code === "sdk_no_delegate_for_mandate");
122
+ });
123
+ it("signs with the delegate keypair + attaches the mandate when delegate-only matches", async () => {
124
+ const auth = freshAuth();
125
+ const compute = freshCompute(auth);
126
+ const mandateId = "mandate:matching";
127
+ await auth.importMandate({
128
+ bundle: makeDelegateBundleText({
129
+ mandateId,
130
+ scopes: ["compute.invoke"],
131
+ }),
132
+ });
133
+ await compute.invokeBedrock({
134
+ mandateId,
135
+ model: "claude-haiku-4-5",
136
+ messages: [{ role: "user", content: "Hi" }],
137
+ maxTokens: 1,
138
+ });
139
+ const env = lastRequestBody?.params?._envelope;
140
+ assert.ok(env, "envelope must be present");
141
+ // verificationMethod must be the delegate's bare multibase pubkey
142
+ // (NOT a `#sphere` DID URL). And the SignedMandate must be inside.
143
+ assert.match(env.proof.verificationMethod, /^z[1-9A-HJ-NP-Za-km-z]+$/, `expected multibase pubkey, got ${env.proof.verificationMethod}`);
144
+ assert.ok(env.mandate, "delegate envelopes must attach the SignedMandate");
145
+ assert.equal(env.mandate.id, mandateId);
146
+ });
147
+ it("still works the owner path when an owner is signed in", async () => {
148
+ const auth = freshAuth();
149
+ const compute = freshCompute(auth);
150
+ // Owner sign-in via recovery — picks up four sphere keys from the
151
+ // recovery file format. Build one inline.
152
+ const issuer = createBrowserIdentity("owner", "Owner");
153
+ const seedHex = (b) => Array.from(b).map((x) => x.toString(16).padStart(2, "0")).join("");
154
+ const recoveryText = JSON.stringify({
155
+ aithos_recovery_version: "0.1.0-plaintext",
156
+ handle: issuer.handle,
157
+ display_name: issuer.displayName,
158
+ did: issuer.did,
159
+ created_at: new Date().toISOString(),
160
+ seeds_hex: {
161
+ root: seedHex(issuer.root.seed),
162
+ public: seedHex(issuer.public.seed),
163
+ circle: seedHex(issuer.circle.seed),
164
+ self: seedHex(issuer.self.seed),
165
+ },
166
+ });
167
+ await auth.signInWithRecovery({ file: recoveryText });
168
+ await compute.invokeBedrock({
169
+ mandateId: "mandate:owner-managed",
170
+ model: "claude-haiku-4-5",
171
+ messages: [{ role: "user", content: "Hi" }],
172
+ maxTokens: 1,
173
+ });
174
+ const env = lastRequestBody?.params?._envelope;
175
+ assert.ok(env, "envelope must be present");
176
+ // Owner path: verificationMethod is a `#public` DID URL, and the
177
+ // envelope MUST NOT carry a mandate (server resolves from
178
+ // params.mandate_id).
179
+ assert.match(env.proof.verificationMethod, /#public$/);
180
+ assert.equal(env.mandate, undefined, "owner-signed envelopes should not attach a mandate");
181
+ });
182
+ });
183
+ //# sourceMappingURL=compute-delegate-path.test.js.map
@@ -9,12 +9,23 @@
9
9
  import { strict as assert } from "node:assert";
10
10
  import { describe, it } from "node:test";
11
11
  import { createBrowserIdentity } from "@aithos/protocol-client";
12
- import { AithosSDK, AithosSDKError } from "../src/index.js";
12
+ import { AithosAuth, AithosSDK, AithosSDKError, memoryKeyStore, noopStore, } from "../src/index.js";
13
+ import { serializeRecoveryFile } from "../src/internal/recovery-file.js";
13
14
  const APP_DID = "did:aithos:app:test";
14
- function makeSdk(fetchImpl) {
15
- const identity = createBrowserIdentity("test-handle", "Test User");
15
+ async function makeSdk(fetchImpl) {
16
+ const id = createBrowserIdentity("test-handle", "Test User");
17
+ const auth = new AithosAuth({
18
+ authBaseUrl: "https://auth.test",
19
+ fetch: (() => {
20
+ throw new Error("auth not used in compute tests");
21
+ }),
22
+ sessionStore: noopStore(),
23
+ keyStore: memoryKeyStore(),
24
+ });
25
+ const { text } = serializeRecoveryFile(id);
26
+ await auth.signInWithRecovery({ file: text });
16
27
  return new AithosSDK({
17
- identity,
28
+ auth,
18
29
  appDid: APP_DID,
19
30
  endpoints: { compute: "https://compute.example.test" },
20
31
  fetch: fetchImpl,
@@ -40,7 +51,7 @@ describe("compute.invokeBedrock — happy path", () => {
40
51
  headers: { "content-type": "application/json" },
41
52
  });
42
53
  };
43
- const sdk = makeSdk(fakeFetch);
54
+ const sdk = await makeSdk(fakeFetch);
44
55
  const out = await sdk.compute.invokeBedrock({
45
56
  mandateId: "mandate:abc",
46
57
  model: "claude-sonnet-4-6",
@@ -71,7 +82,7 @@ describe("compute.invokeBedrock — happy path", () => {
71
82
  headers: { "content-type": "application/json" },
72
83
  });
73
84
  };
74
- const sdk = makeSdk(fakeFetch);
85
+ const sdk = await makeSdk(fakeFetch);
75
86
  await sdk.compute.invokeBedrock({
76
87
  mandateId: "mandate:abc",
77
88
  model: "claude-sonnet-4-6",
@@ -92,7 +103,7 @@ describe("compute.invokeBedrock — errors", () => {
92
103
  const fakeFetch = async () => {
93
104
  throw new Error("fetch failed: ECONNREFUSED");
94
105
  };
95
- const sdk = makeSdk(fakeFetch);
106
+ const sdk = await makeSdk(fakeFetch);
96
107
  await assert.rejects(sdk.compute.invokeBedrock({
97
108
  mandateId: "mandate:abc",
98
109
  model: "claude-sonnet-4-6",
@@ -106,7 +117,7 @@ describe("compute.invokeBedrock — errors", () => {
106
117
  });
107
118
  it("wraps an HTTP non-2xx as AithosSDKError(code='http')", async () => {
108
119
  const fakeFetch = async () => new Response("nope", { status: 503, statusText: "Service Unavailable" });
109
- const sdk = makeSdk(fakeFetch);
120
+ const sdk = await makeSdk(fakeFetch);
110
121
  await assert.rejects(sdk.compute.invokeBedrock({
111
122
  mandateId: "mandate:abc",
112
123
  model: "claude-sonnet-4-6",
@@ -126,7 +137,7 @@ describe("compute.invokeBedrock — errors", () => {
126
137
  data: { balance: 0, required: 1 },
127
138
  },
128
139
  }), { status: 200, headers: { "content-type": "application/json" } });
129
- const sdk = makeSdk(fakeFetch);
140
+ const sdk = await makeSdk(fakeFetch);
130
141
  await assert.rejects(sdk.compute.invokeBedrock({
131
142
  mandateId: "mandate:abc",
132
143
  model: "claude-sonnet-4-6",
@@ -143,7 +154,7 @@ describe("compute.invokeBedrock — errors", () => {
143
154
  status: 200,
144
155
  headers: { "content-type": "application/json" },
145
156
  });
146
- const sdk = makeSdk(fakeFetch);
157
+ const sdk = await makeSdk(fakeFetch);
147
158
  await assert.rejects(sdk.compute.invokeBedrock({
148
159
  mandateId: "mandate:abc",
149
160
  model: "claude-sonnet-4-6",
@@ -165,7 +176,7 @@ describe("compute.invokeBedrock — abort", () => {
165
176
  headers: { "content-type": "application/json" },
166
177
  });
167
178
  };
168
- const sdk = makeSdk(fakeFetch);
179
+ const sdk = await makeSdk(fakeFetch);
169
180
  const ac = new AbortController();
170
181
  await sdk.compute.invokeBedrock({
171
182
  mandateId: "mandate:abc",
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ethos-first-edition.test.d.ts.map
@@ -0,0 +1,248 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 Mathieu Colla
3
+ // Tests for the alpha.7 first-edition path in EthosClient — the case
4
+ // where an Ethos identity exists on api.aithos.be (provisioned by
5
+ // auth.signUp() since alpha.6) but no edition has been published yet.
6
+ //
7
+ // Two flows must work:
8
+ // 1. Reading any zone returns an empty list (instead of throwing
9
+ // "not found: edition").
10
+ // 2. Publishing for the first time builds height=1 from staged
11
+ // mutations and POSTs publish_ethos_edition directly, instead
12
+ // of going through publishZoneEdit (which requires a previous
13
+ // manifest).
14
+ //
15
+ // We mock global fetch end-to-end so the tests run offline.
16
+ import { strict as assert } from "node:assert";
17
+ import { afterEach, beforeEach, describe, it } from "node:test";
18
+ import { createBrowserIdentity } from "@aithos/protocol-client";
19
+ import { AithosAuth, AithosSDKError, EthosNamespace, memoryKeyStore, noopStore, } from "../src/index.js";
20
+ import { serializeRecoveryFile } from "../src/internal/recovery-file.js";
21
+ import { DEFAULT_SDK_ENDPOINTS } from "../src/endpoints.js";
22
+ let fetchCalls = [];
23
+ let savedFetch;
24
+ function installFetchMock(handlers) {
25
+ savedFetch = globalThis.fetch;
26
+ fetchCalls = [];
27
+ globalThis.fetch = (async (input, init) => {
28
+ const url = String(input);
29
+ const method = init?.method ?? "GET";
30
+ const bodyText = typeof init?.body === "string"
31
+ ? init.body
32
+ : init?.body == null
33
+ ? null
34
+ : String(init.body);
35
+ const body = bodyText ? JSON.parse(bodyText) : null;
36
+ const call = { url, method, body };
37
+ fetchCalls.push(call);
38
+ for (const h of handlers) {
39
+ if (!url.includes(h.url))
40
+ continue;
41
+ if (h.rpcMethod && body?.method !== h.rpcMethod)
42
+ continue;
43
+ const out = h.respond(call);
44
+ const status = out.status ?? 200;
45
+ return new Response(JSON.stringify(out.json), {
46
+ status,
47
+ headers: { "content-type": "application/json" },
48
+ });
49
+ }
50
+ throw new Error(`unhandled fetch: ${method} ${url} (rpc: ${body?.method ?? "n/a"})`);
51
+ });
52
+ }
53
+ function uninstallFetchMock() {
54
+ if (savedFetch) {
55
+ globalThis.fetch = savedFetch;
56
+ savedFetch = undefined;
57
+ }
58
+ fetchCalls = [];
59
+ }
60
+ function makeAuth() {
61
+ return new AithosAuth({
62
+ authBaseUrl: "https://auth.test",
63
+ apiBaseUrl: "https://api.test",
64
+ fetch: (() => {
65
+ throw new Error("AithosAuth.fetch must not be called in these tests");
66
+ }),
67
+ sessionStore: noopStore(),
68
+ keyStore: memoryKeyStore(),
69
+ });
70
+ }
71
+ function makeNamespace(auth) {
72
+ return new EthosNamespace({
73
+ auth,
74
+ endpoints: DEFAULT_SDK_ENDPOINTS,
75
+ // EthosNamespace itself doesn't read fetch from this slot today;
76
+ // protocol-client uses the global fetch we mock above.
77
+ fetch: globalThis.fetch.bind(globalThis),
78
+ });
79
+ }
80
+ async function signInAsAlice(auth) {
81
+ const id = createBrowserIdentity("alice", "Alice");
82
+ const { text } = serializeRecoveryFile(id);
83
+ const info = await auth.signInWithRecovery({ file: text });
84
+ return { did: info.did };
85
+ }
86
+ function noEditionYetResponse() {
87
+ // Mirrors the server's primitives-read `notFound("edition for <did>")`:
88
+ // JSON-RPC error code -32020, message starts with "not found: edition for ".
89
+ return {
90
+ json: {
91
+ jsonrpc: "2.0",
92
+ id: "aithos.get_ethos_manifest",
93
+ error: {
94
+ code: -32020,
95
+ message: "not found: edition for did:aithos:zSomething",
96
+ },
97
+ },
98
+ };
99
+ }
100
+ function publishOkResponse() {
101
+ return {
102
+ json: {
103
+ jsonrpc: "2.0",
104
+ id: "publish_ethos_edition",
105
+ result: { ok: true, height: 1, manifest_uri: "s3://aithos/.../manifest.json" },
106
+ },
107
+ };
108
+ }
109
+ /* -------------------------------------------------------------------------- */
110
+ /* Tests */
111
+ /* -------------------------------------------------------------------------- */
112
+ describe("EthosClient — fresh Ethos (no edition published yet)", () => {
113
+ beforeEach(() => {
114
+ fetchCalls = [];
115
+ });
116
+ afterEach(() => {
117
+ uninstallFetchMock();
118
+ });
119
+ it("zone(public).sections() returns [] when server says 'not found: edition'", async () => {
120
+ installFetchMock([
121
+ {
122
+ url: "/mcp/primitives/read",
123
+ rpcMethod: "aithos.get_ethos_manifest",
124
+ respond: noEditionYetResponse,
125
+ },
126
+ ]);
127
+ const auth = makeAuth();
128
+ await signInAsAlice(auth);
129
+ const me = makeNamespace(auth).me();
130
+ const sections = await me.zone("public").sections();
131
+ assert.deepEqual(sections, []);
132
+ });
133
+ it("zone(public).sections() reflects locally staged adds even when no edition exists", async () => {
134
+ installFetchMock([
135
+ {
136
+ url: "/mcp/primitives/read",
137
+ rpcMethod: "aithos.get_ethos_manifest",
138
+ respond: noEditionYetResponse,
139
+ },
140
+ ]);
141
+ const auth = makeAuth();
142
+ await signInAsAlice(auth);
143
+ const me = makeNamespace(auth).me();
144
+ me.zone("public").addSection({ title: "Hello", body: "World" });
145
+ const sections = await me.zone("public").sections();
146
+ assert.equal(sections.length, 1);
147
+ assert.equal(sections[0].title, "Hello");
148
+ });
149
+ it("publish() routes to publish_ethos_edition with height=1 on first publish", async () => {
150
+ let publishBody = null;
151
+ installFetchMock([
152
+ {
153
+ url: "/mcp/primitives/read",
154
+ rpcMethod: "aithos.get_ethos_manifest",
155
+ respond: noEditionYetResponse,
156
+ },
157
+ {
158
+ url: "/mcp/primitives/write",
159
+ rpcMethod: "aithos.publish_ethos_edition",
160
+ respond: (call) => {
161
+ publishBody = call.body;
162
+ return publishOkResponse();
163
+ },
164
+ },
165
+ ]);
166
+ const auth = makeAuth();
167
+ const alice = await signInAsAlice(auth);
168
+ const me = makeNamespace(auth).me();
169
+ me.zone("public").addSection({ title: "First", body: "Hello." });
170
+ me.zone("public").addSection({ title: "Second", body: "World." });
171
+ const r = await me.publish();
172
+ assert.equal(r.editionHeight, 1);
173
+ assert.equal(r.subjectDid, alice.did);
174
+ assert.deepEqual(r.zonesPublished, ["public"]);
175
+ // Verify the wire shape: JSON-RPC publish_ethos_edition with a height=1
176
+ // manifest containing both staged sections.
177
+ assert.equal(publishBody.method, "aithos.publish_ethos_edition");
178
+ const manifest = publishBody.params.manifest;
179
+ assert.equal(manifest.edition.height, 1);
180
+ assert.equal(manifest.edition.prev_hash, null);
181
+ assert.equal(manifest.edition.supersedes, null);
182
+ assert.deepEqual(manifest.zones.public.section_titles, ["First", "Second"]);
183
+ // Envelope is signed under #public.
184
+ const env = publishBody.params._envelope;
185
+ assert.equal(env.method, "aithos.publish_ethos_edition");
186
+ assert.match(env.proof.verificationMethod, /#public$/);
187
+ });
188
+ it("publish() rejects circle/self mutations on first edition", async () => {
189
+ installFetchMock([
190
+ {
191
+ url: "/mcp/primitives/read",
192
+ rpcMethod: "aithos.get_ethos_manifest",
193
+ respond: noEditionYetResponse,
194
+ },
195
+ ]);
196
+ const auth = makeAuth();
197
+ await signInAsAlice(auth);
198
+ const me = makeNamespace(auth).me();
199
+ me.zone("circle").addSection({ title: "Private", body: "..." });
200
+ await assert.rejects(() => me.publish(), (e) => e instanceof AithosSDKError && e.code === "ethos_first_edition_public_only");
201
+ });
202
+ it("publish() rejects update/delete operations on a fresh Ethos", async () => {
203
+ installFetchMock([
204
+ {
205
+ url: "/mcp/primitives/read",
206
+ rpcMethod: "aithos.get_ethos_manifest",
207
+ respond: noEditionYetResponse,
208
+ },
209
+ ]);
210
+ const auth = makeAuth();
211
+ await signInAsAlice(auth);
212
+ const me = makeNamespace(auth).me();
213
+ // Stage a delete for a section that doesn't exist (no edition exists at all).
214
+ me.zone("public")["_parent"]; // type-safety placeholder; we use the public API
215
+ // EthosZone exposes deleteSection — go via that.
216
+ me.zone("public").deleteSection("sec_doesnotexist000");
217
+ await assert.rejects(() => me.publish(), (e) => e instanceof AithosSDKError && e.code === "ethos_first_edition_invalid_op");
218
+ });
219
+ it("publish() surfaces server JSON-RPC errors as ethos_first_edition_rejected", async () => {
220
+ installFetchMock([
221
+ {
222
+ url: "/mcp/primitives/read",
223
+ rpcMethod: "aithos.get_ethos_manifest",
224
+ respond: noEditionYetResponse,
225
+ },
226
+ {
227
+ url: "/mcp/primitives/write",
228
+ rpcMethod: "aithos.publish_ethos_edition",
229
+ respond: () => ({
230
+ json: {
231
+ jsonrpc: "2.0",
232
+ id: "publish_ethos_edition",
233
+ error: {
234
+ code: -32020,
235
+ message: "subject identity not published (call publish_identity first)",
236
+ },
237
+ },
238
+ }),
239
+ },
240
+ ]);
241
+ const auth = makeAuth();
242
+ await signInAsAlice(auth);
243
+ const me = makeNamespace(auth).me();
244
+ me.zone("public").addSection({ title: "Hi", body: "There." });
245
+ await assert.rejects(() => me.publish(), (e) => e instanceof AithosSDKError && e.code === "ethos_first_edition_rejected");
246
+ });
247
+ });
248
+ //# sourceMappingURL=ethos-first-edition.test.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ethos.test.d.ts.map