@structured-id/ui-core 1.2.0 → 2.0.0

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 (101) hide show
  1. package/README.md +9 -5
  2. package/dist/browser-vp/index.d.cts +0 -11
  3. package/dist/browser-vp/index.d.ts +0 -11
  4. package/dist/index.cjs +13721 -9514
  5. package/dist/index.d.cts +4362 -2375
  6. package/dist/index.d.ts +4362 -2375
  7. package/dist/index.js +13937 -9730
  8. package/package.json +3 -3
  9. package/src/browser-vp/crypto.ts +0 -1
  10. package/src/browser-vp/index.ts +0 -2
  11. package/src/browser-vp/prf.ts +0 -2
  12. package/src/browser-vp/storage.ts +0 -2
  13. package/src/browser-vp/types.ts +0 -2
  14. package/src/browser-vp/vp.ts +0 -2
  15. package/src/composables/index.ts +6 -1
  16. package/src/composables/useAccountTransport.test.ts +121 -0
  17. package/src/composables/useAccountTransport.ts +69 -0
  18. package/src/composables/useBffAuth.ts +44 -34
  19. package/src/generated/google/api/httpbody.ts +159 -0
  20. package/src/generated/google/protobuf/any.ts +326 -0
  21. package/src/generated/google/rpc/error_details.ts +1386 -0
  22. package/src/generated/google/rpc/status.ts +127 -0
  23. package/src/generated/sid/v1/account/account.ts +1 -1
  24. package/src/generated/sid/v1/admin/admin.client.ts +15 -12
  25. package/src/generated/sid/v1/admin/admin.ts +24 -89
  26. package/src/generated/sid/v1/admin/enrollment.client.ts +24 -12
  27. package/src/generated/sid/v1/admin/enrollment.ts +65 -3
  28. package/src/generated/sid/v1/admin/events.client.ts +8 -8
  29. package/src/generated/sid/v1/admin/flow.ts +1 -1
  30. package/src/generated/sid/v1/admin/keys.ts +1 -1
  31. package/src/generated/sid/v1/admin/notifications.client.ts +12 -6
  32. package/src/generated/sid/v1/admin/notifications.ts +11 -70
  33. package/src/generated/sid/v1/admin/operations.ts +20 -35
  34. package/src/generated/sid/v1/admin/organizations.client.ts +2 -2
  35. package/src/generated/sid/v1/admin/organizations.ts +15 -32
  36. package/src/generated/sid/v1/admin/provisioning.client.ts +232 -0
  37. package/src/generated/sid/v1/admin/provisioning.ts +1383 -0
  38. package/src/generated/sid/v1/admin/scopes.ts +1 -1
  39. package/src/generated/sid/v1/admin/security.client.ts +22 -24
  40. package/src/generated/sid/v1/admin/security.ts +13 -22
  41. package/src/generated/sid/v1/admin/settings.client.ts +29 -18
  42. package/src/generated/sid/v1/admin/settings.ts +2 -82
  43. package/src/generated/sid/v1/attestation/attestation.client.ts +6 -6
  44. package/src/generated/sid/v1/attestation/attestation.ts +8 -8
  45. package/src/generated/sid/v1/authn/auth.client.ts +101 -58
  46. package/src/generated/sid/v1/authn/auth.ts +842 -414
  47. package/src/generated/sid/v1/authn/flows.ts +0 -12
  48. package/src/generated/sid/v1/authn/issuer.client.ts +489 -0
  49. package/src/generated/sid/v1/authn/issuer.ts +1600 -0
  50. package/src/generated/sid/v1/authn/password_history.client.ts +57 -0
  51. package/src/generated/sid/v1/authn/password_history.ts +578 -0
  52. package/src/generated/sid/v1/authn/system_integration.client.ts +73 -0
  53. package/src/generated/sid/v1/authn/system_integration.ts +218 -0
  54. package/src/generated/sid/v1/authz/authz.ts +1498 -317
  55. package/src/generated/sid/v1/authz/forward_auth.client.ts +97 -0
  56. package/src/generated/sid/v1/authz/forward_auth.ts +79 -0
  57. package/src/generated/sid/v1/authz/governance.client.ts +2 -2
  58. package/src/generated/sid/v1/authz/governance.ts +61 -4
  59. package/src/generated/sid/v1/common/errors.ts +532 -6
  60. package/src/generated/sid/v1/events/event_stream.client.ts +2 -2
  61. package/src/generated/sid/v1/events/event_stream.ts +2 -1
  62. package/src/generated/sid/v1/identity/identity.ts +98 -58
  63. package/src/generated/sid/v1/ids/ids.ts +395 -0
  64. package/src/generated/sid/v1/machine/machine_user.ts +0 -36
  65. package/src/generated/sid/v1/machine/pat.client.ts +2 -19
  66. package/src/generated/sid/v1/machine/pat.ts +1 -186
  67. package/src/generated/sid/v1/projects/projects.client.ts +137 -19
  68. package/src/generated/sid/v1/projects/projects.ts +2105 -405
  69. package/src/generated/sid/v1/scim/scim.client.ts +248 -2
  70. package/src/generated/sid/v1/scim/scim.ts +153 -14
  71. package/src/generated/sid/v1/test/test.ts +1 -1
  72. package/src/i18n/index.ts +0 -11
  73. package/src/index.ts +36 -2
  74. package/src/package-exports.test.ts +18 -0
  75. package/src/profile/components/SidLoginForm.vue +5 -2
  76. package/src/profile/components/SidRegistrationForm.test.ts +58 -1
  77. package/src/profile/components/SidRegistrationForm.vue +110 -12
  78. package/src/profile/components/index.ts +4 -0
  79. package/src/profile/composables/index.ts +2 -1
  80. package/src/profile/composables/useAccountApi.ts +20 -7
  81. package/src/profile/composables/useAuth.test.ts +453 -0
  82. package/src/profile/composables/useAuth.ts +414 -0
  83. package/src/profile/composables/useAuthApi.opaque.test.ts +18 -0
  84. package/src/profile/composables/useAuthApi.ts +188 -20
  85. package/src/profile/composables/useEnrollmentApi.test.ts +33 -0
  86. package/src/profile/composables/useEnrollmentApi.ts +26 -0
  87. package/src/profile/composables/useIdentityApi.ts +17 -7
  88. package/src/profile/composables/useLoginSuccess.test.ts +97 -3
  89. package/src/profile/composables/useLoginSuccess.ts +80 -22
  90. package/src/profile/composables/useSecurityLevel.test.ts +10 -3
  91. package/src/profile/composables/useSecurityLevel.ts +12 -3
  92. package/src/quasar/SidPrincipalInput.vue +4 -7
  93. package/src/utils/normalizePrincipal.test.ts +19 -5
  94. package/src/utils/normalizePrincipal.ts +7 -9
  95. package/src/utils/rpcRefusal.test.ts +151 -0
  96. package/src/utils/rpcRefusal.ts +252 -0
  97. package/src/utils/validators.test.ts +9 -0
  98. package/src/utils/validators.ts +14 -2
  99. package/src/composables/useAuth.ts +0 -40
  100. package/src/profile/composables/useOpaqueAuth.test.ts +0 -290
  101. package/src/profile/composables/useOpaqueAuth.ts +0 -191
