@structured-id/ui-core 2.0.0 → 3.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 (152) hide show
  1. package/README.md +22 -10
  2. package/package.json +25 -43
  3. package/src/components/SidStatusBadge.vue +1 -5
  4. package/src/composables/useAccountTransport.test.ts +2 -1
  5. package/src/composables/useGrpc.test.ts +17 -14
  6. package/src/composables/useGrpc.ts +4 -2
  7. package/src/index.ts +2 -120
  8. package/src/profile/components/ConsentManager.vue +5 -1
  9. package/src/profile/components/ProfileEditor.vue +1 -1
  10. package/src/profile/components/SecuritySettings.vue +5 -1
  11. package/src/profile/components/SessionList.vue +2 -1
  12. package/src/profile/components/SidRegistrationForm.test.ts +173 -1
  13. package/src/profile/components/SidRegistrationForm.vue +88 -32
  14. package/src/profile/components/index.ts +1 -4
  15. package/src/profile/composables/index.ts +1 -0
  16. package/src/profile/composables/operationPlan.test.ts +155 -0
  17. package/src/profile/composables/operationPlan.ts +218 -0
  18. package/src/profile/composables/useAccountApi.ts +4 -4
  19. package/src/profile/composables/useAuth.test.ts +312 -31
  20. package/src/profile/composables/useAuth.ts +176 -30
  21. package/src/profile/composables/useAuthApi.opaque.test.ts +13 -6
  22. package/src/profile/composables/useAuthApi.ts +40 -18
  23. package/src/profile/composables/useCurrentPasswordRequirement.test.ts +196 -0
  24. package/src/profile/composables/useCurrentPasswordRequirement.ts +95 -0
  25. package/src/profile/composables/useEnrollmentApi.test.ts +7 -3
  26. package/src/profile/composables/useEnrollmentApi.ts +3 -5
  27. package/src/profile/composables/useIdentityApi.ts +4 -4
  28. package/src/profile/composables/useSecurityLevel.test.ts +1 -1
  29. package/src/profile/composables/useSecurityLevel.ts +1 -1
  30. package/src/quasar/SidPasswordCeremony.test.ts +253 -0
  31. package/src/quasar/SidPasswordCeremony.vue +456 -0
  32. package/src/quasar/index.ts +2 -0
  33. package/src/utils/rpcRefusal.test.ts +4 -4
  34. package/src/utils/rpcRefusal.ts +8 -4
  35. package/dist/browser-vp/index.cjs +0 -349
  36. package/dist/browser-vp/index.d.cts +0 -276
  37. package/dist/browser-vp/index.d.ts +0 -276
  38. package/dist/browser-vp/index.js +0 -349
  39. package/dist/chunk-C3Q766CY.cjs +0 -125
  40. package/dist/chunk-J5YXRXB5.js +0 -125
  41. package/dist/index.cjs +0 -23581
  42. package/dist/index.d.cts +0 -11539
  43. package/dist/index.d.ts +0 -11539
  44. package/dist/index.js +0 -23581
  45. package/dist/storage-4CB2JTLW.cjs +0 -22
  46. package/dist/storage-THRWREZP.js +0 -22
  47. package/src/browser-vp/crypto.ts +0 -253
  48. package/src/browser-vp/index.test.ts +0 -10
  49. package/src/browser-vp/index.ts +0 -116
  50. package/src/browser-vp/prf.ts +0 -179
  51. package/src/browser-vp/storage.test.ts +0 -9
  52. package/src/browser-vp/storage.ts +0 -163
  53. package/src/browser-vp/types.ts +0 -95
  54. package/src/browser-vp/vp.test.ts +0 -43
  55. package/src/browser-vp/vp.ts +0 -111
  56. package/src/generated/google/api/http.ts +0 -687
  57. package/src/generated/google/api/httpbody.ts +0 -159
  58. package/src/generated/google/protobuf/any.ts +0 -326
  59. package/src/generated/google/protobuf/descriptor.ts +0 -4865
  60. package/src/generated/google/protobuf/duration.ts +0 -230
  61. package/src/generated/google/protobuf/empty.ts +0 -94
  62. package/src/generated/google/protobuf/struct.ts +0 -484
  63. package/src/generated/google/protobuf/timestamp.ts +0 -287
  64. package/src/generated/google/rpc/error_details.ts +0 -1386
  65. package/src/generated/google/rpc/status.ts +0 -127
  66. package/src/generated/sid/v1/account/account.client.ts +0 -527
  67. package/src/generated/sid/v1/account/account.ts +0 -3261
  68. package/src/generated/sid/v1/admin/admin.client.ts +0 -139
  69. package/src/generated/sid/v1/admin/admin.ts +0 -781
  70. package/src/generated/sid/v1/admin/admin_roles.client.ts +0 -181
  71. package/src/generated/sid/v1/admin/admin_roles.ts +0 -1606
  72. package/src/generated/sid/v1/admin/analytics.client.ts +0 -106
  73. package/src/generated/sid/v1/admin/analytics.ts +0 -1017
  74. package/src/generated/sid/v1/admin/branding.client.ts +0 -166
  75. package/src/generated/sid/v1/admin/branding.ts +0 -1906
  76. package/src/generated/sid/v1/admin/enrollment.client.ts +0 -177
  77. package/src/generated/sid/v1/admin/enrollment.ts +0 -1766
  78. package/src/generated/sid/v1/admin/events.client.ts +0 -195
  79. package/src/generated/sid/v1/admin/events.ts +0 -1900
  80. package/src/generated/sid/v1/admin/flow.client.ts +0 -183
  81. package/src/generated/sid/v1/admin/flow.ts +0 -1552
  82. package/src/generated/sid/v1/admin/keys.client.ts +0 -107
  83. package/src/generated/sid/v1/admin/keys.ts +0 -931
  84. package/src/generated/sid/v1/admin/login_experience.client.ts +0 -160
  85. package/src/generated/sid/v1/admin/login_experience.ts +0 -1061
  86. package/src/generated/sid/v1/admin/migration.client.ts +0 -127
  87. package/src/generated/sid/v1/admin/migration.ts +0 -1948
  88. package/src/generated/sid/v1/admin/notifications.client.ts +0 -134
  89. package/src/generated/sid/v1/admin/notifications.ts +0 -1273
  90. package/src/generated/sid/v1/admin/operations.client.ts +0 -241
  91. package/src/generated/sid/v1/admin/operations.ts +0 -2633
  92. package/src/generated/sid/v1/admin/organizations.client.ts +0 -57
  93. package/src/generated/sid/v1/admin/organizations.ts +0 -332
  94. package/src/generated/sid/v1/admin/provisioning.client.ts +0 -232
  95. package/src/generated/sid/v1/admin/provisioning.ts +0 -1383
  96. package/src/generated/sid/v1/admin/scopes.client.ts +0 -171
  97. package/src/generated/sid/v1/admin/scopes.ts +0 -1324
  98. package/src/generated/sid/v1/admin/security.client.ts +0 -410
  99. package/src/generated/sid/v1/admin/security.ts +0 -4077
  100. package/src/generated/sid/v1/admin/sessions.client.ts +0 -114
  101. package/src/generated/sid/v1/admin/sessions.ts +0 -923
  102. package/src/generated/sid/v1/admin/settings.client.ts +0 -351
  103. package/src/generated/sid/v1/admin/settings.ts +0 -2458
  104. package/src/generated/sid/v1/admin/user_profile.client.ts +0 -57
  105. package/src/generated/sid/v1/admin/user_profile.ts +0 -730
  106. package/src/generated/sid/v1/admin/webhooks.client.ts +0 -269
  107. package/src/generated/sid/v1/admin/webhooks.ts +0 -2804
  108. package/src/generated/sid/v1/attestation/attestation.client.ts +0 -115
  109. package/src/generated/sid/v1/attestation/attestation.ts +0 -813
  110. package/src/generated/sid/v1/authn/auth.client.ts +0 -943
  111. package/src/generated/sid/v1/authn/auth.ts +0 -9719
  112. package/src/generated/sid/v1/authn/flows.client.ts +0 -413
  113. package/src/generated/sid/v1/authn/flows.ts +0 -3205
  114. package/src/generated/sid/v1/authn/issuer.client.ts +0 -489
  115. package/src/generated/sid/v1/authn/issuer.ts +0 -1600
  116. package/src/generated/sid/v1/authn/password_history.client.ts +0 -57
  117. package/src/generated/sid/v1/authn/password_history.ts +0 -578
  118. package/src/generated/sid/v1/authn/system_integration.client.ts +0 -73
  119. package/src/generated/sid/v1/authn/system_integration.ts +0 -218
  120. package/src/generated/sid/v1/authz/authz.client.ts +0 -459
  121. package/src/generated/sid/v1/authz/authz.ts +0 -6524
  122. package/src/generated/sid/v1/authz/forward_auth.client.ts +0 -97
  123. package/src/generated/sid/v1/authz/forward_auth.ts +0 -79
  124. package/src/generated/sid/v1/authz/governance.client.ts +0 -102
  125. package/src/generated/sid/v1/authz/governance.ts +0 -875
  126. package/src/generated/sid/v1/common/errors.ts +0 -710
  127. package/src/generated/sid/v1/common/pagination.ts +0 -152
  128. package/src/generated/sid/v1/common/transport.ts +0 -227
  129. package/src/generated/sid/v1/common/types.ts +0 -151
  130. package/src/generated/sid/v1/events/event_stream.client.ts +0 -67
  131. package/src/generated/sid/v1/events/event_stream.ts +0 -375
  132. package/src/generated/sid/v1/federation/upstream.client.ts +0 -142
  133. package/src/generated/sid/v1/federation/upstream.ts +0 -1736
  134. package/src/generated/sid/v1/identity/identity.client.ts +0 -467
  135. package/src/generated/sid/v1/identity/identity.ts +0 -5676
  136. package/src/generated/sid/v1/ids/ids.ts +0 -395
  137. package/src/generated/sid/v1/machine/machine_user.client.ts +0 -219
  138. package/src/generated/sid/v1/machine/machine_user.ts +0 -2364
  139. package/src/generated/sid/v1/machine/pat.client.ts +0 -130
  140. package/src/generated/sid/v1/machine/pat.ts +0 -1083
  141. package/src/generated/sid/v1/projects/projects.client.ts +0 -404
  142. package/src/generated/sid/v1/projects/projects.ts +0 -4446
  143. package/src/generated/sid/v1/scim/scim.client.ts +0 -520
  144. package/src/generated/sid/v1/scim/scim.ts +0 -3182
  145. package/src/generated/sid/v1/test/test.client.ts +0 -232
  146. package/src/generated/sid/v1/test/test.ts +0 -1136
  147. package/src/transport/adaptive.test.ts +0 -728
  148. package/src/transport/adaptive.ts +0 -308
  149. package/src/transport/framing.test.ts +0 -227
  150. package/src/transport/framing.ts +0 -98
  151. package/src/transport/webtransport.test.ts +0 -728
  152. package/src/transport/webtransport.ts +0 -323
