@passflow/core 0.14.1 → 0.16.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 (55) hide show
  1. package/dist/index.cjs +1 -1
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.mjs +2751 -1196
  4. package/dist/index.mjs.map +1 -1
  5. package/dist/lib/api/auth.d.ts +17 -2
  6. package/dist/lib/api/auth.d.ts.map +1 -1
  7. package/dist/lib/api/axios-client.d.ts +47 -2
  8. package/dist/lib/api/axios-client.d.ts.map +1 -1
  9. package/dist/lib/api/index.d.ts +1 -0
  10. package/dist/lib/api/index.d.ts.map +1 -1
  11. package/dist/lib/api/model.d.ts +67 -2
  12. package/dist/lib/api/model.d.ts.map +1 -1
  13. package/dist/lib/api/upgrade.d.ts +39 -0
  14. package/dist/lib/api/upgrade.d.ts.map +1 -0
  15. package/dist/lib/index.d.ts +4 -2
  16. package/dist/lib/index.d.ts.map +1 -1
  17. package/dist/lib/interceptors/fetch-registry.d.ts +96 -0
  18. package/dist/lib/interceptors/fetch-registry.d.ts.map +1 -0
  19. package/dist/lib/passflow.d.ts +103 -1
  20. package/dist/lib/passflow.d.ts.map +1 -1
  21. package/dist/lib/services/auth-service.d.ts +51 -2
  22. package/dist/lib/services/auth-service.d.ts.map +1 -1
  23. package/dist/lib/upgrade/acr.d.ts +85 -0
  24. package/dist/lib/upgrade/acr.d.ts.map +1 -0
  25. package/dist/lib/upgrade/axios-adapter.d.ts +81 -0
  26. package/dist/lib/upgrade/axios-adapter.d.ts.map +1 -0
  27. package/dist/lib/upgrade/challenge.d.ts +158 -3
  28. package/dist/lib/upgrade/challenge.d.ts.map +1 -1
  29. package/dist/lib/upgrade/client.d.ts +29 -0
  30. package/dist/lib/upgrade/client.d.ts.map +1 -0
  31. package/dist/lib/upgrade/controller.d.ts +366 -25
  32. package/dist/lib/upgrade/controller.d.ts.map +1 -1
  33. package/dist/lib/upgrade/core.d.ts +353 -0
  34. package/dist/lib/upgrade/core.d.ts.map +1 -0
  35. package/dist/lib/upgrade/errors.d.ts +208 -0
  36. package/dist/lib/upgrade/errors.d.ts.map +1 -0
  37. package/dist/lib/upgrade/facade.d.ts +183 -0
  38. package/dist/lib/upgrade/facade.d.ts.map +1 -0
  39. package/dist/lib/upgrade/fetch-adapter.d.ts +30 -0
  40. package/dist/lib/upgrade/fetch-adapter.d.ts.map +1 -0
  41. package/dist/lib/upgrade/index.d.ts +15 -0
  42. package/dist/lib/upgrade/index.d.ts.map +1 -1
  43. package/dist/lib/upgrade/markers.d.ts +64 -0
  44. package/dist/lib/upgrade/markers.d.ts.map +1 -0
  45. package/dist/lib/upgrade/replay.d.ts +132 -0
  46. package/dist/lib/upgrade/replay.d.ts.map +1 -0
  47. package/dist/lib/upgrade/select.d.ts.map +1 -1
  48. package/dist/lib/upgrade/session.d.ts +72 -0
  49. package/dist/lib/upgrade/session.d.ts.map +1 -0
  50. package/dist/lib/upgrade/snapshot.d.ts +196 -0
  51. package/dist/lib/upgrade/snapshot.d.ts.map +1 -0
  52. package/dist/lib/upgrade/transport.d.ts +69 -0
  53. package/dist/lib/upgrade/transport.d.ts.map +1 -0
  54. package/dist/tsconfig.build.tsbuildinfo +1 -1
  55. package/package.json +1 -1
