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

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/dist/src/ethos.js CHANGED
@@ -25,7 +25,7 @@
25
25
  // actor and forwards all real work into protocol-client's
26
26
  // `loadEditSnapshot` / `publishZoneEdit` / `publishPublicZoneAsDelegate`
27
27
  // / `publishPrivateZoneAsDelegate`.
28
- import { addSectionToList, deleteSectionFromList, loadEditSnapshot, modifySectionInList, publishPrivateZoneAsDelegate, publishPublicZoneAsDelegate, publishZoneEdit, } from "@aithos/protocol-client";
28
+ import { addSectionToList, AithosRpcError, browserIdentityFromStored, buildSignedEnvelope, buildSignedFirstEditionFromSections, deleteSectionFromList, loadEditSnapshot, modifySectionInList, publishPrivateZoneAsDelegate, publishPublicZoneAsDelegate, publishZoneEdit, signedDidDocument, writeEndpoint, } from "@aithos/protocol-client";
29
29
  import { delegateKeyPair } from "./internal/protocol-client-bridge.js";
30
30
  import { AithosSDKError } from "./types.js";
31
31
  export const ZONE_NAMES = ["public", "circle", "self"];
@@ -38,6 +38,15 @@ export class EthosClient {
38
38
  #actor;
39
39
  #snapshots = new Map();
40
40
  #mutations = [];
41
+ /**
42
+ * Set to `true` when the server reports that no edition has been
43
+ * published yet for this subject (JSON-RPC code -32020 with the message
44
+ * `not found: edition for <did>`). Toggled lazily on the first read /
45
+ * publish attempt and cleared again after a successful first-edition
46
+ * publish so subsequent operations take the regular `publishZoneEdit`
47
+ * path.
48
+ */
49
+ #ethosHasNoEditionYet = false;
41
50
  constructor(actor) {
42
51
  this.#actor = actor;
43
52
  this.subjectDid = actor.subjectDid;
@@ -80,8 +89,15 @@ export class EthosClient {
80
89
  // have staged mutations for. Build everything we need then call
81
90
  // publishZoneEdit once.
82
91
  if (this.#actor.kind === "owner") {
83
- // Need the snapshot regardless we read base sections + zoneBytes.
84
- const snap = await this.#ensureSnapshotOwner();
92
+ // First-edition path: no prior edition exists. Detected when the
93
+ // tolerant snapshot wrapper returned null on a previous read OR
94
+ // when this is the first network touch and the server responds
95
+ // with "not found: edition". Skip the snapshot fetch entirely and
96
+ // build a height=1 manifest from the staged mutations.
97
+ const snap = await this.#tryEnsureSnapshotOwner();
98
+ if (snap === null) {
99
+ return this.#publishFirstEditionOwner();
100
+ }
85
101
  const newPublic = touched.has("public")
86
102
  ? this.#applyMutations("public", snap.publicSections)
87
103
  : undefined;
@@ -175,12 +191,18 @@ export class EthosClient {
175
191
  if (zone !== "public") {
176
192
  throw new AithosSDKError("ethos_anonymous_private_zone", `anonymous reader cannot access the "${zone}" zone`);
177
193
  }
178
- const snap = await this.#ensureSnapshotAnonymous();
179
- const base = snap.publicSections;
194
+ const snap = await this.#tryEnsureSnapshotAnonymous();
195
+ // No edition yet → no base sections; only staged mutations contribute.
196
+ const base = snap === null ? [] : snap.publicSections;
180
197
  return this.#applyMutations(zone, base);
181
198
  }
182
199
  if (this.#actor.kind === "owner") {
183
- const snap = await this.#ensureSnapshotOwner();
200
+ const snap = await this.#tryEnsureSnapshotOwner();
201
+ if (snap === null) {
202
+ // No edition yet for this owner — base sections are empty in every
203
+ // zone. Staged mutations apply on top of an empty list.
204
+ return this.#applyMutations(zone, []);
205
+ }
184
206
  const base = baseSectionsFromSnapshot(snap, zone);
185
207
  // Surface decryption errors clearly (if reading a private zone we
186
208
  // can't unwrap, the snapshot has it in zoneDecryptErrors).
@@ -190,7 +212,10 @@ export class EthosClient {
190
212
  return this.#applyMutations(zone, base);
191
213
  }
192
214
  // Delegate
193
- const snap = await this.#ensureSnapshotDelegate();
215
+ const snap = await this.#tryEnsureSnapshotDelegate();
216
+ if (snap === null) {
217
+ return this.#applyMutations(zone, []);
218
+ }
194
219
  const base = baseSectionsFromSnapshot(snap, zone);
195
220
  if (zone !== "public" && snap.zoneDecryptErrors?.[zone]) {
196
221
  throw new AithosSDKError("ethos_zone_unreadable", `cannot read ${zone}: ${snap.zoneDecryptErrors[zone]}`);
@@ -283,6 +308,57 @@ export class EthosClient {
283
308
  this.#cacheSnapshotAllZones(snap);
284
309
  return snap;
285
310
  }
311
+ /**
312
+ * Wrap {@link #ensureSnapshotOwner} so the "no edition published yet"
313
+ * server response (-32020 with message `not found: edition for <did>`)
314
+ * is converted into `null`. Once converted, {@link #ethosHasNoEditionYet}
315
+ * is set to short-circuit subsequent reads without re-hitting the network.
316
+ *
317
+ * Returns `null` to mean "subject has an identity but no editions yet —
318
+ * treat all zones as empty". Any other error is re-thrown.
319
+ */
320
+ async #tryEnsureSnapshotOwner() {
321
+ if (this.#ethosHasNoEditionYet)
322
+ return null;
323
+ try {
324
+ return await this.#ensureSnapshotOwner();
325
+ }
326
+ catch (e) {
327
+ if (isNoEditionYetError(e)) {
328
+ this.#ethosHasNoEditionYet = true;
329
+ return null;
330
+ }
331
+ throw e;
332
+ }
333
+ }
334
+ async #tryEnsureSnapshotDelegate() {
335
+ if (this.#ethosHasNoEditionYet)
336
+ return null;
337
+ try {
338
+ return await this.#ensureSnapshotDelegate();
339
+ }
340
+ catch (e) {
341
+ if (isNoEditionYetError(e)) {
342
+ this.#ethosHasNoEditionYet = true;
343
+ return null;
344
+ }
345
+ throw e;
346
+ }
347
+ }
348
+ async #tryEnsureSnapshotAnonymous() {
349
+ if (this.#ethosHasNoEditionYet)
350
+ return null;
351
+ try {
352
+ return await this.#ensureSnapshotAnonymous();
353
+ }
354
+ catch (e) {
355
+ if (isNoEditionYetError(e)) {
356
+ this.#ethosHasNoEditionYet = true;
357
+ return null;
358
+ }
359
+ throw e;
360
+ }
361
+ }
286
362
  #cacheSnapshotAllZones(snap) {
287
363
  for (const z of ZONE_NAMES)
288
364
  this.#snapshots.set(z, snap);
@@ -291,6 +367,110 @@ export class EthosClient {
291
367
  this.#mutations = [];
292
368
  this.#snapshots.clear();
293
369
  }
370
+ /* ------------------------------------------------------------------------ */
371
+ /* First-edition publish (owner) */
372
+ /* ------------------------------------------------------------------------ */
373
+ /**
374
+ * Publish height=1 for an owner whose Ethos identity exists on
375
+ * `api.aithos.be` (provisioned by `auth.signUp()` in alpha.6+) but who
376
+ * has no editions yet. Builds the manifest from the staged ADD
377
+ * mutations on the public zone and POSTs `aithos.publish_ethos_edition`.
378
+ *
379
+ * Limitations of the alpha.7 cut:
380
+ * - Public zone only. Staged mutations on circle/self are rejected
381
+ * with `ethos_first_edition_public_only` — those zones can be
382
+ * populated in subsequent editions via the regular
383
+ * `publishZoneEdit` path once the public zone has been seeded.
384
+ * - First-edition publishes don't accept update/delete mutations
385
+ * (there's nothing to update or delete yet) — those are rejected
386
+ * with `ethos_first_edition_invalid_op`.
387
+ */
388
+ async #publishFirstEditionOwner() {
389
+ if (this.#actor.kind !== "owner") {
390
+ // Defensive — caller already checked this branch.
391
+ throw new AithosSDKError("ethos_invalid_actor", "expected owner actor");
392
+ }
393
+ // Validate the staged operation set. First edition = ADDs on public
394
+ // zone only.
395
+ const publicAdds = [];
396
+ for (const m of this.#mutations) {
397
+ if (m.kind !== "add") {
398
+ throw new AithosSDKError("ethos_first_edition_invalid_op", `first edition: cannot ${m.kind} a section before any edition exists; only addSection is supported on a fresh Ethos`, { data: { mutation: m } });
399
+ }
400
+ if (m.zone !== "public") {
401
+ throw new AithosSDKError("ethos_first_edition_public_only", `first edition: only the "public" zone is supported on a fresh Ethos; "${m.zone}" sections can be added after the first publish`, { data: { zone: m.zone } });
402
+ }
403
+ publicAdds.push({ section: m.section });
404
+ }
405
+ if (publicAdds.length === 0) {
406
+ // Should never reach here — publish() short-circuits on empty
407
+ // mutations. Belt-and-braces in case the contract drifts.
408
+ throw new AithosSDKError("ethos_first_edition_empty", "first edition: stage at least one public-zone section before publishing");
409
+ }
410
+ const identity = this.#actor.signers._unsafeStoredIdentity();
411
+ const browserId = browserIdentityFromStored(identity);
412
+ const signedDoc = signedDidDocument(browserId);
413
+ const built = buildSignedFirstEditionFromSections({
414
+ identity: browserId,
415
+ signedDidDoc: signedDoc,
416
+ publicSections: publicAdds.map((a) => a.section),
417
+ });
418
+ const url = writeEndpoint();
419
+ const params = {
420
+ manifest: built.manifest,
421
+ zones: {
422
+ public: { bytes_base64: bytesToBase64Padded(built.publicMarkdownBytes) },
423
+ },
424
+ };
425
+ const envelope = buildSignedEnvelope({
426
+ iss: browserId.did,
427
+ aud: url,
428
+ method: "aithos.publish_ethos_edition",
429
+ verificationMethod: `${browserId.did}#public`,
430
+ params,
431
+ signer: browserId.public,
432
+ });
433
+ const body = JSON.stringify({
434
+ jsonrpc: "2.0",
435
+ id: "publish_ethos_edition",
436
+ method: "aithos.publish_ethos_edition",
437
+ params: { ...params, _envelope: envelope },
438
+ });
439
+ let res;
440
+ try {
441
+ res = await fetch(url, {
442
+ method: "POST",
443
+ headers: { "content-type": "application/json" },
444
+ body,
445
+ });
446
+ }
447
+ catch (e) {
448
+ throw new AithosSDKError("ethos_publish_network", `publish_ethos_edition (first edition): network error: ${e.message ?? "unknown"}`);
449
+ }
450
+ let json;
451
+ try {
452
+ json = (await res.json());
453
+ }
454
+ catch {
455
+ throw new AithosSDKError("ethos_publish_invalid_response", `publish_ethos_edition (first edition): server returned non-JSON (HTTP ${res.status})`);
456
+ }
457
+ if (json.error) {
458
+ throw new AithosSDKError("ethos_first_edition_rejected", `publish_ethos_edition (first edition) rejected: ${json.error.message}`, {
459
+ status: res.status,
460
+ data: { rpc_code: json.error.code, ...(json.error.data ?? {}) },
461
+ });
462
+ }
463
+ // Success: clear the no-edition flag so subsequent reads/publishes
464
+ // take the regular next-edition path.
465
+ this.#ethosHasNoEditionYet = false;
466
+ this.#afterPublish();
467
+ return {
468
+ editionHeight: 1,
469
+ manifestHash: "", // protocol-client surfaces this on later editions; not on first
470
+ subjectDid: browserId.did,
471
+ zonesPublished: ["public"],
472
+ };
473
+ }
294
474
  }
295
475
  /* -------------------------------------------------------------------------- */
296
476
  /* EthosZone — per-zone proxy */
@@ -410,6 +590,36 @@ function projectPublishResult(manifest, subjectDid, zones) {
410
590
  zonesPublished: zones,
411
591
  };
412
592
  }
593
+ /**
594
+ * Detect the server-side "no edition published yet" response. Matches the
595
+ * exact error shape emitted by primitives-read's `notFound("edition for
596
+ * <did>")` helper: JSON-RPC code -32020 + message starting with `not
597
+ * found: edition for `. Other -32020 cases (e.g. "not found: manifest
598
+ * <did>@<height>" raised when the index advertises an edition the S3
599
+ * bucket can't serve) deliberately fall through — they're symptoms, not
600
+ * the "fresh subject" case we're trying to swallow.
601
+ */
602
+ function isNoEditionYetError(e) {
603
+ if (!(e instanceof AithosRpcError))
604
+ return false;
605
+ if (e.code !== -32020)
606
+ return false;
607
+ return typeof e.message === "string"
608
+ && e.message.startsWith("not found: edition for ");
609
+ }
610
+ /**
611
+ * Standard base64 with `=` padding — matches what protocol-client's
612
+ * publishZoneEdit uses for `zones.<zone>.bytes_base64`. The server is
613
+ * tolerant of either padded or unpadded variants per the API contract,
614
+ * but we mirror the existing wire to keep payloads byte-identical for
615
+ * easy diffing in dev tools.
616
+ */
617
+ function bytesToBase64Padded(bytes) {
618
+ let bin = "";
619
+ for (let i = 0; i < bytes.length; i++)
620
+ bin += String.fromCharCode(bytes[i]);
621
+ return btoa(bin);
622
+ }
413
623
  function randomHex(n) {
414
624
  const bytes = new Uint8Array(Math.ceil(n / 2));
415
625
  crypto.getRandomValues(bytes);
@@ -2,6 +2,7 @@ export declare const VERSION = "0.1.0-alpha.5";
2
2
  export { AithosSDK } from "./sdk.js";
3
3
  export type { AithosSDKConfig } from "./types.js";
4
4
  export { AithosSDKError } from "./types.js";
5
+ export { AithosRpcError } from "@aithos/protocol-client";
5
6
  export type { AithosSdkEndpoints } from "./endpoints.js";
6
7
  export { DEFAULT_SDK_ENDPOINTS } from "./endpoints.js";
7
8
  export type { ComputeMessage, InvokeBedrockArgs, InvokeBedrockResult, StopReason, } from "./compute.js";
package/dist/src/index.js CHANGED
@@ -20,6 +20,10 @@
20
20
  export const VERSION = "0.1.0-alpha.5";
21
21
  export { AithosSDK } from "./sdk.js";
22
22
  export { AithosSDKError } from "./types.js";
23
+ // Re-export protocol-client's JSON-RPC error type so consumers can
24
+ // `instanceof`-check server-side errors and inspect the JSON-RPC code
25
+ // without taking a direct dependency on @aithos/protocol-client.
26
+ export { AithosRpcError } from "@aithos/protocol-client";
23
27
  export { DEFAULT_SDK_ENDPOINTS } from "./endpoints.js";
24
28
  export { ComputeNamespace } from "./compute.js";
25
29
  export { WalletNamespace } from "./wallet.js";
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aithos/sdk",
3
- "version": "0.1.0-alpha.6",
3
+ "version": "0.1.0-alpha.7",
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,10 +52,10 @@
52
52
  "node": ">=20"
53
53
  },
54
54
  "peerDependencies": {
55
- "@aithos/protocol-client": ">=0.1.0-alpha.11 <0.2.0"
55
+ "@aithos/protocol-client": ">=0.1.0-alpha.12 <0.2.0"
56
56
  },
57
57
  "devDependencies": {
58
- "@aithos/protocol-client": "^0.1.0-alpha.11",
58
+ "@aithos/protocol-client": "^0.1.0-alpha.12",
59
59
  "@types/node": "^24.12.2",
60
60
  "fake-indexeddb": "^6.2.5",
61
61
  "typescript": "^5.9.2"