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