@memberjunction/realtime-widget 0.0.0 → 5.45.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 +107 -43
- package/dist/embed.d.ts +18 -0
- package/dist/embed.d.ts.map +1 -0
- package/dist/embed.js +33 -0
- package/dist/embed.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +32 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +163 -0
- package/dist/loader.js.map +1 -0
- package/dist/session/visitor-key-cookie.d.ts +20 -0
- package/dist/session/visitor-key-cookie.d.ts.map +1 -0
- package/dist/session/visitor-key-cookie.js +60 -0
- package/dist/session/visitor-key-cookie.js.map +1 -0
- package/dist/session/widget-session-client.d.ts +74 -0
- package/dist/session/widget-session-client.d.ts.map +1 -0
- package/dist/session/widget-session-client.js +147 -0
- package/dist/session/widget-session-client.js.map +1 -0
- package/dist/transport/mock-widget-transport.d.ts +26 -0
- package/dist/transport/mock-widget-transport.d.ts.map +1 -0
- package/dist/transport/mock-widget-transport.js +42 -0
- package/dist/transport/mock-widget-transport.js.map +1 -0
- package/dist/transport/runtime-widget-transport.d.ts +54 -0
- package/dist/transport/runtime-widget-transport.d.ts.map +1 -0
- package/dist/transport/runtime-widget-transport.js +203 -0
- package/dist/transport/runtime-widget-transport.js.map +1 -0
- package/dist/transport/widget-transport.d.ts +41 -0
- package/dist/transport/widget-transport.d.ts.map +1 -0
- package/dist/transport/widget-transport.js +13 -0
- package/dist/transport/widget-transport.js.map +1 -0
- package/dist/types.d.ts +104 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/browser-capabilities.d.ts +36 -0
- package/dist/ui/browser-capabilities.d.ts.map +1 -0
- package/dist/ui/browser-capabilities.js +30 -0
- package/dist/ui/browser-capabilities.js.map +1 -0
- package/dist/ui/support-widget-element.d.ts +149 -0
- package/dist/ui/support-widget-element.d.ts.map +1 -0
- package/dist/ui/support-widget-element.js +540 -0
- package/dist/ui/support-widget-element.js.map +1 -0
- package/dist/ui/tokens.d.ts +16 -0
- package/dist/ui/tokens.d.ts.map +1 -0
- package/dist/ui/tokens.js +135 -0
- package/dist/ui/tokens.js.map +1 -0
- package/dist/voice/channels/base-widget-channel.d.ts +63 -0
- package/dist/voice/channels/base-widget-channel.d.ts.map +1 -0
- package/dist/voice/channels/base-widget-channel.js +36 -0
- package/dist/voice/channels/base-widget-channel.js.map +1 -0
- package/dist/voice/channels/whiteboard-channel.d.ts +38 -0
- package/dist/voice/channels/whiteboard-channel.d.ts.map +1 -0
- package/dist/voice/channels/whiteboard-channel.js +214 -0
- package/dist/voice/channels/whiteboard-channel.js.map +1 -0
- package/dist/voice/channels/widget-channel-host.d.ts +68 -0
- package/dist/voice/channels/widget-channel-host.d.ts.map +1 -0
- package/dist/voice/channels/widget-channel-host.js +111 -0
- package/dist/voice/channels/widget-channel-host.js.map +1 -0
- package/dist/voice/guest-tool-relay.d.ts +14 -0
- package/dist/voice/guest-tool-relay.d.ts.map +1 -0
- package/dist/voice/guest-tool-relay.js +28 -0
- package/dist/voice/guest-tool-relay.js.map +1 -0
- package/dist/voice/guest-voice-mint.d.ts +23 -0
- package/dist/voice/guest-voice-mint.d.ts.map +1 -0
- package/dist/voice/guest-voice-mint.js +61 -0
- package/dist/voice/guest-voice-mint.js.map +1 -0
- package/dist/voice/mock-voice-controller.d.ts +22 -0
- package/dist/voice/mock-voice-controller.d.ts.map +1 -0
- package/dist/voice/mock-voice-controller.js +42 -0
- package/dist/voice/mock-voice-controller.js.map +1 -0
- package/dist/voice/realtime-voice-controller.d.ts +75 -0
- package/dist/voice/realtime-voice-controller.d.ts.map +1 -0
- package/dist/voice/realtime-voice-controller.js +182 -0
- package/dist/voice/realtime-voice-controller.js.map +1 -0
- package/dist/voice/voice-abuse-guard.d.ts +39 -0
- package/dist/voice/voice-abuse-guard.d.ts.map +1 -0
- package/dist/voice/voice-abuse-guard.js +57 -0
- package/dist/voice/voice-abuse-guard.js.map +1 -0
- package/dist/voice/voice-controller.d.ts +39 -0
- package/dist/voice/voice-controller.d.ts.map +1 -0
- package/dist/voice/voice-controller.js +10 -0
- package/dist/voice/voice-controller.js.map +1 -0
- package/package.json +36 -7
package/README.md
CHANGED
|
@@ -1,45 +1,109 @@
|
|
|
1
1
|
# @memberjunction/realtime-widget
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
3
|
+
A **droppable, embeddable public customer-support widget** (text today; voice in W4) for any
|
|
4
|
+
third-party website. A single `<script>` tag plus one mount element opens a support
|
|
5
|
+
conversation with a **pinned MJ agent** — **no MJ login required** — by minting a short-lived
|
|
6
|
+
**anonymous guest session** and reusing MemberJunction's unified agent pathway
|
|
7
|
+
(`ConversationsRuntime` + the GraphQL data provider). It does **not** reimplement chat or agent
|
|
8
|
+
dispatch.
|
|
9
|
+
|
|
10
|
+
Part of the [Agent Bridges & Public Widget program](../../../plans/realtime/bridges-and-widget/README.md)
|
|
11
|
+
(phases **W3+**). See [`public-web-widget.md`](../../../plans/realtime/bridges-and-widget/public-web-widget.md).
|
|
12
|
+
|
|
13
|
+
## Embedding (the entire integration)
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<div
|
|
17
|
+
data-widget-key="pk_live_xxx"
|
|
18
|
+
data-api-url="https://api.yourco.com"
|
|
19
|
+
data-title="Acme Support"
|
|
20
|
+
data-greeting="Hi! How can we help?"
|
|
21
|
+
></div>
|
|
22
|
+
<script type="module" src="https://cdn.yourco.com/mj-widget.js"></script>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The loader reads the `data-*` attributes, calls `POST /widget/session` to mint a guest JWT, and
|
|
26
|
+
mounts `<mj-support-widget>` **inside a shadow DOM** so host CSS cannot bleed in or out. See
|
|
27
|
+
[`examples/blank-host.html`](./examples/blank-host.html).
|
|
28
|
+
|
|
29
|
+
### Programmatic mount
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { mountWidget } from '@memberjunction/realtime-widget';
|
|
33
|
+
await mountWidget({ widgetKey: 'pk_live_xxx', apiUrl: 'https://api.yourco.com', mountTarget: '#support' });
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Content Security Policy (host sites)
|
|
37
|
+
|
|
38
|
+
If your site sends a `Content-Security-Policy` header (it should), the widget needs four allowances:
|
|
39
|
+
the script bundle, the API origin (GraphQL over HTTPS **and** the realtime WebSocket for voice), and —
|
|
40
|
+
because the UI renders inside a shadow DOM with inlined token styles — `style-src 'unsafe-inline'`.
|
|
41
|
+
Replace `https://api.yourco.com` with your MJAPI origin and `https://cdn.yourco.com` with wherever you
|
|
42
|
+
host `mj-widget.js`:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
Content-Security-Policy:
|
|
46
|
+
script-src 'self' https://cdn.yourco.com;
|
|
47
|
+
connect-src 'self' https://api.yourco.com wss://api.yourco.com;
|
|
48
|
+
style-src 'self' 'unsafe-inline';
|
|
49
|
+
img-src 'self' data:;
|
|
50
|
+
frame-ancestors 'self';
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Notes:
|
|
54
|
+
- **`connect-src`** must include BOTH `https://` (the GraphQL mint + agent calls) and `wss://` (the
|
|
55
|
+
realtime voice socket). Voice silently fails to connect if the `wss:` origin is omitted.
|
|
56
|
+
- **`style-src 'unsafe-inline'`** is required because the shadow-DOM component injects its
|
|
57
|
+
design-token stylesheet inline. The shadow root keeps it isolated from your page's styles; it does
|
|
58
|
+
**not** widen your page's own CSP exposure to inline `<style>` you didn't author.
|
|
59
|
+
- **`img-src data:`** covers the inline SVG/emoji affordances.
|
|
60
|
+
- Voice also needs the browser mic permission; on a strict `Permissions-Policy`, allow
|
|
61
|
+
`microphone=(self "https://cdn.yourco.com")` (or your embed origin) or the mic toggle is hidden and
|
|
62
|
+
the widget falls back to text.
|
|
63
|
+
|
|
64
|
+
## Architecture
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
host page → <mj-support-widget> (shadow DOM, design-token-scoped)
|
|
68
|
+
├─ WidgetSessionClient → POST /widget/session (guest JWT, refreshed before expiry)
|
|
69
|
+
└─ IWidgetTransport
|
|
70
|
+
├─ RuntimeWidgetTransport (prod): setupGraphQLClient(guest token) + ConversationsRuntime
|
|
71
|
+
│ → AgentRunner.processMessage({ explicitAgentId: <pinned> }) ← D5
|
|
72
|
+
└─ MockWidgetTransport (tests / offline demo)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- **Auth (D1, pluggable):** the guest session is an `anonymous-embed` magic-link token minted by
|
|
76
|
+
MJServer's `WidgetSessionService`, validated by the same `AuthProviderFactory` path. Strategies:
|
|
77
|
+
`Anonymous` (default), `MagicLinkUpgrade`, `HostIdentity` — selected per widget instance (W5).
|
|
78
|
+
- **Guardrails (D5):** the widget **always** passes the per-instance pinned `explicitAgentId`, and
|
|
79
|
+
the synthesized guest principal carries the restricted **Widget Guest** role (read/create/update
|
|
80
|
+
only on Conversations + Conversation Details). Pinning alone is not enough — see the
|
|
81
|
+
[W0 findings](../../../plans/realtime/bridges-and-widget/spikes/W0-findings.md).
|
|
82
|
+
- **Isolation:** shadow DOM + `all: initial` on `:host`; `--mj-chat-*` tokens are injected into the
|
|
83
|
+
**shadow root**, never `<head>`.
|
|
84
|
+
|
|
85
|
+
## Scripts
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npm run build # tsc → dist/ (ESM)
|
|
89
|
+
npm run bundle # esbuild → dist/mj-widget.js (single-file browser ESM)
|
|
90
|
+
npm run bundle:min # minified variant
|
|
91
|
+
npm run test # vitest (jsdom): session client, element, loader
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Build & bundle status (overnight session)
|
|
95
|
+
|
|
96
|
+
- `npm run build` ✅ and `npm run test` ✅ (15 tests: mint, shadow-DOM isolation, send/receive,
|
|
97
|
+
pinned-agent pass-through, refresh math, loader mount).
|
|
98
|
+
- `npm run bundle` ✅ produces a **self-contained** browser ESM (no unresolved node built-ins).
|
|
99
|
+
It is currently large (~2.9 MB minified) because it bundles the full runtime + GraphQL provider;
|
|
100
|
+
**tree-shaking / code-splitting the bundle is W6 packaging hardening**, tracked, not done here.
|
|
101
|
+
- **Live end-to-end** (`blank-host.html` against a running MJAPI completing a real turn) requires
|
|
102
|
+
MJAPI booted with `widget.enabled=true` — **Auth0-gated** in the overnight sandbox. The code path
|
|
103
|
+
is complete and ready; it was not faked.
|
|
104
|
+
|
|
105
|
+
## Not built here (anti-drift)
|
|
106
|
+
|
|
107
|
+
No new chat engine, no new realtime/voice driver, no new auth subsystem, no second agent-dispatch
|
|
108
|
+
path — all reused. Voice (W4), magic-link upgrade + host identity (W5), and bundle/abuse hardening
|
|
109
|
+
(W6) build on this package.
|
package/dist/embed.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Browser EMBED entry — the bundle the host page loads via `<script src="mj-widget.js">`.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately TINY and free of static value-imports of the heavy modules (the runtime transport and
|
|
5
|
+
* the realtime voice stack). It only defines the custom element and auto-bootstraps from the host's
|
|
6
|
+
* `[data-widget-key]` element; the loader then DYNAMICALLY imports the transport (on mount) and the
|
|
7
|
+
* voice controller (only for Voice/Both widgets). Built with `esbuild --splitting`, this keeps
|
|
8
|
+
* `@memberjunction/graphql-dataprovider` + `@memberjunction/conversations-runtime` and
|
|
9
|
+
* `@memberjunction/ai-realtime-client` in separate chunks that load on demand — so the launcher
|
|
10
|
+
* paints fast and a text-only widget never downloads the voice chunk.
|
|
11
|
+
*
|
|
12
|
+
* The npm public API (with static exports for programmatic consumers) remains `index.ts`; this module
|
|
13
|
+
* is purely the self-running browser bundle entry.
|
|
14
|
+
*
|
|
15
|
+
* @module @memberjunction/realtime-widget
|
|
16
|
+
*/
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=embed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embed.d.ts","sourceRoot":"","sources":["../src/embed.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG"}
|
package/dist/embed.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Browser EMBED entry — the bundle the host page loads via `<script src="mj-widget.js">`.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately TINY and free of static value-imports of the heavy modules (the runtime transport and
|
|
5
|
+
* the realtime voice stack). It only defines the custom element and auto-bootstraps from the host's
|
|
6
|
+
* `[data-widget-key]` element; the loader then DYNAMICALLY imports the transport (on mount) and the
|
|
7
|
+
* voice controller (only for Voice/Both widgets). Built with `esbuild --splitting`, this keeps
|
|
8
|
+
* `@memberjunction/graphql-dataprovider` + `@memberjunction/conversations-runtime` and
|
|
9
|
+
* `@memberjunction/ai-realtime-client` in separate chunks that load on demand — so the launcher
|
|
10
|
+
* paints fast and a text-only widget never downloads the voice chunk.
|
|
11
|
+
*
|
|
12
|
+
* The npm public API (with static exports for programmatic consumers) remains `index.ts`; this module
|
|
13
|
+
* is purely the self-running browser bundle entry.
|
|
14
|
+
*
|
|
15
|
+
* @module @memberjunction/realtime-widget
|
|
16
|
+
*/
|
|
17
|
+
import { defineSupportWidgetElement } from './ui/support-widget-element.js';
|
|
18
|
+
import { bootstrapFromDocument } from './loader.js';
|
|
19
|
+
// When loaded in a browser, register the element and attempt auto-bootstrap from the host's
|
|
20
|
+
// [data-widget-key] element. No-ops cleanly server-side or when nothing matches.
|
|
21
|
+
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
|
22
|
+
defineSupportWidgetElement();
|
|
23
|
+
const run = () => {
|
|
24
|
+
void bootstrapFromDocument();
|
|
25
|
+
};
|
|
26
|
+
if (document.readyState === 'loading') {
|
|
27
|
+
document.addEventListener('DOMContentLoaded', run, { once: true });
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
run();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=embed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embed.js","sourceRoot":"","sources":["../src/embed.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;AAC5E,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEpD,4FAA4F;AAC5F,iFAAiF;AACjF,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC;IACnE,0BAA0B,EAAE,CAAC;IAC7B,MAAM,GAAG,GAAG,GAAS,EAAE;QACnB,KAAK,qBAAqB,EAAE,CAAC;IACjC,CAAC,CAAC;IACF,IAAI,QAAQ,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QACpC,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IACvE,CAAC;SAAM,CAAC;QACJ,GAAG,EAAE,CAAC;IACV,CAAC;AACL,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Public API for @memberjunction/realtime-widget + auto-bootstrap when loaded
|
|
3
|
+
* as a browser bundle. Only exports symbols defined in this package (no cross-package
|
|
4
|
+
* re-exports — CLAUDE rule 5).
|
|
5
|
+
*
|
|
6
|
+
* @module @memberjunction/realtime-widget
|
|
7
|
+
*/
|
|
8
|
+
export * from './types.js';
|
|
9
|
+
export { WidgetSessionClient, type FetchLike } from './session/widget-session-client.js';
|
|
10
|
+
export type { IWidgetTransport, WidgetProgressCallback, WidgetTurnResult } from './transport/widget-transport.js';
|
|
11
|
+
export { MockWidgetTransport } from './transport/mock-widget-transport.js';
|
|
12
|
+
export { RuntimeWidgetTransport } from './transport/runtime-widget-transport.js';
|
|
13
|
+
export type { IVoiceController, VoiceControllerCallbacks, WidgetVoiceState, WidgetVoiceTranscript, } from './voice/voice-controller.js';
|
|
14
|
+
export { MockVoiceController } from './voice/mock-voice-controller.js';
|
|
15
|
+
export { RealtimeVoiceController, type VoiceMintFn, type VoiceMintResult, } from './voice/realtime-voice-controller.js';
|
|
16
|
+
export { VoiceAbuseGuard, DEFAULT_VOICE_LIMITS, type VoiceAbuseLimits, type VoiceAbortReason, } from './voice/voice-abuse-guard.js';
|
|
17
|
+
export { BaseWidgetChannel, type WidgetChannelToolDefinition, type WidgetChannelContext, } from './voice/channels/base-widget-channel.js';
|
|
18
|
+
export { WidgetChannelHost, type ChannelToolRouteResult } from './voice/channels/widget-channel-host.js';
|
|
19
|
+
export { WidgetWhiteboardChannel, LoadWidgetWhiteboardChannel } from './voice/channels/whiteboard-channel.js';
|
|
20
|
+
export { SupportWidgetElement, defineSupportWidgetElement, WIDGET_TAG_NAME } from './ui/support-widget-element.js';
|
|
21
|
+
export { WIDGET_SHADOW_STYLES } from './ui/tokens.js';
|
|
22
|
+
export { mountWidget, bootstrapFromDocument, type WidgetMountDeps } from './loader.js';
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,mBAAmB,EAAE,KAAK,SAAS,EAAE,MAAM,oCAAoC,CAAC;AACzF,YAAY,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAClH,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAC3E,OAAO,EAAE,sBAAsB,EAAE,MAAM,yCAAyC,CAAC;AACjF,YAAY,EACR,gBAAgB,EAChB,wBAAwB,EACxB,gBAAgB,EAChB,qBAAqB,GACxB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,EACH,uBAAuB,EACvB,KAAK,WAAW,EAChB,KAAK,eAAe,GACvB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EACH,eAAe,EACf,oBAAoB,EACpB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,GACxB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACH,iBAAiB,EACjB,KAAK,2BAA2B,EAChC,KAAK,oBAAoB,GAC5B,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAE,iBAAiB,EAAE,KAAK,sBAAsB,EAAE,MAAM,yCAAyC,CAAC;AACzG,OAAO,EAAE,uBAAuB,EAAE,2BAA2B,EAAE,MAAM,wCAAwC,CAAC;AAC9G,OAAO,EAAE,oBAAoB,EAAE,0BAA0B,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACnH,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,qBAAqB,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Public API for @memberjunction/realtime-widget + auto-bootstrap when loaded
|
|
3
|
+
* as a browser bundle. Only exports symbols defined in this package (no cross-package
|
|
4
|
+
* re-exports — CLAUDE rule 5).
|
|
5
|
+
*
|
|
6
|
+
* @module @memberjunction/realtime-widget
|
|
7
|
+
*/
|
|
8
|
+
export * from './types.js';
|
|
9
|
+
export { WidgetSessionClient } from './session/widget-session-client.js';
|
|
10
|
+
export { MockWidgetTransport } from './transport/mock-widget-transport.js';
|
|
11
|
+
export { RuntimeWidgetTransport } from './transport/runtime-widget-transport.js';
|
|
12
|
+
export { MockVoiceController } from './voice/mock-voice-controller.js';
|
|
13
|
+
export { RealtimeVoiceController, } from './voice/realtime-voice-controller.js';
|
|
14
|
+
export { VoiceAbuseGuard, DEFAULT_VOICE_LIMITS, } from './voice/voice-abuse-guard.js';
|
|
15
|
+
export { BaseWidgetChannel, } from './voice/channels/base-widget-channel.js';
|
|
16
|
+
export { WidgetChannelHost } from './voice/channels/widget-channel-host.js';
|
|
17
|
+
export { WidgetWhiteboardChannel, LoadWidgetWhiteboardChannel } from './voice/channels/whiteboard-channel.js';
|
|
18
|
+
export { SupportWidgetElement, defineSupportWidgetElement, WIDGET_TAG_NAME } from './ui/support-widget-element.js';
|
|
19
|
+
export { WIDGET_SHADOW_STYLES } from './ui/tokens.js';
|
|
20
|
+
export { mountWidget, bootstrapFromDocument } from './loader.js';
|
|
21
|
+
import { defineSupportWidgetElement } from './ui/support-widget-element.js';
|
|
22
|
+
import { bootstrapFromDocument } from './loader.js';
|
|
23
|
+
// When loaded in a browser, register the element and attempt auto-bootstrap from the
|
|
24
|
+
// host's [data-widget-key] element. No-ops cleanly server-side or when nothing matches.
|
|
25
|
+
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
|
26
|
+
defineSupportWidgetElement();
|
|
27
|
+
const run = () => {
|
|
28
|
+
void bootstrapFromDocument();
|
|
29
|
+
};
|
|
30
|
+
if (document.readyState === 'loading') {
|
|
31
|
+
document.addEventListener('DOMContentLoaded', run, { once: true });
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
run();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,mBAAmB,EAAkB,MAAM,oCAAoC,CAAC;AAEzF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAC3E,OAAO,EAAE,sBAAsB,EAAE,MAAM,yCAAyC,CAAC;AAOjF,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,EACH,uBAAuB,GAG1B,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EACH,eAAe,EACf,oBAAoB,GAGvB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACH,iBAAiB,GAGpB,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAE,iBAAiB,EAA+B,MAAM,yCAAyC,CAAC;AACzG,OAAO,EAAE,uBAAuB,EAAE,2BAA2B,EAAE,MAAM,wCAAwC,CAAC;AAC9G,OAAO,EAAE,oBAAoB,EAAE,0BAA0B,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACnH,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,qBAAqB,EAAwB,MAAM,aAAa,CAAC;AAEvF,OAAO,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;AAC5E,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEpD,qFAAqF;AACrF,wFAAwF;AACxF,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC;IACnE,0BAA0B,EAAE,CAAC;IAC7B,MAAM,GAAG,GAAG,GAAS,EAAE;QACnB,KAAK,qBAAqB,EAAE,CAAC;IACjC,CAAC,CAAC;IACF,IAAI,QAAQ,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QACpC,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IACvE,CAAC;SAAM,CAAC;QACJ,GAAG,EAAE,CAAC;IACV,CAAC;AACL,CAAC"}
|
package/dist/loader.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The embed loader. Reads `data-widget-key` / `data-api-url` from the
|
|
3
|
+
* host page (the mount div or the script tag), mints a guest session, mounts the
|
|
4
|
+
* `<mj-support-widget>` element, registers the runtime notification adapter scoped to
|
|
5
|
+
* the widget, and schedules token refresh. This is the single entry point the
|
|
6
|
+
* `mj-widget.js` bundle auto-runs.
|
|
7
|
+
*
|
|
8
|
+
* @module @memberjunction/realtime-widget
|
|
9
|
+
*/
|
|
10
|
+
import type { WidgetMountOptions, WidgetSession } from './types.js';
|
|
11
|
+
import { WidgetSessionClient } from './session/widget-session-client.js';
|
|
12
|
+
import type { IWidgetTransport } from './transport/widget-transport.js';
|
|
13
|
+
import type { IVoiceController } from './voice/voice-controller.js';
|
|
14
|
+
import { SupportWidgetElement } from './ui/support-widget-element.js';
|
|
15
|
+
/** Optional injection points so the loader is unit-testable without a network/runtime. */
|
|
16
|
+
export interface WidgetMountDeps {
|
|
17
|
+
/** Builds the session client (defaults to the real fetch-based one). */
|
|
18
|
+
sessionClientFactory?: (apiUrl: string, widgetKey: string) => WidgetSessionClient;
|
|
19
|
+
/** Builds the transport (defaults to RuntimeWidgetTransport; tests inject a mock). May be async (the default lazy-loads). */
|
|
20
|
+
transportFactory?: (apiUrl: string) => IWidgetTransport | Promise<IWidgetTransport>;
|
|
21
|
+
/** Builds the voice controller for voice-enabled instances (defaults to RealtimeVoiceController). May be async (the default lazy-loads). */
|
|
22
|
+
voiceControllerFactory?: (session: WidgetSession) => IVoiceController | Promise<IVoiceController>;
|
|
23
|
+
/** Schedules refresh (defaults to setTimeout); tests can stub. */
|
|
24
|
+
scheduler?: (cb: () => void, delayMs: number) => void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Mints a guest session and mounts a configured widget element. Returns the element.
|
|
28
|
+
*/
|
|
29
|
+
export declare function mountWidget(options: WidgetMountOptions, deps?: WidgetMountDeps): Promise<SupportWidgetElement>;
|
|
30
|
+
/** Reads data-attributes from the host and mounts. Safe to call once on script load. */
|
|
31
|
+
export declare function bootstrapFromDocument(doc?: Document): Promise<SupportWidgetElement | null>;
|
|
32
|
+
//# sourceMappingURL=loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AAEzE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAEpE,OAAO,EAAE,oBAAoB,EAA+C,MAAM,gCAAgC,CAAC;AAEnH,0FAA0F;AAC1F,MAAM,WAAW,eAAe;IAC5B,wEAAwE;IACxE,oBAAoB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,mBAAmB,CAAC;IAClF,6HAA6H;IAC7H,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACpF,4IAA4I;IAC5I,sBAAsB,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAClG,kEAAkE;IAClE,SAAS,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACzD;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,kBAAkB,EAAE,IAAI,GAAE,eAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CA2CxH;AAED,wFAAwF;AACxF,wBAAsB,qBAAqB,CAAC,GAAG,GAAE,QAAmB,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAe1G"}
|
package/dist/loader.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The embed loader. Reads `data-widget-key` / `data-api-url` from the
|
|
3
|
+
* host page (the mount div or the script tag), mints a guest session, mounts the
|
|
4
|
+
* `<mj-support-widget>` element, registers the runtime notification adapter scoped to
|
|
5
|
+
* the widget, and schedules token refresh. This is the single entry point the
|
|
6
|
+
* `mj-widget.js` bundle auto-runs.
|
|
7
|
+
*
|
|
8
|
+
* @module @memberjunction/realtime-widget
|
|
9
|
+
*/
|
|
10
|
+
import { WidgetSessionClient } from './session/widget-session-client.js';
|
|
11
|
+
import { readVisitorKey, writeVisitorKey, clearVisitorKey } from './session/visitor-key-cookie.js';
|
|
12
|
+
import { DEFAULT_VOICE_LIMITS } from './voice/voice-abuse-guard.js';
|
|
13
|
+
import { defineSupportWidgetElement, WIDGET_TAG_NAME } from './ui/support-widget-element.js';
|
|
14
|
+
/**
|
|
15
|
+
* Mints a guest session and mounts a configured widget element. Returns the element.
|
|
16
|
+
*/
|
|
17
|
+
export async function mountWidget(options, deps = {}) {
|
|
18
|
+
defineSupportWidgetElement();
|
|
19
|
+
const client = (deps.sessionClientFactory ?? defaultSessionClient)(options.apiUrl, options.widgetKey);
|
|
20
|
+
// Returning-visitor anchor (RV1): present the durable cookie (if any) so the server can chain this
|
|
21
|
+
// visit to the visitor's prior conversation. Gated server-side on the widget's RememberReturningVisitors
|
|
22
|
+
// toggle — when off, the server returns no visitorKey and we set no cookie below.
|
|
23
|
+
const presentedVisitorKey = readVisitorKey(options.widgetKey);
|
|
24
|
+
const session = await client.Mint(presentedVisitorKey);
|
|
25
|
+
if (session.rememberReturningVisitors && session.visitorKey) {
|
|
26
|
+
writeVisitorKey(options.widgetKey, session.visitorKey);
|
|
27
|
+
}
|
|
28
|
+
const transport = await (deps.transportFactory ?? defaultTransport)(options.apiUrl);
|
|
29
|
+
await transport.Initialize(session);
|
|
30
|
+
const element = document.createElement(WIDGET_TAG_NAME);
|
|
31
|
+
element.Configure({ title: options.title, greeting: options.greeting });
|
|
32
|
+
element.SetSession(session);
|
|
33
|
+
element.SetTransport(transport);
|
|
34
|
+
if (session.modality === 'Voice' || session.modality === 'Both') {
|
|
35
|
+
element.SetVoiceController(await (deps.voiceControllerFactory ?? defaultVoiceController)(session));
|
|
36
|
+
}
|
|
37
|
+
// RV5 "forget me": only wired when the widget remembers returning visitors and a durable key exists.
|
|
38
|
+
// Archives the visitor's server-side memory, then clears the first-party cookie so no linkage remains.
|
|
39
|
+
if (session.rememberReturningVisitors && session.visitorKey) {
|
|
40
|
+
const visitorKey = session.visitorKey;
|
|
41
|
+
element.SetForgetHandler(async () => {
|
|
42
|
+
const result = await client.Forget(visitorKey);
|
|
43
|
+
if (!result.success) {
|
|
44
|
+
throw new Error(result.error ?? 'forget failed');
|
|
45
|
+
}
|
|
46
|
+
clearVisitorKey(options.widgetKey);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
resolveMountTarget(options.mountTarget).appendChild(element);
|
|
50
|
+
registerNotificationAdapter(element);
|
|
51
|
+
scheduleRefresh(client, transport, element, session, deps.scheduler ?? defaultScheduler);
|
|
52
|
+
return element;
|
|
53
|
+
}
|
|
54
|
+
/** Reads data-attributes from the host and mounts. Safe to call once on script load. */
|
|
55
|
+
export async function bootstrapFromDocument(doc = document) {
|
|
56
|
+
const mountEl = doc.querySelector('[data-widget-key]');
|
|
57
|
+
const widgetKey = mountEl?.dataset.widgetKey;
|
|
58
|
+
const apiUrl = mountEl?.dataset.apiUrl;
|
|
59
|
+
if (!mountEl || !widgetKey || !apiUrl) {
|
|
60
|
+
// Nothing to mount — the host hasn't placed a configured element. Not an error.
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return mountWidget({
|
|
64
|
+
widgetKey,
|
|
65
|
+
apiUrl,
|
|
66
|
+
mountTarget: mountEl,
|
|
67
|
+
title: mountEl.dataset.title,
|
|
68
|
+
greeting: mountEl.dataset.greeting,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function defaultSessionClient(apiUrl, widgetKey) {
|
|
72
|
+
return new WidgetSessionClient(apiUrl, widgetKey);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Default transport. DYNAMIC import (CLAUDE rule 8, category 3 — bundle-size deferral): the runtime
|
|
76
|
+
* transport pulls in `@memberjunction/graphql-dataprovider` + `@memberjunction/conversations-runtime`
|
|
77
|
+
* (the heaviest dependency in the bundle). Loading it on demand keeps it OUT of the embed entry chunk
|
|
78
|
+
* so the launcher button paints before the runtime is fetched. Declared in `dependencies`.
|
|
79
|
+
*/
|
|
80
|
+
async function defaultTransport(apiUrl) {
|
|
81
|
+
const { RuntimeWidgetTransport } = await import('./transport/runtime-widget-transport.js');
|
|
82
|
+
return new RuntimeWidgetTransport(apiUrl);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Default voice controller. DYNAMIC import (CLAUDE rule 8, category 3): the realtime controller pulls
|
|
86
|
+
* in `@memberjunction/ai-realtime-client` + provider drivers, needed ONLY for Voice/Both widgets — so
|
|
87
|
+
* a text-only widget never pays for the voice chunk. Declared in `dependencies`.
|
|
88
|
+
*/
|
|
89
|
+
async function defaultVoiceController(session) {
|
|
90
|
+
const [{ RealtimeVoiceController }, { createGuestVoiceMint }, { createGuestToolRelay }] = await Promise.all([
|
|
91
|
+
import('./voice/realtime-voice-controller.js'),
|
|
92
|
+
import('./voice/guest-voice-mint.js'),
|
|
93
|
+
import('./voice/guest-tool-relay.js'),
|
|
94
|
+
]);
|
|
95
|
+
return new RealtimeVoiceController(createGuestVoiceMint(session), voiceLimitsForSession(session),
|
|
96
|
+
// Phase 2: the interactive channels this widget may attach (Whiteboard, …) + the live relay for
|
|
97
|
+
// non-channel tool calls. Empty enabledChannels (the default) leaves voice behavior unchanged.
|
|
98
|
+
session.enabledChannels, createGuestToolRelay());
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Derives the client-side voice ceilings from the widget instance's configured
|
|
102
|
+
* VoiceMaxSessionMinutes (surfaced on the minted session). When the deployment sets no
|
|
103
|
+
* per-widget cap, the guard's built-in defaults apply. This is defense-in-depth — the
|
|
104
|
+
* authoritative cap is enforced server-side at the realtime mint (the ephemeral session's
|
|
105
|
+
* bounded TTL), so a tampered client cannot exceed the deployment's limit.
|
|
106
|
+
*/
|
|
107
|
+
function voiceLimitsForSession(session) {
|
|
108
|
+
if (session.voiceMaxSessionMinutes && session.voiceMaxSessionMinutes > 0) {
|
|
109
|
+
return { ...DEFAULT_VOICE_LIMITS, maxSessionMinutes: session.voiceMaxSessionMinutes };
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
function defaultScheduler(cb, delayMs) {
|
|
114
|
+
setTimeout(cb, delayMs);
|
|
115
|
+
}
|
|
116
|
+
/** Resolves a selector/element/undefined to the mount parent (falls back to <body>). */
|
|
117
|
+
function resolveMountTarget(target) {
|
|
118
|
+
if (target instanceof HTMLElement)
|
|
119
|
+
return target;
|
|
120
|
+
if (typeof target === 'string') {
|
|
121
|
+
const found = document.querySelector(target);
|
|
122
|
+
if (found)
|
|
123
|
+
return found;
|
|
124
|
+
}
|
|
125
|
+
return document.body;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Routes runtime warnings/errors into the widget transcript as system lines. DYNAMIC import
|
|
129
|
+
* (CLAUDE rule 8, category 3): `ConversationsRuntime` is part of the heavy runtime chunk; importing
|
|
130
|
+
* it here (rather than statically at module top) keeps it out of the embed entry chunk. Fire-and-forget
|
|
131
|
+
* — adapter registration is best-effort and must not block the mount. Declared in `dependencies`.
|
|
132
|
+
*/
|
|
133
|
+
function registerNotificationAdapter(element) {
|
|
134
|
+
void import('@memberjunction/conversations-runtime').then(({ ConversationsRuntime }) => {
|
|
135
|
+
ConversationsRuntime.Instance.UseNotificationAdapter({
|
|
136
|
+
Notify: (level, message) => {
|
|
137
|
+
if (level === 'error' || level === 'warning') {
|
|
138
|
+
element.ShowSystemMessage(message);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/** Refreshes the guest token shortly before expiry and re-arms the timer. */
|
|
145
|
+
function scheduleRefresh(client, transport, element, session, scheduler) {
|
|
146
|
+
const delay = WidgetSessionClient.MsUntilRefresh(session, Date.now());
|
|
147
|
+
scheduler(() => {
|
|
148
|
+
void (async () => {
|
|
149
|
+
try {
|
|
150
|
+
const refreshed = await client.Refresh(session.visitorKey);
|
|
151
|
+
transport.UpdateToken(refreshed.token);
|
|
152
|
+
scheduleRefresh(client, transport, element, refreshed, scheduler);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// A failed refresh leaves the current (soon-to-expire) token. Surface the
|
|
156
|
+
// connection-lost banner so the visitor can retry the refresh; on success
|
|
157
|
+
// the banner clears and the refresh timer re-arms (graceful degradation, W6).
|
|
158
|
+
element.ShowConnectionError('Connection to support was lost.', () => scheduleRefresh(client, transport, element, session, scheduler));
|
|
159
|
+
}
|
|
160
|
+
})();
|
|
161
|
+
}, delay);
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The durable returning-visitor anchor (RV3): a long-lived, opaque,
|
|
3
|
+
* first-party cookie holding the server-minted `VisitorKey`. This is identity/auth
|
|
4
|
+
* state (a visitor anchor), NOT a user preference — so a cookie is the right mechanism
|
|
5
|
+
* (analogous to the auth-token exception, CLAUDE rule 9), and it must survive page
|
|
6
|
+
* close (unlike the in-memory guest token). It is ONLY ever set when the widget's
|
|
7
|
+
* `RememberReturningVisitors` toggle is on; the "forget me" control clears it (RV5).
|
|
8
|
+
*
|
|
9
|
+
* The cookie is scoped per widget key so two widgets on the same origin don't share a
|
|
10
|
+
* visitor identity. The value is the opaque base64url key the server returns.
|
|
11
|
+
*
|
|
12
|
+
* @module @memberjunction/realtime-widget
|
|
13
|
+
*/
|
|
14
|
+
/** Reads the persisted VisitorKey for this widget, or undefined when absent / no document. */
|
|
15
|
+
export declare function readVisitorKey(widgetKey: string): string | undefined;
|
|
16
|
+
/** Persists the VisitorKey as a long-lived first-party cookie (Secure on https, SameSite=Lax). */
|
|
17
|
+
export declare function writeVisitorKey(widgetKey: string, visitorKey: string): void;
|
|
18
|
+
/** Clears the persisted VisitorKey (RV5 "forget me"). Expires the cookie immediately. */
|
|
19
|
+
export declare function clearVisitorKey(widgetKey: string): void;
|
|
20
|
+
//# sourceMappingURL=visitor-key-cookie.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visitor-key-cookie.d.ts","sourceRoot":"","sources":["../../src/session/visitor-key-cookie.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAcH,8FAA8F;AAC9F,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAiBpE;AAED,kGAAkG;AAClG,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAQ3E;AAED,yFAAyF;AACzF,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAKvD"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The durable returning-visitor anchor (RV3): a long-lived, opaque,
|
|
3
|
+
* first-party cookie holding the server-minted `VisitorKey`. This is identity/auth
|
|
4
|
+
* state (a visitor anchor), NOT a user preference — so a cookie is the right mechanism
|
|
5
|
+
* (analogous to the auth-token exception, CLAUDE rule 9), and it must survive page
|
|
6
|
+
* close (unlike the in-memory guest token). It is ONLY ever set when the widget's
|
|
7
|
+
* `RememberReturningVisitors` toggle is on; the "forget me" control clears it (RV5).
|
|
8
|
+
*
|
|
9
|
+
* The cookie is scoped per widget key so two widgets on the same origin don't share a
|
|
10
|
+
* visitor identity. The value is the opaque base64url key the server returns.
|
|
11
|
+
*
|
|
12
|
+
* @module @memberjunction/realtime-widget
|
|
13
|
+
*/
|
|
14
|
+
/** Cookie name prefix; the (sanitized) widget key is appended to scope it per deployment. */
|
|
15
|
+
const COOKIE_PREFIX = 'mjwv_';
|
|
16
|
+
/** Default cookie lifetime. The authoritative retention lives server-side (VisitorMemoryRetentionDays);
|
|
17
|
+
* this is just how long the browser keeps presenting the anchor. One year, refreshed on each visit. */
|
|
18
|
+
const COOKIE_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;
|
|
19
|
+
/** Per-deployment cookie name; widget keys are restricted to a cookie-name-safe charset. */
|
|
20
|
+
function cookieName(widgetKey) {
|
|
21
|
+
return COOKIE_PREFIX + widgetKey.replace(/[^A-Za-z0-9_-]/g, '');
|
|
22
|
+
}
|
|
23
|
+
/** Reads the persisted VisitorKey for this widget, or undefined when absent / no document. */
|
|
24
|
+
export function readVisitorKey(widgetKey) {
|
|
25
|
+
if (typeof document === 'undefined' || !document.cookie) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
const name = cookieName(widgetKey) + '=';
|
|
29
|
+
for (const part of document.cookie.split(';')) {
|
|
30
|
+
const trimmed = part.trim();
|
|
31
|
+
if (trimmed.startsWith(name)) {
|
|
32
|
+
const raw = trimmed.slice(name.length);
|
|
33
|
+
try {
|
|
34
|
+
return decodeURIComponent(raw) || undefined;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return raw || undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
/** Persists the VisitorKey as a long-lived first-party cookie (Secure on https, SameSite=Lax). */
|
|
44
|
+
export function writeVisitorKey(widgetKey, visitorKey) {
|
|
45
|
+
if (typeof document === 'undefined' || !visitorKey) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const secure = typeof location !== 'undefined' && location.protocol === 'https:' ? '; Secure' : '';
|
|
49
|
+
document.cookie =
|
|
50
|
+
`${cookieName(widgetKey)}=${encodeURIComponent(visitorKey)}` +
|
|
51
|
+
`; Max-Age=${COOKIE_MAX_AGE_SECONDS}; Path=/; SameSite=Lax${secure}`;
|
|
52
|
+
}
|
|
53
|
+
/** Clears the persisted VisitorKey (RV5 "forget me"). Expires the cookie immediately. */
|
|
54
|
+
export function clearVisitorKey(widgetKey) {
|
|
55
|
+
if (typeof document === 'undefined') {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
document.cookie = `${cookieName(widgetKey)}=; Max-Age=0; Path=/; SameSite=Lax`;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=visitor-key-cookie.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visitor-key-cookie.js","sourceRoot":"","sources":["../../src/session/visitor-key-cookie.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,6FAA6F;AAC7F,MAAM,aAAa,GAAG,OAAO,CAAC;AAE9B;wGACwG;AACxG,MAAM,sBAAsB,GAAG,GAAG,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAElD,4FAA4F;AAC5F,SAAS,UAAU,CAAC,SAAiB;IACjC,OAAO,aAAa,GAAG,SAAS,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC,CAAC;AACpE,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,cAAc,CAAC,SAAiB;IAC5C,IAAI,OAAO,QAAQ,KAAK,WAAW,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;QACtD,OAAO,SAAS,CAAC;IACrB,CAAC;IACD,MAAM,IAAI,GAAG,UAAU,CAAC,SAAS,CAAC,GAAG,GAAG,CAAC;IACzC,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5C,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3B,MAAM,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACvC,IAAI,CAAC;gBACD,OAAO,kBAAkB,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC;YAChD,CAAC;YAAC,MAAM,CAAC;gBACL,OAAO,GAAG,IAAI,SAAS,CAAC;YAC5B,CAAC;QACL,CAAC;IACL,CAAC;IACD,OAAO,SAAS,CAAC;AACrB,CAAC;AAED,kGAAkG;AAClG,MAAM,UAAU,eAAe,CAAC,SAAiB,EAAE,UAAkB;IACjE,IAAI,OAAO,QAAQ,KAAK,WAAW,IAAI,CAAC,UAAU,EAAE,CAAC;QACjD,OAAO;IACX,CAAC;IACD,MAAM,MAAM,GAAG,OAAO,QAAQ,KAAK,WAAW,IAAI,QAAQ,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;IACnG,QAAQ,CAAC,MAAM;QACX,GAAG,UAAU,CAAC,SAAS,CAAC,IAAI,kBAAkB,CAAC,UAAU,CAAC,EAAE;YAC5D,aAAa,sBAAsB,yBAAyB,MAAM,EAAE,CAAC;AAC7E,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,eAAe,CAAC,SAAiB;IAC7C,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC;QAClC,OAAO;IACX,CAAC;IACD,QAAQ,CAAC,MAAM,GAAG,GAAG,UAAU,CAAC,SAAS,CAAC,oCAAoC,CAAC;AACnF,CAAC"}
|