usehuma 1.0.0 → 1.1.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core.ts","../src/index.ts"],"sourcesContent":["/**\n * useHUMA — Core Signal Collector\n * Invisible behavioral biometrics — no PII collected.\n */\n\nimport type { SessionData, HumaVerifyResult, HumaError, HumaOptions } from \"./types\";\n\ntype MousePoint = { x: number; y: number; t: number };\ntype KeyEntry = { interval: number };\ntype ScrollEntry = { interval: number };\ntype ClickEntry = { t: number };\ntype FocusEvent = { type: \"focus\" | \"blur\"; t: number };\n\ninterface Signals {\n mouse: MousePoint[];\n keys: KeyEntry[];\n scrolls: ScrollEntry[];\n clicks: ClickEntry[];\n focusEvents: FocusEvent[];\n startTime: number;\n lastKeyTime: number | null;\n lastScrollTime: number | null;\n}\n\nfunction cv(arr: number[]): number {\n if (arr.length < 2) return 0;\n const mean = arr.reduce((a, b) => a + b, 0) / arr.length;\n if (mean === 0) return 0;\n const variance = arr.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / arr.length;\n return Math.sqrt(variance) / mean;\n}\n\nfunction mouseFeatures(pts: MousePoint[]) {\n if (pts.length < 5) return { speed_cv: 0, direction_changes: 0, sample_count: 0 };\n const speeds: number[] = [];\n const directions: number[] = [];\n for (let i = 1; i < pts.length; i++) {\n const dx = pts[i].x - pts[i - 1].x;\n const dy = pts[i].y - pts[i - 1].y;\n const dt = Math.max(pts[i].t - pts[i - 1].t, 1);\n speeds.push(Math.sqrt(dx * dx + dy * dy) / dt);\n directions.push(Math.atan2(dy, dx));\n }\n let dirChanges = 0;\n for (let j = 1; j < directions.length; j++) {\n if (Math.abs(directions[j] - directions[j - 1]) > 0.3) dirChanges++;\n }\n return { speed_cv: cv(speeds), direction_changes: dirChanges, sample_count: pts.length };\n}\n\nexport class HumaCollector {\n private signals: Signals = {\n mouse: [], keys: [], scrolls: [], clicks: [], focusEvents: [],\n startTime: Date.now(), lastKeyTime: null, lastScrollTime: null,\n };\n private listening = false;\n\n private onMouseMove = (e: MouseEvent) => {\n if (this.signals.mouse.length < 200)\n this.signals.mouse.push({ x: e.clientX, y: e.clientY, t: Date.now() });\n };\n private onKeyDown = () => {\n const now = Date.now();\n if (this.signals.lastKeyTime !== null && this.signals.keys.length < 100)\n this.signals.keys.push({ interval: now - this.signals.lastKeyTime });\n this.signals.lastKeyTime = now;\n };\n private onScroll = () => {\n const now = Date.now();\n if (this.signals.lastScrollTime !== null && this.signals.scrolls.length < 50)\n this.signals.scrolls.push({ interval: now - this.signals.lastScrollTime });\n this.signals.lastScrollTime = now;\n };\n private onClick = () => {\n if (this.signals.clicks.length < 50)\n this.signals.clicks.push({ t: Date.now() });\n };\n private onFocus = () => this.signals.focusEvents.push({ type: \"focus\", t: Date.now() });\n private onBlur = () => this.signals.focusEvents.push({ type: \"blur\", t: Date.now() });\n\n start() {\n if (this.listening || typeof window === \"undefined\") return;\n this.listening = true;\n document.addEventListener(\"mousemove\", this.onMouseMove, { passive: true });\n document.addEventListener(\"keydown\", this.onKeyDown, { passive: true });\n document.addEventListener(\"scroll\", this.onScroll, { passive: true });\n document.addEventListener(\"click\", this.onClick, { passive: true });\n window.addEventListener(\"focus\", this.onFocus, { passive: true });\n window.addEventListener(\"blur\", this.onBlur, { passive: true });\n }\n\n stop() {\n if (!this.listening) return;\n this.listening = false;\n document.removeEventListener(\"mousemove\", this.onMouseMove);\n document.removeEventListener(\"keydown\", this.onKeyDown);\n document.removeEventListener(\"scroll\", this.onScroll);\n document.removeEventListener(\"click\", this.onClick);\n window.removeEventListener(\"focus\", this.onFocus);\n window.removeEventListener(\"blur\", this.onBlur);\n }\n\n extract(): SessionData {\n const { keys, scrolls, clicks, focusEvents, startTime, mouse } = this.signals;\n const m = mouseFeatures(mouse);\n const clickIntervals: number[] = [];\n for (let i = 1; i < clicks.length; i++)\n clickIntervals.push(clicks[i].t - clicks[i - 1].t);\n return {\n time_on_page_ms: Date.now() - startTime,\n key_interval_cv: cv(keys.map(k => k.interval)),\n key_count: keys.length,\n scroll_interval_cv: cv(scrolls.map(s => s.interval)),\n scroll_count: scrolls.length,\n mouse_speed_cv: m.speed_cv,\n mouse_direction_changes: m.direction_changes,\n mouse_sample_count: m.sample_count,\n click_interval_cv: cv(clickIntervals),\n click_count: clicks.length,\n tab_switches: focusEvents.length,\n };\n }\n\n debug(): Record<string, unknown> {\n return { signals: this.signals as unknown, features: this.extract() };\n }\n}\n\n/** Send extracted signals to the HUMA API and return a result. */\nexport async function callHumaApi(\n opts: HumaOptions,\n sessionData: SessionData\n): Promise<HumaVerifyResult> {\n const endpoint = opts.endpoint ?? \"https://humaverify.com/api/v1/verify\";\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${opts.apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({ userId: opts.userId, sessionData }),\n });\n if (!res.ok) {\n const err: HumaError = await res.json().catch(() => ({ error: \"Unknown error\" }));\n throw err;\n }\n return res.json() as Promise<HumaVerifyResult>;\n}\n","/**\n * useHUMA JavaScript SDK\n * Privacy-first human verification — no PII, no Cloudflare dependency.\n * https://humaverify.com\n */\n\nexport { HumaCollector, callHumaApi } from \"./core\";\nexport type {\n SessionData,\n HumaVerifyResult,\n HumaError,\n HumaOptions,\n HumaState,\n} from \"./types\";\n\n/**\n * Vanilla JS verify — for non-React apps.\n *\n * @example\n * import { verify } from 'usehuma';\n *\n * const result = await verify({\n * apiKey: 'huma_live_...',\n * userId: 'user_123',\n * });\n * if (result.human) { ... }\n */\nimport { HumaCollector, callHumaApi } from \"./core\";\nimport type { HumaOptions, HumaVerifyResult } from \"./types\";\n\nlet _globalCollector: HumaCollector | null = null;\n\n/** Auto-start signal collection (call once, early in your app). */\nexport function init() {\n if (typeof window === \"undefined\") return;\n if (_globalCollector) return;\n _globalCollector = new HumaCollector();\n _globalCollector.start();\n}\n\n/**\n * Verify the current user as human using collected behavioral signals.\n * Calls init() automatically if not already started.\n */\nexport async function verify(opts: HumaOptions): Promise<HumaVerifyResult> {\n if (!_globalCollector) init();\n const sessionData = _globalCollector!.extract();\n return callHumaApi(opts, sessionData);\n}\n\n/** Get raw collected signals — useful for debugging. */\nexport function debug(): Record<string, unknown> | null {\n return _globalCollector?.debug() as Record<string, unknown> | null ?? null;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/core.ts","../src/index.ts"],"sourcesContent":["/**\n * useHUMA — Core Signal Collector\n * Invisible behavioral biometrics — no PII collected.\n */\n\nimport type { SessionData, HumaVerifyResult, HumaError, HumaOptions } from \"./types\";\n\ntype MousePoint = { x: number; y: number; t: number };\ntype KeyEntry = { interval: number };\ntype ScrollEntry = { interval: number };\ntype ClickEntry = { t: number };\ntype FocusEvent = { type: \"focus\" | \"blur\"; t: number };\ntype TapEntry = { t: number; duration_ms: number; force: number; touch_count: number };\ntype TouchMove = { speed: number; t: number };\ntype ActiveTouch = { t: number; x: number; y: number };\n\ninterface Signals {\n mouse: MousePoint[];\n keys: KeyEntry[];\n scrolls: ScrollEntry[];\n clicks: ClickEntry[];\n focusEvents: FocusEvent[];\n taps: TapEntry[];\n touchMoves: TouchMove[];\n multiTouchCount: number;\n startTime: number;\n lastKeyTime: number | null;\n lastScrollTime: number | null;\n activeTouches: Record<number, ActiveTouch>;\n}\n\nfunction cv(arr: number[]): number {\n if (arr.length < 2) return 0;\n const mean = arr.reduce((a, b) => a + b, 0) / arr.length;\n if (mean === 0) return 0;\n const variance = arr.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / arr.length;\n return Math.sqrt(variance) / mean;\n}\n\nfunction mouseFeatures(pts: MousePoint[]) {\n if (pts.length < 5) return { speed_cv: 0, direction_changes: 0, sample_count: 0 };\n const speeds: number[] = [];\n const directions: number[] = [];\n for (let i = 1; i < pts.length; i++) {\n const dx = pts[i].x - pts[i - 1].x;\n const dy = pts[i].y - pts[i - 1].y;\n const dt = Math.max(pts[i].t - pts[i - 1].t, 1);\n speeds.push(Math.sqrt(dx * dx + dy * dy) / dt);\n directions.push(Math.atan2(dy, dx));\n }\n let dirChanges = 0;\n for (let j = 1; j < directions.length; j++) {\n if (Math.abs(directions[j] - directions[j - 1]) > 0.3) dirChanges++;\n }\n return { speed_cv: cv(speeds), direction_changes: dirChanges, sample_count: pts.length };\n}\n\nfunction touchFeatures(taps: TapEntry[], moves: TouchMove[], multiTouchCount: number) {\n const tapIntervals: number[] = [];\n for (let i = 1; i < taps.length; i++) tapIntervals.push(taps[i].t - taps[i - 1].t);\n const durations = taps.map(t => t.duration_ms);\n const forces = taps.filter(t => t.force > 0).map(t => t.force);\n const speeds = moves.map(m => m.speed);\n return {\n tap_count: taps.length,\n tap_interval_cv: cv(tapIntervals),\n tap_duration_cv: cv(durations),\n tap_force_cv: cv(forces),\n touch_move_count: moves.length,\n touch_speed_cv: cv(speeds),\n multi_touch_count: multiTouchCount,\n };\n}\n\nexport class HumaCollector {\n private signals: Signals = {\n mouse: [], keys: [], scrolls: [], clicks: [], focusEvents: [],\n taps: [], touchMoves: [], multiTouchCount: 0,\n startTime: Date.now(), lastKeyTime: null, lastScrollTime: null,\n activeTouches: {},\n };\n private listening = false;\n\n private onMouseMove = (e: MouseEvent) => {\n if (this.signals.mouse.length < 200)\n this.signals.mouse.push({ x: e.clientX, y: e.clientY, t: Date.now() });\n };\n private onKeyDown = () => {\n const now = Date.now();\n if (this.signals.lastKeyTime !== null && this.signals.keys.length < 100)\n this.signals.keys.push({ interval: now - this.signals.lastKeyTime });\n this.signals.lastKeyTime = now;\n };\n private onScroll = () => {\n const now = Date.now();\n if (this.signals.lastScrollTime !== null && this.signals.scrolls.length < 50)\n this.signals.scrolls.push({ interval: now - this.signals.lastScrollTime });\n this.signals.lastScrollTime = now;\n };\n private onClick = () => {\n if (this.signals.clicks.length < 50)\n this.signals.clicks.push({ t: Date.now() });\n };\n private onFocus = () => this.signals.focusEvents.push({ type: \"focus\", t: Date.now() });\n private onBlur = () => this.signals.focusEvents.push({ type: \"blur\", t: Date.now() });\n\n private onTouchStart = (e: TouchEvent) => {\n const now = Date.now();\n if (e.touches.length > 1) this.signals.multiTouchCount++;\n for (let i = 0; i < e.changedTouches.length; i++) {\n const t = e.changedTouches[i];\n this.signals.activeTouches[t.identifier] = { t: now, x: t.clientX, y: t.clientY };\n }\n };\n\n private onTouchEnd = (e: TouchEvent) => {\n const now = Date.now();\n for (let i = 0; i < e.changedTouches.length; i++) {\n const touch = e.changedTouches[i];\n const start = this.signals.activeTouches[touch.identifier];\n if (!start) continue;\n delete this.signals.activeTouches[touch.identifier];\n if (this.signals.taps.length < 80) {\n this.signals.taps.push({\n t: now,\n duration_ms: now - start.t,\n force: (touch as Touch & { force?: number }).force ?? 0,\n touch_count: e.touches.length + 1,\n });\n }\n }\n };\n\n private onTouchMove = (e: TouchEvent) => {\n const now = Date.now();\n for (let i = 0; i < e.changedTouches.length; i++) {\n const touch = e.changedTouches[i];\n const start = this.signals.activeTouches[touch.identifier];\n if (!start) continue;\n const dx = touch.clientX - start.x;\n const dy = touch.clientY - start.y;\n const dt = Math.max(now - start.t, 1);\n if (this.signals.touchMoves.length < 100)\n this.signals.touchMoves.push({ speed: Math.sqrt(dx * dx + dy * dy) / dt, t: now });\n this.signals.activeTouches[touch.identifier] = { t: now, x: touch.clientX, y: touch.clientY };\n }\n };\n\n start() {\n if (this.listening || typeof window === \"undefined\") return;\n this.listening = true;\n document.addEventListener(\"mousemove\", this.onMouseMove, { passive: true });\n document.addEventListener(\"keydown\", this.onKeyDown, { passive: true });\n document.addEventListener(\"scroll\", this.onScroll, { passive: true });\n document.addEventListener(\"click\", this.onClick, { passive: true });\n document.addEventListener(\"touchstart\", this.onTouchStart, { passive: true });\n document.addEventListener(\"touchend\", this.onTouchEnd, { passive: true });\n document.addEventListener(\"touchmove\", this.onTouchMove, { passive: true });\n window.addEventListener(\"focus\", this.onFocus, { passive: true });\n window.addEventListener(\"blur\", this.onBlur, { passive: true });\n }\n\n stop() {\n if (!this.listening) return;\n this.listening = false;\n document.removeEventListener(\"mousemove\", this.onMouseMove);\n document.removeEventListener(\"keydown\", this.onKeyDown);\n document.removeEventListener(\"scroll\", this.onScroll);\n document.removeEventListener(\"click\", this.onClick);\n document.removeEventListener(\"touchstart\", this.onTouchStart);\n document.removeEventListener(\"touchend\", this.onTouchEnd);\n document.removeEventListener(\"touchmove\", this.onTouchMove);\n window.removeEventListener(\"focus\", this.onFocus);\n window.removeEventListener(\"blur\", this.onBlur);\n }\n\n extract(): SessionData {\n const { keys, scrolls, clicks, focusEvents, startTime, mouse, taps, touchMoves, multiTouchCount } = this.signals;\n const m = mouseFeatures(mouse);\n const t = touchFeatures(taps, touchMoves, multiTouchCount);\n const clickIntervals: number[] = [];\n for (let i = 1; i < clicks.length; i++)\n clickIntervals.push(clicks[i].t - clicks[i - 1].t);\n return {\n time_on_page_ms: Date.now() - startTime,\n key_interval_cv: cv(keys.map(k => k.interval)),\n key_count: keys.length,\n scroll_interval_cv: cv(scrolls.map(s => s.interval)),\n scroll_count: scrolls.length,\n mouse_speed_cv: m.speed_cv,\n mouse_direction_changes: m.direction_changes,\n mouse_sample_count: m.sample_count,\n click_interval_cv: cv(clickIntervals),\n click_count: clicks.length,\n tab_switches: focusEvents.length,\n tap_count: t.tap_count,\n tap_interval_cv: t.tap_interval_cv,\n tap_duration_cv: t.tap_duration_cv,\n tap_force_cv: t.tap_force_cv,\n touch_move_count: t.touch_move_count,\n touch_speed_cv: t.touch_speed_cv,\n multi_touch_count: t.multi_touch_count,\n };\n }\n\n debug(): Record<string, unknown> {\n return { signals: this.signals as unknown, features: this.extract() };\n }\n}\n\n/** Send extracted signals to the HUMA API and return a result. */\nexport async function callHumaApi(\n opts: HumaOptions,\n sessionData: SessionData\n): Promise<HumaVerifyResult> {\n const endpoint = opts.endpoint ?? \"https://humaverify.com/api/v1/verify\";\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${opts.apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({ userId: opts.userId, sessionData }),\n });\n if (!res.ok) {\n const err: HumaError = await res.json().catch(() => ({ error: \"Unknown error\" }));\n throw err;\n }\n return res.json() as Promise<HumaVerifyResult>;\n}\n","/**\n * useHUMA JavaScript SDK\n * Privacy-first human verification — no PII, no Cloudflare dependency.\n * https://humaverify.com\n */\n\nexport { HumaCollector, callHumaApi } from \"./core\";\nexport type {\n SessionData,\n HumaVerifyResult,\n HumaError,\n HumaOptions,\n HumaState,\n} from \"./types\";\n\n/**\n * Vanilla JS verify — for non-React apps.\n *\n * @example\n * import { verify } from 'usehuma';\n *\n * const result = await verify({\n * apiKey: 'huma_live_...',\n * userId: 'user_123',\n * });\n * if (result.human) { ... }\n */\nimport { HumaCollector, callHumaApi } from \"./core\";\nimport type { HumaOptions, HumaVerifyResult } from \"./types\";\n\nlet _globalCollector: HumaCollector | null = null;\n\n/** Auto-start signal collection (call once, early in your app). */\nexport function init() {\n if (typeof window === \"undefined\") return;\n if (_globalCollector) return;\n _globalCollector = new HumaCollector();\n _globalCollector.start();\n}\n\n/**\n * Verify the current user as human using collected behavioral signals.\n * Calls init() automatically if not already started.\n */\nexport async function verify(opts: HumaOptions): Promise<HumaVerifyResult> {\n if (!_globalCollector) init();\n const sessionData = _globalCollector!.extract();\n return callHumaApi(opts, sessionData);\n}\n\n/** Get raw collected signals — useful for debugging. */\nexport function debug(): Record<string, unknown> | null {\n return _globalCollector?.debug() as Record<string, unknown> | null ?? null;\n}\n\n/**\n * Server-side verify — pass pre-collected signals forwarded from the browser.\n * Use this in your API routes / server actions when you want the final\n * verify call to happen on your backend, not the browser.\n *\n * @example\n * // In your Next.js API route:\n * import { verifyWithSignals } from 'usehuma';\n *\n * const result = await verifyWithSignals({\n * apiKey: process.env.HUMA_API_KEY!,\n * userId: req.body.userId,\n * signals: req.body.humaSignals, // forwarded from browser via Huma.debug()\n * });\n */\nexport async function verifyWithSignals(opts: {\n apiKey: string;\n userId: string;\n signals: import(\"./types\").SessionData;\n endpoint?: string;\n}): Promise<import(\"./types\").HumaVerifyResult> {\n return callHumaApi(\n { apiKey: opts.apiKey, userId: opts.userId, endpoint: opts.endpoint },\n opts.signals\n );\n}\n\n/**\n * Request a step-up challenge when verify() confidence is borderline (30–70%).\n * Returns challenge data to render to the user.\n *\n * @example\n * const result = await verify({ apiKey, userId });\n * if (!result.human && result.confidence > 0.3) {\n * const ch = await requestChallenge({ apiKey, userId });\n * // Render ch.payload to user, collect answer, then:\n * const solved = await solveChallenge({ apiKey, challengeId: ch.challenge_id, answer });\n * if (solved.passed) continueLogin(solved.token);\n * }\n */\nexport async function requestChallenge(opts: {\n apiKey: string;\n userId: string;\n endpoint?: string;\n}): Promise<{ challenge_id: string; type: string; payload: Record<string, unknown>; expires_in_seconds: number }> {\n const base = (opts.endpoint ?? \"https://humaverify.com/api/v1/verify\")\n .replace(/\\/api\\/v1\\/verify$/, \"\")\n .replace(/\\/$/, \"\");\n const res = await fetch(`${base}/api/v1/challenge/create`, {\n method: \"POST\",\n headers: { Authorization: `Bearer ${opts.apiKey}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ userId: opts.userId }),\n });\n if (!res.ok) throw await res.json().catch(() => ({ error: \"Failed to create challenge\" }));\n return res.json();\n}\n\nexport async function solveChallenge(opts: {\n apiKey: string;\n challengeId: string;\n answer: string;\n endpoint?: string;\n}): Promise<{ passed: boolean; token?: string; message?: string; attempts_remaining?: number }> {\n const base = (opts.endpoint ?? \"https://humaverify.com/api/v1/verify\")\n .replace(/\\/api\\/v1\\/verify$/, \"\")\n .replace(/\\/$/, \"\");\n const res = await fetch(`${base}/api/v1/challenge/verify`, {\n method: \"POST\",\n headers: { Authorization: `Bearer ${opts.apiKey}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ challenge_id: opts.challengeId, answer: opts.answer }),\n });\n return res.json();\n}\n"],"mappings":"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","names":[]}
package/dist/react.d.mts CHANGED
@@ -128,6 +128,54 @@ declare function useHumaSession(opts: UseHumaSessionOptions): {
128
128
  anomaly: boolean;
129
129
  stop: () => void;
130
130
  };