@@ -1,27 +1,107 @@
1
1
  /**
2
- * Upgrade orchestration.
2
+ * Upgrade orchestration — ONE state machine, and it is the single authority for
3
+ * lifecycle.
3
4
  *
4
5
  * Turns a parsed RFC 9470 challenge into an elevated session: pick a factor,
5
6
  * ask the user, run the prepare round if the factor needs one, assert, then
6
7
  * commit through `POST /auth/refresh` with the `upgrade` field.
7
8
  *
8
9
  * The controller owns no UI. It calls out to a prompt handler, which the React
9
- * layer (or any consumer) implements — that is what keeps the same logic
10
- * usable from a headless client.
10
+ * layer (or any consumer) implements — that is what keeps the same logic usable
11
+ * from a headless client.
12
+ *
13
+ * ## Why a state machine rather than rules
14
+ *
15
+ * Three review rounds each found two independently-correct lifecycle fixes
16
+ * colliding: fold-versus-single-flight, replay-binding-versus-aggregation, and
17
+ * a prompt outliving the flight whose state had already been cleared. The cause
18
+ * was not any individual rule — it was that lifecycle was specified as rules
19
+ * scattered across four sections, so two of them could each be locally right
20
+ * and jointly impossible. Everything about when state is set, read and cleared
21
+ * lives here, and nothing else writes these fields.
22
+ *
23
+ * ## The states
24
+ *
25
+ * | state | meaning |
26
+ * |--------------|----------------------------------------------------------------|
27
+ * | `idle` | no flight; every field clear; `inFlight === null` |
28
+ * | `collecting` | prompt open, no factor selected yet |
29
+ * | `proving` | a factor is selected; `prepare`/commit on the wire |
30
+ * | `replaying` | proof accepted; waiters replaying their own bound requests |
31
+ * | `settling` | every waiter has reported; the summary is complete |
32
+ * | `done` | prompt settled and every waiter settled; one step from `idle` |
33
+ *
34
+ * ## The decision that shapes all of it
35
+ *
36
+ * **An elevation ends when every waiting request has been retried and settled.**
37
+ * Proof acceptance does NOT end the flight. The previous implementation cleared
38
+ * `inFlight` in a `.finally` on proof completion, and that `.finally` was a real
39
+ * window: a prompt on screen with `inFlight === null`, in which an unrelated
40
+ * challenged request opened a SECOND modal and a waiter could report into a
41
+ * controller that had already forgotten its flight. Clearing happens in one
42
+ * synchronous step, after the prompt has settled, and nowhere else.
43
+ *
44
+ * Two costs the design accepts for that: the prompt stays open for one extra
45
+ * round trip while the replays settle, and a genuinely new challenge arriving in
46
+ * that window queues behind the flight rather than opening a second prompt.
11
47
  */
12
- import type { UpgradeChallenge } from './challenge';
48
+ import type { AssuranceBar, UpgradeChallenge } from './challenge';
13
49
  import { type UpgradeAssertion, type UpgradeFactor, type UpgradeFactorType, type UpgradePrepared } from './factors';
14
50
  import type { PlatformAdapter } from '../platform';
15
- /** What the UI is shown when a upgrade is required. */
51
+ /** What the UI is shown when an upgrade is required. */
16
52
  export type UpgradePromptRequest = {
17
- /** What the server demanded. */
53
+ /** What the server demanded — the MERGED requirement, when several challenges joined. */
18
54
  challenge: UpgradeChallenge;
19
55
  /** Usable factors, already ordered — the suggested one first. */
20
56
  factors: readonly UpgradeFactor[];
21
57
  /** Failure from the previous attempt, when the user is retrying. */
22
58
  previousError?: Error;
59
+ /**
60
+ * Called the moment the user picks a factor and BEFORE any answer is
61
+ * submitted. REQUIRED of every prompt implementation — the SDK's own modal,
62
+ * the `useUpgrade` hook and a render-prop override alike.
63
+ *
64
+ * `selected` is defined off this call and off nothing else. Deriving it from
65
+ * `prompt` resolving would implement the SUBMISSION boundary instead: the
66
+ * shipped `UpgradePromptResult` resolves with the code already collected, so
67
+ * `selected` would stay false for the whole interval in which the user has
68
+ * picked TOTP and is typing six digits — and a stronger challenge arriving
69
+ * then would re-rank the list under their fingers, the exact UI jump the
70
+ * selection boundary exists to prevent.
71
+ *
72
+ * An override that never calls it leaves the controller in `collecting` for
73
+ * the whole ceremony, so a late join re-ranks mid-entry: degraded, not silent,
74
+ * because `onRequirementRaised` still fires and the re-rank is observable.
75
+ */
76
+ onFactorSelected: (factor: UpgradeFactorType) => void;
77
+ /**
78
+ * The channel by which the controller pushes a raised bar and a re-ranked list
79
+ * into an OPEN prompt, which updates without resolving. Only ever used before
80
+ * a factor is selected (branch 2). The one-answer resolution contract is
81
+ * unchanged.
82
+ *
83
+ * A prompt SUBSCRIBES by assigning its own handler to this field on the
84
+ * request object it was handed, synchronously, before it awaits anything:
85
+ *
86
+ * ```ts
87
+ * const prompt: UpgradePromptHandler = (request) => {
88
+ * request.onRequirementRaised = (next) => setView(next);
89
+ * return collectAnswer(request);
90
+ * };
91
+ * ```
92
+ *
93
+ * It is a slot rather than a controller-supplied function because the
94
+ * direction is controller → prompt: only the prompt knows how to re-render
95
+ * itself. An override that leaves it unset shows the old bar, and the raised
96
+ * bar then surfaces as a re-challenged replay reporting `insufficient` —
97
+ * degraded, never silent.
98
+ */
99
+ onRequirementRaised?: (next: {
100
+ challenge: UpgradeChallenge;
101
+ factors: readonly UpgradeFactor[];
102
+ }) => void;
23
103
  };
