@aooth/auth-moost 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/atscript/index.d.mts +2 -2
- package/dist/atscript/index.mjs +2 -2
- package/dist/forms-sF41Fzzn.mjs +380 -0
- package/dist/index.d.mts +1336 -390
- package/dist/index.mjs +3708 -2400
- package/package.json +19 -15
- package/src/atscript/models/forms.as +347 -33
- package/src/atscript/models/forms.as.d.ts +115 -38
- package/dist/forms-BE62OrN1.mjs +0 -230
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aooth/auth-moost",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Moost auth integration for aoothjs — AuthGuard interceptor, useAuth composable, REST endpoints, workflows",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"aoothjs",
|
|
@@ -51,20 +51,22 @@
|
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@atscript/moost-wf": "^0.1.
|
|
55
|
-
"@
|
|
56
|
-
"@aooth/
|
|
57
|
-
"@aooth/
|
|
54
|
+
"@atscript/moost-wf": "^0.1.76",
|
|
55
|
+
"@wooksjs/http-body": "^0.7.15",
|
|
56
|
+
"@aooth/arbac-moost": "^0.1.4",
|
|
57
|
+
"@aooth/auth": "0.1.4",
|
|
58
|
+
"@aooth/user": "0.1.4"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
60
|
-
"@atscript/core": "^0.1.
|
|
61
|
-
"@atscript/typescript": "^0.1.
|
|
62
|
-
"@atscript/ui": "^0.1.
|
|
63
|
-
"@
|
|
64
|
-
"@moostjs/event-
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
61
|
+
"@atscript/core": "^0.1.61",
|
|
62
|
+
"@atscript/typescript": "^0.1.61",
|
|
63
|
+
"@atscript/ui": "^0.1.76",
|
|
64
|
+
"@atscript/ui-fns": "^0.1.76",
|
|
65
|
+
"@moostjs/event-http": "^0.6.17",
|
|
66
|
+
"@moostjs/event-wf": "^0.6.17",
|
|
67
|
+
"moost": "^0.6.17",
|
|
68
|
+
"unplugin-atscript": "^0.1.61",
|
|
69
|
+
"wooks": "^0.7.15"
|
|
68
70
|
},
|
|
69
71
|
"peerDependencies": {
|
|
70
72
|
"@atscript/moost-wf": ">=0.1.58",
|
|
@@ -73,6 +75,7 @@
|
|
|
73
75
|
"@moostjs/event-wf": ">=0.6.10",
|
|
74
76
|
"@wooksjs/event-core": ">=0.7.12",
|
|
75
77
|
"@wooksjs/event-http": ">=0.7.12",
|
|
78
|
+
"@wooksjs/http-body": ">=0.7.12",
|
|
76
79
|
"moost": ">=0.6.10"
|
|
77
80
|
},
|
|
78
81
|
"peerDependenciesMeta": {
|
|
@@ -81,10 +84,11 @@
|
|
|
81
84
|
}
|
|
82
85
|
},
|
|
83
86
|
"scripts": {
|
|
84
|
-
"gen:atscript": "
|
|
87
|
+
"gen:atscript": "asc",
|
|
85
88
|
"build": "vp pack",
|
|
86
89
|
"dev": "vp pack --watch",
|
|
87
90
|
"test": "vp test",
|
|
88
|
-
"check": "vp check"
|
|
91
|
+
"check": "vp check",
|
|
92
|
+
"postinstall": "asc"
|
|
89
93
|
}
|
|
90
94
|
}
|
|
@@ -1,9 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline consent collection — a single dynamic `consents: string[]` field
|
|
3
|
+
* attached to whichever carrier form the user is already filling out.
|
|
4
|
+
* Carrier forms `extends WithInlineConsentForm` to inherit it without
|
|
5
|
+
* duplication.
|
|
6
|
+
*
|
|
7
|
+
* Backend transport: `@wf.context.pass 'pendingConsents'` ships the
|
|
8
|
+
* descriptor array (set by the `prepare-consents` @Step from
|
|
9
|
+
* `ConsentStore.getPendingConsents()`) to the client. The
|
|
10
|
+
* `@ui.form.fn.attr 'pendingConsents'` expression below binds it onto the
|
|
11
|
+
* `AsConsentArray` component (`@atscript/vue-aooth`) which renders one
|
|
12
|
+
* checkbox per descriptor; the user-submitted `string[]` carries back the
|
|
13
|
+
* SUBSET of `descriptor.id`s the user ticked.
|
|
14
|
+
*
|
|
15
|
+
* SPA-side hide-when-empty: `AsConsentArray` self-hides when
|
|
16
|
+
* `pendingConsents` is empty / unset — no `@ui.form.fn.hidden` is needed
|
|
17
|
+
* on the field. A carrier form whose customer hasn't configured any
|
|
18
|
+
* pending consents renders WITHOUT this block.
|
|
19
|
+
*
|
|
20
|
+
* SECURITY (silent-drop): the server-side `processInlineConsent` helper
|
|
21
|
+
* uses its OWN `ctx.pendingConsents` as the authoritative whitelist; any
|
|
22
|
+
* id submitted by the client outside that set is silently dropped (audit
|
|
23
|
+
* invariant — see helper rationale). The client cannot forge audit rows
|
|
24
|
+
* by submitting ids it was never shown.
|
|
25
|
+
*
|
|
26
|
+
* SECURITY (mandatory-by-message): each `ConsentDescriptor.required`
|
|
27
|
+
* non-empty string IS the per-row error copy AND the form-level error the
|
|
28
|
+
* server throws when a required consent is missing from the submitted set.
|
|
29
|
+
* Absent / empty ⇒ optional (the `ConsentEvent.accepted` boolean lets
|
|
30
|
+
* customers persist the un-ticked-optional decision for audit).
|
|
31
|
+
*/
|
|
32
|
+
@wf.context.pass 'pendingConsents'
|
|
33
|
+
@wf.context.pass 'consentsPersisted'
|
|
34
|
+
export interface WithInlineConsentForm {
|
|
35
|
+
@meta.label 'Pending consents'
|
|
36
|
+
@ui.form.component 'AsConsentArray'
|
|
37
|
+
@ui.form.fn.attr 'pendingConsents', '(_, _d, ctx) => ctx.pendingConsents'
|
|
38
|
+
@ui.form.grid.colSpan '12'
|
|
39
|
+
consents: string[]
|
|
40
|
+
}
|
|
41
|
+
|
|
1
42
|
/**
|
|
2
43
|
* Default login credentials form.
|
|
3
44
|
*
|
|
4
45
|
* Override via `setupAuthWorkflows({ forms: { loginCredentials: MyForm } })`.
|
|
46
|
+
*
|
|
47
|
+
* SSO provider ids (configured via
|
|
48
|
+
* `opts.alternateCredentials.ssoProviders[].id`) are NOT declared here —
|
|
49
|
+
* consumers who enable SSO supply their own `loginCredentials` form and add a
|
|
50
|
+
* matching phantom `ui.action` field per provider so
|
|
51
|
+
* `useAtscriptWf(form).resolveAction()` accepts the dynamic ids.
|
|
5
52
|
*/
|
|
53
|
+
@wf.context.pass 'altForgotPassword'
|
|
54
|
+
@wf.context.pass 'altSignup'
|
|
55
|
+
@wf.context.pass 'altMagicLink'
|
|
56
|
+
@ui.form.submit.text 'Sign in'
|
|
6
57
|
export interface LoginCredentialsForm {
|
|
58
|
+
@ui.form.order 10
|
|
7
59
|
@ui.form.type 'text'
|
|
8
60
|
@meta.label 'Username'
|
|
9
61
|
@ui.form.autocomplete 'username'
|
|
@@ -11,19 +63,46 @@ export interface LoginCredentialsForm {
|
|
|
11
63
|
@expect.minLength 1
|
|
12
64
|
username: string
|
|
13
65
|
|
|
66
|
+
@ui.form.order 20
|
|
14
67
|
@ui.form.type 'password'
|
|
15
68
|
@meta.label 'Password'
|
|
16
69
|
@ui.form.autocomplete 'current-password'
|
|
17
70
|
@meta.sensitive
|
|
18
71
|
@meta.required
|
|
19
72
|
@expect.minLength 1
|
|
73
|
+
@ui.form.action 'forgotPassword', 'Forgot password?'
|
|
74
|
+
@ui.form.fn.hidden '(_, _d, ctx) => !ctx.altForgotPassword'
|
|
75
|
+
@wf.action.withData 'forgotPassword'
|
|
20
76
|
password: string
|
|
77
|
+
|
|
78
|
+
@ui.form.order 30
|
|
79
|
+
@ui.form.action 'signup', 'Sign up'
|
|
80
|
+
@ui.form.fn.hidden '(_, _d, ctx) => !ctx.altSignup'
|
|
81
|
+
signup?: ui.action
|
|
82
|
+
|
|
83
|
+
@ui.form.order 40
|
|
84
|
+
@ui.form.action 'magicLink', 'Sign in with a magic link'
|
|
85
|
+
@ui.form.fn.hidden '(_, _d, ctx) => !ctx.altMagicLink'
|
|
86
|
+
magicLink?: ui.action
|
|
21
87
|
}
|
|
22
88
|
|
|
23
89
|
/**
|
|
24
|
-
* MFA code form
|
|
90
|
+
* MFA code form. Shared by TOTP, email-OTP, and SMS-OTP branches — the
|
|
91
|
+
* leading `transportHint` paragraph reads `mfaMethod` + `pinSentTo` (masked
|
|
92
|
+
* recipient) out of the workflow context so the operator knows which factor
|
|
93
|
+
* the workflow is currently verifying. The hint requires `installDynamicResolver()`
|
|
94
|
+
* from `@atscript/ui-fns` on the consumer side; without it `@ui.form.fn.value`
|
|
95
|
+
* stays inert and the paragraph renders empty.
|
|
25
96
|
*/
|
|
97
|
+
@wf.context.pass 'mfaMethod'
|
|
98
|
+
@wf.context.pass 'pinSentTo'
|
|
99
|
+
@wf.context.pass 'mfaMethodCount'
|
|
100
|
+
@wf.context.pass 'mfaBackupCodes'
|
|
101
|
+
@ui.form.submit.text 'Verify'
|
|
26
102
|
export interface MfaCodeForm {
|
|
103
|
+
@ui.form.fn.value '(_, _d, ctx) => ctx.mfaMethod === "totp" ? "Enter the current 6-digit code from your authenticator app." : ctx.mfaMethod ? "Code sent to " + (ctx.pinSentTo || "your " + ctx.mfaMethod) + " — check the dev server console for the code." : "Enter your verification code."'
|
|
104
|
+
transportHint?: ui.paragraph
|
|
105
|
+
|
|
27
106
|
@ui.form.type 'text'
|
|
28
107
|
@meta.label 'Verification code'
|
|
29
108
|
@ui.form.autocomplete 'one-time-code'
|
|
@@ -32,6 +111,14 @@ export interface MfaCodeForm {
|
|
|
32
111
|
@expect.maxLength 12
|
|
33
112
|
@expect.pattern '^[0-9]+$'
|
|
34
113
|
code: string
|
|
114
|
+
|
|
115
|
+
@ui.form.action 'useDifferentMethod', 'Use a different method'
|
|
116
|
+
@ui.form.fn.hidden '(_, _d, ctx) => (ctx.mfaMethodCount ?? 0) < 2'
|
|
117
|
+
useDifferentMethod?: ui.action
|
|
118
|
+
|
|
119
|
+
@ui.form.action 'useBackupCode', 'Use backup code'
|
|
120
|
+
@ui.form.fn.hidden '(_, _d, ctx) => !ctx.mfaBackupCodes'
|
|
121
|
+
useBackupCode?: ui.action
|
|
35
122
|
}
|
|
36
123
|
|
|
37
124
|
/**
|
|
@@ -69,6 +156,9 @@ export interface EmailIdentifierForm {
|
|
|
69
156
|
@ui.form.autocomplete 'email'
|
|
70
157
|
@meta.required
|
|
71
158
|
email: string.email
|
|
159
|
+
|
|
160
|
+
@ui.form.action 'backToLogin', 'Back to sign-in'
|
|
161
|
+
backToLogin?: ui.action
|
|
72
162
|
}
|
|
73
163
|
|
|
74
164
|
/**
|
|
@@ -76,8 +166,27 @@ export interface EmailIdentifierForm {
|
|
|
76
166
|
*
|
|
77
167
|
* `confirmPassword` equality is enforced in the workflow step (cross-field
|
|
78
168
|
* checks are not expressible via atscript annotations).
|
|
169
|
+
*
|
|
170
|
+
* `@wf.context.pass 'passwordPolicies'` whitelists the workflow ctx key so the
|
|
171
|
+
* prior preparePasswordRules / setPassword steps can ship the transferable
|
|
172
|
+
* password-policy rules (`UserService.getTransferablePolicies()`) to the
|
|
173
|
+
* client for rendering rule hints next to the inputs. Without this annotation
|
|
174
|
+
* the key is stripped by `extractPassContext` before reaching the client.
|
|
175
|
+
*
|
|
176
|
+
* Phase 7 — `passwordRules: ui.paragraph` is a phantom display field bound to
|
|
177
|
+
* the `AsPasswordRules` component (`@atscript/vue-aooth`); the
|
|
178
|
+
* `@ui.form.fn.attr 'policies'` expression reads `ctx.passwordPolicies` (the
|
|
179
|
+
* transferable list seeded by the workflow's `prepare-password-rules` @Step)
|
|
180
|
+
* and the `@ui.form.fn.attr 'password'` expression reads
|
|
181
|
+
* `data.newPassword` so the rule-fulfillment readout updates live on every
|
|
182
|
+
* keystroke. `WithInlineConsentForm` continues to supply the inline-consent
|
|
183
|
+
* `consents: string[]` block via `AsConsentArray` (Phase 5).
|
|
79
184
|
*/
|
|
80
|
-
|
|
185
|
+
@wf.context.pass 'passwordPolicies'
|
|
186
|
+
@wf.context.pass 'pendingConsents'
|
|
187
|
+
@wf.context.pass 'consentsPersisted'
|
|
188
|
+
export interface SetPasswordForm extends WithInlineConsentForm {
|
|
189
|
+
@ui.form.order 10
|
|
81
190
|
@ui.form.type 'password'
|
|
82
191
|
@meta.label 'New password'
|
|
83
192
|
@ui.form.autocomplete 'new-password'
|
|
@@ -86,6 +195,7 @@ export interface SetPasswordForm {
|
|
|
86
195
|
@expect.minLength 8
|
|
87
196
|
newPassword: string
|
|
88
197
|
|
|
198
|
+
@ui.form.order 20
|
|
89
199
|
@ui.form.type 'password'
|
|
90
200
|
@meta.label 'Confirm password'
|
|
91
201
|
@ui.form.autocomplete 'new-password'
|
|
@@ -93,6 +203,41 @@ export interface SetPasswordForm {
|
|
|
93
203
|
@meta.required
|
|
94
204
|
@expect.minLength 8
|
|
95
205
|
confirmPassword: string
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Phantom display field — `ui.paragraph` carries no submission value;
|
|
209
|
+
* it exists purely so `AsPasswordRules` (registered on the SPA via
|
|
210
|
+
* `<AsWfForm :components>`) renders one row per `ctx.passwordPolicies`
|
|
211
|
+
* descriptor between the confirm-password input and the action buttons.
|
|
212
|
+
*
|
|
213
|
+
* The `policies` attr reads from workflow context (seeded by the
|
|
214
|
+
* `prepare-password-rules` @Step via
|
|
215
|
+
* `UserService.getTransferablePolicies()`); the `password` attr reads
|
|
216
|
+
* from the live form-data so each row's `data-passed` flag re-evaluates
|
|
217
|
+
* on every keystroke. The `(_, data) => data.newPassword` shape is
|
|
218
|
+
* load-bearing — a regression that froze the `password` attr at first
|
|
219
|
+
* render (e.g. `() => data.newPassword`) would silently lie about
|
|
220
|
+
* policy fulfillment after the user starts typing.
|
|
221
|
+
*/
|
|
222
|
+
@ui.form.order 25
|
|
223
|
+
@meta.label 'Password requirements'
|
|
224
|
+
@ui.form.component 'AsPasswordRules'
|
|
225
|
+
@ui.form.fn.attr 'policies', '(_, _d, ctx) => ctx.passwordPolicies'
|
|
226
|
+
@ui.form.fn.attr 'password', '(_, data) => data.newPassword'
|
|
227
|
+
@ui.form.grid.colSpan '12'
|
|
228
|
+
passwordRules: ui.paragraph
|
|
229
|
+
|
|
230
|
+
@ui.form.order 30
|
|
231
|
+
@ui.form.action 'logout', 'Logout'
|
|
232
|
+
logout?: ui.action
|
|
233
|
+
|
|
234
|
+
@ui.form.order 40
|
|
235
|
+
@ui.form.action 'cancel', 'Cancel'
|
|
236
|
+
cancel?: ui.action
|
|
237
|
+
|
|
238
|
+
@ui.form.order 50
|
|
239
|
+
@ui.form.action 'backToLogin', 'Back to sign-in'
|
|
240
|
+
backToLogin?: ui.action
|
|
96
241
|
}
|
|
97
242
|
|
|
98
243
|
/**
|
|
@@ -121,9 +266,12 @@ export interface InviteForm {
|
|
|
121
266
|
// UX: select-on-array currently renders single-text-per-item via AsArray;
|
|
122
267
|
// dedicated multi-select widget tracked as atscript-ui follow-up.
|
|
123
268
|
@ui.form.type 'select'
|
|
124
|
-
@ui.form.fn.options '(_, _data, context) => Array.isArray(context.availableRoles) ? context.availableRoles.map(r => ({
|
|
269
|
+
@ui.form.fn.options '(_, _data, context) => Array.isArray(context.availableRoles) ? context.availableRoles.map(r => ({ key: r, label: r })) : []'
|
|
125
270
|
@meta.label 'Roles'
|
|
126
271
|
roles?: string[]
|
|
272
|
+
|
|
273
|
+
@ui.form.action 'cancel', 'Cancel'
|
|
274
|
+
cancel?: ui.action
|
|
127
275
|
}
|
|
128
276
|
|
|
129
277
|
/**
|
|
@@ -137,6 +285,9 @@ export interface InviteEmailForm {
|
|
|
137
285
|
@ui.form.autocomplete 'email'
|
|
138
286
|
@meta.required
|
|
139
287
|
email: string.email
|
|
288
|
+
|
|
289
|
+
@ui.form.action 'cancel', 'Cancel'
|
|
290
|
+
cancel?: ui.action
|
|
140
291
|
}
|
|
141
292
|
|
|
142
293
|
/**
|
|
@@ -145,11 +296,15 @@ export interface InviteEmailForm {
|
|
|
145
296
|
* `'email'` or `'shareableLink'`.
|
|
146
297
|
*/
|
|
147
298
|
export interface InviteSendModeForm {
|
|
148
|
-
@ui.form.type '
|
|
299
|
+
@ui.form.type 'radio'
|
|
300
|
+
@ui.form.options 'Email', 'email'
|
|
301
|
+
@ui.form.options 'Shareable link', 'shareableLink'
|
|
149
302
|
@meta.label 'Delivery mode'
|
|
150
303
|
@meta.required
|
|
151
|
-
@expect.pattern '^(email|shareableLink)$'
|
|
152
304
|
mode: string
|
|
305
|
+
|
|
306
|
+
@ui.form.action 'cancel', 'Cancel'
|
|
307
|
+
cancel?: ui.action
|
|
153
308
|
}
|
|
154
309
|
|
|
155
310
|
/**
|
|
@@ -158,25 +313,51 @@ export interface InviteSendModeForm {
|
|
|
158
313
|
* The workflow renders this only when the user has >1 enrolled methods after
|
|
159
314
|
* `opts.mfaTransports` filtering. `methodName` is the `MfaMethod.name`
|
|
160
315
|
* (e.g. `"totp"`, `"email"`, `"sms"`); the workflow itself validates that the
|
|
161
|
-
* supplied value is in the user's enrolled set.
|
|
316
|
+
* supplied value is in the user's enrolled set. The dropdown options are
|
|
317
|
+
* built from `ctx.mfaEnrolledMethods` (a `MfaSummary[]` populated by
|
|
318
|
+
* `prepareMfaOptions`) so the user only sees factors they actually have.
|
|
162
319
|
*/
|
|
320
|
+
@wf.context.pass 'mfaBackupCodes'
|
|
321
|
+
@wf.context.pass 'mfaEnrolledMethods'
|
|
163
322
|
export interface Select2faForm {
|
|
164
|
-
@ui.form.type '
|
|
323
|
+
@ui.form.type 'radio'
|
|
324
|
+
@ui.form.fn.options '(_, _d, ctx) => Array.isArray(ctx.mfaEnrolledMethods) ? ctx.mfaEnrolledMethods.map(m => ({ key: m.methodName, label: m.kind === "totp" ? "TOTP (Authenticator app)" : m.kind === "email" ? "Email" : m.kind === "sms" ? "SMS" : m.kind })) : []'
|
|
165
325
|
@meta.label 'MFA method'
|
|
166
326
|
@meta.required
|
|
167
327
|
methodName: string
|
|
168
328
|
|
|
169
329
|
@ui.form.type 'checkbox'
|
|
170
330
|
@meta.label 'Save as default'
|
|
171
|
-
|
|
331
|
+
@meta.default 'false'
|
|
332
|
+
saveAsDefault: boolean
|
|
333
|
+
|
|
334
|
+
@ui.form.action 'useBackupCode', 'Use backup code'
|
|
335
|
+
@ui.form.fn.hidden '(_, _d, ctx) => !ctx.mfaBackupCodes'
|
|
336
|
+
useBackupCode?: ui.action
|
|
172
337
|
}
|
|
173
338
|
|
|
174
339
|
/**
|
|
175
340
|
* Generic 6-digit pincode form — used by both login and recovery OTP flows.
|
|
176
341
|
*
|
|
177
342
|
* `rememberDevice` is rendered only when `opts.deviceTrust && opts.deviceTrustOptIn`.
|
|
343
|
+
*
|
|
344
|
+
* The leading `transportHint` paragraph reads `mfaMethod` + `pinSentTo` (the
|
|
345
|
+
* masked recipient set by the pincode-send step) out of the workflow context
|
|
346
|
+
* so the operator can see which factor the workflow is currently verifying.
|
|
347
|
+
* Requires `installDynamicResolver()` from `@atscript/ui-fns` on the consumer
|
|
348
|
+
* side; without it `@ui.form.fn.value` stays inert and the paragraph renders empty.
|
|
178
349
|
*/
|
|
350
|
+
@wf.context.pass 'mfaMethod'
|
|
351
|
+
@wf.context.pass 'pinSentTo'
|
|
352
|
+
@wf.context.pass 'mfaMethodCount'
|
|
353
|
+
@wf.context.pass 'mfaBackupCodes'
|
|
354
|
+
@wf.context.pass 'deviceTrustOptIn'
|
|
355
|
+
@wf.context.pass 'recoveryTransportCount'
|
|
356
|
+
@ui.form.submit.text 'Verify'
|
|
179
357
|
export interface PincodeForm {
|
|
358
|
+
@ui.form.fn.value '(_, _d, ctx) => ctx.mfaMethod === "totp" ? "Enter the current 6-digit code from your authenticator app." : ctx.mfaMethod ? "Code sent to " + (ctx.pinSentTo || "your " + ctx.mfaMethod) + " — check the dev server console for the code." : "Enter your verification code."'
|
|
359
|
+
transportHint?: ui.paragraph
|
|
360
|
+
|
|
180
361
|
@ui.form.type 'text'
|
|
181
362
|
@meta.label 'Verification code'
|
|
182
363
|
@ui.form.autocomplete 'one-time-code'
|
|
@@ -188,13 +369,37 @@ export interface PincodeForm {
|
|
|
188
369
|
|
|
189
370
|
@ui.form.type 'checkbox'
|
|
190
371
|
@meta.label 'Remember this device'
|
|
191
|
-
|
|
372
|
+
@meta.default 'false'
|
|
373
|
+
@ui.form.fn.hidden '(_, _d, ctx) => !ctx.deviceTrustOptIn'
|
|
374
|
+
rememberDevice: boolean
|
|
375
|
+
|
|
376
|
+
@ui.form.action 'resend', 'Resend code'
|
|
377
|
+
resend?: ui.action
|
|
378
|
+
|
|
379
|
+
@ui.form.action 'useDifferentMethod', 'Use a different method'
|
|
380
|
+
@ui.form.fn.hidden '(_, _d, ctx) => (ctx.mfaMethodCount ?? 0) < 2'
|
|
381
|
+
useDifferentMethod?: ui.action
|
|
382
|
+
|
|
383
|
+
@ui.form.action 'useBackupCode', 'Use backup code'
|
|
384
|
+
@ui.form.fn.hidden '(_, _d, ctx) => !ctx.mfaBackupCodes'
|
|
385
|
+
useBackupCode?: ui.action
|
|
386
|
+
|
|
387
|
+
@ui.form.action 'backToLogin', 'Back to sign-in'
|
|
388
|
+
backToLogin?: ui.action
|
|
389
|
+
|
|
390
|
+
@ui.form.action 'useDifferentTransport', 'Use a different transport'
|
|
391
|
+
@ui.form.fn.hidden '(_, _d, ctx) => (ctx.recoveryTransportCount ?? 0) < 2'
|
|
392
|
+
useDifferentTransport?: ui.action
|
|
192
393
|
}
|
|
193
394
|
|
|
194
395
|
/**
|
|
195
|
-
* Email-only form for the `
|
|
396
|
+
* Email-only form for the `ask/email` enrollment step.
|
|
196
397
|
*/
|
|
197
|
-
|
|
398
|
+
@wf.context.pass 'pendingConsents'
|
|
399
|
+
@wf.context.pass 'consentsPersisted'
|
|
400
|
+
@wf.context.pass 'otpDisclosure'
|
|
401
|
+
export interface AskEmailForm extends WithInlineConsentForm {
|
|
402
|
+
@ui.form.order 10
|
|
198
403
|
@ui.form.type 'text'
|
|
199
404
|
@meta.label 'Email'
|
|
200
405
|
@ui.form.autocomplete 'email'
|
|
@@ -203,10 +408,14 @@ export interface AskEmailForm {
|
|
|
203
408
|
}
|
|
204
409
|
|
|
205
410
|
/**
|
|
206
|
-
* Phone-only form for the `
|
|
411
|
+
* Phone-only form for the `ask/phone` enrollment step. Free-form text —
|
|
207
412
|
* E.164 normalization happens server-side.
|
|
208
413
|
*/
|
|
209
|
-
|
|
414
|
+
@wf.context.pass 'pendingConsents'
|
|
415
|
+
@wf.context.pass 'consentsPersisted'
|
|
416
|
+
@wf.context.pass 'otpDisclosure'
|
|
417
|
+
export interface AskPhoneForm extends WithInlineConsentForm {
|
|
418
|
+
@ui.form.order 10
|
|
210
419
|
@ui.form.type 'text'
|
|
211
420
|
@meta.label 'Phone (E.164)'
|
|
212
421
|
@ui.form.autocomplete 'tel'
|
|
@@ -215,48 +424,131 @@ export interface AskPhoneForm {
|
|
|
215
424
|
}
|
|
216
425
|
|
|
217
426
|
/**
|
|
218
|
-
*
|
|
427
|
+
* Forced MFA enrollment — method picker for `mfa-enroll-required`. Options
|
|
428
|
+
* come from `ctx.enrollAvailableTransports` so only consumer-enabled
|
|
429
|
+
* transports appear.
|
|
219
430
|
*/
|
|
220
|
-
|
|
431
|
+
@wf.context.pass 'enrollAvailableTransports'
|
|
432
|
+
@wf.context.pass 'enrollMode'
|
|
433
|
+
export interface EnrollPickMethodForm {
|
|
434
|
+
@ui.form.type 'radio'
|
|
435
|
+
@ui.form.fn.options '(_, _d, ctx) => Array.isArray(ctx.enrollAvailableTransports) ? ctx.enrollAvailableTransports.map(t => ({ key: t, label: t === "totp" ? "Authenticator app (TOTP)" : t === "sms" ? "SMS" : t === "email" ? "Email" : t })) : []'
|
|
436
|
+
@meta.label 'Choose a verification method'
|
|
437
|
+
@meta.required
|
|
438
|
+
method: string
|
|
439
|
+
|
|
440
|
+
@ui.form.action 'skip', 'Skip for now'
|
|
441
|
+
@ui.form.fn.hidden '(_, _d, ctx) => ctx.enrollMode !== "optional"'
|
|
442
|
+
skip?: ui.action
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Forced MFA enrollment — address collection for sms/email. TOTP skips this
|
|
447
|
+
* form (secret is provisioned server-side).
|
|
448
|
+
*
|
|
449
|
+
* `skip` is hidden unless `enrollMode === 'optional'` (`'required'` mode
|
|
450
|
+
* forbids backing out mid-flow). `useDifferentMethod` is hidden when the
|
|
451
|
+
* consumer has only one transport configured (nothing to switch to).
|
|
452
|
+
*/
|
|
453
|
+
@wf.context.pass 'enrollMethod'
|
|
454
|
+
@wf.context.pass 'enrollMode'
|
|
455
|
+
@wf.context.pass 'enrollAvailableTransports'
|
|
456
|
+
export interface EnrollAddressForm {
|
|
221
457
|
@ui.form.type 'text'
|
|
222
|
-
@meta.label '
|
|
458
|
+
@meta.label 'Address'
|
|
223
459
|
@meta.required
|
|
224
|
-
|
|
460
|
+
address: string
|
|
225
461
|
|
|
226
|
-
@ui.form.
|
|
227
|
-
@
|
|
462
|
+
@ui.form.action 'skip', 'Skip for now'
|
|
463
|
+
@ui.form.fn.hidden '(_, _d, ctx) => ctx.enrollMode !== "optional"'
|
|
464
|
+
skip?: ui.action
|
|
465
|
+
|
|
466
|
+
@ui.form.action 'useDifferentMethod', 'Use a different method'
|
|
467
|
+
@ui.form.fn.hidden '(_, _d, ctx) => (ctx.enrollAvailableTransports?.length ?? 0) < 2'
|
|
468
|
+
useDifferentMethod?: ui.action
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Forced MFA enrollment — confirm code, shared by all three transports. The
|
|
473
|
+
* leading paragraph swaps between "scan this QR" (totp) and "code sent to …"
|
|
474
|
+
* (sms/email) based on `enrollMethod`; `enrollSecret` / `enrollUri` are passed
|
|
475
|
+
* for the totp QR + manual-entry fallback.
|
|
476
|
+
*/
|
|
477
|
+
@wf.context.pass 'enrollMethod'
|
|
478
|
+
@wf.context.pass 'enrollMode'
|
|
479
|
+
@wf.context.pass 'enrollSecret'
|
|
480
|
+
@wf.context.pass 'enrollUri'
|
|
481
|
+
@wf.context.pass 'enrollAvailableTransports'
|
|
482
|
+
@wf.context.pass 'pinSentTo'
|
|
483
|
+
@ui.form.submit.text 'Confirm'
|
|
484
|
+
export interface EnrollConfirmForm {
|
|
485
|
+
@ui.form.fn.value '(_, _d, ctx) => ctx.enrollMethod === "totp" ? "Scan the QR with your authenticator app, or enter the secret manually. Then type the 6-digit code it generates." : ctx.enrollMethod ? "Code sent to " + (ctx.pinSentTo || "your " + ctx.enrollMethod) + ". Enter it below to confirm." : "Enter the code to confirm enrollment."'
|
|
486
|
+
transportHint?: ui.paragraph
|
|
487
|
+
|
|
488
|
+
@ui.form.type 'text'
|
|
489
|
+
@meta.label 'Code'
|
|
490
|
+
@ui.form.autocomplete 'one-time-code'
|
|
228
491
|
@meta.required
|
|
229
|
-
|
|
492
|
+
@expect.minLength 4
|
|
493
|
+
@expect.maxLength 12
|
|
494
|
+
@expect.pattern '^[0-9]+$'
|
|
495
|
+
code: string
|
|
496
|
+
|
|
497
|
+
@ui.form.action 'resend', 'Resend code'
|
|
498
|
+
@ui.form.fn.hidden '(_, _d, ctx) => ctx.enrollMethod === "totp"'
|
|
499
|
+
resend?: ui.action
|
|
500
|
+
|
|
501
|
+
@ui.form.action 'useDifferentMethod', 'Use a different method'
|
|
502
|
+
@ui.form.fn.hidden '(_, _d, ctx) => (ctx.enrollAvailableTransports?.length ?? 0) < 2'
|
|
503
|
+
useDifferentMethod?: ui.action
|
|
504
|
+
|
|
505
|
+
@ui.form.action 'skip', 'Skip for now'
|
|
506
|
+
@ui.form.fn.hidden '(_, _d, ctx) => ctx.enrollMode !== "optional"'
|
|
507
|
+
skip?: ui.action
|
|
230
508
|
}
|
|
231
509
|
|
|
232
510
|
/**
|
|
233
511
|
* Default minimal profile completion form. Consumers replace via
|
|
234
512
|
* `LoginWorkflowOptions.profileCompleteForm` for richer shapes.
|
|
235
513
|
*/
|
|
236
|
-
|
|
514
|
+
@wf.context.pass 'pendingConsents'
|
|
515
|
+
@wf.context.pass 'consentsPersisted'
|
|
516
|
+
export interface ProfileCompleteForm extends WithInlineConsentForm {
|
|
517
|
+
@ui.form.order 10
|
|
237
518
|
@ui.form.type 'text'
|
|
238
519
|
@meta.label 'First name'
|
|
239
520
|
firstName?: string
|
|
240
521
|
|
|
522
|
+
@ui.form.order 20
|
|
241
523
|
@ui.form.type 'text'
|
|
242
524
|
@meta.label 'Last name'
|
|
243
525
|
lastName?: string
|
|
244
526
|
}
|
|
245
527
|
|
|
246
528
|
/**
|
|
247
|
-
*
|
|
529
|
+
* Standalone consent-bump prompt. Fires for returning users with pending
|
|
530
|
+
* consents (set by `prepare-consents` from `ConsentStore.getPendingConsents`)
|
|
531
|
+
* who did NOT pass through any onboarding carrier form (`AskEmailForm` /
|
|
532
|
+
* `AskPhoneForm` / `SetPasswordForm` / `ProfileCompleteForm`) on this login —
|
|
533
|
+
* those carrier forms collect consents inline via `WithInlineConsentForm`'s
|
|
534
|
+
* inherited `AsConsentArray` field. The bump-prompt only renders the same
|
|
535
|
+
* inherited consent block (no additional fields).
|
|
248
536
|
*/
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
optIn?: boolean
|
|
537
|
+
@wf.context.pass 'pendingConsents'
|
|
538
|
+
@wf.context.pass 'consentsPersisted'
|
|
539
|
+
export interface TermsBumpForm extends WithInlineConsentForm {
|
|
253
540
|
}
|
|
254
541
|
|
|
255
542
|
/**
|
|
256
543
|
* Tenant picker — `tenantId` matches one of `ctx.availableTenants[].id`.
|
|
544
|
+
* Options are built from `ctx.availableTenants` (set by the workflow's
|
|
545
|
+
* `tenant-select` step / `loadTenants` hook); `@wf.context.pass` whitelists
|
|
546
|
+
* the key so it survives `extractPassContext`.
|
|
257
547
|
*/
|
|
548
|
+
@wf.context.pass 'availableTenants'
|
|
258
549
|
export interface TenantSelectForm {
|
|
259
|
-
@ui.form.type '
|
|
550
|
+
@ui.form.type 'radio'
|
|
551
|
+
@ui.form.fn.options '(_, _d, ctx) => Array.isArray(ctx.availableTenants) ? ctx.availableTenants.map(t => ({ key: t.id, label: t.name })) : []'
|
|
260
552
|
@meta.label 'Tenant'
|
|
261
553
|
@meta.required
|
|
262
554
|
tenantId: string
|
|
@@ -264,9 +556,14 @@ export interface TenantSelectForm {
|
|
|
264
556
|
|
|
265
557
|
/**
|
|
266
558
|
* Persona picker — `personaId` matches one of `ctx.availablePersonas[].id`.
|
|
559
|
+
* Options are built from `ctx.availablePersonas` (set by the workflow's
|
|
560
|
+
* `persona-select` step / `loadPersonas` hook); `@wf.context.pass` whitelists
|
|
561
|
+
* the key so it survives `extractPassContext`.
|
|
267
562
|
*/
|
|
563
|
+
@wf.context.pass 'availablePersonas'
|
|
268
564
|
export interface PersonaSelectForm {
|
|
269
|
-
@ui.form.type '
|
|
565
|
+
@ui.form.type 'radio'
|
|
566
|
+
@ui.form.fn.options '(_, _d, ctx) => Array.isArray(ctx.availablePersonas) ? ctx.availablePersonas.map(p => ({ key: p.id, label: p.label })) : []'
|
|
270
567
|
@meta.label 'Persona'
|
|
271
568
|
@meta.required
|
|
272
569
|
personaId: string
|
|
@@ -282,6 +579,12 @@ export interface ConcurrencyLimitForm {
|
|
|
282
579
|
@meta.required
|
|
283
580
|
@expect.pattern '^(logoutOthers|cancel)$'
|
|
284
581
|
action: string
|
|
582
|
+
|
|
583
|
+
@ui.form.action 'cancel', 'Cancel'
|
|
584
|
+
cancel?: ui.action
|
|
585
|
+
|
|
586
|
+
@ui.form.action 'logoutOthers', 'Log out other sessions'
|
|
587
|
+
logoutOthers?: ui.action
|
|
285
588
|
}
|
|
286
589
|
|
|
287
590
|
/**
|
|
@@ -302,24 +605,32 @@ export interface MagicLinkRequestForm {
|
|
|
302
605
|
* `RecoveryWorkflowOptions.deliveryMode === 'choice'`.
|
|
303
606
|
*/
|
|
304
607
|
export interface RecoveryModeSelectForm {
|
|
305
|
-
@ui.form.type '
|
|
608
|
+
@ui.form.type 'radio'
|
|
609
|
+
@ui.form.options 'Magic link', 'magicLink'
|
|
610
|
+
@ui.form.options 'One-time code', 'otp'
|
|
306
611
|
@meta.label 'Recovery method'
|
|
307
612
|
@meta.required
|
|
308
|
-
@expect.pattern '^(magicLink|otp)$'
|
|
309
613
|
mode: string
|
|
614
|
+
|
|
615
|
+
@ui.form.action 'backToLogin', 'Back to sign-in'
|
|
616
|
+
backToLogin?: ui.action
|
|
310
617
|
}
|
|
311
618
|
|
|
312
619
|
/**
|
|
313
620
|
* Recovery factor-verification form — used when
|
|
314
621
|
* `RecoveryWorkflowOptions.requireKnownRecoveryFactor` is true. The user
|
|
315
622
|
* picks a factor type and supplies its value; the server validates against
|
|
316
|
-
* the enrolled factor (phone last-4 or current TOTP code).
|
|
623
|
+
* the enrolled factor (phone last-4 or current TOTP code). Options are
|
|
624
|
+
* built from `ctx.availableRecoveryFactors` (workflow whitelist ∩ user's
|
|
625
|
+
* enrolled factors), so users only see factors they can actually verify
|
|
626
|
+
* AND that the admin hasn't disabled via `opts.preReset.allowedFactors`.
|
|
317
627
|
*/
|
|
628
|
+
@wf.context.pass 'availableRecoveryFactors'
|
|
318
629
|
export interface RecoveryFactorForm {
|
|
319
|
-
@ui.form.type '
|
|
630
|
+
@ui.form.type 'radio'
|
|
631
|
+
@ui.form.fn.options '(_, _d, ctx) => Array.isArray(ctx.availableRecoveryFactors) ? ctx.availableRecoveryFactors : []'
|
|
320
632
|
@meta.label 'Factor'
|
|
321
633
|
@meta.required
|
|
322
|
-
@expect.pattern '^(phone|totp)$'
|
|
323
634
|
factor: string
|
|
324
635
|
|
|
325
636
|
@ui.form.type 'text'
|
|
@@ -328,4 +639,7 @@ export interface RecoveryFactorForm {
|
|
|
328
639
|
@expect.minLength 4
|
|
329
640
|
@expect.maxLength 12
|
|
330
641
|
value: string
|
|
642
|
+
|
|
643
|
+
@ui.form.action 'backToLogin', 'Back to sign-in'
|
|
644
|
+
backToLogin?: ui.action
|
|
331
645
|
}
|