@neta-art/cohub 9.0.1 → 11.0.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 (240) hide show
  1. package/README.md +108 -102
  2. package/dist/board/editor/align.d.ts +11 -0
  3. package/dist/board/editor/align.js +72 -0
  4. package/dist/board/editor/animation.d.ts +17 -0
  5. package/dist/board/editor/animation.js +187 -0
  6. package/dist/board/editor/camera.d.ts +32 -0
  7. package/dist/board/editor/camera.js +137 -0
  8. package/dist/board/editor/clipboard.d.ts +41 -0
  9. package/dist/board/editor/clipboard.js +129 -0
  10. package/dist/board/editor/connection-ports.d.ts +19 -0
  11. package/dist/board/editor/connection-ports.js +85 -0
  12. package/dist/board/editor/context.d.ts +111 -0
  13. package/dist/board/editor/context.js +30 -0
  14. package/dist/board/editor/copy.d.ts +17 -0
  15. package/dist/board/editor/copy.js +121 -0
  16. package/dist/board/editor/creation.d.ts +51 -0
  17. package/dist/board/editor/creation.js +225 -0
  18. package/dist/board/editor/deletion.d.ts +9 -0
  19. package/dist/board/editor/deletion.js +90 -0
  20. package/dist/board/editor/document-edits.d.ts +22 -0
  21. package/dist/board/editor/document-edits.js +193 -0
  22. package/dist/board/editor/document.d.ts +41 -0
  23. package/dist/board/editor/document.js +302 -0
  24. package/dist/board/editor/editor.d.ts +225 -0
  25. package/dist/board/editor/editor.js +439 -0
  26. package/dist/board/editor/index.d.ts +18 -0
  27. package/dist/board/editor/index.js +12 -0
  28. package/dist/board/editor/interaction.d.ts +115 -0
  29. package/dist/board/editor/interaction.js +584 -0
  30. package/dist/board/editor/query.d.ts +27 -0
  31. package/dist/board/editor/query.js +203 -0
  32. package/dist/board/editor/remote.d.ts +11 -0
  33. package/dist/board/editor/remote.js +55 -0
  34. package/dist/board/editor/selection-transform.d.ts +24 -0
  35. package/dist/board/editor/selection-transform.js +81 -0
  36. package/dist/board/editor/selection.d.ts +33 -0
  37. package/dist/board/editor/selection.js +197 -0
  38. package/dist/board/editor/snapping.d.ts +33 -0
  39. package/dist/board/editor/snapping.js +75 -0
  40. package/dist/board/editor/snapshots.d.ts +27 -0
  41. package/dist/board/editor/snapshots.js +132 -0
  42. package/dist/board/editor/spatial.d.ts +41 -0
  43. package/dist/board/editor/spatial.js +191 -0
  44. package/dist/board/editor/state.d.ts +12 -0
  45. package/dist/board/editor/state.js +49 -0
  46. package/dist/board/editor/tool.d.ts +10 -0
  47. package/dist/board/editor/tool.js +19 -0
  48. package/dist/board/editor/wheel.d.ts +5 -0
  49. package/dist/board/editor/wheel.js +17 -0
  50. package/dist/board/export/index.d.ts +28 -6
  51. package/dist/board/export/index.js +47 -13
  52. package/dist/board/export/node/index.d.ts +56 -0
  53. package/dist/board/{headless → export/node}/index.js +26 -21
  54. package/dist/board/export/node/scripts.d.ts +16 -0
  55. package/dist/board/export/node/scripts.js +258 -0
  56. package/dist/board/export/scene.d.ts +8 -5
  57. package/dist/board/export/scene.js +24 -9
  58. package/dist/board/{animation.d.ts → model/animation.d.ts} +8 -5
  59. package/dist/board/{animation.js → model/animation.js} +37 -10
  60. package/dist/board/{core → model}/arrow-geometry.d.ts +2 -2
  61. package/dist/board/{core → model}/arrow-geometry.js +2 -2
  62. package/dist/board/model/authoring.d.ts +20 -0
  63. package/dist/board/model/authoring.js +185 -0
  64. package/dist/board/{core → model}/draw-geometry.d.ts +2 -2
  65. package/dist/board/{core → model}/draw-geometry.js +1 -1
  66. package/dist/board/model/draw-input.d.ts +14 -0
  67. package/dist/board/model/draw-input.js +28 -0
  68. package/dist/board/{core → model}/export-assets.d.ts +1 -1
  69. package/dist/board/{core → model}/export-assets.js +1 -1
  70. package/dist/board/{core → model}/export-plan.d.ts +2 -2
  71. package/dist/board/{core → model}/export-plan.js +3 -2
  72. package/dist/board/{core → model}/file-preview.d.ts +1 -1
  73. package/dist/board/{core → model}/file-preview.js +2 -2
  74. package/dist/board/{core → model}/file-snapshot.d.ts +1 -1
  75. package/dist/board/{core → model}/file-snapshot.js +4 -3
  76. package/dist/board/{geometry.d.ts → model/geometry.d.ts} +3 -3
  77. package/dist/board/{geometry.js → model/geometry.js} +1 -1
  78. package/dist/board/{image-key.d.ts → model/image-key.d.ts} +2 -2
  79. package/dist/board/{image-key.js → model/image-key.js} +7 -3
  80. package/dist/board/model/index.d.ts +42 -0
  81. package/dist/board/model/index.js +39 -0
  82. package/dist/board/model/items/builtin.d.ts +5 -0
  83. package/dist/board/model/items/builtin.js +119 -0
  84. package/dist/board/model/items/create.d.ts +61 -0
  85. package/dist/board/model/items/create.js +272 -0
  86. package/dist/board/model/items/definition.d.ts +35 -0
  87. package/dist/board/model/items/definition.js +18 -0
  88. package/dist/board/model/items/id.d.ts +5 -0
  89. package/dist/board/model/items/id.js +11 -0
  90. package/dist/board/model/items/index.d.ts +6 -0
  91. package/dist/board/model/items/index.js +6 -0
  92. package/dist/board/model/items/registry.d.ts +35 -0
  93. package/dist/board/model/items/registry.js +84 -0
  94. package/dist/board/{media-playback.d.ts → model/media-playback.d.ts} +4 -3
  95. package/dist/board/{media-playback.js → model/media-playback.js} +15 -5
  96. package/dist/board/{media.d.ts → model/media.d.ts} +1 -1
  97. package/dist/board/{media.js → model/media.js} +8 -2
  98. package/dist/board/{core → model}/palette.d.ts +1 -1
  99. package/dist/board/{core → model}/palette.js +1 -1
  100. package/dist/board/{presets.d.ts → model/presets.d.ts} +3 -3
  101. package/dist/board/{presets.js → model/presets.js} +1 -1
  102. package/dist/board/{replay.d.ts → model/replay.d.ts} +5 -5
  103. package/dist/board/{replay.js → model/replay.js} +2 -2
  104. package/dist/board/model/sampling.d.ts +19 -0
  105. package/dist/board/model/sampling.js +96 -0
  106. package/dist/board/{core → model}/scene.d.ts +1 -1
  107. package/dist/board/{core → model}/scene.js +1 -1
  108. package/dist/board/model/script-example.d.ts +4 -0
  109. package/dist/board/model/script-example.js +21 -0
  110. package/dist/board/model/script-hit.d.ts +10 -0
  111. package/dist/board/model/script-hit.js +52 -0
  112. package/dist/board/model/script-index.d.ts +7 -0
  113. package/dist/board/model/script-index.js +15 -0
  114. package/dist/board/model/script-scene.d.ts +24 -0
  115. package/dist/board/model/script-scene.js +267 -0
  116. package/dist/board/model/script.d.ts +105 -0
  117. package/dist/board/model/script.js +1 -0
  118. package/dist/board/model/shape-types.d.ts +8 -0
  119. package/dist/board/model/shape-types.js +8 -0
  120. package/dist/board/{task.d.ts → model/task.d.ts} +4 -4
  121. package/dist/board/{task.js → model/task.js} +3 -3
  122. package/dist/board/{core → model}/text-metrics.d.ts +1 -1
  123. package/dist/board/{core → model}/text-metrics.js +1 -1
  124. package/dist/board/{core → model}/tool-styles.d.ts +1 -1
  125. package/dist/board/{core → model}/tool-styles.js +1 -1
  126. package/dist/board/{runtime/board-player.d.ts → player/index.d.ts} +2 -2
  127. package/dist/board/{runtime/board-player.js → player/index.js} +5 -4
  128. package/dist/board/render/board-background.d.ts +1 -1
  129. package/dist/board/render/clip.d.ts +1 -1
  130. package/dist/board/render/css-color.d.ts +6 -1
  131. package/dist/board/render/css-color.js +34 -20
  132. package/dist/board/render/diagnostics.d.ts +20 -0
  133. package/dist/board/render/diagnostics.js +117 -0
  134. package/dist/board/render/index.d.ts +9 -6
  135. package/dist/board/render/index.js +10 -7
  136. package/dist/board/render/media-interaction.d.ts +2 -2
  137. package/dist/board/render/media-interaction.js +1 -1
  138. package/dist/board/render/palette.d.ts +3 -2
  139. package/dist/board/render/palette.js +7 -3
  140. package/dist/board/render/renderers/arrow-card-renderer.js +4 -3
  141. package/dist/board/render/renderers/audio-card-renderer.js +2 -1
  142. package/dist/board/render/renderers/board-renderer-registry.d.ts +20 -10
  143. package/dist/board/render/renderers/board-renderer-registry.js +17 -17
  144. package/dist/board/render/renderers/custom-card-renderer.d.ts +5 -0
  145. package/dist/board/render/renderers/{sketch-card-renderer.js → custom-card-renderer.js} +16 -15
  146. package/dist/board/render/renderers/draw-card-renderer.js +4 -3
  147. package/dist/board/render/renderers/effect-card-renderer.js +22 -5
  148. package/dist/board/render/renderers/far-plate.js +1 -1
  149. package/dist/board/render/renderers/file-card-renderer.d.ts +1 -1
  150. package/dist/board/render/renderers/file-card-renderer.js +2 -2
  151. package/dist/board/render/renderers/frame-card-renderer.js +30 -14
  152. package/dist/board/render/renderers/image-card-renderer.d.ts +1 -1
  153. package/dist/board/render/renderers/image-card-renderer.js +43 -4
  154. package/dist/board/render/renderers/shape-card-renderer.js +11 -28
  155. package/dist/board/render/renderers/task-card-renderer.js +1 -1
  156. package/dist/board/render/renderers/text-card-renderer.js +6 -4
  157. package/dist/board/render/renderers/unknown-card-renderer.js +1 -0
  158. package/dist/board/render/renderers/video-card-renderer.d.ts +1 -1
  159. package/dist/board/render/renderers/video-card-renderer.js +2 -1
  160. package/dist/board/render/sampling.d.ts +22 -0
  161. package/dist/board/render/sampling.js +21 -0
  162. package/dist/board/render/script-export.d.ts +11 -0
  163. package/dist/board/render/script-export.js +86 -0
  164. package/dist/board/render/script-host.d.ts +31 -0
  165. package/dist/board/render/script-host.js +421 -0
  166. package/dist/board/render/script-modules.d.ts +2 -0
  167. package/dist/board/render/script-modules.js +2 -0
  168. package/dist/board/render/script-runtime.d.ts +29 -0
  169. package/dist/board/render/script-runtime.js +213 -0
  170. package/dist/board/render/script-sandbox.d.ts +4 -0
  171. package/dist/board/render/script-sandbox.js +173 -0
  172. package/dist/board/render/stroke.d.ts +3 -2
  173. package/dist/board/render/stroke.js +23 -2
  174. package/dist/board/render/text-measurement.js +2 -2
  175. package/dist/board/replica/index.d.ts +70 -0
  176. package/dist/board/replica/index.js +276 -0
  177. package/dist/board/stage/assets.d.ts +41 -0
  178. package/dist/board/stage/assets.js +355 -0
  179. package/dist/board/stage/index.d.ts +8 -0
  180. package/dist/board/stage/index.js +8 -0
  181. package/dist/board/stage/lru.d.ts +32 -0
  182. package/dist/board/stage/lru.js +29 -0
  183. package/dist/board/stage/maintenance.d.ts +5 -0
  184. package/dist/board/stage/maintenance.js +11 -0
  185. package/dist/board/stage/pointer.d.ts +8 -0
  186. package/dist/board/stage/pointer.js +33 -0
  187. package/dist/board/stage/scene.d.ts +85 -0
  188. package/dist/board/stage/scene.js +515 -0
  189. package/dist/board/stage/script-pointer.d.ts +19 -0
  190. package/dist/board/stage/script-pointer.js +35 -0
  191. package/dist/board/stage/stage.d.ts +76 -0
  192. package/dist/board/stage/stage.js +774 -0
  193. package/dist/board/stage/texture-pool.d.ts +24 -0
  194. package/dist/board/stage/texture-pool.js +153 -0
  195. package/dist/board/stage/views.d.ts +38 -0
  196. package/dist/board/stage/views.js +93 -0
  197. package/dist/chunks/environment.d.ts +289 -23
  198. package/dist/chunks/host-bridge.d.ts +2 -2
  199. package/dist/chunks/http.d.ts +185 -71
  200. package/dist/chunks/http.js +368 -269
  201. package/dist/chunks/transport.js +3 -1
  202. package/dist/chunks/websocket.d.ts +9 -1
  203. package/dist/chunks/websocket.js +16 -2
  204. package/dist/host-bridge.js +2 -1
  205. package/dist/http.d.ts +2 -2
  206. package/dist/index.d.ts +182 -4
  207. package/dist/index.js +1393 -129
  208. package/dist/media.d.ts +1 -10
  209. package/dist/media.js +16 -27
  210. package/dist/protocol/dist/board-assets.d.ts +1 -0
  211. package/dist/protocol/dist/board-constants.js +2 -2
  212. package/dist/protocol/dist/board-model.d.ts +201 -11
  213. package/dist/protocol/dist/board-model.js +33 -14
  214. package/dist/protocol/dist/board-patch.js +3 -2
  215. package/dist/protocol/dist/board-schema.d.ts +4 -2
  216. package/dist/protocol/dist/board-schema.js +18 -4
  217. package/dist/protocol/dist/board-script-modules.d.ts +25 -0
  218. package/dist/protocol/dist/board-script-modules.js +127 -0
  219. package/dist/protocol/dist/board.d.ts +28 -2
  220. package/dist/protocol/dist/board.js +3 -1
  221. package/dist/protocol/dist/core/media.d.ts +5 -0
  222. package/dist/protocol/dist/core/media.js +39 -0
  223. package/dist/protocol/dist/display/index.d.ts +1 -0
  224. package/dist/protocol/dist/index.d.ts +7 -5
  225. package/dist/protocol/dist/realtime/types.d.ts +1 -0
  226. package/dist/protocol/dist/rtc/index.d.ts +1 -0
  227. package/dist/types.d.ts +1 -1
  228. package/package.json +21 -8
  229. package/dist/board/core/shape-definition.d.ts +0 -23
  230. package/dist/board/core/shape-definition.js +0 -45
  231. package/dist/board/core/shape-types.d.ts +0 -38
  232. package/dist/board/core/shape-types.js +0 -22
  233. package/dist/board/headless/index.d.ts +0 -55
  234. package/dist/board/headless/sketch.d.ts +0 -19
  235. package/dist/board/headless/sketch.js +0 -146
  236. package/dist/board/index.d.ts +0 -30
  237. package/dist/board/index.js +0 -29
  238. package/dist/board/render/renderers/sketch-card-renderer.d.ts +0 -5
  239. package/dist/board/render/sketch-host.d.ts +0 -14
  240. package/dist/board/render/sketch-host.js +0 -216
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { $ as resolveOpenSurface, A as ReferencesApi, B as DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS, C as SessionPatchReducer, D as connectDisplay, E as DisplayConnection, F as ModelsApi, G as DESKTOP_COMMAND_SETTLEMENT_GRACE_SECONDS, H as DESKTOP_COMMAND_OPEN_TIMEOUT_MS, I as GenerationsApi, J as defaultDesktopCommandTimeoutMs, K as DESKTOP_COMMAND_TERMINAL_TTL_SECONDS, L as CronJobsApi, M as PublicAssetsApi, N as SkillsApi, O as ensureRealtimeConnected, P as PromptsApi, Q as parseDesktopCommandId, R as ChannelsApi, S as parseAssistantMessageCommit, T as SpaceDisplaysApi, U as DESKTOP_COMMAND_PAYLOAD_MAX_BYTES, V as DESKTOP_COMMAND_MAX_TIMEOUT_MS, W as DESKTOP_COMMAND_PENDING_TTL_SECONDS, X as isTerminalDesktopCommandStatus, Y as isDesktopCallMethod, Z as parseDesktopCommand, _ as PublicInviteApi, a as DesktopCommandsApi, at as normalizeDisplayPoint, b as SessionGenerationStreamClient, c as ReferralsApi, d as TasksApi, et as NAVIGATION_ERROR_MESSAGE_MAX_LENGTH, f as BoardClient, g as SpacesApi, h as SpaceWebhooksApi, i as WorkCommerceApi, it as DISPLAY_SYSTEM_ACTIONS, j as SearchApi, k as SessionAccessApi, l as UsersApi, m as SpacePublicFilesApi, n as createHttpClient, o as UiCommandsApi, ot as splitDisplayText, p as SpaceClient, q as DESKTOP_COMMAND_VERSION, r as AppCommerceApi, rt as DISPLAY_INPUT_MAX_TEXT, s as AppsApi, t as CohubHttpClient, tt as DISPLAY_ELEMENT_REF_PATTERN, u as UserApi, v as buildSpaceInvitePath, w as createSessionPatchReducer, x as createSessionGenerationStreamClient, y as buildSpacePath, z as DESKTOP_COMMAND_ACCEPT_TIMEOUT_MS } from "./chunks/http.js";
2
- import { A as isUuid, C as isRequestSourceUuid, D as readRequestSourceFromEnv, E as parseRequestSourceFromHeaders, M as isUuidOrShortUuid, O as requestSourceToHeaders, S as isRequestSourceEmpty, T as normalizeRequestSource, _ as COHUB_SOURCE_HEADER, a as sanitizeAccessToken, b as hasRequestSourceIdentity, i as matchesUnauthorizedErrorToken, j as isUuidLike, k as resolveRequestSourceChannel, n as HttpTransport, o as REALTIME_ROOM_EVENT_NAME_PATTERN, r as joinApiUrl, t as HttpError, v as COHUB_SOURCE_HEADER_NAMES, w as mergeRequestSourceIntoMeta, x as isRequestSourceClientId, y as REQUEST_SOURCE_VIA_MAX_LENGTH } from "./chunks/transport.js";
1
+ import { $ as isTerminalDesktopCommandStatus, A as BoardClient, B as CronJobsApi, C as SessionPatchReducer, D as connectDisplay, E as DisplayConnection, F as PublicAssetsApi, G as DESKTOP_COMMAND_OPEN_TIMEOUT_MS, H as DESKTOP_COMMAND_ACCEPT_TIMEOUT_MS, I as SkillsApi, J as DESKTOP_COMMAND_SETTLEMENT_GRACE_SECONDS, K as DESKTOP_COMMAND_PAYLOAD_MAX_BYTES, L as PromptsApi, M as SessionAccessApi, N as ReferencesApi, O as readRtcTransportStats, P as SearchApi, Q as isDesktopCallMethod, R as ModelsApi, S as parseAssistantMessageCommit, T as SpaceDisplaysApi, U as DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS, V as ChannelsApi, W as DESKTOP_COMMAND_MAX_TIMEOUT_MS, X as DESKTOP_COMMAND_VERSION, Y as DESKTOP_COMMAND_TERMINAL_TTL_SECONDS, Z as defaultDesktopCommandTimeoutMs, _ as PublicInviteApi, a as WorkCommerceApi, b as SessionGenerationStreamClient, c as AppsApi, ct as normalizeDisplayPoint, d as UserApi, et as parseDesktopCommand, f as TasksApi, g as SpacesApi, h as SpaceWebhooksApi, i as AppCommerceApi, it as DISPLAY_ELEMENT_REF_PATTERN, j as ensureRealtimeConnected, k as waitForIceGathering, l as ReferralsApi, lt as splitDisplayText, m as SpacePublicFilesApi, n as createHttpClient, nt as resolveOpenSurface, o as DesktopCommandsApi, ot as DISPLAY_INPUT_MAX_TEXT, p as SpaceClient, q as DESKTOP_COMMAND_PENDING_TTL_SECONDS, r as RoomsApi, rt as NAVIGATION_ERROR_MESSAGE_MAX_LENGTH, s as UiCommandsApi, st as DISPLAY_SYSTEM_ACTIONS, t as CohubHttpClient, tt as parseDesktopCommandId, u as UsersApi, v as buildSpaceInvitePath, w as createSessionPatchReducer, x as createSessionGenerationStreamClient, y as buildSpacePath, z as GenerationsApi } from "./chunks/http.js";
2
+ import { A as requestSourceToHeaders, C as isRequestSourceClientId, D as normalizeRequestSource, E as mergeRequestSourceIntoMeta, M as isUuid, N as isUuidLike, O as parseRequestSourceFromHeaders, P as isUuidOrShortUuid, S as hasRequestSourceIdentity, T as isRequestSourceUuid, a as sanitizeAccessToken, b as COHUB_SOURCE_HEADER_NAMES, i as matchesUnauthorizedErrorToken, j as resolveRequestSourceChannel, k as readRequestSourceFromEnv, n as HttpTransport, o as REALTIME_ROOM_EVENT_NAME_PATTERN, r as joinApiUrl, t as HttpError, w as isRequestSourceEmpty, x as REQUEST_SOURCE_VIA_MAX_LENGTH, y as COHUB_SOURCE_HEADER } from "./chunks/transport.js";
3
3
  import { a as normalizeWebsocketUrl, c as resolveExecutionAppId, d as resolveWebBaseUrl, f as resolveWebsocketUrl, g as scopeListHasPermission, h as isUserLevelPermission, i as normalizeVoiceInputWebsocketUrl, l as resolveExecutionToken, m as USER_LEVEL_PERMISSIONS, n as getCohubContext, o as resolveApiBaseUrl, p as PERMISSIONS, r as normalizeBaseUrl, s as resolveCohubEnvironment, t as COHUB_ENVIRONMENTS, u as resolveVoiceInputWebsocketUrl } from "./chunks/environment.js";
