@convai/web-sdk 1.8.0-beta.8 → 1.8.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.
Files changed (113) hide show
  1. package/README.md +45 -213
  2. package/dist/core/ConvaiClient.d.ts +1 -190
  3. package/dist/core/ConvaiClient.d.ts.map +1 -1
  4. package/dist/core/ConvaiClient.js +286 -1306
  5. package/dist/core/ConvaiClient.js.map +1 -1
  6. package/dist/core/MessageHandler.d.ts +1 -19
  7. package/dist/core/MessageHandler.d.ts.map +1 -1
  8. package/dist/core/MessageHandler.js +27 -378
  9. package/dist/core/MessageHandler.js.map +1 -1
  10. package/dist/core/connectRequest.d.ts +4 -17
  11. package/dist/core/connectRequest.d.ts.map +1 -1
  12. package/dist/core/connectRequest.js +17 -28
  13. package/dist/core/connectRequest.js.map +1 -1
  14. package/dist/core/index.d.ts +1 -8
  15. package/dist/core/index.d.ts.map +1 -1
  16. package/dist/core/index.js +1 -13
  17. package/dist/core/index.js.map +1 -1
  18. package/dist/core/logicalTurn.d.ts +6 -4
  19. package/dist/core/logicalTurn.d.ts.map +1 -1
  20. package/dist/core/logicalTurn.js +4 -29
  21. package/dist/core/logicalTurn.js.map +1 -1
  22. package/dist/core/modelOutput.d.ts.map +1 -1
  23. package/dist/core/modelOutput.js +2 -2
  24. package/dist/core/modelOutput.js.map +1 -1
  25. package/dist/core/types.d.ts +22 -860
  26. package/dist/core/types.d.ts.map +1 -1
  27. package/dist/core/types.js.map +1 -1
  28. package/dist/embed/ConvaiChatElement.d.ts +1 -77
  29. package/dist/embed/ConvaiChatElement.d.ts.map +1 -1
  30. package/dist/embed/ConvaiChatElement.js +54 -433
  31. package/dist/embed/ConvaiChatElement.js.map +1 -1
  32. package/dist/embed/chat-embed-v1.js +35 -136
  33. package/dist/embed/chat-embed-v1.js.map +4 -4
  34. package/dist/react/components/ConvaiWidget.d.ts.map +1 -1
  35. package/dist/react/components/ConvaiWidget.js +1 -6
  36. package/dist/react/components/ConvaiWidget.js.map +1 -1
  37. package/dist/react/components/rtc-widget/components/AudioVisualizer.d.ts.map +1 -1
  38. package/dist/react/components/rtc-widget/components/AudioVisualizer.js +1 -28
  39. package/dist/react/components/rtc-widget/components/AudioVisualizer.js.map +1 -1
  40. package/dist/react/components/rtc-widget/components/MarkdownRenderer.d.ts +0 -7
  41. package/dist/react/components/rtc-widget/components/MarkdownRenderer.d.ts.map +1 -1
  42. package/dist/react/components/rtc-widget/components/MarkdownRenderer.js +46 -135
  43. package/dist/react/components/rtc-widget/components/MarkdownRenderer.js.map +1 -1
  44. package/dist/react/components/rtc-widget/components/UserMessage.js +1 -1
  45. package/dist/react/components/rtc-widget/components/UserMessage.js.map +1 -1
  46. package/dist/react/components/rtc-widget/components/conviComponents/VoiceModeOverlay.d.ts.map +1 -1
  47. package/dist/react/components/rtc-widget/components/conviComponents/VoiceModeOverlay.js +49 -93
  48. package/dist/react/components/rtc-widget/components/conviComponents/VoiceModeOverlay.js.map +1 -1
  49. package/dist/react/hooks/useConvaiClient.d.ts.map +1 -1
  50. package/dist/react/hooks/useConvaiClient.js +2 -17
  51. package/dist/react/hooks/useConvaiClient.js.map +1 -1
  52. package/dist/react/index.d.ts +1 -4
  53. package/dist/react/index.d.ts.map +1 -1
  54. package/dist/react/index.js +1 -8
  55. package/dist/react/index.js.map +1 -1
  56. package/dist/vanilla/AudioRenderer.d.ts +1 -3
  57. package/dist/vanilla/AudioRenderer.d.ts.map +1 -1
  58. package/dist/vanilla/AudioRenderer.js +2 -8
  59. package/dist/vanilla/AudioRenderer.js.map +1 -1
  60. package/dist/vanilla/ConvaiWidget.d.ts.map +1 -1
  61. package/dist/vanilla/ConvaiWidget.js +302 -1122
  62. package/dist/vanilla/ConvaiWidget.js.map +1 -1
  63. package/dist/vanilla/WidgetEventSubscriptions.d.ts +1 -1
  64. package/dist/vanilla/WidgetEventSubscriptions.d.ts.map +1 -1
  65. package/dist/vanilla/WidgetEventSubscriptions.js +0 -5
  66. package/dist/vanilla/WidgetEventSubscriptions.js.map +1 -1
  67. package/dist/vanilla/icons.d.ts.map +1 -1
  68. package/dist/vanilla/icons.js +15 -62
  69. package/dist/vanilla/icons.js.map +1 -1
  70. package/dist/vanilla/index.d.ts +2 -6
  71. package/dist/vanilla/index.d.ts.map +1 -1
  72. package/dist/vanilla/index.js +1 -5
  73. package/dist/vanilla/index.js.map +1 -1
  74. package/dist/vanilla/styles.d.ts +1 -18
  75. package/dist/vanilla/styles.d.ts.map +1 -1
  76. package/dist/vanilla/styles.js +29 -141
  77. package/dist/vanilla/styles.js.map +1 -1
  78. package/dist/vanilla/types.d.ts +0 -74
  79. package/dist/vanilla/types.d.ts.map +1 -1
  80. package/dist/vanilla/types.js.map +1 -1
  81. package/dist/version.d.ts +1 -1
  82. package/dist/version.d.ts.map +1 -1
  83. package/dist/version.js +1 -1
  84. package/dist/version.js.map +1 -1
  85. package/package.json +17 -44
  86. package/dist/core/CharacterRoster.d.ts +0 -84
  87. package/dist/core/CharacterRoster.d.ts.map +0 -1
  88. package/dist/core/CharacterRoster.js +0 -348
  89. package/dist/core/CharacterRoster.js.map +0 -1
  90. package/dist/core/CharacterVersionManager.d.ts +0 -100
  91. package/dist/core/CharacterVersionManager.d.ts.map +0 -1
  92. package/dist/core/CharacterVersionManager.js +0 -227
  93. package/dist/core/CharacterVersionManager.js.map +0 -1
  94. package/dist/core/ConvaiRoomError.d.ts +0 -48
  95. package/dist/core/ConvaiRoomError.d.ts.map +0 -1
  96. package/dist/core/ConvaiRoomError.js +0 -111
  97. package/dist/core/ConvaiRoomError.js.map +0 -1
  98. package/dist/core/SSESession.d.ts +0 -59
  99. package/dist/core/SSESession.d.ts.map +0 -1
  100. package/dist/core/SSESession.js +0 -289
  101. package/dist/core/SSESession.js.map +0 -1
  102. package/dist/core/characterReference.d.ts +0 -40
  103. package/dist/core/characterReference.d.ts.map +0 -1
  104. package/dist/core/characterReference.js +0 -72
  105. package/dist/core/characterReference.js.map +0 -1
  106. package/dist/core/rosterRequest.d.ts +0 -57
  107. package/dist/core/rosterRequest.d.ts.map +0 -1
  108. package/dist/core/rosterRequest.js +0 -210
  109. package/dist/core/rosterRequest.js.map +0 -1
  110. package/dist/utils/inlineMarkdown.d.ts +0 -60
  111. package/dist/utils/inlineMarkdown.d.ts.map +0 -1
  112. package/dist/utils/inlineMarkdown.js +0 -183
  113. package/dist/utils/inlineMarkdown.js.map +0 -1
