@aithos/sdk 0.1.0-alpha.4 → 0.1.0-alpha.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +211 -7
  2. package/dist/src/apps.d.ts +155 -0
  3. package/dist/src/apps.js +288 -0
  4. package/dist/src/assets.d.ts +207 -0
  5. package/dist/src/assets.js +533 -0
  6. package/dist/src/auth-api.d.ts +138 -0
  7. package/dist/src/auth-api.js +168 -0
  8. package/dist/src/auth.d.ts +536 -119
  9. package/dist/src/auth.js +1207 -152
  10. package/dist/src/compute.d.ts +251 -9
  11. package/dist/src/compute.js +293 -16
  12. package/dist/src/data-schema-contacts-v1.d.ts +14 -0
  13. package/dist/src/data-schema-contacts-v1.js +28 -0
  14. package/dist/src/data.d.ts +153 -0
  15. package/dist/src/data.js +670 -0
  16. package/dist/src/endpoints.d.ts +9 -0
  17. package/dist/src/endpoints.js +5 -0
  18. package/dist/src/ethos.d.ts +202 -1
  19. package/dist/src/ethos.js +821 -16
  20. package/dist/src/index.d.ts +18 -6
  21. package/dist/src/index.js +39 -6
  22. package/dist/src/internal/delegate-bundle.d.ts +18 -0
  23. package/dist/src/internal/delegate-bundle.js +94 -0
  24. package/dist/src/internal/delegate-state.d.ts +45 -0
  25. package/dist/src/internal/delegate-state.js +120 -0
  26. package/dist/src/internal/envelope.d.ts +77 -0
  27. package/dist/src/internal/envelope.js +154 -0
  28. package/dist/src/internal/owner-signers.d.ts +78 -0
  29. package/dist/src/internal/owner-signers.js +179 -0
  30. package/dist/src/internal/protocol-client-bridge.d.ts +8 -0
  31. package/dist/src/internal/protocol-client-bridge.js +20 -0
  32. package/dist/src/internal/recovery-file.d.ts +29 -0
  33. package/dist/src/internal/recovery-file.js +98 -0
  34. package/dist/src/internal/signer.d.ts +59 -0
  35. package/dist/src/internal/signer.js +86 -0
  36. package/dist/src/key-store.d.ts +128 -0
  37. package/dist/src/key-store.js +244 -0
  38. package/dist/src/mandates.d.ts +163 -1
  39. package/dist/src/mandates.js +286 -8
  40. package/dist/src/react/AithosAsset.d.ts +66 -0
  41. package/dist/src/react/AithosAsset.js +67 -0
  42. package/dist/src/react/context.d.ts +29 -0
  43. package/dist/src/react/context.js +31 -0
  44. package/dist/src/react/index.d.ts +28 -0
  45. package/dist/src/react/index.js +30 -0
  46. package/dist/src/react/use-aithos-asset.d.ts +39 -0
  47. package/dist/src/react/use-aithos-asset.js +118 -0
  48. package/dist/src/sdk.d.ts +46 -3
  49. package/dist/src/sdk.js +49 -23
  50. package/dist/src/wallet.d.ts +4 -6
  51. package/dist/src/wallet.js +18 -8
  52. package/dist/src/web.d.ts +279 -0
  53. package/dist/src/web.js +186 -0
  54. package/dist/test/auth-j3.test.d.ts +2 -0
  55. package/dist/test/auth-j3.test.js +391 -0
  56. package/dist/test/compute-delegate-path.test.d.ts +2 -0
  57. package/dist/test/compute-delegate-path.test.js +183 -0
  58. package/dist/test/compute.test.js +26 -11
  59. package/dist/test/endpoints.test.js +20 -1
  60. package/dist/test/envelope.test.d.ts +2 -0
  61. package/dist/test/envelope.test.js +318 -0
  62. package/dist/test/ethos-first-edition.test.d.ts +2 -0
  63. package/dist/test/ethos-first-edition.test.js +248 -0
  64. package/dist/test/ethos.test.d.ts +2 -0
  65. package/dist/test/ethos.test.js +219 -0
  66. package/dist/test/key-store.test.d.ts +2 -0
  67. package/dist/test/key-store.test.js +161 -0
  68. package/dist/test/mandates-compute.test.d.ts +2 -0
  69. package/dist/test/mandates-compute.test.js +256 -0
  70. package/dist/test/mandates.test.d.ts +2 -0
  71. package/dist/test/mandates.test.js +93 -0
  72. package/dist/test/sdk.test.js +70 -30
  73. package/dist/test/signer.test.d.ts +2 -0
  74. package/dist/test/signer.test.js +117 -0
  75. package/dist/test/signup-bootstrap.test.d.ts +2 -0
  76. package/dist/test/signup-bootstrap.test.js +311 -0
  77. package/dist/test/wallet.test.js +20 -9
  78. package/dist/test/web.test.d.ts +2 -0
  79. package/dist/test/web.test.js +270 -0
  80. package/package.json +18 -3
