@aithos/sdk 0.1.0-alpha.40 → 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 (40) hide show
  1. package/dist/src/apps.d.ts +155 -0
  2. package/dist/src/apps.js +288 -0
  3. package/dist/src/compute.d.ts +30 -0
  4. package/dist/src/index.d.ts +3 -1
  5. package/dist/src/index.js +7 -1
  6. package/dist/src/sdk.d.ts +7 -0
  7. package/dist/src/sdk.js +13 -0
  8. package/dist/test/auth-j3.test.d.ts +2 -0
  9. package/dist/test/auth-j3.test.js +391 -0
  10. package/dist/test/auth.test.d.ts +2 -0
  11. package/dist/test/auth.test.js +175 -0
  12. package/dist/test/compute-delegate-path.test.d.ts +2 -0
  13. package/dist/test/compute-delegate-path.test.js +183 -0
  14. package/dist/test/compute.test.d.ts +2 -0
  15. package/dist/test/compute.test.js +194 -0
  16. package/dist/test/endpoints.test.d.ts +2 -0
  17. package/dist/test/endpoints.test.js +62 -0
  18. package/dist/test/envelope.test.d.ts +2 -0
  19. package/dist/test/envelope.test.js +318 -0
  20. package/dist/test/ethos-first-edition.test.d.ts +2 -0
  21. package/dist/test/ethos-first-edition.test.js +248 -0
  22. package/dist/test/ethos.test.d.ts +2 -0
  23. package/dist/test/ethos.test.js +219 -0
  24. package/dist/test/key-store.test.d.ts +2 -0
  25. package/dist/test/key-store.test.js +161 -0
  26. package/dist/test/mandates-compute.test.d.ts +2 -0
  27. package/dist/test/mandates-compute.test.js +256 -0
  28. package/dist/test/mandates.test.d.ts +2 -0
  29. package/dist/test/mandates.test.js +93 -0
  30. package/dist/test/sdk.test.d.ts +2 -0
  31. package/dist/test/sdk.test.js +126 -0
  32. package/dist/test/signer.test.d.ts +2 -0
  33. package/dist/test/signer.test.js +117 -0
  34. package/dist/test/signup-bootstrap.test.d.ts +2 -0
  35. package/dist/test/signup-bootstrap.test.js +311 -0
  36. package/dist/test/wallet.test.d.ts +2 -0
  37. package/dist/test/wallet.test.js +121 -0
  38. package/dist/test/web.test.d.ts +2 -0
  39. package/dist/test/web.test.js +270 -0
  40. package/package.json +1 -1
