@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.
- package/LICENSE +21 -0
- package/README.md +40 -0
- package/dist/atscript/index.d.mts +2 -0
- package/dist/atscript/index.mjs +2 -0
- package/dist/forms-BE62OrN1.mjs +230 -0
- package/dist/index.d.mts +1279 -0
- package/dist/index.mjs +3347 -0
- package/package.json +90 -0
- package/src/atscript/models/forms.as +331 -0
- package/src/atscript/models/forms.as.d.ts +357 -0
package/dist/index.d.mts
ADDED
|
@@ -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 };
|