@aithos/sdk 0.1.0-alpha.3 → 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-api.d.ts +41 -0
- package/dist/src/auth-api.js +82 -0
- package/dist/src/auth.d.ts +114 -75
- package/dist/src/auth.js +553 -73
- 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 +8 -4
- package/dist/src/index.js +26 -8
- 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/session-store.d.ts +58 -0
- package/dist/src/session-store.js +158 -0
- 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 +4 -3
package/dist/src/wallet.js
CHANGED
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
// compute proxy at `${compute}/v1/invoke`. Read-only DDB GetItem on
|
|
14
14
|
// the wallet table, gated by the same signed envelope verification
|
|
15
15
|
// as compute_invoke. Returns the current balance + daily spent.
|
|
16
|
-
import { buildSignedEnvelope
|
|
16
|
+
import { buildSignedEnvelope } from "@aithos/protocol-client";
|
|
17
17
|
import { computeInvokeUrl, walletTopupCheckoutUrl, } from "./endpoints.js";
|
|
18
|
+
import { ownerKeyPair } from "./internal/protocol-client-bridge.js";
|
|
18
19
|
import { AithosSDKError } from "./types.js";
|
|
19
20
|
/**
|
|
20
21
|
* `sdk.wallet` namespace.
|
|
@@ -29,12 +30,16 @@ export class WalletNamespace {
|
|
|
29
30
|
* hosted URL — the caller is responsible for redirecting the user (e.g.
|
|
30
31
|
* `window.location.href = result.checkoutUrl`).
|
|
31
32
|
*
|
|
32
|
-
* On success, the Stripe webhook will credit
|
|
33
|
+
* On success, the Stripe webhook will credit the user's wallet once the
|
|
33
34
|
* payment clears. Wallet balances are shared across all Aithos apps that
|
|
34
35
|
* use the same DID.
|
|
35
36
|
*/
|
|
36
37
|
async createTopupSession(args) {
|
|
37
|
-
const {
|
|
38
|
+
const { auth, endpoints, fetch: fetchImpl } = this.#deps;
|
|
39
|
+
const owner = auth._getOwnerSigners();
|
|
40
|
+
if (!owner || owner.destroyed) {
|
|
41
|
+
throw new AithosSDKError("sdk_no_owner", "no owner signed in; sign in first");
|
|
42
|
+
}
|
|
38
43
|
const url = walletTopupCheckoutUrl(endpoints);
|
|
39
44
|
let res;
|
|
40
45
|
try {
|
|
@@ -42,7 +47,7 @@ export class WalletNamespace {
|
|
|
42
47
|
method: "POST",
|
|
43
48
|
headers: { "content-type": "application/json" },
|
|
44
49
|
body: JSON.stringify({
|
|
45
|
-
user_did:
|
|
50
|
+
user_did: owner.did,
|
|
46
51
|
pack_id: args.packId,
|
|
47
52
|
success_url: args.successUrl,
|
|
48
53
|
cancel_url: args.cancelUrl,
|
|
@@ -88,16 +93,21 @@ export class WalletNamespace {
|
|
|
88
93
|
* envelope-verify failures from the proxy).
|
|
89
94
|
*/
|
|
90
95
|
async getBalance(args = {}) {
|
|
91
|
-
const {
|
|
96
|
+
const { auth, appDid, endpoints, fetch: fetchImpl } = this.#deps;
|
|
97
|
+
const owner = auth._getOwnerSigners();
|
|
98
|
+
if (!owner || owner.destroyed) {
|
|
99
|
+
throw new AithosSDKError("sdk_no_owner", "no owner signed in; sign in first");
|
|
100
|
+
}
|
|
101
|
+
const publicKp = ownerKeyPair(owner, "public");
|
|
92
102
|
const url = computeInvokeUrl(endpoints);
|
|
93
103
|
const params = { app_did: appDid };
|
|
94
104
|
const envelope = buildSignedEnvelope({
|
|
95
|
-
iss:
|
|
105
|
+
iss: owner.did,
|
|
96
106
|
aud: url,
|
|
97
107
|
method: "aithos.wallet_get_balance",
|
|
98
|
-
verificationMethod: `${
|
|
108
|
+
verificationMethod: `${owner.did}#public`,
|
|
99
109
|
params,
|
|
100
|
-
signer:
|
|
110
|
+
signer: publicKp,
|
|
101
111
|
});
|
|
102
112
|
let res;
|
|
103
113
|
try {
|
|
@@ -0,0 +1,360 @@
|
|
|
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", () => {
|
|
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("readDelegateBundleText accepts string passthrough", async () => {
|
|
107
|
+
const text = delegateBundleText({
|
|
108
|
+
mandateId: "m",
|
|
109
|
+
subjectDid: "did:aithos:z",
|
|
110
|
+
granteeId: "urn:x",
|
|
111
|
+
});
|
|
112
|
+
assert.equal(await readDelegateBundleText(text), text);
|
|
113
|
+
});
|
|
114
|
+
it("rejects missing mandate", () => {
|
|
115
|
+
assert.throws(() => parseDelegateBundle(JSON.stringify({
|
|
116
|
+
aithos_delegate_version: "0.1.0",
|
|
117
|
+
delegate_seed_hex: "11".repeat(32),
|
|
118
|
+
})), AithosSDKError);
|
|
119
|
+
});
|
|
120
|
+
it("rejects malformed delegate seed", () => {
|
|
121
|
+
assert.throws(() => parseDelegateBundle(JSON.stringify({
|
|
122
|
+
aithos_delegate_version: "0.1.0",
|
|
123
|
+
mandate: {
|
|
124
|
+
id: "m",
|
|
125
|
+
subject_did: "did:aithos:z",
|
|
126
|
+
grantee: { id: "u", pubkey: "z6MkXYZ" },
|
|
127
|
+
},
|
|
128
|
+
delegate_seed_hex: "not hex",
|
|
129
|
+
})), AithosSDKError);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
/* -------------------------------------------------------------------------- */
|
|
133
|
+
/* AithosAuth — recovery sign-in */
|
|
134
|
+
/* -------------------------------------------------------------------------- */
|
|
135
|
+
describe("AithosAuth.signInWithRecovery", () => {
|
|
136
|
+
it("hydrates owner signers + persists to keyStore (no JWT)", async () => {
|
|
137
|
+
const keyStore = memoryKeyStore();
|
|
138
|
+
const sessionStore = noopStore();
|
|
139
|
+
const auth = makeAuth({ sessionStore, keyStore });
|
|
140
|
+
assert.equal(auth.canSignAsOwner(), false);
|
|
141
|
+
assert.equal(auth.getOwnerInfo(), null);
|
|
142
|
+
const { text } = recoveryTextFor("alice", "Alice");
|
|
143
|
+
const info = await auth.signInWithRecovery({ file: text });
|
|
144
|
+
assert.equal(info.handle, "alice");
|
|
145
|
+
assert.equal(auth.canSignAsOwner(), true);
|
|
146
|
+
assert.equal(auth.getOwnerInfo()?.did, info.did);
|
|
147
|
+
assert.equal(auth.getCurrentSession(), null, "no JWT for recovery flow");
|
|
148
|
+
const persisted = await keyStore.loadOwner();
|
|
149
|
+
assert.equal(persisted?.did, info.did);
|
|
150
|
+
});
|
|
151
|
+
it("rejects loading a different owner without signOut first", async () => {
|
|
152
|
+
const auth = makeAuth();
|
|
153
|
+
const a = recoveryTextFor("alice", "Alice");
|
|
154
|
+
await auth.signInWithRecovery({ file: a.text });
|
|
155
|
+
const b = recoveryTextFor("bob", "Bob");
|
|
156
|
+
await assert.rejects(() => auth.signInWithRecovery({ file: b.text }), (e) => e instanceof AithosSDKError && e.code === "auth_owner_already_loaded");
|
|
157
|
+
});
|
|
158
|
+
it("clears any stale JWT to keep stores in sync", async () => {
|
|
159
|
+
const keyStore = memoryKeyStore();
|
|
160
|
+
let stored = {
|
|
161
|
+
session: "stale-jwt",
|
|
162
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
163
|
+
did: "did:aithos:zStale",
|
|
164
|
+
handle: "stale",
|
|
165
|
+
blob_b64: "",
|
|
166
|
+
blob_nonce_b64: "",
|
|
167
|
+
blob_version: 0,
|
|
168
|
+
enc_key_b64: "",
|
|
169
|
+
is_first_login: false,
|
|
170
|
+
};
|
|
171
|
+
const sessionStore = {
|
|
172
|
+
get: () => stored,
|
|
173
|
+
set: (s) => {
|
|
174
|
+
stored = s;
|
|
175
|
+
},
|
|
176
|
+
clear: () => {
|
|
177
|
+
stored = null;
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
const auth = makeAuth({ sessionStore, keyStore });
|
|
181
|
+
const { text } = recoveryTextFor("alice", "Alice");
|
|
182
|
+
await auth.signInWithRecovery({ file: text });
|
|
183
|
+
assert.equal(stored, null, "stale JWT must be wiped");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
/* -------------------------------------------------------------------------- */
|
|
187
|
+
/* AithosAuth — importMandate */
|
|
188
|
+
/* -------------------------------------------------------------------------- */
|
|
189
|
+
describe("AithosAuth.importMandate", () => {
|
|
190
|
+
it("registers a delegate, lists it, removes it", async () => {
|
|
191
|
+
const keyStore = memoryKeyStore();
|
|
192
|
+
const auth = makeAuth({ keyStore });
|
|
193
|
+
const text = delegateBundleText({
|
|
194
|
+
mandateId: "mandate:A",
|
|
195
|
+
subjectDid: "did:aithos:zCarol",
|
|
196
|
+
granteeId: "urn:aithos:agent:bob1",
|
|
197
|
+
scopes: ["ethos.read.circle"],
|
|
198
|
+
});
|
|
199
|
+
const info = await auth.importMandate({ bundle: text });
|
|
200
|
+
assert.equal(info.mandateId, "mandate:A");
|
|
201
|
+
assert.equal(info.subjectDid, "did:aithos:zCarol");
|
|
202
|
+
assert.deepEqual(info.scopes, ["ethos.read.circle"]);
|
|
203
|
+
const list = auth.getDelegates();
|
|
204
|
+
assert.equal(list.length, 1);
|
|
205
|
+
assert.equal(list[0]?.mandateId, "mandate:A");
|
|
206
|
+
assert.equal(auth.canSignAsDelegateFor("did:aithos:zCarol"), true);
|
|
207
|
+
assert.equal(auth.canSignAsDelegateFor("did:aithos:zNobody"), false);
|
|
208
|
+
await auth.removeMandate("mandate:A");
|
|
209
|
+
assert.equal(auth.getDelegates().length, 0);
|
|
210
|
+
assert.equal((await keyStore.listDelegates()).length, 0);
|
|
211
|
+
});
|
|
212
|
+
it("works without an owner loaded (delegate-only session)", async () => {
|
|
213
|
+
const auth = makeAuth();
|
|
214
|
+
assert.equal(auth.canSignAsOwner(), false);
|
|
215
|
+
const text = delegateBundleText({
|
|
216
|
+
mandateId: "mandate:Solo",
|
|
217
|
+
subjectDid: "did:aithos:zCarol",
|
|
218
|
+
granteeId: "urn:aithos:agent:solo1",
|
|
219
|
+
});
|
|
220
|
+
await auth.importMandate({ bundle: text });
|
|
221
|
+
assert.equal(auth.canSignAsOwner(), false);
|
|
222
|
+
assert.equal(auth.canSignAsDelegateFor("did:aithos:zCarol"), true);
|
|
223
|
+
});
|
|
224
|
+
it("re-importing the same mandate replaces the prior actor", async () => {
|
|
225
|
+
const auth = makeAuth();
|
|
226
|
+
const text = delegateBundleText({
|
|
227
|
+
mandateId: "mandate:R",
|
|
228
|
+
subjectDid: "did:aithos:zCarol",
|
|
229
|
+
granteeId: "urn:aithos:agent:r",
|
|
230
|
+
});
|
|
231
|
+
await auth.importMandate({ bundle: text });
|
|
232
|
+
await auth.importMandate({ bundle: text });
|
|
233
|
+
assert.equal(auth.getDelegates().length, 1);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
/* -------------------------------------------------------------------------- */
|
|
237
|
+
/* AithosAuth — resume() */
|
|
238
|
+
/* -------------------------------------------------------------------------- */
|
|
239
|
+
describe("AithosAuth.resume", () => {
|
|
240
|
+
it("rehydrates owner + delegates from keyStore on a fresh instance", async () => {
|
|
241
|
+
const keyStore = memoryKeyStore();
|
|
242
|
+
const sessionStore = noopStore();
|
|
243
|
+
// Seed the stores via a first auth instance.
|
|
244
|
+
{
|
|
245
|
+
const auth1 = makeAuth({ keyStore, sessionStore });
|
|
246
|
+
const { text: rt } = recoveryTextFor("alice", "Alice");
|
|
247
|
+
await auth1.signInWithRecovery({ file: rt });
|
|
248
|
+
const dt = delegateBundleText({
|
|
249
|
+
mandateId: "mandate:M1",
|
|
250
|
+
subjectDid: "did:aithos:zCarol",
|
|
251
|
+
granteeId: "urn:x",
|
|
252
|
+
});
|
|
253
|
+
await auth1.importMandate({ bundle: dt });
|
|
254
|
+
}
|
|
255
|
+
// Fresh instance, same stores → resume() must reload everything.
|
|
256
|
+
const auth2 = makeAuth({ keyStore, sessionStore });
|
|
257
|
+
assert.equal(auth2.canSignAsOwner(), false, "before resume, in-memory only");
|
|
258
|
+
await auth2.resume();
|
|
259
|
+
assert.equal(auth2.canSignAsOwner(), true);
|
|
260
|
+
assert.equal(auth2.getOwnerInfo()?.handle, "alice");
|
|
261
|
+
assert.equal(auth2.getDelegates().length, 1);
|
|
262
|
+
assert.equal(auth2.canSignAsDelegateFor("did:aithos:zCarol"), true);
|
|
263
|
+
});
|
|
264
|
+
it("strict mode: JWT in sessionStore but no owner in keyStore → wipes JWT", async () => {
|
|
265
|
+
let jwt = {
|
|
266
|
+
session: "j",
|
|
267
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
268
|
+
did: "did:aithos:zGhost",
|
|
269
|
+
handle: "ghost",
|
|
270
|
+
blob_b64: "",
|
|
271
|
+
blob_nonce_b64: "",
|
|
272
|
+
blob_version: 0,
|
|
273
|
+
enc_key_b64: "",
|
|
274
|
+
is_first_login: false,
|
|
275
|
+
};
|
|
276
|
+
const sessionStore = {
|
|
277
|
+
get: () => jwt,
|
|
278
|
+
set: (s) => {
|
|
279
|
+
jwt = s;
|
|
280
|
+
},
|
|
281
|
+
clear: () => {
|
|
282
|
+
jwt = null;
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
const keyStore = memoryKeyStore();
|
|
286
|
+
const auth = makeAuth({ keyStore, sessionStore });
|
|
287
|
+
await auth.resume();
|
|
288
|
+
assert.equal(jwt, null, "out-of-sync JWT must be cleared");
|
|
289
|
+
assert.equal(auth.canSignAsOwner(), false);
|
|
290
|
+
});
|
|
291
|
+
it("strict mode: JWT and owner disagree on DID → wipes JWT only", async () => {
|
|
292
|
+
const keyStore = memoryKeyStore();
|
|
293
|
+
const { text } = recoveryTextFor("alice", "Alice");
|
|
294
|
+
// Seed the keystore with alice via one instance.
|
|
295
|
+
{
|
|
296
|
+
const auth1 = makeAuth({ keyStore });
|
|
297
|
+
await auth1.signInWithRecovery({ file: text });
|
|
298
|
+
}
|
|
299
|
+
// Now plant a JWT for a DIFFERENT DID in the session store.
|
|
300
|
+
let jwt = {
|
|
301
|
+
session: "j",
|
|
302
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
303
|
+
did: "did:aithos:zSomeoneElse",
|
|
304
|
+
handle: "someone",
|
|
305
|
+
blob_b64: "",
|
|
306
|
+
blob_nonce_b64: "",
|
|
307
|
+
blob_version: 0,
|
|
308
|
+
enc_key_b64: "",
|
|
309
|
+
is_first_login: false,
|
|
310
|
+
};
|
|
311
|
+
const sessionStore = {
|
|
312
|
+
get: () => jwt,
|
|
313
|
+
set: (s) => {
|
|
314
|
+
jwt = s;
|
|
315
|
+
},
|
|
316
|
+
clear: () => {
|
|
317
|
+
jwt = null;
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
const auth2 = makeAuth({ keyStore, sessionStore });
|
|
321
|
+
await auth2.resume();
|
|
322
|
+
assert.equal(jwt, null, "mismatched JWT must be wiped");
|
|
323
|
+
// Owner is preserved (it's the source of truth in strict mode).
|
|
324
|
+
assert.equal(auth2.canSignAsOwner(), true);
|
|
325
|
+
assert.equal(auth2.getOwnerInfo()?.handle, "alice");
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
/* -------------------------------------------------------------------------- */
|
|
329
|
+
/* AithosAuth — signOut */
|
|
330
|
+
/* -------------------------------------------------------------------------- */
|
|
331
|
+
describe("AithosAuth.signOut", () => {
|
|
332
|
+
it("wipes both stores and in-memory state", async () => {
|
|
333
|
+
const keyStore = memoryKeyStore();
|
|
334
|
+
const auth = makeAuth({ keyStore });
|
|
335
|
+
const { text } = recoveryTextFor("alice", "Alice");
|
|
336
|
+
await auth.signInWithRecovery({ file: text });
|
|
337
|
+
const dt = delegateBundleText({
|
|
338
|
+
mandateId: "mandate:X",
|
|
339
|
+
subjectDid: "did:aithos:zCarol",
|
|
340
|
+
granteeId: "urn:x",
|
|
341
|
+
});
|
|
342
|
+
await auth.importMandate({ bundle: dt });
|
|
343
|
+
await auth.signOut();
|
|
344
|
+
assert.equal(auth.canSignAsOwner(), false);
|
|
345
|
+
assert.equal(auth.getOwnerInfo(), null);
|
|
346
|
+
assert.equal(auth.getDelegates().length, 0);
|
|
347
|
+
assert.equal(await keyStore.loadOwner(), null);
|
|
348
|
+
assert.equal((await keyStore.listDelegates()).length, 0);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
/* -------------------------------------------------------------------------- */
|
|
352
|
+
/* Helpers */
|
|
353
|
+
/* -------------------------------------------------------------------------- */
|
|
354
|
+
function bytesToHex(b) {
|
|
355
|
+
let out = "";
|
|
356
|
+
for (let i = 0; i < b.length; i++)
|
|
357
|
+
out += b[i].toString(16).padStart(2, "0");
|
|
358
|
+
return out;
|
|
359
|
+
}
|
|
360
|
+
//# sourceMappingURL=auth-j3.test.js.map
|
|
@@ -9,12 +9,23 @@
|
|
|
9
9
|
import { strict as assert } from "node:assert";
|
|
10
10
|
import { describe, it } from "node:test";
|
|
11
11
|
import { createBrowserIdentity } from "@aithos/protocol-client";
|
|
12
|
-
import { AithosSDK, AithosSDKError } from "../src/index.js";
|
|
12
|
+
import { AithosAuth, AithosSDK, AithosSDKError, memoryKeyStore, noopStore, } from "../src/index.js";
|
|
13
|
+
import { serializeRecoveryFile } from "../src/internal/recovery-file.js";
|
|
13
14
|
const APP_DID = "did:aithos:app:test";
|
|
14
|
-
function makeSdk(fetchImpl) {
|
|
15
|
-
const
|
|
15
|
+
async function makeSdk(fetchImpl) {
|
|
16
|
+
const id = createBrowserIdentity("test-handle", "Test User");
|
|
17
|
+
const auth = new AithosAuth({
|
|
18
|
+
authBaseUrl: "https://auth.test",
|
|
19
|
+
fetch: (() => {
|
|
20
|
+
throw new Error("auth not used in compute tests");
|
|
21
|
+
}),
|
|
22
|
+
sessionStore: noopStore(),
|
|
23
|
+
keyStore: memoryKeyStore(),
|
|
24
|
+
});
|
|
25
|
+
const { text } = serializeRecoveryFile(id);
|
|
26
|
+
await auth.signInWithRecovery({ file: text });
|
|
16
27
|
return new AithosSDK({
|
|
17
|
-
|
|
28
|
+
auth,
|
|
18
29
|
appDid: APP_DID,
|
|
19
30
|
endpoints: { compute: "https://compute.example.test" },
|
|
20
31
|
fetch: fetchImpl,
|
|
@@ -40,7 +51,7 @@ describe("compute.invokeBedrock — happy path", () => {
|
|
|
40
51
|
headers: { "content-type": "application/json" },
|
|
41
52
|
});
|
|
42
53
|
};
|
|
43
|
-
const sdk = makeSdk(fakeFetch);
|
|
54
|
+
const sdk = await makeSdk(fakeFetch);
|
|
44
55
|
const out = await sdk.compute.invokeBedrock({
|
|
45
56
|
mandateId: "mandate:abc",
|
|
46
57
|
model: "claude-sonnet-4-6",
|
|
@@ -71,7 +82,7 @@ describe("compute.invokeBedrock — happy path", () => {
|
|
|
71
82
|
headers: { "content-type": "application/json" },
|
|
72
83
|
});
|
|
73
84
|
};
|
|
74
|
-
const sdk = makeSdk(fakeFetch);
|
|
85
|
+
const sdk = await makeSdk(fakeFetch);
|
|
75
86
|
await sdk.compute.invokeBedrock({
|
|
76
87
|
mandateId: "mandate:abc",
|
|
77
88
|
model: "claude-sonnet-4-6",
|
|
@@ -92,7 +103,7 @@ describe("compute.invokeBedrock — errors", () => {
|
|
|
92
103
|
const fakeFetch = async () => {
|
|
93
104
|
throw new Error("fetch failed: ECONNREFUSED");
|
|
94
105
|
};
|
|
95
|
-
const sdk = makeSdk(fakeFetch);
|
|
106
|
+
const sdk = await makeSdk(fakeFetch);
|
|
96
107
|
await assert.rejects(sdk.compute.invokeBedrock({
|
|
97
108
|
mandateId: "mandate:abc",
|
|
98
109
|
model: "claude-sonnet-4-6",
|
|
@@ -106,7 +117,7 @@ describe("compute.invokeBedrock — errors", () => {
|
|
|
106
117
|
});
|
|
107
118
|
it("wraps an HTTP non-2xx as AithosSDKError(code='http')", async () => {
|
|
108
119
|
const fakeFetch = async () => new Response("nope", { status: 503, statusText: "Service Unavailable" });
|
|
109
|
-
const sdk = makeSdk(fakeFetch);
|
|
120
|
+
const sdk = await makeSdk(fakeFetch);
|
|
110
121
|
await assert.rejects(sdk.compute.invokeBedrock({
|
|
111
122
|
mandateId: "mandate:abc",
|
|
112
123
|
model: "claude-sonnet-4-6",
|
|
@@ -126,7 +137,7 @@ describe("compute.invokeBedrock — errors", () => {
|
|
|
126
137
|
data: { balance: 0, required: 1 },
|
|
127
138
|
},
|
|
128
139
|
}), { status: 200, headers: { "content-type": "application/json" } });
|
|
129
|
-
const sdk = makeSdk(fakeFetch);
|
|
140
|
+
const sdk = await makeSdk(fakeFetch);
|
|
130
141
|
await assert.rejects(sdk.compute.invokeBedrock({
|
|
131
142
|
mandateId: "mandate:abc",
|
|
132
143
|
model: "claude-sonnet-4-6",
|
|
@@ -143,7 +154,7 @@ describe("compute.invokeBedrock — errors", () => {
|
|
|
143
154
|
status: 200,
|
|
144
155
|
headers: { "content-type": "application/json" },
|
|
145
156
|
});
|
|
146
|
-
const sdk = makeSdk(fakeFetch);
|
|
157
|
+
const sdk = await makeSdk(fakeFetch);
|
|
147
158
|
await assert.rejects(sdk.compute.invokeBedrock({
|
|
148
159
|
mandateId: "mandate:abc",
|
|
149
160
|
model: "claude-sonnet-4-6",
|
|
@@ -165,7 +176,7 @@ describe("compute.invokeBedrock — abort", () => {
|
|
|
165
176
|
headers: { "content-type": "application/json" },
|
|
166
177
|
});
|
|
167
178
|
};
|
|
168
|
-
const sdk = makeSdk(fakeFetch);
|
|
179
|
+
const sdk = await makeSdk(fakeFetch);
|
|
169
180
|
const ac = new AbortController();
|
|
170
181
|
await sdk.compute.invokeBedrock({
|
|
171
182
|
mandateId: "mandate:abc",
|