@origonai/web-sdk 0.1.0 → 0.1.2

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 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.0 (unpublished). Chat (orpc over WebTransport) and
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, isTyping) => setTyping(isTyping),
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!' }) // participant server defaults audience 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; SDK fires `onMessageAdded` immediately with `status: 'sending'`, then `onMessageUpdated` with `'delivered'` or `'failed'` after the server acks
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 participant sends omit it and the server resolves the audience 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 participant typing omits metadata, 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 **widget-scoped, not session-scoped** — both verbs address the
76
- `endpoint` given to `initialize()` and need no live session, so a file can be the
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, isTyping)
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,14 @@ 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
+ Message metadata is optional at both levels:
134
+ `metadata?: { audience?: 'internal' | 'all' }`. Missing/null containers remain
135
+ absent, while an explicitly present empty object remains `{}`; unknown
136
+ non-empty audiences are protocol failures. A
137
+ monitor client must always send an explicit known audience even though the
138
+ shared SDK type is optional. Typing callbacks retain required audience plus
139
+ participant identity; watchdogs are isolated by `participantId`.
140
+
124
141
  ## Errors
125
142
 
126
143
  REST-path errors are `ClientError` with a structured shape:
@@ -191,6 +208,26 @@ client.setCallbacks({ onConnected, onPeerAttached, onDisconnected })
191
208
  // `offer` = { sessionId, url, token }, delivered by your control channel.
192
209
  await client.joinSession({ channel: 'voice', ...offer })
193
210
 
211
+ // Recoverable provisioned chat: the host re-runs its authenticated monitor
212
+ // start request. The retry array is explicit and finite; every successful
213
+ // refresh must return the same session id. CX Attach supplies full replay.
214
+ await client.joinSession({
215
+ channel: 'chat',
216
+ ...monitorOffer,
217
+ chat: {
218
+ recovery: {
219
+ replay: 'server',
220
+ delaysMs: [0, 500, 1_000],
221
+ reauthorize: async ({ sessionId, attempt, generation, cause }) => {
222
+ const result = await refreshMonitor({ sessionId, attempt, generation, cause })
223
+ // Return `{sessionId,url,token}` or an authoritative terminal verdict:
224
+ // `{terminal:'ended'|'revoked'|'removed'|'capacity'}`.
225
+ return result
226
+ },
227
+ },
228
+ },
229
+ })
230
+
194
231
  // Listen-only provisioned leg: starts the worklet + receive transport, never
195
232
  // asks for microphone permission, and fires onConnected only after MuteOk.
196
233
  await client.joinSession({
@@ -227,6 +264,17 @@ or unmute fails, the SDK returns to uplink-muted state and releases the local
227
264
  microphone. A late permission result cannot revive capture after Listen or
228
265
  disconnect wins.
229
266
 
267
+ `chat.recovery` likewise changes only provisioned chat joins. Transport errors,
268
+ clean FIN, `stream_overflow`, and an expired Attach credential serialize through
269
+ the caller's `reauthorize` callback. Each response is fenced by a monotonic
270
+ generation and must name the original session; changed-session credentials fail
271
+ closed. Attach provides the authoritative ordered replay and the SDK dedupes it
272
+ against already surfaced message ids, so this mode never calls visitor
273
+ `POST /session/start` or `GET /session/:id`. `ended`, `revoked`, `removed`,
274
+ `capacity`, and retry exhaustion are terminal. Close sends Leave only for a
275
+ currently live Attach; a server-ended session only clears retained typing/UI
276
+ state locally. Omitting `chat.recovery` preserves ordinary visitor recovery.
277
+
230
278
  ## Status
231
279
 
232
280
  - ✅ **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
- url: string;
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
+ /** Closed audience vocabulary; the field holding it may be absent. */
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
- /** Inbound typing indicator from the peer (debounced by SDK watchdog). */
266
- onTyping?: (sessionId: string, channel: Channel, isTyping: boolean) => void;
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
+ /** Optional for participant callers; monitor callers must supply an audience. */
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. WIDGET-scoped and session-less: the URL is built
406
- * from the `endpoint` given to `initialize()`, so an attachment can be the
407
- * first thing a visitor sends — no session is opened, and none is required.
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. Widget-scoped and
415
- * session-less on both arms, so a visitor can remove a file they attached
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;