@neta-art/cohub 10.0.0 → 11.1.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 (155) hide show
  1. package/README.md +42 -7
  2. package/dist/board/editor/animation.d.ts +3 -0
  3. package/dist/board/editor/animation.js +16 -1
  4. package/dist/board/editor/clipboard.js +7 -0
  5. package/dist/board/editor/copy.js +23 -18
  6. package/dist/board/editor/creation.d.ts +7 -3
  7. package/dist/board/editor/creation.js +5 -4
  8. package/dist/board/editor/deletion.js +3 -3
  9. package/dist/board/editor/document.d.ts +8 -2
  10. package/dist/board/editor/document.js +54 -7
  11. package/dist/board/editor/editor.d.ts +22 -7
  12. package/dist/board/editor/editor.js +12 -2
  13. package/dist/board/editor/index.d.ts +2 -2
  14. package/dist/board/editor/interaction.d.ts +8 -2
  15. package/dist/board/editor/interaction.js +34 -1
  16. package/dist/board/editor/query.d.ts +5 -5
  17. package/dist/board/editor/query.js +14 -12
  18. package/dist/board/editor/selection.js +2 -1
  19. package/dist/board/editor/snapshots.d.ts +0 -2
  20. package/dist/board/editor/snapshots.js +4 -20
  21. package/dist/board/export/index.d.ts +25 -4
  22. package/dist/board/export/index.js +45 -12
  23. package/dist/board/export/node/index.d.ts +5 -4
  24. package/dist/board/export/node/index.js +16 -11
  25. package/dist/board/export/node/scripts.d.ts +16 -0
  26. package/dist/board/export/node/scripts.js +258 -0
  27. package/dist/board/export/scene.d.ts +2 -1
  28. package/dist/board/export/scene.js +24 -7
  29. package/dist/board/model/animation.d.ts +4 -1
  30. package/dist/board/model/animation.js +34 -7
  31. package/dist/board/model/authoring.d.ts +20 -0
  32. package/dist/board/model/authoring.js +185 -0
  33. package/dist/board/model/export-plan.js +1 -0
  34. package/dist/board/model/file-preview.js +1 -1
  35. package/dist/board/model/file-snapshot.js +3 -2
  36. package/dist/board/model/image-key.js +7 -3
  37. package/dist/board/model/index.d.ts +15 -7
  38. package/dist/board/model/index.js +16 -9
  39. package/dist/board/model/items/builtin.js +2 -2
  40. package/dist/board/model/items/create.d.ts +19 -4
  41. package/dist/board/model/items/create.js +130 -9
  42. package/dist/board/model/items/index.d.ts +2 -2
  43. package/dist/board/model/items/index.js +2 -2
  44. package/dist/board/model/media-playback.d.ts +2 -1
  45. package/dist/board/model/media-playback.js +15 -5
  46. package/dist/board/model/media.js +7 -1
  47. package/dist/board/model/overlay-placement.d.ts +25 -0
  48. package/dist/board/model/overlay-placement.js +35 -0
  49. package/dist/board/model/replay.d.ts +1 -1
  50. package/dist/board/model/replay.js +1 -0
  51. package/dist/board/model/sampling.d.ts +19 -0
  52. package/dist/board/model/sampling.js +96 -0
  53. package/dist/board/model/script-example.d.ts +4 -0
  54. package/dist/board/model/script-example.js +21 -0
  55. package/dist/board/model/script-hit.d.ts +27 -0
  56. package/dist/board/model/script-hit.js +109 -0
  57. package/dist/board/model/script-index.d.ts +7 -0
  58. package/dist/board/model/script-index.js +15 -0
  59. package/dist/board/model/script-observers.d.ts +18 -0
  60. package/dist/board/model/script-observers.js +92 -0
  61. package/dist/board/model/script-scene.d.ts +24 -0
  62. package/dist/board/model/script-scene.js +294 -0
  63. package/dist/board/model/script.d.ts +149 -0
  64. package/dist/board/model/script.js +1 -0
  65. package/dist/board/render/board-background.js +5 -4
  66. package/dist/board/render/css-color.d.ts +6 -1
  67. package/dist/board/render/css-color.js +34 -20
  68. package/dist/board/render/diagnostics.d.ts +20 -0
  69. package/dist/board/render/diagnostics.js +117 -0
  70. package/dist/board/render/index.d.ts +9 -6
  71. package/dist/board/render/index.js +10 -7
  72. package/dist/board/render/media-interaction.js +1 -1
  73. package/dist/board/render/palette.d.ts +2 -1
  74. package/dist/board/render/palette.js +6 -2
  75. package/dist/board/render/renderers/arrow-card-renderer.js +3 -2
  76. package/dist/board/render/renderers/audio-card-renderer.js +2 -1
  77. package/dist/board/render/renderers/board-renderer-registry.d.ts +25 -5
  78. package/dist/board/render/renderers/board-renderer-registry.js +11 -4
  79. package/dist/board/render/renderers/custom-card-renderer.d.ts +5 -0
  80. package/dist/board/render/renderers/{sketch-card-renderer.js → custom-card-renderer.js} +18 -16
  81. package/dist/board/render/renderers/draw-card-renderer.js +3 -2
  82. package/dist/board/render/renderers/effect-card-renderer.js +22 -5
  83. package/dist/board/render/renderers/frame-card-renderer.js +30 -14
  84. package/dist/board/render/renderers/image-card-renderer.js +43 -4
  85. package/dist/board/render/renderers/shape-card-renderer.js +10 -27
  86. package/dist/board/render/renderers/task-card-renderer.d.ts +2 -1
  87. package/dist/board/render/renderers/task-card-renderer.js +220 -71
  88. package/dist/board/render/renderers/text-card-renderer.js +5 -3
  89. package/dist/board/render/renderers/unknown-card-renderer.js +1 -0
  90. package/dist/board/render/renderers/video-card-renderer.js +2 -1
  91. package/dist/board/render/sampling.d.ts +22 -0
  92. package/dist/board/render/sampling.js +21 -0
  93. package/dist/board/render/script-export.d.ts +11 -0
  94. package/dist/board/render/script-export.js +86 -0
  95. package/dist/board/render/script-host.d.ts +31 -0
  96. package/dist/board/render/script-host.js +446 -0
  97. package/dist/board/render/script-modules.d.ts +2 -0
  98. package/dist/board/render/script-modules.js +2 -0
  99. package/dist/board/render/script-runtime.d.ts +29 -0
  100. package/dist/board/render/script-runtime.js +223 -0
  101. package/dist/board/render/script-sandbox.d.ts +4 -0
  102. package/dist/board/render/script-sandbox.js +173 -0
  103. package/dist/board/render/script-texture.d.ts +11 -0
  104. package/dist/board/render/script-texture.js +33 -0
  105. package/dist/board/render/stroke.d.ts +2 -1
  106. package/dist/board/render/stroke.js +22 -1
  107. package/dist/board/render/text-measurement.js +1 -1
  108. package/dist/board/stage/assets.js +7 -2
  109. package/dist/board/stage/maintenance.d.ts +5 -0
  110. package/dist/board/stage/maintenance.js +11 -0
  111. package/dist/board/stage/scene.d.ts +2 -0
  112. package/dist/board/stage/scene.js +21 -2
  113. package/dist/board/stage/script-pointer.d.ts +20 -0
  114. package/dist/board/stage/script-pointer.js +41 -0
  115. package/dist/board/stage/stage.d.ts +21 -3
  116. package/dist/board/stage/stage.js +350 -47
  117. package/dist/board/stage/texture-pool.js +2 -3
  118. package/dist/chunks/environment.d.ts +305 -27
  119. package/dist/chunks/environment.js +12 -4
  120. package/dist/chunks/host-bridge.d.ts +2 -2
  121. package/dist/chunks/http.d.ts +90 -7
  122. package/dist/chunks/http.js +157 -64
  123. package/dist/chunks/transport.js +4 -2
  124. package/dist/chunks/websocket.d.ts +9 -1
  125. package/dist/chunks/websocket.js +16 -2
  126. package/dist/host-bridge.js +2 -1
  127. package/dist/http.d.ts +3 -3
  128. package/dist/index.d.ts +182 -4
  129. package/dist/index.js +1559 -299
  130. package/dist/protocol/dist/board-assets.d.ts +1 -0
  131. package/dist/protocol/dist/board-constants.js +2 -2
  132. package/dist/protocol/dist/board-model.d.ts +236 -11
  133. package/dist/protocol/dist/board-model.js +46 -17
  134. package/dist/protocol/dist/board-patch.js +2 -1
  135. package/dist/protocol/dist/board-schema.d.ts +4 -2
  136. package/dist/protocol/dist/board-schema.js +18 -4
  137. package/dist/protocol/dist/board-script-modules.d.ts +25 -0
  138. package/dist/protocol/dist/board-script-modules.js +127 -0
  139. package/dist/protocol/dist/board-task.d.ts +22 -0
  140. package/dist/{board/model/task.js → protocol/dist/board-task.js} +34 -7
  141. package/dist/protocol/dist/display/index.d.ts +1 -0
  142. package/dist/protocol/dist/generation/blocks.d.ts +8 -0
  143. package/dist/{generation-blocks.js → protocol/dist/generation/blocks.js} +14 -15
  144. package/dist/protocol/dist/index.d.ts +7 -2
  145. package/dist/protocol/dist/realtime/types.d.ts +1 -0
  146. package/dist/protocol/dist/rtc/index.d.ts +1 -0
  147. package/dist/protocol/dist/task/index.d.ts +4 -0
  148. package/package.json +2 -1
  149. package/dist/board/export/node/sketch.d.ts +0 -19
  150. package/dist/board/export/node/sketch.js +0 -146
  151. package/dist/board/model/task.d.ts +0 -11
  152. package/dist/board/render/renderers/sketch-card-renderer.d.ts +0 -5
  153. package/dist/board/render/sketch-host.d.ts +0 -14
  154. package/dist/board/render/sketch-host.js +0 -216
  155. package/dist/types.d.ts +0 -36
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 createSessionPatchReducer, D as BoardClient, E as connectDisplay, 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 SessionPatchReducer, T as DisplayConnection, 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 buildSpaceInvitePath, a as DesktopCommandsApi, at as normalizeDisplayPoint, b as createSessionGenerationStreamClient, c as ReferralsApi, d as TasksApi, et as NAVIGATION_ERROR_MESSAGE_MAX_LENGTH, f as SpaceClient, g as PublicInviteApi, h as SpacesApi, i as WorkCommerceApi, it as DISPLAY_SYSTEM_ACTIONS, j as SearchApi, k as SessionAccessApi, l as UsersApi, m as SpaceWebhooksApi, n as createHttpClient, o as UiCommandsApi, ot as splitDisplayText, p as SpacePublicFilesApi, 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 buildSpacePath, w as SpaceDisplaysApi, x as parseAssistantMessageCommit, y as SessionGenerationStreamClient, 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(),
@@ -274,8 +381,16 @@ const fileProps = {
274
381
  coverUrl: z.string().optional()
275
382
  }).strict().optional()
276
383
  };
