@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/dist/index.mjs ADDED
@@ -0,0 +1,3347 @@
1
+ import { AuthCredential, AuthError, generateMagicLinkToken } from "@aooth/auth";
2
+ import { current, defineWook, eventTypeKey, key } from "@wooksjs/event-core";
3
+ import { HttpError, useAuthorization, useCookies, useRequest, useResponse, useUrlParams } from "@wooksjs/event-http";
4
+ import { Controller, Injectable, Intercept, Resolve, TInterceptorPriority, defineAfterInterceptor, defineBeforeInterceptor, getMoostMate, useControllerContext } from "moost";
5
+ import { ArbacAction, ArbacResource, getArbacMate } from "@aooth/arbac-moost";
6
+ import { Body, Get, HttpError as HttpError$1, Post } from "@moostjs/event-http";
7
+ import { createAsHttpOutlet, extractPassContext, finishWfAborted, finishWfWithChoice, finishWfWithData, finishWfWithRedirect, handleAsOutletRequest, serializeFormSchema } from "@atscript/moost-wf";
8
+ import { HandleStateStrategy, MoostWf, Step, WfStateStoreMemory, Workflow, WorkflowParam, WorkflowSchema, createEmailOutlet, outletEmail, outletHttp, useWfFinished } from "@moostjs/event-wf";
9
+ import { UserAuthError, UserService, maskEmail, maskPhone, verifyTotpCode } from "@aooth/user";
10
+ import { defineAnnotatedType, throwFeatureDisabled } from "@atscript/typescript/utils";
11
+ //#region src/auth.config.ts
12
+ /**
13
+ * Resolve `AuthOptions` to its `ResolvedAuthOptions` form, applying the same
14
+ * defaults the legacy `MoostAuthConfig` constructor did. Pure — call once per
15
+ * `authGuardInterceptor` factory invocation; the resolved value is stashed in
16
+ * a wook slot for the event chain.
17
+ */
18
+ function resolveAuthOptions(opts = {}) {
19
+ const cookie = {
20
+ name: "aooth_session",
21
+ secure: true,
22
+ sameSite: "lax",
23
+ httpOnly: true,
24
+ path: "/",
25
+ ...opts.cookie
26
+ };
27
+ return {
28
+ cookie,
29
+ refreshCookie: {
30
+ name: "aooth_refresh",
31
+ secure: cookie.secure,
32
+ sameSite: cookie.sameSite,
33
+ httpOnly: cookie.httpOnly,
34
+ ...cookie.domain !== void 0 && { domain: cookie.domain },
35
+ path: "/auth/refresh",
36
+ ...opts.refreshCookie
37
+ },
38
+ enableCookie: opts.enableCookie ?? true,
39
+ enableBearer: opts.enableBearer ?? true
40
+ };
41
+ }
42
+ //#endregion
43
+ //#region src/auth.composables.ts
44
+ const authContextKey = key("auth.context");
45
+ /**
46
+ * Slot populated by `authGuardInterceptor(opts)` on the HTTP event. Workflow
47
+ * step events started with `start({ eventContext: current() })` inherit the
48
+ * HTTP parent chain — `useAuth()` reads the slot transparently from inside
49
+ * step bodies. Exported only for `auth.guard.ts`.
50
+ */
51
+ const authOptionsKey = key("auth.options");
52
+ /**
53
+ * `sameSite` is upper-cased because wooks accepts `'Lax' | 'Strict' | 'None'`
54
+ * while `ResolvedAuthCookieConfig` stores lower-case for ergonomics.
55
+ */
56
+ function cookieAttrsFrom(c, extra) {
57
+ return {
58
+ httpOnly: c.httpOnly,
59
+ secure: c.secure,
60
+ sameSite: c.sameSite.charAt(0).toUpperCase() + c.sameSite.slice(1),
61
+ path: c.path,
62
+ domain: c.domain,
63
+ ...extra
64
+ };
65
+ }
66
+ /**
67
+ * Composable for accessing the current event's auth state + transport helpers.
68
+ *
69
+ * `defineWook` memoizes the bindings object per event so multiple calls share
70
+ * one closure. Identity bindings (`getAuthContext`/`getUserId`/`isAuthenticated`)
71
+ * read the `authContextKey` slot populated by `authGuardInterceptor`. The
72
+ * remaining closures read the `authOptionsKey` slot stashed by that same
73
+ * interceptor; calling them outside an HTTP-or-HTTP-parented event throws
74
+ * `HttpError(500)` loudly — that's a configuration error, not a runtime
75
+ * fallback case.
76
+ */
77
+ const useAuth = defineWook((ctx) => {
78
+ const getAuthContext = () => {
79
+ if (!ctx.has(authContextKey)) return null;
80
+ return ctx.get(authContextKey);
81
+ };
82
+ const getUserId = () => {
83
+ const auth = getAuthContext();
84
+ if (!auth) throw new HttpError(401, "Not authenticated");
85
+ return auth.userId;
86
+ };
87
+ const isAuthenticated = () => getAuthContext() !== null;
88
+ let cachedOptions;
89
+ const requireOptions = () => {
90
+ if (cachedOptions !== void 0) return cachedOptions;
91
+ if (!ctx.has(authOptionsKey)) throw new HttpError(500, "useAuth(): authGuardInterceptor(opts) must be installed on the HTTP event chain");
92
+ cachedOptions = ctx.get(authOptionsKey);
93
+ return cachedOptions;
94
+ };
95
+ const extractToken = () => {
96
+ const options = requireOptions();
97
+ let token;
98
+ if (options.enableBearer) {
99
+ const auth = useAuthorization(ctx);
100
+ if (auth.is("bearer")) token = auth.credentials() ?? void 0;
101
+ }
102
+ if (!token && options.enableCookie) token = useCookies(ctx).getCookie(options.cookie.name) ?? void 0;
103
+ return token;
104
+ };
105
+ const writeCookies = (issue) => {
106
+ const options = requireOptions();
107
+ if (!options.enableCookie) return;
108
+ const response = useResponse(current());
109
+ response.setCookie(options.cookie.name, issue.accessToken, cookieAttrsFrom(options.cookie));
110
+ if (issue.refreshToken) response.setCookie(options.refreshCookie.name, issue.refreshToken, cookieAttrsFrom(options.refreshCookie));
111
+ };
112
+ const clearCookies = () => {
113
+ const options = requireOptions();
114
+ if (!options.enableCookie) return;
115
+ const response = useResponse(current());
116
+ response.setCookie(options.cookie.name, "", cookieAttrsFrom(options.cookie, { maxAge: 0 }));
117
+ response.setCookie(options.refreshCookie.name, "", cookieAttrsFrom(options.refreshCookie, { maxAge: 0 }));
118
+ };
119
+ /**
120
+ * With `enableBearer === false` the response shape still carries `userId` +
121
+ * `accessExpiresAt` so clients can schedule a silent refresh, but the tokens
122
+ * themselves travel only via the cookies set by `writeCookies`.
123
+ */
124
+ const buildLoginResponseFn = (userId, issue) => {
125
+ const options = requireOptions();
126
+ return {
127
+ userId,
128
+ accessExpiresAt: issue.accessExpiresAt,
129
+ ...issue.refreshExpiresAt !== void 0 && { refreshExpiresAt: issue.refreshExpiresAt },
130
+ ...options.enableBearer && {
131
+ accessToken: issue.accessToken,
132
+ ...issue.refreshToken && { refreshToken: issue.refreshToken }
133
+ }
134
+ };
135
+ };
136
+ /**
137
+ * Build the `cookies` map for `useWfFinished({ cookies })`. The outlet
138
+ * trigger's HTTP layer turns the entries into `Set-Cookie` headers, mirroring
139
+ * what `writeCookies()` does for the REST controller.
140
+ */
141
+ const buildFinishedCookies = (issue) => {
142
+ const options = requireOptions();
143
+ if (!options.enableCookie) return void 0;
144
+ const cookies = { [options.cookie.name]: {
145
+ value: issue.accessToken,
146
+ options: cookieAttrsFrom(options.cookie)
147
+ } };
148
+ if (issue.refreshToken) cookies[options.refreshCookie.name] = {
149
+ value: issue.refreshToken,
150
+ options: cookieAttrsFrom(options.refreshCookie)
151
+ };
152
+ return cookies;
153
+ };
154
+ const cookieAttrs = (extra) => {
155
+ return cookieAttrsFrom(requireOptions().cookie, extra);
156
+ };
157
+ return {
158
+ getAuthContext,
159
+ getUserId,
160
+ isAuthenticated,
161
+ get options() {
162
+ return requireOptions();
163
+ },
164
+ extractToken,
165
+ writeCookies,
166
+ clearCookies,
167
+ buildLoginResponse: buildLoginResponseFn,
168
+ buildFinishedCookies,
169
+ cookieAttrs
170
+ };
171
+ });
172
+ /** Internal: only `authGuardInterceptor` writes the slot. */
173
+ function setAuthContext(ctx, value) {
174
+ ctx.set(authContextKey, value);
175
+ }
176
+ //#endregion
177
+ //#region src/auth.guard.ts
178
+ /**
179
+ * `GUARD`-priority interceptor factory that authenticates incoming requests.
180
+ *
181
+ * Returns a configured `TInterceptorDef`. Each invocation captures its own
182
+ * resolved options and stashes them onto the HTTP event context's
183
+ * `authOptionsKey` slot so `useAuth()` (and the workflows that depend on it)
184
+ * can read the same transport config.
185
+ *
186
+ * Token extraction precedence: `Authorization: Bearer ...` wins over cookie
187
+ * when both transports are enabled. On `@Public()` routes a missing or
188
+ * invalid token leaves AuthContext as `null` and the handler runs anyway;
189
+ * on protected routes it throws `HttpError(401)`.
190
+ *
191
+ * Never auto-refreshes — refresh is a separate REST endpoint.
192
+ *
193
+ * No-ops on non-HTTP event contexts (workflow steps, CLI, WS messages). The
194
+ * guard reads tokens from HTTP headers/cookies; nothing to read elsewhere.
195
+ * Authorization for workflow steps is the step's own responsibility (e.g.
196
+ * an admin-only invite endpoint protects its outlet HTTP route, not the
197
+ * step handler).
198
+ */
199
+ function authGuardInterceptor(opts) {
200
+ const resolved = resolveAuthOptions(opts);
201
+ return defineBeforeInterceptor(async () => {
202
+ const ctx = current();
203
+ if (ctx.get(eventTypeKey) !== "http") return;
204
+ ctx.set(authOptionsKey, resolved);
205
+ const cc = useControllerContext(ctx);
206
+ const cMeta = cc.getControllerMeta();
207
+ const isPublic = cc.getMethodMeta()?.authPublic ?? cMeta?.authPublic ?? false;
208
+ const token = useAuth().extractToken();
209
+ if (!token) {
210
+ if (isPublic) {
211
+ setAuthContext(ctx, null);
212
+ return;
213
+ }
214
+ throw new HttpError(401, "Unauthorized");
215
+ }
216
+ const authContext = await (await cc.instantiate(AuthCredential)).validate(token);
217
+ if (!authContext) {
218
+ if (isPublic) {
219
+ setAuthContext(ctx, null);
220
+ return;
221
+ }
222
+ throw new HttpError(401, "Invalid credential");
223
+ }
224
+ setAuthContext(ctx, authContext);
225
+ }, TInterceptorPriority.GUARD);
226
+ }
227
+ /**
228
+ * Decorator-factory sugar for attaching `authGuardInterceptor(opts)` to a
229
+ * specific controller or method instead of globally. Equivalent to
230
+ * `@Intercept(authGuardInterceptor(opts))`.
231
+ */
232
+ function AuthGuarded(opts) {
233
+ return Intercept(authGuardInterceptor(opts));
234
+ }
235
+ //#endregion
236
+ //#region src/auth.mate.ts
237
+ function getAuthMate() {
238
+ return getMoostMate();
239
+ }
240
+ //#endregion
241
+ //#region src/auth.decorator.ts
242
+ /**
243
+ * Marks a route or controller as fully public — opts out of BOTH
244
+ * authentication (auth-moost's bearer guard) AND authorization
245
+ * (arbac-moost's `arbacAuthorizeInterceptor`).
246
+ *
247
+ * `authGuardInterceptor` still runs on `@Public()` handlers — it populates the
248
+ * AuthContext when a valid credential is presented, but does NOT throw when
249
+ * the token is missing or invalid (the handler runs with a `null` AuthContext).
250
+ * The ARBAC interceptor short-circuits entirely via its `isPublic` check.
251
+ *
252
+ * Method-level decoration overrides class-level.
253
+ */
254
+ const Public = () => {
255
+ const auth = getAuthMate().decorate("authPublic", true);
256
+ const arbac = getArbacMate().decorate("arbacPublic", true);
257
+ return ((target, key, descriptor) => {
258
+ auth(target, key, descriptor);
259
+ arbac(target, key, descriptor);
260
+ });
261
+ };
262
+ /**
263
+ * Resolves the authenticated user's id (string) for a handler parameter.
264
+ *
265
+ * Delegates to `useAuth().getUserId()`, which throws `HttpError(401)` when no
266
+ * `AuthContext` is present in the event. There is no `@User()` counterpart —
267
+ * `AuthContext` is credential context, not user profile data, and this library
268
+ * does not own a user profile type.
269
+ */
270
+ const UserId = () => Resolve(() => useAuth().getUserId());
271
+ //#endregion
272
+ //#region \0@oxc-project+runtime@0.129.0/helpers/decorateMetadata.js
273
+ function __decorateMetadata(k, v) {
274
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
275
+ }
276
+ //#endregion
277
+ //#region \0@oxc-project+runtime@0.129.0/helpers/decorate.js
278
+ function __decorate(decorators, target, key, desc) {
279
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
280
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
281
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
282
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
283
+ }
284
+ //#endregion
285
+ //#region src/wf-trigger/provider.ts
286
+ var _ref$4;
287
+ let WfTriggerProvider = class WfTriggerProvider {
288
+ state = new HandleStateStrategy({ store: new WfStateStoreMemory() });
289
+ outlets = [createAsHttpOutlet()];
290
+ token = {
291
+ read: [
292
+ "body",
293
+ "query",
294
+ "cookie"
295
+ ],
296
+ write: "body",
297
+ name: "wfs"
298
+ };
299
+ constructor(wf) {
300
+ this.wf = wf;
301
+ }
302
+ async handle(opts = {}) {
303
+ const wfApp = this.wf.getWfApp();
304
+ const deps = {
305
+ start: (schemaId, context, o) => wfApp.start(schemaId, context, {
306
+ input: o?.input,
307
+ eventContext: o?.eventContext ?? current()
308
+ }),
309
+ resume: (state, o) => wfApp.resume(state, {
310
+ input: o?.input,
311
+ eventContext: o?.eventContext ?? current()
312
+ })
313
+ };
314
+ return handleAsOutletRequest({
315
+ ...opts.allow && { allow: opts.allow },
316
+ state: this.state,
317
+ outlets: this.outlets,
318
+ token: opts.token ?? this.token
319
+ }, deps);
320
+ }
321
+ };
322
+ WfTriggerProvider = __decorate([Injectable(), __decorateMetadata("design:paramtypes", [typeof (_ref$4 = typeof MoostWf !== "undefined" && MoostWf) === "function" ? _ref$4 : Object])], WfTriggerProvider);
323
+ //#endregion
324
+ //#region src/wf-trigger/decorator.ts
325
+ /**
326
+ * Method decorator that turns a handler into a workflow trigger.
327
+ *
328
+ * The handler may have an empty body — the interceptor's after-phase invokes
329
+ * `WfTriggerProvider.handle()` when the handler returns `undefined`. Subclasses
330
+ * that need to short-circuit (e.g. emit a custom error response) just return a
331
+ * non-undefined value from the overridden handler and the trigger is skipped.
332
+ *
333
+ * The single `useControllerContext().instantiate(...)` call is the one
334
+ * documented escape hatch: interceptors are functions, not classes, so there's
335
+ * no ctor to inject into. Every class still uses constructor injection.
336
+ */
337
+ const WfTrigger = (opts = {}) => Intercept(defineAfterInterceptor(async (response, reply) => {
338
+ if (await response !== void 0) return;
339
+ reply(await (await useControllerContext().instantiate(WfTriggerProvider)).handle(opts));
340
+ }, TInterceptorPriority.INTERCEPTOR));
341
+ //#endregion
342
+ //#region \0@oxc-project+runtime@0.129.0/helpers/decorateParam.js
343
+ function __decorateParam(paramIndex, decorator) {
344
+ return function(target, key) {
345
+ decorator(target, key, paramIndex);
346
+ };
347
+ }
348
+ //#endregion
349
+ //#region src/auth.controller.ts
350
+ var _ref$3;
351
+ /** Workflows allowed by the bundled `/auth/trigger` endpoint. Subclasses override `triggerWf()` to extend. */
352
+ const DEFAULT_AUTH_WORKFLOWS = [
353
+ "auth.login",
354
+ "auth.recovery",
355
+ "auth.invite"
356
+ ];
357
+ /** Prefer an explicit body field, fall back to the refresh cookie when enabled. */
358
+ function resolveRefreshToken(auth, body) {
359
+ if (typeof body?.refreshToken === "string") return body.refreshToken;
360
+ if (!auth.options.enableCookie) return void 0;
361
+ return useCookies(current()).getCookie(auth.options.refreshCookie.name) ?? void 0;
362
+ }
363
+ let AuthController = class AuthController {
364
+ constructor(auth) {
365
+ this.auth = auth;
366
+ }
367
+ async logout(body) {
368
+ const auth = useAuth();
369
+ if (!auth.getAuthContext()) throw new HttpError$1(401, "Not authenticated");
370
+ const accessToken = auth.extractToken();
371
+ const refreshToken = resolveRefreshToken(auth, body);
372
+ if (accessToken) try {
373
+ await this.auth.revoke(accessToken);
374
+ } catch {}
375
+ if (refreshToken) try {
376
+ await this.auth.revoke(refreshToken);
377
+ } catch {}
378
+ auth.clearCookies();
379
+ return { ok: true };
380
+ }
381
+ async refresh(body) {
382
+ const auth = useAuth();
383
+ const refreshToken = resolveRefreshToken(auth, body);
384
+ if (!refreshToken) throw new HttpError$1(401, "Missing refresh token");
385
+ let issue;
386
+ try {
387
+ issue = await this.auth.refresh(refreshToken);
388
+ } catch (err) {
389
+ if (err instanceof AuthError) throw new HttpError$1(401, "Invalid refresh token");
390
+ throw err;
391
+ }
392
+ auth.writeCookies(issue);
393
+ const validated = await this.auth.validate(issue.accessToken);
394
+ return auth.buildLoginResponse(validated?.userId ?? "", issue);
395
+ }
396
+ status() {
397
+ const auth = useAuth().getAuthContext();
398
+ if (!auth) throw new HttpError$1(401, "Not authenticated");
399
+ return auth;
400
+ }
401
+ triggerWf() {}
402
+ };
403
+ __decorate([
404
+ Post("logout"),
405
+ Public(),
406
+ __decorateParam(0, Body()),
407
+ __decorateMetadata("design:type", Function),
408
+ __decorateMetadata("design:paramtypes", [Object]),
409
+ __decorateMetadata("design:returntype", Promise)
410
+ ], AuthController.prototype, "logout", null);
411
+ __decorate([
412
+ Post("refresh"),
413
+ Public(),
414
+ __decorateParam(0, Body()),
415
+ __decorateMetadata("design:type", Function),
416
+ __decorateMetadata("design:paramtypes", [Object]),
417
+ __decorateMetadata("design:returntype", Promise)
418
+ ], AuthController.prototype, "refresh", null);
419
+ __decorate([
420
+ Get("status"),
421
+ Public(),
422
+ __decorateMetadata("design:type", Function),
423
+ __decorateMetadata("design:paramtypes", []),
424
+ __decorateMetadata("design:returntype", Object)
425
+ ], AuthController.prototype, "status", null);
426
+ __decorate([
427
+ Post("trigger"),
428
+ Public(),
429
+ WfTrigger({ allow: [...DEFAULT_AUTH_WORKFLOWS] }),
430
+ __decorateMetadata("design:type", Function),
431
+ __decorateMetadata("design:paramtypes", []),
432
+ __decorateMetadata("design:returntype", void 0)
433
+ ], AuthController.prototype, "triggerWf", null);
434
+ AuthController = __decorate([
435
+ Controller("auth"),
436
+ ArbacResource("auth"),
437
+ __decorateMetadata("design:paramtypes", [typeof (_ref$3 = typeof AuthCredential !== "undefined" && AuthCredential) === "function" ? _ref$3 : Object])
438
+ ], AuthController);
439
+ //#endregion
440
+ //#region src/atscript/models/forms.as.js
441
+ var LoginCredentialsForm = class {
442
+ static __is_atscript_annotated_type = true;
443
+ static type = {};
444
+ static metadata = /* @__PURE__ */ new Map();
445
+ static id = "LoginCredentialsForm";
446
+ static toJsonSchema() {
447
+ throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
448
+ }
449
+ };
450
+ var MfaCodeForm = class {
451
+ static __is_atscript_annotated_type = true;
452
+ static type = {};
453
+ static metadata = /* @__PURE__ */ new Map();
454
+ static id = "MfaCodeForm";
455
+ static toJsonSchema() {
456
+ throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
457
+ }
458
+ };
459
+ var BackupCodeForm = class {
460
+ static __is_atscript_annotated_type = true;
461
+ static type = {};
462
+ static metadata = /* @__PURE__ */ new Map();
463
+ static id = "BackupCodeForm";
464
+ static toJsonSchema() {
465
+ throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
466
+ }
467
+ };
468
+ var EmailIdentifierForm = class {
469
+ static __is_atscript_annotated_type = true;
470
+ static type = {};
471
+ static metadata = /* @__PURE__ */ new Map();
472
+ static id = "EmailIdentifierForm";
473
+ static toJsonSchema() {
474
+ throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
475
+ }
476
+ };
477
+ var SetPasswordForm = class {
478
+ static __is_atscript_annotated_type = true;
479
+ static type = {};
480
+ static metadata = /* @__PURE__ */ new Map();
481
+ static id = "SetPasswordForm";
482
+ static toJsonSchema() {
483
+ throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
484
+ }
485
+ };
486
+ var InviteForm = class {
487
+ static __is_atscript_annotated_type = true;
488
+ static type = {};
489
+ static metadata = /* @__PURE__ */ new Map();
490
+ static id = "InviteForm";
491
+ static toJsonSchema() {
492
+ throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
493
+ }
494
+ };
495
+ var InviteEmailForm = class {
496
+ static __is_atscript_annotated_type = true;
497
+ static type = {};
498
+ static metadata = /* @__PURE__ */ new Map();
499
+ static id = "InviteEmailForm";
500
+ static toJsonSchema() {
501
+ throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
502
+ }
503
+ };
504
+ var InviteSendModeForm = class {
505
+ static __is_atscript_annotated_type = true;
506
+ static type = {};
507
+ static metadata = /* @__PURE__ */ new Map();
508
+ static id = "InviteSendModeForm";
509
+ static toJsonSchema() {
510
+ throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
511
+ }
512
+ };
513
+ var Select2faForm = class {
514
+ static __is_atscript_annotated_type = true;
515
+ static type = {};
516
+ static metadata = /* @__PURE__ */ new Map();
517
+ static id = "Select2faForm";
518
+ static toJsonSchema() {
519
+ throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
520
+ }
521
+ };
522
+ var PincodeForm = class {
523
+ static __is_atscript_annotated_type = true;
524
+ static type = {};
525
+ static metadata = /* @__PURE__ */ new Map();
526
+ static id = "PincodeForm";
527
+ static toJsonSchema() {
528
+ throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
529
+ }
530
+ };
531
+ var AskEmailForm = class {
532
+ static __is_atscript_annotated_type = true;
533
+ static type = {};
534
+ static metadata = /* @__PURE__ */ new Map();
535
+ static id = "AskEmailForm";
536
+ static toJsonSchema() {
537
+ throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
538
+ }
539
+ };
540
+ var AskPhoneForm = class {
541
+ static __is_atscript_annotated_type = true;
542
+ static type = {};
543
+ static metadata = /* @__PURE__ */ new Map();
544
+ static id = "AskPhoneForm";
545
+ static toJsonSchema() {
546
+ throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
547
+ }
548
+ };
549
+ var TermsAcceptForm = class {
550
+ static __is_atscript_annotated_type = true;
551
+ static type = {};
552
+ static metadata = /* @__PURE__ */ new Map();
553
+ static id = "TermsAcceptForm";
554
+ static toJsonSchema() {
555
+ throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
556
+ }
557
+ };
558
+ var ProfileCompleteForm = class {
559
+ static __is_atscript_annotated_type = true;
560
+ static type = {};
561
+ static metadata = /* @__PURE__ */ new Map();
562
+ static id = "ProfileCompleteForm";
563
+ static toJsonSchema() {
564
+ throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
565
+ }
566
+ };
567
+ var ConsentMarketingForm = class {
568
+ static __is_atscript_annotated_type = true;
569
+ static type = {};
570
+ static metadata = /* @__PURE__ */ new Map();
571
+ static id = "ConsentMarketingForm";
572
+ static toJsonSchema() {
573
+ throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
574
+ }
575
+ };
576
+ var TenantSelectForm = class {
577
+ static __is_atscript_annotated_type = true;
578
+ static type = {};
579
+ static metadata = /* @__PURE__ */ new Map();
580
+ static id = "TenantSelectForm";
581
+ static toJsonSchema() {
582
+ throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
583
+ }
584
+ };
585
+ var PersonaSelectForm = class {
586
+ static __is_atscript_annotated_type = true;
587
+ static type = {};
588
+ static metadata = /* @__PURE__ */ new Map();
589
+ static id = "PersonaSelectForm";
590
+ static toJsonSchema() {
591
+ throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
592
+ }
593
+ };
594
+ var ConcurrencyLimitForm = class {
595
+ static __is_atscript_annotated_type = true;
596
+ static type = {};
597
+ static metadata = /* @__PURE__ */ new Map();
598
+ static id = "ConcurrencyLimitForm";
599
+ static toJsonSchema() {
600
+ throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
601
+ }
602
+ };
603
+ var MagicLinkRequestForm = class {
604
+ static __is_atscript_annotated_type = true;
605
+ static type = {};
606
+ static metadata = /* @__PURE__ */ new Map();
607
+ static id = "MagicLinkRequestForm";
608
+ static toJsonSchema() {
609
+ throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
610
+ }
611
+ };
612
+ var RecoveryModeSelectForm = class {
613
+ static __is_atscript_annotated_type = true;
614
+ static type = {};
615
+ static metadata = /* @__PURE__ */ new Map();
616
+ static id = "RecoveryModeSelectForm";
617
+ static toJsonSchema() {
618
+ throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
619
+ }
620
+ };
621
+ var RecoveryFactorForm = class {
622
+ static __is_atscript_annotated_type = true;
623
+ static type = {};
624
+ static metadata = /* @__PURE__ */ new Map();
625
+ static id = "RecoveryFactorForm";
626
+ static toJsonSchema() {
627
+ throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
628
+ }
629
+ };
630
+ defineAnnotatedType("object", LoginCredentialsForm).prop("username", defineAnnotatedType().designType("string").tags("string").annotate("ui.form.type", "text").annotate("meta.label", "Username").annotate("ui.form.autocomplete", "username").annotate("meta.required", {}).annotate("expect.minLength", { length: 1 }).$type).prop("password", defineAnnotatedType().designType("string").tags("string").annotate("ui.form.type", "password").annotate("meta.label", "Password").annotate("ui.form.autocomplete", "current-password").annotate("meta.sensitive", true).annotate("meta.required", {}).annotate("expect.minLength", { length: 1 }).$type);
631
+ defineAnnotatedType("object", MfaCodeForm).prop("code", defineAnnotatedType().designType("string").tags("string").annotate("ui.form.type", "text").annotate("meta.label", "Verification code").annotate("ui.form.autocomplete", "one-time-code").annotate("meta.required", {}).annotate("expect.minLength", { length: 4 }).annotate("expect.maxLength", { length: 12 }).annotate("expect.pattern", { pattern: "^[0-9]+$" }, true).$type);
632
+ defineAnnotatedType("object", BackupCodeForm).prop("code", defineAnnotatedType().designType("string").tags("string").annotate("ui.form.type", "text").annotate("meta.label", "Backup code").annotate("ui.form.autocomplete", "one-time-code").annotate("meta.required", {}).annotate("expect.minLength", { length: 4 }).annotate("expect.maxLength", { length: 32 }).annotate("expect.pattern", { pattern: "^[A-Z2-9-]+$" }, true).$type);
633
+ defineAnnotatedType("object", EmailIdentifierForm).prop("email", defineAnnotatedType().designType("string").tags("email", "string").annotate("ui.form.type", "text").annotate("meta.label", "Email").annotate("ui.form.autocomplete", "email").annotate("meta.required", {}).annotate("expect.pattern", {
634
+ pattern: "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$",
635
+ flags: "",
636
+ message: "Invalid email format."
637
+ }, true).$type).annotate("wf.context.pass", "defaults", true);
638
+ defineAnnotatedType("object", SetPasswordForm).prop("newPassword", defineAnnotatedType().designType("string").tags("string").annotate("ui.form.type", "password").annotate("meta.label", "New password").annotate("ui.form.autocomplete", "new-password").annotate("meta.sensitive", true).annotate("meta.required", {}).annotate("expect.minLength", { length: 8 }).$type).prop("confirmPassword", defineAnnotatedType().designType("string").tags("string").annotate("ui.form.type", "password").annotate("meta.label", "Confirm password").annotate("ui.form.autocomplete", "new-password").annotate("meta.sensitive", true).annotate("meta.required", {}).annotate("expect.minLength", { length: 8 }).$type);
639
+ defineAnnotatedType("object", InviteForm).prop("email", defineAnnotatedType().designType("string").tags("email", "string").annotate("ui.form.type", "text").annotate("meta.label", "Email").annotate("ui.form.autocomplete", "email").annotate("meta.required", {}).annotate("expect.pattern", {
640
+ pattern: "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$",
641
+ flags: "",
642
+ message: "Invalid email format."
643
+ }, true).$type).prop("firstName", defineAnnotatedType().designType("string").tags("string").annotate("ui.form.type", "text").annotate("meta.label", "First name").optional().$type).prop("lastName", defineAnnotatedType().designType("string").tags("string").annotate("ui.form.type", "text").annotate("meta.label", "Last name").optional().$type).prop("roles", defineAnnotatedType("array").of(defineAnnotatedType().designType("string").tags("string").$type).annotate("ui.form.type", "select").annotate("ui.form.fn.options", "(_, _data, context) => Array.isArray(context.availableRoles) ? context.availableRoles.map(r => ({ value: r, label: r })) : []").annotate("meta.label", "Roles").optional().$type).annotate("wf.context.pass", "availableRoles", true);
644
+ defineAnnotatedType("object", InviteEmailForm).prop("email", defineAnnotatedType().designType("string").tags("email", "string").annotate("ui.form.type", "text").annotate("meta.label", "Email").annotate("ui.form.autocomplete", "email").annotate("meta.required", {}).annotate("expect.pattern", {
645
+ pattern: "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$",
646
+ flags: "",
647
+ message: "Invalid email format."
648
+ }, true).$type);
649
+ defineAnnotatedType("object", InviteSendModeForm).prop("mode", defineAnnotatedType().designType("string").tags("string").annotate("ui.form.type", "text").annotate("meta.label", "Delivery mode").annotate("meta.required", {}).annotate("expect.pattern", { pattern: "^(email|shareableLink)$" }, true).$type);
650
+ defineAnnotatedType("object", Select2faForm).prop("methodName", defineAnnotatedType().designType("string").tags("string").annotate("ui.form.type", "text").annotate("meta.label", "MFA method").annotate("meta.required", {}).$type).prop("saveAsDefault", defineAnnotatedType().designType("boolean").tags("boolean").annotate("ui.form.type", "checkbox").annotate("meta.label", "Save as default").optional().$type);
651
+ defineAnnotatedType("object", PincodeForm).prop("code", defineAnnotatedType().designType("string").tags("string").annotate("ui.form.type", "text").annotate("meta.label", "Verification code").annotate("ui.form.autocomplete", "one-time-code").annotate("meta.required", {}).annotate("expect.minLength", { length: 4 }).annotate("expect.maxLength", { length: 12 }).annotate("expect.pattern", { pattern: "^[0-9]+$" }, true).$type).prop("rememberDevice", defineAnnotatedType().designType("boolean").tags("boolean").annotate("ui.form.type", "checkbox").annotate("meta.label", "Remember this device").optional().$type);
652
+ defineAnnotatedType("object", AskEmailForm).prop("email", defineAnnotatedType().designType("string").tags("email", "string").annotate("ui.form.type", "text").annotate("meta.label", "Email").annotate("ui.form.autocomplete", "email").annotate("meta.required", {}).annotate("expect.pattern", {
653
+ pattern: "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$",
654
+ flags: "",
655
+ message: "Invalid email format."
656
+ }, true).$type);
657
+ defineAnnotatedType("object", AskPhoneForm).prop("phone", defineAnnotatedType().designType("string").tags("string").annotate("ui.form.type", "text").annotate("meta.label", "Phone (E.164)").annotate("ui.form.autocomplete", "tel").annotate("meta.required", {}).$type);
658
+ defineAnnotatedType("object", TermsAcceptForm).prop("acceptedVersion", defineAnnotatedType().designType("string").tags("string").annotate("ui.form.type", "text").annotate("meta.label", "Accepted version").annotate("meta.required", {}).$type).prop("accepted", defineAnnotatedType().designType("boolean").tags("boolean").annotate("ui.form.type", "checkbox").annotate("meta.label", "I accept the Terms & Conditions").annotate("meta.required", {}).$type);
659
+ defineAnnotatedType("object", ProfileCompleteForm).prop("firstName", defineAnnotatedType().designType("string").tags("string").annotate("ui.form.type", "text").annotate("meta.label", "First name").optional().$type).prop("lastName", defineAnnotatedType().designType("string").tags("string").annotate("ui.form.type", "text").annotate("meta.label", "Last name").optional().$type);
660
+ defineAnnotatedType("object", ConsentMarketingForm).prop("optIn", defineAnnotatedType().designType("boolean").tags("boolean").annotate("ui.form.type", "checkbox").annotate("meta.label", "I would like to receive marketing emails").optional().$type);
661
+ defineAnnotatedType("object", TenantSelectForm).prop("tenantId", defineAnnotatedType().designType("string").tags("string").annotate("ui.form.type", "text").annotate("meta.label", "Tenant").annotate("meta.required", {}).$type);
662
+ defineAnnotatedType("object", PersonaSelectForm).prop("personaId", defineAnnotatedType().designType("string").tags("string").annotate("ui.form.type", "text").annotate("meta.label", "Persona").annotate("meta.required", {}).$type);
663
+ defineAnnotatedType("object", ConcurrencyLimitForm).prop("action", defineAnnotatedType().designType("string").tags("string").annotate("ui.form.type", "text").annotate("meta.label", "Action").annotate("meta.required", {}).annotate("expect.pattern", { pattern: "^(logoutOthers|cancel)$" }, true).$type);
664
+ defineAnnotatedType("object", MagicLinkRequestForm).prop("identifier", defineAnnotatedType().designType("string").tags("string").annotate("ui.form.type", "text").annotate("meta.label", "Email or username").annotate("ui.form.autocomplete", "username").annotate("meta.required", {}).$type);
665
+ defineAnnotatedType("object", RecoveryModeSelectForm).prop("mode", defineAnnotatedType().designType("string").tags("string").annotate("ui.form.type", "text").annotate("meta.label", "Recovery method").annotate("meta.required", {}).annotate("expect.pattern", { pattern: "^(magicLink|otp)$" }, true).$type);
666
+ defineAnnotatedType("object", RecoveryFactorForm).prop("factor", defineAnnotatedType().designType("string").tags("string").annotate("ui.form.type", "text").annotate("meta.label", "Factor").annotate("meta.required", {}).annotate("expect.pattern", { pattern: "^(phone|totp)$" }, true).$type).prop("value", defineAnnotatedType().designType("string").tags("string").annotate("ui.form.type", "text").annotate("meta.label", "Value").annotate("meta.required", {}).annotate("expect.minLength", { length: 4 }).annotate("expect.maxLength", { length: 12 }).$type);
667
+ //#endregion
668
+ //#region src/workflows/login.workflow.options.ts
669
+ const DEFAULT_MFA_CODE_TTL_MS = 300 * 1e3;
670
+ /**
671
+ * Deep-merge defaults with the user-supplied nested pojo. Each group has its
672
+ * own `{ ...defaults, ...input }` line — small enough that pulling in lodash
673
+ * would be silly.
674
+ */
675
+ function mergeLoginOpts(opts = {}) {
676
+ return {
677
+ alternateCredentials: {
678
+ forgotPassword: true,
679
+ signup: false,
680
+ magicLink: false,
681
+ magicLinkSkipsMfa: false,
682
+ magicLinkTtlMs: 30 * 6e4,
683
+ ssoProviders: [],
684
+ recoveryUrl: "/recover",
685
+ signupUrl: "/signup",
686
+ embedRecovery: false,
687
+ ...opts.alternateCredentials
688
+ },
689
+ guards: {
690
+ emailVerifiedRequired: false,
691
+ passwordExpiry: true,
692
+ passwordInitial: true,
693
+ ...opts.guards
694
+ },
695
+ enrollment: {
696
+ ensureEmail: false,
697
+ ensurePhone: false,
698
+ ...opts.enrollment
699
+ },
700
+ mfa: {
701
+ enabled: true,
702
+ transports: [
703
+ "sms",
704
+ "email",
705
+ "totp"
706
+ ],
707
+ backupCodes: true,
708
+ enrollRequired: false,
709
+ pincodeTtlMs: DEFAULT_MFA_CODE_TTL_MS,
710
+ pincodeResendTimeoutMs: 6e4,
711
+ pincodeLength: 6,
712
+ ...opts.mfa
713
+ },
714
+ deviceTrust: {
715
+ enabled: false,
716
+ optIn: true,
717
+ cookieName: "aooth_trusted_device",
718
+ ttlMs: 1440 * 6e4,
719
+ skipsMfa: true,
720
+ bindsTo: "cookie",
721
+ ...opts.deviceTrust
722
+ },
723
+ acceptance: {
724
+ profileCompleteRequired: false,
725
+ consentMarketing: false,
726
+ ...opts.acceptance
727
+ },
728
+ multiContext: {
729
+ tenantSelect: false,
730
+ personaSelect: false,
731
+ ...opts.multiContext
732
+ },
733
+ sessionPolicy: { ...opts.sessionPolicy },
734
+ finalize: {
735
+ auditLogin: true,
736
+ notifyNewDevice: false,
737
+ redirect: false,
738
+ ...opts.finalize
739
+ },
740
+ forms: {
741
+ askEmail: AskEmailForm,
742
+ askPhone: AskPhoneForm,
743
+ backupCode: BackupCodeForm,
744
+ concurrencyLimit: ConcurrencyLimitForm,
745
+ consentMarketing: ConsentMarketingForm,
746
+ loginCredentials: LoginCredentialsForm,
747
+ mfaCode: MfaCodeForm,
748
+ personaSelect: PersonaSelectForm,
749
+ pincode: PincodeForm,
750
+ profileComplete: ProfileCompleteForm,
751
+ select2fa: Select2faForm,
752
+ setPassword: SetPasswordForm,
753
+ tenantSelect: TenantSelectForm,
754
+ termsAccept: TermsAcceptForm,
755
+ ...opts.forms
756
+ }
757
+ };
758
+ }
759
+ //#endregion
760
+ //#region src/workflows/wf-helpers.ts
761
+ /**
762
+ * Internal helpers for the three auth workflows.
763
+ *
764
+ * Co-locates two patterns the @atscript/moost-wf reference docs tell consumers
765
+ * to copy into their own projects (`httpInputRequired` + `validateFormInput`)
766
+ * with auth-specific glue for the password-set error translation.
767
+ *
768
+ * Cookie + finished-response building lives on `useAuth()` (see
769
+ * `auth.composables.ts`) so it shares the same resolved options the guard
770
+ * stashed onto the HTTP event chain.
771
+ */
772
+ /**
773
+ * Special error keys:
774
+ * - `__form` — top-level form-wide error (e.g. "Invalid credentials").
775
+ * - everything else — keyed by field path.
776
+ */
777
+ function httpInputRequired(type, wfContext, errors) {
778
+ const context = { ...extractPassContext(type, wfContext) };
779
+ if (errors) context.errors = errors;
780
+ return outletHttp(serializeFormSchema(type), context);
781
+ }
782
+ /**
783
+ * Returns `null` when valid, or a flat `field → message` map. Top-level errors
784
+ * land on `__form`. `partial: 'deep'` validates only the fields the caller
785
+ * supplied (action-with-data submits).
786
+ */
787
+ function validateFormInput(type, input, opts = {}) {
788
+ const validator = type.validator({
789
+ unknownProps: "strip",
790
+ ...opts.partial === "deep" && { partial: "deep" }
791
+ });
792
+ try {
793
+ validator.validate(input);
794
+ return null;
795
+ } catch (err) {
796
+ if (err !== null && typeof err === "object" && "errors" in err && Array.isArray(err.errors)) {
797
+ const out = {};
798
+ for (const e of err.errors) out[e.path || "__form"] = e.message;
799
+ return out;
800
+ }
801
+ throw err;
802
+ }
803
+ }
804
+ /**
805
+ * Translate password-mutation errors from `UserService.setPassword` /
806
+ * `createUser` into the matching HTTP status. Mirrors `translatePasswordError`
807
+ * in `auth.controller.ts`; kept here so workflow steps don't import from the
808
+ * controller module. All `UserAuthError` shapes from a set-password call are
809
+ * client-side (policy / history / mismatch), so they collapse to 400.
810
+ */
811
+ function translatePasswordSetError(err) {
812
+ if (err instanceof UserAuthError) throw new HttpError$1(400, err.message);
813
+ throw err;
814
+ }
815
+ /**
816
+ * Asserts `ctx.username` is populated. Workflow steps reach for `ctx.username`
817
+ * after `credentials`/`init` has set it; losing it indicates a workflow-state
818
+ * bug, not a client error. Throws `HttpError(500)` on miss; otherwise narrows
819
+ * the field to `string` for the caller via `asserts`.
820
+ */
821
+ function requireUsername(ctx) {
822
+ if (!ctx.username) throw new HttpError$1(500, "Workflow state corrupted: missing username");
823
+ }
824
+ /** Mint a numeric pincode of the requested length using Math.random — fine for OTPs. */
825
+ function generatePincode$1(length) {
826
+ let out = "";
827
+ for (let i = 0; i < length; i++) out += Math.floor(Math.random() * 10).toString();
828
+ return out;
829
+ }
830
+ /**
831
+ * Mint a pincode + its expiry onto `ctx.pin` / `ctx.pinExpire`. Returns the
832
+ * code so the caller can hand it to the delivery transport.
833
+ */
834
+ function mintPin$1(ctx, length, ttlMs) {
835
+ const code = generatePincode$1(length);
836
+ ctx.pin = code;
837
+ ctx.pinExpire = Date.now() + ttlMs;
838
+ return code;
839
+ }
840
+ /**
841
+ * Verify a submitted pincode against `ctx.pin`. Returns a `{ code: '…' }`
842
+ * error map on expired/invalid, or `null` on success. Callers wrap with
843
+ * `httpInputRequired(PincodeForm, ctx, …)` to render.
844
+ */
845
+ function verifyPin$1(ctx, submitted) {
846
+ if (!ctx.pin || !ctx.pinExpire || Date.now() > ctx.pinExpire) return { code: "Code expired" };
847
+ if (submitted !== ctx.pin) return { code: "Invalid code" };
848
+ return null;
849
+ }
850
+ /**
851
+ * Resolve the client IP from the active HTTP request, swallowing the case
852
+ * where there is no HTTP context (unit tests that hand-roll the wf runtime).
853
+ */
854
+ function resolveClientIp() {
855
+ try {
856
+ return useRequest(current()).getIp?.() || void 0;
857
+ } catch {
858
+ return;
859
+ }
860
+ }
861
+ //#endregion
862
+ //#region src/workflows/login.workflow.ts
863
+ /**
864
+ * LoginWorkflow — `wfid = 'auth.login'`.
865
+ *
866
+ * Full step catalog per `WF_LOGIN.md`. Every advanced step is gated by the
867
+ * matching `LoginWorkflowOpts` flag so the default-opts flow matches today's
868
+ * "credentials → optional totp MFA → issue tokens" behaviour with no surprise
869
+ * prompts.
870
+ *
871
+ * **Step routing model.** Phase 1's `credentials` step is the main happy
872
+ * path. Alternate credential paths (`magicLink*`, `passkey`, `ssoCallback`)
873
+ * are stubs that throw `HttpError(501)` — the brief allows ships-as-stub for
874
+ * these. Channel-enrollment loops (`ensureEmail` / `ensurePhone`) are a
875
+ * single-pass ask-and-verify per the brief, since moost-wf does not provide
876
+ * a clean "loop while" primitive at the @WorkflowSchema level.
877
+ *
878
+ * **Alt-action delivery.** Form payloads carry `action?: string` alongside
879
+ * the regular form fields. Each form-bearing step inspects `input.action`
880
+ * for routing rather than wiring `@AltAction()` (the existing trigger
881
+ * controller does not call `useWfAction().setAction()`).
882
+ *
883
+ * **Consumer subclass pattern (Phase 2 reshape).** Consumers subclass
884
+ * `LoginWorkflow` to override `protected` hook methods. The subclass MUST
885
+ * re-apply `@Inherit() @Injectable('FOR_EVENT') @Controller()` and re-declare
886
+ * the constructor signature (TS emits fresh design-paramtypes per class).
887
+ *
888
+ * **Side-effect deps as protected methods (this reshape).** The four
889
+ * optional sender/store/emitter DI providers (EmailSender, SmsSender,
890
+ * DeviceTrustStore, AuditEmitter) have been DROPPED from the constructor.
891
+ * Side-effecting hooks live as `protected` methods consumers override:
892
+ *
893
+ * - `deliver(payload)` — unified email + SMS dispatch. Default throws
894
+ * `Error("deliver() not configured …")`; override to wire your senders.
895
+ * - `audit(event)` — fire audit events. Default: no-op.
896
+ * - `loadTrustedDevice(userId, token, ip?)` — return `true` to grant
897
+ * trust. Default: delegates to `UserService.verifyTrustedDevice`.
898
+ * - `storeTrustedDevice(userId, record)` — persist a freshly issued trust
899
+ * record. Default: delegates to `UserService.addTrustedDevice`.
900
+ * - `revokeTrustedDevice(userId, token)` — remove a trust record.
901
+ * Default: delegates to `UserService.revokeTrustedDevice`.
902
+ *
903
+ * Validation of senders is now INHERENT — the first `deliver()` invocation
904
+ * without an override throws. Boot-time "X required when Y enabled" checks
905
+ * are gone for sender/store/emitter; only data-validity checks remain.
906
+ */
907
+ var _ref$2, _ref2$2;
908
+ function mfaKindOf(methodName) {
909
+ if (methodName === "sms" || methodName === "email" || methodName === "totp") return methodName;
910
+ return null;
911
+ }
912
+ /**
913
+ * Sentinel returned by alt-action handlers that have already short-circuited
914
+ * the step (via `useWfFinished().set(...)` or by mutating ctx to drive the
915
+ * next iteration). The step body re-returns `undefined` after seeing this so
916
+ * the schema advances without running form validation against the alt-action
917
+ * payload (which lacks the form's required fields).
918
+ */
919
+ const ALT_HANDLED$2 = Symbol("ALT_HANDLED");
920
+ /** Mint a numeric pincode of the requested length using Math.random — fine for OTPs. */
921
+ function generatePincode(length) {
922
+ let out = "";
923
+ for (let i = 0; i < length; i++) out += Math.floor(Math.random() * 10).toString();
924
+ return out;
925
+ }
926
+ /**
927
+ * Mint and stash a fresh pincode + its expiry on `ctx`. Returns the code so
928
+ * the caller can hand it to the delivery transport.
929
+ */
930
+ function mintPin(ctx, length, ttlMs) {
931
+ const code = generatePincode(length);
932
+ ctx.pin = code;
933
+ ctx.pinExpire = Date.now() + ttlMs;
934
+ return code;
935
+ }
936
+ /**
937
+ * Verify a submitted pincode against the one stashed on `ctx`. Returns a
938
+ * `{ code: '…' }` error map when expired/invalid, or `null` on success.
939
+ * Callers wrap with `httpInputRequired(PincodeForm, ctx, …)` to render.
940
+ */
941
+ function verifyPin(ctx, submitted) {
942
+ if (!ctx.pin || !ctx.pinExpire || Date.now() > ctx.pinExpire) return { code: "Code expired" };
943
+ if (submitted !== ctx.pin) return { code: "Invalid code" };
944
+ return null;
945
+ }
946
+ /**
947
+ * Construction-time invariants for DATA validity only. Sender/store/emitter
948
+ * absence is no longer checked — those default to fail-loud (`deliver()`) or
949
+ * no-op (`audit()`, `loadTrustedDevice()`) protected methods that consumers
950
+ * override.
951
+ */
952
+ function validateOpts$1(opts) {
953
+ if (opts.mfa.enabled && opts.mfa.transports.length === 0) throw new Error("LoginWorkflow: mfa.transports cannot be empty when mfa.enabled is true");
954
+ }
955
+ let LoginWorkflow = class LoginWorkflow {
956
+ opts;
957
+ users;
958
+ auth;
959
+ constructor(opts, users, auth) {
960
+ this.opts = mergeLoginOpts(opts);
961
+ this.users = users;
962
+ this.auth = auth;
963
+ validateOpts$1(this.opts);
964
+ }
965
+ /**
966
+ * Dispatch an email or SMS event. Default throws — consumers MUST override
967
+ * if any feature that emits is enabled (MFA pincode, ensureEmail/Phone OTP,
968
+ * notifyNewDevice). The throw surfaces at the HTTP layer as 500 on the
969
+ * first event that triggers a send, which is the fail-loud signal.
970
+ */
971
+ async deliver(_payload) {
972
+ throw new Error("LoginWorkflow.deliver() not configured — override to wire your email/sms sender");
973
+ }
974
+ /**
975
+ * Emit an audit event. Default: no-op. Consumers override to fan out to
976
+ * their audit sink (DB table, log file, Kafka topic, …).
977
+ */
978
+ async audit(_event) {}
979
+ /**
980
+ * Verify whether a presented trust-cookie token belongs to `userId` and is
981
+ * still valid. Default: delegates to `UserService.verifyTrustedDevice`
982
+ * (HMAC + persisted record + expiry + IP-binding). Override to use a
983
+ * different trust backend.
984
+ */
985
+ async loadTrustedDevice(userId, token, ip) {
986
+ return this.users.verifyTrustedDevice(userId, token, ip);
987
+ }
988
+ /**
989
+ * Persist a freshly-issued trust record. Default: delegates to
990
+ * `UserService.addTrustedDevice` — the record is appended to the user's
991
+ * `trustedDevices` array on the user store. `userId` is the username the
992
+ * record belongs to (passed alongside since `TrustedDeviceRecord` itself
993
+ * carries no user identifier).
994
+ */
995
+ async storeTrustedDevice(userId, record) {
996
+ await this.users.addTrustedDevice(userId, record);
997
+ }
998
+ /**
999
+ * Revoke a trust record. Default: delegates to
1000
+ * `UserService.revokeTrustedDevice`. Currently unused by the workflow's own
1001
+ * happy path but exposed so consumers can call it from their own "sign out
1002
+ * everywhere" flows for symmetry with `storeTrustedDevice`.
1003
+ */
1004
+ async revokeTrustedDevice(userId, token) {
1005
+ await this.users.revokeTrustedDevice(userId, token);
1006
+ }
1007
+ /**
1008
+ * Mint a new device-trust record + cookie value. Default: delegates to
1009
+ * `UserService.issueTrustedDevice` — produces an HMAC-signed token bound to
1010
+ * `userId` (+ `ip` when `bindsTo === 'cookie+ip'`). Consumers running
1011
+ * multiple instances typically override `loadTrustedDevice`/
1012
+ * `storeTrustedDevice` against Redis but keep this default.
1013
+ */
1014
+ async issueTrustedDevice(userId, ip, ttlMs) {
1015
+ return this.users.issueTrustedDevice(userId, {
1016
+ ttlMs,
1017
+ ...ip !== void 0 && { ip }
1018
+ });
1019
+ }
1020
+ flow() {}
1021
+ init(ctx) {
1022
+ ctx.opts = this.snapshotOpts(this.opts);
1023
+ }
1024
+ /**
1025
+ * Returns the JSON-safe projection of `opts` stashed onto `ctx` for schema
1026
+ * conditions to read. Default: drop the `forms` group (atscript form classes
1027
+ * are not plain JSON) so `AsWfStore`'s plain-JSON persistence doesn't choke.
1028
+ * Step bodies still consult the form classes via `this.opts.forms.*`.
1029
+ *
1030
+ * Consumers who put non-JSON values on `opts` (e.g. by extending the type)
1031
+ * can override this to strip them.
1032
+ */
1033
+ snapshotOpts(opts) {
1034
+ const { forms: _forms, ...rest } = opts;
1035
+ return rest;
1036
+ }
1037
+ async credentials(input, ctx) {
1038
+ if (!input) return httpInputRequired(this.opts.forms.loginCredentials, ctx);
1039
+ if (input.action) {
1040
+ if (this.handleCredentialsAlt(input.action, input.username) === ALT_HANDLED$2) return void 0;
1041
+ }
1042
+ const errors = validateFormInput(this.opts.forms.loginCredentials, input);
1043
+ if (errors) return httpInputRequired(this.opts.forms.loginCredentials, ctx, errors);
1044
+ try {
1045
+ const result = await this.users.login(input.username, input.password);
1046
+ ctx.username = result.user.username;
1047
+ ctx.mfaRequired = result.mfaRequired;
1048
+ if (this.opts.guards.passwordInitial && result.user.password.isInitial) ctx.isPasswordInitial = true;
1049
+ const email = result.user.mfa.methods.find((m) => m.name === "email" && m.confirmed);
1050
+ if (email) {
1051
+ ctx.email = email.value;
1052
+ ctx.emailConfirmed = true;
1053
+ }
1054
+ const phone = result.user.mfa.methods.find((m) => m.name === "sms" && m.confirmed);
1055
+ if (phone) {
1056
+ ctx.phone = phone.value;
1057
+ ctx.phoneConfirmed = true;
1058
+ }
1059
+ } catch (err) {
1060
+ if (err instanceof UserAuthError) {
1061
+ if (err.type === "LOCKED") throw new HttpError$1(423, "Account locked");
1062
+ return httpInputRequired(this.opts.forms.loginCredentials, ctx, { __form: "Invalid credentials" });
1063
+ }
1064
+ throw err;
1065
+ }
1066
+ }
1067
+ handleCredentialsAlt(action, typedUsername) {
1068
+ const alt = this.opts.alternateCredentials;
1069
+ if (action === "forgotPassword" && alt.forgotPassword) {
1070
+ finishWfWithRedirect(this.buildRecoveryUrl(typedUsername), { reason: "forgot-password" });
1071
+ return ALT_HANDLED$2;
1072
+ }
1073
+ if (action === "signup" && alt.signup) {
1074
+ finishWfWithRedirect(alt.signupUrl, { reason: "signup" });
1075
+ return ALT_HANDLED$2;
1076
+ }
1077
+ if (action === "magicLink" && alt.magicLink) throw new HttpError$1(501, "Magic-link login path not implemented in this version");
1078
+ const sso = alt.ssoProviders.find((p) => p.id === action);
1079
+ if (sso) {
1080
+ finishWfWithRedirect(sso.url, { reason: `sso-${sso.id}` });
1081
+ return ALT_HANDLED$2;
1082
+ }
1083
+ }
1084
+ /**
1085
+ * Builds the redirect URL the `forgotPassword` alt-action navigates to.
1086
+ * Receives whatever the user typed into the username field so the recovery
1087
+ * page can pre-fill it. Default:
1088
+ * `${alternateCredentials.recoveryUrl}?username=${encodeURIComponent(username ?? '')}`.
1089
+ */
1090
+ buildRecoveryUrl(username) {
1091
+ return `${this.opts.alternateCredentials.recoveryUrl}?username=${encodeURIComponent(username ?? "")}`;
1092
+ }
1093
+ magicLinkRequest() {
1094
+ throw new HttpError$1(501, "magicLinkRequest step not implemented");
1095
+ }
1096
+ magicLinkSend() {
1097
+ throw new HttpError$1(501, "magicLinkSend step not implemented");
1098
+ }
1099
+ magicLinkVerified() {
1100
+ throw new HttpError$1(501, "magicLinkVerified step not implemented");
1101
+ }
1102
+ passkey() {
1103
+ throw new HttpError$1(501, "passkey step not implemented");
1104
+ }
1105
+ ssoCallback() {
1106
+ throw new HttpError$1(501, "ssoCallback step not implemented");
1107
+ }
1108
+ async ensureEmail(input, ctx) {
1109
+ requireUsername(ctx);
1110
+ if (!ctx.email) {
1111
+ if (!input?.email) return httpInputRequired(this.opts.forms.askEmail, ctx);
1112
+ const errors = validateFormInput(this.opts.forms.askEmail, input);
1113
+ if (errors) return httpInputRequired(this.opts.forms.askEmail, ctx, errors);
1114
+ await this.users.addMfaMethod(ctx.username, {
1115
+ name: "email",
1116
+ value: input.email,
1117
+ confirmed: false
1118
+ });
1119
+ ctx.email = input.email;
1120
+ const code = mintPin(ctx, this.opts.mfa.pincodeLength, this.opts.mfa.pincodeTtlMs);
1121
+ await this.deliver({
1122
+ channel: "email",
1123
+ kind: "login.pincode",
1124
+ recipient: input.email,
1125
+ code,
1126
+ expiresAt: ctx.pinExpire
1127
+ });
1128
+ return httpInputRequired(this.opts.forms.pincode, ctx);
1129
+ }
1130
+ if (!input?.code) return httpInputRequired(this.opts.forms.pincode, ctx);
1131
+ const errors = validateFormInput(this.opts.forms.pincode, input);
1132
+ if (errors) return httpInputRequired(this.opts.forms.pincode, ctx, errors);
1133
+ const pinErr = verifyPin(ctx, input.code);
1134
+ if (pinErr) return httpInputRequired(this.opts.forms.pincode, ctx, pinErr);
1135
+ await this.users.confirmMfaMethod(ctx.username, "email");
1136
+ ctx.emailConfirmed = true;
1137
+ ctx.pin = void 0;
1138
+ ctx.pinExpire = void 0;
1139
+ }
1140
+ async ensurePhone(input, ctx) {
1141
+ requireUsername(ctx);
1142
+ if (!ctx.phone) {
1143
+ if (!input?.phone) return httpInputRequired(this.opts.forms.askPhone, ctx);
1144
+ const errors = validateFormInput(this.opts.forms.askPhone, input);
1145
+ if (errors) return httpInputRequired(this.opts.forms.askPhone, ctx, errors);
1146
+ await this.users.addMfaMethod(ctx.username, {
1147
+ name: "sms",
1148
+ value: input.phone,
1149
+ confirmed: false
1150
+ });
1151
+ ctx.phone = input.phone;
1152
+ const code = mintPin(ctx, this.opts.mfa.pincodeLength, this.opts.mfa.pincodeTtlMs);
1153
+ await this.deliver({
1154
+ channel: "sms",
1155
+ kind: "login.pincode",
1156
+ recipient: input.phone,
1157
+ code,
1158
+ ttlMs: this.opts.mfa.pincodeTtlMs,
1159
+ userId: ctx.username
1160
+ });
1161
+ return httpInputRequired(this.opts.forms.pincode, ctx);
1162
+ }
1163
+ if (!input?.code) return httpInputRequired(this.opts.forms.pincode, ctx);
1164
+ const errors = validateFormInput(this.opts.forms.pincode, input);
1165
+ if (errors) return httpInputRequired(this.opts.forms.pincode, ctx, errors);
1166
+ const pinErr = verifyPin(ctx, input.code);
1167
+ if (pinErr) return httpInputRequired(this.opts.forms.pincode, ctx, pinErr);
1168
+ await this.users.confirmMfaMethod(ctx.username, "sms");
1169
+ ctx.phoneConfirmed = true;
1170
+ ctx.pin = void 0;
1171
+ ctx.pinExpire = void 0;
1172
+ }
1173
+ async checkTrustedDevice(ctx) {
1174
+ if (!ctx.username) return void 0;
1175
+ const cookieValue = useCookies(current()).getCookie(this.opts.deviceTrust.cookieName);
1176
+ if (!cookieValue) {
1177
+ ctx.newDevice = true;
1178
+ return;
1179
+ }
1180
+ const ip = this.opts.deviceTrust.bindsTo === "cookie+ip" ? this.resolveClientIp() : void 0;
1181
+ if (await this.loadTrustedDevice(ctx.username, cookieValue, ip)) {
1182
+ ctx.mfaChecked = true;
1183
+ ctx.deviceTrustToken = cookieValue;
1184
+ } else ctx.newDevice = true;
1185
+ }
1186
+ async prepareMfaOptions(ctx) {
1187
+ if (!ctx.username) return void 0;
1188
+ const user = await this.users.getUser(ctx.username);
1189
+ const allowed = new Set(this.opts.mfa.transports);
1190
+ const summary = this.users.getAvailableMfaMethods(user.mfa).filter((m) => {
1191
+ const kind = mfaKindOf(m.name);
1192
+ return kind !== null && allowed.has(kind);
1193
+ }).map((m) => {
1194
+ return {
1195
+ kind: mfaKindOf(m.name),
1196
+ methodName: m.name,
1197
+ masked: m.masked,
1198
+ isDefault: m.isDefault
1199
+ };
1200
+ });
1201
+ ctx.mfaEnrolledMethods = summary;
1202
+ if (summary.length === 0) {
1203
+ ctx.mfaChecked = true;
1204
+ return;
1205
+ }
1206
+ if (summary.length === 1) {
1207
+ ctx.mfaMethod = summary[0].kind;
1208
+ return;
1209
+ }
1210
+ if (!ctx.ignoreMfaDefault) {
1211
+ const def = summary.find((m) => m.isDefault);
1212
+ if (def) ctx.mfaMethod = def.kind;
1213
+ }
1214
+ }
1215
+ async select2fa(input, ctx) {
1216
+ if (!input) return httpInputRequired(this.opts.forms.select2fa, ctx);
1217
+ if (input.action === "useBackupCode" && this.opts.mfa.backupCodes) return this.handleBackupCode(input.code ? { code: input.code } : void 0, ctx);
1218
+ const errors = validateFormInput(this.opts.forms.select2fa, input);
1219
+ if (errors) return httpInputRequired(this.opts.forms.select2fa, ctx, errors);
1220
+ const picked = (ctx.mfaEnrolledMethods ?? []).find((m) => m.methodName === input.methodName);
1221
+ if (!picked) return httpInputRequired(this.opts.forms.select2fa, ctx, { methodName: "Unknown MFA method" });
1222
+ ctx.mfaMethod = picked.kind;
1223
+ ctx.mfaSaveAsDefault = Boolean(input.saveAsDefault);
1224
+ if (ctx.mfaSaveAsDefault && ctx.username) await this.users.setDefaultMfaMethod(ctx.username, picked.methodName);
1225
+ }
1226
+ async pincodeSendLogin(ctx) {
1227
+ if (!ctx.username || !ctx.mfaMethod) return void 0;
1228
+ const summary = (ctx.mfaEnrolledMethods ?? []).find((m) => m.kind === ctx.mfaMethod);
1229
+ if (!summary) throw new HttpError$1(500, "Workflow state corrupted: missing mfa method");
1230
+ const method = (await this.users.getUser(ctx.username)).mfa.methods.find((m) => m.name === summary.methodName && m.confirmed);
1231
+ if (!method) throw new HttpError$1(500, "MFA method no longer present");
1232
+ const code = mintPin(ctx, this.opts.mfa.pincodeLength, this.opts.mfa.pincodeTtlMs);
1233
+ ctx.pinTimeout = Date.now() + this.opts.mfa.pincodeResendTimeoutMs;
1234
+ if (ctx.mfaMethod === "email") {
1235
+ ctx.pinSentTo = maskEmail(method.value);
1236
+ await this.deliver({
1237
+ channel: "email",
1238
+ kind: "login.pincode",
1239
+ recipient: method.value,
1240
+ code,
1241
+ expiresAt: ctx.pinExpire,
1242
+ userId: ctx.username
1243
+ });
1244
+ } else if (ctx.mfaMethod === "sms") {
1245
+ ctx.pinSentTo = maskPhone(method.value);
1246
+ await this.deliver({
1247
+ channel: "sms",
1248
+ kind: "login.pincode",
1249
+ recipient: method.value,
1250
+ code,
1251
+ ttlMs: this.opts.mfa.pincodeTtlMs,
1252
+ userId: ctx.username
1253
+ });
1254
+ }
1255
+ }
1256
+ async pincodeCheckLogin(input, ctx) {
1257
+ if (!input) return httpInputRequired(this.opts.forms.pincode, ctx);
1258
+ if (input.action === "resend") {
1259
+ if (ctx.pinTimeout && Date.now() < ctx.pinTimeout) {
1260
+ const waitSec = Math.ceil((ctx.pinTimeout - Date.now()) / 1e3);
1261
+ return httpInputRequired(this.opts.forms.pincode, ctx, { __form: `Please wait ${waitSec}s` });
1262
+ }
1263
+ ctx.pin = void 0;
1264
+ ctx.pinExpire = void 0;
1265
+ return httpInputRequired(this.opts.forms.pincode, ctx, { __form: "Code resent" });
1266
+ }
1267
+ if (input.action === "useDifferentMethod") {
1268
+ ctx.ignoreMfaDefault = true;
1269
+ ctx.mfaMethod = void 0;
1270
+ ctx.pin = void 0;
1271
+ ctx.pinExpire = void 0;
1272
+ return;
1273
+ }
1274
+ if (input.action === "useBackupCode" && this.opts.mfa.backupCodes) return this.handleBackupCode(input.code ? { code: input.code } : void 0, ctx);
1275
+ const errors = validateFormInput(this.opts.forms.pincode, input);
1276
+ if (errors) return httpInputRequired(this.opts.forms.pincode, ctx, errors);
1277
+ const pinErr = verifyPin(ctx, input.code);
1278
+ if (pinErr) return httpInputRequired(this.opts.forms.pincode, ctx, pinErr);
1279
+ ctx.mfaChecked = true;
1280
+ ctx.riskStepUpEvaluated = false;
1281
+ if (this.opts.deviceTrust.enabled && this.opts.deviceTrust.optIn) ctx.rememberDevice = Boolean(input.rememberDevice);
1282
+ }
1283
+ async mfaTotp(input, ctx) {
1284
+ if (!input) return httpInputRequired(this.opts.forms.mfaCode, ctx);
1285
+ if (input.action === "useDifferentMethod") {
1286
+ ctx.ignoreMfaDefault = true;
1287
+ ctx.mfaMethod = void 0;
1288
+ return;
1289
+ }
1290
+ if (input.action === "useBackupCode" && this.opts.mfa.backupCodes) return this.handleBackupCode(input.code ? { code: input.code } : void 0, ctx);
1291
+ const errors = validateFormInput(this.opts.forms.mfaCode, input);
1292
+ if (errors) return httpInputRequired(this.opts.forms.mfaCode, ctx, errors);
1293
+ requireUsername(ctx);
1294
+ try {
1295
+ await this.users.verifyMfa(ctx.username, input.code);
1296
+ ctx.mfaChecked = true;
1297
+ ctx.riskStepUpEvaluated = false;
1298
+ if (this.opts.deviceTrust.enabled && this.opts.deviceTrust.optIn) ctx.rememberDevice = Boolean(input.rememberDevice);
1299
+ } catch (err) {
1300
+ if (err instanceof UserAuthError) {
1301
+ if (err.type === "LOCKED") throw new HttpError$1(423, "Account locked");
1302
+ if (err.type === "INACTIVE") throw new HttpError$1(401, "Invalid credentials");
1303
+ if (err.type === "MFA_NOT_CONFIGURED") throw new HttpError$1(400, "No TOTP MFA configured");
1304
+ if (err.type === "MFA_INVALID") {
1305
+ if (err.details?.lockEnds !== void 0) throw new HttpError$1(423, "Account locked");
1306
+ return httpInputRequired(this.opts.forms.mfaCode, ctx, { code: "Invalid code" });
1307
+ }
1308
+ }
1309
+ throw err;
1310
+ }
1311
+ }
1312
+ /**
1313
+ * Backup-code alt-action handler shared by `select2fa`, `pincode-check-login`,
1314
+ * and `mfa-totp`. Validates against `BackupCodeForm` (alphanumeric +
1315
+ * hyphen-grouped — `MfaCodeForm` is digits-only and rejects backup codes
1316
+ * produced by `UserService.generateBackupCodes`).
1317
+ */
1318
+ async handleBackupCode(input, ctx) {
1319
+ if (!input) return httpInputRequired(this.opts.forms.backupCode, ctx);
1320
+ const errors = validateFormInput(this.opts.forms.backupCode, input);
1321
+ if (errors) return httpInputRequired(this.opts.forms.backupCode, ctx, errors);
1322
+ requireUsername(ctx);
1323
+ if (!await this.users.consumeBackupCode(ctx.username, input.code)) return httpInputRequired(this.opts.forms.backupCode, ctx, { code: "Invalid backup code" });
1324
+ ctx.mfaChecked = true;
1325
+ ctx.riskStepUpEvaluated = false;
1326
+ }
1327
+ mfaEnrollRequired() {
1328
+ throw new HttpError$1(501, "mfa-enroll-required step not implemented");
1329
+ }
1330
+ async deviceTrust(ctx) {
1331
+ if (!ctx.username) return void 0;
1332
+ const ip = this.opts.deviceTrust.bindsTo === "cookie+ip" ? this.resolveClientIp() : void 0;
1333
+ const record = await this.issueTrustedDevice(ctx.username, ip, this.opts.deviceTrust.ttlMs);
1334
+ await this.storeTrustedDevice(ctx.username, record);
1335
+ ctx.deviceTrustToken = record.token;
1336
+ useResponse(current()).setCookie(this.opts.deviceTrust.cookieName, record.token, useAuth().cookieAttrs({ maxAge: this.opts.deviceTrust.ttlMs / 1e3 }));
1337
+ }
1338
+ preparePasswordRules(ctx) {
1339
+ ctx.passwordPolicies = this.users.getTransferablePolicies();
1340
+ }
1341
+ async createPasswordForm(input, ctx) {
1342
+ if (!input) return httpInputRequired(this.opts.forms.setPassword, ctx);
1343
+ if (input.action === "logout") {
1344
+ finishWfAborted("logout", { message: {
1345
+ level: "info",
1346
+ text: "Signed out."
1347
+ } });
1348
+ ctx.aborted = true;
1349
+ return;
1350
+ }
1351
+ const errors = validateFormInput(this.opts.forms.setPassword, input);
1352
+ if (errors) return httpInputRequired(this.opts.forms.setPassword, ctx, errors);
1353
+ if (input.newPassword !== input.confirmPassword) return httpInputRequired(this.opts.forms.setPassword, ctx, { confirmPassword: "Passwords do not match" });
1354
+ requireUsername(ctx);
1355
+ try {
1356
+ await this.users.setPassword(ctx.username, input.newPassword);
1357
+ } catch (err) {
1358
+ translatePasswordSetError(err);
1359
+ }
1360
+ ctx.passwordChanged = true;
1361
+ ctx.isPasswordInitial = false;
1362
+ }
1363
+ async termsAccept(input, ctx) {
1364
+ if (!input) return httpInputRequired(this.opts.forms.termsAccept, ctx);
1365
+ if (input.action === "decline") {
1366
+ finishWfAborted("termsDeclined", { message: {
1367
+ level: "info",
1368
+ text: "You must accept to continue"
1369
+ } });
1370
+ ctx.aborted = true;
1371
+ return;
1372
+ }
1373
+ const errors = validateFormInput(this.opts.forms.termsAccept, input);
1374
+ if (errors) return httpInputRequired(this.opts.forms.termsAccept, ctx, errors);
1375
+ if (input.accepted !== true) return httpInputRequired(this.opts.forms.termsAccept, ctx, { accepted: "You must accept the terms" });
1376
+ if (input.acceptedVersion !== this.opts.acceptance.termsVersion) return httpInputRequired(this.opts.forms.termsAccept, ctx, { acceptedVersion: "Version mismatch — please retry" });
1377
+ ctx.termsAcceptedVersion = input.acceptedVersion;
1378
+ ctx.termsAcceptedDone = true;
1379
+ }
1380
+ async profileComplete(input, ctx) {
1381
+ const form = this.opts.forms.profileComplete;
1382
+ if (!input) return httpInputRequired(form, ctx);
1383
+ const errors = validateFormInput(form, input, { partial: "deep" });
1384
+ if (errors) return httpInputRequired(form, ctx, errors);
1385
+ requireUsername(ctx);
1386
+ await this.applyProfile(ctx.username, input);
1387
+ ctx.profileApplied = true;
1388
+ }
1389
+ /**
1390
+ * Persists the profile-complete payload onto the user record. Default:
1391
+ * no-op (the workflow records the form was submitted but writes nothing).
1392
+ * Consumers override to write into their user store.
1393
+ */
1394
+ async applyProfile(_username, _payload) {}
1395
+ async consentMarketing(input, ctx) {
1396
+ if (!input) return httpInputRequired(this.opts.forms.consentMarketing, ctx);
1397
+ requireUsername(ctx);
1398
+ await this.applyConsentMarketing(ctx.username, Boolean(input.optIn));
1399
+ ctx.consentApplied = true;
1400
+ }
1401
+ /**
1402
+ * Persists the marketing consent decision. Default: no-op.
1403
+ */
1404
+ async applyConsentMarketing(_username, _optIn) {}
1405
+ async tenantSelect(input, ctx) {
1406
+ if (!ctx.availableTenants && ctx.username) ctx.availableTenants = await this.loadTenants(ctx.username);
1407
+ if (!input) return httpInputRequired(this.opts.forms.tenantSelect, ctx);
1408
+ const errors = validateFormInput(this.opts.forms.tenantSelect, input);
1409
+ if (errors) return httpInputRequired(this.opts.forms.tenantSelect, ctx, errors);
1410
+ if (!(ctx.availableTenants ?? []).some((t) => t.id === input.tenantId)) return httpInputRequired(this.opts.forms.tenantSelect, ctx, { tenantId: "Unknown tenant" });
1411
+ ctx.selectedTenantId = input.tenantId;
1412
+ }
1413
+ /**
1414
+ * Resolves the user's available tenants. Default: empty array. Consumers
1415
+ * who enable `multiContext.tenantSelect` must override this to return the
1416
+ * tenants the user belongs to.
1417
+ */
1418
+ async loadTenants(_username) {
1419
+ return [];
1420
+ }
1421
+ async personaSelect(input, ctx) {
1422
+ if (!ctx.availablePersonas && ctx.username) ctx.availablePersonas = await this.loadPersonas(ctx.username);
1423
+ if (!input) return httpInputRequired(this.opts.forms.personaSelect, ctx);
1424
+ const errors = validateFormInput(this.opts.forms.personaSelect, input);
1425
+ if (errors) return httpInputRequired(this.opts.forms.personaSelect, ctx, errors);
1426
+ if (!(ctx.availablePersonas ?? []).some((p) => p.id === input.personaId)) return httpInputRequired(this.opts.forms.personaSelect, ctx, { personaId: "Unknown persona" });
1427
+ ctx.selectedPersonaId = input.personaId;
1428
+ }
1429
+ /**
1430
+ * Resolves the user's available personas. Default: empty array. Consumers
1431
+ * who enable `multiContext.personaSelect` must override this.
1432
+ */
1433
+ async loadPersonas(_username) {
1434
+ return [];
1435
+ }
1436
+ async concurrencyLimit(input, ctx) {
1437
+ const cfg = this.opts.sessionPolicy.concurrencyLimit;
1438
+ if (!cfg) return void 0;
1439
+ if (cfg.onLimit === "reject") throw new HttpError$1(429, "Session limit reached");
1440
+ if (!input) return httpInputRequired(this.opts.forms.concurrencyLimit, ctx);
1441
+ if (input.action === "cancel") {
1442
+ finishWfAborted("sessionLimit", { message: {
1443
+ level: "warn",
1444
+ text: "Concurrent session limit reached."
1445
+ } });
1446
+ ctx.aborted = true;
1447
+ return;
1448
+ }
1449
+ const errors = validateFormInput(this.opts.forms.concurrencyLimit, input);
1450
+ if (errors) return httpInputRequired(this.opts.forms.concurrencyLimit, ctx, errors);
1451
+ if (input.action === "logoutOthers" && ctx.username) await this.logoutOtherSessions(ctx.username);
1452
+ }
1453
+ /**
1454
+ * Implements the "log out other sessions" branch of `sessionPolicy.concurrencyLimit`.
1455
+ * Default: no-op. Consumers override to revoke sessions in their auth store.
1456
+ */
1457
+ async logoutOtherSessions(_username) {}
1458
+ async riskStepUp(ctx) {
1459
+ ctx.riskStepUpEvaluated = true;
1460
+ const res = await this.assessRiskStepUp(ctx);
1461
+ if (res.require) {
1462
+ ctx.riskStepUpReason = res.reason ?? "additional verification required";
1463
+ ctx.mfaChecked = false;
1464
+ delete ctx.pin;
1465
+ delete ctx.pinExpire;
1466
+ } else delete ctx.riskStepUpReason;
1467
+ }
1468
+ /**
1469
+ * Risk-step-up assessor. Default: never requires an extra factor. Consumers
1470
+ * override to inspect ctx (IP, geo, time since last login, etc.) and return
1471
+ * `{require: true, reason: '…'}` to force an additional MFA round.
1472
+ */
1473
+ async assessRiskStepUp(_ctx) {
1474
+ return { require: false };
1475
+ }
1476
+ async issue(ctx) {
1477
+ requireUsername(ctx);
1478
+ const issue = await this.auth.issue(ctx.username);
1479
+ ctx.tokensIssued = true;
1480
+ const auth = useAuth();
1481
+ const envelope = {
1482
+ finished: true,
1483
+ data: auth.buildLoginResponse(ctx.username, issue)
1484
+ };
1485
+ useWfFinished().set({
1486
+ type: "data",
1487
+ value: envelope,
1488
+ cookies: auth.buildFinishedCookies(issue)
1489
+ });
1490
+ }
1491
+ async auditLogin(ctx) {
1492
+ await this.audit({
1493
+ kind: "login.success",
1494
+ userId: ctx.username,
1495
+ workflow: "auth.login",
1496
+ method: ctx.mfaMethod ?? (ctx.mfaChecked ? "mfa.skipped" : "password"),
1497
+ ip: this.resolveClientIp(),
1498
+ ...ctx.selectedTenantId && { tenantId: ctx.selectedTenantId }
1499
+ });
1500
+ }
1501
+ async notifyNewDevice(ctx) {
1502
+ if (!ctx.email) return void 0;
1503
+ await this.deliver({
1504
+ channel: "email",
1505
+ kind: "notifyNewDevice",
1506
+ recipient: ctx.email,
1507
+ expiresAt: Date.now(),
1508
+ userId: ctx.username,
1509
+ metadata: { ip: this.resolveClientIp() ?? "" }
1510
+ });
1511
+ }
1512
+ redirect(ctx) {
1513
+ const url = this.resolveRedirect(ctx);
1514
+ if (!url) return void 0;
1515
+ const existing = useWfFinished().get();
1516
+ const envelope = {
1517
+ finished: true,
1518
+ end: {
1519
+ mode: "immediate",
1520
+ action: {
1521
+ type: "redirect",
1522
+ target: url,
1523
+ reason: "finalize-redirect"
1524
+ }
1525
+ }
1526
+ };
1527
+ useWfFinished().set({
1528
+ type: "data",
1529
+ value: envelope,
1530
+ ...existing?.cookies && { cookies: existing.cookies }
1531
+ });
1532
+ ctx.redirectUrl = url;
1533
+ }
1534
+ /**
1535
+ * Resolves the post-login redirect URL. Default reads
1536
+ * `finalize.redirect`: `false` / `null` (the default) → no redirect, the
1537
+ * `issue` step's data response stands (typical for SPAs/API clients);
1538
+ * `'home'` → `/`; `'referer'` → request `Referer` header (undefined when
1539
+ * absent, falling back to the data response).
1540
+ *
1541
+ * Consumers who want a computed redirect override this method.
1542
+ */
1543
+ resolveRedirect(_ctx) {
1544
+ const r = this.opts.finalize.redirect;
1545
+ if (r === false || r === null) return void 0;
1546
+ if (r === "home") return "/";
1547
+ if (r === "referer") try {
1548
+ const headers = useRequest(current()).headers;
1549
+ const ref = headers?.referer ?? headers?.referrer;
1550
+ const first = Array.isArray(ref) ? ref[0] : ref;
1551
+ return typeof first === "string" && first.length > 0 ? first : void 0;
1552
+ } catch {
1553
+ return;
1554
+ }
1555
+ }
1556
+ resolveClientIp() {
1557
+ try {
1558
+ return useRequest(current()).getIp?.() || void 0;
1559
+ } catch {
1560
+ return;
1561
+ }
1562
+ }
1563
+ };
1564
+ __decorate([
1565
+ Workflow("auth.login"),
1566
+ WorkflowSchema([
1567
+ { id: "init" },
1568
+ { id: "credentials" },
1569
+ {
1570
+ id: "magicLinkRequest",
1571
+ condition: () => false
1572
+ },
1573
+ {
1574
+ id: "magicLinkSend",
1575
+ condition: () => false
1576
+ },
1577
+ {
1578
+ id: "magicLinkVerified",
1579
+ condition: () => false
1580
+ },
1581
+ {
1582
+ id: "passkey",
1583
+ condition: () => false
1584
+ },
1585
+ {
1586
+ id: "ssoCallback",
1587
+ condition: () => false
1588
+ },
1589
+ {
1590
+ id: "ensureEmail",
1591
+ condition: (ctx) => !!ctx.username && (ctx.opts.enrollment.ensureEmail || ctx.opts.guards.emailVerifiedRequired) && !ctx.emailConfirmed && !ctx.aborted
1592
+ },
1593
+ {
1594
+ id: "ensurePhone",
1595
+ condition: (ctx) => !!ctx.username && ctx.opts.enrollment.ensurePhone && !ctx.phoneConfirmed && !ctx.aborted
1596
+ },
1597
+ {
1598
+ while: (ctx) => !!(ctx.username && ctx.opts.mfa.enabled && !ctx.mfaChecked && !ctx.aborted),
1599
+ steps: [
1600
+ {
1601
+ id: "check-trusted-device",
1602
+ condition: (ctx) => ctx.opts.deviceTrust.enabled && ctx.opts.deviceTrust.skipsMfa && !ctx.mfaChecked
1603
+ },
1604
+ {
1605
+ id: "prepare-mfa-options",
1606
+ condition: (ctx) => !ctx.mfaChecked
1607
+ },
1608
+ {
1609
+ id: "select2fa",
1610
+ condition: (ctx) => !ctx.mfaChecked && !ctx.mfaMethod && (ctx.mfaEnrolledMethods?.length ?? 0) > 1
1611
+ },
1612
+ {
1613
+ id: "pincode-send-login",
1614
+ condition: (ctx) => !ctx.mfaChecked && (ctx.mfaMethod === "sms" || ctx.mfaMethod === "email") && !ctx.pin
1615
+ },
1616
+ {
1617
+ id: "pincode-check-login",
1618
+ condition: (ctx) => !ctx.mfaChecked && (ctx.mfaMethod === "sms" || ctx.mfaMethod === "email")
1619
+ },
1620
+ {
1621
+ id: "mfa-totp",
1622
+ condition: (ctx) => !ctx.mfaChecked && ctx.mfaMethod === "totp"
1623
+ },
1624
+ {
1625
+ id: "mfa-enroll-required",
1626
+ condition: (ctx) => ctx.opts.mfa.enrollRequired && !ctx.mfaChecked && (ctx.mfaEnrolledMethods?.length ?? 0) === 0
1627
+ },
1628
+ {
1629
+ id: "risk-step-up",
1630
+ condition: (ctx) => !!(ctx.mfaChecked && !ctx.riskStepUpEvaluated)
1631
+ }
1632
+ ]
1633
+ },
1634
+ {
1635
+ id: "device-trust",
1636
+ condition: (ctx) => !!ctx.username && ctx.opts.deviceTrust.enabled && !!ctx.mfaChecked && !!ctx.newDevice && (!ctx.opts.deviceTrust.optIn || !!ctx.rememberDevice) && !ctx.aborted
1637
+ },
1638
+ {
1639
+ id: "prepare-password-rules",
1640
+ condition: (ctx) => !!ctx.isPasswordInitial && !ctx.passwordChanged && !ctx.aborted
1641
+ },
1642
+ {
1643
+ id: "create-password-form",
1644
+ condition: (ctx) => !!ctx.isPasswordInitial && !ctx.passwordChanged && !ctx.aborted
1645
+ },
1646
+ {
1647
+ id: "terms-accept",
1648
+ condition: (ctx) => !!ctx.username && !!ctx.opts.acceptance.termsVersion && ctx.termsAcceptedVersion !== ctx.opts.acceptance.termsVersion && !ctx.termsAcceptedDone && !ctx.aborted
1649
+ },
1650
+ {
1651
+ id: "profile-complete",
1652
+ condition: (ctx) => !!ctx.username && ctx.opts.acceptance.profileCompleteRequired && !ctx.profileApplied && (ctx.profileMissingFields?.length ?? 0) > 0 && !ctx.aborted
1653
+ },
1654
+ {
1655
+ id: "consent-marketing",
1656
+ condition: (ctx) => !!ctx.username && ctx.opts.acceptance.consentMarketing && !ctx.consentApplied && !ctx.aborted
1657
+ },
1658
+ {
1659
+ id: "tenant-select",
1660
+ condition: (ctx) => !!ctx.username && ctx.opts.multiContext.tenantSelect && !ctx.selectedTenantId && (ctx.availableTenants?.length ?? 0) > 1 && !ctx.aborted
1661
+ },
1662
+ {
1663
+ id: "persona-select",
1664
+ condition: (ctx) => !!ctx.username && ctx.opts.multiContext.personaSelect && !ctx.selectedPersonaId && (ctx.availablePersonas?.length ?? 0) > 1 && !ctx.aborted
1665
+ },
1666
+ {
1667
+ id: "concurrency-limit",
1668
+ condition: (ctx) => !!ctx.username && !!ctx.opts.sessionPolicy.concurrencyLimit && (ctx.activeSessions ?? 0) >= ctx.opts.sessionPolicy.concurrencyLimit.max && !ctx.aborted
1669
+ },
1670
+ {
1671
+ id: "issue",
1672
+ condition: (ctx) => !!ctx.username && !ctx.tokensIssued && !ctx.aborted
1673
+ },
1674
+ {
1675
+ id: "audit-login",
1676
+ condition: (ctx) => !!ctx.username && ctx.opts.finalize.auditLogin && !!ctx.tokensIssued && !ctx.aborted
1677
+ },
1678
+ {
1679
+ id: "notify-new-device",
1680
+ condition: (ctx) => !!ctx.username && ctx.opts.finalize.notifyNewDevice && !!ctx.newDevice && !!ctx.tokensIssued && !ctx.aborted
1681
+ },
1682
+ {
1683
+ id: "redirect",
1684
+ condition: (ctx) => !!ctx.username && !!ctx.tokensIssued && !ctx.aborted
1685
+ }
1686
+ ]),
1687
+ __decorateMetadata("design:type", Function),
1688
+ __decorateMetadata("design:paramtypes", []),
1689
+ __decorateMetadata("design:returntype", void 0)
1690
+ ], LoginWorkflow.prototype, "flow", null);
1691
+ __decorate([
1692
+ Step("init"),
1693
+ __decorateParam(0, WorkflowParam("context")),
1694
+ __decorateMetadata("design:type", Function),
1695
+ __decorateMetadata("design:paramtypes", [Object]),
1696
+ __decorateMetadata("design:returntype", void 0)
1697
+ ], LoginWorkflow.prototype, "init", null);
1698
+ __decorate([
1699
+ Step("credentials"),
1700
+ __decorateParam(0, WorkflowParam("input")),
1701
+ __decorateParam(1, WorkflowParam("context")),
1702
+ __decorateMetadata("design:type", Function),
1703
+ __decorateMetadata("design:paramtypes", [Object, Object]),
1704
+ __decorateMetadata("design:returntype", Promise)
1705
+ ], LoginWorkflow.prototype, "credentials", null);
1706
+ __decorate([
1707
+ Step("magicLinkRequest"),
1708
+ __decorateMetadata("design:type", Function),
1709
+ __decorateMetadata("design:paramtypes", []),
1710
+ __decorateMetadata("design:returntype", void 0)
1711
+ ], LoginWorkflow.prototype, "magicLinkRequest", null);
1712
+ __decorate([
1713
+ Step("magicLinkSend"),
1714
+ __decorateMetadata("design:type", Function),
1715
+ __decorateMetadata("design:paramtypes", []),
1716
+ __decorateMetadata("design:returntype", void 0)
1717
+ ], LoginWorkflow.prototype, "magicLinkSend", null);
1718
+ __decorate([
1719
+ Step("magicLinkVerified"),
1720
+ __decorateMetadata("design:type", Function),
1721
+ __decorateMetadata("design:paramtypes", []),
1722
+ __decorateMetadata("design:returntype", void 0)
1723
+ ], LoginWorkflow.prototype, "magicLinkVerified", null);
1724
+ __decorate([
1725
+ Step("passkey"),
1726
+ __decorateMetadata("design:type", Function),
1727
+ __decorateMetadata("design:paramtypes", []),
1728
+ __decorateMetadata("design:returntype", void 0)
1729
+ ], LoginWorkflow.prototype, "passkey", null);
1730
+ __decorate([
1731
+ Step("ssoCallback"),
1732
+ __decorateMetadata("design:type", Function),
1733
+ __decorateMetadata("design:paramtypes", []),
1734
+ __decorateMetadata("design:returntype", void 0)
1735
+ ], LoginWorkflow.prototype, "ssoCallback", null);
1736
+ __decorate([
1737
+ Step("ensureEmail"),
1738
+ __decorateParam(0, WorkflowParam("input")),
1739
+ __decorateParam(1, WorkflowParam("context")),
1740
+ __decorateMetadata("design:type", Function),
1741
+ __decorateMetadata("design:paramtypes", [Object, Object]),
1742
+ __decorateMetadata("design:returntype", Promise)
1743
+ ], LoginWorkflow.prototype, "ensureEmail", null);
1744
+ __decorate([
1745
+ Step("ensurePhone"),
1746
+ __decorateParam(0, WorkflowParam("input")),
1747
+ __decorateParam(1, WorkflowParam("context")),
1748
+ __decorateMetadata("design:type", Function),
1749
+ __decorateMetadata("design:paramtypes", [Object, Object]),
1750
+ __decorateMetadata("design:returntype", Promise)
1751
+ ], LoginWorkflow.prototype, "ensurePhone", null);
1752
+ __decorate([
1753
+ Step("check-trusted-device"),
1754
+ __decorateParam(0, WorkflowParam("context")),
1755
+ __decorateMetadata("design:type", Function),
1756
+ __decorateMetadata("design:paramtypes", [Object]),
1757
+ __decorateMetadata("design:returntype", Promise)
1758
+ ], LoginWorkflow.prototype, "checkTrustedDevice", null);
1759
+ __decorate([
1760
+ Step("prepare-mfa-options"),
1761
+ __decorateParam(0, WorkflowParam("context")),
1762
+ __decorateMetadata("design:type", Function),
1763
+ __decorateMetadata("design:paramtypes", [Object]),
1764
+ __decorateMetadata("design:returntype", Promise)
1765
+ ], LoginWorkflow.prototype, "prepareMfaOptions", null);
1766
+ __decorate([
1767
+ Step("select2fa"),
1768
+ __decorateParam(0, WorkflowParam("input")),
1769
+ __decorateParam(1, WorkflowParam("context")),
1770
+ __decorateMetadata("design:type", Function),
1771
+ __decorateMetadata("design:paramtypes", [Object, Object]),
1772
+ __decorateMetadata("design:returntype", Promise)
1773
+ ], LoginWorkflow.prototype, "select2fa", null);
1774
+ __decorate([
1775
+ Step("pincode-send-login"),
1776
+ __decorateParam(0, WorkflowParam("context")),
1777
+ __decorateMetadata("design:type", Function),
1778
+ __decorateMetadata("design:paramtypes", [Object]),
1779
+ __decorateMetadata("design:returntype", Promise)
1780
+ ], LoginWorkflow.prototype, "pincodeSendLogin", null);
1781
+ __decorate([
1782
+ Step("pincode-check-login"),
1783
+ __decorateParam(0, WorkflowParam("input")),
1784
+ __decorateParam(1, WorkflowParam("context")),
1785
+ __decorateMetadata("design:type", Function),
1786
+ __decorateMetadata("design:paramtypes", [Object, Object]),
1787
+ __decorateMetadata("design:returntype", Promise)
1788
+ ], LoginWorkflow.prototype, "pincodeCheckLogin", null);
1789
+ __decorate([
1790
+ Step("mfa-totp"),
1791
+ __decorateParam(0, WorkflowParam("input")),
1792
+ __decorateParam(1, WorkflowParam("context")),
1793
+ __decorateMetadata("design:type", Function),
1794
+ __decorateMetadata("design:paramtypes", [Object, Object]),
1795
+ __decorateMetadata("design:returntype", Promise)
1796
+ ], LoginWorkflow.prototype, "mfaTotp", null);
1797
+ __decorate([
1798
+ Step("mfa-enroll-required"),
1799
+ __decorateMetadata("design:type", Function),
1800
+ __decorateMetadata("design:paramtypes", []),
1801
+ __decorateMetadata("design:returntype", void 0)
1802
+ ], LoginWorkflow.prototype, "mfaEnrollRequired", null);
1803
+ __decorate([
1804
+ Step("device-trust"),
1805
+ __decorateParam(0, WorkflowParam("context")),
1806
+ __decorateMetadata("design:type", Function),
1807
+ __decorateMetadata("design:paramtypes", [Object]),
1808
+ __decorateMetadata("design:returntype", Promise)
1809
+ ], LoginWorkflow.prototype, "deviceTrust", null);
1810
+ __decorate([
1811
+ Step("prepare-password-rules"),
1812
+ __decorateParam(0, WorkflowParam("context")),
1813
+ __decorateMetadata("design:type", Function),
1814
+ __decorateMetadata("design:paramtypes", [Object]),
1815
+ __decorateMetadata("design:returntype", void 0)
1816
+ ], LoginWorkflow.prototype, "preparePasswordRules", null);
1817
+ __decorate([
1818
+ Step("create-password-form"),
1819
+ __decorateParam(0, WorkflowParam("input")),
1820
+ __decorateParam(1, WorkflowParam("context")),
1821
+ __decorateMetadata("design:type", Function),
1822
+ __decorateMetadata("design:paramtypes", [Object, Object]),
1823
+ __decorateMetadata("design:returntype", Promise)
1824
+ ], LoginWorkflow.prototype, "createPasswordForm", null);
1825
+ __decorate([
1826
+ Step("terms-accept"),
1827
+ __decorateParam(0, WorkflowParam("input")),
1828
+ __decorateParam(1, WorkflowParam("context")),
1829
+ __decorateMetadata("design:type", Function),
1830
+ __decorateMetadata("design:paramtypes", [Object, Object]),
1831
+ __decorateMetadata("design:returntype", Promise)
1832
+ ], LoginWorkflow.prototype, "termsAccept", null);
1833
+ __decorate([
1834
+ Step("profile-complete"),
1835
+ __decorateParam(0, WorkflowParam("input")),
1836
+ __decorateParam(1, WorkflowParam("context")),
1837
+ __decorateMetadata("design:type", Function),
1838
+ __decorateMetadata("design:paramtypes", [Object, Object]),
1839
+ __decorateMetadata("design:returntype", Promise)
1840
+ ], LoginWorkflow.prototype, "profileComplete", null);
1841
+ __decorate([
1842
+ Step("consent-marketing"),
1843
+ __decorateParam(0, WorkflowParam("input")),
1844
+ __decorateParam(1, WorkflowParam("context")),
1845
+ __decorateMetadata("design:type", Function),
1846
+ __decorateMetadata("design:paramtypes", [Object, Object]),
1847
+ __decorateMetadata("design:returntype", Promise)
1848
+ ], LoginWorkflow.prototype, "consentMarketing", null);
1849
+ __decorate([
1850
+ Step("tenant-select"),
1851
+ __decorateParam(0, WorkflowParam("input")),
1852
+ __decorateParam(1, WorkflowParam("context")),
1853
+ __decorateMetadata("design:type", Function),
1854
+ __decorateMetadata("design:paramtypes", [Object, Object]),
1855
+ __decorateMetadata("design:returntype", Promise)
1856
+ ], LoginWorkflow.prototype, "tenantSelect", null);
1857
+ __decorate([
1858
+ Step("persona-select"),
1859
+ __decorateParam(0, WorkflowParam("input")),
1860
+ __decorateParam(1, WorkflowParam("context")),
1861
+ __decorateMetadata("design:type", Function),
1862
+ __decorateMetadata("design:paramtypes", [Object, Object]),
1863
+ __decorateMetadata("design:returntype", Promise)
1864
+ ], LoginWorkflow.prototype, "personaSelect", null);
1865
+ __decorate([
1866
+ Step("concurrency-limit"),
1867
+ __decorateParam(0, WorkflowParam("input")),
1868
+ __decorateParam(1, WorkflowParam("context")),
1869
+ __decorateMetadata("design:type", Function),
1870
+ __decorateMetadata("design:paramtypes", [Object, Object]),
1871
+ __decorateMetadata("design:returntype", Promise)
1872
+ ], LoginWorkflow.prototype, "concurrencyLimit", null);
1873
+ __decorate([
1874
+ Step("risk-step-up"),
1875
+ __decorateParam(0, WorkflowParam("context")),
1876
+ __decorateMetadata("design:type", Function),
1877
+ __decorateMetadata("design:paramtypes", [Object]),
1878
+ __decorateMetadata("design:returntype", Promise)
1879
+ ], LoginWorkflow.prototype, "riskStepUp", null);
1880
+ __decorate([
1881
+ Step("issue"),
1882
+ __decorateParam(0, WorkflowParam("context")),
1883
+ __decorateMetadata("design:type", Function),
1884
+ __decorateMetadata("design:paramtypes", [Object]),
1885
+ __decorateMetadata("design:returntype", Promise)
1886
+ ], LoginWorkflow.prototype, "issue", null);
1887
+ __decorate([
1888
+ Step("audit-login"),
1889
+ __decorateParam(0, WorkflowParam("context")),
1890
+ __decorateMetadata("design:type", Function),
1891
+ __decorateMetadata("design:paramtypes", [Object]),
1892
+ __decorateMetadata("design:returntype", Promise)
1893
+ ], LoginWorkflow.prototype, "auditLogin", null);
1894
+ __decorate([
1895
+ Step("notify-new-device"),
1896
+ __decorateParam(0, WorkflowParam("context")),
1897
+ __decorateMetadata("design:type", Function),
1898
+ __decorateMetadata("design:paramtypes", [Object]),
1899
+ __decorateMetadata("design:returntype", Promise)
1900
+ ], LoginWorkflow.prototype, "notifyNewDevice", null);
1901
+ __decorate([
1902
+ Step("redirect"),
1903
+ __decorateParam(0, WorkflowParam("context")),
1904
+ __decorateMetadata("design:type", Function),
1905
+ __decorateMetadata("design:paramtypes", [Object]),
1906
+ __decorateMetadata("design:returntype", void 0)
1907
+ ], LoginWorkflow.prototype, "redirect", null);
1908
+ LoginWorkflow = __decorate([
1909
+ Public(),
1910
+ Injectable("FOR_EVENT"),
1911
+ Controller(),
1912
+ __decorateMetadata("design:paramtypes", [
1913
+ Object,
1914
+ typeof (_ref$2 = typeof UserService !== "undefined" && UserService) === "function" ? _ref$2 : Object,
1915
+ typeof (_ref2$2 = typeof AuthCredential !== "undefined" && AuthCredential) === "function" ? _ref2$2 : Object
1916
+ ])
1917
+ ], LoginWorkflow);
1918
+ //#endregion
1919
+ //#region src/workflows/recovery.workflow.options.ts
1920
+ /** Magic-link TTL default — also used as the persisted wf-state token TTL. */
1921
+ const DEFAULT_RECOVERY_TOKEN_TTL_MS = 3600 * 1e3;
1922
+ /**
1923
+ * Deep-merge defaults with the user-supplied nested pojo. Each group has its
1924
+ * own `{ ...defaults, ...input }` line — small enough that pulling in lodash
1925
+ * would be silly.
1926
+ */
1927
+ function mergeRecoveryOpts(opts = {}) {
1928
+ const inputDelivery = opts.delivery ?? {};
1929
+ const inputOtp = inputDelivery.otp ?? {};
1930
+ return {
1931
+ delivery: {
1932
+ mode: "magicLink",
1933
+ magicLinkTtlMs: DEFAULT_RECOVERY_TOKEN_TTL_MS,
1934
+ ...inputDelivery,
1935
+ otp: {
1936
+ transports: ["email"],
1937
+ codeLength: 6,
1938
+ ttlMs: 5 * 6e4,
1939
+ resendCooldownMs: 6e4,
1940
+ ...inputOtp
1941
+ }
1942
+ },
1943
+ preReset: {
1944
+ requireKnownFactor: false,
1945
+ ...opts.preReset
1946
+ },
1947
+ postReset: {
1948
+ revokeAllSessions: true,
1949
+ freshLoginRequired: false,
1950
+ loginUrl: "/login",
1951
+ ...opts.postReset
1952
+ },
1953
+ altActions: {
1954
+ backToLogin: true,
1955
+ ...opts.altActions
1956
+ },
1957
+ audit: {
1958
+ enabled: true,
1959
+ ...opts.audit
1960
+ },
1961
+ forms: {
1962
+ emailIdentifier: EmailIdentifierForm,
1963
+ pincode: PincodeForm,
1964
+ recoveryFactor: RecoveryFactorForm,
1965
+ recoveryModeSelect: RecoveryModeSelectForm,
1966
+ setPassword: SetPasswordForm,
1967
+ ...opts.forms
1968
+ }
1969
+ };
1970
+ }
1971
+ //#endregion
1972
+ //#region src/workflows/recovery.workflow.ts
1973
+ /**
1974
+ * RecoveryWorkflow — `wfid = 'auth.recovery'`.
1975
+ *
1976
+ * Full step catalog per `WF_RECOVERY.md`. Defaults give today's 3-step
1977
+ * magic-link flow (`request` → `sendMagicLink` → `setPassword`); consumers
1978
+ * turn on OTP delivery, pre-reset factor verification, fresh-login redirect
1979
+ * etc. via `RecoveryWorkflowOpts`.
1980
+ *
1981
+ * **Step routing model.** Mirrors `LoginWorkflow`: alt-action handlers run
1982
+ * BEFORE form validation (so `backToLogin` works without filling fields) and
1983
+ * return the `ALT_HANDLED` sentinel after short-circuiting via
1984
+ * `useWfFinished().set(...)`. The step body then returns `undefined` so the
1985
+ * schema advances cleanly, with terminal steps gated on `!ctx.aborted` so the
1986
+ * abort response set via `useWfFinished()` is not overwritten.
1987
+ *
1988
+ * **Consumer subclass pattern (Phase 3 reshape).** Consumers subclass
1989
+ * `RecoveryWorkflow` to override `protected` hook methods. The subclass MUST
1990
+ * re-apply `@Inherit() @Injectable('FOR_EVENT') @Controller()` and re-declare
1991
+ * the constructor signature (TS emits fresh design-paramtypes per class).
1992
+ *
1993
+ * **Side-effect deps as protected methods.** The optional sender/emitter DI
1994
+ * providers have been DROPPED from the constructor. The hooks live as
1995
+ * `protected` methods consumers override:
1996
+ *
1997
+ * - `deliver(payload)` — unified email + SMS dispatch (see `DeliverPayload`).
1998
+ * Default throws; override to wire your senders.
1999
+ * - `audit(event)` — fire audit events. Default: no-op.
2000
+ * - `emailToUserId(email)` — resolve recovery email to canonical username.
2001
+ * - `verifyRecoveryFactor(...)` — phone last-4 / TOTP / custom factors.
2002
+ *
2003
+ * Rate-limiting is intentionally NOT part of this workflow — consumers who
2004
+ * want a cap wire it themselves at the HTTP / trigger layer.
2005
+ */
2006
+ var _ref$1, _ref2$1;
2007
+ /**
2008
+ * Sentinel returned by alt-action handlers that have already short-circuited
2009
+ * via `useWfFinished().set(...)`. The step body returns `undefined` after
2010
+ * seeing this so the schema advances without running form validation on the
2011
+ * alt-action payload (which lacks the form's required fields).
2012
+ */
2013
+ const ALT_HANDLED$1 = Symbol("ALT_HANDLED");
2014
+ /**
2015
+ * Construction-time invariants for DATA validity only. Sender/emitter absence
2016
+ * is no longer checked — those default to fail-loud (`deliver()`) or safe
2017
+ * (`audit()` no-op) protected methods that consumers override.
2018
+ */
2019
+ function validateOpts(opts) {
2020
+ if (opts.delivery.mode !== "magicLink" && opts.delivery.otp.transports.length === 0) throw new Error("RecoveryWorkflow: delivery.otp.transports cannot be empty when delivery.mode includes OTP");
2021
+ }
2022
+ let RecoveryWorkflow = class RecoveryWorkflow {
2023
+ opts;
2024
+ users;
2025
+ auth;
2026
+ constructor(opts, users, auth) {
2027
+ this.opts = mergeRecoveryOpts(opts);
2028
+ this.users = users;
2029
+ this.auth = auth;
2030
+ validateOpts(this.opts);
2031
+ }
2032
+ /**
2033
+ * Dispatch an email or SMS event. Default throws — consumers MUST override
2034
+ * if `delivery.mode` ever drives email/SMS (i.e. for any non-`magicLink`
2035
+ * mode AND for `magicLink` mode the `outletEmail` outlet still runs the
2036
+ * email through `createAuthEmailOutlet`'s `EmailSender` — see the trigger
2037
+ * controller wiring; this method covers OTP code dispatch).
2038
+ */
2039
+ async deliver(_payload) {
2040
+ throw new Error("RecoveryWorkflow.deliver() not configured — override to wire your email/sms sender");
2041
+ }
2042
+ /**
2043
+ * Emit an audit event. Default: no-op. Consumers override to fan out to
2044
+ * their audit sink.
2045
+ */
2046
+ async audit(_event) {}
2047
+ flow() {}
2048
+ init(ctx) {
2049
+ ctx.opts = this.snapshotOpts(this.opts);
2050
+ if (this.opts.delivery.mode !== "choice") ctx.resolvedMode = this.opts.delivery.mode;
2051
+ }
2052
+ /**
2053
+ * Returns the JSON-safe projection of `opts` stashed onto `ctx` for schema
2054
+ * conditions to read. Default: drop the `forms` group (atscript form classes
2055
+ * are not plain JSON) so `AsWfStore`'s plain-JSON persistence doesn't choke.
2056
+ * Step bodies still consult the form classes via `this.opts.forms.*`.
2057
+ *
2058
+ * Consumers who extend the opts type with non-JSON values can override this
2059
+ * to strip them so `AsWfStore`'s plain-JSON persistence doesn't choke.
2060
+ */
2061
+ snapshotOpts(opts) {
2062
+ const { forms: _forms, ...rest } = opts;
2063
+ return rest;
2064
+ }
2065
+ async request(input, ctx) {
2066
+ if (!input) {
2067
+ const prefilled = readUsernameQueryParam();
2068
+ const formCtx = { ...ctx };
2069
+ if (prefilled) formCtx.defaults = { email: prefilled };
2070
+ return httpInputRequired(this.opts.forms.emailIdentifier, formCtx);
2071
+ }
2072
+ if (input.action === "backToLogin" && this.opts.altActions.backToLogin) {
2073
+ this.abortToLogin(ctx);
2074
+ return;
2075
+ }
2076
+ const errors = validateFormInput(this.opts.forms.emailIdentifier, input);
2077
+ if (errors) return httpInputRequired(this.opts.forms.emailIdentifier, ctx, errors);
2078
+ const email = input.email;
2079
+ ctx.email = email;
2080
+ let username;
2081
+ try {
2082
+ const userId = await this.emailToUserId(email);
2083
+ if (userId) username = (await this.users.getUser(userId)).username;
2084
+ } catch (err) {
2085
+ if (!(err instanceof UserAuthError) || err.type !== "NOT_FOUND") throw err;
2086
+ }
2087
+ await this.emitRequested(ctx, username);
2088
+ if (!username) {
2089
+ this.finishGeneric();
2090
+ return;
2091
+ }
2092
+ ctx.username = username;
2093
+ if (ctx.resolvedMode === "otp" && !ctx.otpTransport) ctx.otpTransport = this.opts.delivery.otp.transports[0];
2094
+ }
2095
+ /**
2096
+ * Resolves the recovery-step `email` input to the `username` (user-id) that
2097
+ * `UserService.getUser` expects. Default: returns the email unchanged (treats
2098
+ * email as username). Apps whose user model separates `username` from
2099
+ * `email` MUST override this; return `null` when no user matches.
2100
+ */
2101
+ async emailToUserId(email) {
2102
+ return email;
2103
+ }
2104
+ selectMode(input, ctx) {
2105
+ if (!input) return httpInputRequired(this.opts.forms.recoveryModeSelect, ctx);
2106
+ if (input.action === "backToLogin" && this.opts.altActions.backToLogin) {
2107
+ this.abortToLogin(ctx);
2108
+ return;
2109
+ }
2110
+ const errors = validateFormInput(this.opts.forms.recoveryModeSelect, input);
2111
+ if (errors) return httpInputRequired(this.opts.forms.recoveryModeSelect, ctx, errors);
2112
+ const mode = input.mode;
2113
+ ctx.selectedMode = mode;
2114
+ ctx.resolvedMode = mode;
2115
+ if (mode === "otp" && !ctx.otpTransport) ctx.otpTransport = this.opts.delivery.otp.transports[0];
2116
+ }
2117
+ sendMagicLink(ctx) {
2118
+ if (ctx.linkSent) return void 0;
2119
+ ctx.linkSent = true;
2120
+ return {
2121
+ ...outletEmail(ctx.email, "recovery.magicLink", {
2122
+ username: ctx.username,
2123
+ expiresAtMs: this.opts.delivery.magicLinkTtlMs
2124
+ }),
2125
+ expires: Date.now() + this.opts.delivery.magicLinkTtlMs
2126
+ };
2127
+ }
2128
+ async sendOtp(ctx) {
2129
+ requireUsername(ctx);
2130
+ const transport = ctx.otpTransport ?? this.opts.delivery.otp.transports[0] ?? "email";
2131
+ ctx.otpTransport = transport;
2132
+ ctx.otpCodeLength = this.opts.delivery.otp.codeLength;
2133
+ const code = mintPin$1(ctx, this.opts.delivery.otp.codeLength, this.opts.delivery.otp.ttlMs);
2134
+ ctx.pinResendAllowedAt = Date.now() + this.opts.delivery.otp.resendCooldownMs;
2135
+ if (transport === "email") await this.deliver({
2136
+ channel: "email",
2137
+ kind: "recovery.pincode",
2138
+ recipient: ctx.email,
2139
+ code,
2140
+ expiresAt: ctx.pinExpire,
2141
+ userId: ctx.username
2142
+ });
2143
+ else {
2144
+ const phone = await this.resolveUserPhone(ctx.username);
2145
+ await this.deliver({
2146
+ channel: "sms",
2147
+ kind: "recovery.pincode",
2148
+ recipient: phone ?? ctx.email,
2149
+ code,
2150
+ ttlMs: this.opts.delivery.otp.ttlMs,
2151
+ userId: ctx.username
2152
+ });
2153
+ }
2154
+ }
2155
+ async checkOtp(input, ctx) {
2156
+ if (!input) return httpInputRequired(this.opts.forms.pincode, ctx);
2157
+ if (input.action === "backToLogin" && this.opts.altActions.backToLogin) {
2158
+ this.abortToLogin(ctx);
2159
+ return;
2160
+ }
2161
+ if (input.action === "resend") {
2162
+ if (ctx.pinResendAllowedAt && Date.now() < ctx.pinResendAllowedAt) {
2163
+ const waitSec = Math.ceil((ctx.pinResendAllowedAt - Date.now()) / 1e3);
2164
+ return httpInputRequired(this.opts.forms.pincode, ctx, { __form: `Please wait ${waitSec}s` });
2165
+ }
2166
+ delete ctx.pin;
2167
+ delete ctx.pinExpire;
2168
+ return;
2169
+ }
2170
+ if (input.action === "useDifferentTransport") {
2171
+ const transports = this.opts.delivery.otp.transports;
2172
+ if (transports.length < 2) return httpInputRequired(this.opts.forms.pincode, ctx, { __form: "Only one transport configured" });
2173
+ const current = ctx.otpTransport ?? transports[0];
2174
+ ctx.otpTransport = transports.find((t) => t !== current) ?? transports[0];
2175
+ delete ctx.pin;
2176
+ delete ctx.pinExpire;
2177
+ return;
2178
+ }
2179
+ const errors = validateFormInput(this.opts.forms.pincode, input);
2180
+ if (errors) return httpInputRequired(this.opts.forms.pincode, ctx, errors);
2181
+ const pinErr = verifyPin$1(ctx, input.code);
2182
+ if (pinErr) return httpInputRequired(this.opts.forms.pincode, ctx, pinErr);
2183
+ ctx.pinVerified = true;
2184
+ delete ctx.pin;
2185
+ delete ctx.pinExpire;
2186
+ }
2187
+ async verifyFactor(input, ctx) {
2188
+ if (!input) return httpInputRequired(this.opts.forms.recoveryFactor, ctx);
2189
+ if (input.action === "backToLogin" && this.opts.altActions.backToLogin) {
2190
+ this.abortToLogin(ctx);
2191
+ return;
2192
+ }
2193
+ const errors = validateFormInput(this.opts.forms.recoveryFactor, input);
2194
+ if (errors) return httpInputRequired(this.opts.forms.recoveryFactor, ctx, errors);
2195
+ requireUsername(ctx);
2196
+ const factor = input.factor;
2197
+ const value = input.value;
2198
+ if (!await this.verifyRecoveryFactor({
2199
+ factor,
2200
+ value,
2201
+ ctx
2202
+ })) return httpInputRequired(this.opts.forms.recoveryFactor, ctx, { value: "Invalid factor" });
2203
+ ctx.factorVerified = true;
2204
+ }
2205
+ /**
2206
+ * Verifies a recovery factor against the user's enrolled MFA methods.
2207
+ * Default: supports `'phone'` (phone last-4 match) and `'totp'` (current
2208
+ * TOTP code). Returns `true` when the factor matches.
2209
+ *
2210
+ * Consumers extend by overriding to support additional factors (e.g.
2211
+ * security questions); call `super.verifyRecoveryFactor(...)` to keep
2212
+ * the built-in checks.
2213
+ */
2214
+ async verifyRecoveryFactor(input) {
2215
+ const { factor, value, ctx } = input;
2216
+ if (!ctx.username) return false;
2217
+ const user = await this.users.getUser(ctx.username);
2218
+ if (factor === "phone") {
2219
+ const phoneMethod = user.mfa.methods.find((m) => m.name === "sms" && m.confirmed);
2220
+ if (!phoneMethod) return false;
2221
+ return value === phoneMethod.value.slice(-4);
2222
+ }
2223
+ if (factor === "totp") {
2224
+ const totpMethod = user.mfa.methods.find((m) => m.name === "totp" && m.confirmed);
2225
+ if (!totpMethod) return false;
2226
+ return verifyTotpCode(totpMethod.value, value);
2227
+ }
2228
+ return false;
2229
+ }
2230
+ async setPassword(input, ctx) {
2231
+ if (!input) {
2232
+ const formCtx = { ...ctx };
2233
+ formCtx.passwordPolicies = this.users.getTransferablePolicies();
2234
+ return httpInputRequired(this.opts.forms.setPassword, formCtx);
2235
+ }
2236
+ if (input.action === "backToLogin" && this.opts.altActions.backToLogin) {
2237
+ this.abortToLogin(ctx);
2238
+ return;
2239
+ }
2240
+ const errors = validateFormInput(this.opts.forms.setPassword, input);
2241
+ if (errors) return httpInputRequired(this.opts.forms.setPassword, ctx, errors);
2242
+ if (input.newPassword !== input.confirmPassword) return httpInputRequired(this.opts.forms.setPassword, ctx, { confirmPassword: "Passwords do not match" });
2243
+ if (!ctx.username) return httpInputRequired(this.opts.forms.setPassword, ctx, { __form: "Recovery session expired" });
2244
+ try {
2245
+ await this.users.setPassword(ctx.username, input.newPassword);
2246
+ } catch (err) {
2247
+ translatePasswordSetError(err);
2248
+ }
2249
+ ctx.passwordChanged = true;
2250
+ }
2251
+ async revokeSessions(ctx) {
2252
+ if (!ctx.username) return void 0;
2253
+ await this.auth.revokeAllForUser(ctx.username);
2254
+ ctx.sessionsRevoked = true;
2255
+ }
2256
+ async auditStep(ctx) {
2257
+ await this.audit({
2258
+ kind: "recovery.completed",
2259
+ userId: ctx.username,
2260
+ workflow: "auth.recovery",
2261
+ deliveryMode: ctx.resolvedMode ?? this.opts.delivery.mode,
2262
+ ip: resolveClientIp(),
2263
+ ...ctx.sessionsRevoked && { sessionsRevoked: true }
2264
+ });
2265
+ }
2266
+ freshLoginFinish(_ctx) {
2267
+ finishWfWithRedirect(this.opts.postReset.loginUrl, {
2268
+ autoMs: 5e3,
2269
+ skipLabel: "Go now",
2270
+ message: {
2271
+ level: "success",
2272
+ text: "Password updated. Redirecting to sign-in…"
2273
+ },
2274
+ reason: "reset-success"
2275
+ });
2276
+ }
2277
+ async autoLoginFinish(ctx) {
2278
+ requireUsername(ctx);
2279
+ const issue = await this.auth.issue(ctx.username);
2280
+ ctx.tokensIssued = true;
2281
+ const auth = useAuth();
2282
+ const envelope = {
2283
+ finished: true,
2284
+ data: auth.buildLoginResponse(ctx.username, issue)
2285
+ };
2286
+ useWfFinished().set({
2287
+ type: "data",
2288
+ value: envelope,
2289
+ cookies: auth.buildFinishedCookies(issue)
2290
+ });
2291
+ }
2292
+ /**
2293
+ * Send the generic "if an account exists, you'll receive instructions"
2294
+ * finished response. Used for unknown emails so a known/unknown lookup is
2295
+ * indistinguishable to the client (anti-enumeration).
2296
+ */
2297
+ finishGeneric() {
2298
+ finishWfWithData({ sent: true }, {
2299
+ level: "info",
2300
+ text: "If an account exists, you will receive instructions."
2301
+ });
2302
+ }
2303
+ abortToLogin(ctx) {
2304
+ finishWfWithRedirect(this.opts.postReset.loginUrl, { reason: "user-cancelled" });
2305
+ ctx.aborted = true;
2306
+ return ALT_HANDLED$1;
2307
+ }
2308
+ async emitRequested(ctx, username) {
2309
+ if (!this.opts.audit.enabled) return;
2310
+ await this.audit({
2311
+ kind: "recovery.requested",
2312
+ workflow: "auth.recovery",
2313
+ userId: username,
2314
+ email: ctx.email,
2315
+ ip: resolveClientIp()
2316
+ });
2317
+ }
2318
+ async resolveUserPhone(username) {
2319
+ try {
2320
+ return (await this.users.getUser(username)).mfa.methods.find((m) => m.name === "sms" && m.confirmed)?.value;
2321
+ } catch {
2322
+ return;
2323
+ }
2324
+ }
2325
+ };
2326
+ __decorate([
2327
+ Workflow("auth.recovery"),
2328
+ WorkflowSchema([
2329
+ { id: "recoveryInit" },
2330
+ { id: "recoveryRequest" },
2331
+ {
2332
+ id: "recoverySelectMode",
2333
+ condition: (ctx) => !!(ctx.username && ctx.opts.delivery.mode === "choice" && !ctx.selectedMode && !ctx.aborted)
2334
+ },
2335
+ {
2336
+ id: "recoverySendMagicLink",
2337
+ condition: (ctx) => !!(ctx.username && ctx.resolvedMode === "magicLink" && !ctx.aborted)
2338
+ },
2339
+ {
2340
+ while: (ctx) => !!(ctx.username && ctx.resolvedMode === "otp" && !ctx.pinVerified && !ctx.aborted),
2341
+ steps: [{
2342
+ id: "recoverySendOtp",
2343
+ condition: (ctx) => !ctx.pin
2344
+ }, { id: "recoveryCheckOtp" }]
2345
+ },
2346
+ {
2347
+ id: "recoveryVerifyFactor",
2348
+ condition: (ctx) => !!(ctx.username && ctx.opts.preReset.requireKnownFactor && !ctx.factorVerified && (ctx.linkSent || ctx.pinVerified) && !ctx.aborted)
2349
+ },
2350
+ {
2351
+ id: "recoverySetPassword",
2352
+ condition: (ctx) => !!(ctx.username && (ctx.linkSent || ctx.pinVerified) && (!ctx.opts.preReset.requireKnownFactor || ctx.factorVerified) && !ctx.aborted)
2353
+ },
2354
+ {
2355
+ id: "recoveryRevokeSessions",
2356
+ condition: (ctx) => !!(ctx.opts.postReset.revokeAllSessions && ctx.passwordChanged && !ctx.aborted)
2357
+ },
2358
+ {
2359
+ id: "recoveryAudit",
2360
+ condition: (ctx) => !!(ctx.opts.audit.enabled && ctx.passwordChanged && !ctx.aborted)
2361
+ },
2362
+ {
2363
+ id: "recoveryFreshLoginFinish",
2364
+ condition: (ctx) => !!(ctx.opts.postReset.freshLoginRequired && ctx.passwordChanged && !ctx.aborted)
2365
+ },
2366
+ {
2367
+ id: "recoveryAutoLoginFinish",
2368
+ condition: (ctx) => !!(!ctx.opts.postReset.freshLoginRequired && ctx.passwordChanged && !ctx.tokensIssued && !ctx.aborted)
2369
+ }
2370
+ ]),
2371
+ __decorateMetadata("design:type", Function),
2372
+ __decorateMetadata("design:paramtypes", []),
2373
+ __decorateMetadata("design:returntype", void 0)
2374
+ ], RecoveryWorkflow.prototype, "flow", null);
2375
+ __decorate([
2376
+ Step("recoveryInit"),
2377
+ __decorateParam(0, WorkflowParam("context")),
2378
+ __decorateMetadata("design:type", Function),
2379
+ __decorateMetadata("design:paramtypes", [Object]),
2380
+ __decorateMetadata("design:returntype", void 0)
2381
+ ], RecoveryWorkflow.prototype, "init", null);
2382
+ __decorate([
2383
+ Step("recoveryRequest"),
2384
+ __decorateParam(0, WorkflowParam("input")),
2385
+ __decorateParam(1, WorkflowParam("context")),
2386
+ __decorateMetadata("design:type", Function),
2387
+ __decorateMetadata("design:paramtypes", [Object, Object]),
2388
+ __decorateMetadata("design:returntype", Promise)
2389
+ ], RecoveryWorkflow.prototype, "request", null);
2390
+ __decorate([
2391
+ Step("recoverySelectMode"),
2392
+ __decorateParam(0, WorkflowParam("input")),
2393
+ __decorateParam(1, WorkflowParam("context")),
2394
+ __decorateMetadata("design:type", Function),
2395
+ __decorateMetadata("design:paramtypes", [Object, Object]),
2396
+ __decorateMetadata("design:returntype", Object)
2397
+ ], RecoveryWorkflow.prototype, "selectMode", null);
2398
+ __decorate([
2399
+ Step("recoverySendMagicLink"),
2400
+ __decorateParam(0, WorkflowParam("context")),
2401
+ __decorateMetadata("design:type", Function),
2402
+ __decorateMetadata("design:paramtypes", [Object]),
2403
+ __decorateMetadata("design:returntype", Object)
2404
+ ], RecoveryWorkflow.prototype, "sendMagicLink", null);
2405
+ __decorate([
2406
+ Step("recoverySendOtp"),
2407
+ __decorateParam(0, WorkflowParam("context")),
2408
+ __decorateMetadata("design:type", Function),
2409
+ __decorateMetadata("design:paramtypes", [Object]),
2410
+ __decorateMetadata("design:returntype", Promise)
2411
+ ], RecoveryWorkflow.prototype, "sendOtp", null);
2412
+ __decorate([
2413
+ Step("recoveryCheckOtp"),
2414
+ __decorateParam(0, WorkflowParam("input")),
2415
+ __decorateParam(1, WorkflowParam("context")),
2416
+ __decorateMetadata("design:type", Function),
2417
+ __decorateMetadata("design:paramtypes", [Object, Object]),
2418
+ __decorateMetadata("design:returntype", Promise)
2419
+ ], RecoveryWorkflow.prototype, "checkOtp", null);
2420
+ __decorate([
2421
+ Step("recoveryVerifyFactor"),
2422
+ __decorateParam(0, WorkflowParam("input")),
2423
+ __decorateParam(1, WorkflowParam("context")),
2424
+ __decorateMetadata("design:type", Function),
2425
+ __decorateMetadata("design:paramtypes", [Object, Object]),
2426
+ __decorateMetadata("design:returntype", Promise)
2427
+ ], RecoveryWorkflow.prototype, "verifyFactor", null);
2428
+ __decorate([
2429
+ Step("recoverySetPassword"),
2430
+ __decorateParam(0, WorkflowParam("input")),
2431
+ __decorateParam(1, WorkflowParam("context")),
2432
+ __decorateMetadata("design:type", Function),
2433
+ __decorateMetadata("design:paramtypes", [Object, Object]),
2434
+ __decorateMetadata("design:returntype", Promise)
2435
+ ], RecoveryWorkflow.prototype, "setPassword", null);
2436
+ __decorate([
2437
+ Step("recoveryRevokeSessions"),
2438
+ __decorateParam(0, WorkflowParam("context")),
2439
+ __decorateMetadata("design:type", Function),
2440
+ __decorateMetadata("design:paramtypes", [Object]),
2441
+ __decorateMetadata("design:returntype", Promise)
2442
+ ], RecoveryWorkflow.prototype, "revokeSessions", null);
2443
+ __decorate([
2444
+ Step("recoveryAudit"),
2445
+ __decorateParam(0, WorkflowParam("context")),
2446
+ __decorateMetadata("design:type", Function),
2447
+ __decorateMetadata("design:paramtypes", [Object]),
2448
+ __decorateMetadata("design:returntype", Promise)
2449
+ ], RecoveryWorkflow.prototype, "auditStep", null);
2450
+ __decorate([
2451
+ Step("recoveryFreshLoginFinish"),
2452
+ __decorateParam(0, WorkflowParam("context")),
2453
+ __decorateMetadata("design:type", Function),
2454
+ __decorateMetadata("design:paramtypes", [Object]),
2455
+ __decorateMetadata("design:returntype", void 0)
2456
+ ], RecoveryWorkflow.prototype, "freshLoginFinish", null);
2457
+ __decorate([
2458
+ Step("recoveryAutoLoginFinish"),
2459
+ __decorateParam(0, WorkflowParam("context")),
2460
+ __decorateMetadata("design:type", Function),
2461
+ __decorateMetadata("design:paramtypes", [Object]),
2462
+ __decorateMetadata("design:returntype", Promise)
2463
+ ], RecoveryWorkflow.prototype, "autoLoginFinish", null);
2464
+ RecoveryWorkflow = __decorate([
2465
+ Public(),
2466
+ Injectable("FOR_EVENT"),
2467
+ Controller(),
2468
+ __decorateMetadata("design:paramtypes", [
2469
+ Object,
2470
+ typeof (_ref$1 = typeof UserService !== "undefined" && UserService) === "function" ? _ref$1 : Object,
2471
+ typeof (_ref2$1 = typeof AuthCredential !== "undefined" && AuthCredential) === "function" ? _ref2$1 : Object
2472
+ ])
2473
+ ], RecoveryWorkflow);
2474
+ /**
2475
+ * Reads the `?username=` query parameter when the workflow is triggered (e.g.
2476
+ * via the login workflow's `forgotPassword` alt-action). Returns undefined
2477
+ * outside of an HTTP event context (e.g. unit tests that hand-roll the wf
2478
+ * runtime). Used purely for form pre-fill.
2479
+ */
2480
+ function readUsernameQueryParam() {
2481
+ try {
2482
+ const { params } = useUrlParams(current());
2483
+ return params().get("username") ?? void 0;
2484
+ } catch {
2485
+ return;
2486
+ }
2487
+ }
2488
+ //#endregion
2489
+ //#region src/workflows/invite.workflow.options.ts
2490
+ const DEFAULT_INVITE_TOKEN_TTL_MS = 10080 * 60 * 1e3;
2491
+ /**
2492
+ * Deep-merge defaults with the user-supplied nested pojo. Each group has its
2493
+ * own `{ ...defaults, ...input }` line — small enough that pulling in lodash
2494
+ * would be silly.
2495
+ */
2496
+ function mergeInviteOpts(opts = {}) {
2497
+ return {
2498
+ adminForm: {
2499
+ collectRoles: true,
2500
+ ...opts.adminForm
2501
+ },
2502
+ send: {
2503
+ mode: "email",
2504
+ tokenTtlMs: DEFAULT_INVITE_TOKEN_TTL_MS,
2505
+ ...opts.send
2506
+ },
2507
+ accept: {
2508
+ alreadyAcceptedRedirectUrl: "/login",
2509
+ freshLoginRequired: false,
2510
+ loginUrl: "/login",
2511
+ showConfirmation: true,
2512
+ confirmationMessage: "Your account has been created.",
2513
+ ...opts.accept
2514
+ },
2515
+ cancellation: {
2516
+ allowed: true,
2517
+ ...opts.cancellation
2518
+ },
2519
+ audit: {
2520
+ enabled: true,
2521
+ ...opts.audit
2522
+ },
2523
+ forms: {
2524
+ invite: InviteForm,
2525
+ inviteEmail: InviteEmailForm,
2526
+ inviteSendMode: InviteSendModeForm,
2527
+ setPassword: SetPasswordForm,
2528
+ ...opts.forms
2529
+ }
2530
+ };
2531
+ }
2532
+ //#endregion
2533
+ //#region src/workflows/invite.workflow.ts
2534
+ /**
2535
+ * InviteWorkflow — registers three workflow ids:
2536
+ *
2537
+ * - `auth.invite` — admin invites a new user → user accepts (full flow)
2538
+ * - `auth.reInvite` — admin resends to an existing `pendingInvitation` user
2539
+ * - `auth.cancelInvite` — admin hard-deletes a pending invite (gated by
2540
+ * `opts.cancellation.allowed`)
2541
+ *
2542
+ * Full step catalog per `WF_INVITE.md`. Step IDs are workflow-scoped (all
2543
+ * prefixed `invite*`) because `@moostjs/event-wf` registers `@Step('id')`
2544
+ * globally — identical IDs across login/recovery/invite would silently
2545
+ * collide and overwrite handlers. The accept tail (`inviteCheckPending…`
2546
+ * through `inviteAutoLoginFinish`) is shared between `auth.invite` and
2547
+ * `auth.reInvite` schemas by re-referencing the same step IDs.
2548
+ *
2549
+ * **Step routing model.** Same shape as `RecoveryWorkflow`: alt-action
2550
+ * handlers run BEFORE form validation (so `cancel` works without filling
2551
+ * fields) and return the `ALT_HANDLED` sentinel after short-circuiting via
2552
+ * `useWfFinished().set(...)`. Terminal steps gate on `!ctx.aborted` so the
2553
+ * abort response stays.
2554
+ *
2555
+ * **Consumer subclass pattern (Phase 4 reshape).** Consumers subclass
2556
+ * `InviteWorkflow` to override `protected` hook methods. The subclass MUST
2557
+ * re-apply `@Inherit() @Injectable('FOR_EVENT') @Controller()` and re-declare
2558
+ * the constructor signature (TS emits fresh design-paramtypes per class).
2559
+ *
2560
+ * **Side-effect deps as protected methods.** Sender/store/emitter DI
2561
+ * providers have been DROPPED from the constructor. Hooks live as `protected`
2562
+ * methods consumers override:
2563
+ *
2564
+ * - `deliver(payload)` — unified email + SMS dispatch (see `DeliverPayload`).
2565
+ * Default throws; override to wire your senders. The default invite send
2566
+ * uses `outletEmail` (handled by `createAuthEmailOutlet` at the trigger
2567
+ * route) so `deliver()` is only invoked if a consumer's accept-tail steps
2568
+ * drive a manual send. Kept exposed for parity with login/recovery and to
2569
+ * give consumer subclasses a single override seam for future SMS-invite
2570
+ * work.
2571
+ * - `audit(event)` — fire audit events. Default: no-op.
2572
+ *
2573
+ * **Replaceable behaviour as protected methods.**
2574
+ *
2575
+ * - `prepareUser(input)` — extras merged into the freshly-created user row.
2576
+ * - `getAvailableRoles()` — multi-select source for the admin invite form.
2577
+ * - `inferRoles(input)` — derive roles server-side (e.g. AD lookup).
2578
+ * - `applyProfile({username, profile})` — persist the accept-time profile.
2579
+ * - `duplicateCheck({email, existingUser})` — override the structural
2580
+ * duplicate rule (multi-tenant escape hatch).
2581
+ * - `getProfileForm()` — return the consumer's `.as` profile form schema;
2582
+ * `undefined` skips the profile-collection step.
2583
+ *
2584
+ * Rate-limiting is intentionally NOT part of this workflow — consumers who
2585
+ * want a cap wire it themselves at the HTTP / trigger layer.
2586
+ */
2587
+ var _ref, _ref2;
2588
+ /**
2589
+ * Sentinel returned by alt-action handlers that have already short-circuited
2590
+ * via `useWfFinished().set(...)`. The step body returns `undefined` after
2591
+ * seeing this so the schema advances without running form validation on the
2592
+ * alt-action payload (which lacks the form's required fields).
2593
+ */
2594
+ const ALT_HANDLED = Symbol("ALT_HANDLED");
2595
+ /** Audit `kind` → `wfid` reverse map. Default falls through to `auth.invite`. */
2596
+ const AUDIT_WORKFLOW_BY_KIND = {
2597
+ "invite.cancelled": "auth.cancelInvite",
2598
+ "invite.resent": "auth.reInvite"
2599
+ };
2600
+ /** Trim + de-duplicate role identifiers submitted via the admin invite form. */
2601
+ function parseInviteRoles(input) {
2602
+ if (!Array.isArray(input)) return [];
2603
+ const seen = /* @__PURE__ */ new Set();
2604
+ for (const v of input) {
2605
+ const trimmed = typeof v === "string" ? v.trim() : "";
2606
+ if (trimmed) seen.add(trimmed);
2607
+ }
2608
+ return [...seen];
2609
+ }
2610
+ let InviteWorkflow = class InviteWorkflow {
2611
+ opts;
2612
+ users;
2613
+ auth;
2614
+ constructor(opts, users, auth) {
2615
+ this.opts = mergeInviteOpts(opts);
2616
+ this.users = users;
2617
+ this.auth = auth;
2618
+ this.opts;
2619
+ }
2620
+ /**
2621
+ * Dispatch an email or SMS event. Default throws — the default invite send
2622
+ * uses `outletEmail` (handled by `createAuthEmailOutlet`) so this method is
2623
+ * only invoked when a consumer's accept-tail steps drive a manual send.
2624
+ * Override to wire your senders.
2625
+ */
2626
+ async deliver(_payload) {
2627
+ throw new Error("InviteWorkflow.deliver() not configured — override to wire your email/sms sender");
2628
+ }
2629
+ /**
2630
+ * Emit an audit event. Default: no-op. Consumers override to fan out to
2631
+ * their audit sink.
2632
+ */
2633
+ async audit(_event) {}
2634
+ /**
2635
+ * Build the extras dictionary merged into the freshly-created user row in
2636
+ * `invitePreCreateUser`. Default: `{}`. Override to populate e.g. a
2637
+ * required `tenantId`. This is the ONLY seam through which the admin form's
2638
+ * `firstName` / `lastName` reach persistence — map them into your schema's
2639
+ * own columns (e.g. `displayName`) and return them here.
2640
+ */
2641
+ async prepareUser(_input) {
2642
+ return {};
2643
+ }
2644
+ /**
2645
+ * Return the list of selectable role identifiers for the admin invite form.
2646
+ * When defined AND `adminForm.collectRoles` is true → form ships
2647
+ * `ctx.availableRoles` so the UI renders a multi-select AND the
2648
+ * `inviteAdminInviteForm` step rejects admin-submitted roles outside the
2649
+ * list. When `undefined` (default) → no whitelist is enforced and any role
2650
+ * value the admin form supplies is accepted.
2651
+ */
2652
+ async getAvailableRoles() {}
2653
+ /**
2654
+ * Derive roles server-side from the admin-form payload (e.g. email domain
2655
+ * → tenant role, AD lookup). Result is set-unioned with admin-supplied
2656
+ * roles when `adminForm.collectRoles` is true. Default: `[]` (no inference).
2657
+ */
2658
+ async inferRoles(_input) {
2659
+ return [];
2660
+ }
2661
+ /**
2662
+ * Persist the accept-time profile payload. Default: deep-merge into the
2663
+ * user record via `UserService.update(username, profile)`. Override to
2664
+ * route into a separate profile table / external CRM.
2665
+ */
2666
+ async applyProfile(input) {
2667
+ await this.users.update(input.username, input.profile);
2668
+ }
2669
+ /**
2670
+ * Override the structural duplicate rule for `inviteAdminInviteForm`.
2671
+ * Default: any existing row → `'reject'`; nothing → `'allow'`. Multi-tenant
2672
+ * apps that allow re-inviting the same email into a different tenant
2673
+ * override to return `'allow'` for those cases.
2674
+ */
2675
+ async duplicateCheck(input) {
2676
+ return input.existingUser ? "reject" : "allow";
2677
+ }
2678
+ /**
2679
+ * Return the consumer-supplied `.as` form schema rendered in the
2680
+ * `inviteCollectProfile` step. `undefined` (default) skips the step
2681
+ * entirely (just password collection).
2682
+ */
2683
+ getProfileForm() {}
2684
+ /**
2685
+ * Returns the JSON-safe projection of `opts` stashed onto `ctx` for schema
2686
+ * conditions to read. Default: drop the `forms` group (atscript form classes
2687
+ * are not plain JSON) so `AsWfStore`'s plain-JSON persistence doesn't choke.
2688
+ * Step bodies still consult the form classes via `this.opts.forms.*`.
2689
+ *
2690
+ * Consumers who extend the opts type with non-JSON values can override this
2691
+ * to strip them so `AsWfStore`'s plain-JSON persistence doesn't choke.
2692
+ */
2693
+ snapshotOpts(opts) {
2694
+ const { forms: _forms, ...rest } = opts;
2695
+ return rest;
2696
+ }
2697
+ inviteFlow() {}
2698
+ reInviteFlow() {}
2699
+ cancelInviteFlow() {}
2700
+ init(ctx) {
2701
+ ctx.opts = this.snapshotOpts(this.opts);
2702
+ ctx.acceptProfileFormPresent = this.getProfileForm() !== void 0;
2703
+ if (this.opts.send.mode !== "choice") ctx.resolvedSendMode = this.opts.send.mode;
2704
+ }
2705
+ async prepareAvailableRoles(ctx) {
2706
+ const roles = await this.getAvailableRoles();
2707
+ if (roles) ctx.availableRoles = roles;
2708
+ }
2709
+ selectSendMode(input, ctx) {
2710
+ if (!input) return httpInputRequired(this.opts.forms.inviteSendMode, ctx);
2711
+ if (input.action === "cancel") return this.abort(ctx, "cancel");
2712
+ const errors = validateFormInput(this.opts.forms.inviteSendMode, input);
2713
+ if (errors) return httpInputRequired(this.opts.forms.inviteSendMode, ctx, errors);
2714
+ const mode = input.mode;
2715
+ ctx.selectedSendMode = mode;
2716
+ ctx.resolvedSendMode = mode;
2717
+ }
2718
+ async adminInviteForm(input, ctx) {
2719
+ if (!input) return httpInputRequired(this.opts.forms.invite, ctx);
2720
+ if (input.action === "cancel") return this.abort(ctx, "cancel");
2721
+ const errors = validateFormInput(this.opts.forms.invite, input);
2722
+ if (errors) return httpInputRequired(this.opts.forms.invite, ctx, errors);
2723
+ const email = input.email;
2724
+ const parsed = parseInviteRoles(input.roles);
2725
+ if (Array.isArray(ctx.availableRoles)) {
2726
+ const allowed = new Set(ctx.availableRoles);
2727
+ if (parsed.find((r) => !allowed.has(r)) !== void 0) return httpInputRequired(this.opts.forms.invite, ctx, { roles: "Invalid role" });
2728
+ }
2729
+ const existing = await this.loadUserOrNull(email);
2730
+ if (await this.duplicateCheck({
2731
+ email,
2732
+ existingUser: existing
2733
+ }) === "reject") {
2734
+ if (existing?.account?.pendingInvitation) throw new HttpError$1(409, "Invite already pending, use reInvite");
2735
+ if (existing) throw new HttpError$1(409, "User already exists");
2736
+ throw new HttpError$1(409, "Duplicate invite rejected");
2737
+ }
2738
+ ctx.email = email;
2739
+ if (input.firstName) ctx.firstName = input.firstName;
2740
+ if (input.lastName) ctx.lastName = input.lastName;
2741
+ if (parsed.length > 0) ctx.roles = parsed;
2742
+ }
2743
+ async inferRolesStep(ctx) {
2744
+ if (!ctx.email) return void 0;
2745
+ const inferred = await this.inferRoles({
2746
+ email: ctx.email,
2747
+ ...ctx.firstName && { firstName: ctx.firstName },
2748
+ ...ctx.lastName && { lastName: ctx.lastName }
2749
+ });
2750
+ if (inferred.length === 0) return void 0;
2751
+ const merged = new Set([...ctx.roles ?? [], ...inferred]);
2752
+ ctx.roles = Array.from(merged);
2753
+ }
2754
+ async preCreateUser(ctx) {
2755
+ if (!ctx.email) throw new HttpError$1(500, "Workflow state corrupted: missing email");
2756
+ const invitedBy = useAuth().getAuthContext()?.userId;
2757
+ const preparedInput = {
2758
+ email: ctx.email,
2759
+ ...ctx.firstName && { firstName: ctx.firstName },
2760
+ ...ctx.lastName && { lastName: ctx.lastName },
2761
+ roles: ctx.roles ?? [],
2762
+ ...invitedBy && { invitedBy }
2763
+ };
2764
+ const fields = {
2765
+ ...await this.prepareUser(preparedInput),
2766
+ ...ctx.roles && ctx.roles.length > 0 && { roles: ctx.roles }
2767
+ };
2768
+ try {
2769
+ await this.users.createUser(ctx.email, void 0, fields);
2770
+ } catch (err) {
2771
+ if (err instanceof UserAuthError && err.type === "ALREADY_EXISTS") throw new HttpError$1(409, "User already exists");
2772
+ throw err;
2773
+ }
2774
+ await this.users.update(ctx.email, { account: { pendingInvitation: true } });
2775
+ ctx.username = ctx.email;
2776
+ await this.emitAudit("invite.created", ctx);
2777
+ }
2778
+ sendInviteEmail(ctx) {
2779
+ if (ctx.linkSent) return void 0;
2780
+ ctx.linkSent = true;
2781
+ return {
2782
+ ...outletEmail(ctx.email, "invite.magicLink", {
2783
+ username: ctx.username,
2784
+ ...ctx.roles && { roles: ctx.roles },
2785
+ expiresAtMs: this.opts.send.tokenTtlMs
2786
+ }),
2787
+ expires: Date.now() + this.opts.send.tokenTtlMs
2788
+ };
2789
+ }
2790
+ returnShareableLink(ctx) {
2791
+ if (ctx.linkSent) return void 0;
2792
+ ctx.linkSent = true;
2793
+ return {
2794
+ ...outletEmail(ctx.email, "invite.magicLink", {
2795
+ username: ctx.username,
2796
+ ...ctx.roles && { roles: ctx.roles },
2797
+ expiresAtMs: this.opts.send.tokenTtlMs,
2798
+ shareableLink: true
2799
+ }),
2800
+ expires: Date.now() + this.opts.send.tokenTtlMs
2801
+ };
2802
+ }
2803
+ async loadPendingUser(input, ctx) {
2804
+ if (!input) return httpInputRequired(this.opts.forms.inviteEmail, ctx);
2805
+ if (input.action === "cancel") return this.abort(ctx, "cancel");
2806
+ const errors = validateFormInput(this.opts.forms.inviteEmail, input);
2807
+ if (errors) return httpInputRequired(this.opts.forms.inviteEmail, ctx, errors);
2808
+ const email = input.email;
2809
+ const existing = await this.loadUserOrNull(email);
2810
+ if (!existing) throw new HttpError$1(404, "No pending invite for this email");
2811
+ if (!existing.account?.pendingInvitation) throw new HttpError$1(409, "User has already accepted; cannot resend");
2812
+ ctx.email = email;
2813
+ ctx.username = existing.username;
2814
+ const u = existing;
2815
+ if (u.firstName) ctx.firstName = u.firstName;
2816
+ if (u.lastName) ctx.lastName = u.lastName;
2817
+ if (u.roles && u.roles.length > 0) ctx.roles = u.roles;
2818
+ await this.emitAudit("invite.resent", ctx);
2819
+ }
2820
+ async checkPendingInvitation(ctx) {
2821
+ if (!ctx.username) throw new HttpError$1(500, "Workflow state corrupted: missing username at accept");
2822
+ const existing = await this.loadUserOrNull(ctx.username);
2823
+ if (!existing) throw new HttpError$1(410, "This invite has been cancelled");
2824
+ if (!existing.account?.pendingInvitation) ctx.alreadyAccepted = true;
2825
+ }
2826
+ idempotentRedirect(ctx) {
2827
+ const altUrl = this.opts.accept.alreadyAcceptedRedirectUrl;
2828
+ finishWfWithChoice({
2829
+ message: {
2830
+ level: "info",
2831
+ text: "This invite was already accepted."
2832
+ },
2833
+ primary: {
2834
+ label: "Go to sign-in",
2835
+ action: {
2836
+ type: "redirect",
2837
+ target: this.opts.accept.loginUrl,
2838
+ reason: "already-accepted"
2839
+ }
2840
+ },
2841
+ ...altUrl && { options: [{
2842
+ label: "Request a new invite",
2843
+ action: {
2844
+ type: "redirect",
2845
+ target: altUrl,
2846
+ reason: "request-new-invite"
2847
+ }
2848
+ }] }
2849
+ });
2850
+ ctx.aborted = true;
2851
+ }
2852
+ preparePasswordRules(ctx) {
2853
+ ctx.passwordPolicies = this.users.getTransferablePolicies();
2854
+ }
2855
+ async createPasswordForm(input, ctx) {
2856
+ if (!input) return httpInputRequired(this.opts.forms.setPassword, ctx);
2857
+ if (input.action === "cancel") return this.abort(ctx, "cancel");
2858
+ const errors = validateFormInput(this.opts.forms.setPassword, input);
2859
+ if (errors) return httpInputRequired(this.opts.forms.setPassword, ctx, errors);
2860
+ if (input.newPassword !== input.confirmPassword) return httpInputRequired(this.opts.forms.setPassword, ctx, { confirmPassword: "Passwords do not match" });
2861
+ requireUsername(ctx);
2862
+ try {
2863
+ await this.users.setPassword(ctx.username, input.newPassword);
2864
+ } catch (err) {
2865
+ translatePasswordSetError(err);
2866
+ }
2867
+ ctx.passwordSet = true;
2868
+ }
2869
+ async collectProfile(input, ctx) {
2870
+ const form = this.getProfileForm();
2871
+ if (!form) return void 0;
2872
+ if (!input) return httpInputRequired(form, ctx);
2873
+ if (input.action === "skip") {
2874
+ ctx.profile = {};
2875
+ return;
2876
+ }
2877
+ const errors = validateFormInput(form, input, { partial: "deep" });
2878
+ if (errors) return httpInputRequired(form, ctx, errors);
2879
+ ctx.profile = input;
2880
+ }
2881
+ async applyProfileStep(ctx) {
2882
+ requireUsername(ctx);
2883
+ const profile = ctx.profile ?? {};
2884
+ if (Object.keys(profile).length === 0) {
2885
+ ctx.profileApplied = true;
2886
+ return;
2887
+ }
2888
+ await this.applyProfile({
2889
+ username: ctx.username,
2890
+ profile
2891
+ });
2892
+ ctx.profileApplied = true;
2893
+ }
2894
+ async unsetPendingInvitation(ctx) {
2895
+ requireUsername(ctx);
2896
+ await this.users.update(ctx.username, { account: { pendingInvitation: false } });
2897
+ ctx.pendingInvitationCleared = true;
2898
+ }
2899
+ async activateUser(ctx) {
2900
+ requireUsername(ctx);
2901
+ await this.users.activateAccount(ctx.username);
2902
+ ctx.activated = true;
2903
+ await this.emitAudit("invite.accepted", ctx);
2904
+ }
2905
+ confirmation(ctx) {
2906
+ ctx.confirmationShown = true;
2907
+ finishWfWithData({ confirmed: true }, {
2908
+ level: "success",
2909
+ text: this.opts.accept.confirmationMessage
2910
+ });
2911
+ }
2912
+ freshLoginFinish(_ctx) {
2913
+ finishWfWithRedirect(this.opts.accept.loginUrl, { reason: "fresh-login-required" });
2914
+ }
2915
+ async autoLoginFinish(ctx) {
2916
+ requireUsername(ctx);
2917
+ const issue = await this.auth.issue(ctx.username);
2918
+ ctx.tokensIssued = true;
2919
+ const auth = useAuth();
2920
+ const envelope = {
2921
+ finished: true,
2922
+ data: auth.buildLoginResponse(ctx.username, issue)
2923
+ };
2924
+ useWfFinished().set({
2925
+ type: "data",
2926
+ value: envelope,
2927
+ cookies: auth.buildFinishedCookies(issue)
2928
+ });
2929
+ }
2930
+ async cancelInvite(input, ctx) {
2931
+ if (!this.opts.cancellation.allowed) throw new HttpError$1(403, "Invite cancellation is disabled");
2932
+ if (!input) return httpInputRequired(this.opts.forms.inviteEmail, ctx);
2933
+ const errors = validateFormInput(this.opts.forms.inviteEmail, input);
2934
+ if (errors) return httpInputRequired(this.opts.forms.inviteEmail, ctx, errors);
2935
+ const email = input.email;
2936
+ const existing = await this.loadUserOrNull(email);
2937
+ if (!existing) throw new HttpError$1(404, "No invite to cancel for this email");
2938
+ if (!existing.account?.pendingInvitation) throw new HttpError$1(409, "Cannot cancel: user has already accepted the invite");
2939
+ await this.users.deleteUser(existing.username);
2940
+ await this.emitAudit("invite.cancelled", {
2941
+ ...ctx,
2942
+ email,
2943
+ username: existing.username
2944
+ });
2945
+ finishWfWithData({
2946
+ cancelled: true,
2947
+ email
2948
+ }, {
2949
+ level: "info",
2950
+ text: "Invite cancelled."
2951
+ });
2952
+ }
2953
+ abort(ctx, reason) {
2954
+ finishWfAborted(reason, { message: {
2955
+ level: "info",
2956
+ text: "Invite cancelled."
2957
+ } });
2958
+ ctx.aborted = true;
2959
+ return ALT_HANDLED;
2960
+ }
2961
+ async loadUserOrNull(username) {
2962
+ try {
2963
+ return await this.users.getUser(username);
2964
+ } catch (err) {
2965
+ if (err instanceof UserAuthError && err.type === "NOT_FOUND") return null;
2966
+ throw err;
2967
+ }
2968
+ }
2969
+ async emitAudit(kind, ctx) {
2970
+ if (!this.opts.audit.enabled) return;
2971
+ const invitedBy = useAuth().getAuthContext()?.userId;
2972
+ await this.audit({
2973
+ kind,
2974
+ workflow: AUDIT_WORKFLOW_BY_KIND[kind] ?? "auth.invite",
2975
+ ...ctx.username && { userId: ctx.username },
2976
+ ...invitedBy && { invitedBy },
2977
+ ...ctx.email && { email: ctx.email },
2978
+ ip: resolveClientIp()
2979
+ });
2980
+ }
2981
+ };
2982
+ __decorate([
2983
+ Workflow("auth.invite"),
2984
+ WorkflowSchema([
2985
+ { id: "inviteInit" },
2986
+ {
2987
+ id: "invitePrepareAvailableRoles",
2988
+ condition: (ctx) => ctx.opts.adminForm.collectRoles && !ctx.aborted
2989
+ },
2990
+ {
2991
+ id: "inviteSelectSendMode",
2992
+ condition: (ctx) => ctx.opts.send.mode === "choice" && !ctx.resolvedSendMode && !ctx.aborted
2993
+ },
2994
+ {
2995
+ id: "inviteAdminInviteForm",
2996
+ condition: (ctx) => !ctx.email && !ctx.aborted
2997
+ },
2998
+ {
2999
+ id: "inviteInferRolesStep",
3000
+ condition: (ctx) => !!(ctx.email && !ctx.aborted)
3001
+ },
3002
+ {
3003
+ id: "invitePreCreateUser",
3004
+ condition: (ctx) => !!(ctx.email && !ctx.username && !ctx.aborted)
3005
+ },
3006
+ {
3007
+ id: "inviteSendInviteEmail",
3008
+ condition: (ctx) => !!(ctx.username && ctx.resolvedSendMode === "email" && !ctx.aborted)
3009
+ },
3010
+ {
3011
+ id: "inviteReturnShareableLink",
3012
+ condition: (ctx) => !!(ctx.username && ctx.resolvedSendMode === "shareableLink" && !ctx.aborted)
3013
+ },
3014
+ {
3015
+ id: "inviteCheckPendingInvitation",
3016
+ condition: (ctx) => !!(ctx.linkSent && !ctx.aborted)
3017
+ },
3018
+ {
3019
+ id: "inviteIdempotentRedirect",
3020
+ condition: (ctx) => !!(ctx.alreadyAccepted && !ctx.aborted)
3021
+ },
3022
+ {
3023
+ id: "invitePreparePasswordRules",
3024
+ condition: (ctx) => !!(ctx.linkSent && !ctx.alreadyAccepted && !ctx.aborted)
3025
+ },
3026
+ {
3027
+ id: "inviteCreatePasswordForm",
3028
+ condition: (ctx) => !!(ctx.linkSent && !ctx.alreadyAccepted && !ctx.passwordSet && !ctx.aborted)
3029
+ },
3030
+ {
3031
+ id: "inviteCollectProfile",
3032
+ condition: (ctx) => !!(ctx.passwordSet && ctx.acceptProfileFormPresent && !ctx.profile && !ctx.aborted)
3033
+ },
3034
+ {
3035
+ id: "inviteApplyProfile",
3036
+ condition: (ctx) => !!(ctx.passwordSet && ctx.acceptProfileFormPresent && ctx.profile && !ctx.profileApplied && !ctx.aborted)
3037
+ },
3038
+ {
3039
+ id: "inviteUnsetPendingInvitation",
3040
+ condition: (ctx) => !!(ctx.passwordSet && !ctx.pendingInvitationCleared && !ctx.aborted)
3041
+ },
3042
+ {
3043
+ id: "inviteActivateUser",
3044
+ condition: (ctx) => !!(ctx.pendingInvitationCleared && !ctx.activated && !ctx.aborted)
3045
+ },
3046
+ {
3047
+ id: "inviteConfirmation",
3048
+ condition: (ctx) => !!(ctx.activated && ctx.opts.accept.showConfirmation && !ctx.confirmationShown && !ctx.aborted)
3049
+ },
3050
+ {
3051
+ id: "inviteFreshLoginFinish",
3052
+ condition: (ctx) => !!(ctx.activated && ctx.opts.accept.freshLoginRequired && !ctx.aborted)
3053
+ },
3054
+ {
3055
+ id: "inviteAutoLoginFinish",
3056
+ condition: (ctx) => !!(ctx.activated && !ctx.opts.accept.freshLoginRequired && !ctx.tokensIssued && !ctx.aborted)
3057
+ }
3058
+ ]),
3059
+ Public(),
3060
+ __decorateMetadata("design:type", Function),
3061
+ __decorateMetadata("design:paramtypes", []),
3062
+ __decorateMetadata("design:returntype", void 0)
3063
+ ], InviteWorkflow.prototype, "inviteFlow", null);
3064
+ __decorate([
3065
+ Workflow("auth.reInvite"),
3066
+ WorkflowSchema([
3067
+ { id: "inviteInit" },
3068
+ {
3069
+ id: "inviteLoadPendingUser",
3070
+ condition: (ctx) => !ctx.aborted
3071
+ },
3072
+ {
3073
+ id: "inviteSendInviteEmail",
3074
+ condition: (ctx) => !!(ctx.username && ctx.resolvedSendMode === "email" && !ctx.aborted)
3075
+ },
3076
+ {
3077
+ id: "inviteReturnShareableLink",
3078
+ condition: (ctx) => !!(ctx.username && ctx.resolvedSendMode === "shareableLink" && !ctx.aborted)
3079
+ },
3080
+ {
3081
+ id: "inviteCheckPendingInvitation",
3082
+ condition: (ctx) => !!(ctx.linkSent && !ctx.aborted)
3083
+ },
3084
+ {
3085
+ id: "inviteIdempotentRedirect",
3086
+ condition: (ctx) => !!(ctx.alreadyAccepted && !ctx.aborted)
3087
+ },
3088
+ {
3089
+ id: "invitePreparePasswordRules",
3090
+ condition: (ctx) => !!(ctx.linkSent && !ctx.alreadyAccepted && !ctx.aborted)
3091
+ },
3092
+ {
3093
+ id: "inviteCreatePasswordForm",
3094
+ condition: (ctx) => !!(ctx.linkSent && !ctx.alreadyAccepted && !ctx.passwordSet && !ctx.aborted)
3095
+ },
3096
+ {
3097
+ id: "inviteCollectProfile",
3098
+ condition: (ctx) => !!(ctx.passwordSet && ctx.acceptProfileFormPresent && !ctx.profile && !ctx.aborted)
3099
+ },
3100
+ {
3101
+ id: "inviteApplyProfile",
3102
+ condition: (ctx) => !!(ctx.passwordSet && ctx.acceptProfileFormPresent && ctx.profile && !ctx.profileApplied && !ctx.aborted)
3103
+ },
3104
+ {
3105
+ id: "inviteUnsetPendingInvitation",
3106
+ condition: (ctx) => !!(ctx.passwordSet && !ctx.pendingInvitationCleared && !ctx.aborted)
3107
+ },
3108
+ {
3109
+ id: "inviteActivateUser",
3110
+ condition: (ctx) => !!(ctx.pendingInvitationCleared && !ctx.activated && !ctx.aborted)
3111
+ },
3112
+ {
3113
+ id: "inviteConfirmation",
3114
+ condition: (ctx) => !!(ctx.activated && ctx.opts.accept.showConfirmation && !ctx.confirmationShown && !ctx.aborted)
3115
+ },
3116
+ {
3117
+ id: "inviteFreshLoginFinish",
3118
+ condition: (ctx) => !!(ctx.activated && ctx.opts.accept.freshLoginRequired && !ctx.aborted)
3119
+ },
3120
+ {
3121
+ id: "inviteAutoLoginFinish",
3122
+ condition: (ctx) => !!(ctx.activated && !ctx.opts.accept.freshLoginRequired && !ctx.tokensIssued && !ctx.aborted)
3123
+ }
3124
+ ]),
3125
+ Public(),
3126
+ __decorateMetadata("design:type", Function),
3127
+ __decorateMetadata("design:paramtypes", []),
3128
+ __decorateMetadata("design:returntype", void 0)
3129
+ ], InviteWorkflow.prototype, "reInviteFlow", null);
3130
+ __decorate([
3131
+ Workflow("auth.cancelInvite"),
3132
+ WorkflowSchema([{ id: "inviteInit" }, {
3133
+ id: "inviteCancelInvite",
3134
+ condition: (ctx) => !ctx.aborted
3135
+ }]),
3136
+ Public(),
3137
+ __decorateMetadata("design:type", Function),
3138
+ __decorateMetadata("design:paramtypes", []),
3139
+ __decorateMetadata("design:returntype", void 0)
3140
+ ], InviteWorkflow.prototype, "cancelInviteFlow", null);
3141
+ __decorate([
3142
+ Step("inviteInit"),
3143
+ __decorateParam(0, WorkflowParam("context")),
3144
+ __decorateMetadata("design:type", Function),
3145
+ __decorateMetadata("design:paramtypes", [Object]),
3146
+ __decorateMetadata("design:returntype", void 0)
3147
+ ], InviteWorkflow.prototype, "init", null);
3148
+ __decorate([
3149
+ Step("invitePrepareAvailableRoles"),
3150
+ __decorateParam(0, WorkflowParam("context")),
3151
+ __decorateMetadata("design:type", Function),
3152
+ __decorateMetadata("design:paramtypes", [Object]),
3153
+ __decorateMetadata("design:returntype", Promise)
3154
+ ], InviteWorkflow.prototype, "prepareAvailableRoles", null);
3155
+ __decorate([
3156
+ Step("inviteSelectSendMode"),
3157
+ __decorateParam(0, WorkflowParam("input")),
3158
+ __decorateParam(1, WorkflowParam("context")),
3159
+ __decorateMetadata("design:type", Function),
3160
+ __decorateMetadata("design:paramtypes", [Object, Object]),
3161
+ __decorateMetadata("design:returntype", Object)
3162
+ ], InviteWorkflow.prototype, "selectSendMode", null);
3163
+ __decorate([
3164
+ Step("inviteAdminInviteForm"),
3165
+ __decorateParam(0, WorkflowParam("input")),
3166
+ __decorateParam(1, WorkflowParam("context")),
3167
+ __decorateMetadata("design:type", Function),
3168
+ __decorateMetadata("design:paramtypes", [Object, Object]),
3169
+ __decorateMetadata("design:returntype", Promise)
3170
+ ], InviteWorkflow.prototype, "adminInviteForm", null);
3171
+ __decorate([
3172
+ Step("inviteInferRolesStep"),
3173
+ __decorateParam(0, WorkflowParam("context")),
3174
+ __decorateMetadata("design:type", Function),
3175
+ __decorateMetadata("design:paramtypes", [Object]),
3176
+ __decorateMetadata("design:returntype", Promise)
3177
+ ], InviteWorkflow.prototype, "inferRolesStep", null);
3178
+ __decorate([
3179
+ Step("invitePreCreateUser"),
3180
+ __decorateParam(0, WorkflowParam("context")),
3181
+ __decorateMetadata("design:type", Function),
3182
+ __decorateMetadata("design:paramtypes", [Object]),
3183
+ __decorateMetadata("design:returntype", Promise)
3184
+ ], InviteWorkflow.prototype, "preCreateUser", null);
3185
+ __decorate([
3186
+ Step("inviteSendInviteEmail"),
3187
+ Public(),
3188
+ __decorateParam(0, WorkflowParam("context")),
3189
+ __decorateMetadata("design:type", Function),
3190
+ __decorateMetadata("design:paramtypes", [Object]),
3191
+ __decorateMetadata("design:returntype", Object)
3192
+ ], InviteWorkflow.prototype, "sendInviteEmail", null);
3193
+ __decorate([
3194
+ Step("inviteReturnShareableLink"),
3195
+ Public(),
3196
+ __decorateParam(0, WorkflowParam("context")),
3197
+ __decorateMetadata("design:type", Function),
3198
+ __decorateMetadata("design:paramtypes", [Object]),
3199
+ __decorateMetadata("design:returntype", Object)
3200
+ ], InviteWorkflow.prototype, "returnShareableLink", null);
3201
+ __decorate([
3202
+ Step("inviteLoadPendingUser"),
3203
+ __decorateParam(0, WorkflowParam("input")),
3204
+ __decorateParam(1, WorkflowParam("context")),
3205
+ __decorateMetadata("design:type", Function),
3206
+ __decorateMetadata("design:paramtypes", [Object, Object]),
3207
+ __decorateMetadata("design:returntype", Promise)
3208
+ ], InviteWorkflow.prototype, "loadPendingUser", null);
3209
+ __decorate([
3210
+ Step("inviteCheckPendingInvitation"),
3211
+ Public(),
3212
+ __decorateParam(0, WorkflowParam("context")),
3213
+ __decorateMetadata("design:type", Function),
3214
+ __decorateMetadata("design:paramtypes", [Object]),
3215
+ __decorateMetadata("design:returntype", Promise)
3216
+ ], InviteWorkflow.prototype, "checkPendingInvitation", null);
3217
+ __decorate([
3218
+ Step("inviteIdempotentRedirect"),
3219
+ Public(),
3220
+ __decorateParam(0, WorkflowParam("context")),
3221
+ __decorateMetadata("design:type", Function),
3222
+ __decorateMetadata("design:paramtypes", [Object]),
3223
+ __decorateMetadata("design:returntype", void 0)
3224
+ ], InviteWorkflow.prototype, "idempotentRedirect", null);
3225
+ __decorate([
3226
+ Step("invitePreparePasswordRules"),
3227
+ Public(),
3228
+ __decorateParam(0, WorkflowParam("context")),
3229
+ __decorateMetadata("design:type", Function),
3230
+ __decorateMetadata("design:paramtypes", [Object]),
3231
+ __decorateMetadata("design:returntype", void 0)
3232
+ ], InviteWorkflow.prototype, "preparePasswordRules", null);
3233
+ __decorate([
3234
+ Step("inviteCreatePasswordForm"),
3235
+ Public(),
3236
+ __decorateParam(0, WorkflowParam("input")),
3237
+ __decorateParam(1, WorkflowParam("context")),
3238
+ __decorateMetadata("design:type", Function),
3239
+ __decorateMetadata("design:paramtypes", [Object, Object]),
3240
+ __decorateMetadata("design:returntype", Promise)
3241
+ ], InviteWorkflow.prototype, "createPasswordForm", null);
3242
+ __decorate([
3243
+ Step("inviteCollectProfile"),
3244
+ Public(),
3245
+ __decorateParam(0, WorkflowParam("input")),
3246
+ __decorateParam(1, WorkflowParam("context")),
3247
+ __decorateMetadata("design:type", Function),
3248
+ __decorateMetadata("design:paramtypes", [Object, Object]),
3249
+ __decorateMetadata("design:returntype", Promise)
3250
+ ], InviteWorkflow.prototype, "collectProfile", null);
3251
+ __decorate([
3252
+ Step("inviteApplyProfile"),
3253
+ Public(),
3254
+ __decorateParam(0, WorkflowParam("context")),
3255
+ __decorateMetadata("design:type", Function),
3256
+ __decorateMetadata("design:paramtypes", [Object]),
3257
+ __decorateMetadata("design:returntype", Promise)
3258
+ ], InviteWorkflow.prototype, "applyProfileStep", null);
3259
+ __decorate([
3260
+ Step("inviteUnsetPendingInvitation"),
3261
+ Public(),
3262
+ __decorateParam(0, WorkflowParam("context")),
3263
+ __decorateMetadata("design:type", Function),
3264
+ __decorateMetadata("design:paramtypes", [Object]),
3265
+ __decorateMetadata("design:returntype", Promise)
3266
+ ], InviteWorkflow.prototype, "unsetPendingInvitation", null);
3267
+ __decorate([
3268
+ Step("inviteActivateUser"),
3269
+ Public(),
3270
+ __decorateParam(0, WorkflowParam("context")),
3271
+ __decorateMetadata("design:type", Function),
3272
+ __decorateMetadata("design:paramtypes", [Object]),
3273
+ __decorateMetadata("design:returntype", Promise)
3274
+ ], InviteWorkflow.prototype, "activateUser", null);
3275
+ __decorate([
3276
+ Step("inviteConfirmation"),
3277
+ Public(),
3278
+ __decorateParam(0, WorkflowParam("context")),
3279
+ __decorateMetadata("design:type", Function),
3280
+ __decorateMetadata("design:paramtypes", [Object]),
3281
+ __decorateMetadata("design:returntype", void 0)
3282
+ ], InviteWorkflow.prototype, "confirmation", null);
3283
+ __decorate([
3284
+ Step("inviteFreshLoginFinish"),
3285
+ Public(),
3286
+ __decorateParam(0, WorkflowParam("context")),
3287
+ __decorateMetadata("design:type", Function),
3288
+ __decorateMetadata("design:paramtypes", [Object]),
3289
+ __decorateMetadata("design:returntype", void 0)
3290
+ ], InviteWorkflow.prototype, "freshLoginFinish", null);
3291
+ __decorate([
3292
+ Step("inviteAutoLoginFinish"),
3293
+ Public(),
3294
+ __decorateParam(0, WorkflowParam("context")),
3295
+ __decorateMetadata("design:type", Function),
3296
+ __decorateMetadata("design:paramtypes", [Object]),
3297
+ __decorateMetadata("design:returntype", Promise)
3298
+ ], InviteWorkflow.prototype, "autoLoginFinish", null);
3299
+ __decorate([
3300
+ Step("inviteCancelInvite"),
3301
+ __decorateParam(0, WorkflowParam("input")),
3302
+ __decorateParam(1, WorkflowParam("context")),
3303
+ __decorateMetadata("design:type", Function),
3304
+ __decorateMetadata("design:paramtypes", [Object, Object]),
3305
+ __decorateMetadata("design:returntype", Promise)
3306
+ ], InviteWorkflow.prototype, "cancelInvite", null);
3307
+ InviteWorkflow = __decorate([
3308
+ ArbacResource("auth.invite"),
3309
+ ArbacAction("start"),
3310
+ Injectable("FOR_EVENT"),
3311
+ Controller(),
3312
+ __decorateMetadata("design:paramtypes", [
3313
+ Object,
3314
+ typeof (_ref = typeof UserService !== "undefined" && UserService) === "function" ? _ref : Object,
3315
+ typeof (_ref2 = typeof AuthCredential !== "undefined" && AuthCredential) === "function" ? _ref2 : Object
3316
+ ])
3317
+ ], InviteWorkflow);
3318
+ //#endregion
3319
+ //#region src/workflows/auth-email-outlet.ts
3320
+ /**
3321
+ * Build the email outlet that delivers magic links via the consumer's
3322
+ * `EmailSender`. Single-use: pass the same instance into one
3323
+ * `handleWfOutletRequest({ outlets })`.
3324
+ */
3325
+ function createAuthEmailOutlet(deps) {
3326
+ return createEmailOutlet(async (opts) => {
3327
+ const template = opts.template;
3328
+ const ttlHint = typeof opts.context?.expiresAtMs === "number" ? opts.context.expiresAtMs : 0;
3329
+ const expiresAt = Date.now() + (ttlHint || deps.magicLinkTtlMs(template));
3330
+ const url = deps.buildMagicLinkUrl(template, opts.token);
3331
+ const event = {
3332
+ kind: template,
3333
+ recipient: opts.target,
3334
+ url,
3335
+ expiresAt
3336
+ };
3337
+ if (typeof opts.context?.username === "string") event.username = opts.context.username;
3338
+ const rolesRaw = opts.context?.roles;
3339
+ if (Array.isArray(rolesRaw)) {
3340
+ const roles = rolesRaw.filter((r) => typeof r === "string");
3341
+ if (roles.length > 0) event.metadata = { roles };
3342
+ }
3343
+ await deps.emailSender.send(event);
3344
+ });
3345
+ }
3346
+ //#endregion
3347
+ export { AuthController, AuthGuarded, DEFAULT_AUTH_WORKFLOWS, InviteWorkflow, LoginWorkflow, Public, RecoveryWorkflow, UserId, WfTrigger, WfTriggerProvider, authGuardInterceptor, createAuthEmailOutlet, generateMagicLinkToken, getAuthMate, mergeInviteOpts, mergeLoginOpts, mergeRecoveryOpts, parseInviteRoles, useAuth };