@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.
- package/.env.example +6 -0
- package/LICENSE +21 -0
- package/README.md +192 -2
- package/dist/client/createUranoChat.d.ts +16 -0
- package/dist/client/createUranoChat.js +226 -0
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.js +10 -0
- package/dist/client/sse.d.ts +3 -0
- package/dist/client/sse.js +108 -0
- package/dist/client/submitContactForm.d.ts +30 -0
- package/dist/client/submitContactForm.js +65 -0
- package/dist/crypto.d.ts +5 -0
- package/dist/crypto.js +75 -0
- package/dist/flavors/UranoSocialChatElement.d.ts +13 -0
- package/dist/flavors/UranoSocialChatElement.js +178 -0
- package/dist/flavors/UranoSocialContactFormElement.d.ts +9 -0
- package/dist/flavors/UranoSocialContactFormElement.js +98 -0
- package/dist/flavors/styles.css +257 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +26 -0
- package/dist/react/UranoSocialChat.d.ts +38 -0
- package/dist/react/UranoSocialChat.js +118 -0
- package/dist/react/UranoSocialContactForm.d.ts +15 -0
- package/dist/react/UranoSocialContactForm.js +48 -0
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.js +8 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +13 -0
- package/dist/server/proxy.d.ts +16 -0
- package/dist/server/proxy.js +207 -0
- package/dist/server/visitorCookie.d.ts +11 -0
- package/dist/server/visitorCookie.js +43 -0
- package/dist/server/websocket.d.ts +21 -0
- package/dist/server/websocket.js +130 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.js +2 -0
- package/package.json +91 -4
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSocialProxy = createSocialProxy;
|
|
4
|
+
exports.socialProxyRoute = socialProxyRoute;
|
|
5
|
+
const crypto_1 = require("../crypto");
|
|
6
|
+
const visitorCookie_1 = require("./visitorCookie");
|
|
7
|
+
function envConfig(partial) {
|
|
8
|
+
const webhookUrl = partial?.webhookUrl || process.env.URANO_SOCIAL_WEBHOOK_URL || '';
|
|
9
|
+
const webhookSecret = partial?.webhookSecret || process.env.URANO_SOCIAL_WEBHOOK_SECRET || '';
|
|
10
|
+
if (!webhookUrl)
|
|
11
|
+
throw new Error('URANO_SOCIAL_WEBHOOK_URL is required');
|
|
12
|
+
if (!webhookSecret)
|
|
13
|
+
throw new Error('URANO_SOCIAL_WEBHOOK_SECRET is required');
|
|
14
|
+
return {
|
|
15
|
+
webhookUrl: webhookUrl.replace(/\/$/, ''),
|
|
16
|
+
webhookSecret,
|
|
17
|
+
cookieSecret: partial?.cookieSecret || process.env.URANO_SOCIAL_COOKIE_SECRET || webhookSecret,
|
|
18
|
+
cookieName: partial?.cookieName || visitorCookie_1.DEFAULT_COOKIE_NAME,
|
|
19
|
+
cookieMaxAgeSec: partial?.cookieMaxAgeSec,
|
|
20
|
+
defaultDisplayName: partial?.defaultDisplayName,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function json(status, body, headers) {
|
|
24
|
+
return new Response(JSON.stringify(body), {
|
|
25
|
+
status,
|
|
26
|
+
headers: { 'Content-Type': 'application/json', ...(headers || {}) },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async function readJson(request) {
|
|
30
|
+
const text = await request.text();
|
|
31
|
+
if (!text)
|
|
32
|
+
return {};
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(text);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new Error('invalid_json');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Creates a site-local proxy that signs webhook traffic with the server secret.
|
|
42
|
+
* The browser never receives WEBHOOK_SECRET.
|
|
43
|
+
*/
|
|
44
|
+
function createSocialProxy(partial) {
|
|
45
|
+
const config = envConfig(partial);
|
|
46
|
+
async function handle(request) {
|
|
47
|
+
const url = new URL(request.url);
|
|
48
|
+
const method = request.method.toUpperCase();
|
|
49
|
+
if (method === 'OPTIONS') {
|
|
50
|
+
return new Response(null, {
|
|
51
|
+
status: 204,
|
|
52
|
+
headers: {
|
|
53
|
+
'Access-Control-Allow-Origin': request.headers.get('Origin') || '*',
|
|
54
|
+
'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
|
|
55
|
+
'Access-Control-Allow-Headers': 'Content-Type, Idempotency-Key, Last-Event-ID, Accept',
|
|
56
|
+
'Access-Control-Allow-Credentials': 'true',
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const visitor = (0, visitorCookie_1.resolveVisitorId)({
|
|
61
|
+
cookieHeader: request.headers.get('cookie'),
|
|
62
|
+
cookieName: config.cookieName,
|
|
63
|
+
cookieSecret: config.cookieSecret,
|
|
64
|
+
});
|
|
65
|
+
const responseHeaders = new Headers();
|
|
66
|
+
if (visitor.setCookie)
|
|
67
|
+
responseHeaders.append('Set-Cookie', visitor.setCookie);
|
|
68
|
+
if (method === 'GET' && !url.searchParams.get('mode') && !url.searchParams.get('payload')) {
|
|
69
|
+
try {
|
|
70
|
+
const discovery = await fetch(config.webhookUrl, { method: 'GET' });
|
|
71
|
+
const body = await discovery.json().catch(() => ({ ok: discovery.ok }));
|
|
72
|
+
responseHeaders.set('Content-Type', 'application/json');
|
|
73
|
+
return new Response(JSON.stringify({
|
|
74
|
+
ok: true,
|
|
75
|
+
proxy: true,
|
|
76
|
+
visitorId: visitor.visitorId,
|
|
77
|
+
upstream: body,
|
|
78
|
+
}), { status: 200, headers: responseHeaders });
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return json(502, { error: 'upstream_unreachable', message: error?.message }, responseHeaders);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
let clientBody = {};
|
|
85
|
+
try {
|
|
86
|
+
if (method === 'GET') {
|
|
87
|
+
const payloadParam = url.searchParams.get('payload');
|
|
88
|
+
clientBody = payloadParam ? JSON.parse(payloadParam) : {};
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
clientBody = await readJson(request);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return json(400, { error: 'invalid_json' }, responseHeaders);
|
|
96
|
+
}
|
|
97
|
+
const mode = String(url.searchParams.get('mode')
|
|
98
|
+
|| clientBody.mode
|
|
99
|
+
|| ((request.headers.get('Accept') || '').includes('text/event-stream') ? 'sse' : 'wait'));
|
|
100
|
+
const isContact = ['contact', 'email', 'form'].includes(String(clientBody.type || clientBody.channel || '').toLowerCase());
|
|
101
|
+
const outbound = isContact
|
|
102
|
+
? { type: 'contact' }
|
|
103
|
+
: {
|
|
104
|
+
visitorId: visitor.visitorId,
|
|
105
|
+
from: visitor.visitorId,
|
|
106
|
+
};
|
|
107
|
+
if (isContact) {
|
|
108
|
+
if (typeof clientBody.email === 'string')
|
|
109
|
+
outbound.email = clientBody.email.trim().toLowerCase();
|
|
110
|
+
if (typeof clientBody.name === 'string')
|
|
111
|
+
outbound.name = clientBody.name;
|
|
112
|
+
if (typeof clientBody.subject === 'string')
|
|
113
|
+
outbound.subject = clientBody.subject;
|
|
114
|
+
if (typeof clientBody.message === 'string')
|
|
115
|
+
outbound.message = clientBody.message;
|
|
116
|
+
else if (typeof clientBody.text === 'string')
|
|
117
|
+
outbound.message = clientBody.text;
|
|
118
|
+
if (typeof clientBody.phone === 'string')
|
|
119
|
+
outbound.phone = clientBody.phone;
|
|
120
|
+
if (typeof clientBody.company === 'string')
|
|
121
|
+
outbound.company = clientBody.company;
|
|
122
|
+
// Correlate without trusting client visitor identity for CRM email channel.
|
|
123
|
+
outbound.visitorId = visitor.visitorId;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
if (typeof clientBody.text === 'string')
|
|
127
|
+
outbound.text = clientBody.text;
|
|
128
|
+
if (typeof clientBody.sessionId === 'string')
|
|
129
|
+
outbound.sessionId = clientBody.sessionId;
|
|
130
|
+
if (typeof clientBody.SessionId === 'string')
|
|
131
|
+
outbound.sessionId = clientBody.SessionId;
|
|
132
|
+
if (typeof clientBody.displayName === 'string')
|
|
133
|
+
outbound.displayName = clientBody.displayName;
|
|
134
|
+
else if (config.defaultDisplayName)
|
|
135
|
+
outbound.displayName = config.defaultDisplayName;
|
|
136
|
+
if (clientBody.webhookAction)
|
|
137
|
+
outbound.webhookAction = clientBody.webhookAction;
|
|
138
|
+
if (clientBody.turnId)
|
|
139
|
+
outbound.turnId = clientBody.turnId;
|
|
140
|
+
if (clientBody.after)
|
|
141
|
+
outbound.after = clientBody.after;
|
|
142
|
+
if (clientBody.idempotencyKey)
|
|
143
|
+
outbound.idempotencyKey = clientBody.idempotencyKey;
|
|
144
|
+
}
|
|
145
|
+
// Never trust a visitorId / from supplied by the browser for chat identity.
|
|
146
|
+
delete clientBody.visitorId;
|
|
147
|
+
delete clientBody.from;
|
|
148
|
+
const rawBody = JSON.stringify(outbound);
|
|
149
|
+
const signature = (0, crypto_1.signHmacHex)(rawBody, config.webhookSecret);
|
|
150
|
+
const idempotency = request.headers.get('Idempotency-Key')
|
|
151
|
+
|| (typeof outbound.idempotencyKey === 'string' ? outbound.idempotencyKey : undefined);
|
|
152
|
+
const upstreamHeaders = {
|
|
153
|
+
'Content-Type': 'application/json',
|
|
154
|
+
'x-urano-signature': signature,
|
|
155
|
+
'X-Urano-Protocol-Version': '1',
|
|
156
|
+
};
|
|
157
|
+
if (idempotency)
|
|
158
|
+
upstreamHeaders['Idempotency-Key'] = idempotency;
|
|
159
|
+
const lastEventId = request.headers.get('Last-Event-ID');
|
|
160
|
+
if (lastEventId)
|
|
161
|
+
upstreamHeaders['Last-Event-ID'] = lastEventId;
|
|
162
|
+
let upstreamUrl = `${config.webhookUrl}?mode=${encodeURIComponent(mode === 'auto' ? 'sse' : mode)}`;
|
|
163
|
+
let upstreamInit;
|
|
164
|
+
if (method === 'GET') {
|
|
165
|
+
upstreamUrl += `&payload=${encodeURIComponent(rawBody)}`;
|
|
166
|
+
upstreamInit = { method: 'GET', headers: upstreamHeaders };
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
upstreamInit = { method: 'POST', headers: upstreamHeaders, body: rawBody };
|
|
170
|
+
}
|
|
171
|
+
if (mode === 'sse' || request.headers.get('Accept')?.includes('text/event-stream')) {
|
|
172
|
+
upstreamHeaders['Accept'] = 'text/event-stream';
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const upstream = await fetch(upstreamUrl, upstreamInit);
|
|
176
|
+
upstream.headers.forEach((value, key) => {
|
|
177
|
+
const lower = key.toLowerCase();
|
|
178
|
+
if (['transfer-encoding', 'content-encoding', 'content-length', 'set-cookie'].includes(lower))
|
|
179
|
+
return;
|
|
180
|
+
responseHeaders.set(key, value);
|
|
181
|
+
});
|
|
182
|
+
responseHeaders.set('X-Urano-Social-Visitor', visitor.visitorId);
|
|
183
|
+
responseHeaders.set('Cache-Control', 'no-cache, no-transform');
|
|
184
|
+
if (mode === 'sse') {
|
|
185
|
+
responseHeaders.set('Content-Type', 'text/event-stream; charset=utf-8');
|
|
186
|
+
responseHeaders.set('X-Accel-Buffering', 'no');
|
|
187
|
+
}
|
|
188
|
+
return new Response(upstream.body, {
|
|
189
|
+
status: upstream.status,
|
|
190
|
+
headers: responseHeaders,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
return json(502, { error: 'upstream_unreachable', message: error?.message }, responseHeaders);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return { handle };
|
|
198
|
+
}
|
|
199
|
+
/** Convenience for Next.js App Router route handlers. */
|
|
200
|
+
function socialProxyRoute(partial) {
|
|
201
|
+
const proxy = createSocialProxy(partial);
|
|
202
|
+
return {
|
|
203
|
+
GET: (request) => proxy.handle(request),
|
|
204
|
+
POST: (request) => proxy.handle(request),
|
|
205
|
+
OPTIONS: (request) => proxy.handle(request),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const DEFAULT_COOKIE_NAME = "urano_social_vid";
|
|
2
|
+
export declare function parseCookies(header: string | null | undefined): Record<string, string>;
|
|
3
|
+
export declare function resolveVisitorId(opts: {
|
|
4
|
+
cookieHeader?: string | null;
|
|
5
|
+
cookieName?: string;
|
|
6
|
+
cookieSecret: string;
|
|
7
|
+
}): {
|
|
8
|
+
visitorId: string;
|
|
9
|
+
isNew: boolean;
|
|
10
|
+
setCookie?: string;
|
|
11
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_COOKIE_NAME = void 0;
|
|
4
|
+
exports.parseCookies = parseCookies;
|
|
5
|
+
exports.resolveVisitorId = resolveVisitorId;
|
|
6
|
+
const crypto_1 = require("../crypto");
|
|
7
|
+
exports.DEFAULT_COOKIE_NAME = 'urano_social_vid';
|
|
8
|
+
function parseCookies(header) {
|
|
9
|
+
const out = {};
|
|
10
|
+
if (!header)
|
|
11
|
+
return out;
|
|
12
|
+
for (const part of header.split(';')) {
|
|
13
|
+
const idx = part.indexOf('=');
|
|
14
|
+
if (idx < 0)
|
|
15
|
+
continue;
|
|
16
|
+
const key = part.slice(0, idx).trim();
|
|
17
|
+
const value = part.slice(idx + 1).trim();
|
|
18
|
+
if (key)
|
|
19
|
+
out[key] = decodeURIComponent(value);
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
function resolveVisitorId(opts) {
|
|
24
|
+
const name = opts.cookieName || exports.DEFAULT_COOKIE_NAME;
|
|
25
|
+
const cookies = parseCookies(opts.cookieHeader);
|
|
26
|
+
const existing = cookies[name];
|
|
27
|
+
if (existing) {
|
|
28
|
+
const verified = (0, crypto_1.verifyCookieValue)(existing, opts.cookieSecret);
|
|
29
|
+
if (verified)
|
|
30
|
+
return { visitorId: verified, isNew: false };
|
|
31
|
+
}
|
|
32
|
+
const visitorId = (0, crypto_1.randomId)('vis');
|
|
33
|
+
const signed = (0, crypto_1.signCookieValue)(visitorId, opts.cookieSecret);
|
|
34
|
+
const maxAge = 60 * 60 * 24 * 365;
|
|
35
|
+
const setCookie = [
|
|
36
|
+
`${name}=${encodeURIComponent(signed)}`,
|
|
37
|
+
'Path=/',
|
|
38
|
+
`Max-Age=${maxAge}`,
|
|
39
|
+
'HttpOnly',
|
|
40
|
+
'SameSite=Lax',
|
|
41
|
+
].join('; ');
|
|
42
|
+
return { visitorId, isNew: true, setCookie };
|
|
43
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as http from 'http';
|
|
2
|
+
import type { SocialProxyConfig } from '../types';
|
|
3
|
+
export interface AttachSocialWebSocketOptions extends Partial<SocialProxyConfig> {
|
|
4
|
+
/** Path on the site server that accepts WS upgrades, default `/api/urano-social/ws`. */
|
|
5
|
+
path?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Attaches an optional WebSocket bridge to a Node HTTP server.
|
|
9
|
+
* Next App Router cannot upgrade sockets in a route handler — use SSE there.
|
|
10
|
+
* Astro Node / custom servers can call this after `http.createServer`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function attachSocialWebSocket(server: http.Server, partial?: AttachSocialWebSocketOptions): () => void;
|
|
13
|
+
/** Exported for tests — builds the WS envelope the upstream expects. */
|
|
14
|
+
export declare function buildWsEnvelope(payload: Record<string, unknown>, secret: string): {
|
|
15
|
+
payload: string;
|
|
16
|
+
headers: {
|
|
17
|
+
'x-urano-signature': string;
|
|
18
|
+
'Content-Type': string;
|
|
19
|
+
'X-Urano-Protocol-Version': string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.attachSocialWebSocket = attachSocialWebSocket;
|
|
4
|
+
exports.buildWsEnvelope = buildWsEnvelope;
|
|
5
|
+
const crypto_1 = require("../crypto");
|
|
6
|
+
const visitorCookie_1 = require("./visitorCookie");
|
|
7
|
+
/**
|
|
8
|
+
* Attaches an optional WebSocket bridge to a Node HTTP server.
|
|
9
|
+
* Next App Router cannot upgrade sockets in a route handler — use SSE there.
|
|
10
|
+
* Astro Node / custom servers can call this after `http.createServer`.
|
|
11
|
+
*/
|
|
12
|
+
function attachSocialWebSocket(server, partial) {
|
|
13
|
+
const webhookUrl = (partial?.webhookUrl || process.env.URANO_SOCIAL_WEBHOOK_URL || '').replace(/\/$/, '');
|
|
14
|
+
const webhookSecret = partial?.webhookSecret || process.env.URANO_SOCIAL_WEBHOOK_SECRET || '';
|
|
15
|
+
const cookieSecret = partial?.cookieSecret || process.env.URANO_SOCIAL_COOKIE_SECRET || webhookSecret;
|
|
16
|
+
const cookieName = partial?.cookieName || visitorCookie_1.DEFAULT_COOKIE_NAME;
|
|
17
|
+
const path = partial?.path || '/api/urano-social/ws';
|
|
18
|
+
if (!webhookUrl || !webhookSecret) {
|
|
19
|
+
throw new Error('attachSocialWebSocket requires URANO_SOCIAL_WEBHOOK_URL and URANO_SOCIAL_WEBHOOK_SECRET');
|
|
20
|
+
}
|
|
21
|
+
const upstreamWs = webhookUrl.replace(/^http/, 'ws') + '/ws';
|
|
22
|
+
const onUpgrade = (req, socket, head) => {
|
|
23
|
+
const url = new URL(req.url || '/', 'http://localhost');
|
|
24
|
+
if (url.pathname !== path)
|
|
25
|
+
return;
|
|
26
|
+
const visitor = (0, visitorCookie_1.resolveVisitorId)({
|
|
27
|
+
cookieHeader: req.headers.cookie,
|
|
28
|
+
cookieName,
|
|
29
|
+
cookieSecret,
|
|
30
|
+
});
|
|
31
|
+
// Minimal RFC6455 accept handshake, then bridge frames as text JSON.
|
|
32
|
+
// Prefer the host runtime's `ws` package when available; fall back to rejecting.
|
|
33
|
+
let WebSocketImpl;
|
|
34
|
+
try {
|
|
35
|
+
WebSocketImpl = require('ws');
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
socket.write('HTTP/1.1 501 Not Implemented\r\nConnection: close\r\n\r\n');
|
|
39
|
+
socket.destroy();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const wss = new WebSocketImpl.Server({ noServer: true });
|
|
43
|
+
wss.handleUpgrade(req, socket, head, (client) => {
|
|
44
|
+
if (visitor.setCookie) {
|
|
45
|
+
// Cookie already set via prior HTTP; continue.
|
|
46
|
+
}
|
|
47
|
+
const upstream = new WebSocketImpl(upstreamWs);
|
|
48
|
+
let sessionId;
|
|
49
|
+
upstream.on('open', () => {
|
|
50
|
+
client.send(JSON.stringify({ type: 'proxy_ready', visitorId: visitor.visitorId }));
|
|
51
|
+
});
|
|
52
|
+
client.on('message', (data) => {
|
|
53
|
+
let msg;
|
|
54
|
+
try {
|
|
55
|
+
msg = JSON.parse(String(data));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
client.send(JSON.stringify({ type: 'error', error: 'invalid_json' }));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const payloadObj = {
|
|
62
|
+
visitorId: visitor.visitorId,
|
|
63
|
+
from: visitor.visitorId,
|
|
64
|
+
text: typeof msg.text === 'string' ? msg.text : undefined,
|
|
65
|
+
sessionId: typeof msg.sessionId === 'string' ? msg.sessionId : sessionId,
|
|
66
|
+
displayName: typeof msg.displayName === 'string' ? msg.displayName : undefined,
|
|
67
|
+
webhookAction: msg.webhookAction,
|
|
68
|
+
turnId: msg.turnId,
|
|
69
|
+
};
|
|
70
|
+
const payloadStr = JSON.stringify(payloadObj);
|
|
71
|
+
const signature = (0, crypto_1.signHmacHex)(payloadStr, webhookSecret);
|
|
72
|
+
upstream.send(JSON.stringify({
|
|
73
|
+
payload: payloadStr,
|
|
74
|
+
headers: {
|
|
75
|
+
'x-urano-signature': signature,
|
|
76
|
+
'Content-Type': 'application/json',
|
|
77
|
+
'X-Urano-Protocol-Version': '1',
|
|
78
|
+
...(msg.idempotencyKey ? { 'Idempotency-Key': msg.idempotencyKey } : {}),
|
|
79
|
+
},
|
|
80
|
+
}));
|
|
81
|
+
});
|
|
82
|
+
upstream.on('message', (data) => {
|
|
83
|
+
try {
|
|
84
|
+
const evt = JSON.parse(String(data));
|
|
85
|
+
if (evt?.sessionId)
|
|
86
|
+
sessionId = evt.sessionId;
|
|
87
|
+
if (evt?.type === 'session' && evt.sessionId)
|
|
88
|
+
sessionId = evt.sessionId;
|
|
89
|
+
}
|
|
90
|
+
catch { /* forward raw */ }
|
|
91
|
+
if (client.readyState === 1)
|
|
92
|
+
client.send(String(data));
|
|
93
|
+
});
|
|
94
|
+
const closeBoth = () => {
|
|
95
|
+
try {
|
|
96
|
+
client.close();
|
|
97
|
+
}
|
|
98
|
+
catch { /* ignore */ }
|
|
99
|
+
try {
|
|
100
|
+
upstream.close();
|
|
101
|
+
}
|
|
102
|
+
catch { /* ignore */ }
|
|
103
|
+
};
|
|
104
|
+
client.on('close', closeBoth);
|
|
105
|
+
upstream.on('close', closeBoth);
|
|
106
|
+
client.on('error', closeBoth);
|
|
107
|
+
upstream.on('error', (err) => {
|
|
108
|
+
try {
|
|
109
|
+
client.send(JSON.stringify({ type: 'error', error: err.message }));
|
|
110
|
+
}
|
|
111
|
+
catch { /* ignore */ }
|
|
112
|
+
closeBoth();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
server.on('upgrade', onUpgrade);
|
|
117
|
+
return () => server.off('upgrade', onUpgrade);
|
|
118
|
+
}
|
|
119
|
+
/** Exported for tests — builds the WS envelope the upstream expects. */
|
|
120
|
+
function buildWsEnvelope(payload, secret) {
|
|
121
|
+
const payloadStr = JSON.stringify(payload);
|
|
122
|
+
return {
|
|
123
|
+
payload: payloadStr,
|
|
124
|
+
headers: {
|
|
125
|
+
'x-urano-signature': (0, crypto_1.signHmacHex)(payloadStr, secret),
|
|
126
|
+
'Content-Type': 'application/json',
|
|
127
|
+
'X-Urano-Protocol-Version': '1',
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export type SocialTransport = 'sse' | 'wait' | 'ws' | 'async';
|
|
2
|
+
export type WebhookAction = 'status' | 'events' | 'cancel';
|
|
3
|
+
export interface SocialProxyConfig {
|
|
4
|
+
webhookUrl: string;
|
|
5
|
+
webhookSecret: string;
|
|
6
|
+
/** Secret used to sign the HttpOnly visitor cookie. Defaults to webhookSecret. */
|
|
7
|
+
cookieSecret?: string;
|
|
8
|
+
cookieName?: string;
|
|
9
|
+
cookieMaxAgeSec?: number;
|
|
10
|
+
/** Default display name forwarded as displayName when the client omits one. */
|
|
11
|
+
defaultDisplayName?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface SocialClientMessage {
|
|
14
|
+
id: string;
|
|
15
|
+
role: 'user' | 'assistant' | 'system';
|
|
16
|
+
content: string;
|
|
17
|
+
streaming?: boolean;
|
|
18
|
+
turnId?: string;
|
|
19
|
+
createdAt: number;
|
|
20
|
+
}
|
|
21
|
+
export interface SocialChatEvent {
|
|
22
|
+
type: string;
|
|
23
|
+
version?: number;
|
|
24
|
+
id?: string;
|
|
25
|
+
sequence?: number;
|
|
26
|
+
turnId?: string;
|
|
27
|
+
requestId?: string;
|
|
28
|
+
sessionId?: string;
|
|
29
|
+
chunk?: string;
|
|
30
|
+
response?: string;
|
|
31
|
+
agentId?: string;
|
|
32
|
+
toolsUsed?: string[];
|
|
33
|
+
error?: string;
|
|
34
|
+
status?: number;
|
|
35
|
+
[key: string]: unknown;
|
|
36
|
+
}
|
|
37
|
+
export interface SocialSendOptions {
|
|
38
|
+
text: string;
|
|
39
|
+
displayName?: string;
|
|
40
|
+
mode?: 'sse' | 'wait';
|
|
41
|
+
idempotencyKey?: string;
|
|
42
|
+
signal?: AbortSignal;
|
|
43
|
+
}
|
|
44
|
+
export interface SocialControlOptions {
|
|
45
|
+
action: WebhookAction;
|
|
46
|
+
turnId: string;
|
|
47
|
+
after?: string;
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
}
|
|
50
|
+
export interface CreateUranoChatOptions {
|
|
51
|
+
/** Site proxy endpoint, default `/api/urano-social`. */
|
|
52
|
+
endpoint?: string;
|
|
53
|
+
sessionId?: string | null;
|
|
54
|
+
displayName?: string;
|
|
55
|
+
preferredTransport?: 'auto' | 'sse' | 'wait' | 'ws';
|
|
56
|
+
onEvent?: (event: SocialChatEvent) => void;
|
|
57
|
+
onMessages?: (messages: SocialClientMessage[]) => void;
|
|
58
|
+
onError?: (error: Error) => void;
|
|
59
|
+
onStatus?: (status: {
|
|
60
|
+
connected: boolean;
|
|
61
|
+
streaming: boolean;
|
|
62
|
+
sessionId: string | null;
|
|
63
|
+
}) => void;
|
|
64
|
+
}
|
package/dist/types.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,93 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uranotools/urano-social",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Customer-support chat widget for SalesAgentConnector MCP webhooks (SSE / WebSocket / wait)",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc && node scripts/copy-assets.cjs",
|
|
9
|
+
"prepare": "npm run build",
|
|
10
|
+
"prepublishOnly": "npm run build",
|
|
11
|
+
"test": "tsx --test test/*.test.ts",
|
|
12
|
+
"typecheck": "tsc --noEmit",
|
|
13
|
+
"publish:npm": "npm publish --access public"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"urano",
|
|
17
|
+
"chat",
|
|
18
|
+
"customer-support",
|
|
19
|
+
"sse",
|
|
20
|
+
"websocket",
|
|
21
|
+
"salesagent",
|
|
22
|
+
"astro",
|
|
23
|
+
"nextjs"
|
|
24
|
+
],
|
|
25
|
+
"author": "Urano Open Source",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public",
|
|
29
|
+
"registry": "https://registry.npmjs.org/"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE",
|
|
35
|
+
".env.example"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18.0.0"
|
|
39
|
+
},
|
|
40
|
+
"exports": {
|
|
41
|
+
".": {
|
|
42
|
+
"types": "./dist/index.d.ts",
|
|
43
|
+
"import": "./dist/index.js",
|
|
44
|
+
"require": "./dist/index.js",
|
|
45
|
+
"default": "./dist/index.js"
|
|
46
|
+
},
|
|
47
|
+
"./client": {
|
|
48
|
+
"types": "./dist/client/index.d.ts",
|
|
49
|
+
"import": "./dist/client/index.js",
|
|
50
|
+
"require": "./dist/client/index.js",
|
|
51
|
+
"default": "./dist/client/index.js"
|
|
52
|
+
},
|
|
53
|
+
"./server": {
|
|
54
|
+
"types": "./dist/server/index.d.ts",
|
|
55
|
+
"import": "./dist/server/index.js",
|
|
56
|
+
"require": "./dist/server/index.js",
|
|
57
|
+
"default": "./dist/server/index.js"
|
|
58
|
+
},
|
|
59
|
+
"./react": {
|
|
60
|
+
"types": "./dist/react/index.d.ts",
|
|
61
|
+
"import": "./dist/react/index.js",
|
|
62
|
+
"require": "./dist/react/index.js",
|
|
63
|
+
"default": "./dist/react/index.js"
|
|
64
|
+
},
|
|
65
|
+
"./flavors.css": "./dist/flavors/styles.css"
|
|
66
|
+
},
|
|
67
|
+
"peerDependencies": {
|
|
68
|
+
"react": ">=18.0.0"
|
|
69
|
+
},
|
|
70
|
+
"peerDependenciesMeta": {
|
|
71
|
+
"react": {
|
|
72
|
+
"optional": true
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"optionalDependencies": {
|
|
76
|
+
"ws": "^8.18.0"
|
|
77
|
+
},
|
|
78
|
+
"devDependencies": {
|
|
79
|
+
"@types/node": "^20.11.0",
|
|
80
|
+
"@types/react": "^18.3.12",
|
|
81
|
+
"react": "^18.3.1",
|
|
82
|
+
"tsx": "^4.19.2",
|
|
83
|
+
"typescript": "^5.3.3"
|
|
84
|
+
},
|
|
85
|
+
"repository": {
|
|
86
|
+
"type": "git",
|
|
87
|
+
"url": "git+https://github.com/uranotools/urano-social.git"
|
|
88
|
+
},
|
|
89
|
+
"bugs": {
|
|
90
|
+
"url": "https://github.com/uranotools/urano-social/issues"
|
|
91
|
+
},
|
|
92
|
+
"homepage": "https://github.com/uranotools/urano-social#readme"
|
|
93
|
+
}
|