@aithos/sdk 0.1.0-alpha.41 → 0.1.0-alpha.43
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/apps.d.ts +69 -0
- package/dist/src/apps.js +157 -2
- package/dist/src/ethos.js +72 -20
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/test/ethos-first-edition.test.js +125 -2
- package/package.json +4 -3
package/dist/src/apps.d.ts
CHANGED
|
@@ -151,5 +151,74 @@ export declare class AppsNamespace {
|
|
|
151
151
|
* just with the app-credits pack id family.
|
|
152
152
|
*/
|
|
153
153
|
createAppTopupSession(args: CreateAppTopupSessionArgs): Promise<CreateAppTopupSessionResult>;
|
|
154
|
+
/**
|
|
155
|
+
* Build, sign, and PUBLISH a SponsorshipMandate to the authority's
|
|
156
|
+
* `aithos-app-sponsorships` table via `aithos.sponsorship_create`.
|
|
157
|
+
*
|
|
158
|
+
* Combines local signing (see {@link createSponsorshipMandate}) with the
|
|
159
|
+
* server upload step. After this resolves, the sponsorship is active —
|
|
160
|
+
* the next `sdk.compute.invokeBedrock` call targeting `args.audience.appDid`
|
|
161
|
+
* may be sponsored (subject to caps).
|
|
162
|
+
*
|
|
163
|
+
* Requires that the calling owner is the registered `owner_did` of
|
|
164
|
+
* `args.audience.appDid` in `aithos-auth-apps`; otherwise the server
|
|
165
|
+
* rejects with `-32042` "not the owner".
|
|
166
|
+
*/
|
|
167
|
+
publishSponsorship(args: CreateSponsorshipMandateArgs): Promise<{
|
|
168
|
+
mandate: SignedSponsorshipMandate;
|
|
169
|
+
sponsorshipId: string;
|
|
170
|
+
mandateHash: string;
|
|
171
|
+
status: "active";
|
|
172
|
+
createdAt: number;
|
|
173
|
+
}>;
|
|
174
|
+
/**
|
|
175
|
+
* Read the active sponsorship for an app. Open endpoint (no envelope
|
|
176
|
+
* required): the mandate is publicly signed and resolvable anyway.
|
|
177
|
+
*
|
|
178
|
+
* Returns `{ exists: false }` when no row exists for the app.
|
|
179
|
+
*/
|
|
180
|
+
getSponsorship(args: {
|
|
181
|
+
appDid: string;
|
|
182
|
+
signal?: AbortSignal;
|
|
183
|
+
}): Promise<{
|
|
184
|
+
exists: false;
|
|
185
|
+
} | {
|
|
186
|
+
exists: true;
|
|
187
|
+
sponsorshipId: string;
|
|
188
|
+
mandate: SignedSponsorshipMandate;
|
|
189
|
+
mandateHash: string;
|
|
190
|
+
status: "active" | "paused" | "depleted" | "expired" | "revoked";
|
|
191
|
+
poolConsumedLifetime: number;
|
|
192
|
+
createdAt: number;
|
|
193
|
+
lastUpdatedAt: number;
|
|
194
|
+
}>;
|
|
195
|
+
/**
|
|
196
|
+
* Revoke a published sponsorship by `app_did` + `sponsorship_id`. The
|
|
197
|
+
* row is marked `status: "revoked"` server-side; the routing cache is
|
|
198
|
+
* invalidated so the next compute call falls back to the user wallet.
|
|
199
|
+
*/
|
|
200
|
+
revokeSponsorship(args: {
|
|
201
|
+
appDid: string;
|
|
202
|
+
sponsorshipId: string;
|
|
203
|
+
reason: string;
|
|
204
|
+
signal?: AbortSignal;
|
|
205
|
+
}): Promise<{
|
|
206
|
+
status: "revoked";
|
|
207
|
+
}>;
|
|
208
|
+
/**
|
|
209
|
+
* Read the app's wallet balance (= the sponsor pool). Requires owner
|
|
210
|
+
* signature.
|
|
211
|
+
*/
|
|
212
|
+
getAppWalletBalance(args: {
|
|
213
|
+
appDid: string;
|
|
214
|
+
signal?: AbortSignal;
|
|
215
|
+
}): Promise<{
|
|
216
|
+
appDid: string;
|
|
217
|
+
exists: boolean;
|
|
218
|
+
balance: number;
|
|
219
|
+
balancePurchase: number;
|
|
220
|
+
balanceGrant: number;
|
|
221
|
+
dailySpent: number;
|
|
222
|
+
}>;
|
|
154
223
|
}
|
|
155
224
|
//# sourceMappingURL=apps.d.ts.map
|
package/dist/src/apps.js
CHANGED
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
// a CloudFront-fronted read endpoint or direct DDB access. We'll add it
|
|
26
26
|
// in V0.2 once the sponsorship-api Lambda exists; for now, devs check
|
|
27
27
|
// their sponsorship via the AWS Console.
|
|
28
|
-
import { base64url, sign } from "@aithos/protocol-client";
|
|
29
|
-
import { walletTopupCheckoutUrl } from "./endpoints.js";
|
|
28
|
+
import { base64url, buildSignedEnvelope, sign } from "@aithos/protocol-client";
|
|
29
|
+
import { computeInvokeUrl, walletTopupCheckoutUrl } from "./endpoints.js";
|
|
30
30
|
import { ownerKeyPair } from "./internal/protocol-client-bridge.js";
|
|
31
31
|
import { AithosSDKError } from "./types.js";
|
|
32
32
|
/* -------------------------------------------------------------------------- */
|
|
@@ -191,6 +191,161 @@ export class AppsNamespace {
|
|
|
191
191
|
}
|
|
192
192
|
return { checkoutUrl: ok.checkout_url, sessionId: ok.session_id };
|
|
193
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Build, sign, and PUBLISH a SponsorshipMandate to the authority's
|
|
196
|
+
* `aithos-app-sponsorships` table via `aithos.sponsorship_create`.
|
|
197
|
+
*
|
|
198
|
+
* Combines local signing (see {@link createSponsorshipMandate}) with the
|
|
199
|
+
* server upload step. After this resolves, the sponsorship is active —
|
|
200
|
+
* the next `sdk.compute.invokeBedrock` call targeting `args.audience.appDid`
|
|
201
|
+
* may be sponsored (subject to caps).
|
|
202
|
+
*
|
|
203
|
+
* Requires that the calling owner is the registered `owner_did` of
|
|
204
|
+
* `args.audience.appDid` in `aithos-auth-apps`; otherwise the server
|
|
205
|
+
* rejects with `-32042` "not the owner".
|
|
206
|
+
*/
|
|
207
|
+
async publishSponsorship(args) {
|
|
208
|
+
const mandate = await this.createSponsorshipMandate(args);
|
|
209
|
+
const result = await this.#signedRpc({
|
|
210
|
+
method: "aithos.sponsorship_create",
|
|
211
|
+
params: { mandate },
|
|
212
|
+
});
|
|
213
|
+
return {
|
|
214
|
+
mandate,
|
|
215
|
+
sponsorshipId: result.sponsorship_id,
|
|
216
|
+
mandateHash: result.mandate_hash,
|
|
217
|
+
status: result.status,
|
|
218
|
+
createdAt: result.created_at,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Read the active sponsorship for an app. Open endpoint (no envelope
|
|
223
|
+
* required): the mandate is publicly signed and resolvable anyway.
|
|
224
|
+
*
|
|
225
|
+
* Returns `{ exists: false }` when no row exists for the app.
|
|
226
|
+
*/
|
|
227
|
+
async getSponsorship(args) {
|
|
228
|
+
const { endpoints, fetch: fetchImpl } = this.#deps;
|
|
229
|
+
const url = computeInvokeUrl(endpoints);
|
|
230
|
+
let res;
|
|
231
|
+
try {
|
|
232
|
+
res = await fetchImpl(url, {
|
|
233
|
+
method: "POST",
|
|
234
|
+
headers: { "content-type": "application/json" },
|
|
235
|
+
body: JSON.stringify({
|
|
236
|
+
jsonrpc: "2.0",
|
|
237
|
+
id: "aithos.sponsorship_get",
|
|
238
|
+
method: "aithos.sponsorship_get",
|
|
239
|
+
params: { app_did: args.appDid },
|
|
240
|
+
}),
|
|
241
|
+
...(args.signal ? { signal: args.signal } : {}),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
catch (e) {
|
|
245
|
+
throw new AithosSDKError("network", e.message);
|
|
246
|
+
}
|
|
247
|
+
const body = (await res.json());
|
|
248
|
+
if (body.error) {
|
|
249
|
+
throw new AithosSDKError(String(body.error.code), body.error.message);
|
|
250
|
+
}
|
|
251
|
+
const r = body.result ?? {};
|
|
252
|
+
if (r.exists === false || !r.exists) {
|
|
253
|
+
return { exists: false };
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
exists: true,
|
|
257
|
+
sponsorshipId: r.sponsorship_id,
|
|
258
|
+
mandate: r.mandate,
|
|
259
|
+
mandateHash: r.mandate_hash,
|
|
260
|
+
status: r.status,
|
|
261
|
+
poolConsumedLifetime: r.pool_consumed_lifetime ?? 0,
|
|
262
|
+
createdAt: r.created_at ?? 0,
|
|
263
|
+
lastUpdatedAt: r.last_updated_at ?? 0,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Revoke a published sponsorship by `app_did` + `sponsorship_id`. The
|
|
268
|
+
* row is marked `status: "revoked"` server-side; the routing cache is
|
|
269
|
+
* invalidated so the next compute call falls back to the user wallet.
|
|
270
|
+
*/
|
|
271
|
+
async revokeSponsorship(args) {
|
|
272
|
+
const result = await this.#signedRpc({
|
|
273
|
+
method: "aithos.sponsorship_revoke",
|
|
274
|
+
params: {
|
|
275
|
+
app_did: args.appDid,
|
|
276
|
+
sponsorship_id: args.sponsorshipId,
|
|
277
|
+
reason: args.reason,
|
|
278
|
+
},
|
|
279
|
+
...(args.signal ? { signal: args.signal } : {}),
|
|
280
|
+
});
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Read the app's wallet balance (= the sponsor pool). Requires owner
|
|
285
|
+
* signature.
|
|
286
|
+
*/
|
|
287
|
+
async getAppWalletBalance(args) {
|
|
288
|
+
const result = await this.#signedRpc({
|
|
289
|
+
method: "aithos.app_wallet_get_balance",
|
|
290
|
+
params: { app_did: args.appDid },
|
|
291
|
+
...(args.signal ? { signal: args.signal } : {}),
|
|
292
|
+
});
|
|
293
|
+
return {
|
|
294
|
+
appDid: result.app_did,
|
|
295
|
+
exists: result.exists,
|
|
296
|
+
balance: result.balance,
|
|
297
|
+
balancePurchase: result.balance_purchase,
|
|
298
|
+
balanceGrant: result.balance_grant,
|
|
299
|
+
dailySpent: result.daily_spent,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Internal: sign an envelope with the calling owner's `#public` sphere
|
|
304
|
+
* and POST a JSON-RPC request to the compute proxy. Used by the
|
|
305
|
+
* sponsorship CRUD methods above. Mirrors `ComputeNamespace.#signAndPost`.
|
|
306
|
+
*/
|
|
307
|
+
async #signedRpc(opts) {
|
|
308
|
+
const { endpoints, fetch: fetchImpl } = this.#deps;
|
|
309
|
+
const owner = this.#requireOwner();
|
|
310
|
+
const publicKp = ownerKeyPair(owner, "public");
|
|
311
|
+
const url = computeInvokeUrl(endpoints);
|
|
312
|
+
const envelope = buildSignedEnvelope({
|
|
313
|
+
iss: owner.did,
|
|
314
|
+
aud: url,
|
|
315
|
+
method: opts.method,
|
|
316
|
+
verificationMethod: `${owner.did}#public`,
|
|
317
|
+
params: opts.params,
|
|
318
|
+
signer: publicKp,
|
|
319
|
+
});
|
|
320
|
+
let res;
|
|
321
|
+
try {
|
|
322
|
+
res = await fetchImpl(url, {
|
|
323
|
+
method: "POST",
|
|
324
|
+
headers: { "content-type": "application/json" },
|
|
325
|
+
body: JSON.stringify({
|
|
326
|
+
jsonrpc: "2.0",
|
|
327
|
+
id: opts.method,
|
|
328
|
+
method: opts.method,
|
|
329
|
+
params: { ...opts.params, _envelope: envelope },
|
|
330
|
+
}),
|
|
331
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
catch (e) {
|
|
335
|
+
throw new AithosSDKError("network", e.message);
|
|
336
|
+
}
|
|
337
|
+
if (!res.ok) {
|
|
338
|
+
throw new AithosSDKError("http", `HTTP ${res.status} ${res.statusText}`, { status: res.status });
|
|
339
|
+
}
|
|
340
|
+
const body = (await res.json());
|
|
341
|
+
if (body.error) {
|
|
342
|
+
throw new AithosSDKError(String(body.error.code), body.error.message, body.error.data ? { data: body.error.data } : undefined);
|
|
343
|
+
}
|
|
344
|
+
if (body.result === undefined) {
|
|
345
|
+
throw new AithosSDKError("empty", `empty result for ${opts.method}`);
|
|
346
|
+
}
|
|
347
|
+
return body.result;
|
|
348
|
+
}
|
|
194
349
|
#requireOwner() {
|
|
195
350
|
const owner = this.#deps.auth._getOwnerSigners();
|
|
196
351
|
if (!owner || owner.destroyed) {
|
package/dist/src/ethos.js
CHANGED
|
@@ -485,38 +485,72 @@ export class EthosClient {
|
|
|
485
485
|
* Publish height=1 for an owner whose Ethos identity exists on
|
|
486
486
|
* `api.aithos.be` (provisioned by `auth.signUp()` in alpha.6+) but who
|
|
487
487
|
* has no editions yet. Builds the manifest from the staged ADD
|
|
488
|
-
* mutations
|
|
488
|
+
* mutations and POSTs `aithos.publish_ethos_edition`.
|
|
489
489
|
*
|
|
490
|
-
*
|
|
491
|
-
* -
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
*
|
|
490
|
+
* Behaviour:
|
|
491
|
+
* - All three zones (public / circle / self) are supported at
|
|
492
|
+
* height=1 since `@aithos/protocol-client@>=0.1.0-alpha.14`. Circle
|
|
493
|
+
* and self sections are sealed via the same DEK + HKDF wrap
|
|
494
|
+
* machinery that the height>=2 path uses, so they're verifiable by
|
|
495
|
+
* the existing reader path with no special-casing.
|
|
496
|
+
* - If the caller staged only circle / self mutations (typical for
|
|
497
|
+
* an app like Linkedone that writes a personality section straight
|
|
498
|
+
* to `circle`), an `aithos-init` sentinel is auto-injected into
|
|
499
|
+
* the public zone. This preserves the invariant that every Ethos
|
|
500
|
+
* has a non-empty public zone at height=1 — which all resolution
|
|
501
|
+
* flows (handle lookup, public crawl) depend on.
|
|
495
502
|
* - First-edition publishes don't accept update/delete mutations
|
|
496
503
|
* (there's nothing to update or delete yet) — those are rejected
|
|
497
504
|
* with `ethos_first_edition_invalid_op`.
|
|
505
|
+
* - The `ethos_first_edition_public_only` error code is no longer
|
|
506
|
+
* emitted; the bucket-and-auto-inject path replaces it.
|
|
498
507
|
*/
|
|
499
508
|
async #publishFirstEditionOwner() {
|
|
500
509
|
if (this.#actor.kind !== "owner") {
|
|
501
510
|
// Defensive — caller already checked this branch.
|
|
502
511
|
throw new AithosSDKError("ethos_invalid_actor", "expected owner actor");
|
|
503
512
|
}
|
|
504
|
-
// Validate the staged operation set
|
|
505
|
-
//
|
|
513
|
+
// Validate the staged operation set and bucket by zone. First
|
|
514
|
+
// edition = ADD mutations only; update / delete are rejected because
|
|
515
|
+
// there's no prior state to mutate.
|
|
506
516
|
const publicAdds = [];
|
|
517
|
+
const circleAdds = [];
|
|
518
|
+
const selfAdds = [];
|
|
507
519
|
for (const m of this.#mutations) {
|
|
508
520
|
if (m.kind !== "add") {
|
|
509
521
|
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 } });
|
|
510
522
|
}
|
|
511
|
-
if (m.zone
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
523
|
+
if (m.zone === "public")
|
|
524
|
+
publicAdds.push(m.section);
|
|
525
|
+
else if (m.zone === "circle")
|
|
526
|
+
circleAdds.push(m.section);
|
|
527
|
+
else if (m.zone === "self")
|
|
528
|
+
selfAdds.push(m.section);
|
|
529
|
+
}
|
|
530
|
+
if (publicAdds.length === 0 &&
|
|
531
|
+
circleAdds.length === 0 &&
|
|
532
|
+
selfAdds.length === 0) {
|
|
517
533
|
// Should never reach here — publish() short-circuits on empty
|
|
518
534
|
// mutations. Belt-and-braces in case the contract drifts.
|
|
519
|
-
throw new AithosSDKError("ethos_first_edition_empty", "first edition: stage at least one
|
|
535
|
+
throw new AithosSDKError("ethos_first_edition_empty", "first edition: stage at least one section before publishing");
|
|
536
|
+
}
|
|
537
|
+
// Public zone invariant: every Ethos has a non-empty public zone at
|
|
538
|
+
// height=1. If the caller only staged encrypted-zone mutations
|
|
539
|
+
// (typical for apps like Linkedone that write straight to circle),
|
|
540
|
+
// auto-inject an `aithos-init` sentinel — identical in shape to
|
|
541
|
+
// what `ensureInitialized()` would produce if called explicitly.
|
|
542
|
+
if (publicAdds.length === 0) {
|
|
543
|
+
publicAdds.push({
|
|
544
|
+
id: "sec_" + randomHex(12),
|
|
545
|
+
title: "aithos-init",
|
|
546
|
+
body: "Ethos initialized.\n\n" +
|
|
547
|
+
"This section is a sentinel created by `EthosClient.publish()` " +
|
|
548
|
+
"to materialise the subject's first edition alongside the " +
|
|
549
|
+
"encrypted-zone content the caller staged. It is safe to delete " +
|
|
550
|
+
"or edit later — its only purpose is to satisfy the invariant " +
|
|
551
|
+
"that every Ethos has a non-empty public zone at height=1.",
|
|
552
|
+
gamma_ref: "gamma_none_" + randomHex(24),
|
|
553
|
+
});
|
|
520
554
|
}
|
|
521
555
|
const identity = this.#actor.signers._unsafeStoredIdentity();
|
|
522
556
|
const browserId = browserIdentityFromStored(identity);
|
|
@@ -524,14 +558,27 @@ export class EthosClient {
|
|
|
524
558
|
const built = buildSignedFirstEditionFromSections({
|
|
525
559
|
identity: browserId,
|
|
526
560
|
signedDidDoc: signedDoc,
|
|
527
|
-
publicSections: publicAdds
|
|
561
|
+
publicSections: publicAdds,
|
|
562
|
+
...(circleAdds.length > 0 ? { circleSections: circleAdds } : {}),
|
|
563
|
+
...(selfAdds.length > 0 ? { selfSections: selfAdds } : {}),
|
|
528
564
|
});
|
|
529
565
|
const url = writeEndpoint();
|
|
566
|
+
const zonesPayload = {
|
|
567
|
+
public: { bytes_base64: bytesToBase64Padded(built.publicMarkdownBytes) },
|
|
568
|
+
};
|
|
569
|
+
if (built.circleBytes) {
|
|
570
|
+
zonesPayload.circle = {
|
|
571
|
+
bytes_base64: bytesToBase64Padded(built.circleBytes),
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
if (built.selfBytes) {
|
|
575
|
+
zonesPayload.self = {
|
|
576
|
+
bytes_base64: bytesToBase64Padded(built.selfBytes),
|
|
577
|
+
};
|
|
578
|
+
}
|
|
530
579
|
const params = {
|
|
531
580
|
manifest: built.manifest,
|
|
532
|
-
zones:
|
|
533
|
-
public: { bytes_base64: bytesToBase64Padded(built.publicMarkdownBytes) },
|
|
534
|
-
},
|
|
581
|
+
zones: zonesPayload,
|
|
535
582
|
};
|
|
536
583
|
const envelope = buildSignedEnvelope({
|
|
537
584
|
iss: browserId.did,
|
|
@@ -575,11 +622,16 @@ export class EthosClient {
|
|
|
575
622
|
// take the regular next-edition path.
|
|
576
623
|
this.#ethosHasNoEditionYet = false;
|
|
577
624
|
this.#afterPublish();
|
|
625
|
+
const zonesPublished = ["public"];
|
|
626
|
+
if (built.circleBytes)
|
|
627
|
+
zonesPublished.push("circle");
|
|
628
|
+
if (built.selfBytes)
|
|
629
|
+
zonesPublished.push("self");
|
|
578
630
|
return {
|
|
579
631
|
editionHeight: 1,
|
|
580
632
|
manifestHash: "", // protocol-client surfaces this on later editions; not on first
|
|
581
633
|
subjectDid: browserId.did,
|
|
582
|
-
zonesPublished
|
|
634
|
+
zonesPublished,
|
|
583
635
|
};
|
|
584
636
|
}
|
|
585
637
|
}
|
package/dist/src/index.d.ts
CHANGED
package/dist/src/index.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// Public types specific to the SDK (`AithosSDKConfig`, `AithosSDKError`)
|
|
18
18
|
// are exported from here. Endpoint config (`AithosSdkEndpoints`,
|
|
19
19
|
// `DEFAULT_SDK_ENDPOINTS`) likewise.
|
|
20
|
-
export const VERSION = "0.1.0-alpha.
|
|
20
|
+
export const VERSION = "0.1.0-alpha.43";
|
|
21
21
|
export { AithosSDK } from "./sdk.js";
|
|
22
22
|
export { AithosSDKError } from "./types.js";
|
|
23
23
|
// Re-export protocol-client's JSON-RPC error type so consumers can
|
|
@@ -185,19 +185,142 @@ describe("EthosClient — fresh Ethos (no edition published yet)", () => {
|
|
|
185
185
|
assert.equal(env.method, "aithos.publish_ethos_edition");
|
|
186
186
|
assert.match(env.proof.verificationMethod, /#public$/);
|
|
187
187
|
});
|
|
188
|
-
it("publish()
|
|
188
|
+
it("publish() accepts circle mutations on first edition with auto-injected public sentinel", async () => {
|
|
189
|
+
// Regression target: this used to throw `ethos_first_edition_public_only`.
|
|
190
|
+
// Since aithos-sdk@0.1.0-alpha.43 + protocol-client@0.1.0-alpha.14, the
|
|
191
|
+
// SDK auto-injects an `aithos-init` public section and seals the circle
|
|
192
|
+
// sections in the same height=1 manifest.
|
|
193
|
+
let publishBody = null;
|
|
189
194
|
installFetchMock([
|
|
190
195
|
{
|
|
191
196
|
url: "/mcp/primitives/read",
|
|
192
197
|
rpcMethod: "aithos.get_ethos_manifest",
|
|
193
198
|
respond: noEditionYetResponse,
|
|
194
199
|
},
|
|
200
|
+
{
|
|
201
|
+
url: "/mcp/primitives/write",
|
|
202
|
+
rpcMethod: "aithos.publish_ethos_edition",
|
|
203
|
+
respond: (call) => {
|
|
204
|
+
publishBody = call.body;
|
|
205
|
+
return publishOkResponse();
|
|
206
|
+
},
|
|
207
|
+
},
|
|
195
208
|
]);
|
|
196
209
|
const auth = makeAuth();
|
|
197
210
|
await signInAsAlice(auth);
|
|
198
211
|
const me = makeNamespace(auth).me();
|
|
199
212
|
me.zone("circle").addSection({ title: "Private", body: "..." });
|
|
200
|
-
|
|
213
|
+
const r = await me.publish();
|
|
214
|
+
assert.equal(r.editionHeight, 1);
|
|
215
|
+
assert.deepEqual(r.zonesPublished, ["public", "circle"]);
|
|
216
|
+
const manifest = publishBody.params.manifest;
|
|
217
|
+
assert.equal(manifest.edition.height, 1);
|
|
218
|
+
assert.equal(manifest.edition.prev_hash, null);
|
|
219
|
+
// Auto-injected sentinel.
|
|
220
|
+
assert.deepEqual(manifest.zones.public.section_titles, ["aithos-init"]);
|
|
221
|
+
assert.equal(manifest.zones.public.encrypted, false);
|
|
222
|
+
// Sealed circle zone.
|
|
223
|
+
assert.deepEqual(manifest.zones.circle.section_titles, ["Private"]);
|
|
224
|
+
assert.equal(manifest.zones.circle.encrypted, true);
|
|
225
|
+
assert.ok(manifest.zones.circle.cipher, "circle cipher must be present");
|
|
226
|
+
// Both zones uploaded.
|
|
227
|
+
assert.ok(publishBody.params.zones.public?.bytes_base64);
|
|
228
|
+
assert.ok(publishBody.params.zones.circle?.bytes_base64);
|
|
229
|
+
assert.equal(publishBody.params.zones.self, undefined);
|
|
230
|
+
});
|
|
231
|
+
it("publish() preserves the caller's explicit public section when mixed with circle", async () => {
|
|
232
|
+
// When the caller stages BOTH a public and a circle add, the SDK
|
|
233
|
+
// must NOT inject a sentinel — the user's public section is enough.
|
|
234
|
+
let publishBody = null;
|
|
235
|
+
installFetchMock([
|
|
236
|
+
{
|
|
237
|
+
url: "/mcp/primitives/read",
|
|
238
|
+
rpcMethod: "aithos.get_ethos_manifest",
|
|
239
|
+
respond: noEditionYetResponse,
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
url: "/mcp/primitives/write",
|
|
243
|
+
rpcMethod: "aithos.publish_ethos_edition",
|
|
244
|
+
respond: (call) => {
|
|
245
|
+
publishBody = call.body;
|
|
246
|
+
return publishOkResponse();
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
]);
|
|
250
|
+
const auth = makeAuth();
|
|
251
|
+
await signInAsAlice(auth);
|
|
252
|
+
const me = makeNamespace(auth).me();
|
|
253
|
+
me.zone("public").addSection({ title: "About", body: "Public bio." });
|
|
254
|
+
me.zone("circle").addSection({ title: "Notes", body: "Private notes." });
|
|
255
|
+
const r = await me.publish();
|
|
256
|
+
assert.deepEqual(r.zonesPublished, ["public", "circle"]);
|
|
257
|
+
const manifest = publishBody.params.manifest;
|
|
258
|
+
// No sentinel — the user's section is the public titles entry.
|
|
259
|
+
assert.deepEqual(manifest.zones.public.section_titles, ["About"]);
|
|
260
|
+
assert.deepEqual(manifest.zones.circle.section_titles, ["Notes"]);
|
|
261
|
+
});
|
|
262
|
+
it("publish() auto-injects public sentinel when only self mutations are staged", async () => {
|
|
263
|
+
let publishBody = null;
|
|
264
|
+
installFetchMock([
|
|
265
|
+
{
|
|
266
|
+
url: "/mcp/primitives/read",
|
|
267
|
+
rpcMethod: "aithos.get_ethos_manifest",
|
|
268
|
+
respond: noEditionYetResponse,
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
url: "/mcp/primitives/write",
|
|
272
|
+
rpcMethod: "aithos.publish_ethos_edition",
|
|
273
|
+
respond: (call) => {
|
|
274
|
+
publishBody = call.body;
|
|
275
|
+
return publishOkResponse();
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
]);
|
|
279
|
+
const auth = makeAuth();
|
|
280
|
+
await signInAsAlice(auth);
|
|
281
|
+
const me = makeNamespace(auth).me();
|
|
282
|
+
me.zone("self").addSection({ title: "Journal", body: "Private." });
|
|
283
|
+
const r = await me.publish();
|
|
284
|
+
assert.deepEqual(r.zonesPublished, ["public", "self"]);
|
|
285
|
+
const manifest = publishBody.params.manifest;
|
|
286
|
+
assert.deepEqual(manifest.zones.public.section_titles, ["aithos-init"]);
|
|
287
|
+
assert.deepEqual(manifest.zones.self.section_titles, ["Journal"]);
|
|
288
|
+
assert.equal(manifest.zones.self.encrypted, true);
|
|
289
|
+
assert.equal(manifest.zones.circle, undefined);
|
|
290
|
+
});
|
|
291
|
+
it("publish() lands public + circle + self in a single height=1 edition", async () => {
|
|
292
|
+
let publishBody = null;
|
|
293
|
+
installFetchMock([
|
|
294
|
+
{
|
|
295
|
+
url: "/mcp/primitives/read",
|
|
296
|
+
rpcMethod: "aithos.get_ethos_manifest",
|
|
297
|
+
respond: noEditionYetResponse,
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
url: "/mcp/primitives/write",
|
|
301
|
+
rpcMethod: "aithos.publish_ethos_edition",
|
|
302
|
+
respond: (call) => {
|
|
303
|
+
publishBody = call.body;
|
|
304
|
+
return publishOkResponse();
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
]);
|
|
308
|
+
const auth = makeAuth();
|
|
309
|
+
await signInAsAlice(auth);
|
|
310
|
+
const me = makeNamespace(auth).me();
|
|
311
|
+
me.zone("public").addSection({ title: "Bio", body: "Bio body." });
|
|
312
|
+
me.zone("circle").addSection({ title: "Circle", body: "Circle body." });
|
|
313
|
+
me.zone("self").addSection({ title: "Self", body: "Self body." });
|
|
314
|
+
const r = await me.publish();
|
|
315
|
+
assert.equal(r.editionHeight, 1);
|
|
316
|
+
assert.deepEqual(r.zonesPublished, ["public", "circle", "self"]);
|
|
317
|
+
const manifest = publishBody.params.manifest;
|
|
318
|
+
assert.deepEqual(manifest.zones.public.section_titles, ["Bio"]);
|
|
319
|
+
assert.deepEqual(manifest.zones.circle.section_titles, ["Circle"]);
|
|
320
|
+
assert.deepEqual(manifest.zones.self.section_titles, ["Self"]);
|
|
321
|
+
assert.ok(publishBody.params.zones.public?.bytes_base64);
|
|
322
|
+
assert.ok(publishBody.params.zones.circle?.bytes_base64);
|
|
323
|
+
assert.ok(publishBody.params.zones.self?.bytes_base64);
|
|
201
324
|
});
|
|
202
325
|
it("publish() rejects update/delete operations on a fresh Ethos", async () => {
|
|
203
326
|
installFetchMock([
|
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.43",
|
|
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",
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"node": ">=20"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@aithos/protocol-client": ">=0.1.0-alpha.13 <0.2.0",
|
|
60
59
|
"@aithos/assets-crypto": ">=0.1.0-alpha.1 <0.2.0",
|
|
60
|
+
"@aithos/protocol-client": "^0.1.0-alpha.14",
|
|
61
61
|
"react": "^18.0.0 || ^19.0.0"
|
|
62
62
|
},
|
|
63
63
|
"peerDependenciesMeta": {
|
|
@@ -69,7 +69,8 @@
|
|
|
69
69
|
}
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
|
-
"@aithos/
|
|
72
|
+
"@aithos/assets-crypto": "^0.1.0-alpha.1",
|
|
73
|
+
"@aithos/protocol-client": "^0.1.0-alpha.14",
|
|
73
74
|
"@types/node": "^24.12.2",
|
|
74
75
|
"fake-indexeddb": "^6.2.5",
|
|
75
76
|
"typescript": "^5.9.2"
|