@@ -2,7 +2,9 @@ import { describe, it, expect, vi, beforeEach, type Mock } from "vitest";
2
2
  import {
3
3
  createAuth,
4
4
  type ZkppClientApi,
5
+ DEFAULT_STEP_MS,
5
6
  type PasswordOperationProgress,
7
+ WrongCurrentPasswordError,
6
8
  } from "./useAuth";
7
9
 
8
10
  // Mock the gRPC wrappers
@@ -83,6 +85,12 @@ const ANSWER = {
83
85
  evaluatedElement: new Uint8Array([5]),
84
86
  proof: { challenge: new Uint8Array([6]), response: new Uint8Array([7]) },
85
87
  };
88
+ /** The proof as the finish sends it: the evaluator's proof relayed. */
89
+ const WIRE_PROOF = {
90
+ zkppProof: PROOF.proof,
91
+ instances: PROOF.instances,
92
+ evaluationProofs: [ANSWER.proof],
93
+ };
86
94
 
87
95
  function fakeClient(): ZkppClientApi {
88
96
  return {
@@ -100,12 +108,12 @@ function fakeClient(): ZkppClientApi {
100
108
  }
101
109
 
102
110
  let client: ZkppClientApi;
103
- let loader: Mock<[], Promise<ZkppClientApi>>;
111
+ let loader: Mock<() => Promise<ZkppClientApi>>;
104
112
 
105
113
  beforeEach(() => {
106
114
  vi.clearAllMocks();
107
115
  client = fakeClient();
108
- loader = vi.fn<[], Promise<ZkppClientApi>>().mockResolvedValue(client);
116
+ loader = vi.fn<() => Promise<ZkppClientApi>>().mockResolvedValue(client);
109
117
  api.evaluate.mockResolvedValue([ANSWER]);
110
118
  });
111
119
 
@@ -199,10 +207,7 @@ describe("createAuth register", () => {
199
207
  "reg-state",
200
208
  RESPONSE,
201
209
  );
202
- expect(api.regFinish).toHaveBeenCalledWith(OPERATION, RECORD, {
203
- zkppProof: PROOF.proof,
204
- instances: PROOF.instances,
205
- });
210
+ expect(api.regFinish).toHaveBeenCalledWith(OPERATION, RECORD, WIRE_PROOF);
206
211
  expect(result).toEqual({
207
212
  profileId: "profile-uuid",
208
213
  credentialId: "cred-uuid",
@@ -227,16 +232,45 @@ describe("createAuth register", () => {
227
232
  expect(api.regFinish).toHaveBeenCalledWith(OPERATION, RECORD, undefined);
228
233
  });
229
234
 
230
- it("reports progress from 0 through the prover to 1", async () => {
235
+ // The user follows the operation by its steps: on this device, the
236
+ // comparison with previous passwords, the proof (filled by the prover's own
237
+ // progress) and the server's check, in that order and never backwards,
238
+ // ending complete.
239
+ it("reports each step, with the prover filling the proof's share", async () => {
240
+ // Step times learned by earlier tests in this browser would move the shares.
241
+ localStorage.clear();
231
242
  vi.mocked(client.prove).mockImplementation(async (_pw, _start, opts) => {
232
- opts.onProgress?.({ fraction: 0.5, label: "Proving" });
243
+ opts.onProgress?.({ fraction: 0.5, label: "Computing quotient" });
233
244
  return PROOF;
234
245
  });
235
246
  const seen: PasswordOperationProgress[] = [];
236
247
  const { register } = createAuth(loader);
237
248
  await register("bob@test.com", "pw", undefined, (p) => seen.push(p));
238
-
239
- expect(seen.map((p) => p.fraction)).toEqual([0, 0.5, 1]);
249
+ const ms = DEFAULT_STEP_MS;
250
+ const total = ms.protect + ms.compare + ms.prove + ms.verify;
251
+ const proveStart = (ms.protect + ms.compare) / total;
252
+
253
+ expect(seen.map((p) => p.step)).toEqual([
254
+ "protect",
255
+ "compare",
256
+ "prove",
257
+ "prove",
258
+ "verify",
259
+ "verify",
260
+ ]);
261
+ [
262
+ 0,
263
+ ms.protect / total,
264
+ proveStart,
265
+ proveStart + ms.prove / total / 2,
266
+ proveStart + ms.prove / total,
267
+ 1,
268
+ ].forEach((expected, i) =>
269
+ expect(seen[i].fraction).toBeCloseTo(expected, 10),
270
+ );
271
+ expect(seen[3].label, "the prover's stage names stay inside").toBe(
272
+ "Building the proof",
273
+ );
240
274
  });
241
275
 
242
276
  it("refuses to go on when the server prepared no operation", async () => {
@@ -316,21 +350,31 @@ describe("createAuth register", () => {
316
350
  });
317
351
 
318
352
  describe("createAuth changePassword", () => {
353
+ const KE2 = new Uint8Array([4, 4]);
319
354
  beforeEach(() => {
320
- api.challenge.mockResolvedValue({ history: wireContext() });
355
+ api.challenge.mockResolvedValue({
356
+ history: wireContext(),
357
+ credentialResponse: new Uint8Array(),
358
+ });
321
359
  api.execute.mockResolvedValue({ registrationResponse: RESPONSE });
322
- api.changeFinish.mockResolvedValue({});
360
+ api.changeFinish.mockResolvedValue();
323
361
  });
324
362
 
325
- it("challenges, executes the OPAQUE start, proves and finishes", async () => {
363
+ it("fixes the OPAQUE start in the challenge, executes, proves and finishes", async () => {
326
364
  const { changePassword } = createAuth(loader);
327
- await changePassword("cred-1", "N3wP@ssword!");
365
+ await changePassword({
366
+ credentialId: "cred-1",
367
+ newPassword: "N3wP@ssword!",
368
+ });
328
369
 
329
- expect(api.challenge).toHaveBeenCalledWith("cred-1");
370
+ expect(client.registrationStart).toHaveBeenCalledOnce();
371
+ expect(client.registrationStart).toHaveBeenCalledWith("N3wP@ssword!");
372
+ expect(api.challenge).toHaveBeenCalledWith("cred-1", START.request);
373
+ expect(client.loginStart).not.toHaveBeenCalled();
330
374
  expect(api.execute).toHaveBeenCalledWith(
331
375
  OPERATION,
332
376
  "cred-1",
333
- START.request,
377
+ new Uint8Array(),
334
378
  );
335
379
  expect(api.evaluate).toHaveBeenCalledWith(OPERATION, HISTORY.blinded);
336
380
  expect(client.prove).toHaveBeenCalledWith(
@@ -338,28 +382,267 @@ describe("createAuth changePassword", () => {
338
382
  START,
339
383
  expect.objectContaining({ context: clientContext }),
340
384
  );
341
- expect(api.changeFinish).toHaveBeenCalledWith(OPERATION, "cred-1", RECORD, {
342
- zkppProof: PROOF.proof,
343
- instances: PROOF.instances,
385
+ expect(api.changeFinish).toHaveBeenCalledWith(
386
+ OPERATION,
387
+ "cred-1",
388
+ RECORD,
389
+ WIRE_PROOF,
390
+ );
391
+ });
392
+
393
+ // The final report comes after the server committed the change: a progress
394
+ // observer that throws on it cannot turn the committed change into a
395
+ // failure the user would retry with a password that no longer applies.
396
+ it("resolves a committed change when the final progress observer throws", async () => {
397
+ const { changePassword } = createAuth(loader);
398
+ await expect(
399
+ changePassword(
400
+ { credentialId: "cred-1", newPassword: "N3wP@ssword!" },
401
+ (p) => {
402
+ if (p.fraction === 1) throw new Error("gauge unmounted");
403
+ },
404
+ ),
405
+ ).resolves.toBeUndefined();
406
+ expect(api.changeFinish).toHaveBeenCalledOnce();
407
+ });
408
+
409
+ // A change without the current password reports its first step before
410
+ // the new password's OPAQUE start and the challenge, and enters it once,
411
+ // so the gauge starts at once and the step's measured time covers them.
412
+ it("reports the protection step before preparing an unconfirmed change", async () => {
413
+ const order: string[] = [];
414
+ vi.mocked(client.registrationStart).mockImplementation(async () => {
415
+ order.push("registrationStart");
416
+ return START;
344
417
  });
418
+ const { changePassword } = createAuth(loader);
419
+ await changePassword(
420
+ { credentialId: "cred-1", newPassword: "N3wP@ssword!" },
421
+ (p) => {
422
+ if (p.fraction === 0 || order.at(-1) !== `step:${p.step}`)
423
+ order.push(`step:${p.step}`);
424
+ },
425
+ );
426
+ expect(order.slice(0, 2)).toEqual(["step:protect", "registrationStart"]);
427
+ expect(order.filter((e) => e === "step:protect")).toHaveLength(1);
345
428
  });
346
429
 
347
- it("stops before any OPAQUE start when the challenge prepared no operation", async () => {
348
- api.challenge.mockResolvedValue({ history: undefined });
430
+ // The current password is proved with a sign-in carried by the change's
431
+ // own steps: KE1 in the challenge beside the new password's request, KE3
432
+ // in execute, before any proof work. The sign-in runs under the change's
433
+ // context (label, operation, digest of that request), so it confirms this
434
+ // change only and a relayed ordinary sign-in cannot stand for it.
435
+ it("proves the current password inside the change, under its context", async () => {
436
+ api.challenge.mockResolvedValue({
437
+ history: wireContext(),
438
+ credentialResponse: KE2,
439
+ });
440
+ const seen: PasswordOperationProgress[] = [];
349
441
  const { changePassword } = createAuth(loader);
350
- await expect(changePassword("cred-1", "pw")).rejects.toThrow(
351
- /did not prepare/,
442
+ await changePassword(
443
+ {
444
+ credentialId: "cred-1",
445
+ newPassword: "N3wP@ssword!",
446
+ currentPassword: "0ldP@ssword!",
447
+ },
448
+ (p) => seen.push(p),
352
449
  );
353
- expect(client.registrationStart).not.toHaveBeenCalled();
450
+
451
+ const expectedContext = new Uint8Array([
452
+ ...new TextEncoder().encode("SID-PASSWORD-CHANGE-v1"),
453
+ ...OPERATION,
454
+ ...new Uint8Array(
455
+ await crypto.subtle.digest("SHA-256", new Uint8Array(START.request)),
456
+ ),
457
+ ]);
458
+ expect(client.registrationStart).toHaveBeenCalledOnce();
459
+ expect(client.loginStart).toHaveBeenCalledWith("0ldP@ssword!");
460
+ expect(api.challenge).toHaveBeenCalledWith(
461
+ "cred-1",
462
+ START.request,
463
+ new Uint8Array([1, 2, 3]),
464
+ );
465
+ expect(client.loginFinish).toHaveBeenCalledWith(
466
+ "0ldP@ssword!",
467
+ "login-state",
468
+ KE2,
469
+ expectedContext,
470
+ );
471
+ expect(api.execute).toHaveBeenCalledWith(
472
+ OPERATION,
473
+ "cred-1",
474
+ new Uint8Array([7, 8, 9]),
475
+ );
476
+ expect(seen[0]).toMatchObject({ step: "protect", fraction: 0 });
477
+ });
478
+
479
+ // The new password's OPAQUE start is protection work and is measured as
480
+ // such; confirmation covers only the current password's sign-in, so its
481
+ // time does not teach the protection estimate of registrations and resets.
482
+ it("measures the new password's start as protection, the sign-in as confirmation", async () => {
483
+ api.challenge.mockResolvedValue({
484
+ history: wireContext(),
485
+ credentialResponse: KE2,
486
+ });
487
+ const order: string[] = [];
488
+ vi.mocked(client.registrationStart).mockImplementation(async () => {
489
+ order.push("registrationStart");
490
+ return START;
491
+ });
492
+ vi.mocked(client.loginStart).mockImplementation(async () => {
493
+ order.push("loginStart");
494
+ return { request: new Uint8Array([1, 2, 3]), state: "login-state" };
495
+ });
496
+ const { changePassword } = createAuth(loader);
497
+ await changePassword(
498
+ {
499
+ credentialId: "cred-1",
500
+ newPassword: "N3wP@ssword!",
501
+ currentPassword: "0ldP@ssword!",
502
+ },
503
+ (p) => {
504
+ if (order.at(-1) !== `step:${p.step}`) order.push(`step:${p.step}`);
505
+ },
506
+ );
507
+ expect(order.slice(0, 4)).toEqual([
508
+ "step:protect",
509
+ "registrationStart",
510
+ "step:confirm",
511
+ "loginStart",
512
+ ]);
513
+ expect(order.filter((e) => e === "step:protect")).toHaveLength(1);
514
+ });
515
+
516
+ // A client that stops while the challenge is in flight keeps the operation:
517
+ // its start belongs to that client, so the proof and the record are never
518
+ // asked of a newly loaded one.
519
+ it("finishes the change on the client that started it", async () => {
520
+ const fresh = fakeClient();
521
+ loader.mockReset();
522
+ loader.mockResolvedValueOnce(client).mockResolvedValueOnce(fresh);
523
+ api.challenge.mockImplementation(async () => {
524
+ (client as { stopped: boolean }).stopped = true;
525
+ return { history: wireContext(), credentialResponse: new Uint8Array() };
526
+ });
527
+ const { changePassword } = createAuth(loader);
528
+ await changePassword({
529
+ credentialId: "cred-1",
530
+ newPassword: "N3wP@ssword!",
531
+ });
532
+ expect(loader).toHaveBeenCalledOnce();
533
+ expect(fresh.prove).not.toHaveBeenCalled();
534
+ expect(fresh.registrationFinish).not.toHaveBeenCalled();
535
+ expect(client.prove).toHaveBeenCalledOnce();
536
+ });
537
+
538
+ // A wrong current password fails in the client's own sign-in; the server
539
+ // already counted the guess when it issued KE2, so the change stops here
540
+ // with an error the form can name, and nothing more is sent.
541
+ it("stops at a wrong current password", async () => {
542
+ api.challenge.mockResolvedValue({
543
+ history: wireContext(),
544
+ credentialResponse: KE2,
545
+ });
546
+ const refused = new Error("invalid login");
547
+ refused.name = "ZkppInvalidLoginError";
548
+ vi.mocked(client.loginFinish).mockRejectedValue(refused);
549
+ const { changePassword } = createAuth(loader);
550
+ await expect(
551
+ changePassword({
552
+ credentialId: "cred-1",
553
+ newPassword: "N3wP@ssword!",
554
+ currentPassword: "wrong",
555
+ }),
556
+ ).rejects.toBeInstanceOf(WrongCurrentPasswordError);
557
+ expect(api.execute).not.toHaveBeenCalled();
558
+ expect(client.prove).not.toHaveBeenCalled();
559
+ });
560
+
561
+ // Only a sign-in the client refused is a wrong password: a malformed
562
+ // response or another fault comes through as itself, so the form does not
563
+ // ask for a password that was never judged.
564
+ it("reports a sign-in failure other than a refusal as itself", async () => {
565
+ api.challenge.mockResolvedValue({
566
+ history: wireContext(),
567
+ credentialResponse: KE2,
568
+ });
569
+ const fault = new Error("response: invalid length");
570
+ vi.mocked(client.loginFinish).mockRejectedValue(fault);
571
+ const { changePassword } = createAuth(loader);
572
+ await expect(
573
+ changePassword({
574
+ credentialId: "cred-1",
575
+ newPassword: "N3wP@ssword!",
576
+ currentPassword: "0ldP@ssword!",
577
+ }),
578
+ ).rejects.toBe(fault);
354
579
  expect(api.execute).not.toHaveBeenCalled();
355
580
  });
356
581
 
582
+ // An empty current password is a password the user gave, not an omitted
583
+ // one: the change proves it and refuses it, instead of going on unconfirmed.
584
+ it("proves an empty current password instead of skipping the proof", async () => {
585
+ api.challenge.mockResolvedValue({
586
+ history: wireContext(),
587
+ credentialResponse: KE2,
588
+ });
589
+ const refused = new Error("invalid login");
590
+ refused.name = "ZkppInvalidLoginError";
591
+ vi.mocked(client.loginFinish).mockRejectedValue(refused);
592
+ const { changePassword } = createAuth(loader);
593
+ await expect(
594
+ changePassword({
595
+ credentialId: "cred-1",
596
+ newPassword: "N3wP@ssword!",
597
+ currentPassword: "",
598
+ }),
599
+ ).rejects.toBeInstanceOf(WrongCurrentPasswordError);
600
+ expect(client.loginStart).toHaveBeenCalledWith("");
601
+ expect(api.execute).not.toHaveBeenCalled();
602
+ });
603
+
604
+ // A client that stopped answering is a fault, not a wrong password: its
605
+ // own error comes through.
606
+ it("reports a stopped client as itself", async () => {
607
+ api.challenge.mockResolvedValue({
608
+ history: wireContext(),
609
+ credentialResponse: KE2,
610
+ });
611
+ const stopped = new Error("the worker stopped");
612
+ vi.mocked(client.loginFinish).mockImplementation(async () => {
613
+ (client as { stopped: boolean }).stopped = true;
614
+ throw stopped;
615
+ });
616
+ const { changePassword } = createAuth(loader);
617
+ await expect(
618
+ changePassword({
619
+ credentialId: "cred-1",
620
+ newPassword: "N3wP@ssword!",
621
+ currentPassword: "0ldP@ssword!",
622
+ }),
623
+ ).rejects.toBe(stopped);
624
+ expect(api.execute).not.toHaveBeenCalled();
625
+ });
626
+
627
+ it("stops before executing when the challenge prepared no operation", async () => {
628
+ api.challenge.mockResolvedValue({
629
+ history: undefined,
630
+ credentialResponse: new Uint8Array(),
631
+ });
632
+ const { changePassword } = createAuth(loader);
633
+ await expect(
634
+ changePassword({ credentialId: "cred-1", newPassword: "pw" }),
635
+ ).rejects.toThrow(/did not prepare/);
636
+ expect(api.execute).not.toHaveBeenCalled();
637
+ expect(client.prove).not.toHaveBeenCalled();
638
+ });
639
+
357
640
  it("propagates a refused history (password reused) from the finish", async () => {
358
641
  api.changeFinish.mockRejectedValue(new Error("PASSWORD_REUSED"));
359
642
  const { changePassword } = createAuth(loader);
360
- await expect(changePassword("cred-1", "pw")).rejects.toThrow(
361
- "PASSWORD_REUSED",
362
- );
643
+ await expect(
644
+ changePassword({ credentialId: "cred-1", newPassword: "pw" }),
645
+ ).rejects.toThrow("PASSWORD_REUSED");
363
646
  });
364
647
  });
365
648
 
@@ -382,10 +665,7 @@ describe("createAuth resetPassword", () => {
382
665
  "reset-1",
383
666
  OPERATION,
384
667
  RECORD,
385
- {
386
- zkppProof: PROOF.proof,
387
- instances: PROOF.instances,
388
- },
668
+ WIRE_PROOF,
389
669
  );
390
670
  expect(done.sessionId).toBe("sess-1");
391
671
  });
@@ -426,6 +706,7 @@ describe("createAuth login", () => {
426
706
  "pw",
427
707
  "login-state",
428
708
  new Uint8Array([4, 5, 6]),
709
+ new Uint8Array(),
429
710
  );
430
711
  expect(api.loginFinish).toHaveBeenCalledWith(
431
712
  "alice@test.com",