@openchambery/web 2.1.8 → 2.1.9-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/{AgentAvatar-CJQwq8UM.js → AgentAvatar-qLxaEaDh.js} +1 -1
  3. package/dist/assets/{AssistantView-B1Amo-xg.js → AssistantView-CeP8V0X-.js} +3 -3
  4. package/dist/assets/{DiagramView-Bdi3DPR0.js → DiagramView-DRCsjVx9.js} +1 -1
  5. package/dist/assets/{FadeInOnReveal-BNVExwoL.js → FadeInOnReveal-CX8Bkq-l.js} +1 -1
  6. package/dist/assets/{Icon-B4xKSuz3.js → Icon-BCFeHDWx.js} +1 -1
  7. package/dist/assets/{ImageLoadingPlaceholder-xB8WeKks.js → ImageLoadingPlaceholder-DEpMJXzV.js} +1 -1
  8. package/dist/assets/{LoadingImage-CoT3GWhg.js → LoadingImage-BAT9D9wN.js} +1 -1
  9. package/dist/assets/MarkdownRenderer-BvKaN2KX.js +2 -0
  10. package/dist/assets/{MarkdownRendererImpl-C-hRa7L9.js → MarkdownRendererImpl-D6iUJhtT.js} +1 -1
  11. package/dist/assets/{MarkstreamRendererImpl-B1yo0Ww3.js → MarkstreamRendererImpl-B14yVm7-.js} +1 -1
  12. package/dist/assets/{MobileDetailNavigation-BJApd-t2.js → MobileDetailNavigation-e4vO0lXT.js} +1 -1
  13. package/dist/assets/{MobileShareBridge-D2oEcsh-.js → MobileShareBridge-BnA02Mps.js} +2 -2
  14. package/dist/assets/MobileTabPageHeader-nApqJ-1X.js +2 -0
  15. package/dist/assets/{MultiRunWindow--GT06NT5.js → MultiRunWindow-C7Vgvpoz.js} +1 -1
  16. package/dist/assets/{OnboardingScreen-BiLurnX5.js → OnboardingScreen-CqcZEptL.js} +1 -1
  17. package/dist/assets/{OverlayScrollbar-DIsrS93m.js → OverlayScrollbar-BY5HaNrb.js} +1 -1
  18. package/dist/assets/{SettingsView-Ddd_29fB.js → SettingsView-Be7_mr8t.js} +2 -2
  19. package/dist/assets/{SettingsWindow-CFqtELhV.js → SettingsWindow-ZnWK6Itb.js} +1 -1
  20. package/dist/assets/{TerminalView-CKsMJh6u.js → TerminalView-BluEtXH8.js} +1 -1
  21. package/dist/assets/{ToolOutputDialog-Bhmh_bhc.js → ToolOutputDialog-6gouT_GS.js} +1 -1
  22. package/dist/assets/{appThemeRegistry-h6izawnS.js → appThemeRegistry-D59Upe3t.js} +1 -1
  23. package/dist/assets/{assistant-attachment-cache-Bzewqov4.js → assistant-attachment-cache-B2UAUHUZ.js} +1 -1
  24. package/dist/assets/{button-BupmKVUs.js → button-CWSs9Baz.js} +1 -1
  25. package/dist/assets/{collapsible-BAPWS6G-.js → collapsible-ngtoKADo.js} +1 -1
  26. package/dist/assets/{desktopHosts-BZ7iYE-l.js → desktopHosts-C0e7Nes2.js} +1 -1
  27. package/dist/assets/{desktopRelayRestore-S9wEiY2U.js → desktopRelayRestore-DckkHKsi.js} +1 -1
  28. package/dist/assets/{diagram-0a7vH45N.js → diagram-DmlTCkmV.js} +1 -1
  29. package/dist/assets/{dialog-BYLD4Cm3.js → dialog-BMa16qrs.js} +1 -1
  30. package/dist/assets/{dropdown-menu.styles-HOWSubFF.js → dropdown-menu.styles-BNwZmBRp.js} +1 -1
  31. package/dist/assets/{fileMentionTouchSelection-__ErGL_h.js → fileMentionTouchSelection-DbICTM0g.js} +1 -1
  32. package/dist/assets/{fileQueries-DZR-mK8h.js → fileQueries-DVpZC5ha.js} +1 -1
  33. package/dist/assets/{gitApi-mWZ8poVQ.js → gitApi-Bq2BWLAz.js} +1 -1
  34. package/dist/assets/{image-loading-CsxDGgek.js → image-loading-Bj11k4gM.js} +1 -1
  35. package/dist/assets/{input-DgrLdIys.js → input-CALwaZEB.js} +1 -1
  36. package/dist/assets/{insertionBoundaries-Dmazu0IZ.js → insertionBoundaries-n7dVxiuI.js} +1 -1
  37. package/dist/assets/{main-MSG_QVt6.js → main-BlZ-gMwg.js} +2 -2
  38. package/dist/assets/{main-CXGQzYXU.js → main-DRGmBqpD.js} +4 -4
  39. package/dist/assets/{markdownHeightCache-Dyctmwqv.js → markdownHeightCache-DXM_5jTe.js} +1 -1
  40. package/dist/assets/{mcpQueries-DTOutL7f.js → mcpQueries-BxICKLst.js} +1 -1
  41. package/dist/assets/miniChat-PxCQIMUm.js +2 -0
  42. package/dist/assets/mobile-Bs5d4s3_.js +2 -0
  43. package/dist/assets/{multirun-D3rg4G82.js → multirun-BxUSBUb_.js} +1 -1
  44. package/dist/assets/{persistence-B4Ni3Gmv.js → persistence-BKszKaoQ.js} +1 -1
  45. package/dist/assets/{platform-BQPsr8Zl.js → platform-CgwxwfA4.js} +1 -1
  46. package/dist/assets/{projectMeta-XygOZEJv.js → projectMeta-dZJ-hOil.js} +1 -1
  47. package/dist/assets/{relay-image-stream-qI2NTXbG.js → relay-image-stream-SV0lH6aG.js} +1 -1
  48. package/dist/assets/{renderElectronMiniChatApp-xHr7C4om.js → renderElectronMiniChatApp-CGWfxiDc.js} +2 -2
  49. package/dist/assets/{renderMobileApp-DBpUovak.js → renderMobileApp-DJmWaXgS.js} +4 -4
  50. package/dist/assets/runtime-auth-BpCQ-dPr.js +1 -0
  51. package/dist/assets/{runtime-fetch-QCWYlAx0.js → runtime-fetch-WgH0pROs.js} +1 -1
  52. package/dist/assets/{runtime-switch-3ad3yzM7.js → runtime-switch-CMiMO5Yj.js} +1 -1
  53. package/dist/assets/{runtimeConfig-BHsWHIKP.js → runtimeConfig-B-2tboUz.js} +1 -1
  54. package/dist/assets/{runtimeEndpointReset-DICqgh8i.js → runtimeEndpointReset-BbJRibgL.js} +2 -2
  55. package/dist/assets/{scheduledTaskNavigation-C1PrFEhV.js → scheduledTaskNavigation-DQcLqQ52.js} +1 -1
  56. package/dist/assets/{sessionChangeSummary-UOh6VO2F.js → sessionChangeSummary-DTeJLaNT.js} +1 -1
  57. package/dist/assets/{sessionLookup-4_garC6G.js → sessionLookup-_U9nCuBN.js} +1 -1
  58. package/dist/assets/{sortable-tabs-strip-DogWaIrG.js → sortable-tabs-strip-CscHgCaq.js} +1 -1
  59. package/dist/assets/{speechSettingsQueries-_VVWzacC.js → speechSettingsQueries-CdlOc_sI.js} +2 -2
  60. package/dist/assets/{terminalApi-kRMttlcP.js → terminalApi-rxsMWL0o.js} +1 -1
  61. package/dist/assets/{textarea-Boj9axpc.js → textarea-ClV5sMS-.js} +1 -1
  62. package/dist/assets/{tooltip-Dbgnq-Mh.js → tooltip-CV6dvLBp.js} +1 -1
  63. package/dist/assets/{url-Bd61gNgl.js → url-nBOUbtV_.js} +1 -1
  64. package/dist/assets/{useAppFontEffects-CqNs6RYP.js → useAppFontEffects-YhyFj8z3.js} +5 -5
  65. package/dist/assets/{useAssistantUIStore-Dlvb7P0q.js → useAssistantUIStore-OyPm_4M_.js} +1 -1
  66. package/dist/assets/{useDesktopWindowControlsLayout-CxKspVV6.js → useDesktopWindowControlsLayout-jdIXqoD8.js} +1 -1
  67. package/dist/assets/{useEffectiveDirectory-mVpC7Bv1.js → useEffectiveDirectory-COgvBW0Q.js} +1 -1
  68. package/dist/assets/{useFontPreferences-lAw0Cbza.js → useFontPreferences-DqEIKMEW.js} +1 -1
  69. package/dist/assets/{useMobileNavigationStore-CrxO8Htz.js → useMobileNavigationStore-CuOiHjP1.js} +11 -11
  70. package/dist/assets/{useUIStore-CDMI_Yzu.js → useUIStore-CEqU9Ujv.js} +1 -1
  71. package/dist/assets/{useWorkerHighlightedLines-C-0XbkLL.js → useWorkerHighlightedLines-DGH-O6ZJ.js} +1 -1
  72. package/dist/index.html +11 -11
  73. package/dist/mini-chat.html +11 -11
  74. package/dist/mobile.html +11 -11
  75. package/package.json +1 -1
  76. package/server/index.js +0 -13
  77. package/server/lib/git/DOCUMENTATION.md +1 -1
  78. package/server/lib/git/branches.test.js +39 -0
  79. package/server/lib/git/service.js +3 -43
  80. package/server/lib/opencode/DOCUMENTATION.md +1 -1
  81. package/server/lib/opencode/core-routes.js +2 -14
  82. package/server/lib/opencode/core-routes.test.js +6 -10
  83. package/server/lib/relay/DOCUMENTATION.md +22 -2
  84. package/server/lib/relay/relay-server.e2e.test.ts +35 -0
  85. package/server/lib/relay/tunnel-host.js +14 -4
  86. package/server/lib/relay/tunnel-host.test.js +37 -1
  87. package/dist/assets/MarkdownRenderer-BpZwuZgb.js +0 -2
  88. package/dist/assets/MobileTabPageHeader-DKnjmg1G.js +0 -2
  89. package/dist/assets/miniChat-zpQVvq46.js +0 -2
  90. package/dist/assets/mobile-EGZI5jqV.js +0 -2
  91. package/dist/assets/runtime-auth-C8w0nlKy.js +0 -1
