@uranotools/urano-social 0.0.0-stage → 0.3.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.
Files changed (41) hide show
  1. package/.env.example +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +212 -2
  4. package/dist/client/createUranoChat.d.ts +16 -0
  5. package/dist/client/createUranoChat.js +223 -0
  6. package/dist/client/index.d.ts +6 -0
  7. package/dist/client/index.js +4 -0
  8. package/dist/client/recaptcha.d.ts +14 -0
  9. package/dist/client/recaptcha.js +36 -0
  10. package/dist/client/sse.d.ts +3 -0
  11. package/dist/client/sse.js +104 -0
  12. package/dist/client/submitContactForm.d.ts +61 -0
  13. package/dist/client/submitContactForm.js +133 -0
  14. package/dist/crypto.d.ts +5 -0
  15. package/dist/crypto.js +35 -0
  16. package/dist/flavors/UranoSocialChatElement.d.ts +13 -0
  17. package/dist/flavors/UranoSocialChatElement.js +173 -0
  18. package/dist/flavors/UranoSocialContactFormElement.d.ts +9 -0
  19. package/dist/flavors/UranoSocialContactFormElement.js +103 -0
  20. package/dist/flavors/styles.css +257 -0
  21. package/dist/index.d.ts +14 -0
  22. package/dist/index.js +10 -0
  23. package/dist/react/UranoSocialChat.d.ts +38 -0
  24. package/dist/react/UranoSocialChat.js +81 -0
  25. package/dist/react/UranoSocialContactForm.d.ts +19 -0
  26. package/dist/react/UranoSocialContactForm.js +78 -0
  27. package/dist/react/index.d.ts +4 -0
  28. package/dist/react/index.js +2 -0
  29. package/dist/server/antiSpam.d.ts +76 -0
  30. package/dist/server/antiSpam.js +204 -0
  31. package/dist/server/index.d.ts +4 -0
  32. package/dist/server/index.js +4 -0
  33. package/dist/server/proxy.d.ts +16 -0
  34. package/dist/server/proxy.js +253 -0
  35. package/dist/server/visitorCookie.d.ts +11 -0
  36. package/dist/server/visitorCookie.js +38 -0
  37. package/dist/server/websocket.d.ts +21 -0
  38. package/dist/server/websocket.js +126 -0
  39. package/dist/types.d.ts +94 -0
  40. package/dist/types.js +3 -0
  41. package/package.json +92 -4
