@structured-id/ui-core 1.3.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
@@ -11,7 +11,7 @@
11
11
  * on Pallas.
12
12
  *
13
13
  * The client-side crypto is injected as {@link ZkppClientApi} (implemented by
14
- * `@structured-id/opaque-zkpp`), keeping the WASM dependency at the app layer.
14
+ * `@structured-id/opaque`), keeping kernel delivery at the app layer.
15
15
  * gRPC calls go through useAuthApi wrappers.
16
16
  */
17
17
  import {
@@ -46,12 +46,38 @@ export interface RegisterResult {
46
46
  credentialId: string;
47
47
  }
48
48
 
49
- /** Progress of a password operation, for a determinate gauge (0..1). */
50
- export interface PasswordOperationProgress {
51
- fraction: number;
52
- label: string;
49
+ import {
50
+ CONFIRMED_CHANGE_STEPS,
51
+ INSTALL_STEPS,
52
+ OperationPlan,
53
+ type PasswordOperationProgress,
54
+ } from "./operationPlan";
55
+
56
+ /** A password change was refused on this device: the current password is wrong. */
57
+ export class WrongCurrentPasswordError extends Error {
58
+ constructor(cause: unknown) {
59
+ super("The current password is not correct", { cause });
60
+ this.name = "WrongCurrentPasswordError";
61
+ }
53
62
  }
54
63
 
64
+ /** A password change of the signed-in user. */
65
+ export interface PasswordChange {
66
+ /** The password credential to change. */
67
+ credentialId: string;
68
+ newPassword: string;
69
+ /** The current password, proved inside the change; required while the
70
+ * server requires it, accepted whenever given. */
71
+ currentPassword?: string;
72
+ }
73
+
74
+ export {
75
+ DEFAULT_STEP_MS,
76
+ OperationPlan,
77
+ type PasswordOperationProgress,
78
+ type PasswordOperationStep,
79
+ } from "./operationPlan";
80
+
55
81
  /** The operation's history context as the ZKPP client takes it. */
56
82
  export interface ZkppHistoryContext {
57
83
  /** The operation, 16 bytes; every later step carries it. */
@@ -93,7 +119,8 @@ export interface ZkppClientApi {
93
119
  /** True once the client answers no more calls (a worker it runs on stopped). */
94
120
  readonly stopped: boolean;
95
121
  registrationStart(password: string): Promise<ZkppRegistrationStart>;
96
- /** `null` for a password the circuit cannot hold: it installs unproven. */
122
+ /** `null` when the circuit cannot hold the password; the server alone
123
+ * decides whether proof-free setup is permitted. This never means verified. */
97
124
  historyRequest(
98
125
  password: string,
99
126
  ownerDomain: Uint8Array,
@@ -116,16 +143,22 @@ export interface ZkppClientApi {
116
143
  response: Uint8Array,
117
144
  ): Promise<Uint8Array>;
118
145
  loginStart(password: string): Promise<{ request: Uint8Array; state: string }>;
146
+ /** `context` is the OPAQUE context (RFC 9807 §6): empty for an ordinary
147
+ * sign-in, the operation's own for a sign-in inside another operation. A
148
+ * sign-in that does not verify fails with an error named
149
+ * `ZkppInvalidLoginError`; any other failure keeps its own error. */
119
150
  loginFinish(
120
151
  password: string,
121
152
  state: string,
122
153
  response: Uint8Array,
154
+ context: Uint8Array,
123
155
  ): Promise<Uint8Array>;
124
156
  }
125
157
 
126
158
  /**
127
- * Loads the client on first use. Rejects where no client can run (no
128
- * threaded WebAssembly: `ZkppUnavailableError` of the package).
159
+ * Loads the client on first use: public TypeScript or an application-supplied
160
+ * kernel. Automatic selection may use TS after a pre-operation WASM load failure.
161
+ * Errors after an operation starts propagate without moving its state to another kernel.
129
162
  */
130
163
  export type ZkppClientLoader = () => Promise<ZkppClientApi>;
131
164
 
@@ -181,11 +214,57 @@ function historyEvaluations(
181
214
  });
182
215
  }
183
216
 
217
+ /**
218
+ * A sign-in the client refused: it did not verify, so the password was judged
219
+ * wrong. Recognised by the name the client contract gives it, whichever
220
+ * kernel or package copy produced it.
221
+ */
222
+ function isRefusedSignIn(e: unknown): boolean {
223
+ return e instanceof Error && e.name === "ZkppInvalidLoginError";
224
+ }
225
+
226
+ const CHANGE_CONTEXT_LABEL = new TextEncoder().encode("SID-PASSWORD-CHANGE-v1");
227
+
228
+ /**
229
+ * The OPAQUE context (RFC 9807 §6) of the current-password sign-in inside a
230
+ * change: the label, the operation and the digest of the new registration
231
+ * request the operation fixed, so the sign-in confirms this change only.
232
+ */
233
+ async function changeContext(
234
+ operationId: Uint8Array,
235
+ registrationRequest: Uint8Array,
236
+ ): Promise<Uint8Array> {
237
+ // WebCrypto takes only ArrayBuffer-backed views; the copy is one.
238
+ const digest = new Uint8Array(
239
+ await crypto.subtle.digest("SHA-256", new Uint8Array(registrationRequest)),
240
+ );
241
+ const context = new Uint8Array(
242
+ CHANGE_CONTEXT_LABEL.length + operationId.length + digest.length,
243
+ );
244
+ context.set(CHANGE_CONTEXT_LABEL);
245
+ context.set(operationId, CHANGE_CONTEXT_LABEL.length);
246
+ context.set(digest, CHANGE_CONTEXT_LABEL.length + operationId.length);
247
+ return context;
248
+ }
249
+
250
+ /**
251
+ * The proof as the finish sends it, with the evaluator's proofs relayed
252
+ * unchanged, in domain order: the server's history checker verifies them
253
+ * against the proof's own blinded input and evaluated elements.
254
+ */
184
255
  function wireProof(
185
256
  proof: ZkppProof | null,
257
+ evaluations: ZkppHistoryEvaluation[],
186
258
  ): PasswordRegistrationProof | undefined {
187
259
  return proof
188
- ? { zkppProof: proof.proof, instances: proof.instances }
260
+ ? {
261
+ zkppProof: proof.proof,
262
+ instances: proof.instances,
263
+ evaluationProofs: evaluations.map((e) => ({
264
+ challenge: e.proof.challenge,
265
+ response: e.proof.response,
266
+ })),
267
+ }
189
268
  : undefined;
190
269
  }
191
270
 
@@ -226,22 +305,32 @@ export function createAuth(loadClient: ZkppClientLoader) {
226
305
  }
227
306
 
228
307
  /**
229
- * The steps every operation shares: the OPAQUE start, `exchange` (the RPC
230
- * that answers the request and names the operation), the history
308
+ * The steps every operation shares: the OPAQUE start (`started` when the
309
+ * operation already fixed it, with the client that made it), `exchange`
310
+ * (the RPC that answers the request and names the operation), the history
231
311
  * evaluation, the proof and the record.
232
312
  */
233
313
  async function install(
234
314
  password: string,
235
315
  exchange: (request: Uint8Array) => Promise<Exchange>,
236
- onProgress?: (p: PasswordOperationProgress) => void,
316
+ plan: OperationPlan,
317
+ started?: { client: ZkppClientApi; start: ZkppRegistrationStart },
237
318
  ): Promise<Installed> {
238
- const c = await zkpp();
239
- onProgress?.({ fraction: 0, label: "Preparing" });
240
- const start = await c.registrationStart(password);
319
+ // A start stays with the client that made it: its state means nothing to
320
+ // another kernel, so a client that stopped meanwhile fails here as itself.
321
+ const c = started?.client ?? (await zkpp());
322
+ let start: ZkppRegistrationStart;
323
+ if (started) {
324
+ start = started.start;
325
+ } else {
326
+ plan.enter("protect");
327
+ start = await c.registrationStart(password);
328
+ }
241
329
  const { context: wire, registrationResponse } = await exchange(
242
330
  start.request,
243
331
  );
244
332
  const context = historyContext(wire);
333
+ plan.enter("compare");
245
334
  const request = await c.historyRequest(password, context.ownerDomain);
246
335
  const history = request
247
336
  ? {
@@ -251,21 +340,24 @@ export function createAuth(loadClient: ZkppClientLoader) {
251
340
  ),
252
341
  }
253
342
  : null;
343
+ plan.enter("prove");
254
344
  const proof = await c.prove(password, start, {
255
345
  context,
256
346
  history,
257
- onProgress,
347
+ // The prover's own time-based 0..1 fills the proof's share.
348
+ onProgress: (p) => plan.within(p.fraction),
258
349
  });
350
+ // The record (the client's stretching) and the server's check.
351
+ plan.enter("verify");
259
352
  const record = await c.registrationFinish(
260
353
  password,
261
354
  start.state,
262
355
  registrationResponse,
263
356
  );
264
- onProgress?.({ fraction: 1, label: "Done" });
265
357
  return {
266
358
  operationId: context.operationId,
267
359
  record,
268
- proof: wireProof(proof),
360
+ proof: wireProof(proof, history?.evaluations ?? []),
269
361
  };
270
362
  }
271
363
 
@@ -280,6 +372,7 @@ export function createAuth(loadClient: ZkppClientLoader) {
280
372
  claimToken?: string,
281
373
  onProgress?: (p: PasswordOperationProgress) => void,
282
374
  ): Promise<RegisterResult> {
375
+ const plan = new OperationPlan(onProgress);
283
376
  const done = await install(
284
377
  password,
285
378
  async (request) => {
@@ -293,13 +386,14 @@ export function createAuth(loadClient: ZkppClientLoader) {
293
386
  registrationResponse: started.registrationResponse,
294
387
  };
295
388
  },
296
- onProgress,
389
+ plan,
297
390
  );
298
391
  const finished = await opaqueZkppRegistrationFinish(
299
392
  done.operationId,
300
393
  done.record,
301
394
  done.proof,
302
395
  );
396
+ plan.finish();
303
397
  return {
304
398
  profileId: finished.profileId,
305
399
  credentialId: finished.credentialId,
@@ -307,30 +401,76 @@ export function createAuth(loadClient: ZkppClientLoader) {
307
401
  }
308
402
 
309
403
  /**
310
- * Replace the signed-in user's password `credentialId` with `newPassword`:
311
- * the challenge names the operation, the execute step answers the OPAQUE
312
- * request, the finish carries the record with the proof.
404
+ * Replace the signed-in user's password with `change.newPassword`: the
405
+ * challenge fixes the new password's OPAQUE request and names the
406
+ * operation, the execute step answers it, the finish carries the record
407
+ * with the proof. With `change.currentPassword` the change signs in with
408
+ * it inside the same steps (KE1 in the challenge, KE3 in execute) under
409
+ * the change's own context, which the server requires unless its policy
410
+ * says the session's authentication is recent enough
411
+ * ({@link useCurrentPasswordRequirement}); a wrong current password is
412
+ * refused before anything new is proved.
313
413
  */
314
414
  async function changePassword(
315
- credentialId: string,
316
- newPassword: string,
415
+ change: PasswordChange,
317
416
  onProgress?: (p: PasswordOperationProgress) => void,
318
417
  ): Promise<void> {
319
- const challenge = await passwordChangeChallenge(credentialId);
418
+ const { credentialId, newPassword, currentPassword } = change;
419
+ // An empty current password is given, not omitted: it is proved and refused.
420
+ const proves = currentPassword !== undefined;
421
+ const plan = new OperationPlan(
422
+ onProgress,
423
+ proves ? CONFIRMED_CHANGE_STEPS : INSTALL_STEPS,
424
+ );
425
+ const c = await zkpp();
426
+ let challenge: Awaited<ReturnType<typeof passwordChangeChallenge>>;
427
+ let credentialFinalization: Uint8Array = new Uint8Array();
428
+ // The new password's start is protection work in either case; the
429
+ // confirmation (the current password's sign-in) is bound to its request.
430
+ plan.enter("protect");
431
+ const start = await c.registrationStart(newPassword);
432
+ if (currentPassword !== undefined) {
433
+ plan.enter("confirm");
434
+ const login = await c.loginStart(currentPassword);
435
+ challenge = await passwordChangeChallenge(
436
+ credentialId,
437
+ start.request,
438
+ login.request,
439
+ );
440
+ const operation = historyContext(challenge.history).operationId;
441
+ const context = await changeContext(operation, start.request);
442
+ try {
443
+ credentialFinalization = await c.loginFinish(
444
+ currentPassword,
445
+ login.state,
446
+ challenge.credentialResponse,
447
+ context,
448
+ );
449
+ } catch (e) {
450
+ // Only the client's refusal judged the password: the server already
451
+ // counted the guess when it issued KE2, so nothing more is sent. Any
452
+ // other failure (a stopped client, a malformed response) is itself.
453
+ if (isRefusedSignIn(e)) throw new WrongCurrentPasswordError(e);
454
+ throw e;
455
+ }
456
+ } else {
457
+ challenge = await passwordChangeChallenge(credentialId, start.request);
458
+ }
320
459
  const context = historyContext(challenge.history);
321
460
  const done = await install(
322
461
  newPassword,
323
- async (request) => ({
462
+ async () => ({
324
463
  context: challenge.history,
325
464
  registrationResponse: (
326
465
  await passwordChangeExecute(
327
466
  context.operationId,
328
467
  credentialId,
329
- request,
468
+ credentialFinalization,
330
469
  )
331
470
  ).registrationResponse,
332
471
  }),
333
- onProgress,
472
+ plan,
473
+ { client: c, start },
334
474
  );
335
475
  await passwordChangeFinish(
336
476
  done.operationId,
@@ -338,6 +478,7 @@ export function createAuth(loadClient: ZkppClientLoader) {
338
478
  done.record,
339
479
  done.proof,
340
480
  );
481
+ plan.finish();
341
482
  }
342
483
 
343
484
  /**
@@ -350,6 +491,7 @@ export function createAuth(loadClient: ZkppClientLoader) {
350
491
  newPassword: string,
351
492
  onProgress?: (p: PasswordOperationProgress) => void,
352
493
  ): Promise<CompletePasswordResetResponse> {
494
+ const plan = new OperationPlan(onProgress);
353
495
  const prepared = historyContext(context);
354
496
  const done = await install(
355
497
  newPassword,
@@ -359,14 +501,16 @@ export function createAuth(loadClient: ZkppClientLoader) {
359
501
  await executePasswordReset(prepared.operationId, request)
360
502
  ).registrationResponse,
361
503
  }),
362
- onProgress,
504
+ plan,
363
505
  );
364
- return completePasswordReset(
506
+ const completed = await completePasswordReset(
365
507
  resetSessionId,
366
508
  done.operationId,
367
509
  done.record,
368
510
  done.proof,
369
511
  );
512
+ plan.finish();
513
+ return completed;
370
514
  }
371
515
 
372
516
  /**
@@ -383,10 +527,12 @@ export function createAuth(loadClient: ZkppClientLoader) {
383
527
  onStep?.(2);
384
528
  const started = await opaqueLoginStart(identifier, start.request);
385
529
  onStep?.(3);
530
+ // An ordinary sign-in runs under the empty context.
386
531
  const finalization = await c.loginFinish(
387
532
  password,
388
533
  start.state,
389
534
  started.credentialResponse,
535
+ new Uint8Array(),
390
536
  );
391
537
  const finished = await opaqueLoginFinish(
392
538
  identifier,
@@ -14,12 +14,19 @@ const mockOpaqueRegistrationFinish = vi.fn();
14
14
 
15
15
  vi.mock("../../index", () => ({
16
16
  getTransport: vi.fn(),
17
- AuthServiceClient: vi.fn().mockImplementation(() => ({
18
- opaqueLoginStart: mockOpaqueLoginStart,
19
- opaqueLoginFinish: mockOpaqueLoginFinish,
20
- opaqueRegistrationStart: mockOpaqueRegistrationStart,
21
- opaqueRegistrationFinish: mockOpaqueRegistrationFinish,
22
- })),
17
+ getAccountTransport: vi.fn(),
18
+ }));
19
+
20
+ vi.mock("@structured-id/proto/sid/v1/authn/auth.client", () => ({
21
+ // Called with `new`: a constructor mock must be a `function`, not an arrow.
22
+ AuthServiceClient: vi.fn().mockImplementation(function () {
23
+ return {
24
+ opaqueLoginStart: mockOpaqueLoginStart,
25
+ opaqueLoginFinish: mockOpaqueLoginFinish,
26
+ opaqueRegistrationStart: mockOpaqueRegistrationStart,
27
+ opaqueRegistrationFinish: mockOpaqueRegistrationFinish,
28
+ };
29
+ }),
23
30
  }));
24
31
 
25
32
  // authMeta needs to be mocked since it uses useAuth() which needs Vue context
@@ -12,10 +12,9 @@
12
12
  * ceremonies over them (client-side crypto, proofs) live in useAuth.ts,
13
13
  * behind the injected ZkppClientApi: no dependency on the client package.
14
14
  */
15
+ import { getTransport, getAccountTransport } from "../../index";
16
+ import { AuthServiceClient } from "@structured-id/proto/sid/v1/authn/auth.client";
15
17
  import {
16
- getTransport,
17
- getAccountTransport,
18
- AuthServiceClient,
19
18
  StepUpMethod,
20
19
  type OpaqueLoginStartResponse,
21
20
  type OpaqueLoginFinishResponse,
@@ -58,12 +57,13 @@ import {
58
57
  type OpaqueZkppRegistrationFinishResponse,
59
58
  type PasswordChangeChallengeResponse,
60
59
  type PasswordChangeExecuteResponse,
61
- type PasswordChangeFinishResponse,
62
- type PasswordHistoryContext,
63
- type PasswordHistoryEvaluation,
64
- type PasswordRegistrationProof,
65
- PasswordHistoryEvaluatorServiceClient,
66
- } from "../../index";
60
+ } from "@structured-id/proto/sid/v1/authn/auth";
61
+ import type {
62
+ PasswordHistoryContext,
63
+ PasswordHistoryEvaluation,
64
+ PasswordRegistrationProof,
65
+ } from "@structured-id/proto/sid/v1/authn/password_history";
66
+ import { PasswordHistoryEvaluatorServiceClient } from "@structured-id/proto/sid/v1/authn/password_history.client";
67
67
  import { authMeta } from "../auth";
68
68
 
69
69
  /** The history evaluator: in CE served beside the credential service. */
@@ -631,42 +631,66 @@ export async function evaluatePasswordHistory(
631
631
  return response.evaluations;
632
632
  }
633
633
 
634
- /** Prepare a change of the signed-in user's own password `credentialId`. */
634
+ /**
635
+ * Prepare a change of the signed-in user's own password `credentialId`:
636
+ * `registrationRequest` is the new password's OPAQUE start, which the
637
+ * operation fixes; `credentialRequest` (KE1) begins the sign-in that proves
638
+ * the current password, empty when the change does not prove it.
639
+ */
635
640
  export async function passwordChangeChallenge(
636
641
  credentialId: string,
642
+ registrationRequest: Uint8Array,
643
+ credentialRequest: Uint8Array = new Uint8Array(),
637
644
  ): Promise<PasswordChangeChallengeResponse> {
638
645
  const { response } = await account().passwordChangeChallenge(
639
- { credentialId },
646
+ { credentialId, credentialRequest, registrationRequest },
640
647
  { meta: authMeta() },
641
648
  );
642
649
  return response;
643
650
  }
644
651
 
645
- /** The OPAQUE start of the new password under the change's operation. */
652
+ /**
653
+ * The server's answer to the registration request the challenge fixed, with
654
+ * the KE3 of the current-password sign-in the challenge began (empty when
655
+ * it began none).
656
+ */
646
657
  export async function passwordChangeExecute(
647
658
  operationId: Uint8Array,
648
659
  credentialId: string,
649
- registrationRequest: Uint8Array,
660
+ credentialFinalization: Uint8Array = new Uint8Array(),
650
661
  ): Promise<PasswordChangeExecuteResponse> {
651
662
  const { response } = await account().passwordChangeExecute(
652
663
  {
653
664
  operationId: operation(operationId),
654
665
  credentialId,
655
- registrationRequest,
666
+ credentialFinalization,
656
667
  },
657
668
  { meta: authMeta() },
658
669
  );
659
670
  return response;
660
671
  }
661
672
 
673
+ /**
674
+ * How long, in milliseconds from the server's answer, the signed-in session
675
+ * may still change its password without the current password; 0 when it
676
+ * needs it now.
677
+ */
678
+ export async function passwordChangeRequirementMs(): Promise<number> {
679
+ const { response: left } = await account().getPasswordChangeRequirement(
680
+ {},
681
+ { meta: authMeta() },
682
+ );
683
+ return Number(left.seconds) * 1000 + Math.floor(left.nanos / 1_000_000);
684
+ }
685
+
662
686
  /** Finish the change with the new password's record and, when proved, the proof. */
663
687
  export async function passwordChangeFinish(
664
688
  operationId: Uint8Array,
665
689
  credentialId: string,
666
690
  registrationRecord: Uint8Array,
667
691
  proof?: PasswordRegistrationProof,
668
- ): Promise<PasswordChangeFinishResponse> {
669
- const { response } = await account().passwordChangeFinish(
692
+ ): Promise<void> {
693
+ await account().passwordChangeFinish(
670
694
  {
671
695
  operationId: operation(operationId),
672
696
  credentialId,
@@ -675,7 +699,6 @@ export async function passwordChangeFinish(
675
699
  },
676
700
  { meta: authMeta() },
677
701
  );
678
- return response;
679
702
  }
680
703
 
681
704
  // ── OPAQUE ──
@@ -778,7 +801,6 @@ export type {
778
801
  OpaqueZkppRegistrationFinishResponse,
779
802
  PasswordChangeChallengeResponse,
780
803
  PasswordChangeExecuteResponse,
781
- PasswordChangeFinishResponse,
782
804
  PasswordHistoryContext,
783
805
  PasswordHistoryEvaluation,
784
806
  PasswordRegistrationProof,