4
4
  import { a as extractBillingPayload, c as isFeatureNotEntitledError, i as FEATURE_NOT_ENTITLED_ERROR_CODE, l as isHttpErrorCode, n as createWebsocketClient, o as isBillingAccessBlockedCode, r as BILLING_ACCESS_BLOCKED_ERROR_CODE, s as isBillingAccessBlockedError, t as WebsocketClient } from "./chunks/websocket.js";
5
5
  import { VoiceApi, VoiceInputClient, createVoiceInputClient } from "./voice-input.js";
@@ -78,8 +78,95 @@ const UPLOAD_MAX_FILE_BYTES = 1 * GiB;
78
78
  const UPLOAD_MAX_BATCH_BYTES = 2 * GiB;
79
79
  /** Maximum file count of one upload batch. */
80
80
  const UPLOAD_MAX_BATCH_FILES = 2e3;
81
+ //#endregion
82
+ //#region ../protocol/dist/board-url.js
83
+ const BOARD_REMOTE_URL_MAX_LENGTH = 4096;
84
+ function parseIpv4(host) {
85
+ const parts = host.split(".").map(Number);
86
+ return parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255) ? parts : null;
87
+ }
88
+ function isBlockedIpv4(host) {
89
+ const parts = parseIpv4(host);
90
+ if (!parts) return false;
91
+ const [first, second, third] = parts;
92
+ if (first === 0 || first === 10 || first === 127) return true;
93
+ if (first === 100 && second >= 64 && second <= 127) return true;
94
+ if (first === 169 && second === 254) return true;
95
+ if (first === 172 && second >= 16 && second <= 31) return true;
96
+ if (first === 192 && second === 168) return true;
97
+ if (first === 192 && second === 0 && (third === 0 || third === 2)) return true;
98
+ if (first === 192 && second === 88 && third === 99) return true;
99
+ if (first === 198 && (second === 18 || second === 19)) return true;
100
+ if (first === 198 && second === 51 && third === 100) return true;
101
+ if (first === 203 && second === 0 && third === 113) return true;
102
+ return first >= 224;
103
+ }
104
+ function expandIpv6(host) {
105
+ const [head, tail, extra] = host.toLowerCase().split("::");
106
+ if (extra !== void 0) return null;
107
+ const headParts = head ? head.split(":").filter(Boolean) : [];
108
+ const tailParts = tail ? tail.split(":").filter(Boolean) : [];
109
+ const missing = 8 - headParts.length - tailParts.length;
110
+ if (missing < 0 || tail === void 0 && missing !== 0) return null;
111
+ const parts = [
112
+ ...headParts,
113
+ ...Array.from({ length: missing }, () => "0"),
114
+ ...tailParts
115
+ ];
116
+ if (parts.length !== 8 || parts.some((part) => !/^[0-9a-f]{1,4}$/.test(part))) return null;
117
+ return parts.map((part) => part.padStart(4, "0"));
118
+ }
119
+ function isBlockedIpv6(host) {
120
+ const parts = expandIpv6(host);
121
+ if (!parts) return true;
122
+ if (parts.every((part) => part === "0000")) return true;
123
+ if (parts.slice(0, 7).every((part) => part === "0000") && parts[7] === "0001") return true;
124
+ if (parts.slice(0, 5).every((part) => part === "0000") && parts[5] === "ffff") {
125
+ const high = Number.parseInt(parts[6] ?? "0", 16);
126
+ const low = Number.parseInt(parts[7] ?? "0", 16);
127
+ return isBlockedIpv4(`${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`);
128
+ }
129
+ if (parts.slice(0, 6).every((part) => part === "0000")) return true;
130
+ const first = Number.parseInt(parts[0] ?? "0", 16);
131
+ if ((first & 65024) === 64512) return true;
132
+ if ((first & 65472) === 65152 || (first & 65472) === 65216) return true;
133
+ if ((first & 65280) === 65280) return true;
134
+ return parts[0] === "2001" && parts[1] === "0db8";
135
+ }
136
+ function isPublicBoardRemoteAddress(value) {
137
+ const address = value.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
138
+ if (parseIpv4(address)) return !isBlockedIpv4(address);
139
+ return address.includes(":") && !isBlockedIpv6(address);
140
+ }
141
+ function isBlockedHost(hostname) {
142
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
143
+ if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal")) return true;
144
+ if (parseIpv4(host) || host.includes(":")) return !isPublicBoardRemoteAddress(host);
145
+ return false;
146
+ }
147
+ /**
148
+ * Normalize a browser-loadable public HTTP(S) URL. This blocks explicit local
149
+ * addresses; any future server-side fetcher must additionally validate DNS
150
+ * resolution to defend against rebinding.
151
+ */
152
+ function normalizeBoardRemoteUrl(value) {
153
+ if (typeof value !== "string") return void 0;
154
+ const input = value.trim();
155
+ if (!input || input.length > 4096) return void 0;
156
+ try {
157
+ const url = new URL(input);
158
+ if (url.protocol !== "https:" && url.protocol !== "http:") return void 0;
159
+ if (url.username || url.password || isBlockedHost(url.hostname)) return;
160
+ const normalized = url.toString();
161
+ return normalized.length <= 4096 ? normalized : void 0;
162
+ } catch {
163
+ return;
164
+ }
165
+ }
166
+ z.string().max(BOARD_REMOTE_URL_MAX_LENGTH).refine((value) => normalizeBoardRemoteUrl(value) !== void 0, { message: "URL must be a public HTTP(S) URL without credentials" });
81
167
  const BOARD_CAMERA_TARGET = "camera";
82
- const BoardIdSchema = z.string().min(1).max(160).regex(/^[^\s/]+$/, "ids must not contain whitespace or '/'").refine((value) => value !== BOARD_CAMERA_TARGET, "\"camera\" is reserved");
168
+ const BoardKeySchema = z.string().min(1).max(160).regex(/^[^\s/]+$/, "ids must not contain whitespace or '/'");
169
+ const BoardIdSchema = BoardKeySchema.refine((value) => value !== BOARD_CAMERA_TARGET, "\"camera\" is reserved");
83
170
  const finite$1 = z.number().finite();
84
171
  const nonNegative = finite$1.nonnegative();
85
172
  const ratio = finite$1.min(0).max(1);
@@ -126,10 +213,29 @@ const BoardStyleSchema = z.object({
126
213
  trim: ratio.optional()
127
214
  }).strict();
128
215
  function isSafeBoardSourcePath(value) {
129
- return value.length > 0 && value.length <= 4096 && !value.startsWith("/") && !value.includes("\\") && value.split("/").every((part) => part.length > 0 && part !== "." && part !== "..");
216
+ return value.length > 0 && value.length <= 4096 && !value.startsWith("/") && !/^[a-z][a-z\d+.-]*:/i.test(value.trimStart()) && !value.includes("\\") && value.split("/").every((part) => part.length > 0 && part !== "." && part !== "..");
130
217
  }
131
218
  const srcSchema = z.string().refine(isSafeBoardSourcePath, "src must be a relative Space file path");
