@reblu/site-next 1.6.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +464 -0
- package/README.md +21 -0
- 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/package.json +6 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
# @reblu/site-next
|
|
2
|
+
|
|
3
|
+
## 1.7.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 9f54c58: REB-774 — ship `CHANGELOG.md` inside the published npm tarball for every
|
|
8
|
+
`@reblu/site-*` package (verified via `pnpm pack`: it was previously absent,
|
|
9
|
+
since `"files": ["dist"]` limited the tarball to `dist/` and npm only
|
|
10
|
+
auto-includes `README.md`, `LICENSE` and `package.json`). Consumers can now
|
|
11
|
+
read the changelog for the version they installed straight from
|
|
12
|
+
`node_modules/@reblu/<pkg>/CHANGELOG.md`.
|
|
13
|
+
- Updated dependencies [9f54c58]
|
|
14
|
+
- Updated dependencies [9f54c58]
|
|
15
|
+
- @reblu/site-contracts@1.0.1
|
|
16
|
+
- @reblu/site-client@5.0.1
|
|
17
|
+
- @reblu/site-ui@0.5.1
|
|
18
|
+
|
|
19
|
+
## 1.7.0
|
|
20
|
+
|
|
21
|
+
### Minor Changes
|
|
22
|
+
|
|
23
|
+
- deda0c0: REB-762 / REB-763 — a user who asks for a new magic link is told the truth, once
|
|
24
|
+
|
|
25
|
+
Expired link → "Enlace caducado" → re-request from the form on that same page →
|
|
26
|
+
"enlace enviado" appears while "Enlace caducado" is still on screen, unchanged.
|
|
27
|
+
The expired notice is the more alarming of the two, so it is the one believed,
|
|
28
|
+
so the user requests again — within Supabase's 60-second per-user interval,
|
|
29
|
+
which drops the second link before any email is sent while our route answers a
|
|
30
|
+
cheerful 200. Nothing arrives, the screen still says expired, and round it goes.
|
|
31
|
+
|
|
32
|
+
Two changes close the loop:
|
|
33
|
+
- `AuthCallback` drops its verdict about the old link when a new one is
|
|
34
|
+
requested. `LoginForm` dispatches `reblu:magic-link-requested` on `window` and
|
|
35
|
+
the callback listens. This covers the whole class, not the reported case: an
|
|
36
|
+
expired link, an invalid one, an already-used one, and a failed exchange all
|
|
37
|
+
clear. The stale notice is REMOVED rather than dimmed — a greyed-out alarm is
|
|
38
|
+
still read as current.
|
|
39
|
+
- `LoginForm` holds its success panel for a 60-second cooldown with a visible
|
|
40
|
+
countdown, then offers "Pedir otro enlace". There is no submit button to press
|
|
41
|
+
during the window in which Supabase would silently drop the request. It leaks
|
|
42
|
+
nothing: the UI behaves identically for a real and a non-existent address.
|
|
43
|
+
|
|
44
|
+
Copy is now true in both branches. `magicLinkSuccessTitle` is "Revisa tu correo"
|
|
45
|
+
(was "¡Enlace enviado!") and `magicLinkSuccessMessage` no longer asserts a send
|
|
46
|
+
that may never have happened. base-api's `/api/site/auth/magic-link-request`
|
|
47
|
+
matches, in every branch including the deliberate silent 200 on our own IP
|
|
48
|
+
limiter — that silence stays, it is what stops a 429 from being an
|
|
49
|
+
account-existence oracle; only the lie is gone.
|
|
50
|
+
|
|
51
|
+
New public surface, all additive:
|
|
52
|
+
- `LoginForm` gains an optional `cooldownSeconds` prop and two copy keys,
|
|
53
|
+
`magicLinkCooldownMessage` (with a `{seconds}` placeholder) and
|
|
54
|
+
`magicLinkResendCta`. No existing prop changed shape. `AuthCallback`'s props
|
|
55
|
+
are untouched, so an already-published SITE page picks the fix up by
|
|
56
|
+
upgrading the package alone.
|
|
57
|
+
- `MAGIC_LINK_COOLDOWN_SECONDS`, `MAGIC_LINK_REQUESTED_EVENT`,
|
|
58
|
+
`emitMagicLinkRequested`, `onMagicLinkRequested`.
|
|
59
|
+
|
|
60
|
+
**Operators:** 60 is a MIRROR of the Supabase project's "minimum interval per
|
|
61
|
+
user", which we neither own nor can read at runtime — it is measured at 60 s on
|
|
62
|
+
the Dermamedicin project and set to 1 s on the local tenant stack, so the two
|
|
63
|
+
already differ by environment. It is declared once, in
|
|
64
|
+
`MAGIC_LINK_COOLDOWN_SECONDS`. If a project's interval changes, pass
|
|
65
|
+
`cooldownSeconds` for that SITE rather than adding a second literal.
|
|
66
|
+
|
|
67
|
+
### Patch Changes
|
|
68
|
+
|
|
69
|
+
- Updated dependencies [340db30]
|
|
70
|
+
- @reblu/site-contracts@1.0.0
|
|
71
|
+
- @reblu/site-client@5.0.0
|
|
72
|
+
|
|
73
|
+
## 1.6.0
|
|
74
|
+
|
|
75
|
+
### Minor Changes
|
|
76
|
+
|
|
77
|
+
- 0e0a684: Add `@reblu/site-next/recaptcha` (REB-588) — the SITE's half of the contact-form captcha
|
|
78
|
+
contract, so no SITE re-implements Google's flow or verifies a token wrongly.
|
|
79
|
+
|
|
80
|
+
## Why this exists
|
|
81
|
+
|
|
82
|
+
There were four copies of this flow. `apps/base-api` had one, `apps/demo-site` had another,
|
|
83
|
+
and the external `reblu-site-dermamedicin` repo had a third plus its own availability gate.
|
|
84
|
+
They had already drifted: one called `executeRecaptcha('submit_form')`, another
|
|
85
|
+
`'contact_form'`; one carried a retry loop and the others did not; one mounted the provider
|
|
86
|
+
eagerly, one on scroll, one on interaction. reCAPTCHA v3 scores **per action**, so two SITEs
|
|
87
|
+
naming the action differently are building two unrelated score histories for the same
|
|
88
|
+
feature — and a defect fixed in one copy stays live in the other three.
|
|
89
|
+
|
|
90
|
+
An external SITE repo could not share any of it. It can install `@reblu/site-*`; it cannot
|
|
91
|
+
import `apps/base-api/src/components/`, and `@reblu/ui` is `"private": true` so it is not
|
|
92
|
+
publishable and never will be the home for something an external SITE needs. The SDK is the
|
|
93
|
+
only place a decision like this gets made once.
|
|
94
|
+
|
|
95
|
+
## What ships
|
|
96
|
+
- `RECAPTCHA_ACTION` — the one action name. `contact_form` over `submit_form` because it
|
|
97
|
+
names the form rather than the verb; a second protected form needs its own action, and
|
|
98
|
+
`submit_form` would have nothing left to call it. It buys comparable scores and nothing
|
|
99
|
+
more: base-api's `serverCaptchaVerify` checks only `success` and `score` and never reads
|
|
100
|
+
the `action` Google returns, so a token minted for another action verifies just as well.
|
|
101
|
+
Documented in place so nobody mistakes it for enforcement; binding it is a follow-up on
|
|
102
|
+
base-api.
|
|
103
|
+
- `resolveContactFormAvailability(analytics)` / `isContactFormAvailable(a)` — the render
|
|
104
|
+
gate, plus `ContactFormAvailability` and `ContactFormModeExhaustiveCheck`.
|
|
105
|
+
- `deferredRecaptchaKey(siteKey, armed)` — the interaction-mount key.
|
|
106
|
+
- `isSafeRecaptchaSiteKey` / `RECAPTCHA_SITE_KEY_PATTERN` — the site-key shape guard.
|
|
107
|
+
|
|
108
|
+
## Which half lives where — the security split, stated
|
|
109
|
+
|
|
110
|
+
The **site key** is public by design: it travels to the browser inside the script URL, and
|
|
111
|
+
this module handles it. The **secret** never leaves base-api — it is stored per tenant in
|
|
112
|
+
`site_integration_config.recaptchaSecretKey` and read only by `serverCaptchaVerify` when
|
|
113
|
+
`POST /api/site/contact-form` runs.
|
|
114
|
+
|
|
115
|
+
**Nothing in this module, this package, or any `@reblu/site-*` package ever sees the secret
|
|
116
|
+
or calls Google's `siteverify`.** A SITE holding the secret would be a SITE that could be
|
|
117
|
+
tricked into approving its own tokens, and a distributable SDK is the worst possible place
|
|
118
|
+
to put a credential. The SITE mints a token and posts it as `ContactFormInput.captcha`;
|
|
119
|
+
base-api decides. That half already worked, and this changeset does not touch it.
|
|
120
|
+
|
|
121
|
+
## The gate is an allow-list, and that is not a style preference
|
|
122
|
+
|
|
123
|
+
`resolveContactFormAvailability` accepts `DB_ONLY`, `WEBHOOK` and `RESEND`, never
|
|
124
|
+
`mode !== 'DISABLED'`. The deny-list version is a real incident: it rendered a form for
|
|
125
|
+
`NOT_CONFIGURED` too — which is what every freshly provisioned tenant carries until someone
|
|
126
|
+
finishes the wizard (REB-586) — so a visitor typed out a medical enquiry, pressed send, and
|
|
127
|
+
got `503 Contact form is disabled`. They learned the clinic could not be reached only after
|
|
128
|
+
writing to it, which is the worst possible order. An unknown mode, a mode from a base-api
|
|
129
|
+
newer than your SDK, and a config read that threw all resolve to the same answer: no form.
|
|
130
|
+
The page still shows the phone, the email and the address.
|
|
131
|
+
|
|
132
|
+
The key is checked for the same reason. `ContactFormInput.captcha` is required, so an
|
|
133
|
+
operational mode with no usable key is a form whose every submission is dead on arrival.
|
|
134
|
+
`missing-recaptcha-key` and `malformed-recaptcha-key` are separate reasons because they call
|
|
135
|
+
for different fixes by different people.
|
|
136
|
+
|
|
137
|
+
The list is a deliberate duplicate of base-api's `CONTACT_FORM_OPERATIONAL_MODES` — neither
|
|
138
|
+
side may import the other across that trust boundary. **This change does not delete the
|
|
139
|
+
existing copies**, so the list is now written three times: base-api's (authoritative), the
|
|
140
|
+
SDK's, and `apps/demo-site/src/lib/contact-form-mode.ts` (the reference SITE, not yet
|
|
141
|
+
migrated). All three are pinned by
|
|
142
|
+
`apps/base-api/tests/unit/lib/contracts/contact-form-mode-drift.test.ts`, on the same
|
|
143
|
+
pattern as REB-617's analytics-id guard: same members and same order for the two ordered
|
|
144
|
+
lists, same member set for demo-site's `Record`.
|
|
145
|
+
|
|
146
|
+
The type system does not cover this. The SDK's compile-time exhaustiveness check fires only
|
|
147
|
+
when the `ContactFormMode` union GAINS a member, and is blind to base-api NARROWING which
|
|
148
|
+
modes it accepts. Dropping `WEBHOOK` from base-api would otherwise leave every SITE
|
|
149
|
+
rendering a form base-api has stopped accepting — the REB-586 incident, reintroduced
|
|
150
|
+
through the back door. The SDK and base-api also now share one name for the concept; a
|
|
151
|
+
second name for one thing is how the extra copies got made in the first place.
|
|
152
|
+
|
|
153
|
+
## The site-key guard, and its deliberate looseness
|
|
154
|
+
|
|
155
|
+
`recaptchaSiteKey` is the only tenant-controlled field on `/api/site/analytics` with **no
|
|
156
|
+
validation at any layer** — not the nexus form, not the route handler, not the response
|
|
157
|
+
contract. `gaId` and `clarityId` got all three in REB-617; this one was left behind. Its
|
|
158
|
+
severity is lower (the value lands in a script `src` query parameter, not an executing
|
|
159
|
+
script body, so it is not the same XSS sink), but the reliability hole is real: a mistyped
|
|
160
|
+
key renders a form that looks live and silently eats every enquiry.
|
|
161
|
+
|
|
162
|
+
So the SDK re-validates on read, exactly as `@reblu/site-next/analytics` already does for
|
|
163
|
+
the other two — base-api is not the only thing a SITE ever talks to. The pattern is a
|
|
164
|
+
**sink-safety** guard, not a guess at Google's key format: it excludes spaces, quotes,
|
|
165
|
+
newlines, `&` and `#`, and its length bounds (16–128) are far wider than the 40 characters
|
|
166
|
+
Google issues. A bound tight enough to be a real format check would be a bound tight enough
|
|
167
|
+
to take a working tenant offline the day Google changes its key length. The right place to
|
|
168
|
+
tighten is the nexus write path, where an owner can still see and fix the error.
|
|
169
|
+
|
|
170
|
+
## Interaction-mount, verified against the vendor rather than asserted
|
|
171
|
+
|
|
172
|
+
`GoogleReCaptchaProvider` loads `api.js` the moment it mounts, so mounting it with the form
|
|
173
|
+
means every visitor who merely lands on the page to read the phone number has contacted
|
|
174
|
+
Google — on a clinic's contact page, a third-party request nobody asked for, on a route
|
|
175
|
+
whose whole subject is personal data.
|
|
176
|
+
|
|
177
|
+
`deferredRecaptchaKey` returns `''` until `armed`. An empty key mounts the provider and
|
|
178
|
+
injects nothing; swapping the real key in re-runs the library's effect and injects it then,
|
|
179
|
+
**without remounting the provider's children**, so the visitor does not lose focus or a
|
|
180
|
+
half-typed word at the instant the script arrives.
|
|
181
|
+
|
|
182
|
+
Both of those are claims about somebody else's code, and prose asserting them is worthless —
|
|
183
|
+
the library could inject eagerly in its next version and every comment here would quietly
|
|
184
|
+
become a lie while CI stayed green. So `tests/recaptcha-deferred-oracle.test.tsx` drives the
|
|
185
|
+
**real** `react-google-recaptcha-v3` and counts `<script>` tags in the DOM, including a
|
|
186
|
+
control proving a key supplied on the first render _does_ inject. If interaction-mount ever
|
|
187
|
+
stops working, that test goes red.
|
|
188
|
+
|
|
189
|
+
**This is not cookie consent.** Deferring the script narrows the exposure; it does not gate
|
|
190
|
+
it on a lawful basis. That remains REB-642, and this module says so where someone reaching
|
|
191
|
+
for it will read it.
|
|
192
|
+
|
|
193
|
+
## Zero dependencies, and no `'use client'`
|
|
194
|
+
|
|
195
|
+
The subpath imports **nothing** — not React, not `next`, and notably not
|
|
196
|
+
`react-google-recaptcha-v3`, which stays the consuming SITE's dependency. It also carries no
|
|
197
|
+
`'use client'` directive, because the same functions run on both sides of the RSC boundary:
|
|
198
|
+
the server page resolves availability, the client component arms the key.
|
|
199
|
+
|
|
200
|
+
Both properties are gated by `tests/recaptcha-subpath-isolation.test.ts`, which runs a real
|
|
201
|
+
tsup build and asserts on the emitted bytes — the same enforcement `./cache` (REB-489),
|
|
202
|
+
`./json-ld` (REB-622) and `./analytics` (REB-617) already have.
|
|
203
|
+
|
|
204
|
+
## Adoption
|
|
205
|
+
|
|
206
|
+
Additive — no existing export changes, nothing breaks. SITE repos replace their hand-written
|
|
207
|
+
availability check and provider wiring with `resolveContactFormAvailability` +
|
|
208
|
+
`deferredRecaptchaKey`, and switch `executeRecaptcha(...)` to `RECAPTCHA_ACTION`. Note that
|
|
209
|
+
switching the action re-baselines that SITE's reCAPTCHA score history, because Google charts
|
|
210
|
+
each action separately.
|
|
211
|
+
|
|
212
|
+
`apps/demo-site` is **not** migrated here — its form is a ~500-line hand-written component
|
|
213
|
+
built on `@reblu/ui` and its own schema, and folding that into this change would bury the
|
|
214
|
+
SDK surface in an unrelated refactor. Its `contact-form-mode.ts` is re-labelled instead: it
|
|
215
|
+
used to call itself "the template every external SITE repo copies", which this change makes
|
|
216
|
+
wrong, since it lacks the site-key check and its page gate only tests `!!recaptchaSiteKey`.
|
|
217
|
+
Migrating it is a follow-up.
|
|
218
|
+
|
|
219
|
+
## 1.5.2
|
|
220
|
+
|
|
221
|
+
### Patch Changes
|
|
222
|
+
|
|
223
|
+
- Updated dependencies [65e124a]
|
|
224
|
+
- @reblu/site-ui@0.5.0
|
|
225
|
+
|
|
226
|
+
## 1.5.1
|
|
227
|
+
|
|
228
|
+
### Patch Changes
|
|
229
|
+
|
|
230
|
+
- Updated dependencies [e5cda17]
|
|
231
|
+
- Updated dependencies [7dcfb4f]
|
|
232
|
+
- @reblu/site-ui@0.4.0
|
|
233
|
+
- @reblu/site-contracts@0.5.0
|
|
234
|
+
- @reblu/site-client@4.0.0
|
|
235
|
+
|
|
236
|
+
## 1.5.0
|
|
237
|
+
|
|
238
|
+
### Minor Changes
|
|
239
|
+
|
|
240
|
+
- 39ea223: Add `<AnalyticsScripts>` and the analytics id validators (`@reblu/site-next/analytics`)
|
|
241
|
+
— a safe renderer for tenant-configured Google Analytics and Microsoft Clarity tags.
|
|
242
|
+
|
|
243
|
+
Every SITE built on this SDK interpolated the tenant's analytics ids straight into
|
|
244
|
+
the body of an executing `<script>`:
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
gtag('config','${analytics.gaId}');
|
|
248
|
+
…"clarity","script","${analytics.clarityId}");
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
No validation, no escaping, no sanitiser anywhere in the path. A `gaId` of
|
|
252
|
+
`x');alert(document.cookie);//` closes the string literal, closes the call,
|
|
253
|
+
comments out the remainder, and runs arbitrary JS on the tenant's OWN ORIGIN — on
|
|
254
|
+
every page including checkout, same origin as the portal session cookie. That is
|
|
255
|
+
the same trust boundary REB-613 closed on the sink next door (nexus-authored HTML
|
|
256
|
+
reaching `dangerouslySetInnerHTML`), except this one reached the JS interpreter
|
|
257
|
+
directly.
|
|
258
|
+
|
|
259
|
+
`<AnalyticsScripts gaId clarityId />` applies two independent layers:
|
|
260
|
+
1. **Validation** — each id must match its vendor-documented shape
|
|
261
|
+
(`GA_MEASUREMENT_ID_PATTERN` = `/^G-[A-Z0-9]{1,32}$/`, per Google Analytics Help
|
|
262
|
+
12270356; `CLARITY_PROJECT_ID_PATTERN` = `/^[a-z0-9]{1,32}$/`, per Microsoft
|
|
263
|
+
Learn's published demo project `3t0wlogvdz`). A non-matching id renders NO
|
|
264
|
+
script at all — analytics silently does not load and the site keeps serving.
|
|
265
|
+
No minimum length is invented: Google's own docs print the short `G-12345`.
|
|
266
|
+
2. **Escaping** — the id is emitted as a JSON string literal through the existing
|
|
267
|
+
`jsonLdScriptContent` escaper, so even a value that skipped layer 1 (a future
|
|
268
|
+
field with a looser shape, a caller reaching for `gaInitScript` directly) is
|
|
269
|
+
inert data rather than code.
|
|
270
|
+
|
|
271
|
+
Both layers are proven load-bearing by mutation: removing the escaper makes the
|
|
272
|
+
built body actually call `alert` under test; removing the validation makes the
|
|
273
|
+
hostile id reach the `googletagmanager` loader URL.
|
|
274
|
+
|
|
275
|
+
Also exported for consumers that render their own tags: `isValidGaMeasurementId`,
|
|
276
|
+
`isValidClarityProjectId`, `gaInitScript`, `clarityInitScript`,
|
|
277
|
+
`analyticsScriptLiteral`.
|
|
278
|
+
|
|
279
|
+
Ships as its own tsup entry (`./analytics`), isolated from the root `index` barrel
|
|
280
|
+
the same way `./json-ld`, `./cache` and `./route-handlers` are — its only runtime
|
|
281
|
+
dependencies are `next/script` and the local escaper, so importing it must never
|
|
282
|
+
drag in `CtaButtons`/`EditableCta`'s `@reblu/site-ui` + `next/link` client graph.
|
|
283
|
+
|
|
284
|
+
**Action required for SITE repos:** replace hand-rolled GA/Clarity `<Script>`
|
|
285
|
+
blocks with `<AnalyticsScripts gaId={analytics?.gaId} clarityId={analytics?.clarityId} />`.
|
|
286
|
+
`apps/demo-site` migrates onto it in this same change. base-api additionally now
|
|
287
|
+
refuses to store a malformed id (nexus form) and coerces one to `null` on the way
|
|
288
|
+
out of `/api/site/analytics`, so a row written before this fix cannot reach a SITE.
|
|
289
|
+
|
|
290
|
+
## 1.4.0
|
|
291
|
+
|
|
292
|
+
### Minor Changes
|
|
293
|
+
|
|
294
|
+
- fe8b8d9: Add `jsonLdScriptContent(value)` and `<JsonLd>` (`@reblu/site-next/json-ld`) — a
|
|
295
|
+
safe `application/ld+json` `<script>` serialiser/renderer for the App Router.
|
|
296
|
+
|
|
297
|
+
A `<script>` element is HTML's "raw text" context: the parser ends it on the
|
|
298
|
+
first case-insensitive `</script` byte sequence it sees, no matter whether that
|
|
299
|
+
sequence sits inside a JSON string. `JSON.stringify` escapes for JSON syntax
|
|
300
|
+
only, so serialising tenant-authored data (page title, description, …) straight
|
|
301
|
+
into a JSON-LD `<script>` with plain `JSON.stringify` is a stored-XSS sink: a
|
|
302
|
+
value containing `</script><img src=x onerror=alert(1)>` breaks out of the
|
|
303
|
+
element and runs. `apps/demo-site` already carried a local fix for exactly this
|
|
304
|
+
(`generateStructuredData`, escaping `<` only); it has now been extracted here so
|
|
305
|
+
every SITE gets it instead of reinventing it — `demo-site` migrates onto the
|
|
306
|
+
shared helper in this same change.
|
|
307
|
+
|
|
308
|
+
`jsonLdScriptContent` escapes `<`, `>`, `&`, U+2028 and U+2029 to their
|
|
309
|
+
`\uXXXX` form. Every one of those is a standard JSON character escape, so
|
|
310
|
+
`JSON.parse(jsonLdScriptContent(value))` always recovers `value` unchanged —
|
|
311
|
+
this is an escaper, not a strip/allowlist sanitiser. `<` alone closes the
|
|
312
|
+
attack; `>`/`&`/U+2028/U+2029 are defence in depth appropriate for published
|
|
313
|
+
SDK surface (U+2028/U+2029 are valid JSON but invalid raw JS line terminators,
|
|
314
|
+
which has tripped up naive `<script>` consumers before).
|
|
315
|
+
|
|
316
|
+
## Why `@reblu/site-next`, not `@reblu/site-blocks`
|
|
317
|
+
|
|
318
|
+
`@reblu/site-blocks` already ships `sanitizeHtml` for the **HTML** boundary
|
|
319
|
+
(`dangerouslySetInnerHTML`), which looks like the obvious home. It isn't: that
|
|
320
|
+
package's main entry bundles `parse5` + the `hast-util-*` chain + `marked` with
|
|
321
|
+
`treeshake: false` — `dist/index.js` is ~463 KB unminified, and PR #848 spent a
|
|
322
|
+
full round removing exactly that weight (89 KB gzipped) from a client bundle
|
|
323
|
+
that only needed one HTML-escaping helper. A 40-line, dependency-free escaper
|
|
324
|
+
must not drag an HTML parser behind it.
|
|
325
|
+
|
|
326
|
+
`<JsonLd>` is also Next-specific (the official App Router
|
|
327
|
+
`suppressHydrationWarning` pattern), which `@reblu/site-next` already is.
|
|
328
|
+
It ships as its own tsup entry (`./json-ld`), isolated from the root `index`
|
|
329
|
+
barrel the same way `./cache` and `./route-handlers` already are — importing
|
|
330
|
+
it must never drag in `CtaButtons`/`EditableCta`'s `next/link` +
|
|
331
|
+
`@reblu/site-ui` graph.
|
|
332
|
+
|
|
333
|
+
**Measured**: `dist/json-ld.js` is 701 B (451 B gzipped); its only runtime
|
|
334
|
+
import is `react/jsx-runtime`.
|
|
335
|
+
|
|
336
|
+
## Usage
|
|
337
|
+
|
|
338
|
+
```tsx
|
|
339
|
+
import { JsonLd } from '@reblu/site-next/json-ld'
|
|
340
|
+
;<JsonLd id='page' data={page.jsonLd} />
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Or serialise directly:
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
import { jsonLdScriptContent } from '@reblu/site-next/json-ld'
|
|
347
|
+
|
|
348
|
+
const html = jsonLdScriptContent(structuredData)
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
See `@reblu/site-blocks`' README ("Security boundaries") and
|
|
352
|
+
`@reblu/site-next`'s README ("Structured data (JSON-LD)") for the full
|
|
353
|
+
HTML-boundary vs. script-boundary pairing.
|
|
354
|
+
|
|
355
|
+
### Patch Changes
|
|
356
|
+
|
|
357
|
+
- Updated dependencies [eef13bf]
|
|
358
|
+
- @reblu/site-contracts@0.4.0
|
|
359
|
+
- @reblu/site-client@3.0.0
|
|
360
|
+
|
|
361
|
+
## 1.3.2
|
|
362
|
+
|
|
363
|
+
### Patch Changes
|
|
364
|
+
|
|
365
|
+
- Updated dependencies [495d32d]
|
|
366
|
+
- @reblu/site-contracts@0.3.0
|
|
367
|
+
- @reblu/site-client@2.0.0
|
|
368
|
+
|
|
369
|
+
## 1.3.1
|
|
370
|
+
|
|
371
|
+
### Patch Changes
|
|
372
|
+
|
|
373
|
+
- Updated dependencies [d081f12]
|
|
374
|
+
- Updated dependencies [06de596]
|
|
375
|
+
- @reblu/site-contracts@0.2.0
|
|
376
|
+
- @reblu/site-client@1.0.0
|
|
377
|
+
|
|
378
|
+
## 1.3.0
|
|
379
|
+
|
|
380
|
+
### Minor Changes
|
|
381
|
+
|
|
382
|
+
- 33da822: `AuthLogo` is now "logo XOR name" and owns the auth page's heading. When a logo
|
|
383
|
+
resolves for the active theme it renders that image wrapped in a heading whose
|
|
384
|
+
accessible name is `siteName` (via the img `alt`) — so the page always has a
|
|
385
|
+
heading, but the name is never shown as duplicated visible text next to the
|
|
386
|
+
logo. When no logo resolves it renders `siteName` (or the fallback label) as the
|
|
387
|
+
heading text instead of a plain `<span>`. Auth pages should therefore drop their
|
|
388
|
+
separate `<h1>{siteName}</h1>` — keeping it double-rendered the name whenever no
|
|
389
|
+
logo was configured (or only the other theme's logo was).
|
|
390
|
+
|
|
391
|
+
Adds an optional `headingLevel` prop (default `1`) to control that heading
|
|
392
|
+
level. The decorative escape hatch is unchanged: `hideTextFallback` or
|
|
393
|
+
`copy={{ altText: '' }}` still render a bare, unwrapped image for consumers that
|
|
394
|
+
own the heading themselves.
|
|
395
|
+
|
|
396
|
+
### Patch Changes
|
|
397
|
+
|
|
398
|
+
- Updated dependencies [33da822]
|
|
399
|
+
- @reblu/site-ui@0.3.1
|
|
400
|
+
|
|
401
|
+
## 1.2.1
|
|
402
|
+
|
|
403
|
+
### Patch Changes
|
|
404
|
+
|
|
405
|
+
- b0cbc4c: Reclassify `@reblu/site-ui` from a `peerDependency` to a regular `dependency` (`workspace:^`), matching its siblings `@reblu/site-client` and `@reblu/site-contracts`.
|
|
406
|
+
|
|
407
|
+
`@reblu/site-ui` is an internal presentational primitive that `@reblu/site-next` bundles usage of, not a host-provided singleton — unlike `react`/`next`/`next-themes`, which remain peers. Declaring it a peer pinned it to an exact version (`workspace:*` → `0.2.0`), so shipping `@reblu/site-ui@0.3.0` (with the `./tabs`/`./label` subpaths the `./auth` bundle imports) fell out of range and forced a spurious major bump. As a regular dependency (`^0.3.0`) consuming SITEs no longer install `@reblu/site-ui` themselves, resolution to the version carrying `./tabs`/`./label` is automatic, and `@reblu/site-next` stays on 1.x (REB-510).
|
|
408
|
+
|
|
409
|
+
- Updated dependencies [b0cbc4c]
|
|
410
|
+
- @reblu/site-ui@0.3.0
|
|
411
|
+
|
|
412
|
+
## 1.2.0
|
|
413
|
+
|
|
414
|
+
### Minor Changes
|
|
415
|
+
|
|
416
|
+
- 14dd092: Add `AuthLogo` + 4 net-new auth components + `createProxyRoute` factory to `@reblu/site-next/auth` and `@reblu/site-next/route-handlers` (REB-510 slice 2b).
|
|
417
|
+
|
|
418
|
+
`./auth` now also exports:
|
|
419
|
+
- `AuthLogo` — dynamic tenant logo (light/dark × expanded/compact), driven by
|
|
420
|
+
`next-themes` and the 4 `/api/site/config` logo URLs, falling back to
|
|
421
|
+
`siteName` then a generic label when no logo is configured.
|
|
422
|
+
- `ResetPasswordRequest` / `ResetPasswordConfirm` (incl. the REB-378 MFA
|
|
423
|
+
step-up branch) / `SetPassword` / `AuthConfirm` (dual-mode
|
|
424
|
+
`success=true` vs `token_hash`+`type`) — copy-injectable with Spanish
|
|
425
|
+
defaults, extracted from demo-site's hand-rolled auth pages.
|
|
426
|
+
|
|
427
|
+
A new `./route-handlers` subpath (server-only, dependency-free, no
|
|
428
|
+
`'use client'`) exports `createProxyRoute(targetPath, opts?)`, a factory that
|
|
429
|
+
wires a SITE's `app/api/*` Route Handler to base-api's `/api/site/auth/*` in
|
|
430
|
+
one line — forwarding User-Agent/X-Forwarded-For/X-Real-IP, optionally
|
|
431
|
+
`Set-Cookie`, with a `requireJson` guard and a lazy per-request
|
|
432
|
+
`REBLU_API_BASE_URL`/`REBLU_API_KEY` env read (opts still take priority).
|
|
433
|
+
|
|
434
|
+
`next-themes` is now a peer dependency (only required by `AuthLogo`).
|
|
435
|
+
|
|
436
|
+
- ebe163e: Add a UI-free `./cache` subpath for `NextCacheAdapter` and float the SDK deps.
|
|
437
|
+
|
|
438
|
+
`@reblu/site-next@1.0.0` only exposed `.` and `./cta-buttons`. The barrel `.`
|
|
439
|
+
re-exports `NextCacheAdapter` alongside the CTA client components
|
|
440
|
+
(`CtaButtons`/`StickyMobileCta`/`EditableCta` → `@reblu/site-ui` + `next/link`),
|
|
441
|
+
so importing the server-only, dependency-free adapter from a server module still
|
|
442
|
+
dragged in the client boundary — Next 16 + Turbopack RSC then blew up with
|
|
443
|
+
`createContext is not a function` (same class as the Radix-Slot bug, REB-469).
|
|
444
|
+
- Add a dedicated `./cache` subpath export mapping only to
|
|
445
|
+
`next-cache-adapter.ts` (its own `tsup` entry). `NextCacheAdapter` is now
|
|
446
|
+
importable as `@reblu/site-next/cache` without pulling any UI. The barrel `.`
|
|
447
|
+
keeps exporting it for backward compatibility.
|
|
448
|
+
- Float `@reblu/site-client` and `@reblu/site-contracts` from `workspace:*` to
|
|
449
|
+
`workspace:^`, so a published `@reblu/site-next` resolves `^0.1.1` (the
|
|
450
|
+
republished packages that ship `dist/`, REB-487) instead of the exact,
|
|
451
|
+
broken `0.1.0` that `workspace:*` had frozen at publish time.
|
|
452
|
+
|
|
453
|
+
### Patch Changes
|
|
454
|
+
|
|
455
|
+
- Updated dependencies [356e04d]
|
|
456
|
+
- @reblu/site-contracts@0.1.2
|
|
457
|
+
- @reblu/site-client@0.1.2
|
|
458
|
+
|
|
459
|
+
## 1.0.0
|
|
460
|
+
|
|
461
|
+
### Patch Changes
|
|
462
|
+
|
|
463
|
+
- Updated dependencies [873cad4]
|
|
464
|
+
- @reblu/site-ui@0.2.0
|
package/README.md
CHANGED
|
@@ -108,6 +108,27 @@ import { LoginForm, AuthLogo, mapLoginError } from '@reblu/site-next/auth'
|
|
|
108
108
|
| `AuthLogo` | — | Dynamic tenant logo, `next-themes`-driven |
|
|
109
109
|
| `mapLoginError` | — | Maps Supabase `?error=` to Spanish copy |
|
|
110
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
|
+
|
|
111
132
|
`ResetPasswordConfirm`, `SetPassword`, and `AuthConfirm` read their own URL
|
|
112
133
|
params internally via `useSearchParams()`. Next.js requires that hook's
|
|
113
134
|
subtree to render inside a `<Suspense>` boundary — wrap the page/route that
|
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
|