@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
|
-
//
|
|
84
|
-
|
|
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.#
|
|
179
|
-
|
|
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.#
|
|
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.#
|
|
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);
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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,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.
|
|
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.
|
|
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.
|
|
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"
|