219
+ function parseBoardMediaSource(src) {
220
+ if (isSafeBoardSourcePath(src)) return {
221
+ kind: "file",
222
+ value: src
223
+ };
224
+ return normalizeBoardRemoteUrl(src) ? {
225
+ kind: "url",
226
+ value: src
227
+ } : null;
228
+ }
229
+ const BoardMediaSourceSchema = z.string().max(4096).refine((src) => parseBoardMediaSource(src) !== null, "src must be a public HTTP(S) URL or relative Space file path").describe("Prefer a stable public HTTP(S) URL; relative Space file paths remain supported.");
230
+ const BoardScriptSchema = z.object({
231
+ src: srcSchema.refine((path) => /\.m?js$/.test(path), "script src must be a .js or .mjs module"),
232
+ params: z.record(z.string(), z.json()).default({}),
233
+ targets: z.record(z.string().min(1).max(80), BoardIdSchema).default({}),
234
+ seed: z.string().min(1).max(160).optional(),
235
+ enabled: z.boolean().default(true)
236
+ }).strict();
132
237
  const itemEnvelope = {
238
+ script: BoardScriptSchema.optional(),
133
239
  parent: BoardIdSchema.optional(),
134
240
  z: finite$1.optional(),
135
241
  position: BoardVec2Schema.default({
@@ -174,6 +280,7 @@ const BoardFrameItemSchema = z.object({
174
280
  type: z.literal("frame"),
175
281
  props: z.object({
176
282
  label: z.string().max(280).default(""),
283
+ decoration: z.enum(["frame", "none"]).optional(),
177
284
  clip: z.boolean().default(true)
178
285
  }).strict().prefault({})
179
286
  }).strict();
@@ -260,7 +367,7 @@ const BoardArrowItemSchema = z.object({
260
367
  }).strict()
261
368
  }).strict();
262
369
  const fileProps = {
263
- src: srcSchema,
370
+ src: BoardMediaSourceSchema,
264
371
  snapshot: z.object({
265
372
  title: z.string().optional(),
266
373
  mimeType: z.string().optional(),
@@ -389,14 +496,11 @@ const BoardEffectItemSchema = z.object({
389
496
  seed: z.string().min(1).max(160).optional()
390
497
  }).strict()
391
498
  }).strict();
392
- const BoardSketchItemSchema = z.object({
499
+ const BoardCustomItemSchema = z.object({
393
500
  ...boxed(480, 320),
394
- type: z.literal("sketch"),
395
- props: z.object({
396
- src: srcSchema.refine((path) => /\.m?js$/.test(path), "sketch src must be a .js or .mjs module"),
397
- params: z.record(z.string(), z.unknown()).default({}),
398
- seed: z.string().min(1).max(160).optional()
399
- }).strict()
501
+ type: z.literal("custom"),
502
+ script: BoardScriptSchema,
503
+ props: z.object({}).strict().default({})
400
504
  }).strict();
401
505
  const BOARD_EXTENSION_TYPE_PATTERN = /^[a-z][a-z0-9]*(?:\.[a-z0-9-]+)+$/;
402
506
  const BoardExtensionItemSchema = z.object({
@@ -420,7 +524,7 @@ const BOARD_ITEM_SCHEMAS = {
420
524
  file: BoardFileItemSchema,
421
525
  task: BoardTaskItemSchema,
422
526
  effect: BoardEffectItemSchema,
423
- sketch: BoardSketchItemSchema
527
+ custom: BoardCustomItemSchema
424
528
  };
425
529
  const BOARD_ITEM_TYPES = Object.keys(BOARD_ITEM_SCHEMAS);
426
530
  function isBuiltinItemType(type) {
@@ -535,7 +639,7 @@ const BoardAnimationHeaderSchema = z.object({
535
639
  end: z.enum(["hold", "reset"]).default("hold"),
536
640
  markers: z.array(BoardMarkerSchema).max(1e4).default([])
537
641
  }).strict();
538
- const BoardAnimationSchema = BoardAnimationHeaderSchema.extend({ tracks: z.record(BoardIdSchema, BoardTrackSchema).default({}) }).strict();
642
+ const BoardAnimationSchema = BoardAnimationHeaderSchema.extend({ tracks: z.record(BoardKeySchema, BoardTrackSchema).default({}) }).strict();
539
643
  const BOARD_ENTER_PRESETS = [
540
644
  "fade-in",
541
645
  "rise",
@@ -575,7 +679,7 @@ const patchObject = z.record(z.string(), z.unknown());
575
679
  const BoardPatchSchema = z.object({
576
680
  board: patchObject.nullable().optional(),
577
681
  items: z.record(BoardIdSchema, patchObject.nullable()).optional(),
578
- animations: z.record(BoardIdSchema, patchObject.and(z.object({ tracks: z.record(BoardIdSchema, patchObject.nullable()).optional() })).nullable()).optional()
682
+ animations: z.record(BoardIdSchema, patchObject.and(z.object({ tracks: z.record(BoardKeySchema, patchObject.nullable()).optional() })).nullable()).optional()
579
683
  }).strict();
580
684
  //#endregion
581
685
  //#region ../protocol/dist/board-json.js
@@ -1119,7 +1223,8 @@ const NOT_ANIMATABLE = /* @__PURE__ */ new Set([
1119
1223
  "parent",
1120
1224
  "z",
1121
1225
  "locked",
1122
- "metadata"
1226
+ "metadata",
1227
+ "script"
1123
1228
  ]);
1124
1229
  const CAMERA_PROPERTIES = {
1125
1230
  focus: {
@@ -1646,92 +1751,6 @@ function parseBoardDocument(input) {
1646
1751
  } : result;
1647
1752
  }
1648
1753
  //#endregion
1649
- //#region ../protocol/dist/board-url.js
1650
- const BOARD_REMOTE_URL_MAX_LENGTH = 4096;
1651
- function parseIpv4(host) {
1652
- const parts = host.split(".").map(Number);
1653
- return parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255) ? parts : null;
1654
- }
1655
- function isBlockedIpv4(host) {
1656
- const parts = parseIpv4(host);
1657
- if (!parts) return false;
1658
- const [first, second, third] = parts;
1659
- if (first === 0 || first === 10 || first === 127) return true;
1660
- if (first === 100 && second >= 64 && second <= 127) return true;
1661
- if (first === 169 && second === 254) return true;
1662
- if (first === 172 && second >= 16 && second <= 31) return true;
1663
- if (first === 192 && second === 168) return true;
1664
- if (first === 192 && second === 0 && (third === 0 || third === 2)) return true;
1665
- if (first === 192 && second === 88 && third === 99) return true;
1666
- if (first === 198 && (second === 18 || second === 19)) return true;
1667
- if (first === 198 && second === 51 && third === 100) return true;
1668
- if (first === 203 && second === 0 && third === 113) return true;
1669
- return first >= 224;
1670
- }
1671
- function expandIpv6(host) {
1672
- const [head, tail, extra] = host.toLowerCase().split("::");
1673
- if (extra !== void 0) return null;
1674
- const headParts = head ? head.split(":").filter(Boolean) : [];
1675
- const tailParts = tail ? tail.split(":").filter(Boolean) : [];
1676
- const missing = 8 - headParts.length - tailParts.length;
1677
- if (missing < 0 || tail === void 0 && missing !== 0) return null;
1678
- const parts = [
1679
- ...headParts,
1680
- ...Array.from({ length: missing }, () => "0"),
1681
- ...tailParts
1682
- ];
1683
- if (parts.length !== 8 || parts.some((part) => !/^[0-9a-f]{1,4}$/.test(part))) return null;
1684
- return parts.map((part) => part.padStart(4, "0"));
1685
- }
1686
- function isBlockedIpv6(host) {
1687
- const parts = expandIpv6(host);
1688
- if (!parts) return true;
1689
- if (parts.every((part) => part === "0000")) return true;
1690
- if (parts.slice(0, 7).every((part) => part === "0000") && parts[7] === "0001") return true;
1691
- if (parts.slice(0, 5).every((part) => part === "0000") && parts[5] === "ffff") {
1692
- const high = Number.parseInt(parts[6] ?? "0", 16);
1693
- const low = Number.parseInt(parts[7] ?? "0", 16);
1694
- return isBlockedIpv4(`${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`);
1695
- }
1696
- if (parts.slice(0, 6).every((part) => part === "0000")) return true;
1697
- const first = Number.parseInt(parts[0] ?? "0", 16);
1698
- if ((first & 65024) === 64512) return true;
1699
- if ((first & 65472) === 65152 || (first & 65472) === 65216) return true;
1700
- if ((first & 65280) === 65280) return true;
1701
- return parts[0] === "2001" && parts[1] === "0db8";
1702
- }
1703
- function isPublicBoardRemoteAddress(value) {
1704
- const address = value.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
1705
- if (parseIpv4(address)) return !isBlockedIpv4(address);
1706
- return address.includes(":") && !isBlockedIpv6(address);
1707
- }
1708
- function isBlockedHost(hostname) {
1709
- const host = hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
1710
- if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal")) return true;
1711
- if (parseIpv4(host) || host.includes(":")) return !isPublicBoardRemoteAddress(host);
1712
- return false;
1713
- }
1714
- /**
1715
- * Normalize a browser-loadable public HTTP(S) URL. This blocks explicit local
1716
- * addresses; any future server-side fetcher must additionally validate DNS
1717
- * resolution to defend against rebinding.
1718
- */
1719
- function normalizeBoardRemoteUrl(value) {
1720
- if (typeof value !== "string") return void 0;
1721
- const input = value.trim();
1722
- if (!input || input.length > 4096) return void 0;
1723
- try {
1724
- const url = new URL(input);
1725
- if (url.protocol !== "https:" && url.protocol !== "http:") return void 0;
1726
- if (url.username || url.password || isBlockedHost(url.hostname)) return;
1727
- const normalized = url.toString();
1728
- return normalized.length <= 4096 ? normalized : void 0;
1729
- } catch {
1730
- return;
1731
- }
1732
- }
1733
- z.string().max(BOARD_REMOTE_URL_MAX_LENGTH).refine((value) => normalizeBoardRemoteUrl(value) !== void 0, { message: "URL must be a public HTTP(S) URL without credentials" });
1734
- //#endregion
1735
1754
  //#region ../protocol/dist/turn-origin.js
1736
1755
  const record$2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
1737
1756
  const normalizeSessionTurnOrigin = (value) => {
@@ -2030,6 +2049,102 @@ function compileDisplayActions(actions, width, height) {
2030
2049
  if (events.length === 0) throw new DisplayActionError("no action touches the display");
2031
2050
  return events;
2032
2051
  }
2052
+ z.object({
2053
+ urls: z.union([z.string().min(1).max(512), z.array(z.string().min(1).max(512)).min(1).max(16)]),
2054
+ username: z.string().max(512).optional(),
2055
+ credential: z.string().max(512).optional()
2056
+ });
2057
+ const sessionDescriptionSchema = z.object({
2058
+ type: z.enum(["offer", "answer"]),
2059
+ sdp: z.string().min(1).max(65536)
2060
+ });
2061
+ const APP_RTC_DELIVERIES = ["reliable", "latest"];
2062
+ const APP_RTC_PUBLICATION_KINDS = [
2063
+ "data",
2064
+ "audio",
2065
+ "video"
2066
+ ];
2067
+ const APP_RTC_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/;
2068
+ const isAppRtcName = (value) => typeof value === "string" && APP_RTC_NAME_PATTERN.test(value) && !value.toLowerCase().startsWith("cohub.");
2069
+ const APP_RTC_LIMITS = {
2070
+ channelsPerMember: 16,
2071
+ tracksPerMember: 4,
2072
+ dataSubscriptions: 512,
2073
+ mediaSubscriptions: 16,
2074
+ messageBytes: 65536,
2075
+ operationsPerSecond: 10,
2076
+ opensPerSecond: 2,
2077
+ batch: 64
2078
+ };
2079
+ const APP_RTC_QUALITIES = [
2080
+ "high",
2081
+ "medium",
2082
+ "low"
2083
+ ];
2084
+ const APP_RTC_SIMULCAST_LAYERS = [
2085
+ {
2086
+ rid: "f",
2087
+ quality: "high",
2088
+ scaleResolutionDownBy: 1
2089
+ },
2090
+ {
2091
+ rid: "h",
2092
+ quality: "medium",
2093
+ scaleResolutionDownBy: 2
2094
+ },
2095
+ {
2096
+ rid: "q",
2097
+ quality: "low",
2098
+ scaleResolutionDownBy: 4
2099
+ }
2100
+ ];
2101
+ z.object({
2102
+ id: z.string().min(1).max(32),
2103
+ name: z.string().regex(APP_RTC_NAME_PATTERN),
2104
+ kind: z.enum(APP_RTC_PUBLICATION_KINDS),
2105
+ delivery: z.enum(APP_RTC_DELIVERIES).optional(),
2106
+ simulcast: z.boolean().optional()
2107
+ });
2108
+ const nameSchema = z.string().refine(isAppRtcName, "invalid name");
2109
+ const participantIdSchema = z.string().uuid();
2110
+ z.object({
2111
+ ticket: z.string().min(1).max(4096),
2112
+ participantId: participantIdSchema
2113
+ });
2114
+ z.object({ answer: sessionDescriptionSchema.refine((value) => value.type === "answer", "expected an answer") });
2115
+ z.union([z.object({ channels: z.array(z.object({
2116
+ name: nameSchema,
2117
+ delivery: z.enum(APP_RTC_DELIVERIES)
2118
+ })).min(1).max(APP_RTC_LIMITS.channelsPerMember) }), z.object({
2119
+ offer: sessionDescriptionSchema.refine((value) => value.type === "offer", "expected an offer"),
2120
+ tracks: z.array(z.object({
2121
+ name: nameSchema,
2122
+ kind: z.enum(["audio", "video"]),
2123
+ mid: z.string().min(1).max(32),
2124
+ simulcast: z.boolean().optional()
2125
+ })).min(1).max(APP_RTC_LIMITS.tracksPerMember)
2126
+ })]);
2127
+ z.object({
2128
+ ids: z.array(z.string().min(1).max(32)).min(1).max(APP_RTC_LIMITS.channelsPerMember + APP_RTC_LIMITS.tracksPerMember),
2129
+ offer: sessionDescriptionSchema.refine((value) => value.type === "offer", "expected an offer").optional()
2130
+ });
2131
+ const subscriptionRefSchema = z.object({
2132
+ participantId: participantIdSchema,
2133
+ publicationId: z.string().min(1).max(32)
2134
+ });
2135
+ const qualitySchema = z.enum(APP_RTC_QUALITIES);
2136
+ z.object({
2137
+ kind: z.enum(["data", "media"]),
2138
+ items: z.array(subscriptionRefSchema.extend({ quality: qualitySchema.optional() })).min(1).max(APP_RTC_LIMITS.batch)
2139
+ });
2140
+ z.object({ items: z.array(subscriptionRefSchema.extend({
2141
+ mid: z.string().min(1).max(32),
2142
+ quality: qualitySchema.optional()
2143
+ })).min(1).max(APP_RTC_LIMITS.mediaSubscriptions) });
2144
+ z.object({
2145
+ items: z.array(subscriptionRefSchema).min(1).max(APP_RTC_LIMITS.batch),
2146
+ offer: sessionDescriptionSchema.refine((value) => value.type === "offer", "expected an offer").optional()
2147
+ });
2033
2148
  //#endregion
2034
2149
  //#region ../protocol/dist/generation/catalog.js
2035
2150
  function isGenerationModelHidden(model) {
@@ -2765,6 +2880,8 @@ function codexOutput(block) {
2765
2880
  text: block.text
2766
2881
  }] : [];
2767
2882
  }
2883
+ const CODEX_CALL_ID_MAX = 64;
2884
+ const codexCallId = (id) => id.length <= CODEX_CALL_ID_MAX ? id : `call_${fingerprint(id)}`;
2768
2885
  function codexMessageRecords(input, message, warnings) {
2769
2886
  const timestamp = timestampOf(message, input.timestamp ?? DEFAULT_TIMESTAMP);
2770
2887
  const records = [];
@@ -2818,11 +2935,11 @@ function codexMessageRecords(input, message, warnings) {
2818
2935
  type: "function_call",
2819
2936
  name: block.name,
2820
2937
  arguments: JSON.stringify(block.input),
2821
- call_id: block.id
2938
+ call_id: codexCallId(block.id)
2822
2939
  });
2823
2940
  else if (block.type === "tool_result") push(records.length, {
2824
2941
  type: "function_call_output",
2825
- call_id: block.tool_use_id,
2942
+ call_id: codexCallId(block.tool_use_id),
2826
2943
  output: typeof block.content === "string" ? block.content : block.content.flatMap((item) => item.type === "text" ? [item.text] : []).join("\n")
2827
2944
  });
2828
2945
  }
@@ -3134,13 +3251,27 @@ const resolveHarness = (meta) => {
3134
3251
  const value = meta && typeof meta === "object" ? meta.harness : null;
3135
3252
  return isLocalHarness(value) ? value : "cohub";
3136
3253
  };
3254
+ const runtimeThinkingLevelSchema = z.enum([
3255
+ "off",
3256
+ "minimal",
3257
+ "low",
3258
+ "medium",
3259
+ "high",
3260
+ "xhigh",
3261
+ "max"
3262
+ ]);
3137
3263
  const runtimeCapabilitiesSchema = z.object({
3138
3264
  harnesses: z.array(z.enum(["pi", "codex"])).max(2),
3139
3265
  models: z.array(z.object({
3140
3266
  harness: z.enum(["pi", "codex"]),
3141
3267
  provider: z.string().max(100),
3142
3268
  id: z.string().min(1).max(255),
3143
- name: z.string().max(255)
3269
+ name: z.string().max(255),
3270
+ /** The model the Harness runs when none is requested; at most one per Harness. */
3271
+ isDefault: z.boolean().optional(),
3272
+ reasoning: z.boolean().optional(),
3273
+ defaultThinkingLevel: runtimeThinkingLevelSchema.optional(),
3274
+ thinkingLevelMap: z.partialRecord(runtimeThinkingLevelSchema, z.string().max(100).nullable()).optional()
3144
3275
  })).max(2e3)
3145
3276
  });
3146
3277
  const runtimePendingExecutionSchema = z.object({
@@ -3711,6 +3842,11 @@ const APP_APPEARANCE_TOKENS = [
3711
3842
  "brand-ring",
3712
3843
  "brand-contrast-fg",
3713
3844
  "error-fg",
3845
+ "success",
3846
+ "warning",
3847
+ "error",
3848
+ "info",
3849
+ "status-running",
3714
3850
  "selection-bg",
3715
3851
  "overlay-scrim",
3716
3852
  "shadow-subtle",
@@ -3805,6 +3941,33 @@ const buildAppEmbedShellChanged = (input) => ({
3805
3941
  ...input
3806
3942
  });
3807
3943
  //#endregion
3944
+ //#region ../protocol/dist/app-devices.js
3945
+ const APP_DEVICES = [
3946
+ "camera",
3947
+ "microphone",
3948
+ "screen"
3949
+ ];
3950
+ const isAppDevice = (value) => typeof value === "string" && APP_DEVICES.includes(value);
3951
+ function parseAppDevices(value) {
3952
+ const items = typeof value === "string" ? value.split(/[\s,]+/) : Array.isArray(value) ? value : [];
3953
+ const wanted = new Set(items.map((item) => typeof item === "string" ? item.trim().toLowerCase() : item).filter(isAppDevice));
3954
+ return APP_DEVICES.filter((device) => wanted.has(device));
3955
+ }
3956
+ const APP_DEVICES_REQUEST_TYPE = "cohub.app.devices.request";
3957
+ const APP_DEVICES_RESULT_TYPE = "cohub.app.devices.result";
3958
+ function parseAppDevicesResult(value) {
3959
+ if (!value || typeof value !== "object") return null;
3960
+ const record = value;
3961
+ if (record.type !== "cohub.app.devices.result" || typeof record.requestId !== "string") return null;
3962
+ const status = record.status;
3963
+ if (status !== "granted" && status !== "denied" && status !== "unavailable") return null;
3964
+ return {
3965
+ type: APP_DEVICES_RESULT_TYPE,
3966
+ requestId: record.requestId,
3967
+ status
3968
+ };
3969
+ }
3970
+ //#endregion
3808
3971
  //#region src/apis/billing.ts
3809
3972
  var BillingApi = class {
3810
3973
  transport;
@@ -3887,6 +4050,960 @@ var BillingApi = class {
3887
4050
  }
3888
4051
  };
3889
4052
  //#endregion
4053
+ //#region src/apis/app-room-rtc.ts
4054
+ var AppRtcError = class extends Error {
4055
+ code;
4056
+ constructor(code, message) {
4057
+ super(message);
4058
+ this.code = code;
4059
+ this.name = "AppRtcError";
4060
+ }
4061
+ };
4062
+ const SERVER_CODES = /* @__PURE__ */ new Set([
4063
+ "RTC_UNAVAILABLE",
4064
+ "RTC_FORBIDDEN",
4065
+ "RTC_LIMIT_EXCEEDED",
4066
+ "RTC_RATE_LIMITED",
4067
+ "ROOM_NOT_FOUND",
4068
+ "ROOM_EXPIRED"
4069
+ ]);
4070
+ const toAppRtcError = (error) => {
4071
+ if (error instanceof AppRtcError) return error;
4072
+ if (error instanceof HttpError && SERVER_CODES.has(error.code)) return new AppRtcError(error.code, error.message);
4073
+ return new AppRtcError("RTC_FAILED", error instanceof Error ? error.message : String(error));
4074
+ };
4075
+ const HEADER_BYTES = 5;
4076
+ const FLAG_BINARY = 1;
4077
+ const encoder = new TextEncoder();
4078
+ const decoder = new TextDecoder();
4079
+ function encodeAppRtcFrame(data, sequence) {
4080
+ let payload;
4081
+ let flags = 0;
4082
+ if (data instanceof ArrayBuffer) {
4083
+ payload = new Uint8Array(data);
4084
+ flags = FLAG_BINARY;
4085
+ } else if (ArrayBuffer.isView(data)) {
4086
+ payload = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
4087
+ flags = FLAG_BINARY;
4088
+ } else {
4089
+ const json = JSON.stringify(data);
4090
+ if (json === void 0) throw new TypeError("RTC message is not serializable");
4091
+ payload = encoder.encode(json);
4092
+ }
4093
+ if (payload.byteLength > APP_RTC_LIMITS.messageBytes) throw new RangeError(`RTC message exceeds ${APP_RTC_LIMITS.messageBytes} bytes`);
4094
+ const frame = new Uint8Array(HEADER_BYTES + payload.byteLength);
4095
+ frame[0] = flags;
4096
+ new DataView(frame.buffer).setUint32(1, sequence >>> 0);
4097
+ frame.set(payload, HEADER_BYTES);
4098
+ return frame;
4099
+ }
4100
+ function decodeAppRtcFrame(frame) {
4101
+ if (frame.byteLength < HEADER_BYTES) return null;
4102
+ const view = new DataView(frame);
4103
+ const flags = view.getUint8(0);
4104
+ const sequence = view.getUint32(1);
4105
+ const payload = new Uint8Array(frame, HEADER_BYTES);
4106
+ if (flags & FLAG_BINARY) return {
4107
+ data: payload.slice().buffer,
4108
+ sequence
4109
+ };
4110
+ try {
4111
+ return {
4112
+ data: JSON.parse(decoder.decode(payload)),
4113
+ sequence
4114
+ };
4115
+ } catch {
4116
+ return null;
4117
+ }
4118
+ }
4119
+ const isNewerSequence = (next, last) => last === null || next !== last && next - last >>> 0 < 2147483648;
4120
+ const publicationsKey = (members) => members.map((member) => `${member.participantId}:${member.publications?.map((item) => item.id).join(",") ?? ""}`).join(";");
4121
+ const GATHER_TIMEOUT_MS = 2e3;
4122
+ const CONNECT_TIMEOUT_MS = 15e3;
4123
+ const PENDING_RELIABLE_FRAMES = 256;
4124
+ const LATEST_BACKLOG_BYTES = 65536;
4125
+ const RECONNECT_DELAYS_MS = [
4126
+ 500,
4127
+ 1e3,
4128
+ 2e3,
4129
+ 5e3,
4130
+ 1e4,
4131
+ 3e4
4132
+ ];
4133
+ const RATE_LIMIT_RETRY_MS = 1e3;
4134
+ var ConnectionReplacedError = class extends Error {
4135
+ constructor() {
4136
+ super("RTC connection was replaced");
4137
+ this.name = "ConnectionReplacedError";
4138
+ }
4139
+ };
4140
+ const policyOf = (delivery) => delivery === "latest" ? {
4141
+ ordered: false,
4142
+ maxRetransmits: 0
4143
+ } : { ordered: true };
4144
+ const isSessionLost = (error) => error instanceof HttpError && (error.code === "RTC_STALE" || error.code === "RTC_SESSION_NOT_FOUND");
4145
+ const isRoomGone = (error) => error instanceof HttpError && (error.code === "ROOM_NOT_FOUND" || error.code === "ROOM_EXPIRED");
4146
+ const chunk = (items, size) => {
4147
+ const chunks = [];
4148
+ for (let index = 0; index < items.length; index += size) chunks.push(items.slice(index, index + size));
4149
+ return chunks;
4150
+ };
4151
+ const mediaKey = (participantId, publicationId) => `${participantId}:${publicationId}`;
4152
+ var AppRtcChannel = class {
4153
+ rtc;
4154
+ name;
4155
+ delivery;
4156
+ handlers = /* @__PURE__ */ new Set();
4157
+ peerHandlers = /* @__PURE__ */ new Set();
4158
+ openPeers = /* @__PURE__ */ new Set();
4159
+ publication = null;
4160
+ pending = [];
4161
+ sequence = 0;
4162
+ closed = false;
4163
+ constructor(rtc, name, delivery) {
4164
+ this.rtc = rtc;
4165
+ this.name = name;
4166
+ this.delivery = delivery;
4167
+ }
4168
+ get peers() {
4169
+ return [...this.openPeers];
4170
+ }
4171
+ send(data) {
4172
+ if (this.closed || this.rtc.state === "closed") return false;
4173
+ this.sequence = this.sequence + 1 >>> 0;
4174
+ const frame = encodeAppRtcFrame(data, this.sequence);
4175
+ const channel = this.publication?.channel;
4176
+ if (channel?.readyState === "open") return this.write(channel, frame);
4177
+ if (this.delivery === "latest") this.pending = [frame];
4178
+ else if (this.pending.length >= PENDING_RELIABLE_FRAMES) return false;
4179
+ else this.pending.push(frame);
4180
+ return true;
4181
+ }
4182
+ onMessage(handler) {
4183
+ this.handlers.add(handler);
4184
+ return () => this.handlers.delete(handler);
4185
+ }
4186
+ /** Reports when this client starts or stops receiving a peer's channel. */
4187
+ onPeer(handler) {
4188
+ this.peerHandlers.add(handler);
4189
+ return () => this.peerHandlers.delete(handler);
4190
+ }
4191
+ close() {
4192
+ if (this.closed) return;
4193
+ this.closed = true;
4194
+ this.handlers.clear();
4195
+ this.peerHandlers.clear();
4196
+ this.openPeers.clear();
4197
+ this.pending = [];
4198
+ this.rtc.closeChannel(this);
4199
+ }
4200
+ /** @internal */
4201
+ get isOpen() {
4202
+ return !this.closed;
4203
+ }
4204
+ /** @internal */
4205
+ get publicationId() {
4206
+ return this.publication?.id ?? null;
4207
+ }
4208
+ /** @internal */
4209
+ attach(id, channel) {
4210
+ this.publication = {
4211
+ id,
4212
+ channel
4213
+ };
4214
+ channel.binaryType = "arraybuffer";
4215
+ const flush = () => {
4216
+ const frames = this.pending;
4217
+ this.pending = [];
4218
+ for (const frame of frames) this.write(channel, frame);
4219
+ };
4220
+ if (channel.readyState === "open") flush();
4221
+ else channel.addEventListener("open", flush, { once: true });
4222
+ }
4223
+ /** @internal */
4224
+ detach() {
4225
+ this.publication = null;
4226
+ }
4227
+ /** @internal */
4228
+ fail() {
4229
+ this.close();
4230
+ }
4231
+ /** @internal */
4232
+ deliver(from, data) {
4233
+ for (const handler of this.handlers) handler({
4234
+ from,
4235
+ data
4236
+ });
4237
+ }
4238
+ /** @internal */
4239
+ setPeer(participantId, open) {
4240
+ if (open ? this.openPeers.has(participantId) : !this.openPeers.delete(participantId)) return;
4241
+ if (open) this.openPeers.add(participantId);
4242
+ for (const handler of this.peerHandlers) handler({
4243
+ participantId,
4244
+ open
4245
+ });
4246
+ }
4247
+ write(channel, frame) {
4248
+ if (this.delivery === "latest" && channel.bufferedAmount > LATEST_BACKLOG_BYTES) return false;
4249
+ try {
4250
+ channel.send(frame);
4251
+ return true;
4252
+ } catch {
4253
+ return false;
4254
+ }
4255
+ }
4256
+ };
4257
+ var AppRoomRtc = class {
4258
+ host;
4259
+ _state = "connecting";
4260
+ pc = null;
4261
+ sessionId = null;
4262
+ queue = Promise.resolve();
4263
+ reconcileQueued = false;
4264
+ reconcileFailures = 0;
4265
+ reconnectAttempt = 0;
4266
+ reconnectTimer = null;
4267
+ generation = 0;
4268
+ filter = null;
4269
+ channels = /* @__PURE__ */ new Map();
4270
+ channelsToPublish = /* @__PURE__ */ new Set();
4271
+ remoteData = /* @__PURE__ */ new Map();
4272
+ localTracks = /* @__PURE__ */ new Map();
4273
+ remoteMedia = /* @__PURE__ */ new Map();
4274
+ mediaByMid = /* @__PURE__ */ new Map();
4275
+ stateHandlers = /* @__PURE__ */ new Set();
4276
+ trackHandlers = /* @__PURE__ */ new Set();
4277
+ offMembers;
4278
+ constructor(host) {
4279
+ this.host = host;
4280
+ let published = "";
4281
+ this.offMembers = host.onMembersChanged((members) => {
4282
+ const next = publicationsKey(members);
4283
+ if (next === published) return;
4284
+ published = next;
4285
+ this.scheduleReconcile();
4286
+ });
4287
+ }
4288
+ get state() {
4289
+ return this._state;
4290
+ }
4291
+ get tracks() {
4292
+ const tracks = [];
4293
+ for (const [key, item] of this.remoteMedia) {
4294
+ if (!item.track) continue;
4295
+ tracks.push({
4296
+ participantId: item.participantId,
4297
+ name: item.name,
4298
+ kind: item.kind,
4299
+ track: item.track,
4300
+ quality: item.quality ?? null,
4301
+ setQuality: (quality) => this.setQuality(key, quality)
4302
+ });
4303
+ }
4304
+ return tracks;
4305
+ }
4306
+ get candidates() {
4307
+ return this.mediaCandidates().map(({ candidate }) => candidate);
4308
+ }
4309
+ onStateChange(handler) {
4310
+ this.stateHandlers.add(handler);
4311
+ return () => this.stateHandlers.delete(handler);
4312
+ }
4313
+ onTracksChanged(handler) {
4314
+ this.trackHandlers.add(handler);
4315
+ return () => this.trackHandlers.delete(handler);
4316
+ }
4317
+ select(filter) {
4318
+ this.filter = filter;
4319
+ this.scheduleReconcile();
4320
+ }
4321
+ channel(name, options = {}) {
4322
+ if (!isAppRtcName(name)) throw new AppRtcError("RTC_FAILED", `invalid RTC channel name: ${name}`);
4323
+ const delivery = options.delivery ?? "reliable";
4324
+ const existing = this.channels.get(name);
4325
+ if (existing?.isOpen) {
4326
+ if (existing.delivery !== delivery) throw new AppRtcError("RTC_FAILED", `RTC channel ${name} is already open with ${existing.delivery} delivery`);
4327
+ return existing;
4328
+ }
4329
+ if (this._state === "closed") throw new AppRtcError("RTC_CLOSED", "RTC connection is closed");
4330
+ const channel = new AppRtcChannel(this, name, delivery);
4331
+ this.channels.set(name, channel);
4332
+ this.publishChannel(channel);
4333
+ this.scheduleReconcile();
4334
+ return channel;
4335
+ }
4336
+ async publish(tracks) {
4337
+ if (this._state === "closed") throw new AppRtcError("RTC_CLOSED", "RTC connection is closed");
4338
+ const inputs = Object.entries(tracks);
4339
+ for (const [name] of inputs) {
4340
+ if (!isAppRtcName(name)) throw new AppRtcError("RTC_FAILED", `invalid RTC track name: ${name}`);
4341
+ if (this.localTracks.has(name)) throw new AppRtcError("RTC_FAILED", `RTC track ${name} is already published`);
4342
+ }
4343
+ if (this.localTracks.size + inputs.length > APP_RTC_LIMITS.tracksPerMember) throw new AppRtcError("RTC_LIMIT_EXCEEDED", `at most ${APP_RTC_LIMITS.tracksPerMember} tracks per member`);
4344
+ const entries = inputs.map(([name, input]) => {
4345
+ const { track, simulcast } = "track" in input ? input : {
4346
+ track: input,
4347
+ simulcast: false
4348
+ };
4349
+ const kind = track.kind === "audio" ? "audio" : "video";
4350
+ return this.localEntry(name, kind, track, simulcast === true && kind === "video");
4351
+ });
4352
+ for (const entry of entries) this.localTracks.set(entry.name, entry);
4353
+ try {
4354
+ await this.enqueue(() => this.publishTracks(entries.map((entry) => entry.name)));
4355
+ } catch (error) {
4356
+ if (!(error instanceof ConnectionReplacedError)) {
4357
+ for (const entry of entries) this.dropLocal(entry);
4358
+ throw toAppRtcError(error);
4359
+ }
4360
+ }
4361
+ return Object.fromEntries(entries.map((entry) => [entry.name, this.localHandle(entry)]));
4362
+ }
4363
+ async stats() {
4364
+ return this.pc ? readRtcTransportStats(await this.pc.getStats()) : {
4365
+ path: null,
4366
+ relayProtocol: null,
4367
+ rttMs: null
4368
+ };
4369
+ }
4370
+ close() {
4371
+ if (this._state === "closed") return;
4372
+ this.setState("closed");
4373
+ this.offMembers();
4374
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
4375
+ for (const channel of this.channels.values()) channel.close();
4376
+ for (const entry of this.localTracks.values()) entry.track.removeEventListener("ended", entry.onEnded);
4377
+ this.teardown(true);
4378
+ }
4379
+ /** @internal */
4380
+ async start() {
4381
+ try {
4382
+ await this.connect();
4383
+ } catch (error) {
4384
+ throw toAppRtcError(error);
4385
+ }
4386
+ }
4387
+ /** @internal */
4388
+ async verify() {
4389
+ if (this._state === "closed" || !this.sessionId) return;
4390
+ try {
4391
+ await this.request("GET", `/${this.sessionId}`);
4392
+ } catch (error) {
4393
+ if (isSessionLost(error)) this.reconnect();
4394
+ }
4395
+ }
4396
+ /** @internal */
4397
+ closeChannel(channel) {
4398
+ if (this.channels.get(channel.name) === channel) this.channels.delete(channel.name);
4399
+ const id = channel.publicationId;
4400
+ channel.detach();
4401
+ if (id) this.enqueue(() => this.unpublishData([id])).catch(() => void 0);
4402
+ this.scheduleReconcile();
4403
+ }
4404
+ localEntry(name, kind, track, simulcast) {
4405
+ const entry = {
4406
+ name,
4407
+ kind,
4408
+ track,
4409
+ simulcast,
4410
+ transceiver: null,
4411
+ id: null,
4412
+ ended: /* @__PURE__ */ new Set(),
4413
+ onEnded: () => void 0
4414
+ };
4415
+ entry.onEnded = () => {
4416
+ if (this.localTracks.get(name) !== entry) return;
4417
+ for (const handler of entry.ended) handler();
4418
+ this.stopLocal(entry).catch(() => void 0);
4419
+ };
4420
+ track.addEventListener("ended", entry.onEnded);
4421
+ return entry;
4422
+ }
4423
+ localHandle(entry) {
4424
+ return {
4425
+ name: entry.name,
4426
+ kind: entry.kind,
4427
+ simulcast: entry.simulcast,
4428
+ get track() {
4429
+ return entry.track;
4430
+ },
4431
+ replace: async (track) => {
4432
+ if (track.kind !== entry.kind) throw new AppRtcError("RTC_FAILED", `replace ${entry.name} with another ${entry.kind} track`);
4433
+ if (this.localTracks.get(entry.name) !== entry) throw new AppRtcError("RTC_CLOSED", `RTC track ${entry.name} has stopped`);
4434
+ entry.track.removeEventListener("ended", entry.onEnded);
4435
+ entry.track = track;
4436
+ track.addEventListener("ended", entry.onEnded);
4437
+ await entry.transceiver?.sender.replaceTrack(track);
4438
+ },
4439
+ onEnded: (handler) => {
4440
+ entry.ended.add(handler);
4441
+ return () => entry.ended.delete(handler);
4442
+ },
4443
+ stop: () => this.stopLocal(entry)
4444
+ };
4445
+ }
4446
+ dropLocal(entry) {
4447
+ entry.track.removeEventListener("ended", entry.onEnded);
4448
+ if (this.localTracks.get(entry.name) === entry) this.localTracks.delete(entry.name);
4449
+ }
4450
+ stopLocal(entry) {
4451
+ if (this.localTracks.get(entry.name) !== entry) return Promise.resolve();
4452
+ this.dropLocal(entry);
4453
+ return this.enqueue(() => this.unpublishTrack(entry)).catch((error) => {
4454
+ if (error instanceof ConnectionReplacedError) return;
4455
+ throw toAppRtcError(error);
4456
+ });
4457
+ }
4458
+ async publishTracks(names) {
4459
+ const pc = this.requirePc();
4460
+ const entries = names.flatMap((name) => {
4461
+ const entry = this.localTracks.get(name);
4462
+ return entry && !entry.id ? [entry] : [];
4463
+ });
4464
+ if (entries.length === 0) return;
4465
+ for (const entry of entries) entry.transceiver = pc.addTransceiver(entry.track, {
4466
+ direction: "sendonly",
4467
+ ...entry.simulcast ? { sendEncodings: APP_RTC_SIMULCAST_LAYERS.map(({ rid, scaleResolutionDownBy }) => ({
4468
+ rid,
4469
+ scaleResolutionDownBy
4470
+ })) } : {}
4471
+ });
4472
+ const offer = await this.localOffer(pc);
4473
+ let result;
4474
+ try {
4475
+ result = await this.request("POST", `/${this.sessionId}/publish`, {
4476
+ offer,
4477
+ tracks: entries.map((entry) => ({
4478
+ name: entry.name,
4479
+ kind: entry.kind,
4480
+ mid: entry.transceiver?.mid ?? "",
4481
+ ...entry.simulcast ? { simulcast: true } : {}
4482
+ }))
4483
+ });
4484
+ } catch (error) {
4485
+ if (error instanceof ConnectionReplacedError) throw error;
4486
+ await this.settleOffer(pc, void 0).catch(() => void 0);
4487
+ for (const entry of entries) {
4488
+ entry.transceiver?.stop();
4489
+ entry.transceiver = null;
4490
+ }
4491
+ throw error;
4492
+ }
4493
+ await this.settleOffer(pc, result.answer);
4494
+ for (const publication of result.publications) {
4495
+ const entry = entries.find((candidate) => candidate.name === publication.name);
4496
+ if (entry) entry.id = publication.id;
4497
+ }
4498
+ }
4499
+ async unpublishTrack(entry) {
4500
+ if (!entry.id || !entry.transceiver || !this.pc) return;
4501
+ const id = entry.id;
4502
+ entry.transceiver.stop();
4503
+ await this.offerExchange(this.pc, (offer) => this.request("POST", `/${this.sessionId}/unpublish`, {
4504
+ ids: [id],
4505
+ offer
4506
+ }));
4507
+ entry.id = null;
4508
+ }
4509
+ publishChannel(channel) {
4510
+ this.channelsToPublish.add(channel);
4511
+ if (this.channelsToPublish.size > 1 || !this.pc) return;
4512
+ this.enqueue(async () => {
4513
+ const batch = [...this.channelsToPublish];
4514
+ this.channelsToPublish.clear();
4515
+ try {
4516
+ await this.publishChannels(batch);
4517
+ } catch (error) {
4518
+ if (isSessionLost(error) || error instanceof ConnectionReplacedError) throw error;
4519
+ if (error instanceof HttpError && error.code === "RTC_RATE_LIMITED") {
4520
+ setTimeout(() => {
4521
+ for (const item of batch) if (item.isOpen) this.publishChannel(item);
4522
+ }, RATE_LIMIT_RETRY_MS);
4523
+ return;
4524
+ }
4525
+ for (const item of batch) item.fail();
4526
+ console.warn("[Cohub AppRtc] failed to publish channels", error);
4527
+ }
4528
+ }).catch(() => void 0);
4529
+ }
4530
+ async connect() {
4531
+ const generation = ++this.generation;
4532
+ const opened = await this.send("POST", "", {
4533
+ ticket: this.host.ticket,
4534
+ participantId: this.host.participantId()
4535
+ });
4536
+ if (generation !== this.generation || this._state === "closed") {
4537
+ this.send("DELETE", `/${opened.sessionId}`).catch(() => void 0);
4538
+ return;
4539
+ }
4540
+ const pc = new RTCPeerConnection({
4541
+ iceServers: opened.iceServers,
4542
+ bundlePolicy: "max-bundle"
4543
+ });
4544
+ this.pc = pc;
4545
+ this.sessionId = opened.sessionId;
4546
+ pc.addEventListener("connectionstatechange", () => {
4547
+ if (this.pc !== pc) return;
4548
+ if (pc.connectionState === "connected") {
4549
+ this.reconnectAttempt = 0;
4550
+ this.setState("connected");
4551
+ } else if (pc.connectionState === "failed") this.reconnect();
4552
+ });
4553
+ pc.addEventListener("track", (event) => this.onTrack(event));
4554
+ const ready = (async () => {
4555
+ await this.answerOffer(opened.offer);
4556
+ await this.connected(pc);
4557
+ })();
4558
+ this.queue = ready.catch(() => void 0);
4559
+ try {
4560
+ await ready;
4561
+ } catch (error) {
4562
+ if (generation === this.generation) this.reconnect();
4563
+ throw error;
4564
+ }
4565
+ this.channelsToPublish.clear();
4566
+ const republish = [...this.channels.values()].filter((channel) => channel.isOpen);
4567
+ const tracks = [...this.localTracks.keys()];
4568
+ await Promise.all([republish.length ? this.enqueue(() => this.publishChannels(republish)).catch((error) => {
4569
+ if (error instanceof ConnectionReplacedError || isSessionLost(error)) return;
4570
+ for (const channel of republish) channel.fail();
4571
+ console.warn("[Cohub AppRtc] failed to publish channels on the new connection", error);
4572
+ }) : null, tracks.length ? this.enqueue(() => this.publishTracks(tracks)).catch((error) => {
4573
+ if (error instanceof ConnectionReplacedError || isSessionLost(error)) return;
4574
+ console.warn("[Cohub AppRtc] failed to publish tracks on the new connection", error);
4575
+ }) : null]);
4576
+ this.scheduleReconcile();
4577
+ }
4578
+ connected(pc) {
4579
+ if (pc.connectionState === "connected") return Promise.resolve();
4580
+ return new Promise((resolve, reject) => {
4581
+ const timer = setTimeout(() => done(/* @__PURE__ */ new Error("RTC connection timed out")), CONNECT_TIMEOUT_MS);
4582
+ const done = (error) => {
4583
+ clearTimeout(timer);
4584
+ pc.removeEventListener("connectionstatechange", check);
4585
+ if (error) reject(error);
4586
+ else resolve();
4587
+ };
4588
+ const check = () => {
4589
+ if (pc.connectionState === "connected") done();
4590
+ else if (pc.connectionState === "failed" || pc.connectionState === "closed") done(/* @__PURE__ */ new Error("RTC connection failed"));
4591
+ };
4592
+ pc.addEventListener("connectionstatechange", check);
4593
+ });
4594
+ }
4595
+ reconnect() {
4596
+ if (this._state === "closed" || this.reconnectTimer) return;
4597
+ this.setState(this._state === "connecting" ? "connecting" : "reconnecting");
4598
+ this.teardown(true);
4599
+ const delay = RECONNECT_DELAYS_MS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)] ?? 3e4;
4600
+ this.reconnectAttempt += 1;
4601
+ this.reconnectTimer = setTimeout(() => {
4602
+ this.reconnectTimer = null;
4603
+ this.connect().catch((error) => {
4604
+ if (isRoomGone(error) || error instanceof HttpError && (error.status === 403 || error.status === 503)) this.close();
4605
+ else this.reconnect();
4606
+ });
4607
+ }, delay);
4608
+ }
4609
+ teardown(release) {
4610
+ this.generation += 1;
4611
+ const sessionId = this.sessionId;
4612
+ this.sessionId = null;
4613
+ this.queue = Promise.resolve();
4614
+ for (const channel of this.channels.values()) channel.detach();
4615
+ for (const item of this.remoteData.values()) this.dropRemoteData(item);
4616
+ this.remoteData.clear();
4617
+ for (const item of this.localTracks.values()) {
4618
+ item.transceiver = null;
4619
+ item.id = null;
4620
+ }
4621
+ const hadTracks = this.remoteMedia.size > 0;
4622
+ this.remoteMedia.clear();
4623
+ this.mediaByMid.clear();
4624
+ this.pc?.close();
4625
+ this.pc = null;
4626
+ if (hadTracks) this.notifyTracks();
4627
+ if (release && sessionId) this.send("DELETE", `/${sessionId}`).catch(() => void 0);
4628
+ }
4629
+ enqueue(operation) {
4630
+ const generation = this.generation;
4631
+ const run = async () => {
4632
+ if (generation !== this.generation || !this.pc || !this.sessionId) throw new ConnectionReplacedError();
4633
+ try {
4634
+ return await operation();
4635
+ } catch (error) {
4636
+ if (isSessionLost(error)) this.reconnect();
4637
+ throw error;
4638
+ }
4639
+ };
4640
+ const next = this.queue.then(run, run);
4641
+ this.queue = next.catch(() => void 0);
4642
+ return next;
4643
+ }
4644
+ async publishChannels(channels) {
4645
+ const pc = this.requirePc();
4646
+ const open = channels.filter((channel) => channel.isOpen && !channel.publicationId);
4647
+ if (open.length === 0) return;
4648
+ const result = await this.request("POST", `/${this.sessionId}/publish`, { channels: open.map((channel) => ({
4649
+ name: channel.name,
4650
+ delivery: channel.delivery
4651
+ })) });
4652
+ for (const publication of result.publications) {
4653
+ const channel = open.find((item) => item.name === publication.name);
4654
+ const id = result.channelIds?.[publication.id];
4655
+ if (!channel || id === void 0) continue;
4656
+ const dataChannel = pc.createDataChannel(publication.name, {
4657
+ negotiated: true,
4658
+ id,
4659
+ ...policyOf(channel.delivery)
4660
+ });
4661
+ channel.attach(publication.id, dataChannel);
4662
+ if (!channel.isOpen) await this.unpublishData([publication.id]);
4663
+ }
4664
+ }
4665
+ async unpublishData(ids) {
4666
+ await this.request("POST", `/${this.sessionId}/unpublish`, { ids });
4667
+ }
4668
+ async settleOffer(pc, answer) {
4669
+ if (answer) await pc.setRemoteDescription(answer);
4670
+ else if (pc.signalingState === "have-local-offer") await pc.setLocalDescription({ type: "rollback" });
4671
+ }
4672
+ async offerExchange(pc, send) {
4673
+ const offer = await this.localOffer(pc);
4674
+ let answer;
4675
+ try {
4676
+ ({answer} = await send(offer));
4677
+ } catch (error) {
4678
+ if (!(error instanceof ConnectionReplacedError)) await this.settleOffer(pc, void 0).catch(() => void 0);
4679
+ throw error;
4680
+ }
4681
+ await this.settleOffer(pc, answer);
4682
+ }
4683
+ scheduleReconcile() {
4684
+ if (this.reconcileQueued || !this.pc || this._state === "closed") return;
4685
+ this.reconcileQueued = true;
4686
+ this.enqueue(async () => {
4687
+ this.reconcileQueued = false;
4688
+ await this.reconcile();
4689
+ this.reconcileFailures = 0;
4690
+ }).catch((error) => {
4691
+ this.reconcileQueued = false;
4692
+ if (isSessionLost(error) || error instanceof ConnectionReplacedError) return;
4693
+ console.warn("[Cohub AppRtc] failed to update subscriptions", error);
4694
+ if (error instanceof HttpError && error.code === "RTC_LIMIT_EXCEEDED") return;
4695
+ const delay = RECONNECT_DELAYS_MS[Math.min(this.reconcileFailures, RECONNECT_DELAYS_MS.length - 1)] ?? 3e4;
4696
+ this.reconcileFailures += 1;
4697
+ const generation = this.generation;
4698
+ setTimeout(() => {
4699
+ if (generation === this.generation) this.scheduleReconcile();
4700
+ }, delay);
4701
+ });
4702
+ }
4703
+ mediaCandidates() {
4704
+ const self = this.host.participantId();
4705
+ const candidates = [];
4706
+ for (const member of this.host.members()) {
4707
+ if (member.participantId === self) continue;
4708
+ for (const publication of member.publications ?? []) {
4709
+ if (publication.kind === "data") continue;
4710
+ candidates.push({
4711
+ publication,
4712
+ candidate: {
4713
+ participantId: member.participantId,
4714
+ name: publication.name,
4715
+ kind: publication.kind,
4716
+ simulcast: publication.simulcast === true,
4717
+ subscribed: this.remoteMedia.has(mediaKey(member.participantId, publication.id))
4718
+ }
4719
+ });
4720
+ }
4721
+ }
4722
+ return candidates;
4723
+ }
4724
+ async reconcile() {
4725
+ const self = this.host.participantId();
4726
+ const wantedData = /* @__PURE__ */ new Map();
4727
+ for (const member of this.host.members()) {
4728
+ if (member.participantId === self) continue;
4729
+ for (const publication of member.publications ?? []) {
4730
+ if (publication.kind !== "data") continue;
4731
+ const channel = this.channels.get(publication.name);
4732
+ if (channel?.isOpen && channel.delivery === (publication.delivery ?? "reliable")) wantedData.set(mediaKey(member.participantId, publication.id), {
4733
+ participantId: member.participantId,
4734
+ publicationId: publication.id,
4735
+ name: publication.name,
4736
+ delivery: channel.delivery
4737
+ });
4738
+ }
4739
+ }
4740
+ const wantedMedia = /* @__PURE__ */ new Map();
4741
+ for (const { candidate, publication } of this.mediaCandidates()) {
4742
+ if (wantedMedia.size >= APP_RTC_LIMITS.mediaSubscriptions) break;
4743
+ if (this.filter && !this.filter(candidate)) continue;
4744
+ const key = mediaKey(candidate.participantId, publication.id);
4745
+ const quality = this.remoteMedia.get(key)?.quality ?? (candidate.simulcast ? "high" : void 0);
4746
+ wantedMedia.set(key, {
4747
+ participantId: candidate.participantId,
4748
+ publicationId: publication.id,
4749
+ name: candidate.name,
4750
+ kind: candidate.kind,
4751
+ ...quality ? { quality } : {}
4752
+ });
4753
+ }
4754
+ await this.reconcileData(wantedData);
4755
+ await this.reconcileMedia(wantedMedia);
4756
+ if ([...wantedData.keys()].some((key) => !this.remoteData.has(key)) || [...wantedMedia.keys()].some((key) => !this.remoteMedia.has(key))) throw new AppRtcError("RTC_FAILED", "Some RTC subscriptions could not be established");
4757
+ }
4758
+ dropRemoteData(item) {
4759
+ item.channel.close();
4760
+ this.channels.get(item.name)?.setPeer(item.participantId, false);
4761
+ }
4762
+ async reconcileData(wanted) {
4763
+ const pc = this.requirePc();
4764
+ const stale = [...this.remoteData].filter(([key]) => !wanted.has(key));
4765
+ for (const batch of chunk(stale, APP_RTC_LIMITS.batch)) {
4766
+ await this.request("POST", `/${this.sessionId}/unsubscribe`, { items: batch.map(([, item]) => ({
4767
+ participantId: item.participantId,
4768
+ publicationId: item.publicationId
4769
+ })) });
4770
+ for (const [key, item] of batch) {
4771
+ this.dropRemoteData(item);
4772
+ this.remoteData.delete(key);
4773
+ }
4774
+ }
4775
+ const missing = [...wanted].filter(([key]) => !this.remoteData.has(key)).map(([, item]) => item);
4776
+ for (const batch of chunk(missing, APP_RTC_LIMITS.batch)) {
4777
+ const result = await this.request("POST", `/${this.sessionId}/subscribe`, {
4778
+ kind: "data",
4779
+ items: batch.map(({ participantId, publicationId }) => ({
4780
+ participantId,
4781
+ publicationId
4782
+ }))
4783
+ });
4784
+ for (const subscription of result.subscriptions) {
4785
+ const item = batch.find((entry) => entry.participantId === subscription.participantId && entry.publicationId === subscription.publicationId);
4786
+ if (!item || subscription.channelId === void 0) continue;
4787
+ const channel = pc.createDataChannel(item.name, {
4788
+ negotiated: true,
4789
+ id: subscription.channelId,
4790
+ ...policyOf(item.delivery)
4791
+ });
4792
+ channel.binaryType = "arraybuffer";
4793
+ const remote = {
4794
+ name: item.name,
4795
+ participantId: item.participantId,
4796
+ publicationId: item.publicationId,
4797
+ channel,
4798
+ last: null
4799
+ };
4800
+ channel.addEventListener("message", (event) => this.onData(remote, event.data));
4801
+ channel.addEventListener("open", () => {
4802
+ if (this.remoteData.get(mediaKey(remote.participantId, remote.publicationId)) === remote) this.channels.get(remote.name)?.setPeer(remote.participantId, true);
4803
+ });
4804
+ this.remoteData.set(mediaKey(item.participantId, item.publicationId), remote);
4805
+ }
4806
+ }
4807
+ }
4808
+ async reconcileMedia(wanted) {
4809
+ const stale = [...this.remoteMedia].filter(([key]) => !wanted.has(key));
4810
+ const missing = [...wanted].filter(([key]) => !this.remoteMedia.has(key)).map(([, item]) => item);
4811
+ const moves = [];
4812
+ for (const to of [...missing]) {
4813
+ const index = stale.findIndex(([, receiver]) => receiver.kind === to.kind);
4814
+ const found = index >= 0 ? stale.splice(index, 1)[0] : void 0;
4815
+ if (!found) continue;
4816
+ moves.push({
4817
+ from: found[0],
4818
+ receiver: found[1],
4819
+ to
4820
+ });
4821
+ missing.splice(missing.indexOf(to), 1);
4822
+ }
4823
+ if (moves.length) {
4824
+ const moved = await this.retarget(moves.map(({ receiver, to }) => ({
4825
+ ...to,
4826
+ mid: receiver.mid
4827
+ })));
4828
+ for (const { from, receiver, to } of moves) if (moved.has(receiver.mid)) {
4829
+ this.remoteMedia.delete(from);
4830
+ this.remoteMedia.set(mediaKey(to.participantId, to.publicationId), {
4831
+ ...to,
4832
+ mid: receiver.mid,
4833
+ track: receiver.track
4834
+ });
4835
+ this.mediaByMid.set(receiver.mid, mediaKey(to.participantId, to.publicationId));
4836
+ } else {
4837
+ stale.push([from, receiver]);
4838
+ missing.push(to);
4839
+ }
4840
+ if (moved.size) this.notifyTracks();
4841
+ }
4842
+ await this.unsubscribeMedia(stale);
4843
+ await this.subscribeMedia(missing);
4844
+ }
4845
+ async retarget(items) {
4846
+ try {
4847
+ const result = await this.request("PATCH", `/${this.sessionId}/media`, { items: items.map(({ participantId, publicationId, mid, quality }) => ({
4848
+ participantId,
4849
+ publicationId,
4850
+ mid,
4851
+ ...quality ? { quality } : {}
4852
+ })) });
4853
+ if (result.offer) await this.answerOffer(result.offer);
4854
+ return new Set(result.updated.map((item) => item.mid));
4855
+ } catch (error) {
4856
+ if (error instanceof ConnectionReplacedError || isSessionLost(error)) throw error;
4857
+ return /* @__PURE__ */ new Set();
4858
+ }
4859
+ }
4860
+ async setQuality(key, quality) {
4861
+ await this.enqueue(async () => {
4862
+ const item = this.remoteMedia.get(key);
4863
+ if (!item || item.quality === void 0 || item.quality === quality) return;
4864
+ if ((await this.retarget([{
4865
+ ...item,
4866
+ quality
4867
+ }])).has(item.mid)) {
4868
+ item.quality = quality;
4869
+ this.notifyTracks();
4870
+ }
4871
+ }).catch((error) => {
4872
+ if (error instanceof ConnectionReplacedError) return;
4873
+ throw toAppRtcError(error);
4874
+ });
4875
+ }
4876
+ async unsubscribeMedia(stale) {
4877
+ if (stale.length === 0) return;
4878
+ const pc = this.requirePc();
4879
+ for (const [, item] of stale) {
4880
+ pc.getTransceivers().find((transceiver) => transceiver.mid === item.mid)?.stop();
4881
+ item.track = null;
4882
+ }
4883
+ this.notifyTracks();
4884
+ await this.offerExchange(pc, (offer) => this.request("POST", `/${this.sessionId}/unsubscribe`, {
4885
+ items: stale.map(([, item]) => ({
4886
+ participantId: item.participantId,
4887
+ publicationId: item.publicationId
4888
+ })),
4889
+ offer
4890
+ }));
4891
+ for (const [key, item] of stale) {
4892
+ this.remoteMedia.delete(key);
4893
+ this.mediaByMid.delete(item.mid);
4894
+ }
4895
+ }
4896
+ async subscribeMedia(missing) {
4897
+ if (missing.length === 0) return;
4898
+ const result = await this.request("POST", `/${this.sessionId}/subscribe`, {
4899
+ kind: "media",
4900
+ items: missing.map(({ participantId, publicationId, quality }) => ({
4901
+ participantId,
4902
+ publicationId,
4903
+ ...quality ? { quality } : {}
4904
+ }))
4905
+ });
4906
+ for (const subscription of result.subscriptions) {
4907
+ const item = missing.find((entry) => entry.participantId === subscription.participantId && entry.publicationId === subscription.publicationId);
4908
+ if (!item || !subscription.mid) continue;
4909
+ const key = mediaKey(item.participantId, item.publicationId);
4910
+ this.remoteMedia.set(key, {
4911
+ ...item,
4912
+ mid: subscription.mid,
4913
+ track: null
4914
+ });
4915
+ this.mediaByMid.set(subscription.mid, key);
4916
+ }
4917
+ if (!result.offer) return;
4918
+ try {
4919
+ await this.answerOffer(result.offer);
4920
+ } catch (error) {
4921
+ if (!(error instanceof ConnectionReplacedError)) this.reconnect();
4922
+ throw error;
4923
+ }
4924
+ }
4925
+ async answerOffer(offer) {
4926
+ const pc = this.requirePc();
4927
+ await pc.setRemoteDescription(offer);
4928
+ await pc.setLocalDescription(await pc.createAnswer());
4929
+ await waitForIceGathering(pc, GATHER_TIMEOUT_MS);
4930
+ const answer = pc.localDescription;
4931
+ if (!answer) throw new Error("the browser produced no answer");
4932
+ await this.request("PUT", `/${this.sessionId}`, { answer: {
4933
+ type: "answer",
4934
+ sdp: answer.sdp
4935
+ } });
4936
+ }
4937
+ async localOffer(pc) {
4938
+ await pc.setLocalDescription(await pc.createOffer());
4939
+ await waitForIceGathering(pc, GATHER_TIMEOUT_MS);
4940
+ const offer = pc.localDescription;
4941
+ if (!offer) throw new Error("the browser produced no offer");
4942
+ return {
4943
+ type: "offer",
4944
+ sdp: offer.sdp
4945
+ };
4946
+ }
4947
+ async request(method, path, body) {
4948
+ const generation = this.generation;
4949
+ const settled = await this.send(method, path, body).then((value) => ({ value }), (error) => ({ error }));
4950
+ if (generation !== this.generation) throw new ConnectionReplacedError();
4951
+ if ("error" in settled) throw settled.error;
4952
+ return settled.value;
4953
+ }
4954
+ send(method, path, body) {
4955
+ const url = `/api/apps/${encodeURIComponent(this.host.appId)}/realtime/rooms/${encodeURIComponent(this.host.roomId)}/rtc${path}`;
4956
+ return this.host.transport.request(url, {
4957
+ method,
4958
+ ...body === void 0 ? {} : {
4959
+ headers: { "Content-Type": "application/json" },
4960
+ body: JSON.stringify(body)
4961
+ }
4962
+ });
4963
+ }
4964
+ requirePc() {
4965
+ if (!this.pc || !this.sessionId) throw new ConnectionReplacedError();
4966
+ return this.pc;
4967
+ }
4968
+ onData(remote, raw) {
4969
+ if (!(raw instanceof ArrayBuffer)) return;
4970
+ const frame = decodeAppRtcFrame(raw);
4971
+ if (!frame) return;
4972
+ const channel = this.channels.get(remote.name);
4973
+ if (!channel?.isOpen) return;
4974
+ if (channel.delivery === "latest") {
4975
+ if (!isNewerSequence(frame.sequence, remote.last)) return;
4976
+ remote.last = frame.sequence;
4977
+ }
4978
+ channel.deliver(remote.participantId, frame.data);
4979
+ }
4980
+ onTrack(event) {
4981
+ const mid = event.transceiver.mid;
4982
+ const key = mid ? this.mediaByMid.get(mid) : void 0;
4983
+ const item = key ? this.remoteMedia.get(key) : void 0;
4984
+ if (!item || !mid) return;
4985
+ item.track = event.track;
4986
+ event.track.addEventListener("ended", () => {
4987
+ const current = this.mediaByMid.get(mid);
4988
+ const owner = current ? this.remoteMedia.get(current) : void 0;
4989
+ if (owner?.track === event.track) {
4990
+ owner.track = null;
4991
+ this.notifyTracks();
4992
+ }
4993
+ });
4994
+ this.notifyTracks();
4995
+ }
4996
+ notifyTracks() {
4997
+ const tracks = this.tracks;
4998
+ for (const handler of this.trackHandlers) handler(tracks);
4999
+ }
5000
+ setState(state) {
5001
+ if (this._state === state) return;
5002
+ this._state = state;
5003
+ for (const handler of this.stateHandlers) handler(state);
5004
+ }
5005
+ };
5006
+ //#endregion
3890
5007
  //#region src/apis/app-realtime.ts
3891
5008
  const randomId = () => globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
3892
5009
  const textEncoder = new TextEncoder();
@@ -3895,13 +5012,20 @@ const byteLength = (text) => textEncoder.encode(text).byteLength;
3895
5012
  * Ceiling on deltas held during a join, so a stalled handshake cannot buffer without
3896
5013
  * limit. Dropping the tail needs no extra signal: the gap detection below reports it.
3897
5014
  */
3898
- const WORK_ROOM_MAX_JOIN_BUFFER = 512;
5015
+ const APP_ROOM_MAX_JOIN_BUFFER = 512;
3899
5016
  /** Room events that mutate state, so they must not be applied before the join snapshot. */
3900
- const WORK_ROOM_DELTA_EVENTS = /* @__PURE__ */ new Set([
5017
+ const APP_ROOM_DELTA_EVENTS = /* @__PURE__ */ new Set([
3901
5018
  "realtime.room.event",
3902
5019
  "realtime.room.member.joined",
3903
5020
  "realtime.room.member.left",
3904
- "realtime.room.presence.updated"
5021
+ "realtime.room.presence.updated",
5022
+ "realtime.room.publications.updated"
5023
+ ]);
5024
+ const APP_ROOM_MEMBER_EVENTS = /* @__PURE__ */ new Set([
5025
+ "realtime.room.member.joined",
5026
+ "realtime.room.member.left",
5027
+ "realtime.room.presence.updated",
5028
+ "realtime.room.publications.updated"
3905
5029
  ]);
3906
5030
  const requestError = (event) => {
3907
5031
  const payload = event.payload;
@@ -3914,6 +5038,7 @@ const isRoomEvent = (event, roomId) => {
3914
5038
  var AppRoom = class {
3915
5039
  websocket;
3916
5040
  admission;
5041
+ appContext;
3917
5042
  id;
3918
5043
  code;
3919
5044
  createdAt;
@@ -3941,9 +5066,12 @@ var AppRoom = class {
3941
5066
  offOpen;
3942
5067
  offClose;
3943
5068
  offReconnecting;
3944
- constructor(websocket, admission) {
5069
+ instanceId = randomId().replace(/[^A-Za-z0-9_-]/g, "");
5070
+ rtcConnection = null;
5071
+ constructor(websocket, admission, appContext) {
3945
5072
  this.websocket = websocket;
3946
5073
  this.admission = admission;
5074
+ this.appContext = appContext;
3947
5075
  this.id = admission.room.id;
3948
5076
  this.code = admission.room.code;
3949
5077
  this.createdAt = admission.room.createdAt;
@@ -4069,6 +5197,66 @@ var AppRoom = class {
4069
5197
  clientEventId: typeof payload.clientEventId === "string" ? payload.clientEventId : null
4070
5198
  };
4071
5199
  }
5200
+ /** Published events after `after`, oldest first; without it, the newest ones. */
5201
+ async history(input = {}) {
5202
+ this.assertJoined();
5203
+ if (!this.websocket.supportsCapability("realtime.room.history.v1")) throw new Error("Room history is not supported by the Gateway");
5204
+ const requestId = randomId();
5205
+ const response = await this.request(requestId, /* @__PURE__ */ new Set(["realtime.room.history"]), () => this.websocket.readRealtimeRoomHistory({
5206
+ roomId: this.id,
5207
+ ...input,
5208
+ requestId
5209
+ }));
5210
+ return this.toHistory(response.payload);
5211
+ }
5212
+ /** An invite for someone outside the App, e.g. an Agent: `cohub rooms join <invite>`. */
5213
+ async invite(input = {}) {
5214
+ this.assertJoined();
5215
+ const context = this.appContext;
5216
+ if (!context) throw new Error("Invites are only available inside a published App");
5217
+ return context.transport.request(`/api/apps/${encodeURIComponent(context.appId)}/realtime/rooms/${encodeURIComponent(this.id)}/invites`, {
5218
+ method: "POST",
5219
+ headers: { "Content-Type": "application/json" },
5220
+ body: JSON.stringify({
5221
+ ticket: this.admission.ticket,
5222
+ ...input
5223
+ })
5224
+ });
5225
+ }
5226
+ rtc() {
5227
+ this.assertJoined();
5228
+ if (this.rtcConnection) {
5229
+ const current = this.rtcConnection;
5230
+ return current.then((rtc) => {
5231
+ if (rtc.state !== "closed") return rtc;
5232
+ return (this.rtcConnection === current ? this.openRtc() : this.rtcConnection) ?? this.openRtc();
5233
+ });
5234
+ }
5235
+ return this.openRtc();
5236
+ }
5237
+ openRtc() {
5238
+ this.assertJoined();
5239
+ const context = this.appContext;
5240
+ if (!context) return Promise.reject(new AppRtcError("RTC_UNSUPPORTED", "RTC is only available inside a published App"));
5241
+ if (typeof RTCPeerConnection === "undefined") return Promise.reject(new AppRtcError("RTC_UNSUPPORTED", "RTC needs a browser with WebRTC"));
5242
+ if (!this.websocket.supportsCapability("realtime.room.rtc.v1")) return Promise.reject(new AppRtcError("RTC_UNSUPPORTED", "RTC is not supported by the Gateway"));
5243
+ const rtc = new AppRoomRtc({
5244
+ roomId: this.id,
5245
+ appId: context.appId,
5246
+ ticket: this.admission.ticket,
5247
+ transport: context.transport,
5248
+ participantId: () => this._participantId,
5249
+ members: () => this._members,
5250
+ onMembersChanged: (handler) => this.onMembersChanged(handler)
5251
+ });
5252
+ const opening = rtc.start().then(() => rtc, (error) => {
5253
+ rtc.close();
5254
+ if (this.rtcConnection === opening) this.rtcConnection = null;
5255
+ throw error;
5256
+ });
5257
+ this.rtcConnection = opening;
5258
+ return opening;
5259
+ }
4072
5260
  async setPresence(presence) {
4073
5261
  this.assertJoined();
4074
5262
  const requestId = randomId();
@@ -4110,17 +5298,23 @@ var AppRoom = class {
4110
5298
  if (!this.websocket.supportsCapability("realtime.room.v1") && this.websocket.state === "open") throw new Error("Realtime rooms are not supported by the Gateway");
4111
5299
  this.setState(isReconnect ? "reconnecting" : "connecting");
4112
5300
  const requestId = randomId();
5301
+ const after = isReconnect && this.lastSequence > 0 && this.websocket.supportsCapability("realtime.room.history.v1") ? this.lastSequence : void 0;
5302
+ let missed = null;
4113
5303
  this.joinBuffer = [];
4114
5304
  try {
4115
5305
  const payload = (await this.request(requestId, /* @__PURE__ */ new Set(["realtime.room.joined"]), () => this.websocket.joinRealtimeRoom({
4116
5306
  roomId: this.id,
4117
5307
  ticket: this.admission.ticket,
5308
+ instanceId: this.instanceId,
5309
+ after,
4118
5310
  requestId
4119
5311
  }))).payload;
4120
5312
  if (typeof payload.participantId !== "string" || !Array.isArray(payload.members)) throw new Error("invalid room join response");
4121
5313
  this._participantId = payload.participantId;
4122
5314
  this._members = payload.members;
4123
- this.lastSequence = typeof payload.sequence === "number" ? payload.sequence : 0;
5315
+ const sequence = typeof payload.sequence === "number" ? payload.sequence : 0;
5316
+ if (after !== void 0 && sequence > after) missed = await this.catchUp(payload.missed, after, sequence);
5317
+ this.lastSequence = sequence;
4124
5318
  } catch (error) {
4125
5319
  this.joinBuffer = null;
4126
5320
  throw error;
@@ -4128,6 +5322,11 @@ var AppRoom = class {
4128
5322
  this.hasJoined = true;
4129
5323
  this.setState("joined");
4130
5324
  this.notifyMembers();
5325
+ if (missed) {
5326
+ const { lost } = missed;
5327
+ if (lost) for (const handler of this.outOfSyncHandlers) handler(lost[0], lost[1]);
5328
+ for (const event of missed.events) this.emit(event);
5329
+ }
4131
5330
  const buffered = this.joinBuffer ?? [];
4132
5331
  this.joinBuffer = null;
4133
5332
  for (const item of buffered) {
@@ -4135,6 +5334,7 @@ var AppRoom = class {
4135
5334
  if (typeof sequence === "number" && sequence <= this.lastSequence) continue;
4136
5335
  this.handleEvent(item);
4137
5336
  }
5337
+ if (isReconnect) this.rtcConnection?.then((rtc) => rtc.verify()).catch(() => void 0);
4138
5338
  }
4139
5339
  request(requestId, expected, send) {
4140
5340
  return new Promise((resolve, reject) => {
@@ -4182,14 +5382,13 @@ var AppRoom = class {
4182
5382
  this.takePending(event.requestId);
4183
5383
  awaiting.resolve(event);
4184
5384
  }
4185
- if (this.joinBuffer && WORK_ROOM_DELTA_EVENTS.has(event.type)) {
4186
- if (this.joinBuffer.length < WORK_ROOM_MAX_JOIN_BUFFER) this.joinBuffer.push(event);
5385
+ if (this.joinBuffer && APP_ROOM_DELTA_EVENTS.has(event.type)) {
5386
+ if (this.joinBuffer.length < APP_ROOM_MAX_JOIN_BUFFER) this.joinBuffer.push(event);
4187
5387
  return;
4188
5388
  }
4189
5389
  const payload = event.payload;
4190
- const sequence = typeof payload.sequence === "number" ? payload.sequence : null;
4191
- if (sequence !== null) this.observeSequence(sequence);
4192
- if (event.type === "realtime.room.member.joined" || event.type === "realtime.room.member.left" || event.type === "realtime.room.presence.updated") {
5390
+ if (APP_ROOM_DELTA_EVENTS.has(event.type) && typeof payload.sequence === "number") this.observeSequence(payload.sequence);
5391
+ if (APP_ROOM_MEMBER_EVENTS.has(event.type)) {
4193
5392
  const member = payload.member;
4194
5393
  if (member?.participantId) {
4195
5394
  const members = new Map(this._members.map((item) => [item.participantId, item]));
@@ -4209,11 +5408,10 @@ var AppRoom = class {
4209
5408
  return;
4210
5409
  }
4211
5410
  if (event.type !== "realtime.room.event") return;
4212
- const roomEvent = event;
4213
- const roomPayload = roomEvent.payload;
4214
- const item = {
4215
- id: roomEvent.id,
4216
- timestamp: roomEvent.timestamp,
5411
+ const { id, timestamp, payload: roomPayload } = event;
5412
+ this.emit({
5413
+ id,
5414
+ timestamp,
4217
5415
  roomId: roomPayload.roomId,
4218
5416
  sequence: roomPayload.sequence,
4219
5417
  type: roomPayload.event,
@@ -4221,7 +5419,35 @@ var AppRoom = class {
4221
5419
  clientEventId: roomPayload.clientEventId,
4222
5420
  sender: roomPayload.sender,
4223
5421
  self: roomPayload.sender.participantId === this.participantId
5422
+ });
5423
+ }
5424
+ async catchUp(first, after, sequence) {
5425
+ if (!first) return {
5426
+ events: [],
5427
+ lost: [after + 1, sequence]
5428
+ };
5429
+ const events = this.toHistory(first).events;
5430
+ const { truncated } = first;
5431
+ let { cursor } = first;
5432
+ while (cursor < sequence) {
5433
+ const requestId = randomId();
5434
+ const page = (await this.request(requestId, /* @__PURE__ */ new Set(["realtime.room.history"]), () => this.websocket.readRealtimeRoomHistory({
5435
+ roomId: this.id,
5436
+ after: cursor,
5437
+ requestId
5438
+ })).catch(() => null))?.payload;
5439
+ if (!page || page.cursor <= cursor) break;
5440
+ events.push(...this.toHistory(page).events);
5441
+ cursor = page.cursor;
5442
+ }
5443
+ const kept = events.filter((event) => event.sequence <= sequence);
5444
+ const lost = truncated ? [after + 1, kept[0]?.sequence ?? sequence] : cursor < sequence ? [cursor + 1, sequence] : null;
5445
+ return {
5446
+ events: kept,
5447
+ lost: lost && lost[1] > lost[0] ? lost : null
4224
5448
  };
5449
+ }
5450
+ emit(item) {
4225
5451
  this.handlers.get(item.type)?.forEach((handler) => {
4226
5452
  handler(item);
4227
5453
  });
@@ -4229,6 +5455,18 @@ var AppRoom = class {
4229
5455
  handler(item);
4230
5456
  });
4231
5457
  }
5458
+ toHistory(history) {
5459
+ return {
5460
+ events: history.events.map((event) => ({
5461
+ ...event,
5462
+ data: event.data,
5463
+ roomId: this.id,
5464
+ self: event.sender.participantId === this.participantId
5465
+ })),
5466
+ cursor: history.cursor,
5467
+ truncated: history.truncated
5468
+ };
5469
+ }
4232
5470
  observeSequence(sequence) {
4233
5471
  if (this.lastSequence > 0 && sequence > this.lastSequence + 1) for (const handler of this.outOfSyncHandlers) handler(this.lastSequence + 1, sequence);
4234
5472
  if (sequence > this.lastSequence) this.lastSequence = sequence;
@@ -4260,6 +5498,9 @@ var AppRoom = class {
4260
5498
  this.dispose();
4261
5499
  }
4262
5500
  dispose() {
5501
+ const rtc = this.rtcConnection;
5502
+ this.rtcConnection = null;
5503
+ rtc?.then((connection) => connection.close()).catch(() => void 0);
4263
5504
  this.offEvent();
4264
5505
  this.offOpen();
4265
5506
  this.offClose();
@@ -4278,28 +5519,33 @@ var AppRealtimeApi = class {
4278
5519
  this.getContext = getContext;
4279
5520
  }
4280
5521
  async createRoom(input = {}) {
4281
- const admission = await this.transport.request(`/api/apps/${encodeURIComponent(await this.requireAppId())}/realtime/rooms`, {
5522
+ const appId = await this.requireAppId();
5523
+ const admission = await this.transport.request(`/api/apps/${encodeURIComponent(appId)}/realtime/rooms`, {
4282
5524
  method: "POST",
4283
5525
  headers: { "Content-Type": "application/json" },
4284
5526
  body: JSON.stringify(input)
4285
5527
  });
4286
- return this.openRoom(admission);
5528
+ return this.openRoom(appId, admission);
4287
5529
  }
4288
5530
  async joinRoom(input) {
4289
- const admission = await this.transport.request(`/api/apps/${encodeURIComponent(await this.requireAppId())}/realtime/rooms/join`, {
5531
+ const appId = await this.requireAppId();
5532
+ const admission = await this.transport.request(`/api/apps/${encodeURIComponent(appId)}/realtime/rooms/join`, {
4290
5533
  method: "POST",
4291
5534
  headers: { "Content-Type": "application/json" },
4292
5535
  body: JSON.stringify(input)
4293
5536
  });
4294
- return this.openRoom(admission);
5537
+ return this.openRoom(appId, admission);
4295
5538
  }
4296
5539
  async requireAppId() {
4297
5540
  const context = await this.getContext();
4298
5541
  if (!context?.app?.id) throw new Error("App context is unavailable — not running inside a published app runtime.");
4299
5542
  return context.app.id;
4300
5543
  }
4301
- async openRoom(admission) {
4302
- const room = new AppRoom(this.websocket, admission);
5544
+ async openRoom(appId, admission) {
5545
+ const room = new AppRoom(this.websocket, admission, {
5546
+ transport: this.transport,
5547
+ appId
5548
+ });
4303
5549
  try {
4304
5550
  await room.connect();
4305
5551
  return room;
@@ -4566,10 +5812,12 @@ var AppWindowApi = class {
4566
5812
  }
4567
5813
  /** Report the tab title, save status, and whether work is still unsaved. */
4568
5814
  setState(patch) {
4569
- this.state = {
5815
+ const next = {
4570
5816
  ...this.state,
4571
5817
  ...patch
4572
5818
  };
5819
+ if (this.stateReported && next.title === this.state.title && next.status === this.state.status && next.dirty === this.state.dirty) return;
5820
+ this.state = next;
4573
5821
  this.stateReported = true;
4574
5822
  this.listen();
4575
5823
  this.transport.notify?.(buildAppRuntimeWindowState(this.state));
@@ -5376,6 +6624,18 @@ var AppRuntimeApi = class {
5376
6624
  reason: response === null ? "timeout" : "unsupported"
5377
6625
  };
5378
6626
  }
6627
+ /** Asks for devices declared with `<meta name="cohub:devices">`; allowing the camera or microphone reloads the App once. */
6628
+ async requestDevices(devices) {
6629
+ const wanted = parseAppDevices(devices);
6630
+ if (wanted.length === 0) throw new AppRuntimeError("invalid_request", `Request at least one of: ${APP_DEVICES.join(", ")}.`);
6631
+ const context = await this.context().catch(() => null);
6632
+ if (!context || context.mode === "broker") return "granted";
6633
+ if (!context.capabilities?.devices) return "unavailable";
6634
+ return parseAppDevicesResult(await this.transport.request({
6635
+ type: APP_DEVICES_REQUEST_TYPE,
6636
+ devices: wanted
6637
+ }, { timeoutMs: 6e5 }))?.status ?? "unavailable";
6638
+ }
5379
6639
  /**
5380
6640
  * Asks the host to close this App: a workspace tab closes, an embedded page
5381
6641
  * forwards the request to its embedder, a standalone page closes the tab.
@@ -5740,6 +7000,7 @@ var CohubClient = class {
5740
7000
  voice;
5741
7001
  apps;
5742
7002
  appCommerce;
7003
+ rooms;
5743
7004
  navigation;
5744
7005
  /** @deprecated Use `client.desktop`. */
5745
7006
  get ui() {
@@ -5819,6 +7080,7 @@ var CohubClient = class {
5819
7080
  this.referrals = new ReferralsApi(this.transport);
5820
7081
  this.apps = new AppsApi(this.transport);
5821
7082
  this.appCommerce = new AppCommerceApi(this.transport);
7083
+ this.rooms = new RoomsApi(this.transport);
5822
7084
  this.navigation = { open: (target, options) => this.appRuntime.navigationOpen(typeof target === "string" ? {
5823
7085
  kind: "app",
5824
7086
  ref: target
@@ -5875,6 +7137,7 @@ sync: (root) => {
5875
7137
  } },
5876
7138
  /** Ask the host to close this App's surface. */
5877
7139
  requestClose: () => this.appRuntime.requestClose(),
7140
+ devices: { request: (devices) => this.appRuntime.requestDevices(devices) },
5878
7141
  /**
5879
7142
  * Request that the host update this overlay's geometry or pointer hit
5880
7143
  * regions. Only meaningful for `overlay` surfaces; ignored otherwise.
@@ -6548,7 +7811,8 @@ function createAppBridgeCore(config) {
6548
7811
  return {
6549
7812
  capabilities: {
6550
7813
  authorization: 2,
6551
- serverGrants: true
7814
+ serverGrants: true,
7815
+ ...config.devices ? { devices: true } : {}
6552
7816
  },
6553
7817
  mode: authorizationContext.surface === "broker" ? "broker" : "bridge",
6554
7818
  app: {
@@ -7814,4 +9078,4 @@ function generationOutputSource(result, outputIndex) {
7814
9078
  return `data:${declared && MEDIA_TYPE_PATTERN.test(declared) ? declared : DEFAULT_MEDIA_TYPES[String(block.type)] ?? "application/octet-stream"};base64,${data}`;
7815
9079
  }
7816
9080
  //#endregion
7817
- export { APP_APPEARANCE_TOKENS, APP_COMPOSER_CHIP_CONTENT_MAX_BYTES, APP_COMPOSER_CHIP_KEY_MAX_LENGTH, APP_COMPOSER_CHIP_LABEL_MAX_LENGTH, APP_SURFACE_READY_TIMEOUT_MS, APP_SURFACE_REQUEST_TIMEOUT_MS, AppCommerceApi, AppRealtimeApi, AppRefParseError, AppRoom, AppRuntimeApi, AppRuntimeApi as WorkRuntimeApi, AppRuntimeError, AppSurfaceApi, AppWindowApi, AppsApi, BILLING_ACCESS_BLOCKED_ERROR_CODE, BOARD_COLOR_TOKENS, BOARD_ITEM_TYPES, BillingApi, BoardAnimationSchema, BoardClient, BoardPatchSchema, BoardTrackSchema, COHUB_ENVIRONMENTS, COHUB_SOURCE_HEADER, COHUB_SOURCE_HEADER_NAMES, CohubClient, CohubHttpClient, DESKTOP_COMMAND_ACCEPT_TIMEOUT_MS, DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS, DESKTOP_COMMAND_MAX_TIMEOUT_MS, DESKTOP_COMMAND_OPEN_TIMEOUT_MS, DESKTOP_COMMAND_PAYLOAD_MAX_BYTES, DESKTOP_COMMAND_PENDING_TTL_SECONDS, DESKTOP_COMMAND_SETTLEMENT_GRACE_SECONDS, DESKTOP_COMMAND_TERMINAL_TTL_SECONDS, DESKTOP_COMMAND_VERSION, DISPLAY_ACTION_TYPES, DISPLAY_ELEMENT_REF_PATTERN, DISPLAY_SYSTEM_ACTIONS, DesktopCommandsApi, DisplayActionError, DisplayConnection, FEATURE_NOT_ENTITLED_ERROR_CODE, GenerationPolicyError, HOME_SPACE_SLUG, HttpError, NATIVE_INGEST_MAX_TURNS, NATIVE_SYNC_SOURCE, OriginBrokerTransport, PERMISSIONS, ParentBridgeTransport, PopupBrokerTransport, REQUEST_SOURCE_VIA_MAX_LENGTH, RUNTIME_ARCHIVE_SEGMENT_BYTES, RUNTIME_MAX_FRAME_BYTES, RUNTIME_PROTOCOL_VERSION, RUNTIME_RECOVERY_BATCH_SIZE, ReferencesApi, ReferralsApi, SETTLED_TURN_STATUSES, SessionGenerationStreamClient, SessionPatchReducer, SpaceDisplaysApi, SpacePublicFilesApi, SpaceWebhooksApi, UPLOAD_MAX_BATCH_BYTES, UPLOAD_MAX_BATCH_FILES, UPLOAD_MAX_FILE_BYTES, USER_LEVEL_PERMISSIONS, UiCommandsApi, UsersApi, VoiceApi, VoiceInputClient, WebsocketClient, WorkCommerceApi, WorkRealtimeApi, WorkRefParseError, WorkRoom, WorkSurfaceApi, appAppearanceVar, applyAppAppearance, assertGenerationRequestAllowedByPolicy, attachAppEmbed, buildAppSurfaceRequest, buildSpaceInvitePath, buildSpacePath, clearGrantedAppScopes, compileDisplayActions, connectDisplay, contextToPiMessages, createAppBridgeCore, createAppBridgeCore as createWorkBridgeCore, createAppRuntime, createAppRuntime as createWorkRuntime, createCohubClient, createHttpClient, createOriginAppResolver, createSessionGenerationStreamClient, createSessionPatchReducer, createSlugAppIdResolver, createSlugAppIdResolver as createSlugWorkIdResolver, createVoiceInputClient, createWebsocketClient, decodeGenerationPolicy, defaultDesktopCommandTimeoutMs, encodeGenerationPolicy, extractBillingPayload, filterDiscoverableGenerationModels, filterGenerationDeclarationsByPolicy, filterSpacePickerItems, findGenerationModelPolicy, fingerprintProjectionTurns, formatAppRef, formatWorkRef, generationOutputSource, getAllowedGenerationModelIds, getCohubContext, harnessArchiveIndexSchema, hasGrantedAppScopes, hasRequestSourceIdentity, isActiveGenerationTask, isAppId, isBillingAccessBlockedCode, isBillingAccessBlockedError, isDesktopCallMethod, isFeatureNotEntitledError, isGenerationModelHidden, isHttpErrorCode, isLocalHarness, isNativeClientTurn, isProjectionCompaction, isRequestSourceClientId, isRequestSourceEmpty, isRequestSourceUuid, isTerminalDesktopCommandStatus, isUserLevelPermission, isUuid, isWorkId, joinApiUrl, matchesUnauthorizedErrorToken, mergeRequestSourceIntoMeta, nativeIngestTurnSchema, nativeRuntimeEventSchema, nativeTurnCompleteSchema, nativeTurnProgressSchema, normalizeBaseUrl, normalizeDisplayPoint, normalizeGenerationPolicy, normalizeRequestSource, normalizeSessionTurnOrigin, normalizeSpacePickerQuery, normalizeVoiceInputWebsocketUrl, normalizeWebsocketUrl, orderSpacePickerItems, parseAppRef, parseAppSurfaceReady, parseAppSurfaceResponse, parseAssistantMessageCommit, parseBoardDocument, parseBoardItem, parseDesktopCommand, parseGenerationPolicyFromEnv, parseRequestSourceFromHeaders, parseSpaceRef, parseWorkRef, projectNativeMessageMeta, projectNativeSession, readRequestSourceFromEnv, readSentTurns, readSessionTurnOrigin, requestSourceToHeaders, resolveApiBaseUrl, resolveAppTransport, resolveAppTransport as resolveWorkTransport, resolveCohubEnvironment, resolveExecutionAppId, resolveExecutionToken, resolveHarness, resolveOpenSurface, resolveRequestSourceChannel, resolveVoiceInputWebsocketUrl, resolveWebBaseUrl, resolveWebsocketUrl, runtimeCommandSchema, runtimeEventSchema, runtimeNativeStopSchema, runtimeReadySchema, sanitizeAccessToken, scopeListHasPermission, selectRuntimeContextMessages, selectSpacePickerItems, serializeProjection, serializeProjectionRecords, setGrantedAppScopes, toGenerationTaskView, trimProjectionTurnsToCompaction, validateArchiveBoundary };
9081
+ export { APP_APPEARANCE_TOKENS, APP_COMPOSER_CHIP_CONTENT_MAX_BYTES, APP_COMPOSER_CHIP_KEY_MAX_LENGTH, APP_COMPOSER_CHIP_LABEL_MAX_LENGTH, APP_DEVICES, APP_SURFACE_READY_TIMEOUT_MS, APP_SURFACE_REQUEST_TIMEOUT_MS, AppCommerceApi, AppRealtimeApi, AppRefParseError, AppRoom, AppRoomRtc, AppRtcChannel, AppRtcError, AppRuntimeApi, AppRuntimeApi as WorkRuntimeApi, AppRuntimeError, AppSurfaceApi, AppWindowApi, AppsApi, BILLING_ACCESS_BLOCKED_ERROR_CODE, BOARD_COLOR_TOKENS, BOARD_ITEM_TYPES, BillingApi, BoardAnimationSchema, BoardClient, BoardPatchSchema, BoardTrackSchema, COHUB_ENVIRONMENTS, COHUB_SOURCE_HEADER, COHUB_SOURCE_HEADER_NAMES, CohubClient, CohubHttpClient, DESKTOP_COMMAND_ACCEPT_TIMEOUT_MS, DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS, DESKTOP_COMMAND_MAX_TIMEOUT_MS, DESKTOP_COMMAND_OPEN_TIMEOUT_MS, DESKTOP_COMMAND_PAYLOAD_MAX_BYTES, DESKTOP_COMMAND_PENDING_TTL_SECONDS, DESKTOP_COMMAND_SETTLEMENT_GRACE_SECONDS, DESKTOP_COMMAND_TERMINAL_TTL_SECONDS, DESKTOP_COMMAND_VERSION, DISPLAY_ACTION_TYPES, DISPLAY_ELEMENT_REF_PATTERN, DISPLAY_SYSTEM_ACTIONS, DesktopCommandsApi, DisplayActionError, DisplayConnection, FEATURE_NOT_ENTITLED_ERROR_CODE, GenerationPolicyError, HOME_SPACE_SLUG, HttpError, NATIVE_INGEST_MAX_TURNS, NATIVE_SYNC_SOURCE, OriginBrokerTransport, PERMISSIONS, ParentBridgeTransport, PopupBrokerTransport, REQUEST_SOURCE_VIA_MAX_LENGTH, RUNTIME_ARCHIVE_SEGMENT_BYTES, RUNTIME_MAX_FRAME_BYTES, RUNTIME_PROTOCOL_VERSION, RUNTIME_RECOVERY_BATCH_SIZE, ReferencesApi, ReferralsApi, RoomsApi, SETTLED_TURN_STATUSES, SessionGenerationStreamClient, SessionPatchReducer, SpaceDisplaysApi, SpacePublicFilesApi, SpaceWebhooksApi, UPLOAD_MAX_BATCH_BYTES, UPLOAD_MAX_BATCH_FILES, UPLOAD_MAX_FILE_BYTES, USER_LEVEL_PERMISSIONS, UiCommandsApi, UsersApi, VoiceApi, VoiceInputClient, WebsocketClient, WorkCommerceApi, WorkRealtimeApi, WorkRefParseError, WorkRoom, WorkSurfaceApi, appAppearanceVar, applyAppAppearance, assertGenerationRequestAllowedByPolicy, attachAppEmbed, buildAppSurfaceRequest, buildSpaceInvitePath, buildSpacePath, clearGrantedAppScopes, compileDisplayActions, connectDisplay, contextToPiMessages, createAppBridgeCore, createAppBridgeCore as createWorkBridgeCore, createAppRuntime, createAppRuntime as createWorkRuntime, createCohubClient, createHttpClient, createOriginAppResolver, createSessionGenerationStreamClient, createSessionPatchReducer, createSlugAppIdResolver, createSlugAppIdResolver as createSlugWorkIdResolver, createVoiceInputClient, createWebsocketClient, decodeGenerationPolicy, defaultDesktopCommandTimeoutMs, encodeGenerationPolicy, extractBillingPayload, filterDiscoverableGenerationModels, filterGenerationDeclarationsByPolicy, filterSpacePickerItems, findGenerationModelPolicy, fingerprintProjectionTurns, formatAppRef, formatWorkRef, generationOutputSource, getAllowedGenerationModelIds, getCohubContext, harnessArchiveIndexSchema, hasGrantedAppScopes, hasRequestSourceIdentity, isActiveGenerationTask, isAppId, isBillingAccessBlockedCode, isBillingAccessBlockedError, isDesktopCallMethod, isFeatureNotEntitledError, isGenerationModelHidden, isHttpErrorCode, isLocalHarness, isNativeClientTurn, isProjectionCompaction, isRequestSourceClientId, isRequestSourceEmpty, isRequestSourceUuid, isTerminalDesktopCommandStatus, isUserLevelPermission, isUuid, isWorkId, joinApiUrl, matchesUnauthorizedErrorToken, mergeRequestSourceIntoMeta, nativeIngestTurnSchema, nativeRuntimeEventSchema, nativeTurnCompleteSchema, nativeTurnProgressSchema, normalizeBaseUrl, normalizeDisplayPoint, normalizeGenerationPolicy, normalizeRequestSource, normalizeSessionTurnOrigin, normalizeSpacePickerQuery, normalizeVoiceInputWebsocketUrl, normalizeWebsocketUrl, orderSpacePickerItems, parseAppRef, parseAppSurfaceReady, parseAppSurfaceResponse, parseAssistantMessageCommit, parseBoardDocument, parseBoardItem, parseDesktopCommand, parseGenerationPolicyFromEnv, parseRequestSourceFromHeaders, parseSpaceRef, parseWorkRef, projectNativeMessageMeta, projectNativeSession, readRequestSourceFromEnv, readSentTurns, readSessionTurnOrigin, requestSourceToHeaders, resolveApiBaseUrl, resolveAppTransport, resolveAppTransport as resolveWorkTransport, resolveCohubEnvironment, resolveExecutionAppId, resolveExecutionToken, resolveHarness, resolveOpenSurface, resolveRequestSourceChannel, resolveVoiceInputWebsocketUrl, resolveWebBaseUrl, resolveWebsocketUrl, runtimeCommandSchema, runtimeEventSchema, runtimeNativeStopSchema, runtimeReadySchema, sanitizeAccessToken, scopeListHasPermission, selectRuntimeContextMessages, selectSpacePickerItems, serializeProjection, serializeProjectionRecords, setGrantedAppScopes, toGenerationTaskView, trimProjectionTurnsToCompaction, validateArchiveBoundary };