@@ -0,0 +1,257 @@
1
+ :root, .urano-social-host {
2
+ --us-bg: #0f1419;
3
+ --us-panel: #1a2332;
4
+ --us-border: #2a3a4f;
5
+ --us-text: #e8eef6;
6
+ --us-muted: #8fa3b8;
7
+ --us-accent: #3dba9a;
8
+ --us-accent-contrast: #06241c;
9
+ --us-user-bg: #243447;
10
+ --us-bot-bg: #15202b;
11
+ --us-danger: #e35d6a;
12
+ --us-radius: 14px;
13
+ --us-shadow: 0 18px 50px rgba(0, 0, 0, 0.35);
14
+ --us-font: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
15
+ --us-z: 2147483000;
16
+ }
17
+
18
+ .urano-social-host {
19
+ font-family: var(--us-font);
20
+ color: var(--us-text);
21
+ box-sizing: border-box;
22
+ }
23
+
24
+ .urano-social-host *,
25
+ .urano-social-host *::before,
26
+ .urano-social-host *::after {
27
+ box-sizing: border-box;
28
+ }
29
+
30
+ .urano-social-launcher {
31
+ position: fixed;
32
+ right: 20px;
33
+ bottom: 20px;
34
+ z-index: var(--us-z);
35
+ width: 56px;
36
+ height: 56px;
37
+ border: 0;
38
+ border-radius: 999px;
39
+ background: var(--us-accent);
40
+ color: var(--us-accent-contrast);
41
+ font-size: 22px;
42
+ cursor: pointer;
43
+ box-shadow: var(--us-shadow);
44
+ }
45
+
46
+ .urano-social-panel {
47
+ display: flex;
48
+ flex-direction: column;
49
+ background: var(--us-panel);
50
+ border: 1px solid var(--us-border);
51
+ border-radius: var(--us-radius);
52
+ box-shadow: var(--us-shadow);
53
+ overflow: hidden;
54
+ }
55
+
56
+ .urano-social-host[data-flavor="floating"] .urano-social-panel {
57
+ position: fixed;
58
+ right: 20px;
59
+ bottom: 88px;
60
+ z-index: var(--us-z);
61
+ width: min(380px, calc(100vw - 24px));
62
+ height: min(560px, calc(100vh - 120px));
63
+ }
64
+
65
+ .urano-social-host[data-flavor="sidebar"] {
66
+ position: fixed;
67
+ top: 0;
68
+ right: 0;
69
+ z-index: var(--us-z);
70
+ height: 100vh;
71
+ width: min(380px, 100vw);
72
+ }
73
+
74
+ .urano-social-host[data-flavor="sidebar"] .urano-social-panel {
75
+ height: 100%;
76
+ border-radius: 0;
77
+ border-right: 0;
78
+ border-top: 0;
79
+ border-bottom: 0;
80
+ }
81
+
82
+ .urano-social-header {
83
+ display: flex;
84
+ align-items: center;
85
+ justify-content: space-between;
86
+ gap: 12px;
87
+ padding: 14px 16px;
88
+ border-bottom: 1px solid var(--us-border);
89
+ background: var(--us-bg);
90
+ }
91
+
92
+ .urano-social-header h2 {
93
+ margin: 0;
94
+ font-size: 15px;
95
+ font-weight: 600;
96
+ }
97
+
98
+ .urano-social-header button {
99
+ border: 0;
100
+ background: transparent;
101
+ color: var(--us-muted);
102
+ cursor: pointer;
103
+ font-size: 18px;
104
+ }
105
+
106
+ .urano-social-messages {
107
+ flex: 1;
108
+ overflow: auto;
109
+ padding: 16px;
110
+ display: flex;
111
+ flex-direction: column;
112
+ gap: 10px;
113
+ background: var(--us-bg);
114
+ }
115
+
116
+ .urano-social-empty {
117
+ margin: auto;
118
+ text-align: center;
119
+ color: var(--us-muted);
120
+ font-size: 14px;
121
+ padding: 24px;
122
+ }
123
+
124
+ .urano-social-bubble {
125
+ max-width: 85%;
126
+ padding: 10px 12px;
127
+ border-radius: 12px;
128
+ font-size: 14px;
129
+ line-height: 1.45;
130
+ white-space: pre-wrap;
131
+ word-break: break-word;
132
+ }
133
+
134
+ .urano-social-bubble[data-role="user"] {
135
+ align-self: flex-end;
136
+ background: var(--us-user-bg);
137
+ }
138
+
139
+ .urano-social-bubble[data-role="assistant"] {
140
+ align-self: flex-start;
141
+ background: var(--us-bot-bg);
142
+ border: 1px solid var(--us-border);
143
+ }
144
+
145
+ .urano-social-bubble[data-streaming="true"]::after {
146
+ content: "▍";
147
+ display: inline-block;
148
+ margin-left: 2px;
149
+ animation: us-blink 1s steps(2, jump-none) infinite;
150
+ }
151
+
152
+ @keyframes us-blink {
153
+ to { opacity: 0; }
154
+ }
155
+
156
+ .urano-social-composer {
157
+ display: flex;
158
+ gap: 8px;
159
+ padding: 12px;
160
+ border-top: 1px solid var(--us-border);
161
+ background: var(--us-panel);
162
+ }
163
+
164
+ .urano-social-composer input,
165
+ .urano-social-composer textarea {
166
+ flex: 1;
167
+ resize: none;
168
+ min-height: 42px;
169
+ max-height: 120px;
170
+ border-radius: 10px;
171
+ border: 1px solid var(--us-border);
172
+ background: var(--us-bg);
173
+ color: var(--us-text);
174
+ padding: 10px 12px;
175
+ font: inherit;
176
+ }
177
+
178
+ .urano-social-composer button {
179
+ border: 0;
180
+ border-radius: 10px;
181
+ background: var(--us-accent);
182
+ color: var(--us-accent-contrast);
183
+ padding: 0 14px;
184
+ font-weight: 600;
185
+ cursor: pointer;
186
+ }
187
+
188
+ .urano-social-composer button:disabled {
189
+ opacity: 0.55;
190
+ cursor: not-allowed;
191
+ }
192
+
193
+ .urano-social-error {
194
+ color: var(--us-danger);
195
+ font-size: 12px;
196
+ padding: 0 12px 8px;
197
+ }
198
+
199
+ .urano-social-host[data-open="false"] .urano-social-panel {
200
+ display: none;
201
+ }
202
+
203
+ .urano-social-host[data-flavor="sidebar"][data-open="false"] {
204
+ display: none;
205
+ }
206
+
207
+ /* Contact form (light DOM / React) */
208
+ .urano-social-contact {
209
+ display: flex;
210
+ flex-direction: column;
211
+ gap: 10px;
212
+ padding: 16px;
213
+ max-width: 420px;
214
+ background: var(--us-panel, #1a2332);
215
+ border: 1px solid var(--us-border, #2a3a4f);
216
+ border-radius: var(--us-radius, 14px);
217
+ color: var(--us-text, #e8eef6);
218
+ font-family: var(--us-font, ui-sans-serif, system-ui, sans-serif);
219
+ }
220
+
221
+ .urano-social-contact h3 {
222
+ margin: 0 0 4px;
223
+ font-size: 16px;
224
+ }
225
+
226
+ .urano-social-contact label {
227
+ display: flex;
228
+ flex-direction: column;
229
+ gap: 4px;
230
+ font-size: 12px;
231
+ color: var(--us-muted, #8fa3b8);
232
+ }
233
+
234
+ .urano-social-contact input,
235
+ .urano-social-contact textarea {
236
+ border: 1px solid var(--us-border, #2a3a4f);
237
+ border-radius: 10px;
238
+ background: var(--us-bg, #0f1419);
239
+ color: var(--us-text, #e8eef6);
240
+ padding: 10px 12px;
241
+ font: inherit;
242
+ }
243
+
244
+ .urano-social-contact button {
245
+ border: 0;
246
+ border-radius: 10px;
247
+ background: var(--us-accent, #3dba9a);
248
+ color: var(--us-accent-contrast, #06241c);
249
+ padding: 10px 14px;
250
+ font-weight: 600;
251
+ cursor: pointer;
252
+ }
253
+
254
+ .urano-social-contact button:disabled {
255
+ opacity: 0.55;
256
+ cursor: not-allowed;
257
+ }
@@ -0,0 +1,14 @@
1
+ export { createUranoChat } from './client/createUranoChat';
2
+ export type { UranoChat } from './client/createUranoChat';
3
+ export { readSseStream, parseSseChunk } from './client/sse';
4
+ export { submitContactForm, fetchContactTicket } from './client/submitContactForm';
5
+ export type { ContactFormPayload, ContactFormResult, ContactTicketResponse, SubmitContactFormOptions, } from './client/submitContactForm';
6
+ export { createSocialProxy, socialProxyRoute } from './server/proxy';
7
+ export { attachSocialWebSocket, buildWsEnvelope } from './server/websocket';
8
+ export { registerUranoSocialChat, UranoSocialChatElement } from './flavors/UranoSocialChatElement';
9
+ export type { SocialFlavor } from './flavors/UranoSocialChatElement';
10
+ export { registerUranoSocialContactForm, UranoSocialContactFormElement } from './flavors/UranoSocialContactFormElement';
11
+ export { signHmacHex, verifyHmacHex, randomId } from './crypto';
12
+ export { DEFAULT_HONEYPOT_FIELD, DEFAULT_RECAPTCHA_ACTION, } from './types';
13
+ export type { SocialProxyConfig, ContactAntiSpamConfig, SocialClientMessage, SocialChatEvent, CreateUranoChatOptions, SocialSendOptions, SocialTransport, } from './types';
14
+ export { loadRecaptchaV3, executeRecaptchaV3 } from './client/recaptcha';
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export { createUranoChat } from './client/createUranoChat';
2
+ export { readSseStream, parseSseChunk } from './client/sse';
3
+ export { submitContactForm, fetchContactTicket } from './client/submitContactForm';
4
+ export { createSocialProxy, socialProxyRoute } from './server/proxy';
5
+ export { attachSocialWebSocket, buildWsEnvelope } from './server/websocket';
6
+ export { registerUranoSocialChat, UranoSocialChatElement } from './flavors/UranoSocialChatElement';
7
+ export { registerUranoSocialContactForm, UranoSocialContactFormElement } from './flavors/UranoSocialContactFormElement';
8
+ export { signHmacHex, verifyHmacHex, randomId } from './crypto';
9
+ export { DEFAULT_HONEYPOT_FIELD, DEFAULT_RECAPTCHA_ACTION, } from './types';
10
+ export { loadRecaptchaV3, executeRecaptchaV3 } from './client/recaptcha';
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import { type UranoChat } from '../client/createUranoChat';
3
+ import type { SocialClientMessage } from '../types';
4
+ export type SocialFlavor = 'floating' | 'sidebar' | 'headless';
5
+ export interface UranoSocialChatProps {
6
+ flavor?: SocialFlavor;
7
+ endpoint?: string;
8
+ title?: string;
9
+ displayName?: string;
10
+ className?: string;
11
+ open?: boolean;
12
+ defaultOpen?: boolean;
13
+ onOpenChange?: (open: boolean) => void;
14
+ renderHeader?: (props: {
15
+ title: string;
16
+ onClose: () => void;
17
+ }) => React.ReactNode;
18
+ renderMessages?: (props: {
19
+ messages: SocialClientMessage[];
20
+ }) => React.ReactNode;
21
+ renderMessage?: (message: SocialClientMessage) => React.ReactNode;
22
+ renderComposer?: (props: {
23
+ disabled: boolean;
24
+ value: string;
25
+ onChange: (value: string) => void;
26
+ onSubmit: () => void;
27
+ }) => React.ReactNode;
28
+ renderLauncher?: (props: {
29
+ open: boolean;
30
+ onToggle: () => void;
31
+ }) => React.ReactNode;
32
+ renderEmpty?: () => React.ReactNode;
33
+ }
34
+ export declare function UranoSocialChat(props: UranoSocialChatProps): React.JSX.Element | null;
35
+ export declare function useUranoChat(options: {
36
+ endpoint?: string;
37
+ displayName?: string;
38
+ }): UranoChat;
@@ -0,0 +1,81 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
3
+ import { createUranoChat } from '../client/createUranoChat';
4
+ export function UranoSocialChat(props) {
5
+ const flavor = props.flavor || 'floating';
6
+ const [open, setOpen] = useState(props.open ?? props.defaultOpen ?? flavor === 'sidebar');
7
+ const [messages, setMessages] = useState([]);
8
+ const [streaming, setStreaming] = useState(false);
9
+ const [error, setError] = useState('');
10
+ const [draft, setDraft] = useState('');
11
+ const chatRef = useRef(null);
12
+ useEffect(() => {
13
+ if (props.open !== undefined)
14
+ setOpen(props.open);
15
+ }, [props.open]);
16
+ useEffect(() => {
17
+ const chat = createUranoChat({
18
+ endpoint: props.endpoint || '/api/urano-social',
19
+ displayName: props.displayName,
20
+ onMessages: setMessages,
21
+ onError: (err) => setError(err.message),
22
+ onStatus: (status) => setStreaming(status.streaming),
23
+ });
24
+ chatRef.current = chat;
25
+ return () => chat.destroy();
26
+ }, [props.endpoint, props.displayName]);
27
+ const setOpenBoth = (next) => {
28
+ setOpen(next);
29
+ props.onOpenChange?.(next);
30
+ };
31
+ const onSubmit = async () => {
32
+ const text = draft.trim();
33
+ if (!text || !chatRef.current)
34
+ return;
35
+ setDraft('');
36
+ setError('');
37
+ try {
38
+ await chatRef.current.send(text);
39
+ }
40
+ catch (err) {
41
+ setError(err?.message || 'send_failed');
42
+ }
43
+ };
44
+ const header = props.renderHeader
45
+ ? props.renderHeader({ title: props.title || 'Atención al cliente', onClose: () => setOpenBoth(false) })
46
+ : (_jsxs("div", { className: "urano-social-header", children: [_jsx("h2", { children: props.title || 'Atención al cliente' }), _jsx("button", { type: "button", onClick: () => setOpenBoth(false), "aria-label": "Cerrar", children: "\u00D7" })] }));
47
+ const messageList = props.renderMessages
48
+ ? props.renderMessages({ messages })
49
+ : (_jsx("div", { className: "urano-social-messages", children: messages.length === 0
50
+ ? (props.renderEmpty?.() || _jsx("div", { className: "urano-social-empty", children: "Escribe tu consulta y te atendemos." }))
51
+ : messages.map((m) => (props.renderMessage
52
+ ? _jsx(React.Fragment, { children: props.renderMessage(m) }, m.id)
53
+ : (_jsx("div", { className: "urano-social-bubble", "data-role": m.role, "data-streaming": m.streaming ? 'true' : 'false', children: m.content }, m.id)))) }));
54
+ const composer = props.renderComposer
55
+ ? props.renderComposer({
56
+ disabled: streaming,
57
+ value: draft,
58
+ onChange: setDraft,
59
+ onSubmit,
60
+ })
61
+ : (_jsxs("form", { className: "urano-social-composer", onSubmit: (e) => {
62
+ e.preventDefault();
63
+ void onSubmit();
64
+ }, children: [_jsx("textarea", { rows: 1, value: draft, disabled: streaming, placeholder: "Escribe un mensaje\u2026", onChange: (e) => setDraft(e.target.value) }), _jsx("button", { type: "submit", disabled: streaming || !draft.trim(), children: "Enviar" })] }));
65
+ const launcher = flavor === 'floating'
66
+ ? (props.renderLauncher
67
+ ? props.renderLauncher({ open, onToggle: () => setOpenBoth(!open) })
68
+ : _jsx("button", { type: "button", className: "urano-social-launcher", onClick: () => setOpenBoth(!open), children: "\uD83D\uDCAC" }))
69
+ : null;
70
+ if (flavor === 'headless')
71
+ return null;
72
+ return (_jsxs("div", { className: ['urano-social-host', props.className].filter(Boolean).join(' '), "data-flavor": flavor, "data-open": String(open), children: [launcher, _jsxs("div", { className: "urano-social-panel", children: [header, messageList, error ? _jsx("div", { className: "urano-social-error", children: error }) : null, composer] })] }));
73
+ }
74
+ export function useUranoChat(options) {
75
+ const chat = useMemo(() => createUranoChat({
76
+ endpoint: options.endpoint,
77
+ displayName: options.displayName,
78
+ }), [options.endpoint, options.displayName]);
79
+ useEffect(() => () => chat.destroy(), [chat]);
80
+ return chat;
81
+ }
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ export interface UranoSocialContactFormProps {
3
+ endpoint?: string;
4
+ title?: string;
5
+ submitLabel?: string;
6
+ className?: string;
7
+ /** Public reCAPTCHA v3 site key — paste from Google admin; secret stays server-side. */
8
+ recaptchaSiteKey?: string;
9
+ recaptchaAction?: string;
10
+ honeypotField?: string;
11
+ onSuccess?: (text: string) => void;
12
+ onError?: (error: string) => void;
13
+ renderFields?: (props: {
14
+ values: Record<string, string>;
15
+ setValue: (key: string, value: string) => void;
16
+ disabled: boolean;
17
+ }) => React.ReactNode;
18
+ }
19
+ export declare function UranoSocialContactForm(props: UranoSocialContactFormProps): React.JSX.Element;
@@ -0,0 +1,78 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { fetchContactTicket, submitContactForm } from '../client/submitContactForm';
4
+ import { DEFAULT_HONEYPOT_FIELD } from '../types';
5
+ export function UranoSocialContactForm(props) {
6
+ const honeypotField = props.honeypotField || DEFAULT_HONEYPOT_FIELD;
7
+ const endpoint = props.endpoint || '/api/urano-social';
8
+ const [values, setValues] = useState({
9
+ name: '',
10
+ email: '',
11
+ subject: '',
12
+ message: '',
13
+ [honeypotField]: '',
14
+ });
15
+ const [ticket, setTicket] = useState('');
16
+ const [busy, setBusy] = useState(false);
17
+ const [error, setError] = useState('');
18
+ const [success, setSuccess] = useState('');
19
+ useEffect(() => {
20
+ let cancelled = false;
21
+ fetchContactTicket(endpoint)
22
+ .then((res) => { if (!cancelled)
23
+ setTicket(res.ticket); })
24
+ .catch(() => { });
25
+ return () => { cancelled = true; };
26
+ }, [endpoint]);
27
+ const setValue = (key, value) => {
28
+ setValues((prev) => ({ ...prev, [key]: value }));
29
+ };
30
+ const onSubmit = async (ev) => {
31
+ ev.preventDefault();
32
+ if (busy)
33
+ return;
34
+ setBusy(true);
35
+ setError('');
36
+ setSuccess('');
37
+ try {
38
+ const result = await submitContactForm({
39
+ name: values.name,
40
+ email: values.email,
41
+ subject: values.subject,
42
+ message: values.message,
43
+ website: values[honeypotField] || '',
44
+ contactTicket: ticket || undefined,
45
+ }, {
46
+ endpoint,
47
+ recaptchaSiteKey: props.recaptchaSiteKey,
48
+ recaptchaAction: props.recaptchaAction,
49
+ });
50
+ if (!result.ok)
51
+ throw new Error(result.error || 'send_failed');
52
+ const msg = 'Enviado. Te responderemos por correo.';
53
+ setSuccess(msg);
54
+ props.onSuccess?.(result.text || msg);
55
+ setValues({
56
+ name: '',
57
+ email: '',
58
+ subject: '',
59
+ message: '',
60
+ [honeypotField]: '',
61
+ });
62
+ const next = await fetchContactTicket(endpoint).catch(() => null);
63
+ if (next)
64
+ setTicket(next.ticket);
65
+ }
66
+ catch (e) {
67
+ const msg = e?.message || 'send_failed';
68
+ setError(msg);
69
+ props.onError?.(msg);
70
+ }
71
+ finally {
72
+ setBusy(false);
73
+ }
74
+ };
75
+ return (_jsxs("form", { className: ['urano-social-contact', props.className].filter(Boolean).join(' '), onSubmit: onSubmit, children: [_jsx("h3", { children: props.title || 'Contáctanos' }), props.renderFields
76
+ ? props.renderFields({ values, setValue, disabled: busy })
77
+ : (_jsxs(_Fragment, { children: [_jsxs("label", { children: ["Nombre", _jsx("input", { value: values.name, disabled: busy, onChange: (e) => setValue('name', e.target.value) })] }), _jsxs("label", { children: ["Email", _jsx("input", { type: "email", required: true, value: values.email, disabled: busy, onChange: (e) => setValue('email', e.target.value) })] }), _jsxs("label", { children: ["Asunto", _jsx("input", { value: values.subject, disabled: busy, onChange: (e) => setValue('subject', e.target.value) })] }), _jsxs("label", { children: ["Mensaje", _jsx("textarea", { required: true, rows: 4, value: values.message, disabled: busy, onChange: (e) => setValue('message', e.target.value) })] })] })), _jsxs("label", { "aria-hidden": "true", style: { position: 'absolute', left: '-10000px', top: 'auto', width: 1, height: 1, overflow: 'hidden' }, children: [honeypotField, _jsx("input", { tabIndex: -1, autoComplete: "off", name: honeypotField, value: values[honeypotField] || '', onChange: (e) => setValue(honeypotField, e.target.value) })] }), error ? _jsx("p", { className: "urano-social-error", children: error }) : null, success ? _jsx("p", { style: { color: 'var(--us-accent, #3dba9a)', fontSize: 12 }, children: success }) : null, _jsx("button", { type: "submit", disabled: busy, children: props.submitLabel || 'Enviar' })] }));
78
+ }
@@ -0,0 +1,4 @@
1
+ export { UranoSocialChat, useUranoChat } from './UranoSocialChat';
2
+ export type { UranoSocialChatProps, SocialFlavor } from './UranoSocialChat';
3
+ export { UranoSocialContactForm } from './UranoSocialContactForm';
4
+ export type { UranoSocialContactFormProps } from './UranoSocialContactForm';
@@ -0,0 +1,2 @@
1
+ export { UranoSocialChat, useUranoChat } from './UranoSocialChat';
2
+ export { UranoSocialContactForm } from './UranoSocialContactForm';
@@ -0,0 +1,76 @@
1
+ import { DEFAULT_HONEYPOT_FIELD, DEFAULT_RECAPTCHA_ACTION, type ContactAntiSpamConfig } from '../types';
2
+ export { DEFAULT_HONEYPOT_FIELD, DEFAULT_RECAPTCHA_ACTION };
3
+ export declare const DEFAULT_RECAPTCHA_MIN_SCORE = 0.5;
4
+ export declare const DEFAULT_CONTACT_MIN_AGE_MS = 2500;
5
+ export declare const DEFAULT_CONTACT_MAX_AGE_MS: number;
6
+ export declare const DEFAULT_CONTACT_MAX_PER_HOUR = 5;
7
+ export declare const DEFAULT_CONTACT_MAX_MESSAGE_CHARS = 4000;
8
+ export declare function resolveAntiSpamConfig(partial?: Partial<ContactAntiSpamConfig>): ContactAntiSpamConfig;
9
+ export declare function issueContactTicket(secret: string, now?: number): string;
10
+ export declare function verifyContactTicket(ticket: string | undefined, secret: string, opts: {
11
+ minAgeMs: number;
12
+ maxAgeMs: number;
13
+ }, now?: number): {
14
+ ok: true;
15
+ issuedAt: number;
16
+ } | {
17
+ ok: false;
18
+ error: string;
19
+ };
20
+ export declare function checkContactRateLimit(key: string, maxPerHour: number, now?: number): {
21
+ ok: true;
22
+ } | {
23
+ ok: false;
24
+ error: string;
25
+ retryAfterSec: number;
26
+ };
27
+ /** Test helper — clears in-memory rate buckets. */
28
+ export declare function resetContactRateLimits(): void;
29
+ export declare function validateContactFields(input: {
30
+ email?: unknown;
31
+ message?: unknown;
32
+ maxMessageChars: number;
33
+ }): {
34
+ ok: true;
35
+ email: string;
36
+ message: string;
37
+ } | {
38
+ ok: false;
39
+ error: string;
40
+ };
41
+ export declare function verifyRecaptchaV3(opts: {
42
+ secret: string;
43
+ token: string;
44
+ minScore: number;
45
+ expectedAction: string;
46
+ remoteIp?: string | null;
47
+ }): Promise<{
48
+ ok: true;
49
+ score: number;
50
+ } | {
51
+ ok: false;
52
+ error: string;
53
+ }>;
54
+ export declare function clientIpFromRequest(request: Request): string | null;
55
+ /**
56
+ * Runs contact anti-spam gates. On honeypot hit returns softOk so the proxy
57
+ * can fake a success without calling the AI webhook.
58
+ */
59
+ export declare function enforceContactAntiSpam(opts: {
60
+ body: Record<string, unknown>;
61
+ request: Request;
62
+ visitorId: string;
63
+ ticketSecret: string;
64
+ config: ContactAntiSpamConfig;
65
+ }): Promise<{
66
+ ok: true;
67
+ } | {
68
+ ok: false;
69
+ softOk: true;
70
+ fakeText: string;
71
+ } | {
72
+ ok: false;
73
+ status: number;
74
+ error: string;
75
+ retryAfterSec?: number;
76
+ }>;