@fstacksoftware/chatwoot-embed 1.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/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # @fstacksoftware/chatwoot-embed
2
+
3
+ Chatwoot widget cho **Shopify embedded app** (Remix/React):
4
+
5
+ - Giữ phiên chat khi app chạy trong iframe Shopify admin (cookie bị chặn → shim qua localStorage)
6
+ - Identity validation (HMAC) tự động
7
+ - Lưu phiên vào contact attribute `init_session` → **mọi thiết bị của 1 shop dùng chung 1 cuộc chat**
8
+
9
+ ## Cài đặt
10
+
11
+ ```bash
12
+ npm i @fstacksoftware/chatwoot-embed
13
+ ```
14
+
15
+ ## Chuẩn bị phía Chatwoot (1 lần)
16
+
17
+ 1. Inbox (Website) → **Configuration → Identity Validation** → copy HMAC token
18
+ 2. Inbox → Settings: bật _Allow messages after conversation resolved_, tắt _Allow users to end conversation_
19
+ 3. Tạo agent riêng (vd `API Bot`) → Profile → copy **Access Token**
20
+ 4. Token phiên không hết hạn:
21
+ ```bash
22
+ docker compose exec rails bundle exec rails runner \
23
+ 'c=InstallationConfig.find_or_initialize_by(name:"WIDGET_TOKEN_EXPIRY"); c.value="36500"; c.locked=false; c.save!'
24
+ ```
25
+
26
+ ## ENV
27
+
28
+ ```env
29
+ CHATWOOT_BASE_URL=https://we.fstack.asia
30
+ CHATWOOT_WEBSITE_TOKEN=xxx
31
+ CHATWOOT_ACCOUNT_ID=1
32
+ CHATWOOT_API_TOKEN=xxx
33
+ CHATWOOT_HMAC_TOKEN=xxx
34
+ ```
35
+
36
+ ## Sử dụng (Remix + Shopify)
37
+
38
+ ### 1. Server instance — `app/chatwoot.server.ts`
39
+
40
+ ```ts
41
+ import { createChatwootServer } from "@fstacksoftware/chatwoot-embed";
42
+
43
+ export const chatwoot = createChatwootServer({
44
+ baseUrl: process.env.CHATWOOT_BASE_URL!,
45
+ websiteToken: process.env.CHATWOOT_WEBSITE_TOKEN!,
46
+ apiToken: process.env.CHATWOOT_API_TOKEN!,
47
+ accountId: process.env.CHATWOOT_ACCOUNT_ID,
48
+ hmacToken: process.env.CHATWOOT_HMAC_TOKEN,
49
+ });
50
+ ```
51
+
52
+ ### 2. Loader — `app/root.tsx`
53
+
54
+ ```ts
55
+ import { chatwoot } from "./chatwoot.server";
56
+
57
+ export async function loader({ request }: LoaderFunctionArgs) {
58
+ const { session } = await authenticate.admin(request);
59
+ return {
60
+ // ...
61
+ chatwootBoot: await chatwoot.getBootData(session.shop),
62
+ };
63
+ }
64
+ ```
65
+
66
+ ### 3. Route lưu phiên — `app/routes/api.chatwoot-session.ts`
67
+
68
+ ```ts
69
+ import type { ActionFunctionArgs } from "@remix-run/node";
70
+ import { authenticate } from "../shopify.server";
71
+ import { chatwoot } from "../chatwoot.server";
72
+
73
+ export async function action({ request }: ActionFunctionArgs) {
74
+ const { session } = await authenticate.admin(request);
75
+ return chatwoot.handleSessionRequest(request, session.shop);
76
+ }
77
+ ```
78
+
79
+ ### 4. Client
80
+
81
+ ```tsx
82
+ import { useChatwoot, openChat } from "@fstacksoftware/chatwoot-embed";
83
+
84
+ function App() {
85
+ const { chatwootBoot } = useLoaderData<typeof loader>();
86
+ const shop = useShop(); // dữ liệu shop của app bạn
87
+
88
+ useChatwoot({
89
+ boot: chatwootBoot,
90
+ user: { name: shop?.name, email: shop?.email },
91
+ attributes: { app: "Kivoly", shopify_plan: shop?.planDisplayName },
92
+ disabled:
93
+ typeof window !== "undefined" &&
94
+ localStorage.getItem("supportAgent") === "true",
95
+ });
96
+
97
+ return <button onClick={openChat}>Chat with us</button>;
98
+ }
99
+ ```
100
+
101
+ Hoặc dạng component: `<ChatwootWidget boot={chatwootBoot} user={...} />`
102
+
103
+ ## API — tất cả import từ `@fstacksoftware/chatwoot-embed`
104
+
105
+ **Server** (loader/action):
106
+
107
+ | Hàm | Mô tả |
108
+ | ----------------------------------------------------- | ------------------------------------------------------------------------------- |
109
+ | `createChatwootServer(config)` | Tạo instance |
110
+ | `.getBootData(identifier)` | `{ baseUrl, websiteToken, identifier, identifierHash, initSession }` cho loader |
111
+ | `.handleSessionRequest(request, identifier)` | Xử lý POST lưu `init_session` |
112
+ | `await .identifierHash(identifier)` | HMAC SHA256 (Web Crypto) |
113
+ | `.getInitSession(id)` / `.saveInitSession(id, token)` | Đọc/ghi thủ công |
114
+
115
+ **React**: `useChatwoot({ boot, user?, attributes?, sessionEndpoint?, settings?, disabled? })`, `<ChatwootWidget />`
116
+
117
+ **Client**: `openChat`, `closeChat`, `toggleChat`, `setChatVisible`, `setChatAttributes`, `resetChatwoot`, `getChatwoot`, `loadChatwoot`, `onChatwootReady`, `identifyChatwoot`
118
+
119
+ > Chỉ gọi `createChatwootServer(...)` trong file `*.server.ts` để `apiToken`/`hmacToken` không bao giờ vào bundle client.
120
+
121
+ ## Bảo mật
122
+
123
+ - `apiToken`, `hmacToken` chỉ dùng ở server (`/server` entry).
124
+ - `init_session` chỉ được ghi khi token thuộc đúng contact của shop đã `authenticate.admin`.
125
+ - Nên bật **Enforce user identity validation** sau khi chạy ổn.
package/dist/index.cjs ADDED
@@ -0,0 +1,340 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ChatwootWidget: () => ChatwootWidget,
24
+ closeChat: () => closeChat,
25
+ createChatwootServer: () => createChatwootServer,
26
+ getChatwoot: () => getChatwoot,
27
+ identifyChatwoot: () => identifyChatwoot,
28
+ installCookieShim: () => installCookieShim,
29
+ isInIframe: () => isInIframe,
30
+ loadChatwoot: () => loadChatwoot,
31
+ onChatwootReady: () => onChatwootReady,
32
+ openChat: () => openChat,
33
+ resetChatwoot: () => resetChatwoot,
34
+ setChatAttributes: () => setChatAttributes,
35
+ setChatVisible: () => setChatVisible,
36
+ toggleChat: () => toggleChat,
37
+ useChatwoot: () => useChatwoot
38
+ });
39
+ module.exports = __toCommonJS(index_exports);
40
+
41
+ // src/client.ts
42
+ var STORE_PREFIX = "cwcookie:";
43
+ var CW_COOKIE_RE = /^\s*(cw_conversation|cw_user_[A-Za-z0-9]+)=([^;]*)/;
44
+ var shimInstalled = false;
45
+ var tokenListener;
46
+ var isBrowser = () => typeof window !== "undefined" && typeof document !== "undefined";
47
+ var storage = {
48
+ get(k) {
49
+ try {
50
+ return localStorage.getItem(k);
51
+ } catch {
52
+ return null;
53
+ }
54
+ },
55
+ set(k, v) {
56
+ try {
57
+ localStorage.setItem(k, v);
58
+ } catch {
59
+ }
60
+ },
61
+ remove(k) {
62
+ try {
63
+ localStorage.removeItem(k);
64
+ } catch {
65
+ }
66
+ },
67
+ keys(prefix) {
68
+ const out = [];
69
+ try {
70
+ for (let i = 0; i < localStorage.length; i++) {
71
+ const k = localStorage.key(i);
72
+ if (k?.startsWith(prefix)) out.push(k.slice(prefix.length));
73
+ }
74
+ } catch {
75
+ }
76
+ return out;
77
+ }
78
+ };
79
+ var isInIframe = () => {
80
+ if (!isBrowser()) return false;
81
+ try {
82
+ return window.self !== window.top;
83
+ } catch {
84
+ return true;
85
+ }
86
+ };
87
+ var installCookieShim = () => {
88
+ if (!isBrowser() || shimInstalled || !isInIframe()) return;
89
+ const native = Object.getOwnPropertyDescriptor(Document.prototype, "cookie");
90
+ if (!native?.get || !native?.set) return;
91
+ const nGet = native.get;
92
+ const nSet = native.set;
93
+ Object.defineProperty(document, "cookie", {
94
+ configurable: true,
95
+ get() {
96
+ let value = nGet.call(document);
97
+ for (const name of storage.keys(STORE_PREFIX)) {
98
+ if (new RegExp(`(^|;\\s*)${name}=`).test(value)) continue;
99
+ const v = storage.get(STORE_PREFIX + name);
100
+ if (v) value = value ? `${value}; ${name}=${v}` : `${name}=${v}`;
101
+ }
102
+ return value;
103
+ },
104
+ set(raw) {
105
+ const m = CW_COOKIE_RE.exec(raw);
106
+ if (m) {
107
+ const [, name, val] = m;
108
+ const expired = !val || /expires=Thu, 01 Jan 1970/i.test(raw) || /max-age=(0|-)/i.test(raw);
109
+ if (expired) {
110
+ storage.remove(STORE_PREFIX + name);
111
+ } else {
112
+ const changed = storage.get(STORE_PREFIX + name) !== val;
113
+ storage.set(STORE_PREFIX + name, val);
114
+ if (changed && name === "cw_conversation") tokenListener?.(val);
115
+ }
116
+ }
117
+ nSet.call(document, raw);
118
+ }
119
+ });
120
+ shimInstalled = true;
121
+ };
122
+ var seedSession = (token) => {
123
+ if (isInIframe()) storage.set(`${STORE_PREFIX}cw_conversation`, token);
124
+ else document.cookie = `cw_conversation=${token}; path=/; max-age=31536000; SameSite=Lax`;
125
+ };
126
+ var getChatwoot = () => isBrowser() ? window.$chatwoot : void 0;
127
+ var loadChatwoot = (opts) => {
128
+ if (!isBrowser() || !opts.baseUrl || !opts.websiteToken) return;
129
+ if (window.chatwootSDK || document.getElementById("chatwoot-sdk")) return;
130
+ const baseUrl = opts.baseUrl.replace(/\/$/, "");
131
+ tokenListener = (token) => {
132
+ opts.onSessionToken?.(token);
133
+ if (opts.sessionEndpoint) {
134
+ fetch(opts.sessionEndpoint, {
135
+ method: "POST",
136
+ headers: { "Content-Type": "application/json" },
137
+ body: JSON.stringify({ token })
138
+ }).catch(() => {
139
+ });
140
+ }
141
+ };
142
+ installCookieShim();
143
+ if (opts.sessionToken) seedSession(opts.sessionToken);
144
+ window.chatwootSettings = { position: "right", type: "standard", ...opts.settings };
145
+ const script = document.createElement("script");
146
+ script.id = "chatwoot-sdk";
147
+ script.src = `${baseUrl}/packs/js/sdk.js`;
148
+ script.async = true;
149
+ script.onload = () => window.chatwootSDK?.run({ websiteToken: opts.websiteToken, baseUrl });
150
+ document.body.appendChild(script);
151
+ };
152
+ var onChatwootReady = (callback) => {
153
+ if (!isBrowser()) return () => {
154
+ };
155
+ if (window.$chatwoot?.hasLoaded) {
156
+ callback();
157
+ return () => {
158
+ };
159
+ }
160
+ window.addEventListener("chatwoot:ready", callback);
161
+ return () => window.removeEventListener("chatwoot:ready", callback);
162
+ };
163
+ var identifyChatwoot = (identifier, user, attributes) => {
164
+ const cw = getChatwoot();
165
+ if (!cw || !identifier) return;
166
+ cw.setUser(identifier, user);
167
+ if (attributes && Object.keys(attributes).length) cw.setCustomAttributes(attributes);
168
+ };
169
+ var openChat = () => getChatwoot()?.toggle("open");
170
+ var closeChat = () => getChatwoot()?.toggle("close");
171
+ var toggleChat = () => {
172
+ const cw = getChatwoot();
173
+ if (cw) cw.toggle(cw.isOpen ? "close" : "open");
174
+ };
175
+ var setChatVisible = (visible) => getChatwoot()?.toggleBubbleVisibility(visible ? "show" : "hide");
176
+ var setChatAttributes = (attrs) => getChatwoot()?.setCustomAttributes(attrs);
177
+ var resetChatwoot = () => getChatwoot()?.reset();
178
+
179
+ // src/react.ts
180
+ var import_react = require("react");
181
+ function useChatwoot({
182
+ boot,
183
+ user,
184
+ attributes,
185
+ sessionEndpoint = "/api/chatwoot-session",
186
+ settings,
187
+ disabled
188
+ }) {
189
+ const loaded = (0, import_react.useRef)(false);
190
+ (0, import_react.useEffect)(() => {
191
+ if (disabled || !boot || loaded.current) return;
192
+ loaded.current = true;
193
+ loadChatwoot({
194
+ baseUrl: boot.baseUrl,
195
+ websiteToken: boot.websiteToken,
196
+ sessionToken: boot.initSession,
197
+ sessionEndpoint: sessionEndpoint || void 0,
198
+ settings
199
+ });
200
+ }, [disabled, boot?.baseUrl, boot?.websiteToken]);
201
+ const userKey = JSON.stringify(user ?? {});
202
+ const attrKey = JSON.stringify(attributes ?? {});
203
+ (0, import_react.useEffect)(() => {
204
+ if (disabled || !boot?.identifier) return;
205
+ return onChatwootReady(
206
+ () => identifyChatwoot(boot.identifier, { ...user, identifier_hash: boot.identifierHash || void 0 }, attributes)
207
+ );
208
+ }, [disabled, boot?.identifier, boot?.identifierHash, userKey, attrKey]);
209
+ }
210
+ function ChatwootWidget(props) {
211
+ useChatwoot(props);
212
+ return null;
213
+ }
214
+
215
+ // src/server.ts
216
+ var decodeJwt = (token) => {
217
+ try {
218
+ const b64 = token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/");
219
+ const json = decodeURIComponent(
220
+ atob(b64.padEnd(b64.length + (4 - b64.length % 4) % 4, "=")).split("").map((c) => "%" + c.charCodeAt(0).toString(16).padStart(2, "0")).join("")
221
+ );
222
+ return JSON.parse(json);
223
+ } catch {
224
+ return null;
225
+ }
226
+ };
227
+ var hmacSha256Hex = async (key, message) => {
228
+ const enc = new TextEncoder();
229
+ const k = await globalThis.crypto.subtle.importKey("raw", enc.encode(key), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
230
+ const sig = await globalThis.crypto.subtle.sign("HMAC", k, enc.encode(message));
231
+ return Array.from(new Uint8Array(sig), (b) => b.toString(16).padStart(2, "0")).join("");
232
+ };
233
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
234
+ function createChatwootServer(config) {
235
+ const base = config.baseUrl.replace(/\/$/, "");
236
+ const account = config.accountId ?? 1;
237
+ const attr = config.sessionAttribute ?? "init_session";
238
+ const timeout = config.timeoutMs ?? 5e3;
239
+ const log = config.logger ?? console;
240
+ const ttl = config.cacheTtlMs ?? 10 * 6e4;
241
+ const cache = /* @__PURE__ */ new Map();
242
+ const cacheSet = (id, token) => {
243
+ if (!ttl) return;
244
+ if (cache.size > 5e3) cache.clear();
245
+ cache.set(id, { token, until: Date.now() + ttl });
246
+ };
247
+ const api = async (path, init = {}) => {
248
+ try {
249
+ const res = await fetch(`${base}/api/v1/accounts/${account}${path}`, {
250
+ ...init,
251
+ headers: { "Content-Type": "application/json", api_access_token: config.apiToken, ...init.headers ?? {} },
252
+ signal: AbortSignal.timeout(timeout)
253
+ });
254
+ if (!res.ok) {
255
+ log.error(`[chatwoot] ${init.method ?? "GET"} ${path} -> ${res.status}`);
256
+ return null;
257
+ }
258
+ return await res.json();
259
+ } catch (e) {
260
+ log.error(`[chatwoot] ${path}: ${e.message}`);
261
+ return null;
262
+ }
263
+ };
264
+ const identifierHash = async (identifier) => config.hmacToken && identifier ? hmacSha256Hex(config.hmacToken, identifier) : "";
265
+ const isUsableToken = (token) => {
266
+ const p = decodeJwt(token);
267
+ return !!p?.source_id && (!p.exp || p.exp * 1e3 > Date.now() + 864e5);
268
+ };
269
+ const findContact = async (identifier) => {
270
+ const data = await api(`/contacts/search?q=${encodeURIComponent(identifier)}`);
271
+ return data?.payload?.find((c) => c.identifier === identifier) ?? null;
272
+ };
273
+ const getInitSession = async (identifier) => {
274
+ const hit = cache.get(identifier);
275
+ if (hit && hit.until > Date.now() && (!hit.token || isUsableToken(hit.token))) return hit.token;
276
+ const raw = (await findContact(identifier))?.custom_attributes?.[attr];
277
+ const token = typeof raw === "string" && isUsableToken(raw) ? raw : null;
278
+ if (token) cacheSet(identifier, token);
279
+ return token;
280
+ };
281
+ const saveInitSession = async (identifier, token) => {
282
+ const payload = decodeJwt(token);
283
+ if (!payload?.source_id) return { ok: false, reason: "invalid_token" };
284
+ let contact = null;
285
+ for (let i = 0; i < 6 && !contact; i++) {
286
+ if (i) await sleep(1e3);
287
+ contact = await findContact(identifier);
288
+ }
289
+ if (!contact) return { ok: false, reason: "contact_not_found" };
290
+ const full = await api(`/contacts/${contact.id}`);
291
+ const sources = full?.payload?.contact_inboxes?.map((ci) => ci.source_id);
292
+ if (sources && !sources.includes(payload.source_id)) return { ok: false, reason: "token_not_owned" };
293
+ if (contact.custom_attributes?.[attr] === token) {
294
+ cacheSet(identifier, token);
295
+ return { ok: true, reason: "unchanged" };
296
+ }
297
+ const updated = await api(`/contacts/${contact.id}`, {
298
+ method: "PUT",
299
+ body: JSON.stringify({ custom_attributes: { ...contact.custom_attributes ?? {}, [attr]: token } })
300
+ });
301
+ if (updated) cacheSet(identifier, token);
302
+ return updated ? { ok: true, reason: "saved" } : { ok: false, reason: "update_failed" };
303
+ };
304
+ const getBootData = async (identifier) => ({
305
+ baseUrl: base,
306
+ websiteToken: config.websiteToken,
307
+ identifier,
308
+ identifierHash: await identifierHash(identifier),
309
+ initSession: await getInitSession(identifier)
310
+ });
311
+ const handleSessionRequest = async (request, identifier) => {
312
+ const body = await request.json().catch(() => ({}));
313
+ if (typeof body.token !== "string" || body.token.split(".").length !== 3) {
314
+ return Response.json({ ok: false, reason: "invalid_token" }, { status: 400 });
315
+ }
316
+ const result = await saveInitSession(identifier, body.token);
317
+ return Response.json(result, { status: result.ok ? 200 : 422 });
318
+ };
319
+ const clearCache = (identifier) => identifier ? cache.delete(identifier) : cache.clear();
320
+ return { identifierHash, findContact, clearCache, getInitSession, saveInitSession, getBootData, handleSessionRequest };
321
+ }
322
+ // Annotate the CommonJS export names for ESM import in node:
323
+ 0 && (module.exports = {
324
+ ChatwootWidget,
325
+ closeChat,
326
+ createChatwootServer,
327
+ getChatwoot,
328
+ identifyChatwoot,
329
+ installCookieShim,
330
+ isInIframe,
331
+ loadChatwoot,
332
+ onChatwootReady,
333
+ openChat,
334
+ resetChatwoot,
335
+ setChatAttributes,
336
+ setChatVisible,
337
+ toggleChat,
338
+ useChatwoot
339
+ });
340
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/react.ts","../src/server.ts"],"sourcesContent":["// 1 entry duy nhất: import { ... } from '@fstacksoftware/chatwoot-embed'\nexport * from \"./client\";\nexport * from \"./react\";\nexport * from \"./server\";\nexport type * from \"./types\";\n","// Client: chạy trên trình duyệt\nimport type { ChatwootInstance, ChatwootSettings, ChatwootUser } from './types';\n\n\nconst STORE_PREFIX = 'cwcookie:';\nconst CW_COOKIE_RE = /^\\s*(cw_conversation|cw_user_[A-Za-z0-9]+)=([^;]*)/;\n\nlet shimInstalled = false;\nlet tokenListener: ((token: string) => void) | undefined;\n\nconst isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined';\n\nconst storage = {\n\tget(k: string) {\n\t\ttry { return localStorage.getItem(k); } catch { return null; }\n\t},\n\tset(k: string, v: string) {\n\t\ttry { localStorage.setItem(k, v); } catch { /* blocked */ }\n\t},\n\tremove(k: string) {\n\t\ttry { localStorage.removeItem(k); } catch { /* blocked */ }\n\t},\n\tkeys(prefix: string) {\n\t\tconst out: string[] = [];\n\t\ttry {\n\t\t\tfor (let i = 0; i < localStorage.length; i++) {\n\t\t\t\tconst k = localStorage.key(i);\n\t\t\t\tif (k?.startsWith(prefix)) out.push(k.slice(prefix.length));\n\t\t\t}\n\t\t} catch { /* blocked */ }\n\t\treturn out;\n\t},\n};\n\nexport const isInIframe = () => {\n\tif (!isBrowser()) return false;\n\ttry {\n\t\treturn window.self !== window.top;\n\t} catch {\n\t\treturn true;\n\t}\n};\n\n/**\n * Trong iframe khác site (Shopify admin), cookie SameSite=Lax của SDK bị chặn -> mỗi lần F5 là phiên mới.\n * Shim document.cookie cho key cw_*: ghi -> localStorage, đọc -> từ localStorage.\n */\nexport const installCookieShim = () => {\n\tif (!isBrowser() || shimInstalled || !isInIframe()) return;\n\tconst native = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie');\n\tif (!native?.get || !native?.set) return;\n\tconst nGet = native.get;\n\tconst nSet = native.set;\n\n\tObject.defineProperty(document, 'cookie', {\n\t\tconfigurable: true,\n\t\tget() {\n\t\t\tlet value: string = nGet.call(document);\n\t\t\tfor (const name of storage.keys(STORE_PREFIX)) {\n\t\t\t\tif (new RegExp(`(^|;\\\\s*)${name}=`).test(value)) continue;\n\t\t\t\tconst v = storage.get(STORE_PREFIX + name);\n\t\t\t\tif (v) value = value ? `${value}; ${name}=${v}` : `${name}=${v}`;\n\t\t\t}\n\t\t\treturn value;\n\t\t},\n\t\tset(raw: string) {\n\t\t\tconst m = CW_COOKIE_RE.exec(raw);\n\t\t\tif (m) {\n\t\t\t\tconst [, name, val] = m;\n\t\t\t\tconst expired = !val || /expires=Thu, 01 Jan 1970/i.test(raw) || /max-age=(0|-)/i.test(raw);\n\t\t\t\tif (expired) {\n\t\t\t\t\tstorage.remove(STORE_PREFIX + name);\n\t\t\t\t} else {\n\t\t\t\t\tconst changed = storage.get(STORE_PREFIX + name) !== val;\n\t\t\t\t\tstorage.set(STORE_PREFIX + name, val);\n\t\t\t\t\tif (changed && name === 'cw_conversation') tokenListener?.(val);\n\t\t\t\t}\n\t\t\t}\n\t\t\tnSet.call(document, raw);\n\t\t},\n\t});\n\tshimInstalled = true;\n};\n\nconst seedSession = (token: string) => {\n\tif (isInIframe()) storage.set(`${STORE_PREFIX}cw_conversation`, token);\n\telse document.cookie = `cw_conversation=${token}; path=/; max-age=31536000; SameSite=Lax`;\n};\n\nexport type LoadChatwootOptions = {\n\tbaseUrl: string;\n\twebsiteToken: string;\n\t/** Token phiên đã lưu (init_session) -> mở lại đúng phiên/cuộc chat cũ */\n\tsessionToken?: string | null;\n\t/** Gọi khi Chatwoot cấp token phiên mới */\n\tonSessionToken?: (token: string) => void;\n\t/** Nếu set, tự POST { token } tới endpoint này khi có token mới (vd: /api/chatwoot-session) */\n\tsessionEndpoint?: string;\n\tsettings?: ChatwootSettings;\n};\n\nexport const getChatwoot = (): ChatwootInstance | undefined => (isBrowser() ? window.$chatwoot : undefined);\n\nexport const loadChatwoot = (opts: LoadChatwootOptions) => {\n\tif (!isBrowser() || !opts.baseUrl || !opts.websiteToken) return;\n\tif (window.chatwootSDK || document.getElementById('chatwoot-sdk')) return;\n\n\tconst baseUrl = opts.baseUrl.replace(/\\/$/, '');\n\ttokenListener = (token) => {\n\t\topts.onSessionToken?.(token);\n\t\tif (opts.sessionEndpoint) {\n\t\t\tfetch(opts.sessionEndpoint, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\t\tbody: JSON.stringify({ token }),\n\t\t\t}).catch(() => {});\n\t\t}\n\t};\n\n\tinstallCookieShim(); // phải trước khi inject sdk.js\n\tif (opts.sessionToken) seedSession(opts.sessionToken);\n\n\twindow.chatwootSettings = { position: 'right', type: 'standard', ...opts.settings };\n\tconst script = document.createElement('script');\n\tscript.id = 'chatwoot-sdk';\n\tscript.src = `${baseUrl}/packs/js/sdk.js`;\n\tscript.async = true;\n\tscript.onload = () => window.chatwootSDK?.run({ websiteToken: opts.websiteToken, baseUrl });\n\tdocument.body.appendChild(script);\n};\n\n/** Chạy callback khi widget sẵn sàng. Trả về hàm huỷ đăng ký. */\nexport const onChatwootReady = (callback: () => void) => {\n\tif (!isBrowser()) return () => {};\n\tif (window.$chatwoot?.hasLoaded) {\n\t\tcallback();\n\t\treturn () => {};\n\t}\n\twindow.addEventListener('chatwoot:ready', callback);\n\treturn () => window.removeEventListener('chatwoot:ready', callback);\n};\n\nexport const identifyChatwoot = (\n\tidentifier: string,\n\tuser: ChatwootUser,\n\tattributes?: Record<string, unknown>\n) => {\n\tconst cw = getChatwoot();\n\tif (!cw || !identifier) return;\n\tcw.setUser(identifier, user);\n\tif (attributes && Object.keys(attributes).length) cw.setCustomAttributes(attributes);\n};\n\nexport const openChat = () => getChatwoot()?.toggle('open');\nexport const closeChat = () => getChatwoot()?.toggle('close');\nexport const toggleChat = () => {\n\tconst cw = getChatwoot();\n\tif (cw) cw.toggle(cw.isOpen ? 'close' : 'open');\n};\nexport const setChatVisible = (visible: boolean) => getChatwoot()?.toggleBubbleVisibility(visible ? 'show' : 'hide');\nexport const setChatAttributes = (attrs: Record<string, unknown>) => getChatwoot()?.setCustomAttributes(attrs);\nexport const resetChatwoot = () => getChatwoot()?.reset();\n","import { useEffect, useRef } from 'react';\nimport { identifyChatwoot, loadChatwoot, onChatwootReady } from './client';\nimport type { ChatwootBootData, ChatwootSettings, ChatwootUser } from './types';\n\n\n\nexport type UseChatwootOptions = {\n\t/** Từ server: createChatwootServer(...).getBootData(shop) */\n\tboot: ChatwootBootData | null | undefined;\n\tuser?: Omit<ChatwootUser, 'identifier_hash'>;\n\tattributes?: Record<string, unknown>;\n\t/** Endpoint lưu init_session. Mặc định /api/chatwoot-session. false = tắt */\n\tsessionEndpoint?: string | false;\n\tsettings?: ChatwootSettings;\n\t/** true = không load widget (vd: support agent) */\n\tdisabled?: boolean;\n};\n\nexport function useChatwoot({\n\tboot,\n\tuser,\n\tattributes,\n\tsessionEndpoint = '/api/chatwoot-session',\n\tsettings,\n\tdisabled,\n}: UseChatwootOptions) {\n\tconst loaded = useRef(false);\n\n\tuseEffect(() => {\n\t\tif (disabled || !boot || loaded.current) return;\n\t\tloaded.current = true;\n\t\tloadChatwoot({\n\t\t\tbaseUrl: boot.baseUrl,\n\t\t\twebsiteToken: boot.websiteToken,\n\t\t\tsessionToken: boot.initSession,\n\t\t\tsessionEndpoint: sessionEndpoint || undefined,\n\t\t\tsettings,\n\t\t});\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, [disabled, boot?.baseUrl, boot?.websiteToken]);\n\n\tconst userKey = JSON.stringify(user ?? {});\n\tconst attrKey = JSON.stringify(attributes ?? {});\n\n\tuseEffect(() => {\n\t\tif (disabled || !boot?.identifier) return;\n\t\treturn onChatwootReady(() =>\n\t\t\tidentifyChatwoot(boot.identifier, { ...user, identifier_hash: boot.identifierHash || undefined }, attributes)\n\t\t);\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, [disabled, boot?.identifier, boot?.identifierHash, userKey, attrKey]);\n}\n\n/** Component tiện dụng: <ChatwootWidget boot={...} user={...} /> */\nexport function ChatwootWidget(props: UseChatwootOptions) {\n\tuseChatwoot(props);\n\treturn null;\n}\n","// Server-side helpers (gọi trong loader/action). Dùng Web Crypto -> không phụ thuộc node:crypto,\n// nên package chỉ cần 1 entry. apiToken/hmacToken chỉ được truyền ở code server (.server.ts).\nimport type { ChatwootBootData } from './types';\n\nexport type ChatwootServerConfig = {\n\tbaseUrl: string;\n\twebsiteToken: string;\n\t/** Access Token của agent dùng cho API (nên tạo agent riêng) */\n\tapiToken: string;\n\taccountId?: number | string;\n\t/** Inbox → Configuration → Identity Validation */\n\thmacToken?: string;\n\t/** Tên custom attribute lưu phiên. Mặc định: init_session */\n\tsessionAttribute?: string;\n\ttimeoutMs?: number;\n\t/** Cache init_session trong RAM theo identifier. Mặc định 10 phút. 0 = tắt */\n\tcacheTtlMs?: number;\n\tlogger?: Pick<Console, 'error'>;\n};\n\ntype CwContact = {\n\tid: number;\n\tidentifier: string | null;\n\tcustom_attributes?: Record<string, unknown>;\n\tcontact_inboxes?: { source_id: string }[];\n};\n\ntype JwtPayload = { source_id?: string; inbox_id?: number; exp?: number };\n\nexport type SaveResult = {\n\tok: boolean;\n\treason: 'saved' | 'unchanged' | 'invalid_token' | 'contact_not_found' | 'token_not_owned' | 'update_failed';\n};\n\nconst decodeJwt = (token: string): JwtPayload | null => {\n\ttry {\n\t\tconst b64 = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');\n\t\tconst json = decodeURIComponent(\n\t\t\tatob(b64.padEnd(b64.length + ((4 - (b64.length % 4)) % 4), '='))\n\t\t\t\t.split('')\n\t\t\t\t.map((c) => '%' + c.charCodeAt(0).toString(16).padStart(2, '0'))\n\t\t\t\t.join('')\n\t\t);\n\t\treturn JSON.parse(json);\n\t} catch {\n\t\treturn null;\n\t}\n};\n\nconst hmacSha256Hex = async (key: string, message: string) => {\n\tconst enc = new TextEncoder();\n\tconst k = await globalThis.crypto.subtle.importKey('raw', enc.encode(key), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);\n\tconst sig = await globalThis.crypto.subtle.sign('HMAC', k, enc.encode(message));\n\treturn Array.from(new Uint8Array(sig), (b) => b.toString(16).padStart(2, '0')).join('');\n};\n\nconst sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));\n\nexport function createChatwootServer(config: ChatwootServerConfig) {\n\tconst base = config.baseUrl.replace(/\\/$/, '');\n\tconst account = config.accountId ?? 1;\n\tconst attr = config.sessionAttribute ?? 'init_session';\n\tconst timeout = config.timeoutMs ?? 5000;\n\tconst log = config.logger ?? console;\n\tconst ttl = config.cacheTtlMs ?? 10 * 60_000;\n\tconst cache = new Map<string, { token: string | null; until: number }>();\n\tconst cacheSet = (id: string, token: string | null) => {\n\t\tif (!ttl) return;\n\t\tif (cache.size > 5000) cache.clear(); // chặn phình RAM\n\t\tcache.set(id, { token, until: Date.now() + ttl });\n\t};\n\n\tconst api = async <T>(path: string, init: RequestInit = {}): Promise<T | null> => {\n\t\ttry {\n\t\t\tconst res = await fetch(`${base}/api/v1/accounts/${account}${path}`, {\n\t\t\t\t...init,\n\t\t\t\theaders: { 'Content-Type': 'application/json', api_access_token: config.apiToken, ...(init.headers ?? {}) },\n\t\t\t\tsignal: AbortSignal.timeout(timeout),\n\t\t\t});\n\t\t\tif (!res.ok) {\n\t\t\t\tlog.error(`[chatwoot] ${init.method ?? 'GET'} ${path} -> ${res.status}`);\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\treturn (await res.json()) as T;\n\t\t} catch (e) {\n\t\t\tlog.error(`[chatwoot] ${path}: ${(e as Error).message}`);\n\t\t\treturn null;\n\t\t}\n\t};\n\n\tconst identifierHash = async (identifier: string) =>\n\t\tconfig.hmacToken && identifier ? hmacSha256Hex(config.hmacToken, identifier) : '';\n\n\tconst isUsableToken = (token: string) => {\n\t\tconst p = decodeJwt(token);\n\t\treturn !!p?.source_id && (!p.exp || p.exp * 1000 > Date.now() + 86_400_000);\n\t};\n\n\tconst findContact = async (identifier: string) => {\n\t\tconst data = await api<{ payload: CwContact[] }>(`/contacts/search?q=${encodeURIComponent(identifier)}`);\n\t\treturn data?.payload?.find((c) => c.identifier === identifier) ?? null;\n\t};\n\n\tconst getInitSession = async (identifier: string) => {\n\t\tconst hit = cache.get(identifier);\n\t\tif (hit && hit.until > Date.now() && (!hit.token || isUsableToken(hit.token))) return hit.token;\n\n\t\tconst raw = (await findContact(identifier))?.custom_attributes?.[attr];\n\t\tconst token = typeof raw === 'string' && isUsableToken(raw) ? raw : null;\n\t\tif (token) cacheSet(identifier, token); // không cache null: shop mới sẽ có token ngay sau lần chat đầu\n\t\treturn token;\n\t};\n\n\tconst saveInitSession = async (identifier: string, token: string): Promise<SaveResult> => {\n\t\tconst payload = decodeJwt(token);\n\t\tif (!payload?.source_id) return { ok: false, reason: 'invalid_token' };\n\n\t\tlet contact: CwContact | null = null;\n\t\tfor (let i = 0; i < 6 && !contact; i++) {\n\t\t\tif (i) await sleep(1000);\n\t\t\tcontact = await findContact(identifier);\n\t\t}\n\t\tif (!contact) return { ok: false, reason: 'contact_not_found' };\n\n\t\tconst full = await api<{ payload: CwContact }>(`/contacts/${contact.id}`);\n\t\tconst sources = full?.payload?.contact_inboxes?.map((ci) => ci.source_id);\n\t\tif (sources && !sources.includes(payload.source_id)) return { ok: false, reason: 'token_not_owned' };\n\t\tif (contact.custom_attributes?.[attr] === token) {\n\t\t\tcacheSet(identifier, token);\n\t\t\treturn { ok: true, reason: 'unchanged' };\n\t\t}\n\n\t\tconst updated = await api(`/contacts/${contact.id}`, {\n\t\t\tmethod: 'PUT',\n\t\t\tbody: JSON.stringify({ custom_attributes: { ...(contact.custom_attributes ?? {}), [attr]: token } }),\n\t\t});\n\t\tif (updated) cacheSet(identifier, token);\n\t\treturn updated ? { ok: true, reason: 'saved' } : { ok: false, reason: 'update_failed' };\n\t};\n\n\t/** Gọi trong loader, trả về cho client */\n\tconst getBootData = async (identifier: string): Promise<ChatwootBootData> => ({\n\t\tbaseUrl: base,\n\t\twebsiteToken: config.websiteToken,\n\t\tidentifier,\n\t\tidentifierHash: await identifierHash(identifier),\n\t\tinitSession: await getInitSession(identifier),\n\t});\n\n\t/** Dùng trong route action: return chatwoot.handleSessionRequest(request, session.shop) */\n\tconst handleSessionRequest = async (request: Request, identifier: string) => {\n\t\tconst body = (await request.json().catch(() => ({}))) as { token?: unknown };\n\t\tif (typeof body.token !== 'string' || body.token.split('.').length !== 3) {\n\t\t\treturn Response.json({ ok: false, reason: 'invalid_token' }, { status: 400 });\n\t\t}\n\t\tconst result = await saveInitSession(identifier, body.token);\n\t\treturn Response.json(result, { status: result.ok ? 200 : 422 });\n\t};\n\n\tconst clearCache = (identifier?: string) => (identifier ? cache.delete(identifier) : cache.clear());\n\n\treturn { identifierHash, findContact, clearCache, getInitSession, saveInitSession, getBootData, handleSessionRequest };\n}\n\nexport type ChatwootServer = ReturnType<typeof createChatwootServer>;\n"],"mappings":"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","names":[]}
@@ -0,0 +1,133 @@
1
+ type ChatwootUser = {
2
+ name?: string;
3
+ email?: string;
4
+ phone_number?: string;
5
+ avatar_url?: string;
6
+ identifier_hash?: string;
7
+ };
8
+ type ChatwootSettings = {
9
+ position?: 'left' | 'right';
10
+ type?: 'standard' | 'expanded_bubble';
11
+ locale?: string;
12
+ hideMessageBubble?: boolean;
13
+ launcherTitle?: string;
14
+ darkMode?: 'light' | 'auto';
15
+ [key: string]: unknown;
16
+ };
17
+ type ChatwootInstance = {
18
+ hasLoaded?: boolean;
19
+ isOpen?: boolean;
20
+ toggle: (state?: 'open' | 'close') => void;
21
+ toggleBubbleVisibility: (state: 'show' | 'hide') => void;
22
+ setUser: (identifier: string, user: ChatwootUser) => void;
23
+ setCustomAttributes: (attrs: Record<string, unknown>) => void;
24
+ deleteCustomAttribute: (key: string) => void;
25
+ setLocale: (locale: string) => void;
26
+ setLabel: (label: string) => void;
27
+ removeLabel: (label: string) => void;
28
+ reset: () => void;
29
+ };
30
+ /** Dữ liệu server trả cho client (qua loader) */
31
+ type ChatwootBootData = {
32
+ baseUrl: string;
33
+ websiteToken: string;
34
+ identifier: string;
35
+ identifierHash: string;
36
+ initSession: string | null;
37
+ };
38
+ declare global {
39
+ interface Window {
40
+ chatwootSettings?: ChatwootSettings;
41
+ chatwootSDK?: {
42
+ run: (cfg: {
43
+ websiteToken: string;
44
+ baseUrl: string;
45
+ }) => void;
46
+ };
47
+ $chatwoot?: ChatwootInstance;
48
+ }
49
+ }
50
+
51
+ declare const isInIframe: () => boolean;
52
+ /**
53
+ * Trong iframe khác site (Shopify admin), cookie SameSite=Lax của SDK bị chặn -> mỗi lần F5 là phiên mới.
54
+ * Shim document.cookie cho key cw_*: ghi -> localStorage, đọc -> từ localStorage.
55
+ */
56
+ declare const installCookieShim: () => void;
57
+ type LoadChatwootOptions = {
58
+ baseUrl: string;
59
+ websiteToken: string;
60
+ /** Token phiên đã lưu (init_session) -> mở lại đúng phiên/cuộc chat cũ */
61
+ sessionToken?: string | null;
62
+ /** Gọi khi Chatwoot cấp token phiên mới */
63
+ onSessionToken?: (token: string) => void;
64
+ /** Nếu set, tự POST { token } tới endpoint này khi có token mới (vd: /api/chatwoot-session) */
65
+ sessionEndpoint?: string;
66
+ settings?: ChatwootSettings;
67
+ };
68
+ declare const getChatwoot: () => ChatwootInstance | undefined;
69
+ declare const loadChatwoot: (opts: LoadChatwootOptions) => void;
70
+ /** Chạy callback khi widget sẵn sàng. Trả về hàm huỷ đăng ký. */
71
+ declare const onChatwootReady: (callback: () => void) => () => void;
72
+ declare const identifyChatwoot: (identifier: string, user: ChatwootUser, attributes?: Record<string, unknown>) => void;
73
+ declare const openChat: () => void | undefined;
74
+ declare const closeChat: () => void | undefined;
75
+ declare const toggleChat: () => void;
76
+ declare const setChatVisible: (visible: boolean) => void | undefined;
77
+ declare const setChatAttributes: (attrs: Record<string, unknown>) => void | undefined;
78
+ declare const resetChatwoot: () => void | undefined;
79
+
80
+ type UseChatwootOptions = {
81
+ /** Từ server: createChatwootServer(...).getBootData(shop) */
82
+ boot: ChatwootBootData | null | undefined;
83
+ user?: Omit<ChatwootUser, 'identifier_hash'>;
84
+ attributes?: Record<string, unknown>;
85
+ /** Endpoint lưu init_session. Mặc định /api/chatwoot-session. false = tắt */
86
+ sessionEndpoint?: string | false;
87
+ settings?: ChatwootSettings;
88
+ /** true = không load widget (vd: support agent) */
89
+ disabled?: boolean;
90
+ };
91
+ declare function useChatwoot({ boot, user, attributes, sessionEndpoint, settings, disabled, }: UseChatwootOptions): void;
92
+ /** Component tiện dụng: <ChatwootWidget boot={...} user={...} /> */
93
+ declare function ChatwootWidget(props: UseChatwootOptions): null;
94
+
95
+ type ChatwootServerConfig = {
96
+ baseUrl: string;
97
+ websiteToken: string;
98
+ /** Access Token của agent dùng cho API (nên tạo agent riêng) */
99
+ apiToken: string;
100
+ accountId?: number | string;
101
+ /** Inbox → Configuration → Identity Validation */
102
+ hmacToken?: string;
103
+ /** Tên custom attribute lưu phiên. Mặc định: init_session */
104
+ sessionAttribute?: string;
105
+ timeoutMs?: number;
106
+ /** Cache init_session trong RAM theo identifier. Mặc định 10 phút. 0 = tắt */
107
+ cacheTtlMs?: number;
108
+ logger?: Pick<Console, 'error'>;
109
+ };
110
+ type CwContact = {
111
+ id: number;
112
+ identifier: string | null;
113
+ custom_attributes?: Record<string, unknown>;
114
+ contact_inboxes?: {
115
+ source_id: string;
116
+ }[];
117
+ };
118
+ type SaveResult = {
119
+ ok: boolean;
120
+ reason: 'saved' | 'unchanged' | 'invalid_token' | 'contact_not_found' | 'token_not_owned' | 'update_failed';
121
+ };
122
+ declare function createChatwootServer(config: ChatwootServerConfig): {
123
+ identifierHash: (identifier: string) => Promise<string>;
124
+ findContact: (identifier: string) => Promise<CwContact | null>;
125
+ clearCache: (identifier?: string) => boolean | void;
126
+ getInitSession: (identifier: string) => Promise<string | null>;
127
+ saveInitSession: (identifier: string, token: string) => Promise<SaveResult>;
128
+ getBootData: (identifier: string) => Promise<ChatwootBootData>;
129
+ handleSessionRequest: (request: Request, identifier: string) => Promise<Response>;
130
+ };
131
+ type ChatwootServer = ReturnType<typeof createChatwootServer>;
132
+
133
+ export { type ChatwootBootData, type ChatwootInstance, type ChatwootServer, type ChatwootServerConfig, type ChatwootSettings, type ChatwootUser, ChatwootWidget, type LoadChatwootOptions, type SaveResult, type UseChatwootOptions, closeChat, createChatwootServer, getChatwoot, identifyChatwoot, installCookieShim, isInIframe, loadChatwoot, onChatwootReady, openChat, resetChatwoot, setChatAttributes, setChatVisible, toggleChat, useChatwoot };
@@ -0,0 +1,133 @@
1
+ type ChatwootUser = {
2
+ name?: string;
3
+ email?: string;
4
+ phone_number?: string;
5
+ avatar_url?: string;
6
+ identifier_hash?: string;
7
+ };
8
+ type ChatwootSettings = {
9
+ position?: 'left' | 'right';
10
+ type?: 'standard' | 'expanded_bubble';
11
+ locale?: string;
12
+ hideMessageBubble?: boolean;
13
+ launcherTitle?: string;
14
+ darkMode?: 'light' | 'auto';
15
+ [key: string]: unknown;
16
+ };
17
+ type ChatwootInstance = {
18
+ hasLoaded?: boolean;
19
+ isOpen?: boolean;
20
+ toggle: (state?: 'open' | 'close') => void;
21
+ toggleBubbleVisibility: (state: 'show' | 'hide') => void;
22
+ setUser: (identifier: string, user: ChatwootUser) => void;
23
+ setCustomAttributes: (attrs: Record<string, unknown>) => void;
24
+ deleteCustomAttribute: (key: string) => void;
25
+ setLocale: (locale: string) => void;
26
+ setLabel: (label: string) => void;
27
+ removeLabel: (label: string) => void;
28
+ reset: () => void;
29
+ };
30
+ /** Dữ liệu server trả cho client (qua loader) */
31
+ type ChatwootBootData = {
32
+ baseUrl: string;
33
+ websiteToken: string;
34
+ identifier: string;
35
+ identifierHash: string;
36
+ initSession: string | null;
37
+ };
38
+ declare global {
39
+ interface Window {
40
+ chatwootSettings?: ChatwootSettings;
41
+ chatwootSDK?: {
42
+ run: (cfg: {
43
+ websiteToken: string;
44
+ baseUrl: string;
45
+ }) => void;
46
+ };
47
+ $chatwoot?: ChatwootInstance;
48
+ }
49
+ }
50
+
51
+ declare const isInIframe: () => boolean;
52
+ /**
53
+ * Trong iframe khác site (Shopify admin), cookie SameSite=Lax của SDK bị chặn -> mỗi lần F5 là phiên mới.
54
+ * Shim document.cookie cho key cw_*: ghi -> localStorage, đọc -> từ localStorage.
55
+ */
56
+ declare const installCookieShim: () => void;
57
+ type LoadChatwootOptions = {
58
+ baseUrl: string;
59
+ websiteToken: string;
60
+ /** Token phiên đã lưu (init_session) -> mở lại đúng phiên/cuộc chat cũ */
61
+ sessionToken?: string | null;
62
+ /** Gọi khi Chatwoot cấp token phiên mới */
63
+ onSessionToken?: (token: string) => void;
64
+ /** Nếu set, tự POST { token } tới endpoint này khi có token mới (vd: /api/chatwoot-session) */
65
+ sessionEndpoint?: string;
66
+ settings?: ChatwootSettings;
67
+ };
68
+ declare const getChatwoot: () => ChatwootInstance | undefined;
69
+ declare const loadChatwoot: (opts: LoadChatwootOptions) => void;
70
+ /** Chạy callback khi widget sẵn sàng. Trả về hàm huỷ đăng ký. */
71
+ declare const onChatwootReady: (callback: () => void) => () => void;
72
+ declare const identifyChatwoot: (identifier: string, user: ChatwootUser, attributes?: Record<string, unknown>) => void;
73
+ declare const openChat: () => void | undefined;
74
+ declare const closeChat: () => void | undefined;
75
+ declare const toggleChat: () => void;
76
+ declare const setChatVisible: (visible: boolean) => void | undefined;
77
+ declare const setChatAttributes: (attrs: Record<string, unknown>) => void | undefined;
78
+ declare const resetChatwoot: () => void | undefined;
79
+
80
+ type UseChatwootOptions = {
81
+ /** Từ server: createChatwootServer(...).getBootData(shop) */
82
+ boot: ChatwootBootData | null | undefined;
83
+ user?: Omit<ChatwootUser, 'identifier_hash'>;
84
+ attributes?: Record<string, unknown>;
85
+ /** Endpoint lưu init_session. Mặc định /api/chatwoot-session. false = tắt */
86
+ sessionEndpoint?: string | false;
87
+ settings?: ChatwootSettings;
88
+ /** true = không load widget (vd: support agent) */
89
+ disabled?: boolean;
90
+ };
91
+ declare function useChatwoot({ boot, user, attributes, sessionEndpoint, settings, disabled, }: UseChatwootOptions): void;
92
+ /** Component tiện dụng: <ChatwootWidget boot={...} user={...} /> */
93
+ declare function ChatwootWidget(props: UseChatwootOptions): null;
94
+
95
+ type ChatwootServerConfig = {
96
+ baseUrl: string;
97
+ websiteToken: string;
98
+ /** Access Token của agent dùng cho API (nên tạo agent riêng) */
99
+ apiToken: string;
100
+ accountId?: number | string;
101
+ /** Inbox → Configuration → Identity Validation */
102
+ hmacToken?: string;
103
+ /** Tên custom attribute lưu phiên. Mặc định: init_session */
104
+ sessionAttribute?: string;
105
+ timeoutMs?: number;
106
+ /** Cache init_session trong RAM theo identifier. Mặc định 10 phút. 0 = tắt */
107
+ cacheTtlMs?: number;
108
+ logger?: Pick<Console, 'error'>;
109
+ };
110
+ type CwContact = {
111
+ id: number;
112
+ identifier: string | null;
113
+ custom_attributes?: Record<string, unknown>;
114
+ contact_inboxes?: {
115
+ source_id: string;
116
+ }[];
117
+ };
118
+ type SaveResult = {
119
+ ok: boolean;
120
+ reason: 'saved' | 'unchanged' | 'invalid_token' | 'contact_not_found' | 'token_not_owned' | 'update_failed';
121
+ };
122
+ declare function createChatwootServer(config: ChatwootServerConfig): {
123
+ identifierHash: (identifier: string) => Promise<string>;
124
+ findContact: (identifier: string) => Promise<CwContact | null>;
125
+ clearCache: (identifier?: string) => boolean | void;
126
+ getInitSession: (identifier: string) => Promise<string | null>;
127
+ saveInitSession: (identifier: string, token: string) => Promise<SaveResult>;
128
+ getBootData: (identifier: string) => Promise<ChatwootBootData>;
129
+ handleSessionRequest: (request: Request, identifier: string) => Promise<Response>;
130
+ };
131
+ type ChatwootServer = ReturnType<typeof createChatwootServer>;
132
+
133
+ export { type ChatwootBootData, type ChatwootInstance, type ChatwootServer, type ChatwootServerConfig, type ChatwootSettings, type ChatwootUser, ChatwootWidget, type LoadChatwootOptions, type SaveResult, type UseChatwootOptions, closeChat, createChatwootServer, getChatwoot, identifyChatwoot, installCookieShim, isInIframe, loadChatwoot, onChatwootReady, openChat, resetChatwoot, setChatAttributes, setChatVisible, toggleChat, useChatwoot };
package/dist/index.js ADDED
@@ -0,0 +1,299 @@
1
+ // src/client.ts
2
+ var STORE_PREFIX = "cwcookie:";
3
+ var CW_COOKIE_RE = /^\s*(cw_conversation|cw_user_[A-Za-z0-9]+)=([^;]*)/;
4
+ var shimInstalled = false;
5
+ var tokenListener;
6
+ var isBrowser = () => typeof window !== "undefined" && typeof document !== "undefined";
7
+ var storage = {
8
+ get(k) {
9
+ try {
10
+ return localStorage.getItem(k);
11
+ } catch {
12
+ return null;
13
+ }
14
+ },
15
+ set(k, v) {
16
+ try {
17
+ localStorage.setItem(k, v);
18
+ } catch {
19
+ }
20
+ },
21
+ remove(k) {
22
+ try {
23
+ localStorage.removeItem(k);
24
+ } catch {
25
+ }
26
+ },
27
+ keys(prefix) {
28
+ const out = [];
29
+ try {
30
+ for (let i = 0; i < localStorage.length; i++) {
31
+ const k = localStorage.key(i);
32
+ if (k?.startsWith(prefix)) out.push(k.slice(prefix.length));
33
+ }
34
+ } catch {
35
+ }
36
+ return out;
37
+ }
38
+ };
39
+ var isInIframe = () => {
40
+ if (!isBrowser()) return false;
41
+ try {
42
+ return window.self !== window.top;
43
+ } catch {
44
+ return true;
45
+ }
46
+ };
47
+ var installCookieShim = () => {
48
+ if (!isBrowser() || shimInstalled || !isInIframe()) return;
49
+ const native = Object.getOwnPropertyDescriptor(Document.prototype, "cookie");
50
+ if (!native?.get || !native?.set) return;
51
+ const nGet = native.get;
52
+ const nSet = native.set;
53
+ Object.defineProperty(document, "cookie", {
54
+ configurable: true,
55
+ get() {
56
+ let value = nGet.call(document);
57
+ for (const name of storage.keys(STORE_PREFIX)) {
58
+ if (new RegExp(`(^|;\\s*)${name}=`).test(value)) continue;
59
+ const v = storage.get(STORE_PREFIX + name);
60
+ if (v) value = value ? `${value}; ${name}=${v}` : `${name}=${v}`;
61
+ }
62
+ return value;
63
+ },
64
+ set(raw) {
65
+ const m = CW_COOKIE_RE.exec(raw);
66
+ if (m) {
67
+ const [, name, val] = m;
68
+ const expired = !val || /expires=Thu, 01 Jan 1970/i.test(raw) || /max-age=(0|-)/i.test(raw);
69
+ if (expired) {
70
+ storage.remove(STORE_PREFIX + name);
71
+ } else {
72
+ const changed = storage.get(STORE_PREFIX + name) !== val;
73
+ storage.set(STORE_PREFIX + name, val);
74
+ if (changed && name === "cw_conversation") tokenListener?.(val);
75
+ }
76
+ }
77
+ nSet.call(document, raw);
78
+ }
79
+ });
80
+ shimInstalled = true;
81
+ };
82
+ var seedSession = (token) => {
83
+ if (isInIframe()) storage.set(`${STORE_PREFIX}cw_conversation`, token);
84
+ else document.cookie = `cw_conversation=${token}; path=/; max-age=31536000; SameSite=Lax`;
85
+ };
86
+ var getChatwoot = () => isBrowser() ? window.$chatwoot : void 0;
87
+ var loadChatwoot = (opts) => {
88
+ if (!isBrowser() || !opts.baseUrl || !opts.websiteToken) return;
89
+ if (window.chatwootSDK || document.getElementById("chatwoot-sdk")) return;
90
+ const baseUrl = opts.baseUrl.replace(/\/$/, "");
91
+ tokenListener = (token) => {
92
+ opts.onSessionToken?.(token);
93
+ if (opts.sessionEndpoint) {
94
+ fetch(opts.sessionEndpoint, {
95
+ method: "POST",
96
+ headers: { "Content-Type": "application/json" },
97
+ body: JSON.stringify({ token })
98
+ }).catch(() => {
99
+ });
100
+ }
101
+ };
102
+ installCookieShim();
103
+ if (opts.sessionToken) seedSession(opts.sessionToken);
104
+ window.chatwootSettings = { position: "right", type: "standard", ...opts.settings };
105
+ const script = document.createElement("script");
106
+ script.id = "chatwoot-sdk";
107
+ script.src = `${baseUrl}/packs/js/sdk.js`;
108
+ script.async = true;
109
+ script.onload = () => window.chatwootSDK?.run({ websiteToken: opts.websiteToken, baseUrl });
110
+ document.body.appendChild(script);
111
+ };
112
+ var onChatwootReady = (callback) => {
113
+ if (!isBrowser()) return () => {
114
+ };
115
+ if (window.$chatwoot?.hasLoaded) {
116
+ callback();
117
+ return () => {
118
+ };
119
+ }
120
+ window.addEventListener("chatwoot:ready", callback);
121
+ return () => window.removeEventListener("chatwoot:ready", callback);
122
+ };
123
+ var identifyChatwoot = (identifier, user, attributes) => {
124
+ const cw = getChatwoot();
125
+ if (!cw || !identifier) return;
126
+ cw.setUser(identifier, user);
127
+ if (attributes && Object.keys(attributes).length) cw.setCustomAttributes(attributes);
128
+ };
129
+ var openChat = () => getChatwoot()?.toggle("open");
130
+ var closeChat = () => getChatwoot()?.toggle("close");
131
+ var toggleChat = () => {
132
+ const cw = getChatwoot();
133
+ if (cw) cw.toggle(cw.isOpen ? "close" : "open");
134
+ };
135
+ var setChatVisible = (visible) => getChatwoot()?.toggleBubbleVisibility(visible ? "show" : "hide");
136
+ var setChatAttributes = (attrs) => getChatwoot()?.setCustomAttributes(attrs);
137
+ var resetChatwoot = () => getChatwoot()?.reset();
138
+
139
+ // src/react.ts
140
+ import { useEffect, useRef } from "react";
141
+ function useChatwoot({
142
+ boot,
143
+ user,
144
+ attributes,
145
+ sessionEndpoint = "/api/chatwoot-session",
146
+ settings,
147
+ disabled
148
+ }) {
149
+ const loaded = useRef(false);
150
+ useEffect(() => {
151
+ if (disabled || !boot || loaded.current) return;
152
+ loaded.current = true;
153
+ loadChatwoot({
154
+ baseUrl: boot.baseUrl,
155
+ websiteToken: boot.websiteToken,
156
+ sessionToken: boot.initSession,
157
+ sessionEndpoint: sessionEndpoint || void 0,
158
+ settings
159
+ });
160
+ }, [disabled, boot?.baseUrl, boot?.websiteToken]);
161
+ const userKey = JSON.stringify(user ?? {});
162
+ const attrKey = JSON.stringify(attributes ?? {});
163
+ useEffect(() => {
164
+ if (disabled || !boot?.identifier) return;
165
+ return onChatwootReady(
166
+ () => identifyChatwoot(boot.identifier, { ...user, identifier_hash: boot.identifierHash || void 0 }, attributes)
167
+ );
168
+ }, [disabled, boot?.identifier, boot?.identifierHash, userKey, attrKey]);
169
+ }
170
+ function ChatwootWidget(props) {
171
+ useChatwoot(props);
172
+ return null;
173
+ }
174
+
175
+ // src/server.ts
176
+ var decodeJwt = (token) => {
177
+ try {
178
+ const b64 = token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/");
179
+ const json = decodeURIComponent(
180
+ atob(b64.padEnd(b64.length + (4 - b64.length % 4) % 4, "=")).split("").map((c) => "%" + c.charCodeAt(0).toString(16).padStart(2, "0")).join("")
181
+ );
182
+ return JSON.parse(json);
183
+ } catch {
184
+ return null;
185
+ }
186
+ };
187
+ var hmacSha256Hex = async (key, message) => {
188
+ const enc = new TextEncoder();
189
+ const k = await globalThis.crypto.subtle.importKey("raw", enc.encode(key), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
190
+ const sig = await globalThis.crypto.subtle.sign("HMAC", k, enc.encode(message));
191
+ return Array.from(new Uint8Array(sig), (b) => b.toString(16).padStart(2, "0")).join("");
192
+ };
193
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
194
+ function createChatwootServer(config) {
195
+ const base = config.baseUrl.replace(/\/$/, "");
196
+ const account = config.accountId ?? 1;
197
+ const attr = config.sessionAttribute ?? "init_session";
198
+ const timeout = config.timeoutMs ?? 5e3;
199
+ const log = config.logger ?? console;
200
+ const ttl = config.cacheTtlMs ?? 10 * 6e4;
201
+ const cache = /* @__PURE__ */ new Map();
202
+ const cacheSet = (id, token) => {
203
+ if (!ttl) return;
204
+ if (cache.size > 5e3) cache.clear();
205
+ cache.set(id, { token, until: Date.now() + ttl });
206
+ };
207
+ const api = async (path, init = {}) => {
208
+ try {
209
+ const res = await fetch(`${base}/api/v1/accounts/${account}${path}`, {
210
+ ...init,
211
+ headers: { "Content-Type": "application/json", api_access_token: config.apiToken, ...init.headers ?? {} },
212
+ signal: AbortSignal.timeout(timeout)
213
+ });
214
+ if (!res.ok) {
215
+ log.error(`[chatwoot] ${init.method ?? "GET"} ${path} -> ${res.status}`);
216
+ return null;
217
+ }
218
+ return await res.json();
219
+ } catch (e) {
220
+ log.error(`[chatwoot] ${path}: ${e.message}`);
221
+ return null;
222
+ }
223
+ };
224
+ const identifierHash = async (identifier) => config.hmacToken && identifier ? hmacSha256Hex(config.hmacToken, identifier) : "";
225
+ const isUsableToken = (token) => {
226
+ const p = decodeJwt(token);
227
+ return !!p?.source_id && (!p.exp || p.exp * 1e3 > Date.now() + 864e5);
228
+ };
229
+ const findContact = async (identifier) => {
230
+ const data = await api(`/contacts/search?q=${encodeURIComponent(identifier)}`);
231
+ return data?.payload?.find((c) => c.identifier === identifier) ?? null;
232
+ };
233
+ const getInitSession = async (identifier) => {
234
+ const hit = cache.get(identifier);
235
+ if (hit && hit.until > Date.now() && (!hit.token || isUsableToken(hit.token))) return hit.token;
236
+ const raw = (await findContact(identifier))?.custom_attributes?.[attr];
237
+ const token = typeof raw === "string" && isUsableToken(raw) ? raw : null;
238
+ if (token) cacheSet(identifier, token);
239
+ return token;
240
+ };
241
+ const saveInitSession = async (identifier, token) => {
242
+ const payload = decodeJwt(token);
243
+ if (!payload?.source_id) return { ok: false, reason: "invalid_token" };
244
+ let contact = null;
245
+ for (let i = 0; i < 6 && !contact; i++) {
246
+ if (i) await sleep(1e3);
247
+ contact = await findContact(identifier);
248
+ }
249
+ if (!contact) return { ok: false, reason: "contact_not_found" };
250
+ const full = await api(`/contacts/${contact.id}`);
251
+ const sources = full?.payload?.contact_inboxes?.map((ci) => ci.source_id);
252
+ if (sources && !sources.includes(payload.source_id)) return { ok: false, reason: "token_not_owned" };
253
+ if (contact.custom_attributes?.[attr] === token) {
254
+ cacheSet(identifier, token);
255
+ return { ok: true, reason: "unchanged" };
256
+ }
257
+ const updated = await api(`/contacts/${contact.id}`, {
258
+ method: "PUT",
259
+ body: JSON.stringify({ custom_attributes: { ...contact.custom_attributes ?? {}, [attr]: token } })
260
+ });
261
+ if (updated) cacheSet(identifier, token);
262
+ return updated ? { ok: true, reason: "saved" } : { ok: false, reason: "update_failed" };
263
+ };
264
+ const getBootData = async (identifier) => ({
265
+ baseUrl: base,
266
+ websiteToken: config.websiteToken,
267
+ identifier,
268
+ identifierHash: await identifierHash(identifier),
269
+ initSession: await getInitSession(identifier)
270
+ });
271
+ const handleSessionRequest = async (request, identifier) => {
272
+ const body = await request.json().catch(() => ({}));
273
+ if (typeof body.token !== "string" || body.token.split(".").length !== 3) {
274
+ return Response.json({ ok: false, reason: "invalid_token" }, { status: 400 });
275
+ }
276
+ const result = await saveInitSession(identifier, body.token);
277
+ return Response.json(result, { status: result.ok ? 200 : 422 });
278
+ };
279
+ const clearCache = (identifier) => identifier ? cache.delete(identifier) : cache.clear();
280
+ return { identifierHash, findContact, clearCache, getInitSession, saveInitSession, getBootData, handleSessionRequest };
281
+ }
282
+ export {
283
+ ChatwootWidget,
284
+ closeChat,
285
+ createChatwootServer,
286
+ getChatwoot,
287
+ identifyChatwoot,
288
+ installCookieShim,
289
+ isInIframe,
290
+ loadChatwoot,
291
+ onChatwootReady,
292
+ openChat,
293
+ resetChatwoot,
294
+ setChatAttributes,
295
+ setChatVisible,
296
+ toggleChat,
297
+ useChatwoot
298
+ };
299
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts","../src/react.ts","../src/server.ts"],"sourcesContent":["// Client: chạy trên trình duyệt\nimport type { ChatwootInstance, ChatwootSettings, ChatwootUser } from './types';\n\n\nconst STORE_PREFIX = 'cwcookie:';\nconst CW_COOKIE_RE = /^\\s*(cw_conversation|cw_user_[A-Za-z0-9]+)=([^;]*)/;\n\nlet shimInstalled = false;\nlet tokenListener: ((token: string) => void) | undefined;\n\nconst isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined';\n\nconst storage = {\n\tget(k: string) {\n\t\ttry { return localStorage.getItem(k); } catch { return null; }\n\t},\n\tset(k: string, v: string) {\n\t\ttry { localStorage.setItem(k, v); } catch { /* blocked */ }\n\t},\n\tremove(k: string) {\n\t\ttry { localStorage.removeItem(k); } catch { /* blocked */ }\n\t},\n\tkeys(prefix: string) {\n\t\tconst out: string[] = [];\n\t\ttry {\n\t\t\tfor (let i = 0; i < localStorage.length; i++) {\n\t\t\t\tconst k = localStorage.key(i);\n\t\t\t\tif (k?.startsWith(prefix)) out.push(k.slice(prefix.length));\n\t\t\t}\n\t\t} catch { /* blocked */ }\n\t\treturn out;\n\t},\n};\n\nexport const isInIframe = () => {\n\tif (!isBrowser()) return false;\n\ttry {\n\t\treturn window.self !== window.top;\n\t} catch {\n\t\treturn true;\n\t}\n};\n\n/**\n * Trong iframe khác site (Shopify admin), cookie SameSite=Lax của SDK bị chặn -> mỗi lần F5 là phiên mới.\n * Shim document.cookie cho key cw_*: ghi -> localStorage, đọc -> từ localStorage.\n */\nexport const installCookieShim = () => {\n\tif (!isBrowser() || shimInstalled || !isInIframe()) return;\n\tconst native = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie');\n\tif (!native?.get || !native?.set) return;\n\tconst nGet = native.get;\n\tconst nSet = native.set;\n\n\tObject.defineProperty(document, 'cookie', {\n\t\tconfigurable: true,\n\t\tget() {\n\t\t\tlet value: string = nGet.call(document);\n\t\t\tfor (const name of storage.keys(STORE_PREFIX)) {\n\t\t\t\tif (new RegExp(`(^|;\\\\s*)${name}=`).test(value)) continue;\n\t\t\t\tconst v = storage.get(STORE_PREFIX + name);\n\t\t\t\tif (v) value = value ? `${value}; ${name}=${v}` : `${name}=${v}`;\n\t\t\t}\n\t\t\treturn value;\n\t\t},\n\t\tset(raw: string) {\n\t\t\tconst m = CW_COOKIE_RE.exec(raw);\n\t\t\tif (m) {\n\t\t\t\tconst [, name, val] = m;\n\t\t\t\tconst expired = !val || /expires=Thu, 01 Jan 1970/i.test(raw) || /max-age=(0|-)/i.test(raw);\n\t\t\t\tif (expired) {\n\t\t\t\t\tstorage.remove(STORE_PREFIX + name);\n\t\t\t\t} else {\n\t\t\t\t\tconst changed = storage.get(STORE_PREFIX + name) !== val;\n\t\t\t\t\tstorage.set(STORE_PREFIX + name, val);\n\t\t\t\t\tif (changed && name === 'cw_conversation') tokenListener?.(val);\n\t\t\t\t}\n\t\t\t}\n\t\t\tnSet.call(document, raw);\n\t\t},\n\t});\n\tshimInstalled = true;\n};\n\nconst seedSession = (token: string) => {\n\tif (isInIframe()) storage.set(`${STORE_PREFIX}cw_conversation`, token);\n\telse document.cookie = `cw_conversation=${token}; path=/; max-age=31536000; SameSite=Lax`;\n};\n\nexport type LoadChatwootOptions = {\n\tbaseUrl: string;\n\twebsiteToken: string;\n\t/** Token phiên đã lưu (init_session) -> mở lại đúng phiên/cuộc chat cũ */\n\tsessionToken?: string | null;\n\t/** Gọi khi Chatwoot cấp token phiên mới */\n\tonSessionToken?: (token: string) => void;\n\t/** Nếu set, tự POST { token } tới endpoint này khi có token mới (vd: /api/chatwoot-session) */\n\tsessionEndpoint?: string;\n\tsettings?: ChatwootSettings;\n};\n\nexport const getChatwoot = (): ChatwootInstance | undefined => (isBrowser() ? window.$chatwoot : undefined);\n\nexport const loadChatwoot = (opts: LoadChatwootOptions) => {\n\tif (!isBrowser() || !opts.baseUrl || !opts.websiteToken) return;\n\tif (window.chatwootSDK || document.getElementById('chatwoot-sdk')) return;\n\n\tconst baseUrl = opts.baseUrl.replace(/\\/$/, '');\n\ttokenListener = (token) => {\n\t\topts.onSessionToken?.(token);\n\t\tif (opts.sessionEndpoint) {\n\t\t\tfetch(opts.sessionEndpoint, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\t\tbody: JSON.stringify({ token }),\n\t\t\t}).catch(() => {});\n\t\t}\n\t};\n\n\tinstallCookieShim(); // phải trước khi inject sdk.js\n\tif (opts.sessionToken) seedSession(opts.sessionToken);\n\n\twindow.chatwootSettings = { position: 'right', type: 'standard', ...opts.settings };\n\tconst script = document.createElement('script');\n\tscript.id = 'chatwoot-sdk';\n\tscript.src = `${baseUrl}/packs/js/sdk.js`;\n\tscript.async = true;\n\tscript.onload = () => window.chatwootSDK?.run({ websiteToken: opts.websiteToken, baseUrl });\n\tdocument.body.appendChild(script);\n};\n\n/** Chạy callback khi widget sẵn sàng. Trả về hàm huỷ đăng ký. */\nexport const onChatwootReady = (callback: () => void) => {\n\tif (!isBrowser()) return () => {};\n\tif (window.$chatwoot?.hasLoaded) {\n\t\tcallback();\n\t\treturn () => {};\n\t}\n\twindow.addEventListener('chatwoot:ready', callback);\n\treturn () => window.removeEventListener('chatwoot:ready', callback);\n};\n\nexport const identifyChatwoot = (\n\tidentifier: string,\n\tuser: ChatwootUser,\n\tattributes?: Record<string, unknown>\n) => {\n\tconst cw = getChatwoot();\n\tif (!cw || !identifier) return;\n\tcw.setUser(identifier, user);\n\tif (attributes && Object.keys(attributes).length) cw.setCustomAttributes(attributes);\n};\n\nexport const openChat = () => getChatwoot()?.toggle('open');\nexport const closeChat = () => getChatwoot()?.toggle('close');\nexport const toggleChat = () => {\n\tconst cw = getChatwoot();\n\tif (cw) cw.toggle(cw.isOpen ? 'close' : 'open');\n};\nexport const setChatVisible = (visible: boolean) => getChatwoot()?.toggleBubbleVisibility(visible ? 'show' : 'hide');\nexport const setChatAttributes = (attrs: Record<string, unknown>) => getChatwoot()?.setCustomAttributes(attrs);\nexport const resetChatwoot = () => getChatwoot()?.reset();\n","import { useEffect, useRef } from 'react';\nimport { identifyChatwoot, loadChatwoot, onChatwootReady } from './client';\nimport type { ChatwootBootData, ChatwootSettings, ChatwootUser } from './types';\n\n\n\nexport type UseChatwootOptions = {\n\t/** Từ server: createChatwootServer(...).getBootData(shop) */\n\tboot: ChatwootBootData | null | undefined;\n\tuser?: Omit<ChatwootUser, 'identifier_hash'>;\n\tattributes?: Record<string, unknown>;\n\t/** Endpoint lưu init_session. Mặc định /api/chatwoot-session. false = tắt */\n\tsessionEndpoint?: string | false;\n\tsettings?: ChatwootSettings;\n\t/** true = không load widget (vd: support agent) */\n\tdisabled?: boolean;\n};\n\nexport function useChatwoot({\n\tboot,\n\tuser,\n\tattributes,\n\tsessionEndpoint = '/api/chatwoot-session',\n\tsettings,\n\tdisabled,\n}: UseChatwootOptions) {\n\tconst loaded = useRef(false);\n\n\tuseEffect(() => {\n\t\tif (disabled || !boot || loaded.current) return;\n\t\tloaded.current = true;\n\t\tloadChatwoot({\n\t\t\tbaseUrl: boot.baseUrl,\n\t\t\twebsiteToken: boot.websiteToken,\n\t\t\tsessionToken: boot.initSession,\n\t\t\tsessionEndpoint: sessionEndpoint || undefined,\n\t\t\tsettings,\n\t\t});\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, [disabled, boot?.baseUrl, boot?.websiteToken]);\n\n\tconst userKey = JSON.stringify(user ?? {});\n\tconst attrKey = JSON.stringify(attributes ?? {});\n\n\tuseEffect(() => {\n\t\tif (disabled || !boot?.identifier) return;\n\t\treturn onChatwootReady(() =>\n\t\t\tidentifyChatwoot(boot.identifier, { ...user, identifier_hash: boot.identifierHash || undefined }, attributes)\n\t\t);\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, [disabled, boot?.identifier, boot?.identifierHash, userKey, attrKey]);\n}\n\n/** Component tiện dụng: <ChatwootWidget boot={...} user={...} /> */\nexport function ChatwootWidget(props: UseChatwootOptions) {\n\tuseChatwoot(props);\n\treturn null;\n}\n","// Server-side helpers (gọi trong loader/action). Dùng Web Crypto -> không phụ thuộc node:crypto,\n// nên package chỉ cần 1 entry. apiToken/hmacToken chỉ được truyền ở code server (.server.ts).\nimport type { ChatwootBootData } from './types';\n\nexport type ChatwootServerConfig = {\n\tbaseUrl: string;\n\twebsiteToken: string;\n\t/** Access Token của agent dùng cho API (nên tạo agent riêng) */\n\tapiToken: string;\n\taccountId?: number | string;\n\t/** Inbox → Configuration → Identity Validation */\n\thmacToken?: string;\n\t/** Tên custom attribute lưu phiên. Mặc định: init_session */\n\tsessionAttribute?: string;\n\ttimeoutMs?: number;\n\t/** Cache init_session trong RAM theo identifier. Mặc định 10 phút. 0 = tắt */\n\tcacheTtlMs?: number;\n\tlogger?: Pick<Console, 'error'>;\n};\n\ntype CwContact = {\n\tid: number;\n\tidentifier: string | null;\n\tcustom_attributes?: Record<string, unknown>;\n\tcontact_inboxes?: { source_id: string }[];\n};\n\ntype JwtPayload = { source_id?: string; inbox_id?: number; exp?: number };\n\nexport type SaveResult = {\n\tok: boolean;\n\treason: 'saved' | 'unchanged' | 'invalid_token' | 'contact_not_found' | 'token_not_owned' | 'update_failed';\n};\n\nconst decodeJwt = (token: string): JwtPayload | null => {\n\ttry {\n\t\tconst b64 = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');\n\t\tconst json = decodeURIComponent(\n\t\t\tatob(b64.padEnd(b64.length + ((4 - (b64.length % 4)) % 4), '='))\n\t\t\t\t.split('')\n\t\t\t\t.map((c) => '%' + c.charCodeAt(0).toString(16).padStart(2, '0'))\n\t\t\t\t.join('')\n\t\t);\n\t\treturn JSON.parse(json);\n\t} catch {\n\t\treturn null;\n\t}\n};\n\nconst hmacSha256Hex = async (key: string, message: string) => {\n\tconst enc = new TextEncoder();\n\tconst k = await globalThis.crypto.subtle.importKey('raw', enc.encode(key), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);\n\tconst sig = await globalThis.crypto.subtle.sign('HMAC', k, enc.encode(message));\n\treturn Array.from(new Uint8Array(sig), (b) => b.toString(16).padStart(2, '0')).join('');\n};\n\nconst sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));\n\nexport function createChatwootServer(config: ChatwootServerConfig) {\n\tconst base = config.baseUrl.replace(/\\/$/, '');\n\tconst account = config.accountId ?? 1;\n\tconst attr = config.sessionAttribute ?? 'init_session';\n\tconst timeout = config.timeoutMs ?? 5000;\n\tconst log = config.logger ?? console;\n\tconst ttl = config.cacheTtlMs ?? 10 * 60_000;\n\tconst cache = new Map<string, { token: string | null; until: number }>();\n\tconst cacheSet = (id: string, token: string | null) => {\n\t\tif (!ttl) return;\n\t\tif (cache.size > 5000) cache.clear(); // chặn phình RAM\n\t\tcache.set(id, { token, until: Date.now() + ttl });\n\t};\n\n\tconst api = async <T>(path: string, init: RequestInit = {}): Promise<T | null> => {\n\t\ttry {\n\t\t\tconst res = await fetch(`${base}/api/v1/accounts/${account}${path}`, {\n\t\t\t\t...init,\n\t\t\t\theaders: { 'Content-Type': 'application/json', api_access_token: config.apiToken, ...(init.headers ?? {}) },\n\t\t\t\tsignal: AbortSignal.timeout(timeout),\n\t\t\t});\n\t\t\tif (!res.ok) {\n\t\t\t\tlog.error(`[chatwoot] ${init.method ?? 'GET'} ${path} -> ${res.status}`);\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\treturn (await res.json()) as T;\n\t\t} catch (e) {\n\t\t\tlog.error(`[chatwoot] ${path}: ${(e as Error).message}`);\n\t\t\treturn null;\n\t\t}\n\t};\n\n\tconst identifierHash = async (identifier: string) =>\n\t\tconfig.hmacToken && identifier ? hmacSha256Hex(config.hmacToken, identifier) : '';\n\n\tconst isUsableToken = (token: string) => {\n\t\tconst p = decodeJwt(token);\n\t\treturn !!p?.source_id && (!p.exp || p.exp * 1000 > Date.now() + 86_400_000);\n\t};\n\n\tconst findContact = async (identifier: string) => {\n\t\tconst data = await api<{ payload: CwContact[] }>(`/contacts/search?q=${encodeURIComponent(identifier)}`);\n\t\treturn data?.payload?.find((c) => c.identifier === identifier) ?? null;\n\t};\n\n\tconst getInitSession = async (identifier: string) => {\n\t\tconst hit = cache.get(identifier);\n\t\tif (hit && hit.until > Date.now() && (!hit.token || isUsableToken(hit.token))) return hit.token;\n\n\t\tconst raw = (await findContact(identifier))?.custom_attributes?.[attr];\n\t\tconst token = typeof raw === 'string' && isUsableToken(raw) ? raw : null;\n\t\tif (token) cacheSet(identifier, token); // không cache null: shop mới sẽ có token ngay sau lần chat đầu\n\t\treturn token;\n\t};\n\n\tconst saveInitSession = async (identifier: string, token: string): Promise<SaveResult> => {\n\t\tconst payload = decodeJwt(token);\n\t\tif (!payload?.source_id) return { ok: false, reason: 'invalid_token' };\n\n\t\tlet contact: CwContact | null = null;\n\t\tfor (let i = 0; i < 6 && !contact; i++) {\n\t\t\tif (i) await sleep(1000);\n\t\t\tcontact = await findContact(identifier);\n\t\t}\n\t\tif (!contact) return { ok: false, reason: 'contact_not_found' };\n\n\t\tconst full = await api<{ payload: CwContact }>(`/contacts/${contact.id}`);\n\t\tconst sources = full?.payload?.contact_inboxes?.map((ci) => ci.source_id);\n\t\tif (sources && !sources.includes(payload.source_id)) return { ok: false, reason: 'token_not_owned' };\n\t\tif (contact.custom_attributes?.[attr] === token) {\n\t\t\tcacheSet(identifier, token);\n\t\t\treturn { ok: true, reason: 'unchanged' };\n\t\t}\n\n\t\tconst updated = await api(`/contacts/${contact.id}`, {\n\t\t\tmethod: 'PUT',\n\t\t\tbody: JSON.stringify({ custom_attributes: { ...(contact.custom_attributes ?? {}), [attr]: token } }),\n\t\t});\n\t\tif (updated) cacheSet(identifier, token);\n\t\treturn updated ? { ok: true, reason: 'saved' } : { ok: false, reason: 'update_failed' };\n\t};\n\n\t/** Gọi trong loader, trả về cho client */\n\tconst getBootData = async (identifier: string): Promise<ChatwootBootData> => ({\n\t\tbaseUrl: base,\n\t\twebsiteToken: config.websiteToken,\n\t\tidentifier,\n\t\tidentifierHash: await identifierHash(identifier),\n\t\tinitSession: await getInitSession(identifier),\n\t});\n\n\t/** Dùng trong route action: return chatwoot.handleSessionRequest(request, session.shop) */\n\tconst handleSessionRequest = async (request: Request, identifier: string) => {\n\t\tconst body = (await request.json().catch(() => ({}))) as { token?: unknown };\n\t\tif (typeof body.token !== 'string' || body.token.split('.').length !== 3) {\n\t\t\treturn Response.json({ ok: false, reason: 'invalid_token' }, { status: 400 });\n\t\t}\n\t\tconst result = await saveInitSession(identifier, body.token);\n\t\treturn Response.json(result, { status: result.ok ? 200 : 422 });\n\t};\n\n\tconst clearCache = (identifier?: string) => (identifier ? cache.delete(identifier) : cache.clear());\n\n\treturn { identifierHash, findContact, clearCache, getInitSession, saveInitSession, getBootData, handleSessionRequest };\n}\n\nexport type ChatwootServer = ReturnType<typeof createChatwootServer>;\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@fstacksoftware/chatwoot-embed",
3
+ "version": "1.2.0",
4
+ "description": "Chatwoot widget for Shopify embedded apps: persistent session in iframe, HMAC identity, 1 conversation per shop",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "files": [
9
+ "dist",
10
+ "README.md"
11
+ ],
12
+ "main": "./dist/index.cjs",
13
+ "module": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js",
19
+ "require": "./dist/index.cjs"
20
+ }
21
+ },
22
+ "scripts": {
23
+ "build": "tsup",
24
+ "typecheck": "tsc --noEmit",
25
+ "prepublishOnly": "npm run typecheck && npm run build"
26
+ },
27
+ "peerDependencies": {
28
+ "react": ">=17"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^20.0.0",
32
+ "@types/react": "^18.0.0",
33
+ "react": "^18.0.0",
34
+ "tsup": "^8.0.0",
35
+ "typescript": "^5.4.0"
36
+ },
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ }
43
+ }