131
+ type ChallengeTarget = {
132
+ id: string;
133
+ x: number;
134
+ y: number;
135
+ label: string;
136
+ };
137
+ type ChallengeFailed = {
138
+ passed: false;
139
+ message: string;
140
+ attempts_remaining?: number;
141
+ };
142
+ type HumaChallengeProps = {
143
+ /** challenge_id from POST /api/v1/challenge/create */
144
+ challengeId: string;
145
+ /** type from challenge create response */
146
+ type: "click_targets" | "pattern";
147
+ /** payload from challenge create response */
148
+ payload: {
149
+ instruction: string;
150
+ targets?: ChallengeTarget[];
151
+ sequence?: number[];
152
+ };
153
+ apiKey: string;
154
+ endpoint?: string;
155
+ onPassed: (token: string) => void;
156
+ onFailed?: (result: ChallengeFailed) => void;
157
+ /** Called when user exhausts all attempts */
158
+ onBlocked?: () => void;
159
+ };
160
+ /**
161
+ * Renders a step-up verification challenge when behavioral signals are inconclusive.
162
+ * Supports "click_targets" and "pattern" challenge types.
163
+ *
164
+ * @example
165
+ * // After verify() returns { human: false, confidence: 0.45 }:
166
+ * const ch = await fetch('/api/v1/challenge/create', { ... });
167
+ * return (
168
+ * <HumaChallenge
169
+ * challengeId={ch.challenge_id}
170
+ * type={ch.type}
171
+ * payload={ch.payload}
172
+ * apiKey="huma_live_..."
173
+ * onPassed={(token) => continueLogin(token)}
174
+ * onBlocked={() => router.push('/blocked')}
175
+ * />
176
+ * );
177
+ */
178
+ declare function HumaChallenge({ challengeId, type, payload, apiKey, endpoint, onPassed, onFailed, onBlocked, }: HumaChallengeProps): react_jsx_runtime.JSX.Element;
131
179
  type HumaVerifyButtonProps = {
132
180
  apiKey: string;
133
181
  userId: string;
@@ -154,4 +202,4 @@ type HumaVerifyButtonProps = {
154
202
  */
155
203
  declare function HumaVerifyButton({ apiKey, userId, endpoint, onVerified, onBlocked, children, className, style, }: HumaVerifyButtonProps): react_jsx_runtime.JSX.Element;
156
204
 
157
- export { HumaGate, HumaProvider, HumaVerifyButton, useHuma, useHumaContext, useHumaSession };
205
+ export { HumaChallenge, HumaGate, HumaProvider, HumaVerifyButton, useHuma, useHumaContext, useHumaSession };
package/dist/react.d.ts CHANGED
@@ -128,6 +128,54 @@ declare function useHumaSession(opts: UseHumaSessionOptions): {
128
128
  anomaly: boolean;
129
129
  stop: () => void;
130
130
  };
131
+ type ChallengeTarget = {
132
+ id: string;
133
+ x: number;
134
+ y: number;
135
+ label: string;
136
+ };
137
+ type ChallengeFailed = {
138
+ passed: false;
139
+ message: string;
140
+ attempts_remaining?: number;
141
+ };
142
+ type HumaChallengeProps = {
143
+ /** challenge_id from POST /api/v1/challenge/create */
144
+ challengeId: string;
145
+ /** type from challenge create response */
146
+ type: "click_targets" | "pattern";
147
+ /** payload from challenge create response */
148
+ payload: {
149
+ instruction: string;
150
+ targets?: ChallengeTarget[];
151
+ sequence?: number[];
152
+ };
153
+ apiKey: string;
154
+ endpoint?: string;
155
+ onPassed: (token: string) => void;
156
+ onFailed?: (result: ChallengeFailed) => void;
157
+ /** Called when user exhausts all attempts */
158
+ onBlocked?: () => void;
159
+ };
160
+ /**
161
+ * Renders a step-up verification challenge when behavioral signals are inconclusive.
162
+ * Supports "click_targets" and "pattern" challenge types.
163
+ *
164
+ * @example
165
+ * // After verify() returns { human: false, confidence: 0.45 }:
166
+ * const ch = await fetch('/api/v1/challenge/create', { ... });
167
+ * return (
168
+ * <HumaChallenge
169
+ * challengeId={ch.challenge_id}
170
+ * type={ch.type}
171
+ * payload={ch.payload}
172
+ * apiKey="huma_live_..."
173
+ * onPassed={(token) => continueLogin(token)}
174
+ * onBlocked={() => router.push('/blocked')}
175
+ * />
176
+ * );
177
+ */
178
+ declare function HumaChallenge({ challengeId, type, payload, apiKey, endpoint, onPassed, onFailed, onBlocked, }: HumaChallengeProps): react_jsx_runtime.JSX.Element;
131
179
  type HumaVerifyButtonProps = {
132
180
  apiKey: string;
133
181
  userId: string;
@@ -154,4 +202,4 @@ type HumaVerifyButtonProps = {
154
202
  */
155
203
  declare function HumaVerifyButton({ apiKey, userId, endpoint, onVerified, onBlocked, children, className, style, }: HumaVerifyButtonProps): react_jsx_runtime.JSX.Element;
156
204
 
157
- export { HumaGate, HumaProvider, HumaVerifyButton, useHuma, useHumaContext, useHumaSession };
205
+ export { HumaChallenge, HumaGate, HumaProvider, HumaVerifyButton, useHuma, useHumaContext, useHumaSession };
package/dist/react.js CHANGED
@@ -38,6 +38,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
38
38
  // src/react.tsx
39
39
  var react_exports = {};
40
40
  __export(react_exports, {
41
+ HumaChallenge: () => HumaChallenge,
41
42
  HumaGate: () => HumaGate,
42
43
  HumaProvider: () => HumaProvider,
43
44
  HumaVerifyButton: () => HumaVerifyButton,
@@ -73,6 +74,22 @@ function mouseFeatures(pts) {
73
74
  }
74
75
  return { speed_cv: cv(speeds), direction_changes: dirChanges, sample_count: pts.length };
75
76
  }
77
+ function touchFeatures(taps, moves, multiTouchCount) {
78
+ const tapIntervals = [];
79
+ for (let i = 1; i < taps.length; i++) tapIntervals.push(taps[i].t - taps[i - 1].t);
80
+ const durations = taps.map((t) => t.duration_ms);
81
+ const forces = taps.filter((t) => t.force > 0).map((t) => t.force);
82
+ const speeds = moves.map((m) => m.speed);
83
+ return {
84
+ tap_count: taps.length,
85
+ tap_interval_cv: cv(tapIntervals),
86
+ tap_duration_cv: cv(durations),
87
+ tap_force_cv: cv(forces),
88
+ touch_move_count: moves.length,
89
+ touch_speed_cv: cv(speeds),
90
+ multi_touch_count: multiTouchCount
91
+ };
92
+ }
76
93
  var HumaCollector = class {
77
94
  constructor() {
78
95
  this.signals = {
@@ -81,9 +98,13 @@ var HumaCollector = class {
81
98
  scrolls: [],
82
99
  clicks: [],
83
100
  focusEvents: [],
101
+ taps: [],
102
+ touchMoves: [],
103
+ multiTouchCount: 0,
84
104
  startTime: Date.now(),
85
105
  lastKeyTime: null,
86
- lastScrollTime: null
106
+ lastScrollTime: null,
107
+ activeTouches: {}
87
108
  };
88
109
  this.listening = false;
89
110
  this.onMouseMove = (e) => {
@@ -108,6 +129,46 @@ var HumaCollector = class {
108
129
  };
109
130
  this.onFocus = () => this.signals.focusEvents.push({ type: "focus", t: Date.now() });
110
131
  this.onBlur = () => this.signals.focusEvents.push({ type: "blur", t: Date.now() });
132
+ this.onTouchStart = (e) => {
133
+ const now = Date.now();
134
+ if (e.touches.length > 1) this.signals.multiTouchCount++;
135
+ for (let i = 0; i < e.changedTouches.length; i++) {
136
+ const t = e.changedTouches[i];
137
+ this.signals.activeTouches[t.identifier] = { t: now, x: t.clientX, y: t.clientY };
138
+ }
139
+ };
140
+ this.onTouchEnd = (e) => {
141
+ var _a;
142
+ const now = Date.now();
143
+ for (let i = 0; i < e.changedTouches.length; i++) {
144
+ const touch = e.changedTouches[i];
145
+ const start = this.signals.activeTouches[touch.identifier];
146
+ if (!start) continue;
147
+ delete this.signals.activeTouches[touch.identifier];
148
+ if (this.signals.taps.length < 80) {
149
+ this.signals.taps.push({
150
+ t: now,
151
+ duration_ms: now - start.t,
152
+ force: (_a = touch.force) != null ? _a : 0,
153
+ touch_count: e.touches.length + 1
154
+ });
155
+ }
156
+ }
157
+ };
158
+ this.onTouchMove = (e) => {
159
+ const now = Date.now();
160
+ for (let i = 0; i < e.changedTouches.length; i++) {
161
+ const touch = e.changedTouches[i];
162
+ const start = this.signals.activeTouches[touch.identifier];
163
+ if (!start) continue;
164
+ const dx = touch.clientX - start.x;
165
+ const dy = touch.clientY - start.y;
166
+ const dt = Math.max(now - start.t, 1);
167
+ if (this.signals.touchMoves.length < 100)
168
+ this.signals.touchMoves.push({ speed: Math.sqrt(dx * dx + dy * dy) / dt, t: now });
169
+ this.signals.activeTouches[touch.identifier] = { t: now, x: touch.clientX, y: touch.clientY };
170
+ }
171
+ };
111
172
  }
112
173
  start() {
113
174
  if (this.listening || typeof window === "undefined") return;
@@ -116,6 +177,9 @@ var HumaCollector = class {
116
177
  document.addEventListener("keydown", this.onKeyDown, { passive: true });
117
178
  document.addEventListener("scroll", this.onScroll, { passive: true });
118
179
  document.addEventListener("click", this.onClick, { passive: true });
180
+ document.addEventListener("touchstart", this.onTouchStart, { passive: true });
181
+ document.addEventListener("touchend", this.onTouchEnd, { passive: true });
182
+ document.addEventListener("touchmove", this.onTouchMove, { passive: true });
119
183
  window.addEventListener("focus", this.onFocus, { passive: true });
120
184
  window.addEventListener("blur", this.onBlur, { passive: true });
121
185
  }
@@ -126,12 +190,16 @@ var HumaCollector = class {
126
190
  document.removeEventListener("keydown", this.onKeyDown);
127
191
  document.removeEventListener("scroll", this.onScroll);
128
192
  document.removeEventListener("click", this.onClick);
193
+ document.removeEventListener("touchstart", this.onTouchStart);
194
+ document.removeEventListener("touchend", this.onTouchEnd);
195
+ document.removeEventListener("touchmove", this.onTouchMove);
129
196
  window.removeEventListener("focus", this.onFocus);
130
197
  window.removeEventListener("blur", this.onBlur);
131
198
  }
132
199
  extract() {
133
- const { keys, scrolls, clicks, focusEvents, startTime, mouse } = this.signals;
200
+ const { keys, scrolls, clicks, focusEvents, startTime, mouse, taps, touchMoves, multiTouchCount } = this.signals;
134
201
  const m = mouseFeatures(mouse);
202
+ const t = touchFeatures(taps, touchMoves, multiTouchCount);
135
203
  const clickIntervals = [];
136
204
  for (let i = 1; i < clicks.length; i++)
137
205
  clickIntervals.push(clicks[i].t - clicks[i - 1].t);
@@ -146,7 +214,14 @@ var HumaCollector = class {
146
214
  mouse_sample_count: m.sample_count,
147
215
  click_interval_cv: cv(clickIntervals),
148
216
  click_count: clicks.length,
149
- tab_switches: focusEvents.length
217
+ tab_switches: focusEvents.length,
218
+ tap_count: t.tap_count,
219
+ tap_interval_cv: t.tap_interval_cv,
220
+ tap_duration_cv: t.tap_duration_cv,
221
+ tap_force_cv: t.tap_force_cv,
222
+ touch_move_count: t.touch_move_count,
223
+ touch_speed_cv: t.touch_speed_cv,
224
+ multi_touch_count: t.multi_touch_count
150
225
  };
151
226
  }
152
227
  debug() {
@@ -299,6 +374,169 @@ function useHumaSession(opts) {
299
374
  }, [opts.intervalMs]);
300
375
  return { lastResult, anomaly, stop };
301
376
  }
377
+ var DOT_POSITIONS = {
378
+ 1: { x: 16.5, y: 16.5 },
379
+ 2: { x: 50, y: 16.5 },
380
+ 3: { x: 83.5, y: 16.5 },
381
+ 4: { x: 16.5, y: 50 },
382
+ 5: { x: 50, y: 50 },
383
+ 6: { x: 83.5, y: 50 },
384
+ 7: { x: 16.5, y: 83.5 },
385
+ 8: { x: 50, y: 83.5 },
386
+ 9: { x: 83.5, y: 83.5 }
387
+ };
388
+ function HumaChallenge({
389
+ challengeId,
390
+ type,
391
+ payload,
392
+ apiKey,
393
+ endpoint,
394
+ onPassed,
395
+ onFailed,
396
+ onBlocked
397
+ }) {
398
+ const [loading, setLoading] = (0, import_react.useState)(false);
399
+ const [error, setError] = (0, import_react.useState)(null);
400
+ const [attemptsLeft, setAttemptsLeft] = (0, import_react.useState)(3);
401
+ const [userSequence, setUserSequence] = (0, import_react.useState)([]);
402
+ const [showingSequence, setShowingSequence] = (0, import_react.useState)(type === "pattern");
403
+ const [highlightedDot, setHighlightedDot] = (0, import_react.useState)(null);
404
+ (0, import_react.useEffect)(() => {
405
+ if (type !== "pattern" || !payload.sequence) return;
406
+ let i = 0;
407
+ const interval = setInterval(() => {
408
+ var _a;
409
+ setHighlightedDot((_a = payload.sequence[i]) != null ? _a : null);
410
+ i++;
411
+ if (i > payload.sequence.length) {
412
+ clearInterval(interval);
413
+ setShowingSequence(false);
414
+ setHighlightedDot(null);
415
+ }
416
+ }, 700);
417
+ return () => clearInterval(interval);
418
+ }, []);
419
+ const submitAnswer = async (answer) => {
420
+ var _a, _b;
421
+ setLoading(true);
422
+ setError(null);
423
+ const verifyEndpoint = (endpoint != null ? endpoint : "https://humaverify.com").replace(/\/api\/v1\/verify$/, "").replace(/\/$/, "") + "/api/v1/challenge/verify";
424
+ try {
425
+ const res = await fetch(verifyEndpoint, {
426
+ method: "POST",
427
+ headers: {
428
+ Authorization: `Bearer ${apiKey}`,
429
+ "Content-Type": "application/json"
430
+ },
431
+ body: JSON.stringify({ challenge_id: challengeId, answer })
432
+ });
433
+ const data = await res.json();
434
+ if (data.passed) {
435
+ onPassed(data.token);
436
+ } else {
437
+ const remaining = (_a = data.attempts_remaining) != null ? _a : 0;
438
+ setAttemptsLeft(remaining);
439
+ setError((_b = data.message) != null ? _b : "Incorrect");
440
+ onFailed == null ? void 0 : onFailed(data);
441
+ if (remaining <= 0) onBlocked == null ? void 0 : onBlocked();
442
+ if (type === "pattern") setUserSequence([]);
443
+ }
444
+ } catch (e) {
445
+ setError("Network error \u2014 please try again");
446
+ } finally {
447
+ setLoading(false);
448
+ }
449
+ };
450
+ const handleTargetClick = (targetId) => {
451
+ if (loading) return;
452
+ submitAnswer(targetId);
453
+ };
454
+ const handleDotClick = (dot) => {
455
+ if (loading || showingSequence) return;
456
+ const next = [...userSequence, dot];
457
+ setUserSequence(next);
458
+ if (payload.sequence && next.length === payload.sequence.length) {
459
+ submitAnswer(next.join("-"));
460
+ }
461
+ };
462
+ const containerStyle = {
463
+ fontFamily: "'DM Mono', 'Courier New', monospace",
464
+ background: "#0d0d0d",
465
+ border: "1px solid rgba(125,211,168,0.2)",
466
+ padding: "24px",
467
+ maxWidth: 360,
468
+ color: "#e2e8f0"
469
+ };
470
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: containerStyle, role: "dialog", "aria-label": "Human verification challenge", children: [
471
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 10, letterSpacing: "0.15em", textTransform: "uppercase", color: "rgba(125,211,168,0.7)", marginBottom: 4 }, children: "HUMA \u2014 Step-up Verification" }),
472
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 13, color: "#e2e8f0", marginBottom: 20, lineHeight: 1.5 }, children: showingSequence ? "Watch the sequence..." : payload.instruction }),
473
+ type === "click_targets" && payload.targets && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { position: "relative", width: "100%", paddingTop: "80%", background: "#161616", border: "1px solid #2d2d2d", marginBottom: 16 }, children: payload.targets.map((t) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
474
+ "button",
475
+ {
476
+ onClick: () => handleTargetClick(t.id),
477
+ disabled: loading,
478
+ style: {
479
+ position: "absolute",
480
+ left: `${t.x}%`,
481
+ top: `${t.y}%`,
482
+ transform: "translate(-50%, -50%)",
483
+ fontSize: 28,
484
+ background: "transparent",
485
+ border: "none",
486
+ cursor: loading ? "default" : "pointer",
487
+ padding: 8,
488
+ borderRadius: 8,
489
+ transition: "transform 0.1s"
490
+ },
491
+ "aria-label": `Target ${t.label}`,
492
+ children: t.label
493
+ },
494
+ t.id
495
+ )) }),
496
+ type === "pattern" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { position: "relative", width: "100%", paddingTop: "100%", background: "#161616", border: "1px solid #2d2d2d", borderRadius: 4, marginBottom: 16 }, children: [1, 2, 3, 4, 5, 6, 7, 8, 9].map((dot) => {
497
+ const pos = DOT_POSITIONS[dot];
498
+ const isActive = userSequence.includes(dot);
499
+ const isHl = highlightedDot === dot;
500
+ const size = 18;
501
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
502
+ "button",
503
+ {
504
+ onClick: () => handleDotClick(dot),
505
+ disabled: loading || showingSequence || isActive,
506
+ style: {
507
+ position: "absolute",
508
+ left: `${pos.x}%`,
509
+ top: `${pos.y}%`,
510
+ transform: "translate(-50%, -50%)",
511
+ width: size,
512
+ height: size,
513
+ borderRadius: "50%",
514
+ background: isHl ? "rgba(125,211,168,0.9)" : isActive ? "rgba(125,211,168,0.5)" : "rgba(255,255,255,0.15)",
515
+ border: isHl ? "2px solid #7dd3a8" : "2px solid transparent",
516
+ cursor: loading || showingSequence || isActive ? "default" : "pointer",
517
+ transition: "background 0.15s, border 0.15s"
518
+ },
519
+ "aria-label": `Dot ${dot}`
520
+ },
521
+ dot
522
+ );
523
+ }) }),
524
+ error && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { fontSize: 11, color: "#f87171", marginBottom: 12, letterSpacing: "0.05em" }, children: [
525
+ error,
526
+ attemptsLeft > 0 ? ` \u2014 ${attemptsLeft} attempt${attemptsLeft !== 1 ? "s" : ""} remaining` : ""
527
+ ] }),
528
+ loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 11, color: "rgba(125,211,168,0.7)", letterSpacing: "0.08em" }, children: "Verifying\u2026" }),
529
+ type === "pattern" && !showingSequence && userSequence.length > 0 && !loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
530
+ "button",
531
+ {
532
+ onClick: () => setUserSequence([]),
533
+ style: { fontSize: 10, color: "#64748b", background: "transparent", border: "none", cursor: "pointer", letterSpacing: "0.08em" },
534
+ children: "Reset pattern"
535
+ }
536
+ ),
537
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 9, color: "#334155", marginTop: 16, letterSpacing: "0.05em" }, children: "Secured by HUMA \xB7 humaverify.com" })
538
+ ] });
539
+ }
302
540
  function HumaVerifyButton({
303
541
  apiKey,
304
542
  userId,
@@ -320,6 +558,7 @@ function HumaVerifyButton({
320
558
  }
321
559
  // Annotate the CommonJS export names for ESM import in node:
322
560
  0 && (module.exports = {
561
+ HumaChallenge,
323
562
  HumaGate,
324
563
  HumaProvider,
325
564
  HumaVerifyButton,