@origonai/web-sdk 0.1.0 → 0.1.1
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 +55 -10
- package/dist/index.d.ts +90 -10
- package/dist/origon-web-sdk.js +1310 -1046
- package/dist/origon-web-sdk.js.map +1 -1
- package/docs/contract.md +59 -0
- package/docs/new-chat-protocol.md +21 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Origon Web SDK — chat and voice session client for browsers.
|
|
4
4
|
|
|
5
|
-
> **Status**: v0.
|
|
5
|
+
> **Status**: v0.6.0 (unpublished). Chat (orpc over WebTransport) and
|
|
6
6
|
> voice (WebTransport + WASM Opus) are both implemented — see
|
|
7
7
|
> [Status](#status). **WebTransport is required for both channels**;
|
|
8
8
|
> browsers without it (Safari, Firefox today) are unsupported.
|
|
@@ -25,6 +25,7 @@ const client = getSessionManager()
|
|
|
25
25
|
client.initialize({
|
|
26
26
|
endpoint: 'https://your-backend.example.com',
|
|
27
27
|
token: '<bearer>', // optional; omit for anonymous users
|
|
28
|
+
// attachmentBaseUrl: '/cx/core/chat/attachment', // authenticated agent lane
|
|
28
29
|
userId: '<your-id>', // optional; auto-generated UUID v7 if not provided
|
|
29
30
|
bundleId: 'com.example.app',
|
|
30
31
|
})
|
|
@@ -34,13 +35,13 @@ await client.authenticate()
|
|
|
34
35
|
client.setCallbacks({
|
|
35
36
|
onMessageAdded: (sessionId, channel, msg) => render(msg),
|
|
36
37
|
onMessageUpdated: (sessionId, channel, { id, message }) => update(id, message),
|
|
37
|
-
onTyping: (sessionId, channel,
|
|
38
|
+
onTyping: (sessionId, channel, update) => setTyping(update),
|
|
38
39
|
onDisconnected: (sessionId, channel, { reason }) => console.log('closed:', reason),
|
|
39
40
|
})
|
|
40
41
|
|
|
41
42
|
const { sessionId } = await client.startSession({ channel: 'chat' })
|
|
42
43
|
|
|
43
|
-
await client.sendMessage(sessionId, { text: 'Hello!' })
|
|
44
|
+
await client.sendMessage(sessionId, { text: 'Hello!' }) // audience defaults to all
|
|
44
45
|
|
|
45
46
|
client.notifyTyping(sessionId) // call on each keystroke; SDK debounces
|
|
46
47
|
|
|
@@ -57,7 +58,7 @@ await client.endSession(sessionId)
|
|
|
57
58
|
|
|
58
59
|
### Sessions
|
|
59
60
|
- `startSession({ channel, sessionId?, data? })` — originate a chat or voice session (issues `POST /session/start`)
|
|
60
|
-
- `joinSession({ channel, sessionId, url, token, voice? })` — attach to a session provisioned out of band, skipping `POST /session/start`; `voice.receiveOnly` starts playout with no microphone and awaits server uplink mute before connecting (see [Joining a provisioned session](#joining-a-provisioned-session))
|
|
61
|
+
- `joinSession({ channel, sessionId, url, token, chat?, voice? })` — attach to a session provisioned out of band, skipping `POST /session/start`; `chat.recovery` opts into caller-authorized server-replay recovery, while `voice.receiveOnly` starts playout with no microphone and awaits server uplink mute before connecting (see [Joining a provisioned session](#joining-a-provisioned-session))
|
|
61
62
|
- `endSession(sessionId)` — close a session
|
|
62
63
|
- `endAllSessions()` — close every active session
|
|
63
64
|
- `migrateSessionId(oldId, newId)` — re-key a live session whose id the control plane reassigned mid-call, without a media re-dial
|
|
@@ -67,18 +68,25 @@ await client.endSession(sessionId)
|
|
|
67
68
|
- `getSession(sessionId)` — GET `/session/{id}` (transcript + control)
|
|
68
69
|
|
|
69
70
|
### Chat
|
|
70
|
-
- `sendMessage(sessionId, payload)` — post a message
|
|
71
|
-
- `notifyTyping(sessionId)` — call on each keystroke; SDK debounces (3s window) and emits typing on/off automatically
|
|
71
|
+
- `sendMessage(sessionId, payload)` — post a message with optional `metadata: {audience}`; ordinary sends default to `all`. The SDK fires `onMessageAdded` immediately with `status: 'sending'`, then `onMessageUpdated` with `'delivered'` or `'failed'` after the server acks
|
|
72
|
+
- `notifyTyping(sessionId, metadata?)` — call on each keystroke; ordinary typing defaults to `all`, and the SDK debounces (3s window) and emits typing on/off automatically
|
|
72
73
|
- `stopTyping(sessionId)` — flush typing-off immediately
|
|
73
74
|
|
|
74
75
|
### Attachments
|
|
75
|
-
Attachments are
|
|
76
|
-
`endpoint`
|
|
77
|
-
first thing a visitor sends.
|
|
76
|
+
Attachments are not session-scoped. With no `attachmentBaseUrl`, both verbs use
|
|
77
|
+
the widget `endpoint` lane and suppress the top-level token, so a file can be the
|
|
78
|
+
first thing a visitor sends. Authenticated consumers set a same-origin top-level
|
|
79
|
+
`attachmentBaseUrl` (absolute or root-relative) together with the existing
|
|
80
|
+
top-level token; upload and delete then send it as a bearer. Missing token,
|
|
81
|
+
malformed, credential-bearing, query/hash,
|
|
82
|
+
or cross-origin bases are rejected during `initialize()`, before any network.
|
|
78
83
|
|
|
79
84
|
- `uploadAttachment(file, { uploadId, onProgress? })` — streamed upload with progress
|
|
80
85
|
- `deleteAttachment(idOrUploadId)` — dual-purpose: cancels in-flight uploads if `idOrUploadId` matches a pending `uploadId`; otherwise DELETEs the stored attachment
|
|
81
86
|
|
|
87
|
+
An uploaded `Attachment.url` is optional until the canonical message echo
|
|
88
|
+
returns the server URL. `localUrl` remains the client-only preview.
|
|
89
|
+
|
|
82
90
|
## Callbacks
|
|
83
91
|
|
|
84
92
|
`active` is directory liveness, not stored status. The SDK requires the field
|
|
@@ -93,7 +101,8 @@ consumer can host multiple sessions.
|
|
|
93
101
|
// Chat
|
|
94
102
|
onMessageAdded?: (sessionId, channel, message)
|
|
95
103
|
onMessageUpdated?: (sessionId, channel, { id, message })
|
|
96
|
-
onTyping?: (sessionId, channel,
|
|
104
|
+
onTyping?: (sessionId, channel, { participantId, role, userId, userName,
|
|
105
|
+
state, metadata: { audience } })
|
|
97
106
|
onSessionUpdated?: (sessionId, channel)
|
|
98
107
|
|
|
99
108
|
// Lifecycle (both channels — chat terminals land on onDisconnected too)
|
|
@@ -121,6 +130,11 @@ consumer can host multiple sessions.
|
|
|
121
130
|
The outbound `payload.role` is a local-only hint for the provisional
|
|
122
131
|
`onMessageAdded` row; it's stripped before POST.
|
|
123
132
|
|
|
133
|
+
Every message has required `metadata: { audience: 'internal' | 'all' }`.
|
|
134
|
+
Missing or unknown wire metadata is a protocol failure, not a compatibility
|
|
135
|
+
default. Typing callbacks carry the same required audience plus participant
|
|
136
|
+
identity; watchdogs are isolated by `participantId`.
|
|
137
|
+
|
|
124
138
|
## Errors
|
|
125
139
|
|
|
126
140
|
REST-path errors are `ClientError` with a structured shape:
|
|
@@ -191,6 +205,26 @@ client.setCallbacks({ onConnected, onPeerAttached, onDisconnected })
|
|
|
191
205
|
// `offer` = { sessionId, url, token }, delivered by your control channel.
|
|
192
206
|
await client.joinSession({ channel: 'voice', ...offer })
|
|
193
207
|
|
|
208
|
+
// Recoverable provisioned chat: the host re-runs its authenticated monitor
|
|
209
|
+
// start request. The retry array is explicit and finite; every successful
|
|
210
|
+
// refresh must return the same session id. CX Attach supplies full replay.
|
|
211
|
+
await client.joinSession({
|
|
212
|
+
channel: 'chat',
|
|
213
|
+
...monitorOffer,
|
|
214
|
+
chat: {
|
|
215
|
+
recovery: {
|
|
216
|
+
replay: 'server',
|
|
217
|
+
delaysMs: [0, 500, 1_000],
|
|
218
|
+
reauthorize: async ({ sessionId, attempt, generation, cause }) => {
|
|
219
|
+
const result = await refreshMonitor({ sessionId, attempt, generation, cause })
|
|
220
|
+
// Return `{sessionId,url,token}` or an authoritative terminal verdict:
|
|
221
|
+
// `{terminal:'ended'|'revoked'|'removed'|'capacity'}`.
|
|
222
|
+
return result
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
})
|
|
227
|
+
|
|
194
228
|
// Listen-only provisioned leg: starts the worklet + receive transport, never
|
|
195
229
|
// asks for microphone permission, and fires onConnected only after MuteOk.
|
|
196
230
|
await client.joinSession({
|
|
@@ -227,6 +261,17 @@ or unmute fails, the SDK returns to uplink-muted state and releases the local
|
|
|
227
261
|
microphone. A late permission result cannot revive capture after Listen or
|
|
228
262
|
disconnect wins.
|
|
229
263
|
|
|
264
|
+
`chat.recovery` likewise changes only provisioned chat joins. Transport errors,
|
|
265
|
+
clean FIN, `stream_overflow`, and an expired Attach credential serialize through
|
|
266
|
+
the caller's `reauthorize` callback. Each response is fenced by a monotonic
|
|
267
|
+
generation and must name the original session; changed-session credentials fail
|
|
268
|
+
closed. Attach provides the authoritative ordered replay and the SDK dedupes it
|
|
269
|
+
against already surfaced message ids, so this mode never calls visitor
|
|
270
|
+
`POST /session/start` or `GET /session/:id`. `ended`, `revoked`, `removed`,
|
|
271
|
+
`capacity`, and retry exhaustion are terminal. Close sends Leave only for a
|
|
272
|
+
currently live Attach; a server-ended session only clears retained typing/UI
|
|
273
|
+
state locally. Omitting `chat.recovery` preserves ordinary visitor recovery.
|
|
274
|
+
|
|
230
275
|
## Status
|
|
231
276
|
|
|
232
277
|
- ✅ **Phase 1 — Chat** — orpc over WebTransport (protobuf wire),
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ export declare interface Attachment {
|
|
|
2
2
|
id: string;
|
|
3
3
|
name: string;
|
|
4
4
|
contentType: string;
|
|
5
|
-
|
|
5
|
+
/** Server URL; absent on a provisional upload until the canonical echo. */
|
|
6
|
+
url?: string;
|
|
6
7
|
/**
|
|
7
8
|
* Client-only preview source (`blob:`, `file://`) the UI can render
|
|
8
9
|
* immediately. Stripped from outgoing wire bodies, preserved across
|
|
@@ -38,6 +39,9 @@ export declare interface AudioWorkletStats {
|
|
|
38
39
|
|
|
39
40
|
export declare type Channel = 'chat' | 'voice';
|
|
40
41
|
|
|
42
|
+
/** Audience carried explicitly on every live-monitoring message/typing frame. */
|
|
43
|
+
export declare type ChatAudience = 'internal' | 'all';
|
|
44
|
+
|
|
41
45
|
export declare class ClientError extends Error {
|
|
42
46
|
readonly kind: ClientErrorKind;
|
|
43
47
|
readonly status?: number;
|
|
@@ -90,6 +94,12 @@ export declare interface Credentials {
|
|
|
90
94
|
token?: string;
|
|
91
95
|
userId?: string;
|
|
92
96
|
attributes?: Record<string, unknown>;
|
|
97
|
+
/**
|
|
98
|
+
* Optional same-origin attachment route base for authenticated consumers
|
|
99
|
+
* (for example `/cx/core/chat/attachment`). When absent, uploads retain the
|
|
100
|
+
* widget endpoint and suppress any top-level token.
|
|
101
|
+
*/
|
|
102
|
+
attachmentBaseUrl?: string;
|
|
93
103
|
/**
|
|
94
104
|
* Optional absolute base URL the voice assets are fetched from
|
|
95
105
|
* (`<base>/audio/audio-processor.js` and
|
|
@@ -157,6 +167,8 @@ export declare interface JoinSessionOptions {
|
|
|
157
167
|
token: string;
|
|
158
168
|
/** Voice-only join behavior. Omit to retain the normal microphone join. */
|
|
159
169
|
voice?: VoiceJoinOptions;
|
|
170
|
+
/** Chat-only recovery for a session provisioned by another control plane. */
|
|
171
|
+
chat?: ProvisionedChatJoinOptions;
|
|
160
172
|
}
|
|
161
173
|
|
|
162
174
|
export declare interface Message {
|
|
@@ -177,6 +189,7 @@ export declare interface Message {
|
|
|
177
189
|
errorText?: string;
|
|
178
190
|
status: MessageStatus;
|
|
179
191
|
state: MessageState;
|
|
192
|
+
metadata: MessageMetadata;
|
|
180
193
|
/**
|
|
181
194
|
* Lifecycle action for a `role:'system'` message — `'queued' | 'joined' |
|
|
182
195
|
* 'ended'` (kept an open `string`: the server may add more; a distinct
|
|
@@ -230,6 +243,10 @@ export declare interface MessageCard {
|
|
|
230
243
|
buttons: MessageButton[];
|
|
231
244
|
}
|
|
232
245
|
|
|
246
|
+
export declare interface MessageMetadata {
|
|
247
|
+
audience: ChatAudience;
|
|
248
|
+
}
|
|
249
|
+
|
|
233
250
|
export declare type MessageRole = 'ai' | 'external' | 'user' | 'system';
|
|
234
251
|
|
|
235
252
|
export declare type MessageState = 'streaming' | 'completed';
|
|
@@ -249,6 +266,52 @@ export declare class OrpcError extends Error {
|
|
|
249
266
|
constructor(code: number, reason: string);
|
|
250
267
|
}
|
|
251
268
|
|
|
269
|
+
/** Additive options for an out-of-band provisioned chat join. */
|
|
270
|
+
export declare interface ProvisionedChatJoinOptions {
|
|
271
|
+
recovery?: ProvisionedChatRecoveryOptions;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Why a provisioned chat needs fresh credentials. */
|
|
275
|
+
export declare type ProvisionedChatRecoveryCause = 'transportError' | 'cleanFin' | 'streamOverflow' | 'unauthenticated';
|
|
276
|
+
|
|
277
|
+
/** Explicit, finite provisioned-chat recovery policy. */
|
|
278
|
+
export declare interface ProvisionedChatRecoveryOptions {
|
|
279
|
+
/**
|
|
280
|
+
* Re-run the caller's authenticated provision/start request. The SDK never
|
|
281
|
+
* substitutes visitor `/session/start` or history calls on this path.
|
|
282
|
+
*/
|
|
283
|
+
reauthorize: (request: ProvisionedChatRecoveryRequest) => Promise<ProvisionedChatRecoveryResult>;
|
|
284
|
+
/** CX Attach supplies the authoritative full replay; no client history read. */
|
|
285
|
+
replay: 'server';
|
|
286
|
+
/**
|
|
287
|
+
* Delay before each attempt, in milliseconds. Must contain 1–8 finite,
|
|
288
|
+
* non-negative values; the array length is the hard attempt bound.
|
|
289
|
+
*/
|
|
290
|
+
delaysMs: readonly number[];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Context passed to the caller-owned monitor start/reauthorization request. */
|
|
294
|
+
export declare interface ProvisionedChatRecoveryRequest {
|
|
295
|
+
sessionId: string;
|
|
296
|
+
/** One-based position in the caller's explicit retry schedule. */
|
|
297
|
+
attempt: number;
|
|
298
|
+
/** Monotonic fence for discarding a refresh that resolves after close. */
|
|
299
|
+
generation: number;
|
|
300
|
+
cause: ProvisionedChatRecoveryCause;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* A fresh same-session credential triple, or an authoritative terminal
|
|
305
|
+
* control-plane verdict. Thrown refresher errors are transient and consume the
|
|
306
|
+
* current retry slot.
|
|
307
|
+
*/
|
|
308
|
+
export declare type ProvisionedChatRecoveryResult = StartSessionResponse | {
|
|
309
|
+
terminal: ProvisionedChatTerminalReason;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
/** Terminal control-plane verdicts that must never enter the retry ladder. */
|
|
313
|
+
export declare type ProvisionedChatTerminalReason = 'ended' | 'revoked' | 'removed' | 'capacity';
|
|
314
|
+
|
|
252
315
|
/**
|
|
253
316
|
* Optional callbacks the SDK fires through `SessionManager.setCallbacks`.
|
|
254
317
|
* All callbacks receive `sessionId` and `channel` as their first two args
|
|
@@ -262,8 +325,8 @@ export declare interface SdkCallbacks {
|
|
|
262
325
|
id: string;
|
|
263
326
|
message: Message;
|
|
264
327
|
}) => void;
|
|
265
|
-
/**
|
|
266
|
-
onTyping?: (sessionId: string, channel: Channel,
|
|
328
|
+
/** Participant-aware inbound typing update (debounced by a per-participant watchdog). */
|
|
329
|
+
onTyping?: (sessionId: string, channel: Channel, update: TypingUpdate) => void;
|
|
267
330
|
/** Session id changed (e.g. server promoted a temp id, or reattach assigned a new one). */
|
|
268
331
|
onSessionUpdated?: (sessionId: string, channel: Channel) => void;
|
|
269
332
|
onConnected?: (sessionId: string, channel: Channel) => void;
|
|
@@ -295,6 +358,8 @@ export declare interface SendMessagePayload {
|
|
|
295
358
|
text?: string;
|
|
296
359
|
html?: string;
|
|
297
360
|
attachments?: Attachment[];
|
|
361
|
+
/** Defaults to `{ audience: 'all' }` for ordinary visitor compatibility. */
|
|
362
|
+
metadata?: MessageMetadata;
|
|
298
363
|
/**
|
|
299
364
|
* Local-only hint for the provisional `onMessageAdded` role. Defaults to
|
|
300
365
|
* `'external'` when absent; staff dashboards set `'user'`. Stripped before
|
|
@@ -333,6 +398,8 @@ export declare class SessionManager {
|
|
|
333
398
|
private sessionHttp;
|
|
334
399
|
private attributes;
|
|
335
400
|
private assetBaseUrl;
|
|
401
|
+
private attachmentBaseUrl;
|
|
402
|
+
private attachmentBearer;
|
|
336
403
|
private readonly dispatcher;
|
|
337
404
|
private readonly sessions;
|
|
338
405
|
private readonly pendingUploads;
|
|
@@ -387,7 +454,7 @@ export declare class SessionManager {
|
|
|
387
454
|
*/
|
|
388
455
|
migrateSessionId(oldId: string, newId: string): void;
|
|
389
456
|
sendMessage(sessionId: string, payload: SendMessagePayload): Promise<Message>;
|
|
390
|
-
notifyTyping(sessionId: string): void;
|
|
457
|
+
notifyTyping(sessionId: string, metadata?: TypingMetadata): void;
|
|
391
458
|
stopTyping(sessionId: string): void;
|
|
392
459
|
setMute(sessionId: string, scope: MuteScope): Promise<void>;
|
|
393
460
|
/** Enable microphone coaching on a receive-only voice join. */
|
|
@@ -402,18 +469,17 @@ export declare class SessionManager {
|
|
|
402
469
|
*/
|
|
403
470
|
subscribeAudioStats(sessionId: string, cb: (stats: AudioWorkletStats) => void): () => void;
|
|
404
471
|
/**
|
|
405
|
-
* Upload an attachment.
|
|
406
|
-
*
|
|
407
|
-
*
|
|
472
|
+
* Upload an attachment. It remains session-less: widget callers use the
|
|
473
|
+
* tokenless endpoint lane, while an explicit same-origin attachmentBaseUrl
|
|
474
|
+
* selects the authenticated agent lane and uses the top-level token.
|
|
408
475
|
* Needs `authenticate()` only for the local policy precheck (absent config
|
|
409
476
|
* defers the whole decision to the server).
|
|
410
477
|
*/
|
|
411
478
|
uploadAttachment(file: File, options: UploadOptions): Promise<Attachment>;
|
|
412
479
|
/**
|
|
413
480
|
* Dual purpose: cancel an in-flight upload (when `idOrUploadId` matches a
|
|
414
|
-
* pending `uploadId`) OR DELETE a stored attachment by id.
|
|
415
|
-
*
|
|
416
|
-
* before sending anything.
|
|
481
|
+
* pending `uploadId`) OR DELETE a stored attachment by id. The route/auth
|
|
482
|
+
* lane matches upload: explicit base is authenticated; widget is tokenless.
|
|
417
483
|
*/
|
|
418
484
|
deleteAttachment(idOrUploadId: string): Promise<void>;
|
|
419
485
|
private requireChatSession;
|
|
@@ -446,6 +512,20 @@ export declare interface StartSessionResponse {
|
|
|
446
512
|
token: string;
|
|
447
513
|
}
|
|
448
514
|
|
|
515
|
+
export declare interface TypingMetadata {
|
|
516
|
+
audience: ChatAudience;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** Participant-aware typing update; one participant never clears another. */
|
|
520
|
+
export declare interface TypingUpdate {
|
|
521
|
+
participantId: string;
|
|
522
|
+
role: string;
|
|
523
|
+
userId: string;
|
|
524
|
+
userName: string;
|
|
525
|
+
state: 'on' | 'off';
|
|
526
|
+
metadata: TypingMetadata;
|
|
527
|
+
}
|
|
528
|
+
|
|
449
529
|
export declare interface UploadOptions {
|
|
450
530
|
/** Caller-issued id used to cancel the upload via `delete_attachment`. */
|
|
451
531
|
uploadId: string;
|