@flamingo-stack/openframe-frontend-core 0.0.572 → 0.0.573-1933.5266.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.
Files changed (122) hide show
  1. package/dist/{chunk-3ZCIC4MS.js → chunk-2BNPLJSU.js} +3 -3
  2. package/dist/{chunk-2IMAYSYH.cjs → chunk-47Y537OE.cjs} +19 -19
  3. package/dist/{chunk-2IMAYSYH.cjs.map → chunk-47Y537OE.cjs.map} +1 -1
  4. package/dist/{chunk-LMRBXA2F.cjs → chunk-5TVO35AW.cjs} +3 -3
  5. package/dist/{chunk-LMRBXA2F.cjs.map → chunk-5TVO35AW.cjs.map} +1 -1
  6. package/dist/{chunk-YSG6JYBP.cjs → chunk-5ZKZK3V2.cjs} +16 -16
  7. package/dist/{chunk-YSG6JYBP.cjs.map → chunk-5ZKZK3V2.cjs.map} +1 -1
  8. package/dist/{chunk-VKZDIRXC.cjs → chunk-66CKTUIV.cjs} +3 -3
  9. package/dist/{chunk-VKZDIRXC.cjs.map → chunk-66CKTUIV.cjs.map} +1 -1
  10. package/dist/{chunk-OQNIQVET.js → chunk-6QJYO3SB.js} +21 -3
  11. package/dist/chunk-6QJYO3SB.js.map +1 -0
  12. package/dist/{chunk-RSVRM35D.js → chunk-6ZC6IC6H.js} +2 -2
  13. package/dist/{chunk-7Y2MC6LN.cjs → chunk-7D3KGRH5.cjs} +37 -37
  14. package/dist/{chunk-7Y2MC6LN.cjs.map → chunk-7D3KGRH5.cjs.map} +1 -1
  15. package/dist/{chunk-R6UKDAGZ.js → chunk-BNAN4WLF.js} +6 -6
  16. package/dist/{chunk-JS3U2V7J.js → chunk-CNNRCN6S.js} +3 -3
  17. package/dist/{chunk-P5MRXAXN.cjs → chunk-D5GJWW6Z.cjs} +10 -10
  18. package/dist/{chunk-P5MRXAXN.cjs.map → chunk-D5GJWW6Z.cjs.map} +1 -1
  19. package/dist/{chunk-7A7SH2RI.js → chunk-E4W7RVFZ.js} +2 -2
  20. package/dist/{chunk-IGGHHVOD.js → chunk-EHQNJDP6.js} +3 -3
  21. package/dist/{chunk-L6C7SYRU.js → chunk-ET5V44BF.js} +4 -4
  22. package/dist/{chunk-MVR5MU7B.js → chunk-G5EH3A2U.js} +4 -4
  23. package/dist/{chunk-ZHI7PPI7.cjs → chunk-HV3SJHX2.cjs} +7 -7
  24. package/dist/{chunk-ZHI7PPI7.cjs.map → chunk-HV3SJHX2.cjs.map} +1 -1
  25. package/dist/{chunk-5PFN7X2H.cjs → chunk-IHW7HEFU.cjs} +22 -22
  26. package/dist/{chunk-5PFN7X2H.cjs.map → chunk-IHW7HEFU.cjs.map} +1 -1
  27. package/dist/{chunk-QCWTNMWZ.js → chunk-KELXUVJW.js} +25 -6
  28. package/dist/{chunk-QCWTNMWZ.js.map → chunk-KELXUVJW.js.map} +1 -1
  29. package/dist/{chunk-A3WAKKCV.cjs → chunk-LC4AKB7U.cjs} +3 -3
  30. package/dist/{chunk-A3WAKKCV.cjs.map → chunk-LC4AKB7U.cjs.map} +1 -1
  31. package/dist/{chunk-II52DW7X.js → chunk-OZGDWUVN.js} +2 -2
  32. package/dist/{chunk-3CWVBREW.cjs → chunk-P2MTEESX.cjs} +64 -46
  33. package/dist/chunk-P2MTEESX.cjs.map +1 -0
  34. package/dist/{chunk-GWDOWAHU.cjs → chunk-PLBXVJIN.cjs} +3 -3
  35. package/dist/{chunk-GWDOWAHU.cjs.map → chunk-PLBXVJIN.cjs.map} +1 -1
  36. package/dist/{chunk-6JZXYDWC.js → chunk-PVZZGQMZ.js} +5 -5
  37. package/dist/{chunk-W6PFJ6RY.cjs → chunk-QXEXMPXV.cjs} +7 -7
  38. package/dist/{chunk-W6PFJ6RY.cjs.map → chunk-QXEXMPXV.cjs.map} +1 -1
  39. package/dist/{chunk-VNYEX66A.js → chunk-SVARR3OQ.js} +6 -6
  40. package/dist/{chunk-ZIF4IJ5L.cjs → chunk-VKYND7JR.cjs} +53 -53
  41. package/dist/{chunk-ZIF4IJ5L.cjs.map → chunk-VKYND7JR.cjs.map} +1 -1
  42. package/dist/{chunk-IZBQBUDP.js → chunk-X3VKHMHA.js} +3 -3
  43. package/dist/{chunk-RXMY3LTP.js → chunk-X4M3KSFH.js} +2 -2
  44. package/dist/{chunk-5LFT3RJQ.cjs → chunk-Y24CV7JN.cjs} +17 -17
  45. package/dist/{chunk-5LFT3RJQ.cjs.map → chunk-Y24CV7JN.cjs.map} +1 -1
  46. package/dist/{chunk-GC3TT3UF.cjs → chunk-YNBE6G5I.cjs} +26 -7
  47. package/dist/chunk-YNBE6G5I.cjs.map +1 -0
  48. package/dist/{chunk-KXKMVZUG.js → chunk-YW2JM4EC.js} +2 -2
  49. package/dist/{chunk-LE4RPH3E.cjs → chunk-YYO5RZJU.cjs} +36 -36
  50. package/dist/{chunk-LE4RPH3E.cjs.map → chunk-YYO5RZJU.cjs.map} +1 -1
  51. package/dist/components/case-studies/index.cjs +9 -9
  52. package/dist/components/case-studies/index.js +3 -3
  53. package/dist/components/contact/index.cjs +4 -4
  54. package/dist/components/contact/index.js +3 -3
  55. package/dist/components/docs/index.cjs +7 -7
  56. package/dist/components/docs/index.js +6 -6
  57. package/dist/components/embeds/index.cjs +4 -4
  58. package/dist/components/embeds/index.js +3 -3
  59. package/dist/components/faq/index.cjs +4 -4
  60. package/dist/components/faq/index.js +3 -3
  61. package/dist/components/features/index.cjs +3 -3
  62. package/dist/components/features/index.js +2 -2
  63. package/dist/components/help-center-pages/index.cjs +23 -23
  64. package/dist/components/help-center-pages/index.js +14 -14
  65. package/dist/components/index.cjs +118 -118
  66. package/dist/components/index.js +13 -13
  67. package/dist/components/meeting-scheduler/index.cjs +19 -19
  68. package/dist/components/meeting-scheduler/index.cjs.map +1 -1
  69. package/dist/components/meeting-scheduler/index.js +3 -3
  70. package/dist/components/meeting-scheduler/index.js.map +1 -1
  71. package/dist/components/navigation/index.cjs +6 -6
  72. package/dist/components/navigation/index.js +5 -5
  73. package/dist/components/onboarding-guides/index.cjs +6 -6
  74. package/dist/components/onboarding-guides/index.js +5 -5
  75. package/dist/components/tickets/index.cjs +7 -7
  76. package/dist/components/tickets/index.js +6 -6
  77. package/dist/components/ui/honeypot-field.d.ts +11 -0
  78. package/dist/components/ui/honeypot-field.d.ts.map +1 -1
  79. package/dist/components/ui/index.cjs +3 -3
  80. package/dist/components/ui/index.js +2 -2
  81. package/dist/hooks/index.cjs +2 -2
  82. package/dist/hooks/index.js +1 -1
  83. package/dist/hooks/use-humanity-signals.d.ts +1 -1
  84. package/dist/hooks/use-meeting-booking.d.ts +3 -3
  85. package/dist/hooks/use-meeting-booking.d.ts.map +1 -1
  86. package/dist/index.cjs +9 -7
  87. package/dist/index.cjs.map +1 -1
  88. package/dist/index.js +8 -6
  89. package/dist/index.js.map +1 -1
  90. package/dist/utils/humanity-signals.cjs +24 -5
  91. package/dist/utils/humanity-signals.cjs.map +1 -1
  92. package/dist/utils/humanity-signals.d.ts +27 -4
  93. package/dist/utils/humanity-signals.d.ts.map +1 -1
  94. package/dist/utils/humanity-signals.js +24 -6
  95. package/dist/utils/humanity-signals.js.map +1 -1
  96. package/dist/utils/index.cjs +24 -5
  97. package/dist/utils/index.cjs.map +1 -1
  98. package/dist/utils/index.js +24 -6
  99. package/dist/utils/index.js.map +1 -1
  100. package/package.json +1 -1
  101. package/src/components/ui/honeypot-field.tsx +27 -1
  102. package/src/hooks/use-meeting-booking.ts +7 -5
  103. package/src/utils/__tests__/hubspot-meetings-convention.test.ts +4 -2
  104. package/src/utils/__tests__/humanity-signals.test.ts +75 -0
  105. package/src/utils/humanity-signals.ts +56 -8
  106. package/dist/chunk-3CWVBREW.cjs.map +0 -1
  107. package/dist/chunk-GC3TT3UF.cjs.map +0 -1
  108. package/dist/chunk-OQNIQVET.js.map +0 -1
  109. /package/dist/{chunk-3ZCIC4MS.js.map → chunk-2BNPLJSU.js.map} +0 -0
  110. /package/dist/{chunk-RSVRM35D.js.map → chunk-6ZC6IC6H.js.map} +0 -0
  111. /package/dist/{chunk-R6UKDAGZ.js.map → chunk-BNAN4WLF.js.map} +0 -0
  112. /package/dist/{chunk-JS3U2V7J.js.map → chunk-CNNRCN6S.js.map} +0 -0
  113. /package/dist/{chunk-7A7SH2RI.js.map → chunk-E4W7RVFZ.js.map} +0 -0
  114. /package/dist/{chunk-IGGHHVOD.js.map → chunk-EHQNJDP6.js.map} +0 -0
  115. /package/dist/{chunk-L6C7SYRU.js.map → chunk-ET5V44BF.js.map} +0 -0
  116. /package/dist/{chunk-MVR5MU7B.js.map → chunk-G5EH3A2U.js.map} +0 -0
  117. /package/dist/{chunk-II52DW7X.js.map → chunk-OZGDWUVN.js.map} +0 -0
  118. /package/dist/{chunk-6JZXYDWC.js.map → chunk-PVZZGQMZ.js.map} +0 -0
  119. /package/dist/{chunk-VNYEX66A.js.map → chunk-SVARR3OQ.js.map} +0 -0
  120. /package/dist/{chunk-IZBQBUDP.js.map → chunk-X3VKHMHA.js.map} +0 -0
  121. /package/dist/{chunk-RXMY3LTP.js.map → chunk-X4M3KSFH.js.map} +0 -0
  122. /package/dist/{chunk-KXKMVZUG.js.map → chunk-YW2JM4EC.js.map} +0 -0
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  SoftwareInfo,
10
10
  SoftwareSourceBadge,