@@ -0,0 +1,186 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 Mathieu Colla
3
+ // Web namespace — `aithos.web_extract` through the web-extractor proxy.
4
+ //
5
+ // Same JSON-RPC + signed-envelope protocol as the compute namespace, but
6
+ // against a separate Aithos service (`extract.aithos.be`). Pricing is a
7
+ // flat 1 microcredit per successful extraction (refunded on failure).
8
+ //
9
+ // The mandate scope is `web.extract` (exported as {@link WEB_EXTRACT_SCOPE}
10
+ // for owner mint-time use). A delegate that holds only this scope can read
11
+ // pages on the owner's behalf without gaining LLM-spend authority.
12
+ //
13
+ // Signing follows the same owner-vs-delegate logic as the compute namespace
14
+ // (see ComputeNamespace.#resolveSigner). The duplication is bounded — both
15
+ // namespaces' `#signAndPost` helpers can later move into a shared internal
16
+ // once a third primitive arrives.
17
+ import { buildSignedEnvelope, } from "@aithos/protocol-client";
18
+ import { webInvokeUrl, } from "./endpoints.js";
19
+ import { delegateKeyPair, ownerKeyPair, } from "./internal/protocol-client-bridge.js";
20
+ import { AithosSDKError } from "./types.js";
21
+ /** Opt-in scope a mandate must carry to invoke `aithos.web_extract`. */
22
+ export const WEB_EXTRACT_SCOPE = "web.extract";
23
+ /**
24
+ * `sdk.web` namespace — Aithos's web extraction primitive.
25
+ *
26
+ * Designed so a downstream agent can read the static content of any
27
+ * public page (HTML, purged CSS, computed visual signature) without
28
+ * involving an LLM — saving ~30× over a Bedrock-based extraction in
29
+ * both latency and cost.
30
+ *
31
+ * @throws {AithosSDKError} — same error taxonomy as `sdk.compute`,
32
+ * including `-32071` (insufficient balance with `{required, available}`
33
+ * in `data`) and `-32042` (mandate scope mismatch).
34
+ */
35
+ export class WebNamespace {
36
+ #deps;
37
+ constructor(deps) {
38
+ this.#deps = deps;
39
+ }
40
+ /**
41
+ * Extract a public webpage. Returns the cleaned HTML, purged CSS and
42
+ * a deterministic visual signature (palette, typography, dominant
43
+ * radii, spacing, layout mode, component digests).
44
+ */
45
+ async extract(args) {
46
+ const { endpoints, fetch: fetchImpl } = this.#deps;
47
+ const choice = this.#resolveSigner(args.mandateId);
48
+ const url = webInvokeUrl(endpoints);
49
+ const params = {
50
+ app_did: this.#deps.appDid,
51
+ mandate_id: this.#resolveMandateIdForWire(args.mandateId, choice),
52
+ url: args.url,
53
+ };
54
+ if (args.waitUntil !== undefined)
55
+ params.waitUntil = args.waitUntil;
56
+ if (args.timeoutMs !== undefined)
57
+ params.timeoutMs = args.timeoutMs;
58
+ if (args.idempotencyKey !== undefined) {
59
+ params.idempotencyKey = args.idempotencyKey;
60
+ }
61
+ return await this.#signAndPost({
62
+ url,
63
+ method: "aithos.web_extract",
64
+ params,
65
+ choice,
66
+ fetchImpl,
67
+ signal: args.signal,
68
+ });
69
+ }
70
+ /**
71
+ * Fetch a single asset (image / font / css / json …) server-side,
72
+ * bypassing browser CORS. Returns the bytes as base64 + content-type.
73
+ *
74
+ * Use when `fetch(url, {mode: "cors"})` and `<img crossOrigin>`
75
+ * canvas readback both fail because the asset server doesn't return
76
+ * Access-Control-Allow-Origin headers — typical for production
77
+ * sites' logos hosted on the main domain.
78
+ *
79
+ * For the common "logo of a webpage" case the lambda already
80
+ * resolves and embeds the best symbol-only logo in
81
+ * {@link extract}'s `data.logo` field; you only need fetchAsset
82
+ * when extract's logo doesn't fit, when picking up secondary
83
+ * assets (og:image, hero image, document download), or when
84
+ * fetching an asset on a page you haven't extracted.
85
+ *
86
+ * Costs 1 mc per successful fetch, full refund on failure. Server
87
+ * caps: 15 s timeout, 10 MB body, http/https only.
88
+ */
89
+ async fetchAsset(args) {
90
+ const { endpoints, fetch: fetchImpl } = this.#deps;
91
+ const choice = this.#resolveSigner(args.mandateId);
92
+ const url = webInvokeUrl(endpoints);
93
+ const params = {
94
+ app_did: this.#deps.appDid,
95
+ mandate_id: this.#resolveMandateIdForWire(args.mandateId, choice),
96
+ url: args.url,
97
+ };
98
+ return await this.#signAndPost({
99
+ url,
100
+ method: "aithos.web_fetch_asset",
101
+ params,
102
+ choice,
103
+ fetchImpl,
104
+ signal: args.signal,
105
+ });
106
+ }
107
+ /* ----------------------------- internals ----------------------------- */
108
+ #resolveSigner(mandateId) {
109
+ const { auth } = this.#deps;
110
+ const owner = auth._getOwnerSigners();
111
+ const ownerLoaded = owner !== null && !owner.destroyed;
112
+ if (ownerLoaded) {
113
+ const publicKp = ownerKeyPair(owner, "public");
114
+ return {
115
+ kind: "owner",
116
+ iss: owner.did,
117
+ verificationMethod: `${owner.did}#public`,
118
+ signer: publicKp,
119
+ mandate: undefined,
120
+ };
121
+ }
122
+ if (mandateId === undefined || mandateId.length === 0) {
123
+ throw new AithosSDKError("sdk_no_signer", "no owner signed in and no mandateId provided — pass a mandateId for a delegate session, or sign in as an owner first.");
124
+ }
125
+ const actor = auth._getDelegateActor(mandateId);
126
+ if (!actor || actor.destroyed) {
127
+ throw new AithosSDKError("sdk_no_delegate_for_mandate", `no owner signed in and no imported delegate mandate matches '${mandateId}'. Sign in as an owner, or import a delegate bundle for that mandate via auth.importMandate.`);
128
+ }
129
+ const kp = delegateKeyPair(actor);
130
+ return {
131
+ kind: "delegate",
132
+ iss: actor.subjectDid,
133
+ verificationMethod: actor.granteePubkeyMultibase,
134
+ signer: kp,
135
+ mandate: actor.mandate,
136
+ };
137
+ }
138
+ #resolveMandateIdForWire(explicit, choice) {
139
+ if (explicit && explicit.length > 0)
140
+ return explicit;
141
+ if (choice.kind === "delegate")
142
+ return choice.mandate.id;
143
+ return `${choice.iss}#self`;
144
+ }
145
+ async #signAndPost(opts) {
146
+ const { url, method, params, choice, fetchImpl, signal } = opts;
147
+ const envelope = buildSignedEnvelope({
148
+ iss: choice.iss,
149
+ aud: url,
150
+ method,
151
+ verificationMethod: choice.verificationMethod,
152
+ params,
153
+ signer: choice.signer,
154
+ ...(choice.kind === "delegate" ? { mandate: choice.mandate } : {}),
155
+ });
156
+ let res;
157
+ try {
158
+ res = await fetchImpl(url, {
159
+ method: "POST",
160
+ headers: { "content-type": "application/json" },
161
+ body: JSON.stringify({
162
+ jsonrpc: "2.0",
163
+ id: method,
164
+ method,
165
+ params: { ...params, _envelope: envelope },
166
+ }),
167
+ ...(signal ? { signal } : {}),
168
+ });
169
+ }
170
+ catch (e) {
171
+ throw new AithosSDKError("network", e.message);
172
+ }
173
+ if (!res.ok) {
174
+ throw new AithosSDKError("http", `HTTP ${res.status} ${res.statusText}`, { status: res.status });
175
+ }
176
+ const body = (await res.json());
177
+ if (body.error) {
178
+ throw new AithosSDKError(String(body.error.code), body.error.message, body.error.data ? { data: body.error.data } : undefined);
179
+ }
180
+ if (!body.result) {
181
+ throw new AithosSDKError("empty", "empty result from web extractor proxy");
182
+ }
183
+ return body.result;
184
+ }
185
+ }
186
+ //# sourceMappingURL=web.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=auth-j3.test.d.ts.map
@@ -0,0 +1,391 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 Mathieu Colla
3
+ // Tests for J3 — KeyStore integration, signInWithRecovery,
4
+ // importMandate, resume(), signOut(), state accessors.
5
+ import { strict as assert } from "node:assert";
6
+ import { describe, it } from "node:test";
7
+ import { createBrowserIdentity } from "@aithos/protocol-client";
8
+ import { AithosAuth, AithosSDKError, memoryKeyStore, noopStore, } from "../src/index.js";
9
+ import { parseDelegateBundle, readDelegateBundleText, } from "../src/internal/delegate-bundle.js";
10
+ import { parseRecoveryFile, serializeRecoveryFile, } from "../src/internal/recovery-file.js";
11
+ /* -------------------------------------------------------------------------- */
12
+ /* Test helpers */
13
+ /* -------------------------------------------------------------------------- */
14
+ function makeAuth(opts = {}) {
15
+ return new AithosAuth({
16
+ authBaseUrl: "https://auth.test",
17
+ fetch: (() => {
18
+ throw new Error("network not expected in this test");
19
+ }),
20
+ sessionStore: opts.sessionStore ?? noopStore(),
21
+ keyStore: opts.keyStore ?? memoryKeyStore(),
22
+ });
23
+ }
24
+ /** Build a recovery-file JSON string from a fresh BrowserIdentity. */
25
+ function recoveryTextFor(handle, displayName) {
26
+ const id = createBrowserIdentity(handle, displayName);
27
+ const { text } = serializeRecoveryFile(id);
28
+ return { text, did: id.did };
29
+ }
30
+ function delegateBundleText(args) {
31
+ return JSON.stringify({
32
+ aithos_delegate_version: "0.1.0",
33
+ mandate: {
34
+ id: args.mandateId,
35
+ subject_did: args.subjectDid,
36
+ grantee: {
37
+ id: args.granteeId,
38
+ pubkey: args.granteePubkeyMultibase ?? "z6MkqGenericPubKey",
39
+ },
40
+ scopes: args.scopes ?? ["ethos.read.public"],
41
+ },
42
+ delegate_seed_hex: "11".repeat(32),
43
+ });
44
+ }
45
+ /* -------------------------------------------------------------------------- */
46
+ /* parseRecoveryFile / serializeRecoveryFile */
47
+ /* -------------------------------------------------------------------------- */
48
+ describe("recovery file: parse + serialize", () => {
49
+ it("round-trips a fresh identity", () => {
50
+ const id = createBrowserIdentity("alice", "Alice");
51
+ const { text } = serializeRecoveryFile(id);
52
+ const parsed = parseRecoveryFile(text);
53
+ assert.equal(parsed.did, id.did);
54
+ assert.equal(parsed.handle, id.handle);
55
+ assert.equal(parsed.displayName, id.displayName);
56
+ assert.equal(parsed.seedsHex.root.length, 64);
57
+ });
58
+ it("accepts the runOnboarding shape (warning + created_at)", () => {
59
+ const id = createBrowserIdentity("bob", "Bob");
60
+ const text = JSON.stringify({
61
+ aithos_recovery_version: "0.1.0-plaintext",
62
+ warning: "this is plaintext, store offline",
63
+ handle: id.handle,
64
+ display_name: id.displayName,
65
+ did: id.did,
66
+ created_at: new Date().toISOString(),
67
+ seeds_hex: {
68
+ root: bytesToHex(id.root.seed),
69
+ public: bytesToHex(id.public.seed),
70
+ circle: bytesToHex(id.circle.seed),
71
+ self: bytesToHex(id.self.seed),
72
+ },
73
+ public_keys_multibase: {},
74
+ });
75
+ const parsed = parseRecoveryFile(text);
76
+ assert.equal(parsed.did, id.did);
77
+ });
78
+ it("rejects unsupported versions", () => {
79
+ assert.throws(() => parseRecoveryFile(JSON.stringify({ aithos_recovery_version: "9.9.9" })), (e) => e instanceof AithosSDKError && e.code === "auth_invalid_recovery_file");
80
+ });
81
+ it("rejects malformed seeds", () => {
82
+ const id = createBrowserIdentity("alice", "Alice");
83
+ const { text } = serializeRecoveryFile(id);
84
+ const obj = JSON.parse(text);
85
+ obj.seeds_hex.root = "not hex";
86
+ assert.throws(() => parseRecoveryFile(JSON.stringify(obj)), AithosSDKError);
87
+ });
88
+ });
89
+ /* -------------------------------------------------------------------------- */
90
+ /* parseDelegateBundle */
91
+ /* -------------------------------------------------------------------------- */
92
+ describe("delegate bundle: parse", () => {
93
+ it("parses a well-formed bundle (legacy subject_did field)", () => {
94
+ const text = delegateBundleText({
95
+ mandateId: "mandate:01H8XYZ",
96
+ subjectDid: "did:aithos:zCarol",
97
+ granteeId: "urn:aithos:agent:bob1",
98
+ scopes: ["ethos.read.public", "ethos.write.public"],
99
+ });
100
+ const parsed = parseDelegateBundle(text);
101
+ assert.equal(parsed.mandateId, "mandate:01H8XYZ");
102
+ assert.equal(parsed.subjectDid, "did:aithos:zCarol");
103
+ assert.equal(parsed.granteeId, "urn:aithos:agent:bob1");
104
+ assert.equal(parsed.delegateSeedHex.length, 64);
105
+ });
106
+ it("parses a bundle minted by mintDelegateBundle (issuer field)", () => {
107
+ // Real wire shape emitted by `mintDelegateBundle` in protocol-client:
108
+ // SignedMandate carries the subject's DID under `issuer`, NOT
109
+ // `subject_did`. Regression test for the import flow that broke
110
+ // every freshly-minted mandate before this fix.
111
+ const text = JSON.stringify({
112
+ aithos_delegate_version: "0.1.0",
113
+ mandate: {
114
+ "aithos-mandate": "0.1",
115
+ id: "mandate:01H8ISSUER",
116
+ issuer: "did:aithos:zCarol",
117
+ issued_by_key: "did:aithos:zCarol#root",
118
+ grantee: {
119
+ id: "urn:aithos:agent:bob1",
120
+ pubkey: "z6MkqGenericPubKey",
121
+ },
122
+ actor_sphere: "self",
123
+ scopes: ["ethos.read.public", "ethos.write.public"],
124
+ not_before: "2026-05-10T00:00:00Z",
125
+ not_after: "2026-05-11T00:00:00Z",
126
+ issued_at: "2026-05-10T00:00:00Z",
127
+ nonce: "abc",
128
+ signature: { alg: "ed25519", key: "...", value: "..." },
129
+ },
130
+ delegate_seed_hex: "11".repeat(32),
131
+ });
132
+ const parsed = parseDelegateBundle(text);
133
+ assert.equal(parsed.subjectDid, "did:aithos:zCarol");
134
+ assert.equal(parsed.mandateId, "mandate:01H8ISSUER");
135
+ assert.equal(parsed.granteeId, "urn:aithos:agent:bob1");
136
+ });
137
+ it("readDelegateBundleText accepts string passthrough", async () => {
138
+ const text = delegateBundleText({
139
+ mandateId: "m",
140
+ subjectDid: "did:aithos:z",
141
+ granteeId: "urn:x",
142
+ });
143
+ assert.equal(await readDelegateBundleText(text), text);
144
+ });
145
+ it("rejects missing mandate", () => {
146
+ assert.throws(() => parseDelegateBundle(JSON.stringify({
147
+ aithos_delegate_version: "0.1.0",
148
+ delegate_seed_hex: "11".repeat(32),
149
+ })), AithosSDKError);
150
+ });
151
+ it("rejects malformed delegate seed", () => {
152
+ assert.throws(() => parseDelegateBundle(JSON.stringify({
153
+ aithos_delegate_version: "0.1.0",
154
+ mandate: {
155
+ id: "m",
156
+ subject_did: "did:aithos:z",
157
+ grantee: { id: "u", pubkey: "z6MkXYZ" },
158
+ },
159
+ delegate_seed_hex: "not hex",
160
+ })), AithosSDKError);
161
+ });
162
+ });
163
+ /* -------------------------------------------------------------------------- */
164
+ /* AithosAuth — recovery sign-in */
165
+ /* -------------------------------------------------------------------------- */
166
+ describe("AithosAuth.signInWithRecovery", () => {
167
+ it("hydrates owner signers + persists to keyStore (no JWT)", async () => {
168
+ const keyStore = memoryKeyStore();
169
+ const sessionStore = noopStore();
170
+ const auth = makeAuth({ sessionStore, keyStore });
171
+ assert.equal(auth.canSignAsOwner(), false);
172
+ assert.equal(auth.getOwnerInfo(), null);
173
+ const { text } = recoveryTextFor("alice", "Alice");
174
+ const info = await auth.signInWithRecovery({ file: text });
175
+ assert.equal(info.handle, "alice");
176
+ assert.equal(auth.canSignAsOwner(), true);
177
+ assert.equal(auth.getOwnerInfo()?.did, info.did);
178
+ assert.equal(auth.getCurrentSession(), null, "no JWT for recovery flow");
179
+ const persisted = await keyStore.loadOwner();
180
+ assert.equal(persisted?.did, info.did);
181
+ });
182
+ it("rejects loading a different owner without signOut first", async () => {
183
+ const auth = makeAuth();
184
+ const a = recoveryTextFor("alice", "Alice");
185
+ await auth.signInWithRecovery({ file: a.text });
186
+ const b = recoveryTextFor("bob", "Bob");
187
+ await assert.rejects(() => auth.signInWithRecovery({ file: b.text }), (e) => e instanceof AithosSDKError && e.code === "auth_owner_already_loaded");
188
+ });
189
+ it("clears any stale JWT to keep stores in sync", async () => {
190
+ const keyStore = memoryKeyStore();
191
+ let stored = {
192
+ session: "stale-jwt",
193
+ exp: Math.floor(Date.now() / 1000) + 3600,
194
+ did: "did:aithos:zStale",
195
+ handle: "stale",
196
+ blob_b64: "",
197
+ blob_nonce_b64: "",
198
+ blob_version: 0,
199
+ enc_key_b64: "",
200
+ is_first_login: false,
201
+ };
202
+ const sessionStore = {
203
+ get: () => stored,
204
+ set: (s) => {
205
+ stored = s;
206
+ },
207
+ clear: () => {
208
+ stored = null;
209
+ },
210
+ };
211
+ const auth = makeAuth({ sessionStore, keyStore });
212
+ const { text } = recoveryTextFor("alice", "Alice");
213
+ await auth.signInWithRecovery({ file: text });
214
+ assert.equal(stored, null, "stale JWT must be wiped");
215
+ });
216
+ });
217
+ /* -------------------------------------------------------------------------- */
218
+ /* AithosAuth — importMandate */
219
+ /* -------------------------------------------------------------------------- */
220
+ describe("AithosAuth.importMandate", () => {
221
+ it("registers a delegate, lists it, removes it", async () => {
222
+ const keyStore = memoryKeyStore();
223
+ const auth = makeAuth({ keyStore });
224
+ const text = delegateBundleText({
225
+ mandateId: "mandate:A",
226
+ subjectDid: "did:aithos:zCarol",
227
+ granteeId: "urn:aithos:agent:bob1",
228
+ scopes: ["ethos.read.circle"],
229
+ });
230
+ const info = await auth.importMandate({ bundle: text });
231
+ assert.equal(info.mandateId, "mandate:A");
232
+ assert.equal(info.subjectDid, "did:aithos:zCarol");
233
+ assert.deepEqual(info.scopes, ["ethos.read.circle"]);
234
+ const list = auth.getDelegates();
235
+ assert.equal(list.length, 1);
236
+ assert.equal(list[0]?.mandateId, "mandate:A");
237
+ assert.equal(auth.canSignAsDelegateFor("did:aithos:zCarol"), true);
238
+ assert.equal(auth.canSignAsDelegateFor("did:aithos:zNobody"), false);
239
+ await auth.removeMandate("mandate:A");
240
+ assert.equal(auth.getDelegates().length, 0);
241
+ assert.equal((await keyStore.listDelegates()).length, 0);
242
+ });
243
+ it("works without an owner loaded (delegate-only session)", async () => {
244
+ const auth = makeAuth();
245
+ assert.equal(auth.canSignAsOwner(), false);
246
+ const text = delegateBundleText({
247
+ mandateId: "mandate:Solo",
248
+ subjectDid: "did:aithos:zCarol",
249
+ granteeId: "urn:aithos:agent:solo1",
250
+ });
251
+ await auth.importMandate({ bundle: text });
252
+ assert.equal(auth.canSignAsOwner(), false);
253
+ assert.equal(auth.canSignAsDelegateFor("did:aithos:zCarol"), true);
254
+ });
255
+ it("re-importing the same mandate replaces the prior actor", async () => {
256
+ const auth = makeAuth();
257
+ const text = delegateBundleText({
258
+ mandateId: "mandate:R",
259
+ subjectDid: "did:aithos:zCarol",
260
+ granteeId: "urn:aithos:agent:r",
261
+ });
262
+ await auth.importMandate({ bundle: text });
263
+ await auth.importMandate({ bundle: text });
264
+ assert.equal(auth.getDelegates().length, 1);
265
+ });
266
+ });
267
+ /* -------------------------------------------------------------------------- */
268
+ /* AithosAuth — resume() */
269
+ /* -------------------------------------------------------------------------- */
270
+ describe("AithosAuth.resume", () => {
271
+ it("rehydrates owner + delegates from keyStore on a fresh instance", async () => {
272
+ const keyStore = memoryKeyStore();
273
+ const sessionStore = noopStore();
274
+ // Seed the stores via a first auth instance.
275
+ {
276
+ const auth1 = makeAuth({ keyStore, sessionStore });
277
+ const { text: rt } = recoveryTextFor("alice", "Alice");
278
+ await auth1.signInWithRecovery({ file: rt });
279
+ const dt = delegateBundleText({
280
+ mandateId: "mandate:M1",
281
+ subjectDid: "did:aithos:zCarol",
282
+ granteeId: "urn:x",
283
+ });
284
+ await auth1.importMandate({ bundle: dt });
285
+ }
286
+ // Fresh instance, same stores → resume() must reload everything.
287
+ const auth2 = makeAuth({ keyStore, sessionStore });
288
+ assert.equal(auth2.canSignAsOwner(), false, "before resume, in-memory only");
289
+ await auth2.resume();
290
+ assert.equal(auth2.canSignAsOwner(), true);
291
+ assert.equal(auth2.getOwnerInfo()?.handle, "alice");
292
+ assert.equal(auth2.getDelegates().length, 1);
293
+ assert.equal(auth2.canSignAsDelegateFor("did:aithos:zCarol"), true);
294
+ });
295
+ it("strict mode: JWT in sessionStore but no owner in keyStore → wipes JWT", async () => {
296
+ let jwt = {
297
+ session: "j",
298
+ exp: Math.floor(Date.now() / 1000) + 3600,
299
+ did: "did:aithos:zGhost",
300
+ handle: "ghost",
301
+ blob_b64: "",
302
+ blob_nonce_b64: "",
303
+ blob_version: 0,
304
+ enc_key_b64: "",
305
+ is_first_login: false,
306
+ };
307
+ const sessionStore = {
308
+ get: () => jwt,
309
+ set: (s) => {
310
+ jwt = s;
311
+ },
312
+ clear: () => {
313
+ jwt = null;
314
+ },
315
+ };
316
+ const keyStore = memoryKeyStore();
317
+ const auth = makeAuth({ keyStore, sessionStore });
318
+ await auth.resume();
319
+ assert.equal(jwt, null, "out-of-sync JWT must be cleared");
320
+ assert.equal(auth.canSignAsOwner(), false);
321
+ });
322
+ it("strict mode: JWT and owner disagree on DID → wipes JWT only", async () => {
323
+ const keyStore = memoryKeyStore();
324
+ const { text } = recoveryTextFor("alice", "Alice");
325
+ // Seed the keystore with alice via one instance.
326
+ {
327
+ const auth1 = makeAuth({ keyStore });
328
+ await auth1.signInWithRecovery({ file: text });
329
+ }
330
+ // Now plant a JWT for a DIFFERENT DID in the session store.
331
+ let jwt = {
332
+ session: "j",
333
+ exp: Math.floor(Date.now() / 1000) + 3600,
334
+ did: "did:aithos:zSomeoneElse",
335
+ handle: "someone",
336
+ blob_b64: "",
337
+ blob_nonce_b64: "",
338
+ blob_version: 0,
339
+ enc_key_b64: "",
340
+ is_first_login: false,
341
+ };
342
+ const sessionStore = {
343
+ get: () => jwt,
344
+ set: (s) => {
345
+ jwt = s;
346
+ },
347
+ clear: () => {
348
+ jwt = null;
349
+ },
350
+ };
351
+ const auth2 = makeAuth({ keyStore, sessionStore });
352
+ await auth2.resume();
353
+ assert.equal(jwt, null, "mismatched JWT must be wiped");
354
+ // Owner is preserved (it's the source of truth in strict mode).
355
+ assert.equal(auth2.canSignAsOwner(), true);
356
+ assert.equal(auth2.getOwnerInfo()?.handle, "alice");
357
+ });
358
+ });
359
+ /* -------------------------------------------------------------------------- */
360
+ /* AithosAuth — signOut */
361
+ /* -------------------------------------------------------------------------- */
362
+ describe("AithosAuth.signOut", () => {
363
+ it("wipes both stores and in-memory state", async () => {
364
+ const keyStore = memoryKeyStore();
365
+ const auth = makeAuth({ keyStore });
366
+ const { text } = recoveryTextFor("alice", "Alice");
367
+ await auth.signInWithRecovery({ file: text });
368
+ const dt = delegateBundleText({
369
+ mandateId: "mandate:X",
370
+ subjectDid: "did:aithos:zCarol",
371
+ granteeId: "urn:x",
372
+ });
373
+ await auth.importMandate({ bundle: dt });
374
+ await auth.signOut();
375
+ assert.equal(auth.canSignAsOwner(), false);
376
+ assert.equal(auth.getOwnerInfo(), null);
377
+ assert.equal(auth.getDelegates().length, 0);
378
+ assert.equal(await keyStore.loadOwner(), null);
379
+ assert.equal((await keyStore.listDelegates()).length, 0);
380
+ });
381
+ });
382
+ /* -------------------------------------------------------------------------- */
383
+ /* Helpers */
384
+ /* -------------------------------------------------------------------------- */
385
+ function bytesToHex(b) {
386
+ let out = "";
387
+ for (let i = 0; i < b.length; i++)
388
+ out += b[i].toString(16).padStart(2, "0");
389
+ return out;
390
+ }
391
+ //# sourceMappingURL=auth-j3.test.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=compute-delegate-path.test.d.ts.map