@uranotools/urano-social 0.0.0-stage → 0.2.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 (37) hide show
  1. package/.env.example +6 -0
  2. package/LICENSE +21 -0
  3. package/README.md +192 -2
  4. package/dist/client/createUranoChat.d.ts +16 -0
  5. package/dist/client/createUranoChat.js +226 -0
  6. package/dist/client/index.d.ts +5 -0
  7. package/dist/client/index.js +10 -0
  8. package/dist/client/sse.d.ts +3 -0
  9. package/dist/client/sse.js +108 -0
  10. package/dist/client/submitContactForm.d.ts +30 -0
  11. package/dist/client/submitContactForm.js +65 -0
  12. package/dist/crypto.d.ts +5 -0
  13. package/dist/crypto.js +75 -0
  14. package/dist/flavors/UranoSocialChatElement.d.ts +13 -0
  15. package/dist/flavors/UranoSocialChatElement.js +178 -0
  16. package/dist/flavors/UranoSocialContactFormElement.d.ts +9 -0
  17. package/dist/flavors/UranoSocialContactFormElement.js +98 -0
  18. package/dist/flavors/styles.css +257 -0
  19. package/dist/index.d.ts +12 -0
  20. package/dist/index.js +26 -0
  21. package/dist/react/UranoSocialChat.d.ts +38 -0
  22. package/dist/react/UranoSocialChat.js +118 -0
  23. package/dist/react/UranoSocialContactForm.d.ts +15 -0
  24. package/dist/react/UranoSocialContactForm.js +48 -0
  25. package/dist/react/index.d.ts +4 -0
  26. package/dist/react/index.js +8 -0
  27. package/dist/server/index.d.ts +3 -0
  28. package/dist/server/index.js +13 -0
  29. package/dist/server/proxy.d.ts +16 -0
  30. package/dist/server/proxy.js +207 -0
  31. package/dist/server/visitorCookie.d.ts +11 -0
  32. package/dist/server/visitorCookie.js +43 -0
  33. package/dist/server/websocket.d.ts +21 -0
  34. package/dist/server/websocket.js +130 -0
  35. package/dist/types.d.ts +64 -0
  36. package/dist/types.js +2 -0
  37. package/package.json +91 -4
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.submitContactForm = submitContactForm;
4
+ /**
5
+ * Submits a contact form through the site proxy.
6
+ * Default transport is GET + mode=wait (signed upstream by the server).
7
+ */
8
+ async function submitContactForm(input, options = {}) {
9
+ const endpoint = (options.endpoint || '/api/urano-social').replace(/\/$/, '');
10
+ const email = String(input.email || '').trim();
11
+ const message = String(input.message || '').trim();
12
+ if (!email || !message) {
13
+ return { ok: false, status: 400, error: 'email_and_message_required' };
14
+ }
15
+ const payload = {
16
+ type: 'contact',
17
+ email,
18
+ name: input.name || undefined,
19
+ subject: input.subject || undefined,
20
+ message,
21
+ phone: input.phone || undefined,
22
+ company: input.company || undefined,
23
+ };
24
+ const useGet = options.useGet !== false;
25
+ let response;
26
+ if (useGet) {
27
+ const url = `${endpoint}?mode=wait&payload=${encodeURIComponent(JSON.stringify(payload))}`;
28
+ response = await fetch(url, {
29
+ method: 'GET',
30
+ credentials: 'include',
31
+ signal: options.signal,
32
+ headers: { Accept: 'application/json' },
33
+ });
34
+ }
35
+ else {
36
+ response = await fetch(`${endpoint}?mode=wait`, {
37
+ method: 'POST',
38
+ credentials: 'include',
39
+ signal: options.signal,
40
+ headers: {
41
+ 'Content-Type': 'application/json',
42
+ Accept: 'application/json',
43
+ },
44
+ body: JSON.stringify(payload),
45
+ });
46
+ }
47
+ const raw = await response.json().catch(() => ({}));
48
+ if (!response.ok) {
49
+ return {
50
+ ok: false,
51
+ status: response.status,
52
+ error: String(raw.error || raw.message || `http_${response.status}`),
53
+ raw,
54
+ };
55
+ }
56
+ return {
57
+ ok: true,
58
+ status: response.status,
59
+ text: String(raw.text || raw.response || ''),
60
+ sessionId: raw.sessionId ? String(raw.sessionId) : undefined,
61
+ turnId: raw.turnId ? String(raw.turnId) : undefined,
62
+ delivered: raw.delivered ? String(raw.delivered) : undefined,
63
+ raw,
64
+ };
65
+ }
@@ -0,0 +1,5 @@
1
+ export declare function signHmacHex(payload: string | Buffer, secret: string): string;
2
+ export declare function verifyHmacHex(payload: string | Buffer, signature: string, secret: string): boolean;
3
+ export declare function randomId(prefix?: string): string;
4
+ export declare function signCookieValue(value: string, secret: string): string;
5
+ export declare function verifyCookieValue(signed: string, secret: string): string | null;
package/dist/crypto.js ADDED
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.signHmacHex = signHmacHex;
37
+ exports.verifyHmacHex = verifyHmacHex;
38
+ exports.randomId = randomId;
39
+ exports.signCookieValue = signCookieValue;
40
+ exports.verifyCookieValue = verifyCookieValue;
41
+ const crypto = __importStar(require("crypto"));
42
+ function signHmacHex(payload, secret) {
43
+ return crypto.createHmac('sha256', secret).update(payload).digest('hex');
44
+ }
45
+ function verifyHmacHex(payload, signature, secret) {
46
+ try {
47
+ const expected = signHmacHex(payload, secret);
48
+ const clean = String(signature || '').replace(/^(sha256=|sha1=)/i, '');
49
+ const a = Buffer.from(clean, 'hex');
50
+ const b = Buffer.from(expected, 'hex');
51
+ if (a.length !== b.length || a.length === 0)
52
+ return false;
53
+ return crypto.timingSafeEqual(a, b);
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ function randomId(prefix = 'vis') {
60
+ return `${prefix}_${crypto.randomBytes(16).toString('hex')}`;
61
+ }
62
+ function signCookieValue(value, secret) {
63
+ const sig = signHmacHex(value, secret);
64
+ return `${value}.${sig}`;
65
+ }
66
+ function verifyCookieValue(signed, secret) {
67
+ const idx = signed.lastIndexOf('.');
68
+ if (idx <= 0)
69
+ return null;
70
+ const value = signed.slice(0, idx);
71
+ const sig = signed.slice(idx + 1);
72
+ if (!verifyHmacHex(value, sig, secret))
73
+ return null;
74
+ return value;
75
+ }
@@ -0,0 +1,13 @@
1
+ import { type UranoChat } from '../client/createUranoChat';
2
+ export type SocialFlavor = 'floating' | 'sidebar' | 'headless';
3
+ export declare class UranoSocialChatElement extends HTMLElement {
4
+ #private;
5
+ static get observedAttributes(): string[];
6
+ constructor();
7
+ connectedCallback(): void;
8
+ disconnectedCallback(): void;
9
+ attributeChangedCallback(): void;
10
+ get chat(): UranoChat | null;
11
+ render(): void;
12
+ }
13
+ export declare function registerUranoSocialChat(tagName?: string): void;
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UranoSocialChatElement = void 0;
4
+ exports.registerUranoSocialChat = registerUranoSocialChat;
5
+ const createUranoChat_1 = require("../client/createUranoChat");
6
+ class UranoSocialChatElement extends HTMLElement {
7
+ static get observedAttributes() {
8
+ return ['flavor', 'endpoint', 'title', 'open', 'displayname'];
9
+ }
10
+ #chat = null;
11
+ #shadow;
12
+ #open = false;
13
+ #messages = [];
14
+ #error = '';
15
+ #draft = '';
16
+ constructor() {
17
+ super();
18
+ this.#shadow = this.attachShadow({ mode: 'open' });
19
+ }
20
+ connectedCallback() {
21
+ this.#open = this.getAttribute('open') === 'true' || this.getAttribute('flavor') === 'sidebar';
22
+ this.#chat = (0, createUranoChat_1.createUranoChat)({
23
+ endpoint: this.getAttribute('endpoint') || '/api/urano-social',
24
+ displayName: this.getAttribute('displayname') || undefined,
25
+ onMessages: (msgs) => {
26
+ this.#messages = msgs;
27
+ this.render();
28
+ },
29
+ onError: (err) => {
30
+ this.#error = err.message;
31
+ this.render();
32
+ },
33
+ onStatus: () => this.render(),
34
+ });
35
+ this.render();
36
+ }
37
+ disconnectedCallback() {
38
+ this.#chat?.destroy();
39
+ this.#chat = null;
40
+ }
41
+ attributeChangedCallback() {
42
+ if (!this.#chat)
43
+ return;
44
+ const name = this.getAttribute('displayname');
45
+ if (name)
46
+ this.#chat.setDisplayName(name);
47
+ this.render();
48
+ }
49
+ get chat() {
50
+ return this.#chat;
51
+ }
52
+ render() {
53
+ const flavor = (this.getAttribute('flavor') || 'floating');
54
+ const title = this.getAttribute('title') || 'Atención al cliente';
55
+ const streaming = !!this.#chat?.streaming;
56
+ if (flavor === 'headless') {
57
+ this.#shadow.innerHTML = '';
58
+ return;
59
+ }
60
+ const bubbles = this.#messages.map((m) => `
61
+ <div class="urano-social-bubble" data-role="${m.role}" data-streaming="${m.streaming ? 'true' : 'false'}" part="message">
62
+ <slot name="message-${m.id}">${escapeHtml(m.content)}</slot>
63
+ </div>
64
+ `).join('');
65
+ this.#shadow.innerHTML = `
66
+ <link rel="stylesheet" href="">
67
+ <style>
68
+ @import url("data:text/css,");
69
+ </style>
70
+ <div class="urano-social-host" data-flavor="${flavor}" data-open="${this.#open}">
71
+ ${flavor === 'floating' ? `
72
+ <slot name="launcher">
73
+ <button type="button" class="urano-social-launcher" part="launcher" aria-label="Abrir chat">💬</button>
74
+ </slot>` : ''}
75
+ <div class="urano-social-panel" part="panel">
76
+ <slot name="header">
77
+ <div class="urano-social-header" part="header">
78
+ <h2>${escapeHtml(title)}</h2>
79
+ <button type="button" class="urano-social-close" aria-label="Cerrar">×</button>
80
+ </div>
81
+ </slot>
82
+ <slot name="messages">
83
+ <div class="urano-social-messages" part="messages">
84
+ ${bubbles || `<slot name="empty"><div class="urano-social-empty" part="empty">Escribe tu consulta y te atendemos.</div></slot>`}
85
+ </div>
86
+ </slot>
87
+ ${this.#error ? `<div class="urano-social-error">${escapeHtml(this.#error)}</div>` : ''}
88
+ <slot name="composer">
89
+ <form class="urano-social-composer" part="composer">
90
+ <textarea rows="1" placeholder="Escribe un mensaje…" ${streaming ? 'disabled' : ''}>${escapeHtml(this.#draft)}</textarea>
91
+ <button type="submit" ${streaming ? 'disabled' : ''}>Enviar</button>
92
+ </form>
93
+ </slot>
94
+ </div>
95
+ </div>
96
+ `;
97
+ // Inline critical CSS so the element works without a bundler CSS import.
98
+ const style = document.createElement('style');
99
+ style.textContent = getEmbeddedCss();
100
+ this.#shadow.prepend(style);
101
+ this.#shadow.querySelector('.urano-social-launcher')?.addEventListener('click', () => {
102
+ this.#open = !this.#open;
103
+ this.render();
104
+ });
105
+ this.#shadow.querySelector('.urano-social-close')?.addEventListener('click', () => {
106
+ this.#open = false;
107
+ this.render();
108
+ });
109
+ const form = this.#shadow.querySelector('form');
110
+ const textarea = form?.querySelector('textarea');
111
+ textarea?.addEventListener('input', () => {
112
+ this.#draft = textarea.value;
113
+ });
114
+ form?.addEventListener('submit', async (ev) => {
115
+ ev.preventDefault();
116
+ const text = (textarea?.value || '').trim();
117
+ if (!text || !this.#chat)
118
+ return;
119
+ this.#error = '';
120
+ this.#draft = '';
121
+ try {
122
+ await this.#chat.send(text);
123
+ }
124
+ catch (error) {
125
+ this.#error = error?.message || 'send_failed';
126
+ this.render();
127
+ }
128
+ });
129
+ const scroller = this.#shadow.querySelector('.urano-social-messages');
130
+ if (scroller)
131
+ scroller.scrollTop = scroller.scrollHeight;
132
+ }
133
+ }
134
+ exports.UranoSocialChatElement = UranoSocialChatElement;
135
+ function escapeHtml(value) {
136
+ return String(value)
137
+ .replace(/&/g, '&amp;')
138
+ .replace(/</g, '&lt;')
139
+ .replace(/>/g, '&gt;')
140
+ .replace(/"/g, '&quot;');
141
+ }
142
+ function getEmbeddedCss() {
143
+ // Keep a compact copy for Shadow DOM; full theme also ships as flavors.css.
144
+ return `
145
+ :host { all: initial; }
146
+ .urano-social-host{font-family:ui-sans-serif,system-ui,sans-serif;color:#e8eef6;--us-bg:#0f1419;--us-panel:#1a2332;--us-border:#2a3a4f;--us-accent:#3dba9a;--us-accent-contrast:#06241c;--us-user-bg:#243447;--us-bot-bg:#15202b;--us-danger:#e35d6a;--us-muted:#8fa3b8;--us-radius:14px;--us-shadow:0 18px 50px rgba(0,0,0,.35);--us-z:2147483000}
147
+ .urano-social-host *,.urano-social-host *::before,.urano-social-host *::after{box-sizing:border-box}
148
+ .urano-social-launcher{position:fixed;right:20px;bottom:20px;z-index:var(--us-z);width:56px;height:56px;border:0;border-radius:999px;background:var(--us-accent);color:var(--us-accent-contrast);font-size:22px;cursor:pointer;box-shadow:var(--us-shadow)}
149
+ .urano-social-panel{display:flex;flex-direction:column;background:var(--us-panel);border:1px solid var(--us-border);border-radius:var(--us-radius);box-shadow:var(--us-shadow);overflow:hidden}
150
+ .urano-social-host[data-flavor=floating] .urano-social-panel{position:fixed;right:20px;bottom:88px;z-index:var(--us-z);width:min(380px,calc(100vw - 24px));height:min(560px,calc(100vh - 120px))}
151
+ .urano-social-host[data-flavor=sidebar]{position:fixed;top:0;right:0;z-index:var(--us-z);height:100vh;width:min(380px,100vw)}
152
+ .urano-social-host[data-flavor=sidebar] .urano-social-panel{height:100%;border-radius:0;border-right:0;border-top:0;border-bottom:0}
153
+ .urano-social-header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 16px;border-bottom:1px solid var(--us-border);background:var(--us-bg)}
154
+ .urano-social-header h2{margin:0;font-size:15px;font-weight:600}
155
+ .urano-social-header button{border:0;background:transparent;color:var(--us-muted);cursor:pointer;font-size:18px}
156
+ .urano-social-messages{flex:1;overflow:auto;padding:16px;display:flex;flex-direction:column;gap:10px;background:var(--us-bg)}
157
+ .urano-social-empty{margin:auto;text-align:center;color:var(--us-muted);font-size:14px;padding:24px}
158
+ .urano-social-bubble{max-width:85%;padding:10px 12px;border-radius:12px;font-size:14px;line-height:1.45;white-space:pre-wrap;word-break:break-word}
159
+ .urano-social-bubble[data-role=user]{align-self:flex-end;background:var(--us-user-bg)}
160
+ .urano-social-bubble[data-role=assistant]{align-self:flex-start;background:var(--us-bot-bg);border:1px solid var(--us-border)}
161
+ .urano-social-bubble[data-streaming=true]::after{content:"▍";margin-left:2px;animation:us-blink 1s steps(2,jump-none) infinite}
162
+ @keyframes us-blink{to{opacity:0}}
163
+ .urano-social-composer{display:flex;gap:8px;padding:12px;border-top:1px solid var(--us-border);background:var(--us-panel)}
164
+ .urano-social-composer textarea{flex:1;resize:none;min-height:42px;border-radius:10px;border:1px solid var(--us-border);background:var(--us-bg);color:#e8eef6;padding:10px 12px;font:inherit}
165
+ .urano-social-composer button{border:0;border-radius:10px;background:var(--us-accent);color:var(--us-accent-contrast);padding:0 14px;font-weight:600;cursor:pointer}
166
+ .urano-social-composer button:disabled{opacity:.55;cursor:not-allowed}
167
+ .urano-social-error{color:var(--us-danger);font-size:12px;padding:0 12px 8px}
168
+ .urano-social-host[data-open=false] .urano-social-panel{display:none}
169
+ .urano-social-host[data-flavor=sidebar][data-open=false]{display:none}
170
+ `;
171
+ }
172
+ function registerUranoSocialChat(tagName = 'urano-social-chat') {
173
+ if (typeof customElements === 'undefined')
174
+ return;
175
+ if (!customElements.get(tagName)) {
176
+ customElements.define(tagName, UranoSocialChatElement);
177
+ }
178
+ }
@@ -0,0 +1,9 @@
1
+ export declare class UranoSocialContactFormElement extends HTMLElement {
2
+ #private;
3
+ static get observedAttributes(): string[];
4
+ constructor();
5
+ connectedCallback(): void;
6
+ attributeChangedCallback(): void;
7
+ render(): void;
8
+ }
9
+ export declare function registerUranoSocialContactForm(tagName?: string): void;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UranoSocialContactFormElement = void 0;
4
+ exports.registerUranoSocialContactForm = registerUranoSocialContactForm;
5
+ const submitContactForm_1 = require("../client/submitContactForm");
6
+ class UranoSocialContactFormElement extends HTMLElement {
7
+ static get observedAttributes() {
8
+ return ['endpoint', 'title', 'submit-label'];
9
+ }
10
+ #shadow;
11
+ #busy = false;
12
+ #error = '';
13
+ #success = '';
14
+ constructor() {
15
+ super();
16
+ this.#shadow = this.attachShadow({ mode: 'open' });
17
+ }
18
+ connectedCallback() {
19
+ this.render();
20
+ }
21
+ attributeChangedCallback() {
22
+ this.render();
23
+ }
24
+ render() {
25
+ const title = this.getAttribute('title') || 'Contáctanos';
26
+ const submitLabel = this.getAttribute('submit-label') || 'Enviar';
27
+ this.#shadow.innerHTML = `
28
+ <style>${css}</style>
29
+ <form class="us-contact" part="form">
30
+ <slot name="header"><h3 part="title">${escapeHtml(title)}</h3></slot>
31
+ <label>Nombre<input name="name" type="text" autocomplete="name" /></label>
32
+ <label>Email<input name="email" type="email" required autocomplete="email" /></label>
33
+ <label>Asunto<input name="subject" type="text" /></label>
34
+ <label>Mensaje<textarea name="message" rows="4" required></textarea></label>
35
+ ${this.#error ? `<p class="err" part="error">${escapeHtml(this.#error)}</p>` : ''}
36
+ ${this.#success ? `<p class="ok" part="success">${escapeHtml(this.#success)}</p>` : ''}
37
+ <button type="submit" ${this.#busy ? 'disabled' : ''} part="submit">${escapeHtml(submitLabel)}</button>
38
+ </form>
39
+ `;
40
+ const form = this.#shadow.querySelector('form');
41
+ form?.addEventListener('submit', async (ev) => {
42
+ ev.preventDefault();
43
+ if (this.#busy)
44
+ return;
45
+ const data = new FormData(form);
46
+ this.#busy = true;
47
+ this.#error = '';
48
+ this.#success = '';
49
+ this.render();
50
+ try {
51
+ const result = await (0, submitContactForm_1.submitContactForm)({
52
+ name: String(data.get('name') || ''),
53
+ email: String(data.get('email') || ''),
54
+ subject: String(data.get('subject') || ''),
55
+ message: String(data.get('message') || ''),
56
+ }, { endpoint: this.getAttribute('endpoint') || '/api/urano-social', useGet: true });
57
+ if (!result.ok)
58
+ throw new Error(result.error || 'send_failed');
59
+ this.#success = result.text
60
+ ? 'Enviado. Te responderemos por correo.'
61
+ : 'Enviado. Revisa tu bandeja de entrada.';
62
+ }
63
+ catch (e) {
64
+ this.#error = e?.message || 'send_failed';
65
+ }
66
+ finally {
67
+ this.#busy = false;
68
+ this.render();
69
+ }
70
+ });
71
+ }
72
+ }
73
+ exports.UranoSocialContactFormElement = UranoSocialContactFormElement;
74
+ function escapeHtml(value) {
75
+ return String(value)
76
+ .replace(/&/g, '&amp;')
77
+ .replace(/</g, '&lt;')
78
+ .replace(/>/g, '&gt;')
79
+ .replace(/"/g, '&quot;');
80
+ }
81
+ const css = `
82
+ :host{all:initial;display:block;font-family:ui-sans-serif,system-ui,sans-serif;color:#e8eef6}
83
+ .us-contact{display:flex;flex-direction:column;gap:10px;padding:16px;background:#1a2332;border:1px solid #2a3a4f;border-radius:14px;max-width:420px}
84
+ h3{margin:0 0 4px;font-size:16px}
85
+ label{display:flex;flex-direction:column;gap:4px;font-size:12px;color:#8fa3b8}
86
+ input,textarea{border:1px solid #2a3a4f;border-radius:10px;background:#0f1419;color:#e8eef6;padding:10px 12px;font:inherit}
87
+ button{border:0;border-radius:10px;background:#3dba9a;color:#06241c;padding:10px 14px;font-weight:600;cursor:pointer}
88
+ button:disabled{opacity:.55;cursor:not-allowed}
89
+ .err{color:#e35d6a;font-size:12px;margin:0}
90
+ .ok{color:#3dba9a;font-size:12px;margin:0}
91
+ `;
92
+ function registerUranoSocialContactForm(tagName = 'urano-social-contact') {
93
+ if (typeof customElements === 'undefined')
94
+ return;
95
+ if (!customElements.get(tagName)) {
96
+ customElements.define(tagName, UranoSocialContactFormElement);
97
+ }
98
+ }