@@ -0,0 +1,453 @@
1
+ import { describe, it, expect, vi, beforeEach, type Mock } from "vitest";
2
+ import {
3
+ createAuth,
4
+ type ZkppClientApi,
5
+ type PasswordOperationProgress,
6
+ } from "./useAuth";
7
+
8
+ // Mock the gRPC wrappers
9
+ vi.mock("./useAuthApi", () => ({
10
+ completePasswordReset: vi.fn(),
11
+ evaluatePasswordHistory: vi.fn(),
12
+ executePasswordReset: vi.fn(),
13
+ opaqueLoginStart: vi.fn(),
14
+ opaqueLoginFinish: vi.fn(),
15
+ opaqueZkppRegistrationStart: vi.fn(),
16
+ opaqueZkppRegistrationFinish: vi.fn(),
17
+ passwordChangeChallenge: vi.fn(),
18
+ passwordChangeExecute: vi.fn(),
19
+ passwordChangeFinish: vi.fn(),
20
+ }));
21
+
22
+ import {
23
+ completePasswordReset,
24
+ evaluatePasswordHistory,
25
+ executePasswordReset,
26
+ opaqueLoginStart,
27
+ opaqueLoginFinish,
28
+ opaqueZkppRegistrationStart,
29
+ opaqueZkppRegistrationFinish,
30
+ passwordChangeChallenge,
31
+ passwordChangeExecute,
32
+ passwordChangeFinish,
33
+ type PasswordHistoryContext,
34
+ } from "./useAuthApi";
35
+
36
+ const api = {
37
+ completeReset: vi.mocked(completePasswordReset),
38
+ evaluate: vi.mocked(evaluatePasswordHistory),
39
+ executeReset: vi.mocked(executePasswordReset),
40
+ loginStart: vi.mocked(opaqueLoginStart),
41
+ loginFinish: vi.mocked(opaqueLoginFinish),
42
+ regStart: vi.mocked(opaqueZkppRegistrationStart),
43
+ regFinish: vi.mocked(opaqueZkppRegistrationFinish),
44
+ challenge: vi.mocked(passwordChangeChallenge),
45
+ execute: vi.mocked(passwordChangeExecute),
46
+ changeFinish: vi.mocked(passwordChangeFinish),
47
+ };
48
+
49
+ // ── Fixtures ──
50
+
51
+ const OPERATION = new Uint8Array(16).fill(7);
52
+ const OWNER_DOMAIN = new Uint8Array(32).fill(1);
53
+ const DOMAIN = new Uint8Array(32).fill(2);
54
+ const KEY = new Uint8Array(32).fill(3);
55
+
56
+ /** The history context as the server's preparing RPC returns it. */
57
+ function wireContext(): PasswordHistoryContext {
58
+ return {
59
+ operationId: { value: OPERATION },
60
+ ownerDomain: OWNER_DOMAIN,
61
+ domains: [{ comparisonDomain: DOMAIN, evaluatorPublicKey: KEY }],
62
+ policyVersion: 1,
63
+ };
64
+ }
65
+
66
+ /** The same context as the client takes it. */
67
+ const clientContext = {
68
+ operationId: OPERATION,
69
+ ownerDomain: OWNER_DOMAIN,
70
+ domains: [{ comparisonDomain: DOMAIN, evaluatorPublicKey: KEY }],
71
+ policyVersion: 1,
72
+ };
73
+
74
+ const START = { request: new Uint8Array([10, 11]), state: "reg-state" };
75
+ const HISTORY = { blind: new Uint8Array([1]), blinded: new Uint8Array([2]) };
76
+ const PROOF = {
77
+ proof: new Uint8Array([9, 9]),
78
+ instances: [new Uint8Array(32)],
79
+ };
80
+ const RECORD = new Uint8Array([20, 21]);
81
+ const RESPONSE = new Uint8Array([13, 14]);
82
+ const ANSWER = {
83
+ evaluatedElement: new Uint8Array([5]),
84
+ proof: { challenge: new Uint8Array([6]), response: new Uint8Array([7]) },
85
+ };
86
+
87
+ function fakeClient(): ZkppClientApi {
88
+ return {
89
+ stopped: false,
90
+ registrationStart: vi.fn().mockResolvedValue(START),
91
+ historyRequest: vi.fn().mockResolvedValue(HISTORY),
92
+ prove: vi.fn().mockResolvedValue(PROOF),
93
+ registrationFinish: vi.fn().mockResolvedValue(RECORD),
94
+ loginStart: vi.fn().mockResolvedValue({
95
+ request: new Uint8Array([1, 2, 3]),
96
+ state: "login-state",
97
+ }),
98
+ loginFinish: vi.fn().mockResolvedValue(new Uint8Array([7, 8, 9])),
99
+ };
100
+ }
101
+
102
+ let client: ZkppClientApi;
103
+ let loader: Mock<[], Promise<ZkppClientApi>>;
104
+
105
+ beforeEach(() => {
106
+ vi.clearAllMocks();
107
+ client = fakeClient();
108
+ loader = vi.fn<[], Promise<ZkppClientApi>>().mockResolvedValue(client);
109
+ api.evaluate.mockResolvedValue([ANSWER]);
110
+ });
111
+
112
+ describe("createAuth prewarm", () => {
113
+ beforeEach(() => {
114
+ api.loginStart.mockResolvedValue({
115
+ credentialResponse: new Uint8Array([4]),
116
+ serverLoginState: "server-state",
117
+ });
118
+ api.loginFinish.mockResolvedValue({
119
+ sessionId: "s",
120
+ accessToken: "t",
121
+ expiresIn: 60n,
122
+ });
123
+ });
124
+
125
+ // Loading the client when the page opens: the kernel's download and
126
+ // compilation are done before the user submits, and the operation that
127
+ // follows uses the same client instead of loading another.
128
+ it("loads the client once and the next operation reuses it", async () => {
129
+ const { prewarm, login } = createAuth(loader);
130
+ await prewarm();
131
+ await login("bob@test.com", "P@ssw0rd!");
132
+ expect(loader).toHaveBeenCalledTimes(1);
133
+ expect(client.loginStart).toHaveBeenCalledOnce();
134
+ });
135
+
136
+ // A prewarm that fails does not break the page: it resolves, and the next
137
+ // operation loads again and reports its own failure if there is one.
138
+ it("resolves after a failed load and leaves the next operation to load again", async () => {
139
+ loader.mockRejectedValueOnce(new Error("offline"));
140
+ const { prewarm, login } = createAuth(loader);
141
+ await expect(prewarm()).resolves.toBeUndefined();
142
+ await login("bob@test.com", "P@ssw0rd!");
143
+ expect(loader).toHaveBeenCalledTimes(2);
144
+ });
145
+
146
+ // A client whose worker stopped (after prewarm or an earlier operation)
147
+ // answers nothing again: the next operation starts on a newly loaded one.
148
+ it("loads a new client for the next operation after the client stopped", async () => {
149
+ const stoppedClient = { ...fakeClient(), stopped: true };
150
+ const fresh = fakeClient();
151
+ loader.mockReset();
152
+ loader.mockResolvedValueOnce(stoppedClient).mockResolvedValueOnce(fresh);
153
+ const { prewarm, login } = createAuth(loader);
154
+ await prewarm();
155
+ await login("bob@test.com", "P@ssw0rd!");
156
+ expect(loader).toHaveBeenCalledTimes(2);
157
+ expect(stoppedClient.loginStart).not.toHaveBeenCalled();
158
+ expect(fresh.loginStart).toHaveBeenCalledOnce();
159
+ });
160
+ });
161
+
162
+ describe("createAuth register", () => {
163
+ beforeEach(() => {
164
+ api.regStart.mockResolvedValue({
165
+ registrationResponse: RESPONSE,
166
+ history: wireContext(),
167
+ });
168
+ api.regFinish.mockResolvedValue({
169
+ profileId: "profile-uuid",
170
+ credentialId: "cred-uuid",
171
+ });
172
+ });
173
+
174
+ it("runs the operation: start, evaluate, prove, finish with the proof", async () => {
175
+ const { register } = createAuth(loader);
176
+ const result = await register("bob@test.com", "N3wP@ssword!", "sidclaim_x");
177
+
178
+ expect(client.registrationStart).toHaveBeenCalledWith("N3wP@ssword!");
179
+ expect(api.regStart).toHaveBeenCalledWith(
180
+ "bob@test.com",
181
+ START.request,
182
+ "sidclaim_x",
183
+ );
184
+ expect(client.historyRequest).toHaveBeenCalledWith(
185
+ "N3wP@ssword!",
186
+ OWNER_DOMAIN,
187
+ );
188
+ expect(api.evaluate).toHaveBeenCalledWith(OPERATION, HISTORY.blinded);
189
+ expect(client.prove).toHaveBeenCalledWith(
190
+ "N3wP@ssword!",
191
+ START,
192
+ expect.objectContaining({
193
+ context: clientContext,
194
+ history: { request: HISTORY, evaluations: [ANSWER] },
195
+ }),
196
+ );
197
+ expect(client.registrationFinish).toHaveBeenCalledWith(
198
+ "N3wP@ssword!",
199
+ "reg-state",
200
+ RESPONSE,
201
+ );
202
+ expect(api.regFinish).toHaveBeenCalledWith(OPERATION, RECORD, {
203
+ zkppProof: PROOF.proof,
204
+ instances: PROOF.instances,
205
+ });
206
+ expect(result).toEqual({
207
+ profileId: "profile-uuid",
208
+ credentialId: "cred-uuid",
209
+ });
210
+ });
211
+
212
+ // A password the circuit cannot hold has no history request: nothing is
213
+ // evaluated or proved, and the finish carries no proof.
214
+ it("installs a password without a proof when the client cannot prove it", async () => {
215
+ vi.mocked(client.historyRequest).mockResolvedValue(null);
216
+ vi.mocked(client.prove).mockResolvedValue(null);
217
+
218
+ const { register } = createAuth(loader);
219
+ await register("bob@test.com", "x".repeat(200));
220
+
221
+ expect(api.evaluate).not.toHaveBeenCalled();
222
+ expect(client.prove).toHaveBeenCalledWith(
223
+ "x".repeat(200),
224
+ START,
225
+ expect.objectContaining({ history: null }),
226
+ );
227
+ expect(api.regFinish).toHaveBeenCalledWith(OPERATION, RECORD, undefined);
228
+ });
229
+
230
+ it("reports progress from 0 through the prover to 1", async () => {
231
+ vi.mocked(client.prove).mockImplementation(async (_pw, _start, opts) => {
232
+ opts.onProgress?.({ fraction: 0.5, label: "Proving" });
233
+ return PROOF;
234
+ });
235
+ const seen: PasswordOperationProgress[] = [];
236
+ const { register } = createAuth(loader);
237
+ await register("bob@test.com", "pw", undefined, (p) => seen.push(p));
238
+
239
+ expect(seen.map((p) => p.fraction)).toEqual([0, 0.5, 1]);
240
+ });
241
+
242
+ it("refuses to go on when the server prepared no operation", async () => {
243
+ api.regStart.mockResolvedValue({
244
+ registrationResponse: RESPONSE,
245
+ history: undefined,
246
+ });
247
+ const { register } = createAuth(loader);
248
+ await expect(register("bob@test.com", "pw")).rejects.toThrow(
249
+ /did not prepare/,
250
+ );
251
+ expect(client.prove).not.toHaveBeenCalled();
252
+ expect(api.regFinish).not.toHaveBeenCalled();
253
+ });
254
+
255
+ // A client that fails mid-operation (its worker stopped while proving,
256
+ // a history request it cannot make) ends the operation: nothing reaches
257
+ // the finish, so no password is installed without its proof, and the
258
+ // operation is not taken up by another client.
259
+ it("ends the operation when the proof fails", async () => {
260
+ vi.mocked(client.prove).mockRejectedValue(new Error("WASM worker failed"));
261
+ const { register } = createAuth(loader);
262
+ await expect(register("bob@test.com", "pw")).rejects.toThrow(
263
+ "WASM worker failed",
264
+ );
265
+ expect(client.registrationFinish).not.toHaveBeenCalled();
266
+ expect(api.regFinish).not.toHaveBeenCalled();
267
+ expect(loader).toHaveBeenCalledTimes(1);
268
+ });
269
+
270
+ it("ends the operation when the history request fails", async () => {
271
+ vi.mocked(client.historyRequest).mockRejectedValue(
272
+ new Error("WASM worker failed"),
273
+ );
274
+ const { register } = createAuth(loader);
275
+ await expect(register("bob@test.com", "pw")).rejects.toThrow(
276
+ "WASM worker failed",
277
+ );
278
+ expect(api.evaluate).not.toHaveBeenCalled();
279
+ expect(client.prove).not.toHaveBeenCalled();
280
+ expect(api.regFinish).not.toHaveBeenCalled();
281
+ expect(loader).toHaveBeenCalledTimes(1);
282
+ });
283
+
284
+ it("refuses an evaluator answer without a proof", async () => {
285
+ api.evaluate.mockResolvedValue([
286
+ { evaluatedElement: new Uint8Array([5]), proof: undefined },
287
+ ]);
288
+ const { register } = createAuth(loader);
289
+ await expect(register("bob@test.com", "pw")).rejects.toThrow(
290
+ /without a proof/,
291
+ );
292
+ expect(client.prove).not.toHaveBeenCalled();
293
+ });
294
+
295
+ it("propagates gRPC errors from the start", async () => {
296
+ api.regStart.mockRejectedValue(new Error("ALREADY_EXISTS"));
297
+ const { register } = createAuth(loader);
298
+ await expect(register("taken@test.com", "pw")).rejects.toThrow(
299
+ "ALREADY_EXISTS",
300
+ );
301
+ });
302
+
303
+ it("loads the client once and retries a failed load", async () => {
304
+ loader
305
+ .mockRejectedValueOnce(new Error("ZKPP needs WebAssembly"))
306
+ .mockResolvedValue(client);
307
+ const { register } = createAuth(loader);
308
+
309
+ await expect(register("bob@test.com", "pw")).rejects.toThrow(
310
+ "ZKPP needs WebAssembly",
311
+ );
312
+ await register("bob@test.com", "pw");
313
+ await register("bob@test.com", "pw");
314
+ expect(loader).toHaveBeenCalledTimes(2);
315
+ });
316
+ });
317
+
318
+ describe("createAuth changePassword", () => {
319
+ beforeEach(() => {
320
+ api.challenge.mockResolvedValue({ history: wireContext() });
321
+ api.execute.mockResolvedValue({ registrationResponse: RESPONSE });
322
+ api.changeFinish.mockResolvedValue({});
323
+ });
324
+
325
+ it("challenges, executes the OPAQUE start, proves and finishes", async () => {
326
+ const { changePassword } = createAuth(loader);
327
+ await changePassword("cred-1", "N3wP@ssword!");
328
+
329
+ expect(api.challenge).toHaveBeenCalledWith("cred-1");
330
+ expect(api.execute).toHaveBeenCalledWith(
331
+ OPERATION,
332
+ "cred-1",
333
+ START.request,
334
+ );
335
+ expect(api.evaluate).toHaveBeenCalledWith(OPERATION, HISTORY.blinded);
336
+ expect(client.prove).toHaveBeenCalledWith(
337
+ "N3wP@ssword!",
338
+ START,
339
+ expect.objectContaining({ context: clientContext }),
340
+ );
341
+ expect(api.changeFinish).toHaveBeenCalledWith(OPERATION, "cred-1", RECORD, {
342
+ zkppProof: PROOF.proof,
343
+ instances: PROOF.instances,
344
+ });
345
+ });
346
+
347
+ it("stops before any OPAQUE start when the challenge prepared no operation", async () => {
348
+ api.challenge.mockResolvedValue({ history: undefined });
349
+ const { changePassword } = createAuth(loader);
350
+ await expect(changePassword("cred-1", "pw")).rejects.toThrow(
351
+ /did not prepare/,
352
+ );
353
+ expect(client.registrationStart).not.toHaveBeenCalled();
354
+ expect(api.execute).not.toHaveBeenCalled();
355
+ });
356
+
357
+ it("propagates a refused history (password reused) from the finish", async () => {
358
+ api.changeFinish.mockRejectedValue(new Error("PASSWORD_REUSED"));
359
+ const { changePassword } = createAuth(loader);
360
+ await expect(changePassword("cred-1", "pw")).rejects.toThrow(
361
+ "PASSWORD_REUSED",
362
+ );
363
+ });
364
+ });
365
+
366
+ describe("createAuth resetPassword", () => {
367
+ beforeEach(() => {
368
+ api.executeReset.mockResolvedValue({ registrationResponse: RESPONSE });
369
+ api.completeReset.mockResolvedValue({
370
+ sessionId: "sess-1",
371
+ accessToken: "jwt",
372
+ expiresIn: 3600,
373
+ });
374
+ });
375
+
376
+ it("executes the OPAQUE start under the reset operation and completes with the proof", async () => {
377
+ const { resetPassword } = createAuth(loader);
378
+ const done = await resetPassword("reset-1", wireContext(), "N3wP@ssword!");
379
+
380
+ expect(api.executeReset).toHaveBeenCalledWith(OPERATION, START.request);
381
+ expect(api.completeReset).toHaveBeenCalledWith(
382
+ "reset-1",
383
+ OPERATION,
384
+ RECORD,
385
+ {
386
+ zkppProof: PROOF.proof,
387
+ instances: PROOF.instances,
388
+ },
389
+ );
390
+ expect(done.sessionId).toBe("sess-1");
391
+ });
392
+
393
+ it("refuses a verification that prepared no operation", async () => {
394
+ const { resetPassword } = createAuth(loader);
395
+ await expect(resetPassword("reset-1", undefined, "pw")).rejects.toThrow(
396
+ /did not prepare/,
397
+ );
398
+ expect(client.registrationStart).not.toHaveBeenCalled();
399
+ });
400
+ });
401
+
402
+ describe("createAuth login", () => {
403
+ beforeEach(() => {
404
+ api.loginStart.mockResolvedValue({
405
+ credentialResponse: new Uint8Array([4, 5, 6]),
406
+ serverLoginState: "server-state",
407
+ });
408
+ api.loginFinish.mockResolvedValue({
409
+ sessionId: "sess-1",
410
+ accessToken: "jwt",
411
+ expiresIn: BigInt(3600),
412
+ } as never);
413
+ });
414
+
415
+ it("signs in with the Pallas client in two round trips", async () => {
416
+ const { login } = createAuth(loader);
417
+ const steps: number[] = [];
418
+ const result = await login("alice@test.com", "pw", (s) => steps.push(s));
419
+
420
+ expect(client.loginStart).toHaveBeenCalledWith("pw");
421
+ expect(api.loginStart).toHaveBeenCalledWith(
422
+ "alice@test.com",
423
+ new Uint8Array([1, 2, 3]),
424
+ );
425
+ expect(client.loginFinish).toHaveBeenCalledWith(
426
+ "pw",
427
+ "login-state",
428
+ new Uint8Array([4, 5, 6]),
429
+ );
430
+ expect(api.loginFinish).toHaveBeenCalledWith(
431
+ "alice@test.com",
432
+ new Uint8Array([7, 8, 9]),
433
+ "server-state",
434
+ );
435
+ expect(steps).toEqual([1, 2, 3]);
436
+ expect(result).toEqual({
437
+ sessionId: "sess-1",
438
+ accessToken: "jwt",
439
+ expiresIn: 3600,
440
+ });
441
+ });
442
+
443
+ it("propagates a wrong password (the client cannot finish)", async () => {
444
+ vi.mocked(client.loginFinish).mockRejectedValue(
445
+ new Error("login finish: InvalidLoginError"),
446
+ );
447
+ const { login } = createAuth(loader);
448
+ await expect(login("alice@test.com", "wrong")).rejects.toThrow(
449
+ "InvalidLoginError",
450
+ );
451
+ expect(api.loginFinish).not.toHaveBeenCalled();
452
+ });
453
+ });