@aooth/auth-moost 0.1.22 → 0.1.24
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.
- package/dist/atscript/index.d.mts +38 -20
- package/dist/atscript/index.mjs +2 -2
- package/dist/{forms-uqegc32h.mjs → forms-xaBNc5Ng.mjs} +19 -3
- package/dist/index.d.mts +307 -27
- package/dist/index.mjs +483 -71
- package/package.json +9 -9
- package/src/atscript/models/forms.as +45 -2
- package/src/atscript/models/forms.as.d.ts +38 -19
package/dist/index.d.mts
CHANGED
|
@@ -1590,12 +1590,40 @@ interface AuthWfAddMfaState {
|
|
|
1590
1590
|
* replay-resistant). Gates the one-time swap + the management menu.
|
|
1591
1591
|
*/
|
|
1592
1592
|
stepUpDone?: boolean;
|
|
1593
|
+
/**
|
|
1594
|
+
* The user has explicitly consented to the step-up pincode dispatch —
|
|
1595
|
+
* either by submitting `manage-stepup-confirm`'s "we'll send a code to
|
|
1596
|
+
* ma•••@x" notice, or by picking a factor on `select-2fa` (choosing
|
|
1597
|
+
* "Email (ma•••@x)" and submitting IS the consent). Gates the
|
|
1598
|
+
* `manage-stepup-confirm` pause so opening the manage dialog never
|
|
1599
|
+
* dispatches a code as a side effect. Also set when
|
|
1600
|
+
* `resolveStepUpConfirmBeforeSend` opts the deployment out of the pause.
|
|
1601
|
+
* Server-only; sms/email step-up only (TOTP dispatches nothing).
|
|
1602
|
+
*/
|
|
1603
|
+
stepUpConfirmed?: boolean;
|
|
1593
1604
|
/** The management action the user picked on the menu. */
|
|
1594
1605
|
action?: "add" | "replace" | "remove";
|
|
1595
1606
|
/** The transport the chosen `action` applies to. */
|
|
1596
1607
|
target?: MfaTransport;
|
|
1597
1608
|
/** Set by `confirm-remove-mfa` so `finish-add-mfa` can report which factor was removed. */
|
|
1598
1609
|
removed?: MfaTransport;
|
|
1610
|
+
/**
|
|
1611
|
+
* Removing a factor is currently impossible: the user has exactly one
|
|
1612
|
+
* confirmed (policy-allowed) factor and `mfaPolicy.mode === 'required'`.
|
|
1613
|
+
* Computed by `manage-menu` before its pause and mirrored to
|
|
1614
|
+
* `public.manage.removeBlocked` so `ManageMfaForm` omits the Remove option
|
|
1615
|
+
* (and explains why) instead of offering an operation that can never
|
|
1616
|
+
* succeed. Re-checked server-side in `manage-menu` + `confirm-remove-mfa`.
|
|
1617
|
+
*/
|
|
1618
|
+
removeBlocked?: boolean;
|
|
1619
|
+
/**
|
|
1620
|
+
* Set by `confirm-remove-mfa` when it arrives in an un-removable state
|
|
1621
|
+
* (stale/crafted route — the menu filters these): the step aborts to the
|
|
1622
|
+
* `finish-add-mfa` terminal with a reason-specific message instead of
|
|
1623
|
+
* pausing on a form whose only submit re-throws the same guard error
|
|
1624
|
+
* (a dead-end loop, since the manage forms hide their built-in cancel).
|
|
1625
|
+
*/
|
|
1626
|
+
blocked?: "last-required-factor" | "method-locked";
|
|
1599
1627
|
}
|
|
1600
1628
|
/**
|
|
1601
1629
|
* Self-signup flow state. Populated by `init-signup` (policy from
|
|
@@ -1694,12 +1722,15 @@ interface AuthWfPublicState {
|
|
|
1694
1722
|
mfaEnroll?: Pick<AuthWfMfaEnrollState, "method" | "mode" | "availableTransports" | "secret" | "uri">;
|
|
1695
1723
|
/**
|
|
1696
1724
|
* Mirrors the manage-MFA menu inputs — the un-enrolled transports the user
|
|
1697
|
-
* can Add
|
|
1698
|
-
*
|
|
1725
|
+
* can Add, the locked transports to omit from Change/Remove, and the
|
|
1726
|
+
* `removeBlocked` flag that omits the Remove option for the last confirmed
|
|
1727
|
+
* factor under a `required` policy. The enrolled method list the menu
|
|
1728
|
+
* cross-references is `public.mfa.enrolledMethods`.
|
|
1699
1729
|
*/
|
|
1700
1730
|
manage?: {
|
|
1701
1731
|
candidates?: MfaTransport[];
|
|
1702
1732
|
locked?: MfaTransport[];
|
|
1733
|
+
removeBlocked?: boolean;
|
|
1703
1734
|
};
|
|
1704
1735
|
/** Mirrors `ctx.defaults` — prefill source for the recovery email field. */
|
|
1705
1736
|
defaults?: {
|
|
@@ -1754,6 +1785,14 @@ interface AuthWfCtx {
|
|
|
1754
1785
|
aborted?: boolean;
|
|
1755
1786
|
isFirstLogin?: boolean;
|
|
1756
1787
|
newPasswordRequired?: boolean;
|
|
1788
|
+
/**
|
|
1789
|
+
* Idempotency latch for the single `record-login` funnel step. Set true once
|
|
1790
|
+
* a login has been stamped this run — by the `credentials` step (the password
|
|
1791
|
+
* path stamps eagerly via `users.login()`) or by `record-login` itself — so
|
|
1792
|
+
* the funnel never double-writes `account.lastLogin`. Server-only flow
|
|
1793
|
+
* control; never `@wf.context.pass`-ed to the client.
|
|
1794
|
+
*/
|
|
1795
|
+
loginRecorded?: boolean;
|
|
1757
1796
|
autoLogin?: boolean;
|
|
1758
1797
|
consents?: AuthWfConsentsState;
|
|
1759
1798
|
pincode?: AuthWfPincodeUiState;
|
|
@@ -1888,7 +1927,8 @@ interface AuthWorkflowOpts {
|
|
|
1888
1927
|
enrollConfirm?: TAtscriptAnnotatedType; /** Manage-MFA menu (Add / Change / Remove) shown after step-up. */
|
|
1889
1928
|
manageMfa?: TAtscriptAnnotatedType; /** Confirm-removal pause for the manage-MFA "Remove" action. */
|
|
1890
1929
|
removeMfaConfirm?: TAtscriptAnnotatedType; /** Password re-auth — step-up fallback when no factor is MFA-challengeable. */
|
|
1891
|
-
passwordReauth?: TAtscriptAnnotatedType;
|
|
1930
|
+
passwordReauth?: TAtscriptAnnotatedType; /** Step-up dispatch consent — "we'll send a code to ma•••@x" notice before the step-up pincode send. */
|
|
1931
|
+
stepUpConfirm?: TAtscriptAnnotatedType;
|
|
1892
1932
|
select2fa?: TAtscriptAnnotatedType;
|
|
1893
1933
|
mfaCode?: TAtscriptAnnotatedType;
|
|
1894
1934
|
pincode?: TAtscriptAnnotatedType;
|
|
@@ -1944,6 +1984,7 @@ interface ResolvedAuthWorkflowOpts {
|
|
|
1944
1984
|
manageMfa: TAtscriptAnnotatedType;
|
|
1945
1985
|
removeMfaConfirm: TAtscriptAnnotatedType;
|
|
1946
1986
|
passwordReauth: TAtscriptAnnotatedType;
|
|
1987
|
+
stepUpConfirm: TAtscriptAnnotatedType;
|
|
1947
1988
|
select2fa: TAtscriptAnnotatedType;
|
|
1948
1989
|
mfaCode: TAtscriptAnnotatedType;
|
|
1949
1990
|
pincode: TAtscriptAnnotatedType;
|
|
@@ -2091,6 +2132,47 @@ declare class AuthWorkflow {
|
|
|
2091
2132
|
* mirrors `notifyNewDevice`'s posture). Never called by the base class.
|
|
2092
2133
|
*/
|
|
2093
2134
|
protected sendSecurityAlert(ctx: AuthWfCtx, reason: string, context?: Record<string, unknown>): Promise<void>;
|
|
2135
|
+
/**
|
|
2136
|
+
* A user just authenticated and a session is being established — interactive
|
|
2137
|
+
* login, federated (SSO) login, OR invite/signup/recovery auto-login. Fired
|
|
2138
|
+
* from the single `record-login` funnel, AFTER `account.lastLogin` is stamped
|
|
2139
|
+
* and BEFORE the session/code is delivered, so a throw aborts the login
|
|
2140
|
+
* atomically (no half-issued session). `ctx.subject` is set; read
|
|
2141
|
+
* `ctx.isFirstLogin` to distinguish the very first sign-in, `ctx.oauth` for
|
|
2142
|
+
* federated context. Does NOT fire for the no-session "fresh-login" finalize
|
|
2143
|
+
* (where the user is redirected to sign in separately).
|
|
2144
|
+
*/
|
|
2145
|
+
protected afterLogin(_ctx: AuthWfCtx): void | Promise<void>;
|
|
2146
|
+
/**
|
|
2147
|
+
* An invitee finished accepting their invite — account activated — whether or
|
|
2148
|
+
* not the flow auto-logged-them-in. Fires once, before the finalize terminal.
|
|
2149
|
+
* (An auto-login invite ALSO fires {@link afterLogin}.)
|
|
2150
|
+
*/
|
|
2151
|
+
protected afterInvitationAccepted(_ctx: AuthWfCtx): void | Promise<void>;
|
|
2152
|
+
/**
|
|
2153
|
+
* A self-signup account was created + activated (post password-set, post
|
|
2154
|
+
* activate). Fires once, before the auto-login finalize. (Signup always
|
|
2155
|
+
* auto-logins, so {@link afterLogin} fires too.)
|
|
2156
|
+
*/
|
|
2157
|
+
protected afterSignup(_ctx: AuthWfCtx): void | Promise<void>;
|
|
2158
|
+
/**
|
|
2159
|
+
* A recovery password reset completed. Fires once even when an `admin-only`
|
|
2160
|
+
* lock survived the reset (the password DID change) — so it runs ahead of the
|
|
2161
|
+
* still-locked guard. An auto-login recovery ALSO fires {@link afterLogin}.
|
|
2162
|
+
*/
|
|
2163
|
+
protected afterPasswordReset(_ctx: AuthWfCtx): void | Promise<void>;
|
|
2164
|
+
/**
|
|
2165
|
+
* An already-authenticated user changed their own password (change-password
|
|
2166
|
+
* flow). NOT a login — the session is rotated, not established — so
|
|
2167
|
+
* {@link afterLogin} does NOT fire.
|
|
2168
|
+
*/
|
|
2169
|
+
protected afterPasswordChanged(_ctx: AuthWfCtx): void | Promise<void>;
|
|
2170
|
+
/**
|
|
2171
|
+
* A user added, changed, or removed an MFA factor (add-mfa flow). NOT fired
|
|
2172
|
+
* on a cancel / nothing-to-do finish. The user KEEPS their session (no
|
|
2173
|
+
* re-issue), so {@link afterLogin} does NOT fire.
|
|
2174
|
+
*/
|
|
2175
|
+
protected afterMfaChanged(_ctx: AuthWfCtx): void | Promise<void>;
|
|
2094
2176
|
/**
|
|
2095
2177
|
* Return the list of selectable role identifiers for the admin invite form.
|
|
2096
2178
|
* Mirrors the prior `InviteWorkflow.getAvailableRoles()` consumer hook —
|
|
@@ -2236,6 +2318,39 @@ declare class AuthWorkflow {
|
|
|
2236
2318
|
* typed would confirm an unproven inbox. Never asked for TOTP.
|
|
2237
2319
|
*/
|
|
2238
2320
|
protected resolveEnrollPreConfirmed(_ctx: AuthWfCtx, _method: MfaTransport, _address: string): boolean | Promise<boolean>;
|
|
2321
|
+
/**
|
|
2322
|
+
* Pin the enrolment address for an sms/email transport — the policy seam
|
|
2323
|
+
* for deployments whose factor must be BOUND to an account record (e.g.
|
|
2324
|
+
* staff MFA locked to the work mailbox so portal access dies with it at
|
|
2325
|
+
* offboarding; a free-text form would let a self-service swap to a personal
|
|
2326
|
+
* inbox defeat that control entirely). Asked by `enroll-address` BEFORE its
|
|
2327
|
+
* form renders, with the staged transport (ctx-first, extra positional arg —
|
|
2328
|
+
* same convention as `resolveEnrollPreConfirmed`).
|
|
2329
|
+
*
|
|
2330
|
+
* Returning a string stages it as the enrolment address (normalized via
|
|
2331
|
+
* `normalizeMfaAddress`; the free-text form is SKIPPED — the same staging
|
|
2332
|
+
* seam consumer pre-seeding uses, so the user is never shown a form whose
|
|
2333
|
+
* only valid input is one known string). `'collect'` (the default) keeps
|
|
2334
|
+
* the free-text form. A pinned address composes with the rest of the trio
|
|
2335
|
+
* machinery untouched: `enroll-send` dispatches the pincode to it, and
|
|
2336
|
+
* `resolveEnrollPreConfirmed` may vouch it (a deployment pinning to a
|
|
2337
|
+
* verified-by-construction address gets the no-code path for free).
|
|
2338
|
+
*
|
|
2339
|
+
* ```ts
|
|
2340
|
+
* protected async resolveEnrollAddress(ctx: AuthWfCtx, method: MfaTransport) {
|
|
2341
|
+
* if (method !== "email") return "collect";
|
|
2342
|
+
* const user = await this.users.getUser(ctx.subject!);
|
|
2343
|
+
* return (user as { email?: string }).email ?? "collect";
|
|
2344
|
+
* }
|
|
2345
|
+
* ```
|
|
2346
|
+
*
|
|
2347
|
+
* The returned address is trusted as-is (no `validateMfaAddress` pass) —
|
|
2348
|
+
* the deployment is authoritative for its own records. An empty/blank
|
|
2349
|
+
* return falls back to `'collect'`. For nuanced RULES on a user-typed
|
|
2350
|
+
* address (domain allowlists, record comparisons) override the ctx-first
|
|
2351
|
+
* {@link validateMfaAddress} instead.
|
|
2352
|
+
*/
|
|
2353
|
+
protected resolveEnrollAddress(_ctx: AuthWfCtx, _method: MfaTransport): string | "collect" | Promise<string | "collect">;
|
|
2239
2354
|
/**
|
|
2240
2355
|
* Resolve the finalize policy. Reached from login.flow. `auditLogin` is
|
|
2241
2356
|
* dropped from the shape per §2 — audit moved out of the workflow layer.
|
|
@@ -2315,6 +2430,33 @@ declare class AuthWorkflow {
|
|
|
2315
2430
|
* ```
|
|
2316
2431
|
*/
|
|
2317
2432
|
protected resolveLockedMfaTransports(_ctx: AuthWfCtx): MfaTransport[] | Promise<MfaTransport[]>;
|
|
2433
|
+
/**
|
|
2434
|
+
* Whether the manage-MFA step-up must collect explicit consent BEFORE
|
|
2435
|
+
* dispatching its sms/email pincode (the `manage-stepup-confirm` pause:
|
|
2436
|
+
* "To continue, we will send a verification code to ma•••@x"). Default
|
|
2437
|
+
* `true` — nothing should email/text the user as a side effect of opening
|
|
2438
|
+
* a manage dialog: a user who opened it by mistake (or just to look)
|
|
2439
|
+
* closes it with zero codes consumed, no resend cooldown burnt. Override
|
|
2440
|
+
* to `false` to restore the zero-click dispatch (the code is already in
|
|
2441
|
+
* flight when the first form renders). Never asked for TOTP step-up
|
|
2442
|
+
* (nothing is dispatched) and not consulted by the login flow (its
|
|
2443
|
+
* challenge is mid-authentication, where zero-click is the norm).
|
|
2444
|
+
*/
|
|
2445
|
+
protected resolveStepUpConfirmBeforeSend(_ctx: AuthWfCtx): boolean | Promise<boolean>;
|
|
2446
|
+
/**
|
|
2447
|
+
* What the user's authenticator app shows as the ACCOUNT half of
|
|
2448
|
+
* "issuer: account" for a TOTP enrolment (the issuer half is
|
|
2449
|
+
* `resolveMfaPolicy().issuer` / `opts.totpIssuer`). Cosmetic only — never
|
|
2450
|
+
* used for lookup — but it is how a user with several entries tells
|
|
2451
|
+
* accounts apart, and it is encoded into the `otpauth://` URI at
|
|
2452
|
+
* secret-provisioning time, so it lives in the authenticator FOREVER
|
|
2453
|
+
* (re-labeling requires re-enrolment). Default prefers a human-readable
|
|
2454
|
+
* identifier the flow already carries (`ctx.email` — invite/recovery/
|
|
2455
|
+
* signup) and otherwise loads the user's `username`; the stable-uuid
|
|
2456
|
+
* `ctx.subject` is the last resort. Override for a richer label (display
|
|
2457
|
+
* name, tenant-qualified email, …).
|
|
2458
|
+
*/
|
|
2459
|
+
protected resolveTotpAccountLabel(ctx: AuthWfCtx): string | Promise<string>;
|
|
2318
2460
|
/**
|
|
2319
2461
|
* Resolve the channel-OTP disclosure copy rendered beneath the email/phone
|
|
2320
2462
|
* input on `AskEmailForm` / `AskPhoneForm`. Reached from login.flow Phase 3.
|
|
@@ -2583,6 +2725,18 @@ declare class AuthWorkflow {
|
|
|
2583
2725
|
* and resend.
|
|
2584
2726
|
*/
|
|
2585
2727
|
private mintAndSendEnrollPincode;
|
|
2728
|
+
/**
|
|
2729
|
+
* Idempotent TOTP secret provisioning into wf-state ONLY (the QR renders
|
|
2730
|
+
* from `public.mfaEnroll.secret/uri`; the user record is written on confirm
|
|
2731
|
+
* — write-on-confirm). The single implementation behind BOTH provisioning
|
|
2732
|
+
* sites — `enroll-pick-method`'s auto-pick/picker tail and `enroll-totp-qr`
|
|
2733
|
+
* (covers the manage add/change path where the picker is skipped) — so the
|
|
2734
|
+
* account label baked into the `otpauth://` URI cannot drift between them.
|
|
2735
|
+
* The label comes from {@link resolveTotpAccountLabel} (human-readable
|
|
2736
|
+
* default); blank falls back to the subject uuid so the URI always carries
|
|
2737
|
+
* SOME account discriminator.
|
|
2738
|
+
*/
|
|
2739
|
+
private provisionTotpSecret;
|
|
2586
2740
|
/**
|
|
2587
2741
|
* Drop the per-enrolment scratch fields (the `mfaEnroll` provisioning fields +
|
|
2588
2742
|
* the pincode timers/`sentTo`) off ctx — the shared teardown used by the
|
|
@@ -2600,8 +2754,24 @@ declare class AuthWorkflow {
|
|
|
2600
2754
|
* or `undefined` when valid. Email must look like an email; SMS is permissive
|
|
2601
2755
|
* E.164-ish (normalized by {@link normalizeMfaAddress}). Override for stricter
|
|
2602
2756
|
* (e.g. libphonenumber) validation.
|
|
2757
|
+
*
|
|
2758
|
+
* Ctx-first and async-capable, so record-based rules need no ctx-stash
|
|
2759
|
+
* workaround — an override can load the account and compare directly
|
|
2760
|
+
* (e.g. domain-allowlist the typed inbox, or require it to match a
|
|
2761
|
+
* record field). To PIN the address outright — never show the free-text
|
|
2762
|
+
* form at all — use {@link resolveEnrollAddress} instead; this hook is for
|
|
2763
|
+
* nuanced rules on what the user typed.
|
|
2764
|
+
*
|
|
2765
|
+
* ```ts
|
|
2766
|
+
* protected async validateMfaAddress(ctx: AuthWfCtx, method: MfaTransport, value: string) {
|
|
2767
|
+
* if (method === "email" && !value.trim().toLowerCase().endsWith("@corp.example")) {
|
|
2768
|
+
* return "Use your corporate email address";
|
|
2769
|
+
* }
|
|
2770
|
+
* return super.validateMfaAddress(ctx, method, value);
|
|
2771
|
+
* }
|
|
2772
|
+
* ```
|
|
2603
2773
|
*/
|
|
2604
|
-
protected validateMfaAddress(method: MfaTransport, value: string): string | undefined
|
|
2774
|
+
protected validateMfaAddress(_ctx: AuthWfCtx, method: MfaTransport, value: string): string | undefined | Promise<string | undefined>;
|
|
2605
2775
|
/**
|
|
2606
2776
|
* Light normalization of a validated MFA address before it is stored. Default:
|
|
2607
2777
|
* trims, and strips spacing/punctuation from SMS numbers while KEEPING the
|
|
@@ -2869,10 +3039,17 @@ declare class AuthWorkflow {
|
|
|
2869
3039
|
/**
|
|
2870
3040
|
* Terminal for the manage-MFA flow. The user KEEPS their current session (no
|
|
2871
3041
|
* re-issue, no cookies) — a plain data finish. Outcomes, in priority order:
|
|
2872
|
-
* removed → changed (`replace` + done) → added (done) →
|
|
2873
|
-
* (
|
|
3042
|
+
* removed → changed (`replace` + done) → added (done) → blocked
|
|
3043
|
+
* (un-removable operation aborted by `confirm-remove-mfa`) →
|
|
3044
|
+
* nothing-available (zero candidates, never had to step-up) → cancelled.
|
|
3045
|
+
*/
|
|
3046
|
+
finishAddMfa(ctx: AuthWfCtx): undefined | Promise<undefined>;
|
|
3047
|
+
/**
|
|
3048
|
+
* `finish-add-mfa`'s envelope construction, extracted pure so the outcome
|
|
3049
|
+
* priority (removed → changed → added → blocked → nothing-available →
|
|
3050
|
+
* cancelled) is unit-testable without a wf event context.
|
|
2874
3051
|
*/
|
|
2875
|
-
|
|
3052
|
+
protected buildAddMfaFinishEnvelope(ctx: AuthWfCtx): WfFinished;
|
|
2876
3053
|
/**
|
|
2877
3054
|
* Resolve which transports the user may NOT change/remove via the manage flow
|
|
2878
3055
|
* (calls {@link resolveLockedMfaTransports}) and write them to
|
|
@@ -2887,6 +3064,22 @@ declare class AuthWorkflow {
|
|
|
2887
3064
|
* encapsulated when no durable store is wired (the registry default).
|
|
2888
3065
|
*/
|
|
2889
3066
|
manageStepUpDone(ctx: AuthWfCtx): undefined;
|
|
3067
|
+
/**
|
|
3068
|
+
* Manage-MFA step-up consent — pauses on `StepUpConfirmForm` ("To continue,
|
|
3069
|
+
* we will send a verification code to ma•••@x") BEFORE `pincode-send`
|
|
3070
|
+
* dispatches the step-up code, so opening the manage dialog never consumes
|
|
3071
|
+
* a code send as a side effect. Fires only on the auto-picked paths (single
|
|
3072
|
+
* factor, or default factor) — an explicit `select-2fa` pick already
|
|
3073
|
+
* counts as consent (`select2fa` sets `stepUpConfirmed`). `Continue`
|
|
3074
|
+
* consents and the SAME engine pass mints + sends; `useDifferentMethod`
|
|
3075
|
+
* re-opens the picker; `cancel` aborts with nothing dispatched (the
|
|
3076
|
+
* schema's `{ break: aborted }` right after this step keeps the pair from
|
|
3077
|
+
* sending the declined code). Gated by {@link resolveStepUpConfirmBeforeSend}
|
|
3078
|
+
* (default on) — an opt-out marks consent and falls straight through.
|
|
3079
|
+
*/
|
|
3080
|
+
manageStepUpConfirm(ctx: AuthWfCtx): undefined | Promise<undefined>;
|
|
3081
|
+
/** `manage-stepup-confirm` tail — opt-out fall-through or the consent pause. */
|
|
3082
|
+
private applyStepUpConfirm;
|
|
2890
3083
|
/**
|
|
2891
3084
|
* Manage-MFA password re-auth — the step-up FALLBACK when the user's only
|
|
2892
3085
|
* confirmed factor(s) are of kinds the policy no longer allows, so nothing is
|
|
@@ -2900,6 +3093,15 @@ declare class AuthWorkflow {
|
|
|
2900
3093
|
* subject), and `verifyPassword` is the same check `changePassword` enforces.
|
|
2901
3094
|
*/
|
|
2902
3095
|
managePasswordReauth(ctx: AuthWfCtx): Promise<undefined>;
|
|
3096
|
+
/**
|
|
3097
|
+
* The keep-at-least-one rule: removing the user's LAST confirmed factor under
|
|
3098
|
+
* a `required` policy can never succeed. The single source for the predicate
|
|
3099
|
+
* `manage-menu` mirrors into `addMfa.removeBlocked` (to drop the Remove option)
|
|
3100
|
+
* AND `confirm-remove-mfa` re-checks before its pause (defence in depth) — so
|
|
3101
|
+
* a policy change (e.g. "keep at least two", or a backup-codes exception)
|
|
3102
|
+
* lands in one place, not two copies that can drift.
|
|
3103
|
+
*/
|
|
3104
|
+
private isLastRequiredFactor;
|
|
2903
3105
|
/**
|
|
2904
3106
|
* Manage-MFA menu — pauses on `ManageMfaForm` and routes the chosen
|
|
2905
3107
|
* `operation` (`add:<t>` / `replace:<t>` / `remove:<t>`). Only reached when
|
|
@@ -2912,8 +3114,13 @@ declare class AuthWorkflow {
|
|
|
2912
3114
|
/**
|
|
2913
3115
|
* Manage-MFA remove confirmation. Pauses on `RemoveMfaConfirmForm`; the
|
|
2914
3116
|
* 'Remove' submit performs the removal, 'Cancel' aborts. Re-checks the locked
|
|
2915
|
-
* set
|
|
2916
|
-
*
|
|
3117
|
+
* set and the keep-at-least-one rule (LAST confirmed factor under a
|
|
3118
|
+
* `required` policy) BEFORE the pause — and an un-removable state aborts to
|
|
3119
|
+
* the `finish-add-mfa` terminal (reason on `addMfa.blocked`) instead of
|
|
3120
|
+
* pausing: `manage-menu` filters these operations out, so arriving here
|
|
3121
|
+
* blocked means a stale/crafted route, and a retryable form whose only
|
|
3122
|
+
* submit re-throws the same guard would be a dead-end loop (the manage
|
|
3123
|
+
* forms hide their built-in cancel — the host owns it).
|
|
2917
3124
|
*/
|
|
2918
3125
|
confirmRemoveMfa(ctx: AuthWfCtx): Promise<undefined>;
|
|
2919
3126
|
askChannel(ctx: AuthWfCtx, channel: "email" | "phone"): Promise<unknown>;
|
|
@@ -2996,17 +3203,30 @@ declare class AuthWorkflow {
|
|
|
2996
3203
|
enrollPickMethod(ctx: AuthWfCtx): undefined | Promise<undefined>;
|
|
2997
3204
|
/**
|
|
2998
3205
|
* Unified MFA-enrol phase 2 (collect sms/email address). Not invoked for
|
|
2999
|
-
* totp.
|
|
3000
|
-
*
|
|
3001
|
-
*
|
|
3002
|
-
*
|
|
3003
|
-
*
|
|
3004
|
-
*
|
|
3005
|
-
*
|
|
3006
|
-
*
|
|
3007
|
-
*
|
|
3008
|
-
|
|
3009
|
-
|
|
3206
|
+
* totp. Asks {@link resolveEnrollAddress} FIRST — a deployment that pins
|
|
3207
|
+
* the address (factor bound to an account record) stages it here and the
|
|
3208
|
+
* free-text form never renders; this single call site covers every trio
|
|
3209
|
+
* path (picker, auto-pick, manage add/replace pre-seed), and `enroll-send`
|
|
3210
|
+
* dispatches to the pinned address in the same engine pass. Otherwise
|
|
3211
|
+
* (`'collect'`) handles `skip` (opt-in) / `cancel` (manage) /
|
|
3212
|
+
* `useDifferentMethod`, validates the typed address server-side via the
|
|
3213
|
+
* ctx-first {@link validateMfaAddress} (the client `@ui.form.validate`
|
|
3214
|
+
* hint is advisory), then STAGES the candidate value in wf-state
|
|
3215
|
+
* (`m.address`) — the user record is written only on confirm
|
|
3216
|
+
* (write-on-confirm), so an ADD leaves no partial row and a REPLACE keeps
|
|
3217
|
+
* the old confirmed value live until the new code verifies in
|
|
3218
|
+
* `enroll-confirm`. Collection ONLY: the pincode dispatch lives in
|
|
3219
|
+
* `enroll-send` (same engine pass, no extra round-trip), so a consumer
|
|
3220
|
+
* pre-seeding `mfaEnroll.address` — which skips this whole step via its
|
|
3221
|
+
* schema condition — still gets exactly one code.
|
|
3222
|
+
*/
|
|
3223
|
+
enrollAddress(ctx: AuthWfCtx): undefined | Promise<undefined>;
|
|
3224
|
+
/**
|
|
3225
|
+
* `enroll-address` tail — stage a pinned address, or run the free-text
|
|
3226
|
+
* collect pause (skip/cancel/useDifferentMethod triage + ctx-first
|
|
3227
|
+
* validation + write-on-confirm staging).
|
|
3228
|
+
*/
|
|
3229
|
+
private collectEnrollAddress;
|
|
3010
3230
|
/**
|
|
3011
3231
|
* Unified MFA-enrol dispatch (sms/email only) — the trio's ONLY pincode
|
|
3012
3232
|
* send. A separate step (the canonical "send if no pin" gate, mirroring
|
|
@@ -3178,6 +3398,52 @@ declare class AuthWorkflow {
|
|
|
3178
3398
|
* also zeroes `failedLoginAttempts`, so the next login starts clean.
|
|
3179
3399
|
*/
|
|
3180
3400
|
unlockAccount(ctx: AuthWfCtx): Promise<undefined>;
|
|
3401
|
+
/**
|
|
3402
|
+
* The SINGLE login-completion funnel. Every flow that establishes an
|
|
3403
|
+
* authenticated session routes through here — placed in each login schema
|
|
3404
|
+
* right after the guards and BEFORE the delivery terminal (`issue` /
|
|
3405
|
+
* `mint-authz-code` / `finalize-auto-login`) — so a login can never be
|
|
3406
|
+
* delivered without passing this point. Two uniform jobs:
|
|
3407
|
+
* 1. Stamp `account.lastLogin` exactly once (the sole workflow writer of it).
|
|
3408
|
+
* 2. Fire the `afterLogin` customer hook.
|
|
3409
|
+
*
|
|
3410
|
+
* Idempotent per run via `ctx.loginRecorded`: the password `credentials` path
|
|
3411
|
+
* already stamped eagerly through `users.login()` and latched the flag, so the
|
|
3412
|
+
* stamp NO-OPS there (no double write) — but `afterLogin` still fires, so the
|
|
3413
|
+
* hook runs exactly once for password logins too. Federated (`sso-callback`)
|
|
3414
|
+
* and auto-login (invite/signup/recovery) paths never call `login()`, so this
|
|
3415
|
+
* is their sole stamp. Self-gates on `ctx.subject`. Runs AFTER
|
|
3416
|
+
* `prepare-semantic-flags` derived `isFirstLogin`, so a genuine first
|
|
3417
|
+
* federated login still observes `isFirstLogin === true`.
|
|
3418
|
+
*
|
|
3419
|
+
* A throw (from the stamp or the hook) aborts the flow BEFORE delivery, so the
|
|
3420
|
+
* login fails atomically — no half-issued session.
|
|
3421
|
+
*/
|
|
3422
|
+
recordLogin(ctx: AuthWfCtx): undefined | Promise<undefined>;
|
|
3423
|
+
/**
|
|
3424
|
+
* Recovery-only guard, extracted from the finalize terminals so they stay pure
|
|
3425
|
+
* delivery. An `admin-only` lockout never self-unlocks, so a reset that left
|
|
3426
|
+
* the account frozen must NOT be confirmed-as-success OR auto-logged-in
|
|
3427
|
+
* (minting tokens would defeat the very freeze). When still locked it emits the
|
|
3428
|
+
* warn terminal and sets `ctx.aborted`, so the schema's following `{ break }`
|
|
3429
|
+
* skips BOTH the `record-login` funnel and the finalize terminals — a frozen
|
|
3430
|
+
* account is never stamped or logged in. The schema gates this on
|
|
3431
|
+
* `ctx.lockout?.mode === "admin-only"` (the only mode that can reach finalize
|
|
3432
|
+
* still locked: self-service ran `unlock-account`; temporary auto-expires).
|
|
3433
|
+
*/
|
|
3434
|
+
recoveryLockCheck(ctx: AuthWfCtx): Promise<undefined>;
|
|
3435
|
+
/**
|
|
3436
|
+
* Thin dispatcher steps for the flow-specific lifecycle hooks. Each is its own
|
|
3437
|
+
* schema step (rather than an in-terminal call) because its flow's finalize
|
|
3438
|
+
* terminals are SHARED across flows (`finalize-auto-login` serves invite +
|
|
3439
|
+
* recovery + signup; `finalize-fresh-login` serves invite + recovery), so a
|
|
3440
|
+
* dedicated step in the flow-specific schema fires the right hook without
|
|
3441
|
+
* ctx-discrimination inside a shared terminal. Named `fire*` so the overridable
|
|
3442
|
+
* `after*` hooks keep the clean public name.
|
|
3443
|
+
*/
|
|
3444
|
+
fireInvitationAccepted(ctx: AuthWfCtx): void | Promise<void>;
|
|
3445
|
+
fireSignup(ctx: AuthWfCtx): void | Promise<void>;
|
|
3446
|
+
firePasswordReset(ctx: AuthWfCtx): void | Promise<void>;
|
|
3181
3447
|
/**
|
|
3182
3448
|
* Issue access + refresh tokens via `auth.issue`. Stashes the login
|
|
3183
3449
|
* response envelope on `useWfFinished` so downstream `redirect` can
|
|
@@ -3251,7 +3517,7 @@ declare class AuthWorkflow {
|
|
|
3251
3517
|
* confirmation first. Discriminated by ctx-slot presence
|
|
3252
3518
|
* (`ctx.postReset` → recovery; otherwise invite).
|
|
3253
3519
|
*/
|
|
3254
|
-
finalizeFreshLogin(ctx: AuthWfCtx): undefined
|
|
3520
|
+
finalizeFreshLogin(ctx: AuthWfCtx): undefined;
|
|
3255
3521
|
/**
|
|
3256
3522
|
* Post-reset store read: did an `admin-only` lockout survive the password
|
|
3257
3523
|
* reset (account still frozen)? Callers MUST first confirm
|
|
@@ -3272,10 +3538,17 @@ declare class AuthWorkflow {
|
|
|
3272
3538
|
*/
|
|
3273
3539
|
private finishRecoveryReset;
|
|
3274
3540
|
/**
|
|
3275
|
-
* Auto-login finalize — invite + recovery
|
|
3276
|
-
* and stashes the login response envelope on
|
|
3277
|
-
* preserves any `message` set by an earlier terminal
|
|
3278
|
-
* the SPA paints the confirmation text alongside the
|
|
3541
|
+
* Auto-login finalize — invite + recovery + signup. PURE delivery: issues
|
|
3542
|
+
* access + refresh tokens and stashes the login response envelope on
|
|
3543
|
+
* `useWfFinished`. Invite preserves any `message` set by an earlier terminal
|
|
3544
|
+
* (`confirmation`) so the SPA paints the confirmation text alongside the
|
|
3545
|
+
* tokens (WF-INVITE-020).
|
|
3546
|
+
*
|
|
3547
|
+
* It records NOTHING and guards NOTHING: `account.lastLogin` + the
|
|
3548
|
+
* `afterLogin` hook are owned by the upstream `record-login` funnel step, and
|
|
3549
|
+
* the admin-only-survived-lock guard by the upstream `recovery-lock-check`
|
|
3550
|
+
* step — both of which `{ break }`/gate this step out when they apply, so a
|
|
3551
|
+
* still-frozen account never reaches here.
|
|
3279
3552
|
*/
|
|
3280
3553
|
finalizeAutoLogin(ctx: AuthWfCtx): Promise<undefined>;
|
|
3281
3554
|
/**
|
|
@@ -3476,12 +3749,19 @@ declare class AuthWorkflow {
|
|
|
3476
3749
|
* 3. STEP-UP (only when `stepUpRequired`): re-verify identity before any
|
|
3477
3750
|
* change — `mfaStepUpLoop` challenges an EXISTING factor when one is still
|
|
3478
3751
|
* challengeable (`stepUpMode==='mfa'`), else `manage-password-reauth` falls
|
|
3479
|
-
* back to the account password (`stepUpMode==='password'`).
|
|
3752
|
+
* back to the account password (`stepUpMode==='password'`). The sms/email
|
|
3753
|
+
* challenge collects explicit consent (`manage-stepup-confirm`) BEFORE
|
|
3754
|
+
* dispatching its pincode — opening the dialog never sends a code as a
|
|
3755
|
+
* side effect (see `resolveStepUpConfirmBeforeSend`). On success
|
|
3480
3756
|
* `manage-stepup-done` swaps off the encapsulated start onto the durable
|
|
3481
3757
|
* `store` strategy (server-anchored, replay-resistant; mirrors login's
|
|
3482
3758
|
* swap-after-credentials).
|
|
3483
3759
|
* 4. `manage-menu` (only when `stepUpRequired`) — pick add / change / remove +
|
|
3484
|
-
* target; pre-seeds `mfaEnroll.method` for add/change.
|
|
3760
|
+
* target; pre-seeds `mfaEnroll.method` for add/change. Un-offerable
|
|
3761
|
+
* operations never render: locked transports drop their Change/Remove
|
|
3762
|
+
* options, and the LAST factor under a `required` policy drops Remove
|
|
3763
|
+
* (`removeBlocked`) — `confirm-remove-mfa` aborts to the finish terminal
|
|
3764
|
+
* if a blocked remove arrives anyway (no retryable dead-end form).
|
|
3485
3765
|
* 5. Route: `confirm-remove-mfa` for remove; otherwise the REUSED enrol trio
|
|
3486
3766
|
* (`enroll-pick-method` → `enroll-address` / `enroll-totp-qr` →
|
|
3487
3767
|
* `enroll-confirm`). A zero-MFA user skips step-up + menu and lands on the
|