@chanl/widget-sdk 0.2.0-canary.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 +257 -0
- package/dist/auth.d.ts +26 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +36 -0
- package/dist/auth.js.map +1 -0
- package/dist/chat/chat-client.d.ts +81 -0
- package/dist/chat/chat-client.d.ts.map +1 -0
- package/dist/chat/chat-client.js +192 -0
- package/dist/chat/chat-client.js.map +1 -0
- package/dist/chat/stream-parser.d.ts +20 -0
- package/dist/chat/stream-parser.d.ts.map +1 -0
- package/dist/chat/stream-parser.js +134 -0
- package/dist/chat/stream-parser.js.map +1 -0
- package/dist/chat/widget-config.d.ts +7 -0
- package/dist/chat/widget-config.d.ts.map +1 -0
- package/dist/chat/widget-config.js +26 -0
- package/dist/chat/widget-config.js.map +1 -0
- package/dist/client.d.ts +66 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +49 -0
- package/dist/client.js.map +1 -0
- package/dist/defaults.d.ts +12 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +27 -0
- package/dist/defaults.js.map +1 -0
- package/dist/embed/loader-types.d.ts +119 -0
- package/dist/embed/loader-types.d.ts.map +1 -0
- package/dist/embed/loader-types.js +20 -0
- package/dist/embed/loader-types.js.map +1 -0
- package/dist/embed/loader.d.ts +101 -0
- package/dist/embed/loader.d.ts.map +1 -0
- package/dist/embed/loader.js +439 -0
- package/dist/embed/loader.js.map +1 -0
- package/dist/embed/v1.global.js +5 -0
- package/dist/embed/v1.global.js.map +1 -0
- package/dist/events.d.ts +10 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +25 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +14 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +3 -0
- package/dist/logger.js.map +1 -0
- package/dist/next/index.d.ts +58 -0
- package/dist/next/index.d.ts.map +1 -0
- package/dist/next/index.js +83 -0
- package/dist/next/index.js.map +1 -0
- package/dist/react/index.d.ts +16 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +20 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/types.d.ts +27 -0
- package/dist/react/types.d.ts.map +1 -0
- package/dist/react/types.js +8 -0
- package/dist/react/types.js.map +1 -0
- package/dist/react/use-chanl.d.ts +27 -0
- package/dist/react/use-chanl.d.ts.map +1 -0
- package/dist/react/use-chanl.js +57 -0
- package/dist/react/use-chanl.js.map +1 -0
- package/dist/react/use-chat.d.ts +32 -0
- package/dist/react/use-chat.d.ts.map +1 -0
- package/dist/react/use-chat.js +224 -0
- package/dist/react/use-chat.js.map +1 -0
- package/dist/react/use-voice.d.ts +37 -0
- package/dist/react/use-voice.d.ts.map +1 -0
- package/dist/react/use-voice.js +268 -0
- package/dist/react/use-voice.js.map +1 -0
- package/dist/react/widget.d.ts +43 -0
- package/dist/react/widget.d.ts.map +1 -0
- package/dist/react/widget.js +188 -0
- package/dist/react/widget.js.map +1 -0
- package/dist/storage/session-storage.d.ts +48 -0
- package/dist/storage/session-storage.d.ts.map +1 -0
- package/dist/storage/session-storage.js +84 -0
- package/dist/storage/session-storage.js.map +1 -0
- package/dist/types.d.ts +140 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/voice/audio-recorder.d.ts +43 -0
- package/dist/voice/audio-recorder.d.ts.map +1 -0
- package/dist/voice/audio-recorder.js +127 -0
- package/dist/voice/audio-recorder.js.map +1 -0
- package/dist/voice/index.d.ts +13 -0
- package/dist/voice/index.d.ts.map +1 -0
- package/dist/voice/index.js +16 -0
- package/dist/voice/index.js.map +1 -0
- package/dist/voice/mock-mode.d.ts +93 -0
- package/dist/voice/mock-mode.d.ts.map +1 -0
- package/dist/voice/mock-mode.js +375 -0
- package/dist/voice/mock-mode.js.map +1 -0
- package/dist/voice/transports/index.d.ts +5 -0
- package/dist/voice/transports/index.d.ts.map +1 -0
- package/dist/voice/transports/index.js +10 -0
- package/dist/voice/transports/index.js.map +1 -0
- package/dist/voice/transports/transport.d.ts +70 -0
- package/dist/voice/transports/transport.d.ts.map +1 -0
- package/dist/voice/transports/transport.js +12 -0
- package/dist/voice/transports/transport.js.map +1 -0
- package/dist/voice/transports/vapi.d.ts +147 -0
- package/dist/voice/transports/vapi.d.ts.map +1 -0
- package/dist/voice/transports/vapi.js +337 -0
- package/dist/voice/transports/vapi.js.map +1 -0
- package/dist/voice/transports/webrtc.d.ts +58 -0
- package/dist/voice/transports/webrtc.d.ts.map +1 -0
- package/dist/voice/transports/webrtc.js +318 -0
- package/dist/voice/transports/webrtc.js.map +1 -0
- package/dist/voice/transports/websocket.d.ts +39 -0
- package/dist/voice/transports/websocket.d.ts.map +1 -0
- package/dist/voice/transports/websocket.js +280 -0
- package/dist/voice/transports/websocket.js.map +1 -0
- package/dist/voice/types.d.ts +323 -0
- package/dist/voice/types.d.ts.map +1 -0
- package/dist/voice/types.js +41 -0
- package/dist/voice/types.js.map +1 -0
- package/dist/voice/utils.d.ts +22 -0
- package/dist/voice/utils.d.ts.map +1 -0
- package/dist/voice/utils.js +44 -0
- package/dist/voice/utils.js.map +1 -0
- package/dist/voice/voice-client.d.ts +231 -0
- package/dist/voice/voice-client.d.ts.map +1 -0
- package/dist/voice/voice-client.js +1187 -0
- package/dist/voice/voice-client.js.map +1 -0
- package/package.json +91 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
'use client';
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.ChanlWidget = ChanlWidget;
|
|
38
|
+
/**
|
|
39
|
+
* <ChanlWidget> — v1 drop-in React component wrapping the embed loader.
|
|
40
|
+
*
|
|
41
|
+
* Minimal by design: takes props, calls `ChanlChat.init()` on mount,
|
|
42
|
+
* `ChanlChat.destroy()` on unmount (deferred 150ms to survive React
|
|
43
|
+
* StrictMode double-mount in dev). No provider, no event bus, no
|
|
44
|
+
* programmatic API surface on the component itself — those land in v2
|
|
45
|
+
* via `<ChanlProvider>` + `useChanlClient()`.
|
|
46
|
+
*
|
|
47
|
+
* Identity (`user` + `userHash`) is forwarded to the iframe as URL params.
|
|
48
|
+
* Compute `userHash` server-side using `computeUserHash` from
|
|
49
|
+
* `@chanl/widget-sdk/next`:
|
|
50
|
+
*
|
|
51
|
+
* ```tsx
|
|
52
|
+
* // app/dashboard/page.tsx (React Server Component)
|
|
53
|
+
* import { computeUserHash } from '@chanl/widget-sdk/next';
|
|
54
|
+
* import { ChanlWidget } from '@chanl/widget-sdk/react';
|
|
55
|
+
*
|
|
56
|
+
* export default async function Page() {
|
|
57
|
+
* const user = await getCurrentUser(); // Firebase/Clerk/NextAuth/Auth0/custom
|
|
58
|
+
* const userHash = computeUserHash(user.id, process.env.CHANL_IDENTITY_SECRET!);
|
|
59
|
+
* return (
|
|
60
|
+
* <ChanlWidget
|
|
61
|
+
* pubKey={process.env.NEXT_PUBLIC_CHANL_PUB_KEY!}
|
|
62
|
+
* agentId="agent_xxx"
|
|
63
|
+
* user={{ externalId: user.id, email: user.email, name: user.name }}
|
|
64
|
+
* userHash={userHash}
|
|
65
|
+
* />
|
|
66
|
+
* );
|
|
67
|
+
* }
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* Performance note: per react-best-practices `rerender-dependencies`,
|
|
71
|
+
* the re-init effect depends on primitive fields only (`externalId`,
|
|
72
|
+
* `theme`, etc.), not on the `user` object identity. If you pass a fresh
|
|
73
|
+
* `user` object on every parent render but the underlying primitives are
|
|
74
|
+
* stable, the widget will NOT re-init. Other user fields (email, name,
|
|
75
|
+
* avatarUrl, attributes) are captured at init time from the latest render
|
|
76
|
+
* — they flow in but do not trigger re-init by themselves.
|
|
77
|
+
*/
|
|
78
|
+
const react_1 = require("react");
|
|
79
|
+
function ChanlWidget(props) {
|
|
80
|
+
const { pubKey, agentId, user, userHash, baseUrl, chatHost, theme, position, color, branding, agentName, agentSubtitle, agentInitials, agentAvatarUrl, greeting, placeholder, darkBg, privacyUrl, locale, } = props;
|
|
81
|
+
// Hold the latest config in a ref so the effect body reads the freshest
|
|
82
|
+
// props even when the effect re-runs for primitive-only dependency changes.
|
|
83
|
+
const latestPropsRef = (0, react_1.useRef)(props);
|
|
84
|
+
latestPropsRef.current = props;
|
|
85
|
+
// Defer destroy by 150ms — StrictMode's intentional double-mount in dev
|
|
86
|
+
// runs mount → unmount → mount within that window, and the second mount
|
|
87
|
+
// cancels the pending destroy from the first unmount.
|
|
88
|
+
const destroyTimer = (0, react_1.useRef)(null);
|
|
89
|
+
(0, react_1.useEffect)(() => {
|
|
90
|
+
if (typeof window === 'undefined')
|
|
91
|
+
return;
|
|
92
|
+
// Cancel any pending destroy from a previous cleanup (StrictMode).
|
|
93
|
+
if (destroyTimer.current) {
|
|
94
|
+
clearTimeout(destroyTimer.current);
|
|
95
|
+
destroyTimer.current = null;
|
|
96
|
+
}
|
|
97
|
+
// `cancelled` guards the async loader — if the component unmounts
|
|
98
|
+
// before the dynamic import resolves (fast navigation), skip init.
|
|
99
|
+
let cancelled = false;
|
|
100
|
+
const build = () => {
|
|
101
|
+
const p = latestPropsRef.current;
|
|
102
|
+
return {
|
|
103
|
+
agentId: p.agentId,
|
|
104
|
+
apiKey: p.pubKey,
|
|
105
|
+
user: p.user,
|
|
106
|
+
userHash: p.userHash,
|
|
107
|
+
baseUrl: p.baseUrl,
|
|
108
|
+
chatHost: p.chatHost,
|
|
109
|
+
theme: p.theme,
|
|
110
|
+
position: p.position,
|
|
111
|
+
color: p.color,
|
|
112
|
+
branding: p.branding,
|
|
113
|
+
agentName: p.agentName,
|
|
114
|
+
agentSubtitle: p.agentSubtitle,
|
|
115
|
+
agentInitials: p.agentInitials,
|
|
116
|
+
agentAvatarUrl: p.agentAvatarUrl,
|
|
117
|
+
greeting: p.greeting,
|
|
118
|
+
placeholder: p.placeholder,
|
|
119
|
+
darkBg: p.darkBg,
|
|
120
|
+
privacyUrl: p.privacyUrl,
|
|
121
|
+
locale: p.locale,
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
// Lazily load the loader module. Side-effect import runs the IIFE
|
|
125
|
+
// which registers `window.ChanlChat`. Stays out of the SSR bundle
|
|
126
|
+
// and avoids leaking `declare global` into type consumers.
|
|
127
|
+
void Promise.resolve().then(() => __importStar(require('../embed/loader'))).then(() => {
|
|
128
|
+
if (cancelled)
|
|
129
|
+
return;
|
|
130
|
+
const ChanlChat = window.ChanlChat;
|
|
131
|
+
if (!ChanlChat)
|
|
132
|
+
return;
|
|
133
|
+
if (ChanlChat._initialized) {
|
|
134
|
+
try {
|
|
135
|
+
ChanlChat.destroy();
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// ignore
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
ChanlChat.init(build());
|
|
142
|
+
});
|
|
143
|
+
return () => {
|
|
144
|
+
cancelled = true;
|
|
145
|
+
destroyTimer.current = setTimeout(() => {
|
|
146
|
+
if (typeof window !== 'undefined') {
|
|
147
|
+
const live = window.ChanlChat;
|
|
148
|
+
if (live && live._initialized) {
|
|
149
|
+
try {
|
|
150
|
+
live.destroy();
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// ignore
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
destroyTimer.current = null;
|
|
158
|
+
}, 150);
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
// Primitive-only dependencies per react-best-practices `rerender-dependencies`.
|
|
162
|
+
// Re-init fires when any of these change. Non-primitive user fields
|
|
163
|
+
// (email, name, avatarUrl, attributes) flow via the ref on the next
|
|
164
|
+
// init — they don't trigger re-init on their own.
|
|
165
|
+
[
|
|
166
|
+
agentId,
|
|
167
|
+
pubKey,
|
|
168
|
+
user?.externalId,
|
|
169
|
+
userHash,
|
|
170
|
+
baseUrl,
|
|
171
|
+
chatHost,
|
|
172
|
+
theme,
|
|
173
|
+
position,
|
|
174
|
+
color,
|
|
175
|
+
branding,
|
|
176
|
+
agentName,
|
|
177
|
+
agentSubtitle,
|
|
178
|
+
agentInitials,
|
|
179
|
+
agentAvatarUrl,
|
|
180
|
+
greeting,
|
|
181
|
+
placeholder,
|
|
182
|
+
darkBg,
|
|
183
|
+
privacyUrl,
|
|
184
|
+
locale,
|
|
185
|
+
]);
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=widget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"widget.js","sourceRoot":"","sources":["../../src/react/widget.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Widget Session Storage — localStorage helpers for the persistent-session
|
|
3
|
+
* feature (int-050).
|
|
4
|
+
*
|
|
5
|
+
* Lifecycle:
|
|
6
|
+
* 1. Widget loader on host page reads `_chanl_session_{publicKey}_{agentId}`
|
|
7
|
+
* on init.
|
|
8
|
+
* 2. If present, loader passes the token into the iframe URL so the
|
|
9
|
+
* chat-client can call GET /interactions/:id/resume.
|
|
10
|
+
* 3. On createSession or successful resume, the iframe posts the new
|
|
11
|
+
* token back to the loader via postMessage; loader writes it.
|
|
12
|
+
* 4. On 4xx from resume (signature mismatch, expired, deleted), the
|
|
13
|
+
* iframe posts `session-token-cleared`; loader clears the key.
|
|
14
|
+
*
|
|
15
|
+
* Per-publicKey + per-agent scoping: a single host site can mount multiple
|
|
16
|
+
* agents (sales + support widgets side-by-side) — each gets its own
|
|
17
|
+
* resumable session. publicKey is already in loader config; agentId is
|
|
18
|
+
* already in the widget mount call. No extra wiring needed to namespace.
|
|
19
|
+
*
|
|
20
|
+
* Failure mode: localStorage can throw (Safari private mode, quota
|
|
21
|
+
* exceeded, sandboxed iframe with storage blocked). All three helpers
|
|
22
|
+
* catch and fail-silent so the widget keeps working — just without
|
|
23
|
+
* resumability (= fresh session every load, the pre-int-050 behavior).
|
|
24
|
+
*
|
|
25
|
+
* Note: these helpers run in the LOADER's host-origin context. The chat
|
|
26
|
+
* iframe is on a different origin and CANNOT touch this localStorage —
|
|
27
|
+
* all I/O bounces through postMessage. Anyone tempted to call these from
|
|
28
|
+
* inside the iframe will succeed locally but the loader won't see the
|
|
29
|
+
* value because they're separate origin localStorage stores.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Read a previously-stored session token. Returns the raw JWT string, or
|
|
33
|
+
* `null` when no token exists, the storage call throws, or we're in a
|
|
34
|
+
* non-browser context (SSR).
|
|
35
|
+
*/
|
|
36
|
+
export declare function readSessionToken(publicKey: string, agentId: string): string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Persist a freshly-issued session token. No-op on storage error so the
|
|
39
|
+
* widget continues to function (just without resumability on next load).
|
|
40
|
+
*/
|
|
41
|
+
export declare function writeSessionToken(publicKey: string, agentId: string, token: string): void;
|
|
42
|
+
/**
|
|
43
|
+
* Clear the stored token. Called when the server tells us the token is no
|
|
44
|
+
* longer valid (403/410/404) or when the host explicitly resets the
|
|
45
|
+
* conversation via the "New conversation" button.
|
|
46
|
+
*/
|
|
47
|
+
export declare function clearSessionToken(publicKey: string, agentId: string): void;
|
|
48
|
+
//# sourceMappingURL=session-storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-storage.d.ts","sourceRoot":"","sources":["../../src/storage/session-storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAQH;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOlF;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAOzF;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAO1E"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Widget Session Storage — localStorage helpers for the persistent-session
|
|
4
|
+
* feature (int-050).
|
|
5
|
+
*
|
|
6
|
+
* Lifecycle:
|
|
7
|
+
* 1. Widget loader on host page reads `_chanl_session_{publicKey}_{agentId}`
|
|
8
|
+
* on init.
|
|
9
|
+
* 2. If present, loader passes the token into the iframe URL so the
|
|
10
|
+
* chat-client can call GET /interactions/:id/resume.
|
|
11
|
+
* 3. On createSession or successful resume, the iframe posts the new
|
|
12
|
+
* token back to the loader via postMessage; loader writes it.
|
|
13
|
+
* 4. On 4xx from resume (signature mismatch, expired, deleted), the
|
|
14
|
+
* iframe posts `session-token-cleared`; loader clears the key.
|
|
15
|
+
*
|
|
16
|
+
* Per-publicKey + per-agent scoping: a single host site can mount multiple
|
|
17
|
+
* agents (sales + support widgets side-by-side) — each gets its own
|
|
18
|
+
* resumable session. publicKey is already in loader config; agentId is
|
|
19
|
+
* already in the widget mount call. No extra wiring needed to namespace.
|
|
20
|
+
*
|
|
21
|
+
* Failure mode: localStorage can throw (Safari private mode, quota
|
|
22
|
+
* exceeded, sandboxed iframe with storage blocked). All three helpers
|
|
23
|
+
* catch and fail-silent so the widget keeps working — just without
|
|
24
|
+
* resumability (= fresh session every load, the pre-int-050 behavior).
|
|
25
|
+
*
|
|
26
|
+
* Note: these helpers run in the LOADER's host-origin context. The chat
|
|
27
|
+
* iframe is on a different origin and CANNOT touch this localStorage —
|
|
28
|
+
* all I/O bounces through postMessage. Anyone tempted to call these from
|
|
29
|
+
* inside the iframe will succeed locally but the loader won't see the
|
|
30
|
+
* value because they're separate origin localStorage stores.
|
|
31
|
+
*/
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.readSessionToken = readSessionToken;
|
|
34
|
+
exports.writeSessionToken = writeSessionToken;
|
|
35
|
+
exports.clearSessionToken = clearSessionToken;
|
|
36
|
+
const KEY_PREFIX = '_chanl_session_';
|
|
37
|
+
function makeKey(publicKey, agentId) {
|
|
38
|
+
return `${KEY_PREFIX}${publicKey}_${agentId}`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Read a previously-stored session token. Returns the raw JWT string, or
|
|
42
|
+
* `null` when no token exists, the storage call throws, or we're in a
|
|
43
|
+
* non-browser context (SSR).
|
|
44
|
+
*/
|
|
45
|
+
function readSessionToken(publicKey, agentId) {
|
|
46
|
+
if (typeof window === 'undefined' || !window.localStorage)
|
|
47
|
+
return null;
|
|
48
|
+
try {
|
|
49
|
+
return window.localStorage.getItem(makeKey(publicKey, agentId));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Persist a freshly-issued session token. No-op on storage error so the
|
|
57
|
+
* widget continues to function (just without resumability on next load).
|
|
58
|
+
*/
|
|
59
|
+
function writeSessionToken(publicKey, agentId, token) {
|
|
60
|
+
if (typeof window === 'undefined' || !window.localStorage)
|
|
61
|
+
return;
|
|
62
|
+
try {
|
|
63
|
+
window.localStorage.setItem(makeKey(publicKey, agentId), token);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// localStorage quota, sandboxed iframe, etc. Fail silent.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Clear the stored token. Called when the server tells us the token is no
|
|
71
|
+
* longer valid (403/410/404) or when the host explicitly resets the
|
|
72
|
+
* conversation via the "New conversation" button.
|
|
73
|
+
*/
|
|
74
|
+
function clearSessionToken(publicKey, agentId) {
|
|
75
|
+
if (typeof window === 'undefined' || !window.localStorage)
|
|
76
|
+
return;
|
|
77
|
+
try {
|
|
78
|
+
window.localStorage.removeItem(makeKey(publicKey, agentId));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// ignore
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=session-storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-storage.js","sourceRoot":"","sources":["../../src/storage/session-storage.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;;AAaH,4CAOC;AAMD,8CAOC;AAOD,8CAOC;AA7CD,MAAM,UAAU,GAAG,iBAAiB,CAAC;AAErC,SAAS,OAAO,CAAC,SAAiB,EAAE,OAAe;IACjD,OAAO,GAAG,UAAU,GAAG,SAAS,IAAI,OAAO,EAAE,CAAC;AAChD,CAAC;AAED;;;;GAIG;AACH,SAAgB,gBAAgB,CAAC,SAAiB,EAAE,OAAe;IACjE,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,CAAC,MAAM,CAAC,YAAY;QAAE,OAAO,IAAI,CAAC;IACvE,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;IAClE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAgB,iBAAiB,CAAC,SAAiB,EAAE,OAAe,EAAE,KAAa;IACjF,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,CAAC,MAAM,CAAC,YAAY;QAAE,OAAO;IAClE,IAAI,CAAC;QACH,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,KAAK,CAAC,CAAC;IAClE,CAAC;IAAC,MAAM,CAAC;QACP,0DAA0D;IAC5D,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,SAAgB,iBAAiB,CAAC,SAAiB,EAAE,OAAe;IAClE,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,CAAC,MAAM,CAAC,YAAY;QAAE,OAAO;IAClE,IAAI,CAAC;QACH,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;IAC9D,CAAC;IAAC,MAAM,CAAC;QACP,SAAS;IACX,CAAC;AACH,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chanl Chat SDK — Standalone types
|
|
3
|
+
* No external dependencies.
|
|
4
|
+
*/
|
|
5
|
+
export interface ChatClientConfig {
|
|
6
|
+
apiKey: string;
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
/**
|
|
9
|
+
* Identified end-user. When present, activates the widget identity
|
|
10
|
+
* path — backend verifies `userHash` as HMAC-SHA256(externalId, secret),
|
|
11
|
+
* upserts a Customer by externalId, and attaches customer context to the
|
|
12
|
+
* chat session. Custom attributes flow into the agent prompt as
|
|
13
|
+
* `{{customer:<key>}}` variables.
|
|
14
|
+
*/
|
|
15
|
+
user?: ChanlUser;
|
|
16
|
+
/** HMAC-SHA256 of `user.externalId` using workspace identity secret. */
|
|
17
|
+
userHash?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Stable anon-Lead identifier read from `localStorage._chanl_anon` on the
|
|
20
|
+
* chat-host iframe origin. Used when `user` is absent to persist memory
|
|
21
|
+
* across sessions for cold visitors.
|
|
22
|
+
*/
|
|
23
|
+
anonymousId?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Identified end-user for the widget identity path (Intercom-style).
|
|
27
|
+
*
|
|
28
|
+
* The host app resolves its own user via whatever auth system it already
|
|
29
|
+
* has (Firebase, Clerk, NextAuth, Auth0, custom) and passes the stable
|
|
30
|
+
* `externalId`. The `userHash` is computed server-side via
|
|
31
|
+
* `computeUserHash(externalId, CHANL_IDENTITY_SECRET)` from the
|
|
32
|
+
* `@chanl/widget-sdk/next` subpath.
|
|
33
|
+
*/
|
|
34
|
+
export interface ChanlUser {
|
|
35
|
+
/**
|
|
36
|
+
* Stable external user identifier — any string the host app uses for
|
|
37
|
+
* the user in its own system (Firebase uid, Auth0 sub, database id, ...).
|
|
38
|
+
*/
|
|
39
|
+
externalId: string;
|
|
40
|
+
email?: string;
|
|
41
|
+
name?: string;
|
|
42
|
+
avatarUrl?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Custom attributes (plan, role, workspace id, etc.). Flow into the
|
|
45
|
+
* agent prompt as `customer:<key>` variables automatically.
|
|
46
|
+
*/
|
|
47
|
+
attributes?: Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
export interface CreateSessionInput {
|
|
50
|
+
systemPrompt?: string;
|
|
51
|
+
variables?: Record<string, string>;
|
|
52
|
+
/** @deprecated prefer `user.externalId` for client-side embeds. */
|
|
53
|
+
customerId?: string;
|
|
54
|
+
/** HMAC-SHA256 of the identifier (externalId when `user` is present, otherwise customerId). */
|
|
55
|
+
userHash?: string;
|
|
56
|
+
/** Identified end-user — activates the widget identity path. */
|
|
57
|
+
user?: ChanlUser;
|
|
58
|
+
/**
|
|
59
|
+
* Anonymous browser identifier (Intercom Lead path). Stable UUID minted in
|
|
60
|
+
* `localStorage._chanl_anon` on the chat-host iframe origin so cold visitors
|
|
61
|
+
* persist across sessions even without a verified `user`. Backend upserts a
|
|
62
|
+
* Customer with `externalId = "anon_<uuid>"`. Ignored when `user` or
|
|
63
|
+
* `customerId` is present.
|
|
64
|
+
*/
|
|
65
|
+
anonymousId?: string;
|
|
66
|
+
metadata?: Record<string, unknown>;
|
|
67
|
+
toolsetId?: string;
|
|
68
|
+
}
|
|
69
|
+
export interface ChatSession {
|
|
70
|
+
interactionId: string;
|
|
71
|
+
sessionId: string;
|
|
72
|
+
agentId: string;
|
|
73
|
+
agentName?: string;
|
|
74
|
+
model?: string;
|
|
75
|
+
createdAt: string;
|
|
76
|
+
/**
|
|
77
|
+
* Signed JWT (HS256) issued by the server. Widget loader stores this in
|
|
78
|
+
* localStorage keyed by publicKey+agentId and presents it back on resume
|
|
79
|
+
* calls so the server can verify session ownership. Absent when the
|
|
80
|
+
* workspace has no identitySecret configured — session still works but
|
|
81
|
+
* cannot be resumed across host-page refreshes.
|
|
82
|
+
*/
|
|
83
|
+
sessionToken?: string;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Response from GET /interactions/:id/resume. Rehydrates the conversation
|
|
87
|
+
* view + carries a refreshed token (rolling TTL — widget MUST replace its
|
|
88
|
+
* stored token with the new value).
|
|
89
|
+
*/
|
|
90
|
+
export interface ResumedChatSession extends ChatSession {
|
|
91
|
+
status: string;
|
|
92
|
+
messages: Array<{
|
|
93
|
+
role: 'user' | 'assistant' | 'system';
|
|
94
|
+
content: string;
|
|
95
|
+
timestamp?: string;
|
|
96
|
+
}>;
|
|
97
|
+
/** Refreshed JWT — overwrite the stored one in localStorage. */
|
|
98
|
+
sessionToken: string;
|
|
99
|
+
}
|
|
100
|
+
export interface ToolInvocation {
|
|
101
|
+
toolCallId: string;
|
|
102
|
+
toolName: string;
|
|
103
|
+
state: 'call' | 'result';
|
|
104
|
+
args?: Record<string, unknown>;
|
|
105
|
+
result?: unknown;
|
|
106
|
+
}
|
|
107
|
+
export interface MessagePart {
|
|
108
|
+
type: 'text' | 'tool-invocation';
|
|
109
|
+
text?: string;
|
|
110
|
+
toolInvocation?: ToolInvocation;
|
|
111
|
+
}
|
|
112
|
+
export interface ChatMessageResponse {
|
|
113
|
+
sessionId: string;
|
|
114
|
+
message: string;
|
|
115
|
+
parts?: MessagePart[];
|
|
116
|
+
toolCalls?: unknown[];
|
|
117
|
+
latencyMs?: number;
|
|
118
|
+
}
|
|
119
|
+
export interface WidgetConfig {
|
|
120
|
+
id: string;
|
|
121
|
+
agent: {
|
|
122
|
+
id: string;
|
|
123
|
+
name: string;
|
|
124
|
+
};
|
|
125
|
+
launcher: {
|
|
126
|
+
position: string;
|
|
127
|
+
modality: 'voice' | 'chat' | 'both';
|
|
128
|
+
};
|
|
129
|
+
style: {
|
|
130
|
+
primaryColor?: string;
|
|
131
|
+
backgroundColor?: string;
|
|
132
|
+
logoUrl?: string;
|
|
133
|
+
agentAvatar?: string;
|
|
134
|
+
};
|
|
135
|
+
banner?: {
|
|
136
|
+
header?: string;
|
|
137
|
+
description?: string;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";AAAA;;;GAGG"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface AudioRecorderConfig {
|
|
2
|
+
/** Audio track constraints for getUserMedia */
|
|
3
|
+
trackConstraints?: MediaTrackConstraints;
|
|
4
|
+
/** Time slice in milliseconds for data chunks */
|
|
5
|
+
timeSlice?: number;
|
|
6
|
+
/** Callback for each chunk of audio data */
|
|
7
|
+
onData: (data: string) => void;
|
|
8
|
+
/** Callback for recording state changes */
|
|
9
|
+
onStateChange?: (state: RecordingState) => void;
|
|
10
|
+
/** Callback for errors */
|
|
11
|
+
onError?: (error: Error) => void;
|
|
12
|
+
}
|
|
13
|
+
export type RecordingState = 'inactive' | 'recording' | 'paused';
|
|
14
|
+
/**
|
|
15
|
+
* High-quality audio recorder using extendable-media-recorder
|
|
16
|
+
* Handles WAV encoding and streaming of audio data
|
|
17
|
+
*/
|
|
18
|
+
export declare class AudioRecorder {
|
|
19
|
+
private recorder;
|
|
20
|
+
private stream;
|
|
21
|
+
private readonly config;
|
|
22
|
+
constructor({ trackConstraints, timeSlice, onData, onStateChange, onError, }: AudioRecorderConfig);
|
|
23
|
+
/**
|
|
24
|
+
* Initialize the WAV encoder and start recording
|
|
25
|
+
*/
|
|
26
|
+
start(): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Stop recording and cleanup resources
|
|
29
|
+
*/
|
|
30
|
+
stop(): void;
|
|
31
|
+
/**
|
|
32
|
+
* Get current recording state
|
|
33
|
+
*/
|
|
34
|
+
getState(): RecordingState;
|
|
35
|
+
/**
|
|
36
|
+
* Check if currently recording
|
|
37
|
+
*/
|
|
38
|
+
isRecording(): boolean;
|
|
39
|
+
private handleDataAvailable;
|
|
40
|
+
private handleStop;
|
|
41
|
+
private cleanup;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=audio-recorder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio-recorder.d.ts","sourceRoot":"","sources":["../../src/voice/audio-recorder.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,mBAAmB;IAClC,+CAA+C;IAC/C,gBAAgB,CAAC,EAAE,qBAAqB,CAAC;IACzC,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,2CAA2C;IAC3C,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IAChD,0BAA0B;IAC1B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAED,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,WAAW,GAAG,QAAQ,CAAC;AAEjE;;;GAGG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAA+B;IAC/C,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgC;gBAE3C,EACV,gBAA6C,EAC7C,SAAc,EACd,MAAM,EACN,aAAyB,EACzB,OAAoD,GACrD,EAAE,mBAAmB;IAUtB;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAoC5B;;OAEG;IACH,IAAI,IAAI,IAAI;IAcZ;;OAEG;IACH,QAAQ,IAAI,cAAc;IAI1B;;OAEG;IACH,WAAW,IAAI,OAAO;YAIR,mBAAmB;IAajC,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,OAAO;CAiBhB"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* AudioRecorder captures microphone audio, down-samples to 16 kHz mono and encodes
|
|
4
|
+
* to base64-encoded linear16 PCM chunks suitable for the backend.
|
|
5
|
+
*
|
|
6
|
+
* Note: implementation keeps things simple; production apps may want to use a
|
|
7
|
+
* WebAudio AudioWorklet for lower latency.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.AudioRecorder = void 0;
|
|
11
|
+
const extendable_media_recorder_1 = require("extendable-media-recorder");
|
|
12
|
+
const extendable_media_recorder_wav_encoder_1 = require("extendable-media-recorder-wav-encoder");
|
|
13
|
+
const utils_1 = require("./utils");
|
|
14
|
+
/**
|
|
15
|
+
* High-quality audio recorder using extendable-media-recorder
|
|
16
|
+
* Handles WAV encoding and streaming of audio data
|
|
17
|
+
*/
|
|
18
|
+
class AudioRecorder {
|
|
19
|
+
constructor({ trackConstraints = { echoCancellation: true }, timeSlice = 10, onData, onStateChange = () => { }, onError = (e) => console.error('[AudioRecorder]', e), }) {
|
|
20
|
+
this.recorder = null;
|
|
21
|
+
this.stream = null;
|
|
22
|
+
this.config = {
|
|
23
|
+
trackConstraints,
|
|
24
|
+
timeSlice,
|
|
25
|
+
onData,
|
|
26
|
+
onStateChange,
|
|
27
|
+
onError,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Initialize the WAV encoder and start recording
|
|
32
|
+
*/
|
|
33
|
+
async start() {
|
|
34
|
+
try {
|
|
35
|
+
// Register WAV encoder if not already registered
|
|
36
|
+
if (!extendable_media_recorder_1.MediaRecorder.isTypeSupported('audio/wav')) {
|
|
37
|
+
console.debug("[AudioRecorder] Registering WAV encoder");
|
|
38
|
+
await (0, extendable_media_recorder_1.register)(await (0, extendable_media_recorder_wav_encoder_1.connect)());
|
|
39
|
+
}
|
|
40
|
+
// Get audio stream
|
|
41
|
+
this.stream = await navigator.mediaDevices.getUserMedia({
|
|
42
|
+
audio: this.config.trackConstraints,
|
|
43
|
+
});
|
|
44
|
+
const micSettings = this.stream?.getAudioTracks()[0].getSettings();
|
|
45
|
+
console.debug('[AudioRecorder] Mic settings:', micSettings);
|
|
46
|
+
// Create recorder with WAV encoding
|
|
47
|
+
this.recorder = new extendable_media_recorder_1.MediaRecorder(this.stream, {
|
|
48
|
+
mimeType: 'audio/wav',
|
|
49
|
+
});
|
|
50
|
+
// Set up event handlers
|
|
51
|
+
this.recorder.ondataavailable = this.handleDataAvailable.bind(this);
|
|
52
|
+
this.recorder.onstop = this.handleStop.bind(this);
|
|
53
|
+
this.recorder.onerror = (event) => this.config.onError(new Error(event.error.message));
|
|
54
|
+
this.recorder.addEventListener('statechange', () => this.config.onStateChange(this.recorder?.state ?? 'inactive'));
|
|
55
|
+
// Start recording
|
|
56
|
+
this.recorder.start(this.config.timeSlice);
|
|
57
|
+
console.debug('[AudioRecorder] Started recording');
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
this.config.onError(error instanceof Error ? error : new Error('Failed to start recording'));
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Stop recording and cleanup resources
|
|
66
|
+
*/
|
|
67
|
+
stop() {
|
|
68
|
+
if (!this.recorder)
|
|
69
|
+
return;
|
|
70
|
+
try {
|
|
71
|
+
if (this.recorder.state === 'recording') {
|
|
72
|
+
this.recorder.stop();
|
|
73
|
+
}
|
|
74
|
+
this.cleanup();
|
|
75
|
+
console.debug('[AudioRecorder] Stopped recording');
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
this.config.onError(error instanceof Error ? error : new Error('Failed to stop recording'));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get current recording state
|
|
83
|
+
*/
|
|
84
|
+
getState() {
|
|
85
|
+
return this.recorder?.state ?? 'inactive';
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Check if currently recording
|
|
89
|
+
*/
|
|
90
|
+
isRecording() {
|
|
91
|
+
return this.recorder?.state === 'recording';
|
|
92
|
+
}
|
|
93
|
+
async handleDataAvailable(event) {
|
|
94
|
+
if (!event.data.size)
|
|
95
|
+
return;
|
|
96
|
+
try {
|
|
97
|
+
const base64 = await (0, utils_1.blobToBase64)(event.data);
|
|
98
|
+
if (base64) {
|
|
99
|
+
this.config.onData(base64);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
this.config.onError(error instanceof Error ? error : new Error('Failed to convert audio data'));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
handleStop() {
|
|
107
|
+
this.cleanup();
|
|
108
|
+
}
|
|
109
|
+
cleanup() {
|
|
110
|
+
// Stop all tracks
|
|
111
|
+
this.stream?.getTracks().forEach(track => track.stop());
|
|
112
|
+
this.stream = null;
|
|
113
|
+
// Clear recorder
|
|
114
|
+
if (this.recorder) {
|
|
115
|
+
this.recorder.onstop = null;
|
|
116
|
+
this.recorder.ondataavailable = null;
|
|
117
|
+
this.recorder.onerror = null;
|
|
118
|
+
this.recorder.removeEventListener('dataavailable', this.handleDataAvailable.bind(this));
|
|
119
|
+
this.recorder.removeEventListener('stop', this.handleStop.bind(this));
|
|
120
|
+
this.recorder.removeEventListener('error', () => { });
|
|
121
|
+
this.recorder.removeEventListener('statechange', () => { });
|
|
122
|
+
this.recorder = null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
exports.AudioRecorder = AudioRecorder;
|
|
127
|
+
//# sourceMappingURL=audio-recorder.js.map
|