@reblu/site-next 1.5.2 → 1.7.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 +120 -1
- package/dist/auth.cjs +89 -7
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +82 -2
- package/dist/auth.d.ts +82 -2
- package/dist/auth.js +84 -6
- package/dist/auth.js.map +1 -1
- package/dist/recaptcha.cjs +75 -0
- package/dist/recaptcha.cjs.map +1 -0
- package/dist/recaptcha.d.cts +264 -0
- package/dist/recaptcha.d.ts +264 -0
- package/dist/recaptcha.js +44 -0
- package/dist/recaptcha.js.map +1 -0
- package/package.json +14 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @reblu/site-next
|
|
2
2
|
|
|
3
|
-
Next.js glue for Reblu SITEs (REB-418, REB-510).
|
|
3
|
+
Next.js glue for Reblu SITEs (REB-418, REB-510). Five responsibilities, all
|
|
4
4
|
intentionally Next-coupled so the lower SDK layers —
|
|
5
5
|
[`@reblu/site-client`](../site-client), [`@reblu/site-contracts`](../site-contracts),
|
|
6
6
|
[`@reblu/site-ui`](../site-ui) — stay framework-agnostic:
|
|
@@ -15,6 +15,9 @@ intentionally Next-coupled so the lower SDK layers —
|
|
|
15
15
|
factory that proxies them to base-api's `/api/site/auth/*`.
|
|
16
16
|
4. **`<JsonLd>`** (`./json-ld`) — safe `application/ld+json` `<script>`
|
|
17
17
|
rendering; see "Structured data (JSON-LD)" below.
|
|
18
|
+
5. **reCAPTCHA v3** (`./recaptcha`) — the SITE's half of the contact-form
|
|
19
|
+
captcha contract: the canonical action, the render gate, and the
|
|
20
|
+
interaction-mount key. See "reCAPTCHA v3" below.
|
|
18
21
|
|
|
19
22
|
## Install
|
|
20
23
|
|
|
@@ -105,6 +108,27 @@ import { LoginForm, AuthLogo, mapLoginError } from '@reblu/site-next/auth'
|
|
|
105
108
|
| `AuthLogo` | — | Dynamic tenant logo, `next-themes`-driven |
|
|
106
109
|
| `mapLoginError` | — | Maps Supabase `?error=` to Spanish copy |
|
|
107
110
|
|
|
111
|
+
### Recovering from an expired magic link (REB-762 / REB-763)
|
|
112
|
+
|
|
113
|
+
A SITE may render `LoginForm` on the same page as `AuthCallback`, so someone who
|
|
114
|
+
arrives with a dead link can ask for a new one without leaving. Two things make
|
|
115
|
+
that path work, and both are on by default — no wiring in the SITE:
|
|
116
|
+
|
|
117
|
+
- **A successful request clears the callback's verdict.** `LoginForm` dispatches
|
|
118
|
+
the `reblu:magic-link-requested` DOM event on `window`; `AuthCallback` listens
|
|
119
|
+
and drops a terminal verdict about the old link ("Enlace caducado", "Enlace
|
|
120
|
+
inválido", or a failed exchange), rendering nothing so the form's own message
|
|
121
|
+
is the only one on screen. Both components stay independently usable: alone,
|
|
122
|
+
one dispatches to nobody and the other hears nothing. `emitMagicLinkRequested`
|
|
123
|
+
and `onMagicLinkRequested` are exported if a SITE wants to join in.
|
|
124
|
+
- **A 60-second cooldown on the form.** Supabase Auth drops a second magic link
|
|
125
|
+
requested inside its per-project "minimum interval per user" *before any email
|
|
126
|
+
is sent*, while the request route still answers 200. The success panel holds
|
|
127
|
+
the form for `MAGIC_LINK_COOLDOWN_SECONDS` with a visible countdown, then
|
|
128
|
+
offers "Pedir otro enlace". The interval is the Supabase project's setting,
|
|
129
|
+
not ours — `MAGIC_LINK_COOLDOWN_SECONDS` mirrors it in one place, and a SITE
|
|
130
|
+
whose project uses a different value passes `cooldownSeconds`.
|
|
131
|
+
|
|
108
132
|
`ResetPasswordConfirm`, `SetPassword`, and `AuthConfirm` read their own URL
|
|
109
133
|
params internally via `useSearchParams()`. Next.js requires that hook's
|
|
110
134
|
subtree to render inside a `<Suspense>` boundary — wrap the page/route that
|
|
@@ -158,6 +182,101 @@ must never drag in `CtaButtons`/`EditableCta`'s client-component graph.
|
|
|
158
182
|
Measured: `dist/json-ld.js` is 701 B (451 B gzipped) and its only runtime
|
|
159
183
|
import is `react/jsx-runtime` — no parser, no sanitiser chain.
|
|
160
184
|
|
|
185
|
+
## reCAPTCHA v3
|
|
186
|
+
|
|
187
|
+
`@reblu/site-next/recaptcha` (REB-588, zero dependencies, no `'use client'`) is
|
|
188
|
+
the SITE's half of the captcha contract. It does **not** load Google's script
|
|
189
|
+
and does **not** verify tokens — it makes the three decisions every SITE was
|
|
190
|
+
otherwise making alone and differently.
|
|
191
|
+
|
|
192
|
+
### Which half lives where
|
|
193
|
+
|
|
194
|
+
| | Where | Who holds it |
|
|
195
|
+
| --- | --- | --- |
|
|
196
|
+
| **Site key** | `/api/site/analytics` → the browser | Public by design. This module. |
|
|
197
|
+
| **Secret** | `site_integration_config.recaptchaSecretKey`, read only by base-api | **Never leaves base-api.** Not in this package, not in any `@reblu/site-*` package, not in the browser. |
|
|
198
|
+
| **Verification** | `POST /api/site/contact-form` → Google `siteverify` | base-api, per tenant. |
|
|
199
|
+
|
|
200
|
+
The SITE mints a token and posts it as `ContactFormInput.captcha`; base-api
|
|
201
|
+
decides. A SITE holding the secret would be a SITE that could be tricked into
|
|
202
|
+
approving its own tokens, and a distributable SDK is the worst possible place to
|
|
203
|
+
put a credential. **Nothing here calls `siteverify`.**
|
|
204
|
+
|
|
205
|
+
### Should this form render at all?
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
import {
|
|
209
|
+
isContactFormAvailable,
|
|
210
|
+
resolveContactFormAvailability,
|
|
211
|
+
} from '@reblu/site-next/recaptcha'
|
|
212
|
+
|
|
213
|
+
const availability = resolveContactFormAvailability(analytics)
|
|
214
|
+
if (!isContactFormAvailable(availability)) return <ContactDetailsOnly />
|
|
215
|
+
|
|
216
|
+
// availability.recaptchaSiteKey is a confirmed, usable key
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
An **allow-list** (`DB_ONLY`, `WEBHOOK`, `RESEND`), never `mode !== 'DISABLED'`,
|
|
220
|
+
held to the same members in the same order as base-api's own list by a drift
|
|
221
|
+
guard (`apps/base-api/tests/unit/lib/contracts/contact-form-mode-drift.test.ts`).
|
|
222
|
+
The deny-list version is a real incident: it rendered a form for
|
|
223
|
+
`NOT_CONFIGURED` too — which is what every freshly provisioned tenant carries
|
|
224
|
+
until someone finishes the wizard (REB-586) — so a visitor typed out a medical
|
|
225
|
+
enquiry, pressed send, and got `503 Contact form is disabled`. They learned the
|
|
226
|
+
clinic could not be reached only after writing to it. An unknown mode, a mode
|
|
227
|
+
from a base-api newer than your SDK, and a config read that threw all resolve to
|
|
228
|
+
the same answer: no form. The page still shows the phone and the address.
|
|
229
|
+
|
|
230
|
+
The key is checked too, because `ContactFormInput.captcha` is required: an
|
|
231
|
+
accepting mode with no usable key is a form whose every submission is dead on
|
|
232
|
+
arrival. `missing-recaptcha-key` and `malformed-recaptcha-key` are separate
|
|
233
|
+
reasons on purpose — one owner never filled the field, the other filled it with
|
|
234
|
+
the wrong thing, and as of REB-588 **nothing validates that column** in nexus, on
|
|
235
|
+
the way out of `/api/site/analytics`, or in the response contract.
|
|
236
|
+
|
|
237
|
+
### Load Google only when someone actually uses the form
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
import { RECAPTCHA_ACTION, deferredRecaptchaKey } from '@reblu/site-next/recaptcha'
|
|
241
|
+
|
|
242
|
+
const [armed, setArmed] = useState(false)
|
|
243
|
+
|
|
244
|
+
<GoogleReCaptchaProvider reCaptchaKey={deferredRecaptchaKey(siteKey, armed)}>
|
|
245
|
+
<form onFocusCapture={() => setArmed(true)}>…</form>
|
|
246
|
+
</GoogleReCaptchaProvider>
|
|
247
|
+
|
|
248
|
+
// at submit: await executeRecaptcha(RECAPTCHA_ACTION)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`GoogleReCaptchaProvider` loads `api.js` the moment it mounts, so mounting it
|
|
252
|
+
with the form means every visitor who merely lands on the page to read the phone
|
|
253
|
+
number has contacted Google. `deferredRecaptchaKey` returns `''` until `armed`,
|
|
254
|
+
and an empty key mounts the provider while injecting nothing; swapping the real
|
|
255
|
+
key in re-runs the library's effect and injects it then, **without remounting
|
|
256
|
+
the children** — so the visitor never loses focus or a half-typed word.
|
|
257
|
+
|
|
258
|
+
Both of those are claims about the vendor's code, so they are pinned by
|
|
259
|
+
`tests/recaptcha-deferred-oracle.test.tsx`, which drives the real
|
|
260
|
+
`react-google-recaptcha-v3` and counts script tags in the DOM. If a future
|
|
261
|
+
version injects eagerly, that test goes red rather than this README quietly
|
|
262
|
+
becoming false.
|
|
263
|
+
|
|
264
|
+
**This is not cookie consent.** Deferring the script narrows the exposure; it
|
|
265
|
+
does not gate it on a lawful basis. That is REB-642.
|
|
266
|
+
|
|
267
|
+
`RECAPTCHA_ACTION` is exported because reCAPTCHA v3 scores **per action**: two
|
|
268
|
+
SITEs calling `executeRecaptcha('submit_form')` and `'contact_form'` build two
|
|
269
|
+
unrelated score histories for the same feature.
|
|
270
|
+
|
|
271
|
+
It is **not a security control.** base-api's `serverCaptchaVerify` checks only
|
|
272
|
+
`success` and `score`, and never reads the `action` Google returns — so a token
|
|
273
|
+
minted for a different action on the same site verifies exactly as well. Binding
|
|
274
|
+
the token to this form needs base-api to assert `action === RECAPTCHA_ACTION`;
|
|
275
|
+
until it does, treat the constant as consistency, not enforcement.
|
|
276
|
+
|
|
277
|
+
`react-google-recaptcha-v3` stays **your** dependency — the subpath imports
|
|
278
|
+
nothing at all, guarded by `tests/recaptcha-subpath-isolation.test.ts`.
|
|
279
|
+
|
|
161
280
|
## Build
|
|
162
281
|
|
|
163
282
|
`tsup` → dual ESM/CJS + `.d.ts`. Code-splitting is **off** so each entry keeps its
|
package/dist/auth.cjs
CHANGED
|
@@ -35,10 +35,14 @@ __export(auth_exports, {
|
|
|
35
35
|
AuthConfirm: () => AuthConfirm,
|
|
36
36
|
AuthLogo: () => AuthLogo,
|
|
37
37
|
LoginForm: () => LoginForm,
|
|
38
|
+
MAGIC_LINK_COOLDOWN_SECONDS: () => MAGIC_LINK_COOLDOWN_SECONDS,
|
|
39
|
+
MAGIC_LINK_REQUESTED_EVENT: () => MAGIC_LINK_REQUESTED_EVENT,
|
|
38
40
|
ResetPasswordConfirm: () => ResetPasswordConfirm,
|
|
39
41
|
ResetPasswordRequest: () => ResetPasswordRequest,
|
|
40
42
|
SetPassword: () => SetPassword,
|
|
41
|
-
|
|
43
|
+
emitMagicLinkRequested: () => emitMagicLinkRequested,
|
|
44
|
+
mapLoginError: () => mapLoginError,
|
|
45
|
+
onMagicLinkRequested: () => onMagicLinkRequested
|
|
42
46
|
});
|
|
43
47
|
module.exports = __toCommonJS(auth_exports);
|
|
44
48
|
|
|
@@ -58,6 +62,27 @@ function isSafeRedirectUrl(url) {
|
|
|
58
62
|
return ["http", "https"].includes(scheme) || clean.startsWith("/");
|
|
59
63
|
}
|
|
60
64
|
|
|
65
|
+
// src/auth/magic-link-cooldown.ts
|
|
66
|
+
var MAGIC_LINK_COOLDOWN_SECONDS = 60;
|
|
67
|
+
|
|
68
|
+
// src/auth/magic-link-events.ts
|
|
69
|
+
var MAGIC_LINK_REQUESTED_EVENT = "reblu:magic-link-requested";
|
|
70
|
+
function emitMagicLinkRequested() {
|
|
71
|
+
if (typeof window === "undefined") return;
|
|
72
|
+
window.dispatchEvent(new CustomEvent(MAGIC_LINK_REQUESTED_EVENT));
|
|
73
|
+
}
|
|
74
|
+
function onMagicLinkRequested(listener) {
|
|
75
|
+
if (typeof window === "undefined") return () => {
|
|
76
|
+
};
|
|
77
|
+
const handler = () => {
|
|
78
|
+
listener();
|
|
79
|
+
};
|
|
80
|
+
window.addEventListener(MAGIC_LINK_REQUESTED_EVENT, handler);
|
|
81
|
+
return () => {
|
|
82
|
+
window.removeEventListener(MAGIC_LINK_REQUESTED_EVENT, handler);
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
61
86
|
// src/auth/login-form.tsx
|
|
62
87
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
63
88
|
var DEFAULT_COPY = {
|
|
@@ -70,8 +95,15 @@ var DEFAULT_COPY = {
|
|
|
70
95
|
passwordSubmitCta: "Iniciar sesi\xF3n",
|
|
71
96
|
forgotPasswordCta: "\xBFOlvidaste tu contrase\xF1a?",
|
|
72
97
|
magicLinkSubmitCta: "Enviar enlace de acceso",
|
|
73
|
-
magicLinkSuccessTitle: "
|
|
74
|
-
|
|
98
|
+
magicLinkSuccessTitle: "Revisa tu correo",
|
|
99
|
+
// True in every branch, and leaks nothing: the request path answers an
|
|
100
|
+
// identical 200 whether or not the account exists, and whether or not an
|
|
101
|
+
// email actually went out (REB-762). "¡Enlace enviado!" asserted a send that
|
|
102
|
+
// may never have happened — the user then waited for something that was not
|
|
103
|
+
// coming.
|
|
104
|
+
magicLinkSuccessMessage: "Si la cuenta existe, recibir\xE1s un enlace en unos minutos. Si ya lo has pedido hace poco, espera antes de volver a intentarlo.",
|
|
105
|
+
magicLinkCooldownMessage: "Podr\xE1s pedir otro enlace en {seconds} s.",
|
|
106
|
+
magicLinkResendCta: "Pedir otro enlace",
|
|
75
107
|
mfaTitle: "Verificaci\xF3n en dos pasos",
|
|
76
108
|
mfaMessage: "Introduce el c\xF3digo de 6 d\xEDgitos de tu aplicaci\xF3n autenticadora.",
|
|
77
109
|
mfaCodeLabel: "C\xF3digo de verificaci\xF3n",
|
|
@@ -95,6 +127,7 @@ function LoginForm(props) {
|
|
|
95
127
|
copy,
|
|
96
128
|
className,
|
|
97
129
|
defaultMethod = "password",
|
|
130
|
+
cooldownSeconds = MAGIC_LINK_COOLDOWN_SECONDS,
|
|
98
131
|
onPasswordSubmitted,
|
|
99
132
|
onMagicLinkSubmitted,
|
|
100
133
|
onMfaSubmitted
|
|
@@ -107,12 +140,37 @@ function LoginForm(props) {
|
|
|
107
140
|
const [password, setPassword] = React.useState("");
|
|
108
141
|
const [mfa, setMfa] = React.useState(null);
|
|
109
142
|
const [mfaCode, setMfaCode] = React.useState("");
|
|
143
|
+
const [cooldownLeft, setCooldownLeft] = React.useState(0);
|
|
144
|
+
const successHeadingRef = React.useRef(null);
|
|
145
|
+
const magicEmailRef = React.useRef(null);
|
|
146
|
+
const returningFromSuccessRef = React.useRef(false);
|
|
147
|
+
const isCoolingDown = cooldownLeft > 0;
|
|
148
|
+
React.useEffect(() => {
|
|
149
|
+
if (!isCoolingDown) return;
|
|
150
|
+
const id = setInterval(() => {
|
|
151
|
+
setCooldownLeft((s) => Math.max(0, s - 1));
|
|
152
|
+
}, 1e3);
|
|
153
|
+
return () => {
|
|
154
|
+
clearInterval(id);
|
|
155
|
+
};
|
|
156
|
+
}, [isCoolingDown]);
|
|
157
|
+
React.useEffect(() => {
|
|
158
|
+
if (state === "magic-link-success") successHeadingRef.current?.focus();
|
|
159
|
+
if (state === "idle" && returningFromSuccessRef.current) {
|
|
160
|
+
returningFromSuccessRef.current = false;
|
|
161
|
+
magicEmailRef.current?.focus();
|
|
162
|
+
}
|
|
163
|
+
}, [state]);
|
|
110
164
|
const resetTransientState = React.useCallback(() => {
|
|
111
165
|
setState("idle");
|
|
112
166
|
setError(null);
|
|
113
167
|
setMfa(null);
|
|
114
168
|
setMfaCode("");
|
|
115
169
|
}, []);
|
|
170
|
+
const handleResend = () => {
|
|
171
|
+
returningFromSuccessRef.current = true;
|
|
172
|
+
resetTransientState();
|
|
173
|
+
};
|
|
116
174
|
const handleTabChange = (next) => {
|
|
117
175
|
if (next !== "magic-link" && next !== "password") return;
|
|
118
176
|
setMethod(next);
|
|
@@ -137,6 +195,8 @@ function LoginForm(props) {
|
|
|
137
195
|
});
|
|
138
196
|
if (res.ok) {
|
|
139
197
|
setState("magic-link-success");
|
|
198
|
+
setCooldownLeft(cooldownSeconds);
|
|
199
|
+
emitMagicLinkRequested();
|
|
140
200
|
onMagicLinkSubmitted?.(trimmed);
|
|
141
201
|
return;
|
|
142
202
|
}
|
|
@@ -279,9 +339,18 @@ function LoginForm(props) {
|
|
|
279
339
|
] });
|
|
280
340
|
}
|
|
281
341
|
if (state === "magic-link-success") {
|
|
282
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: (0, import_site_ui.cn)("space-y-
|
|
283
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "text-xl font-semibold", children: c.magicLinkSuccessTitle }),
|
|
284
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-muted-foreground", children: c.magicLinkSuccessMessage })
|
|
342
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: (0, import_site_ui.cn)("space-y-3 text-center", className), children: [
|
|
343
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "text-xl font-semibold", ref: successHeadingRef, tabIndex: -1, children: c.magicLinkSuccessTitle }),
|
|
344
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-muted-foreground", children: c.magicLinkSuccessMessage }),
|
|
345
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "status", "aria-live": "polite", className: "sr-only", children: cooldownLeft > 0 ? "" : c.magicLinkResendCta }),
|
|
346
|
+
cooldownLeft > 0 ? (
|
|
347
|
+
// A ticking number is VISUAL. Every text mutation inside a live
|
|
348
|
+
// region is a separate announcement, so marking this one would queue
|
|
349
|
+
// ~60 polite interruptions; the heading above already took focus and
|
|
350
|
+
// spoke the new state once. The only transition worth announcing is
|
|
351
|
+
// the end of the wait, which the region above carries.
|
|
352
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-muted-foreground text-sm", children: c.magicLinkCooldownMessage.replaceAll("{seconds}", String(cooldownLeft)) })
|
|
353
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_site_ui.Button, { type: "button", variant: "outline", onClick: handleResend, children: c.magicLinkResendCta })
|
|
285
354
|
] });
|
|
286
355
|
}
|
|
287
356
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: (0, import_site_ui.cn)("space-y-6", className), children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_tabs.Tabs, { value: method, onValueChange: handleTabChange, children: [
|
|
@@ -341,6 +410,7 @@ function LoginForm(props) {
|
|
|
341
410
|
import_site_ui.Input,
|
|
342
411
|
{
|
|
343
412
|
id: "login-email-magic",
|
|
413
|
+
ref: magicEmailRef,
|
|
344
414
|
type: "email",
|
|
345
415
|
autoComplete: "email",
|
|
346
416
|
placeholder: c.emailPlaceholder,
|
|
@@ -361,6 +431,7 @@ function LoginForm(props) {
|
|
|
361
431
|
var React2 = __toESM(require("react"), 1);
|
|
362
432
|
var import_site_ui2 = require("@reblu/site-ui");
|
|
363
433
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
434
|
+
var TERMINAL_FAILURE_STATES = ["expired", "network", "malformed"];
|
|
364
435
|
var DEFAULT_COPY2 = {
|
|
365
436
|
loadingTitle: "Autenticando...",
|
|
366
437
|
loadingMessage: "Estamos verificando tu identidad.",
|
|
@@ -472,7 +543,14 @@ function AuthCallback({
|
|
|
472
543
|
if (state === "network") retryRef.current?.focus();
|
|
473
544
|
if (state === "expired" || state === "malformed") linkRef.current?.focus();
|
|
474
545
|
}, [state]);
|
|
546
|
+
React2.useEffect(
|
|
547
|
+
() => onMagicLinkRequested(() => {
|
|
548
|
+
setState((current) => TERMINAL_FAILURE_STATES.includes(current) ? "stale" : current);
|
|
549
|
+
}),
|
|
550
|
+
[]
|
|
551
|
+
);
|
|
475
552
|
const isLoading = state === "mounting" || state === "parsing" || state === "exchanging";
|
|
553
|
+
if (state === "stale") return null;
|
|
476
554
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("main", { className: (0, import_site_ui2.cn)("flex min-h-screen items-center justify-center", className), children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { "aria-live": "polite", className: "w-full max-w-md space-y-4 p-6 text-center", children: [
|
|
477
555
|
isLoading && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { children: [
|
|
478
556
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { className: "text-xl font-semibold", children: c.loadingTitle }),
|
|
@@ -1093,9 +1171,13 @@ function AuthConfirm(props) {
|
|
|
1093
1171
|
AuthConfirm,
|
|
1094
1172
|
AuthLogo,
|
|
1095
1173
|
LoginForm,
|
|
1174
|
+
MAGIC_LINK_COOLDOWN_SECONDS,
|
|
1175
|
+
MAGIC_LINK_REQUESTED_EVENT,
|
|
1096
1176
|
ResetPasswordConfirm,
|
|
1097
1177
|
ResetPasswordRequest,
|
|
1098
1178
|
SetPassword,
|
|
1099
|
-
|
|
1179
|
+
emitMagicLinkRequested,
|
|
1180
|
+
mapLoginError,
|
|
1181
|
+
onMagicLinkRequested
|
|
1100
1182
|
});
|
|
1101
1183
|
//# sourceMappingURL=auth.cjs.map
|