@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
|
@@ -23,9 +23,53 @@
|
|
|
23
23
|
* password would confirm a leaked one. It stays in the union only for a future server
|
|
24
24
|
* step that is past the password (an admin view, a signed-in refresh) and may say so.
|
|
25
25
|
*/
|
|
26
|
-
type AuthErrorCode = "invalid_credentials" | "registration_closed" | "email_taken" | "invitation_invalid" | "invitation_expired" | "token_invalid"
|
|
26
|
+
type AuthErrorCode = "invalid_credentials" | "registration_closed" | "email_taken" | "invitation_invalid" | "invitation_expired" | "token_invalid"
|
|
27
|
+
/** 0.29.1: a link that WAS valid and ran out (verification, reset) — keksdose's step-3
|
|
28
|
+
* refusals tell it from `token_invalid`, so a page can say "expired, ask for a new one". */
|
|
29
|
+
| "token_expired" | "account_inactive";
|
|
27
30
|
/**
|
|
28
|
-
*
|
|
31
|
+
* 0.30.0: the coded refusals of user administration and the account's own settings
|
|
32
|
+
* (docs/user-admin-harmonization.md §4, §6) — server-kit 0.4.0's `AccountErrorCode`,
|
|
33
|
+
* value for value. The same family of answer as the sign-in codes (`{detail, code}`,
|
|
34
|
+
* plus `companies` on kastlan's `last_admin`), read by the same {@link authErrorCode}.
|
|
35
|
+
*
|
|
36
|
+
* - `last_admin` — the action would leave no active admin: deactivating, demoting or
|
|
37
|
+
* deleting the last one (in kastlan, of a company; the answer names the `companies`).
|
|
38
|
+
* - `self_action` — an admin acting on their own account where that is never allowed.
|
|
39
|
+
* - `other_companies` — kastlan: the account belongs to other companies too, so a
|
|
40
|
+
* company admin may not deactivate it; "Remove from company" instead (§9.6).
|
|
41
|
+
* - `household_has_members` — keksdose's guard on a deletion request (§6.4).
|
|
42
|
+
* - `confirmation_required`, `confirmation_mismatch` — the action's confirmation level
|
|
43
|
+
* asked for a typed address (or a tick), and the request carried none, or another
|
|
44
|
+
* address (§4.2).
|
|
45
|
+
* - `password_incorrect` — the CURRENT password is wrong on a signed-in route (the email
|
|
46
|
+
* change, the deletion request). `400`, not `401`: an app's client reads a 401 as an
|
|
47
|
+
* ended session and signs the user out, the wrong answer to a typo. Every other one
|
|
48
|
+
* is a `409`.
|
|
49
|
+
*/
|
|
50
|
+
type AccountErrorCode = "last_admin" | "self_action" | "other_companies" | "household_has_members" | "confirmation_required" | "confirmation_mismatch" | "password_incorrect";
|
|
51
|
+
/**
|
|
52
|
+
* 0.31.0: the demo's coded refusals (docs/landing-demo-harmonization.md §6.2) —
|
|
53
|
+
* server-kit 0.5's `DemoErrorCode`, value for value, answered as `{detail, code}` like
|
|
54
|
+
* the others and read by the same {@link authErrorCode}.
|
|
55
|
+
*
|
|
56
|
+
* - `demo_disabled` — `404`: the demo switch is off (it is "not there", not forbidden).
|
|
57
|
+
* - `demo_rate_limited` — `429` with `Retry-After`: the per-IP window of demo starts.
|
|
58
|
+
* - `demo_capacity` — `429`, no `Retry-After`: the cap of live demo users.
|
|
59
|
+
* - `demo_not_ready` — `503`: the demo data has not been seeded yet.
|
|
60
|
+
* - `demo_read_only` — `403`, model R: any write by a demo user. The app's write lock
|
|
61
|
+
* already explains it, so it shows no toast (§5.4).
|
|
62
|
+
* - `demo_refused` — `403`, both models: an action a demo never may — a way in or out,
|
|
63
|
+
* mail, uploads, the account export (§6.4).
|
|
64
|
+
*/
|
|
65
|
+
type DemoErrorCode = "demo_disabled" | "demo_rate_limited" | "demo_capacity" | "demo_not_ready" | "demo_read_only" | "demo_refused";
|
|
66
|
+
/** Every code the kit reads: the sign-in refusals, the account ones (0.30.0) and the
|
|
67
|
+
* demo's (0.31.0). */
|
|
68
|
+
type KitErrorCode = AuthErrorCode | AccountErrorCode | DemoErrorCode;
|
|
69
|
+
/**
|
|
70
|
+
* The code an error carries — an {@link AuthErrorCode}, since 0.30.0 an
|
|
71
|
+
* {@link AccountErrorCode}, since 0.31.0 a {@link DemoErrorCode} — or `undefined` when it
|
|
72
|
+
* carries none.
|
|
29
73
|
*
|
|
30
74
|
* Reads the parsed response body wherever the apps' HTTP clients put it, without
|
|
31
75
|
* depending on any of them:
|
|
@@ -42,7 +86,7 @@ type AuthErrorCode = "invalid_credentials" | "registration_closed" | "email_take
|
|
|
42
86
|
* read as a refusal. The HTTP status is not checked: the code is the contract, and the
|
|
43
87
|
* status differs per refusal (401, 403, 409, 410).
|
|
44
88
|
*/
|
|
45
|
-
declare function authErrorCode(err: unknown):
|
|
89
|
+
declare function authErrorCode(err: unknown): KitErrorCode | undefined;
|
|
46
90
|
/**
|
|
47
91
|
* Whether `err` is a coded auth refusal — any of them, or the one named:
|
|
48
92
|
*
|
|
@@ -53,8 +97,61 @@ declare function authErrorCode(err: unknown): AuthErrorCode | undefined;
|
|
|
53
97
|
*
|
|
54
98
|
* The kit's `SignInForm` and `RegisterForm` call it on what their callbacks reject
|
|
55
99
|
* with, so an app's `onSubmit` can simply let its client's error through. See
|
|
56
|
-
* {@link authErrorCode} for the shapes it reads.
|
|
100
|
+
* {@link authErrorCode} for the shapes it reads. Since 0.30.0 it knows the account codes
|
|
101
|
+
* too: `isAuthError(err, "last_admin")`; since 0.31.0 the demo's:
|
|
102
|
+
* `isAuthError(err, "demo_read_only")`.
|
|
103
|
+
*/
|
|
104
|
+
declare function isAuthError(err: unknown, code?: KitErrorCode): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Whether `err` is a throttled request — HTTP `429 Too Many Requests` (0.30.0, from the
|
|
107
|
+
* apps' 0.29 adoption: every app answered a throttled sign-in, sign-up or reset through
|
|
108
|
+
* its own `describeError`, with three wordings of one sentence).
|
|
109
|
+
*
|
|
110
|
+
* A throttle carries no `code` in the contract — the STATUS is the answer, set by the
|
|
111
|
+
* apps' limiters (server-kit's `AuthLimiters`, keksdose's per-IP limit) — so this reads
|
|
112
|
+
* the status, wherever the apps' HTTP clients put it, without depending on any of them:
|
|
113
|
+
*
|
|
114
|
+
* 1. `err.response.status` — axios (all three apps' clients);
|
|
115
|
+
* 2. `err.status` — ofetch's `FetchError`, the generated OpenAPI clients' `ApiError`, and
|
|
116
|
+
* a `Response` thrown as it came;
|
|
117
|
+
* 3. `err.statusCode` — ofetch's alias, and Node-style errors.
|
|
118
|
+
*
|
|
119
|
+
* There is no `rate_limited` code: keksdose answers its 429s uncoded, with a
|
|
120
|
+
* `Retry-After` header, and the status is the one signal every limiter gives.
|
|
121
|
+
*
|
|
122
|
+
* The kit's `SignInForm`, `RegisterForm`, `ForgotPasswordForm` and `ResetPasswordForm`
|
|
123
|
+
* call it AFTER the app's `describeError` — an app with words of its own keeps them —
|
|
124
|
+
* and before their generic failure, so "Too many attempts. Try again in 30 s." (with
|
|
125
|
+
* {@link retryAfterSeconds}) or "… Wait a moment and try again." needs no app code.
|
|
126
|
+
*/
|
|
127
|
+
declare function isRateLimited(err: unknown): boolean;
|
|
128
|
+
/**
|
|
129
|
+
* How long a throttled request asks to wait, in whole seconds — its `Retry-After`
|
|
130
|
+
* header (0.30.0, keksdose: its 429s carry no code, only the header) — or `undefined`
|
|
131
|
+
* when there is none, or none that parses.
|
|
132
|
+
*
|
|
133
|
+
* Read where the apps' clients keep the response headers, without depending on any of
|
|
134
|
+
* them: `err.response.headers` (axios' `AxiosHeaders` or a plain record; ofetch's
|
|
135
|
+
* `FetchError.response`, a fetch `Headers`), then `err.headers` (a `Response` thrown as
|
|
136
|
+
* it came). The value is delta-seconds, or an HTTP date turned into seconds from `now`
|
|
137
|
+
* (default: the present moment) — never below 0.
|
|
138
|
+
*
|
|
139
|
+
* The forms hand it to their `rateLimited` label when {@link isRateLimited} is true:
|
|
140
|
+
* "Too many attempts. Try again in 30 s." — and, without one, "… Wait a moment and try
|
|
141
|
+
* again." A browser only exposes `Retry-After` to a cross-origin page when the server
|
|
142
|
+
* lists it in `Access-Control-Expose-Headers`.
|
|
143
|
+
*/
|
|
144
|
+
declare function retryAfterSeconds(err: unknown, now?: number | Date): number | undefined;
|
|
145
|
+
/**
|
|
146
|
+
* The English of every `rateLimited` label (0.30.0): "Too many attempts. Try again in
|
|
147
|
+
* 30 s." with a wait under a minute, in whole minutes from one minute up (keksdose's
|
|
148
|
+
* reset throttle answers in hours, and "3600 s" is a number nobody reads), and "Wait a
|
|
149
|
+
* moment and try again." without one — or with a `0`.
|
|
150
|
+
*
|
|
151
|
+
* @internal The namespaces' English; not part of the barrel.
|
|
57
152
|
*/
|
|
58
|
-
declare function
|
|
153
|
+
declare function englishRateLimited(seconds?: number): string;
|
|
154
|
+
/** "30 s", "2 min" — a wait in English, rounded up. @internal */
|
|
155
|
+
declare function englishWait(seconds: number): string;
|
|
59
156
|
|
|
60
|
-
export { type AuthErrorCode, authErrorCode, isAuthError };
|
|
157
|
+
export { type AccountErrorCode, type AuthErrorCode, type DemoErrorCode, type KitErrorCode, authErrorCode, englishRateLimited, englishWait, isAuthError, isRateLimited, retryAfterSeconds };
|
package/dist/auth/auth-errors.js
CHANGED
|
@@ -6,7 +6,21 @@ const CODES = /* @__PURE__ */ new Set([
|
|
|
6
6
|
"invitation_invalid",
|
|
7
7
|
"invitation_expired",
|
|
8
8
|
"token_invalid",
|
|
9
|
-
"
|
|
9
|
+
"token_expired",
|
|
10
|
+
"account_inactive",
|
|
11
|
+
"last_admin",
|
|
12
|
+
"self_action",
|
|
13
|
+
"other_companies",
|
|
14
|
+
"household_has_members",
|
|
15
|
+
"confirmation_required",
|
|
16
|
+
"confirmation_mismatch",
|
|
17
|
+
"password_incorrect",
|
|
18
|
+
"demo_disabled",
|
|
19
|
+
"demo_rate_limited",
|
|
20
|
+
"demo_capacity",
|
|
21
|
+
"demo_not_ready",
|
|
22
|
+
"demo_read_only",
|
|
23
|
+
"demo_refused"
|
|
10
24
|
]);
|
|
11
25
|
const isBag = (value) => typeof value === "object" && value !== null;
|
|
12
26
|
function known(value) {
|
|
@@ -25,8 +39,60 @@ function isAuthError(err, code) {
|
|
|
25
39
|
const found = authErrorCode(err);
|
|
26
40
|
return found !== void 0 && (code === void 0 || found === code);
|
|
27
41
|
}
|
|
42
|
+
function statusOf(value) {
|
|
43
|
+
return typeof value === "number" && Number.isInteger(value) ? value : void 0;
|
|
44
|
+
}
|
|
45
|
+
function isRateLimited(err) {
|
|
46
|
+
if (!isBag(err)) return false;
|
|
47
|
+
const response = err.response;
|
|
48
|
+
const status = (isBag(response) ? statusOf(response.status) : void 0) ?? statusOf(err.status) ?? statusOf(err.statusCode);
|
|
49
|
+
return status === 429;
|
|
50
|
+
}
|
|
51
|
+
function headerOf(headers, name) {
|
|
52
|
+
if (!isBag(headers)) return void 0;
|
|
53
|
+
if (typeof headers.get === "function") {
|
|
54
|
+
try {
|
|
55
|
+
const value = headers.get.call(headers, name);
|
|
56
|
+
if (value !== void 0 && value !== null) return value;
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
61
|
+
if (key.toLowerCase() === name) return value;
|
|
62
|
+
}
|
|
63
|
+
return void 0;
|
|
64
|
+
}
|
|
65
|
+
function parseRetryAfter(value, now) {
|
|
66
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
67
|
+
if (typeof raw === "number") return Number.isFinite(raw) && raw >= 0 ? Math.ceil(raw) : void 0;
|
|
68
|
+
if (typeof raw !== "string") return void 0;
|
|
69
|
+
const text = raw.trim();
|
|
70
|
+
if (/^\d+$/.test(text)) return Number(text);
|
|
71
|
+
if (!/[a-z]/i.test(text)) return void 0;
|
|
72
|
+
const at = Date.parse(text);
|
|
73
|
+
if (Number.isNaN(at)) return void 0;
|
|
74
|
+
return Math.max(0, Math.ceil((at - now) / 1e3));
|
|
75
|
+
}
|
|
76
|
+
function retryAfterSeconds(err, now = Date.now()) {
|
|
77
|
+
if (!isBag(err)) return void 0;
|
|
78
|
+
const at = typeof now === "number" ? now : now.getTime();
|
|
79
|
+
const response = err.response;
|
|
80
|
+
const value = (isBag(response) ? headerOf(response.headers, "retry-after") : void 0) ?? headerOf(err.headers, "retry-after");
|
|
81
|
+
return value === void 0 ? void 0 : parseRetryAfter(value, at);
|
|
82
|
+
}
|
|
83
|
+
function englishRateLimited(seconds) {
|
|
84
|
+
if (!seconds || seconds <= 0) return "Too many attempts. Wait a moment and try again.";
|
|
85
|
+
return `Too many attempts. Try again in ${englishWait(seconds)}.`;
|
|
86
|
+
}
|
|
87
|
+
function englishWait(seconds) {
|
|
88
|
+
return seconds < 60 ? `${Math.ceil(seconds)} s` : `${Math.ceil(seconds / 60)} min`;
|
|
89
|
+
}
|
|
28
90
|
export {
|
|
29
91
|
authErrorCode,
|
|
30
|
-
|
|
92
|
+
englishRateLimited,
|
|
93
|
+
englishWait,
|
|
94
|
+
isAuthError,
|
|
95
|
+
isRateLimited,
|
|
96
|
+
retryAfterSeconds
|
|
31
97
|
};
|
|
32
98
|
//# sourceMappingURL=auth-errors.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/auth/auth-errors.ts"],"sourcesContent":["/**\n * The coded refusals of sign-in and sign-up (docs/auth-harmonization.md §5.2): every\n * refusal the auth routes answer carries a machine-readable `code`, and the pages\n * recognise the CODE, never the server's sentence.\n *\n * Today each app's pages print the server's `detail` string (keksdose's\n * `extractApiErrorMessage`), which is English on every locale and, on a credential\n * check, would be an account-enumeration oracle if it ever said more than one thing.\n * keksdose already keys one refusal on a code (`features/budgets/plan-limit.ts`,\n * `402 {detail, code: \"plan_budget_limit\"}`); §5.2 makes that the rule for auth. Each\n * app switches its server and its pages together (§9).\n *\n * - `invalid_credentials` — unknown address, wrong password AND deactivated account:\n * one answer, in the same time (§2.10). `401`.\n * - `registration_closed` — the sign-up gate refused the address. `403`.\n * - `email_taken` — an account with this address exists. `409`.\n * - `invitation_invalid`, `invitation_expired` — the `invite` token of a sign-up or an\n * accept (§4.4).\n * - `token_invalid` — a one-time token (reset, verification, a sign-in challenge) is\n * unknown, spent or expired.\n * - `account_inactive` — **no server answers it.** §2.10 decided that a deactivated\n * account gets `invalid_credentials`, because a separate answer after a correct\n * password would confirm a leaked one. It stays in the union only for a future server\n * step that is past the password (an admin view, a signed-in refresh) and may say so.\n */\nexport type AuthErrorCode =\n | \"invalid_credentials\"\n | \"registration_closed\"\n | \"email_taken\"\n | \"invitation_invalid\"\n | \"invitation_expired\"\n | \"token_invalid\"\n | \"account_inactive\";\n\nconst CODES: ReadonlySet<string> = new Set<AuthErrorCode>([\n \"invalid_credentials\",\n \"registration_closed\",\n \"email_taken\",\n \"invitation_invalid\",\n \"invitation_expired\",\n \"token_invalid\",\n \"account_inactive\",\n]);\n\ntype Bag = Record<string, unknown>;\n\nconst isBag = (value: unknown): value is Bag => typeof value === \"object\" && value !== null;\n\n/** A known code, or undefined. */\nfunction known(value: unknown): AuthErrorCode | undefined {\n return typeof value === \"string\" && CODES.has(value) ? (value as AuthErrorCode) : undefined;\n}\n\n/** `{code}` on a parsed body, or FastAPI's `{detail: {code}}` — what an\n * `HTTPException(detail={\"code\": …})` serialises to. */\nfunction codeOfBody(body: unknown): AuthErrorCode | undefined {\n if (!isBag(body)) return undefined;\n return known(body.code) ?? (isBag(body.detail) ? known(body.detail.code) : undefined);\n}\n\n/**\n * The {@link AuthErrorCode} an error carries, or `undefined` when it carries none.\n *\n * Reads the parsed response body wherever the apps' HTTP clients put it, without\n * depending on any of them:\n *\n * 1. `err.response.data` — axios (keksdose's, kastlan's and Kurvenschmiede's clients;\n * keksdose's `isPlanBudgetLimitRefusal` reads the same path);\n * 2. `err.data` — ofetch's `FetchError`, and most `fetch` wrappers that parse the body;\n * 3. `err.body` — the generated OpenAPI clients' `ApiError`;\n * 4. `err` itself — a body thrown as it came (`throw await res.json()`).\n *\n * In each, `code` or FastAPI's nested `detail.code`. Last, `err.code` — but only when\n * it is one of the codes above: axios writes its OWN codes there (`ERR_NETWORK`,\n * `ECONNABORTED`, `ERR_BAD_REQUEST`), and so does Node, so an unknown value is never\n * read as a refusal. The HTTP status is not checked: the code is the contract, and the\n * status differs per refusal (401, 403, 409, 410).\n */\nexport function authErrorCode(err: unknown): AuthErrorCode | undefined {\n if (!isBag(err)) return undefined;\n const response = err.response;\n return (\n (isBag(response) ? codeOfBody(response.data) : undefined) ??\n codeOfBody(err.data) ??\n codeOfBody(err.body) ??\n codeOfBody(err)\n );\n}\n\n/**\n * Whether `err` is a coded auth refusal — any of them, or the one named:\n *\n * ```ts\n * try { await api.register(values) }\n * catch (err) { if (isAuthError(err, \"email_taken\")) … }\n * ```\n *\n * The kit's `SignInForm` and `RegisterForm` call it on what their callbacks reject\n * with, so an app's `onSubmit` can simply let its client's error through. See\n * {@link authErrorCode} for the shapes it reads.\n */\nexport function isAuthError(err: unknown, code?: AuthErrorCode): boolean {\n const found = authErrorCode(err);\n return found !== undefined && (code === undefined || found === code);\n}\n"],"mappings":";AAkCA,MAAM,QAA6B,oBAAI,IAAmB;AAAA,EACxD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAID,MAAM,QAAQ,CAAC,UAAiC,OAAO,UAAU,YAAY,UAAU;AAGvF,SAAS,MAAM,OAA2C;AACxD,SAAO,OAAO,UAAU,YAAY,MAAM,IAAI,KAAK,IAAK,QAA0B;AACpF;AAIA,SAAS,WAAW,MAA0C;AAC5D,MAAI,CAAC,MAAM,IAAI,EAAG,QAAO;AACzB,SAAO,MAAM,KAAK,IAAI,MAAM,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK,OAAO,IAAI,IAAI;AAC7E;AAoBO,SAAS,cAAc,KAAyC;AACrE,MAAI,CAAC,MAAM,GAAG,EAAG,QAAO;AACxB,QAAM,WAAW,IAAI;AACrB,UACG,MAAM,QAAQ,IAAI,WAAW,SAAS,IAAI,IAAI,WAC/C,WAAW,IAAI,IAAI,KACnB,WAAW,IAAI,IAAI,KACnB,WAAW,GAAG;AAElB;AAcO,SAAS,YAAY,KAAc,MAA+B;AACvE,QAAM,QAAQ,cAAc,GAAG;AAC/B,SAAO,UAAU,WAAc,SAAS,UAAa,UAAU;AACjE;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/auth/auth-errors.ts"],"sourcesContent":["/**\n * The coded refusals of sign-in and sign-up (docs/auth-harmonization.md §5.2): every\n * refusal the auth routes answer carries a machine-readable `code`, and the pages\n * recognise the CODE, never the server's sentence.\n *\n * Today each app's pages print the server's `detail` string (keksdose's\n * `extractApiErrorMessage`), which is English on every locale and, on a credential\n * check, would be an account-enumeration oracle if it ever said more than one thing.\n * keksdose already keys one refusal on a code (`features/budgets/plan-limit.ts`,\n * `402 {detail, code: \"plan_budget_limit\"}`); §5.2 makes that the rule for auth. Each\n * app switches its server and its pages together (§9).\n *\n * - `invalid_credentials` — unknown address, wrong password AND deactivated account:\n * one answer, in the same time (§2.10). `401`.\n * - `registration_closed` — the sign-up gate refused the address. `403`.\n * - `email_taken` — an account with this address exists. `409`.\n * - `invitation_invalid`, `invitation_expired` — the `invite` token of a sign-up or an\n * accept (§4.4).\n * - `token_invalid` — a one-time token (reset, verification, a sign-in challenge) is\n * unknown, spent or expired.\n * - `account_inactive` — **no server answers it.** §2.10 decided that a deactivated\n * account gets `invalid_credentials`, because a separate answer after a correct\n * password would confirm a leaked one. It stays in the union only for a future server\n * step that is past the password (an admin view, a signed-in refresh) and may say so.\n */\nexport type AuthErrorCode =\n | \"invalid_credentials\"\n | \"registration_closed\"\n | \"email_taken\"\n | \"invitation_invalid\"\n | \"invitation_expired\"\n | \"token_invalid\"\n /** 0.29.1: a link that WAS valid and ran out (verification, reset) — keksdose's step-3\n * refusals tell it from `token_invalid`, so a page can say \"expired, ask for a new one\". */\n | \"token_expired\"\n | \"account_inactive\";\n\n/**\n * 0.30.0: the coded refusals of user administration and the account's own settings\n * (docs/user-admin-harmonization.md §4, §6) — server-kit 0.4.0's `AccountErrorCode`,\n * value for value. The same family of answer as the sign-in codes (`{detail, code}`,\n * plus `companies` on kastlan's `last_admin`), read by the same {@link authErrorCode}.\n *\n * - `last_admin` — the action would leave no active admin: deactivating, demoting or\n * deleting the last one (in kastlan, of a company; the answer names the `companies`).\n * - `self_action` — an admin acting on their own account where that is never allowed.\n * - `other_companies` — kastlan: the account belongs to other companies too, so a\n * company admin may not deactivate it; \"Remove from company\" instead (§9.6).\n * - `household_has_members` — keksdose's guard on a deletion request (§6.4).\n * - `confirmation_required`, `confirmation_mismatch` — the action's confirmation level\n * asked for a typed address (or a tick), and the request carried none, or another\n * address (§4.2).\n * - `password_incorrect` — the CURRENT password is wrong on a signed-in route (the email\n * change, the deletion request). `400`, not `401`: an app's client reads a 401 as an\n * ended session and signs the user out, the wrong answer to a typo. Every other one\n * is a `409`.\n */\nexport type AccountErrorCode =\n | \"last_admin\"\n | \"self_action\"\n | \"other_companies\"\n | \"household_has_members\"\n | \"confirmation_required\"\n | \"confirmation_mismatch\"\n | \"password_incorrect\";\n\n/**\n * 0.31.0: the demo's coded refusals (docs/landing-demo-harmonization.md §6.2) —\n * server-kit 0.5's `DemoErrorCode`, value for value, answered as `{detail, code}` like\n * the others and read by the same {@link authErrorCode}.\n *\n * - `demo_disabled` — `404`: the demo switch is off (it is \"not there\", not forbidden).\n * - `demo_rate_limited` — `429` with `Retry-After`: the per-IP window of demo starts.\n * - `demo_capacity` — `429`, no `Retry-After`: the cap of live demo users.\n * - `demo_not_ready` — `503`: the demo data has not been seeded yet.\n * - `demo_read_only` — `403`, model R: any write by a demo user. The app's write lock\n * already explains it, so it shows no toast (§5.4).\n * - `demo_refused` — `403`, both models: an action a demo never may — a way in or out,\n * mail, uploads, the account export (§6.4).\n */\nexport type DemoErrorCode =\n | \"demo_disabled\"\n | \"demo_rate_limited\"\n | \"demo_capacity\"\n | \"demo_not_ready\"\n | \"demo_read_only\"\n | \"demo_refused\";\n\n/** Every code the kit reads: the sign-in refusals, the account ones (0.30.0) and the\n * demo's (0.31.0). */\nexport type KitErrorCode = AuthErrorCode | AccountErrorCode | DemoErrorCode;\n\nconst CODES: ReadonlySet<string> = new Set<KitErrorCode>([\n \"invalid_credentials\",\n \"registration_closed\",\n \"email_taken\",\n \"invitation_invalid\",\n \"invitation_expired\",\n \"token_invalid\",\n \"token_expired\",\n \"account_inactive\",\n \"last_admin\",\n \"self_action\",\n \"other_companies\",\n \"household_has_members\",\n \"confirmation_required\",\n \"confirmation_mismatch\",\n \"password_incorrect\",\n \"demo_disabled\",\n \"demo_rate_limited\",\n \"demo_capacity\",\n \"demo_not_ready\",\n \"demo_read_only\",\n \"demo_refused\",\n]);\n\ntype Bag = Record<string, unknown>;\n\nconst isBag = (value: unknown): value is Bag => typeof value === \"object\" && value !== null;\n\n/** A known code, or undefined. */\nfunction known(value: unknown): KitErrorCode | undefined {\n return typeof value === \"string\" && CODES.has(value) ? (value as KitErrorCode) : undefined;\n}\n\n/** `{code}` on a parsed body, or FastAPI's `{detail: {code}}` — what an\n * `HTTPException(detail={\"code\": …})` serialises to. */\nfunction codeOfBody(body: unknown): KitErrorCode | undefined {\n if (!isBag(body)) return undefined;\n return known(body.code) ?? (isBag(body.detail) ? known(body.detail.code) : undefined);\n}\n\n/**\n * The code an error carries — an {@link AuthErrorCode}, since 0.30.0 an\n * {@link AccountErrorCode}, since 0.31.0 a {@link DemoErrorCode} — or `undefined` when it\n * carries none.\n *\n * Reads the parsed response body wherever the apps' HTTP clients put it, without\n * depending on any of them:\n *\n * 1. `err.response.data` — axios (keksdose's, kastlan's and Kurvenschmiede's clients;\n * keksdose's `isPlanBudgetLimitRefusal` reads the same path);\n * 2. `err.data` — ofetch's `FetchError`, and most `fetch` wrappers that parse the body;\n * 3. `err.body` — the generated OpenAPI clients' `ApiError`;\n * 4. `err` itself — a body thrown as it came (`throw await res.json()`).\n *\n * In each, `code` or FastAPI's nested `detail.code`. Last, `err.code` — but only when\n * it is one of the codes above: axios writes its OWN codes there (`ERR_NETWORK`,\n * `ECONNABORTED`, `ERR_BAD_REQUEST`), and so does Node, so an unknown value is never\n * read as a refusal. The HTTP status is not checked: the code is the contract, and the\n * status differs per refusal (401, 403, 409, 410).\n */\nexport function authErrorCode(err: unknown): KitErrorCode | undefined {\n if (!isBag(err)) return undefined;\n const response = err.response;\n return (\n (isBag(response) ? codeOfBody(response.data) : undefined) ??\n codeOfBody(err.data) ??\n codeOfBody(err.body) ??\n codeOfBody(err)\n );\n}\n\n/**\n * Whether `err` is a coded auth refusal — any of them, or the one named:\n *\n * ```ts\n * try { await api.register(values) }\n * catch (err) { if (isAuthError(err, \"email_taken\")) … }\n * ```\n *\n * The kit's `SignInForm` and `RegisterForm` call it on what their callbacks reject\n * with, so an app's `onSubmit` can simply let its client's error through. See\n * {@link authErrorCode} for the shapes it reads. Since 0.30.0 it knows the account codes\n * too: `isAuthError(err, \"last_admin\")`; since 0.31.0 the demo's:\n * `isAuthError(err, \"demo_read_only\")`.\n */\nexport function isAuthError(err: unknown, code?: KitErrorCode): boolean {\n const found = authErrorCode(err);\n return found !== undefined && (code === undefined || found === code);\n}\n\n/** An HTTP status as a number, or undefined. */\nfunction statusOf(value: unknown): number | undefined {\n return typeof value === \"number\" && Number.isInteger(value) ? value : undefined;\n}\n\n/**\n * Whether `err` is a throttled request — HTTP `429 Too Many Requests` (0.30.0, from the\n * apps' 0.29 adoption: every app answered a throttled sign-in, sign-up or reset through\n * its own `describeError`, with three wordings of one sentence).\n *\n * A throttle carries no `code` in the contract — the STATUS is the answer, set by the\n * apps' limiters (server-kit's `AuthLimiters`, keksdose's per-IP limit) — so this reads\n * the status, wherever the apps' HTTP clients put it, without depending on any of them:\n *\n * 1. `err.response.status` — axios (all three apps' clients);\n * 2. `err.status` — ofetch's `FetchError`, the generated OpenAPI clients' `ApiError`, and\n * a `Response` thrown as it came;\n * 3. `err.statusCode` — ofetch's alias, and Node-style errors.\n *\n * There is no `rate_limited` code: keksdose answers its 429s uncoded, with a\n * `Retry-After` header, and the status is the one signal every limiter gives.\n *\n * The kit's `SignInForm`, `RegisterForm`, `ForgotPasswordForm` and `ResetPasswordForm`\n * call it AFTER the app's `describeError` — an app with words of its own keeps them —\n * and before their generic failure, so \"Too many attempts. Try again in 30 s.\" (with\n * {@link retryAfterSeconds}) or \"… Wait a moment and try again.\" needs no app code.\n */\nexport function isRateLimited(err: unknown): boolean {\n if (!isBag(err)) return false;\n const response = err.response;\n const status =\n (isBag(response) ? statusOf(response.status) : undefined) ?? statusOf(err.status) ?? statusOf(err.statusCode);\n return status === 429;\n}\n\n/** A header's value from a `Headers` (fetch, ofetch's `response`), axios' `AxiosHeaders`\n * or a plain record — whose keys may come in any case. */\nfunction headerOf(headers: unknown, name: string): unknown {\n if (!isBag(headers)) return undefined;\n if (typeof headers.get === \"function\") {\n try {\n const value: unknown = (headers.get as (key: string) => unknown).call(headers, name);\n if (value !== undefined && value !== null) return value;\n } catch {\n // A `get` that is not a header lookup; the record below is.\n }\n }\n for (const [key, value] of Object.entries(headers)) {\n if (key.toLowerCase() === name) return value;\n }\n return undefined;\n}\n\n/** `Retry-After` as seconds from `now`: delta-seconds (\"30\"), or an HTTP date\n * (\"Wed, 07 Oct 2026 10:00:00 GMT\") counted from now, never below 0 (RFC 9110 §10.2.3). */\nfunction parseRetryAfter(value: unknown, now: number): number | undefined {\n const raw = Array.isArray(value) ? (value as unknown[])[0] : value;\n if (typeof raw === \"number\") return Number.isFinite(raw) && raw >= 0 ? Math.ceil(raw) : undefined;\n if (typeof raw !== \"string\") return undefined;\n const text = raw.trim();\n if (/^\\d+$/.test(text)) return Number(text);\n // An HTTP date names its day and month (\"Wed, 07 Oct 2026 …\"). Without a letter it is\n // a malformed number (\"-5\", \"1.5\"), which `Date.parse` would happily read as a year.\n if (!/[a-z]/i.test(text)) return undefined;\n const at = Date.parse(text);\n if (Number.isNaN(at)) return undefined;\n return Math.max(0, Math.ceil((at - now) / 1000));\n}\n\n/**\n * How long a throttled request asks to wait, in whole seconds — its `Retry-After`\n * header (0.30.0, keksdose: its 429s carry no code, only the header) — or `undefined`\n * when there is none, or none that parses.\n *\n * Read where the apps' clients keep the response headers, without depending on any of\n * them: `err.response.headers` (axios' `AxiosHeaders` or a plain record; ofetch's\n * `FetchError.response`, a fetch `Headers`), then `err.headers` (a `Response` thrown as\n * it came). The value is delta-seconds, or an HTTP date turned into seconds from `now`\n * (default: the present moment) — never below 0.\n *\n * The forms hand it to their `rateLimited` label when {@link isRateLimited} is true:\n * \"Too many attempts. Try again in 30 s.\" — and, without one, \"… Wait a moment and try\n * again.\" A browser only exposes `Retry-After` to a cross-origin page when the server\n * lists it in `Access-Control-Expose-Headers`.\n */\nexport function retryAfterSeconds(err: unknown, now: number | Date = Date.now()): number | undefined {\n if (!isBag(err)) return undefined;\n const at = typeof now === \"number\" ? now : now.getTime();\n const response = err.response;\n const value =\n (isBag(response) ? headerOf(response.headers, \"retry-after\") : undefined) ?? headerOf(err.headers, \"retry-after\");\n return value === undefined ? undefined : parseRetryAfter(value, at);\n}\n\n/**\n * The English of every `rateLimited` label (0.30.0): \"Too many attempts. Try again in\n * 30 s.\" with a wait under a minute, in whole minutes from one minute up (keksdose's\n * reset throttle answers in hours, and \"3600 s\" is a number nobody reads), and \"Wait a\n * moment and try again.\" without one — or with a `0`.\n *\n * @internal The namespaces' English; not part of the barrel.\n */\nexport function englishRateLimited(seconds?: number): string {\n if (!seconds || seconds <= 0) return \"Too many attempts. Wait a moment and try again.\";\n return `Too many attempts. Try again in ${englishWait(seconds)}.`;\n}\n\n/** \"30 s\", \"2 min\" — a wait in English, rounded up. @internal */\nexport function englishWait(seconds: number): string {\n return seconds < 60 ? `${Math.ceil(seconds)} s` : `${Math.ceil(seconds / 60)} min`;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -21,8 +21,14 @@ interface ForgotPasswordLabels {
|
|
|
21
21
|
/** A request that did not go through: a throttle, an outage. Never "unknown
|
|
22
22
|
* address" — the endpoint does not say. */
|
|
23
23
|
error: string;
|
|
24
|
+
/** 0.30.0: the request was throttled — HTTP 429 ({@link isRateLimited}) — given the
|
|
25
|
+
* `Retry-After` wait in seconds, or `undefined` without one. OPTIONAL, like every key
|
|
26
|
+
* added to a shipped interface; the provider's `forgotPassword`, then English, fill
|
|
27
|
+
* it. */
|
|
28
|
+
rateLimited?: (seconds?: number) => string;
|
|
24
29
|
}
|
|
25
|
-
|
|
30
|
+
/** `Required`: every key, the 0.30 one included, has its English here. */
|
|
31
|
+
declare const DEFAULT_FORGOT_PASSWORD_LABELS: Required<ForgotPasswordLabels>;
|
|
26
32
|
interface ForgotPasswordFormProps extends Omit<ComponentPropsWithoutRef<"div">, "onSubmit" | "children" | "title"> {
|
|
27
33
|
/**
|
|
28
34
|
* The address was submitted, trimmed: send `POST /auth/password-reset/request`.
|
|
@@ -38,8 +44,9 @@ interface ForgotPasswordFormProps extends Omit<ComponentPropsWithoutRef<"div">,
|
|
|
38
44
|
* whose password just failed need not type it twice. */
|
|
39
45
|
defaultEmail?: string;
|
|
40
46
|
/**
|
|
41
|
-
* The app's words for a failed request (
|
|
42
|
-
*
|
|
47
|
+
* The app's words for a failed request ("Too many requests — try again in an hour"),
|
|
48
|
+
* or `undefined` for the kit's: `rateLimited` for a bare `429` (0.30.0), else `error`.
|
|
49
|
+
* The same hook as `SignInForm`'s.
|
|
43
50
|
*/
|
|
44
51
|
describeError?: (error: unknown) => ReactNode | undefined;
|
|
45
52
|
/** App content under the intro — a notice, a support address. */
|
|
@@ -7,6 +7,7 @@ import { useKitLabels } from "../i18n/kit-labels.js";
|
|
|
7
7
|
import { AlertBanner } from "../components/alert-banner.js";
|
|
8
8
|
import { TextLink } from "../components/text-link.js";
|
|
9
9
|
import { Button, Input } from "../components/ui.js";
|
|
10
|
+
import { englishRateLimited, isRateLimited, retryAfterSeconds } from "./auth-errors.js";
|
|
10
11
|
import { FORM_HEADING_CLASS, INTRO_CLASS, OutcomeMessage, isThenable, useFocusWhen } from "./status-parts.js";
|
|
11
12
|
const DEFAULT_FORGOT_PASSWORD_LABELS = {
|
|
12
13
|
title: "Forgot password",
|
|
@@ -16,7 +17,8 @@ const DEFAULT_FORGOT_PASSWORD_LABELS = {
|
|
|
16
17
|
sent: (email) => `If an account exists for ${email}, the link is on its way.`,
|
|
17
18
|
sentHint: "The link is valid for one hour and works exactly once. Check your spam folder too.",
|
|
18
19
|
backToSignIn: "Back to sign in",
|
|
19
|
-
error: "The request failed. Please try again later."
|
|
20
|
+
error: "The request failed. Please try again later.",
|
|
21
|
+
rateLimited: englishRateLimited
|
|
20
22
|
};
|
|
21
23
|
function ForgotPasswordForm({
|
|
22
24
|
onSubmit,
|
|
@@ -50,6 +52,7 @@ function ForgotPasswordForm({
|
|
|
50
52
|
back
|
|
51
53
|
] });
|
|
52
54
|
}
|
|
55
|
+
const failureFor = (error) => describeError?.(error) ?? (isRateLimited(error) ? (labels.rateLimited ?? DEFAULT_FORGOT_PASSWORD_LABELS.rateLimited)(retryAfterSeconds(error)) : labels.error);
|
|
53
56
|
const submit = (e) => {
|
|
54
57
|
e.preventDefault();
|
|
55
58
|
const address = email.trim();
|
|
@@ -59,7 +62,7 @@ function ForgotPasswordForm({
|
|
|
59
62
|
try {
|
|
60
63
|
result = onSubmit(address);
|
|
61
64
|
} catch (error) {
|
|
62
|
-
setFailure(
|
|
65
|
+
setFailure(failureFor(error));
|
|
63
66
|
return;
|
|
64
67
|
}
|
|
65
68
|
if (!isThenable(result)) {
|
|
@@ -74,7 +77,7 @@ function ForgotPasswordForm({
|
|
|
74
77
|
},
|
|
75
78
|
(error) => {
|
|
76
79
|
setPending(false);
|
|
77
|
-
setFailure(
|
|
80
|
+
setFailure(failureFor(error));
|
|
78
81
|
}
|
|
79
82
|
);
|
|
80
83
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/auth/forgot-password-form.tsx"],"sourcesContent":["import { useId, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, FormEvent, ReactNode } from \"react\";\nimport { MailCheck } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { TextLink } from \"../components/text-link\";\nimport { Button, Input } from \"../components/ui\";\nimport { FORM_HEADING_CLASS, INTRO_CLASS, OutcomeMessage, isThenable, useFocusWhen } from \"./status-parts\";\nimport type { AuthHeadingLevel } from \"./status-parts\";\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\n/** Every string the form renders — the `forgotPassword` namespace. keksdose's words\n * (`auth.forgot.*`), which kastlan took over unchanged. */\nexport interface ForgotPasswordLabels {\n /** The heading. */\n title: string;\n /** Under the heading, before the field. */\n intro: string;\n /** The email field's label. */\n email: string;\n submit: string;\n /** The one answer, whatever the server knows — given the address as typed. */\n sent: (email: string) => string;\n /** Under it: how long the link lives, and where else to look. */\n sentHint: string;\n /** The link back to the sign-in page, on both states. */\n backToSignIn: string;\n /** A request that did not go through: a throttle, an outage. Never \"unknown\n * address\" — the endpoint does not say. */\n error: string;\n}\n\nexport const DEFAULT_FORGOT_PASSWORD_LABELS: ForgotPasswordLabels = {\n title: \"Forgot password\",\n intro: \"Enter your account’s email address. We’ll send you a link to choose a new password.\",\n email: \"Email\",\n submit: \"Send the link\",\n sent: (email) => `If an account exists for ${email}, the link is on its way.`,\n sentHint: \"The link is valid for one hour and works exactly once. Check your spam folder too.\",\n backToSignIn: \"Back to sign in\",\n error: \"The request failed. Please try again later.\",\n};\n\n/* ── The form ────────────────────────────────────────────────────────────── */\n\nexport interface ForgotPasswordFormProps\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onSubmit\" | \"children\" | \"title\"> {\n /**\n * The address was submitted, trimmed: send `POST /auth/password-reset/request`.\n * Resolve when the server answered — whatever it answered: it is `204` whether or not\n * an account exists. Reject only when the request did not go through (a throttle, an\n * outage); the form then stays, with the address, and says so. The form never sends a\n * request itself (§8).\n */\n onSubmit: (email: string) => Promise<unknown> | void;\n /** \"Back to sign in\" — the app's `/login`. Default `/login`. */\n signInHref?: string;\n /** The field's start value — the address the sign-in form already had, so someone\n * whose password just failed need not type it twice. */\n defaultEmail?: string;\n /**\n * The app's words for a failed request (a `429`: \"Too many requests — try again in\n * an hour\"), or `undefined` for the kit's `error`. The same hook as `SignInForm`'s.\n */\n describeError?: (error: unknown) => ReactNode | undefined;\n /** App content under the intro — a notice, a support address. */\n formContent?: ReactNode;\n /** The heading. Default `labels.title`; `null` draws none, for a page that puts it\n * in `AuthLayout`'s `title` instead. */\n title?: ReactNode;\n /** The heading's level. Default `h2`, under the page's `h1`. */\n headingAs?: AuthHeadingLevel;\n labels?: Partial<ForgotPasswordLabels>;\n}\n\n/**\n * \"I forgot my password\" for every app (docs/auth-harmonization.md §7): one field, then\n * the same sentence whatever the answer — keksdose's `ForgotPasswordPage`\n * (features/auth/forgot-password-page.tsx), kastlan's copy of it, as one component.\n *\n * **It always ends on the same message**: \"If an account exists for …, the link is on\n * its way.\" The server answers `204` whether or not the address has an account — so the\n * page cannot be used to find out who has one — and the page must not undo that by\n * saying more than the server knows. Worded as a promise (\"we sent you a link\"), a typo\n * would look like a mail-delivery problem and send the person waiting instead of trying\n * their other address (keksdose #198). The address shown is the one they typed, so a\n * typo is visible.\n *\n * The only failure it can show is a request that did not go through (throttled, the\n * server down), never \"unknown address\". §4.5's fallback to a tagged address\n * (`you+kastlan@…` when `you@…` has no account) is the server's and changes nothing here.\n *\n * The submit is not disabled while the field is empty — a browser that autofills\n * withholds the value until the first interaction, and a disabled button would swallow\n * that first click; the field's own `required` answers an empty submit instead. After a\n * submit the confirmation takes focus, because the button that had it is gone with the\n * form.\n */\nexport function ForgotPasswordForm({\n onSubmit,\n signInHref = \"/login\",\n defaultEmail = \"\",\n describeError,\n formContent,\n title,\n headingAs: Heading = \"h2\",\n labels: labelsProp,\n className,\n ...rest\n}: ForgotPasswordFormProps) {\n const labels = useKitLabels(\"forgotPassword\", DEFAULT_FORGOT_PASSWORD_LABELS, labelsProp);\n const [email, setEmail] = useState(defaultEmail);\n const [pending, setPending] = useState(false);\n const [failure, setFailure] = useState<ReactNode>(null);\n const [sentTo, setSentTo] = useState<string | null>(null);\n const headingId = useId();\n const sentRef = useRef<HTMLDivElement>(null);\n useFocusWhen(sentTo !== null, sentRef);\n\n const heading =\n title === null ? null : (\n <Heading id={headingId} className={FORM_HEADING_CLASS}>\n {title ?? labels.title}\n </Heading>\n );\n const back = (\n <p className=\"text-center text-sm\">\n <TextLink href={signInHref} tone=\"primary\">\n {labels.backToSignIn}\n </TextLink>\n </p>\n );\n\n if (sentTo !== null) {\n return (\n <div {...rest} data-state=\"sent\" className={cn(\"space-y-3\", className)}>\n {heading}\n <OutcomeMessage icon={<MailCheck className=\"text-[var(--success)]\" />} focusRef={sentRef}>\n <p className={INTRO_CLASS}>{labels.sent(sentTo)}</p>\n <p className=\"text-xs text-[var(--text-muted)]\">{labels.sentHint}</p>\n </OutcomeMessage>\n {back}\n </div>\n );\n }\n\n const submit = (e: FormEvent) => {\n e.preventDefault();\n const address = email.trim();\n if (pending || address === \"\") return;\n setFailure(null);\n let result: unknown;\n try {\n result = onSubmit(address);\n } catch (error) {\n setFailure(describeError?.(error) ?? labels.error);\n return;\n }\n if (!isThenable(result)) {\n setSentTo(address);\n return;\n }\n setPending(true);\n result.then(\n () => {\n setPending(false);\n setSentTo(address);\n },\n (error: unknown) => {\n setPending(false);\n setFailure(describeError?.(error) ?? labels.error);\n },\n );\n };\n\n return (\n <div {...rest} data-state=\"form\" className={cn(\"space-y-3\", className)}>\n <form\n className=\"space-y-3\"\n aria-labelledby={title === null ? undefined : headingId}\n onSubmit={submit}\n >\n {heading}\n <p className={INTRO_CLASS}>{labels.intro}</p>\n {formContent}\n <Input\n type=\"email\"\n name=\"email\"\n autoComplete=\"email\"\n label={labels.email}\n value={email}\n onChange={(e) => {\n setEmail(e.target.value);\n if (failure) setFailure(null);\n }}\n readOnly={pending}\n required\n />\n {failure != null && (\n <AlertBanner tone=\"danger\" size=\"sm\" role=\"alert\">\n {failure}\n </AlertBanner>\n )}\n <Button type=\"submit\" className=\"w-full\" pending={pending}>\n {labels.submit}\n </Button>\n </form>\n {back}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/auth/forgot-password-form.tsx"],"sourcesContent":["import { useId, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, FormEvent, ReactNode } from \"react\";\nimport { MailCheck } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { TextLink } from \"../components/text-link\";\nimport { Button, Input } from \"../components/ui\";\nimport { englishRateLimited, isRateLimited, retryAfterSeconds } from \"./auth-errors\";\nimport { FORM_HEADING_CLASS, INTRO_CLASS, OutcomeMessage, isThenable, useFocusWhen } from \"./status-parts\";\nimport type { AuthHeadingLevel } from \"./status-parts\";\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\n/** Every string the form renders — the `forgotPassword` namespace. keksdose's words\n * (`auth.forgot.*`), which kastlan took over unchanged. */\nexport interface ForgotPasswordLabels {\n /** The heading. */\n title: string;\n /** Under the heading, before the field. */\n intro: string;\n /** The email field's label. */\n email: string;\n submit: string;\n /** The one answer, whatever the server knows — given the address as typed. */\n sent: (email: string) => string;\n /** Under it: how long the link lives, and where else to look. */\n sentHint: string;\n /** The link back to the sign-in page, on both states. */\n backToSignIn: string;\n /** A request that did not go through: a throttle, an outage. Never \"unknown\n * address\" — the endpoint does not say. */\n error: string;\n /** 0.30.0: the request was throttled — HTTP 429 ({@link isRateLimited}) — given the\n * `Retry-After` wait in seconds, or `undefined` without one. OPTIONAL, like every key\n * added to a shipped interface; the provider's `forgotPassword`, then English, fill\n * it. */\n rateLimited?: (seconds?: number) => string;\n}\n\n/** `Required`: every key, the 0.30 one included, has its English here. */\nexport const DEFAULT_FORGOT_PASSWORD_LABELS: Required<ForgotPasswordLabels> = {\n title: \"Forgot password\",\n intro: \"Enter your account’s email address. We’ll send you a link to choose a new password.\",\n email: \"Email\",\n submit: \"Send the link\",\n sent: (email) => `If an account exists for ${email}, the link is on its way.`,\n sentHint: \"The link is valid for one hour and works exactly once. Check your spam folder too.\",\n backToSignIn: \"Back to sign in\",\n error: \"The request failed. Please try again later.\",\n rateLimited: englishRateLimited,\n};\n\n/* ── The form ────────────────────────────────────────────────────────────── */\n\nexport interface ForgotPasswordFormProps\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onSubmit\" | \"children\" | \"title\"> {\n /**\n * The address was submitted, trimmed: send `POST /auth/password-reset/request`.\n * Resolve when the server answered — whatever it answered: it is `204` whether or not\n * an account exists. Reject only when the request did not go through (a throttle, an\n * outage); the form then stays, with the address, and says so. The form never sends a\n * request itself (§8).\n */\n onSubmit: (email: string) => Promise<unknown> | void;\n /** \"Back to sign in\" — the app's `/login`. Default `/login`. */\n signInHref?: string;\n /** The field's start value — the address the sign-in form already had, so someone\n * whose password just failed need not type it twice. */\n defaultEmail?: string;\n /**\n * The app's words for a failed request (\"Too many requests — try again in an hour\"),\n * or `undefined` for the kit's: `rateLimited` for a bare `429` (0.30.0), else `error`.\n * The same hook as `SignInForm`'s.\n */\n describeError?: (error: unknown) => ReactNode | undefined;\n /** App content under the intro — a notice, a support address. */\n formContent?: ReactNode;\n /** The heading. Default `labels.title`; `null` draws none, for a page that puts it\n * in `AuthLayout`'s `title` instead. */\n title?: ReactNode;\n /** The heading's level. Default `h2`, under the page's `h1`. */\n headingAs?: AuthHeadingLevel;\n labels?: Partial<ForgotPasswordLabels>;\n}\n\n/**\n * \"I forgot my password\" for every app (docs/auth-harmonization.md §7): one field, then\n * the same sentence whatever the answer — keksdose's `ForgotPasswordPage`\n * (features/auth/forgot-password-page.tsx), kastlan's copy of it, as one component.\n *\n * **It always ends on the same message**: \"If an account exists for …, the link is on\n * its way.\" The server answers `204` whether or not the address has an account — so the\n * page cannot be used to find out who has one — and the page must not undo that by\n * saying more than the server knows. Worded as a promise (\"we sent you a link\"), a typo\n * would look like a mail-delivery problem and send the person waiting instead of trying\n * their other address (keksdose #198). The address shown is the one they typed, so a\n * typo is visible.\n *\n * The only failure it can show is a request that did not go through (throttled, the\n * server down), never \"unknown address\". §4.5's fallback to a tagged address\n * (`you+kastlan@…` when `you@…` has no account) is the server's and changes nothing here.\n *\n * The submit is not disabled while the field is empty — a browser that autofills\n * withholds the value until the first interaction, and a disabled button would swallow\n * that first click; the field's own `required` answers an empty submit instead. After a\n * submit the confirmation takes focus, because the button that had it is gone with the\n * form.\n */\nexport function ForgotPasswordForm({\n onSubmit,\n signInHref = \"/login\",\n defaultEmail = \"\",\n describeError,\n formContent,\n title,\n headingAs: Heading = \"h2\",\n labels: labelsProp,\n className,\n ...rest\n}: ForgotPasswordFormProps) {\n const labels = useKitLabels(\"forgotPassword\", DEFAULT_FORGOT_PASSWORD_LABELS, labelsProp);\n const [email, setEmail] = useState(defaultEmail);\n const [pending, setPending] = useState(false);\n const [failure, setFailure] = useState<ReactNode>(null);\n const [sentTo, setSentTo] = useState<string | null>(null);\n const headingId = useId();\n const sentRef = useRef<HTMLDivElement>(null);\n useFocusWhen(sentTo !== null, sentRef);\n\n const heading =\n title === null ? null : (\n <Heading id={headingId} className={FORM_HEADING_CLASS}>\n {title ?? labels.title}\n </Heading>\n );\n const back = (\n <p className=\"text-center text-sm\">\n <TextLink href={signInHref} tone=\"primary\">\n {labels.backToSignIn}\n </TextLink>\n </p>\n );\n\n if (sentTo !== null) {\n return (\n <div {...rest} data-state=\"sent\" className={cn(\"space-y-3\", className)}>\n {heading}\n <OutcomeMessage icon={<MailCheck className=\"text-[var(--success)]\" />} focusRef={sentRef}>\n <p className={INTRO_CLASS}>{labels.sent(sentTo)}</p>\n <p className=\"text-xs text-[var(--text-muted)]\">{labels.sentHint}</p>\n </OutcomeMessage>\n {back}\n </div>\n );\n }\n\n // The app's words first, then the throttle's, then the kit's catch-all.\n const failureFor = (error: unknown): ReactNode =>\n describeError?.(error) ??\n (isRateLimited(error)\n ? (labels.rateLimited ?? DEFAULT_FORGOT_PASSWORD_LABELS.rateLimited)(retryAfterSeconds(error))\n : labels.error);\n\n const submit = (e: FormEvent) => {\n e.preventDefault();\n const address = email.trim();\n if (pending || address === \"\") return;\n setFailure(null);\n let result: unknown;\n try {\n result = onSubmit(address);\n } catch (error) {\n setFailure(failureFor(error));\n return;\n }\n if (!isThenable(result)) {\n setSentTo(address);\n return;\n }\n setPending(true);\n result.then(\n () => {\n setPending(false);\n setSentTo(address);\n },\n (error: unknown) => {\n setPending(false);\n setFailure(failureFor(error));\n },\n );\n };\n\n return (\n <div {...rest} data-state=\"form\" className={cn(\"space-y-3\", className)}>\n <form\n className=\"space-y-3\"\n aria-labelledby={title === null ? undefined : headingId}\n onSubmit={submit}\n >\n {heading}\n <p className={INTRO_CLASS}>{labels.intro}</p>\n {formContent}\n <Input\n type=\"email\"\n name=\"email\"\n autoComplete=\"email\"\n label={labels.email}\n value={email}\n onChange={(e) => {\n setEmail(e.target.value);\n if (failure) setFailure(null);\n }}\n readOnly={pending}\n required\n />\n {failure != null && (\n <AlertBanner tone=\"danger\" size=\"sm\" role=\"alert\">\n {failure}\n </AlertBanner>\n )}\n <Button type=\"submit\" className=\"w-full\" pending={pending}>\n {labels.submit}\n </Button>\n </form>\n {back}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import 'react';
|
|
2
|
-
export {
|
|
2
|
+
export { cB as DEFAULT_REGISTER_LABELS, ei as RegisterForm, ej as RegisterFormProps, ek as RegisterLabels, el as RegisterValues } from '../delete-account-setting-DCFS5Vof.js';
|
|
3
|
+
import '../components/tooltip.js';
|
|
4
|
+
import '../hooks/use-anchored-rect.js';
|
|
5
|
+
import '../lib/clipping.js';
|
|
3
6
|
import '../data-table-labels-DUELeXpa.js';
|
|
4
7
|
import '../components/data-table-sort.js';
|
|
5
8
|
import '../components/swipeable-row.js';
|
|
@@ -9,10 +12,6 @@ import '../components/popover.js';
|
|
|
9
12
|
import '../components/chip.js';
|
|
10
13
|
import 'lucide-react';
|
|
11
14
|
import '../components/field-sync.js';
|
|
12
|
-
import '../components/tooltip.js';
|
|
13
|
-
import '../hooks/use-anchored-rect.js';
|
|
14
|
-
import '../lib/clipping.js';
|
|
15
|
-
import '../components/field-parts.js';
|
|
16
15
|
import '../wizard/types.js';
|
|
17
16
|
import '../tour/tour.js';
|
|
18
17
|
import '../search/command-palette.js';
|
|
@@ -55,6 +54,14 @@ import '../components/image-grid.js';
|
|
|
55
54
|
import '../components/lightbox.js';
|
|
56
55
|
import '../components/write-lock.js';
|
|
57
56
|
import './sign-in-form.js';
|
|
57
|
+
import '../landing/access.js';
|
|
58
|
+
import '../landing/landing-labels.js';
|
|
59
|
+
import '../shell/topbar-controls.js';
|
|
60
|
+
import '../theme/palette-presets.js';
|
|
61
|
+
import '../theme/chart-palette.js';
|
|
62
|
+
import '../theme/theme-store.js';
|
|
63
|
+
import 'zustand/middleware';
|
|
64
|
+
import 'zustand';
|
|
58
65
|
import './complete-name-dialog.js';
|
|
59
66
|
import './forgot-password-form.js';
|
|
60
67
|
import './status-parts.js';
|
|
@@ -63,8 +70,15 @@ import './verify-email.js';
|
|
|
63
70
|
import './not-found-page.js';
|
|
64
71
|
import './accept-invitation.js';
|
|
65
72
|
import '../shell/company-switcher.js';
|
|
66
|
-
import '../
|
|
73
|
+
import '../account/email-change-setting.js';
|
|
74
|
+
import '../account/sessions-setting.js';
|
|
67
75
|
import '../lib/format.js';
|
|
76
|
+
import '../account/data-export-setting.js';
|
|
77
|
+
import '../components/account-chips.js';
|
|
78
|
+
import '../admin/admin-parts.js';
|
|
79
|
+
import '../admin/admin-action-log.js';
|
|
80
|
+
import '../settings/settings-labels.js';
|
|
81
|
+
import '../demo/demo-labels.js';
|
|
68
82
|
import '../components/reauth-dialog.js';
|
|
69
83
|
import '../components/server-wake.js';
|
|
70
84
|
import '../lib/server-wake.js';
|
|
@@ -78,9 +92,4 @@ import '../components/sign-chip.js';
|
|
|
78
92
|
import '../components/column-mapper.js';
|
|
79
93
|
import '../lib/column-mapping.js';
|
|
80
94
|
import '../lib/table-text.js';
|
|
81
|
-
import '../
|
|
82
|
-
import '../theme/palette-presets.js';
|
|
83
|
-
import '../theme/chart-palette.js';
|
|
84
|
-
import '../theme/theme-store.js';
|
|
85
|
-
import 'zustand/middleware';
|
|
86
|
-
import 'zustand';
|
|
95
|
+
import '../components/field-parts.js';
|
|
@@ -10,7 +10,7 @@ import { LanguageSelect } from "../components/language-select.js";
|
|
|
10
10
|
import { LegalAcceptCheckbox } from "../components/legal-page.js";
|
|
11
11
|
import { TextLink } from "../components/text-link.js";
|
|
12
12
|
import { Button, Input } from "../components/ui.js";
|
|
13
|
-
import { authErrorCode } from "./auth-errors.js";
|
|
13
|
+
import { authErrorCode, englishRateLimited, isRateLimited, retryAfterSeconds } from "./auth-errors.js";
|
|
14
14
|
import { emailParts, taggedEmail } from "./email-tag.js";
|
|
15
15
|
import { newPasswordProblem, personNameOk, PERSON_NAME_MAX_LENGTH } from "./form-rules.js";
|
|
16
16
|
import { NewPasswordFields } from "./new-password-fields.js";
|
|
@@ -33,7 +33,8 @@ const DEFAULT_REGISTER_LABELS = {
|
|
|
33
33
|
registrationClosed: "New accounts are by invitation only. Ask the operator to invite your email address.",
|
|
34
34
|
invitationInvalid: "This invitation link is not valid.",
|
|
35
35
|
invitationExpired: "This invitation has expired. Ask for a new one.",
|
|
36
|
-
failed: "Registration failed. Please try again."
|
|
36
|
+
failed: "Registration failed. Please try again.",
|
|
37
|
+
rateLimited: englishRateLimited
|
|
37
38
|
};
|
|
38
39
|
const ALL_CODES = KIT_LANGUAGES.map((language) => language.code);
|
|
39
40
|
function browserLanguages() {
|
|
@@ -44,6 +45,7 @@ function RegisterForm({
|
|
|
44
45
|
onSubmit,
|
|
45
46
|
emailTag,
|
|
46
47
|
invitedEmail,
|
|
48
|
+
defaultEmail,
|
|
47
49
|
locale: localeProp,
|
|
48
50
|
defaultLocale,
|
|
49
51
|
onLocaleChange,
|
|
@@ -67,7 +69,7 @@ function RegisterForm({
|
|
|
67
69
|
const invited = invitedEmail !== void 0;
|
|
68
70
|
const [firstName, setFirstName] = useState("");
|
|
69
71
|
const [lastName, setLastName] = useState("");
|
|
70
|
-
const [email, setEmail] = useState(invitedEmail ?? "");
|
|
72
|
+
const [email, setEmail] = useState(invitedEmail ?? defaultEmail ?? "");
|
|
71
73
|
const [password, setPassword] = useState("");
|
|
72
74
|
const [confirm, setConfirm] = useState("");
|
|
73
75
|
const [ownLocale, setOwnLocale] = useState(
|
|
@@ -129,7 +131,7 @@ function RegisterForm({
|
|
|
129
131
|
return;
|
|
130
132
|
}
|
|
131
133
|
setFailure(
|
|
132
|
-
hasMessage(own) ? own : code === "registration_closed" ? labels.registrationClosed : code === "invitation_invalid" ? labels.invitationInvalid : code === "invitation_expired" ? labels.invitationExpired : labels.failed
|
|
134
|
+
hasMessage(own) ? own : isRateLimited(error) ? (labels.rateLimited ?? DEFAULT_REGISTER_LABELS.rateLimited)(retryAfterSeconds(error)) : code === "registration_closed" ? labels.registrationClosed : code === "invitation_invalid" ? labels.invitationInvalid : code === "invitation_expired" ? labels.invitationExpired : labels.failed
|
|
133
135
|
);
|
|
134
136
|
}
|
|
135
137
|
);
|