@flamingo-stack/openframe-frontend-core 0.0.227 → 0.0.228
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-E3DB4D43.cjs → chunk-6PWR6K2K.cjs} +9 -9
- package/dist/{chunk-E3DB4D43.cjs.map → chunk-6PWR6K2K.cjs.map} +1 -1
- package/dist/{chunk-K252FYE5.js → chunk-G6EKFWIC.js} +2213 -2192
- package/dist/chunk-G6EKFWIC.js.map +1 -0
- package/dist/{chunk-JUWPBMQM.cjs → chunk-GYRRSOTR.cjs} +50 -2
- package/dist/chunk-GYRRSOTR.cjs.map +1 -0
- package/dist/{chunk-LEM4FLCU.cjs → chunk-HLKC2OBT.cjs} +12 -12
- package/dist/{chunk-LEM4FLCU.cjs.map → chunk-HLKC2OBT.cjs.map} +1 -1
- package/dist/{chunk-QFC725WU.cjs → chunk-L65QBRV5.cjs} +59 -38
- package/dist/chunk-L65QBRV5.cjs.map +1 -0
- package/dist/{chunk-53EA4QPZ.js → chunk-LLTUSEHF.js} +50 -2
- package/dist/chunk-LLTUSEHF.js.map +1 -0
- package/dist/{chunk-CUBRHI6D.js → chunk-LWJJP64X.js} +3 -3
- package/dist/{chunk-X3KKG6IU.js → chunk-MT5GO55P.js} +2 -2
- package/dist/{chunk-EURLSRHW.js → chunk-QLOU4BDD.js} +10 -5
- package/dist/chunk-QLOU4BDD.js.map +1 -0
- package/dist/{chunk-HO2VEMEX.cjs → chunk-TAWNR7IW.cjs} +32 -27
- package/dist/chunk-TAWNR7IW.cjs.map +1 -0
- package/dist/{chunk-E3WJQQGW.cjs → chunk-VXYYZFVV.cjs} +7 -7
- package/dist/{chunk-E3WJQQGW.cjs.map → chunk-VXYYZFVV.cjs.map} +1 -1
- package/dist/{chunk-YE62NF6I.js → chunk-WLCBY6ZO.js} +2 -2
- package/dist/components/chat/index.cjs +3 -3
- package/dist/components/chat/index.js +2 -2
- package/dist/components/contact/contact-form.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/contact/index.js +3 -3
- package/dist/components/features/index.cjs +3 -3
- package/dist/components/features/index.js +2 -2
- package/dist/components/features/waitlist-form.d.ts +6 -3
- package/dist/components/features/waitlist-form.d.ts.map +1 -1
- package/dist/components/index.cjs +81 -79
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +7 -5
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +3 -3
- package/dist/components/navigation/index.js +2 -2
- package/dist/components/onboarding-guides/index.cjs +26 -26
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/tickets/index.cjs +67 -67
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/honeypot-field.d.ts +18 -0
- package/dist/components/ui/honeypot-field.d.ts.map +1 -0
- package/dist/components/ui/index.cjs +5 -3
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +4 -2
- package/dist/hooks/index.cjs +4 -2
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +3 -1
- package/dist/hooks/use-humanity-signals.d.ts +26 -0
- package/dist/hooks/use-humanity-signals.d.ts.map +1 -0
- package/dist/index.cjs +19 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +18 -2
- package/dist/utils/humanity-signals.cjs +30 -0
- package/dist/utils/humanity-signals.cjs.map +1 -0
- package/dist/utils/humanity-signals.d.ts +45 -0
- package/dist/utils/humanity-signals.d.ts.map +1 -0
- package/dist/utils/humanity-signals.js +23 -0
- package/dist/utils/humanity-signals.js.map +1 -0
- package/dist/utils/index.cjs +26 -0
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +21 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +7 -1
- package/src/components/contact/contact-form.tsx +11 -1
- package/src/components/features/waitlist-form.tsx +14 -4
- package/src/components/ui/honeypot-field.tsx +30 -0
- package/src/components/ui/index.ts +1 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/use-humanity-signals.ts +47 -0
- package/src/utils/humanity-signals.ts +57 -0
- package/src/utils/index.ts +6 -0
- package/dist/chunk-53EA4QPZ.js.map +0 -1
- package/dist/chunk-EURLSRHW.js.map +0 -1
- package/dist/chunk-HO2VEMEX.cjs.map +0 -1
- package/dist/chunk-JUWPBMQM.cjs.map +0 -1
- package/dist/chunk-K252FYE5.js.map +0 -1
- package/dist/chunk-QFC725WU.cjs.map +0 -1
- /package/dist/{chunk-CUBRHI6D.js.map → chunk-LWJJP64X.js.map} +0 -0
- /package/dist/{chunk-X3KKG6IU.js.map → chunk-MT5GO55P.js.map} +0 -0
- /package/dist/{chunk-YE62NF6I.js.map → chunk-WLCBY6ZO.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flamingo-stack/openframe-frontend-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.228",
|
|
4
4
|
"description": "Shared design system and components for all Flamingo platforms",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -132,6 +132,12 @@
|
|
|
132
132
|
"require": "./dist/utils/index.cjs",
|
|
133
133
|
"default": "./dist/utils/index.js"
|
|
134
134
|
},
|
|
135
|
+
"./utils/humanity-signals": {
|
|
136
|
+
"types": "./dist/utils/humanity-signals.d.ts",
|
|
137
|
+
"import": "./dist/utils/humanity-signals.js",
|
|
138
|
+
"require": "./dist/utils/humanity-signals.cjs",
|
|
139
|
+
"default": "./dist/utils/humanity-signals.js"
|
|
140
|
+
},
|
|
135
141
|
"./styles": {
|
|
136
142
|
"types": "./src/styles/index.d.ts",
|
|
137
143
|
"import": "./src/styles/index.css",
|
|
@@ -44,6 +44,8 @@ import {
|
|
|
44
44
|
Label,
|
|
45
45
|
} from '../ui'
|
|
46
46
|
import { useContactSubmission } from '../../hooks/use-contact-submission'
|
|
47
|
+
import { useHumanitySignals } from '../../hooks/use-humanity-signals'
|
|
48
|
+
import { HoneypotField } from '../ui/honeypot-field'
|
|
47
49
|
import {
|
|
48
50
|
ChatAttachmentAddButton,
|
|
49
51
|
ChatAttachmentChipStrip,
|
|
@@ -180,6 +182,10 @@ export function ContactForm({
|
|
|
180
182
|
// request when `onCustomSubmit` is active.
|
|
181
183
|
const [customSubmitting, setCustomSubmitting] = useState(false)
|
|
182
184
|
|
|
185
|
+
// Invisible bot-protection signals (honeypot + timing). Spread into the
|
|
186
|
+
// submit payload for BOTH the built-in and custom paths; reset on success.
|
|
187
|
+
const { honeypotInputProps, getSignals, resetSignals } = useHumanitySignals()
|
|
188
|
+
|
|
183
189
|
const isSubmitting = onCustomSubmit ? customSubmitting : builtInSubmission.isSubmitting
|
|
184
190
|
// `isSuccess` only ever fires on the built-in path; custom callers
|
|
185
191
|
// own their own UX (no "Message Sent!" button-label flicker).
|
|
@@ -206,7 +212,7 @@ export function ContactForm({
|
|
|
206
212
|
if (isSubmitting) return
|
|
207
213
|
if (attachmentsEnabled && attachments.hasInflightUploads) return
|
|
208
214
|
try {
|
|
209
|
-
const payload = { ...data, ...(rdtCid && { rdt_cid: rdtCid }) }
|
|
215
|
+
const payload = { ...data, ...(rdtCid && { rdt_cid: rdtCid }), ...getSignals() }
|
|
210
216
|
const readyAttachments = attachmentsEnabled ? attachments.readyAttachments : []
|
|
211
217
|
if (onCustomSubmit) {
|
|
212
218
|
setCustomSubmitting(true)
|
|
@@ -220,6 +226,7 @@ export function ContactForm({
|
|
|
220
226
|
}
|
|
221
227
|
onSubmitSuccess?.()
|
|
222
228
|
reset()
|
|
229
|
+
resetSignals()
|
|
223
230
|
if (attachmentsEnabled) attachments.clear()
|
|
224
231
|
} catch {
|
|
225
232
|
// Error toast is owned by the active flow:
|
|
@@ -287,6 +294,9 @@ export function ContactForm({
|
|
|
287
294
|
{!showHelpCategory && <input type="hidden" {...register('helpCategory')} />}
|
|
288
295
|
{!showMessage && <input type="hidden" {...register('message')} />}
|
|
289
296
|
|
|
297
|
+
{/* Invisible honeypot — real users never fill it; bots that fill every field trip it. */}
|
|
298
|
+
<HoneypotField {...honeypotInputProps} />
|
|
299
|
+
|
|
290
300
|
{/* Extra top field (e.g. Subject for ticket forms). Rendered
|
|
291
301
|
outside the schema-driven layout so the caller fully owns
|
|
292
302
|
label / placeholder / state. */}
|
|
@@ -5,6 +5,9 @@ import { Button } from '../ui/button'
|
|
|
5
5
|
import { CheckboxBlock } from '../ui/checkbox-block'
|
|
6
6
|
import { Input } from '../ui/input'
|
|
7
7
|
import { PhoneInput } from '../ui/phone-input'
|
|
8
|
+
import { HoneypotField } from '../ui/honeypot-field'
|
|
9
|
+
import { useHumanitySignals } from '../../hooks/use-humanity-signals'
|
|
10
|
+
import type { HumanitySignals } from '../../utils/humanity-signals'
|
|
8
11
|
import { useToast } from '../../hooks/use-toast'
|
|
9
12
|
import { cn } from '../../utils/cn'
|
|
10
13
|
import { hasGenericEmailDomain } from '../../utils/generic-domain-utils'
|
|
@@ -19,10 +22,12 @@ export interface WaitlistFormProps {
|
|
|
19
22
|
/** Optional CSS classes for the container */
|
|
20
23
|
className?: string
|
|
21
24
|
/**
|
|
22
|
-
* Registration handler — called with email
|
|
23
|
-
*
|
|
25
|
+
* Registration handler — called with email, optional E.164 phone, and the
|
|
26
|
+
* invisible bot-protection signals (honeypot + timing) to forward into the
|
|
27
|
+
* POST body. Must throw on failure (toast is handled by the form). `signals`
|
|
28
|
+
* is optional for backward compatibility with older callers.
|
|
24
29
|
*/
|
|
25
|
-
onRegister: (email: string, phone?: string) => Promise<void>
|
|
30
|
+
onRegister: (email: string, phone?: string, signals?: HumanitySignals) => Promise<void>
|
|
26
31
|
/** Whether a registration request is currently in flight */
|
|
27
32
|
isSubmitting?: boolean
|
|
28
33
|
/** Whether registration completed successfully */
|
|
@@ -88,6 +93,7 @@ export function WaitlistForm({
|
|
|
88
93
|
const [phone, setPhone] = useState(defaultPhone)
|
|
89
94
|
const [countryCode, setCountryCode] = useState<CountryCode>('US')
|
|
90
95
|
const { toast } = useToast()
|
|
96
|
+
const { honeypotInputProps, getSignals, resetSignals } = useHumanitySignals()
|
|
91
97
|
const [smsConsent, setSmsConsent] = useState(false)
|
|
92
98
|
const [isClient, setIsClient] = useState(false)
|
|
93
99
|
const [isPhoneInvalid, setIsPhoneInvalid] = useState(false)
|
|
@@ -137,7 +143,8 @@ export function WaitlistForm({
|
|
|
137
143
|
const finalPhone = phone ? formatPhoneE164(phone, countryCode) : undefined
|
|
138
144
|
|
|
139
145
|
try {
|
|
140
|
-
await onRegister(email, finalPhone)
|
|
146
|
+
await onRegister(email, finalPhone, getSignals())
|
|
147
|
+
resetSignals()
|
|
141
148
|
} catch {
|
|
142
149
|
// caller's onRegister should handle its own error toasts if needed
|
|
143
150
|
}
|
|
@@ -183,6 +190,9 @@ export function WaitlistForm({
|
|
|
183
190
|
className
|
|
184
191
|
)}
|
|
185
192
|
>
|
|
193
|
+
{/* Invisible honeypot — real users never fill it; bots that fill every field trip it. */}
|
|
194
|
+
<HoneypotField {...honeypotInputProps} />
|
|
195
|
+
|
|
186
196
|
{/* Email Input */}
|
|
187
197
|
<Input
|
|
188
198
|
type="email"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from 'react'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* HoneypotField — an invisible decoy input for bot detection. Real users never
|
|
7
|
+
* see or fill it; naive bots that fill every field trip it (the server blocks
|
|
8
|
+
* any submission where this field is non-empty).
|
|
9
|
+
*
|
|
10
|
+
* Made invisible WITHOUT `type="hidden"` / `display:none` (sophisticated bots
|
|
11
|
+
* skip those): an off-screen, zero-size, aria-hidden, non-focusable,
|
|
12
|
+
* non-pointer wrapper. Layout utilities only — no colours/hex (ODS-clean). No
|
|
13
|
+
* <label> (avoids id coupling); `aria-hidden` + `tabIndex={-1}` keep it out of
|
|
14
|
+
* the accessibility tree and tab order.
|
|
15
|
+
*
|
|
16
|
+
* forwardRef so `<HoneypotField {...honeypotInputProps} />` (which carries the
|
|
17
|
+
* ref from useHumanitySignals) works on both React 18 and 19.
|
|
18
|
+
*/
|
|
19
|
+
export const HoneypotField = forwardRef<HTMLInputElement, { name: string }>(
|
|
20
|
+
function HoneypotField({ name }, ref) {
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
aria-hidden="true"
|
|
24
|
+
className="absolute w-px h-px p-0 -m-px overflow-hidden pointer-events-none opacity-0"
|
|
25
|
+
>
|
|
26
|
+
<input ref={ref} type="text" name={name} tabIndex={-1} autoComplete="off" />
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
},
|
|
30
|
+
)
|
|
@@ -10,6 +10,7 @@ export * from './checkbox-block'
|
|
|
10
10
|
export * from './checkbox-with-description'
|
|
11
11
|
export * from './date-picker'
|
|
12
12
|
export * from './field-wrapper'
|
|
13
|
+
export * from './honeypot-field'
|
|
13
14
|
export * from './info-card'
|
|
14
15
|
export * from './info-row'
|
|
15
16
|
export * from './input'
|
package/src/hooks/index.ts
CHANGED
|
@@ -27,3 +27,7 @@ export * from './use-access-code-integration'
|
|
|
27
27
|
|
|
28
28
|
// OG placeholder URL builder hook (requires host-supplied URL builder)
|
|
29
29
|
export * from './use-og-placeholder'
|
|
30
|
+
|
|
31
|
+
// Invisible bot-protection client primitive (honeypot ref + submit-timing).
|
|
32
|
+
// Pairs with the server-safe decision fn in `utils/humanity-signals`.
|
|
33
|
+
export * from './use-humanity-signals'
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useRef } from 'react'
|
|
4
|
+
import {
|
|
5
|
+
HONEYPOT_FIELD,
|
|
6
|
+
ELAPSED_MS_FIELD,
|
|
7
|
+
type HumanitySignals,
|
|
8
|
+
} from '../utils/humanity-signals'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* useHumanitySignals — client primitive backing the invisible bot-protection
|
|
12
|
+
* layer. Owns the honeypot <input> ref + a mount timestamp, and exposes the
|
|
13
|
+
* props to render the field plus a getter that produces the wire object to
|
|
14
|
+
* merge into a form's POST body. Reused by every public form (contact,
|
|
15
|
+
* waitlist, ticket, review) so the signal shape lives in exactly one place.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* const { honeypotInputProps, getSignals, resetSignals } = useHumanitySignals()
|
|
19
|
+
* // render: <HoneypotField {...honeypotInputProps} />
|
|
20
|
+
* // submit: fetch(url, { body: JSON.stringify({ ...formData, ...getSignals() }) })
|
|
21
|
+
* // after a successful submit: resetSignals()
|
|
22
|
+
*
|
|
23
|
+
* The two signals are origin-independent (they ride in the body), so they keep
|
|
24
|
+
* working when the form is embedded behind a reverse-proxy / prefixed URL.
|
|
25
|
+
*/
|
|
26
|
+
export function useHumanitySignals() {
|
|
27
|
+
const ref = useRef<HTMLInputElement>(null)
|
|
28
|
+
// performance.now() is monotonic (immune to wall-clock skew). SSR-guarded
|
|
29
|
+
// even though the hook only executes on the client.
|
|
30
|
+
const mountedAt = useRef<number>(typeof performance !== 'undefined' ? performance.now() : 0)
|
|
31
|
+
|
|
32
|
+
const getSignals = useCallback(
|
|
33
|
+
(): HumanitySignals => ({
|
|
34
|
+
[HONEYPOT_FIELD]: ref.current?.value ?? '',
|
|
35
|
+
[ELAPSED_MS_FIELD]:
|
|
36
|
+
typeof performance !== 'undefined' ? Math.round(performance.now() - mountedAt.current) : 0,
|
|
37
|
+
}),
|
|
38
|
+
[],
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const resetSignals = useCallback(() => {
|
|
42
|
+
if (ref.current) ref.current.value = ''
|
|
43
|
+
if (typeof performance !== 'undefined') mountedAt.current = performance.now()
|
|
44
|
+
}, [])
|
|
45
|
+
|
|
46
|
+
return { honeypotInputProps: { ref, name: HONEYPOT_FIELD }, getSignals, resetSignals } as const
|
|
47
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Humanity signals — invisible bot-protection primitives shared by the lib's
|
|
3
|
+
* public forms (client) and the hub's per-route `verifyHuman` gate (server).
|
|
4
|
+
*
|
|
5
|
+
* PURE + React-free on purpose: this module is a tsup SERVER entry (no
|
|
6
|
+
* "use client" banner) so the hub can import it server-side without pulling a
|
|
7
|
+
* client-reference boundary — same pattern as `schemas/contact-schema` and
|
|
8
|
+
* `components/features/mux-origins`.
|
|
9
|
+
*
|
|
10
|
+
* Two origin-independent signals travel in the POST body: a honeypot (a hidden
|
|
11
|
+
* field real users never fill) and timing (ms from form mount to submit).
|
|
12
|
+
* `evaluateHumanitySignals` is the SINGLE source of truth for the block/allow
|
|
13
|
+
* decision — the hub imports + calls it rather than re-implementing the rules.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Hidden honeypot field name. Innocuous + autofill-resistant (deliberately NOT name/email). */
|
|
17
|
+
export const HONEYPOT_FIELD = 'contact_url_confirm'
|
|
18
|
+
/** Client-measured ms between form mount and submit. */
|
|
19
|
+
export const ELAPSED_MS_FIELD = 'form_elapsed_ms'
|
|
20
|
+
/** Default minimum fill time (ms). A submit faster than this is treated as a bot. */
|
|
21
|
+
export const DEFAULT_MIN_FILL_MS = 700
|
|
22
|
+
|
|
23
|
+
/** Keyed wire object produced by `useHumanitySignals().getSignals()` and spread into the POST body. */
|
|
24
|
+
export type HumanitySignals = Record<string, string | number>
|
|
25
|
+
|
|
26
|
+
/** Result of {@link evaluateHumanitySignals}. */
|
|
27
|
+
export type HumanityVerdict = { ok: true } | { ok: false; reason: 'honeypot' | 'too_fast' }
|
|
28
|
+
|
|
29
|
+
/** Tolerant reader — never throws; missing/garbage timing → null. */
|
|
30
|
+
export function extractHumanitySignals(body: unknown): { honeypot: string; elapsedMs: number | null } {
|
|
31
|
+
const b = (body ?? {}) as Record<string, unknown>
|
|
32
|
+
const rawHp = b[HONEYPOT_FIELD]
|
|
33
|
+
// A legit client always sends a STRING here (getSignals → ref.value ?? ''),
|
|
34
|
+
// so ANY present non-string value is a bot filling the decoy with a non-string
|
|
35
|
+
// to dodge the empty-check — coerce to a (non-empty) string so it still trips.
|
|
36
|
+
// null/undefined → '' = the correct "field absent / unfilled" allow case.
|
|
37
|
+
const honeypot = rawHp == null ? '' : String(rawHp)
|
|
38
|
+
const rawMs = b[ELAPSED_MS_FIELD]
|
|
39
|
+
const elapsedMs = typeof rawMs === 'number' && Number.isFinite(rawMs) ? rawMs : null
|
|
40
|
+
return { honeypot, elapsedMs }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* SINGLE decision fn for honeypot + timing (the hub's `verifyHuman` imports + calls this):
|
|
45
|
+
* - honeypot non-empty → bot (real users never fill the off-screen field)
|
|
46
|
+
* - elapsed below `minFillMs` → bot (humans take time; a MISSING timing value never blocks)
|
|
47
|
+
*/
|
|
48
|
+
export function evaluateHumanitySignals(body: unknown, opts: { minFillMs: number }): HumanityVerdict {
|
|
49
|
+
const { honeypot, elapsedMs } = extractHumanitySignals(body)
|
|
50
|
+
if (honeypot.trim() !== '') return { ok: false, reason: 'honeypot' }
|
|
51
|
+
if (elapsedMs !== null && elapsedMs < opts.minFillMs) return { ok: false, reason: 'too_fast' }
|
|
52
|
+
return { ok: true }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Parse a comma-separated env string → trimmed, non-empty entries (undefined → []). */
|
|
56
|
+
export const splitCsvEnv = (s?: string): string[] =>
|
|
57
|
+
s?.split(',').map((t) => t.trim()).filter(Boolean) ?? []
|
package/src/utils/index.ts
CHANGED
|
@@ -242,3 +242,9 @@ export {
|
|
|
242
242
|
type ContentHrefOptions,
|
|
243
243
|
type ComposeContentUrl,
|
|
244
244
|
} from './content-href'
|
|
245
|
+
|
|
246
|
+
// Invisible bot-protection signals (honeypot + timing) — pure + server-safe so
|
|
247
|
+
// the hub's per-route `verifyHuman` gate imports the SAME `evaluateHumanitySignals`
|
|
248
|
+
// decision fn the lib forms feed. Also exported via the granular subpath
|
|
249
|
+
// `./utils/humanity-signals` for server-only consumers.
|
|
250
|
+
export * from './humanity-signals'
|