@@ -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=auth.test.d.ts.map
@@ -0,0 +1,175 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 Mathieu Colla
3
+ // Unit tests for AithosAuth — Sign in with Google flow.
4
+ import { strict as assert } from "node:assert";
5
+ import { describe, it } from "node:test";
6
+ import { AithosAuth, AithosSDKError } from "../src/index.js";
7
+ /** Tiny window-shim that records calls instead of actually navigating. */
8
+ function makeFakeWindow(initialHref) {
9
+ let href = initialHref;
10
+ let assigned = null;
11
+ let replacedHref = null;
12
+ const win = {
13
+ location: {
14
+ get href() {
15
+ return href;
16
+ },
17
+ assign(target) {
18
+ assigned = target;
19
+ },
20
+ },
21
+ history: {
22
+ replaceState(_state, _title, url) {
23
+ replacedHref = url;
24
+ href = url;
25
+ },
26
+ },
27
+ };
28
+ return {
29
+ win: win,
30
+ get assigned() {
31
+ return assigned;
32
+ },
33
+ get replacedHref() {
34
+ return replacedHref;
35
+ },
36
+ };
37
+ }
38
+ function fakeSession(overrides = {}) {
39
+ return {
40
+ session: "jwt-token-here",
41
+ exp: Math.floor(Date.now() / 1000) + 3600,
42
+ did: "did:aithos:zABC123",
43
+ handle: "alice-x9y2",
44
+ blob_b64: "",
45
+ blob_nonce_b64: "",
46
+ blob_version: 0,
47
+ enc_key_b64: "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=",
48
+ is_first_login: true,
49
+ ...overrides,
50
+ };
51
+ }
52
+ /* -------------------------------------------------------------------------- */
53
+ /* signInWithGoogle */
54
+ /* -------------------------------------------------------------------------- */
55
+ describe("AithosAuth.signInWithGoogle", () => {
56
+ it("navigates to /auth/sso/google/start with no params by default", () => {
57
+ const w = makeFakeWindow("https://app.aithos.be/login");
58
+ const auth = new AithosAuth({
59
+ authBaseUrl: "https://auth.example.test",
60
+ window: w.win,
61
+ });
62
+ assert.throws(() => auth.signInWithGoogle(), AithosSDKError);
63
+ assert.equal(w.assigned, "https://auth.example.test/auth/sso/google/start");
64
+ });
65
+ it("forwards appState as the app_state query param", () => {
66
+ const w = makeFakeWindow("https://app.aithos.be/login");
67
+ const auth = new AithosAuth({
68
+ authBaseUrl: "https://auth.example.test",
69
+ window: w.win,
70
+ });
71
+ assert.throws(() => auth.signInWithGoogle({ appState: "/dashboard" }), AithosSDKError);
72
+ const url = new URL(w.assigned);
73
+ assert.equal(url.searchParams.get("app_state"), "/dashboard");
74
+ });
75
+ it("rejects appState longer than 1024 chars without navigating", () => {
76
+ const w = makeFakeWindow("https://app.aithos.be/login");
77
+ const auth = new AithosAuth({
78
+ authBaseUrl: "https://auth.example.test",
79
+ window: w.win,
80
+ });
81
+ const tooLong = "x".repeat(1025);
82
+ assert.throws(() => auth.signInWithGoogle({ appState: tooLong }), (e) => e instanceof AithosSDKError && e.code === "auth_app_state_too_long");
83
+ assert.equal(w.assigned, null, "must not have navigated");
84
+ });
85
+ it("trims a trailing slash from authBaseUrl", () => {
86
+ const w = makeFakeWindow("https://app.aithos.be/");
87
+ const auth = new AithosAuth({
88
+ authBaseUrl: "https://auth.example.test/",
89
+ window: w.win,
90
+ });
91
+ assert.throws(() => auth.signInWithGoogle(), AithosSDKError);
92
+ assert.equal(w.assigned, "https://auth.example.test/auth/sso/google/start");
93
+ });
94
+ });
95
+ /* -------------------------------------------------------------------------- */
96
+ /* handleCallback */
97
+ /* -------------------------------------------------------------------------- */
98
+ describe("AithosAuth.handleCallback", () => {
99
+ it("returns null when the URL has no aithos_code", async () => {
100
+ const w = makeFakeWindow("https://app.aithos.be/auth/callback");
101
+ const auth = new AithosAuth({ window: w.win, fetch: undefinedFetch() });
102
+ const session = await auth.handleCallback();
103
+ assert.equal(session, null);
104
+ });
105
+ it("exchanges the code, returns the session, and strips query params", async () => {
106
+ const w = makeFakeWindow("https://app.aithos.be/auth/callback?aithos_code=abc123XYZ_-456&app_state=/dashboard");
107
+ const session = fakeSession({ is_first_login: true });
108
+ let capturedBody;
109
+ const fakeFetch = async (input, init) => {
110
+ assert.equal(typeof input === "string" ? input : input.toString(), "https://auth.example.test/auth/sso/exchange");
111
+ capturedBody = JSON.parse(init?.body);
112
+ return new Response(JSON.stringify(session), {
113
+ status: 200,
114
+ headers: { "content-type": "application/json" },
115
+ });
116
+ };
117
+ const auth = new AithosAuth({
118
+ authBaseUrl: "https://auth.example.test",
119
+ window: w.win,
120
+ fetch: fakeFetch,
121
+ });
122
+ const out = await auth.handleCallback();
123
+ assert.deepEqual(out, session);
124
+ assert.equal(capturedBody?.aithos_code, "abc123XYZ_-456");
125
+ assert.equal(w.replacedHref, "https://app.aithos.be/auth/callback", "callback params must be stripped from the URL");
126
+ });
127
+ it("throws AithosSDKError with the backend code on aithos_error", async () => {
128
+ const w = makeFakeWindow("https://app.aithos.be/auth/callback?aithos_error=google_id_token&app_state=/dashboard");
129
+ const auth = new AithosAuth({
130
+ authBaseUrl: "https://auth.example.test",
131
+ window: w.win,
132
+ fetch: undefinedFetch(),
133
+ });
134
+ await assert.rejects(auth.handleCallback(), (e) => e instanceof AithosSDKError && e.code === "auth_google_id_token");
135
+ // URL is cleaned even on error so a refresh doesn't loop the message.
136
+ assert.equal(w.replacedHref, "https://app.aithos.be/auth/callback");
137
+ });
138
+ it("wraps a 410 'code_consumed' as AithosSDKError(code='auth_code_consumed')", async () => {
139
+ const w = makeFakeWindow("https://app.aithos.be/auth/callback?aithos_code=abc123XYZ_-456");
140
+ const fakeFetch = async () => new Response(JSON.stringify({ error: "aithos_code expired or already used", code: "code_consumed" }), { status: 410, headers: { "content-type": "application/json" } });
141
+ const auth = new AithosAuth({
142
+ authBaseUrl: "https://auth.example.test",
143
+ window: w.win,
144
+ fetch: fakeFetch,
145
+ });
146
+ await assert.rejects(auth.handleCallback(), (e) => e instanceof AithosSDKError &&
147
+ e.code === "auth_code_consumed" &&
148
+ e.status === 410);
149
+ });
150
+ it("returns null in non-browser environments (no window)", async () => {
151
+ // No `window` injected and `globalThis.window` is undefined under Node test.
152
+ const auth = new AithosAuth({ window: undefined, fetch: undefinedFetch() });
153
+ const session = await auth.handleCallback();
154
+ assert.equal(session, null);
155
+ });
156
+ });
157
+ /* -------------------------------------------------------------------------- */
158
+ /* signOut */
159
+ /* -------------------------------------------------------------------------- */
160
+ describe("AithosAuth.signOut", () => {
161
+ it("resolves immediately (sessions are stateless)", async () => {
162
+ const auth = new AithosAuth({ window: undefined, fetch: undefinedFetch() });
163
+ await auth.signOut();
164
+ });
165
+ });
166
+ /* -------------------------------------------------------------------------- */
167
+ /* Helpers */
168
+ /* -------------------------------------------------------------------------- */
169
+ /** A fetch that fails the test if invoked — for code paths that mustn't fetch. */
170
+ function undefinedFetch() {
171
+ return async () => {
172
+ throw new Error("fetch should not have been called");
173
+ };
174
+ }
175
+ //# sourceMappingURL=auth.test.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=compute-delegate-path.test.d.ts.map