24
- /** The user's answer to a upgrade prompt. */
104
+ /** The user's answer to an upgrade prompt. */
25
105
  export type UpgradePromptResult = {
26
106
  type: 'submit';
27
107
  factor: UpgradeFactorType;
@@ -33,28 +113,185 @@ export type UpgradePromptResult = {
33
113
  export type UpgradePromptHandler = (request: UpgradePromptRequest) => Promise<UpgradePromptResult>;
34
114
  /** Transport seam — the controller never touches axios directly, so it stays testable. */
35
115
  export type UpgradeTransport = {
36
- /** `POST /auth/upgrade/begin`. Only called for factors whose `requiresPrepare` is true. */
37
- prepare(method: UpgradeFactorType): Promise<UpgradePrepared>;
116
+ /**
117
+ * `POST /auth/upgrade/start`. Only called for factors whose `requiresPrepare`
118
+ * is true.
119
+ *
120
+ * Resolves `undefined` when no prepare round was needed after all — a one-shot
121
+ * factor, or a server that answers `prepare_not_required` because its view of
122
+ * the factor differs from ours. Both are recoveries: the factor then asserts
123
+ * with no `prepared`, which is already the TOTP path.
124
+ */
125
+ prepare(method: UpgradeFactorType): Promise<UpgradePrepared | undefined>;
38
126
  /** `POST /auth/refresh` carrying `upgrade`. Resolves once elevated tokens are stored. */
39
127
  commit(factor: UpgradeFactorType, assertion: UpgradeAssertion): Promise<void>;
40
128
  };
129
+ /** The six states. The controller is always in exactly one. */
130
+ export type UpgradeFlightState = 'idle' | 'collecting' | 'proving' | 'replaying' | 'settling' | 'done';
131
+ /**
132
+ * Who is waiting on this elevation.
133
+ *
134
+ * A `request` waiter owns a replay bound to its own URL and method. An
135
+ * `imperative` waiter has nothing to replay, but when its caller STATED a bar
136
+ * it still has a locally checkable requirement — which is why `requested`
137
+ * exists, and why an AAL3 imperative call joining an AAL2 flight can no longer
138
+ * be reported as `elevated`.
139
+ */
140
+ export type Waiter = {
141
+ kind: 'request';
142
+ url: string;
143
+ method: string;
144
+ } | {
145
+ kind: 'imperative';
146
+ requested?: AssuranceBar;
147
+ };
148
+ /** A waiter's terminal classification. `pending` until it reports. */
149
+ export type WaiterOutcome = 'pending' | 'replayed' | 'insufficient' | 'failed';
150
+ /** What a waiter reports once its own replay has settled. */
151
+ export type WaiterReport = {
152
+ outcome: 'replayed' | 'insufficient' | 'failed';
153
+ obtained?: AssuranceBar;
154
+ required?: AssuranceBar;
155
+ };
156
+ /**
157
+ * A registered waiter.
158
+ *
159
+ * `report()` is the declared inbound edge: classification happens in the
160
+ * adapter, after `elevate()` resolved, and this is the channel by which it
161
+ * reaches the controller. Because `waiters` is cleared in the final transition
162
+ * rather than on proof completion, the entries are still alive when the summary
163
+ * reads them.
164
+ */
165
+ export type WaiterEntry = Waiter & {
166
+ outcome: WaiterOutcome;
167
+ obtained?: AssuranceBar;
168
+ required?: AssuranceBar;
169
+ /** Called exactly once. Idempotent: later calls are ignored. */
170
+ report(result: WaiterReport): void;
171
+ };
172
+ /** The proof's own outcome, which is NOT any operation's outcome. */
173
+ export type UpgradeProof = {
174
+ status: 'proven';
175
+ obtained: AssuranceBar;
176
+ } | {
177
+ status: 'failed';
178
+ reason: Error;
179
+ };
180
+ /** What a successful `elevate()` hands back to its caller. */
181
+ export type UpgradeElevation = {
182
+ /** This caller's own registered waiter. `report()` is its edge back. */
183
+ waiter: WaiterEntry;
184
+ /** The bar the proof reached, as far as the client can know it. */
185
+ obtained: AssuranceBar;
186
+ };
187
+ /** One row of the per-flight outcome summary. */
188
+ export type UpgradeWaiterSummary = {
189
+ url?: string;
190
+ method?: string;
191
+ outcome: WaiterOutcome;
192
+ obtained?: AssuranceBar;
193
+ required?: AssuranceBar;
194
+ };
195
+ /**
196
+ * The per-flight outcome summary: a pure function of `waiters` plus `proof`.
197
+ *
198
+ * `terminal` is what the prompt may report. The modal must NOT say "success"
199
+ * while any waiter is re-challenged — for AAL3 endpoints with TOTP-only users
200
+ * that is the NORMAL path, not an edge case, and a design cannot both claim the
201
+ * server is the authority and delete the path by which its second refusal
202
+ * reaches the user.
203
+ */
204
+ export type UpgradeFlightSummary = {
205
+ proof: UpgradeProof;
206
+ waiters: UpgradeWaiterSummary[];
207
+ terminal: 'success' | 'insufficient' | 'failed';
208
+ /** The bars of the STRONGEST unmet waiter, so the message is honest. */
209
+ insufficient?: {
210
+ obtained: AssuranceBar;
211
+ required: AssuranceBar;
212
+ };
213
+ /** The proof failure, when the flight never produced a proof. */
214
+ error?: Error;
215
+ };
41
216
  export type UpgradeControllerOptions = {
42
217
  transport: UpgradeTransport;
43
218
  prompt: UpgradePromptHandler;
44
219
  platform: PlatformAdapter;
45
220
  /** Defaults to the factors this SDK can complete. */
46
221
  factors?: readonly UpgradeFactor[];
47
- /** How many times the user may answer wrongly before the elevation gives up. Server allows four. */
222
+ /** How many times the user may answer wrongly before the elevation gives up. */
48
223
  maxAttempts?: number;
224
+ /**
225
+ * The bar the elevation actually reached, when the host can read it — from the
226
+ * `acr` claim of the freshly stored access token, for instance.
227
+ *
228
+ * Without it the controller reports the bar the ceremony was RANKED for, which
229
+ * is the honest client-side approximation: the server scores `acr` from the
230
+ * base session plus the verified factor and is the only authority, and a
231
+ * replay that is re-challenged corrects the approximation immediately.
232
+ */
233
+ readObtainedBar?: () => AssuranceBar | undefined;
234
+ /**
235
+ * Called once per flight, in the settling transition, after every waiter has
236
+ * reported. This is how the open prompt is given its terminal state, and how
237
+ * the React layer gets exactly one terminal state per flight.
238
+ */
239
+ onFlightSettled?: (summary: UpgradeFlightSummary) => void;
240
+ /**
241
+ * Liveness guard: how long to wait for waiters to report before settling the
242
+ * flight anyway. A waiter that never calls `report()` is an adapter bug, and
243
+ * without a bound it would hold `inFlight` open forever — so every later
244
+ * elevation would join a dead flight, which is a worse failure than settling
245
+ * early. Defaults to 30s.
246
+ */
247
+ replaySettleTimeoutMs?: number;
49
248
  };
50
- /** Raised when the user dismissed the upgrade rather than failing it. */
249
+ /**
250
+ * Raised when the user dismissed the upgrade rather than failing it.
251
+ *
252
+ * `cause` is carried for one case only: a prompt implementation that threw or
253
+ * rejected is treated as a failed COLLECTION and settles on the cancellation
254
+ * contract, because there is no answer and no proof — but the thrown error is
255
+ * the only evidence of an integration bug, so it travels rather than being
256
+ * swallowed.
257
+ */
51
258
  export declare class UpgradeCancelledError extends Error {
52
- constructor();
259
+ readonly code: "upgrade_cancelled";
260
+ /** Declared here rather than inherited: `Error.cause` is ES2022, this build targets ES2021 libs. */
261
+ readonly cause?: unknown;
262
+ constructor(cause?: unknown);
53
263
  }
54
264
  /** Raised when no registered factor can satisfy the challenge. */
55
265
  export declare class UpgradeUnsatisfiableError extends Error {
266
+ readonly code: "upgrade_unsatisfiable";
56
267
  constructor(message?: string);
57
268
  }
269
+ /** The bar a challenge demands, in the shape the errors and the hook share. */
270
+ export declare const barOf: (challenge: UpgradeChallenge) => AssuranceBar;
271
+ /**
272
+ * Does `obtained` reach `required`?
273
+ *
274
+ * Only the `acr` axis is decidable client-side. A completed proof always
275
+ * satisfies a freshness demand, since it just happened, and a requirement with
276
+ * no level constrains nothing on this axis — so both of those are `true`.
277
+ *
278
+ * **An UNKNOWN obtained level does not meet a bar.** This is the one place in
279
+ * the feature where fail-open is wrong, and §13.4 revision 6 draws the line
280
+ * explicitly: fail-open governs *recovery* — availability, not logging the user
281
+ * out, not blocking the app — never the *granting of access*. Answering "meets"
282
+ * for a level we could not read is granting access on no evidence.
283
+ *
284
+ * It is NOT pre-judging satisfiability, and the difference is what §13.2 line
285
+ * 1599 and §13.4 line 2213 protect: nothing is refused outright, no elevation
286
+ * is declined, no factor is filtered. The SDK merely declines to ASSERT that a
287
+ * bar was reached; the server's replay re-challenge decides, and that surfaces
288
+ * as §13.3's `insufficient` with both bars attached. The only waiters this can
289
+ * settle locally are imperative ones that STATED a bar, which have no replay to
290
+ * ask the server about.
291
+ */
292
+ export declare const barMeets: (obtained: AssuranceBar, required: AssuranceBar) => boolean;
293
+ /** Field-wise merge of two requirements: `acr` tightens up, freshness tightens down. */
294
+ export declare const mergeChallenges: (current: UpgradeChallenge, incoming: UpgradeChallenge) => UpgradeChallenge;
58
295
  export declare class UpgradeController {
59
296
  private readonly transport;
60
297
  private readonly prompt;
@@ -62,33 +299,137 @@ export declare class UpgradeController {
62
299
  private readonly registry;
63
300
  private readonly available;
64
301
  private readonly maxAttempts;
302
+ private readonly readObtainedBar?;
303
+ private readonly onFlightSettled?;
304
+ private readonly settleTimeoutMs;
305
+ /** The current state. Public for diagnostics and tests; nothing outside writes it. */
306
+ private state;
65
307
  /**
66
308
  * The elevation currently running.
67
309
  *
68
- * Upgrade is single-flight on purpose. A screen that fires several protected
69
- * requests at once gets several challenges, and every one of them must land
70
- * on ONE prompt — otherwise the user sees a stack of modals and every
71
- * additional elevation burns a challenge for nothing.
310
+ * Single-flight is the STATE SET, not a flag: `elevate` returns the existing
311
+ * flight in every state but `idle`, and starts one only from `idle`. A screen
312
+ * that fires several protected requests at once gets several challenges, and
313
+ * every one of them must land on ONE prompt — otherwise the user sees a stack
314
+ * of modals and every extra elevation burns a challenge for nothing.
72
315
  */
73
316
  private inFlight;
317
+ /** The merged requirement. Written at registration and at every join. */
318
+ private requirement;
319
+ /** Non-null from the moment the prompt reports a selection, and from nowhere else. */
320
+ private selected;
321
+ /** The requirement in force when the factor was selected — what the ceremony was ranked for. */
322
+ private rankedRequirement;
323
+ private registrations;
324
+ private proof;
325
+ /** Factors the SERVER refused for this user, dropped for the duration of this flight only. */
326
+ private pruned;
327
+ /** The request handed to the open prompt. Its `onRequirementRaised` slot is branch 2's channel. */
328
+ private openPrompt;
329
+ /** Set while `awaitReports` is waiting; called by the reporting path. See H7. */
330
+ private reportsSettled?;
74
331
  constructor(options: UpgradeControllerOptions);
332
+ /** The current lifecycle state. Read-only. */
333
+ get currentState(): UpgradeFlightState;
334
+ /** True while a flight is open — from the first challenge until the last waiter settled. */
335
+ get active(): boolean;
75
336
  /**
76
337
  * Elevates the session so the challenged request can be retried.
77
338
  *
78
- * Concurrent callers share one elevation: the first starts it, the rest await
79
- * the same promise and all resolve together. Callers retry their own request
80
- * once this resolves; it rejects if the user cancelled or ran out of attempts.
339
+ * Every call registers a waiter, including the imperative one. Concurrent
340
+ * callers share one elevation and one prompt; the returned promise settles for
341
+ * THIS caller when the proof settles, and carries that caller's own waiter
342
+ * entry so it can report its replay's outcome back.
343
+ *
344
+ * It resolves when the server accepted a factor and recorded the elevation. It
345
+ * rejects for cancelled, exhausted, unsatisfiable, unavailable, session-changed
346
+ * and non-retryable transport failure — and, for a waiter whose own demanded
347
+ * bar the proof did not reach, with `UpgradeInsufficientError`.
348
+ */
349
+ elevate(challenge: UpgradeChallenge, waiter?: Waiter): Promise<UpgradeElevation>;
350
+ /** Registers a waiter and the bar it arrived demanding. */
351
+ private register;
352
+ /**
353
+ * Folds a joining challenge into the running flight.
354
+ *
355
+ * The boundary is factor SELECTION. Before it, nothing is on the wire and
356
+ * there is no race, so a strictly stronger challenge folds in place: the
357
+ * requirement rises, the list is re-ranked for the higher bar, and the open
358
+ * prompt is told. After it, a ceremony is running — a `prepare` round trip, a
359
+ * TOTP commit, a WebAuthn ceremony holding the platform authenticator — and
360
+ * neither interrupting it nor starting a second concurrent flight is
361
+ * acceptable, so it completes at the bar it was ranked for and each waiter's
362
+ * own replay decides its own fate.
363
+ */
364
+ private join;
365
+ /** The factors to offer right now: ranked for the merged bar, minus what the server refused. */
366
+ private offeredFactors;
367
+ /** The waiter's inbound edge. Idempotent — a second call is ignored. */
368
+ private receiveReport;
369
+ /** Resolves `awaitReports`' promise when the last outstanding report lands. */
370
+ private notifyReportsSettled;
371
+ /**
372
+ * The flight, start to finish.
373
+ *
374
+ * Nothing here clears state: clearing is one synchronous step at the very end,
375
+ * after the prompt has settled, so nothing can observe a half-cleared
376
+ * controller and no waiter can report into a flight the controller has
377
+ * forgotten.
378
+ */
379
+ private runFlight;
380
+ /**
381
+ * Settles one waiter against the proof.
382
+ *
383
+ * An imperative waiter has no operation to replay, so it is classified here
384
+ * and nowhere else: with no stated bar it simply succeeded, and with a stated
385
+ * bar the proof is checked against it — which is what stops an AAL3 imperative
386
+ * call that joined an AAL2 flight from resolving as though it had been
387
+ * elevated to AAL3.
388
+ *
389
+ * A REQUEST waiter is never pre-judged here, and §13 is explicit about it
390
+ * twice: "a demand above 3 is never refused client-side — the SDK does not
391
+ * pre-judge satisfiability (§13.4) — it ranks factors for the highest bar it
392
+ * knows and lets the server's replay re-challenge decide, surfacing as
393
+ * §13.3's `insufficient`", and "never refuse outright on the default path".
394
+ * The reason is not conformance alone: the client-side comparison is the
395
+ * fragile half — ACR ordinals, the minimum-of-list rule — so refusing here
396
+ * can skip a replay the server would have accepted, and it loses the
397
+ * `elevated-insufficient` diagnostic the replay path raises. So the waiter is
398
+ * released whatever the bar comparison says; `handleChallenge` replays, reads
399
+ * the server's answer, and reports `insufficient` if that is what it is.
400
+ */
401
+ private release;
402
+ /**
403
+ * Waits for every released waiter to report, under the liveness bound.
404
+ *
405
+ * Report-driven, not polled. The previous implementation re-armed a
406
+ * `setTimeout(…, 0)` until the last waiter reported, which burned a timer per
407
+ * event-loop turn for the whole replay window and made the state machine
408
+ * untestable under fake timers — `runAllTimers()` never terminates against a
409
+ * self-re-arming zero-delay timer. `receiveReport` is the one edge on which a
410
+ * waiter stops being pending, so it resolves this promise directly.
411
+ *
412
+ * The deadline stays: a waiter that never reports is an adapter bug, and
413
+ * settling it as `failed` is better than holding `inFlight` open forever,
414
+ * which would make every later elevation join a dead flight.
81
415
  */
82
- elevate(challenge: UpgradeChallenge): Promise<void>;
83
- private run;
416
+ private awaitReports;
417
+ /** The summary: a pure function of the waiters and the proof. */
418
+ private summarise;
84
419
  /**
85
- * Runs one full proof for a single factor.
420
+ * Collects an answer and proves it, re-prompting on every recoverable failure.
86
421
  *
87
- * The prepare round is re-run on every attempt rather than cached: a
88
- * challenge is single-use and the server consumes it atomically, so a failed
89
- * attempt has already spent it. Reusing the id would fail with
422
+ * The prepare round is re-run on every attempt rather than cached: a challenge
423
+ * is single-use and the server consumes it atomically, so a failed attempt has
424
+ * already spent it. Reusing the id would fail with
90
425
  * `ErrorUpgradeChallengeInvalid` and look like a bad code to the user.
91
426
  */
427
+ private prove;
428
+ /** Opens (or re-opens) the prompt and wires the two prompt → controller edges. */
429
+ private collect;
430
+ /** Maps a proof failure to a terminal reason, or `undefined` when the user may retry. */
431
+ private classifyProofFailure;
432
+ /** Runs one full proof for a single factor. */
92
433
  private proveAndCommit;
93
434
  }
94
435
  //# sourceMappingURL=controller.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../../lib/upgrade/controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAElB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EAErB,MAAM,WAAW,CAAC;AAGnB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,uDAAuD;AACvD,MAAM,MAAM,oBAAoB,GAAG;IACjC,gCAAgC;IAChC,SAAS,EAAE,gBAAgB,CAAC;IAC5B,iEAAiE;IACjE,OAAO,EAAE,SAAS,aAAa,EAAE,CAAC;IAClC,oEAAoE;IACpE,aAAa,CAAC,EAAE,KAAK,CAAC;CACvB,CAAC;AAEF,6CAA6C;AAC7C,MAAM,MAAM,mBAAmB,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEpH,8EAA8E;AAC9E,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,EAAE,oBAAoB,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAEnG,0FAA0F;AAC1F,MAAM,MAAM,gBAAgB,GAAG;IAC7B,2FAA2F;IAC3F,OAAO,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAC7D,yFAAyF;IACzF,MAAM,CAAC,MAAM,EAAE,iBAAiB,EAAE,SAAS,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/E,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,SAAS,EAAE,gBAAgB,CAAC;IAC5B,MAAM,EAAE,oBAAoB,CAAC;IAC7B,QAAQ,EAAE,eAAe,CAAC;IAC1B,qDAAqD;IACrD,OAAO,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IACnC,oGAAoG;IACpG,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,yEAAyE;AACzE,qBAAa,qBAAsB,SAAQ,KAAK;;CAK/C;AAED,kEAAkE;AAClE,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,OAAO,SAAkE;CAItF;AASD,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmB;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;IAC9C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkB;IAC3C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwC;IACjE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA2B;IACrD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IAErC;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,CAA8B;gBAElC,OAAO,EAAE,wBAAwB;IAS7C;;;;;;OAMG;IACH,OAAO,CAAC,SAAS,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;YAUrC,GAAG;IAoCjB;;;;;;;OAOG;YACW,cAAc;CAK7B"}
1
+ {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../../lib/upgrade/controller.ts"],"names":[],"mappings":"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"}