@aithos/sdk 0.1.0-alpha.4 → 0.1.0-alpha.5
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/auth.d.ts +94 -136
- package/dist/src/auth.js +440 -159
- package/dist/src/compute.d.ts +8 -6
- package/dist/src/compute.js +19 -11
- package/dist/src/ethos.d.ts +117 -1
- package/dist/src/ethos.js +417 -16
- package/dist/src/index.d.ts +7 -4
- package/dist/src/index.js +17 -5
- package/dist/src/internal/delegate-bundle.d.ts +18 -0
- package/dist/src/internal/delegate-bundle.js +89 -0
- package/dist/src/internal/delegate-state.d.ts +45 -0
- package/dist/src/internal/delegate-state.js +120 -0
- package/dist/src/internal/owner-signers.d.ts +78 -0
- package/dist/src/internal/owner-signers.js +179 -0
- package/dist/src/internal/protocol-client-bridge.d.ts +8 -0
- package/dist/src/internal/protocol-client-bridge.js +20 -0
- package/dist/src/internal/recovery-file.d.ts +29 -0
- package/dist/src/internal/recovery-file.js +98 -0
- package/dist/src/internal/signer.d.ts +59 -0
- package/dist/src/internal/signer.js +86 -0
- package/dist/src/key-store.d.ts +128 -0
- package/dist/src/key-store.js +244 -0
- package/dist/src/mandates.d.ts +88 -1
- package/dist/src/mandates.js +185 -8
- package/dist/src/sdk.d.ts +36 -3
- package/dist/src/sdk.js +27 -23
- package/dist/src/wallet.d.ts +4 -6
- package/dist/src/wallet.js +18 -8
- package/dist/test/auth-j3.test.d.ts +2 -0
- package/dist/test/auth-j3.test.js +360 -0
- package/dist/test/compute.test.js +22 -11
- package/dist/test/ethos.test.d.ts +2 -0
- package/dist/test/ethos.test.js +219 -0
- package/dist/test/key-store.test.d.ts +2 -0
- package/dist/test/key-store.test.js +161 -0
- package/dist/test/mandates.test.d.ts +2 -0
- package/dist/test/mandates.test.js +93 -0
- package/dist/test/sdk.test.js +64 -30
- package/dist/test/signer.test.d.ts +2 -0
- package/dist/test/signer.test.js +117 -0
- package/dist/test/wallet.test.js +20 -9
- package/package.json +2 -1
package/dist/src/compute.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { AithosAuth } from "./auth.js";
|
|
2
2
|
import { type AithosSdkEndpoints } from "./endpoints.js";
|
|
3
3
|
export interface ComputeMessage {
|
|
4
4
|
readonly role: "user" | "assistant";
|
|
@@ -45,15 +45,16 @@ export interface InvokeBedrockResult {
|
|
|
45
45
|
readonly auditId: string;
|
|
46
46
|
}
|
|
47
47
|
export interface ComputeNamespaceDeps {
|
|
48
|
-
readonly
|
|
48
|
+
readonly auth: AithosAuth;
|
|
49
49
|
readonly appDid: string;
|
|
50
50
|
readonly endpoints: AithosSdkEndpoints;
|
|
51
51
|
readonly fetch: typeof fetch;
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
54
|
* `sdk.compute` namespace. Constructed once by the {@link AithosSDK}
|
|
55
|
-
* constructor;
|
|
56
|
-
*
|
|
55
|
+
* constructor; reads the active owner from the supplied
|
|
56
|
+
* {@link AithosAuth} on every call so signing material follows the
|
|
57
|
+
* latest sign-in/sign-out state.
|
|
57
58
|
*/
|
|
58
59
|
export declare class ComputeNamespace {
|
|
59
60
|
#private;
|
|
@@ -63,8 +64,9 @@ export declare class ComputeNamespace {
|
|
|
63
64
|
* {@link InvokeBedrockArgs} and {@link InvokeBedrockResult}.
|
|
64
65
|
*
|
|
65
66
|
* @throws {AithosSDKError} on protocol errors. The `code` field is one of
|
|
66
|
-
* `network`, `http`, `empty`, or any code returned by
|
|
67
|
-
* (`quota_exceeded`, `mandate_revoked`, `insufficient_credits`,
|
|
67
|
+
* `sdk_no_owner`, `network`, `http`, `empty`, or any code returned by
|
|
68
|
+
* the proxy (`quota_exceeded`, `mandate_revoked`, `insufficient_credits`,
|
|
69
|
+
* …).
|
|
68
70
|
*/
|
|
69
71
|
invokeBedrock(args: InvokeBedrockArgs): Promise<InvokeBedrockResult>;
|
|
70
72
|
}
|
package/dist/src/compute.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Wraps the JSON-RPC + signed-envelope protocol of `compute.aithos.be`
|
|
6
6
|
// behind an ergonomic method on the SDK. Internally builds a §11 envelope
|
|
7
|
-
// signed with the user's public-sphere key
|
|
8
|
-
// `${compute}/v1/invoke`.
|
|
7
|
+
// signed with the user's public-sphere key (read from
|
|
8
|
+
// {@link AithosAuth} at call time) and posts it to `${compute}/v1/invoke`.
|
|
9
9
|
//
|
|
10
10
|
// Server-side guarantees (enforced by the proxy, not by this lib):
|
|
11
11
|
// - Envelope signature verified against the user's `did.json`.
|
|
@@ -16,13 +16,15 @@
|
|
|
16
16
|
//
|
|
17
17
|
// MVP scope: single-shot invocation. Multi-turn agentic loops (with native
|
|
18
18
|
// tool calling on the proxy) will land in a follow-up.
|
|
19
|
-
import { buildSignedEnvelope
|
|
19
|
+
import { buildSignedEnvelope } from "@aithos/protocol-client";
|
|
20
20
|
import { computeInvokeUrl, } from "./endpoints.js";
|
|
21
|
+
import { ownerKeyPair } from "./internal/protocol-client-bridge.js";
|
|
21
22
|
import { AithosSDKError } from "./types.js";
|
|
22
23
|
/**
|
|
23
24
|
* `sdk.compute` namespace. Constructed once by the {@link AithosSDK}
|
|
24
|
-
* constructor;
|
|
25
|
-
*
|
|
25
|
+
* constructor; reads the active owner from the supplied
|
|
26
|
+
* {@link AithosAuth} on every call so signing material follows the
|
|
27
|
+
* latest sign-in/sign-out state.
|
|
26
28
|
*/
|
|
27
29
|
export class ComputeNamespace {
|
|
28
30
|
#deps;
|
|
@@ -34,11 +36,17 @@ export class ComputeNamespace {
|
|
|
34
36
|
* {@link InvokeBedrockArgs} and {@link InvokeBedrockResult}.
|
|
35
37
|
*
|
|
36
38
|
* @throws {AithosSDKError} on protocol errors. The `code` field is one of
|
|
37
|
-
* `network`, `http`, `empty`, or any code returned by
|
|
38
|
-
* (`quota_exceeded`, `mandate_revoked`, `insufficient_credits`,
|
|
39
|
+
* `sdk_no_owner`, `network`, `http`, `empty`, or any code returned by
|
|
40
|
+
* the proxy (`quota_exceeded`, `mandate_revoked`, `insufficient_credits`,
|
|
41
|
+
* …).
|
|
39
42
|
*/
|
|
40
43
|
async invokeBedrock(args) {
|
|
41
|
-
const {
|
|
44
|
+
const { auth, appDid, endpoints, fetch: fetchImpl } = this.#deps;
|
|
45
|
+
const owner = auth._getOwnerSigners();
|
|
46
|
+
if (!owner || owner.destroyed) {
|
|
47
|
+
throw new AithosSDKError("sdk_no_owner", "no owner signed in; sign in via auth.signIn / signUp / signInWithGoogle / signInWithRecovery first");
|
|
48
|
+
}
|
|
49
|
+
const publicKp = ownerKeyPair(owner, "public");
|
|
42
50
|
const url = computeInvokeUrl(endpoints);
|
|
43
51
|
const idempotencyKey = args.idempotencyKey ?? generateIdempotencyKey();
|
|
44
52
|
const params = {
|
|
@@ -55,12 +63,12 @@ export class ComputeNamespace {
|
|
|
55
63
|
if (args.temperature !== undefined)
|
|
56
64
|
params.temperature = args.temperature;
|
|
57
65
|
const envelope = buildSignedEnvelope({
|
|
58
|
-
iss:
|
|
66
|
+
iss: owner.did,
|
|
59
67
|
aud: url,
|
|
60
68
|
method: "aithos.compute_invoke",
|
|
61
|
-
verificationMethod: `${
|
|
69
|
+
verificationMethod: `${owner.did}#public`,
|
|
62
70
|
params,
|
|
63
|
-
signer:
|
|
71
|
+
signer: publicKp,
|
|
64
72
|
});
|
|
65
73
|
let res;
|
|
66
74
|
try {
|
package/dist/src/ethos.d.ts
CHANGED
|
@@ -1,2 +1,118 @@
|
|
|
1
|
-
|
|
1
|
+
import { type Section } from "@aithos/protocol-client";
|
|
2
|
+
import type { AithosAuth } from "./auth.js";
|
|
3
|
+
import type { AithosSdkEndpoints } from "./endpoints.js";
|
|
4
|
+
import type { DelegateActor } from "./internal/delegate-state.js";
|
|
5
|
+
import type { OwnerSigners } from "./internal/owner-signers.js";
|
|
6
|
+
export type ZoneName = "public" | "circle" | "self";
|
|
7
|
+
export declare const ZONE_NAMES: readonly ZoneName[];
|
|
8
|
+
export interface AddSectionInput {
|
|
9
|
+
readonly title: string;
|
|
10
|
+
readonly body: string;
|
|
11
|
+
readonly tags?: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
export interface UpdateSectionPatch {
|
|
14
|
+
readonly title?: string;
|
|
15
|
+
readonly body?: string;
|
|
16
|
+
readonly tags?: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
export type StagedChange = {
|
|
19
|
+
readonly kind: "add";
|
|
20
|
+
readonly zone: ZoneName;
|
|
21
|
+
readonly section: Section;
|
|
22
|
+
} | {
|
|
23
|
+
readonly kind: "update";
|
|
24
|
+
readonly zone: ZoneName;
|
|
25
|
+
readonly sectionId: string;
|
|
26
|
+
readonly patch: UpdateSectionPatch;
|
|
27
|
+
} | {
|
|
28
|
+
readonly kind: "delete";
|
|
29
|
+
readonly zone: ZoneName;
|
|
30
|
+
readonly sectionId: string;
|
|
31
|
+
};
|
|
32
|
+
export interface PublishResult {
|
|
33
|
+
/** New manifest height after publish. */
|
|
34
|
+
readonly editionHeight: number;
|
|
35
|
+
/** SHA-256 hex of the canonical manifest. */
|
|
36
|
+
readonly manifestHash: string;
|
|
37
|
+
/** DID of the subject we published for. */
|
|
38
|
+
readonly subjectDid: string;
|
|
39
|
+
/** Zones whose contents changed in this edition. */
|
|
40
|
+
readonly zonesPublished: readonly ZoneName[];
|
|
41
|
+
}
|
|
42
|
+
type ActorOwner = {
|
|
43
|
+
readonly kind: "owner";
|
|
44
|
+
readonly subjectDid: string;
|
|
45
|
+
readonly signers: OwnerSigners;
|
|
46
|
+
};
|
|
47
|
+
type ActorDelegate = {
|
|
48
|
+
readonly kind: "delegate";
|
|
49
|
+
readonly subjectDid: string;
|
|
50
|
+
readonly actor: DelegateActor;
|
|
51
|
+
};
|
|
52
|
+
type ActorAnonymous = {
|
|
53
|
+
readonly kind: "anonymous";
|
|
54
|
+
readonly subjectDid: string;
|
|
55
|
+
};
|
|
56
|
+
type Actor = ActorOwner | ActorDelegate | ActorAnonymous;
|
|
57
|
+
export declare class EthosClient {
|
|
58
|
+
#private;
|
|
59
|
+
readonly subjectDid: string;
|
|
60
|
+
readonly mode: Actor["kind"];
|
|
61
|
+
constructor(actor: Actor);
|
|
62
|
+
/** Return the per-zone proxy. */
|
|
63
|
+
zone(name: ZoneName): EthosZone;
|
|
64
|
+
hasPendingChanges(): boolean;
|
|
65
|
+
pendingChanges(): readonly StagedChange[];
|
|
66
|
+
discard(): void;
|
|
67
|
+
/**
|
|
68
|
+
* Build and publish a new edition with all staged mutations applied.
|
|
69
|
+
* Throws if there's nothing staged. After a successful publish, the
|
|
70
|
+
* mutation buffer is cleared and any cached snapshot is invalidated
|
|
71
|
+
* so the next read picks up the fresh edition.
|
|
72
|
+
*/
|
|
73
|
+
publish(): Promise<PublishResult>;
|
|
74
|
+
_readZone(zone: ZoneName): Promise<readonly Section[]>;
|
|
75
|
+
_stageAdd(zone: ZoneName, input: AddSectionInput): void;
|
|
76
|
+
_stageUpdate(zone: ZoneName, sectionId: string, patch: UpdateSectionPatch): void;
|
|
77
|
+
_stageDelete(zone: ZoneName, sectionId: string): void;
|
|
78
|
+
}
|
|
79
|
+
export declare class EthosZone {
|
|
80
|
+
#private;
|
|
81
|
+
constructor(parent: EthosClient, name: ZoneName);
|
|
82
|
+
get name(): ZoneName;
|
|
83
|
+
/** Effective sections (persisted + staged mutations applied). */
|
|
84
|
+
sections(): Promise<readonly Section[]>;
|
|
85
|
+
addSection(input: AddSectionInput): void;
|
|
86
|
+
updateSection(sectionId: string, patch: UpdateSectionPatch): void;
|
|
87
|
+
deleteSection(sectionId: string): void;
|
|
88
|
+
}
|
|
89
|
+
export interface EthosNamespaceDeps {
|
|
90
|
+
readonly auth: AithosAuth;
|
|
91
|
+
readonly endpoints: AithosSdkEndpoints;
|
|
92
|
+
readonly fetch: typeof fetch;
|
|
93
|
+
}
|
|
94
|
+
export declare class EthosNamespace {
|
|
95
|
+
#private;
|
|
96
|
+
constructor(deps: EthosNamespaceDeps);
|
|
97
|
+
/**
|
|
98
|
+
* EthosClient for the currently signed-in owner. Throws if there is
|
|
99
|
+
* no owner — callers should check `auth.canSignAsOwner()` first or
|
|
100
|
+
* surface the error to the user as "please sign in".
|
|
101
|
+
*/
|
|
102
|
+
me(): EthosClient;
|
|
103
|
+
/**
|
|
104
|
+
* EthosClient for an arbitrary subject DID. The mode is resolved at
|
|
105
|
+
* construction time:
|
|
106
|
+
* - if `did` matches the currently signed-in owner → owner mode
|
|
107
|
+
* - else if a mandate held by `auth` covers this subject → delegate mode
|
|
108
|
+
* - else → anonymous read-only mode
|
|
109
|
+
*
|
|
110
|
+
* Async signature so future implementations may do an eager manifest
|
|
111
|
+
* fetch (e.g. to fail fast on unknown DIDs); today resolution is sync
|
|
112
|
+
* and the actual fetch happens on the first `sections()` / `publish()`
|
|
113
|
+
* call.
|
|
114
|
+
*/
|
|
115
|
+
of(did: string): Promise<EthosClient>;
|
|
116
|
+
}
|
|
117
|
+
export {};
|
|
2
118
|
//# sourceMappingURL=ethos.d.ts.map
|
package/dist/src/ethos.js
CHANGED
|
@@ -1,21 +1,422 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright 2026 Mathieu Colla
|
|
3
|
-
//
|
|
3
|
+
// `sdk.ethos` namespace — high-level ethos editing.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
// `@aithos/protocol-client` for the common operations: parsing zone
|
|
7
|
-
// markdown, composing zone documents, building signed editions of the
|
|
8
|
-
// user's ethos.
|
|
5
|
+
// Lazy + commit pattern:
|
|
9
6
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
|
|
7
|
+
// const me = sdk.ethos.me();
|
|
8
|
+
// me.zone("public").addSection({ title, body });
|
|
9
|
+
// me.zone("circle").deleteSection(id);
|
|
10
|
+
// await me.publish(); // 1 edition
|
|
11
|
+
//
|
|
12
|
+
// Mutations stage in memory. `sections()` / `pendingChanges()` /
|
|
13
|
+
// `publish()` are the points where the snapshot is fetched (memoized
|
|
14
|
+
// per zone) and the staged changes are applied on top.
|
|
15
|
+
//
|
|
16
|
+
// Three actor modes:
|
|
17
|
+
// - owner — sdk.ethos.me() returns this; full access to public
|
|
18
|
+
// + private zones using OwnerSigners
|
|
19
|
+
// - delegate — sdk.ethos.of(did) when a mandate matches this
|
|
20
|
+
// subject; capability bounded by mandate scopes
|
|
21
|
+
// - anonymous — sdk.ethos.of(did) when no mandate matches; public
|
|
22
|
+
// zone read-only, every write rejected
|
|
23
|
+
//
|
|
24
|
+
// The namespace stays thin: it builds an EthosClient with the right
|
|
25
|
+
// actor and forwards all real work into protocol-client's
|
|
26
|
+
// `loadEditSnapshot` / `publishZoneEdit` / `publishPublicZoneAsDelegate`
|
|
27
|
+
// / `publishPrivateZoneAsDelegate`.
|
|
28
|
+
import { addSectionToList, deleteSectionFromList, loadEditSnapshot, modifySectionInList, publishPrivateZoneAsDelegate, publishPublicZoneAsDelegate, publishZoneEdit, } from "@aithos/protocol-client";
|
|
29
|
+
import { delegateKeyPair } from "./internal/protocol-client-bridge.js";
|
|
30
|
+
import { AithosSDKError } from "./types.js";
|
|
31
|
+
export const ZONE_NAMES = ["public", "circle", "self"];
|
|
32
|
+
/* -------------------------------------------------------------------------- */
|
|
33
|
+
/* EthosClient — per-subject working buffer */
|
|
34
|
+
/* -------------------------------------------------------------------------- */
|
|
35
|
+
export class EthosClient {
|
|
36
|
+
subjectDid;
|
|
37
|
+
mode;
|
|
38
|
+
#actor;
|
|
39
|
+
#snapshots = new Map();
|
|
40
|
+
#mutations = [];
|
|
41
|
+
constructor(actor) {
|
|
42
|
+
this.#actor = actor;
|
|
43
|
+
this.subjectDid = actor.subjectDid;
|
|
44
|
+
this.mode = actor.kind;
|
|
45
|
+
}
|
|
46
|
+
/** Return the per-zone proxy. */
|
|
47
|
+
zone(name) {
|
|
48
|
+
if (!ZONE_NAMES.includes(name)) {
|
|
49
|
+
throw new AithosSDKError("ethos_invalid_zone", `unknown zone "${String(name)}"`);
|
|
50
|
+
}
|
|
51
|
+
return new EthosZone(this, name);
|
|
52
|
+
}
|
|
53
|
+
hasPendingChanges() {
|
|
54
|
+
return this.#mutations.length > 0;
|
|
55
|
+
}
|
|
56
|
+
pendingChanges() {
|
|
57
|
+
return this.#mutations.slice();
|
|
58
|
+
}
|
|
59
|
+
discard() {
|
|
60
|
+
this.#mutations = [];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build and publish a new edition with all staged mutations applied.
|
|
64
|
+
* Throws if there's nothing staged. After a successful publish, the
|
|
65
|
+
* mutation buffer is cleared and any cached snapshot is invalidated
|
|
66
|
+
* so the next read picks up the fresh edition.
|
|
67
|
+
*/
|
|
68
|
+
async publish() {
|
|
69
|
+
if (this.#actor.kind === "anonymous") {
|
|
70
|
+
throw new AithosSDKError("ethos_anonymous_cannot_publish", "anonymous reader has no signing capability");
|
|
71
|
+
}
|
|
72
|
+
if (this.#mutations.length === 0) {
|
|
73
|
+
throw new AithosSDKError("ethos_nothing_to_publish", "no staged mutations; call addSection / updateSection / deleteSection first");
|
|
74
|
+
}
|
|
75
|
+
// Compute effective sections per zone, but only for zones that
|
|
76
|
+
// actually have staged mutations. Zones without staged changes
|
|
77
|
+
// roll forward by reusing protocol-client's existing bytes.
|
|
78
|
+
const touched = new Set(this.#mutations.map((m) => m.zone));
|
|
79
|
+
// Owner-side path is the most common: owner can write any zone they
|
|
80
|
+
// have staged mutations for. Build everything we need then call
|
|
81
|
+
// publishZoneEdit once.
|
|
82
|
+
if (this.#actor.kind === "owner") {
|
|
83
|
+
// Need the snapshot regardless — we read base sections + zoneBytes.
|
|
84
|
+
const snap = await this.#ensureSnapshotOwner();
|
|
85
|
+
const newPublic = touched.has("public")
|
|
86
|
+
? this.#applyMutations("public", snap.publicSections)
|
|
87
|
+
: undefined;
|
|
88
|
+
const newCircle = touched.has("circle")
|
|
89
|
+
? this.#applyMutations("circle", snap.circleSections ?? [])
|
|
90
|
+
: undefined;
|
|
91
|
+
const newSelf = touched.has("self")
|
|
92
|
+
? this.#applyMutations("self", snap.selfSections ?? [])
|
|
93
|
+
: undefined;
|
|
94
|
+
const identity = this.#actor.signers._unsafeStoredIdentity();
|
|
95
|
+
try {
|
|
96
|
+
const result = await publishZoneEdit({
|
|
97
|
+
identity,
|
|
98
|
+
snapshot: snap,
|
|
99
|
+
// protocol-client's publishZoneEdit always wants newPublic; if we
|
|
100
|
+
// didn't touch public we re-publish the existing public sections.
|
|
101
|
+
newPublicSections: newPublic ?? snap.publicSections,
|
|
102
|
+
...(newCircle ? { newCircleSections: newCircle } : {}),
|
|
103
|
+
...(newSelf ? { newSelfSections: newSelf } : {}),
|
|
104
|
+
});
|
|
105
|
+
this.#afterPublish();
|
|
106
|
+
return projectPublishResult(result.manifest, this.subjectDid, [
|
|
107
|
+
...touched,
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
// identity holds raw seeds via reference; not our copy to zeroize
|
|
112
|
+
// (the underlying seeds live in the OwnerSigners' private fields).
|
|
113
|
+
// Drop our reference to the temporary StoredIdentity object.
|
|
114
|
+
void identity;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Delegate path. Per-zone scope check + per-zone protocol-client
|
|
118
|
+
// function. For a multi-zone delegate publish we'd need to call
|
|
119
|
+
// multiple endpoints; we iterate zones, calling the right primitive.
|
|
120
|
+
if (this.#actor.kind === "delegate") {
|
|
121
|
+
const actor = this.#actor.actor;
|
|
122
|
+
const stored = delegateActorToStored(actor);
|
|
123
|
+
// Validate scopes upfront.
|
|
124
|
+
for (const zone of touched) {
|
|
125
|
+
const required = `ethos.write.${zone}`;
|
|
126
|
+
const scopes = actor.mandate["scopes"] ?? [];
|
|
127
|
+
if (!Array.isArray(scopes) || !scopes.includes(required)) {
|
|
128
|
+
throw new AithosSDKError("ethos_delegate_scope_missing", `delegate mandate does not grant ${required}`, { data: { mandateId: actor.mandateId, scopes } });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const snap = await this.#ensureSnapshotDelegate();
|
|
132
|
+
// Public-zone update — use publishPublicZoneAsDelegate.
|
|
133
|
+
if (touched.has("public")) {
|
|
134
|
+
const newPublic = this.#applyMutations("public", snap.publicSections);
|
|
135
|
+
await publishPublicZoneAsDelegate({
|
|
136
|
+
delegate: stored,
|
|
137
|
+
snapshot: snap,
|
|
138
|
+
newPublicSections: newPublic,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
// Private zones — one publishPrivateZoneAsDelegate call each.
|
|
142
|
+
for (const zone of ["circle", "self"]) {
|
|
143
|
+
if (!touched.has(zone))
|
|
144
|
+
continue;
|
|
145
|
+
const base = (zone === "circle"
|
|
146
|
+
? snap.circleSections
|
|
147
|
+
: snap.selfSections) ?? [];
|
|
148
|
+
const newSections = this.#applyMutations(zone, base);
|
|
149
|
+
await publishPrivateZoneAsDelegate({
|
|
150
|
+
delegate: stored,
|
|
151
|
+
snapshot: snap,
|
|
152
|
+
zone,
|
|
153
|
+
newSections,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
this.#afterPublish();
|
|
157
|
+
// We don't have a single "manifest" to project from since each call
|
|
158
|
+
// returned its own. Use the last touched call's snapshot as the
|
|
159
|
+
// base for height — fetch fresh on next read anyway.
|
|
160
|
+
return {
|
|
161
|
+
editionHeight: snap.manifest.edition.height + 1,
|
|
162
|
+
manifestHash: "", // not surfaced by protocol-client today on the delegate path
|
|
163
|
+
subjectDid: this.subjectDid,
|
|
164
|
+
zonesPublished: [...touched],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
// Unreachable — anonymous case returned earlier.
|
|
168
|
+
throw new AithosSDKError("ethos_invalid_actor", "unsupported actor for publish()");
|
|
169
|
+
}
|
|
170
|
+
/* ------------------------------------------------------------------------ */
|
|
171
|
+
/* Internal — snapshot management */
|
|
172
|
+
/* ------------------------------------------------------------------------ */
|
|
173
|
+
async _readZone(zone) {
|
|
174
|
+
if (this.#actor.kind === "anonymous") {
|
|
175
|
+
if (zone !== "public") {
|
|
176
|
+
throw new AithosSDKError("ethos_anonymous_private_zone", `anonymous reader cannot access the "${zone}" zone`);
|
|
177
|
+
}
|
|
178
|
+
const snap = await this.#ensureSnapshotAnonymous();
|
|
179
|
+
const base = snap.publicSections;
|
|
180
|
+
return this.#applyMutations(zone, base);
|
|
181
|
+
}
|
|
182
|
+
if (this.#actor.kind === "owner") {
|
|
183
|
+
const snap = await this.#ensureSnapshotOwner();
|
|
184
|
+
const base = baseSectionsFromSnapshot(snap, zone);
|
|
185
|
+
// Surface decryption errors clearly (if reading a private zone we
|
|
186
|
+
// can't unwrap, the snapshot has it in zoneDecryptErrors).
|
|
187
|
+
if (zone !== "public" && snap.zoneDecryptErrors?.[zone]) {
|
|
188
|
+
throw new AithosSDKError("ethos_zone_unreadable", `cannot read ${zone}: ${snap.zoneDecryptErrors[zone]}`);
|
|
189
|
+
}
|
|
190
|
+
return this.#applyMutations(zone, base);
|
|
191
|
+
}
|
|
192
|
+
// Delegate
|
|
193
|
+
const snap = await this.#ensureSnapshotDelegate();
|
|
194
|
+
const base = baseSectionsFromSnapshot(snap, zone);
|
|
195
|
+
if (zone !== "public" && snap.zoneDecryptErrors?.[zone]) {
|
|
196
|
+
throw new AithosSDKError("ethos_zone_unreadable", `cannot read ${zone}: ${snap.zoneDecryptErrors[zone]}`);
|
|
197
|
+
}
|
|
198
|
+
return this.#applyMutations(zone, base);
|
|
199
|
+
}
|
|
200
|
+
_stageAdd(zone, input) {
|
|
201
|
+
if (this.#actor.kind === "anonymous") {
|
|
202
|
+
throw new AithosSDKError("ethos_anonymous_write", "anonymous reader cannot stage mutations");
|
|
203
|
+
}
|
|
204
|
+
const section = {
|
|
205
|
+
id: "sec_" + randomHex(12),
|
|
206
|
+
title: input.title,
|
|
207
|
+
body: input.body,
|
|
208
|
+
gamma_ref: "gamma_none_" + randomHex(24),
|
|
209
|
+
...(input.tags && input.tags.length > 0 ? { tags: input.tags } : {}),
|
|
210
|
+
};
|
|
211
|
+
this.#mutations.push({ kind: "add", zone, section });
|
|
212
|
+
}
|
|
213
|
+
_stageUpdate(zone, sectionId, patch) {
|
|
214
|
+
if (this.#actor.kind === "anonymous") {
|
|
215
|
+
throw new AithosSDKError("ethos_anonymous_write", "anonymous reader cannot stage mutations");
|
|
216
|
+
}
|
|
217
|
+
this.#mutations.push({ kind: "update", zone, sectionId, patch });
|
|
218
|
+
}
|
|
219
|
+
_stageDelete(zone, sectionId) {
|
|
220
|
+
if (this.#actor.kind === "anonymous") {
|
|
221
|
+
throw new AithosSDKError("ethos_anonymous_write", "anonymous reader cannot stage mutations");
|
|
222
|
+
}
|
|
223
|
+
this.#mutations.push({ kind: "delete", zone, sectionId });
|
|
224
|
+
}
|
|
225
|
+
/** Apply staged mutations for a zone on top of base sections. */
|
|
226
|
+
#applyMutations(zone, base) {
|
|
227
|
+
let acc = base;
|
|
228
|
+
for (const m of this.#mutations) {
|
|
229
|
+
if (m.zone !== zone)
|
|
230
|
+
continue;
|
|
231
|
+
switch (m.kind) {
|
|
232
|
+
case "add":
|
|
233
|
+
acc = addSectionToList(acc, {
|
|
234
|
+
title: m.section.title,
|
|
235
|
+
body: m.section.body,
|
|
236
|
+
...(m.section.tags ? { tags: m.section.tags } : {}),
|
|
237
|
+
});
|
|
238
|
+
break;
|
|
239
|
+
case "update":
|
|
240
|
+
acc = modifySectionInList(acc, {
|
|
241
|
+
sectionId: m.sectionId,
|
|
242
|
+
...(m.patch.title !== undefined ? { title: m.patch.title } : {}),
|
|
243
|
+
...(m.patch.body !== undefined ? { body: m.patch.body } : {}),
|
|
244
|
+
...(m.patch.tags !== undefined ? { tags: m.patch.tags } : {}),
|
|
245
|
+
});
|
|
246
|
+
break;
|
|
247
|
+
case "delete":
|
|
248
|
+
acc = deleteSectionFromList(acc, m.sectionId);
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return acc;
|
|
253
|
+
}
|
|
254
|
+
async #ensureSnapshotOwner() {
|
|
255
|
+
const cached = this.#snapshots.get("public");
|
|
256
|
+
if (cached)
|
|
257
|
+
return cached;
|
|
258
|
+
if (this.#actor.kind !== "owner") {
|
|
259
|
+
throw new AithosSDKError("ethos_invalid_actor", "expected owner actor");
|
|
260
|
+
}
|
|
261
|
+
const identity = this.#actor.signers._unsafeStoredIdentity();
|
|
262
|
+
const snap = await loadEditSnapshot(this.subjectDid, identity);
|
|
263
|
+
this.#cacheSnapshotAllZones(snap);
|
|
264
|
+
return snap;
|
|
265
|
+
}
|
|
266
|
+
async #ensureSnapshotDelegate() {
|
|
267
|
+
const cached = this.#snapshots.get("public");
|
|
268
|
+
if (cached)
|
|
269
|
+
return cached;
|
|
270
|
+
if (this.#actor.kind !== "delegate") {
|
|
271
|
+
throw new AithosSDKError("ethos_invalid_actor", "expected delegate actor");
|
|
272
|
+
}
|
|
273
|
+
const stored = delegateActorToStored(this.#actor.actor);
|
|
274
|
+
const snap = await loadEditSnapshot(this.subjectDid, undefined, stored);
|
|
275
|
+
this.#cacheSnapshotAllZones(snap);
|
|
276
|
+
return snap;
|
|
277
|
+
}
|
|
278
|
+
async #ensureSnapshotAnonymous() {
|
|
279
|
+
const cached = this.#snapshots.get("public");
|
|
280
|
+
if (cached)
|
|
281
|
+
return cached;
|
|
282
|
+
const snap = await loadEditSnapshot(this.subjectDid);
|
|
283
|
+
this.#cacheSnapshotAllZones(snap);
|
|
284
|
+
return snap;
|
|
285
|
+
}
|
|
286
|
+
#cacheSnapshotAllZones(snap) {
|
|
287
|
+
for (const z of ZONE_NAMES)
|
|
288
|
+
this.#snapshots.set(z, snap);
|
|
289
|
+
}
|
|
290
|
+
#afterPublish() {
|
|
291
|
+
this.#mutations = [];
|
|
292
|
+
this.#snapshots.clear();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/* -------------------------------------------------------------------------- */
|
|
296
|
+
/* EthosZone — per-zone proxy */
|
|
297
|
+
/* -------------------------------------------------------------------------- */
|
|
298
|
+
export class EthosZone {
|
|
299
|
+
#parent;
|
|
300
|
+
#name;
|
|
301
|
+
constructor(parent, name) {
|
|
302
|
+
this.#parent = parent;
|
|
303
|
+
this.#name = name;
|
|
304
|
+
}
|
|
305
|
+
get name() {
|
|
306
|
+
return this.#name;
|
|
307
|
+
}
|
|
308
|
+
/** Effective sections (persisted + staged mutations applied). */
|
|
309
|
+
async sections() {
|
|
310
|
+
return this.#parent._readZone(this.#name);
|
|
311
|
+
}
|
|
312
|
+
addSection(input) {
|
|
313
|
+
this.#parent._stageAdd(this.#name, input);
|
|
314
|
+
}
|
|
315
|
+
updateSection(sectionId, patch) {
|
|
316
|
+
this.#parent._stageUpdate(this.#name, sectionId, patch);
|
|
317
|
+
}
|
|
318
|
+
deleteSection(sectionId) {
|
|
319
|
+
this.#parent._stageDelete(this.#name, sectionId);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
export class EthosNamespace {
|
|
323
|
+
#deps;
|
|
324
|
+
constructor(deps) {
|
|
325
|
+
this.#deps = deps;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* EthosClient for the currently signed-in owner. Throws if there is
|
|
329
|
+
* no owner — callers should check `auth.canSignAsOwner()` first or
|
|
330
|
+
* surface the error to the user as "please sign in".
|
|
331
|
+
*/
|
|
332
|
+
me() {
|
|
333
|
+
const signers = this.#deps.auth._getOwnerSigners();
|
|
334
|
+
if (!signers || signers.destroyed) {
|
|
335
|
+
throw new AithosSDKError("ethos_no_owner", "no owner signed in; sign in with email/password, Google, or a recovery file first");
|
|
336
|
+
}
|
|
337
|
+
return new EthosClient({
|
|
338
|
+
kind: "owner",
|
|
339
|
+
subjectDid: signers.did,
|
|
340
|
+
signers,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* EthosClient for an arbitrary subject DID. The mode is resolved at
|
|
345
|
+
* construction time:
|
|
346
|
+
* - if `did` matches the currently signed-in owner → owner mode
|
|
347
|
+
* - else if a mandate held by `auth` covers this subject → delegate mode
|
|
348
|
+
* - else → anonymous read-only mode
|
|
349
|
+
*
|
|
350
|
+
* Async signature so future implementations may do an eager manifest
|
|
351
|
+
* fetch (e.g. to fail fast on unknown DIDs); today resolution is sync
|
|
352
|
+
* and the actual fetch happens on the first `sections()` / `publish()`
|
|
353
|
+
* call.
|
|
354
|
+
*/
|
|
355
|
+
async of(did) {
|
|
356
|
+
const owner = this.#deps.auth._getOwnerSigners();
|
|
357
|
+
if (owner && !owner.destroyed && owner.did === did) {
|
|
358
|
+
return new EthosClient({
|
|
359
|
+
kind: "owner",
|
|
360
|
+
subjectDid: did,
|
|
361
|
+
signers: owner,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
const delegate = this.#deps.auth._findDelegateForSubject(did);
|
|
365
|
+
if (delegate && !delegate.destroyed) {
|
|
366
|
+
return new EthosClient({
|
|
367
|
+
kind: "delegate",
|
|
368
|
+
subjectDid: did,
|
|
369
|
+
actor: delegate,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
return new EthosClient({ kind: "anonymous", subjectDid: did });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/* -------------------------------------------------------------------------- */
|
|
376
|
+
/* Helpers */
|
|
377
|
+
/* -------------------------------------------------------------------------- */
|
|
378
|
+
function baseSectionsFromSnapshot(snap, zone) {
|
|
379
|
+
switch (zone) {
|
|
380
|
+
case "public":
|
|
381
|
+
return snap.publicSections;
|
|
382
|
+
case "circle":
|
|
383
|
+
return snap.circleSections ?? [];
|
|
384
|
+
case "self":
|
|
385
|
+
return snap.selfSections ?? [];
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function delegateActorToStored(a) {
|
|
389
|
+
const kp = delegateKeyPair(a);
|
|
390
|
+
let hex = "";
|
|
391
|
+
for (let i = 0; i < kp.seed.length; i++) {
|
|
392
|
+
hex += kp.seed[i].toString(16).padStart(2, "0");
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
version: "0.1.0",
|
|
396
|
+
subjectDid: a.subjectDid,
|
|
397
|
+
mandate: a.mandate,
|
|
398
|
+
mandateId: a.mandateId,
|
|
399
|
+
granteeId: a.granteeId,
|
|
400
|
+
granteePubkeyMultibase: a.granteePubkeyMultibase,
|
|
401
|
+
delegateSeedHex: hex,
|
|
402
|
+
importedAt: new Date().toISOString(),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function projectPublishResult(manifest, subjectDid, zones) {
|
|
406
|
+
return {
|
|
407
|
+
editionHeight: manifest.edition.height,
|
|
408
|
+
manifestHash: manifest.bundle_id ?? "",
|
|
409
|
+
subjectDid,
|
|
410
|
+
zonesPublished: zones,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function randomHex(n) {
|
|
414
|
+
const bytes = new Uint8Array(Math.ceil(n / 2));
|
|
415
|
+
crypto.getRandomValues(bytes);
|
|
416
|
+
let hex = "";
|
|
417
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
418
|
+
hex += bytes[i].toString(16).padStart(2, "0");
|
|
419
|
+
}
|
|
420
|
+
return hex.slice(0, n);
|
|
421
|
+
}
|
|
21
422
|
//# sourceMappingURL=ethos.js.map
|