11
11
  ToolBadge
12
- } from "./chunk-JS3U2V7J.js";
12
+ } from "./chunk-CNNRCN6S.js";
13
13
  import {
14
14
  AppHeader,
15
15
  AppLayout,
@@ -28,14 +28,14 @@ import {
28
28
  SlidingSidebar,
29
29
  TicketAlertsButton,
30
30
  TopNavigation
31
- } from "./chunk-L6C7SYRU.js";
31
+ } from "./chunk-ET5V44BF.js";
32
32
  import {
33
33
  MobileNavigationDropdown,
34
34
  MultiLevelNavigation,
35
35
  StickySectionNav,
36
36
  useSectionNavigation
37
- } from "./chunk-II52DW7X.js";
38
- import "./chunk-RXMY3LTP.js";
37
+ } from "./chunk-OZGDWUVN.js";
38
+ import "./chunk-X4M3KSFH.js";
39
39
  import {
40
40
  ADMIN_APPROVAL_REQUEST_CONTEXT_TYPE,
41
41
  AIEnrichButton,
@@ -576,7 +576,7 @@ import {
576
576
  withFirstTouchAttribution,
577
577
  withWalkthroughOpenParam,
578
578
  writeDismissCookie
579
- } from "./chunk-OQNIQVET.js";
579
+ } from "./chunk-6QJYO3SB.js";
580
580
  import "./chunk-L2SLFCXP.js";
581
581
  import "./chunk-BJXDHKUX.js";