@@ -414,7 +414,7 @@ When adding or changing Host HTTP APIs that mobile/desktop clients reach over Pr
414
414
  - `GET /api/passkeys`
415
415
  - `DELETE /api/passkeys/:id`
416
416
  - `POST /api/auth/reset`
417
- - `POST /api/client-auth/pairing/sessions`: accepts an optional `relayUrl` when Relay is included. An owner UI session or the local `desktop-local` shell client may set a custom endpoint (other client bearers receive HTTP 403). Canonical form is `ws://`/`wss://` scheme/host/path: userinfo is rejected (HTTP 400); query and fragment are stripped and are not part of endpoint identity. The Host persists the switch and reconnects its control connection before returning the pairing-v2 Relay candidate; an `OPENCHAMBER_RELAY_URL` override stays authoritative.
417
+ - `POST /api/client-auth/pairing/sessions`: accepts an optional `relayUrl` when Relay is included. An owner UI session, local `desktop-local` shell, or paired `desktop` operator may select a custom endpoint, including through Relay. Other client kinds cannot create pairing sessions. Canonical form is `ws://`/`wss://` scheme/host/path: userinfo is rejected (HTTP 400); query and fragment are stripped and are not part of endpoint identity. The Host adds a new endpoint without replacing existing endpoints before returning the pairing-v2 Relay candidate; an `OPENCHAMBER_RELAY_URL` override stays authoritative. The remote-host dialog permits custom endpoint editing unless that override locks it; remote selections are not saved as local-host preferences.
418
418
  - `GET /api/client-auth/pairing/transports`: returns direct transport availability. `relayAvailable` is true for desktop and SSH-managed remote hosts (`OPENCHAMBER_RUNTIME=desktop|ssh-remote`); local `dev` / `web` / ordinary CLI servers report false and omit Relay URL fields so the create-device dialog never defaults to Anywhere or probes the relay.