@@ -1,210 +0,0 @@
1
- import { isCharacterUuid } from "./characterReference.js";
2
- /**
3
- * Validation and request-body construction for multi-character rooms.
4
- *
5
- * Pure: no transport, no client state. Every rule below runs before a request
6
- * is built, so an illegal topology costs nothing and reports the rule it broke
7
- * rather than a 422 from the runtime.
8
- */
9
- /**
10
- * Server-wide roster cap. Checked client-side only to fail fast with a clear
11
- * message — the server stays authoritative, and a plan may configure a lower
12
- * `max_characters` that surfaces as 403 `MULTI_CHARACTER_ROSTER_LIMIT_EXCEEDED`.
13
- * This is an operational limit, not a protocol maximum, so it must not leak
14
- * into any public type.
15
- */
16
- export const MULTI_CHARACTER_MAX_CHARACTERS = 50;
17
- /** `shared_session_key` and `connect_attempt_id` share one charset. */
18
- const KEY_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
19
- /** True when this config asks for a multi-character room. */
20
- export function isRosterConfig(config) {
21
- return Array.isArray(config.characters) && config.characters.length > 0;
22
- }
23
- function assertKey(value, field) {
24
- if (value === undefined)
25
- return;
26
- if (!KEY_PATTERN.test(value)) {
27
- throw new Error(`${field} must be 1-128 characters of letters, digits, hyphen or underscore`);
28
- }
29
- }
30
- function nonblank(value) {
31
- return typeof value === "string" && value.trim().length > 0;
32
- }
33
- /**
34
- * Validate a roster create config.
35
- *
36
- * Throws on the first broken rule. Ordered so the most structural problems
37
- * (which topology did you mean?) report before the detail ones.
38
- */
39
- export function validateRosterConfig(config) {
40
- const characters = config.characters ?? [];
41
- if (config.characterId) {
42
- throw new Error("Pass either characterId or characters, not both");
43
- }
44
- if (characters.length === 0) {
45
- throw new Error("characters must contain at least one entry");
46
- }
47
- if (characters.length > MULTI_CHARACTER_MAX_CHARACTERS) {
48
- throw new Error(`characters accepts at most ${MULTI_CHARACTER_MAX_CHARACTERS} entries; ` +
49
- `your plan may allow fewer`);
50
- }
51
- const seenSessions = new Set();
52
- characters.forEach((spec, index) => {
53
- if (!spec || !nonblank(spec.characterId)) {
54
- throw new Error(`characters[${index}] requires a characterId`);
55
- }
56
- // A roster entry is typed as a bare UUID on the runtime. The version
57
- // selectors that single-character `characterId` has accepted since
58
- // 1.8.0-beta.5 are a 422 here, and the failure would otherwise only show
59
- // up as an opaque validation error from the server.
60
- if (!isCharacterUuid(spec.characterId.trim())) {
61
- throw new Error(`characters[${index}].characterId must be a bare character UUID; ` +
62
- `version selectors are not supported inside a roster`);
63
- }
64
- if (spec.characterSessionId !== undefined) {
65
- if (!nonblank(spec.characterSessionId)) {
66
- throw new Error(`characters[${index}].characterSessionId must be nonblank when supplied`);
67
- }
68
- if (seenSessions.has(spec.characterSessionId)) {
69
- throw new Error(`characters[${index}].characterSessionId is repeated; resume ids must be unique within the roster`);
70
- }
71
- seenSessions.add(spec.characterSessionId);
72
- }
73
- });
74
- if (config.characterSessionId) {
75
- throw new Error("characterSessionId is not valid with characters; use characters[].characterSessionId");
76
- }
77
- if (!nonblank(config.endUserId)) {
78
- throw new Error("characters requires a nonblank endUserId");
79
- }
80
- if (config.transport && config.transport !== "livekit") {
81
- throw new Error("Multi-character rooms require the LiveKit transport");
82
- }
83
- if (config.interactionApiUrl) {
84
- throw new Error("Multi-character rooms require the LiveKit transport; interactionApiUrl selects SSE");
85
- }
86
- if (config.characterVersion) {
87
- throw new Error("characterVersion applies to a single character; roster entries take bare UUIDs");
88
- }
89
- assertKey(config.sharedSessionKey, "sharedSessionKey");
90
- assertKey(config.connectAttemptId, "connectAttemptId");
91
- if (config.maxNumParticipants !== undefined &&
92
- (!Number.isInteger(config.maxNumParticipants) ||
93
- config.maxNumParticipants < 1)) {
94
- throw new Error("maxNumParticipants must be a positive integer");
95
- }
96
- }
97
- /**
98
- * Reject a config that negotiates v2 output protocols alongside room options
99
- * the runtime cannot combine them with.
100
- *
101
- * Verified against staging 2026-09-08: `capabilities.actionProtocolVersion: 2`
102
- * or `modelOutputVersion: 2` is answered with 422
103
- * `unsupported_action_protocol_topology` / `unsupported_model_output_topology`
104
- * unless the request uses `mode='create'`, a singular `characterId`, no
105
- * `sharedSessionKey`, and `maxNumParticipants` of 1. Version 1 carries no such
106
- * constraint.
107
- *
108
- * The published contract mentions only the multi-character half of this. The
109
- * `sharedSessionKey` and `maxNumParticipants` constraints bite in
110
- * single-character sessions too, which is why this runs on every connect
111
- * rather than only the roster path.
112
- */
113
- export function validateProtocolTopology(config) {
114
- const negotiatesV2 = config.capabilities?.actionProtocolVersion === 2 ||
115
- config.capabilities?.modelOutputVersion === 2;
116
- if (!negotiatesV2) {
117
- // Tools need v2, so without it they are refused whatever the topology.
118
- if (config.actionConfig?.tools?.length) {
119
- throw new Error("actionConfig.tools requires capabilities.actionProtocolVersion: 2");
120
- }
121
- return;
122
- }
123
- const offenders = [];
124
- if (isRosterConfig(config))
125
- offenders.push("characters");
126
- if (config.sharedSessionKey)
127
- offenders.push("sharedSessionKey");
128
- if (config.maxNumParticipants !== undefined && config.maxNumParticipants !== 1) {
129
- offenders.push("maxNumParticipants greater than 1");
130
- }
131
- if (offenders.length === 0)
132
- return;
133
- throw new Error(`Negotiated v2 output protocols require a single-character room: ` +
134
- `remove ${offenders.join(", ")}, or drop capabilities to version 1. ` +
135
- (offenders.includes("characters")
136
- ? "Multi-character rooms cannot use action protocol v2, and therefore cannot use client tools. "
137
- : "") +
138
- "The runtime rejects this combination with unsupported_action_protocol_topology.");
139
- }
140
- /** Validate join options. */
141
- export function validateJoinOptions(options) {
142
- const locators = Number(nonblank(options?.roomSessionId)) +
143
- Number(nonblank(options?.sharedSessionKey));
144
- if (locators !== 1) {
145
- throw new Error("joinRoom requires exactly one of roomSessionId or sharedSessionKey");
146
- }
147
- if (!nonblank(options.endUserId)) {
148
- throw new Error("joinRoom requires a nonblank endUserId");
149
- }
150
- // Guard the shape even though the type forbids it: a plain-JS caller, or a
151
- // config object spread in wholesale, would otherwise send topology that the
152
- // runtime rejects with a less specific message.
153
- const stray = options;
154
- if (stray.characters || stray.characterId || stray.characterSessionId) {
155
- throw new Error("joinRoom must not carry character topology; the room's roster is server-owned");
156
- }
157
- assertKey(options.sharedSessionKey, "sharedSessionKey");
158
- assertKey(options.connectAttemptId, "connectAttemptId");
159
- }
160
- /**
161
- * The roster half of a create request. Merged into the existing single-character
162
- * body by the caller, so every current option (blendshape, TTS/STT, dynamic
163
- * info, action, scene, VAD, vision) continues to apply to every member.
164
- */
165
- export function buildRosterConnectFields(config) {
166
- const characters = (config.characters ?? []).map((spec) => ({
167
- character_id: spec.characterId.trim(),
168
- ...(spec.characterSessionId
169
- ? { character_session_id: spec.characterSessionId }
170
- : {}),
171
- }));
172
- return {
173
- // Both carry server defaults that already match, but a roster room is
174
- // rejected outright without them, so they are stated rather than assumed.
175
- mode: "create",
176
- spawn_agent: true,
177
- characters,
178
- ...(config.sharedSessionKey
179
- ? { shared_session_key: config.sharedSessionKey }
180
- : {}),
181
- ...(config.connectAttemptId
182
- ? { connect_attempt_id: config.connectAttemptId }
183
- : {}),
184
- ...(config.maxNumParticipants !== undefined
185
- ? { max_num_participants: config.maxNumParticipants }
186
- : {}),
187
- };
188
- }
189
- /**
190
- * The complete join request body. A join carries a locator and the human, and
191
- * nothing else — the roster is loaded from persisted room state, and resending
192
- * topology is rejected.
193
- */
194
- export function buildJoinRequestBody(options) {
195
- return {
196
- mode: "join",
197
- transport: "livekit",
198
- ...(options.roomSessionId
199
- ? { room_session_id: options.roomSessionId }
200
- : { shared_session_key: options.sharedSessionKey }),
201
- end_user_id: options.endUserId,
202
- ...(options.endUserMetadata
203
- ? { end_user_metadata: options.endUserMetadata }
204
- : {}),
205
- ...(options.connectAttemptId
206
- ? { connect_attempt_id: options.connectAttemptId }
207
- : {}),
208
- };
209
- }
210
- //# sourceMappingURL=rosterRequest.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"rosterRequest.js","sourceRoot":"","sources":["../../src/core/rosterRequest.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n ConvaiCharacterSpec,\n ConvaiConfig,\n JoinRoomOptions,\n} from \"./types\";\nimport { isCharacterUuid } from \"./characterReference\";\n\n/**\n * Validation and request-body construction for multi-character rooms.\n *\n * Pure: no transport, no client state. Every rule below runs before a request\n * is built, so an illegal topology costs nothing and reports the rule it broke\n * rather than a 422 from the runtime.\n */\n\n/**\n * Server-wide roster cap. Checked client-side only to fail fast with a clear\n * message — the server stays authoritative, and a plan may configure a lower\n * `max_characters` that surfaces as 403 `MULTI_CHARACTER_ROSTER_LIMIT_EXCEEDED`.\n * This is an operational limit, not a protocol maximum, so it must not leak\n * into any public type.\n */\nexport const MULTI_CHARACTER_MAX_CHARACTERS = 50;\n\n/** `shared_session_key` and `connect_attempt_id` share one charset. */\nconst KEY_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;\n\n/** True when this config asks for a multi-character room. */\nexport function isRosterConfig(\n config: Pick<ConvaiConfig, \"characters\">,\n): boolean {\n return Array.isArray(config.characters) && config.characters.length > 0;\n}\n\nfunction assertKey(value: string | undefined, field: string): void {\n if (value === undefined) return;\n if (!KEY_PATTERN.test(value)) {\n throw new Error(\n `${field} must be 1-128 characters of letters, digits, hyphen or underscore`,\n );\n }\n}\n\nfunction nonblank(value: string | undefined | null): boolean {\n return typeof value === \"string\" && value.trim().length > 0;\n}\n\n/**\n * Validate a roster create config.\n *\n * Throws on the first broken rule. Ordered so the most structural problems\n * (which topology did you mean?) report before the detail ones.\n */\nexport function validateRosterConfig(config: ConvaiConfig): void {\n const characters = config.characters ?? [];\n\n if (config.characterId) {\n throw new Error(\"Pass either characterId or characters, not both\");\n }\n if (characters.length === 0) {\n throw new Error(\"characters must contain at least one entry\");\n }\n if (characters.length > MULTI_CHARACTER_MAX_CHARACTERS) {\n throw new Error(\n `characters accepts at most ${MULTI_CHARACTER_MAX_CHARACTERS} entries; ` +\n `your plan may allow fewer`,\n );\n }\n\n const seenSessions = new Set<string>();\n characters.forEach((spec, index) => {\n if (!spec || !nonblank(spec.characterId)) {\n throw new Error(\n `characters[${index}] requires a characterId`,\n );\n }\n // A roster entry is typed as a bare UUID on the runtime. The version\n // selectors that single-character `characterId` has accepted since\n // 1.8.0-beta.5 are a 422 here, and the failure would otherwise only show\n // up as an opaque validation error from the server.\n if (!isCharacterUuid(spec.characterId.trim())) {\n throw new Error(\n `characters[${index}].characterId must be a bare character UUID; ` +\n `version selectors are not supported inside a roster`,\n );\n }\n if (spec.characterSessionId !== undefined) {\n if (!nonblank(spec.characterSessionId)) {\n throw new Error(\n `characters[${index}].characterSessionId must be nonblank when supplied`,\n );\n }\n if (seenSessions.has(spec.characterSessionId)) {\n throw new Error(\n `characters[${index}].characterSessionId is repeated; resume ids must be unique within the roster`,\n );\n }\n seenSessions.add(spec.characterSessionId);\n }\n });\n\n if (config.characterSessionId) {\n throw new Error(\n \"characterSessionId is not valid with characters; use characters[].characterSessionId\",\n );\n }\n if (!nonblank(config.endUserId)) {\n throw new Error(\"characters requires a nonblank endUserId\");\n }\n if (config.transport && config.transport !== \"livekit\") {\n throw new Error(\"Multi-character rooms require the LiveKit transport\");\n }\n if (config.interactionApiUrl) {\n throw new Error(\n \"Multi-character rooms require the LiveKit transport; interactionApiUrl selects SSE\",\n );\n }\n if (config.characterVersion) {\n throw new Error(\n \"characterVersion applies to a single character; roster entries take bare UUIDs\",\n );\n }\n assertKey(config.sharedSessionKey, \"sharedSessionKey\");\n assertKey(config.connectAttemptId, \"connectAttemptId\");\n if (\n config.maxNumParticipants !== undefined &&\n (!Number.isInteger(config.maxNumParticipants) ||\n config.maxNumParticipants < 1)\n ) {\n throw new Error(\"maxNumParticipants must be a positive integer\");\n }\n}\n\n/**\n * Reject a config that negotiates v2 output protocols alongside room options\n * the runtime cannot combine them with.\n *\n * Verified against staging 2026-09-08: `capabilities.actionProtocolVersion: 2`\n * or `modelOutputVersion: 2` is answered with 422\n * `unsupported_action_protocol_topology` / `unsupported_model_output_topology`\n * unless the request uses `mode='create'`, a singular `characterId`, no\n * `sharedSessionKey`, and `maxNumParticipants` of 1. Version 1 carries no such\n * constraint.\n *\n * The published contract mentions only the multi-character half of this. The\n * `sharedSessionKey` and `maxNumParticipants` constraints bite in\n * single-character sessions too, which is why this runs on every connect\n * rather than only the roster path.\n */\nexport function validateProtocolTopology(config: ConvaiConfig): void {\n const negotiatesV2 =\n config.capabilities?.actionProtocolVersion === 2 ||\n config.capabilities?.modelOutputVersion === 2;\n\n if (!negotiatesV2) {\n // Tools need v2, so without it they are refused whatever the topology.\n if (config.actionConfig?.tools?.length) {\n throw new Error(\n \"actionConfig.tools requires capabilities.actionProtocolVersion: 2\",\n );\n }\n return;\n }\n\n const offenders: string[] = [];\n if (isRosterConfig(config)) offenders.push(\"characters\");\n if (config.sharedSessionKey) offenders.push(\"sharedSessionKey\");\n if (config.maxNumParticipants !== undefined && config.maxNumParticipants !== 1) {\n offenders.push(\"maxNumParticipants greater than 1\");\n }\n if (offenders.length === 0) return;\n\n throw new Error(\n `Negotiated v2 output protocols require a single-character room: ` +\n `remove ${offenders.join(\", \")}, or drop capabilities to version 1. ` +\n (offenders.includes(\"characters\")\n ? \"Multi-character rooms cannot use action protocol v2, and therefore cannot use client tools. \"\n : \"\") +\n \"The runtime rejects this combination with unsupported_action_protocol_topology.\",\n );\n}\n\n/** Validate join options. */\nexport function validateJoinOptions(options: JoinRoomOptions): void {\n const locators =\n Number(nonblank(options?.roomSessionId)) +\n Number(nonblank(options?.sharedSessionKey));\n if (locators !== 1) {\n throw new Error(\n \"joinRoom requires exactly one of roomSessionId or sharedSessionKey\",\n );\n }\n if (!nonblank(options.endUserId)) {\n throw new Error(\"joinRoom requires a nonblank endUserId\");\n }\n // Guard the shape even though the type forbids it: a plain-JS caller, or a\n // config object spread in wholesale, would otherwise send topology that the\n // runtime rejects with a less specific message.\n const stray = options as unknown as Record<string, unknown>;\n if (stray.characters || stray.characterId || stray.characterSessionId) {\n throw new Error(\n \"joinRoom must not carry character topology; the room's roster is server-owned\",\n );\n }\n assertKey(options.sharedSessionKey, \"sharedSessionKey\");\n assertKey(options.connectAttemptId, \"connectAttemptId\");\n}\n\n/**\n * The roster half of a create request. Merged into the existing single-character\n * body by the caller, so every current option (blendshape, TTS/STT, dynamic\n * info, action, scene, VAD, vision) continues to apply to every member.\n */\nexport function buildRosterConnectFields(\n config: ConvaiConfig,\n): Record<string, unknown> {\n const characters = (config.characters ?? []).map(\n (spec: ConvaiCharacterSpec) => ({\n character_id: spec.characterId.trim(),\n ...(spec.characterSessionId\n ? { character_session_id: spec.characterSessionId }\n : {}),\n }),\n );\n\n return {\n // Both carry server defaults that already match, but a roster room is\n // rejected outright without them, so they are stated rather than assumed.\n mode: \"create\",\n spawn_agent: true,\n characters,\n ...(config.sharedSessionKey\n ? { shared_session_key: config.sharedSessionKey }\n : {}),\n ...(config.connectAttemptId\n ? { connect_attempt_id: config.connectAttemptId }\n : {}),\n ...(config.maxNumParticipants !== undefined\n ? { max_num_participants: config.maxNumParticipants }\n : {}),\n };\n}\n\n/**\n * The complete join request body. A join carries a locator and the human, and\n * nothing else — the roster is loaded from persisted room state, and resending\n * topology is rejected.\n */\nexport function buildJoinRequestBody(\n options: JoinRoomOptions,\n): Record<string, unknown> {\n return {\n mode: \"join\",\n transport: \"livekit\",\n ...(options.roomSessionId\n ? { room_session_id: options.roomSessionId }\n : { shared_session_key: options.sharedSessionKey }),\n end_user_id: options.endUserId,\n ...(options.endUserMetadata\n ? { end_user_metadata: options.endUserMetadata }\n : {}),\n ...(options.connectAttemptId\n ? { connect_attempt_id: options.connectAttemptId }\n : {}),\n };\n}\n"]}
@@ -1,60 +0,0 @@
1
- /**
2
- * Inline markdown tokenizer shared by the React and vanilla chat bubbles.
3
- *
4
- * Core-service can hand the client bot text with links preserved
5
- * (`preserve_links_in_output` on /connect): `[text](url)`, `![alt](url)` and
6
- * bare `http(s)://` URLs arrive verbatim, everything else is still flattened
7
- * server-side. This turns one line of that text into nodes a renderer can
8
- * map onto DOM or JSX.
9
- *
10
- * Only `http:` and `https:` become links or images. Anything else — including
11
- * `javascript:` — stays literal text, regardless of what the server sent.
12
- * A link whose closing paren has not streamed in yet is left as text too, so
13
- * a bubble does not flicker between shapes mid-response.
14
- */
15
- export type MediaKind = "image" | "video";
16
- export type InlineNode = {
17
- kind: "text";
18
- text: string;
19
- } | {
20
- kind: "strong";
21
- text: string;
22
- } | {
23
- kind: "em";
24
- text: string;
25
- } | {
26
- kind: "link";
27
- href: string;
28
- label: string;
29
- media: MediaKind | null;
30
- } | {
31
- kind: "image";
32
- src: string;
33
- alt: string;
34
- }
35
- /**
36
- * A `[label](target)` whose scheme is not on the allowlist. It carries the
37
- * label so a renderer can show the words the character wrote instead of raw
38
- * markdown, and the target so it can be surfaced on hover — but a renderer
39
- * must never put `href` on an anchor. That is the whole point of the node.
40
- */
41
- | {
42
- kind: "blocked";
43
- href: string;
44
- label: string;
45
- };
46
- /** Classify a URL by its path extension; null for anything not previewable. */
47
- export declare function mediaKindForUrl(url: string): MediaKind | null;
48
- /**
49
- * A bare URL is its own label, and a long one wraps over several lines before
50
- * the message even starts — in a 400px panel that swamps the bubble. Compact it
51
- * the way a browser address bar does: keep what identifies the target, elide
52
- * the middle. `https://a.example.com/x/y/z/file.mp4` → `a.example.com/…/file.mp4`
53
- *
54
- * Presentation only: the href is always the full URL.
55
- */
56
- export declare function compactUrl(url: string, max?: number): string;
57
- /** True when `url` uses a scheme safe to expose as an anchor href. */
58
- export declare function isSafeHref(url: string): boolean;
59
- export declare function parseInlineMarkdown(line: string): InlineNode[];
60
- //# sourceMappingURL=inlineMarkdown.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"inlineMarkdown.d.ts","sourceRoot":"","sources":["../../src/utils/inlineMarkdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;AAE1C,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC5B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAAA;CAAE,GACtE;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE;AAC7C;;;;;GAKG;GACD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAKrD,+EAA+E;AAC/E,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAU7D;AAoBD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,SAAK,GAAG,MAAM,CAkBxD;AAED,sEAAsE;AACtE,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAG/C;AAqDD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,EAAE,CA4C9D"}
@@ -1,183 +0,0 @@
1
- /**
2
- * Inline markdown tokenizer shared by the React and vanilla chat bubbles.
3
- *
4
- * Core-service can hand the client bot text with links preserved
5
- * (`preserve_links_in_output` on /connect): `[text](url)`, `![alt](url)` and
6
- * bare `http(s)://` URLs arrive verbatim, everything else is still flattened
7
- * server-side. This turns one line of that text into nodes a renderer can
8
- * map onto DOM or JSX.
9
- *
10
- * Only `http:` and `https:` become links or images. Anything else — including
11
- * `javascript:` — stays literal text, regardless of what the server sent.
12
- * A link whose closing paren has not streamed in yet is left as text too, so
13
- * a bubble does not flicker between shapes mid-response.
14
- */
15
- const IMAGE_EXT = /\.(png|jpe?g|gif|webp|avif)$/i;
16
- const VIDEO_EXT = /\.(mp4|webm|mov|ogg)$/i;
17
- /** Classify a URL by its path extension; null for anything not previewable. */
18
- export function mediaKindForUrl(url) {
19
- let pathname;
20
- try {
21
- pathname = new URL(url).pathname;
22
- }
23
- catch {
24
- return null;
25
- }
26
- if (IMAGE_EXT.test(pathname))
27
- return "image";
28
- if (VIDEO_EXT.test(pathname))
29
- return "video";
30
- return null;
31
- }
32
- /**
33
- * Schemes a renderer may turn into an anchor.
34
- *
35
- * The list is an allowlist, never a denylist, because the server does NOT
36
- * sanitise schemes — core-service echoes `[x](javascript:alert(1))` back
37
- * verbatim (verified against dev4, 2026-09-10). The character's output is
38
- * attacker-influenced text, so this is the only thing standing between a
39
- * prompt-injected `javascript:` href and script execution in the host page.
40
- * Anything not listed here stays literal text.
41
- */
42
- const SAFE_SCHEMES = ["http", "https", "ftp", "ftps", "mailto", "tel", "sms"];
43
- /** Schemes with an authority; the rest (mailto/tel/sms) are opaque. */
44
- const NETWORK_SCHEMES = ["http", "https", "ftp", "ftps"];
45
- const schemeAlt = SAFE_SCHEMES.join("|");
46
- const networkAlt = NETWORK_SCHEMES.join("|");
47
- /**
48
- * A bare URL is its own label, and a long one wraps over several lines before
49
- * the message even starts — in a 400px panel that swamps the bubble. Compact it
50
- * the way a browser address bar does: keep what identifies the target, elide
51
- * the middle. `https://a.example.com/x/y/z/file.mp4` → `a.example.com/…/file.mp4`
52
- *
53
- * Presentation only: the href is always the full URL.
54
- */
55
- export function compactUrl(url, max = 44) {
56
- if (url.length <= max)
57
- return url;
58
- let host;
59
- let pathname;
60
- try {
61
- ({ host, pathname } = new URL(url));
62
- }
63
- catch {
64
- return `${url.slice(0, max - 1)}…`;
65
- }
66
- const file = pathname.split("/").filter(Boolean).pop();
67
- if (!file)
68
- return host.length <= max ? host : `${host.slice(0, max - 1)}…`;
69
- const compact = `${host}/…/${file}`;
70
- if (compact.length <= max)
71
- return compact;
72
- // A long host would otherwise eat the budget and clip the filename down to
73
- // its extension. The filename identifies what the link actually is, so drop
74
- // the host instead of shortening it.
75
- if (file.length <= max)
76
- return file;
77
- return `…${file.slice(-(max - 1))}`;
78
- }
79
- /** True when `url` uses a scheme safe to expose as an anchor href. */
80
- export function isSafeHref(url) {
81
- const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(url)?.[1];
82
- return scheme !== undefined && SAFE_SCHEMES.includes(scheme.toLowerCase());
83
- }
84
- // Alternatives are tried in this order at every position:
85
- // 1. ![alt](url) / [text](url), with an optional CommonMark "title"
86
- // 2. <url> autolink, angle brackets consumed
87
- // 3. bare URL, unless it is the target of a link still streaming in
88
- // 4. **bold** / __bold__ / *em* / _em_
89
- // Matching URLs before emphasis is what keeps `_` inside a URL literal.
90
- // Alternative 1 accepts ANY scheme so an unsafe one can be recognised and
91
- // re-emitted as literal text; without that, `[x](javascript:…)` would fall
92
- // through to the emphasis rules and get partially mangled.
93
- const TOKEN = new RegExp([
94
- // The target allows one level of balanced parens, so `javascript:alert(1)`
95
- // and `…/a_(b).pdf` are consumed whole. Without it the inner `)` ended the
96
- // match and the outer one was left stranded in the text.
97
- `(!?)\\[([^\\]]*)\\]\\(\\s*((?:[^\\s()]|\\([^()]*\\))+)(?:\\s+"[^"]*")?\\s*\\)`,
98
- `<((?:${schemeAlt}):[^\\s<>]+)>`,
99
- `(?<!\\]\\()((?:${networkAlt}):\\/\\/[^\\s<>"'\`]+|(?:mailto|tel|sms):[^\\s<>"'\`]+)`,
100
- `(\\*\\*|__)(.*?)\\6`,
101
- `\\*([^*]+)\\*`,
102
- `_([^_]+)_`,
103
- ].join("|"), "gi");
104
- const TRAILING_PUNCTUATION = ".,;:!?'\"";
105
- /**
106
- * Sentence punctuation after a bare URL belongs to the sentence, and an
107
- * unbalanced `)` belongs to the surrounding parenthetical.
108
- */
109
- function splitTrailingPunctuation(raw) {
110
- let url = raw;
111
- while (url.length > 0) {
112
- const last = url[url.length - 1];
113
- if (TRAILING_PUNCTUATION.includes(last)) {
114
- url = url.slice(0, -1);
115
- continue;
116
- }
117
- if (last === ")") {
118
- const opens = (url.match(/\(/g) ?? []).length;
119
- const closes = (url.match(/\)/g) ?? []).length;
120
- if (closes > opens) {
121
- url = url.slice(0, -1);
122
- continue;
123
- }
124
- }
125
- break;
126
- }
127
- return [url, raw.slice(url.length)];
128
- }
129
- export function parseInlineMarkdown(line) {
130
- const nodes = [];
131
- const pushText = (text) => {
132
- if (!text)
133
- return;
134
- const last = nodes[nodes.length - 1];
135
- if (last?.kind === "text")
136
- last.text += text;
137
- else
138
- nodes.push({ kind: "text", text });
139
- };
140
- let lastIndex = 0;
141
- TOKEN.lastIndex = 0;
142
- let match;
143
- while ((match = TOKEN.exec(line)) !== null) {
144
- pushText(line.slice(lastIndex, match.index));
145
- const [, bang, label, linkUrl, autoUrl, bareUrl, , boldText, starEm, underscoreEm] = match;
146
- if (linkUrl !== undefined) {
147
- if (!isSafeHref(linkUrl)) {
148
- // Not an anchor, ever. It keeps the label so the bubble reads as the
149
- // character wrote it rather than showing raw markdown, and the target
150
- // so a renderer can reveal it on hover.
151
- nodes.push({ kind: "blocked", href: linkUrl, label: bang ? label || linkUrl : label });
152
- }
153
- else if (bang) {
154
- nodes.push({ kind: "image", src: linkUrl, alt: label });
155
- }
156
- else {
157
- nodes.push({ kind: "link", href: linkUrl, label, media: mediaKindForUrl(linkUrl) });
158
- }
159
- }
160
- else if (autoUrl !== undefined) {
161
- nodes.push({ kind: "link", href: autoUrl, label: autoUrl, media: mediaKindForUrl(autoUrl) });
162
- }
163
- else if (bareUrl !== undefined) {
164
- const [href, rest] = splitTrailingPunctuation(bareUrl);
165
- if (href)
166
- nodes.push({ kind: "link", href, label: href, media: mediaKindForUrl(href) });
167
- pushText(rest);
168
- }
169
- else if (boldText !== undefined) {
170
- nodes.push({ kind: "strong", text: boldText });
171
- }
172
- else if (starEm !== undefined) {
173
- nodes.push({ kind: "em", text: starEm });
174
- }
175
- else if (underscoreEm !== undefined) {
176
- nodes.push({ kind: "em", text: underscoreEm });
177
- }
178
- lastIndex = match.index + match[0].length;
179
- }
180
- pushText(line.slice(lastIndex));
181
- return nodes;
182
- }
183
- //# sourceMappingURL=inlineMarkdown.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"inlineMarkdown.js","sourceRoot":"","sources":["../../src/utils/inlineMarkdown.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Inline markdown tokenizer shared by the React and vanilla chat bubbles.\n *\n * Core-service can hand the client bot text with links preserved\n * (`preserve_links_in_output` on /connect): `[text](url)`, `![alt](url)` and\n * bare `http(s)://` URLs arrive verbatim, everything else is still flattened\n * server-side. This turns one line of that text into nodes a renderer can\n * map onto DOM or JSX.\n *\n * Only `http:` and `https:` become links or images. Anything else — including\n * `javascript:` — stays literal text, regardless of what the server sent.\n * A link whose closing paren has not streamed in yet is left as text too, so\n * a bubble does not flicker between shapes mid-response.\n */\n\nexport type MediaKind = \"image\" | \"video\";\n\nexport type InlineNode =\n | { kind: \"text\"; text: string }\n | { kind: \"strong\"; text: string }\n | { kind: \"em\"; text: string }\n | { kind: \"link\"; href: string; label: string; media: MediaKind | null }\n | { kind: \"image\"; src: string; alt: string }\n /**\n * A `[label](target)` whose scheme is not on the allowlist. It carries the\n * label so a renderer can show the words the character wrote instead of raw\n * markdown, and the target so it can be surfaced on hover — but a renderer\n * must never put `href` on an anchor. That is the whole point of the node.\n */\n | { kind: \"blocked\"; href: string; label: string };\n\nconst IMAGE_EXT = /\\.(png|jpe?g|gif|webp|avif)$/i;\nconst VIDEO_EXT = /\\.(mp4|webm|mov|ogg)$/i;\n\n/** Classify a URL by its path extension; null for anything not previewable. */\nexport function mediaKindForUrl(url: string): MediaKind | null {\n let pathname: string;\n try {\n pathname = new URL(url).pathname;\n } catch {\n return null;\n }\n if (IMAGE_EXT.test(pathname)) return \"image\";\n if (VIDEO_EXT.test(pathname)) return \"video\";\n return null;\n}\n\n/**\n * Schemes a renderer may turn into an anchor.\n *\n * The list is an allowlist, never a denylist, because the server does NOT\n * sanitise schemes — core-service echoes `[x](javascript:alert(1))` back\n * verbatim (verified against dev4, 2026-09-10). The character's output is\n * attacker-influenced text, so this is the only thing standing between a\n * prompt-injected `javascript:` href and script execution in the host page.\n * Anything not listed here stays literal text.\n */\nconst SAFE_SCHEMES = [\"http\", \"https\", \"ftp\", \"ftps\", \"mailto\", \"tel\", \"sms\"];\n\n/** Schemes with an authority; the rest (mailto/tel/sms) are opaque. */\nconst NETWORK_SCHEMES = [\"http\", \"https\", \"ftp\", \"ftps\"];\n\nconst schemeAlt = SAFE_SCHEMES.join(\"|\");\nconst networkAlt = NETWORK_SCHEMES.join(\"|\");\n\n/**\n * A bare URL is its own label, and a long one wraps over several lines before\n * the message even starts — in a 400px panel that swamps the bubble. Compact it\n * the way a browser address bar does: keep what identifies the target, elide\n * the middle. `https://a.example.com/x/y/z/file.mp4` → `a.example.com/…/file.mp4`\n *\n * Presentation only: the href is always the full URL.\n */\nexport function compactUrl(url: string, max = 44): string {\n if (url.length <= max) return url;\n let host: string;\n let pathname: string;\n try {\n ({ host, pathname } = new URL(url));\n } catch {\n return `${url.slice(0, max - 1)}…`;\n }\n const file = pathname.split(\"/\").filter(Boolean).pop();\n if (!file) return host.length <= max ? host : `${host.slice(0, max - 1)}…`;\n const compact = `${host}/…/${file}`;\n if (compact.length <= max) return compact;\n // A long host would otherwise eat the budget and clip the filename down to\n // its extension. The filename identifies what the link actually is, so drop\n // the host instead of shortening it.\n if (file.length <= max) return file;\n return `…${file.slice(-(max - 1))}`;\n}\n\n/** True when `url` uses a scheme safe to expose as an anchor href. */\nexport function isSafeHref(url: string): boolean {\n const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(url)?.[1];\n return scheme !== undefined && SAFE_SCHEMES.includes(scheme.toLowerCase());\n}\n\n// Alternatives are tried in this order at every position:\n// 1. ![alt](url) / [text](url), with an optional CommonMark \"title\"\n// 2. <url> autolink, angle brackets consumed\n// 3. bare URL, unless it is the target of a link still streaming in\n// 4. **bold** / __bold__ / *em* / _em_\n// Matching URLs before emphasis is what keeps `_` inside a URL literal.\n// Alternative 1 accepts ANY scheme so an unsafe one can be recognised and\n// re-emitted as literal text; without that, `[x](javascript:…)` would fall\n// through to the emphasis rules and get partially mangled.\nconst TOKEN = new RegExp(\n [\n // The target allows one level of balanced parens, so `javascript:alert(1)`\n // and `…/a_(b).pdf` are consumed whole. Without it the inner `)` ended the\n // match and the outer one was left stranded in the text.\n `(!?)\\\\[([^\\\\]]*)\\\\]\\\\(\\\\s*((?:[^\\\\s()]|\\\\([^()]*\\\\))+)(?:\\\\s+\"[^\"]*\")?\\\\s*\\\\)`,\n `<((?:${schemeAlt}):[^\\\\s<>]+)>`,\n `(?<!\\\\]\\\\()((?:${networkAlt}):\\\\/\\\\/[^\\\\s<>\"'\\`]+|(?:mailto|tel|sms):[^\\\\s<>\"'\\`]+)`,\n `(\\\\*\\\\*|__)(.*?)\\\\6`,\n `\\\\*([^*]+)\\\\*`,\n `_([^_]+)_`,\n ].join(\"|\"),\n \"gi\",\n);\n\nconst TRAILING_PUNCTUATION = \".,;:!?'\\\"\";\n\n/**\n * Sentence punctuation after a bare URL belongs to the sentence, and an\n * unbalanced `)` belongs to the surrounding parenthetical.\n */\nfunction splitTrailingPunctuation(raw: string): [string, string] {\n let url = raw;\n while (url.length > 0) {\n const last = url[url.length - 1];\n if (TRAILING_PUNCTUATION.includes(last)) {\n url = url.slice(0, -1);\n continue;\n }\n if (last === \")\") {\n const opens = (url.match(/\\(/g) ?? []).length;\n const closes = (url.match(/\\)/g) ?? []).length;\n if (closes > opens) {\n url = url.slice(0, -1);\n continue;\n }\n }\n break;\n }\n return [url, raw.slice(url.length)];\n}\n\nexport function parseInlineMarkdown(line: string): InlineNode[] {\n const nodes: InlineNode[] = [];\n const pushText = (text: string) => {\n if (!text) return;\n const last = nodes[nodes.length - 1];\n if (last?.kind === \"text\") last.text += text;\n else nodes.push({ kind: \"text\", text });\n };\n\n let lastIndex = 0;\n TOKEN.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = TOKEN.exec(line)) !== null) {\n pushText(line.slice(lastIndex, match.index));\n const [, bang, label, linkUrl, autoUrl, bareUrl, , boldText, starEm, underscoreEm] = match;\n\n if (linkUrl !== undefined) {\n if (!isSafeHref(linkUrl)) {\n // Not an anchor, ever. It keeps the label so the bubble reads as the\n // character wrote it rather than showing raw markdown, and the target\n // so a renderer can reveal it on hover.\n nodes.push({ kind: \"blocked\", href: linkUrl, label: bang ? label || linkUrl : label });\n } else if (bang) {\n nodes.push({ kind: \"image\", src: linkUrl, alt: label });\n } else {\n nodes.push({ kind: \"link\", href: linkUrl, label, media: mediaKindForUrl(linkUrl) });\n }\n } else if (autoUrl !== undefined) {\n nodes.push({ kind: \"link\", href: autoUrl, label: autoUrl, media: mediaKindForUrl(autoUrl) });\n } else if (bareUrl !== undefined) {\n const [href, rest] = splitTrailingPunctuation(bareUrl);\n if (href) nodes.push({ kind: \"link\", href, label: href, media: mediaKindForUrl(href) });\n pushText(rest);\n } else if (boldText !== undefined) {\n nodes.push({ kind: \"strong\", text: boldText });\n } else if (starEm !== undefined) {\n nodes.push({ kind: \"em\", text: starEm });\n } else if (underscoreEm !== undefined) {\n nodes.push({ kind: \"em\", text: underscoreEm });\n }\n lastIndex = match.index + match[0].length;\n }\n pushText(line.slice(lastIndex));\n return nodes;\n}\n"]}