@eifi1/ui-kit 0.29.0 → 0.31.0
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/README.md +65 -6
- package/dist/account/account-parts.d.ts +21 -0
- package/dist/account/account-parts.js +40 -0
- package/dist/account/account-parts.js.map +1 -0
- package/dist/account/data-export-setting.d.ts +73 -0
- package/dist/account/data-export-setting.js +114 -0
- package/dist/account/data-export-setting.js.map +1 -0
- package/dist/account/delete-account-setting.d.ts +95 -0
- package/dist/account/delete-account-setting.js +200 -0
- package/dist/account/delete-account-setting.js.map +1 -0
- package/dist/account/email-change-setting.d.ts +136 -0
- package/dist/account/email-change-setting.js +278 -0
- package/dist/account/email-change-setting.js.map +1 -0
- package/dist/account/sessions-setting.d.ts +110 -0
- package/dist/account/sessions-setting.js +147 -0
- package/dist/account/sessions-setting.js.map +1 -0
- package/dist/admin/admin-action-confirm.d.ts +95 -0
- package/dist/admin/admin-action-confirm.js +276 -0
- package/dist/admin/admin-action-confirm.js.map +1 -0
- package/dist/admin/admin-action-log.d.ts +105 -0
- package/dist/admin/admin-action-log.js +153 -0
- package/dist/admin/admin-action-log.js.map +1 -0
- package/dist/admin/admin-parts.d.ts +40 -0
- package/dist/admin/admin-parts.js +45 -0
- package/dist/admin/admin-parts.js.map +1 -0
- package/dist/admin/invitations-panel.d.ts +95 -0
- package/dist/admin/invitations-panel.js +290 -0
- package/dist/admin/invitations-panel.js.map +1 -0
- package/dist/admin/reviewer-scope-editor.d.ts +95 -0
- package/dist/admin/reviewer-scope-editor.js +134 -0
- package/dist/admin/reviewer-scope-editor.js.map +1 -0
- package/dist/admin/role-select.d.ts +95 -0
- package/dist/admin/role-select.js +128 -0
- package/dist/admin/role-select.js.map +1 -0
- package/dist/admin/transfer-ownership-dialog.d.ts +95 -0
- package/dist/admin/transfer-ownership-dialog.js +83 -0
- package/dist/admin/transfer-ownership-dialog.js.map +1 -0
- package/dist/admin/user-roster.d.ts +95 -0
- package/dist/admin/user-roster.js +285 -0
- package/dist/admin/user-roster.js.map +1 -0
- package/dist/auth/auth-errors.d.ts +103 -6
- package/dist/auth/auth-errors.js +68 -2
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/forgot-password-form.d.ts +10 -3
- package/dist/auth/forgot-password-form.js +6 -3
- package/dist/auth/forgot-password-form.js.map +1 -1
- package/dist/auth/register-form.d.ts +21 -12
- package/dist/auth/register-form.js +6 -4
- package/dist/auth/register-form.js.map +1 -1
- package/dist/auth/reset-password-form.d.ts +10 -3
- package/dist/auth/reset-password-form.js +6 -3
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/sign-in-form.d.ts +75 -9
- package/dist/auth/sign-in-form.js +105 -15
- package/dist/auth/sign-in-form.js.map +1 -1
- package/dist/auth/verify-email.d.ts +7 -2
- package/dist/auth/verify-email.js +44 -8
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/chart.d.ts +20 -11
- package/dist/components/account-chips.d.ts +47 -7
- package/dist/components/account-chips.js +19 -3
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/account-settings-labels.d.ts +24 -1
- package/dist/components/account-settings-labels.js +6 -2
- package/dist/components/account-settings-labels.js.map +1 -1
- package/dist/components/account-settings.d.ts +69 -8
- package/dist/components/account-settings.js +133 -25
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +21 -12
- package/dist/components/button-group.d.ts +21 -12
- package/dist/components/calculator.d.ts +21 -12
- package/dist/components/confirm-dialog.d.ts +21 -12
- package/dist/components/copy-button.d.ts +18 -9
- package/dist/components/danger-confirm.d.ts +21 -12
- package/dist/components/data-table.js +8 -1
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +9 -4
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/facing-pair.d.ts +21 -12
- package/dist/components/file-button.d.ts +21 -12
- package/dist/components/file-dropzone.d.ts +21 -12
- package/dist/components/form-actions.d.ts +21 -12
- package/dist/components/iban-input.d.ts +21 -12
- package/dist/components/language-select.d.ts +21 -12
- package/dist/components/legal-page.d.ts +21 -12
- package/dist/components/legal.d.ts +21 -12
- package/dist/components/money-field.d.ts +21 -12
- package/dist/components/number-field.d.ts +21 -12
- package/dist/components/number-input.d.ts +21 -12
- package/dist/components/numpad-sheet.d.ts +21 -12
- package/dist/components/passkeys-setting.js +2 -1
- package/dist/components/passkeys-setting.js.map +1 -1
- package/dist/components/phone-input.d.ts +21 -12
- package/dist/components/series-chart.d.ts +21 -12
- package/dist/components/settings-fields.d.ts +55 -13
- package/dist/components/settings-fields.js +20 -1
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/share-card.d.ts +20 -11
- package/dist/components/text-link.d.ts +21 -12
- package/dist/components/time-input.d.ts +21 -12
- package/dist/components/ui.d.ts +17 -8
- package/dist/{register-form-V1OeNlMr.d.ts → delete-account-setting-DCFS5Vof.d.ts} +6612 -5612
- package/dist/demo/demo-banner.d.ts +51 -0
- package/dist/demo/demo-banner.js +97 -0
- package/dist/demo/demo-banner.js.map +1 -0
- package/dist/demo/demo-countdown.d.ts +58 -0
- package/dist/demo/demo-countdown.js +79 -0
- package/dist/demo/demo-countdown.js.map +1 -0
- package/dist/demo/demo-ended.d.ts +45 -0
- package/dist/demo/demo-ended.js +42 -0
- package/dist/demo/demo-ended.js.map +1 -0
- package/dist/demo/demo-labels.d.ts +53 -0
- package/dist/demo/demo-labels.js +29 -0
- package/dist/demo/demo-labels.js.map +1 -0
- package/dist/demo/demo-session.d.ts +30 -0
- package/dist/demo/demo-session.js +8 -0
- package/dist/demo/demo-session.js.map +1 -0
- package/dist/demo/demo-start.d.ts +54 -0
- package/dist/demo/demo-start.js +68 -0
- package/dist/demo/demo-start.js.map +1 -0
- package/dist/feedback/feedback-attachment.d.ts +21 -12
- package/dist/feedback/feedback-context.d.ts +21 -12
- package/dist/feedback/feedback-crash.d.ts +21 -12
- package/dist/feedback/feedback-dialog.d.ts +21 -12
- package/dist/feedback/feedback-inbox.d.ts +21 -12
- package/dist/feedback/feedback-inbox.js +12 -2
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-labels.d.ts +21 -12
- package/dist/feedback/feedback-labels.js +1 -0
- package/dist/feedback/feedback-labels.js.map +1 -1
- package/dist/feedback/feedback-menu.d.ts +21 -12
- package/dist/feedback/feedback-record.d.ts +21 -12
- package/dist/feedback/feedback-record.js +2 -0
- package/dist/feedback/feedback-record.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +21 -12
- package/dist/feedback/feedback-row-detail.js +7 -0
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.d.ts +21 -12
- package/dist/feedback/feedback-submit.d.ts +21 -12
- package/dist/feedback/feedback-swipe.d.ts +22 -13
- package/dist/feedback/feedback-swipe.js.map +1 -1
- package/dist/feedback/feedback-table.d.ts +18 -9
- package/dist/feedback/feedback-thread.d.ts +21 -12
- package/dist/feedback.d.ts +21 -12
- package/dist/hooks/use-anchored-panel.d.ts +6 -2
- package/dist/hooks/use-anchored-panel.js +2 -1
- package/dist/hooks/use-anchored-panel.js.map +1 -1
- package/dist/hooks/use-file-drop.d.ts +21 -12
- package/dist/i18n/defaults.d.ts +21 -12
- package/dist/i18n/defaults.js +28 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +21 -12
- package/dist/i18n/german.js +298 -12
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +14 -5
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +15 -6
- package/dist/i18n/locales/de-CH.d.ts +21 -12
- package/dist/i18n/locales/en.d.ts +21 -12
- package/dist/i18n/locales/es.d.ts +21 -12
- package/dist/i18n/locales/es.js +287 -10
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +21 -12
- package/dist/i18n/locales/fr.js +290 -7
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +21 -12
- package/dist/i18n/locales/hu.js +288 -12
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +21 -12
- package/dist/i18n/locales/it.js +289 -7
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +21 -12
- package/dist/i18n/locales/zh.js +279 -7
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +21 -12
- package/dist/i18n/review.js +50 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +31 -4
- package/dist/index.js +148 -0
- package/dist/index.js.map +1 -1
- package/dist/landing/access.d.ts +48 -0
- package/dist/landing/access.js +38 -0
- package/dist/landing/access.js.map +1 -0
- package/dist/landing/landing-actions.d.ts +138 -0
- package/dist/landing/landing-actions.js +39 -0
- package/dist/landing/landing-actions.js.map +1 -0
- package/dist/landing/landing-labels.d.ts +43 -0
- package/dist/landing/landing-labels.js +23 -0
- package/dist/landing/landing-labels.js.map +1 -0
- package/dist/landing/landing-sections.d.ts +218 -0
- package/dist/landing/landing-sections.js +167 -0
- package/dist/landing/landing-sections.js.map +1 -0
- package/dist/landing/page-seo.d.ts +99 -0
- package/dist/landing/page-seo.js +121 -0
- package/dist/landing/page-seo.js.map +1 -0
- package/dist/landing/public-header.d.ts +148 -0
- package/dist/landing/public-header.js +49 -0
- package/dist/landing/public-header.js.map +1 -0
- package/dist/landing/routing.d.ts +218 -0
- package/dist/landing/routing.js +81 -0
- package/dist/landing/routing.js.map +1 -0
- package/dist/rhf/fields.d.ts +21 -12
- package/dist/rhf/form.d.ts +21 -12
- package/dist/rhf.d.ts +21 -12
- package/dist/settings/settings-catalogue.d.ts +84 -0
- package/dist/settings/settings-catalogue.js +47 -0
- package/dist/settings/settings-catalogue.js.map +1 -0
- package/dist/settings/settings-context.d.ts +31 -0
- package/dist/settings/settings-context.js +11 -0
- package/dist/settings/settings-context.js.map +1 -0
- package/dist/settings/settings-heading.d.ts +50 -0
- package/dist/settings/settings-heading.js +27 -0
- package/dist/settings/settings-heading.js.map +1 -0
- package/dist/settings/settings-labels.d.ts +41 -0
- package/dist/settings/settings-labels.js +38 -0
- package/dist/settings/settings-labels.js.map +1 -0
- package/dist/settings/settings-layout.d.ts +77 -0
- package/dist/settings/settings-layout.js +242 -0
- package/dist/settings/settings-layout.js.map +1 -0
- package/dist/settings/settings-section.d.ts +133 -0
- package/dist/settings/settings-section.js +63 -0
- package/dist/settings/settings-section.js.map +1 -0
- package/dist/settings/use-account-language.d.ts +83 -0
- package/dist/settings/use-account-language.js +62 -0
- package/dist/settings/use-account-language.js.map +1 -0
- package/dist/settings/use-settings-focus.d.ts +34 -0
- package/dist/settings/use-settings-focus.js +77 -0
- package/dist/settings/use-settings-focus.js.map +1 -0
- package/dist/settings/use-settings-route.d.ts +118 -0
- package/dist/settings/use-settings-route.js +120 -0
- package/dist/settings/use-settings-route.js.map +1 -0
- package/dist/shell/app-shell.d.ts +27 -12
- package/dist/shell/app-shell.js +3 -2
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/top-bar-brand.d.ts +21 -12
- package/dist/shell.d.ts +25 -6
- package/dist/shell.js +56 -0
- package/dist/shell.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +21 -12
- package/dist/wizard.d.ts +21 -12
- package/package.json +2 -1
- package/src/account/account-parts.ts +67 -0
- package/src/account/data-export-setting.tsx +219 -0
- package/src/account/delete-account-setting.tsx +363 -0
- package/src/account/email-change-setting.tsx +454 -0
- package/src/account/sessions-setting.tsx +284 -0
- package/src/admin/admin-action-confirm.tsx +495 -0
- package/src/admin/admin-action-log.tsx +297 -0
- package/src/admin/admin-parts.ts +82 -0
- package/src/admin/invitations-panel.tsx +523 -0
- package/src/admin/reviewer-scope-editor.tsx +234 -0
- package/src/admin/role-select.tsx +286 -0
- package/src/admin/transfer-ownership-dialog.tsx +172 -0
- package/src/admin/user-roster.tsx +573 -0
- package/src/auth/auth-errors.ts +196 -8
- package/src/auth/forgot-password-form.tsx +21 -5
- package/src/auth/register-form.tsx +24 -6
- package/src/auth/reset-password-form.tsx +19 -5
- package/src/auth/sign-in-form.tsx +241 -38
- package/src/auth/verify-email.tsx +59 -8
- package/src/components/account-chips.tsx +64 -7
- package/src/components/account-settings-labels.ts +28 -1
- package/src/components/account-settings.tsx +231 -34
- package/src/components/data-table.tsx +8 -0
- package/src/components/date-picker.tsx +9 -4
- package/src/components/passkeys-setting.tsx +2 -1
- package/src/components/settings-fields.tsx +50 -1
- package/src/demo/demo-banner.tsx +159 -0
- package/src/demo/demo-countdown.ts +141 -0
- package/src/demo/demo-ended.tsx +89 -0
- package/src/demo/demo-labels.ts +78 -0
- package/src/demo/demo-session.ts +32 -0
- package/src/demo/demo-start.tsx +150 -0
- package/src/feedback/feedback-inbox.tsx +21 -8
- package/src/feedback/feedback-labels.ts +2 -1
- package/src/feedback/feedback-record.ts +10 -7
- package/src/feedback/feedback-row-detail.tsx +28 -4
- package/src/feedback/feedback-swipe.tsx +1 -1
- package/src/hooks/use-anchored-panel.ts +14 -3
- package/src/i18n/defaults.ts +28 -0
- package/src/i18n/german.ts +327 -5
- package/src/i18n/kit-labels.tsx +42 -0
- package/src/i18n/locales/es.ts +313 -3
- package/src/i18n/locales/fr.ts +316 -0
- package/src/i18n/locales/hu.ts +308 -5
- package/src/i18n/locales/it.ts +313 -0
- package/src/i18n/locales/zh.ts +286 -0
- package/src/i18n/review.ts +49 -0
- package/src/index.ts +184 -0
- package/src/landing/access.ts +97 -0
- package/src/landing/landing-actions.tsx +92 -0
- package/src/landing/landing-labels.ts +59 -0
- package/src/landing/landing-sections.tsx +337 -0
- package/src/landing/page-seo.ts +251 -0
- package/src/landing/public-header.tsx +120 -0
- package/src/landing/routing.tsx +209 -0
- package/src/settings/settings-catalogue.ts +132 -0
- package/src/settings/settings-context.ts +33 -0
- package/src/settings/settings-heading.tsx +65 -0
- package/src/settings/settings-labels.ts +75 -0
- package/src/settings/settings-layout.tsx +379 -0
- package/src/settings/settings-section.tsx +108 -0
- package/src/settings/use-account-language.ts +146 -0
- package/src/settings/use-settings-focus.ts +120 -0
- package/src/settings/use-settings-route.ts +258 -0
- package/src/shell/app-shell.tsx +10 -2
- package/src/shell.ts +45 -0
package/src/auth/auth-errors.ts
CHANGED
|
@@ -30,16 +30,88 @@ export type AuthErrorCode =
|
|
|
30
30
|
| "invitation_invalid"
|
|
31
31
|
| "invitation_expired"
|
|
32
32
|
| "token_invalid"
|
|
33
|
+
/** 0.29.1: a link that WAS valid and ran out (verification, reset) — keksdose's step-3
|
|
34
|
+
* refusals tell it from `token_invalid`, so a page can say "expired, ask for a new one". */
|
|
35
|
+
| "token_expired"
|
|
33
36
|
| "account_inactive";
|
|
34
37
|
|
|
35
|
-
|
|
38
|
+
/**
|
|
39
|
+
* 0.30.0: the coded refusals of user administration and the account's own settings
|
|
40
|
+
* (docs/user-admin-harmonization.md §4, §6) — server-kit 0.4.0's `AccountErrorCode`,
|
|
41
|
+
* value for value. The same family of answer as the sign-in codes (`{detail, code}`,
|
|
42
|
+
* plus `companies` on kastlan's `last_admin`), read by the same {@link authErrorCode}.
|
|
43
|
+
*
|
|
44
|
+
* - `last_admin` — the action would leave no active admin: deactivating, demoting or
|
|
45
|
+
* deleting the last one (in kastlan, of a company; the answer names the `companies`).
|
|
46
|
+
* - `self_action` — an admin acting on their own account where that is never allowed.
|
|
47
|
+
* - `other_companies` — kastlan: the account belongs to other companies too, so a
|
|
48
|
+
* company admin may not deactivate it; "Remove from company" instead (§9.6).
|
|
49
|
+
* - `household_has_members` — keksdose's guard on a deletion request (§6.4).
|
|
50
|
+
* - `confirmation_required`, `confirmation_mismatch` — the action's confirmation level
|
|
51
|
+
* asked for a typed address (or a tick), and the request carried none, or another
|
|
52
|
+
* address (§4.2).
|
|
53
|
+
* - `password_incorrect` — the CURRENT password is wrong on a signed-in route (the email
|
|
54
|
+
* change, the deletion request). `400`, not `401`: an app's client reads a 401 as an
|
|
55
|
+
* ended session and signs the user out, the wrong answer to a typo. Every other one
|
|
56
|
+
* is a `409`.
|
|
57
|
+
*/
|
|
58
|
+
export type AccountErrorCode =
|
|
59
|
+
| "last_admin"
|
|
60
|
+
| "self_action"
|
|
61
|
+
| "other_companies"
|
|
62
|
+
| "household_has_members"
|
|
63
|
+
| "confirmation_required"
|
|
64
|
+
| "confirmation_mismatch"
|
|
65
|
+
| "password_incorrect";
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 0.31.0: the demo's coded refusals (docs/landing-demo-harmonization.md §6.2) —
|
|
69
|
+
* server-kit 0.5's `DemoErrorCode`, value for value, answered as `{detail, code}` like
|
|
70
|
+
* the others and read by the same {@link authErrorCode}.
|
|
71
|
+
*
|
|
72
|
+
* - `demo_disabled` — `404`: the demo switch is off (it is "not there", not forbidden).
|
|
73
|
+
* - `demo_rate_limited` — `429` with `Retry-After`: the per-IP window of demo starts.
|
|
74
|
+
* - `demo_capacity` — `429`, no `Retry-After`: the cap of live demo users.
|
|
75
|
+
* - `demo_not_ready` — `503`: the demo data has not been seeded yet.
|
|
76
|
+
* - `demo_read_only` — `403`, model R: any write by a demo user. The app's write lock
|
|
77
|
+
* already explains it, so it shows no toast (§5.4).
|
|
78
|
+
* - `demo_refused` — `403`, both models: an action a demo never may — a way in or out,
|
|
79
|
+
* mail, uploads, the account export (§6.4).
|
|
80
|
+
*/
|
|
81
|
+
export type DemoErrorCode =
|
|
82
|
+
| "demo_disabled"
|
|
83
|
+
| "demo_rate_limited"
|
|
84
|
+
| "demo_capacity"
|
|
85
|
+
| "demo_not_ready"
|
|
86
|
+
| "demo_read_only"
|
|
87
|
+
| "demo_refused";
|
|
88
|
+
|
|
89
|
+
/** Every code the kit reads: the sign-in refusals, the account ones (0.30.0) and the
|
|
90
|
+
* demo's (0.31.0). */
|
|
91
|
+
export type KitErrorCode = AuthErrorCode | AccountErrorCode | DemoErrorCode;
|
|
92
|
+
|
|
93
|
+
const CODES: ReadonlySet<string> = new Set<KitErrorCode>([
|
|
36
94
|
"invalid_credentials",
|
|
37
95
|
"registration_closed",
|
|
38
96
|
"email_taken",
|
|
39
97
|
"invitation_invalid",
|
|
40
98
|
"invitation_expired",
|
|
41
99
|
"token_invalid",
|
|
100
|
+
"token_expired",
|
|
42
101
|
"account_inactive",
|
|
102
|
+
"last_admin",
|
|
103
|
+
"self_action",
|
|
104
|
+
"other_companies",
|
|
105
|
+
"household_has_members",
|
|
106
|
+
"confirmation_required",
|
|
107
|
+
"confirmation_mismatch",
|
|
108
|
+
"password_incorrect",
|
|
109
|
+
"demo_disabled",
|
|
110
|
+
"demo_rate_limited",
|
|
111
|
+
"demo_capacity",
|
|
112
|
+
"demo_not_ready",
|
|
113
|
+
"demo_read_only",
|
|
114
|
+
"demo_refused",
|
|
43
115
|
]);
|
|
44
116
|
|
|
45
117
|
type Bag = Record<string, unknown>;
|
|
@@ -47,19 +119,21 @@ type Bag = Record<string, unknown>;
|
|
|
47
119
|
const isBag = (value: unknown): value is Bag => typeof value === "object" && value !== null;
|
|
48
120
|
|
|
49
121
|
/** A known code, or undefined. */
|
|
50
|
-
function known(value: unknown):
|
|
51
|
-
return typeof value === "string" && CODES.has(value) ? (value as
|
|
122
|
+
function known(value: unknown): KitErrorCode | undefined {
|
|
123
|
+
return typeof value === "string" && CODES.has(value) ? (value as KitErrorCode) : undefined;
|
|
52
124
|
}
|
|
53
125
|
|
|
54
126
|
/** `{code}` on a parsed body, or FastAPI's `{detail: {code}}` — what an
|
|
55
127
|
* `HTTPException(detail={"code": …})` serialises to. */
|
|
56
|
-
function codeOfBody(body: unknown):
|
|
128
|
+
function codeOfBody(body: unknown): KitErrorCode | undefined {
|
|
57
129
|
if (!isBag(body)) return undefined;
|
|
58
130
|
return known(body.code) ?? (isBag(body.detail) ? known(body.detail.code) : undefined);
|
|
59
131
|
}
|
|
60
132
|
|
|
61
133
|
/**
|
|
62
|
-
* The
|
|
134
|
+
* The code an error carries — an {@link AuthErrorCode}, since 0.30.0 an
|
|
135
|
+
* {@link AccountErrorCode}, since 0.31.0 a {@link DemoErrorCode} — or `undefined` when it
|
|
136
|
+
* carries none.
|
|
63
137
|
*
|
|
64
138
|
* Reads the parsed response body wherever the apps' HTTP clients put it, without
|
|
65
139
|
* depending on any of them:
|
|
@@ -76,7 +150,7 @@ function codeOfBody(body: unknown): AuthErrorCode | undefined {
|
|
|
76
150
|
* read as a refusal. The HTTP status is not checked: the code is the contract, and the
|
|
77
151
|
* status differs per refusal (401, 403, 409, 410).
|
|
78
152
|
*/
|
|
79
|
-
export function authErrorCode(err: unknown):
|
|
153
|
+
export function authErrorCode(err: unknown): KitErrorCode | undefined {
|
|
80
154
|
if (!isBag(err)) return undefined;
|
|
81
155
|
const response = err.response;
|
|
82
156
|
return (
|
|
@@ -97,9 +171,123 @@ export function authErrorCode(err: unknown): AuthErrorCode | undefined {
|
|
|
97
171
|
*
|
|
98
172
|
* The kit's `SignInForm` and `RegisterForm` call it on what their callbacks reject
|
|
99
173
|
* with, so an app's `onSubmit` can simply let its client's error through. See
|
|
100
|
-
* {@link authErrorCode} for the shapes it reads.
|
|
174
|
+
* {@link authErrorCode} for the shapes it reads. Since 0.30.0 it knows the account codes
|
|
175
|
+
* too: `isAuthError(err, "last_admin")`; since 0.31.0 the demo's:
|
|
176
|
+
* `isAuthError(err, "demo_read_only")`.
|
|
101
177
|
*/
|
|
102
|
-
export function isAuthError(err: unknown, code?:
|
|
178
|
+
export function isAuthError(err: unknown, code?: KitErrorCode): boolean {
|
|
103
179
|
const found = authErrorCode(err);
|
|
104
180
|
return found !== undefined && (code === undefined || found === code);
|
|
105
181
|
}
|
|
182
|
+
|
|
183
|
+
/** An HTTP status as a number, or undefined. */
|
|
184
|
+
function statusOf(value: unknown): number | undefined {
|
|
185
|
+
return typeof value === "number" && Number.isInteger(value) ? value : undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Whether `err` is a throttled request — HTTP `429 Too Many Requests` (0.30.0, from the
|
|
190
|
+
* apps' 0.29 adoption: every app answered a throttled sign-in, sign-up or reset through
|
|
191
|
+
* its own `describeError`, with three wordings of one sentence).
|
|
192
|
+
*
|
|
193
|
+
* A throttle carries no `code` in the contract — the STATUS is the answer, set by the
|
|
194
|
+
* apps' limiters (server-kit's `AuthLimiters`, keksdose's per-IP limit) — so this reads
|
|
195
|
+
* the status, wherever the apps' HTTP clients put it, without depending on any of them:
|
|
196
|
+
*
|
|
197
|
+
* 1. `err.response.status` — axios (all three apps' clients);
|
|
198
|
+
* 2. `err.status` — ofetch's `FetchError`, the generated OpenAPI clients' `ApiError`, and
|
|
199
|
+
* a `Response` thrown as it came;
|
|
200
|
+
* 3. `err.statusCode` — ofetch's alias, and Node-style errors.
|
|
201
|
+
*
|
|
202
|
+
* There is no `rate_limited` code: keksdose answers its 429s uncoded, with a
|
|
203
|
+
* `Retry-After` header, and the status is the one signal every limiter gives.
|
|
204
|
+
*
|
|
205
|
+
* The kit's `SignInForm`, `RegisterForm`, `ForgotPasswordForm` and `ResetPasswordForm`
|
|
206
|
+
* call it AFTER the app's `describeError` — an app with words of its own keeps them —
|
|
207
|
+
* and before their generic failure, so "Too many attempts. Try again in 30 s." (with
|
|
208
|
+
* {@link retryAfterSeconds}) or "… Wait a moment and try again." needs no app code.
|
|
209
|
+
*/
|
|
210
|
+
export function isRateLimited(err: unknown): boolean {
|
|
211
|
+
if (!isBag(err)) return false;
|
|
212
|
+
const response = err.response;
|
|
213
|
+
const status =
|
|
214
|
+
(isBag(response) ? statusOf(response.status) : undefined) ?? statusOf(err.status) ?? statusOf(err.statusCode);
|
|
215
|
+
return status === 429;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** A header's value from a `Headers` (fetch, ofetch's `response`), axios' `AxiosHeaders`
|
|
219
|
+
* or a plain record — whose keys may come in any case. */
|
|
220
|
+
function headerOf(headers: unknown, name: string): unknown {
|
|
221
|
+
if (!isBag(headers)) return undefined;
|
|
222
|
+
if (typeof headers.get === "function") {
|
|
223
|
+
try {
|
|
224
|
+
const value: unknown = (headers.get as (key: string) => unknown).call(headers, name);
|
|
225
|
+
if (value !== undefined && value !== null) return value;
|
|
226
|
+
} catch {
|
|
227
|
+
// A `get` that is not a header lookup; the record below is.
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
231
|
+
if (key.toLowerCase() === name) return value;
|
|
232
|
+
}
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** `Retry-After` as seconds from `now`: delta-seconds ("30"), or an HTTP date
|
|
237
|
+
* ("Wed, 07 Oct 2026 10:00:00 GMT") counted from now, never below 0 (RFC 9110 §10.2.3). */
|
|
238
|
+
function parseRetryAfter(value: unknown, now: number): number | undefined {
|
|
239
|
+
const raw = Array.isArray(value) ? (value as unknown[])[0] : value;
|
|
240
|
+
if (typeof raw === "number") return Number.isFinite(raw) && raw >= 0 ? Math.ceil(raw) : undefined;
|
|
241
|
+
if (typeof raw !== "string") return undefined;
|
|
242
|
+
const text = raw.trim();
|
|
243
|
+
if (/^\d+$/.test(text)) return Number(text);
|
|
244
|
+
// An HTTP date names its day and month ("Wed, 07 Oct 2026 …"). Without a letter it is
|
|
245
|
+
// a malformed number ("-5", "1.5"), which `Date.parse` would happily read as a year.
|
|
246
|
+
if (!/[a-z]/i.test(text)) return undefined;
|
|
247
|
+
const at = Date.parse(text);
|
|
248
|
+
if (Number.isNaN(at)) return undefined;
|
|
249
|
+
return Math.max(0, Math.ceil((at - now) / 1000));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* How long a throttled request asks to wait, in whole seconds — its `Retry-After`
|
|
254
|
+
* header (0.30.0, keksdose: its 429s carry no code, only the header) — or `undefined`
|
|
255
|
+
* when there is none, or none that parses.
|
|
256
|
+
*
|
|
257
|
+
* Read where the apps' clients keep the response headers, without depending on any of
|
|
258
|
+
* them: `err.response.headers` (axios' `AxiosHeaders` or a plain record; ofetch's
|
|
259
|
+
* `FetchError.response`, a fetch `Headers`), then `err.headers` (a `Response` thrown as
|
|
260
|
+
* it came). The value is delta-seconds, or an HTTP date turned into seconds from `now`
|
|
261
|
+
* (default: the present moment) — never below 0.
|
|
262
|
+
*
|
|
263
|
+
* The forms hand it to their `rateLimited` label when {@link isRateLimited} is true:
|
|
264
|
+
* "Too many attempts. Try again in 30 s." — and, without one, "… Wait a moment and try
|
|
265
|
+
* again." A browser only exposes `Retry-After` to a cross-origin page when the server
|
|
266
|
+
* lists it in `Access-Control-Expose-Headers`.
|
|
267
|
+
*/
|
|
268
|
+
export function retryAfterSeconds(err: unknown, now: number | Date = Date.now()): number | undefined {
|
|
269
|
+
if (!isBag(err)) return undefined;
|
|
270
|
+
const at = typeof now === "number" ? now : now.getTime();
|
|
271
|
+
const response = err.response;
|
|
272
|
+
const value =
|
|
273
|
+
(isBag(response) ? headerOf(response.headers, "retry-after") : undefined) ?? headerOf(err.headers, "retry-after");
|
|
274
|
+
return value === undefined ? undefined : parseRetryAfter(value, at);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The English of every `rateLimited` label (0.30.0): "Too many attempts. Try again in
|
|
279
|
+
* 30 s." with a wait under a minute, in whole minutes from one minute up (keksdose's
|
|
280
|
+
* reset throttle answers in hours, and "3600 s" is a number nobody reads), and "Wait a
|
|
281
|
+
* moment and try again." without one — or with a `0`.
|
|
282
|
+
*
|
|
283
|
+
* @internal The namespaces' English; not part of the barrel.
|
|
284
|
+
*/
|
|
285
|
+
export function englishRateLimited(seconds?: number): string {
|
|
286
|
+
if (!seconds || seconds <= 0) return "Too many attempts. Wait a moment and try again.";
|
|
287
|
+
return `Too many attempts. Try again in ${englishWait(seconds)}.`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** "30 s", "2 min" — a wait in English, rounded up. @internal */
|
|
291
|
+
export function englishWait(seconds: number): string {
|
|
292
|
+
return seconds < 60 ? `${Math.ceil(seconds)} s` : `${Math.ceil(seconds / 60)} min`;
|
|
293
|
+
}
|
|
@@ -6,6 +6,7 @@ import { useKitLabels } from "../i18n/kit-labels";
|
|
|
6
6
|
import { AlertBanner } from "../components/alert-banner";
|
|
7
7
|
import { TextLink } from "../components/text-link";
|
|
8
8
|
import { Button, Input } from "../components/ui";
|
|
9
|
+
import { englishRateLimited, isRateLimited, retryAfterSeconds } from "./auth-errors";
|
|
9
10
|
import { FORM_HEADING_CLASS, INTRO_CLASS, OutcomeMessage, isThenable, useFocusWhen } from "./status-parts";
|
|
10
11
|
import type { AuthHeadingLevel } from "./status-parts";
|
|
11
12
|
|
|
@@ -30,9 +31,15 @@ export interface ForgotPasswordLabels {
|
|
|
30
31
|
/** A request that did not go through: a throttle, an outage. Never "unknown
|
|
31
32
|
* address" — the endpoint does not say. */
|
|
32
33
|
error: string;
|
|
34
|
+
/** 0.30.0: the request was throttled — HTTP 429 ({@link isRateLimited}) — given the
|
|
35
|
+
* `Retry-After` wait in seconds, or `undefined` without one. OPTIONAL, like every key
|
|
36
|
+
* added to a shipped interface; the provider's `forgotPassword`, then English, fill
|
|
37
|
+
* it. */
|
|
38
|
+
rateLimited?: (seconds?: number) => string;
|
|
33
39
|
}
|
|
34
40
|
|
|
35
|
-
|
|
41
|
+
/** `Required`: every key, the 0.30 one included, has its English here. */
|
|
42
|
+
export const DEFAULT_FORGOT_PASSWORD_LABELS: Required<ForgotPasswordLabels> = {
|
|
36
43
|
title: "Forgot password",
|
|
37
44
|
intro: "Enter your account’s email address. We’ll send you a link to choose a new password.",
|
|
38
45
|
email: "Email",
|
|
@@ -41,6 +48,7 @@ export const DEFAULT_FORGOT_PASSWORD_LABELS: ForgotPasswordLabels = {
|
|
|
41
48
|
sentHint: "The link is valid for one hour and works exactly once. Check your spam folder too.",
|
|
42
49
|
backToSignIn: "Back to sign in",
|
|
43
50
|
error: "The request failed. Please try again later.",
|
|
51
|
+
rateLimited: englishRateLimited,
|
|
44
52
|
};
|
|
45
53
|
|
|
46
54
|
/* ── The form ────────────────────────────────────────────────────────────── */
|
|
@@ -61,8 +69,9 @@ export interface ForgotPasswordFormProps
|
|
|
61
69
|
* whose password just failed need not type it twice. */
|
|
62
70
|
defaultEmail?: string;
|
|
63
71
|
/**
|
|
64
|
-
* The app's words for a failed request (
|
|
65
|
-
*
|
|
72
|
+
* The app's words for a failed request ("Too many requests — try again in an hour"),
|
|
73
|
+
* or `undefined` for the kit's: `rateLimited` for a bare `429` (0.30.0), else `error`.
|
|
74
|
+
* The same hook as `SignInForm`'s.
|
|
66
75
|
*/
|
|
67
76
|
describeError?: (error: unknown) => ReactNode | undefined;
|
|
68
77
|
/** App content under the intro — a notice, a support address. */
|
|
@@ -146,6 +155,13 @@ export function ForgotPasswordForm({
|
|
|
146
155
|
);
|
|
147
156
|
}
|
|
148
157
|
|
|
158
|
+
// The app's words first, then the throttle's, then the kit's catch-all.
|
|
159
|
+
const failureFor = (error: unknown): ReactNode =>
|
|
160
|
+
describeError?.(error) ??
|
|
161
|
+
(isRateLimited(error)
|
|
162
|
+
? (labels.rateLimited ?? DEFAULT_FORGOT_PASSWORD_LABELS.rateLimited)(retryAfterSeconds(error))
|
|
163
|
+
: labels.error);
|
|
164
|
+
|
|
149
165
|
const submit = (e: FormEvent) => {
|
|
150
166
|
e.preventDefault();
|
|
151
167
|
const address = email.trim();
|
|
@@ -155,7 +171,7 @@ export function ForgotPasswordForm({
|
|
|
155
171
|
try {
|
|
156
172
|
result = onSubmit(address);
|
|
157
173
|
} catch (error) {
|
|
158
|
-
setFailure(
|
|
174
|
+
setFailure(failureFor(error));
|
|
159
175
|
return;
|
|
160
176
|
}
|
|
161
177
|
if (!isThenable(result)) {
|
|
@@ -170,7 +186,7 @@ export function ForgotPasswordForm({
|
|
|
170
186
|
},
|
|
171
187
|
(error: unknown) => {
|
|
172
188
|
setPending(false);
|
|
173
|
-
setFailure(
|
|
189
|
+
setFailure(failureFor(error));
|
|
174
190
|
},
|
|
175
191
|
);
|
|
176
192
|
};
|
|
@@ -11,7 +11,7 @@ import { LanguageSelect } from "../components/language-select";
|
|
|
11
11
|
import { LegalAcceptCheckbox } from "../components/legal-page";
|
|
12
12
|
import { TextLink } from "../components/text-link";
|
|
13
13
|
import { Button, Input } from "../components/ui";
|
|
14
|
-
import { authErrorCode } from "./auth-errors";
|
|
14
|
+
import { authErrorCode, englishRateLimited, isRateLimited, retryAfterSeconds } from "./auth-errors";
|
|
15
15
|
import { emailParts, taggedEmail } from "./email-tag";
|
|
16
16
|
import { newPasswordProblem, personNameOk, PERSON_NAME_MAX_LENGTH } from "./form-rules";
|
|
17
17
|
import { NewPasswordFields } from "./new-password-fields";
|
|
@@ -71,9 +71,15 @@ export interface RegisterLabels {
|
|
|
71
71
|
invitationExpired: string;
|
|
72
72
|
/** Any other failure. */
|
|
73
73
|
failed: string;
|
|
74
|
+
/** 0.30.0: the request was throttled — HTTP 429 ({@link isRateLimited}) — given the
|
|
75
|
+
* `Retry-After` wait in seconds, or `undefined` without one. OPTIONAL, like every key
|
|
76
|
+
* added to a shipped interface (an annotated call site must keep compiling); the
|
|
77
|
+
* provider's `register`, then English, fill it. */
|
|
78
|
+
rateLimited?: (seconds?: number) => string;
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
|
|
81
|
+
/** `Required`: every key, the 0.30 one included, has its English here. */
|
|
82
|
+
export const DEFAULT_REGISTER_LABELS: Required<RegisterLabels> = {
|
|
77
83
|
firstName: "First name",
|
|
78
84
|
lastName: "Last name",
|
|
79
85
|
email: "Email",
|
|
@@ -94,6 +100,7 @@ export const DEFAULT_REGISTER_LABELS: RegisterLabels = {
|
|
|
94
100
|
invitationInvalid: "This invitation link is not valid.",
|
|
95
101
|
invitationExpired: "This invitation has expired. Ask for a new one.",
|
|
96
102
|
failed: "Registration failed. Please try again.",
|
|
103
|
+
rateLimited: englishRateLimited,
|
|
97
104
|
};
|
|
98
105
|
|
|
99
106
|
export interface RegisterFormProps extends Omit<ComponentPropsWithoutRef<"div">, "onSubmit" | "children"> {
|
|
@@ -119,6 +126,13 @@ export interface RegisterFormProps extends Omit<ComponentPropsWithoutRef<"div">,
|
|
|
119
126
|
* it — the server accepts exactly that variant — and go back.
|
|
120
127
|
*/
|
|
121
128
|
invitedEmail?: string;
|
|
129
|
+
/**
|
|
130
|
+
* 0.29.1: an address to start with that stays EDITABLE, with the `+tag` suggestion still
|
|
131
|
+
* offered — for a link anyone can build (keksdose's token-free `/register?email=…`
|
|
132
|
+
* beta link, kept while an app moves to invitations). Unlike `invitedEmail` it proves
|
|
133
|
+
* nothing, so it locks nothing. `invitedEmail` wins when both are given.
|
|
134
|
+
*/
|
|
135
|
+
defaultEmail?: string;
|
|
122
136
|
/**
|
|
123
137
|
* The language field's value — controlled. Pass the app's UI language
|
|
124
138
|
* (`i18n.resolvedLanguage`) with `onLocaleChange` switching it, and the field IS the
|
|
@@ -158,8 +172,9 @@ export interface RegisterFormProps extends Omit<ComponentPropsWithoutRef<"div">,
|
|
|
158
172
|
privacyHref?: string;
|
|
159
173
|
/** "Already have an account? Sign in" goes here — `/login`. Left out, no line. */
|
|
160
174
|
signInHref?: string;
|
|
161
|
-
/** The app's own words for a failure the kit cannot know (
|
|
162
|
-
* `undefined` for the kit's. Asked first, for every
|
|
175
|
+
/** The app's own words for a failure the kit cannot know (the device offline, a
|
|
176
|
+
* throttle with its wait), or `undefined` for the kit's. Asked first, for every
|
|
177
|
+
* failure; a bare `429` needs none since 0.30.0 — the kit says `rateLimited`. */
|
|
163
178
|
describeError?: (error: unknown) => ReactNode | undefined;
|
|
164
179
|
labels?: Partial<RegisterLabels>;
|
|
165
180
|
}
|
|
@@ -211,6 +226,7 @@ export function RegisterForm({
|
|
|
211
226
|
onSubmit,
|
|
212
227
|
emailTag,
|
|
213
228
|
invitedEmail,
|
|
229
|
+
defaultEmail,
|
|
214
230
|
locale: localeProp,
|
|
215
231
|
defaultLocale,
|
|
216
232
|
onLocaleChange,
|
|
@@ -234,7 +250,7 @@ export function RegisterForm({
|
|
|
234
250
|
const invited = invitedEmail !== undefined;
|
|
235
251
|
const [firstName, setFirstName] = useState("");
|
|
236
252
|
const [lastName, setLastName] = useState("");
|
|
237
|
-
const [email, setEmail] = useState(invitedEmail ?? "");
|
|
253
|
+
const [email, setEmail] = useState(invitedEmail ?? defaultEmail ?? "");
|
|
238
254
|
const [password, setPassword] = useState("");
|
|
239
255
|
const [confirm, setConfirm] = useState("");
|
|
240
256
|
const [ownLocale, setOwnLocale] = useState<string>(
|
|
@@ -312,7 +328,9 @@ export function RegisterForm({
|
|
|
312
328
|
setFailure(
|
|
313
329
|
hasMessage(own)
|
|
314
330
|
? own
|
|
315
|
-
:
|
|
331
|
+
: isRateLimited(error)
|
|
332
|
+
? (labels.rateLimited ?? DEFAULT_REGISTER_LABELS.rateLimited)(retryAfterSeconds(error))
|
|
333
|
+
: code === "registration_closed"
|
|
316
334
|
? labels.registrationClosed
|
|
317
335
|
: code === "invitation_invalid"
|
|
318
336
|
? labels.invitationInvalid
|
|
@@ -6,7 +6,7 @@ import { useKitLabels } from "../i18n/kit-labels";
|
|
|
6
6
|
import { AlertBanner } from "../components/alert-banner";
|
|
7
7
|
import { LoadingState } from "../components/loading-state";
|
|
8
8
|
import { Button, EmptyState } from "../components/ui";
|
|
9
|
-
import { authErrorCode } from "./auth-errors";
|
|
9
|
+
import { authErrorCode, englishRateLimited, isRateLimited, retryAfterSeconds } from "./auth-errors";
|
|
10
10
|
import { newPasswordProblem } from "./form-rules";
|
|
11
11
|
import { NewPasswordFields } from "./new-password-fields";
|
|
12
12
|
import {
|
|
@@ -53,9 +53,15 @@ export interface ResetPasswordLabels {
|
|
|
53
53
|
sessionsEnded: string;
|
|
54
54
|
/** The button out — to sign-in, with the address filled in. */
|
|
55
55
|
signIn: string;
|
|
56
|
+
/** 0.30.0: the save was throttled — HTTP 429 ({@link isRateLimited}) — given the
|
|
57
|
+
* `Retry-After` wait in seconds, or `undefined` without one. OPTIONAL, like every key
|
|
58
|
+
* added to a shipped interface; the provider's `resetPassword`, then English, fill
|
|
59
|
+
* it. */
|
|
60
|
+
rateLimited?: (seconds?: number) => string;
|
|
56
61
|
}
|
|
57
62
|
|
|
58
|
-
|
|
63
|
+
/** `Required`: every key, the 0.30 one included, has its English here. */
|
|
64
|
+
export const DEFAULT_RESET_PASSWORD_LABELS: Required<ResetPasswordLabels> = {
|
|
59
65
|
title: "Choose a new password",
|
|
60
66
|
checking: "Checking the link…",
|
|
61
67
|
intro: (email) => `You’re setting a new password for ${email}.`,
|
|
@@ -71,6 +77,7 @@ export const DEFAULT_RESET_PASSWORD_LABELS: ResetPasswordLabels = {
|
|
|
71
77
|
success: "Your password has been changed. You can sign in with it now.",
|
|
72
78
|
sessionsEnded: "Signed-in devices were signed out — you’ll need to sign in again there.",
|
|
73
79
|
signIn: "Go to sign in",
|
|
80
|
+
rateLimited: englishRateLimited,
|
|
74
81
|
};
|
|
75
82
|
|
|
76
83
|
/* ── Types ───────────────────────────────────────────────────────────────── */
|
|
@@ -135,8 +142,9 @@ export interface ResetPasswordFormProps
|
|
|
135
142
|
/** "Request a new link" — the app's "Forgot password" page. Default
|
|
136
143
|
* `/forgot-password`. */
|
|
137
144
|
forgotHref?: string;
|
|
138
|
-
/** The app's words for a failed save (
|
|
139
|
-
*
|
|
145
|
+
/** The app's words for a failed save (the device offline), or `undefined` for the
|
|
146
|
+
* kit's: `rateLimited` for a bare `429` (0.30.0), else `failed`. Not asked for
|
|
147
|
+
* `token_invalid`, which is a state. */
|
|
140
148
|
describeError?: (error: unknown) => ReactNode | undefined;
|
|
141
149
|
/** The heading. Default `labels.title`; `null` draws none, for a page that puts it
|
|
142
150
|
* in `AuthLayout`'s `title` instead. */
|
|
@@ -297,7 +305,13 @@ export function ResetPasswordForm({
|
|
|
297
305
|
setPhase({ token, phase: { kind: "invalid" } });
|
|
298
306
|
return;
|
|
299
307
|
}
|
|
300
|
-
|
|
308
|
+
// The app's words first, then the throttle's, then the kit's catch-all.
|
|
309
|
+
setFailure(
|
|
310
|
+
describeError?.(error) ??
|
|
311
|
+
(isRateLimited(error)
|
|
312
|
+
? (labels.rateLimited ?? DEFAULT_RESET_PASSWORD_LABELS.rateLimited)(retryAfterSeconds(error))
|
|
313
|
+
: labels.failed),
|
|
314
|
+
);
|
|
301
315
|
},
|
|
302
316
|
);
|
|
303
317
|
};
|