@aooth/auth-moost 0.1.3 → 0.1.4
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 +2 -2
- package/dist/atscript/index.mjs +2 -2
- package/dist/forms-sF41Fzzn.mjs +380 -0
- package/dist/index.d.mts +1328 -393
- package/dist/index.mjs +3630 -2363
- package/package.json +19 -15
- package/src/atscript/models/forms.as +347 -33
- package/src/atscript/models/forms.as.d.ts +115 -38
- package/dist/forms-BE62OrN1.mjs +0 -230
package/dist/index.d.mts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { Mate, TInterceptorDef, TMateParamMeta, TMoostMetadata } from "moost";
|
|
1
2
|
import { AuthContext, AuthContext as AuthContext$1, AuthCredential, AuthEmailEvent, AuthEmailKind, AuthEmailKind as AuthEmailKind$1, AuthSmsEvent, AuthSmsKind, AuthSmsKind as AuthSmsKind$1, BuildMagicLinkUrl, BuildMagicLinkUrl as BuildMagicLinkUrl$1, EmailSender, EmailSender as EmailSender$1, IssueResult, IssueResult as IssueResult$1, SmsSender, generateMagicLinkToken } from "@aooth/auth";
|
|
2
3
|
import * as _$_wooksjs_event_core0 from "@wooksjs/event-core";
|
|
3
4
|
import { TCookieAttributesInput } from "@wooksjs/event-http";
|
|
4
|
-
import { Mate, TInterceptorDef, TMateParamMeta, TMoostMetadata } from "moost";
|
|
5
|
-
import { MoostWf, WfOutlet, WfOutletTokenConfig, WfStateStrategy } from "@moostjs/event-wf";
|
|
6
5
|
import { TrustedDeviceRecord, UserCredentials, UserService } from "@aooth/user";
|
|
6
|
+
import { WfFinished } from "@atscript/moost-wf";
|
|
7
|
+
import { MoostWf, WfOutlet, WfOutletTokenConfig, WfStateStrategy } from "@moostjs/event-wf";
|
|
7
8
|
import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
|
|
8
9
|
|
|
9
10
|
//#region src/auth.config.d.ts
|
|
@@ -46,6 +47,431 @@ interface ResolvedAuthOptions {
|
|
|
46
47
|
enableBearer: boolean;
|
|
47
48
|
}
|
|
48
49
|
//#endregion
|
|
50
|
+
//#region src/auth.opts.d.ts
|
|
51
|
+
declare class AuthOpts {
|
|
52
|
+
/** Pincode infrastructure shared by login MFA, invite MFA, and recovery OTP. */
|
|
53
|
+
mfa: {
|
|
54
|
+
pincodeLength: number;
|
|
55
|
+
pincodeTtlMs: number;
|
|
56
|
+
pincodeResendTimeoutMs: number;
|
|
57
|
+
};
|
|
58
|
+
/** Magic-link TTL shared by login (alt-credentials), invite, recovery. */
|
|
59
|
+
magicLinkTtlMs: number;
|
|
60
|
+
/** Canonical login URL — used by invite (post-accept redirect) and recovery (abort-to-login + post-reset redirect) as the resolver-default loginUrl. */
|
|
61
|
+
loginUrl: string;
|
|
62
|
+
/** TOTP provisioning issuer — used by login MFA and invite MFA enrollment. */
|
|
63
|
+
totpIssuer: string;
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/workflows/auth-workflow.base.d.ts
|
|
67
|
+
/**
|
|
68
|
+
* Method names for the MFA enrollment helper. Re-exported from
|
|
69
|
+
* `login.workflow.options` as `MfaTransport` (kept as the public alias) so
|
|
70
|
+
* existing consumers don't need to switch import paths.
|
|
71
|
+
*/
|
|
72
|
+
type MfaTransport = "sms" | "email" | "totp";
|
|
73
|
+
/**
|
|
74
|
+
* Context shape consumed by the `enrollPickPhase` / `enrollAddressPhase` /
|
|
75
|
+
* `enrollConfirmPhase` helpers. Both `LoginWfCtx` and
|
|
76
|
+
* `InviteWfCtx` extend this implicitly (they declare the same field set).
|
|
77
|
+
* Kept structural so neither workflow's full ctx union has to be imported
|
|
78
|
+
* here — base stays workflow-agnostic.
|
|
79
|
+
*/
|
|
80
|
+
interface MfaEnrollCtx {
|
|
81
|
+
enrollMethod?: MfaTransport;
|
|
82
|
+
enrollAddress?: string;
|
|
83
|
+
enrollSecret?: string;
|
|
84
|
+
enrollUri?: string;
|
|
85
|
+
enrollAvailableTransports?: MfaTransport[];
|
|
86
|
+
enrollDone?: boolean;
|
|
87
|
+
pin?: string;
|
|
88
|
+
pinExpire?: number;
|
|
89
|
+
pinSentTo?: string;
|
|
90
|
+
/**
|
|
91
|
+
* Next-allowed-resend timestamp for the Phase 3 confirm pincode (sms/email
|
|
92
|
+
* only). Written by the Phase 2 initial send + the Phase 3 `resend`
|
|
93
|
+
* alt-action; consulted by `resend` to throttle re-emits. Mirrors the
|
|
94
|
+
* `pinTimeout` pattern used by the login `pincode-check-login` step.
|
|
95
|
+
*/
|
|
96
|
+
enrollPincodeCooldown?: number;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Looser structural mirror of `DeliverPayload` from `login.workflow.ts`.
|
|
100
|
+
* The base file mustn't import from a sibling workflow file; the concrete
|
|
101
|
+
* workflow's strict discriminated union is structurally assignable to this.
|
|
102
|
+
*/
|
|
103
|
+
interface DeliverPayloadLike {
|
|
104
|
+
channel: "email" | "sms";
|
|
105
|
+
kind: string;
|
|
106
|
+
recipient: string;
|
|
107
|
+
code?: string;
|
|
108
|
+
expiresAt?: number;
|
|
109
|
+
ttlMs?: number;
|
|
110
|
+
userId?: string;
|
|
111
|
+
}
|
|
112
|
+
interface MfaEnrollDeps {
|
|
113
|
+
ctx: MfaEnrollCtx;
|
|
114
|
+
username: string;
|
|
115
|
+
users: UserService;
|
|
116
|
+
/** Concrete workflow's `deliver` hook, narrowed to the payloads this flow emits. */
|
|
117
|
+
deliver: (payload: DeliverPayloadLike) => Promise<void>;
|
|
118
|
+
forms: {
|
|
119
|
+
pickMethod: TAtscriptAnnotatedType;
|
|
120
|
+
address: TAtscriptAnnotatedType;
|
|
121
|
+
confirm: TAtscriptAnnotatedType;
|
|
122
|
+
};
|
|
123
|
+
transports: MfaTransport[];
|
|
124
|
+
pincodeLength: number;
|
|
125
|
+
pincodeTtlMs: number;
|
|
126
|
+
/**
|
|
127
|
+
* Per-method resend cooldown for the Phase 3 confirm pincode (sms/email).
|
|
128
|
+
* Mirrors `LoginWorkflowOpts.mfa.pincodeResendTimeoutMs`.
|
|
129
|
+
*/
|
|
130
|
+
pincodeResendTimeoutMs: number;
|
|
131
|
+
/** TOTP provisioning issuer (rendered in the authenticator app). */
|
|
132
|
+
issuer: string;
|
|
133
|
+
/**
|
|
134
|
+
* Enrollment policy. `'required'` runs through all 3 phases. `'optional'`
|
|
135
|
+
* additionally watches for a `skip` action on the Phase 1 pickMethod form —
|
|
136
|
+
* a skip click short-circuits by setting `ctx.enrollDone = true`. The
|
|
137
|
+
* caller is expected to gate this step out entirely when `mode === 'disabled'`.
|
|
138
|
+
*/
|
|
139
|
+
mode: "required" | "optional";
|
|
140
|
+
/**
|
|
141
|
+
* Optional bridge fired at the end of `enrollConfirmPhase` (or on a skip in
|
|
142
|
+
* `'optional'` mode at any phase) — right after `enrollDone` flips true. Login
|
|
143
|
+
* uses this to mirror `enrollDone` → `mfaChecked` so its outer MFA while-loop
|
|
144
|
+
* (gated on `!mfaChecked`) exits. Invite omits it because its enrollment
|
|
145
|
+
* while-loop is gated on `!enrollDone` directly.
|
|
146
|
+
*/
|
|
147
|
+
onComplete?: (ctx: MfaEnrollCtx) => void;
|
|
148
|
+
}
|
|
149
|
+
/** Workflow context shape expected by `mintPin` + `verifyPin`. */
|
|
150
|
+
interface PinCtx {
|
|
151
|
+
pin?: string;
|
|
152
|
+
pinExpire?: number;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Structural ctx shape consumed by `processInlineConsent`. Mirrors the
|
|
156
|
+
* relevant subset of the workflow ctx types so the helper stays
|
|
157
|
+
* workflow-agnostic. The helper consumes only the dynamic `pendingConsents`
|
|
158
|
+
* descriptor array (populated by `prepare-consents` from
|
|
159
|
+
* `ConsentStore.getPendingConsents()`) and the per-run booking fields it
|
|
160
|
+
* writes itself — the prior static `acceptance` / `termsVersion` branches
|
|
161
|
+
* were retired in Phase 6 along with the matching `ctx.acceptance` field
|
|
162
|
+
* on each workflow ctx type.
|
|
163
|
+
*/
|
|
164
|
+
interface InlineConsentCtx {
|
|
165
|
+
/**
|
|
166
|
+
* Descriptors the user still needs to be prompted for — set once by
|
|
167
|
+
* `prepare-consents` after username-bind. Empty / unset ⇒ no consents to
|
|
168
|
+
* collect (helper short-circuits).
|
|
169
|
+
*/
|
|
170
|
+
pendingConsents?: ConsentDescriptorLike[];
|
|
171
|
+
/**
|
|
172
|
+
* Subset of descriptor ids the user ticked on the carrier form. Set by
|
|
173
|
+
* `processInlineConsent` after silent-dropping unknown ids — `pendingConsents`
|
|
174
|
+
* is the server's whitelist, NOT what the client posts.
|
|
175
|
+
*/
|
|
176
|
+
acceptedConsentIds?: string[];
|
|
177
|
+
/**
|
|
178
|
+
* Wall-clock ms at the moment `processInlineConsent` resolved the
|
|
179
|
+
* `consents` array. Captured here so the persisted `ConsentEvent.at`
|
|
180
|
+
* reflects user-action time, not write-time — surviving a paused
|
|
181
|
+
* workflow's resume gap.
|
|
182
|
+
*/
|
|
183
|
+
consentsDecidedAt?: number;
|
|
184
|
+
/**
|
|
185
|
+
* Set true by `persist-consents` after the batched `consentStore.save`
|
|
186
|
+
* call fires (or after the step short-circuits with no pending consents).
|
|
187
|
+
* Gates the helper from re-staging on a subsequent carrier-form submission.
|
|
188
|
+
*/
|
|
189
|
+
consentsPersisted?: boolean;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Structural alias of `ConsentDescriptor` — kept inline so this module
|
|
193
|
+
* doesn't import from `../consent.store.ts` (which would create a cycle:
|
|
194
|
+
* consent.store.ts already imports `ConsentEvent` from here).
|
|
195
|
+
*/
|
|
196
|
+
interface ConsentDescriptorLike {
|
|
197
|
+
id: string;
|
|
198
|
+
text: string;
|
|
199
|
+
required?: string;
|
|
200
|
+
version?: string;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Subset of the carrier-form payload that `processInlineConsent` reads.
|
|
204
|
+
* Phase 5 replaces the pre-existing static `{ acceptedTerms?, marketingOptIn? }`
|
|
205
|
+
* pair with a single dynamic `consents: string[]` — the SUBSET of descriptor
|
|
206
|
+
* ids the user ticked in the `AsConsentArray` (`@atscript/vue-aooth`)
|
|
207
|
+
* component. The server reads `pendingConsents` from its own ctx (NOT from
|
|
208
|
+
* this input) to decide which ids are valid; unknown ids are silently
|
|
209
|
+
* dropped (audit-grade defense — see helper rationale).
|
|
210
|
+
*/
|
|
211
|
+
interface InlineConsentInput {
|
|
212
|
+
consents?: string[];
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Consent event emitted to the `ConsentStore.save(username, events)` DI
|
|
216
|
+
* provider. Storage shape is intentionally the consumer's call — Mongo users
|
|
217
|
+
* typically push the events onto an embedded array, SQL users insert into an
|
|
218
|
+
* audit table, event-bus users publish to a topic. The library batches all
|
|
219
|
+
* collected events from a single workflow run into one call: ONE event per
|
|
220
|
+
* pending descriptor (audit-friendly default — declined-optional consents
|
|
221
|
+
* are persisted too, so customers can prove the user was asked; customers
|
|
222
|
+
* who want only accepted events filter in their `save()` override). The
|
|
223
|
+
* `accepted` boolean is explicit per row — `true` when the user ticked the
|
|
224
|
+
* matching descriptor, `false` when an optional descriptor went un-ticked.
|
|
225
|
+
*/
|
|
226
|
+
interface ConsentEvent {
|
|
227
|
+
/** Identifier from the matching `ConsentDescriptor.id`. */
|
|
228
|
+
id: string;
|
|
229
|
+
/** Whether the user ticked this descriptor (`false` for un-ticked optionals). */
|
|
230
|
+
accepted: boolean;
|
|
231
|
+
/** Stamped from the matching `ConsentDescriptor.version` (when set). */
|
|
232
|
+
version?: string;
|
|
233
|
+
/**
|
|
234
|
+
* Wall-clock ms at the moment `processInlineConsent` resolved the user's
|
|
235
|
+
* carrier-form submission (NOT at write-time — captured BEFORE the batched
|
|
236
|
+
* `consentStore.save` call so a paused-workflow resume gap doesn't drift
|
|
237
|
+
* the timestamp).
|
|
238
|
+
*/
|
|
239
|
+
at: number;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Structural alias for `ReturnType<typeof useAtscriptWf>` — only the
|
|
243
|
+
* `requireInput` method is consumed by `processInlineConsent`, kept narrow
|
|
244
|
+
* so callers can pass any form's wf handle without TS choking on the form-
|
|
245
|
+
* specific `resolveInput` return type.
|
|
246
|
+
*/
|
|
247
|
+
type WfRequireInputOnly = {
|
|
248
|
+
requireInput(opts?: {
|
|
249
|
+
errors?: Record<string, string>;
|
|
250
|
+
formMessage?: string;
|
|
251
|
+
}): unknown;
|
|
252
|
+
};
|
|
253
|
+
declare class AuthWorkflowBase {
|
|
254
|
+
/**
|
|
255
|
+
* Asserts `ctx.username` is populated. Workflow steps reach for `ctx.username`
|
|
256
|
+
* after `credentials`/`init` has set it; losing it indicates a workflow-state
|
|
257
|
+
* bug, not a client error. Throws `HttpError(500)` on miss; otherwise narrows
|
|
258
|
+
* the field to `string` for the caller via `asserts`.
|
|
259
|
+
*/
|
|
260
|
+
protected requireUsername<T extends {
|
|
261
|
+
username?: string;
|
|
262
|
+
}>(ctx: T): asserts ctx is T & {
|
|
263
|
+
username: string;
|
|
264
|
+
};
|
|
265
|
+
/**
|
|
266
|
+
* Wrap an `UserStore` mutation that can race (`withCas`-backed paths:
|
|
267
|
+
* `consumeBackupCode`, `addMfaMethod`, `confirmMfaMethod`, `addTrustedDevice`)
|
|
268
|
+
* so a CAS retry-budget exhaustion surfaces as 409 Conflict — the canonical
|
|
269
|
+
* OCC status — rather than bubbling to the moost default 500. Client SHOULD
|
|
270
|
+
* retry; a 500 falsely implies the server is broken. Other `UserAuthError`
|
|
271
|
+
* shapes pass through unchanged so step-local catch blocks (e.g. ALREADY_EXISTS
|
|
272
|
+
* → 409 with a different reason) still see them.
|
|
273
|
+
*/
|
|
274
|
+
protected withStoreErrorTranslation<T>(op: () => Promise<T>): Promise<T>;
|
|
275
|
+
/**
|
|
276
|
+
* Resolve the client IP from the active HTTP request, swallowing the case
|
|
277
|
+
* where there is no HTTP context (unit tests that hand-roll the wf runtime).
|
|
278
|
+
*/
|
|
279
|
+
protected resolveClientIp(): string | undefined;
|
|
280
|
+
/**
|
|
281
|
+
* Mint a numeric pincode and stash it + its expiry onto `ctx`. Returns the
|
|
282
|
+
* code so the caller can hand it to the delivery transport.
|
|
283
|
+
*/
|
|
284
|
+
protected mintPin(ctx: PinCtx, length: number, ttlMs: number): string;
|
|
285
|
+
/**
|
|
286
|
+
* Verify a submitted pincode against `ctx.pin`. Returns a `{ code: '…' }`
|
|
287
|
+
* error map on expired/invalid, or `null` on success. Callers wrap the result
|
|
288
|
+
* with `useAtscriptWf(PincodeForm).requireInput({ errors })`.
|
|
289
|
+
*/
|
|
290
|
+
protected verifyPin(ctx: PinCtx, submitted: string | undefined): {
|
|
291
|
+
code: string;
|
|
292
|
+
} | null;
|
|
293
|
+
/**
|
|
294
|
+
* Validate + stash inline-consent fields submitted on a carrier form.
|
|
295
|
+
*
|
|
296
|
+
* SECURITY (silent-drop): the server reads its OWN `ctx.pendingConsents`
|
|
297
|
+
* (set once by `prepare-consents` from `ConsentStore.getPendingConsents()`)
|
|
298
|
+
* as the authoritative whitelist of valid descriptor ids. Any id in the
|
|
299
|
+
* user-submitted `input.consents` array that does NOT match a current
|
|
300
|
+
* pending descriptor is SILENTLY DROPPED — no error, no log, no signal
|
|
301
|
+
* back to the client. This preserves the audit-grade
|
|
302
|
+
* "what user saw is what server records" invariant: an attacker
|
|
303
|
+
* submitting `consents: ['terms', 'gdpr-forged-id']` against a descriptor
|
|
304
|
+
* list of only `['terms']` cannot forge an audit record for the
|
|
305
|
+
* never-displayed `'gdpr-forged-id'` consent. Surfacing the drop would
|
|
306
|
+
* leak the consent universe (probing surface), so the defense is silent.
|
|
307
|
+
*
|
|
308
|
+
* SECURITY (mandatory-by-message): each descriptor's `required` field is
|
|
309
|
+
* the load-bearing mandatory flag. A non-empty string means the consent
|
|
310
|
+
* is MANDATORY and that string IS the per-row error message — the
|
|
311
|
+
* `AsConsentArray` component surfaces it inline per descriptor; the
|
|
312
|
+
* server throws the SAME copy as a form-level error on the bound
|
|
313
|
+
* `consents` field when the first required descriptor is missing from
|
|
314
|
+
* the submitted set. Absent / empty `required` ⇒ optional consent — the
|
|
315
|
+
* un-ticked descriptor is still persisted as `{accepted: false}` (audit
|
|
316
|
+
* default — proves the user was asked).
|
|
317
|
+
*
|
|
318
|
+
* Idempotency: once `ctx.consentsPersisted` is true, the helper is a
|
|
319
|
+
* no-op. Same for `ctx.pendingConsents` being empty / unset — no
|
|
320
|
+
* pending = nothing to validate (the carrier-form's `AsConsentArray`
|
|
321
|
+
* also self-hides on empty `pendingConsents`).
|
|
322
|
+
*/
|
|
323
|
+
protected processInlineConsent(ctx: InlineConsentCtx, input: InlineConsentInput, wf: WfRequireInputOnly): void;
|
|
324
|
+
/**
|
|
325
|
+
* Batched consent persistence — shared `persist-consents` step body for
|
|
326
|
+
* `LoginWorkflow` / `InviteWorkflow` / `RecoveryWorkflow`. Fans one
|
|
327
|
+
* `ConsentEvent` per pending descriptor out to the `ConsentStore.save`
|
|
328
|
+
* DI provider in a single call. Audit-friendly default: declined-optional
|
|
329
|
+
* consents are persisted with `accepted: false` (customers who want only
|
|
330
|
+
* accepted events filter in their `save()` override). `accepted` is
|
|
331
|
+
* derived per descriptor by `acceptedConsentIds.has(id)`. Idempotent via
|
|
332
|
+
* `ctx.consentsPersisted`; short-circuits with no events when
|
|
333
|
+
* `pendingConsents` is empty (defensive — the schema condition gates on
|
|
334
|
+
* `consentsDecidedAt` which is only set when pending was non-empty).
|
|
335
|
+
*
|
|
336
|
+
* Each workflow's `@Step("persist-consents")` method is a one-liner
|
|
337
|
+
* delegate to this helper — the @Step decorator must stay on the
|
|
338
|
+
* subclass so the wf engine registers the step id under the correct
|
|
339
|
+
* controller, but the body lives here once.
|
|
340
|
+
*/
|
|
341
|
+
protected runPersistConsents(ctx: InlineConsentCtx & {
|
|
342
|
+
username?: string;
|
|
343
|
+
}, consentStore: ConsentStore): Promise<undefined>;
|
|
344
|
+
/**
|
|
345
|
+
* Phase 1 of MFA enrollment. Picks the method (auto-pick if only one
|
|
346
|
+
* transport, otherwise pause for the picker form), handles the `skip`
|
|
347
|
+
* alt-action in `'optional'` mode, and — when TOTP is picked — provisions
|
|
348
|
+
* the secret idempotently in the same step body so the next iteration can
|
|
349
|
+
* proceed straight to confirm. Sync-friendly return type because the
|
|
350
|
+
* auto-pick branch and the picker-form branch both stay synchronous; the
|
|
351
|
+
* TOTP-provisioning tail is the only async path.
|
|
352
|
+
*
|
|
353
|
+
* Atomic boundary: after this helper runs, `ctx.enrollMethod` is set AND
|
|
354
|
+
* (for totp) `ctx.enrollSecret` is provisioned. Confirm doesn't need to
|
|
355
|
+
* worry about provisioning.
|
|
356
|
+
*/
|
|
357
|
+
protected enrollPickPhase(deps: MfaEnrollDeps): undefined | Promise<undefined>;
|
|
358
|
+
/**
|
|
359
|
+
* Phase 2 of MFA enrollment. Collects the sms/email address, persists it as
|
|
360
|
+
* an unconfirmed method, mints + dispatches the pincode. Handles `skip` /
|
|
361
|
+
* `useDifferentMethod` alt-actions. Not invoked for totp (no address to
|
|
362
|
+
* collect — the schema condition gates it out).
|
|
363
|
+
*/
|
|
364
|
+
protected enrollAddressPhase(deps: MfaEnrollDeps): Promise<undefined>;
|
|
365
|
+
/**
|
|
366
|
+
* Phase 3 of MFA enrollment. Verifies the user-submitted code (TOTP or
|
|
367
|
+
* pincode), marks the method confirmed, sets it as the default, and flags
|
|
368
|
+
* `ctx.enrollDone = true`. Handles `skip` / `useDifferentMethod` / `resend`
|
|
369
|
+
* alt-actions (cleanup on the first two; resend re-mints + redispatches).
|
|
370
|
+
*
|
|
371
|
+
* Idempotently provisions the TOTP secret at the top when missing — covers
|
|
372
|
+
* the path where a consumer setter override (e.g. `inviteSetupMfa`)
|
|
373
|
+
* pre-picks totp, leaving pickPhase's schema gate closed; the secret IS
|
|
374
|
+
* what confirm needs, so confirm guarantees it exists. For sms/email the
|
|
375
|
+
* unconfirmed method row + pincode are written by addressPhase, so there's
|
|
376
|
+
* nothing to provision here.
|
|
377
|
+
*/
|
|
378
|
+
protected enrollConfirmPhase(deps: MfaEnrollDeps): Promise<undefined>;
|
|
379
|
+
/**
|
|
380
|
+
* Send a pincode for the active sms/email enrollment method and stamp
|
|
381
|
+
* `ctx.pinSentTo` with the masked recipient. Shared by Phase 2 initial
|
|
382
|
+
* dispatch and Phase 3 `resend`. Caller is responsible for `mintPin` +
|
|
383
|
+
* stamping `enrollPincodeCooldown` BEFORE calling — this just dispatches.
|
|
384
|
+
* Not called for TOTP (no pincode to send).
|
|
385
|
+
*/
|
|
386
|
+
protected sendEnrollPincode(ctx: MfaEnrollCtx, deps: MfaEnrollDeps, address: string, code: string): Promise<void>;
|
|
387
|
+
/**
|
|
388
|
+
* Cleanup any partially-persisted enrollment state (unconfirmed method row +
|
|
389
|
+
* ctx scratch). Called when the user picks `skip` or `useDifferentMethod`
|
|
390
|
+
* mid-flow on Phase 3, where the unconfirmed method has already been written
|
|
391
|
+
* via `addMfaMethod` (Phase 1 for totp, Phase 2 for sms/email). On
|
|
392
|
+
* `useDifferentMethod` the caller relies on `enrollMethod` being cleared so
|
|
393
|
+
* the loop re-enters Phase 1.
|
|
394
|
+
*/
|
|
395
|
+
protected cleanupEnrollment(ctx: MfaEnrollCtx, users: UserService, username: string): Promise<void>;
|
|
396
|
+
}
|
|
397
|
+
//#endregion
|
|
398
|
+
//#region src/consent.store.d.ts
|
|
399
|
+
/**
|
|
400
|
+
* Descriptor for a single consent prompt. Customers' ConsentStore.getPendingConsents
|
|
401
|
+
* returns an array of these — the workflow transports them via `@wf.context.pass`
|
|
402
|
+
* to the SPA carrier form, which renders one row per descriptor through the
|
|
403
|
+
* `AsConsentArray` component (`@atscript/vue-aooth`). The user-submitted
|
|
404
|
+
* `consents: string[]` field on the carrier form posts back the SUBSET of
|
|
405
|
+
* `id`s the user accepted; the server-side `processInlineConsent` helper
|
|
406
|
+
* silently drops any ids that don't match a current descriptor (audit-grade
|
|
407
|
+
* "what user saw is what server records" invariant — preventing an attacker
|
|
408
|
+
* from forging audit rows for consents the user was never shown).
|
|
409
|
+
*/
|
|
410
|
+
interface ConsentDescriptor {
|
|
411
|
+
/** Stable identifier — committed to the user-submitted `consents: string[]`
|
|
412
|
+
* array and stamped on the persisted `ConsentEvent` (customer-defined:
|
|
413
|
+
* `'terms'`, `'marketing'`, `'jurisdiction-gdpr'`, …). */
|
|
414
|
+
id: string;
|
|
415
|
+
/** User-facing label / disclosure text. Markdown links are allowed; the
|
|
416
|
+
* SPA component will sanitize-render. */
|
|
417
|
+
text: string;
|
|
418
|
+
/**
|
|
419
|
+
* When present and non-empty, the consent is MANDATORY and this string IS
|
|
420
|
+
* the per-row error message surfaced by `AsConsentArray` (and the
|
|
421
|
+
* server-side `processInlineConsent` form-level error) when the user
|
|
422
|
+
* submits without ticking it. Absent or empty string ⇒ optional consent —
|
|
423
|
+
* a `ConsentEvent` with `accepted: false` is still persisted so the audit
|
|
424
|
+
* record proves the user was asked.
|
|
425
|
+
*/
|
|
426
|
+
required?: string;
|
|
427
|
+
/** Stamped onto persisted ConsentEvent for versioned policies (terms, ...). */
|
|
428
|
+
version?: string;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Injectable DI seam for consent persistence + the customer-defined consent
|
|
432
|
+
* universe. SINGLETON-scoped (one instance per app lifetime). All methods are
|
|
433
|
+
* no-op defaults — customers extend this class and replace via
|
|
434
|
+
* `createReplaceRegistry([ConsentStore, MyConsentStore])`.
|
|
435
|
+
*/
|
|
436
|
+
declare class ConsentStore {
|
|
437
|
+
/**
|
|
438
|
+
* Returns descriptors for consents this user still needs to accept on the
|
|
439
|
+
* next prompt boundary. Empty array → no consent step renders. The workflow
|
|
440
|
+
* passes its identity + (optionally) the channel it's about to use so the
|
|
441
|
+
* customer's impl can prompt different consent sets per workflow/channel.
|
|
442
|
+
*/
|
|
443
|
+
getPendingConsents(_username: string | undefined, _ctx: {
|
|
444
|
+
workflow: string;
|
|
445
|
+
channel?: "email" | "sms";
|
|
446
|
+
}): Promise<ConsentDescriptor[]>;
|
|
447
|
+
/**
|
|
448
|
+
* Persist a batch of captured consent events. Default: no-op. Override to
|
|
449
|
+
* write to your audit table / event store / whatever your legal team
|
|
450
|
+
* requires.
|
|
451
|
+
*/
|
|
452
|
+
save(_username: string, _events: ConsentEvent[]): Promise<void>;
|
|
453
|
+
/**
|
|
454
|
+
* Read consent history for a user, optionally filtered by event id.
|
|
455
|
+
* Used by getPendingConsents impls that want to compute "has the user
|
|
456
|
+
* already accepted version vN" without maintaining a separate index.
|
|
457
|
+
*/
|
|
458
|
+
read(_username: string, _filter?: {
|
|
459
|
+
id?: string;
|
|
460
|
+
}): Promise<ConsentEvent[]>;
|
|
461
|
+
/**
|
|
462
|
+
* Fired by LoginWorkflow's verify/:channel step AFTER pincode validates,
|
|
463
|
+
* i.e., AFTER channel ownership is confirmed. The disclosure text is the
|
|
464
|
+
* literal copy the user saw beneath the input field at ask/:channel time
|
|
465
|
+
* (resolveOtpDisclosure result) — passed through so the customer's record
|
|
466
|
+
* pins exactly what was shown to the user.
|
|
467
|
+
*
|
|
468
|
+
* Default: no-op. Disclosure-only is sufficient for transactional OTPs
|
|
469
|
+
* under TCPA/PECR/CASL/GDPR. Override if your jurisdiction or legal team
|
|
470
|
+
* requires affirmative consent capture for OTP channels.
|
|
471
|
+
*/
|
|
472
|
+
recordOtpChannelConsent(_username: string, _channel: "email" | "sms", _target: string, _disclosure: string): Promise<void>;
|
|
473
|
+
}
|
|
474
|
+
//#endregion
|
|
49
475
|
//#region src/auth.guard.d.ts
|
|
50
476
|
/**
|
|
51
477
|
* `GUARD`-priority interceptor factory that authenticates incoming requests.
|
|
@@ -76,7 +502,7 @@ declare function authGuardInterceptor(opts?: AuthOptions): TInterceptorDef;
|
|
|
76
502
|
*/
|
|
77
503
|
declare function AuthGuarded(opts?: AuthOptions): ClassDecorator & MethodDecorator;
|
|
78
504
|
//#endregion
|
|
79
|
-
//#region ../../node_modules/.pnpm/@wooksjs+event-wf@0.7.
|
|
505
|
+
//#region ../../node_modules/.pnpm/@wooksjs+event-wf@0.7.15_@prostojs+logger@0.4.3_@wooksjs+event-core@0.7.15_@wooksjs+eve_11afd4acf41dda7cd1e566c02621a2f8/node_modules/@wooksjs/event-wf/dist/index.d.ts
|
|
80
506
|
interface WfFinishedResponse {
|
|
81
507
|
type: 'redirect' | 'data';
|
|
82
508
|
/** Redirect URL or response body */
|
|
@@ -210,15 +636,15 @@ declare function getAuthMate(): AuthMate;
|
|
|
210
636
|
//#endregion
|
|
211
637
|
//#region src/auth.controller.d.ts
|
|
212
638
|
/** Workflows allowed by the bundled `/auth/trigger` endpoint. Subclasses override `triggerWf()` to extend. */
|
|
213
|
-
declare const DEFAULT_AUTH_WORKFLOWS: readonly ["auth
|
|
639
|
+
declare const DEFAULT_AUTH_WORKFLOWS: readonly ["auth/login/flow", "auth/recovery/flow", "auth/invite/start"];
|
|
214
640
|
/**
|
|
215
641
|
* Public REST endpoints for credential management. Four endpoints total:
|
|
216
642
|
*
|
|
217
643
|
* - `POST /auth/logout` — best-effort token revocation + cookie clear.
|
|
218
644
|
* - `POST /auth/refresh` — rotate access/refresh tokens.
|
|
219
645
|
* - `GET /auth/status` — return the current `AuthContext`.
|
|
220
|
-
* - `POST /auth/trigger` — single workflow trigger covering `auth
|
|
221
|
-
* `auth
|
|
646
|
+
* - `POST /auth/trigger` — single workflow trigger covering `auth/login/flow`,
|
|
647
|
+
* `auth/recovery/flow`, and `auth/invite/start`.
|
|
222
648
|
*
|
|
223
649
|
* The historical `/auth/login` and `/auth/password` endpoints were dropped —
|
|
224
650
|
* both flows go through the workflow trigger now (full MFA / SSO / etc.
|
|
@@ -230,11 +656,37 @@ declare const DEFAULT_AUTH_WORKFLOWS: readonly ["auth.login", "auth.recovery", "
|
|
|
230
656
|
*/
|
|
231
657
|
declare class AuthController {
|
|
232
658
|
protected readonly auth: AuthCredential;
|
|
233
|
-
|
|
659
|
+
protected readonly users?: UserService | undefined;
|
|
660
|
+
constructor(auth: AuthCredential, users?: UserService | undefined);
|
|
234
661
|
logout(body: AuthLogoutBody | undefined): Promise<AuthOkResponse>;
|
|
235
662
|
refresh(body: AuthRefreshBody | undefined): Promise<AuthLoginResponse>;
|
|
236
663
|
status(): AuthContext$1;
|
|
237
664
|
triggerWf(): void;
|
|
665
|
+
/**
|
|
666
|
+
* Side route mapping a redeemed-invite `uid` to the same idempotent
|
|
667
|
+
* envelope the `inviteIdempotentRedirect` workflow step renders. The SPA
|
|
668
|
+
* falls through to this when an invite magic-link is re-clicked after
|
|
669
|
+
* redemption: the wf state store has already evicted the finished state
|
|
670
|
+
* (returns 410) so the workflow can't re-enter `inviteCheckPendingInvitation`,
|
|
671
|
+
* but the user-id baked into the magic-link URL by `buildMagicLinkUrl(…, {
|
|
672
|
+
* userId })` lets the SPA resolve the "already accepted" condition itself.
|
|
673
|
+
*
|
|
674
|
+
* `@Public()` — invitees aren't signed in at this point.
|
|
675
|
+
*
|
|
676
|
+
* Defaults for `loginUrl` / `alreadyAcceptedRedirectUrl` mirror the bundled
|
|
677
|
+
* `InviteWorkflowOpts` defaults (`/login` / `/login`). Subclasses override
|
|
678
|
+
* `resolveInvitePostRedemption()` to read live workflow opts.
|
|
679
|
+
*/
|
|
680
|
+
invitePostRedemption(uid: string | undefined): Promise<WfFinished>;
|
|
681
|
+
/**
|
|
682
|
+
* URLs used by `invitePostRedemption`. Defaults mirror
|
|
683
|
+
* `mergeInviteOpts({})` so subclasses that customize either of those
|
|
684
|
+
* options can override here to keep the side route in sync.
|
|
685
|
+
*/
|
|
686
|
+
protected resolveInvitePostRedemption(): {
|
|
687
|
+
loginUrl: string;
|
|
688
|
+
alreadyAcceptedRedirectUrl: string;
|
|
689
|
+
};
|
|
238
690
|
}
|
|
239
691
|
//#endregion
|
|
240
692
|
//#region src/wf-trigger/decorator.d.ts
|
|
@@ -271,6 +723,11 @@ declare const WfTrigger: (opts?: WfTriggerOpts) => ClassDecorator & MethodDecora
|
|
|
271
723
|
* Uses `handleAsOutletRequest` (not `MoostWf.handleOutlet`) because the atscript
|
|
272
724
|
* wrapper restores the `finished: true` marker that `<AsWfForm>` keys off — the
|
|
273
725
|
* bare wooks request handler strips it during `useWfFinished()` unwrap.
|
|
726
|
+
*
|
|
727
|
+
* The trigger is a thin pass-through to `handleAsOutletRequest`: the new
|
|
728
|
+
* `@atscript/moost-wf` wire envelope is `{ wfs, input: { action?, formData? } }`,
|
|
729
|
+
* and the wf engine reads action + form data directly from `body.input`. No
|
|
730
|
+
* app-level bridging of `body.action` is needed.
|
|
274
731
|
*/
|
|
275
732
|
declare class WfTriggerProvider {
|
|
276
733
|
protected readonly wf: MoostWf;
|
|
@@ -298,7 +755,7 @@ interface AuditEvent {
|
|
|
298
755
|
kind: string;
|
|
299
756
|
/** Auth-scoped user identity (the `username` resolved by the workflow). */
|
|
300
757
|
userId?: string;
|
|
301
|
-
/** Workflow id that emitted the event (e.g. `auth
|
|
758
|
+
/** Workflow id that emitted the event (e.g. `auth/login/flow`). */
|
|
302
759
|
workflow?: string;
|
|
303
760
|
/** Source IP (when the workflow could resolve one). */
|
|
304
761
|
ip?: string;
|
|
@@ -313,7 +770,6 @@ interface AuditEmitter {
|
|
|
313
770
|
//#endregion
|
|
314
771
|
//#region src/workflows/login.workflow.options.d.ts
|
|
315
772
|
type LoginRedirect = "referer" | "home" | false | null;
|
|
316
|
-
type MfaTransport = "sms" | "email" | "totp";
|
|
317
773
|
interface SsoProvider {
|
|
318
774
|
id: string;
|
|
319
775
|
label: string;
|
|
@@ -324,60 +780,11 @@ interface ConcurrencyLimitOptions {
|
|
|
324
780
|
onLimit: "reject" | "kickPrompt";
|
|
325
781
|
}
|
|
326
782
|
interface LoginWorkflowOpts {
|
|
327
|
-
alternateCredentials?: {
|
|
328
|
-
forgotPassword?: boolean;
|
|
329
|
-
signup?: boolean;
|
|
330
|
-
magicLink?: boolean;
|
|
331
|
-
magicLinkSkipsMfa?: boolean;
|
|
332
|
-
magicLinkTtlMs?: number;
|
|
333
|
-
ssoProviders?: SsoProvider[];
|
|
334
|
-
recoveryUrl?: string;
|
|
335
|
-
signupUrl?: string;
|
|
336
|
-
embedRecovery?: boolean;
|
|
337
|
-
};
|
|
338
|
-
guards?: {
|
|
339
|
-
emailVerifiedRequired?: boolean;
|
|
340
|
-
passwordExpiry?: boolean;
|
|
341
|
-
passwordInitial?: boolean;
|
|
342
|
-
};
|
|
343
|
-
enrollment?: {
|
|
344
|
-
ensureEmail?: boolean;
|
|
345
|
-
ensurePhone?: boolean;
|
|
346
|
-
};
|
|
347
|
-
mfa?: {
|
|
348
|
-
enabled?: boolean;
|
|
349
|
-
transports?: MfaTransport[];
|
|
350
|
-
backupCodes?: boolean;
|
|
351
|
-
enrollRequired?: boolean;
|
|
352
|
-
pincodeTtlMs?: number;
|
|
353
|
-
pincodeResendTimeoutMs?: number; /** Numeric length of the server-generated OTP for SMS/email pincodes. */
|
|
354
|
-
pincodeLength?: number;
|
|
355
|
-
};
|
|
356
783
|
deviceTrust?: {
|
|
357
|
-
enabled?: boolean;
|
|
358
|
-
optIn?: boolean;
|
|
359
784
|
cookieName?: string;
|
|
360
785
|
ttlMs?: number;
|
|
361
|
-
skipsMfa?: boolean;
|
|
362
786
|
bindsTo?: "cookie" | "cookie+ip";
|
|
363
787
|
};
|
|
364
|
-
acceptance?: {
|
|
365
|
-
termsVersion?: string;
|
|
366
|
-
profileCompleteRequired?: boolean;
|
|
367
|
-
consentMarketing?: boolean;
|
|
368
|
-
};
|
|
369
|
-
multiContext?: {
|
|
370
|
-
tenantSelect?: boolean;
|
|
371
|
-
personaSelect?: boolean;
|
|
372
|
-
};
|
|
373
|
-
sessionPolicy?: {
|
|
374
|
-
concurrencyLimit?: ConcurrencyLimitOptions;
|
|
375
|
-
};
|
|
376
|
-
finalize?: {
|
|
377
|
-
auditLogin?: boolean;
|
|
378
|
-
notifyNewDevice?: boolean;
|
|
379
|
-
redirect?: LoginRedirect;
|
|
380
|
-
};
|
|
381
788
|
/**
|
|
382
789
|
* Replaceable form schemas. Each field defaults to the corresponding
|
|
383
790
|
* `.as` form shipped under `@aooth/auth-moost/atscript/models`; supply a
|
|
@@ -388,7 +795,9 @@ interface LoginWorkflowOpts {
|
|
|
388
795
|
askPhone?: TAtscriptAnnotatedType;
|
|
389
796
|
backupCode?: TAtscriptAnnotatedType;
|
|
390
797
|
concurrencyLimit?: TAtscriptAnnotatedType;
|
|
391
|
-
|
|
798
|
+
enrollAddress?: TAtscriptAnnotatedType;
|
|
799
|
+
enrollConfirm?: TAtscriptAnnotatedType;
|
|
800
|
+
enrollPickMethod?: TAtscriptAnnotatedType;
|
|
392
801
|
loginCredentials?: TAtscriptAnnotatedType;
|
|
393
802
|
mfaCode?: TAtscriptAnnotatedType;
|
|
394
803
|
personaSelect?: TAtscriptAnnotatedType;
|
|
@@ -397,78 +806,28 @@ interface LoginWorkflowOpts {
|
|
|
397
806
|
select2fa?: TAtscriptAnnotatedType;
|
|
398
807
|
setPassword?: TAtscriptAnnotatedType;
|
|
399
808
|
tenantSelect?: TAtscriptAnnotatedType;
|
|
400
|
-
|
|
809
|
+
termsBump?: TAtscriptAnnotatedType;
|
|
401
810
|
};
|
|
402
811
|
}
|
|
403
812
|
/**
|
|
404
813
|
* Fully-resolved view used by the workflow at runtime — every nested group is
|
|
405
|
-
* always populated by `mergeLoginOpts`, so
|
|
406
|
-
* `
|
|
407
|
-
*
|
|
408
|
-
* Fields without sensible defaults (e.g. `termsVersion`, `concurrencyLimit`)
|
|
409
|
-
* stay optional inside their group.
|
|
814
|
+
* always populated by `mergeLoginOpts`, so step bodies can read
|
|
815
|
+
* `this.opts.<group>.<flag>` directly without optional chaining.
|
|
410
816
|
*/
|
|
411
817
|
interface ResolvedLoginWorkflowOpts {
|
|
412
|
-
alternateCredentials: {
|
|
413
|
-
forgotPassword: boolean;
|
|
414
|
-
signup: boolean;
|
|
415
|
-
magicLink: boolean;
|
|
416
|
-
magicLinkSkipsMfa: boolean;
|
|
417
|
-
magicLinkTtlMs: number;
|
|
418
|
-
ssoProviders: SsoProvider[];
|
|
419
|
-
recoveryUrl: string;
|
|
420
|
-
signupUrl: string;
|
|
421
|
-
embedRecovery: boolean;
|
|
422
|
-
};
|
|
423
|
-
guards: {
|
|
424
|
-
emailVerifiedRequired: boolean;
|
|
425
|
-
passwordExpiry: boolean;
|
|
426
|
-
passwordInitial: boolean;
|
|
427
|
-
};
|
|
428
|
-
enrollment: {
|
|
429
|
-
ensureEmail: boolean;
|
|
430
|
-
ensurePhone: boolean;
|
|
431
|
-
};
|
|
432
|
-
mfa: {
|
|
433
|
-
enabled: boolean;
|
|
434
|
-
transports: MfaTransport[];
|
|
435
|
-
backupCodes: boolean;
|
|
436
|
-
enrollRequired: boolean;
|
|
437
|
-
pincodeTtlMs: number;
|
|
438
|
-
pincodeResendTimeoutMs: number;
|
|
439
|
-
pincodeLength: number;
|
|
440
|
-
};
|
|
441
818
|
deviceTrust: {
|
|
442
|
-
enabled: boolean;
|
|
443
|
-
optIn: boolean;
|
|
444
819
|
cookieName: string;
|
|
445
820
|
ttlMs: number;
|
|
446
|
-
skipsMfa: boolean;
|
|
447
821
|
bindsTo: "cookie" | "cookie+ip";
|
|
448
822
|
};
|
|
449
|
-
acceptance: {
|
|
450
|
-
termsVersion?: string;
|
|
451
|
-
profileCompleteRequired: boolean;
|
|
452
|
-
consentMarketing: boolean;
|
|
453
|
-
};
|
|
454
|
-
multiContext: {
|
|
455
|
-
tenantSelect: boolean;
|
|
456
|
-
personaSelect: boolean;
|
|
457
|
-
};
|
|
458
|
-
sessionPolicy: {
|
|
459
|
-
concurrencyLimit?: ConcurrencyLimitOptions;
|
|
460
|
-
};
|
|
461
|
-
finalize: {
|
|
462
|
-
auditLogin: boolean;
|
|
463
|
-
notifyNewDevice: boolean;
|
|
464
|
-
redirect: LoginRedirect;
|
|
465
|
-
};
|
|
466
823
|
forms: {
|
|
467
824
|
askEmail: TAtscriptAnnotatedType;
|
|
468
825
|
askPhone: TAtscriptAnnotatedType;
|
|
469
826
|
backupCode: TAtscriptAnnotatedType;
|
|
470
827
|
concurrencyLimit: TAtscriptAnnotatedType;
|
|
471
|
-
|
|
828
|
+
enrollAddress: TAtscriptAnnotatedType;
|
|
829
|
+
enrollConfirm: TAtscriptAnnotatedType;
|
|
830
|
+
enrollPickMethod: TAtscriptAnnotatedType;
|
|
472
831
|
loginCredentials: TAtscriptAnnotatedType;
|
|
473
832
|
mfaCode: TAtscriptAnnotatedType;
|
|
474
833
|
personaSelect: TAtscriptAnnotatedType;
|
|
@@ -477,7 +836,7 @@ interface ResolvedLoginWorkflowOpts {
|
|
|
477
836
|
select2fa: TAtscriptAnnotatedType;
|
|
478
837
|
setPassword: TAtscriptAnnotatedType;
|
|
479
838
|
tenantSelect: TAtscriptAnnotatedType;
|
|
480
|
-
|
|
839
|
+
termsBump: TAtscriptAnnotatedType;
|
|
481
840
|
};
|
|
482
841
|
}
|
|
483
842
|
/**
|
|
@@ -489,33 +848,168 @@ declare function mergeLoginOpts(opts?: LoginWorkflowOpts): ResolvedLoginWorkflow
|
|
|
489
848
|
//#endregion
|
|
490
849
|
//#region src/workflows/login.workflow.d.ts
|
|
491
850
|
interface LoginWfCtx {
|
|
492
|
-
|
|
851
|
+
/**
|
|
852
|
+
* Whether the user must complete profile fields BEFORE token issuance.
|
|
853
|
+
* Populated by `prepare-profile` from `resolveProfile(ctx).required`.
|
|
854
|
+
* Default-false matches the prior behavior (most consumers don't gate logins
|
|
855
|
+
* on profile completion). Read by the `profile-complete` schema condition.
|
|
856
|
+
*/
|
|
857
|
+
profileCompleteRequired?: boolean;
|
|
858
|
+
alternateCredentials?: {
|
|
859
|
+
forgotPassword: boolean;
|
|
860
|
+
signup: boolean;
|
|
861
|
+
magicLink: boolean;
|
|
862
|
+
magicLinkSkipsMfa: boolean;
|
|
863
|
+
ssoProviders: SsoProvider[];
|
|
864
|
+
recoveryUrl: string;
|
|
865
|
+
signupUrl: string;
|
|
866
|
+
embedRecovery: boolean;
|
|
867
|
+
};
|
|
868
|
+
deviceTrust?: {
|
|
869
|
+
enabled: boolean;
|
|
870
|
+
optIn: boolean;
|
|
871
|
+
skipsMfa: boolean;
|
|
872
|
+
};
|
|
873
|
+
enrollment?: {
|
|
874
|
+
ensureEmail: boolean;
|
|
875
|
+
ensurePhone: boolean;
|
|
876
|
+
};
|
|
877
|
+
finalize?: {
|
|
878
|
+
auditLogin: boolean;
|
|
879
|
+
notifyNewDevice: boolean;
|
|
880
|
+
redirect: LoginRedirect;
|
|
881
|
+
};
|
|
882
|
+
guards?: {
|
|
883
|
+
passwordInitial: boolean;
|
|
884
|
+
passwordExpiry: boolean;
|
|
885
|
+
emailVerifiedRequired: boolean;
|
|
886
|
+
};
|
|
887
|
+
mfaConfig?: {
|
|
888
|
+
backupCodes: boolean;
|
|
889
|
+
};
|
|
890
|
+
multiContext?: {
|
|
891
|
+
tenantSelect: boolean;
|
|
892
|
+
personaSelect: boolean;
|
|
893
|
+
};
|
|
894
|
+
sessionPolicy?: {
|
|
895
|
+
concurrencyLimit?: ConcurrencyLimitOptions;
|
|
896
|
+
};
|
|
493
897
|
username?: string;
|
|
494
898
|
/** Legacy alias for `pwReset`; kept until tests migrate. */
|
|
495
899
|
mfaRequired?: boolean;
|
|
496
900
|
isPasswordInitial?: boolean;
|
|
497
901
|
usedMagicLink?: boolean;
|
|
902
|
+
/**
|
|
903
|
+
* 3-state MFA policy:
|
|
904
|
+
* - `'required'` — MFA enforced; users with 0 methods MUST enroll (no skip).
|
|
905
|
+
* - `'optional'` — MFA prompted; users with 0 methods see an enrollment
|
|
906
|
+
* form that offers a `skip` action (in-flight opt-out).
|
|
907
|
+
* - `'disabled'` — MFA loops never fire; Phase 4 is skipped entirely.
|
|
908
|
+
*/
|
|
909
|
+
mfaMode?: "required" | "optional" | "disabled";
|
|
910
|
+
availableMfaTransports?: MfaTransport[];
|
|
911
|
+
/** Pre-selected MFA transport (e.g. existing-user default, single-transport auto-pick). */
|
|
912
|
+
currentMfa?: MfaTransport;
|
|
498
913
|
email?: string;
|
|
499
914
|
emailConfirmed?: boolean;
|
|
500
915
|
phone?: string;
|
|
501
916
|
phoneConfirmed?: boolean;
|
|
917
|
+
/**
|
|
918
|
+
* Disclosure text rendered beneath the channel input field on
|
|
919
|
+
* `AskEmailForm` / `AskPhoneForm` at ask-time (BEFORE the user submits
|
|
920
|
+
* their email/phone — typing + submitting constitutes implied consent).
|
|
921
|
+
* Forwarded to `consentStore.recordOtpChannelConsent` at `verify/:channel`
|
|
922
|
+
* AFTER the channel is confirmed as an MFA method, so the persisted record
|
|
923
|
+
* pins the literal copy the user actually saw. The disclosure text itself
|
|
924
|
+
* is GENERIC per-channel (no target templated in) — the verified target
|
|
925
|
+
* is captured as a separate audit-record field.
|
|
926
|
+
*/
|
|
927
|
+
otpDisclosure?: string;
|
|
502
928
|
mfaEnrolledMethods?: MfaSummary[];
|
|
503
929
|
mfaMethod?: "sms" | "email" | "totp";
|
|
504
930
|
mfaSaveAsDefault?: boolean;
|
|
505
931
|
ignoreMfaDefault?: boolean;
|
|
506
932
|
mfaChecked?: boolean;
|
|
933
|
+
/**
|
|
934
|
+
* Set true the first time the user picks `useBackupCode` on the MFA step,
|
|
935
|
+
* so the workflow remembers to route the subsequent `BackupCodeForm`
|
|
936
|
+
* submission (which carries no `action`) through `handleBackupCode` instead
|
|
937
|
+
* of falling through to `verifyMfa` / pincode-verify.
|
|
938
|
+
*/
|
|
939
|
+
usingBackupCode?: boolean;
|
|
507
940
|
/** Counter incremented by the `risk-step-up` step so MFA reruns for the extra factor. */
|
|
508
941
|
mfaRunsRemaining?: number;
|
|
942
|
+
/** Mirror of `mfaEnrolledMethods.length`. Passed to client forms via `@wf.context.pass` so action buttons (`useDifferentMethod`) can hide when only one method exists. */
|
|
943
|
+
mfaMethodCount?: number;
|
|
944
|
+
/** Mirror of `opts.mfa.backupCodes`. Passed to client forms so `useBackupCode` can hide when backup codes are disabled. */
|
|
945
|
+
mfaBackupCodes?: boolean;
|
|
946
|
+
altForgotPassword?: boolean;
|
|
947
|
+
altSignup?: boolean;
|
|
948
|
+
altMagicLink?: boolean;
|
|
949
|
+
enrollMethod?: MfaTransport;
|
|
950
|
+
enrollAddress?: string;
|
|
951
|
+
/** TOTP secret in flight (passed to UI via `@wf.context.pass` for QR rendering). */
|
|
952
|
+
enrollSecret?: string;
|
|
953
|
+
/** Provisioning URI for TOTP QR rendering. */
|
|
954
|
+
enrollUri?: string;
|
|
955
|
+
/** Mirror of `ctx.availableMfaTransports`, surfaced to `EnrollPickMethodForm` via `@wf.context.pass`. */
|
|
956
|
+
enrollAvailableTransports?: MfaTransport[];
|
|
957
|
+
/**
|
|
958
|
+
* Mirror of `ctx.mfaMode` (only set when not `'disabled'`). Surfaced to
|
|
959
|
+
* `EnrollPickMethodForm` via `@wf.context.pass` so the `skip` action can
|
|
960
|
+
* hide unless mode is `'optional'`.
|
|
961
|
+
*/
|
|
962
|
+
enrollMode?: "required" | "optional";
|
|
963
|
+
/** Set true by `enrollConfirmPhase` (or `enrollPickPhase`/`enrollAddressPhase` on `skip` in `'optional'` mode); mirrored to `mfaChecked` via `buildLoginEnrollDeps` `onComplete`. */
|
|
964
|
+
enrollDone?: boolean;
|
|
965
|
+
/** Phase 3 confirm-pincode resend cooldown (sms/email). See `MfaEnrollCtx.enrollPincodeCooldown`. */
|
|
966
|
+
enrollPincodeCooldown?: number;
|
|
509
967
|
pin?: string;
|
|
510
968
|
pinExpire?: number;
|
|
511
969
|
pinTimeout?: number;
|
|
512
970
|
pinSentTo?: string;
|
|
971
|
+
/**
|
|
972
|
+
* Per-method "next-allowed-send-at" timestamp. Written by
|
|
973
|
+
* `pincode-send-login` after each send and consulted by `select2fa` to
|
|
974
|
+
* reject re-picking a method while it's still in cooldown. Closes the
|
|
975
|
+
* `useDifferentMethod → same method → fresh SMS` abuse loop: without this
|
|
976
|
+
* an attacker (or an impatient user) can spam SMS/email by alternating
|
|
977
|
+
* methods. Persists across `delete ctx.pin` (resend/useDifferentMethod)
|
|
978
|
+
* so the throttle survives a method switch.
|
|
979
|
+
*/
|
|
980
|
+
pincodeCooldowns?: {
|
|
981
|
+
sms?: number;
|
|
982
|
+
email?: number;
|
|
983
|
+
};
|
|
513
984
|
deviceTrustToken?: string;
|
|
514
985
|
/** Set true at MFA gate when no trust cookie matched → trigger `notify-new-device`. */
|
|
515
986
|
newDevice?: boolean;
|
|
516
987
|
/** Captured from the OTP/pincode form when `opts.deviceTrust.optIn`. */
|
|
517
988
|
rememberDevice?: boolean;
|
|
518
|
-
|
|
989
|
+
/** Mirror of `opts.deviceTrust.optIn`. Passed to `PincodeForm` so the `rememberDevice` checkbox can hide when the consumer's device-trust is off-by-default (no user choice to make). */
|
|
990
|
+
deviceTrustOptIn?: boolean;
|
|
991
|
+
/**
|
|
992
|
+
* Descriptors for the customer-defined general consents (terms, marketing,
|
|
993
|
+
* jurisdiction, ...) the user still needs to accept. Populated once by
|
|
994
|
+
* `prepare-consents` after username-bind. Phase 5 will migrate carrier
|
|
995
|
+
* forms to consume this array; Phase 4 populates transport only.
|
|
996
|
+
*/
|
|
997
|
+
pendingConsents?: ConsentDescriptor[];
|
|
998
|
+
/**
|
|
999
|
+
* Subset of `pendingConsents[].id` the user ticked on the carrier form —
|
|
1000
|
+
* set by `processInlineConsent` after silent-dropping unknown ids.
|
|
1001
|
+
* Consumed by `persist-consents` to compute `accepted: boolean` per
|
|
1002
|
+
* pending descriptor.
|
|
1003
|
+
*/
|
|
1004
|
+
acceptedConsentIds?: string[];
|
|
1005
|
+
/**
|
|
1006
|
+
* Wall-clock ms at the moment `processInlineConsent` resolved the
|
|
1007
|
+
* `consents` carrier-form submission (NOT at write-time — captured BEFORE
|
|
1008
|
+
* the batched `persist-consents` step so a paused-workflow resume gap
|
|
1009
|
+
* doesn't drift the timestamp). Also the schema-gate for the
|
|
1010
|
+
* `persist-consents` step — set ⇒ a carrier form has collected consents.
|
|
1011
|
+
*/
|
|
1012
|
+
consentsDecidedAt?: number;
|
|
519
1013
|
profileMissingFields?: string[];
|
|
520
1014
|
availableTenants?: Array<{
|
|
521
1015
|
id: string;
|
|
@@ -530,9 +1024,15 @@ interface LoginWfCtx {
|
|
|
530
1024
|
riskStepUpReason?: string;
|
|
531
1025
|
activeSessions?: number;
|
|
532
1026
|
passwordChanged?: boolean;
|
|
533
|
-
termsAcceptedDone?: boolean;
|
|
534
1027
|
profileApplied?: boolean;
|
|
535
|
-
|
|
1028
|
+
/**
|
|
1029
|
+
* Set true by `persist-consents` after the batched `consentStore.save`
|
|
1030
|
+
* call fires (or after the step short-circuits with no pending consents).
|
|
1031
|
+
* Gates `processInlineConsent` from re-staging on subsequent carrier
|
|
1032
|
+
* forms in the same workflow run, and the `persist-consents` schema
|
|
1033
|
+
* condition from re-firing.
|
|
1034
|
+
*/
|
|
1035
|
+
consentsPersisted?: boolean;
|
|
536
1036
|
tokensIssued?: boolean;
|
|
537
1037
|
redirectUrl?: string;
|
|
538
1038
|
/**
|
|
@@ -544,6 +1044,32 @@ interface LoginWfCtx {
|
|
|
544
1044
|
/** Tracks whether `risk-step-up` has already been evaluated this iteration. */
|
|
545
1045
|
riskStepUpEvaluated?: boolean;
|
|
546
1046
|
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Per-group policy override shape consumed by `resolveXxx(ctx)` subclass
|
|
1049
|
+
* overrides. Mirrors the `ctx.<group>` fields that the `prepare-<group>`
|
|
1050
|
+
* @Step methods populate — one entry per resolver. Library users typically
|
|
1051
|
+
* accept a payload of this shape on their `LoginWorkflow` subclass ctor /
|
|
1052
|
+
* test harness and have each `resolveXxx` return its matching key (falling
|
|
1053
|
+
* back to `super.resolveXxx(ctx)` for unset groups).
|
|
1054
|
+
*/
|
|
1055
|
+
interface LoginPolicyOverrides {
|
|
1056
|
+
/**
|
|
1057
|
+
* Override the profile-completion policy (`{ required: boolean }`) — the
|
|
1058
|
+
* boolean is mirrored onto `ctx.profileCompleteRequired` by `prepare-profile`
|
|
1059
|
+
* and read by the `profile-complete` schema condition.
|
|
1060
|
+
*/
|
|
1061
|
+
profile?: {
|
|
1062
|
+
required: boolean;
|
|
1063
|
+
};
|
|
1064
|
+
alternateCredentials?: NonNullable<LoginWfCtx["alternateCredentials"]>;
|
|
1065
|
+
deviceTrust?: NonNullable<LoginWfCtx["deviceTrust"]>;
|
|
1066
|
+
enrollment?: NonNullable<LoginWfCtx["enrollment"]>;
|
|
1067
|
+
finalize?: NonNullable<LoginWfCtx["finalize"]>;
|
|
1068
|
+
guards?: NonNullable<LoginWfCtx["guards"]>;
|
|
1069
|
+
mfaConfig?: NonNullable<LoginWfCtx["mfaConfig"]>;
|
|
1070
|
+
multiContext?: NonNullable<LoginWfCtx["multiContext"]>;
|
|
1071
|
+
sessionPolicy?: NonNullable<LoginWfCtx["sessionPolicy"]>;
|
|
1072
|
+
}
|
|
547
1073
|
interface MfaSummary {
|
|
548
1074
|
kind: "sms" | "email" | "totp";
|
|
549
1075
|
/** Underlying `MfaMethod.name` so the workflow can call into UserService. */
|
|
@@ -583,14 +1109,16 @@ interface DeliverSms {
|
|
|
583
1109
|
userId?: string;
|
|
584
1110
|
}
|
|
585
1111
|
type DeliverPayload = DeliverEmail | DeliverSms;
|
|
586
|
-
declare class LoginWorkflow {
|
|
1112
|
+
declare class LoginWorkflow extends AuthWorkflowBase {
|
|
587
1113
|
protected readonly opts: ResolvedLoginWorkflowOpts;
|
|
588
1114
|
protected readonly users: UserService;
|
|
589
1115
|
protected readonly auth: AuthCredential;
|
|
590
|
-
|
|
1116
|
+
protected readonly authOpts: AuthOpts;
|
|
1117
|
+
protected readonly consentStore: ConsentStore;
|
|
1118
|
+
constructor(opts: LoginWorkflowOpts, users: UserService, auth: AuthCredential, authOpts: AuthOpts, consentStore: ConsentStore);
|
|
591
1119
|
/**
|
|
592
1120
|
* Dispatch an email or SMS event. Default throws — consumers MUST override
|
|
593
|
-
* if any feature that emits is enabled (MFA pincode,
|
|
1121
|
+
* if any feature that emits is enabled (MFA pincode, ask/verify channel OTP,
|
|
594
1122
|
* notifyNewDevice). The throw surfaces at the HTTP layer as 500 on the
|
|
595
1123
|
* first event that triggers a send, which is the fail-loud signal.
|
|
596
1124
|
*/
|
|
@@ -630,62 +1158,197 @@ declare class LoginWorkflow {
|
|
|
630
1158
|
* `storeTrustedDevice` against Redis but keep this default.
|
|
631
1159
|
*/
|
|
632
1160
|
protected issueTrustedDevice(userId: string, ip: string | undefined, ttlMs: number): Promise<TrustedDeviceRecord>;
|
|
1161
|
+
/**
|
|
1162
|
+
* Resolve the profile-completion policy. Returns `{ required: boolean }` —
|
|
1163
|
+
* whether the user must complete profile fields (e.g. `firstName` /
|
|
1164
|
+
* `lastName`) BEFORE token issuance. Override per-tenant or per-user to
|
|
1165
|
+
* gate logins on missing profile fields; the boolean is mirrored onto
|
|
1166
|
+
* `ctx.profileCompleteRequired` by `prepare-profile` and read by the
|
|
1167
|
+
* `profile-complete` schema condition (which AND-s the gate with
|
|
1168
|
+
* `ctx.profileMissingFields.length > 0` so the step only fires when the
|
|
1169
|
+
* consumer has surfaced fields to collect — typically populated by a
|
|
1170
|
+
* `credentials` override that hydrates `ctx.profileMissingFields` from
|
|
1171
|
+
* the user row).
|
|
1172
|
+
*
|
|
1173
|
+
* Default-false matches the prior behavior — most consumers don't gate
|
|
1174
|
+
* logins on profile completion. Async-friendly via the union return type —
|
|
1175
|
+
* sync defaults stay sync (engine fast path).
|
|
1176
|
+
*/
|
|
1177
|
+
protected resolveProfile(_ctx: LoginWfCtx): {
|
|
1178
|
+
required: boolean;
|
|
1179
|
+
} | Promise<{
|
|
1180
|
+
required: boolean;
|
|
1181
|
+
}>;
|
|
1182
|
+
/**
|
|
1183
|
+
* Resolve the alternate-credentials policy (forgot-password / signup /
|
|
1184
|
+
* magic-link / SSO providers + their URLs). Override to enable/disable per
|
|
1185
|
+
* tenant. Sync default; async overrides supported.
|
|
1186
|
+
*/
|
|
1187
|
+
protected resolveAlternateCredentials(_ctx: LoginWfCtx): NonNullable<LoginWfCtx["alternateCredentials"]> | Promise<NonNullable<LoginWfCtx["alternateCredentials"]>>;
|
|
1188
|
+
/**
|
|
1189
|
+
* Resolve the device-trust policy (enabled / opt-in / skipsMfa). Infrastructure
|
|
1190
|
+
* (cookieName / ttlMs / bindsTo) still lives on `this.opts.deviceTrust` since
|
|
1191
|
+
* those are app-wide constants, not per-request policy. Sync/async friendly.
|
|
1192
|
+
*/
|
|
1193
|
+
protected resolveDeviceTrust(_ctx: LoginWfCtx): NonNullable<LoginWfCtx["deviceTrust"]> | Promise<NonNullable<LoginWfCtx["deviceTrust"]>>;
|
|
1194
|
+
/**
|
|
1195
|
+
* Resolve the channel-enrollment policy (ensureEmail / ensurePhone gates).
|
|
1196
|
+
* Override to force email/phone capture per user segment. Sync/async friendly.
|
|
1197
|
+
*/
|
|
1198
|
+
protected resolveEnrollment(_ctx: LoginWfCtx): NonNullable<LoginWfCtx["enrollment"]> | Promise<NonNullable<LoginWfCtx["enrollment"]>>;
|
|
1199
|
+
/**
|
|
1200
|
+
* Resolve the finalize policy (audit emission / new-device notification /
|
|
1201
|
+
* redirect target). Override to drive per-tenant audit-log routing or
|
|
1202
|
+
* per-app redirect targets. Sync/async friendly.
|
|
1203
|
+
*/
|
|
1204
|
+
protected resolveFinalize(_ctx: LoginWfCtx): NonNullable<LoginWfCtx["finalize"]> | Promise<NonNullable<LoginWfCtx["finalize"]>>;
|
|
1205
|
+
/**
|
|
1206
|
+
* Resolve the guards policy (passwordInitial / passwordExpiry /
|
|
1207
|
+
* emailVerifiedRequired). Override per-tenant to tighten or loosen the
|
|
1208
|
+
* post-credentials gates. Sync/async friendly.
|
|
1209
|
+
*/
|
|
1210
|
+
protected resolveGuards(_ctx: LoginWfCtx): NonNullable<LoginWfCtx["guards"]> | Promise<NonNullable<LoginWfCtx["guards"]>>;
|
|
1211
|
+
/**
|
|
1212
|
+
* Resolve the MFA config (currently only backup-codes availability — pincode
|
|
1213
|
+
* timings stay on `this.opts.mfa` as infrastructure). Override to enable or
|
|
1214
|
+
* disable backup-code redemption per tenant. Sync/async friendly.
|
|
1215
|
+
*/
|
|
1216
|
+
protected resolveMfaConfig(_ctx: LoginWfCtx): NonNullable<LoginWfCtx["mfaConfig"]> | Promise<NonNullable<LoginWfCtx["mfaConfig"]>>;
|
|
1217
|
+
/**
|
|
1218
|
+
* Resolve the multi-context policy (tenantSelect / personaSelect prompts).
|
|
1219
|
+
* Override to require a tenant/persona pick when the user has more than one.
|
|
1220
|
+
* Sync/async friendly.
|
|
1221
|
+
*/
|
|
1222
|
+
protected resolveMultiContext(_ctx: LoginWfCtx): NonNullable<LoginWfCtx["multiContext"]> | Promise<NonNullable<LoginWfCtx["multiContext"]>>;
|
|
1223
|
+
/**
|
|
1224
|
+
* Resolve the disclosure text rendered beneath the channel input field on
|
|
1225
|
+
* `AskEmailForm` / `AskPhoneForm` at ask-time — BEFORE the user submits
|
|
1226
|
+
* their email/phone. Default returns a TCPA / PECR / CASL / GDPR-safe
|
|
1227
|
+
* English paragraph that is GENERIC per channel (no target templated in,
|
|
1228
|
+
* since the user hasn't submitted it yet). Override per-tenant or per-
|
|
1229
|
+
* locale to swap copy (e.g. i18n catalog lookup). The resolved string is
|
|
1230
|
+
* mirrored onto `ctx.otpDisclosure`, transported to the SPA via
|
|
1231
|
+
* `@wf.context.pass`, and forwarded to
|
|
1232
|
+
* `consentStore.recordOtpChannelConsent` at `verify/:channel` AFTER the
|
|
1233
|
+
* pincode validates AND the channel is confirmed as an MFA method — the
|
|
1234
|
+
* persisted audit record pins BOTH the literal disclosure copy the user
|
|
1235
|
+
* saw AND the verified target as a separate field.
|
|
1236
|
+
*
|
|
1237
|
+
* Disclosure-only is sufficient for transactional security codes by default;
|
|
1238
|
+
* customers wanting affirmative consent capture override
|
|
1239
|
+
* `ConsentStore.recordOtpChannelConsent` instead. Sync/async friendly.
|
|
1240
|
+
*/
|
|
1241
|
+
protected resolveOtpDisclosure(_ctx: LoginWfCtx, channel: "email" | "phone"): string | Promise<string>;
|
|
1242
|
+
/**
|
|
1243
|
+
* Resolve the session policy (concurrency limit). Override to enforce a
|
|
1244
|
+
* per-tenant or per-user max-concurrent-sessions cap with reject / kickPrompt
|
|
1245
|
+
* behaviour. Sync/async friendly.
|
|
1246
|
+
*/
|
|
1247
|
+
protected resolveSessionPolicy(_ctx: LoginWfCtx): NonNullable<LoginWfCtx["sessionPolicy"]> | Promise<NonNullable<LoginWfCtx["sessionPolicy"]>>;
|
|
1248
|
+
/**
|
|
1249
|
+
* Call `resolveProfile(ctx)` and mirror `result.required` onto
|
|
1250
|
+
* `ctx.profileCompleteRequired`. Promise-branched body preserves the engine's
|
|
1251
|
+
* sync fast path: a sync `resolveProfile` override skips the microtask
|
|
1252
|
+
* allocation, while an `async` override is awaited via `.then` before the
|
|
1253
|
+
* `profile-complete` schema condition reads the boolean. The resolved POJO
|
|
1254
|
+
* is intentionally NOT stashed on ctx as a group — `profileCompleteRequired`
|
|
1255
|
+
* is the only field, so a top-level boolean keeps the ctx shape flat.
|
|
1256
|
+
*/
|
|
1257
|
+
prepareProfile(ctx: LoginWfCtx): undefined | Promise<undefined>;
|
|
1258
|
+
prepareAlternateCredentials(ctx: LoginWfCtx): undefined | Promise<undefined>;
|
|
1259
|
+
prepareDeviceTrust(ctx: LoginWfCtx): undefined | Promise<undefined>;
|
|
1260
|
+
prepareEnrollment(ctx: LoginWfCtx): undefined | Promise<undefined>;
|
|
1261
|
+
prepareFinalize(ctx: LoginWfCtx): undefined | Promise<undefined>;
|
|
1262
|
+
prepareGuards(ctx: LoginWfCtx): undefined | Promise<undefined>;
|
|
1263
|
+
prepareMfaConfig(ctx: LoginWfCtx): undefined | Promise<undefined>;
|
|
1264
|
+
prepareMultiContext(ctx: LoginWfCtx): undefined | Promise<undefined>;
|
|
1265
|
+
prepareSessionPolicy(ctx: LoginWfCtx): undefined | Promise<undefined>;
|
|
1266
|
+
/**
|
|
1267
|
+
* Populate `ctx.pendingConsents` with the customer-defined general-consent
|
|
1268
|
+
* descriptors (terms, marketing, jurisdiction, ...) the user still needs to
|
|
1269
|
+
* accept. Phase 4 transport only — nothing reads `ctx.pendingConsents` yet;
|
|
1270
|
+
* Phase 5 will migrate carrier forms (`SetPasswordForm`, `ProfileCompleteForm`,
|
|
1271
|
+
* ...) from the `WithInlineConsentForm` static-checkbox mixin onto this
|
|
1272
|
+
* dynamic array.
|
|
1273
|
+
*
|
|
1274
|
+
* Username MUST be bound before we fetch consents — customer impls key
|
|
1275
|
+
* history by user, and pre-bind there's no identity to dedup against. The
|
|
1276
|
+
* schema places this step AFTER the `!ctx.username` break gate, so the
|
|
1277
|
+
* `if (!ctx.username)` guard is belt-and-brace for future refactors that
|
|
1278
|
+
* might re-order the schema.
|
|
1279
|
+
*/
|
|
1280
|
+
prepareConsents(ctx: LoginWfCtx): undefined | Promise<undefined>;
|
|
633
1281
|
flow(): void;
|
|
634
|
-
init(ctx: LoginWfCtx): undefined;
|
|
635
1282
|
/**
|
|
636
|
-
*
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
*
|
|
1283
|
+
* First step of the workflow; remains as a no-op override hook for
|
|
1284
|
+
* consumers (e.g. seeding pre-flight ctx fields, capturing request metadata).
|
|
1285
|
+
* The pre-PR policy-pojo-on-ctx stash was dropped — policy now lives on
|
|
1286
|
+
* `ctx.<group>` populated by the dedicated `prepare-<group>` steps.
|
|
640
1287
|
*
|
|
641
|
-
*
|
|
642
|
-
*
|
|
1288
|
+
* Return type is `undefined | Promise<undefined>` so consumers can override
|
|
1289
|
+
* with `async init(...)` without the default fast-path paying a Promise
|
|
1290
|
+
* allocation (the wf engine awaits only when the return value is a Promise).
|
|
643
1291
|
*/
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
1292
|
+
init(_ctx: LoginWfCtx): undefined | Promise<undefined>;
|
|
1293
|
+
/**
|
|
1294
|
+
* Prepare MFA setup: writes `ctx.mfaMode`, `ctx.availableMfaTransports`, and
|
|
1295
|
+
* (when the user is resolvable) pre-selects `ctx.currentMfa` from the
|
|
1296
|
+
* existing-user `defaultMethod` or the single-available-transport auto-pick.
|
|
1297
|
+
* Override to compute any of the three from tenant policy / user attrs /
|
|
1298
|
+
* request context. Return type allows a sync override (skip the promise
|
|
1299
|
+
* round-trip) when no async work is needed — the default body is async only
|
|
1300
|
+
* because of the `users.getUser` lookup for `currentMfa`.
|
|
1301
|
+
*/
|
|
1302
|
+
prepareMfaSetup(ctx: LoginWfCtx): undefined | Promise<undefined>;
|
|
1303
|
+
credentials(ctx: LoginWfCtx): Promise<unknown>;
|
|
650
1304
|
private handleCredentialsAlt;
|
|
651
1305
|
/**
|
|
652
|
-
*
|
|
1306
|
+
* Resolves the redirect URL the `forgotPassword` alt-action navigates to.
|
|
653
1307
|
* Receives whatever the user typed into the username field so the recovery
|
|
654
1308
|
* page can pre-fill it. Default:
|
|
655
1309
|
* `${alternateCredentials.recoveryUrl}?username=${encodeURIComponent(username ?? '')}`.
|
|
1310
|
+
* Sync return type only — the caller (`credentials` step's alt-action
|
|
1311
|
+
* handler) uses the URL inline; consumers needing async URL construction
|
|
1312
|
+
* should override the `credentials` @Step instead. The resolved
|
|
1313
|
+
* `alternateCredentials` policy is supplied by the caller so the base impl
|
|
1314
|
+
* doesn't have to re-call `resolveAlternateCredentials`.
|
|
656
1315
|
*/
|
|
657
|
-
protected
|
|
658
|
-
magicLinkRequest():
|
|
659
|
-
magicLinkSend():
|
|
660
|
-
magicLinkVerified():
|
|
661
|
-
passkey():
|
|
662
|
-
ssoCallback():
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
code?: string;
|
|
666
|
-
} | undefined, ctx: LoginWfCtx): Promise<unknown>;
|
|
667
|
-
ensurePhone(input: {
|
|
668
|
-
phone?: string;
|
|
669
|
-
code?: string;
|
|
670
|
-
} | undefined, ctx: LoginWfCtx): Promise<unknown>;
|
|
1316
|
+
protected resolveRecoveryUrl(username: string | undefined, alt: NonNullable<LoginWfCtx["alternateCredentials"]>): string;
|
|
1317
|
+
magicLinkRequest(): void | Promise<void>;
|
|
1318
|
+
magicLinkSend(): void | Promise<void>;
|
|
1319
|
+
magicLinkVerified(): void | Promise<void>;
|
|
1320
|
+
passkey(): void | Promise<void>;
|
|
1321
|
+
ssoCallback(): void | Promise<void>;
|
|
1322
|
+
ask(ctx: LoginWfCtx, channel: "email" | "phone"): Promise<unknown>;
|
|
1323
|
+
verify(ctx: LoginWfCtx, channel: "email" | "phone"): Promise<unknown>;
|
|
671
1324
|
checkTrustedDevice(ctx: LoginWfCtx): Promise<undefined>;
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
1325
|
+
/**
|
|
1326
|
+
* Load + summarise the user's enrolled MFA methods (filtered against
|
|
1327
|
+
* `ctx.availableMfaTransports`) and mirror the form-gating flags
|
|
1328
|
+
* (`mfaMethodCount`, `mfaBackupCodes`, `deviceTrustOptIn`) onto ctx. Pure
|
|
1329
|
+
* data-load — no selection decision. Split out of the old
|
|
1330
|
+
* `prepare-mfa-options` step so consumers can override the load/summary
|
|
1331
|
+
* shape (custom MFA inventory source) without copying the selection
|
|
1332
|
+
* heuristics in `selectMfaMethod`.
|
|
1333
|
+
*/
|
|
1334
|
+
loadEnrolledMfaMethods(ctx: LoginWfCtx): Promise<undefined>;
|
|
1335
|
+
/**
|
|
1336
|
+
* Pick which MFA method to use from the already-loaded
|
|
1337
|
+
* `ctx.mfaEnrolledMethods` summary. Decision-only — no IO. Honors
|
|
1338
|
+
* `ctx.currentMfa` (pre-selected by `prepareMfaSetup` from the user's
|
|
1339
|
+
* `defaultMethod` or single-transport auto-pick), auto-picks when only one
|
|
1340
|
+
* method is enrolled, falls back to the `isDefault` method. All paths are
|
|
1341
|
+
* gated on `!ctx.ignoreMfaDefault` so the `useDifferentMethod` re-pick flow
|
|
1342
|
+
* (which sets the flag) skips straight to the `select2fa` picker. Split out
|
|
1343
|
+
* of the old `prepare-mfa-options` step so consumers can override selection
|
|
1344
|
+
* heuristics (e.g. risk-based per-tenant defaults) without re-implementing
|
|
1345
|
+
* the load/summary.
|
|
1346
|
+
*/
|
|
1347
|
+
selectMfaMethod(ctx: LoginWfCtx): undefined | Promise<undefined>;
|
|
1348
|
+
select2fa(ctx: LoginWfCtx): Promise<unknown>;
|
|
678
1349
|
pincodeSendLogin(ctx: LoginWfCtx): Promise<undefined>;
|
|
679
|
-
pincodeCheckLogin(
|
|
680
|
-
|
|
681
|
-
action?: string;
|
|
682
|
-
rememberDevice?: boolean;
|
|
683
|
-
} | undefined, ctx: LoginWfCtx): Promise<unknown>;
|
|
684
|
-
mfaTotp(input: {
|
|
685
|
-
code?: string;
|
|
686
|
-
action?: string;
|
|
687
|
-
rememberDevice?: boolean;
|
|
688
|
-
} | undefined, ctx: LoginWfCtx): Promise<unknown>;
|
|
1350
|
+
pincodeCheckLogin(ctx: LoginWfCtx): Promise<unknown>;
|
|
1351
|
+
mfaTotp(ctx: LoginWfCtx): Promise<unknown>;
|
|
689
1352
|
/**
|
|
690
1353
|
* Backup-code alt-action handler shared by `select2fa`, `pincode-check-login`,
|
|
691
1354
|
* and `mfa-totp`. Validates against `BackupCodeForm` (alphanumeric +
|
|
@@ -693,37 +1356,60 @@ declare class LoginWorkflow {
|
|
|
693
1356
|
* produced by `UserService.generateBackupCodes`).
|
|
694
1357
|
*/
|
|
695
1358
|
private handleBackupCode;
|
|
696
|
-
|
|
1359
|
+
/**
|
|
1360
|
+
* Forced MFA enrollment — Phase 1 (pick method). Auto-picks a single
|
|
1361
|
+
* transport, otherwise pauses for the picker form. When TOTP is picked, the
|
|
1362
|
+
* secret is provisioned in the same step body (see `enrollPickPhase`).
|
|
1363
|
+
* Sync-friendly return: the auto-pick branch and the picker-form branch are
|
|
1364
|
+
* both synchronous; only the TOTP-provisioning tail is async.
|
|
1365
|
+
*/
|
|
1366
|
+
loginEnrollPickMethod(ctx: LoginWfCtx): undefined | Promise<undefined>;
|
|
1367
|
+
/**
|
|
1368
|
+
* Forced MFA enrollment — Phase 2 (collect sms/email address + send
|
|
1369
|
+
* pincode). Gated out for totp by the schema condition.
|
|
1370
|
+
*/
|
|
1371
|
+
loginEnrollAddress(ctx: LoginWfCtx): undefined | Promise<undefined>;
|
|
1372
|
+
/**
|
|
1373
|
+
* Forced MFA enrollment — Phase 3 (verify code + activate method). Fires
|
|
1374
|
+
* `onComplete` to bridge `enrollDone` → `mfaChecked` so login's outer MFA
|
|
1375
|
+
* while-loop (gated on `!mfaChecked`) exits.
|
|
1376
|
+
*/
|
|
1377
|
+
loginEnrollConfirm(ctx: LoginWfCtx): undefined | Promise<undefined>;
|
|
1378
|
+
/**
|
|
1379
|
+
* Build the `MfaEnrollDeps` payload shared by all three login enrollment
|
|
1380
|
+
* step bodies. Sets `ctx.enrollMode` (mirrored onto ctx so
|
|
1381
|
+
* `EnrollPickMethodForm` can hide the `skip` action unless mode is
|
|
1382
|
+
* `'optional'`) and supplies `onComplete` to mirror `enrollDone` →
|
|
1383
|
+
* `mfaChecked` for login's loop-exit signal.
|
|
1384
|
+
*/
|
|
1385
|
+
private buildLoginEnrollDeps;
|
|
697
1386
|
deviceTrust(ctx: LoginWfCtx): Promise<undefined>;
|
|
698
|
-
preparePasswordRules(ctx: LoginWfCtx): undefined
|
|
699
|
-
createPasswordForm(
|
|
700
|
-
|
|
701
|
-
confirmPassword?: string;
|
|
702
|
-
action?: string;
|
|
703
|
-
} | undefined, ctx: LoginWfCtx): Promise<unknown>;
|
|
704
|
-
termsAccept(input: {
|
|
705
|
-
acceptedVersion?: string;
|
|
706
|
-
accepted?: boolean;
|
|
707
|
-
action?: string;
|
|
708
|
-
} | undefined, ctx: LoginWfCtx): Promise<unknown>;
|
|
709
|
-
profileComplete(input: Record<string, unknown> | undefined, ctx: LoginWfCtx): Promise<unknown>;
|
|
1387
|
+
preparePasswordRules(ctx: LoginWfCtx): undefined | Promise<undefined>;
|
|
1388
|
+
createPasswordForm(ctx: LoginWfCtx): Promise<unknown>;
|
|
1389
|
+
profileComplete(ctx: LoginWfCtx): Promise<unknown>;
|
|
710
1390
|
/**
|
|
711
1391
|
* Persists the profile-complete payload onto the user record. Default:
|
|
712
1392
|
* no-op (the workflow records the form was submitted but writes nothing).
|
|
713
1393
|
* Consumers override to write into their user store.
|
|
714
1394
|
*/
|
|
715
1395
|
protected applyProfile(_username: string, _payload: Record<string, unknown>): Promise<void>;
|
|
716
|
-
consentMarketing(input: {
|
|
717
|
-
optIn?: boolean;
|
|
718
|
-
action?: string;
|
|
719
|
-
} | undefined, ctx: LoginWfCtx): Promise<unknown>;
|
|
720
1396
|
/**
|
|
721
|
-
*
|
|
1397
|
+
* Standalone terms re-acceptance prompt for returning users whose accepted
|
|
1398
|
+
* terms version is stale — the consumer's `ConsentStore.getPendingConsents`
|
|
1399
|
+
* returned a non-empty descriptor list (typically a bumped terms version)
|
|
1400
|
+
* and no onboarding carrier form (ask-email/ask-phone/set-password/
|
|
1401
|
+
* profile-complete) ran to capture them via the dynamic `consents: string[]`
|
|
1402
|
+
* carrier field. The body delegates to `processInlineConsent`, which
|
|
1403
|
+
* handles validation + ctx writes identically to the inline path.
|
|
722
1404
|
*/
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
1405
|
+
termsBumpPrompt(ctx: LoginWfCtx): undefined;
|
|
1406
|
+
/**
|
|
1407
|
+
* Batched consent persistence — delegates to
|
|
1408
|
+
* `AuthWorkflowBase.runPersistConsents`. See that helper for the full
|
|
1409
|
+
* audit-friendly-default / idempotency / silent-drop contract.
|
|
1410
|
+
*/
|
|
1411
|
+
persistConsentsStep(ctx: LoginWfCtx): Promise<undefined>;
|
|
1412
|
+
tenantSelect(ctx: LoginWfCtx): Promise<unknown>;
|
|
727
1413
|
/**
|
|
728
1414
|
* Resolves the user's available tenants. Default: empty array. Consumers
|
|
729
1415
|
* who enable `multiContext.tenantSelect` must override this to return the
|
|
@@ -733,9 +1419,7 @@ declare class LoginWorkflow {
|
|
|
733
1419
|
id: string;
|
|
734
1420
|
name: string;
|
|
735
1421
|
}>>;
|
|
736
|
-
personaSelect(
|
|
737
|
-
personaId?: string;
|
|
738
|
-
} | undefined, ctx: LoginWfCtx): Promise<unknown>;
|
|
1422
|
+
personaSelect(ctx: LoginWfCtx): Promise<unknown>;
|
|
739
1423
|
/**
|
|
740
1424
|
* Resolves the user's available personas. Default: empty array. Consumers
|
|
741
1425
|
* who enable `multiContext.personaSelect` must override this.
|
|
@@ -744,9 +1428,15 @@ declare class LoginWorkflow {
|
|
|
744
1428
|
id: string;
|
|
745
1429
|
label: string;
|
|
746
1430
|
}>>;
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1431
|
+
loadActiveSessionsStep(ctx: LoginWfCtx): Promise<undefined>;
|
|
1432
|
+
/**
|
|
1433
|
+
* Return the number of active (non-revoked, non-expired) sessions for the
|
|
1434
|
+
* user — consulted by `concurrency-limit` to decide whether the kickPrompt
|
|
1435
|
+
* branch fires. Default returns `0` (no enforcement). Override with a real
|
|
1436
|
+
* count from your credential store or session table.
|
|
1437
|
+
*/
|
|
1438
|
+
protected loadActiveSessions(_username: string): Promise<number>;
|
|
1439
|
+
concurrencyLimit(ctx: LoginWfCtx): Promise<unknown>;
|
|
750
1440
|
/**
|
|
751
1441
|
* Implements the "log out other sessions" branch of `sessionPolicy.concurrencyLimit`.
|
|
752
1442
|
* Default: no-op. Consumers override to revoke sessions in their auth store.
|
|
@@ -754,18 +1444,19 @@ declare class LoginWorkflow {
|
|
|
754
1444
|
protected logoutOtherSessions(_username: string): Promise<void>;
|
|
755
1445
|
riskStepUp(ctx: LoginWfCtx): Promise<undefined>;
|
|
756
1446
|
/**
|
|
757
|
-
*
|
|
758
|
-
*
|
|
759
|
-
* `{require: true, reason: '…'}`
|
|
1447
|
+
* Resolves whether to require an additional MFA round (risk step-up).
|
|
1448
|
+
* Default: never requires an extra factor. Consumers override to inspect ctx
|
|
1449
|
+
* (IP, geo, time since last login, etc.) and return `{require: true, reason: '…'}`
|
|
1450
|
+
* to force an additional MFA round.
|
|
760
1451
|
*/
|
|
761
|
-
protected
|
|
1452
|
+
protected resolveRiskStepUp(_ctx: LoginWfCtx): Promise<{
|
|
762
1453
|
require: boolean;
|
|
763
1454
|
reason?: string;
|
|
764
1455
|
}>;
|
|
765
1456
|
issue(ctx: LoginWfCtx): Promise<void>;
|
|
766
1457
|
auditLogin(ctx: LoginWfCtx): Promise<undefined>;
|
|
767
1458
|
notifyNewDevice(ctx: LoginWfCtx): Promise<undefined>;
|
|
768
|
-
redirect(ctx: LoginWfCtx): undefined
|
|
1459
|
+
redirect(ctx: LoginWfCtx): undefined | Promise<undefined>;
|
|
769
1460
|
/**
|
|
770
1461
|
* Resolves the post-login redirect URL. Default reads
|
|
771
1462
|
* `finalize.redirect`: `false` / `null` (the default) → no redirect, the
|
|
@@ -773,40 +1464,17 @@ declare class LoginWorkflow {
|
|
|
773
1464
|
* `'home'` → `/`; `'referer'` → request `Referer` header (undefined when
|
|
774
1465
|
* absent, falling back to the data response).
|
|
775
1466
|
*
|
|
776
|
-
*
|
|
1467
|
+
* Sync return type only — the caller (`redirect` @Step's default body)
|
|
1468
|
+
* uses the URL inline; consumers needing async redirect resolution should
|
|
1469
|
+
* override the `redirect` @Step instead.
|
|
777
1470
|
*/
|
|
778
|
-
protected resolveRedirect(
|
|
779
|
-
private resolveClientIp;
|
|
1471
|
+
protected resolveRedirect(ctx: LoginWfCtx): string | undefined;
|
|
780
1472
|
}
|
|
781
1473
|
//#endregion
|
|
782
1474
|
//#region src/workflows/recovery.workflow.options.d.ts
|
|
783
1475
|
type RecoveryDeliveryMode = "magicLink" | "otp" | "choice";
|
|
784
1476
|
type RecoveryOtpTransport = "sms" | "email";
|
|
785
1477
|
interface RecoveryWorkflowOpts {
|
|
786
|
-
delivery?: {
|
|
787
|
-
mode?: RecoveryDeliveryMode;
|
|
788
|
-
magicLinkTtlMs?: number;
|
|
789
|
-
otp?: {
|
|
790
|
-
transports?: RecoveryOtpTransport[];
|
|
791
|
-
codeLength?: number;
|
|
792
|
-
ttlMs?: number;
|
|
793
|
-
resendCooldownMs?: number;
|
|
794
|
-
};
|
|
795
|
-
};
|
|
796
|
-
preReset?: {
|
|
797
|
-
requireKnownFactor?: boolean;
|
|
798
|
-
};
|
|
799
|
-
postReset?: {
|
|
800
|
-
revokeAllSessions?: boolean;
|
|
801
|
-
freshLoginRequired?: boolean;
|
|
802
|
-
loginUrl?: string;
|
|
803
|
-
};
|
|
804
|
-
altActions?: {
|
|
805
|
-
backToLogin?: boolean;
|
|
806
|
-
};
|
|
807
|
-
audit?: {
|
|
808
|
-
enabled?: boolean;
|
|
809
|
-
};
|
|
810
1478
|
/**
|
|
811
1479
|
* Replaceable form schemas. Each field defaults to the corresponding
|
|
812
1480
|
* `.as` form shipped under `@aooth/auth-moost/atscript/models`.
|
|
@@ -821,34 +1489,10 @@ interface RecoveryWorkflowOpts {
|
|
|
821
1489
|
}
|
|
822
1490
|
/**
|
|
823
1491
|
* Fully-resolved view used by the workflow at runtime — every nested group is
|
|
824
|
-
* always populated by `mergeRecoveryOpts`, so
|
|
825
|
-
* `
|
|
1492
|
+
* always populated by `mergeRecoveryOpts`, so step bodies can read
|
|
1493
|
+
* `this.opts.<group>.<flag>` directly without optional chaining.
|
|
826
1494
|
*/
|
|
827
1495
|
interface ResolvedRecoveryWorkflowOpts {
|
|
828
|
-
delivery: {
|
|
829
|
-
mode: RecoveryDeliveryMode;
|
|
830
|
-
magicLinkTtlMs: number;
|
|
831
|
-
otp: {
|
|
832
|
-
transports: RecoveryOtpTransport[];
|
|
833
|
-
codeLength: number;
|
|
834
|
-
ttlMs: number;
|
|
835
|
-
resendCooldownMs: number;
|
|
836
|
-
};
|
|
837
|
-
};
|
|
838
|
-
preReset: {
|
|
839
|
-
requireKnownFactor: boolean;
|
|
840
|
-
};
|
|
841
|
-
postReset: {
|
|
842
|
-
revokeAllSessions: boolean;
|
|
843
|
-
freshLoginRequired: boolean;
|
|
844
|
-
loginUrl: string;
|
|
845
|
-
};
|
|
846
|
-
altActions: {
|
|
847
|
-
backToLogin: boolean;
|
|
848
|
-
};
|
|
849
|
-
audit: {
|
|
850
|
-
enabled: boolean;
|
|
851
|
-
};
|
|
852
1496
|
forms: {
|
|
853
1497
|
emailIdentifier: TAtscriptAnnotatedType;
|
|
854
1498
|
pincode: TAtscriptAnnotatedType;
|
|
@@ -866,11 +1510,29 @@ declare function mergeRecoveryOpts(opts?: RecoveryWorkflowOpts): ResolvedRecover
|
|
|
866
1510
|
//#endregion
|
|
867
1511
|
//#region src/workflows/recovery.workflow.d.ts
|
|
868
1512
|
interface RecoveryWfCtx {
|
|
869
|
-
|
|
1513
|
+
delivery?: {
|
|
1514
|
+
mode: RecoveryDeliveryMode;
|
|
1515
|
+
otpTransports: RecoveryOtpTransport[];
|
|
1516
|
+
};
|
|
1517
|
+
preReset?: {
|
|
1518
|
+
requireKnownFactor: boolean;
|
|
1519
|
+
allowedFactors?: Array<"phone" | "totp">;
|
|
1520
|
+
};
|
|
1521
|
+
postReset?: {
|
|
1522
|
+
revokeAllSessions: boolean;
|
|
1523
|
+
freshLoginRequired: boolean;
|
|
1524
|
+
loginUrl: string;
|
|
1525
|
+
};
|
|
1526
|
+
altActions?: {
|
|
1527
|
+
backToLogin: boolean;
|
|
1528
|
+
};
|
|
1529
|
+
audit?: {
|
|
1530
|
+
enabled: boolean;
|
|
1531
|
+
};
|
|
870
1532
|
email?: string;
|
|
871
1533
|
username?: string;
|
|
872
1534
|
selectedMode?: "magicLink" | "otp";
|
|
873
|
-
/** Resolved delivery mode the workflow committed to (
|
|
1535
|
+
/** Resolved delivery mode the workflow committed to (set by `prepare-delivery` for fixed modes, by `select-mode` for `'choice'`). */
|
|
874
1536
|
resolvedMode?: "magicLink" | "otp";
|
|
875
1537
|
otpTransport?: "sms" | "email";
|
|
876
1538
|
otpCodeLength?: number;
|
|
@@ -878,19 +1540,69 @@ interface RecoveryWfCtx {
|
|
|
878
1540
|
pinExpire?: number;
|
|
879
1541
|
pinResendAllowedAt?: number;
|
|
880
1542
|
pinVerified?: boolean;
|
|
1543
|
+
/** Mirror of `delivery.otpTransports.length`. Passed to `PincodeForm` so the `useDifferentTransport` action hides when only one transport is configured. */
|
|
1544
|
+
recoveryTransportCount?: number;
|
|
881
1545
|
linkSent?: boolean;
|
|
882
1546
|
factorVerified?: boolean;
|
|
1547
|
+
/**
|
|
1548
|
+
* Recovery factors the user is actually able to verify on this attempt —
|
|
1549
|
+
* intersection of `preReset.allowedFactors` (workflow whitelist) and
|
|
1550
|
+
* what the user has enrolled (e.g. phone only if a confirmed SMS method
|
|
1551
|
+
* exists). Populated by `verify-factor` before its form pauses and
|
|
1552
|
+
* consumed by `RecoveryFactorForm` via `@wf.context.pass` to render only
|
|
1553
|
+
* the available radio options.
|
|
1554
|
+
*/
|
|
1555
|
+
availableRecoveryFactors?: Array<{
|
|
1556
|
+
key: string;
|
|
1557
|
+
label: string;
|
|
1558
|
+
}>;
|
|
883
1559
|
passwordChanged?: boolean;
|
|
884
1560
|
sessionsRevoked?: boolean;
|
|
885
1561
|
tokensIssued?: boolean;
|
|
1562
|
+
/**
|
|
1563
|
+
* Subset of `pendingConsents[].id` the user ticked — set by
|
|
1564
|
+
* `processInlineConsent` after silent-dropping unknown ids.
|
|
1565
|
+
*/
|
|
1566
|
+
acceptedConsentIds?: string[];
|
|
1567
|
+
/**
|
|
1568
|
+
* Wall-clock ms when `processInlineConsent` resolved the carrier-form
|
|
1569
|
+
* submission. Also the schema-gate for `persist-consents`.
|
|
1570
|
+
*/
|
|
1571
|
+
consentsDecidedAt?: number;
|
|
1572
|
+
/** Set true by `persist-consents` after the batched `consentStore.save` call fires. */
|
|
1573
|
+
consentsPersisted?: boolean;
|
|
1574
|
+
/**
|
|
1575
|
+
* Descriptors for the customer-defined consents (terms, marketing,
|
|
1576
|
+
* jurisdiction, ...) the user still needs to accept. Populated once by
|
|
1577
|
+
* `prepare-consents` after username-bind; consumed by `WithInlineConsentForm`'s
|
|
1578
|
+
* dynamic `AsConsentArray` field on the carrier form (Phase 5).
|
|
1579
|
+
*/
|
|
1580
|
+
pendingConsents?: ConsentDescriptor[];
|
|
886
1581
|
/** Set by abort alt-actions (`backToLogin`). Gates all terminal steps. */
|
|
887
1582
|
aborted?: boolean;
|
|
888
1583
|
}
|
|
889
|
-
|
|
1584
|
+
/**
|
|
1585
|
+
* Per-group policy override shape consumed by `resolveXxx(ctx)` subclass
|
|
1586
|
+
* overrides. Mirrors the `ctx.<group>` fields that the `prepare-<group>`
|
|
1587
|
+
* @Step methods populate — one entry per resolver. Library users typically
|
|
1588
|
+
* accept a payload of this shape on their `RecoveryWorkflow` subclass ctor /
|
|
1589
|
+
* test harness and have each `resolveXxx` return its matching key (falling
|
|
1590
|
+
* back to `super.resolveXxx(ctx)` for unset groups).
|
|
1591
|
+
*/
|
|
1592
|
+
interface RecoveryPolicyOverrides {
|
|
1593
|
+
delivery?: NonNullable<RecoveryWfCtx["delivery"]>;
|
|
1594
|
+
preReset?: NonNullable<RecoveryWfCtx["preReset"]>;
|
|
1595
|
+
postReset?: NonNullable<RecoveryWfCtx["postReset"]>;
|
|
1596
|
+
altActions?: NonNullable<RecoveryWfCtx["altActions"]>;
|
|
1597
|
+
audit?: NonNullable<RecoveryWfCtx["audit"]>;
|
|
1598
|
+
}
|
|
1599
|
+
declare class RecoveryWorkflow extends AuthWorkflowBase {
|
|
890
1600
|
protected readonly opts: ResolvedRecoveryWorkflowOpts;
|
|
891
1601
|
protected readonly users: UserService;
|
|
892
1602
|
protected readonly auth: AuthCredential;
|
|
893
|
-
|
|
1603
|
+
protected readonly authOpts: AuthOpts;
|
|
1604
|
+
protected readonly consentStore: ConsentStore;
|
|
1605
|
+
constructor(opts: RecoveryWorkflowOpts, users: UserService, auth: AuthCredential, authOpts: AuthOpts, consentStore: ConsentStore);
|
|
894
1606
|
/**
|
|
895
1607
|
* Dispatch an email or SMS event. Default throws — consumers MUST override
|
|
896
1608
|
* if `delivery.mode` ever drives email/SMS (i.e. for any non-`magicLink`
|
|
@@ -904,22 +1616,76 @@ declare class RecoveryWorkflow {
|
|
|
904
1616
|
* their audit sink.
|
|
905
1617
|
*/
|
|
906
1618
|
protected audit(_event: AuditEvent): Promise<void>;
|
|
1619
|
+
/**
|
|
1620
|
+
* Resolve the delivery policy (mode + OTP transports). Override per-tenant
|
|
1621
|
+
* to drive magic-link vs OTP delivery preferences. Sync/async friendly.
|
|
1622
|
+
*/
|
|
1623
|
+
protected resolveDelivery(_ctx: RecoveryWfCtx): NonNullable<RecoveryWfCtx["delivery"]> | Promise<NonNullable<RecoveryWfCtx["delivery"]>>;
|
|
1624
|
+
/**
|
|
1625
|
+
* Resolve the pre-reset policy (requireKnownFactor + allowedFactors whitelist).
|
|
1626
|
+
* Override to enforce a factor check between the magic-link / OTP step and
|
|
1627
|
+
* the new-password form. `allowedFactors` omitted means both phone and TOTP
|
|
1628
|
+
* are eligible. Sync/async friendly.
|
|
1629
|
+
*/
|
|
1630
|
+
protected resolvePreReset(_ctx: RecoveryWfCtx): NonNullable<RecoveryWfCtx["preReset"]> | Promise<NonNullable<RecoveryWfCtx["preReset"]>>;
|
|
1631
|
+
/**
|
|
1632
|
+
* Resolve the post-reset policy (session revocation / fresh-login redirect /
|
|
1633
|
+
* loginUrl). Override per-tenant. `loginUrl` defaults to
|
|
1634
|
+
* `this.authOpts.loginUrl` (the cross-workflow shared login URL); customers
|
|
1635
|
+
* can still override per-tenant by overriding this resolver — the field
|
|
1636
|
+
* stays on the policy surface. Sync/async friendly.
|
|
1637
|
+
*/
|
|
1638
|
+
protected resolvePostReset(_ctx: RecoveryWfCtx): NonNullable<RecoveryWfCtx["postReset"]> | Promise<NonNullable<RecoveryWfCtx["postReset"]>>;
|
|
1639
|
+
/**
|
|
1640
|
+
* Resolve the alt-actions policy (whether `backToLogin` is offered on the
|
|
1641
|
+
* recovery forms). Override to hide the escape hatch per-tenant. Sync/async
|
|
1642
|
+
* friendly.
|
|
1643
|
+
*/
|
|
1644
|
+
protected resolveAltActions(_ctx: RecoveryWfCtx): NonNullable<RecoveryWfCtx["altActions"]> | Promise<NonNullable<RecoveryWfCtx["altActions"]>>;
|
|
1645
|
+
/**
|
|
1646
|
+
* Resolve the audit policy (whether recovery.* audit events fire). Override
|
|
1647
|
+
* to route audit-log emission per-tenant. Sync/async friendly.
|
|
1648
|
+
*/
|
|
1649
|
+
protected resolveAudit(_ctx: RecoveryWfCtx): NonNullable<RecoveryWfCtx["audit"]> | Promise<NonNullable<RecoveryWfCtx["audit"]>>;
|
|
1650
|
+
prepareDelivery(ctx: RecoveryWfCtx): undefined | Promise<undefined>;
|
|
1651
|
+
/**
|
|
1652
|
+
* Apply resolved delivery to ctx — also auto-resolves derived ctx fields:
|
|
1653
|
+
* - `resolvedMode` (when mode !== 'choice' — `'choice'` defers to `select-mode`)
|
|
1654
|
+
* - `recoveryTransportCount` (mirrored to ctx for the `useDifferentTransport` form gate)
|
|
1655
|
+
*
|
|
1656
|
+
* Validates the otpTransports-not-empty invariant at step time (replacing
|
|
1657
|
+
* the old construction-time `validateOpts` check; the value is now
|
|
1658
|
+
* ctx-driven so the check has to fire at step time).
|
|
1659
|
+
*/
|
|
1660
|
+
private applyResolvedDelivery;
|
|
1661
|
+
preparePreReset(ctx: RecoveryWfCtx): undefined | Promise<undefined>;
|
|
1662
|
+
preparePostReset(ctx: RecoveryWfCtx): undefined | Promise<undefined>;
|
|
1663
|
+
prepareAltActions(ctx: RecoveryWfCtx): undefined | Promise<undefined>;
|
|
1664
|
+
prepareAudit(ctx: RecoveryWfCtx): undefined | Promise<undefined>;
|
|
1665
|
+
/**
|
|
1666
|
+
* Populate `ctx.pendingConsents` with the customer-defined general-consent
|
|
1667
|
+
* descriptors (terms, marketing, jurisdiction, ...) the user still needs to
|
|
1668
|
+
* accept. Phase 4 transport only — nothing reads `ctx.pendingConsents` yet;
|
|
1669
|
+
* Phase 5 will migrate the carrier `SetPasswordForm` from the
|
|
1670
|
+
* `WithInlineConsentForm` static-checkbox mixin onto this dynamic array.
|
|
1671
|
+
*
|
|
1672
|
+
* Username MUST be bound before we fetch consents — the schema places this
|
|
1673
|
+
* step AFTER the `!ctx.username` break gate, so the `if (!ctx.username)`
|
|
1674
|
+
* guard is belt-and-brace for future refactors that might re-order the
|
|
1675
|
+
* schema.
|
|
1676
|
+
*/
|
|
1677
|
+
prepareConsents(ctx: RecoveryWfCtx): undefined | Promise<undefined>;
|
|
907
1678
|
flow(): void;
|
|
908
|
-
init(ctx: RecoveryWfCtx): undefined;
|
|
909
1679
|
/**
|
|
910
|
-
*
|
|
911
|
-
*
|
|
912
|
-
* are not plain JSON) so `AsWfStore`'s plain-JSON persistence doesn't choke.
|
|
913
|
-
* Step bodies still consult the form classes via `this.opts.forms.*`.
|
|
1680
|
+
* First step of the workflow; remains as a no-op override hook for
|
|
1681
|
+
* consumers. Policy populated by the dedicated `prepare-<group>` steps.
|
|
914
1682
|
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
1683
|
+
* Return type is `undefined | Promise<undefined>` so consumers can override
|
|
1684
|
+
* with `async init(...)` without the default fast-path paying a Promise
|
|
1685
|
+
* allocation (the wf engine awaits only when the return value is a Promise).
|
|
917
1686
|
*/
|
|
918
|
-
|
|
919
|
-
request(
|
|
920
|
-
email?: string;
|
|
921
|
-
action?: string;
|
|
922
|
-
} | undefined, ctx: RecoveryWfCtx): Promise<unknown>;
|
|
1687
|
+
init(_ctx: RecoveryWfCtx): undefined | Promise<undefined>;
|
|
1688
|
+
request(ctx: RecoveryWfCtx): Promise<unknown>;
|
|
923
1689
|
/**
|
|
924
1690
|
* Resolves the recovery-step `email` input to the `username` (user-id) that
|
|
925
1691
|
* `UserService.getUser` expects. Default: returns the email unchanged (treats
|
|
@@ -927,21 +1693,24 @@ declare class RecoveryWorkflow {
|
|
|
927
1693
|
* `email` MUST override this; return `null` when no user matches.
|
|
928
1694
|
*/
|
|
929
1695
|
protected emailToUserId(email: string): Promise<string | null>;
|
|
930
|
-
selectMode(
|
|
931
|
-
mode?: string;
|
|
932
|
-
action?: string;
|
|
933
|
-
} | undefined, ctx: RecoveryWfCtx): unknown;
|
|
1696
|
+
selectMode(ctx: RecoveryWfCtx): unknown;
|
|
934
1697
|
sendMagicLink(ctx: RecoveryWfCtx): unknown;
|
|
935
1698
|
sendOtp(ctx: RecoveryWfCtx): Promise<undefined>;
|
|
936
|
-
checkOtp(
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1699
|
+
checkOtp(ctx: RecoveryWfCtx): Promise<unknown>;
|
|
1700
|
+
verifyFactor(ctx: RecoveryWfCtx): Promise<unknown>;
|
|
1701
|
+
/**
|
|
1702
|
+
* Returns the factor options to show on `RecoveryFactorForm`. Default:
|
|
1703
|
+
* intersection of `preReset.allowedFactors` (workflow whitelist —
|
|
1704
|
+
* `undefined` means both `phone` and `totp` are eligible) and the kinds
|
|
1705
|
+
* the user has actually enrolled (`phone` if a confirmed SMS method
|
|
1706
|
+
* exists, `totp` if a confirmed TOTP method exists). Override to add
|
|
1707
|
+
* custom factors (e.g. security questions) — call `super` to keep the
|
|
1708
|
+
* built-in pair.
|
|
1709
|
+
*/
|
|
1710
|
+
protected loadAvailableRecoveryFactors(ctx: RecoveryWfCtx): Promise<Array<{
|
|
1711
|
+
key: string;
|
|
1712
|
+
label: string;
|
|
1713
|
+
}>>;
|
|
945
1714
|
/**
|
|
946
1715
|
* Verifies a recovery factor against the user's enrolled MFA methods.
|
|
947
1716
|
* Default: supports `'phone'` (phone last-4 match) and `'totp'` (current
|
|
@@ -956,14 +1725,16 @@ declare class RecoveryWorkflow {
|
|
|
956
1725
|
value: string;
|
|
957
1726
|
ctx: RecoveryWfCtx;
|
|
958
1727
|
}): Promise<boolean>;
|
|
959
|
-
setPassword(
|
|
960
|
-
newPassword?: string;
|
|
961
|
-
confirmPassword?: string;
|
|
962
|
-
action?: string;
|
|
963
|
-
} | undefined, ctx: RecoveryWfCtx): Promise<unknown>;
|
|
1728
|
+
setPassword(ctx: RecoveryWfCtx): Promise<unknown>;
|
|
964
1729
|
revokeSessions(ctx: RecoveryWfCtx): Promise<undefined>;
|
|
965
1730
|
auditStep(ctx: RecoveryWfCtx): Promise<undefined>;
|
|
966
|
-
|
|
1731
|
+
/**
|
|
1732
|
+
* Batched consent persistence — delegates to
|
|
1733
|
+
* `AuthWorkflowBase.runPersistConsents`. See that helper for the full
|
|
1734
|
+
* audit-friendly-default / idempotency / silent-drop contract.
|
|
1735
|
+
*/
|
|
1736
|
+
persistConsentsStep(ctx: RecoveryWfCtx): Promise<undefined>;
|
|
1737
|
+
freshLoginFinish(ctx: RecoveryWfCtx): undefined | Promise<undefined>;
|
|
967
1738
|
autoLoginFinish(ctx: RecoveryWfCtx): Promise<undefined>;
|
|
968
1739
|
/**
|
|
969
1740
|
* Send the generic "if an account exists, you'll receive instructions"
|
|
@@ -994,31 +1765,14 @@ interface PreparedUserInput {
|
|
|
994
1765
|
type DuplicateAction = "allow" | "reject" | "reuseAsReInvite";
|
|
995
1766
|
type InviteSendMode = "email" | "shareableLink" | "choice";
|
|
996
1767
|
interface InviteWorkflowOpts {
|
|
997
|
-
adminForm?: {
|
|
998
|
-
collectRoles?: boolean;
|
|
999
|
-
};
|
|
1000
|
-
send?: {
|
|
1001
|
-
mode?: InviteSendMode;
|
|
1002
|
-
tokenTtlMs?: number;
|
|
1003
|
-
};
|
|
1004
|
-
accept?: {
|
|
1005
|
-
alreadyAcceptedRedirectUrl?: string;
|
|
1006
|
-
freshLoginRequired?: boolean;
|
|
1007
|
-
loginUrl?: string;
|
|
1008
|
-
showConfirmation?: boolean;
|
|
1009
|
-
confirmationMessage?: string;
|
|
1010
|
-
};
|
|
1011
|
-
cancellation?: {
|
|
1012
|
-
allowed?: boolean;
|
|
1013
|
-
};
|
|
1014
|
-
audit?: {
|
|
1015
|
-
enabled?: boolean;
|
|
1016
|
-
};
|
|
1017
1768
|
/**
|
|
1018
1769
|
* Replaceable form schemas. Each field defaults to the corresponding
|
|
1019
1770
|
* `.as` form shipped under `@aooth/auth-moost/atscript/models`.
|
|
1020
1771
|
*/
|
|
1021
1772
|
forms?: {
|
|
1773
|
+
enrollAddress?: TAtscriptAnnotatedType;
|
|
1774
|
+
enrollConfirm?: TAtscriptAnnotatedType;
|
|
1775
|
+
enrollPickMethod?: TAtscriptAnnotatedType;
|
|
1022
1776
|
invite?: TAtscriptAnnotatedType;
|
|
1023
1777
|
inviteEmail?: TAtscriptAnnotatedType;
|
|
1024
1778
|
inviteSendMode?: TAtscriptAnnotatedType;
|
|
@@ -1027,31 +1781,14 @@ interface InviteWorkflowOpts {
|
|
|
1027
1781
|
}
|
|
1028
1782
|
/**
|
|
1029
1783
|
* Fully-resolved view used by the workflow at runtime — every nested group is
|
|
1030
|
-
* always populated by `mergeInviteOpts`, so
|
|
1031
|
-
* `
|
|
1784
|
+
* always populated by `mergeInviteOpts`, so step bodies can read
|
|
1785
|
+
* `this.opts.<group>.<flag>` directly without optional chaining.
|
|
1032
1786
|
*/
|
|
1033
1787
|
interface ResolvedInviteWorkflowOpts {
|
|
1034
|
-
adminForm: {
|
|
1035
|
-
collectRoles: boolean;
|
|
1036
|
-
};
|
|
1037
|
-
send: {
|
|
1038
|
-
mode: InviteSendMode;
|
|
1039
|
-
tokenTtlMs: number;
|
|
1040
|
-
};
|
|
1041
|
-
accept: {
|
|
1042
|
-
alreadyAcceptedRedirectUrl: string;
|
|
1043
|
-
freshLoginRequired: boolean;
|
|
1044
|
-
loginUrl: string;
|
|
1045
|
-
showConfirmation: boolean;
|
|
1046
|
-
confirmationMessage: string;
|
|
1047
|
-
};
|
|
1048
|
-
cancellation: {
|
|
1049
|
-
allowed: boolean;
|
|
1050
|
-
};
|
|
1051
|
-
audit: {
|
|
1052
|
-
enabled: boolean;
|
|
1053
|
-
};
|
|
1054
1788
|
forms: {
|
|
1789
|
+
enrollAddress: TAtscriptAnnotatedType;
|
|
1790
|
+
enrollConfirm: TAtscriptAnnotatedType;
|
|
1791
|
+
enrollPickMethod: TAtscriptAnnotatedType;
|
|
1055
1792
|
invite: TAtscriptAnnotatedType;
|
|
1056
1793
|
inviteEmail: TAtscriptAnnotatedType;
|
|
1057
1794
|
inviteSendMode: TAtscriptAnnotatedType;
|
|
@@ -1072,14 +1809,35 @@ type InvitePrepareUserInput = PreparedUserInput;
|
|
|
1072
1809
|
//#endregion
|
|
1073
1810
|
//#region src/workflows/invite.workflow.d.ts
|
|
1074
1811
|
interface InviteWfCtx {
|
|
1075
|
-
|
|
1812
|
+
adminForm?: {
|
|
1813
|
+
collectRoles: boolean;
|
|
1814
|
+
};
|
|
1815
|
+
send?: {
|
|
1816
|
+
mode: InviteSendMode;
|
|
1817
|
+
};
|
|
1818
|
+
accept?: {
|
|
1819
|
+
alreadyAcceptedRedirectUrl: string;
|
|
1820
|
+
freshLoginRequired: boolean;
|
|
1821
|
+
loginUrl: string;
|
|
1822
|
+
showConfirmation: boolean;
|
|
1823
|
+
confirmationMessage: string;
|
|
1824
|
+
};
|
|
1825
|
+
cancellation?: {
|
|
1826
|
+
allowed: boolean;
|
|
1827
|
+
};
|
|
1828
|
+
audit?: {
|
|
1829
|
+
enabled: boolean;
|
|
1830
|
+
};
|
|
1831
|
+
mfa?: {
|
|
1832
|
+
issuer: string;
|
|
1833
|
+
};
|
|
1076
1834
|
/** Boolean projection of `this.getProfileForm() !== undefined` — schema gates on it. */
|
|
1077
1835
|
acceptProfileFormPresent?: boolean;
|
|
1078
1836
|
/**
|
|
1079
|
-
* Populated by `
|
|
1837
|
+
* Populated by `prepare-available-roles` when the override returns a list.
|
|
1080
1838
|
* Surfaced into the `InviteForm` via `@wf.context.pass 'availableRoles'` so
|
|
1081
1839
|
* the role multi-select renders the whitelisted choices; also used by
|
|
1082
|
-
* `
|
|
1840
|
+
* `admin-form` to reject admin-submitted roles outside the list.
|
|
1083
1841
|
*/
|
|
1084
1842
|
availableRoles?: string[];
|
|
1085
1843
|
email?: string;
|
|
@@ -1088,27 +1846,97 @@ interface InviteWfCtx {
|
|
|
1088
1846
|
firstName?: string;
|
|
1089
1847
|
lastName?: string;
|
|
1090
1848
|
roles?: string[];
|
|
1091
|
-
/**
|
|
1849
|
+
/**
|
|
1850
|
+
* Extras dict prepared by `build-user-extras` (calls `prepareUser`) and
|
|
1851
|
+
* consumed by `create-user` to populate the user-row fields beyond the
|
|
1852
|
+
* base credential shape. Split apart so consumers can inject e.g. a
|
|
1853
|
+
* tenant-validation step between extras-build and create-user without
|
|
1854
|
+
* copying either body.
|
|
1855
|
+
*/
|
|
1856
|
+
userExtras?: Record<string, unknown>;
|
|
1857
|
+
/** Populated by `select-send-mode` (when `send.mode === 'choice'`). */
|
|
1092
1858
|
selectedSendMode?: "email" | "shareableLink";
|
|
1093
|
-
/** Resolved send mode the workflow committed to (set in `
|
|
1859
|
+
/** Resolved send mode the workflow committed to (set in `prepare-send` or `select-send-mode`). */
|
|
1094
1860
|
resolvedSendMode?: "email" | "shareableLink";
|
|
1095
|
-
/** Populated by `
|
|
1861
|
+
/** Populated by `return-shareable-link` so the admin's UI can display it. */
|
|
1096
1862
|
shareableLinkUrl?: string;
|
|
1097
|
-
/** Marks that `
|
|
1863
|
+
/** Marks that `send-email` already emitted the outlet — resume → advance. */
|
|
1098
1864
|
linkSent?: boolean;
|
|
1099
|
-
/** Detected at `
|
|
1865
|
+
/** Detected at `check-pending-invitation`; triggers `idempotent-redirect`. */
|
|
1100
1866
|
alreadyAccepted?: boolean;
|
|
1101
1867
|
passwordSet?: boolean;
|
|
1102
|
-
|
|
1868
|
+
enrollMethod?: "sms" | "email" | "totp";
|
|
1869
|
+
enrollAddress?: string;
|
|
1870
|
+
enrollSecret?: string;
|
|
1871
|
+
enrollUri?: string;
|
|
1872
|
+
enrollAvailableTransports?: Array<"sms" | "email" | "totp">;
|
|
1873
|
+
/**
|
|
1874
|
+
* MFA policy (set by `inviteSetupMfa` setter — overridable per consumer).
|
|
1875
|
+
* - `'required'` — invitee MUST enroll a second factor BEFORE activation.
|
|
1876
|
+
* - `'optional'` — invitee is prompted but may `skip` the enrollment form.
|
|
1877
|
+
* - `'disabled'` — enrollment loop is skipped entirely.
|
|
1878
|
+
*/
|
|
1879
|
+
mfaMode?: "required" | "optional" | "disabled";
|
|
1880
|
+
/** Available MFA transports (set by `inviteSetupMfa` setter — overridable per consumer). */
|
|
1881
|
+
availableMfaTransports?: Array<"sms" | "email" | "totp">;
|
|
1882
|
+
/**
|
|
1883
|
+
* Mirror of `ctx.mfaMode` (only set when not `'disabled'`). Surfaced to
|
|
1884
|
+
* `EnrollPickMethodForm` via `@wf.context.pass` so the `skip` action can
|
|
1885
|
+
* hide unless mode is `'optional'`.
|
|
1886
|
+
*/
|
|
1887
|
+
enrollMode?: "required" | "optional";
|
|
1888
|
+
enrollDone?: boolean;
|
|
1889
|
+
/** Phase 3 confirm-pincode resend cooldown (sms/email). See `MfaEnrollCtx.enrollPincodeCooldown`. */
|
|
1890
|
+
enrollPincodeCooldown?: number;
|
|
1891
|
+
/** Pincode scratch shared with the enrollment helper. */
|
|
1892
|
+
pin?: string;
|
|
1893
|
+
pinExpire?: number;
|
|
1894
|
+
pinSentTo?: string;
|
|
1895
|
+
/** Raw input from `collect-profile`. */
|
|
1103
1896
|
profile?: Record<string, unknown>;
|
|
1104
1897
|
profileApplied?: boolean;
|
|
1105
1898
|
pendingInvitationCleared?: boolean;
|
|
1106
1899
|
activated?: boolean;
|
|
1107
1900
|
confirmationShown?: boolean;
|
|
1108
1901
|
tokensIssued?: boolean;
|
|
1902
|
+
/**
|
|
1903
|
+
* Subset of `pendingConsents[].id` the user ticked — set by
|
|
1904
|
+
* `processInlineConsent` after silent-dropping unknown ids.
|
|
1905
|
+
*/
|
|
1906
|
+
acceptedConsentIds?: string[];
|
|
1907
|
+
/**
|
|
1908
|
+
* Wall-clock ms when `processInlineConsent` resolved the carrier-form
|
|
1909
|
+
* submission. Also the schema-gate for `persist-consents`.
|
|
1910
|
+
*/
|
|
1911
|
+
consentsDecidedAt?: number;
|
|
1912
|
+
/** Set true by `persist-consents` after the batched `consentStore.save` call fires. */
|
|
1913
|
+
consentsPersisted?: boolean;
|
|
1914
|
+
/**
|
|
1915
|
+
* Descriptors for the customer-defined consents (terms, marketing,
|
|
1916
|
+
* jurisdiction, ...) the user still needs to accept. Populated once by
|
|
1917
|
+
* `prepare-consents` after username-bind; consumed by `WithInlineConsentForm`'s
|
|
1918
|
+
* dynamic `AsConsentArray` field on the carrier form (Phase 5).
|
|
1919
|
+
*/
|
|
1920
|
+
pendingConsents?: ConsentDescriptor[];
|
|
1109
1921
|
/** Set true by abort alt-actions (`cancel`). Gates all terminal steps. */
|
|
1110
1922
|
aborted?: boolean;
|
|
1111
1923
|
}
|
|
1924
|
+
/**
|
|
1925
|
+
* Per-group policy override shape consumed by `resolveXxx(ctx)` subclass
|
|
1926
|
+
* overrides. Mirrors the `ctx.<group>` fields that the `prepare-<group>`
|
|
1927
|
+
* @Step methods populate — one entry per resolver. Library users typically
|
|
1928
|
+
* accept a payload of this shape on their `InviteWorkflow` subclass ctor /
|
|
1929
|
+
* test harness and have each `resolveXxx` return its matching key (falling
|
|
1930
|
+
* back to `super.resolveXxx(ctx)` for unset groups).
|
|
1931
|
+
*/
|
|
1932
|
+
interface InvitePolicyOverrides {
|
|
1933
|
+
adminForm?: NonNullable<InviteWfCtx["adminForm"]>;
|
|
1934
|
+
send?: NonNullable<InviteWfCtx["send"]>;
|
|
1935
|
+
accept?: NonNullable<InviteWfCtx["accept"]>;
|
|
1936
|
+
cancellation?: NonNullable<InviteWfCtx["cancellation"]>;
|
|
1937
|
+
audit?: NonNullable<InviteWfCtx["audit"]>;
|
|
1938
|
+
mfa?: NonNullable<InviteWfCtx["mfa"]>;
|
|
1939
|
+
}
|
|
1112
1940
|
/** Trim + de-duplicate role identifiers submitted via the admin invite form. */
|
|
1113
1941
|
declare function parseInviteRoles(input?: string[]): string[];
|
|
1114
1942
|
/**
|
|
@@ -1116,7 +1944,9 @@ declare function parseInviteRoles(input?: string[]): string[];
|
|
|
1116
1944
|
* inherit the class-level `@ArbacResource('auth.invite') @ArbacAction('start')`
|
|
1117
1945
|
* so every admin-side step event is gated. Apps that wire
|
|
1118
1946
|
* `arbacAuthorizeInterceptor` globally grant admin a single rule:
|
|
1119
|
-
* `allow('auth.invite', 'start')`.
|
|
1947
|
+
* `allow('auth.invite', 'start')`. The ARBAC resource name is intentionally
|
|
1948
|
+
* distinct from the wfid path (`auth/invite/start`) — RBAC policy ids and
|
|
1949
|
+
* wfid namespacing are separate naming schemes.
|
|
1120
1950
|
*
|
|
1121
1951
|
* The three `@Workflow` body methods (`inviteFlow` / `reInviteFlow` /
|
|
1122
1952
|
* `cancelInviteFlow`) are `@Public()` because the wf adapter dispatches the
|
|
@@ -1127,20 +1957,22 @@ declare function parseInviteRoles(input?: string[]): string[];
|
|
|
1127
1957
|
*
|
|
1128
1958
|
* Phase-B steps (post `ctx.linkSent`, accept tail) are method-level
|
|
1129
1959
|
* `@Public()` because they fire on the anonymous magic-link resume.
|
|
1130
|
-
* `
|
|
1131
|
-
*
|
|
1132
|
-
*
|
|
1133
|
-
*
|
|
1960
|
+
* `send-email` / `return-shareable-link` are the boundary: also `@Public()`
|
|
1961
|
+
* because the @prostojs/wf runtime re-enters the saved step on resume (the
|
|
1962
|
+
* loop restarts at `indexes[level]`, not after it). Their bodies are
|
|
1963
|
+
* idempotent via `if (ctx.linkSent) return`.
|
|
1134
1964
|
*
|
|
1135
|
-
* `auth
|
|
1136
|
-
* confirms in their own UI; no anonymous boundary), so their phase-A
|
|
1137
|
-
* stay class-gated under the same `auth.invite` / `start` grant.
|
|
1965
|
+
* `auth/invite/resend` / `auth/invite/cancel` are admin-only end-to-end
|
|
1966
|
+
* (admin confirms in their own UI; no anonymous boundary), so their phase-A
|
|
1967
|
+
* steps stay class-gated under the same `auth.invite` / `start` grant.
|
|
1138
1968
|
*/
|
|
1139
|
-
declare class InviteWorkflow {
|
|
1969
|
+
declare class InviteWorkflow extends AuthWorkflowBase {
|
|
1140
1970
|
protected readonly opts: ResolvedInviteWorkflowOpts;
|
|
1141
1971
|
protected readonly users: UserService;
|
|
1142
1972
|
protected readonly auth: AuthCredential;
|
|
1143
|
-
|
|
1973
|
+
protected readonly authOpts: AuthOpts;
|
|
1974
|
+
protected readonly consentStore: ConsentStore;
|
|
1975
|
+
constructor(opts: InviteWorkflowOpts, users: UserService, auth: AuthCredential, authOpts: AuthOpts, consentStore: ConsentStore);
|
|
1144
1976
|
/**
|
|
1145
1977
|
* Dispatch an email or SMS event. Default throws — the default invite send
|
|
1146
1978
|
* uses `outletEmail` (handled by `createAuthEmailOutlet`) so this method is
|
|
@@ -1165,7 +1997,7 @@ declare class InviteWorkflow {
|
|
|
1165
1997
|
* Return the list of selectable role identifiers for the admin invite form.
|
|
1166
1998
|
* When defined AND `adminForm.collectRoles` is true → form ships
|
|
1167
1999
|
* `ctx.availableRoles` so the UI renders a multi-select AND the
|
|
1168
|
-
* `
|
|
2000
|
+
* `admin-form` step rejects admin-submitted roles outside the
|
|
1169
2001
|
* list. When `undefined` (default) → no whitelist is enforced and any role
|
|
1170
2002
|
* value the admin form supplies is accepted.
|
|
1171
2003
|
*/
|
|
@@ -1190,7 +2022,7 @@ declare class InviteWorkflow {
|
|
|
1190
2022
|
profile: Record<string, unknown>;
|
|
1191
2023
|
}): Promise<void>;
|
|
1192
2024
|
/**
|
|
1193
|
-
* Override the structural duplicate rule for `
|
|
2025
|
+
* Override the structural duplicate rule for `admin-form`.
|
|
1194
2026
|
* Default: any existing row → `'reject'`; nothing → `'allow'`. Multi-tenant
|
|
1195
2027
|
* apps that allow re-inviting the same email into a different tenant
|
|
1196
2028
|
* override to return `'allow'` for those cases.
|
|
@@ -1201,62 +2033,157 @@ declare class InviteWorkflow {
|
|
|
1201
2033
|
}): Promise<DuplicateAction>;
|
|
1202
2034
|
/**
|
|
1203
2035
|
* Return the consumer-supplied `.as` form schema rendered in the
|
|
1204
|
-
* `
|
|
2036
|
+
* `collect-profile` step. `undefined` (default) skips the step
|
|
1205
2037
|
* entirely (just password collection).
|
|
1206
2038
|
*/
|
|
1207
2039
|
protected getProfileForm(): TAtscriptAnnotatedType | undefined;
|
|
1208
2040
|
/**
|
|
1209
|
-
*
|
|
1210
|
-
*
|
|
1211
|
-
|
|
1212
|
-
|
|
2041
|
+
* Resolve the admin-form policy (whether to collect roles on the admin
|
|
2042
|
+
* invite form). Override per-tenant. Sync/async friendly.
|
|
2043
|
+
*/
|
|
2044
|
+
protected resolveAdminForm(_ctx: InviteWfCtx): NonNullable<InviteWfCtx["adminForm"]> | Promise<NonNullable<InviteWfCtx["adminForm"]>>;
|
|
2045
|
+
/**
|
|
2046
|
+
* Resolve the send-mode policy (`'email'` / `'shareableLink'` / `'choice'`).
|
|
2047
|
+
* Override to drive per-tenant magic-link delivery preferences. Sync/async
|
|
2048
|
+
* friendly.
|
|
2049
|
+
*/
|
|
2050
|
+
protected resolveSend(_ctx: InviteWfCtx): NonNullable<InviteWfCtx["send"]> | Promise<NonNullable<InviteWfCtx["send"]>>;
|
|
2051
|
+
/**
|
|
2052
|
+
* Resolve the accept-tail policy (idempotent-redirect URL, fresh-login gate,
|
|
2053
|
+
* loginUrl, confirmation message). Override per-tenant. Sync/async friendly.
|
|
2054
|
+
* `loginUrl` defaults to `this.authOpts.loginUrl` (the cross-workflow shared
|
|
2055
|
+
* login URL); customers can still override per-tenant by overriding this
|
|
2056
|
+
* resolver — the field stays on the policy surface.
|
|
2057
|
+
*/
|
|
2058
|
+
protected resolveAccept(_ctx: InviteWfCtx): NonNullable<InviteWfCtx["accept"]> | Promise<NonNullable<InviteWfCtx["accept"]>>;
|
|
2059
|
+
/**
|
|
2060
|
+
* Resolve the cancellation policy (whether `auth.cancelInvite` is allowed).
|
|
2061
|
+
* Override to disable hard-delete per-tenant. Sync/async friendly.
|
|
2062
|
+
*/
|
|
2063
|
+
protected resolveCancellation(_ctx: InviteWfCtx): NonNullable<InviteWfCtx["cancellation"]> | Promise<NonNullable<InviteWfCtx["cancellation"]>>;
|
|
2064
|
+
/**
|
|
2065
|
+
* Resolve the audit policy (whether invite.* audit events fire). Override
|
|
2066
|
+
* to route audit-log emission per-tenant. Sync/async friendly.
|
|
2067
|
+
*/
|
|
2068
|
+
protected resolveAudit(_ctx: InviteWfCtx): NonNullable<InviteWfCtx["audit"]> | Promise<NonNullable<InviteWfCtx["audit"]>>;
|
|
2069
|
+
/**
|
|
2070
|
+
* Resolve the MFA-issuer policy (TOTP provisioning issuer string rendered
|
|
2071
|
+
* in the authenticator app). Default tracks `this.authOpts.totpIssuer` —
|
|
2072
|
+
* customers override the resolver for per-tenant issuers. Pincode timers/
|
|
2073
|
+
* length live on `AuthOpts.mfa`. Sync/async friendly.
|
|
2074
|
+
*/
|
|
2075
|
+
protected resolveMfa(_ctx: InviteWfCtx): NonNullable<InviteWfCtx["mfa"]> | Promise<NonNullable<InviteWfCtx["mfa"]>>;
|
|
2076
|
+
prepareAdminForm(ctx: InviteWfCtx): undefined | Promise<undefined>;
|
|
2077
|
+
prepareSend(ctx: InviteWfCtx): undefined | Promise<undefined>;
|
|
2078
|
+
prepareAccept(ctx: InviteWfCtx): undefined | Promise<undefined>;
|
|
2079
|
+
prepareCancellation(ctx: InviteWfCtx): undefined | Promise<undefined>;
|
|
2080
|
+
prepareAudit(ctx: InviteWfCtx): undefined | Promise<undefined>;
|
|
2081
|
+
prepareMfa(ctx: InviteWfCtx): undefined | Promise<undefined>;
|
|
2082
|
+
/**
|
|
2083
|
+
* Populate `ctx.pendingConsents` with the customer-defined general-consent
|
|
2084
|
+
* descriptors (terms, marketing, jurisdiction, ...) the invitee still needs
|
|
2085
|
+
* to accept. Phase 4 transport only — nothing reads `ctx.pendingConsents`
|
|
2086
|
+
* yet; Phase 5 will migrate the carrier `SetPasswordForm` from the
|
|
2087
|
+
* `WithInlineConsentForm` static-checkbox mixin onto this dynamic array.
|
|
1213
2088
|
*
|
|
1214
|
-
*
|
|
1215
|
-
*
|
|
2089
|
+
* Username MUST be bound before we fetch consents — schema places this step
|
|
2090
|
+
* AFTER `check-pending-invitation` (which sets `ctx.username` from the
|
|
2091
|
+
* pending-invite row) inside the `linkSent` accept-tail subflow, so the
|
|
2092
|
+
* `if (!ctx.username)` guard is belt-and-brace. `@Public()` is required
|
|
2093
|
+
* because this step fires on the anonymous magic-link resume side of the
|
|
2094
|
+
* workflow.
|
|
1216
2095
|
*/
|
|
1217
|
-
|
|
2096
|
+
prepareConsents(ctx: InviteWfCtx): undefined | Promise<undefined>;
|
|
1218
2097
|
inviteFlow(): void;
|
|
1219
2098
|
reInviteFlow(): void;
|
|
1220
2099
|
cancelInviteFlow(): void;
|
|
1221
|
-
init(ctx: InviteWfCtx): undefined
|
|
2100
|
+
init(ctx: InviteWfCtx): undefined | Promise<undefined>;
|
|
1222
2101
|
prepareAvailableRoles(ctx: InviteWfCtx): Promise<undefined>;
|
|
1223
|
-
selectSendMode(
|
|
1224
|
-
|
|
1225
|
-
action?: string;
|
|
1226
|
-
} | undefined, ctx: InviteWfCtx): unknown;
|
|
1227
|
-
adminInviteForm(input: {
|
|
1228
|
-
email?: string;
|
|
1229
|
-
firstName?: string;
|
|
1230
|
-
lastName?: string;
|
|
1231
|
-
roles?: string[];
|
|
1232
|
-
action?: string;
|
|
1233
|
-
} | undefined, ctx: InviteWfCtx): Promise<unknown>;
|
|
2102
|
+
selectSendMode(ctx: InviteWfCtx): unknown;
|
|
2103
|
+
adminInviteForm(ctx: InviteWfCtx): Promise<unknown>;
|
|
1234
2104
|
inferRolesStep(ctx: InviteWfCtx): Promise<undefined>;
|
|
1235
|
-
|
|
2105
|
+
/**
|
|
2106
|
+
* Build the extras dict that `create-user` merges into the new user
|
|
2107
|
+
* row. Calls `prepareUser({email, firstName, lastName, roles, invitedBy})`
|
|
2108
|
+
* and writes the result onto `ctx.userExtras`. Split out of the old
|
|
2109
|
+
* `invitePreCreateUser` step so consumers can inject e.g. a
|
|
2110
|
+
* tenant-validation step between extras-build and create-user without
|
|
2111
|
+
* copying the createUser body.
|
|
2112
|
+
*/
|
|
2113
|
+
buildUserExtras(ctx: InviteWfCtx): Promise<undefined>;
|
|
2114
|
+
/**
|
|
2115
|
+
* Create the user row from `ctx.userExtras` (plus the admin-supplied
|
|
2116
|
+
* `ctx.roles`), translate store-level CONFLICT into HTTP 409, then stamp
|
|
2117
|
+
* `pendingInvitation = true` via a deep-merge update so the
|
|
2118
|
+
* `createUser`-applied account defaults (`active: false`, `locked: false`)
|
|
2119
|
+
* survive. Split out of the old `invitePreCreateUser` step so consumers can
|
|
2120
|
+
* override extras-build (`build-user-extras`) without touching the
|
|
2121
|
+
* store-write transaction.
|
|
2122
|
+
*/
|
|
2123
|
+
createUserStep(ctx: InviteWfCtx): Promise<undefined>;
|
|
1236
2124
|
sendInviteEmail(ctx: InviteWfCtx): unknown;
|
|
1237
2125
|
returnShareableLink(ctx: InviteWfCtx): unknown;
|
|
1238
|
-
loadPendingUser(
|
|
1239
|
-
email?: string;
|
|
1240
|
-
action?: string;
|
|
1241
|
-
} | undefined, ctx: InviteWfCtx): Promise<unknown>;
|
|
2126
|
+
loadPendingUser(ctx: InviteWfCtx): Promise<unknown>;
|
|
1242
2127
|
checkPendingInvitation(ctx: InviteWfCtx): Promise<undefined>;
|
|
1243
|
-
idempotentRedirect(ctx: InviteWfCtx): undefined
|
|
1244
|
-
preparePasswordRules(ctx: InviteWfCtx): undefined
|
|
1245
|
-
createPasswordForm(
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
2128
|
+
idempotentRedirect(ctx: InviteWfCtx): undefined | Promise<undefined>;
|
|
2129
|
+
preparePasswordRules(ctx: InviteWfCtx): undefined | Promise<undefined>;
|
|
2130
|
+
createPasswordForm(ctx: InviteWfCtx): Promise<unknown>;
|
|
2131
|
+
/**
|
|
2132
|
+
* Build the `MfaEnrollDeps` payload shared by all three invite enrollment
|
|
2133
|
+
* step bodies. Sets `ctx.enrollMode` (mirrored onto ctx so
|
|
2134
|
+
* `EnrollPickMethodForm` can hide the `skip` action unless mode is
|
|
2135
|
+
* `'optional'`). Omits `onComplete` because invite's enrollment while-loop
|
|
2136
|
+
* is gated on `!enrollDone` directly — no mirror needed.
|
|
2137
|
+
*/
|
|
2138
|
+
private buildInviteEnrollDeps;
|
|
2139
|
+
/**
|
|
2140
|
+
* Prepare MFA enrolment setup: writes `ctx.mfaMode`,
|
|
2141
|
+
* `ctx.availableMfaTransports`, and pre-picks `ctx.enrollMethod` when only
|
|
2142
|
+
* one transport is available. Override to compute any of the three from
|
|
2143
|
+
* tenant policy / invitee role / request context in a single hook. Return
|
|
2144
|
+
* type allows a sync override (skip the promise round-trip) when no async
|
|
2145
|
+
* work is needed.
|
|
2146
|
+
*/
|
|
2147
|
+
inviteSetupMfa(ctx: InviteWfCtx): undefined | Promise<undefined>;
|
|
2148
|
+
/**
|
|
2149
|
+
* Forced MFA enrollment — Phase 1 (pick method). Auto-picks a single
|
|
2150
|
+
* transport, otherwise pauses for the picker form. When TOTP is picked, the
|
|
2151
|
+
* secret is provisioned in the same step body (see `enrollPickPhase`).
|
|
2152
|
+
*/
|
|
2153
|
+
inviteEnrollPickMethod(ctx: InviteWfCtx): undefined | Promise<undefined>;
|
|
2154
|
+
/**
|
|
2155
|
+
* Forced MFA enrollment — Phase 2 (collect sms/email address + send
|
|
2156
|
+
* pincode). Gated out for totp by the schema condition.
|
|
2157
|
+
*/
|
|
2158
|
+
inviteEnrollAddress(ctx: InviteWfCtx): undefined | Promise<undefined>;
|
|
2159
|
+
/**
|
|
2160
|
+
* Forced MFA enrollment — Phase 3 (verify code + activate method). Sets
|
|
2161
|
+
* `enrollDone` on success, which the schema's enrollment while-loop reads
|
|
2162
|
+
* as the exit signal directly.
|
|
2163
|
+
*/
|
|
2164
|
+
inviteEnrollConfirm(ctx: InviteWfCtx): undefined | Promise<undefined>;
|
|
2165
|
+
collectProfile(ctx: InviteWfCtx): Promise<unknown>;
|
|
1251
2166
|
applyProfileStep(ctx: InviteWfCtx): Promise<undefined>;
|
|
2167
|
+
/**
|
|
2168
|
+
* Consumer extension point — override in your subclass to inject extra
|
|
2169
|
+
* accept-tail logic (input pauses, alt actions, persistence). Default:
|
|
2170
|
+
* no-op. Runs AFTER profile collection, BEFORE activation. Signature is
|
|
2171
|
+
* intentionally arg-less; read ctx + form input via composables
|
|
2172
|
+
* (`useWfState`, `useAtscriptWf`) in the override body.
|
|
2173
|
+
*/
|
|
2174
|
+
inviteExtraStep(): unknown;
|
|
2175
|
+
/**
|
|
2176
|
+
* Batched consent persistence — delegates to
|
|
2177
|
+
* `AuthWorkflowBase.runPersistConsents`. See that helper for the full
|
|
2178
|
+
* audit-friendly-default / idempotency / silent-drop contract.
|
|
2179
|
+
*/
|
|
2180
|
+
persistConsentsStep(ctx: InviteWfCtx): Promise<undefined>;
|
|
1252
2181
|
unsetPendingInvitation(ctx: InviteWfCtx): Promise<undefined>;
|
|
1253
2182
|
activateUser(ctx: InviteWfCtx): Promise<undefined>;
|
|
1254
|
-
confirmation(ctx: InviteWfCtx): undefined
|
|
1255
|
-
freshLoginFinish(
|
|
2183
|
+
confirmation(ctx: InviteWfCtx): undefined | Promise<undefined>;
|
|
2184
|
+
freshLoginFinish(ctx: InviteWfCtx): undefined | Promise<undefined>;
|
|
1256
2185
|
autoLoginFinish(ctx: InviteWfCtx): Promise<undefined>;
|
|
1257
|
-
cancelInvite(
|
|
1258
|
-
email?: string;
|
|
1259
|
-
} | undefined, ctx: InviteWfCtx): Promise<unknown>;
|
|
2186
|
+
cancelInvite(ctx: InviteWfCtx): Promise<unknown>;
|
|
1260
2187
|
private abort;
|
|
1261
2188
|
private loadUserOrNull;
|
|
1262
2189
|
private emitAudit;
|
|
@@ -1264,13 +2191,13 @@ declare class InviteWorkflow {
|
|
|
1264
2191
|
//#endregion
|
|
1265
2192
|
//#region src/workflows/default-workflows.d.ts
|
|
1266
2193
|
declare class DefaultLoginWorkflow extends LoginWorkflow {
|
|
1267
|
-
constructor(users: UserService, auth: AuthCredential);
|
|
2194
|
+
constructor(users: UserService, auth: AuthCredential, authOpts: AuthOpts, consentStore: ConsentStore);
|
|
1268
2195
|
}
|
|
1269
2196
|
declare class DefaultInviteWorkflow extends InviteWorkflow {
|
|
1270
|
-
constructor(users: UserService, auth: AuthCredential);
|
|
2197
|
+
constructor(users: UserService, auth: AuthCredential, authOpts: AuthOpts, consentStore: ConsentStore);
|
|
1271
2198
|
}
|
|
1272
2199
|
declare class DefaultRecoveryWorkflow extends RecoveryWorkflow {
|
|
1273
|
-
constructor(users: UserService, auth: AuthCredential);
|
|
2200
|
+
constructor(users: UserService, auth: AuthCredential, authOpts: AuthOpts, consentStore: ConsentStore);
|
|
1274
2201
|
}
|
|
1275
2202
|
//#endregion
|
|
1276
2203
|
//#region src/workflows/auth-email-outlet.d.ts
|
|
@@ -1287,4 +2214,12 @@ interface AuthEmailOutletDeps {
|
|
|
1287
2214
|
*/
|
|
1288
2215
|
declare function createAuthEmailOutlet(deps: AuthEmailOutletDeps): WfOutlet;
|
|
1289
2216
|
//#endregion
|
|
1290
|
-
|
|
2217
|
+
//#region src/workflows/auth-shareable-link-outlet.d.ts
|
|
2218
|
+
interface AuthShareableLinkOutletDeps {
|
|
2219
|
+
buildMagicLinkUrl: BuildMagicLinkUrl$1;
|
|
2220
|
+
/** Fallback TTL when the workflow context omits `expiresAtMs`. */
|
|
2221
|
+
magicLinkTtlMs: (kind: AuthEmailKind$1) => number;
|
|
2222
|
+
}
|
|
2223
|
+
declare function createAuthShareableLinkOutlet(deps: AuthShareableLinkOutletDeps): WfOutlet;
|
|
2224
|
+
//#endregion
|
|
2225
|
+
export { type AuditEmitter, type AuditEvent, type AuthBindings, type AuthContext, AuthController, type AuthEmailEvent, type AuthEmailKind, type AuthEmailOutletDeps, AuthGuarded, type AuthLoginResponse, type AuthLogoutBody, type AuthOkResponse, type AuthOptions, AuthOpts, type AuthRefreshBody, type AuthShareableLinkOutletDeps, type AuthSmsEvent, type AuthSmsKind, type BuildMagicLinkUrl, type ConcurrencyLimitOptions, type ConsentDescriptor, type ConsentEvent, ConsentStore, DEFAULT_AUTH_WORKFLOWS, DefaultInviteWorkflow, DefaultLoginWorkflow, DefaultRecoveryWorkflow, type DeliverEmail, type DeliverPayload, type DeliverSms, type DuplicateAction, type EmailSender, type InvitePolicyOverrides, type InvitePrepareUserInput, type InviteSendMode, type InviteWfCtx, InviteWorkflow, type InviteWorkflowOpts, type IssueResult, type LoginPolicyOverrides, type LoginRedirect, type LoginWfCtx, LoginWorkflow, type LoginWorkflowOpts, type MfaSummary, type MfaTransport, type PreparedUserInput, Public, type RecoveryDeliveryMode, type RecoveryOtpTransport, type RecoveryPolicyOverrides, type RecoveryWfCtx, RecoveryWorkflow, type RecoveryWorkflowOpts, type ResolvedAuthCookieConfig, type ResolvedAuthOptions, type ResolvedInviteWorkflowOpts, type ResolvedLoginWorkflowOpts, type ResolvedRecoveryWorkflowOpts, type SmsSender, type SsoProvider, type TAuthMeta, UserId, WfTrigger, type WfTriggerOpts, WfTriggerProvider, authGuardInterceptor, createAuthEmailOutlet, createAuthShareableLinkOutlet, generateMagicLinkToken, getAuthMate, mergeInviteOpts, mergeLoginOpts, mergeRecoveryOpts, parseInviteRoles, useAuth };
|