419
419
  - `GET /connect`
420
420
  - `POST /api/system/probe-url`
@@ -395,7 +395,6 @@ export const registerAuthAndAccessRoutes = (app, dependencies) => {
395
395
  // [] when relay status is unavailable. Gates remote-bearer pairing requests
396
396
  // that carry a relayUrl: paired desktops may only use an endpoint the host
397
397
  // already has (echo of any configured endpoint, never a re-point).
398
- getEffectiveRelayUrls = async () => [],
399
398
  // Candidates for EVERY configured relay endpoint (multi-relay), or null /
400
399
  // a non-array when only the single-endpoint accessor is available. Feeds
401
400
  // the connection-candidates refresh so paired devices learn the full
@@ -897,19 +896,8 @@ export const registerAuthAndAccessRoutes = (app, dependencies) => {
897
896
  if (req.body?.relayUrl !== undefined && !requestedRelayUrl) {
898
897
  return res.status(400).json({ error: 'Relay URL must use ws:// or wss://' });
899
898
  }
900
- // Changing the Host Relay endpoint set is an owner-UI or local
901
- // desktop-shell action. Paired desktop operators reach this handler too,
902
- // but may only name an endpoint the host already uses — anything else
903
- // would re-point (or extend) the Host relay from a remote bearer.
904
- if (requestedRelayUrl !== undefined && authContext?.type === 'client') {
905
- const actingClient = authContext.client || await clientRecordFromAuthContext(authContext);
906
- if (actingClient?.clientKind !== 'desktop-local') {
907
- const effectiveRelayUrls = await getEffectiveRelayUrls();
908
- if (!Array.isArray(effectiveRelayUrls) || !effectiveRelayUrls.includes(requestedRelayUrl)) {
909
- return res.status(403).json({ error: 'Only the owner UI session can set a custom Relay endpoint' });
910
- }
911
- }
912
- }
899
+ // The create-auth gate grants paired desktop operators the same pairing
900
+ // endpoint selection as the owner, including adding a new Relay endpoint.
913
901
  const candidates = await pairingServerCandidates(req, {
914
902
  preferredServerUrl: req.body?.serverUrl,
915
903
  includeRelay: typeof req.body?.includeRelay === 'boolean' ? req.body.includeRelay : undefined,
@@ -990,12 +990,10 @@ describe('core-routes', () => {
990
990
  expect(dependencies.clientPairingRuntime.createPairingSession).not.toHaveBeenCalled();
991
991
  });
992
992
 
993
- it('forbids paired desktop clients from re-pointing the Relay endpoint when pairing', async () => {
994
- const getRelayPairingCandidate = vi.fn();
995
- const getEffectiveRelayUrls = vi.fn(async () => ['wss://relay.example/ws']);
993
+ it('lets paired desktop clients select a new Relay endpoint when pairing', async () => {
994
+ const getRelayPairingCandidate = vi.fn(async ({ relayUrl }) => ({ type: 'relay', relayUrl }));
996
995
  const { app, dependencies } = createPairingRouteApp({
997
996
  getRelayPairingCandidate,
998
- getEffectiveRelayUrls,
999
997
  uiAuthController: {
1000
998
  resolveAuthContext: vi.fn(async () => ({
1001
999
  type: 'client',
@@ -1020,11 +1018,11 @@ describe('core-routes', () => {
1020
1018
 
1021
1019
  await request(app)
1022
1020
  .post('/api/client-auth/pairing/sessions')
1023
- .send({ includeRelay: true, relayUrl: 'wss://attacker.example/ws' })
1024
- .expect(403, { error: 'Only the owner UI session can set a custom Relay endpoint' });
1021
+ .send({ includeRelay: true, relayUrl: 'wss://custom-relay.example/ws' })
1022
+ .expect(201);
1025
1023
 
1026
- expect(getRelayPairingCandidate).not.toHaveBeenCalled();
1027
- expect(dependencies.clientPairingRuntime.createPairingSession).not.toHaveBeenCalled();
1024
+ expect(getRelayPairingCandidate).toHaveBeenCalledWith({ ensureEnabled: true, relayUrl: 'wss://custom-relay.example/ws' });
1025
+ expect(dependencies.clientPairingRuntime.createPairingSession).toHaveBeenCalledWith(expect.objectContaining({ usesRelay: true, createdByClientId: 'remote-desktop-1' }));
1028
1026
  });
1029
1027
 
1030
1028
  it('lets paired desktop clients echo the current Relay endpoint when pairing', async () => {
@@ -1035,10 +1033,8 @@ describe('core-routes', () => {
1035
1033
  hostEncPubJwk: { kty: 'EC', crv: 'P-256', x: 'aaa', y: 'bbb' },
1036
1034
  priority: 30,
1037
1035
  }));
1038
- const getEffectiveRelayUrls = vi.fn(async () => ['wss://relay.example/ws', 'wss://relay2.example/ws']);
1039
1036
  const { app } = createPairingRouteApp({
1040
1037
  getRelayPairingCandidate,
1041
- getEffectiveRelayUrls,
1042
1038
  uiAuthController: {
1043
1039
  resolveAuthContext: vi.fn(async () => ({
1044
1040
  type: 'client',
@@ -19,7 +19,7 @@ Traffic is modeled as three stacked layers. The relay understands only Layer 1;
19
19
  ## Entrypoints and structure
20
20
 
21
21
  Host side (`packages/web/server/lib/relay/`):
22
- - `service.js` — thin entrypoint: relay config (enabled flag + endpoints), the management routes (`GET/POST /api/openchamber/relay/{status,enable,disable}`, `POST /api/openchamber/relay/endpoints`), a `getPairingCandidate()` accessor (the relay transport candidate folded into pairing-v2 links when enabled, consumed by the pairing-session route in `core-routes.js`), and lifecycle wiring. **Multi-relay:** `settings.privateRelay = { enabled, relayUrl, extraRelayUrls }` — the host dials EVERY configured endpoint simultaneously with one identity (same serverId); clients treat `serverId + relayUrl` as the instance identity, so the same machine exposed via two domains appears as two separately named instances. The primary (`relayUrl`) owns push registration and stays the default pairing candidate. A pairing link created for an endpoint the host does not have yet APPENDS it as an extra endpoint (owner request only) instead of replacing the primary, so devices paired over the old domain keep working. `POST /relay/endpoints` (owner session / `desktop-local` only) replaces the extra-endpoint list wholesale and starts/stops host connections incrementally. Paired desktops may pair over any CONFIGURED endpoint but can never add one. Host lifecycle is gated by `isRelayHostRuntime()` (`desktop` or `ssh-remote`): other runtimes (`bun run dev`, `dev:web:hmr`, ordinary CLI `serve`, VS Code, plain `node server`) report `state: 'unavailable'`, refuse `/relay/enable`, `/relay/disable`, `/relay/endpoints`, and relay pairing with 403, never mint a relay identity, never advertise `relayAvailable`, and never call `startRelayHost`. Direct `node server/index.js` forces `OPENCHAMBER_RUNTIME=web` unless it is already `ssh-remote`, so a leftover desktop env cannot open the host-control socket. On host runtimes, started from `packages/web/server/index.js` when demand/opt-in enables the relay. Desktop auto-clears `privateRelay.enabled` when demand drops; `ssh-remote` keeps an explicit opt-in (e.g. `openchamber serve --relay-host`) sticky until disabled. The relay endpoint defaults to the OpenChamber-hosted relay but can be pinned to a self-hosted relay via the `OPENCHAMBER_RELAY_URL` env var; when set it overrides the stored setting for the host connection, the pairing candidate, and status, so paired clients inherit the endpoint automatically. Endpoint identity is scheme/host/path only: accepted schemes are `ws://` and `wss://`; URLs with userinfo are rejected (no silent default fallback); query and fragment are stripped before persistence and pairing candidates. Custom endpoint persistence and Host control-connection switching are explicit authenticated management actions. Pairing creation with `relayUrl` requires an owner UI session or the local `desktop-local` shell client; `/relay/enable` follows its API auth gate. `derivePushSendUrlFromRelayUrl` maps a canonical Relay `wss://`/`ws://` URL to the same host's `https://`/`http://` `/v1/push/send` path (register is `/v1/push/register-token`); `OPENCHAMBER_PUSH_RELAY_URL` on the Host overrides that derivation. The isolated Push process lives in `packages/relay-server/src/push/` and does not share Layer 1 credentials or tunnel state.
22
+ - `service.js` — thin entrypoint: relay config (enabled flag + endpoints), the management routes (`GET/POST /api/openchamber/relay/{status,enable,disable}`, `POST /api/openchamber/relay/endpoints`), a `getPairingCandidate()` accessor (the relay transport candidate folded into pairing-v2 links when enabled, consumed by the pairing-session route in `core-routes.js`), and lifecycle wiring. **Multi-relay:** `settings.privateRelay = { enabled, relayUrl, extraRelayUrls }` — the host dials EVERY configured endpoint simultaneously with one identity (same serverId); clients treat `serverId + relayUrl` as the instance identity, so the same machine exposed via two domains appears as two separately named instances. The primary (`relayUrl`) owns push registration and stays the default pairing candidate. A pairing link created for an endpoint the host does not have yet APPENDS it as an extra endpoint instead of replacing the primary, so devices paired over the old domain keep working. `POST /relay/endpoints` (owner session / `desktop-local` only) replaces the extra-endpoint list wholesale and starts/stops host connections incrementally. Paired desktop operators may also add an endpoint through pairing creation. Host lifecycle is gated by `isRelayHostRuntime()` (`desktop` or `ssh-remote`): other runtimes (`bun run dev`, `dev:web:hmr`, ordinary CLI `serve`, VS Code, plain `node server`) report `state: 'unavailable'`, refuse `/relay/enable`, `/relay/disable`, `/relay/endpoints`, and relay pairing with 403, never mint a relay identity, never advertise `relayAvailable`, and never call `startRelayHost`. Direct `node server/index.js` forces `OPENCHAMBER_RUNTIME=web` unless it is already `ssh-remote`, so a leftover desktop env cannot open the host-control socket. On host runtimes, started from `packages/web/server/index.js` when demand/opt-in enables the relay. Desktop auto-clears `privateRelay.enabled` when demand drops; `ssh-remote` keeps an explicit opt-in (e.g. `openchamber serve --relay-host`) sticky until disabled. The relay endpoint defaults to the OpenChamber-hosted relay but can be pinned to a self-hosted relay via the `OPENCHAMBER_RELAY_URL` env var; when set it overrides the stored setting for the host connection, the pairing candidate, and status, so paired clients inherit the endpoint automatically. Endpoint identity is scheme/host/path only: accepted schemes are `ws://` and `wss://`; URLs with userinfo are rejected (no silent default fallback); query and fragment are stripped before persistence and pairing candidates. Custom endpoint persistence and Host control-connection switching are explicit authenticated management actions. Pairing creation with `relayUrl` requires an owner UI session, local `desktop-local` shell client, or paired `desktop` operator; `/relay/enable` follows its API auth gate. `derivePushSendUrlFromRelayUrl` maps a canonical Relay `wss://`/`ws://` URL to the same host's `https://`/`http://` `/v1/push/send` path (register is `/v1/push/register-token`); `OPENCHAMBER_PUSH_RELAY_URL` on the Host overrides that derivation. The isolated Push process lives in `packages/relay-server/src/push/` and does not share Layer 1 credentials or tunnel state.
23
23
  - `identity.js` — the host's stable identity: the long-lived signing keypair (shared with the push relay, defines the routing id) plus a long-lived encryption keypair (the E2EE trust anchor). Reused across restarts; never rotated implicitly.
24
24
  - `signing-key.js` — storage/derivation of the signing keypair and the routing id, shared with the notifications runtime. Canonical public JWK hashing matches `packages/relay-server/src/push/crypto.js`.
25
25
  - `host-client.js` — the long-lived connection manager: one outbound control connection to the relay, a per-client data connection for each connected device, reconnect/backoff, and the E2EE responder handshake per connection. Control reconnect backoff does not reset on WebSocket `open`; `consecutiveFailures` clears only after the control socket stays up for `controlStableMs` (15s default), so an open-then-drop loop backs off to 30s instead of retrying every 1s.
@@ -61,7 +61,7 @@ See also `packages/web/server/lib/opencode/DOCUMENTATION.md` (“Catalog / OpenC
61
61
  ## Authentication model
62
62
 
63
63
  - The tunnel is **transport only**. The OpenChamber server still authenticates every tunneled request exactly as it authenticates a direct remote client. The relay path grants reachability, not authorization.
64
- - A paired **desktop** client is a device-fleet operator: through the tunnel it can create pairing links, list every paired device, and revoke other devices (parity with the SSH attach flow, where the connecting desktop authenticates as the operator). Mobile (and any other) paired clients stay scoped to their own record. Re-pointing the Host relay endpoint set from a remote bearer remains forbidden — a paired desktop may name any endpoint the host already has, but adding/removing endpoints (including `relayUrl` in pairing requests for a new domain) is an owner-UI / local-desktop-shell action.
64
+ - A paired **desktop** client is a device-fleet operator: through the tunnel it can create pairing links using existing or custom Relay endpoints, list every paired device, and revoke other devices (parity with the SSH attach flow). Pairing with a new endpoint appends it without replacing existing endpoints. Wholesale endpoint-list replacement/removal remains an owner-UI / local-desktop-shell action. Mobile (and any other) paired clients stay scoped to their own record.
65
65
  - Clients carry their normal credential. HTTP and SSE requests authenticate with the client's bearer token (a header). **WebSocket upgrades cannot send headers**, so they authenticate with a short-lived URL-scoped token minted beforehand and passed as a query parameter. This asymmetry is important when adding new WebSocket features (see the skill).
66
66
  - Desktop switch preflight calls `/auth/session` with the saved bearer over a pinned tunnel before adopting it. `/health` and cached reachability badges provide transport information only. Failed or timed-out probes close their temporary tunnel and preserve the active runtime; successful switch probes transfer ownership of the authenticated tunnel to the runtime.
67
67
  - The host authenticates itself to the relay with a signed handshake using its long-lived signing key.
@@ -75,6 +75,26 @@ See also `packages/web/server/lib/opencode/DOCUMENTATION.md` (“Catalog / OpenC
75
75
  4. **Handshake.** Over that connection pair, client and host run the E2EE handshake and derive a shared encrypted channel the relay cannot read.
76
76
  5. **Traffic.** All normal app traffic is multiplexed and encrypted through that channel. On the host, decrypted requests are dispatched to the local server over loopback; responses stream back encrypted. Reconnects re-establish a fresh channel and the app's existing retry machinery recovers.
77
77
 
78
+ ## Negotiated JSON response compression
79
+
80
+ Each HTTP request may advertise `acceptEncoding: 'gzip'` in its tunnel JSON head
81
+ when the client supports `DecompressionStream`. The Host may then set
82
+ `encoding: 'gzip'` on the response head and stream gzip bytes as ordinary HttpBody
83
+ frames **before encryption**. The client decompresses before exposing its Fetch
84
+ Response; HTTP headers describe the decoded body, with no compressed content-length
85
+ or content-encoding leaked to callers. Wire framing, batching and crypto counters
86
+ are unchanged. Old Hosts ignore the opt-in; old clients (including native Android
87
+ notifications) omit it and receive uncompressed bodies.
88
+
89
+ Only application/json and application/*+json bodies qualify. Known decoded bodies
90
+ under 1 KiB, HEAD/no-body responses, SSE and binary assets are not compressed.
91
+ Unknown-length JSON is streamed rather than buffered in full; loopback fetch's
92
+ automatic decompression is accounted for before checking length. Cancellation and
93
+ errors propagate through the stream pipeline. Host tests cover negotiation and
94
+ legacy/SSE/binary exclusions; client tests cover malformed gzip and cancellation;
95
+ the authenticated compiled-Relay integration test checks decoded snapshot fidelity
96
+ and encrypted wire-byte reduction on 4,000 session summaries.
97
+
78
98
  ## Candidate refresh (staying off the relay when direct works)
79
99
 
80
100
  Pairing-payload transport candidates are a snapshot: when DHCP hands the host
@@ -36,6 +36,9 @@ import { requestSpeechSession } from '../../../../ui/src/lib/voice/direct-sessio
36
36
 
37
37
  const timeoutMs = 10_000;
38
38
  const previewPng = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aD1sAAAAASUVORK5CYII=', 'base64');
39
+ const initializationSnapshot = { sessions: Array.from({ length: 4000 }, (_, id) => ({
40
+ id: `session-${id}`, title: `Session ${id}`, directory: '/repo/project', time: { created: 1000, updated: 2000 },
41
+ })) };
39
42
  const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
40
43
 
41
44
  const eventually = async (predicate: () => boolean | Promise<boolean>, message: string, timeout = timeoutMs) => {
@@ -215,6 +218,10 @@ const startOrigin = async () => {
215
218
  });
216
219
  return;
217
220
  }
221
+ if (pathname === '/api/e2e/initialization' && req.method === 'GET') {
222
+ await uiAuth.requireAuth(req, res, () => res.json(initializationSnapshot));
223
+ return;
224
+ }
218
225
  if (pathname === '/api/agent' && req.method === 'GET') {
219
226
  await uiAuth.requireAuth(req, res, () => {
220
227
  const directory = new URL(req.url ?? '/', 'http://origin').searchParams.get('location[directory]');
@@ -401,10 +408,32 @@ describe('private relay compiled-process boundary', () => {
401
408
  origin = await startOrigin();
402
409
  const identity = await buildIdentity();
403
410
  host = startRelayHost({ relayUrl: relay.url, identity, getLocalPort: () => origin.port, onStatus: () => {}, logger: { warn: () => {}, info: () => {} } });
411
+ let receivedBytes = 0;
404
412
  client = createRelayTunnelClient({
405
413
  relayUrl: relay.url, serverId: identity.serverId, hostEncPubJwk: identity.hostEncPubJwk,
406
414
  helloRetryMs: 50, helloTimeoutMs: 2_000, pingIntervalMs: 500, pingTimeoutMs: 250,
407
415
  reconnectBaseDelayMs: 50, reconnectMaxDelayMs: 250, batchWindowMs: 5,
416
+ createWireSocket: (url) => {
417
+ const socket = new globalThis.WebSocket(url);
418
+ socket.binaryType = 'arraybuffer';
419
+ const wire = {
420
+ get readyState() { return socket.readyState; },
421
+ send: (data: string | ArrayBuffer | Uint8Array) => socket.send(data as string | ArrayBuffer),
422
+ close: (code?: number, reason?: string) => socket.close(code, reason),
423
+ onopen: null as (() => void) | null,
424
+ onmessage: null as ((event: { data: unknown }) => void) | null,
425
+ onclose: null as ((event: { code: number; reason: string }) => void) | null,
426
+ onerror: null as (() => void) | null,
427
+ };
428
+ socket.onopen = () => wire.onopen?.();
429
+ socket.onmessage = (event) => {
430
+ receivedBytes += typeof event.data === 'string' ? Buffer.byteLength(event.data) : event.data.byteLength;
431
+ wire.onmessage?.({ data: event.data });
432
+ };
433
+ socket.onclose = (event) => wire.onclose?.({ code: event.code, reason: event.reason });
434
+ socket.onerror = () => wire.onerror?.();
435
+ return wire;
436
+ },
408
437
  });
409
438
 
410
439
  await eventually(() => host!.getStatus().state === 'connected' && client!.getStatus().state === 'connected', 'host and TypeScript client did not connect');
@@ -415,6 +444,12 @@ describe('private relay compiled-process boundary', () => {
415
444
  expect(httpResponse.status).toBe(200);
416
445
  expect(await httpResponse.json()).toEqual({ body: 'stream-body', hasRelayConnection: true });
417
446
 
447
+ const beforeSnapshot = receivedBytes;
448
+ const compressed = await client.fetch('/api/e2e/initialization', { headers: { authorization: 'Bearer client-token' } });
449
+ expect(compressed.status).toBe(200);
450
+ expect(await compressed.json()).toEqual(initializationSnapshot);
451
+ expect(receivedBytes - beforeSnapshot).toBeLessThan(Buffer.byteLength(JSON.stringify(initializationSnapshot)) / 4);
452
+
418
453
  const sseResponse = await client.fetch('/api/e2e/sse', { headers: { authorization: 'Bearer client-token' } });
419
454
  const reader = sseResponse.body!.getReader();
420
455
  const chunks: Uint8Array[] = [];
@@ -258,11 +258,21 @@ export const createTunnelHost = ({
258
258
  if (STRIPPED_RESPONSE_HEADERS.has(name)) continue;
259
259
  responseHeaders[name] = value;
260
260
  }
261
- await sendJson(TunnelFrameType.HttpResponse, streamId, { status: response.status, headers: responseHeaders });
262
-
263
261
  try {
264
- if (response.body) {
265
- for await (const chunk of response.body) {
262
+ const contentType = (response.headers.get('content-type') ?? '').split(';', 1)[0].trim().toLowerCase();
263
+ const contentLength = response.headers.has('content-encoding') ? null : response.headers.get('content-length');
264
+ // Compress finite JSON responses before encryption, never SSE or binary assets.
265
+ // Old clients omit acceptEncoding and old hosts ignore it, preserving v1 framing.
266
+ const gzip = request.acceptEncoding === 'gzip' && typeof CompressionStream === 'function'
267
+ && method !== 'HEAD' && response.body !== null
268
+ && (contentType === 'application/json' || /^application\/[^/]+\+json$/.test(contentType))
269
+ && (contentLength === null || Number(contentLength) >= 1024);
270
+ const body = gzip ? response.body.pipeThrough(new CompressionStream('gzip')) : response.body;
271
+ await sendJson(TunnelFrameType.HttpResponse, streamId, {
272
+ status: response.status, headers: responseHeaders, ...(gzip ? { encoding: 'gzip' } : {}),
273
+ });
274
+ if (body) {
275
+ for await (const chunk of body) {
266
276
  if (closed || stream.abort.signal.aborted) return;
267
277
  const bytes = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
268
278
  for (const piece of chunkPayload(bytes, MAX_TUNNEL_PAYLOAD_BYTES)) {
@@ -7,7 +7,7 @@
7
7
  // corrupted payload. This test must stay red until send serialization is fixed.
8
8
  // Production send path is intentionally not changed here.
9
9
 
10
- import { describe, expect, it } from 'bun:test';
10
+ import { describe, expect, it, vi } from 'vitest';
11
11
 
12
12
  import {
13
13
  MAX_TUNNEL_PAYLOAD_BYTES,
@@ -68,6 +68,42 @@ const waitFor = async (predicate, { timeoutMs = 2000, intervalMs = 5 } = {}) =>
68
68
  throw new Error('waitFor timed out');
69
69
  };
70
70
 
71
+ describe('negotiated JSON response compression', () => {
72
+ it.each([
73
+ { acceptEncoding: 'gzip', contentType: 'application/json', compressed: true },
74
+ { acceptEncoding: 'gzip', contentType: 'application/problem+json', compressed: true },
75
+ { acceptEncoding: undefined, contentType: 'application/json', compressed: false },
76
+ { acceptEncoding: 'gzip', contentType: 'text/event-stream', compressed: false },
77
+ { acceptEncoding: 'gzip', contentType: 'image/png', compressed: false },
78
+ { acceptEncoding: 'gzip', contentType: 'application/json', small: true, compressed: false },
79
+ ])('preserves body and legacy/SSE behavior for $contentType / $acceptEncoding / small=$small', async ({ acceptEncoding, contentType, small, compressed }) => {
80
+ const text = JSON.stringify({ rows: Array.from({ length: small ? 1 : 4000 }, (_, id) => ({ id, title: 'Session summary', directory: '/repo/project' })) });
81
+ const raw = textEncoder.encode(text);
82
+ const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(raw, {
83
+ headers: { 'content-type': contentType, 'content-length': String(raw.byteLength) },
84
+ }));
85
+ const frames = [];
86
+ const host = createTunnelHost({ connectionId: 'compression-test', getLocalPort: () => 9,
87
+ sendFrame: (frame) => { frames.push(decodeTunnelFrame(frame)); }, getBufferedAmount: () => 0 });
88
+ try {
89
+ await host.handleFrame(encodeTunnelFrame(TunnelFrameType.HttpRequest, 1, encodeJsonPayload({
90
+ method: 'GET', path: '/api/test', query: '', headers: {}, ...(acceptEncoding ? { acceptEncoding } : {}),
91
+ })));
92
+ await waitFor(() => frames.some((frame) => frame.frameType === TunnelFrameType.StreamEnd));
93
+ const head = JSON.parse(textDecoder.decode(frames.find((frame) => frame.frameType === TunnelFrameType.HttpResponse).payload));
94
+ expect(head.encoding).toBe(compressed ? 'gzip' : undefined);
95
+ expect(head.headers['content-length']).toBeUndefined();
96
+ const body = new Blob(frames.filter((frame) => frame.frameType === TunnelFrameType.HttpBody).map((frame) => frame.payload));
97
+ if (compressed) {
98
+ expect(body.size).toBeLessThan(raw.byteLength / 4);
99
+ expect(await new Response(body.stream().pipeThrough(new DecompressionStream('gzip'))).text()).toBe(text);
100
+ } else {
101
+ expect(await body.text()).toBe(text);
102
+ }
103
+ } finally { host.close(); fetchMock.mockRestore(); }
104
+ });
105
+ });
106
+
71
107
  describe('isTunnelWsPathAllowed', () => {
72
108
  it('allows exact terminal/dictation/event sockets and preview proxy prefix', () => {
73
109
  expect(isTunnelWsPathAllowed('/api/terminal/ws')).toBe(true);
@@ -1,2 +0,0 @@
1
- const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/MarkstreamRendererImpl-B1yo0Ww3.js","assets/rolldown-runtime-hePW80VL.js","assets/vendor-base-ui-CdzJ6My_.js","assets/useUIStore-CDMI_Yzu.js","assets/runtime-switch-3ad3yzM7.js","assets/runtime-auth-C8w0nlKy.js","assets/vendor-zustand-o69pgTEK.js","assets/vendor-react-BP2cRVqX.js","assets/vendor-markstream-react-Bm0cO70Z.js","assets/vendor-aparajita-capacitor-secure-storage-xol_6BsB.js","assets/vendor-markstream-core-Cm34iPLL.js","assets/vendor-floating-ui-dom-BHhLtF5l.js","assets/vendor-floating-ui-core-Co1I1lFV.js","assets/vendor-reactuses-core-BpfIZ1tg.js","assets/vendor-lodash-es-DfLbG5Af.js","assets/vendor-js-cookie-hSOiFTtX.js","assets/button-BupmKVUs.js","assets/persistence-B4Ni3Gmv.js","assets/runtime-fetch-QCWYlAx0.js","assets/vendor-pathe-vBmvQE8f.js","assets/vendor-tanstack-query-core-BWz0ose6.js","assets/store-CG7Ytsg9.js","assets/vendor-class-variance-authority-BLUaYARZ.js","assets/vendor-tailwind-merge-BicNWLTg.js","assets/useMobileNavigationStore-CrxO8Htz.js","assets/platform-BQPsr8Zl.js","assets/vendor-opencode-client-MF2GBrFx.js","assets/vendor-tanstack-react-query-CdqCAt40.js","assets/vendor-sonner-ZS8XIOVl.js","assets/useAssistantUIStore-Dlvb7P0q.js","assets/vendor-zod-Dzk5kdUU.js","assets/vendor-fuse.js-DB5wDJbI.js","assets/useThemeSystem-8jhE4Zyk.js","assets/tooltip-Dbgnq-Mh.js","assets/image-loading-CsxDGgek.js","assets/fileQueries-DZR-mK8h.js","assets/useRuntimeAPIs-Cx8S1HLz.js","assets/relay-image-stream-qI2NTXbG.js","assets/image-loading-CObM4tcX.css","assets/url-Bd61gNgl.js","assets/useEffectiveDirectory-mVpC7Bv1.js","assets/LoadingImage-CoT3GWhg.js","assets/ImageLoadingPlaceholder-xB8WeKks.js","assets/markdownHeightCache-Dyctmwqv.js","assets/dualLimitLru-D8Sphgua.js","assets/MarkdownRendererImpl-C-hRa7L9.js","assets/vendor-katex-DNVSTX6g.js","assets/markdown-worker-DFNZ3KJ_.js","assets/FadeInOnReveal-BNVExwoL.js","assets/markdownTheme-CV7_0QAL.js","assets/vendor-pierre-diffs-DPBtNDJ5.js","assets/vendor-hast-util-to-html-Bw_SgZ1t.js","assets/vendor-character-entities-legacy-Wut6vMuj.js","assets/vendor-character-entities-html4-DlN_yAFd.js","assets/vendor-ccount-CyGsA0u7.js","assets/vendor-comma-separated-tokens-B9ouxNNS.js","assets/vendor-oniguruma-to-es-CrHzjjSU.js","assets/vendor-oniguruma-parser-D2DIvhSP.js","assets/vendor-diff-Dq7CZNim.js","assets/vendor-lru_map-BMOrLc9L.js","assets/vendor-morphdom-BbKiNDAu.js","assets/vendor-dompurify-CeJNOzik.js","assets/vendor-pele-C1WTNWUm.js","assets/vendor-marked-WVekxjAN.js","assets/vendor-remend-C3cXc-tf.js","assets/MarkstreamRendererImpl-DKhVy3nF.css"])))=>i.map(i=>d[i]);
2
- import{r as e}from"./rolldown-runtime-hePW80VL.js";import{a as t}from"./vendor-zustand-o69pgTEK.js";import{it as n,rt as r}from"./vendor-base-ui-CdzJ6My_.js";import{n as i}from"./vendor-react-BP2cRVqX.js";import{o as a}from"./vendor-aparajita-capacitor-secure-storage-xol_6BsB.js";import{r as o}from"./button-BupmKVUs.js";import{i as s,n as c,t as l}from"./markdownHeightCache-Dyctmwqv.js";var u=i(),d=e(n(),1),f=`openchamber:chunk-import-reload`,p=250,m=3e4,h=[/Importing a module script failed/i,/Failed to fetch dynamically imported module/i,/error loading dynamically imported module/i,/Loading chunk \S+ failed/i,/ChunkLoadError/i];function g(e){if(e instanceof Error)return`${e.name}\n${e.message}\n${e.stack??``}`;if(typeof e==`string`)return e;try{return JSON.stringify(e)}catch{return String(e)}}function _(e){let t=g(e);return h.some(e=>e.test(t))}function v(e){return`2.1.8:${g(e).slice(0,500)}`}function y(e){if(typeof window>`u`)return;let t=Date.now(),n=v(e),r=null;try{let e=window.sessionStorage.getItem(f);if(e)try{r=JSON.parse(e)}catch{r=null}}catch{return}let i=typeof r?.timestamp==`number`?r.timestamp:0;if(!(r?.signature===n&&t-i<m)){try{window.sessionStorage.setItem(f,JSON.stringify({signature:n,timestamp:t}))}catch{return}console.error(`[refresh-debug] chunk-import-reload`,{error:g(e).slice(0,1e3)}),window.setTimeout(()=>{window.location.reload()},0)}}async function b(e,t={}){let n=t.retries??1,r;for(let t=0;t<=n;t+=1)try{return await e()}catch(e){if(r=e,!_(e)||t>=n)break;await new Promise(e=>setTimeout(e,p*(t+1)))}throw _(r)&&y(r),r}var x=e=>(0,d.lazy)(()=>b(e)),S=(e,t)=>{try{let n=t??(typeof localStorage>`u`?null:localStorage);return n?n.getItem(e)===`1`:!1}catch{return!1}},C=(e,t)=>{try{let n=t??(typeof localStorage>`u`?null:localStorage);return!n||n.getItem(e)!==`0`}catch{return!0}},w=(e,t)=>{try{typeof localStorage<`u`&&localStorage.setItem(e,t?`1`:`0`)}catch{}},T=`oc:legend-timeline`,E=e=>S(T,e),D=`oc:markstream-react`,O=e=>C(D,e),k=t(e=>({legendTimelineEnabled:E(),setLegendTimelineEnabled:t=>{w(T,t),e({legendTimelineEnabled:t})},markstreamReactEnabled:O(),setMarkstreamReactEnabled:t=>{w(D,t),e({markstreamReactEnabled:t})}})),A=null,j=()=>(A??=a(()=>import(`./MarkstreamRendererImpl-B1yo0Ww3.js`),__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65])).catch(e=>{throw A=null,e}),A),M=()=>{j().catch(()=>void 0)},N=null,P=()=>(N??=a(()=>import(`./MarkdownRendererImpl-C-hRa7L9.js`).then(e=>e.n),__vite__mapDeps([45,1,4,5,18,17,6,2,3,7,19,20,25,26,9,46,13,14,15,16,21,22,23,24,27,28,29,30,31,44,32,34,35,36,37,38,39,47,40,48,49,50,51,52,53,54,55,56,57,58,59,43,60,61,62,63,64])).catch(e=>{throw N=null,e}),N),F=()=>{P().catch(()=>void 0),O()&&M()},I=class extends d.Component{state={hasError:!1};static getDerivedStateFromError(){return{hasError:!0}}componentDidCatch(e){console.warn(`[markstream-react] falling back to the current Markdown renderer`,e)}render(){return this.state.hasError?this.props.fallback:this.props.children}},L=d.createContext(!0),R=()=>d.useContext(L),z=r(),B=x(()=>P().then(e=>({default:e.MarkdownRenderer}))),V=x(()=>j().then(e=>({default:e.MarkstreamRenderer}))),H=x(()=>P().then(e=>({default:e.SimpleMarkdownRenderer}))),U=e=>e===`tool`?`markdown-content markdown-tool`:e===`reasoning`?`markdown-content markdown-reasoning`:`markdown-content leading-relaxed`,W=e=>{let t=(0,u.c)(15),n=k(q),r=typeof e.content==`string`?e.content:``,i=typeof e.variant==`string`?e.variant:`assistant`,a;t[0]!==r||t[1]!==n||t[2]!==e.reserveMeasuredHeight||t[3]!==i?(a=e.reserveMeasuredHeight?c(l(r,n?`markstream:${i}`:i)):void 0,t[0]=r,t[1]=n,t[2]=e.reserveMeasuredHeight,t[3]=i,t[4]=a):a=t[4];let d=a,f;t[5]!==e.className||t[6]!==e.variant?(f=o(`relative break-words w-full min-w-0`,U(e.variant),typeof e.className==`string`?e.className:void 0),t[5]=e.className,t[6]=e.variant,t[7]=f):f=t[7];let p;t[8]!==r||t[9]!==e.animated||t[10]!==d?(p=(0,z.jsx)(s,{animated:e.animated,content:r,reservedHeight:d}),t[8]=r,t[9]=e.animated,t[10]=d,t[11]=p):p=t[11];let m;return t[12]!==f||t[13]!==p?(m=(0,z.jsx)(`div`,{className:f,"aria-busy":`true`,"data-markdown-hydration":`deferred`,"data-markdown-ready":`false`,children:p}),t[12]=f,t[13]=p,t[14]=m):m=t[14],m},G=e=>{let t=(0,u.c)(9),n=R(),r=k(J);if(!n&&e.isStreaming!==!0){let n;return t[0]===e?n=t[1]:(n=(0,z.jsx)(W,{...e,animated:!1,reserveMeasuredHeight:!0}),t[0]=e,t[1]=n),n}let i;t[2]===e?i=t[3]:(i=(0,z.jsx)(d.Suspense,{fallback:(0,z.jsx)(W,{...e,reserveMeasuredHeight:!0}),children:(0,z.jsx)(B,{...e})}),t[2]=e,t[3]=i);let a=i;if(!r)return a;let o;t[4]===e?o=t[5]:(o=(0,z.jsx)(d.Suspense,{fallback:(0,z.jsx)(W,{...e,reserveMeasuredHeight:!0}),children:(0,z.jsx)(V,{...e})}),t[4]=e,t[5]=o);let s;return t[6]!==a||t[7]!==o?(s=(0,z.jsx)(I,{fallback:a,children:o}),t[6]=a,t[7]=o,t[8]=s):s=t[8],s},K=e=>{let t=(0,u.c)(4);if(!R()){let n;return t[0]===e?n=t[1]:(n=(0,z.jsx)(W,{...e,animated:!1}),t[0]=e,t[1]=n),n}let n;return t[2]===e?n=t[3]:(n=(0,z.jsx)(d.Suspense,{fallback:(0,z.jsx)(W,{...e}),children:(0,z.jsx)(H,{...e})}),t[2]=e,t[3]=n),n};function q(e){return e.markstreamReactEnabled}function J(e){return e.markstreamReactEnabled}export{k as a,F as i,K as n,x as o,L as r,G as t};