277
- const timedFileProps = {
384
+ const BoardMediaOriginSchema = z.object({
385
+ taskRunId: z.string().min(1).max(160),
386
+ artifactId: z.string().min(1).max(240)
387
+ }).strict();
388
+ const mediaProps = {
278
389
  ...fileProps,
390
+ origin: BoardMediaOriginSchema.optional()
391
+ };
392
+ const timedFileProps = {
393
+ ...mediaProps,
279
394
  time: nonNegative.optional()
280
395
  };
281
396
  const BoardCropSchema = z.object({
@@ -288,7 +403,7 @@ const BoardImageItemSchema = z.object({
288
403
  ...boxed(640, 360),
289
404
  type: z.literal("image"),
290
405
  props: z.object({
291
- ...fileProps,
406
+ ...mediaProps,
292
407
  crop: BoardCropSchema.optional()
293
408
  }).strict()
294
409
  }).strict();
@@ -353,8 +468,10 @@ const BoardTaskSnapshotSchema = z.object({
353
468
  title: z.string().min(1).max(240),
354
469
  model: z.string().max(160).optional(),
355
470
  promptExcerpt: z.string().max(480).optional(),
471
+ errorExcerpt: z.string().max(240).optional(),
356
472
  artifactCount: z.number().int().nonnegative(),
357
473
  artifacts: z.array(BoardTaskArtifactSchema).max(6).default([]),
474
+ startedAt: z.string().max(64).optional(),
358
475
  updatedAt: z.string().optional()
359
476
  }).strict();
360
477
  const BoardTaskItemSchema = z.object({
@@ -362,7 +479,7 @@ const BoardTaskItemSchema = z.object({
362
479
  type: z.literal("task"),
363
480
  props: z.object({
364
481
  taskRunId: z.string().min(1).max(160),
365
- snapshot: BoardTaskSnapshotSchema
482
+ snapshot: BoardTaskSnapshotSchema.describe("Maintained by the server for tasks in the Board's Space; writers may omit it.")
366
483
  }).strict()
367
484
  }).strict();
368
485
  const BOARD_EFFECT_KINDS = [
@@ -389,14 +506,11 @@ const BoardEffectItemSchema = z.object({
389
506
  seed: z.string().min(1).max(160).optional()
390
507
  }).strict()
391
508
  }).strict();
392
- const BoardSketchItemSchema = z.object({
509
+ const BoardCustomItemSchema = z.object({
393
510
  ...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()
511
+ type: z.literal("custom"),
512
+ script: BoardScriptSchema,
513
+ props: z.object({}).strict().default({})
400
514
  }).strict();
401
515
  const BOARD_EXTENSION_TYPE_PATTERN = /^[a-z][a-z0-9]*(?:\.[a-z0-9-]+)+$/;
402
516
  const BoardExtensionItemSchema = z.object({
@@ -420,7 +534,7 @@ const BOARD_ITEM_SCHEMAS = {
420
534
  file: BoardFileItemSchema,
421
535
  task: BoardTaskItemSchema,
422
536
  effect: BoardEffectItemSchema,
423
- sketch: BoardSketchItemSchema
537
+ custom: BoardCustomItemSchema
424
538
  };
425
539
  const BOARD_ITEM_TYPES = Object.keys(BOARD_ITEM_SCHEMAS);
426
540
  function isBuiltinItemType(type) {
@@ -535,7 +649,7 @@ const BoardAnimationHeaderSchema = z.object({
535
649
  end: z.enum(["hold", "reset"]).default("hold"),
536
650
  markers: z.array(BoardMarkerSchema).max(1e4).default([])
537
651
  }).strict();
538
- const BoardAnimationSchema = BoardAnimationHeaderSchema.extend({ tracks: z.record(BoardIdSchema, BoardTrackSchema).default({}) }).strict();
652
+ const BoardAnimationSchema = BoardAnimationHeaderSchema.extend({ tracks: z.record(BoardKeySchema, BoardTrackSchema).default({}) }).strict();
539
653
  const BOARD_ENTER_PRESETS = [
540
654
  "fade-in",
541
655
  "rise",
@@ -575,7 +689,7 @@ const patchObject = z.record(z.string(), z.unknown());
575
689
  const BoardPatchSchema = z.object({
576
690
  board: patchObject.nullable().optional(),
577
691
  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()
692
+ animations: z.record(BoardIdSchema, patchObject.and(z.object({ tracks: z.record(BoardKeySchema, patchObject.nullable()).optional() })).nullable()).optional()
579
693
  }).strict();
580
694
  //#endregion
581
695
  //#region ../protocol/dist/board-json.js
@@ -1119,7 +1233,8 @@ const NOT_ANIMATABLE = /* @__PURE__ */ new Set([
1119
1233
  "parent",
1120
1234
  "z",
1121
1235
  "locked",
1122
- "metadata"
1236
+ "metadata",
1237
+ "script"
1123
1238
  ]);
1124
1239
  const CAMERA_PROPERTIES = {
1125
1240
  focus: {
@@ -1646,96 +1761,132 @@ function parseBoardDocument(input) {
1646
1761
  } : result;
1647
1762
  }
1648
1763
  //#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;
1764
+ //#region ../protocol/dist/generation/blocks.js
1765
+ const EXCERPT_LIMIT = 240;
1766
+ function asRecord(value) {
1767
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
1654
1768
  }
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;
1769
+ function cleanExcerpt(value, limit = EXCERPT_LIMIT) {
1770
+ if (typeof value !== "string") return void 0;
1771
+ const clean = value.replace(/\s+/g, " ").trim();
1772
+ if (!clean) return void 0;
1773
+ return clean.length > limit ? `${clean.slice(0, limit - 3).trimEnd()}...` : clean;
1670
1774
  }
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"));
1775
+ function blockText(block) {
1776
+ return cleanExcerpt(block.text ?? block.content ?? block.value);
1685
1777
  }
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}`);
1778
+ function blockUrl(block) {
1779
+ const source = asRecord(block.source);
1780
+ return normalizeBoardRemoteUrl(source?.url ?? source?.src ?? block.url ?? block.src);
1781
+ }
1782
+ function blockMimeType(block) {
1783
+ const source = asRecord(block.source);
1784
+ const value = source?.mediaType ?? source?.media_type ?? source?.mimeType ?? block.mediaType ?? block.media_type ?? block.mimeType;
1785
+ return typeof value === "string" ? value : void 0;
1786
+ }
1787
+ function positiveNumber(value) {
1788
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
1789
+ }
1790
+ function blockNaturalSize(block) {
1791
+ const source = asRecord(block.source);
1792
+ const naturalWidth = positiveNumber(source?.width ?? source?.naturalWidth ?? block.width ?? block.naturalWidth);
1793
+ const naturalHeight = positiveNumber(source?.height ?? source?.naturalHeight ?? block.height ?? block.naturalHeight);
1794
+ return {
1795
+ ...naturalWidth ? { naturalWidth } : {},
1796
+ ...naturalHeight ? { naturalHeight } : {}
1797
+ };
1798
+ }
1799
+ function contentBlocks(value) {
1800
+ return Array.isArray(value) ? value.filter((item) => asRecord(item)) : [];
1801
+ }
1802
+ function taskData(run) {
1803
+ const payload = asRecord(run.payload);
1804
+ return asRecord(payload?.data) ?? payload;
1805
+ }
1806
+ function generationOutput(run) {
1807
+ return contentBlocks(asRecord(run.result)?.output);
1808
+ }
1809
+ function generationPrompt(run) {
1810
+ for (const block of contentBlocks(taskData(run)?.content)) {
1811
+ const text = blockText(block);
1812
+ if (text) return text;
1695
1813
  }
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
1814
  }
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);
1815
+ function blockMeta(block) {
1816
+ return asRecord(block.meta);
1707
1817
  }
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;
1818
+ function blockIdentity(block) {
1819
+ const meta = blockMeta(block);
1820
+ const value = meta?.id ?? meta?.clip_id ?? meta?.clipId ?? block.id ?? block.clip_id ?? block.clipId;
1821
+ if (typeof value !== "string" && typeof value !== "number") return void 0;
1822
+ return cleanExcerpt(String(value), 220);
1823
+ }
1824
+ function blockTitle(block) {
1825
+ return cleanExcerpt(blockMeta(block)?.title ?? block.title ?? block.name, 240);
1826
+ }
1827
+ function blockDurationMs(block) {
1828
+ const source = asRecord(block.source);
1829
+ const meta = blockMeta(block);
1830
+ const milliseconds = positiveNumber(source?.durationMs ?? meta?.durationMs ?? block.durationMs);
1831
+ if (milliseconds) return Math.round(milliseconds);
1832
+ const seconds = positiveNumber(source?.duration ?? meta?.duration ?? block.duration);
1833
+ return seconds ? Math.round(seconds * 1e3) : void 0;
1834
+ }
1835
+ function blockPreviewUrl(block) {
1836
+ const source = asRecord(block.source);
1837
+ return normalizeBoardRemoteUrl(source?.poster ?? source?.thumbnail ?? source?.previewUrl ?? block.poster ?? block.thumbnail ?? block.previewUrl);
1713
1838
  }
1714
1839
  /**
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.
1840
+ * Image roles that depict playable media, by the media types they may cover;
1841
+ * `last_frame` stays a result of its own.
1718
1842
  */
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
- }
1843
+ const COVER_ROLES = /* @__PURE__ */ new Map([
1844
+ ["first_frame", /* @__PURE__ */ new Set(["video"])],
1845
+ ["cover", /* @__PURE__ */ new Set(["video", "audio"])],
1846
+ ["poster", /* @__PURE__ */ new Set(["video", "audio"])],
1847
+ ["thumbnail", /* @__PURE__ */ new Set(["video", "audio"])]
1848
+ ]);
1849
+ function isPlayable(block) {
1850
+ return block.type === "video" || block.type === "audio";
1851
+ }
1852
+ /**
1853
+ * Pair playable media with its cover image, returning media index → cover
1854
+ * index. An image sharing the provider id wins (music tracks and their art,
1855
+ * shared by every media of that id); otherwise cover-role images pair in
1856
+ * order with remaining media their role fits (a video and its `first_frame`).
1857
+ * Covers need a remote URL, so an inline image is never folded away.
1858
+ */
1859
+ function generationCovers(blocks) {
1860
+ const covers = /* @__PURE__ */ new Map();
1861
+ const isCover = (block) => block.type === "image" && blockUrl(block) !== void 0 && blockMeta(block)?.role !== "last_frame";
1862
+ blocks.forEach((block, index) => {
1863
+ const id = blockIdentity(block);
1864
+ if (!isPlayable(block) || id === void 0) return;
1865
+ const cover = blocks.findIndex((candidate) => isCover(candidate) && blockIdentity(candidate) === id);
1866
+ if (cover >= 0) covers.set(index, cover);
1867
+ });
1868
+ const paired = new Set(covers.values());
1869
+ const roleCovers = blocks.flatMap((block, index) => {
1870
+ const role = blockMeta(block)?.role;
1871
+ const types = typeof role === "string" ? COVER_ROLES.get(role) : void 0;
1872
+ return isCover(block) && !paired.has(index) && types ? [{
1873
+ index,
1874
+ types
1875
+ }] : [];
1876
+ });
1877
+ blocks.forEach((block, index) => {
1878
+ if (!isPlayable(block) || covers.has(index)) return;
1879
+ const at = roleCovers.findIndex(({ types }) => types.has(String(block.type)));
1880
+ const [cover] = at < 0 ? [] : roleCovers.splice(at, 1);
1881
+ if (cover) covers.set(index, cover.index);
1882
+ });
1883
+ return covers;
1732
1884
  }
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
1885
  //#endregion
1735
1886
  //#region ../protocol/dist/turn-origin.js
1736
- const record$2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
1887
+ const record$1 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
1737
1888
  const normalizeSessionTurnOrigin = (value) => {
1738
- const input = record$2(value);
1889
+ const input = record$1(value);
1739
1890
  if (!input || ![
1740
1891
  "prompt",
1741
1892
  "scheduled_prompt",
@@ -1755,12 +1906,12 @@ const normalizeSessionTurnOrigin = (value) => {
1755
1906
  };
1756
1907
  /** Reads durable provenance; legacy background tasks only knew their caller's Session/Turn. */
1757
1908
  const readSessionTurnOrigin = (meta, spaceId) => {
1758
- const input = record$2(meta);
1909
+ const input = record$1(meta);
1759
1910
  const origin = normalizeSessionTurnOrigin(input?.origin);
1760
1911
  if (origin) return origin;
1761
- const context = record$2(input?.context);
1912
+ const context = record$1(input?.context);
1762
1913
  if (context?.kind !== "background_bash_task" || !isUuidLike(spaceId)) return null;
1763
- const legacy = record$2(context.origin);
1914
+ const legacy = record$1(context.origin);
1764
1915
  return normalizeSessionTurnOrigin({
1765
1916
  ...legacy,
1766
1917
  spaceId,
@@ -1775,9 +1926,9 @@ const KINDS = [
1775
1926
  "background_task",
1776
1927
  "hook"
1777
1928
  ];
1778
- const record$1 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
1929
+ const record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
1779
1930
  const normalizeSentTurnRef = (value) => {
1780
- const input = record$1(value);
1931
+ const input = record(value);
1781
1932
  if (!input) return null;
1782
1933
  if (!isUuidLike(input.sessionId) || !isUuidLike(input.turnId)) return null;
1783
1934
  const kind = String(input.kind);
@@ -1796,7 +1947,7 @@ const normalizeSentTurnRef = (value) => {
1796
1947
  };
1797
1948
  /** Deduplicates by child Turn, so a retried request cannot render one twice. */
1798
1949
  const readSentTurns = (meta) => {
1799
- const raw = record$1(meta)?.messagesSent;
1950
+ const raw = record(meta)?.messagesSent;
1800
1951
  if (!Array.isArray(raw)) return [];
1801
1952
  const seen = /* @__PURE__ */ new Set();
1802
1953
  const refs = [];
@@ -2030,6 +2181,102 @@ function compileDisplayActions(actions, width, height) {
2030
2181
  if (events.length === 0) throw new DisplayActionError("no action touches the display");
2031
2182
  return events;
2032
2183
  }
2184
+ z.object({
2185
+ urls: z.union([z.string().min(1).max(512), z.array(z.string().min(1).max(512)).min(1).max(16)]),
2186
+ username: z.string().max(512).optional(),
2187
+ credential: z.string().max(512).optional()
2188
+ });
2189
+ const sessionDescriptionSchema = z.object({
2190
+ type: z.enum(["offer", "answer"]),
2191
+ sdp: z.string().min(1).max(65536)
2192
+ });
2193
+ const APP_RTC_DELIVERIES = ["reliable", "latest"];
2194
+ const APP_RTC_PUBLICATION_KINDS = [
2195
+ "data",
2196
+ "audio",
2197
+ "video"
2198
+ ];
2199
+ const APP_RTC_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/;
2200
+ const isAppRtcName = (value) => typeof value === "string" && APP_RTC_NAME_PATTERN.test(value) && !value.toLowerCase().startsWith("cohub.");
2201
+ const APP_RTC_LIMITS = {
2202
+ channelsPerMember: 16,
2203
+ tracksPerMember: 4,
2204
+ dataSubscriptions: 512,
2205
+ mediaSubscriptions: 16,
2206
+ messageBytes: 65536,
2207
+ operationsPerSecond: 10,
2208
+ opensPerSecond: 2,
2209
+ batch: 64
2210
+ };
2211
+ const APP_RTC_QUALITIES = [
2212
+ "high",
2213
+ "medium",
2214
+ "low"
2215
+ ];
2216
+ const APP_RTC_SIMULCAST_LAYERS = [
2217
+ {
2218
+ rid: "f",
2219
+ quality: "high",
2220
+ scaleResolutionDownBy: 1
2221
+ },
2222
+ {
2223
+ rid: "h",
2224
+ quality: "medium",
2225
+ scaleResolutionDownBy: 2
2226
+ },
2227
+ {
2228
+ rid: "q",
2229
+ quality: "low",
2230
+ scaleResolutionDownBy: 4
2231
+ }
2232
+ ];
2233
+ z.object({
2234
+ id: z.string().min(1).max(32),
2235
+ name: z.string().regex(APP_RTC_NAME_PATTERN),
2236
+ kind: z.enum(APP_RTC_PUBLICATION_KINDS),
2237
+ delivery: z.enum(APP_RTC_DELIVERIES).optional(),
2238
+ simulcast: z.boolean().optional()
2239
+ });
2240
+ const nameSchema = z.string().refine(isAppRtcName, "invalid name");
2241
+ const participantIdSchema = z.string().uuid();
2242
+ z.object({
2243
+ ticket: z.string().min(1).max(4096),
2244
+ participantId: participantIdSchema
2245
+ });
2246
+ z.object({ answer: sessionDescriptionSchema.refine((value) => value.type === "answer", "expected an answer") });
2247
+ z.union([z.object({ channels: z.array(z.object({
2248
+ name: nameSchema,
2249
+ delivery: z.enum(APP_RTC_DELIVERIES)
2250
+ })).min(1).max(APP_RTC_LIMITS.channelsPerMember) }), z.object({
2251
+ offer: sessionDescriptionSchema.refine((value) => value.type === "offer", "expected an offer"),
2252
+ tracks: z.array(z.object({
2253
+ name: nameSchema,
2254
+ kind: z.enum(["audio", "video"]),
2255
+ mid: z.string().min(1).max(32),
2256
+ simulcast: z.boolean().optional()
2257
+ })).min(1).max(APP_RTC_LIMITS.tracksPerMember)
2258
+ })]);
2259
+ z.object({
2260
+ ids: z.array(z.string().min(1).max(32)).min(1).max(APP_RTC_LIMITS.channelsPerMember + APP_RTC_LIMITS.tracksPerMember),
2261
+ offer: sessionDescriptionSchema.refine((value) => value.type === "offer", "expected an offer").optional()
2262
+ });
2263
+ const subscriptionRefSchema = z.object({
2264
+ participantId: participantIdSchema,
2265
+ publicationId: z.string().min(1).max(32)
2266
+ });
2267
+ const qualitySchema = z.enum(APP_RTC_QUALITIES);
2268
+ z.object({
2269
+ kind: z.enum(["data", "media"]),
2270
+ items: z.array(subscriptionRefSchema.extend({ quality: qualitySchema.optional() })).min(1).max(APP_RTC_LIMITS.batch)
2271
+ });
2272
+ z.object({ items: z.array(subscriptionRefSchema.extend({
2273
+ mid: z.string().min(1).max(32),
2274
+ quality: qualitySchema.optional()
2275
+ })).min(1).max(APP_RTC_LIMITS.mediaSubscriptions) });
2276
+ z.object({
2277
+ items: z.array(subscriptionRefSchema).min(1).max(APP_RTC_LIMITS.batch),
2278
+ offer: sessionDescriptionSchema.refine((value) => value.type === "offer", "expected an offer").optional()
2279
+ });
2033
2280
  //#endregion
2034
2281
  //#region ../protocol/dist/generation/catalog.js
2035
2282
  function isGenerationModelHidden(model) {
@@ -3727,6 +3974,11 @@ const APP_APPEARANCE_TOKENS = [
3727
3974
  "brand-ring",
3728
3975
  "brand-contrast-fg",
3729
3976
  "error-fg",
3977
+ "success",
3978
+ "warning",
3979
+ "error",
3980
+ "info",
3981
+ "status-running",
3730
3982
  "selection-bg",
3731
3983
  "overlay-scrim",
3732
3984
  "shadow-subtle",
@@ -3821,6 +4073,33 @@ const buildAppEmbedShellChanged = (input) => ({
3821
4073
  ...input
3822
4074
  });
3823
4075
  //#endregion
4076
+ //#region ../protocol/dist/app-devices.js
4077
+ const APP_DEVICES = [
4078
+ "camera",
4079
+ "microphone",
4080
+ "screen"
4081
+ ];
4082
+ const isAppDevice = (value) => typeof value === "string" && APP_DEVICES.includes(value);
4083
+ function parseAppDevices(value) {
4084
+ const items = typeof value === "string" ? value.split(/[\s,]+/) : Array.isArray(value) ? value : [];
4085
+ const wanted = new Set(items.map((item) => typeof item === "string" ? item.trim().toLowerCase() : item).filter(isAppDevice));
4086
+ return APP_DEVICES.filter((device) => wanted.has(device));
4087
+ }
4088
+ const APP_DEVICES_REQUEST_TYPE = "cohub.app.devices.request";
4089
+ const APP_DEVICES_RESULT_TYPE = "cohub.app.devices.result";
4090
+ function parseAppDevicesResult(value) {
4091
+ if (!value || typeof value !== "object") return null;
4092
+ const record = value;
4093
+ if (record.type !== "cohub.app.devices.result" || typeof record.requestId !== "string") return null;
4094
+ const status = record.status;
4095
+ if (status !== "granted" && status !== "denied" && status !== "unavailable") return null;
4096
+ return {
4097
+ type: APP_DEVICES_RESULT_TYPE,
4098
+ requestId: record.requestId,
4099
+ status
4100
+ };
4101
+ }
4102
+ //#endregion
3824
4103
  //#region src/apis/billing.ts
3825
4104
  var BillingApi = class {
3826
4105
  transport;
@@ -3842,64 +4121,1018 @@ var BillingApi = class {
3842
4121
  async getCatalog() {
3843
4122
  return this.transport.request("/api/billing/catalog");
3844
4123
  }
3845
- async getFeatureEntitlement(featureKey) {
3846
- return this.transport.request(`/api/billing/features/${encodeURIComponent(featureKey)}`);
4124
+ async getFeatureEntitlement(featureKey) {
4125
+ return this.transport.request(`/api/billing/features/${encodeURIComponent(featureKey)}`);
4126
+ }
4127
+ async getSubscriptions(input) {
4128
+ const params = new URLSearchParams();
4129
+ if (input?.page) params.set("page", String(input.page));
4130
+ if (input?.limit) params.set("limit", String(input.limit));
4131
+ const query = params.toString();
4132
+ return this.transport.request(`/api/billing/subscriptions${query ? `?${query}` : ""}`);
4133
+ }
4134
+ /** Confirms a returned checkout against the provider's order/subscription state. */
4135
+ async confirmCheckout(productKey, checkoutId) {
4136
+ return this.transport.request("/api/billing/checkout-confirmation", {
4137
+ method: "POST",
4138
+ body: JSON.stringify({
4139
+ productKey,
4140
+ checkoutId
4141
+ })
4142
+ });
4143
+ }
4144
+ async previewPromotionCode(input) {
4145
+ return this.transport.request("/api/billing/promotion-code-preview", {
4146
+ method: "POST",
4147
+ body: JSON.stringify(input)
4148
+ });
4149
+ }
4150
+ async createOrder(productKey, input) {
4151
+ return this.transport.request("/api/billing/orders", {
4152
+ method: "POST",
4153
+ body: JSON.stringify({
4154
+ ...input ?? {},
4155
+ productKey
4156
+ })
4157
+ });
4158
+ }
4159
+ async createSubscription(productKey, input) {
4160
+ return this.transport.request("/api/billing/subscriptions", {
4161
+ method: "POST",
4162
+ body: JSON.stringify({
4163
+ ...input ?? {},
4164
+ productKey
4165
+ })
4166
+ });
4167
+ }
4168
+ async cancelSubscriptionCheckout(subscriptionId) {
4169
+ return this.transport.request(`/api/billing/subscriptions/${encodeURIComponent(subscriptionId)}/checkout`, { method: "DELETE" });
4170
+ }
4171
+ async cancelSubscriptionAutoRenew(subscriptionId) {
4172
+ return this.transport.request(`/api/billing/subscriptions/${encodeURIComponent(subscriptionId)}`, {
4173
+ method: "PATCH",
4174
+ body: JSON.stringify({ cancelAtPeriodEnd: true })
4175
+ });
4176
+ }
4177
+ async createRedemption(input) {
4178
+ return this.transport.request("/api/billing/redemptions", {
4179
+ method: "POST",
4180
+ body: JSON.stringify(input)
4181
+ });
4182
+ }
4183
+ };
4184
+ //#endregion
4185
+ //#region src/apis/app-room-rtc.ts
4186
+ var AppRtcError = class extends Error {
4187
+ code;
4188
+ constructor(code, message) {
4189
+ super(message);
4190
+ this.code = code;
4191
+ this.name = "AppRtcError";
4192
+ }
4193
+ };
4194
+ const SERVER_CODES = /* @__PURE__ */ new Set([
4195
+ "RTC_UNAVAILABLE",
4196
+ "RTC_FORBIDDEN",
4197
+ "RTC_LIMIT_EXCEEDED",
4198
+ "RTC_RATE_LIMITED",
4199
+ "ROOM_NOT_FOUND",
4200
+ "ROOM_EXPIRED"
4201
+ ]);
4202
+ const toAppRtcError = (error) => {
4203
+ if (error instanceof AppRtcError) return error;
4204
+ if (error instanceof HttpError && SERVER_CODES.has(error.code)) return new AppRtcError(error.code, error.message);
4205
+ return new AppRtcError("RTC_FAILED", error instanceof Error ? error.message : String(error));
4206
+ };
4207
+ const HEADER_BYTES = 5;
4208
+ const FLAG_BINARY = 1;
4209
+ const encoder = new TextEncoder();
4210
+ const decoder = new TextDecoder();
4211
+ function encodeAppRtcFrame(data, sequence) {
4212
+ let payload;
4213
+ let flags = 0;
4214
+ if (data instanceof ArrayBuffer) {
4215
+ payload = new Uint8Array(data);
4216
+ flags = FLAG_BINARY;
4217
+ } else if (ArrayBuffer.isView(data)) {
4218
+ payload = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
4219
+ flags = FLAG_BINARY;
4220
+ } else {
4221
+ const json = JSON.stringify(data);
4222
+ if (json === void 0) throw new TypeError("RTC message is not serializable");
4223
+ payload = encoder.encode(json);
4224
+ }
4225
+ if (payload.byteLength > APP_RTC_LIMITS.messageBytes) throw new RangeError(`RTC message exceeds ${APP_RTC_LIMITS.messageBytes} bytes`);
4226
+ const frame = new Uint8Array(HEADER_BYTES + payload.byteLength);
4227
+ frame[0] = flags;
4228
+ new DataView(frame.buffer).setUint32(1, sequence >>> 0);
4229
+ frame.set(payload, HEADER_BYTES);
4230
+ return frame;
4231
+ }
4232
+ function decodeAppRtcFrame(frame) {
4233
+ if (frame.byteLength < HEADER_BYTES) return null;
4234
+ const view = new DataView(frame);
4235
+ const flags = view.getUint8(0);
4236
+ const sequence = view.getUint32(1);
4237
+ const payload = new Uint8Array(frame, HEADER_BYTES);
4238
+ if (flags & FLAG_BINARY) return {
4239
+ data: payload.slice().buffer,
4240
+ sequence
4241
+ };
4242
+ try {
4243
+ return {
4244
+ data: JSON.parse(decoder.decode(payload)),
4245
+ sequence
4246
+ };
4247
+ } catch {
4248
+ return null;
4249
+ }
4250
+ }
4251
+ const isNewerSequence = (next, last) => last === null || next !== last && next - last >>> 0 < 2147483648;
4252
+ const publicationsKey = (members) => members.map((member) => `${member.participantId}:${member.publications?.map((item) => item.id).join(",") ?? ""}`).join(";");
4253
+ const GATHER_TIMEOUT_MS = 2e3;
4254
+ const CONNECT_TIMEOUT_MS = 15e3;
4255
+ const PENDING_RELIABLE_FRAMES = 256;
4256
+ const LATEST_BACKLOG_BYTES = 65536;
4257
+ const RECONNECT_DELAYS_MS = [
4258
+ 500,
4259
+ 1e3,
4260
+ 2e3,
4261
+ 5e3,
4262
+ 1e4,
4263
+ 3e4
4264
+ ];
4265
+ const RATE_LIMIT_RETRY_MS = 1e3;
4266
+ var ConnectionReplacedError = class extends Error {
4267
+ constructor() {
4268
+ super("RTC connection was replaced");
4269
+ this.name = "ConnectionReplacedError";
4270
+ }
4271
+ };
4272
+ const policyOf = (delivery) => delivery === "latest" ? {
4273
+ ordered: false,
4274
+ maxRetransmits: 0
4275
+ } : { ordered: true };
4276
+ const isSessionLost = (error) => error instanceof HttpError && (error.code === "RTC_STALE" || error.code === "RTC_SESSION_NOT_FOUND");
4277
+ const isRoomGone = (error) => error instanceof HttpError && (error.code === "ROOM_NOT_FOUND" || error.code === "ROOM_EXPIRED");
4278
+ const chunk = (items, size) => {
4279
+ const chunks = [];
4280
+ for (let index = 0; index < items.length; index += size) chunks.push(items.slice(index, index + size));
4281
+ return chunks;
4282
+ };
4283
+ const mediaKey = (participantId, publicationId) => `${participantId}:${publicationId}`;
4284
+ var AppRtcChannel = class {
4285
+ rtc;
4286
+ name;
4287
+ delivery;
4288
+ handlers = /* @__PURE__ */ new Set();
4289
+ peerHandlers = /* @__PURE__ */ new Set();
4290
+ openPeers = /* @__PURE__ */ new Set();
4291
+ publication = null;
4292
+ pending = [];
4293
+ sequence = 0;
4294
+ closed = false;
4295
+ constructor(rtc, name, delivery) {
4296
+ this.rtc = rtc;
4297
+ this.name = name;
4298
+ this.delivery = delivery;
4299
+ }
4300
+ get peers() {
4301
+ return [...this.openPeers];
4302
+ }
4303
+ send(data) {
4304
+ if (this.closed || this.rtc.state === "closed") return false;
4305
+ this.sequence = this.sequence + 1 >>> 0;
4306
+ const frame = encodeAppRtcFrame(data, this.sequence);
4307
+ const channel = this.publication?.channel;
4308
+ if (channel?.readyState === "open") return this.write(channel, frame);
4309
+ if (this.delivery === "latest") this.pending = [frame];
4310
+ else if (this.pending.length >= PENDING_RELIABLE_FRAMES) return false;
4311
+ else this.pending.push(frame);
4312
+ return true;
4313
+ }
4314
+ onMessage(handler) {
4315
+ this.handlers.add(handler);
4316
+ return () => this.handlers.delete(handler);
4317
+ }
4318
+ /** Reports when this client starts or stops receiving a peer's channel. */
4319
+ onPeer(handler) {
4320
+ this.peerHandlers.add(handler);
4321
+ return () => this.peerHandlers.delete(handler);
4322
+ }
4323
+ close() {
4324
+ if (this.closed) return;
4325
+ this.closed = true;
4326
+ this.handlers.clear();
4327
+ this.peerHandlers.clear();
4328
+ this.openPeers.clear();
4329
+ this.pending = [];
4330
+ this.rtc.closeChannel(this);
4331
+ }
4332
+ /** @internal */
4333
+ get isOpen() {
4334
+ return !this.closed;
4335
+ }
4336
+ /** @internal */
4337
+ get publicationId() {
4338
+ return this.publication?.id ?? null;
4339
+ }
4340
+ /** @internal */
4341
+ attach(id, channel) {
4342
+ this.publication = {
4343
+ id,
4344
+ channel
4345
+ };
4346
+ channel.binaryType = "arraybuffer";
4347
+ const flush = () => {
4348
+ const frames = this.pending;
4349
+ this.pending = [];
4350
+ for (const frame of frames) this.write(channel, frame);
4351
+ };
4352
+ if (channel.readyState === "open") flush();
4353
+ else channel.addEventListener("open", flush, { once: true });
4354
+ }
4355
+ /** @internal */
4356
+ detach() {
4357
+ this.publication = null;
4358
+ }
4359
+ /** @internal */
4360
+ fail() {
4361
+ this.close();
4362
+ }
4363
+ /** @internal */
4364
+ deliver(from, data) {
4365
+ for (const handler of this.handlers) handler({
4366
+ from,
4367
+ data
4368
+ });
4369
+ }
4370
+ /** @internal */
4371
+ setPeer(participantId, open) {
4372
+ if (open ? this.openPeers.has(participantId) : !this.openPeers.delete(participantId)) return;
4373
+ if (open) this.openPeers.add(participantId);
4374
+ for (const handler of this.peerHandlers) handler({
4375
+ participantId,
4376
+ open
4377
+ });
4378
+ }
4379
+ write(channel, frame) {
4380
+ if (this.delivery === "latest" && channel.bufferedAmount > LATEST_BACKLOG_BYTES) return false;
4381
+ try {
4382
+ channel.send(frame);
4383
+ return true;
4384
+ } catch {
4385
+ return false;
4386
+ }
4387
+ }
4388
+ };
4389
+ var AppRoomRtc = class {
4390
+ host;
4391
+ _state = "connecting";
4392
+ pc = null;
4393
+ sessionId = null;
4394
+ queue = Promise.resolve();
4395
+ reconcileQueued = false;
4396
+ reconcileFailures = 0;
4397
+ reconnectAttempt = 0;
4398
+ reconnectTimer = null;
4399
+ generation = 0;
4400
+ filter = null;
4401
+ channels = /* @__PURE__ */ new Map();
4402
+ channelsToPublish = /* @__PURE__ */ new Set();
4403
+ remoteData = /* @__PURE__ */ new Map();
4404
+ localTracks = /* @__PURE__ */ new Map();
4405
+ remoteMedia = /* @__PURE__ */ new Map();
4406
+ mediaByMid = /* @__PURE__ */ new Map();
4407
+ stateHandlers = /* @__PURE__ */ new Set();
4408
+ trackHandlers = /* @__PURE__ */ new Set();
4409
+ offMembers;
4410
+ constructor(host) {
4411
+ this.host = host;
4412
+ let published = "";
4413
+ this.offMembers = host.onMembersChanged((members) => {
4414
+ const next = publicationsKey(members);
4415
+ if (next === published) return;
4416
+ published = next;
4417
+ this.scheduleReconcile();
4418
+ });
4419
+ }
4420
+ get state() {
4421
+ return this._state;
4422
+ }
4423
+ get tracks() {
4424
+ const tracks = [];
4425
+ for (const [key, item] of this.remoteMedia) {
4426
+ if (!item.track) continue;
4427
+ tracks.push({
4428
+ participantId: item.participantId,
4429
+ name: item.name,
4430
+ kind: item.kind,
4431
+ track: item.track,
4432
+ quality: item.quality ?? null,
4433
+ setQuality: (quality) => this.setQuality(key, quality)
4434
+ });
4435
+ }
4436
+ return tracks;
4437
+ }
4438
+ get candidates() {
4439
+ return this.mediaCandidates().map(({ candidate }) => candidate);
4440
+ }
4441
+ onStateChange(handler) {
4442
+ this.stateHandlers.add(handler);
4443
+ return () => this.stateHandlers.delete(handler);
4444
+ }
4445
+ onTracksChanged(handler) {
4446
+ this.trackHandlers.add(handler);
4447
+ return () => this.trackHandlers.delete(handler);
4448
+ }
4449
+ select(filter) {
4450
+ this.filter = filter;
4451
+ this.scheduleReconcile();
4452
+ }
4453
+ channel(name, options = {}) {
4454
+ if (!isAppRtcName(name)) throw new AppRtcError("RTC_FAILED", `invalid RTC channel name: ${name}`);
4455
+ const delivery = options.delivery ?? "reliable";
4456
+ const existing = this.channels.get(name);
4457
+ if (existing?.isOpen) {
4458
+ if (existing.delivery !== delivery) throw new AppRtcError("RTC_FAILED", `RTC channel ${name} is already open with ${existing.delivery} delivery`);
4459
+ return existing;
4460
+ }
4461
+ if (this._state === "closed") throw new AppRtcError("RTC_CLOSED", "RTC connection is closed");
4462
+ const channel = new AppRtcChannel(this, name, delivery);
4463
+ this.channels.set(name, channel);
4464
+ this.publishChannel(channel);
4465
+ this.scheduleReconcile();
4466
+ return channel;
4467
+ }
4468
+ async publish(tracks) {
4469
+ if (this._state === "closed") throw new AppRtcError("RTC_CLOSED", "RTC connection is closed");
4470
+ const inputs = Object.entries(tracks);
4471
+ for (const [name] of inputs) {
4472
+ if (!isAppRtcName(name)) throw new AppRtcError("RTC_FAILED", `invalid RTC track name: ${name}`);
4473
+ if (this.localTracks.has(name)) throw new AppRtcError("RTC_FAILED", `RTC track ${name} is already published`);
4474
+ }
4475
+ 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`);
4476
+ const entries = inputs.map(([name, input]) => {
4477
+ const { track, simulcast } = "track" in input ? input : {
4478
+ track: input,
4479
+ simulcast: false
4480
+ };
4481
+ const kind = track.kind === "audio" ? "audio" : "video";
4482
+ return this.localEntry(name, kind, track, simulcast === true && kind === "video");
4483
+ });
4484
+ for (const entry of entries) this.localTracks.set(entry.name, entry);
4485
+ try {
4486
+ await this.enqueue(() => this.publishTracks(entries.map((entry) => entry.name)));
4487
+ } catch (error) {
4488
+ if (!(error instanceof ConnectionReplacedError)) {
4489
+ for (const entry of entries) this.dropLocal(entry);
4490
+ throw toAppRtcError(error);
4491
+ }
4492
+ }
4493
+ return Object.fromEntries(entries.map((entry) => [entry.name, this.localHandle(entry)]));
4494
+ }
4495
+ async stats() {
4496
+ return this.pc ? readRtcTransportStats(await this.pc.getStats()) : {
4497
+ path: null,
4498
+ relayProtocol: null,
4499
+ rttMs: null
4500
+ };
4501
+ }
4502
+ close() {
4503
+ if (this._state === "closed") return;
4504
+ this.setState("closed");
4505
+ this.offMembers();
4506
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
4507
+ for (const channel of this.channels.values()) channel.close();
4508
+ for (const entry of this.localTracks.values()) entry.track.removeEventListener("ended", entry.onEnded);
4509
+ this.teardown(true);
4510
+ }
4511
+ /** @internal */
4512
+ async start() {
4513
+ try {
4514
+ await this.connect();
4515
+ } catch (error) {
4516
+ throw toAppRtcError(error);
4517
+ }
4518
+ }
4519
+ /** @internal */
4520
+ async verify() {
4521
+ if (this._state === "closed" || !this.sessionId) return;
4522
+ try {
4523
+ await this.request("GET", `/${this.sessionId}`);
4524
+ } catch (error) {
4525
+ if (isSessionLost(error)) this.reconnect();
4526
+ }
4527
+ }
4528
+ /** @internal */
4529
+ closeChannel(channel) {
4530
+ if (this.channels.get(channel.name) === channel) this.channels.delete(channel.name);
4531
+ const id = channel.publicationId;
4532
+ channel.detach();
4533
+ if (id) this.enqueue(() => this.unpublishData([id])).catch(() => void 0);
4534
+ this.scheduleReconcile();
4535
+ }
4536
+ localEntry(name, kind, track, simulcast) {
4537
+ const entry = {
4538
+ name,
4539
+ kind,
4540
+ track,
4541
+ simulcast,
4542
+ transceiver: null,
4543
+ id: null,
4544
+ ended: /* @__PURE__ */ new Set(),
4545
+ onEnded: () => void 0
4546
+ };
4547
+ entry.onEnded = () => {
4548
+ if (this.localTracks.get(name) !== entry) return;
4549
+ for (const handler of entry.ended) handler();
4550
+ this.stopLocal(entry).catch(() => void 0);
4551
+ };
4552
+ track.addEventListener("ended", entry.onEnded);
4553
+ return entry;
4554
+ }
4555
+ localHandle(entry) {
4556
+ return {
4557
+ name: entry.name,
4558
+ kind: entry.kind,
4559
+ simulcast: entry.simulcast,
4560
+ get track() {
4561
+ return entry.track;
4562
+ },
4563
+ replace: async (track) => {
4564
+ if (track.kind !== entry.kind) throw new AppRtcError("RTC_FAILED", `replace ${entry.name} with another ${entry.kind} track`);
4565
+ if (this.localTracks.get(entry.name) !== entry) throw new AppRtcError("RTC_CLOSED", `RTC track ${entry.name} has stopped`);
4566
+ entry.track.removeEventListener("ended", entry.onEnded);
4567
+ entry.track = track;
4568
+ track.addEventListener("ended", entry.onEnded);
4569
+ await entry.transceiver?.sender.replaceTrack(track);
4570
+ },
4571
+ onEnded: (handler) => {
4572
+ entry.ended.add(handler);
4573
+ return () => entry.ended.delete(handler);
4574
+ },
4575
+ stop: () => this.stopLocal(entry)
4576
+ };
4577
+ }
4578
+ dropLocal(entry) {
4579
+ entry.track.removeEventListener("ended", entry.onEnded);
4580
+ if (this.localTracks.get(entry.name) === entry) this.localTracks.delete(entry.name);
4581
+ }
4582
+ stopLocal(entry) {
4583
+ if (this.localTracks.get(entry.name) !== entry) return Promise.resolve();
4584
+ this.dropLocal(entry);
4585
+ return this.enqueue(() => this.unpublishTrack(entry)).catch((error) => {
4586
+ if (error instanceof ConnectionReplacedError) return;
4587
+ throw toAppRtcError(error);
4588
+ });
4589
+ }
4590
+ async publishTracks(names) {
4591
+ const pc = this.requirePc();
4592
+ const entries = names.flatMap((name) => {
4593
+ const entry = this.localTracks.get(name);
4594
+ return entry && !entry.id ? [entry] : [];
4595
+ });
4596
+ if (entries.length === 0) return;
4597
+ for (const entry of entries) entry.transceiver = pc.addTransceiver(entry.track, {
4598
+ direction: "sendonly",
4599
+ ...entry.simulcast ? { sendEncodings: APP_RTC_SIMULCAST_LAYERS.map(({ rid, scaleResolutionDownBy }) => ({
4600
+ rid,
4601
+ scaleResolutionDownBy
4602
+ })) } : {}
4603
+ });
4604
+ const offer = await this.localOffer(pc);
4605
+ let result;
4606
+ try {
4607
+ result = await this.request("POST", `/${this.sessionId}/publish`, {
4608
+ offer,
4609
+ tracks: entries.map((entry) => ({
4610
+ name: entry.name,
4611
+ kind: entry.kind,
4612
+ mid: entry.transceiver?.mid ?? "",
4613
+ ...entry.simulcast ? { simulcast: true } : {}
4614
+ }))
4615
+ });
4616
+ } catch (error) {
4617
+ if (error instanceof ConnectionReplacedError) throw error;
4618
+ await this.settleOffer(pc, void 0).catch(() => void 0);
4619
+ for (const entry of entries) {
4620
+ entry.transceiver?.stop();
4621
+ entry.transceiver = null;
4622
+ }
4623
+ throw error;
4624
+ }
4625
+ await this.settleOffer(pc, result.answer);
4626
+ for (const publication of result.publications) {
4627
+ const entry = entries.find((candidate) => candidate.name === publication.name);
4628
+ if (entry) entry.id = publication.id;
4629
+ }
4630
+ }
4631
+ async unpublishTrack(entry) {
4632
+ if (!entry.id || !entry.transceiver || !this.pc) return;
4633
+ const id = entry.id;
4634
+ entry.transceiver.stop();
4635
+ await this.offerExchange(this.pc, (offer) => this.request("POST", `/${this.sessionId}/unpublish`, {
4636
+ ids: [id],
4637
+ offer
4638
+ }));
4639
+ entry.id = null;
4640
+ }
4641
+ publishChannel(channel) {
4642
+ this.channelsToPublish.add(channel);
4643
+ if (this.channelsToPublish.size > 1 || !this.pc) return;
4644
+ this.enqueue(async () => {
4645
+ const batch = [...this.channelsToPublish];
4646
+ this.channelsToPublish.clear();
4647
+ try {
4648
+ await this.publishChannels(batch);
4649
+ } catch (error) {
4650
+ if (isSessionLost(error) || error instanceof ConnectionReplacedError) throw error;
4651
+ if (error instanceof HttpError && error.code === "RTC_RATE_LIMITED") {
4652
+ setTimeout(() => {
4653
+ for (const item of batch) if (item.isOpen) this.publishChannel(item);
4654
+ }, RATE_LIMIT_RETRY_MS);
4655
+ return;
4656
+ }
4657
+ for (const item of batch) item.fail();
4658
+ console.warn("[Cohub AppRtc] failed to publish channels", error);
4659
+ }
4660
+ }).catch(() => void 0);
4661
+ }
4662
+ async connect() {
4663
+ const generation = ++this.generation;
4664
+ const opened = await this.send("POST", "", {
4665
+ ticket: this.host.ticket,
4666
+ participantId: this.host.participantId()
4667
+ });
4668
+ if (generation !== this.generation || this._state === "closed") {
4669
+ this.send("DELETE", `/${opened.sessionId}`).catch(() => void 0);
4670
+ return;
4671
+ }
4672
+ const pc = new RTCPeerConnection({
4673
+ iceServers: opened.iceServers,
4674
+ bundlePolicy: "max-bundle"
4675
+ });
4676
+ this.pc = pc;
4677
+ this.sessionId = opened.sessionId;
4678
+ pc.addEventListener("connectionstatechange", () => {
4679
+ if (this.pc !== pc) return;
4680
+ if (pc.connectionState === "connected") {
4681
+ this.reconnectAttempt = 0;
4682
+ this.setState("connected");
4683
+ } else if (pc.connectionState === "failed") this.reconnect();
4684
+ });
4685
+ pc.addEventListener("track", (event) => this.onTrack(event));
4686
+ const ready = (async () => {
4687
+ await this.answerOffer(opened.offer);
4688
+ await this.connected(pc);
4689
+ })();
4690
+ this.queue = ready.catch(() => void 0);
4691
+ try {
4692
+ await ready;
4693
+ } catch (error) {
4694
+ if (generation === this.generation) this.reconnect();
4695
+ throw error;
4696
+ }
4697
+ this.channelsToPublish.clear();
4698
+ const republish = [...this.channels.values()].filter((channel) => channel.isOpen);
4699
+ const tracks = [...this.localTracks.keys()];
4700
+ await Promise.all([republish.length ? this.enqueue(() => this.publishChannels(republish)).catch((error) => {
4701
+ if (error instanceof ConnectionReplacedError || isSessionLost(error)) return;
4702
+ for (const channel of republish) channel.fail();
4703
+ console.warn("[Cohub AppRtc] failed to publish channels on the new connection", error);
4704
+ }) : null, tracks.length ? this.enqueue(() => this.publishTracks(tracks)).catch((error) => {
4705
+ if (error instanceof ConnectionReplacedError || isSessionLost(error)) return;
4706
+ console.warn("[Cohub AppRtc] failed to publish tracks on the new connection", error);
4707
+ }) : null]);
4708
+ this.scheduleReconcile();
4709
+ }
4710
+ connected(pc) {
4711
+ if (pc.connectionState === "connected") return Promise.resolve();
4712
+ return new Promise((resolve, reject) => {
4713
+ const timer = setTimeout(() => done(/* @__PURE__ */ new Error("RTC connection timed out")), CONNECT_TIMEOUT_MS);
4714
+ const done = (error) => {
4715
+ clearTimeout(timer);
4716
+ pc.removeEventListener("connectionstatechange", check);
4717
+ if (error) reject(error);
4718
+ else resolve();
4719
+ };
4720
+ const check = () => {
4721
+ if (pc.connectionState === "connected") done();
4722
+ else if (pc.connectionState === "failed" || pc.connectionState === "closed") done(/* @__PURE__ */ new Error("RTC connection failed"));
4723
+ };
4724
+ pc.addEventListener("connectionstatechange", check);
4725
+ });
4726
+ }
4727
+ reconnect() {
4728
+ if (this._state === "closed" || this.reconnectTimer) return;
4729
+ this.setState(this._state === "connecting" ? "connecting" : "reconnecting");
4730
+ this.teardown(true);
4731
+ const delay = RECONNECT_DELAYS_MS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)] ?? 3e4;
4732
+ this.reconnectAttempt += 1;
4733
+ this.reconnectTimer = setTimeout(() => {
4734
+ this.reconnectTimer = null;
4735
+ this.connect().catch((error) => {
4736
+ if (isRoomGone(error) || error instanceof HttpError && (error.status === 403 || error.status === 503)) this.close();
4737
+ else this.reconnect();
4738
+ });
4739
+ }, delay);
4740
+ }
4741
+ teardown(release) {
4742
+ this.generation += 1;
4743
+ const sessionId = this.sessionId;
4744
+ this.sessionId = null;
4745
+ this.queue = Promise.resolve();
4746
+ for (const channel of this.channels.values()) channel.detach();
4747
+ for (const item of this.remoteData.values()) this.dropRemoteData(item);
4748
+ this.remoteData.clear();
4749
+ for (const item of this.localTracks.values()) {
4750
+ item.transceiver = null;
4751
+ item.id = null;
4752
+ }
4753
+ const hadTracks = this.remoteMedia.size > 0;
4754
+ this.remoteMedia.clear();
4755
+ this.mediaByMid.clear();
4756
+ this.pc?.close();
4757
+ this.pc = null;
4758
+ if (hadTracks) this.notifyTracks();
4759
+ if (release && sessionId) this.send("DELETE", `/${sessionId}`).catch(() => void 0);
4760
+ }
4761
+ enqueue(operation) {
4762
+ const generation = this.generation;
4763
+ const run = async () => {
4764
+ if (generation !== this.generation || !this.pc || !this.sessionId) throw new ConnectionReplacedError();
4765
+ try {
4766
+ return await operation();
4767
+ } catch (error) {
4768
+ if (isSessionLost(error)) this.reconnect();
4769
+ throw error;
4770
+ }
4771
+ };
4772
+ const next = this.queue.then(run, run);
4773
+ this.queue = next.catch(() => void 0);
4774
+ return next;
4775
+ }
4776
+ async publishChannels(channels) {
4777
+ const pc = this.requirePc();
4778
+ const open = channels.filter((channel) => channel.isOpen && !channel.publicationId);
4779
+ if (open.length === 0) return;
4780
+ const result = await this.request("POST", `/${this.sessionId}/publish`, { channels: open.map((channel) => ({
4781
+ name: channel.name,
4782
+ delivery: channel.delivery
4783
+ })) });
4784
+ for (const publication of result.publications) {
4785
+ const channel = open.find((item) => item.name === publication.name);
4786
+ const id = result.channelIds?.[publication.id];
4787
+ if (!channel || id === void 0) continue;
4788
+ const dataChannel = pc.createDataChannel(publication.name, {
4789
+ negotiated: true,
4790
+ id,
4791
+ ...policyOf(channel.delivery)
4792
+ });
4793
+ channel.attach(publication.id, dataChannel);
4794
+ if (!channel.isOpen) await this.unpublishData([publication.id]);
4795
+ }
4796
+ }
4797
+ async unpublishData(ids) {
4798
+ await this.request("POST", `/${this.sessionId}/unpublish`, { ids });
3847
4799
  }
3848
- async getSubscriptions(input) {
3849
- const params = new URLSearchParams();
3850
- if (input?.page) params.set("page", String(input.page));
3851
- if (input?.limit) params.set("limit", String(input.limit));
3852
- const query = params.toString();
3853
- return this.transport.request(`/api/billing/subscriptions${query ? `?${query}` : ""}`);
4800
+ async settleOffer(pc, answer) {
4801
+ if (answer) await pc.setRemoteDescription(answer);
4802
+ else if (pc.signalingState === "have-local-offer") await pc.setLocalDescription({ type: "rollback" });
3854
4803
  }
3855
- /** Confirms a returned checkout against the provider's order/subscription state. */
3856
- async confirmCheckout(productKey, checkoutId) {
3857
- return this.transport.request("/api/billing/checkout-confirmation", {
3858
- method: "POST",
3859
- body: JSON.stringify({
3860
- productKey,
3861
- checkoutId
3862
- })
4804
+ async offerExchange(pc, send) {
4805
+ const offer = await this.localOffer(pc);
4806
+ let answer;
4807
+ try {
4808
+ ({answer} = await send(offer));
4809
+ } catch (error) {
4810
+ if (!(error instanceof ConnectionReplacedError)) await this.settleOffer(pc, void 0).catch(() => void 0);
4811
+ throw error;
4812
+ }
4813
+ await this.settleOffer(pc, answer);
4814
+ }
4815
+ scheduleReconcile() {
4816
+ if (this.reconcileQueued || !this.pc || this._state === "closed") return;
4817
+ this.reconcileQueued = true;
4818
+ this.enqueue(async () => {
4819
+ this.reconcileQueued = false;
4820
+ await this.reconcile();
4821
+ this.reconcileFailures = 0;
4822
+ }).catch((error) => {
4823
+ this.reconcileQueued = false;
4824
+ if (isSessionLost(error) || error instanceof ConnectionReplacedError) return;
4825
+ console.warn("[Cohub AppRtc] failed to update subscriptions", error);
4826
+ if (error instanceof HttpError && error.code === "RTC_LIMIT_EXCEEDED") return;
4827
+ const delay = RECONNECT_DELAYS_MS[Math.min(this.reconcileFailures, RECONNECT_DELAYS_MS.length - 1)] ?? 3e4;
4828
+ this.reconcileFailures += 1;
4829
+ const generation = this.generation;
4830
+ setTimeout(() => {
4831
+ if (generation === this.generation) this.scheduleReconcile();
4832
+ }, delay);
3863
4833
  });
3864
4834
  }
3865
- async previewPromotionCode(input) {
3866
- return this.transport.request("/api/billing/promotion-code-preview", {
3867
- method: "POST",
3868
- body: JSON.stringify(input)
3869
- });
4835
+ mediaCandidates() {
4836
+ const self = this.host.participantId();
4837
+ const candidates = [];
4838
+ for (const member of this.host.members()) {
4839
+ if (member.participantId === self) continue;
4840
+ for (const publication of member.publications ?? []) {
4841
+ if (publication.kind === "data") continue;
4842
+ candidates.push({
4843
+ publication,
4844
+ candidate: {
4845
+ participantId: member.participantId,
4846
+ name: publication.name,
4847
+ kind: publication.kind,
4848
+ simulcast: publication.simulcast === true,
4849
+ subscribed: this.remoteMedia.has(mediaKey(member.participantId, publication.id))
4850
+ }
4851
+ });
4852
+ }
4853
+ }
4854
+ return candidates;
4855
+ }
4856
+ async reconcile() {
4857
+ const self = this.host.participantId();
4858
+ const wantedData = /* @__PURE__ */ new Map();
4859
+ for (const member of this.host.members()) {
4860
+ if (member.participantId === self) continue;
4861
+ for (const publication of member.publications ?? []) {
4862
+ if (publication.kind !== "data") continue;
4863
+ const channel = this.channels.get(publication.name);
4864
+ if (channel?.isOpen && channel.delivery === (publication.delivery ?? "reliable")) wantedData.set(mediaKey(member.participantId, publication.id), {
4865
+ participantId: member.participantId,
4866
+ publicationId: publication.id,
4867
+ name: publication.name,
4868
+ delivery: channel.delivery
4869
+ });
4870
+ }
4871
+ }
4872
+ const wantedMedia = /* @__PURE__ */ new Map();
4873
+ for (const { candidate, publication } of this.mediaCandidates()) {
4874
+ if (wantedMedia.size >= APP_RTC_LIMITS.mediaSubscriptions) break;
4875
+ if (this.filter && !this.filter(candidate)) continue;
4876
+ const key = mediaKey(candidate.participantId, publication.id);
4877
+ const quality = this.remoteMedia.get(key)?.quality ?? (candidate.simulcast ? "high" : void 0);
4878
+ wantedMedia.set(key, {
4879
+ participantId: candidate.participantId,
4880
+ publicationId: publication.id,
4881
+ name: candidate.name,
4882
+ kind: candidate.kind,
4883
+ ...quality ? { quality } : {}
4884
+ });
4885
+ }
4886
+ await this.reconcileData(wantedData);
4887
+ await this.reconcileMedia(wantedMedia);
4888
+ 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");
4889
+ }
4890
+ dropRemoteData(item) {
4891
+ item.channel.close();
4892
+ this.channels.get(item.name)?.setPeer(item.participantId, false);
4893
+ }
4894
+ async reconcileData(wanted) {
4895
+ const pc = this.requirePc();
4896
+ const stale = [...this.remoteData].filter(([key]) => !wanted.has(key));
4897
+ for (const batch of chunk(stale, APP_RTC_LIMITS.batch)) {
4898
+ await this.request("POST", `/${this.sessionId}/unsubscribe`, { items: batch.map(([, item]) => ({
4899
+ participantId: item.participantId,
4900
+ publicationId: item.publicationId
4901
+ })) });
4902
+ for (const [key, item] of batch) {
4903
+ this.dropRemoteData(item);
4904
+ this.remoteData.delete(key);
4905
+ }
4906
+ }
4907
+ const missing = [...wanted].filter(([key]) => !this.remoteData.has(key)).map(([, item]) => item);
4908
+ for (const batch of chunk(missing, APP_RTC_LIMITS.batch)) {
4909
+ const result = await this.request("POST", `/${this.sessionId}/subscribe`, {
4910
+ kind: "data",
4911
+ items: batch.map(({ participantId, publicationId }) => ({
4912
+ participantId,
4913
+ publicationId
4914
+ }))
4915
+ });
4916
+ for (const subscription of result.subscriptions) {
4917
+ const item = batch.find((entry) => entry.participantId === subscription.participantId && entry.publicationId === subscription.publicationId);
4918
+ if (!item || subscription.channelId === void 0) continue;
4919
+ const channel = pc.createDataChannel(item.name, {
4920
+ negotiated: true,
4921
+ id: subscription.channelId,
4922
+ ...policyOf(item.delivery)
4923
+ });
4924
+ channel.binaryType = "arraybuffer";
4925
+ const remote = {
4926
+ name: item.name,
4927
+ participantId: item.participantId,
4928
+ publicationId: item.publicationId,
4929
+ channel,
4930
+ last: null
4931
+ };
4932
+ channel.addEventListener("message", (event) => this.onData(remote, event.data));
4933
+ channel.addEventListener("open", () => {
4934
+ if (this.remoteData.get(mediaKey(remote.participantId, remote.publicationId)) === remote) this.channels.get(remote.name)?.setPeer(remote.participantId, true);
4935
+ });
4936
+ this.remoteData.set(mediaKey(item.participantId, item.publicationId), remote);
4937
+ }
4938
+ }
3870
4939
  }
3871
- async createOrder(productKey, input) {
3872
- return this.transport.request("/api/billing/orders", {
3873
- method: "POST",
3874
- body: JSON.stringify({
3875
- ...input ?? {},
3876
- productKey
3877
- })
4940
+ async reconcileMedia(wanted) {
4941
+ const stale = [...this.remoteMedia].filter(([key]) => !wanted.has(key));
4942
+ const missing = [...wanted].filter(([key]) => !this.remoteMedia.has(key)).map(([, item]) => item);
4943
+ const moves = [];
4944
+ for (const to of [...missing]) {
4945
+ const index = stale.findIndex(([, receiver]) => receiver.kind === to.kind);
4946
+ const found = index >= 0 ? stale.splice(index, 1)[0] : void 0;
4947
+ if (!found) continue;
4948
+ moves.push({
4949
+ from: found[0],
4950
+ receiver: found[1],
4951
+ to
4952
+ });
4953
+ missing.splice(missing.indexOf(to), 1);
4954
+ }
4955
+ if (moves.length) {
4956
+ const moved = await this.retarget(moves.map(({ receiver, to }) => ({
4957
+ ...to,
4958
+ mid: receiver.mid
4959
+ })));
4960
+ for (const { from, receiver, to } of moves) if (moved.has(receiver.mid)) {
4961
+ this.remoteMedia.delete(from);
4962
+ this.remoteMedia.set(mediaKey(to.participantId, to.publicationId), {
4963
+ ...to,
4964
+ mid: receiver.mid,
4965
+ track: receiver.track
4966
+ });
4967
+ this.mediaByMid.set(receiver.mid, mediaKey(to.participantId, to.publicationId));
4968
+ } else {
4969
+ stale.push([from, receiver]);
4970
+ missing.push(to);
4971
+ }
4972
+ if (moved.size) this.notifyTracks();
4973
+ }
4974
+ await this.unsubscribeMedia(stale);
4975
+ await this.subscribeMedia(missing);
4976
+ }
4977
+ async retarget(items) {
4978
+ try {
4979
+ const result = await this.request("PATCH", `/${this.sessionId}/media`, { items: items.map(({ participantId, publicationId, mid, quality }) => ({
4980
+ participantId,
4981
+ publicationId,
4982
+ mid,
4983
+ ...quality ? { quality } : {}
4984
+ })) });
4985
+ if (result.offer) await this.answerOffer(result.offer);
4986
+ return new Set(result.updated.map((item) => item.mid));
4987
+ } catch (error) {
4988
+ if (error instanceof ConnectionReplacedError || isSessionLost(error)) throw error;
4989
+ return /* @__PURE__ */ new Set();
4990
+ }
4991
+ }
4992
+ async setQuality(key, quality) {
4993
+ await this.enqueue(async () => {
4994
+ const item = this.remoteMedia.get(key);
4995
+ if (!item || item.quality === void 0 || item.quality === quality) return;
4996
+ if ((await this.retarget([{
4997
+ ...item,
4998
+ quality
4999
+ }])).has(item.mid)) {
5000
+ item.quality = quality;
5001
+ this.notifyTracks();
5002
+ }
5003
+ }).catch((error) => {
5004
+ if (error instanceof ConnectionReplacedError) return;
5005
+ throw toAppRtcError(error);
3878
5006
  });
3879
5007
  }
3880
- async createSubscription(productKey, input) {
3881
- return this.transport.request("/api/billing/subscriptions", {
3882
- method: "POST",
3883
- body: JSON.stringify({
3884
- ...input ?? {},
3885
- productKey
3886
- })
5008
+ async unsubscribeMedia(stale) {
5009
+ if (stale.length === 0) return;
5010
+ const pc = this.requirePc();
5011
+ for (const [, item] of stale) {
5012
+ pc.getTransceivers().find((transceiver) => transceiver.mid === item.mid)?.stop();
5013
+ item.track = null;
5014
+ }
5015
+ this.notifyTracks();
5016
+ await this.offerExchange(pc, (offer) => this.request("POST", `/${this.sessionId}/unsubscribe`, {
5017
+ items: stale.map(([, item]) => ({
5018
+ participantId: item.participantId,
5019
+ publicationId: item.publicationId
5020
+ })),
5021
+ offer
5022
+ }));
5023
+ for (const [key, item] of stale) {
5024
+ this.remoteMedia.delete(key);
5025
+ this.mediaByMid.delete(item.mid);
5026
+ }
5027
+ }
5028
+ async subscribeMedia(missing) {
5029
+ if (missing.length === 0) return;
5030
+ const result = await this.request("POST", `/${this.sessionId}/subscribe`, {
5031
+ kind: "media",
5032
+ items: missing.map(({ participantId, publicationId, quality }) => ({
5033
+ participantId,
5034
+ publicationId,
5035
+ ...quality ? { quality } : {}
5036
+ }))
3887
5037
  });
5038
+ for (const subscription of result.subscriptions) {
5039
+ const item = missing.find((entry) => entry.participantId === subscription.participantId && entry.publicationId === subscription.publicationId);
5040
+ if (!item || !subscription.mid) continue;
5041
+ const key = mediaKey(item.participantId, item.publicationId);
5042
+ this.remoteMedia.set(key, {
5043
+ ...item,
5044
+ mid: subscription.mid,
5045
+ track: null
5046
+ });
5047
+ this.mediaByMid.set(subscription.mid, key);
5048
+ }
5049
+ if (!result.offer) return;
5050
+ try {
5051
+ await this.answerOffer(result.offer);
5052
+ } catch (error) {
5053
+ if (!(error instanceof ConnectionReplacedError)) this.reconnect();
5054
+ throw error;
5055
+ }
3888
5056
  }
3889
- async cancelSubscriptionCheckout(subscriptionId) {
3890
- return this.transport.request(`/api/billing/subscriptions/${encodeURIComponent(subscriptionId)}/checkout`, { method: "DELETE" });
5057
+ async answerOffer(offer) {
5058
+ const pc = this.requirePc();
5059
+ await pc.setRemoteDescription(offer);
5060
+ await pc.setLocalDescription(await pc.createAnswer());
5061
+ await waitForIceGathering(pc, GATHER_TIMEOUT_MS);
5062
+ const answer = pc.localDescription;
5063
+ if (!answer) throw new Error("the browser produced no answer");
5064
+ await this.request("PUT", `/${this.sessionId}`, { answer: {
5065
+ type: "answer",
5066
+ sdp: answer.sdp
5067
+ } });
5068
+ }
5069
+ async localOffer(pc) {
5070
+ await pc.setLocalDescription(await pc.createOffer());
5071
+ await waitForIceGathering(pc, GATHER_TIMEOUT_MS);
5072
+ const offer = pc.localDescription;
5073
+ if (!offer) throw new Error("the browser produced no offer");
5074
+ return {
5075
+ type: "offer",
5076
+ sdp: offer.sdp
5077
+ };
3891
5078
  }
3892
- async cancelSubscriptionAutoRenew(subscriptionId) {
3893
- return this.transport.request(`/api/billing/subscriptions/${encodeURIComponent(subscriptionId)}`, {
3894
- method: "PATCH",
3895
- body: JSON.stringify({ cancelAtPeriodEnd: true })
5079
+ async request(method, path, body) {
5080
+ const generation = this.generation;
5081
+ const settled = await this.send(method, path, body).then((value) => ({ value }), (error) => ({ error }));
5082
+ if (generation !== this.generation) throw new ConnectionReplacedError();
5083
+ if ("error" in settled) throw settled.error;
5084
+ return settled.value;
5085
+ }
5086
+ send(method, path, body) {
5087
+ const url = `/api/apps/${encodeURIComponent(this.host.appId)}/realtime/rooms/${encodeURIComponent(this.host.roomId)}/rtc${path}`;
5088
+ return this.host.transport.request(url, {
5089
+ method,
5090
+ ...body === void 0 ? {} : {
5091
+ headers: { "Content-Type": "application/json" },
5092
+ body: JSON.stringify(body)
5093
+ }
3896
5094
  });
3897
5095
  }
3898
- async createRedemption(input) {
3899
- return this.transport.request("/api/billing/redemptions", {
3900
- method: "POST",
3901
- body: JSON.stringify(input)
5096
+ requirePc() {
5097
+ if (!this.pc || !this.sessionId) throw new ConnectionReplacedError();
5098
+ return this.pc;
5099
+ }
5100
+ onData(remote, raw) {
5101
+ if (!(raw instanceof ArrayBuffer)) return;
5102
+ const frame = decodeAppRtcFrame(raw);
5103
+ if (!frame) return;
5104
+ const channel = this.channels.get(remote.name);
5105
+ if (!channel?.isOpen) return;
5106
+ if (channel.delivery === "latest") {
5107
+ if (!isNewerSequence(frame.sequence, remote.last)) return;
5108
+ remote.last = frame.sequence;
5109
+ }
5110
+ channel.deliver(remote.participantId, frame.data);
5111
+ }
5112
+ onTrack(event) {
5113
+ const mid = event.transceiver.mid;
5114
+ const key = mid ? this.mediaByMid.get(mid) : void 0;
5115
+ const item = key ? this.remoteMedia.get(key) : void 0;
5116
+ if (!item || !mid) return;
5117
+ item.track = event.track;
5118
+ event.track.addEventListener("ended", () => {
5119
+ const current = this.mediaByMid.get(mid);
5120
+ const owner = current ? this.remoteMedia.get(current) : void 0;
5121
+ if (owner?.track === event.track) {
5122
+ owner.track = null;
5123
+ this.notifyTracks();
5124
+ }
3902
5125
  });
5126
+ this.notifyTracks();
5127
+ }
5128
+ notifyTracks() {
5129
+ const tracks = this.tracks;
5130
+ for (const handler of this.trackHandlers) handler(tracks);
5131
+ }
5132
+ setState(state) {
5133
+ if (this._state === state) return;
5134
+ this._state = state;
5135
+ for (const handler of this.stateHandlers) handler(state);
3903
5136
  }
3904
5137
  };
3905
5138
  //#endregion
@@ -3911,13 +5144,20 @@ const byteLength = (text) => textEncoder.encode(text).byteLength;
3911
5144
  * Ceiling on deltas held during a join, so a stalled handshake cannot buffer without
3912
5145
  * limit. Dropping the tail needs no extra signal: the gap detection below reports it.
3913
5146
  */
3914
- const WORK_ROOM_MAX_JOIN_BUFFER = 512;
5147
+ const APP_ROOM_MAX_JOIN_BUFFER = 512;
3915
5148
  /** Room events that mutate state, so they must not be applied before the join snapshot. */
3916
- const WORK_ROOM_DELTA_EVENTS = /* @__PURE__ */ new Set([
5149
+ const APP_ROOM_DELTA_EVENTS = /* @__PURE__ */ new Set([
3917
5150
  "realtime.room.event",
3918
5151
  "realtime.room.member.joined",
3919
5152
  "realtime.room.member.left",
3920
- "realtime.room.presence.updated"
5153
+ "realtime.room.presence.updated",
5154
+ "realtime.room.publications.updated"
5155
+ ]);
5156
+ const APP_ROOM_MEMBER_EVENTS = /* @__PURE__ */ new Set([
5157
+ "realtime.room.member.joined",
5158
+ "realtime.room.member.left",
5159
+ "realtime.room.presence.updated",
5160
+ "realtime.room.publications.updated"
3921
5161
  ]);
3922
5162
  const requestError = (event) => {
3923
5163
  const payload = event.payload;
@@ -3930,6 +5170,7 @@ const isRoomEvent = (event, roomId) => {
3930
5170
  var AppRoom = class {
3931
5171
  websocket;
3932
5172
  admission;
5173
+ appContext;
3933
5174
  id;
3934
5175
  code;
3935
5176
  createdAt;
@@ -3957,9 +5198,12 @@ var AppRoom = class {
3957
5198
  offOpen;
3958
5199
  offClose;
3959
5200
  offReconnecting;
3960
- constructor(websocket, admission) {
5201
+ instanceId = randomId().replace(/[^A-Za-z0-9_-]/g, "");
5202
+ rtcConnection = null;
5203
+ constructor(websocket, admission, appContext) {
3961
5204
  this.websocket = websocket;
3962
5205
  this.admission = admission;
5206
+ this.appContext = appContext;
3963
5207
  this.id = admission.room.id;
3964
5208
  this.code = admission.room.code;
3965
5209
  this.createdAt = admission.room.createdAt;
@@ -4085,6 +5329,66 @@ var AppRoom = class {
4085
5329
  clientEventId: typeof payload.clientEventId === "string" ? payload.clientEventId : null
4086
5330
  };
4087
5331
  }
5332
+ /** Published events after `after`, oldest first; without it, the newest ones. */
5333
+ async history(input = {}) {
5334
+ this.assertJoined();
5335
+ if (!this.websocket.supportsCapability("realtime.room.history.v1")) throw new Error("Room history is not supported by the Gateway");
5336
+ const requestId = randomId();
5337
+ const response = await this.request(requestId, /* @__PURE__ */ new Set(["realtime.room.history"]), () => this.websocket.readRealtimeRoomHistory({
5338
+ roomId: this.id,
5339
+ ...input,
5340
+ requestId
5341
+ }));
5342
+ return this.toHistory(response.payload);
5343
+ }
5344
+ /** An invite for someone outside the App, e.g. an Agent: `cohub rooms join <invite>`. */
5345
+ async invite(input = {}) {
5346
+ this.assertJoined();
5347
+ const context = this.appContext;
5348
+ if (!context) throw new Error("Invites are only available inside a published App");
5349
+ return context.transport.request(`/api/apps/${encodeURIComponent(context.appId)}/realtime/rooms/${encodeURIComponent(this.id)}/invites`, {
5350
+ method: "POST",
5351
+ headers: { "Content-Type": "application/json" },
5352
+ body: JSON.stringify({
5353
+ ticket: this.admission.ticket,
5354
+ ...input
5355
+ })
5356
+ });
5357
+ }
5358
+ rtc() {
5359
+ this.assertJoined();
5360
+ if (this.rtcConnection) {
5361
+ const current = this.rtcConnection;
5362
+ return current.then((rtc) => {
5363
+ if (rtc.state !== "closed") return rtc;
5364
+ return (this.rtcConnection === current ? this.openRtc() : this.rtcConnection) ?? this.openRtc();
5365
+ });
5366
+ }
5367
+ return this.openRtc();
5368
+ }
5369
+ openRtc() {
5370
+ this.assertJoined();
5371
+ const context = this.appContext;
5372
+ if (!context) return Promise.reject(new AppRtcError("RTC_UNSUPPORTED", "RTC is only available inside a published App"));
5373
+ if (typeof RTCPeerConnection === "undefined") return Promise.reject(new AppRtcError("RTC_UNSUPPORTED", "RTC needs a browser with WebRTC"));
5374
+ if (!this.websocket.supportsCapability("realtime.room.rtc.v1")) return Promise.reject(new AppRtcError("RTC_UNSUPPORTED", "RTC is not supported by the Gateway"));
5375
+ const rtc = new AppRoomRtc({
5376
+ roomId: this.id,
5377
+ appId: context.appId,
5378
+ ticket: this.admission.ticket,
5379
+ transport: context.transport,
5380
+ participantId: () => this._participantId,
5381
+ members: () => this._members,
5382
+ onMembersChanged: (handler) => this.onMembersChanged(handler)
5383
+ });
5384
+ const opening = rtc.start().then(() => rtc, (error) => {
5385
+ rtc.close();
5386
+ if (this.rtcConnection === opening) this.rtcConnection = null;
5387
+ throw error;
5388
+ });
5389
+ this.rtcConnection = opening;
5390
+ return opening;
5391
+ }
4088
5392
  async setPresence(presence) {
4089
5393
  this.assertJoined();
4090
5394
  const requestId = randomId();
@@ -4126,17 +5430,23 @@ var AppRoom = class {
4126
5430
  if (!this.websocket.supportsCapability("realtime.room.v1") && this.websocket.state === "open") throw new Error("Realtime rooms are not supported by the Gateway");
4127
5431
  this.setState(isReconnect ? "reconnecting" : "connecting");
4128
5432
  const requestId = randomId();
5433
+ const after = isReconnect && this.lastSequence > 0 && this.websocket.supportsCapability("realtime.room.history.v1") ? this.lastSequence : void 0;
5434
+ let missed = null;
4129
5435
  this.joinBuffer = [];
4130
5436
  try {
4131
5437
  const payload = (await this.request(requestId, /* @__PURE__ */ new Set(["realtime.room.joined"]), () => this.websocket.joinRealtimeRoom({
4132
5438
  roomId: this.id,
4133
5439
  ticket: this.admission.ticket,
5440
+ instanceId: this.instanceId,
5441
+ after,
4134
5442
  requestId
4135
5443
  }))).payload;
4136
5444
  if (typeof payload.participantId !== "string" || !Array.isArray(payload.members)) throw new Error("invalid room join response");
4137
5445
  this._participantId = payload.participantId;
4138
5446
  this._members = payload.members;
4139
- this.lastSequence = typeof payload.sequence === "number" ? payload.sequence : 0;
5447
+ const sequence = typeof payload.sequence === "number" ? payload.sequence : 0;
5448
+ if (after !== void 0 && sequence > after) missed = await this.catchUp(payload.missed, after, sequence);
5449
+ this.lastSequence = sequence;
4140
5450
  } catch (error) {
4141
5451
  this.joinBuffer = null;
4142
5452
  throw error;
@@ -4144,6 +5454,11 @@ var AppRoom = class {
4144
5454
  this.hasJoined = true;
4145
5455
  this.setState("joined");
4146
5456
  this.notifyMembers();
5457
+ if (missed) {
5458
+ const { lost } = missed;
5459
+ if (lost) for (const handler of this.outOfSyncHandlers) handler(lost[0], lost[1]);
5460
+ for (const event of missed.events) this.emit(event);
5461
+ }
4147
5462
  const buffered = this.joinBuffer ?? [];
4148
5463
  this.joinBuffer = null;
4149
5464
  for (const item of buffered) {
@@ -4151,6 +5466,7 @@ var AppRoom = class {
4151
5466
  if (typeof sequence === "number" && sequence <= this.lastSequence) continue;
4152
5467
  this.handleEvent(item);
4153
5468
  }
5469
+ if (isReconnect) this.rtcConnection?.then((rtc) => rtc.verify()).catch(() => void 0);
4154
5470
  }
4155
5471
  request(requestId, expected, send) {
4156
5472
  return new Promise((resolve, reject) => {
@@ -4198,14 +5514,13 @@ var AppRoom = class {
4198
5514
  this.takePending(event.requestId);
4199
5515
  awaiting.resolve(event);
4200
5516
  }
4201
- if (this.joinBuffer && WORK_ROOM_DELTA_EVENTS.has(event.type)) {
4202
- if (this.joinBuffer.length < WORK_ROOM_MAX_JOIN_BUFFER) this.joinBuffer.push(event);
5517
+ if (this.joinBuffer && APP_ROOM_DELTA_EVENTS.has(event.type)) {
5518
+ if (this.joinBuffer.length < APP_ROOM_MAX_JOIN_BUFFER) this.joinBuffer.push(event);
4203
5519
  return;
4204
5520
  }
4205
5521
  const payload = event.payload;
4206
- const sequence = typeof payload.sequence === "number" ? payload.sequence : null;
4207
- if (sequence !== null) this.observeSequence(sequence);
4208
- if (event.type === "realtime.room.member.joined" || event.type === "realtime.room.member.left" || event.type === "realtime.room.presence.updated") {
5522
+ if (APP_ROOM_DELTA_EVENTS.has(event.type) && typeof payload.sequence === "number") this.observeSequence(payload.sequence);
5523
+ if (APP_ROOM_MEMBER_EVENTS.has(event.type)) {
4209
5524
  const member = payload.member;
4210
5525
  if (member?.participantId) {
4211
5526
  const members = new Map(this._members.map((item) => [item.participantId, item]));
@@ -4225,11 +5540,10 @@ var AppRoom = class {
4225
5540
  return;
4226
5541
  }
4227
5542
  if (event.type !== "realtime.room.event") return;
4228
- const roomEvent = event;
4229
- const roomPayload = roomEvent.payload;
4230
- const item = {
4231
- id: roomEvent.id,
4232
- timestamp: roomEvent.timestamp,
5543
+ const { id, timestamp, payload: roomPayload } = event;
5544
+ this.emit({
5545
+ id,
5546
+ timestamp,
4233
5547
  roomId: roomPayload.roomId,
4234
5548
  sequence: roomPayload.sequence,
4235
5549
  type: roomPayload.event,
@@ -4237,7 +5551,35 @@ var AppRoom = class {
4237
5551
  clientEventId: roomPayload.clientEventId,
4238
5552
  sender: roomPayload.sender,
4239
5553
  self: roomPayload.sender.participantId === this.participantId
5554
+ });
5555
+ }
5556
+ async catchUp(first, after, sequence) {
5557
+ if (!first) return {
5558
+ events: [],
5559
+ lost: [after + 1, sequence]
5560
+ };
5561
+ const events = this.toHistory(first).events;
5562
+ const { truncated } = first;
5563
+ let { cursor } = first;
5564
+ while (cursor < sequence) {
5565
+ const requestId = randomId();
5566
+ const page = (await this.request(requestId, /* @__PURE__ */ new Set(["realtime.room.history"]), () => this.websocket.readRealtimeRoomHistory({
5567
+ roomId: this.id,
5568
+ after: cursor,
5569
+ requestId
5570
+ })).catch(() => null))?.payload;
5571
+ if (!page || page.cursor <= cursor) break;
5572
+ events.push(...this.toHistory(page).events);
5573
+ cursor = page.cursor;
5574
+ }
5575
+ const kept = events.filter((event) => event.sequence <= sequence);
5576
+ const lost = truncated ? [after + 1, kept[0]?.sequence ?? sequence] : cursor < sequence ? [cursor + 1, sequence] : null;
5577
+ return {
5578
+ events: kept,
5579
+ lost: lost && lost[1] > lost[0] ? lost : null
4240
5580
  };
5581
+ }
5582
+ emit(item) {
4241
5583
  this.handlers.get(item.type)?.forEach((handler) => {
4242
5584
  handler(item);
4243
5585
  });
@@ -4245,6 +5587,18 @@ var AppRoom = class {
4245
5587
  handler(item);
4246
5588
  });
4247
5589
  }
5590
+ toHistory(history) {
5591
+ return {
5592
+ events: history.events.map((event) => ({
5593
+ ...event,
5594
+ data: event.data,
5595
+ roomId: this.id,
5596
+ self: event.sender.participantId === this.participantId
5597
+ })),
5598
+ cursor: history.cursor,
5599
+ truncated: history.truncated
5600
+ };
5601
+ }
4248
5602
  observeSequence(sequence) {
4249
5603
  if (this.lastSequence > 0 && sequence > this.lastSequence + 1) for (const handler of this.outOfSyncHandlers) handler(this.lastSequence + 1, sequence);
4250
5604
  if (sequence > this.lastSequence) this.lastSequence = sequence;
@@ -4276,6 +5630,9 @@ var AppRoom = class {
4276
5630
  this.dispose();
4277
5631
  }
4278
5632
  dispose() {
5633
+ const rtc = this.rtcConnection;
5634
+ this.rtcConnection = null;
5635
+ rtc?.then((connection) => connection.close()).catch(() => void 0);
4279
5636
  this.offEvent();
4280
5637
  this.offOpen();
4281
5638
  this.offClose();
@@ -4294,28 +5651,33 @@ var AppRealtimeApi = class {
4294
5651
  this.getContext = getContext;
4295
5652
  }
4296
5653
  async createRoom(input = {}) {
4297
- const admission = await this.transport.request(`/api/apps/${encodeURIComponent(await this.requireAppId())}/realtime/rooms`, {
5654
+ const appId = await this.requireAppId();
5655
+ const admission = await this.transport.request(`/api/apps/${encodeURIComponent(appId)}/realtime/rooms`, {
4298
5656
  method: "POST",
4299
5657
  headers: { "Content-Type": "application/json" },
4300
5658
  body: JSON.stringify(input)
4301
5659
  });
4302
- return this.openRoom(admission);
5660
+ return this.openRoom(appId, admission);
4303
5661
  }
4304
5662
  async joinRoom(input) {
4305
- const admission = await this.transport.request(`/api/apps/${encodeURIComponent(await this.requireAppId())}/realtime/rooms/join`, {
5663
+ const appId = await this.requireAppId();
5664
+ const admission = await this.transport.request(`/api/apps/${encodeURIComponent(appId)}/realtime/rooms/join`, {
4306
5665
  method: "POST",
4307
5666
  headers: { "Content-Type": "application/json" },
4308
5667
  body: JSON.stringify(input)
4309
5668
  });
4310
- return this.openRoom(admission);
5669
+ return this.openRoom(appId, admission);
4311
5670
  }
4312
5671
  async requireAppId() {
4313
5672
  const context = await this.getContext();
4314
5673
  if (!context?.app?.id) throw new Error("App context is unavailable — not running inside a published app runtime.");
4315
5674
  return context.app.id;
4316
5675
  }
4317
- async openRoom(admission) {
4318
- const room = new AppRoom(this.websocket, admission);
5676
+ async openRoom(appId, admission) {
5677
+ const room = new AppRoom(this.websocket, admission, {
5678
+ transport: this.transport,
5679
+ appId
5680
+ });
4319
5681
  try {
4320
5682
  await room.connect();
4321
5683
  return room;
@@ -4582,10 +5944,12 @@ var AppWindowApi = class {
4582
5944
  }
4583
5945
  /** Report the tab title, save status, and whether work is still unsaved. */
4584
5946
  setState(patch) {
4585
- this.state = {
5947
+ const next = {
4586
5948
  ...this.state,
4587
5949
  ...patch
4588
5950
  };
5951
+ if (this.stateReported && next.title === this.state.title && next.status === this.state.status && next.dirty === this.state.dirty) return;
5952
+ this.state = next;
4589
5953
  this.stateReported = true;
4590
5954
  this.listen();
4591
5955
  this.transport.notify?.(buildAppRuntimeWindowState(this.state));
@@ -5392,6 +6756,18 @@ var AppRuntimeApi = class {
5392
6756
  reason: response === null ? "timeout" : "unsupported"
5393
6757
  };
5394
6758
  }
6759
+ /** Asks for devices declared with `<meta name="cohub:devices">`; allowing the camera or microphone reloads the App once. */
6760
+ async requestDevices(devices) {
6761
+ const wanted = parseAppDevices(devices);
6762
+ if (wanted.length === 0) throw new AppRuntimeError("invalid_request", `Request at least one of: ${APP_DEVICES.join(", ")}.`);
6763
+ const context = await this.context().catch(() => null);
6764
+ if (!context || context.mode === "broker") return "granted";
6765
+ if (!context.capabilities?.devices) return "unavailable";
6766
+ return parseAppDevicesResult(await this.transport.request({
6767
+ type: APP_DEVICES_REQUEST_TYPE,
6768
+ devices: wanted
6769
+ }, { timeoutMs: 6e5 }))?.status ?? "unavailable";
6770
+ }
5395
6771
  /**
5396
6772
  * Asks the host to close this App: a workspace tab closes, an embedded page
5397
6773
  * forwards the request to its embedder, a standalone page closes the tab.
@@ -5756,6 +7132,7 @@ var CohubClient = class {
5756
7132
  voice;
5757
7133
  apps;
5758
7134
  appCommerce;
7135
+ rooms;
5759
7136
  navigation;
5760
7137
  /** @deprecated Use `client.desktop`. */
5761
7138
  get ui() {
@@ -5786,7 +7163,10 @@ var CohubClient = class {
5786
7163
  const appTransport = resolveAppTransport(appRuntime, appIdResolver, createOriginAppResolver({
5787
7164
  apiBaseUrl,
5788
7165
  fetch: options.fetch
5789
- }), resolveWebBaseUrl({ env: options.env }));
7166
+ }), resolveWebBaseUrl({
7167
+ env: options.env,
7168
+ url: options.webBaseUrl
7169
+ }));
5790
7170
  this.appRuntime = createAppRuntime(appTransport, appRuntime?.appId, appIdResolver);
5791
7171
  this.appWindow = new AppWindowApi(appTransport, {
5792
7172
  context: () => this.appRuntime.context(),
@@ -5835,6 +7215,7 @@ var CohubClient = class {
5835
7215
  this.referrals = new ReferralsApi(this.transport);
5836
7216
  this.apps = new AppsApi(this.transport);
5837
7217
  this.appCommerce = new AppCommerceApi(this.transport);
7218
+ this.rooms = new RoomsApi(this.transport);
5838
7219
  this.navigation = { open: (target, options) => this.appRuntime.navigationOpen(typeof target === "string" ? {
5839
7220
  kind: "app",
5840
7221
  ref: target
@@ -5891,6 +7272,7 @@ sync: (root) => {
5891
7272
  } },
5892
7273
  /** Ask the host to close this App's surface. */
5893
7274
  requestClose: () => this.appRuntime.requestClose(),
7275
+ devices: { request: (devices) => this.appRuntime.requestDevices(devices) },
5894
7276
  /**
5895
7277
  * Request that the host update this overlay's geometry or pointer hit
5896
7278
  * regions. Only meaningful for `overlay` surfaces; ignored otherwise.
@@ -6564,7 +7946,8 @@ function createAppBridgeCore(config) {
6564
7946
  return {
6565
7947
  capabilities: {
6566
7948
  authorization: 2,
6567
- serverGrants: true
7949
+ serverGrants: true,
7950
+ ...config.devices ? { devices: true } : {}
6568
7951
  },
6569
7952
  mode: authorizationContext.surface === "broker" ? "broker" : "bridge",
6570
7953
  app: {
@@ -7623,129 +9006,6 @@ function createAppBridgeCore(config) {
7623
9006
  };
7624
9007
  }
7625
9008
  //#endregion
7626
- //#region src/generation-blocks.ts
7627
- /** Tolerant readers for generation payloads, shared by every task projection. */
7628
- const EXCERPT_LIMIT = 240;
7629
- function record(value) {
7630
- return value && typeof value === "object" && !Array.isArray(value) ? value : null;
7631
- }
7632
- function cleanExcerpt(value, limit = EXCERPT_LIMIT) {
7633
- if (typeof value !== "string") return void 0;
7634
- const clean = value.replace(/\s+/g, " ").trim();
7635
- if (!clean) return void 0;
7636
- return clean.length > limit ? `${clean.slice(0, limit - 3).trimEnd()}...` : clean;
7637
- }
7638
- function blockText(block) {
7639
- return cleanExcerpt(block.text ?? block.content ?? block.value);
7640
- }
7641
- function blockUrl(block) {
7642
- const source = record(block.source);
7643
- return normalizeBoardRemoteUrl(source?.url ?? source?.src ?? block.url ?? block.src);
7644
- }
7645
- function blockMimeType(block) {
7646
- const source = record(block.source);
7647
- const value = source?.mediaType ?? source?.media_type ?? source?.mimeType ?? block.mediaType ?? block.media_type ?? block.mimeType;
7648
- return typeof value === "string" ? value : void 0;
7649
- }
7650
- function positiveNumber(value) {
7651
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
7652
- }
7653
- function blockNaturalSize(block) {
7654
- const source = record(block.source);
7655
- const naturalWidth = positiveNumber(source?.width ?? source?.naturalWidth ?? block.width ?? block.naturalWidth);
7656
- const naturalHeight = positiveNumber(source?.height ?? source?.naturalHeight ?? block.height ?? block.naturalHeight);
7657
- return {
7658
- ...naturalWidth ? { naturalWidth } : {},
7659
- ...naturalHeight ? { naturalHeight } : {}
7660
- };
7661
- }
7662
- function contentBlocks(value) {
7663
- return Array.isArray(value) ? value.filter((item) => record(item)) : [];
7664
- }
7665
- function taskData(run) {
7666
- const payload = record(run.payload);
7667
- return record(payload?.data) ?? payload;
7668
- }
7669
- function generationOutput(run) {
7670
- return contentBlocks(record(run.result)?.output);
7671
- }
7672
- function generationPrompt(run) {
7673
- for (const block of contentBlocks(taskData(run)?.content)) {
7674
- const text = blockText(block);
7675
- if (text) return text;
7676
- }
7677
- }
7678
- function blockMeta(block) {
7679
- return record(block.meta);
7680
- }
7681
- function blockIdentity(block) {
7682
- const meta = blockMeta(block);
7683
- const value = meta?.id ?? meta?.clip_id ?? meta?.clipId ?? block.id ?? block.clip_id ?? block.clipId;
7684
- if (typeof value !== "string" && typeof value !== "number") return void 0;
7685
- return cleanExcerpt(String(value), 220);
7686
- }
7687
- function blockTitle(block) {
7688
- return cleanExcerpt(blockMeta(block)?.title ?? block.title ?? block.name, 240);
7689
- }
7690
- function blockDurationMs(block) {
7691
- const source = record(block.source);
7692
- const meta = blockMeta(block);
7693
- const milliseconds = positiveNumber(source?.durationMs ?? meta?.durationMs ?? block.durationMs);
7694
- if (milliseconds) return Math.round(milliseconds);
7695
- const seconds = positiveNumber(source?.duration ?? meta?.duration ?? block.duration);
7696
- return seconds ? Math.round(seconds * 1e3) : void 0;
7697
- }
7698
- function blockPreviewUrl(block) {
7699
- const source = record(block.source);
7700
- return normalizeBoardRemoteUrl(source?.poster ?? source?.thumbnail ?? source?.previewUrl ?? block.poster ?? block.thumbnail ?? block.previewUrl);
7701
- }
7702
- /**
7703
- * Image roles that depict playable media, by the media types they may cover;
7704
- * `last_frame` stays a result of its own.
7705
- */
7706
- const COVER_ROLES = /* @__PURE__ */ new Map([
7707
- ["first_frame", /* @__PURE__ */ new Set(["video"])],
7708
- ["cover", /* @__PURE__ */ new Set(["video", "audio"])],
7709
- ["poster", /* @__PURE__ */ new Set(["video", "audio"])],
7710
- ["thumbnail", /* @__PURE__ */ new Set(["video", "audio"])]
7711
- ]);
7712
- function isPlayable(block) {
7713
- return block.type === "video" || block.type === "audio";
7714
- }
7715
- /**
7716
- * Pair playable media with its cover image, returning media index → cover
7717
- * index. An image sharing the provider id wins (music tracks and their art,
7718
- * shared by every media of that id); otherwise cover-role images pair in
7719
- * order with remaining media their role fits (a video and its `first_frame`).
7720
- * Covers need a remote URL, so an inline image is never folded away.
7721
- */
7722
- function generationCovers(blocks) {
7723
- const covers = /* @__PURE__ */ new Map();
7724
- const isCover = (block) => block.type === "image" && blockUrl(block) !== void 0 && blockMeta(block)?.role !== "last_frame";
7725
- blocks.forEach((block, index) => {
7726
- const id = blockIdentity(block);
7727
- if (!isPlayable(block) || id === void 0) return;
7728
- const cover = blocks.findIndex((candidate) => isCover(candidate) && blockIdentity(candidate) === id);
7729
- if (cover >= 0) covers.set(index, cover);
7730
- });
7731
- const paired = new Set(covers.values());
7732
- const roleCovers = blocks.flatMap((block, index) => {
7733
- const role = blockMeta(block)?.role;
7734
- const types = typeof role === "string" ? COVER_ROLES.get(role) : void 0;
7735
- return isCover(block) && !paired.has(index) && types ? [{
7736
- index,
7737
- types
7738
- }] : [];
7739
- });
7740
- blocks.forEach((block, index) => {
7741
- if (!isPlayable(block) || covers.has(index)) return;
7742
- const at = roleCovers.findIndex(({ types }) => types.has(String(block.type)));
7743
- const [cover] = at < 0 ? [] : roleCovers.splice(at, 1);
7744
- if (cover) covers.set(index, cover.index);
7745
- });
7746
- return covers;
7747
- }
7748
- //#endregion
7749
9009
  //#region src/generation-task.ts
7750
9010
  const INLINE_KEYS = [
7751
9011
  "data",
@@ -7753,7 +9013,7 @@ const INLINE_KEYS = [
7753
9013
  "contentBase64"
7754
9014
  ];
7755
9015
  function isDeferred(block) {
7756
- return block.deferredBase64 === true || record(block.source)?.deferredBase64 === true;
9016
+ return block.deferredBase64 === true || asRecord(block.source)?.deferredBase64 === true;
7757
9017
  }
7758
9018
  const MEDIA_TYPE_PATTERN = /^(image|video|audio)\/[\w.+-]+$/;
7759
9019
  const DEFAULT_MEDIA_TYPES = {
@@ -7796,7 +9056,7 @@ function projectOutputs(run) {
7796
9056
  }
7797
9057
  function toGenerationTaskView(run) {
7798
9058
  const data = taskData(run);
7799
- const result = record(run.result);
9059
+ const result = asRecord(run.result);
7800
9060
  const model = data?.model ?? result?.model;
7801
9061
  return {
7802
9062
  id: run.id,
@@ -7819,15 +9079,15 @@ function isActiveGenerationTask(task) {
7819
9079
  }
7820
9080
  /** Playable source of an output: its URL, or a data URL for inline payloads. */
7821
9081
  function generationOutputSource(result, outputIndex) {
7822
- const block = contentBlocks(record(result)?.output)[outputIndex];
9082
+ const block = contentBlocks(asRecord(result)?.output)[outputIndex];
7823
9083
  if (!block) return null;
7824
9084
  const url = blockUrl(block);
7825
9085
  if (url) return url;
7826
- const source = record(block.source);
9086
+ const source = asRecord(block.source);
7827
9087
  const data = INLINE_KEYS.map((key) => source?.[key] ?? block[key]).find((value) => typeof value === "string" && value !== "");
7828
9088
  if (!data) return null;
7829
9089
  const declared = blockMimeType(block);
7830
9090
  return `data:${declared && MEDIA_TYPE_PATTERN.test(declared) ? declared : DEFAULT_MEDIA_TYPES[String(block.type)] ?? "application/octet-stream"};base64,${data}`;
7831
9091
  }
7832
9092
  //#endregion
7833
- 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 };
9093
+ 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 };