582
582
  import {
@@ -586,6 +586,7 @@ import {
586
586
  GraphQLIntrospector,
587
587
  HONEYPOT_FIELD,
588
588
  HUMANITY_SIGNAL_KEYS,
589
+ LEGACY_HONEYPOT_FIELD,
589
590
  ToastCard,
590
591
  Toaster,
591
592
  batchFetchAuthenticatedImages,
@@ -682,7 +683,7 @@ import {
682
683
  validateSchema,
683
684
  validateVariables,
684
685
  variablesToUrlParams
685
- } from "./chunk-QCWTNMWZ.js";
686
+ } from "./chunk-KELXUVJW.js";
686
687
  import "./chunk-ZRIVCLPR.js";
687
688
  import {
688
689
  CONTENT_REF_GROUPS,
@@ -2080,6 +2081,7 @@ export {
2080
2081
  JsonDetailList,
2081
2082
  JumpInIcon,
2082
2083
  KnowledgeBaseLinksManager,
2084
+ LEGACY_HONEYPOT_FIELD,
2083
2085
  LOG_SEVERITY_COLORS,
2084
2086
  LOG_SEVERITY_LABELS,
2085
2087
  Label,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/logs-list.tsx","../src/components/log-severity-dot.tsx","../src/types/logs.types.ts","../src/assets/index.ts"],"sourcesContent":["import { type FC, forwardRef, useRef } from 'react';\nimport type { LogsListProps, LogEntry } from '../types/logs.types';\nimport { cn } from '../utils/cn';\nimport { LogSeverityDot } from './log-severity-dot';\nimport { ToolIcon } from './tool-icon';\n\nconst formatTimestamp = (timestamp: string | Date): string => {\n const date = timestamp instanceof Date ? timestamp : new Date(timestamp);\n\n // UTC getters so the timestamp is identical on server (UTC) and client\n // (local) — otherwise React #418 hydration mismatch.\n const year = date.getUTCFullYear();\n const month = String(date.getUTCMonth() + 1).padStart(2, '0');\n const day = String(date.getUTCDate()).padStart(2, '0');\n const hours = String(date.getUTCHours()).padStart(2, '0');\n const minutes = String(date.getUTCMinutes()).padStart(2, '0');\n\n return `${year}/${month}/${day},${hours}:${minutes}`;\n};\n\nconst LogCard: FC<{\n log: LogEntry;\n isLast: boolean;\n showConnector: boolean;\n onClick?: () => void;\n}> = ({ log, isLast, showConnector, onClick }) => {\n return (\n <div className=\"relative\">\n <div\n className={cn(\n 'relative box-border flex w-full items-start gap-3 rounded px-1 py-2',\n 'cursor-pointer transition-colors hover:bg-ods-bg-hover/50',\n )}\n onClick={onClick}\n role=\"button\"\n tabIndex={0}\n onKeyDown={e => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onClick?.();\n }\n }}\n >\n <LogSeverityDot severity={log.severity} size=\"md\" />\n\n <div className=\"min-w-0 flex-1 space-y-1\">\n <p className=\"text-ods-text-primary text-h6\">{log.title}</p>\n <div className=\"flex items-center gap-2\">\n <p className=\"text-ods-text-secondary text-h5\">{formatTimestamp(log.timestamp)}</p>\n {log.toolType && <ToolIcon toolType={log.toolType} size={16} />}\n </div>\n </div>\n </div>\n\n {showConnector && !isLast && (\n <div\n className=\"absolute left-[15px] w-[2px] bg-ods-border\"\n style={{\n top: '28px',\n bottom: '-10px',\n }}\n aria-hidden=\"true\"\n />\n )}\n </div>\n );\n};\n\nexport const LogsList = forwardRef<HTMLDivElement, LogsListProps>(\n (\n {\n logs,\n maxHeight = '400px',\n showConnector = true,\n onLogClick,\n loading = false,\n emptyMessage = 'No logs to display',\n className,\n },\n ref,\n ) => {\n const containerRef = useRef<HTMLDivElement>(null);\n\n const isFullHeight = maxHeight === '100%';\n\n const getContainerStyles = () => {\n if (isFullHeight) return undefined;\n return { maxHeight, minHeight: '200px' };\n };\n\n const getContainerClasses = () => {\n if (isFullHeight) return 'h-full';\n return '';\n };\n\n if (loading) {\n return (\n <div\n ref={ref}\n className={cn(\n 'rounded-lg border border-ods-border bg-ods-card p-4',\n 'flex items-center justify-center',\n getContainerClasses(),\n className,\n )}\n style={getContainerStyles()}\n >\n <div className=\"text-ods-text-muted text-h6\">Loading logs...</div>\n </div>\n );\n }\n\n if (logs.length === 0) {\n return (\n <div\n ref={ref}\n className={cn(\n 'rounded-lg border border-ods-border bg-ods-card p-4',\n 'flex items-center justify-center',\n getContainerClasses(),\n className,\n )}\n style={getContainerStyles()}\n >\n <div className=\"text-ods-text-muted text-h6\">{emptyMessage}</div>\n </div>\n );\n }\n\n return (\n <div\n ref={ref}\n className={cn(\n 'relative rounded-lg border border-ods-border bg-ods-card',\n 'flex flex-col overflow-hidden',\n getContainerClasses(),\n className,\n )}\n style={getContainerStyles()}\n >\n <div ref={containerRef} className=\"flex-1 overflow-y-auto px-4 py-3\">\n {logs.map((log, index) => (\n <LogCard\n key={log.id}\n log={log}\n isLast={index === logs.length - 1}\n showConnector={showConnector}\n onClick={() => onLogClick?.(log)}\n />\n ))}\n </div>\n </div>\n );\n },\n);\n\nLogsList.displayName = 'LogsList';\n","import { forwardRef } from 'react';\nimport { type LogSeverityDotProps, LOG_SEVERITY_COLORS, LOG_SEVERITY_LABELS } from '../types/logs.types';\nimport { cn } from '../utils/cn';\n\nexport const LogSeverityDot = forwardRef<HTMLDivElement, LogSeverityDotProps>(\n ({ severity, size = 'md', className }, ref) => {\n const sizeClasses = {\n sm: 'w-4 h-4',\n md: 'w-6 h-6',\n lg: 'w-8 h-8',\n };\n\n const dotSizeClasses = {\n sm: 'w-2 h-2',\n md: 'w-3 h-3',\n lg: 'w-4 h-4',\n };\n\n return (\n <div\n ref={ref}\n className={cn('relative flex shrink-0 items-center justify-center', sizeClasses[size], className)}\n aria-label={`${LOG_SEVERITY_LABELS[severity]} severity`}\n >\n <div\n className={cn('rounded-full', dotSizeClasses[size])}\n style={{ backgroundColor: LOG_SEVERITY_COLORS[severity] }}\n />\n </div>\n );\n },\n);\n\nLogSeverityDot.displayName = 'LogSeverityDot';\n","import type { ToolType } from './tool.types';\n\nexport type LogSeverity = 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL';\n\nexport interface LogEntry {\n id: string;\n ingestDay: string;\n eventType: string;\n severity: LogSeverity;\n title: string;\n timestamp: string | Date;\n toolType?: ToolType;\n toolIcon?: string;\n message?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface LogsListProps {\n logs: LogEntry[];\n maxHeight?: string | number;\n showConnector?: boolean;\n onLogClick?: (log: LogEntry) => void;\n loading?: boolean;\n emptyMessage?: string;\n className?: string;\n}\n\nexport interface LogSeverityDotProps {\n severity: LogSeverity;\n size?: 'sm' | 'md' | 'lg';\n className?: string;\n}\n\nexport const LOG_SEVERITY_COLORS: Record<LogSeverity, string> = {\n DEBUG: '#888888',\n INFO: '#888888',\n WARNING: '#e1b32f',\n ERROR: '#f36666',\n CRITICAL: '#b43b3b',\n};\n\nexport const LOG_SEVERITY_LABELS: Record<LogSeverity, string> = {\n DEBUG: 'Debug',\n INFO: 'Info',\n WARNING: 'Warning',\n ERROR: 'Error',\n CRITICAL: 'Critical',\n};\n","// Assets exports\n// Font files and other static assets\n// Platform utilities will be available when needed\n\nexport const assets = {} as const;\nexport * from './fae-avatar';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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","names":["forwardRef","jsx","forwardRef"]}
1
+ {"version":3,"sources":["../src/components/logs-list.tsx","../src/components/log-severity-dot.tsx","../src/types/logs.types.ts","../src/assets/index.ts"],"sourcesContent":["import { type FC, forwardRef, useRef } from 'react';\nimport type { LogsListProps, LogEntry } from '../types/logs.types';\nimport { cn } from '../utils/cn';\nimport { LogSeverityDot } from './log-severity-dot';\nimport { ToolIcon } from './tool-icon';\n\nconst formatTimestamp = (timestamp: string | Date): string => {\n const date = timestamp instanceof Date ? timestamp : new Date(timestamp);\n\n // UTC getters so the timestamp is identical on server (UTC) and client\n // (local) — otherwise React #418 hydration mismatch.\n const year = date.getUTCFullYear();\n const month = String(date.getUTCMonth() + 1).padStart(2, '0');\n const day = String(date.getUTCDate()).padStart(2, '0');\n const hours = String(date.getUTCHours()).padStart(2, '0');\n const minutes = String(date.getUTCMinutes()).padStart(2, '0');\n\n return `${year}/${month}/${day},${hours}:${minutes}`;\n};\n\nconst LogCard: FC<{\n log: LogEntry;\n isLast: boolean;\n showConnector: boolean;\n onClick?: () => void;\n}> = ({ log, isLast, showConnector, onClick }) => {\n return (\n <div className=\"relative\">\n <div\n className={cn(\n 'relative box-border flex w-full items-start gap-3 rounded px-1 py-2',\n 'cursor-pointer transition-colors hover:bg-ods-bg-hover/50',\n )}\n onClick={onClick}\n role=\"button\"\n tabIndex={0}\n onKeyDown={e => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onClick?.();\n }\n }}\n >\n <LogSeverityDot severity={log.severity} size=\"md\" />\n\n <div className=\"min-w-0 flex-1 space-y-1\">\n <p className=\"text-ods-text-primary text-h6\">{log.title}</p>\n <div className=\"flex items-center gap-2\">\n <p className=\"text-ods-text-secondary text-h5\">{formatTimestamp(log.timestamp)}</p>\n {log.toolType && <ToolIcon toolType={log.toolType} size={16} />}\n </div>\n </div>\n </div>\n\n {showConnector && !isLast && (\n <div\n className=\"absolute left-[15px] w-[2px] bg-ods-border\"\n style={{\n top: '28px',\n bottom: '-10px',\n }}\n aria-hidden=\"true\"\n />\n )}\n </div>\n );\n};\n\nexport const LogsList = forwardRef<HTMLDivElement, LogsListProps>(\n (\n {\n logs,\n maxHeight = '400px',\n showConnector = true,\n onLogClick,\n loading = false,\n emptyMessage = 'No logs to display',\n className,\n },\n ref,\n ) => {\n const containerRef = useRef<HTMLDivElement>(null);\n\n const isFullHeight = maxHeight === '100%';\n\n const getContainerStyles = () => {\n if (isFullHeight) return undefined;\n return { maxHeight, minHeight: '200px' };\n };\n\n const getContainerClasses = () => {\n if (isFullHeight) return 'h-full';\n return '';\n };\n\n if (loading) {\n return (\n <div\n ref={ref}\n className={cn(\n 'rounded-lg border border-ods-border bg-ods-card p-4',\n 'flex items-center justify-center',\n getContainerClasses(),\n className,\n )}\n style={getContainerStyles()}\n >\n <div className=\"text-ods-text-muted text-h6\">Loading logs...</div>\n </div>\n );\n }\n\n if (logs.length === 0) {\n return (\n <div\n ref={ref}\n className={cn(\n 'rounded-lg border border-ods-border bg-ods-card p-4',\n 'flex items-center justify-center',\n getContainerClasses(),\n className,\n )}\n style={getContainerStyles()}\n >\n <div className=\"text-ods-text-muted text-h6\">{emptyMessage}</div>\n </div>\n );\n }\n\n return (\n <div\n ref={ref}\n className={cn(\n 'relative rounded-lg border border-ods-border bg-ods-card',\n 'flex flex-col overflow-hidden',\n getContainerClasses(),\n className,\n )}\n style={getContainerStyles()}\n >\n <div ref={containerRef} className=\"flex-1 overflow-y-auto px-4 py-3\">\n {logs.map((log, index) => (\n <LogCard\n key={log.id}\n log={log}\n isLast={index === logs.length - 1}\n showConnector={showConnector}\n onClick={() => onLogClick?.(log)}\n />\n ))}\n </div>\n </div>\n );\n },\n);\n\nLogsList.displayName = 'LogsList';\n","import { forwardRef } from 'react';\nimport { type LogSeverityDotProps, LOG_SEVERITY_COLORS, LOG_SEVERITY_LABELS } from '../types/logs.types';\nimport { cn } from '../utils/cn';\n\nexport const LogSeverityDot = forwardRef<HTMLDivElement, LogSeverityDotProps>(\n ({ severity, size = 'md', className }, ref) => {\n const sizeClasses = {\n sm: 'w-4 h-4',\n md: 'w-6 h-6',\n lg: 'w-8 h-8',\n };\n\n const dotSizeClasses = {\n sm: 'w-2 h-2',\n md: 'w-3 h-3',\n lg: 'w-4 h-4',\n };\n\n return (\n <div\n ref={ref}\n className={cn('relative flex shrink-0 items-center justify-center', sizeClasses[size], className)}\n aria-label={`${LOG_SEVERITY_LABELS[severity]} severity`}\n >\n <div\n className={cn('rounded-full', dotSizeClasses[size])}\n style={{ backgroundColor: LOG_SEVERITY_COLORS[severity] }}\n />\n </div>\n );\n },\n);\n\nLogSeverityDot.displayName = 'LogSeverityDot';\n","import type { ToolType } from './tool.types';\n\nexport type LogSeverity = 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL';\n\nexport interface LogEntry {\n id: string;\n ingestDay: string;\n eventType: string;\n severity: LogSeverity;\n title: string;\n timestamp: string | Date;\n toolType?: ToolType;\n toolIcon?: string;\n message?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface LogsListProps {\n logs: LogEntry[];\n maxHeight?: string | number;\n showConnector?: boolean;\n onLogClick?: (log: LogEntry) => void;\n loading?: boolean;\n emptyMessage?: string;\n className?: string;\n}\n\nexport interface LogSeverityDotProps {\n severity: LogSeverity;\n size?: 'sm' | 'md' | 'lg';\n className?: string;\n}\n\nexport const LOG_SEVERITY_COLORS: Record<LogSeverity, string> = {\n DEBUG: '#888888',\n INFO: '#888888',\n WARNING: '#e1b32f',\n ERROR: '#f36666',\n CRITICAL: '#b43b3b',\n};\n\nexport const LOG_SEVERITY_LABELS: Record<LogSeverity, string> = {\n DEBUG: 'Debug',\n INFO: 'Info',\n WARNING: 'Warning',\n ERROR: 'Error',\n CRITICAL: 'Critical',\n};\n","// Assets exports\n// Font files and other static assets\n// Platform utilities will be available when needed\n\nexport const assets = {} as const;\nexport * from './fae-avatar';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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","names":["forwardRef","jsx","forwardRef"]}
@@ -1,23 +1,41 @@
1
1
  'use strict';
2
2
 
3
3
  // src/utils/humanity-signals.ts
4
- var HONEYPOT_FIELD = "contact_url_confirm";
4
+ var HONEYPOT_FIELD = "form_extra_note";
5
+ var LEGACY_HONEYPOT_FIELD = "contact_url_confirm";
5
6
  var ELAPSED_MS_FIELD = "form_elapsed_ms";
6
7
  var DEFAULT_MIN_FILL_MS = 700;
7
- var HUMANITY_SIGNAL_KEYS = [HONEYPOT_FIELD, ELAPSED_MS_FIELD];
8
+ var HUMANITY_SIGNAL_KEYS = [HONEYPOT_FIELD, LEGACY_HONEYPOT_FIELD, ELAPSED_MS_FIELD];
8
9
  function extractHumanitySignals(body) {
9
10
  const b = body ?? {};
10
- const rawHp = b[HONEYPOT_FIELD];
11
+ const rawHp = b[HONEYPOT_FIELD] ?? b[LEGACY_HONEYPOT_FIELD];
11
12
  const honeypot = rawHp == null ? "" : String(rawHp);
12
13
  const rawMs = b[ELAPSED_MS_FIELD];
13
14
  const elapsedMs = typeof rawMs === "number" && Number.isFinite(rawMs) ? rawMs : null;
14
15
  return { honeypot, elapsedMs };
15
16
  }
17
+ function honeypotCopiedFromAnotherField(body, honeypot) {
18
+ const b = body ?? {};
19
+ const signalKeys = HUMANITY_SIGNAL_KEYS;
20
+ for (const [key, value] of Object.entries(b)) {
21
+ if (signalKeys.includes(key)) continue;
22
+ if (typeof value === "string" && value === honeypot) return true;
23
+ if (value && typeof value === "object" && !Array.isArray(value)) {
24
+ for (const nested of Object.values(value)) {
25
+ if (typeof nested === "string" && nested === honeypot) return true;
26
+ }
27
+ }
28
+ }
29
+ return false;
30
+ }
16
31
  function evaluateHumanitySignals(body, opts) {
17
32
  const { honeypot, elapsedMs } = extractHumanitySignals(body);
18
- if (honeypot.trim() !== "") return { ok: false, reason: "honeypot" };
33
+ const filled = honeypot.trim() !== "";
34
+ if (filled && !honeypotCopiedFromAnotherField(body, honeypot)) {
35
+ return { ok: false, reason: "honeypot" };
36
+ }
19
37
  if (elapsedMs !== null && elapsedMs < opts.minFillMs) return { ok: false, reason: "too_fast" };
20
- return { ok: true };
38
+ return filled ? { ok: true, note: "honeypot_autofill" } : { ok: true };
21
39
  }
22
40
  var splitCsvEnv = (s) => s?.split(",").map((t) => t.trim()).filter(Boolean) ?? [];
23
41
 
@@ -25,6 +43,7 @@ exports.DEFAULT_MIN_FILL_MS = DEFAULT_MIN_FILL_MS;
25
43
  exports.ELAPSED_MS_FIELD = ELAPSED_MS_FIELD;
26
44
  exports.HONEYPOT_FIELD = HONEYPOT_FIELD;
27
45
  exports.HUMANITY_SIGNAL_KEYS = HUMANITY_SIGNAL_KEYS;
46
+ exports.LEGACY_HONEYPOT_FIELD = LEGACY_HONEYPOT_FIELD;
28
47
  exports.evaluateHumanitySignals = evaluateHumanitySignals;
29
48
  exports.extractHumanitySignals = extractHumanitySignals;
30
49
  exports.splitCsvEnv = splitCsvEnv;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/humanity-signals.ts"],"names":[],"mappings":";;;AAgBO,IAAM,cAAA,GAAiB;AAEvB,IAAM,gBAAA,GAAmB;AAEzB,IAAM,mBAAA,GAAsB;AAS5B,IAAM,oBAAA,GAAuB,CAAC,cAAA,EAAgB,gBAAgB;AAS9D,SAAS,uBAAuB,IAAA,EAA+D;AACpG,EAAA,MAAM,CAAA,GAAK,QAAQ,EAAC;AACpB,EAAA,MAAM,KAAA,GAAQ,EAAE,cAAc,CAAA;AAK9B,EAAA,MAAM,QAAA,GAAW,KAAA,IAAS,IAAA,GAAO,EAAA,GAAK,OAAO,KAAK,CAAA;AAClD,EAAA,MAAM,KAAA,GAAQ,EAAE,gBAAgB,CAAA;AAChC,EAAA,MAAM,SAAA,GAAY,OAAO,KAAA,KAAU,QAAA,IAAY,OAAO,QAAA,CAAS,KAAK,IAAI,KAAA,GAAQ,IAAA;AAChF,EAAA,OAAO,EAAE,UAAU,SAAA,EAAU;AAC/B;AAOO,SAAS,uBAAA,CAAwB,MAAe,IAAA,EAA8C;AACnG,EAAA,MAAM,EAAE,QAAA,EAAU,SAAA,EAAU,GAAI,uBAAuB,IAAI,CAAA;AAC3D,EAAA,IAAI,QAAA,CAAS,MAAK,KAAM,EAAA,SAAW,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,UAAA,EAAW;AACnE,EAAA,IAAI,SAAA,KAAc,IAAA,IAAQ,SAAA,GAAY,IAAA,CAAK,SAAA,SAAkB,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,UAAA,EAAW;AAC7F,EAAA,OAAO,EAAE,IAAI,IAAA,EAAK;AACpB;AAGO,IAAM,cAAc,CAAC,CAAA,KAC1B,CAAA,EACI,KAAA,CAAM,GAAG,CAAA,CACV,GAAA,CAAI,CAAA,CAAA,KAAK,CAAA,CAAE,MAAM,CAAA,CACjB,MAAA,CAAO,OAAO,KAAK","file":"humanity-signals.cjs","sourcesContent":["/**\n * Humanity signals — invisible bot-protection primitives shared by the lib's\n * public forms (client) and the hub's per-route `verifyHuman` gate (server).\n *\n * PURE + React-free on purpose: this module is a tsup SERVER entry (no\n * \"use client\" banner) so the hub can import it server-side without pulling a\n * client-reference boundary — same pattern as `schemas/contact-schema` and\n * `components/features/mux-origins`.\n *\n * Two origin-independent signals travel in the POST body: a honeypot (a hidden\n * field real users never fill) and timing (ms from form mount to submit).\n * `evaluateHumanitySignals` is the SINGLE source of truth for the block/allow\n * decision — the hub imports + calls it rather than re-implementing the rules.\n */\n\n/** Hidden honeypot field name. Innocuous + autofill-resistant (deliberately NOT name/email). */\nexport const HONEYPOT_FIELD = 'contact_url_confirm';\n/** Client-measured ms between form mount and submit. */\nexport const ELAPSED_MS_FIELD = 'form_elapsed_ms';\n/** Default minimum fill time (ms). A submit faster than this is treated as a bot. */\nexport const DEFAULT_MIN_FILL_MS = 700;\n\n/**\n * Every humanity-signal key that rides in a public form's POST body.\n * Server-side handlers that forward form payloads upstream (HubSpot booking,\n * CRM pushes, …) MUST strip by THIS array — never hand-typed strings — so a\n * field rename here propagates everywhere and the honeypot value can never\n * silently leak into an upstream record.\n */\nexport const HUMANITY_SIGNAL_KEYS = [HONEYPOT_FIELD, ELAPSED_MS_FIELD] as const;\n\n/** Keyed wire object produced by `useHumanitySignals().getSignals()` and spread into the POST body. */\nexport type HumanitySignals = Record<string, string | number>;\n\n/** Result of {@link evaluateHumanitySignals}. */\nexport type HumanityVerdict = { ok: true } | { ok: false; reason: 'honeypot' | 'too_fast' };\n\n/** Tolerant reader — never throws; missing/garbage timing → null. */\nexport function extractHumanitySignals(body: unknown): { honeypot: string; elapsedMs: number | null } {\n const b = (body ?? {}) as Record<string, unknown>;\n const rawHp = b[HONEYPOT_FIELD];\n // A legit client always sends a STRING here (getSignals → ref.value ?? ''),\n // so ANY present non-string value is a bot filling the decoy with a non-string\n // to dodge the empty-check — coerce to a (non-empty) string so it still trips.\n // null/undefined → '' = the correct \"field absent / unfilled\" allow case.\n const honeypot = rawHp == null ? '' : String(rawHp);\n const rawMs = b[ELAPSED_MS_FIELD];\n const elapsedMs = typeof rawMs === 'number' && Number.isFinite(rawMs) ? rawMs : null;\n return { honeypot, elapsedMs };\n}\n\n/**\n * SINGLE decision fn for honeypot + timing (the hub's `verifyHuman` imports + calls this):\n * - honeypot non-empty → bot (real users never fill the off-screen field)\n * - elapsed below `minFillMs` → bot (humans take time; a MISSING timing value never blocks)\n */\nexport function evaluateHumanitySignals(body: unknown, opts: { minFillMs: number }): HumanityVerdict {\n const { honeypot, elapsedMs } = extractHumanitySignals(body);\n if (honeypot.trim() !== '') return { ok: false, reason: 'honeypot' };\n if (elapsedMs !== null && elapsedMs < opts.minFillMs) return { ok: false, reason: 'too_fast' };\n return { ok: true };\n}\n\n/** Parse a comma-separated env string → trimmed, non-empty entries (undefined → []). */\nexport const splitCsvEnv = (s?: string): string[] =>\n s\n ?.split(',')\n .map(t => t.trim())\n .filter(Boolean) ?? [];\n"]}
1
+ {"version":3,"sources":["../../src/utils/humanity-signals.ts"],"names":[],"mappings":";;;AA8BO,IAAM,cAAA,GAAiB;AAMvB,IAAM,qBAAA,GAAwB;AAE9B,IAAM,gBAAA,GAAmB;AAEzB,IAAM,mBAAA,GAAsB;AAS5B,IAAM,oBAAA,GAAuB,CAAC,cAAA,EAAgB,qBAAA,EAAuB,gBAAgB;AASrF,SAAS,uBAAuB,IAAA,EAA+D;AACpG,EAAA,MAAM,CAAA,GAAK,QAAQ,EAAC;AAEpB,EAAA,MAAM,KAAA,GAAQ,CAAA,CAAE,cAAc,CAAA,IAAK,EAAE,qBAAqB,CAAA;AAK1D,EAAA,MAAM,QAAA,GAAW,KAAA,IAAS,IAAA,GAAO,EAAA,GAAK,OAAO,KAAK,CAAA;AAClD,EAAA,MAAM,KAAA,GAAQ,EAAE,gBAAgB,CAAA;AAChC,EAAA,MAAM,SAAA,GAAY,OAAO,KAAA,KAAU,QAAA,IAAY,OAAO,QAAA,CAAS,KAAK,IAAI,KAAA,GAAQ,IAAA;AAChF,EAAA,OAAO,EAAE,UAAU,SAAA,EAAU;AAC/B;AASA,SAAS,8BAAA,CAA+B,MAAe,QAAA,EAA2B;AAChF,EAAA,MAAM,CAAA,GAAK,QAAQ,EAAC;AACpB,EAAA,MAAM,UAAA,GAAgC,oBAAA;AACtC,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,CAAC,CAAA,EAAG;AAC5C,IAAA,IAAI,UAAA,CAAW,QAAA,CAAS,GAAG,CAAA,EAAG;AAC9B,IAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,KAAU,UAAU,OAAO,IAAA;AAC5D,IAAA,IAAI,KAAA,IAAS,OAAO,KAAA,KAAU,QAAA,IAAY,CAAC,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AAC/D,MAAA,KAAA,MAAW,MAAA,IAAU,MAAA,CAAO,MAAA,CAAO,KAAgC,CAAA,EAAG;AACpE,QAAA,IAAI,OAAO,MAAA,KAAW,QAAA,IAAY,MAAA,KAAW,UAAU,OAAO,IAAA;AAAA,MAChE;AAAA,IACF;AAAA,EACF;AACA,EAAA,OAAO,KAAA;AACT;AASO,SAAS,uBAAA,CAAwB,MAAe,IAAA,EAA8C;AACnG,EAAA,MAAM,EAAE,QAAA,EAAU,SAAA,EAAU,GAAI,uBAAuB,IAAI,CAAA;AAC3D,EAAA,MAAM,MAAA,GAAS,QAAA,CAAS,IAAA,EAAK,KAAM,EAAA;AACnC,EAAA,IAAI,MAAA,IAAU,CAAC,8BAAA,CAA+B,IAAA,EAAM,QAAQ,CAAA,EAAG;AAC7D,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,UAAA,EAAW;AAAA,EACzC;AACA,EAAA,IAAI,SAAA,KAAc,IAAA,IAAQ,SAAA,GAAY,IAAA,CAAK,SAAA,SAAkB,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,UAAA,EAAW;AAC7F,EAAA,OAAO,MAAA,GAAS,EAAE,EAAA,EAAI,IAAA,EAAM,MAAM,mBAAA,EAAoB,GAAI,EAAE,EAAA,EAAI,IAAA,EAAK;AACvE;AAGO,IAAM,cAAc,CAAC,CAAA,KAC1B,CAAA,EACI,KAAA,CAAM,GAAG,CAAA,CACV,GAAA,CAAI,CAAA,CAAA,KAAK,CAAA,CAAE,MAAM,CAAA,CACjB,MAAA,CAAO,OAAO,KAAK","file":"humanity-signals.cjs","sourcesContent":["/**\n * Humanity signals — invisible bot-protection primitives shared by the lib's\n * public forms (client) and the hub's per-route `verifyHuman` gate (server).\n *\n * PURE + React-free on purpose: this module is a tsup SERVER entry (no\n * \"use client\" banner) so the hub can import it server-side without pulling a\n * client-reference boundary — same pattern as `schemas/contact-schema` and\n * `components/features/mux-origins`.\n *\n * Two origin-independent signals travel in the POST body: a honeypot (a hidden\n * field real users never fill) and timing (ms from form mount to submit).\n * `evaluateHumanitySignals` is the SINGLE source of truth for the block/allow\n * decision — the hub imports + calls it rather than re-implementing the rules.\n *\n * FALSE-POSITIVE HISTORY (2026-08-27): the honeypot was named\n * `contact_url_confirm`, and browser/password-manager autofill (which ignores\n * `autocomplete=\"off\"` and matches \"url\"/\"confirm\" name heuristics) filled it\n * for REAL users — every production `BOT_DETECTED` in the log window was a\n * legitimate Chrome user whose autofill tripped the decoy. Three layers now\n * prevent a recurrence; keep all three when touching this system:\n * 1. The field name avoids every autofill-heuristic token (name/email/\n * phone/url/website/confirm/company/address/code/…).\n * 2. `HoneypotField` renders `readOnly`-until-focus + password-manager\n * ignore attributes — autofill skips read-only inputs.\n * 3. `evaluateHumanitySignals` forgives a filled decoy whose value was\n * COPIED from another field in the same body (the autofill signature —\n * a human, not a bot).\n */\n\n/** Hidden honeypot field name. Deliberately free of autofill-heuristic tokens (see module doc). */\nexport const HONEYPOT_FIELD = 'form_extra_note';\n/**\n * Prior honeypot field name — still EVALUATED (a stale-lib embedder's client\n * keeps its bot protection) and still STRIPPED before upstream forwarding.\n * Remove once no deployed embedder ships a lib older than the rename.\n */\nexport const LEGACY_HONEYPOT_FIELD = 'contact_url_confirm';\n/** Client-measured ms between form mount and submit. */\nexport const ELAPSED_MS_FIELD = 'form_elapsed_ms';\n/** Default minimum fill time (ms). A submit faster than this is treated as a bot. */\nexport const DEFAULT_MIN_FILL_MS = 700;\n\n/**\n * Every humanity-signal key that rides in a public form's POST body.\n * Server-side handlers that forward form payloads upstream (HubSpot booking,\n * CRM pushes, …) MUST strip by THIS array — never hand-typed strings — so a\n * field rename here propagates everywhere and the honeypot value can never\n * silently leak into an upstream record.\n */\nexport const HUMANITY_SIGNAL_KEYS = [HONEYPOT_FIELD, LEGACY_HONEYPOT_FIELD, ELAPSED_MS_FIELD] as const;\n\n/** Keyed wire object produced by `useHumanitySignals().getSignals()` and spread into the POST body. */\nexport type HumanitySignals = Record<string, string | number>;\n\n/** Result of {@link evaluateHumanitySignals}. */\nexport type HumanityVerdict = { ok: true; note?: 'honeypot_autofill' } | { ok: false; reason: 'honeypot' | 'too_fast' };\n\n/** Tolerant reader — never throws; missing/garbage timing → null. */\nexport function extractHumanitySignals(body: unknown): { honeypot: string; elapsedMs: number | null } {\n const b = (body ?? {}) as Record<string, unknown>;\n // Current field name first; a stale-lib client still posts the legacy name.\n const rawHp = b[HONEYPOT_FIELD] ?? b[LEGACY_HONEYPOT_FIELD];\n // A legit client always sends a STRING here (getSignals → ref.value ?? ''),\n // so ANY present non-string value is a bot filling the decoy with a non-string\n // to dodge the empty-check — coerce to a (non-empty) string so it still trips.\n // null/undefined → '' = the correct \"field absent / unfilled\" allow case.\n const honeypot = rawHp == null ? '' : String(rawHp);\n const rawMs = b[ELAPSED_MS_FIELD];\n const elapsedMs = typeof rawMs === 'number' && Number.isFinite(rawMs) ? rawMs : null;\n return { honeypot, elapsedMs };\n}\n\n/**\n * Was the decoy value COPIED from a real field in the same body? That is the\n * autofill signature: browsers and password-manager extensions fill the hidden\n * input with the same value they put in a visible field (email, phone, …) —\n * a human with autofill, not a bot. Scans top-level string values plus one\n * nested level (the booking form's custom `formFields` object).\n */\nfunction honeypotCopiedFromAnotherField(body: unknown, honeypot: string): boolean {\n const b = (body ?? {}) as Record<string, unknown>;\n const signalKeys: readonly string[] = HUMANITY_SIGNAL_KEYS;\n for (const [key, value] of Object.entries(b)) {\n if (signalKeys.includes(key)) continue;\n if (typeof value === 'string' && value === honeypot) return true;\n if (value && typeof value === 'object' && !Array.isArray(value)) {\n for (const nested of Object.values(value as Record<string, unknown>)) {\n if (typeof nested === 'string' && nested === honeypot) return true;\n }\n }\n }\n return false;\n}\n\n/**\n * SINGLE decision fn for honeypot + timing (the hub's `verifyHuman` imports + calls this):\n * - honeypot non-empty → bot (real users never fill the off-screen field) — UNLESS the\n * value was copied from another field in the body (autofill reached the decoy → human;\n * the verdict carries `note: 'honeypot_autofill'` so the caller can log it)\n * - elapsed below `minFillMs` → bot (humans take time; a MISSING timing value never blocks)\n */\nexport function evaluateHumanitySignals(body: unknown, opts: { minFillMs: number }): HumanityVerdict {\n const { honeypot, elapsedMs } = extractHumanitySignals(body);\n const filled = honeypot.trim() !== '';\n if (filled && !honeypotCopiedFromAnotherField(body, honeypot)) {\n return { ok: false, reason: 'honeypot' };\n }\n if (elapsedMs !== null && elapsedMs < opts.minFillMs) return { ok: false, reason: 'too_fast' };\n return filled ? { ok: true, note: 'honeypot_autofill' } : { ok: true };\n}\n\n/** Parse a comma-separated env string → trimmed, non-empty entries (undefined → []). */\nexport const splitCsvEnv = (s?: string): string[] =>\n s\n ?.split(',')\n .map(t => t.trim())\n .filter(Boolean) ?? [];\n"]}
@@ -11,9 +11,29 @@
11
11
  * field real users never fill) and timing (ms from form mount to submit).
12
12
  * `evaluateHumanitySignals` is the SINGLE source of truth for the block/allow
13
13
  * decision — the hub imports + calls it rather than re-implementing the rules.
14
+ *
15
+ * FALSE-POSITIVE HISTORY (2026-08-27): the honeypot was named
16
+ * `contact_url_confirm`, and browser/password-manager autofill (which ignores
17
+ * `autocomplete="off"` and matches "url"/"confirm" name heuristics) filled it
18
+ * for REAL users — every production `BOT_DETECTED` in the log window was a
19
+ * legitimate Chrome user whose autofill tripped the decoy. Three layers now
20
+ * prevent a recurrence; keep all three when touching this system:
21
+ * 1. The field name avoids every autofill-heuristic token (name/email/
22
+ * phone/url/website/confirm/company/address/code/…).
23
+ * 2. `HoneypotField` renders `readOnly`-until-focus + password-manager
24
+ * ignore attributes — autofill skips read-only inputs.
25
+ * 3. `evaluateHumanitySignals` forgives a filled decoy whose value was
26
+ * COPIED from another field in the same body (the autofill signature —
27
+ * a human, not a bot).
28
+ */
29
+ /** Hidden honeypot field name. Deliberately free of autofill-heuristic tokens (see module doc). */
30
+ export declare const HONEYPOT_FIELD = "form_extra_note";
31
+ /**
32
+ * Prior honeypot field name — still EVALUATED (a stale-lib embedder's client
33
+ * keeps its bot protection) and still STRIPPED before upstream forwarding.
34
+ * Remove once no deployed embedder ships a lib older than the rename.
14
35
  */
15
- /** Hidden honeypot field name. Innocuous + autofill-resistant (deliberately NOT name/email). */
16
- export declare const HONEYPOT_FIELD = "contact_url_confirm";
36
+ export declare const LEGACY_HONEYPOT_FIELD = "contact_url_confirm";
17
37
  /** Client-measured ms between form mount and submit. */
18
38
  export declare const ELAPSED_MS_FIELD = "form_elapsed_ms";
19
39
  /** Default minimum fill time (ms). A submit faster than this is treated as a bot. */
@@ -25,12 +45,13 @@ export declare const DEFAULT_MIN_FILL_MS = 700;
25
45
  * field rename here propagates everywhere and the honeypot value can never
26
46
  * silently leak into an upstream record.
27
47
  */
28
- export declare const HUMANITY_SIGNAL_KEYS: readonly ["contact_url_confirm", "form_elapsed_ms"];
48
+ export declare const HUMANITY_SIGNAL_KEYS: readonly ["form_extra_note", "contact_url_confirm", "form_elapsed_ms"];
29
49
  /** Keyed wire object produced by `useHumanitySignals().getSignals()` and spread into the POST body. */
30
50
  export type HumanitySignals = Record<string, string | number>;
31
51
  /** Result of {@link evaluateHumanitySignals}. */
32
52
  export type HumanityVerdict = {
33
53
  ok: true;
54
+ note?: 'honeypot_autofill';
34
55
  } | {
35
56
  ok: false;
36
57
  reason: 'honeypot' | 'too_fast';
@@ -42,7 +63,9 @@ export declare function extractHumanitySignals(body: unknown): {
42
63
  };
43
64
  /**
44
65
  * 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)
66
+ * - honeypot non-empty → bot (real users never fill the off-screen field) — UNLESS the
67
+ * value was copied from another field in the body (autofill reached the decoy → human;
68
+ * the verdict carries `note: 'honeypot_autofill'` so the caller can log it)
46
69
  * - elapsed below `minFillMs` → bot (humans take time; a MISSING timing value never blocks)
47
70
  */
48
71
  export declare function evaluateHumanitySignals(body: unknown, opts: {
@@ -1 +1 @@
1
- {"version":3,"file":"humanity-signals.d.ts","sourceRoot":"","sources":["../../src/utils/humanity-signals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,gGAAgG;AAChG,eAAO,MAAM,cAAc,wBAAwB,CAAC;AACpD,wDAAwD;AACxD,eAAO,MAAM,gBAAgB,oBAAoB,CAAC;AAClD,qFAAqF;AACrF,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAEvC;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,qDAA8C,CAAC;AAEhF,uGAAuG;AACvG,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;AAE9D,iDAAiD;AACjD,MAAM,MAAM,eAAe,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,UAAU,GAAG,UAAU,CAAA;CAAE,CAAC;AAE5F,qEAAqE;AACrE,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,OAAO,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAWpG;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,eAAe,CAKnG;AAED,wFAAwF;AACxF,eAAO,MAAM,WAAW,GAAI,IAAI,MAAM,KAAG,MAAM,EAIrB,CAAC"}
1
+ {"version":3,"file":"humanity-signals.d.ts","sourceRoot":"","sources":["../../src/utils/humanity-signals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,mGAAmG;AACnG,eAAO,MAAM,cAAc,oBAAoB,CAAC;AAChD;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,wBAAwB,CAAC;AAC3D,wDAAwD;AACxD,eAAO,MAAM,gBAAgB,oBAAoB,CAAC;AAClD,qFAAqF;AACrF,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAEvC;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,wEAAqE,CAAC;AAEvG,uGAAuG;AACvG,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;AAE9D,iDAAiD;AACjD,MAAM,MAAM,eAAe,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,CAAC,EAAE,mBAAmB,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,UAAU,GAAG,UAAU,CAAA;CAAE,CAAC;AAExH,qEAAqE;AACrE,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,OAAO,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAYpG;AAwBD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,eAAe,CAQnG;AAED,wFAAwF;AACxF,eAAO,MAAM,WAAW,GAAI,IAAI,MAAM,KAAG,MAAM,EAIrB,CAAC"}
@@ -1,24 +1,42 @@
1
1
  // src/utils/humanity-signals.ts
2
- var HONEYPOT_FIELD = "contact_url_confirm";
2
+ var HONEYPOT_FIELD = "form_extra_note";
3
+ var LEGACY_HONEYPOT_FIELD = "contact_url_confirm";
3
4
  var ELAPSED_MS_FIELD = "form_elapsed_ms";
4
5
  var DEFAULT_MIN_FILL_MS = 700;
5
- var HUMANITY_SIGNAL_KEYS = [HONEYPOT_FIELD, ELAPSED_MS_FIELD];
6
+ var HUMANITY_SIGNAL_KEYS = [HONEYPOT_FIELD, LEGACY_HONEYPOT_FIELD, ELAPSED_MS_FIELD];
6
7
  function extractHumanitySignals(body) {
7
8
  const b = body ?? {};
8
- const rawHp = b[HONEYPOT_FIELD];
9
+ const rawHp = b[HONEYPOT_FIELD] ?? b[LEGACY_HONEYPOT_FIELD];
9
10
  const honeypot = rawHp == null ? "" : String(rawHp);
10
11
  const rawMs = b[ELAPSED_MS_FIELD];
11
12
  const elapsedMs = typeof rawMs === "number" && Number.isFinite(rawMs) ? rawMs : null;
12
13
  return { honeypot, elapsedMs };
13
14
  }
15
+ function honeypotCopiedFromAnotherField(body, honeypot) {
16
+ const b = body ?? {};
17
+ const signalKeys = HUMANITY_SIGNAL_KEYS;
18
+ for (const [key, value] of Object.entries(b)) {
19
+ if (signalKeys.includes(key)) continue;
20
+ if (typeof value === "string" && value === honeypot) return true;
21
+ if (value && typeof value === "object" && !Array.isArray(value)) {
22
+ for (const nested of Object.values(value)) {
23
+ if (typeof nested === "string" && nested === honeypot) return true;
24
+ }
25
+ }
26
+ }
27
+ return false;
28
+ }
14
29
  function evaluateHumanitySignals(body, opts) {
15
30
  const { honeypot, elapsedMs } = extractHumanitySignals(body);
16
- if (honeypot.trim() !== "") return { ok: false, reason: "honeypot" };
31
+ const filled = honeypot.trim() !== "";
32
+ if (filled && !honeypotCopiedFromAnotherField(body, honeypot)) {
33
+ return { ok: false, reason: "honeypot" };
34
+ }
17
35
  if (elapsedMs !== null && elapsedMs < opts.minFillMs) return { ok: false, reason: "too_fast" };
18
- return { ok: true };
36
+ return filled ? { ok: true, note: "honeypot_autofill" } : { ok: true };
19
37
  }
20
38
  var splitCsvEnv = (s) => s?.split(",").map((t) => t.trim()).filter(Boolean) ?? [];
21
39
 
22
- export { DEFAULT_MIN_FILL_MS, ELAPSED_MS_FIELD, HONEYPOT_FIELD, HUMANITY_SIGNAL_KEYS, evaluateHumanitySignals, extractHumanitySignals, splitCsvEnv };
40
+ export { DEFAULT_MIN_FILL_MS, ELAPSED_MS_FIELD, HONEYPOT_FIELD, HUMANITY_SIGNAL_KEYS, LEGACY_HONEYPOT_FIELD, evaluateHumanitySignals, extractHumanitySignals, splitCsvEnv };
23
41
  //# sourceMappingURL=humanity-signals.js.map
24
42
  //# sourceMappingURL=humanity-signals.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/humanity-signals.ts"],"names":[],"mappings":";AAgBO,IAAM,cAAA,GAAiB;AAEvB,IAAM,gBAAA,GAAmB;AAEzB,IAAM,mBAAA,GAAsB;AAS5B,IAAM,oBAAA,GAAuB,CAAC,cAAA,EAAgB,gBAAgB;AAS9D,SAAS,uBAAuB,IAAA,EAA+D;AACpG,EAAA,MAAM,CAAA,GAAK,QAAQ,EAAC;AACpB,EAAA,MAAM,KAAA,GAAQ,EAAE,cAAc,CAAA;AAK9B,EAAA,MAAM,QAAA,GAAW,KAAA,IAAS,IAAA,GAAO,EAAA,GAAK,OAAO,KAAK,CAAA;AAClD,EAAA,MAAM,KAAA,GAAQ,EAAE,gBAAgB,CAAA;AAChC,EAAA,MAAM,SAAA,GAAY,OAAO,KAAA,KAAU,QAAA,IAAY,OAAO,QAAA,CAAS,KAAK,IAAI,KAAA,GAAQ,IAAA;AAChF,EAAA,OAAO,EAAE,UAAU,SAAA,EAAU;AAC/B;AAOO,SAAS,uBAAA,CAAwB,MAAe,IAAA,EAA8C;AACnG,EAAA,MAAM,EAAE,QAAA,EAAU,SAAA,EAAU,GAAI,uBAAuB,IAAI,CAAA;AAC3D,EAAA,IAAI,QAAA,CAAS,MAAK,KAAM,EAAA,SAAW,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,UAAA,EAAW;AACnE,EAAA,IAAI,SAAA,KAAc,IAAA,IAAQ,SAAA,GAAY,IAAA,CAAK,SAAA,SAAkB,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,UAAA,EAAW;AAC7F,EAAA,OAAO,EAAE,IAAI,IAAA,EAAK;AACpB;AAGO,IAAM,cAAc,CAAC,CAAA,KAC1B,CAAA,EACI,KAAA,CAAM,GAAG,CAAA,CACV,GAAA,CAAI,CAAA,CAAA,KAAK,CAAA,CAAE,MAAM,CAAA,CACjB,MAAA,CAAO,OAAO,KAAK","file":"humanity-signals.js","sourcesContent":["/**\n * Humanity signals — invisible bot-protection primitives shared by the lib's\n * public forms (client) and the hub's per-route `verifyHuman` gate (server).\n *\n * PURE + React-free on purpose: this module is a tsup SERVER entry (no\n * \"use client\" banner) so the hub can import it server-side without pulling a\n * client-reference boundary — same pattern as `schemas/contact-schema` and\n * `components/features/mux-origins`.\n *\n * Two origin-independent signals travel in the POST body: a honeypot (a hidden\n * field real users never fill) and timing (ms from form mount to submit).\n * `evaluateHumanitySignals` is the SINGLE source of truth for the block/allow\n * decision — the hub imports + calls it rather than re-implementing the rules.\n */\n\n/** Hidden honeypot field name. Innocuous + autofill-resistant (deliberately NOT name/email). */\nexport const HONEYPOT_FIELD = 'contact_url_confirm';\n/** Client-measured ms between form mount and submit. */\nexport const ELAPSED_MS_FIELD = 'form_elapsed_ms';\n/** Default minimum fill time (ms). A submit faster than this is treated as a bot. */\nexport const DEFAULT_MIN_FILL_MS = 700;\n\n/**\n * Every humanity-signal key that rides in a public form's POST body.\n * Server-side handlers that forward form payloads upstream (HubSpot booking,\n * CRM pushes, …) MUST strip by THIS array — never hand-typed strings — so a\n * field rename here propagates everywhere and the honeypot value can never\n * silently leak into an upstream record.\n */\nexport const HUMANITY_SIGNAL_KEYS = [HONEYPOT_FIELD, ELAPSED_MS_FIELD] as const;\n\n/** Keyed wire object produced by `useHumanitySignals().getSignals()` and spread into the POST body. */\nexport type HumanitySignals = Record<string, string | number>;\n\n/** Result of {@link evaluateHumanitySignals}. */\nexport type HumanityVerdict = { ok: true } | { ok: false; reason: 'honeypot' | 'too_fast' };\n\n/** Tolerant reader — never throws; missing/garbage timing → null. */\nexport function extractHumanitySignals(body: unknown): { honeypot: string; elapsedMs: number | null } {\n const b = (body ?? {}) as Record<string, unknown>;\n const rawHp = b[HONEYPOT_FIELD];\n // A legit client always sends a STRING here (getSignals → ref.value ?? ''),\n // so ANY present non-string value is a bot filling the decoy with a non-string\n // to dodge the empty-check — coerce to a (non-empty) string so it still trips.\n // null/undefined → '' = the correct \"field absent / unfilled\" allow case.\n const honeypot = rawHp == null ? '' : String(rawHp);\n const rawMs = b[ELAPSED_MS_FIELD];\n const elapsedMs = typeof rawMs === 'number' && Number.isFinite(rawMs) ? rawMs : null;\n return { honeypot, elapsedMs };\n}\n\n/**\n * SINGLE decision fn for honeypot + timing (the hub's `verifyHuman` imports + calls this):\n * - honeypot non-empty → bot (real users never fill the off-screen field)\n * - elapsed below `minFillMs` → bot (humans take time; a MISSING timing value never blocks)\n */\nexport function evaluateHumanitySignals(body: unknown, opts: { minFillMs: number }): HumanityVerdict {\n const { honeypot, elapsedMs } = extractHumanitySignals(body);\n if (honeypot.trim() !== '') return { ok: false, reason: 'honeypot' };\n if (elapsedMs !== null && elapsedMs < opts.minFillMs) return { ok: false, reason: 'too_fast' };\n return { ok: true };\n}\n\n/** Parse a comma-separated env string → trimmed, non-empty entries (undefined → []). */\nexport const splitCsvEnv = (s?: string): string[] =>\n s\n ?.split(',')\n .map(t => t.trim())\n .filter(Boolean) ?? [];\n"]}
1
+ {"version":3,"sources":["../../src/utils/humanity-signals.ts"],"names":[],"mappings":";AA8BO,IAAM,cAAA,GAAiB;AAMvB,IAAM,qBAAA,GAAwB;AAE9B,IAAM,gBAAA,GAAmB;AAEzB,IAAM,mBAAA,GAAsB;AAS5B,IAAM,oBAAA,GAAuB,CAAC,cAAA,EAAgB,qBAAA,EAAuB,gBAAgB;AASrF,SAAS,uBAAuB,IAAA,EAA+D;AACpG,EAAA,MAAM,CAAA,GAAK,QAAQ,EAAC;AAEpB,EAAA,MAAM,KAAA,GAAQ,CAAA,CAAE,cAAc,CAAA,IAAK,EAAE,qBAAqB,CAAA;AAK1D,EAAA,MAAM,QAAA,GAAW,KAAA,IAAS,IAAA,GAAO,EAAA,GAAK,OAAO,KAAK,CAAA;AAClD,EAAA,MAAM,KAAA,GAAQ,EAAE,gBAAgB,CAAA;AAChC,EAAA,MAAM,SAAA,GAAY,OAAO,KAAA,KAAU,QAAA,IAAY,OAAO,QAAA,CAAS,KAAK,IAAI,KAAA,GAAQ,IAAA;AAChF,EAAA,OAAO,EAAE,UAAU,SAAA,EAAU;AAC/B;AASA,SAAS,8BAAA,CAA+B,MAAe,QAAA,EAA2B;AAChF,EAAA,MAAM,CAAA,GAAK,QAAQ,EAAC;AACpB,EAAA,MAAM,UAAA,GAAgC,oBAAA;AACtC,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,CAAC,CAAA,EAAG;AAC5C,IAAA,IAAI,UAAA,CAAW,QAAA,CAAS,GAAG,CAAA,EAAG;AAC9B,IAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,KAAU,UAAU,OAAO,IAAA;AAC5D,IAAA,IAAI,KAAA,IAAS,OAAO,KAAA,KAAU,QAAA,IAAY,CAAC,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AAC/D,MAAA,KAAA,MAAW,MAAA,IAAU,MAAA,CAAO,MAAA,CAAO,KAAgC,CAAA,EAAG;AACpE,QAAA,IAAI,OAAO,MAAA,KAAW,QAAA,IAAY,MAAA,KAAW,UAAU,OAAO,IAAA;AAAA,MAChE;AAAA,IACF;AAAA,EACF;AACA,EAAA,OAAO,KAAA;AACT;AASO,SAAS,uBAAA,CAAwB,MAAe,IAAA,EAA8C;AACnG,EAAA,MAAM,EAAE,QAAA,EAAU,SAAA,EAAU,GAAI,uBAAuB,IAAI,CAAA;AAC3D,EAAA,MAAM,MAAA,GAAS,QAAA,CAAS,IAAA,EAAK,KAAM,EAAA;AACnC,EAAA,IAAI,MAAA,IAAU,CAAC,8BAAA,CAA+B,IAAA,EAAM,QAAQ,CAAA,EAAG;AAC7D,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,UAAA,EAAW;AAAA,EACzC;AACA,EAAA,IAAI,SAAA,KAAc,IAAA,IAAQ,SAAA,GAAY,IAAA,CAAK,SAAA,SAAkB,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,UAAA,EAAW;AAC7F,EAAA,OAAO,MAAA,GAAS,EAAE,EAAA,EAAI,IAAA,EAAM,MAAM,mBAAA,EAAoB,GAAI,EAAE,EAAA,EAAI,IAAA,EAAK;AACvE;AAGO,IAAM,cAAc,CAAC,CAAA,KAC1B,CAAA,EACI,KAAA,CAAM,GAAG,CAAA,CACV,GAAA,CAAI,CAAA,CAAA,KAAK,CAAA,CAAE,MAAM,CAAA,CACjB,MAAA,CAAO,OAAO,KAAK","file":"humanity-signals.js","sourcesContent":["/**\n * Humanity signals — invisible bot-protection primitives shared by the lib's\n * public forms (client) and the hub's per-route `verifyHuman` gate (server).\n *\n * PURE + React-free on purpose: this module is a tsup SERVER entry (no\n * \"use client\" banner) so the hub can import it server-side without pulling a\n * client-reference boundary — same pattern as `schemas/contact-schema` and\n * `components/features/mux-origins`.\n *\n * Two origin-independent signals travel in the POST body: a honeypot (a hidden\n * field real users never fill) and timing (ms from form mount to submit).\n * `evaluateHumanitySignals` is the SINGLE source of truth for the block/allow\n * decision — the hub imports + calls it rather than re-implementing the rules.\n *\n * FALSE-POSITIVE HISTORY (2026-08-27): the honeypot was named\n * `contact_url_confirm`, and browser/password-manager autofill (which ignores\n * `autocomplete=\"off\"` and matches \"url\"/\"confirm\" name heuristics) filled it\n * for REAL users — every production `BOT_DETECTED` in the log window was a\n * legitimate Chrome user whose autofill tripped the decoy. Three layers now\n * prevent a recurrence; keep all three when touching this system:\n * 1. The field name avoids every autofill-heuristic token (name/email/\n * phone/url/website/confirm/company/address/code/…).\n * 2. `HoneypotField` renders `readOnly`-until-focus + password-manager\n * ignore attributes — autofill skips read-only inputs.\n * 3. `evaluateHumanitySignals` forgives a filled decoy whose value was\n * COPIED from another field in the same body (the autofill signature —\n * a human, not a bot).\n */\n\n/** Hidden honeypot field name. Deliberately free of autofill-heuristic tokens (see module doc). */\nexport const HONEYPOT_FIELD = 'form_extra_note';\n/**\n * Prior honeypot field name — still EVALUATED (a stale-lib embedder's client\n * keeps its bot protection) and still STRIPPED before upstream forwarding.\n * Remove once no deployed embedder ships a lib older than the rename.\n */\nexport const LEGACY_HONEYPOT_FIELD = 'contact_url_confirm';\n/** Client-measured ms between form mount and submit. */\nexport const ELAPSED_MS_FIELD = 'form_elapsed_ms';\n/** Default minimum fill time (ms). A submit faster than this is treated as a bot. */\nexport const DEFAULT_MIN_FILL_MS = 700;\n\n/**\n * Every humanity-signal key that rides in a public form's POST body.\n * Server-side handlers that forward form payloads upstream (HubSpot booking,\n * CRM pushes, …) MUST strip by THIS array — never hand-typed strings — so a\n * field rename here propagates everywhere and the honeypot value can never\n * silently leak into an upstream record.\n */\nexport const HUMANITY_SIGNAL_KEYS = [HONEYPOT_FIELD, LEGACY_HONEYPOT_FIELD, ELAPSED_MS_FIELD] as const;\n\n/** Keyed wire object produced by `useHumanitySignals().getSignals()` and spread into the POST body. */\nexport type HumanitySignals = Record<string, string | number>;\n\n/** Result of {@link evaluateHumanitySignals}. */\nexport type HumanityVerdict = { ok: true; note?: 'honeypot_autofill' } | { ok: false; reason: 'honeypot' | 'too_fast' };\n\n/** Tolerant reader — never throws; missing/garbage timing → null. */\nexport function extractHumanitySignals(body: unknown): { honeypot: string; elapsedMs: number | null } {\n const b = (body ?? {}) as Record<string, unknown>;\n // Current field name first; a stale-lib client still posts the legacy name.\n const rawHp = b[HONEYPOT_FIELD] ?? b[LEGACY_HONEYPOT_FIELD];\n // A legit client always sends a STRING here (getSignals → ref.value ?? ''),\n // so ANY present non-string value is a bot filling the decoy with a non-string\n // to dodge the empty-check — coerce to a (non-empty) string so it still trips.\n // null/undefined → '' = the correct \"field absent / unfilled\" allow case.\n const honeypot = rawHp == null ? '' : String(rawHp);\n const rawMs = b[ELAPSED_MS_FIELD];\n const elapsedMs = typeof rawMs === 'number' && Number.isFinite(rawMs) ? rawMs : null;\n return { honeypot, elapsedMs };\n}\n\n/**\n * Was the decoy value COPIED from a real field in the same body? That is the\n * autofill signature: browsers and password-manager extensions fill the hidden\n * input with the same value they put in a visible field (email, phone, …) —\n * a human with autofill, not a bot. Scans top-level string values plus one\n * nested level (the booking form's custom `formFields` object).\n */\nfunction honeypotCopiedFromAnotherField(body: unknown, honeypot: string): boolean {\n const b = (body ?? {}) as Record<string, unknown>;\n const signalKeys: readonly string[] = HUMANITY_SIGNAL_KEYS;\n for (const [key, value] of Object.entries(b)) {\n if (signalKeys.includes(key)) continue;\n if (typeof value === 'string' && value === honeypot) return true;\n if (value && typeof value === 'object' && !Array.isArray(value)) {\n for (const nested of Object.values(value as Record<string, unknown>)) {\n if (typeof nested === 'string' && nested === honeypot) return true;\n }\n }\n }\n return false;\n}\n\n/**\n * SINGLE decision fn for honeypot + timing (the hub's `verifyHuman` imports + calls this):\n * - honeypot non-empty → bot (real users never fill the off-screen field) — UNLESS the\n * value was copied from another field in the body (autofill reached the decoy → human;\n * the verdict carries `note: 'honeypot_autofill'` so the caller can log it)\n * - elapsed below `minFillMs` → bot (humans take time; a MISSING timing value never blocks)\n */\nexport function evaluateHumanitySignals(body: unknown, opts: { minFillMs: number }): HumanityVerdict {\n const { honeypot, elapsedMs } = extractHumanitySignals(body);\n const filled = honeypot.trim() !== '';\n if (filled && !honeypotCopiedFromAnotherField(body, honeypot)) {\n return { ok: false, reason: 'honeypot' };\n }\n if (elapsedMs !== null && elapsedMs < opts.minFillMs) return { ok: false, reason: 'too_fast' };\n return filled ? { ok: true, note: 'honeypot_autofill' } : { ok: true };\n}\n\n/** Parse a comma-separated env string → trimmed, non-empty entries (undefined → []). */\nexport const splitCsvEnv = (s?: string): string[] =>\n s\n ?.split(',')\n .map(t => t.trim())\n .filter(Boolean) ?? [];\n"]}
@@ -4529,23 +4529,41 @@ function buildDefaultHref(basePath, slug) {
4529
4529
  }
4530
4530
 
4531
4531
  // src/utils/humanity-signals.ts
4532
- var HONEYPOT_FIELD = "contact_url_confirm";
4532
+ var HONEYPOT_FIELD = "form_extra_note";
4533
+ var LEGACY_HONEYPOT_FIELD = "contact_url_confirm";
4533
4534
  var ELAPSED_MS_FIELD = "form_elapsed_ms";
4534
4535
  var DEFAULT_MIN_FILL_MS = 700;
4535
- var HUMANITY_SIGNAL_KEYS = [HONEYPOT_FIELD, ELAPSED_MS_FIELD];
4536
+ var HUMANITY_SIGNAL_KEYS = [HONEYPOT_FIELD, LEGACY_HONEYPOT_FIELD, ELAPSED_MS_FIELD];
4536
4537
  function extractHumanitySignals(body) {
4537
4538
  const b = body ?? {};
4538
- const rawHp = b[HONEYPOT_FIELD];
4539
+ const rawHp = b[HONEYPOT_FIELD] ?? b[LEGACY_HONEYPOT_FIELD];
4539
4540
  const honeypot = rawHp == null ? "" : String(rawHp);
4540
4541
  const rawMs = b[ELAPSED_MS_FIELD];
4541
4542
  const elapsedMs = typeof rawMs === "number" && Number.isFinite(rawMs) ? rawMs : null;
4542
4543
  return { honeypot, elapsedMs };
4543
4544
  }
4545
+ function honeypotCopiedFromAnotherField(body, honeypot) {
4546
+ const b = body ?? {};
4547
+ const signalKeys = HUMANITY_SIGNAL_KEYS;
4548
+ for (const [key, value] of Object.entries(b)) {
4549
+ if (signalKeys.includes(key)) continue;
4550
+ if (typeof value === "string" && value === honeypot) return true;
4551
+ if (value && typeof value === "object" && !Array.isArray(value)) {
4552
+ for (const nested of Object.values(value)) {
4553
+ if (typeof nested === "string" && nested === honeypot) return true;
4554
+ }
4555
+ }
4556
+ }
4557
+ return false;
4558
+ }
4544
4559
  function evaluateHumanitySignals(body, opts) {
4545
4560
  const { honeypot, elapsedMs } = extractHumanitySignals(body);
4546
- if (honeypot.trim() !== "") return { ok: false, reason: "honeypot" };
4561
+ const filled = honeypot.trim() !== "";
4562
+ if (filled && !honeypotCopiedFromAnotherField(body, honeypot)) {
4563
+ return { ok: false, reason: "honeypot" };
4564
+ }
4547
4565
  if (elapsedMs !== null && elapsedMs < opts.minFillMs) return { ok: false, reason: "too_fast" };
4548
- return { ok: true };
4566
+ return filled ? { ok: true, note: "honeypot_autofill" } : { ok: true };
4549
4567
  }
4550
4568
  var splitCsvEnv = (s) => s?.split(",").map((t) => t.trim()).filter(Boolean) ?? [];
4551
4569
 
@@ -4945,6 +4963,7 @@ exports.HONEYPOT_FIELD = HONEYPOT_FIELD;
4945
4963
  exports.HUB_HEADER_OFFSET_PX = HUB_HEADER_OFFSET_PX;
4946
4964
  exports.HUMANITY_SIGNAL_KEYS = HUMANITY_SIGNAL_KEYS;
4947
4965
  exports.ICON_REGISTRY = ICON_REGISTRY;
4966
+ exports.LEGACY_HONEYPOT_FIELD = LEGACY_HONEYPOT_FIELD;
4948
4967
  exports.MAX_MONTH_OFFSET = MAX_MONTH_OFFSET;
4949
4968
  exports.OPENFRAME_DEV_SECTIONS = OPENFRAME_DEV_SECTIONS;
4950
4969
  exports.OS_PLATFORMS = OS_PLATFORMS;