@aooth/auth-moost 0.1.1

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.
@@ -0,0 +1,1279 @@
1
+ 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
+ import * as _$_wooksjs_event_core0 from "@wooksjs/event-core";
3
+ 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
+ import { TrustedDeviceRecord, UserCredentials, UserService } from "@aooth/user";
7
+ import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
8
+
9
+ //#region src/auth.config.d.ts
10
+ /** Resolved cookie attributes. Same shape is used for both access + refresh. */
11
+ interface ResolvedAuthCookieConfig {
12
+ name: string;
13
+ secure: boolean;
14
+ sameSite: "lax" | "strict" | "none";
15
+ httpOnly: boolean;
16
+ path: string;
17
+ domain?: string;
18
+ }
19
+ interface AuthOptions {
20
+ cookie?: Partial<ResolvedAuthCookieConfig>;
21
+ /**
22
+ * Refresh-cookie attribute overrides. Defaults to `name='aooth_refresh'`,
23
+ * `path='/auth/refresh'`; inherits `secure/sameSite/httpOnly/domain` from
24
+ * the access cookie unless overridden here.
25
+ */
26
+ refreshCookie?: Partial<ResolvedAuthCookieConfig>;
27
+ /** Read the session token from a cookie. Default: `true`. */
28
+ enableCookie?: boolean;
29
+ /** Read the session token from `Authorization: Bearer ...`. Default: `true`. */
30
+ enableBearer?: boolean;
31
+ }
32
+ /**
33
+ * Fully-resolved options carried by `authGuardInterceptor(opts)` through the
34
+ * HTTP event slot. `useAuth().options` returns this shape so consumers can type
35
+ * variables without re-resolving defaults.
36
+ */
37
+ interface ResolvedAuthOptions {
38
+ cookie: ResolvedAuthCookieConfig;
39
+ /**
40
+ * Narrow `path: '/auth/refresh'` ensures the refresh token only travels to
41
+ * the refresh endpoint. Transport attrs (`secure/sameSite/httpOnly/domain`)
42
+ * are inherited from {@link cookie} unless overridden.
43
+ */
44
+ refreshCookie: ResolvedAuthCookieConfig;
45
+ enableCookie: boolean;
46
+ enableBearer: boolean;
47
+ }
48
+ //#endregion
49
+ //#region src/auth.guard.d.ts
50
+ /**
51
+ * `GUARD`-priority interceptor factory that authenticates incoming requests.
52
+ *
53
+ * Returns a configured `TInterceptorDef`. Each invocation captures its own
54
+ * resolved options and stashes them onto the HTTP event context's
55
+ * `authOptionsKey` slot so `useAuth()` (and the workflows that depend on it)
56
+ * can read the same transport config.
57
+ *
58
+ * Token extraction precedence: `Authorization: Bearer ...` wins over cookie
59
+ * when both transports are enabled. On `@Public()` routes a missing or
60
+ * invalid token leaves AuthContext as `null` and the handler runs anyway;
61
+ * on protected routes it throws `HttpError(401)`.
62
+ *
63
+ * Never auto-refreshes — refresh is a separate REST endpoint.
64
+ *
65
+ * No-ops on non-HTTP event contexts (workflow steps, CLI, WS messages). The
66
+ * guard reads tokens from HTTP headers/cookies; nothing to read elsewhere.
67
+ * Authorization for workflow steps is the step's own responsibility (e.g.
68
+ * an admin-only invite endpoint protects its outlet HTTP route, not the
69
+ * step handler).
70
+ */
71
+ declare function authGuardInterceptor(opts?: AuthOptions): TInterceptorDef;
72
+ /**
73
+ * Decorator-factory sugar for attaching `authGuardInterceptor(opts)` to a
74
+ * specific controller or method instead of globally. Equivalent to
75
+ * `@Intercept(authGuardInterceptor(opts))`.
76
+ */
77
+ declare function AuthGuarded(opts?: AuthOptions): ClassDecorator & MethodDecorator;
78
+ //#endregion
79
+ //#region ../../node_modules/.pnpm/@wooksjs+event-wf@0.7.12_@prostojs+logger@0.4.3_@wooksjs+event-core@0.7.12_@wooksjs+eve_83c9b5cff779134c35c379089be157ae/node_modules/@wooksjs/event-wf/dist/index.d.ts
80
+ interface WfFinishedResponse {
81
+ type: 'redirect' | 'data';
82
+ /** Redirect URL or response body */
83
+ value: unknown;
84
+ /** HTTP status code (default 200 for data, 302 for redirect) */
85
+ status?: number;
86
+ /** Cookies to set */
87
+ cookies?: Record<string, {
88
+ value: string;
89
+ options?: Record<string, unknown>;
90
+ }>;
91
+ }
92
+ /**
93
+ * Composable to set the completion response for a finished workflow.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * // Redirect after login
98
+ * useWfFinished().set({ type: 'redirect', value: '/dashboard' })
99
+ *
100
+ * // Return data
101
+ * useWfFinished().set({ type: 'data', value: { success: true } })
102
+ * ```
103
+ */
104
+ //#endregion
105
+ //#region src/auth.dto.d.ts
106
+ /** POST /auth/refresh request body. Falls back to the refresh cookie when absent. */
107
+ interface AuthRefreshBody {
108
+ refreshToken?: string;
109
+ }
110
+ /**
111
+ * Optional POST /auth/logout body. The refresh cookie's narrow `path:
112
+ * '/auth/refresh'` keeps the browser from sending it to `/auth/logout`, so
113
+ * token-style clients (or clients that want to revoke a paired refresh in the
114
+ * same call) submit it explicitly here.
115
+ */
116
+ interface AuthLogoutBody {
117
+ refreshToken?: string;
118
+ }
119
+ /**
120
+ * POST /auth/refresh response body. Also returned by workflow finalize steps
121
+ * (e.g. `LoginWorkflow`) when issuing tokens after a successful flow.
122
+ *
123
+ * Tokens are populated only when `enableBearer` is true. With `enableBearer=false`
124
+ * the body still echoes `userId` + `accessExpiresAt` so the caller can schedule
125
+ * a silent refresh; the actual tokens travel only in cookies.
126
+ */
127
+ interface AuthLoginResponse {
128
+ userId: string;
129
+ accessExpiresAt: number;
130
+ refreshExpiresAt?: number;
131
+ accessToken?: string;
132
+ refreshToken?: string;
133
+ }
134
+ /** POST /auth/logout response body. */
135
+ interface AuthOkResponse {
136
+ ok: true;
137
+ }
138
+ //#endregion
139
+ //#region src/auth.composables.d.ts
140
+ interface AuthBindings {
141
+ getAuthContext<TClaims extends object = Record<string, unknown>>(): AuthContext$1<TClaims> | null;
142
+ /** @throws `HttpError(401)` if no `AuthContext` is present in the event. */
143
+ getUserId(): string;
144
+ isAuthenticated(): boolean;
145
+ /** @throws `HttpError(500)` when no `authGuardInterceptor(opts)` is on the chain. */
146
+ readonly options: ResolvedAuthOptions;
147
+ /** Same precedence as `authGuardInterceptor`: Bearer wins when both enabled. */
148
+ extractToken(): string | undefined;
149
+ writeCookies(issue: IssueResult$1): void;
150
+ clearCookies(): void;
151
+ buildLoginResponse(userId: string, issue: IssueResult$1): AuthLoginResponse;
152
+ buildFinishedCookies(issue: IssueResult$1): WfFinishedResponse["cookies"];
153
+ /** Build cookie attrs from the resolved access-cookie config, with optional overrides. */
154
+ cookieAttrs(extra?: TCookieAttributesInput): TCookieAttributesInput;
155
+ }
156
+ /**
157
+ * Composable for accessing the current event's auth state + transport helpers.
158
+ *
159
+ * `defineWook` memoizes the bindings object per event so multiple calls share
160
+ * one closure. Identity bindings (`getAuthContext`/`getUserId`/`isAuthenticated`)
161
+ * read the `authContextKey` slot populated by `authGuardInterceptor`. The
162
+ * remaining closures read the `authOptionsKey` slot stashed by that same
163
+ * interceptor; calling them outside an HTTP-or-HTTP-parented event throws
164
+ * `HttpError(500)` loudly — that's a configuration error, not a runtime
165
+ * fallback case.
166
+ */
167
+ declare const useAuth: _$_wooksjs_event_core0.WookComposable<AuthBindings>;
168
+ //#endregion
169
+ //#region src/auth.decorator.d.ts
170
+ /**
171
+ * Marks a route or controller as fully public — opts out of BOTH
172
+ * authentication (auth-moost's bearer guard) AND authorization
173
+ * (arbac-moost's `arbacAuthorizeInterceptor`).
174
+ *
175
+ * `authGuardInterceptor` still runs on `@Public()` handlers — it populates the
176
+ * AuthContext when a valid credential is presented, but does NOT throw when
177
+ * the token is missing or invalid (the handler runs with a `null` AuthContext).
178
+ * The ARBAC interceptor short-circuits entirely via its `isPublic` check.
179
+ *
180
+ * Method-level decoration overrides class-level.
181
+ */
182
+ declare const Public: () => ClassDecorator & MethodDecorator;
183
+ /**
184
+ * Resolves the authenticated user's id (string) for a handler parameter.
185
+ *
186
+ * Delegates to `useAuth().getUserId()`, which throws `HttpError(401)` when no
187
+ * `AuthContext` is present in the event. There is no `@User()` counterpart —
188
+ * `AuthContext` is credential context, not user profile data, and this library
189
+ * does not own a user profile type.
190
+ */
191
+ declare const UserId: () => ParameterDecorator & PropertyDecorator;
192
+ //#endregion
193
+ //#region src/auth.mate.d.ts
194
+ /**
195
+ * Auth metadata fields written by `@Public()` and read by `authGuardInterceptor`.
196
+ * Merged into `TMoostMetadata` so other moost-aware tooling sees them.
197
+ */
198
+ interface TAuthMeta {
199
+ authPublic?: boolean;
200
+ }
201
+ declare module "moost" {
202
+ interface TMoostMetadata extends TAuthMeta {}
203
+ }
204
+ type AuthMate = Mate<TMoostMetadata & {
205
+ params: TMateParamMeta[];
206
+ }, TMoostMetadata & {
207
+ params: TMateParamMeta[];
208
+ }>;
209
+ declare function getAuthMate(): AuthMate;
210
+ //#endregion
211
+ //#region src/auth.controller.d.ts
212
+ /** Workflows allowed by the bundled `/auth/trigger` endpoint. Subclasses override `triggerWf()` to extend. */
213
+ declare const DEFAULT_AUTH_WORKFLOWS: readonly ["auth.login", "auth.recovery", "auth.invite"];
214
+ /**
215
+ * Public REST endpoints for credential management. Four endpoints total:
216
+ *
217
+ * - `POST /auth/logout` — best-effort token revocation + cookie clear.
218
+ * - `POST /auth/refresh` — rotate access/refresh tokens.
219
+ * - `GET /auth/status` — return the current `AuthContext`.
220
+ * - `POST /auth/trigger` — single workflow trigger covering `auth.login`,
221
+ * `auth.recovery`, and `auth.invite`.
222
+ *
223
+ * The historical `/auth/login` and `/auth/password` endpoints were dropped —
224
+ * both flows go through the workflow trigger now (full MFA / SSO / etc.
225
+ * surface lives in `LoginWorkflow` and `RecoveryWorkflow`).
226
+ *
227
+ * Exported so consumers can subclass to add app-specific workflow ids to the
228
+ * allow-list (override `triggerWf()` with a different `@WfTrigger({ allow })`)
229
+ * or to inject custom outlets / state stores by subclassing `WfTriggerProvider`.
230
+ */
231
+ declare class AuthController {
232
+ protected readonly auth: AuthCredential;
233
+ constructor(auth: AuthCredential);
234
+ logout(body: AuthLogoutBody | undefined): Promise<AuthOkResponse>;
235
+ refresh(body: AuthRefreshBody | undefined): Promise<AuthLoginResponse>;
236
+ status(): AuthContext$1;
237
+ triggerWf(): void;
238
+ }
239
+ //#endregion
240
+ //#region src/wf-trigger/decorator.d.ts
241
+ interface WfTriggerOpts {
242
+ /** Whitelist of workflow ids the trigger may start/resume. Defaults to the provider's setting. */
243
+ allow?: string[];
244
+ /** Per-endpoint token wire override. Defaults to the provider's wire (`{read:['body','query','cookie'], write:'body', name:'wfs'}`). */
245
+ token?: WfOutletTokenConfig;
246
+ }
247
+ /**
248
+ * Method decorator that turns a handler into a workflow trigger.
249
+ *
250
+ * The handler may have an empty body — the interceptor's after-phase invokes
251
+ * `WfTriggerProvider.handle()` when the handler returns `undefined`. Subclasses
252
+ * that need to short-circuit (e.g. emit a custom error response) just return a
253
+ * non-undefined value from the overridden handler and the trigger is skipped.
254
+ *
255
+ * The single `useControllerContext().instantiate(...)` call is the one
256
+ * documented escape hatch: interceptors are functions, not classes, so there's
257
+ * no ctor to inject into. Every class still uses constructor injection.
258
+ */
259
+ declare const WfTrigger: (opts?: WfTriggerOpts) => ClassDecorator & MethodDecorator;
260
+ //#endregion
261
+ //#region src/wf-trigger/provider.d.ts
262
+ /**
263
+ * DI singleton owning the workflow-trigger wiring: state persistence, outlets,
264
+ * and token wire. Consumers subclass to swap the state store, add outlets
265
+ * (email, SMS, ...), or override `handle()` for per-request dispatch logic.
266
+ *
267
+ * Defaults are intentionally minimal: in-memory state + HTTP outlet only.
268
+ * Production deployments swap in a persistent `WfStateStore` and any outlets
269
+ * they need by extending this class and re-binding via `setReplaceRegistry`.
270
+ *
271
+ * Uses `handleAsOutletRequest` (not `MoostWf.handleOutlet`) because the atscript
272
+ * wrapper restores the `finished: true` marker that `<AsWfForm>` keys off — the
273
+ * bare wooks request handler strips it during `useWfFinished()` unwrap.
274
+ */
275
+ declare class WfTriggerProvider {
276
+ protected readonly wf: MoostWf;
277
+ protected state: WfStateStrategy;
278
+ protected outlets: WfOutlet[];
279
+ protected token: WfOutletTokenConfig;
280
+ constructor(wf: MoostWf);
281
+ handle(opts?: {
282
+ allow?: string[];
283
+ token?: WfOutletTokenConfig;
284
+ }): Promise<unknown>;
285
+ }
286
+ //#endregion
287
+ //#region src/audit/index.d.ts
288
+ /**
289
+ * Audit event emitter — used by `LoginWorkflow.audit-login` (and future
290
+ * recovery / invite audit steps) to fan out login.success and similar events
291
+ * to consumer-supplied sinks (DB table, log file, Kafka topic).
292
+ *
293
+ * Aoothjs ships no concrete sink. Workflow subclasses override the
294
+ * `audit(event)` protected method to wire their preferred sink; when not
295
+ * overridden the workflow's default implementation is a no-op.
296
+ */
297
+ interface AuditEvent {
298
+ kind: string;
299
+ /** Auth-scoped user identity (the `username` resolved by the workflow). */
300
+ userId?: string;
301
+ /** Workflow id that emitted the event (e.g. `auth.login`). */
302
+ workflow?: string;
303
+ /** Source IP (when the workflow could resolve one). */
304
+ ip?: string;
305
+ /** User-agent header. */
306
+ userAgent?: string;
307
+ /** Free-form payload — `method`, `tenantId`, etc. */
308
+ [key: string]: unknown;
309
+ }
310
+ interface AuditEmitter {
311
+ emit(event: AuditEvent): Promise<void> | void;
312
+ }
313
+ //#endregion
314
+ //#region src/workflows/login.workflow.options.d.ts
315
+ type LoginRedirect = "referer" | "home" | false | null;
316
+ type MfaTransport = "sms" | "email" | "totp";
317
+ interface SsoProvider {
318
+ id: string;
319
+ label: string;
320
+ url: string;
321
+ }
322
+ interface ConcurrencyLimitOptions {
323
+ max: number;
324
+ onLimit: "reject" | "kickPrompt";
325
+ }
326
+ 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
+ deviceTrust?: {
357
+ enabled?: boolean;
358
+ optIn?: boolean;
359
+ cookieName?: string;
360
+ ttlMs?: number;
361
+ skipsMfa?: boolean;
362
+ bindsTo?: "cookie" | "cookie+ip";
363
+ };
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
+ /**
382
+ * Replaceable form schemas. Each field defaults to the corresponding
383
+ * `.as` form shipped under `@aooth/auth-moost/atscript/models`; supply a
384
+ * subset to override only the forms you want to swap.
385
+ */
386
+ forms?: {
387
+ askEmail?: TAtscriptAnnotatedType;
388
+ askPhone?: TAtscriptAnnotatedType;
389
+ backupCode?: TAtscriptAnnotatedType;
390
+ concurrencyLimit?: TAtscriptAnnotatedType;
391
+ consentMarketing?: TAtscriptAnnotatedType;
392
+ loginCredentials?: TAtscriptAnnotatedType;
393
+ mfaCode?: TAtscriptAnnotatedType;
394
+ personaSelect?: TAtscriptAnnotatedType;
395
+ pincode?: TAtscriptAnnotatedType;
396
+ profileComplete?: TAtscriptAnnotatedType;
397
+ select2fa?: TAtscriptAnnotatedType;
398
+ setPassword?: TAtscriptAnnotatedType;
399
+ tenantSelect?: TAtscriptAnnotatedType;
400
+ termsAccept?: TAtscriptAnnotatedType;
401
+ };
402
+ }
403
+ /**
404
+ * Fully-resolved view used by the workflow at runtime — every nested group is
405
+ * always populated by `mergeLoginOpts`, so schema conditions can read
406
+ * `ctx.opts.<group>.<flag>` directly without optional chaining.
407
+ *
408
+ * Fields without sensible defaults (e.g. `termsVersion`, `concurrencyLimit`)
409
+ * stay optional inside their group.
410
+ */
411
+ 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
+ deviceTrust: {
442
+ enabled: boolean;
443
+ optIn: boolean;
444
+ cookieName: string;
445
+ ttlMs: number;
446
+ skipsMfa: boolean;
447
+ bindsTo: "cookie" | "cookie+ip";
448
+ };
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
+ forms: {
467
+ askEmail: TAtscriptAnnotatedType;
468
+ askPhone: TAtscriptAnnotatedType;
469
+ backupCode: TAtscriptAnnotatedType;
470
+ concurrencyLimit: TAtscriptAnnotatedType;
471
+ consentMarketing: TAtscriptAnnotatedType;
472
+ loginCredentials: TAtscriptAnnotatedType;
473
+ mfaCode: TAtscriptAnnotatedType;
474
+ personaSelect: TAtscriptAnnotatedType;
475
+ pincode: TAtscriptAnnotatedType;
476
+ profileComplete: TAtscriptAnnotatedType;
477
+ select2fa: TAtscriptAnnotatedType;
478
+ setPassword: TAtscriptAnnotatedType;
479
+ tenantSelect: TAtscriptAnnotatedType;
480
+ termsAccept: TAtscriptAnnotatedType;
481
+ };
482
+ }
483
+ /**
484
+ * Deep-merge defaults with the user-supplied nested pojo. Each group has its
485
+ * own `{ ...defaults, ...input }` line — small enough that pulling in lodash
486
+ * would be silly.
487
+ */
488
+ declare function mergeLoginOpts(opts?: LoginWorkflowOpts): ResolvedLoginWorkflowOpts;
489
+ //#endregion
490
+ //#region src/workflows/login.workflow.d.ts
491
+ interface LoginWfCtx {
492
+ opts?: ResolvedLoginWorkflowOpts;
493
+ username?: string;
494
+ /** Legacy alias for `pwReset`; kept until tests migrate. */
495
+ mfaRequired?: boolean;
496
+ isPasswordInitial?: boolean;
497
+ usedMagicLink?: boolean;
498
+ email?: string;
499
+ emailConfirmed?: boolean;
500
+ phone?: string;
501
+ phoneConfirmed?: boolean;
502
+ mfaEnrolledMethods?: MfaSummary[];
503
+ mfaMethod?: "sms" | "email" | "totp";
504
+ mfaSaveAsDefault?: boolean;
505
+ ignoreMfaDefault?: boolean;
506
+ mfaChecked?: boolean;
507
+ /** Counter incremented by the `risk-step-up` step so MFA reruns for the extra factor. */
508
+ mfaRunsRemaining?: number;
509
+ pin?: string;
510
+ pinExpire?: number;
511
+ pinTimeout?: number;
512
+ pinSentTo?: string;
513
+ deviceTrustToken?: string;
514
+ /** Set true at MFA gate when no trust cookie matched → trigger `notify-new-device`. */
515
+ newDevice?: boolean;
516
+ /** Captured from the OTP/pincode form when `opts.deviceTrust.optIn`. */
517
+ rememberDevice?: boolean;
518
+ termsAcceptedVersion?: string;
519
+ profileMissingFields?: string[];
520
+ availableTenants?: Array<{
521
+ id: string;
522
+ name: string;
523
+ }>;
524
+ selectedTenantId?: string;
525
+ availablePersonas?: Array<{
526
+ id: string;
527
+ label: string;
528
+ }>;
529
+ selectedPersonaId?: string;
530
+ riskStepUpReason?: string;
531
+ activeSessions?: number;
532
+ passwordChanged?: boolean;
533
+ termsAcceptedDone?: boolean;
534
+ profileApplied?: boolean;
535
+ consentApplied?: boolean;
536
+ tokensIssued?: boolean;
537
+ redirectUrl?: string;
538
+ /**
539
+ * Set true by abort alt-actions (`logout`, `decline`, `cancel`). All terminal
540
+ * steps (`issue`, `audit-login`, `notify-new-device`, `redirect`) gate on
541
+ * `!ctx.aborted` so the abort response set via `useWfFinished()` stays.
542
+ */
543
+ aborted?: boolean;
544
+ /** Tracks whether `risk-step-up` has already been evaluated this iteration. */
545
+ riskStepUpEvaluated?: boolean;
546
+ }
547
+ interface MfaSummary {
548
+ kind: "sms" | "email" | "totp";
549
+ /** Underlying `MfaMethod.name` so the workflow can call into UserService. */
550
+ methodName: string;
551
+ masked: string;
552
+ isDefault: boolean;
553
+ }
554
+ /**
555
+ * Unified payload for `deliver()` — discriminated by `channel`. `kind`
556
+ * narrows further to the template the consumer should render. The two
557
+ * channels do not share a fields set (email carries `url` for magic links
558
+ * and `expiresAt`; SMS always carries a pincode + `ttlMs`).
559
+ */
560
+ interface DeliverEmail {
561
+ channel: "email";
562
+ /** Template kind — discriminator the consumer uses to pick which email template to render. */
563
+ kind: AuthEmailKind$1;
564
+ recipient: string;
565
+ /** Numeric pincode (set for `*.pincode` kinds). */
566
+ code?: string;
567
+ /** Magic-link URL (set for `*.magicLink` kinds). */
568
+ url?: string;
569
+ /** Absolute expiry timestamp for the link/code (ms epoch). */
570
+ expiresAt?: number;
571
+ /** Associated user id, when known. */
572
+ userId?: string;
573
+ /** Extra context (e.g. `roles` for invite emails, IP/UA for notifyNewDevice). */
574
+ metadata?: Record<string, unknown>;
575
+ }
576
+ interface DeliverSms {
577
+ channel: "sms";
578
+ kind: AuthSmsKind$1;
579
+ recipient: string;
580
+ /** SMS always carries a pincode — that's the only thing SMS gets used for in this lib. */
581
+ code: string;
582
+ ttlMs?: number;
583
+ userId?: string;
584
+ }
585
+ type DeliverPayload = DeliverEmail | DeliverSms;
586
+ declare class LoginWorkflow {
587
+ protected readonly opts: ResolvedLoginWorkflowOpts;
588
+ protected readonly users: UserService;
589
+ protected readonly auth: AuthCredential;
590
+ constructor(opts: LoginWorkflowOpts, users: UserService, auth: AuthCredential);
591
+ /**
592
+ * Dispatch an email or SMS event. Default throws — consumers MUST override
593
+ * if any feature that emits is enabled (MFA pincode, ensureEmail/Phone OTP,
594
+ * notifyNewDevice). The throw surfaces at the HTTP layer as 500 on the
595
+ * first event that triggers a send, which is the fail-loud signal.
596
+ */
597
+ protected deliver(_payload: DeliverPayload): Promise<void>;
598
+ /**
599
+ * Emit an audit event. Default: no-op. Consumers override to fan out to
600
+ * their audit sink (DB table, log file, Kafka topic, …).
601
+ */
602
+ protected audit(_event: AuditEvent): Promise<void>;
603
+ /**
604
+ * Verify whether a presented trust-cookie token belongs to `userId` and is
605
+ * still valid. Default: delegates to `UserService.verifyTrustedDevice`
606
+ * (HMAC + persisted record + expiry + IP-binding). Override to use a
607
+ * different trust backend.
608
+ */
609
+ protected loadTrustedDevice(userId: string, token: string, ip?: string): Promise<boolean>;
610
+ /**
611
+ * Persist a freshly-issued trust record. Default: delegates to
612
+ * `UserService.addTrustedDevice` — the record is appended to the user's
613
+ * `trustedDevices` array on the user store. `userId` is the username the
614
+ * record belongs to (passed alongside since `TrustedDeviceRecord` itself
615
+ * carries no user identifier).
616
+ */
617
+ protected storeTrustedDevice(userId: string, record: TrustedDeviceRecord): Promise<void>;
618
+ /**
619
+ * Revoke a trust record. Default: delegates to
620
+ * `UserService.revokeTrustedDevice`. Currently unused by the workflow's own
621
+ * happy path but exposed so consumers can call it from their own "sign out
622
+ * everywhere" flows for symmetry with `storeTrustedDevice`.
623
+ */
624
+ protected revokeTrustedDevice(userId: string, token: string): Promise<void>;
625
+ /**
626
+ * Mint a new device-trust record + cookie value. Default: delegates to
627
+ * `UserService.issueTrustedDevice` — produces an HMAC-signed token bound to
628
+ * `userId` (+ `ip` when `bindsTo === 'cookie+ip'`). Consumers running
629
+ * multiple instances typically override `loadTrustedDevice`/
630
+ * `storeTrustedDevice` against Redis but keep this default.
631
+ */
632
+ protected issueTrustedDevice(userId: string, ip: string | undefined, ttlMs: number): Promise<TrustedDeviceRecord>;
633
+ flow(): void;
634
+ init(ctx: LoginWfCtx): undefined;
635
+ /**
636
+ * Returns the JSON-safe projection of `opts` stashed onto `ctx` for schema
637
+ * conditions to read. Default: drop the `forms` group (atscript form classes
638
+ * are not plain JSON) so `AsWfStore`'s plain-JSON persistence doesn't choke.
639
+ * Step bodies still consult the form classes via `this.opts.forms.*`.
640
+ *
641
+ * Consumers who put non-JSON values on `opts` (e.g. by extending the type)
642
+ * can override this to strip them.
643
+ */
644
+ protected snapshotOpts(opts: ResolvedLoginWorkflowOpts): ResolvedLoginWorkflowOpts;
645
+ credentials(input: {
646
+ username?: string;
647
+ password?: string;
648
+ action?: string;
649
+ } | undefined, ctx: LoginWfCtx): Promise<unknown>;
650
+ private handleCredentialsAlt;
651
+ /**
652
+ * Builds the redirect URL the `forgotPassword` alt-action navigates to.
653
+ * Receives whatever the user typed into the username field so the recovery
654
+ * page can pre-fill it. Default:
655
+ * `${alternateCredentials.recoveryUrl}?username=${encodeURIComponent(username ?? '')}`.
656
+ */
657
+ protected buildRecoveryUrl(username?: string): string;
658
+ magicLinkRequest(): never;
659
+ magicLinkSend(): never;
660
+ magicLinkVerified(): never;
661
+ passkey(): never;
662
+ ssoCallback(): never;
663
+ ensureEmail(input: {
664
+ email?: string;
665
+ code?: string;
666
+ } | undefined, ctx: LoginWfCtx): Promise<unknown>;
667
+ ensurePhone(input: {
668
+ phone?: string;
669
+ code?: string;
670
+ } | undefined, ctx: LoginWfCtx): Promise<unknown>;
671
+ checkTrustedDevice(ctx: LoginWfCtx): Promise<undefined>;
672
+ prepareMfaOptions(ctx: LoginWfCtx): Promise<undefined>;
673
+ select2fa(input: {
674
+ methodName?: string;
675
+ saveAsDefault?: boolean;
676
+ action?: string;
677
+ } | undefined, ctx: LoginWfCtx): Promise<unknown>;
678
+ pincodeSendLogin(ctx: LoginWfCtx): Promise<undefined>;
679
+ pincodeCheckLogin(input: {
680
+ code?: string;
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>;
689
+ /**
690
+ * Backup-code alt-action handler shared by `select2fa`, `pincode-check-login`,
691
+ * and `mfa-totp`. Validates against `BackupCodeForm` (alphanumeric +
692
+ * hyphen-grouped — `MfaCodeForm` is digits-only and rejects backup codes
693
+ * produced by `UserService.generateBackupCodes`).
694
+ */
695
+ private handleBackupCode;
696
+ mfaEnrollRequired(): never;
697
+ deviceTrust(ctx: LoginWfCtx): Promise<undefined>;
698
+ preparePasswordRules(ctx: LoginWfCtx): undefined;
699
+ createPasswordForm(input: {
700
+ newPassword?: string;
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>;
710
+ /**
711
+ * Persists the profile-complete payload onto the user record. Default:
712
+ * no-op (the workflow records the form was submitted but writes nothing).
713
+ * Consumers override to write into their user store.
714
+ */
715
+ 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
+ /**
721
+ * Persists the marketing consent decision. Default: no-op.
722
+ */
723
+ protected applyConsentMarketing(_username: string, _optIn: boolean): Promise<void>;
724
+ tenantSelect(input: {
725
+ tenantId?: string;
726
+ } | undefined, ctx: LoginWfCtx): Promise<unknown>;
727
+ /**
728
+ * Resolves the user's available tenants. Default: empty array. Consumers
729
+ * who enable `multiContext.tenantSelect` must override this to return the
730
+ * tenants the user belongs to.
731
+ */
732
+ protected loadTenants(_username: string): Promise<Array<{
733
+ id: string;
734
+ name: string;
735
+ }>>;
736
+ personaSelect(input: {
737
+ personaId?: string;
738
+ } | undefined, ctx: LoginWfCtx): Promise<unknown>;
739
+ /**
740
+ * Resolves the user's available personas. Default: empty array. Consumers
741
+ * who enable `multiContext.personaSelect` must override this.
742
+ */
743
+ protected loadPersonas(_username: string): Promise<Array<{
744
+ id: string;
745
+ label: string;
746
+ }>>;
747
+ concurrencyLimit(input: {
748
+ action?: string;
749
+ } | undefined, ctx: LoginWfCtx): Promise<unknown>;
750
+ /**
751
+ * Implements the "log out other sessions" branch of `sessionPolicy.concurrencyLimit`.
752
+ * Default: no-op. Consumers override to revoke sessions in their auth store.
753
+ */
754
+ protected logoutOtherSessions(_username: string): Promise<void>;
755
+ riskStepUp(ctx: LoginWfCtx): Promise<undefined>;
756
+ /**
757
+ * Risk-step-up assessor. Default: never requires an extra factor. Consumers
758
+ * override to inspect ctx (IP, geo, time since last login, etc.) and return
759
+ * `{require: true, reason: '…'}` to force an additional MFA round.
760
+ */
761
+ protected assessRiskStepUp(_ctx: LoginWfCtx): Promise<{
762
+ require: boolean;
763
+ reason?: string;
764
+ }>;
765
+ issue(ctx: LoginWfCtx): Promise<void>;
766
+ auditLogin(ctx: LoginWfCtx): Promise<undefined>;
767
+ notifyNewDevice(ctx: LoginWfCtx): Promise<undefined>;
768
+ redirect(ctx: LoginWfCtx): undefined;
769
+ /**
770
+ * Resolves the post-login redirect URL. Default reads
771
+ * `finalize.redirect`: `false` / `null` (the default) → no redirect, the
772
+ * `issue` step's data response stands (typical for SPAs/API clients);
773
+ * `'home'` → `/`; `'referer'` → request `Referer` header (undefined when
774
+ * absent, falling back to the data response).
775
+ *
776
+ * Consumers who want a computed redirect override this method.
777
+ */
778
+ protected resolveRedirect(_ctx: LoginWfCtx): string | undefined;
779
+ private resolveClientIp;
780
+ }
781
+ //#endregion
782
+ //#region src/workflows/recovery.workflow.options.d.ts
783
+ type RecoveryDeliveryMode = "magicLink" | "otp" | "choice";
784
+ type RecoveryOtpTransport = "sms" | "email";
785
+ 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
+ /**
811
+ * Replaceable form schemas. Each field defaults to the corresponding
812
+ * `.as` form shipped under `@aooth/auth-moost/atscript/models`.
813
+ */
814
+ forms?: {
815
+ emailIdentifier?: TAtscriptAnnotatedType;
816
+ pincode?: TAtscriptAnnotatedType;
817
+ recoveryFactor?: TAtscriptAnnotatedType;
818
+ recoveryModeSelect?: TAtscriptAnnotatedType;
819
+ setPassword?: TAtscriptAnnotatedType;
820
+ };
821
+ }
822
+ /**
823
+ * Fully-resolved view used by the workflow at runtime — every nested group is
824
+ * always populated by `mergeRecoveryOpts`, so schema conditions can read
825
+ * `ctx.opts.<group>.<flag>` directly without optional chaining.
826
+ */
827
+ 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
+ forms: {
853
+ emailIdentifier: TAtscriptAnnotatedType;
854
+ pincode: TAtscriptAnnotatedType;
855
+ recoveryFactor: TAtscriptAnnotatedType;
856
+ recoveryModeSelect: TAtscriptAnnotatedType;
857
+ setPassword: TAtscriptAnnotatedType;
858
+ };
859
+ }
860
+ /**
861
+ * Deep-merge defaults with the user-supplied nested pojo. Each group has its
862
+ * own `{ ...defaults, ...input }` line — small enough that pulling in lodash
863
+ * would be silly.
864
+ */
865
+ declare function mergeRecoveryOpts(opts?: RecoveryWorkflowOpts): ResolvedRecoveryWorkflowOpts;
866
+ //#endregion
867
+ //#region src/workflows/recovery.workflow.d.ts
868
+ interface RecoveryWfCtx {
869
+ opts?: ResolvedRecoveryWorkflowOpts;
870
+ email?: string;
871
+ username?: string;
872
+ selectedMode?: "magicLink" | "otp";
873
+ /** Resolved delivery mode the workflow committed to (populated by `selectMode` or `init`). */
874
+ resolvedMode?: "magicLink" | "otp";
875
+ otpTransport?: "sms" | "email";
876
+ otpCodeLength?: number;
877
+ pin?: string;
878
+ pinExpire?: number;
879
+ pinResendAllowedAt?: number;
880
+ pinVerified?: boolean;
881
+ linkSent?: boolean;
882
+ factorVerified?: boolean;
883
+ passwordChanged?: boolean;
884
+ sessionsRevoked?: boolean;
885
+ tokensIssued?: boolean;
886
+ /** Set by abort alt-actions (`backToLogin`). Gates all terminal steps. */
887
+ aborted?: boolean;
888
+ }
889
+ declare class RecoveryWorkflow {
890
+ protected readonly opts: ResolvedRecoveryWorkflowOpts;
891
+ protected readonly users: UserService;
892
+ protected readonly auth: AuthCredential;
893
+ constructor(opts: RecoveryWorkflowOpts, users: UserService, auth: AuthCredential);
894
+ /**
895
+ * Dispatch an email or SMS event. Default throws — consumers MUST override
896
+ * if `delivery.mode` ever drives email/SMS (i.e. for any non-`magicLink`
897
+ * mode AND for `magicLink` mode the `outletEmail` outlet still runs the
898
+ * email through `createAuthEmailOutlet`'s `EmailSender` — see the trigger
899
+ * controller wiring; this method covers OTP code dispatch).
900
+ */
901
+ protected deliver(_payload: DeliverPayload): Promise<void>;
902
+ /**
903
+ * Emit an audit event. Default: no-op. Consumers override to fan out to
904
+ * their audit sink.
905
+ */
906
+ protected audit(_event: AuditEvent): Promise<void>;
907
+ flow(): void;
908
+ init(ctx: RecoveryWfCtx): undefined;
909
+ /**
910
+ * Returns the JSON-safe projection of `opts` stashed onto `ctx` for schema
911
+ * conditions to read. Default: drop the `forms` group (atscript form classes
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.*`.
914
+ *
915
+ * Consumers who extend the opts type with non-JSON values can override this
916
+ * to strip them so `AsWfStore`'s plain-JSON persistence doesn't choke.
917
+ */
918
+ protected snapshotOpts(opts: ResolvedRecoveryWorkflowOpts): ResolvedRecoveryWorkflowOpts;
919
+ request(input: {
920
+ email?: string;
921
+ action?: string;
922
+ } | undefined, ctx: RecoveryWfCtx): Promise<unknown>;
923
+ /**
924
+ * Resolves the recovery-step `email` input to the `username` (user-id) that
925
+ * `UserService.getUser` expects. Default: returns the email unchanged (treats
926
+ * email as username). Apps whose user model separates `username` from
927
+ * `email` MUST override this; return `null` when no user matches.
928
+ */
929
+ protected emailToUserId(email: string): Promise<string | null>;
930
+ selectMode(input: {
931
+ mode?: string;
932
+ action?: string;
933
+ } | undefined, ctx: RecoveryWfCtx): unknown;
934
+ sendMagicLink(ctx: RecoveryWfCtx): unknown;
935
+ sendOtp(ctx: RecoveryWfCtx): Promise<undefined>;
936
+ checkOtp(input: {
937
+ code?: string;
938
+ action?: string;
939
+ } | undefined, ctx: RecoveryWfCtx): Promise<unknown>;
940
+ verifyFactor(input: {
941
+ factor?: string;
942
+ value?: string;
943
+ action?: string;
944
+ } | undefined, ctx: RecoveryWfCtx): Promise<unknown>;
945
+ /**
946
+ * Verifies a recovery factor against the user's enrolled MFA methods.
947
+ * Default: supports `'phone'` (phone last-4 match) and `'totp'` (current
948
+ * TOTP code). Returns `true` when the factor matches.
949
+ *
950
+ * Consumers extend by overriding to support additional factors (e.g.
951
+ * security questions); call `super.verifyRecoveryFactor(...)` to keep
952
+ * the built-in checks.
953
+ */
954
+ protected verifyRecoveryFactor(input: {
955
+ factor: string;
956
+ value: string;
957
+ ctx: RecoveryWfCtx;
958
+ }): Promise<boolean>;
959
+ setPassword(input: {
960
+ newPassword?: string;
961
+ confirmPassword?: string;
962
+ action?: string;
963
+ } | undefined, ctx: RecoveryWfCtx): Promise<unknown>;
964
+ revokeSessions(ctx: RecoveryWfCtx): Promise<undefined>;
965
+ auditStep(ctx: RecoveryWfCtx): Promise<undefined>;
966
+ freshLoginFinish(_ctx: RecoveryWfCtx): undefined;
967
+ autoLoginFinish(ctx: RecoveryWfCtx): Promise<undefined>;
968
+ /**
969
+ * Send the generic "if an account exists, you'll receive instructions"
970
+ * finished response. Used for unknown emails so a known/unknown lookup is
971
+ * indistinguishable to the client (anti-enumeration).
972
+ */
973
+ private finishGeneric;
974
+ private abortToLogin;
975
+ private emitRequested;
976
+ private resolveUserPhone;
977
+ }
978
+ //#endregion
979
+ //#region src/workflows/invite.workflow.options.d.ts
980
+ /**
981
+ * Input passed to {@link InviteWorkflow.prepareUser}. The workflow resolves the
982
+ * admin form to these fields before calling the hook, so the override sees a
983
+ * fully-typed payload regardless of which optional fields the admin filled in.
984
+ */
985
+ interface PreparedUserInput {
986
+ email: string;
987
+ firstName?: string;
988
+ lastName?: string;
989
+ roles: string[];
990
+ /** Admin's `username` (`useAuth().getAuthContext()?.userId` at invite time). */
991
+ invitedBy?: string;
992
+ }
993
+ /** Return value of {@link InviteWorkflow.duplicateCheck}. */
994
+ type DuplicateAction = "allow" | "reject" | "reuseAsReInvite";
995
+ type InviteSendMode = "email" | "shareableLink" | "choice";
996
+ 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
+ /**
1018
+ * Replaceable form schemas. Each field defaults to the corresponding
1019
+ * `.as` form shipped under `@aooth/auth-moost/atscript/models`.
1020
+ */
1021
+ forms?: {
1022
+ invite?: TAtscriptAnnotatedType;
1023
+ inviteEmail?: TAtscriptAnnotatedType;
1024
+ inviteSendMode?: TAtscriptAnnotatedType;
1025
+ setPassword?: TAtscriptAnnotatedType;
1026
+ };
1027
+ }
1028
+ /**
1029
+ * Fully-resolved view used by the workflow at runtime — every nested group is
1030
+ * always populated by `mergeInviteOpts`, so schema conditions can read
1031
+ * `ctx.opts.<group>.<flag>` directly without optional chaining.
1032
+ */
1033
+ 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
+ forms: {
1055
+ invite: TAtscriptAnnotatedType;
1056
+ inviteEmail: TAtscriptAnnotatedType;
1057
+ inviteSendMode: TAtscriptAnnotatedType;
1058
+ setPassword: TAtscriptAnnotatedType;
1059
+ };
1060
+ }
1061
+ /**
1062
+ * Deep-merge defaults with the user-supplied nested pojo. Each group has its
1063
+ * own `{ ...defaults, ...input }` line — small enough that pulling in lodash
1064
+ * would be silly.
1065
+ */
1066
+ declare function mergeInviteOpts(opts?: InviteWorkflowOpts): ResolvedInviteWorkflowOpts;
1067
+ /**
1068
+ * Backwards-compat alias for the prior input-shape name. Consumers who type
1069
+ * their `prepareUser()` override against this still compile.
1070
+ */
1071
+ type InvitePrepareUserInput = PreparedUserInput;
1072
+ //#endregion
1073
+ //#region src/workflows/invite.workflow.d.ts
1074
+ interface InviteWfCtx {
1075
+ opts?: ResolvedInviteWorkflowOpts;
1076
+ /** Boolean projection of `this.getProfileForm() !== undefined` — schema gates on it. */
1077
+ acceptProfileFormPresent?: boolean;
1078
+ /**
1079
+ * Populated by `invitePrepareAvailableRoles` when the override returns a list.
1080
+ * Surfaced into the `InviteForm` via `@wf.context.pass 'availableRoles'` so
1081
+ * the role multi-select renders the whitelisted choices; also used by
1082
+ * `inviteAdminInviteForm` to reject admin-submitted roles outside the list.
1083
+ */
1084
+ availableRoles?: string[];
1085
+ email?: string;
1086
+ /** Typically same as `email`; consumers can override the mapping. */
1087
+ username?: string;
1088
+ firstName?: string;
1089
+ lastName?: string;
1090
+ roles?: string[];
1091
+ /** Populated by `inviteSelectSendMode` (when `send.mode === 'choice'`). */
1092
+ selectedSendMode?: "email" | "shareableLink";
1093
+ /** Resolved send mode the workflow committed to (set in `inviteInit` or `inviteSelectSendMode`). */
1094
+ resolvedSendMode?: "email" | "shareableLink";
1095
+ /** Populated by `inviteReturnShareableLink` so the admin's UI can display it. */
1096
+ shareableLinkUrl?: string;
1097
+ /** Marks that `inviteSendInviteEmail` already emitted the outlet — resume → advance. */
1098
+ linkSent?: boolean;
1099
+ /** Detected at `inviteCheckPendingInvitation`; triggers `inviteIdempotentRedirect`. */
1100
+ alreadyAccepted?: boolean;
1101
+ passwordSet?: boolean;
1102
+ /** Raw input from `inviteCollectProfile`. */
1103
+ profile?: Record<string, unknown>;
1104
+ profileApplied?: boolean;
1105
+ pendingInvitationCleared?: boolean;
1106
+ activated?: boolean;
1107
+ confirmationShown?: boolean;
1108
+ tokensIssued?: boolean;
1109
+ /** Set true by abort alt-actions (`cancel`). Gates all terminal steps. */
1110
+ aborted?: boolean;
1111
+ }
1112
+ /** Trim + de-duplicate role identifiers submitted via the admin invite form. */
1113
+ declare function parseInviteRoles(input?: string[]): string[];
1114
+ /**
1115
+ * **Per-step ARBAC model.** Phase-A steps (admin-side, pre magic-link send)
1116
+ * inherit the class-level `@ArbacResource('auth.invite') @ArbacAction('start')`
1117
+ * so every admin-side step event is gated. Apps that wire
1118
+ * `arbacAuthorizeInterceptor` globally grant admin a single rule:
1119
+ * `allow('auth.invite', 'start')`.
1120
+ *
1121
+ * The three `@Workflow` body methods (`inviteFlow` / `reInviteFlow` /
1122
+ * `cancelInviteFlow`) are `@Public()` because the wf adapter dispatches the
1123
+ * flow body on EVERY `start()` / `resume()` call — gating it would 401 the
1124
+ * anonymous magic-link resume before any step runs. The real gate is the
1125
+ * step methods themselves, which the wf runtime invokes through the same
1126
+ * interceptor chain.
1127
+ *
1128
+ * Phase-B steps (post `ctx.linkSent`, accept tail) are method-level
1129
+ * `@Public()` because they fire on the anonymous magic-link resume.
1130
+ * `inviteSendInviteEmail` / `inviteReturnShareableLink` are the boundary:
1131
+ * also `@Public()` because the @prostojs/wf runtime re-enters the saved step
1132
+ * on resume (the loop restarts at `indexes[level]`, not after it). Their
1133
+ * bodies are idempotent via `if (ctx.linkSent) return`.
1134
+ *
1135
+ * `auth.reInvite` / `auth.cancelInvite` are admin-only end-to-end (admin
1136
+ * confirms in their own UI; no anonymous boundary), so their phase-A steps
1137
+ * stay class-gated under the same `auth.invite` / `start` grant.
1138
+ */
1139
+ declare class InviteWorkflow {
1140
+ protected readonly opts: ResolvedInviteWorkflowOpts;
1141
+ protected readonly users: UserService;
1142
+ protected readonly auth: AuthCredential;
1143
+ constructor(opts: InviteWorkflowOpts, users: UserService, auth: AuthCredential);
1144
+ /**
1145
+ * Dispatch an email or SMS event. Default throws — the default invite send
1146
+ * uses `outletEmail` (handled by `createAuthEmailOutlet`) so this method is
1147
+ * only invoked when a consumer's accept-tail steps drive a manual send.
1148
+ * Override to wire your senders.
1149
+ */
1150
+ protected deliver(_payload: DeliverPayload): Promise<void>;
1151
+ /**
1152
+ * Emit an audit event. Default: no-op. Consumers override to fan out to
1153
+ * their audit sink.
1154
+ */
1155
+ protected audit(_event: AuditEvent): Promise<void>;
1156
+ /**
1157
+ * Build the extras dictionary merged into the freshly-created user row in
1158
+ * `invitePreCreateUser`. Default: `{}`. Override to populate e.g. a
1159
+ * required `tenantId`. This is the ONLY seam through which the admin form's
1160
+ * `firstName` / `lastName` reach persistence — map them into your schema's
1161
+ * own columns (e.g. `displayName`) and return them here.
1162
+ */
1163
+ protected prepareUser(_input: PreparedUserInput): Promise<Record<string, unknown>>;
1164
+ /**
1165
+ * Return the list of selectable role identifiers for the admin invite form.
1166
+ * When defined AND `adminForm.collectRoles` is true → form ships
1167
+ * `ctx.availableRoles` so the UI renders a multi-select AND the
1168
+ * `inviteAdminInviteForm` step rejects admin-submitted roles outside the
1169
+ * list. When `undefined` (default) → no whitelist is enforced and any role
1170
+ * value the admin form supplies is accepted.
1171
+ */
1172
+ protected getAvailableRoles(): Promise<string[] | undefined>;
1173
+ /**
1174
+ * Derive roles server-side from the admin-form payload (e.g. email domain
1175
+ * → tenant role, AD lookup). Result is set-unioned with admin-supplied
1176
+ * roles when `adminForm.collectRoles` is true. Default: `[]` (no inference).
1177
+ */
1178
+ protected inferRoles(_input: {
1179
+ email: string;
1180
+ firstName?: string;
1181
+ lastName?: string;
1182
+ }): Promise<string[]>;
1183
+ /**
1184
+ * Persist the accept-time profile payload. Default: deep-merge into the
1185
+ * user record via `UserService.update(username, profile)`. Override to
1186
+ * route into a separate profile table / external CRM.
1187
+ */
1188
+ protected applyProfile(input: {
1189
+ username: string;
1190
+ profile: Record<string, unknown>;
1191
+ }): Promise<void>;
1192
+ /**
1193
+ * Override the structural duplicate rule for `inviteAdminInviteForm`.
1194
+ * Default: any existing row → `'reject'`; nothing → `'allow'`. Multi-tenant
1195
+ * apps that allow re-inviting the same email into a different tenant
1196
+ * override to return `'allow'` for those cases.
1197
+ */
1198
+ protected duplicateCheck(input: {
1199
+ email: string;
1200
+ existingUser: UserCredentials | null;
1201
+ }): Promise<DuplicateAction>;
1202
+ /**
1203
+ * Return the consumer-supplied `.as` form schema rendered in the
1204
+ * `inviteCollectProfile` step. `undefined` (default) skips the step
1205
+ * entirely (just password collection).
1206
+ */
1207
+ protected getProfileForm(): TAtscriptAnnotatedType | undefined;
1208
+ /**
1209
+ * Returns the JSON-safe projection of `opts` stashed onto `ctx` for schema
1210
+ * conditions to read. Default: drop the `forms` group (atscript form classes
1211
+ * are not plain JSON) so `AsWfStore`'s plain-JSON persistence doesn't choke.
1212
+ * Step bodies still consult the form classes via `this.opts.forms.*`.
1213
+ *
1214
+ * Consumers who extend the opts type with non-JSON values can override this
1215
+ * to strip them so `AsWfStore`'s plain-JSON persistence doesn't choke.
1216
+ */
1217
+ protected snapshotOpts(opts: ResolvedInviteWorkflowOpts): ResolvedInviteWorkflowOpts;
1218
+ inviteFlow(): void;
1219
+ reInviteFlow(): void;
1220
+ cancelInviteFlow(): void;
1221
+ init(ctx: InviteWfCtx): undefined;
1222
+ prepareAvailableRoles(ctx: InviteWfCtx): Promise<undefined>;
1223
+ selectSendMode(input: {
1224
+ mode?: string;
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>;
1234
+ inferRolesStep(ctx: InviteWfCtx): Promise<undefined>;
1235
+ preCreateUser(ctx: InviteWfCtx): Promise<undefined>;
1236
+ sendInviteEmail(ctx: InviteWfCtx): unknown;
1237
+ returnShareableLink(ctx: InviteWfCtx): unknown;
1238
+ loadPendingUser(input: {
1239
+ email?: string;
1240
+ action?: string;
1241
+ } | undefined, ctx: InviteWfCtx): Promise<unknown>;
1242
+ checkPendingInvitation(ctx: InviteWfCtx): Promise<undefined>;
1243
+ idempotentRedirect(ctx: InviteWfCtx): undefined;
1244
+ preparePasswordRules(ctx: InviteWfCtx): undefined;
1245
+ createPasswordForm(input: {
1246
+ newPassword?: string;
1247
+ confirmPassword?: string;
1248
+ action?: string;
1249
+ } | undefined, ctx: InviteWfCtx): Promise<unknown>;
1250
+ collectProfile(input: Record<string, unknown> | undefined, ctx: InviteWfCtx): Promise<unknown>;
1251
+ applyProfileStep(ctx: InviteWfCtx): Promise<undefined>;
1252
+ unsetPendingInvitation(ctx: InviteWfCtx): Promise<undefined>;
1253
+ activateUser(ctx: InviteWfCtx): Promise<undefined>;
1254
+ confirmation(ctx: InviteWfCtx): undefined;
1255
+ freshLoginFinish(_ctx: InviteWfCtx): undefined;
1256
+ autoLoginFinish(ctx: InviteWfCtx): Promise<undefined>;
1257
+ cancelInvite(input: {
1258
+ email?: string;
1259
+ } | undefined, ctx: InviteWfCtx): Promise<unknown>;
1260
+ private abort;
1261
+ private loadUserOrNull;
1262
+ private emitAudit;
1263
+ }
1264
+ //#endregion
1265
+ //#region src/workflows/auth-email-outlet.d.ts
1266
+ interface AuthEmailOutletDeps {
1267
+ emailSender: EmailSender$1;
1268
+ buildMagicLinkUrl: BuildMagicLinkUrl$1;
1269
+ /** Fallback TTL when the workflow context omits `expiresAtMs`. */
1270
+ magicLinkTtlMs: (kind: AuthEmailKind$1) => number;
1271
+ }
1272
+ /**
1273
+ * Build the email outlet that delivers magic links via the consumer's
1274
+ * `EmailSender`. Single-use: pass the same instance into one
1275
+ * `handleWfOutletRequest({ outlets })`.
1276
+ */
1277
+ declare function createAuthEmailOutlet(deps: AuthEmailOutletDeps): WfOutlet;
1278
+ //#endregion
1279
+ 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, type AuthRefreshBody, type AuthSmsEvent, type AuthSmsKind, type BuildMagicLinkUrl, type ConcurrencyLimitOptions, DEFAULT_AUTH_WORKFLOWS, type DeliverEmail, type DeliverPayload, type DeliverSms, type DuplicateAction, type EmailSender, type InvitePrepareUserInput, type InviteSendMode, type InviteWfCtx, InviteWorkflow, type InviteWorkflowOpts, type IssueResult, type LoginRedirect, type LoginWfCtx, LoginWorkflow, type LoginWorkflowOpts, type MfaSummary, type MfaTransport, type PreparedUserInput, Public, type RecoveryDeliveryMode, type RecoveryOtpTransport, 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, generateMagicLinkToken, getAuthMate, mergeInviteOpts, mergeLoginOpts, mergeRecoveryOpts, parseInviteRoles, useAuth };