@q9labsai/chalk-client 4.1.15 → 4.1.16

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 (130) hide show
  1. package/README.md +56 -2
  2. package/dist/access/manager.d.ts.map +1 -1
  3. package/dist/{chunk-LQSXEWLL.js → chunk-BD7FRQ67.js} +2061 -326
  4. package/dist/{chunk-6ONSFCIY.js → chunk-E4OK5K6N.js} +7 -3
  5. package/dist/{chunk-NKOKY2AE.js → chunk-EWTITNJ3.js} +15 -101
  6. package/dist/chunk-HW5TTZL2.js +99 -0
  7. package/dist/{chunk-LSDDADCN.js → chunk-M3S7HNQF.js} +1656 -517
  8. package/dist/{chunk-PQNSA7ZN.js → chunk-O7WTAXAY.js} +610 -73
  9. package/dist/{chunk-PWUU6T3R.js → chunk-OKIL2ELF.js} +1185 -874
  10. package/dist/chunk-RCDXIDCN.js +33 -0
  11. package/dist/client.d.ts +1412 -463
  12. package/dist/client.d.ts.map +1 -1
  13. package/dist/connection/dependencies.d.ts +10 -0
  14. package/dist/connection/dependencies.d.ts.map +1 -1
  15. package/dist/connection/index.d.ts +2 -0
  16. package/dist/connection/index.d.ts.map +1 -1
  17. package/dist/connection/lifecycle.d.ts.map +1 -1
  18. package/dist/connection/production.d.ts +4 -1
  19. package/dist/connection/production.d.ts.map +1 -1
  20. package/dist/connection/types.d.ts +2 -2
  21. package/dist/connection/types.d.ts.map +1 -1
  22. package/dist/effect.js +343 -130
  23. package/dist/feedback/collector.d.ts +14 -0
  24. package/dist/feedback/collector.d.ts.map +1 -0
  25. package/dist/feedback/context.d.ts +38 -0
  26. package/dist/feedback/context.d.ts.map +1 -0
  27. package/dist/feedback/controller.d.ts +4 -0
  28. package/dist/feedback/controller.d.ts.map +1 -0
  29. package/dist/feedback/index.d.ts +7 -0
  30. package/dist/feedback/index.d.ts.map +1 -0
  31. package/dist/feedback/transport.d.ts +29 -0
  32. package/dist/feedback/transport.d.ts.map +1 -0
  33. package/dist/feedback/types.d.ts +202 -0
  34. package/dist/feedback/types.d.ts.map +1 -0
  35. package/dist/feedback/validation.d.ts +30 -0
  36. package/dist/feedback/validation.d.ts.map +1 -0
  37. package/dist/generated/http-api.d.ts +1401 -462
  38. package/dist/generated/http-api.d.ts.map +1 -1
  39. package/dist/generated/schemas.d.ts +2601 -858
  40. package/dist/generated/schemas.d.ts.map +1 -1
  41. package/dist/index.d.ts +1 -0
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +150 -35
  44. package/dist/media/camera-uplink.d.ts +13 -0
  45. package/dist/media/camera-uplink.d.ts.map +1 -0
  46. package/dist/media/capture-error.d.ts +27 -0
  47. package/dist/media/capture-error.d.ts.map +1 -0
  48. package/dist/media/client.d.ts +4 -0
  49. package/dist/media/client.d.ts.map +1 -1
  50. package/dist/media/index.d.ts +1 -0
  51. package/dist/media/index.d.ts.map +1 -1
  52. package/dist/media/index.js +5 -1
  53. package/dist/media/plane.d.ts +4 -0
  54. package/dist/media/plane.d.ts.map +1 -1
  55. package/dist/media/remote-track-response.d.ts +6 -0
  56. package/dist/media/remote-track-response.d.ts.map +1 -0
  57. package/dist/media/transport.d.ts.map +1 -1
  58. package/dist/media/types.d.ts +20 -2
  59. package/dist/media/types.d.ts.map +1 -1
  60. package/dist/public-invites/client.d.ts.map +1 -1
  61. package/dist/public-invites/index.js +4 -3
  62. package/dist/public-invites/types.d.ts +4 -2
  63. package/dist/public-invites/types.d.ts.map +1 -1
  64. package/dist/server/access-refresh.d.cts +8 -0
  65. package/dist/server/access-refresh.d.ts +9 -0
  66. package/dist/server/access-refresh.d.ts.map +1 -0
  67. package/dist/server/client.d.ts.map +1 -1
  68. package/dist/server/index.cjs +181 -20
  69. package/dist/server/index.d.cts +2 -1
  70. package/dist/server/index.d.ts +2 -1
  71. package/dist/server/index.d.ts.map +1 -1
  72. package/dist/server/index.js +179 -19
  73. package/dist/server/types.d.cts +74 -0
  74. package/dist/server/types.d.ts +74 -0
  75. package/dist/server/types.d.ts.map +1 -1
  76. package/dist/space-client/chat-controller.d.ts +3 -0
  77. package/dist/space-client/chat-controller.d.ts.map +1 -1
  78. package/dist/space-client/controller-effects.d.ts +6 -0
  79. package/dist/space-client/controller-effects.d.ts.map +1 -1
  80. package/dist/space-client/core.d.ts +12 -1
  81. package/dist/space-client/core.d.ts.map +1 -1
  82. package/dist/space-client/core.test.helpers.d.ts +1 -0
  83. package/dist/space-client/core.test.helpers.d.ts.map +1 -1
  84. package/dist/space-client/effect.d.ts +5 -0
  85. package/dist/space-client/effect.d.ts.map +1 -1
  86. package/dist/space-client/episode-diagnostic-runtime.d.ts +3 -0
  87. package/dist/space-client/episode-diagnostic-runtime.d.ts.map +1 -1
  88. package/dist/space-client/errors.d.ts.map +1 -1
  89. package/dist/space-client/index.d.ts +4 -1
  90. package/dist/space-client/index.d.ts.map +1 -1
  91. package/dist/space-client/media-controller.d.ts.map +1 -1
  92. package/dist/space-client/offline-actions-storage.d.ts +66 -0
  93. package/dist/space-client/offline-actions-storage.d.ts.map +1 -0
  94. package/dist/space-client/offline-actions.d.ts +36 -0
  95. package/dist/space-client/offline-actions.d.ts.map +1 -0
  96. package/dist/space-client/participants-controller.d.ts +2 -1
  97. package/dist/space-client/participants-controller.d.ts.map +1 -1
  98. package/dist/space-client/recording-controller.d.ts +20 -0
  99. package/dist/space-client/recording-controller.d.ts.map +1 -0
  100. package/dist/space-client/reload-rejoin.d.ts +24 -0
  101. package/dist/space-client/reload-rejoin.d.ts.map +1 -0
  102. package/dist/space-client/space-client.d.ts.map +1 -1
  103. package/dist/space-client/store.d.ts +3 -1
  104. package/dist/space-client/store.d.ts.map +1 -1
  105. package/dist/space-client/types.d.ts +40 -4
  106. package/dist/space-client/types.d.ts.map +1 -1
  107. package/dist/sync/index.js +2 -1
  108. package/dist/sync/v1-client.d.ts +1 -5
  109. package/dist/sync/v1-client.d.ts.map +1 -1
  110. package/dist/sync/v1-collaboration-state.d.ts +65 -0
  111. package/dist/sync/v1-collaboration-state.d.ts.map +1 -0
  112. package/dist/sync/v1-command-scheduler.d.ts +54 -0
  113. package/dist/sync/v1-command-scheduler.d.ts.map +1 -0
  114. package/dist/sync/v1-deferred.d.ts +8 -0
  115. package/dist/sync/v1-deferred.d.ts.map +1 -0
  116. package/dist/sync/v1-error.d.ts +5 -0
  117. package/dist/sync/v1-error.d.ts.map +1 -0
  118. package/dist/sync/v1-frame-signature.d.ts +2 -0
  119. package/dist/sync/v1-frame-signature.d.ts.map +1 -0
  120. package/dist/sync/v1-live-target-coordinator.d.ts +32 -0
  121. package/dist/sync/v1-live-target-coordinator.d.ts.map +1 -0
  122. package/dist/sync/v1-types.d.ts +3 -0
  123. package/dist/sync/v1-types.d.ts.map +1 -1
  124. package/dist/telemetry/index.js +9 -7
  125. package/dist/telemetry/reconnect.d.ts +7 -0
  126. package/dist/telemetry/reconnect.d.ts.map +1 -0
  127. package/dist/telemetry/trace.d.ts +1 -1
  128. package/dist/telemetry/trace.d.ts.map +1 -1
  129. package/dist/whiteboard/v1-client.d.ts.map +1 -1
  130. package/package.json +1 -1
@@ -1,26 +1,38 @@
1
1
  import {
2
2
  CloudflareRTKClient,
3
3
  CloudflareSFUClient,
4
- createCloudflareSFUHTTPTransport
5
- } from "./chunk-PQNSA7ZN.js";
6
- import {
7
- createBrowserSyncLifecycle,
8
- createBrowserWebSocketFactory,
9
- createV1SyncClient
10
- } from "./chunk-PWUU6T3R.js";
4
+ createCloudflareSFUHTTPTransport,
5
+ describeMediaCaptureError
6
+ } from "./chunk-O7WTAXAY.js";
11
7
  import {
12
8
  AccessGrantError,
13
9
  requireParsedAccessGrant,
14
10
  validEpisodeDiagnosticCredential
15
- } from "./chunk-LSDDADCN.js";
11
+ } from "./chunk-M3S7HNQF.js";
12
+ import {
13
+ normalizeTelemetryAttributes
14
+ } from "./chunk-EWTITNJ3.js";
16
15
  import {
17
16
  createTraceContext,
17
+ journeyHeaders,
18
18
  parseTraceparent
19
- } from "./chunk-NKOKY2AE.js";
19
+ } from "./chunk-HW5TTZL2.js";
20
+ import {
21
+ ChatAttachmentMimeTypeSchema,
22
+ V1SyncError,
23
+ createBrowserSyncLifecycle,
24
+ createBrowserWebSocketFactory,
25
+ createV1SyncClient
26
+ } from "./chunk-OKIL2ELF.js";
27
+ import {
28
+ recordInitialConnection,
29
+ recordReconnect
30
+ } from "./chunk-RCDXIDCN.js";
20
31
 
21
32
  // src/access/manager.ts
22
33
  import { Clock, Context, Data, Effect, Layer, SynchronizedRef } from "effect";
23
34
  var DEFAULT_REFRESH_WINDOW_MS = 6e4;
35
+ var ACCESS_PROVIDER_TIMEOUT_MS = 7e3;
24
36
  var ConnectionAccessFailure = class extends Data.TaggedError("ConnectionAccessFailure") {
25
37
  };
26
38
  var ConnectionAccessService = class extends Context.Service()("@chalk/client/ConnectionAccess") {
@@ -34,7 +46,7 @@ var makeConnectionAccessLayer = (provider, refreshWindowMs = DEFAULT_REFRESH_WIN
34
46
  Effect.tap((result) => Effect.sync(() => notifyAutomaticMediaReplacement(automaticMediaReplacementListeners, result))),
35
47
  Effect.map((result) => result.access)
36
48
  );
37
- const initialize = (reason = "join") => fetch(reason, false, false).pipe(Effect.catchTag("ConnectionAccessFailure", (failure2) => failure2.code === "access.invalid" && SynchronizedRef.getUnsafe(state).access === null ? fetch(reason, false, true) : Effect.fail(failure2)));
49
+ const initialize = (reason = "join") => fetch(reason, false, false).pipe(Effect.catchTag("ConnectionAccessFailure", (failure3) => failure3.code === "access.invalid" && SynchronizedRef.getUnsafe(state).access === null ? fetch(reason, false, true) : Effect.fail(failure3)));
38
50
  const millisecondsUntilRefresh = Effect.gen(function* () {
39
51
  const access = yield* SynchronizedRef.get(state);
40
52
  if (!access.access) return null;
@@ -70,7 +82,10 @@ function fetchAccessGrant(provider, input) {
70
82
  }
71
83
  const mediaExpired = currentAccess !== null && expiresAt(currentAccess.media.expiresAt) <= now;
72
84
  const shouldReplaceMediaConnection = input.replaceMediaConnection || mediaExpired;
73
- const next = yield* provider(connectionAccessRequest(input, shouldReplaceMediaConnection));
85
+ const next = yield* provider(connectionAccessRequest(input, shouldReplaceMediaConnection)).pipe(
86
+ Effect.timeout(ACCESS_PROVIDER_TIMEOUT_MS),
87
+ Effect.mapError((cause) => cause instanceof ConnectionAccessFailure ? cause : new ConnectionAccessFailure({ code: "access.unavailable", cause }))
88
+ );
74
89
  yield* Effect.try({
75
90
  try: () => validateFetchedAccess(currentAccess, next, now, shouldReplaceMediaConnection),
76
91
  catch: (cause) => new ConnectionAccessFailure({ code: "access.invalid", cause })
@@ -168,12 +183,12 @@ var ChalkChatFileError = class extends Error {
168
183
  operation;
169
184
  code;
170
185
  recoverable;
171
- constructor(failure2, options) {
172
- super(failure2.message, options);
186
+ constructor(failure3, options) {
187
+ super(failure3.message, options);
173
188
  this.name = "ChalkChatFileError";
174
- this.operation = failure2.operation;
175
- this.code = failure2.code;
176
- this.recoverable = failure2.recoverable;
189
+ this.operation = failure3.operation;
190
+ this.code = failure3.code;
191
+ this.recoverable = failure3.recoverable;
177
192
  }
178
193
  };
179
194
 
@@ -291,12 +306,12 @@ var ChalkWhiteboardV1Error = class extends Error {
291
306
  operation;
292
307
  code;
293
308
  recoverable;
294
- constructor(failure2, options) {
295
- super(failure2.message, options);
309
+ constructor(failure3, options) {
310
+ super(failure3.message, options);
296
311
  this.name = "ChalkWhiteboardV1Error";
297
- this.operation = failure2.operation;
298
- this.code = failure2.code;
299
- this.recoverable = failure2.recoverable;
312
+ this.operation = failure3.operation;
313
+ this.code = failure3.code;
314
+ this.recoverable = failure3.recoverable;
300
315
  }
301
316
  };
302
317
 
@@ -1028,7 +1043,7 @@ var ChalkWhiteboardV1Client = class {
1028
1043
  return () => this.#summaryListeners.delete(listener);
1029
1044
  }
1030
1045
  submitUpdate(input) {
1031
- this.#assertLive("submit_update");
1046
+ if (!this.#started || !this.#sceneId) this.#assertLive("submit_update");
1032
1047
  if (!this.#canDraw || !this.#capabilities.includes("drawWhiteboard")) {
1033
1048
  throw error(failure("submit_update", "permission_denied", false, "Whiteboard draw permission is required."));
1034
1049
  }
@@ -1042,7 +1057,7 @@ var ChalkWhiteboardV1Client = class {
1042
1057
  });
1043
1058
  }
1044
1059
  sendCursor(input) {
1045
- if (this.#phase !== "live" || !this.#canDraw) return;
1060
+ if (this.#phase !== "live" || this.#summaryStatus !== "ready" || this.#snapshots.size > 0 || !this.#canDraw) return;
1046
1061
  const now = this.#now();
1047
1062
  if (now - this.#lastCursorAt < 1e3 / WhiteboardV1ProtocolLimits.cursorRatePerSecond) return;
1048
1063
  const frame = { type: "cursor", x: input.x, y: input.y };
@@ -1328,6 +1343,7 @@ var ChalkWhiteboardV1Client = class {
1328
1343
  this.#sceneId = frame.scene_id;
1329
1344
  this.#revision = frame.revision;
1330
1345
  this.#publishSummary("ready", null);
1346
+ this.#flushQueuedOperations();
1331
1347
  this.#emit({
1332
1348
  type: "snapshot",
1333
1349
  sceneId: frame.scene_id,
@@ -1375,7 +1391,10 @@ var ChalkWhiteboardV1Client = class {
1375
1391
  deferred = { resolve, reject, settled: false };
1376
1392
  });
1377
1393
  this.#operations.set(frame.operation_id, { pending, deferred, retries: 0 });
1378
- this.#sendOperation(frame);
1394
+ if (this.#phase === "live" && this.#snapshots.size === 0) {
1395
+ if (this.#waitingForOperations) this.#awaitingOperationIds.add(frame.operation_id);
1396
+ this.#sendOperation(frame);
1397
+ }
1379
1398
  return promise;
1380
1399
  }
1381
1400
  #commit(frame) {
@@ -1423,6 +1442,13 @@ var ChalkWhiteboardV1Client = class {
1423
1442
  rejectDeferred(this.#initialSnapshot, snapshotError);
1424
1443
  this.#initialSnapshot = null;
1425
1444
  }
1445
+ if (frame.recoverable) this.#flushQueuedOperations();
1446
+ }
1447
+ #flushQueuedOperations() {
1448
+ if (this.#phase !== "live" || this.#snapshots.size > 0) return;
1449
+ for (const entry of this.#operations.values()) {
1450
+ if (!this.#awaitingOperationIds.has(entry.pending.operationId)) this.#sendOperation(entry.pending.frame);
1451
+ }
1426
1452
  }
1427
1453
  #scheduleOperationRetry(operationId, entry) {
1428
1454
  if (this.#operationRetryTimers.has(operationId)) return;
@@ -1462,8 +1488,6 @@ var ChalkWhiteboardV1Client = class {
1462
1488
  this.#phase = "connecting";
1463
1489
  this.#waitingForOperations = false;
1464
1490
  this.#participantId = null;
1465
- this.#capabilities = [];
1466
- this.#canDraw = false;
1467
1491
  this.#rejectSnapshots(failure("request_snapshot", "unavailable", true, "Whiteboard connection interrupted."));
1468
1492
  this.#publishSummary("recovering", failure("start_scene_subscription", "unavailable", true, "Whiteboard connection interrupted."));
1469
1493
  }
@@ -1705,11 +1729,11 @@ function createChalkWhiteboardV1Client(options) {
1705
1729
  function createBrowserMediaDevices(mediaDevices = globalThis.navigator?.mediaDevices) {
1706
1730
  return {
1707
1731
  async getUserMedia(constraints) {
1708
- if (!mediaDevices?.getUserMedia) throw new TypeError("Browser media capture is unavailable");
1732
+ if (!mediaDevices?.getUserMedia) throw new DOMException("Browser media capture is unavailable", "NotSupportedError");
1709
1733
  return mediaDevices.getUserMedia(constraints);
1710
1734
  },
1711
1735
  async getDisplayMedia(constraints) {
1712
- if (!mediaDevices?.getDisplayMedia) throw new TypeError("Browser display capture is unavailable");
1736
+ if (!mediaDevices?.getDisplayMedia) throw new DOMException("Browser display capture is unavailable", "NotSupportedError");
1713
1737
  return mediaDevices.getDisplayMedia(constraints);
1714
1738
  },
1715
1739
  async enumerateDevices() {
@@ -1802,12 +1826,14 @@ function createMediaClient(apiBaseURL, input) {
1802
1826
  } : void 0,
1803
1827
  transport: createCloudflareSFUHTTPTransport({
1804
1828
  apiBaseURL,
1829
+ telemetry: input.telemetry,
1805
1830
  credential: input.credential,
1806
1831
  tenantId: subject.tenantId,
1807
1832
  spaceId: subject.spaceId,
1808
1833
  episodeId: subject.episodeId,
1809
1834
  participantId: subject.participantId
1810
1835
  }),
1836
+ recordReconnect: input.recordReconnect,
1811
1837
  onError: input.onFailure,
1812
1838
  ...input.recordRtcSummary ? { onRtcSummary: input.recordRtcSummary } : {},
1813
1839
  onScreenEnded: input.onScreenEnded
@@ -1832,8 +1858,12 @@ function createSFUConnectionMediaClient(client) {
1832
1858
  function bindConnectionMediaClient(client, restart) {
1833
1859
  return {
1834
1860
  setLocalPublicationTarget: client.setLocalPublicationTarget.bind(client),
1861
+ ...client.setLocalSourceIntent ? { setLocalSourceIntent: client.setLocalSourceIntent.bind(client) } : {},
1862
+ ...client.closeForcedLocalPublication ? { closeForcedLocalPublication: client.closeForcedLocalPublication.bind(client) } : {},
1835
1863
  observeLocalPublications: client.observeLocalPublications.bind(client),
1836
1864
  observeRemotePublications: client.observeRemotePublications.bind(client),
1865
+ ...client.remotePublicationsChanged ? { remotePublicationsChanged: client.remotePublicationsChanged.bind(client) } : {},
1866
+ ...client.remotePublicationResumed ? { remotePublicationResumed: client.remotePublicationResumed.bind(client) } : {},
1837
1867
  start: client.start.bind(client),
1838
1868
  stop: client.stop.bind(client),
1839
1869
  restart,
@@ -1849,6 +1879,7 @@ function createSyncClient(syncURL, input) {
1849
1879
  token: input.token,
1850
1880
  mediaPlane: input.media,
1851
1881
  telemetry: input.telemetry,
1882
+ recordReconnect: input.recordReconnect,
1852
1883
  persistenceScope: `${input.access.subject.tenantId}:${input.access.subject.episodeId}:${input.access.subject.participantId}`
1853
1884
  });
1854
1885
  }
@@ -1941,12 +1972,12 @@ var ConnectionError = class extends Error {
1941
1972
  code;
1942
1973
  action;
1943
1974
  recoverable;
1944
- constructor(failure2, options) {
1945
- super(failure2.message, options);
1975
+ constructor(failure3, options) {
1976
+ super(failure3.message, options);
1946
1977
  this.name = "ConnectionError";
1947
- this.code = failure2.code;
1948
- this.action = failure2.action;
1949
- this.recoverable = failure2.recoverable;
1978
+ this.code = failure3.code;
1979
+ this.action = failure3.action;
1980
+ this.recoverable = failure3.recoverable;
1950
1981
  }
1951
1982
  };
1952
1983
 
@@ -1965,7 +1996,7 @@ var makeConnectionLifecycleLayer = (options) => {
1965
1996
  const platform = { ...defaults, ...options.dependencies };
1966
1997
  const access = makeConnectionAccessLayer(
1967
1998
  (request) => Effect2.tryPromise({
1968
- try: () => Promise.resolve(options.access(request)),
1999
+ try: (signal) => Promise.resolve(options.access(request ? { ...request, signal } : void 0)),
1969
2000
  catch: (cause) => new ConnectionAccessFailure({ code: "access.unavailable", cause })
1970
2001
  }),
1971
2002
  options.accessRefreshWindowMs
@@ -2006,6 +2037,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2006
2037
  sync: null,
2007
2038
  media: null,
2008
2039
  syncSnapshot: null,
2040
+ syncNoticeUnresponsive: false,
2009
2041
  mediaSnapshot: null,
2010
2042
  failure: null,
2011
2043
  initialMedia: (intent) => Effect2.tryPromise({
@@ -2023,7 +2055,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2023
2055
  closed: false,
2024
2056
  epoch: 0
2025
2057
  };
2026
- const foreign6 = (operation, code = "internal_error", message = "A browser transport operation failed") => Effect2.tryPromise({ try: operation, catch: (cause) => lifecycleFailure(code, true, message, cause) });
2058
+ const foreign7 = (operation, code = "internal_error", message = "A browser transport operation failed") => Effect2.tryPromise({ try: operation, catch: (cause) => lifecycleFailure(code, true, message, cause) });
2027
2059
  const toPromise = (effect) => Effect2.runPromiseWith(context)(effect);
2028
2060
  const publish = () => Effect2.sync(() => snapshotFor(model, access.currentUnsafe())).pipe(
2029
2061
  Effect2.tap((snapshot) => SubscriptionRef.set(snapshotRef, snapshot)),
@@ -2096,6 +2128,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2096
2128
  model.sync = null;
2097
2129
  model.media = null;
2098
2130
  model.syncSnapshot = null;
2131
+ model.syncNoticeUnresponsive = false;
2099
2132
  model.mediaSnapshot = null;
2100
2133
  yield* emitPorts();
2101
2134
  if (scope) yield* Scope.close(scope, Exit.void);
@@ -2104,9 +2137,17 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2104
2137
  });
2105
2138
  const bindSync = (scope, sync) => Effect2.gen(function* () {
2106
2139
  model.syncBindingCleanup?.();
2140
+ model.syncNoticeUnresponsive = false;
2107
2141
  let latestSnapshot = null;
2108
2142
  let snapshotQueued = false;
2109
2143
  const unsubscribe = sync.subscribe((snapshot) => {
2144
+ if (sync === model.sync && snapshot.connection.phase === "live") {
2145
+ const notice = snapshot.connection.noticeUnresponsive === true;
2146
+ if (notice !== model.syncNoticeUnresponsive) {
2147
+ model.syncNoticeUnresponsive = notice;
2148
+ void Effect2.runForkWith(context)(publish());
2149
+ }
2150
+ }
2110
2151
  latestSnapshot = snapshot;
2111
2152
  if (snapshotQueued) return;
2112
2153
  snapshotQueued = true;
@@ -2167,7 +2208,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2167
2208
  );
2168
2209
  const trace = (step, effect) => {
2169
2210
  const span = diagnostics.startSpan({ step, state: model.state, epoch: model.epoch });
2170
- return effect.pipe(
2211
+ return traceRecovery(step, effect, recordInitialConnection).pipe(
2171
2212
  Effect2.tap(() => Effect2.sync(() => span.end({ state: model.state, epoch: model.epoch, outcome: "succeeded" }))),
2172
2213
  Effect2.tapError(() => Effect2.sync(() => span.end({ state: model.state, epoch: model.epoch, outcome: "failed" })))
2173
2214
  );
@@ -2195,6 +2236,8 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2195
2236
  access: grant,
2196
2237
  credential: () => toPromise(access.getMediaToken()),
2197
2238
  replaceMediaConnection: () => toPromise(access.refresh("media_recovery", true)).then((replacement) => replacement.media),
2239
+ recordReconnect: options.recordReconnect,
2240
+ telemetry: options.telemetry,
2198
2241
  ...options.recordRtcSummary ? { recordRtcSummary: options.recordRtcSummary } : {},
2199
2242
  onFailure: () => enqueueBackground(handleMediaFailure()),
2200
2243
  onScreenEnded: () => enqueueBackground(notifyScreenEnded())
@@ -2206,7 +2249,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2206
2249
  const sync = yield* trace(
2207
2250
  "create_sync_client",
2208
2251
  Effect2.try({
2209
- try: () => platform.createSyncClient({ access: grant, token: () => toPromise(access.getSyncToken()), media, telemetry: options.telemetry }),
2252
+ try: () => platform.createSyncClient({ access: grant, token: () => toPromise(access.getSyncToken()), media, telemetry: options.telemetry, recordReconnect: options.recordReconnect }),
2210
2253
  catch: (cause) => accessRejected(cause) ? lifecycleFailure("invalid_access", false, "Access was rejected", cause) : lifecycleFailure("sync_start_failed", true, "The sync layer could not start", cause)
2211
2254
  })
2212
2255
  );
@@ -2216,9 +2259,9 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2216
2259
  [
2217
2260
  trace(
2218
2261
  "start_media",
2219
- foreign6(() => media.start(stream), "media_start_failed", "The media layer could not start")
2262
+ foreign7(() => media.start(stream), "media_start_failed", "The media layer could not start")
2220
2263
  ),
2221
- trace("start_sync", foreign6(() => sync.start(), "sync_start_failed", "The sync layer could not start").pipe(Effect2.andThen(trace("wait_for_sync_live", waitForSyncLive(sync, boundedInteger(options.syncStartupTimeoutMs, START_TIMEOUT_MS, 1, 6e4))))))
2264
+ trace("start_sync", foreign7(() => sync.start(), "sync_start_failed", "The sync layer could not start").pipe(Effect2.andThen(trace("wait_for_sync_live", waitForSyncLive(sync, boundedInteger(options.syncStartupTimeoutMs, START_TIMEOUT_MS, 1, 6e4))))))
2222
2265
  ],
2223
2266
  { concurrency: "unbounded", discard: true }
2224
2267
  );
@@ -2226,17 +2269,17 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2226
2269
  Effect2.matchEffect({
2227
2270
  onFailure: (cause) => Effect2.gen(function* () {
2228
2271
  stopStream(stream);
2229
- const failure2 = failureFromCause(cause, "sync_start_failed", "The Connection could not start");
2230
- model.failure = toFailure(failure2);
2272
+ const failure3 = failureFromCause(cause, "sync_start_failed", "The Connection could not start");
2273
+ model.failure = toFailure(failure3);
2231
2274
  if (model.joinCancelled) {
2232
2275
  span.end({ state: model.state, epoch: model.epoch, outcome: "cancelled", code: "invalid_state" });
2233
2276
  return yield* Effect2.fail(lifecycleFailure("invalid_state", false, "Join was cancelled by Leave"));
2234
2277
  }
2235
2278
  yield* stopPorts();
2236
- if (!retried && failure2.code === "invalid_access") return yield* performJoin("access_retry", true);
2279
+ if (!retried && failure3.code === "invalid_access") return yield* performJoin("access_retry", true);
2237
2280
  yield* transition("failed");
2238
- span.end({ state: model.state, epoch: model.epoch, outcome: "failed", code: failure2.code });
2239
- return yield* Effect2.fail(failure2);
2281
+ span.end({ state: model.state, epoch: model.epoch, outcome: "failed", code: failure3.code });
2282
+ return yield* Effect2.fail(failure3);
2240
2283
  }),
2241
2284
  onSuccess: () => Effect2.gen(function* () {
2242
2285
  model.syncSnapshot = model.sync?.getSnapshot() ?? null;
@@ -2262,7 +2305,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2262
2305
  const leaveIsConfirmed = () => Effect2.gen(function* leaveIsConfirmedEffect() {
2263
2306
  const sync = model.sync;
2264
2307
  if (!sync || model.episodeEndConfirmed || access.currentUnsafe() === null) return true;
2265
- const leave = foreign6(() => sync.leave()).pipe(Effect2.timeout(LEAVE_TIMEOUT_MS));
2308
+ const leave = foreign7(() => sync.leave()).pipe(Effect2.timeout(LEAVE_TIMEOUT_MS));
2266
2309
  return (yield* Effect2.exit(leave))._tag === "Success";
2267
2310
  });
2268
2311
  const withFreshAccess = (operation) => Effect2.gen(function* () {
@@ -2290,6 +2333,19 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2290
2333
  })
2291
2334
  );
2292
2335
  });
2336
+ const traceRecovery = (step, operation, recordStep = recordReconnect) => Effect2.gen(function* () {
2337
+ const started = yield* Clock2.currentTimeMillis;
2338
+ recordStep(options.recordReconnect, step, { boundary: "start" });
2339
+ return yield* operation.pipe(
2340
+ Effect2.onExit(
2341
+ (exit) => Clock2.currentTimeMillis.pipe(
2342
+ Effect2.map((now) => {
2343
+ recordStep(options.recordReconnect, step, { boundary: "end", duration_ms: Math.max(0, now - started) }, exit._tag === "Success" ? "succeeded" : "failed");
2344
+ })
2345
+ )
2346
+ )
2347
+ );
2348
+ });
2293
2349
  const recover = (kind) => Effect2.gen(function* recoverEffect() {
2294
2350
  if (!active(model)) return;
2295
2351
  if (model.recoveryQueued === kind) return;
@@ -2312,10 +2368,11 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2312
2368
  for (let attempt = 1; attempt <= plan.attempts; attempt += 1) {
2313
2369
  const remaining = plan.deadline - (yield* Clock2.currentTimeMillis);
2314
2370
  if (remaining <= 0) return null;
2371
+ recordReconnect(options.recordReconnect, "attempt", { recovery_kind: plan.kind, attempt });
2315
2372
  diagnostics.record({ event: "recovery_attempt", state: model.state, epoch: model.epoch, attempt });
2316
2373
  const outcome = yield* Effect2.exit(recoveryOperation(plan.kind).pipe(Effect2.timeout(remaining)));
2317
2374
  if (outcome._tag === "Success") return attempt;
2318
- if (attempt < plan.attempts) yield* Effect2.sleep(recoveryDelay(plan, attempt));
2375
+ if (attempt < plan.attempts) yield* traceRecovery("lifecycle_backoff", Effect2.sleep(recoveryDelay(plan, attempt)));
2319
2376
  }
2320
2377
  return null;
2321
2378
  });
@@ -2339,9 +2396,13 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2339
2396
  const media = model.media;
2340
2397
  const sync = model.sync;
2341
2398
  if (!media || !sync) return yield* Effect2.fail(lifecycleFailure("invalid_state", false, "Media recovery requires active ports"));
2342
- const grant = yield* access.refresh("media_recovery", true).pipe(Effect2.mapError(accessFailure));
2399
+ recordReconnect(options.recordReconnect, "media_decision", { strategy: "full_rebuild" });
2400
+ const grant = yield* traceRecovery("access_refresh", access.refresh("media_recovery", true).pipe(Effect2.mapError(accessFailure)));
2343
2401
  const restartInput = grant.media.provider === "cloudflare_sfu" ? grant.media.clientPayload : grant.media;
2344
- yield* foreign6(() => media.restart(restartInput));
2402
+ yield* traceRecovery(
2403
+ "media_rebuild",
2404
+ foreign7(() => media.restart(restartInput))
2405
+ );
2345
2406
  model.mediaSnapshot = media.getSnapshot();
2346
2407
  yield* waitForSyncLive(sync, boundedInteger(options.recovery?.budgetMs, RECOVERY_BUDGET_MS, 1, 6e4));
2347
2408
  });
@@ -2354,12 +2415,12 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2354
2415
  model.syncBindingCleanup?.();
2355
2416
  model.sync = null;
2356
2417
  const sync = yield* Effect2.try({
2357
- try: () => platform.createSyncClient({ access: grant, token: () => toPromise(access.getSyncToken()), media, telemetry: options.telemetry }),
2418
+ try: () => platform.createSyncClient({ access: grant, token: () => toPromise(access.getSyncToken()), media, telemetry: options.telemetry, recordReconnect: options.recordReconnect }),
2358
2419
  catch: (cause) => lifecycleFailure("sync_start_failed", true, "The sync layer could not start", cause)
2359
2420
  });
2360
2421
  model.sync = sync;
2361
2422
  yield* bindSync(scope, sync);
2362
- yield* foreign6(() => sync.start());
2423
+ yield* foreign7(() => sync.start());
2363
2424
  yield* waitForSyncLive(sync, boundedInteger(options.recovery?.budgetMs, RECOVERY_BUDGET_MS, 1, 6e4));
2364
2425
  yield* emitPorts();
2365
2426
  });
@@ -2374,9 +2435,9 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2374
2435
  const isCurrentSyncSnapshot = (sync, snapshot) => sync === model.sync && snapshot !== model.syncSnapshot;
2375
2436
  const syncSubjectMismatched = (subject, snapshot) => subject !== null && snapshot.participantId !== null && (subject.participantId !== snapshot.participantId || subject.participantGeneration !== snapshot.participantGeneration);
2376
2437
  const episodeEnded = (snapshot) => snapshot.control?.status === "ended" || snapshot.optimisticControl?.status === "ended";
2377
- const syncNeedsRecovery = (snapshot) => active(model) && (snapshot.connection.phase === "terminal" || snapshot.connection.phase === "recovering");
2378
- const failForSnapshot = (failure2) => Effect2.gen(function* failForSnapshotEffect() {
2379
- model.failure = toFailure(failure2);
2438
+ const syncNeedsRecovery = (snapshot) => active(model) && snapshot.connection.phase === "terminal";
2439
+ const failForSnapshot = (failure3) => Effect2.gen(function* failForSnapshotEffect() {
2440
+ model.failure = toFailure(failure3);
2380
2441
  yield* stopPorts();
2381
2442
  yield* transition("failed");
2382
2443
  });
@@ -2392,7 +2453,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2392
2453
  const media = model.media;
2393
2454
  if (!media || !active(model)) return;
2394
2455
  const restartInput = grant.media.provider === "cloudflare_sfu" ? grant.media.clientPayload : grant.media;
2395
- const restarted = yield* Effect2.exit(foreign6(() => media.restart(restartInput), "media_start_failed", "The media client could not apply a replacement access grant"));
2456
+ const restarted = yield* Effect2.exit(foreign7(() => media.restart(restartInput), "media_start_failed", "The media client could not apply a replacement access grant"));
2396
2457
  if (restarted._tag === "Failure") {
2397
2458
  yield* recover("media");
2398
2459
  return;
@@ -2502,7 +2563,7 @@ function snapshotFor(model, access) {
2502
2563
  state: model.state,
2503
2564
  subject: subjectFor(access),
2504
2565
  episode: episodeFor(access, control),
2505
- connection: Object.freeze({ sync: syncPhase(model.syncSnapshot?.connection.phase), media: mediaPhase(model.mediaSnapshot?.connection.phase) }),
2566
+ connection: Object.freeze({ sync: model.syncSnapshot?.connection.phase === "live" && model.syncNoticeUnresponsive ? "unresponsive" : syncPhase(model.syncSnapshot?.connection.phase), media: mediaPhase(model.mediaSnapshot?.connection.phase) }),
2506
2567
  failure: model.failure ? Object.freeze({ ...model.failure }) : null
2507
2568
  });
2508
2569
  }
@@ -2578,6 +2639,10 @@ var CONNECTION_ERROR_CODE_MAP = {
2578
2639
  invalid_access: "access.invalid",
2579
2640
  access_unavailable: "access.unavailable",
2580
2641
  permission_denied: "media.permission_denied",
2642
+ device_not_found: "media.device_not_found",
2643
+ device_busy: "media.device_busy",
2644
+ device_constraint_invalid: "media.device_constraint_invalid",
2645
+ media_capture_failed: "media.capture_failed",
2581
2646
  sync_start_failed: "connection.sync_start_failed",
2582
2647
  media_start_failed: "connection.media_start_failed",
2583
2648
  join_cleanup_unconfirmed: "connection.join_cleanup_unconfirmed",
@@ -2610,6 +2675,9 @@ function normalizeClientError(cause, fallback = "client.internal_error") {
2610
2675
  message: cause.message
2611
2676
  });
2612
2677
  }
2678
+ if (cause instanceof V1SyncError && ["signaling_timeout", "negotiation_timeout", "media_timeout"].includes(cause.code)) {
2679
+ return new SpaceClientError({ code: "media.timeout", recoverable: true, message: "Media did not respond in time. Try again." });
2680
+ }
2613
2681
  if (isAccessInvalid(cause)) {
2614
2682
  return new SpaceClientError({ code: "access.invalid", recoverable: true, message: "The access grant was rejected" });
2615
2683
  }
@@ -2633,6 +2701,7 @@ function connectionErrorCode(code, action) {
2633
2701
 
2634
2702
  // src/space-client/store.ts
2635
2703
  import { Context as Context4, Effect as Effect3, Layer as Layer4, SubscriptionRef as SubscriptionRef2 } from "effect";
2704
+ var INTERRUPTED_CONNECTION_PHASES = /* @__PURE__ */ new Set(["unresponsive", "connecting", "recovering"]);
2636
2705
  var empty = (value) => Object.freeze(value);
2637
2706
  var EMPTY = empty({
2638
2707
  connection: empty({ status: "idle", episode: null, lastError: null }),
@@ -2648,6 +2717,8 @@ var EMPTY = empty({
2648
2717
  }),
2649
2718
  chat: empty({ status: "idle", messages: empty([]), pendingSends: empty([]), readReceipts: empty([]), unreadCount: 0, pagination: empty({ cursor: null, hasOlder: false, historyTruncated: false }), lastError: null }),
2650
2719
  reactions: empty({ active: empty([]) }),
2720
+ recording: empty({ current: null }),
2721
+ offline: empty({ pending: empty([]), decisionNeeded: false, policy: "send" }),
2651
2722
  whiteboard: empty({ open: false, engine: empty({ status: "unsubscribed", sceneId: null, revision: null, presenting: false, error: null }) })
2652
2723
  });
2653
2724
  var SpaceStore = class _SpaceStore {
@@ -2672,8 +2743,9 @@ var SpaceStore = class _SpaceStore {
2672
2743
  updateConnection(snapshot) {
2673
2744
  const current = this.getSnapshot().connection;
2674
2745
  const error2 = snapshot.failure ? empty({ code: mapCode(snapshot.failure.code), recoverable: snapshot.failure.recoverable, message: snapshot.failure.message }) : null;
2675
- if (current.status === snapshot.state && sameEpisode(current.episode, snapshot.episode) && sameError(current.lastError, error2)) return;
2676
- this.#replace("connection", empty({ status: snapshot.state, episode: snapshot.episode, lastError: error2 }));
2746
+ const status = snapshot.state === "live" && (INTERRUPTED_CONNECTION_PHASES.has(snapshot.connection.sync) || INTERRUPTED_CONNECTION_PHASES.has(snapshot.connection.media)) ? "reconnecting" : snapshot.state;
2747
+ if (current.status === status && sameEpisode(current.episode, snapshot.episode) && sameError(current.lastError, error2)) return;
2748
+ this.#replace("connection", empty({ status, episode: snapshot.episode, lastError: error2 }));
2677
2749
  }
2678
2750
  updateSelf(value) {
2679
2751
  this.#replace("self", value);
@@ -2690,6 +2762,12 @@ var SpaceStore = class _SpaceStore {
2690
2762
  updateReactions(value) {
2691
2763
  this.#replace("reactions", value);
2692
2764
  }
2765
+ updateRecording(value) {
2766
+ this.#replace("recording", value);
2767
+ }
2768
+ updateOffline(value) {
2769
+ this.#replace("offline", value);
2770
+ }
2693
2771
  updateWhiteboard(value) {
2694
2772
  this.#replace("whiteboard", value);
2695
2773
  }
@@ -2728,11 +2806,943 @@ function sameError(left, right) {
2728
2806
  return left.code === right.code && left.recoverable === right.recoverable && left.message === right.message;
2729
2807
  }
2730
2808
 
2809
+ // src/feedback/types.ts
2810
+ var FEEDBACK_EVIDENCE_SCHEMA_VERSION = "FeedbackEvidence/v1";
2811
+ var FEEDBACK_REQUEST_SCHEMA_VERSION = "FeedbackReportRequest/v1";
2812
+ var FEEDBACK_RECEIPT_SCHEMA_VERSION = "FeedbackReportReceipt/v1";
2813
+ var FEEDBACK_SCREENSHOT_SCHEMA_VERSION = "FeedbackScreenshot/v1";
2814
+ var FEEDBACK_LOCAL_STATE_REGISTRY_VERSION = "FeedbackLocalState/v1";
2815
+ var FEEDBACK_COOKIE_REGISTRY_VERSION = "FeedbackCookies/v1";
2816
+ var FEEDBACK_MAX_MESSAGE_BYTES = 8e3;
2817
+ var FEEDBACK_MAX_EVIDENCE_BYTES = 128 * 1024;
2818
+ var FEEDBACK_MAX_REQUEST_BYTES = 1 << 20;
2819
+ var FEEDBACK_MAX_SCREENSHOT_BYTES = 450 * 1024;
2820
+ var FEEDBACK_MAX_SCREENSHOT_WIDTH = 1920;
2821
+ var FEEDBACK_MAX_SCREENSHOT_HEIGHT = 1080;
2822
+ var FEEDBACK_MAX_TELEMETRY_EVENTS = 50;
2823
+ var FEEDBACK_MAX_DIAGNOSTIC_EVENTS = 50;
2824
+ var FEEDBACK_MAX_LOCAL_STATE_ENTRIES = 32;
2825
+ var FEEDBACK_MAX_COOKIE_ENTRIES = 16;
2826
+
2827
+ // src/feedback/validation.ts
2828
+ import { isDiagnosticReference, parseDiagnosticEventDraft, redactDiagnosticAttributes } from "@q9labsai/diagnostics-contracts";
2829
+ var encoder3 = new TextEncoder();
2830
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
2831
+ var TRACE_ID_PATTERN = /^(?=.*[1-9a-f])[0-9a-f]{32}$/u;
2832
+ var SPAN_ID_PATTERN = /^(?=.*[1-9a-f])[0-9a-f]{16}$/u;
2833
+ var TRACEPARENT_PATTERN = /^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/iu;
2834
+ var SAFE_TOKEN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@+/=-]{0,127}$/u;
2835
+ var DASHBOARD_ACTION_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/u;
2836
+ var UNSAFE_VALUE_PATTERN = /(?:https?:\/\/|wss?:\/\/|bearer\s+[a-z0-9._~+\/-]+|-----begin|candidate:|v=0\r?\n|\b(?:\d{1,3}\.){3}\d{1,3}\b|[\w.+-]+@[\w.-]+\.[a-z]{2,})/iu;
2837
+ var TELEMETRY_KEYS = ["version", "event_id", "journey_id", "sequence", "occurred_at", "name", "phase", "state", "origin_kind", "first_observed_layer", "upstream_visibility", "parent_event_id", "trace_id", "span_id", "traceparent", "tracestate", "attributes"];
2838
+ var TELEMETRY_NAMES = ["journey.started", "journey.phase", "journey.terminal", "journey.linked", "http.request", "sync.frame", "rtc.summary", "diagnostic.timeline"];
2839
+ var TELEMETRY_PHASES = ["root", "authentication", "signaling", "media", "recovery", "terminal"];
2840
+ var TELEMETRY_STATES = ["started", "in_progress", "succeeded", "failed", "cancelled", "observed"];
2841
+ var TELEMETRY_ORIGINS = ["client", "diagnostic", "http", "rtc", "sync"];
2842
+ var TELEMETRY_VISIBILITY = ["local", "propagated"];
2843
+ var FeedbackValidationError = class extends Error {
2844
+ issues;
2845
+ constructor(message, issues = [{ path: "$", message }]) {
2846
+ super(message);
2847
+ this.name = "FeedbackValidationError";
2848
+ this.issues = issues;
2849
+ }
2850
+ };
2851
+ function validateFeedbackReportRequest(input) {
2852
+ const issues = [];
2853
+ if (typeof input !== "object" || input === null || Array.isArray(input)) return failure2("$", "expected a feedback request object");
2854
+ checkKeys(input, ["schema_version", "category", "message", "source", "evidence", "screenshot"], issues, "$", false);
2855
+ if (property(input, "schema_version") !== FEEDBACK_REQUEST_SCHEMA_VERSION) issues.push({ path: "$.schema_version", message: "unsupported feedback request schema" });
2856
+ const fields = validateFeedbackRequestFields(input, issues);
2857
+ validateFeedbackScreenshotConsistency(fields, issues);
2858
+ if (!fields.category || !fields.source || !fields.message || !fields.evidence || !fields.screenshot_valid || issues.length > 0) return { ok: false, issues };
2859
+ const request = {
2860
+ schema_version: FEEDBACK_REQUEST_SCHEMA_VERSION,
2861
+ category: fields.category,
2862
+ message: fields.message,
2863
+ source: fields.source,
2864
+ evidence: fields.evidence,
2865
+ ...fields.screenshot ? { screenshot: fields.screenshot } : {}
2866
+ };
2867
+ return validateFeedbackRequestSize(request, issues);
2868
+ }
2869
+ function validateFeedbackRequestFields(input, issues) {
2870
+ const category = enumValue(property(input, "category"), ["bug", "feature_request", "other"], "$.category", issues);
2871
+ const source = enumValue(property(input, "source"), ["embedded", "chalk_web", "chalk_mobile", "dashboard"], "$.source", issues);
2872
+ const message = validateMessage(property(input, "message"), issues);
2873
+ const evidenceResult = validateFeedbackEvidence(property(input, "evidence"));
2874
+ appendIssues(issues, evidenceResult);
2875
+ const screenshotResult = validateRequestScreenshot(property(input, "screenshot"));
2876
+ appendIssues(issues, screenshotResult);
2877
+ return {
2878
+ category,
2879
+ source,
2880
+ message,
2881
+ evidence: evidenceResult.ok ? evidenceResult.value : void 0,
2882
+ screenshot: screenshotResult.ok ? screenshotResult.value : void 0,
2883
+ screenshot_valid: screenshotResult.ok
2884
+ };
2885
+ }
2886
+ function validateRequestScreenshot(input) {
2887
+ return input === void 0 ? { ok: true, value: void 0 } : validateFeedbackScreenshot(input);
2888
+ }
2889
+ function validateFeedbackScreenshotConsistency(fields, issues) {
2890
+ if (!fields.evidence) return;
2891
+ if (!fields.screenshot && requiresScreenshot(fields.evidence)) {
2892
+ issues.push({ path: "$.screenshot", message: "a captured screenshot payload is required" });
2893
+ return;
2894
+ }
2895
+ if (!fields.screenshot) return;
2896
+ if (!requiresScreenshot(fields.evidence)) issues.push({ path: "$.evidence.screenshot.state", message: "screenshot state must indicate a capture" });
2897
+ if (fields.evidence.screenshot.captured_at !== fields.screenshot.captured_at) issues.push({ path: "$.screenshot.captured_at", message: "screenshot timestamps must match" });
2898
+ }
2899
+ function requiresScreenshot(evidence) {
2900
+ return evidence.screenshot.state === "captured" || evidence.screenshot.state === "partial";
2901
+ }
2902
+ function validateFeedbackRequestSize(request, issues) {
2903
+ if (encodedByteLength(request) > FEEDBACK_MAX_REQUEST_BYTES) issues.push({ path: "$", message: "feedback request exceeds 1 MiB" });
2904
+ return issues.length === 0 ? { ok: true, value: request } : { ok: false, issues };
2905
+ }
2906
+ function appendIssues(issues, result) {
2907
+ if (!result.ok) issues.push(...result.issues);
2908
+ }
2909
+ function parseFeedbackReportRequest(input) {
2910
+ const result = validateFeedbackReportRequest(input);
2911
+ if (result.ok) return result.value;
2912
+ throw new FeedbackValidationError("Invalid feedback request", result.issues);
2913
+ }
2914
+ function validateFeedbackEvidence(input) {
2915
+ const issues = [];
2916
+ if (typeof input !== "object" || input === null || Array.isArray(input)) return failure2("$", "expected a feedback evidence object");
2917
+ checkKeys(input, ["schema_version", "collected_at", "app", "sdk", "platform", "connection", "scope", "correlations", "diagnostics", "local_state", "cookies", "screenshot"], issues, "$", false);
2918
+ if (property(input, "schema_version") !== FEEDBACK_EVIDENCE_SCHEMA_VERSION) issues.push({ path: "$.schema_version", message: "unsupported feedback evidence schema" });
2919
+ const app = validateApp(property(input, "app"), issues);
2920
+ const sdk = validateSDK(property(input, "sdk"), issues);
2921
+ const platform = validatePlatform(property(input, "platform"), issues);
2922
+ const connection = validateConnection(property(input, "connection"), issues);
2923
+ const scope = validateScope(property(input, "scope"), issues);
2924
+ const correlations = validateCorrelations(property(input, "correlations"), issues);
2925
+ const diagnostics = validateDiagnostics(property(input, "diagnostics"), issues);
2926
+ const localState = validateLocalState(property(input, "local_state"), issues);
2927
+ const cookies = validateCookies(property(input, "cookies"), issues);
2928
+ const screenshot = validateScreenshotState(property(input, "screenshot"), issues);
2929
+ if (!sdk || !platform || !correlations || !diagnostics || !localState || !cookies || !screenshot || issues.length > 0) return { ok: false, issues };
2930
+ const collectedAt = checkDateTimeValue(property(input, "collected_at"), "$.collected_at", issues);
2931
+ if (!collectedAt) return { ok: false, issues };
2932
+ const evidence = {
2933
+ schema_version: FEEDBACK_EVIDENCE_SCHEMA_VERSION,
2934
+ collected_at: collectedAt,
2935
+ ...app ? { app } : {},
2936
+ sdk,
2937
+ platform,
2938
+ ...connection ? { connection } : {},
2939
+ ...scope ? { scope } : {},
2940
+ correlations,
2941
+ diagnostics,
2942
+ local_state: localState,
2943
+ cookies,
2944
+ screenshot
2945
+ };
2946
+ if (encodedByteLength(evidence) > FEEDBACK_MAX_EVIDENCE_BYTES) return failure2("$", "feedback evidence exceeds 128 KiB");
2947
+ return { ok: true, value: evidence };
2948
+ }
2949
+ function parseFeedbackEvidence(input) {
2950
+ const result = validateFeedbackEvidence(input);
2951
+ if (result.ok) return result.value;
2952
+ throw new FeedbackValidationError("Invalid feedback evidence", result.issues);
2953
+ }
2954
+ function validateFeedbackScreenshot(input) {
2955
+ const issues = [];
2956
+ if (typeof input !== "object" || input === null || Array.isArray(input)) return failure2("$", "expected a screenshot object");
2957
+ checkKeys(input, ["schema_version", "mime_type", "width", "height", "captured_at", "data_base64"], issues, "$", false);
2958
+ if (property(input, "schema_version") !== FEEDBACK_SCREENSHOT_SCHEMA_VERSION) issues.push({ path: "$.schema_version", message: "unsupported screenshot schema" });
2959
+ const mimeType = enumValue(property(input, "mime_type"), ["image/jpeg", "image/png", "image/webp"], "$.mime_type", issues);
2960
+ const width = boundedInteger2(property(input, "width"), 1, FEEDBACK_MAX_SCREENSHOT_WIDTH, "$.width", issues);
2961
+ const height = boundedInteger2(property(input, "height"), 1, FEEDBACK_MAX_SCREENSHOT_HEIGHT, "$.height", issues);
2962
+ const capturedAt = property(input, "captured_at");
2963
+ checkDateTime(capturedAt, "$.captured_at", issues);
2964
+ const dataInput = property(input, "data_base64");
2965
+ const data = typeof dataInput === "string" ? dataInput : void 0;
2966
+ if (!data || !validBase64(data)) issues.push({ path: "$.data_base64", message: "screenshot data must be base64" });
2967
+ else if (base64ByteLength(data) > FEEDBACK_MAX_SCREENSHOT_BYTES) issues.push({ path: "$.data_base64", message: "screenshot exceeds 450 KiB" });
2968
+ if (!mimeType || width === void 0 || height === void 0 || typeof capturedAt !== "string" || !data || issues.length > 0) return { ok: false, issues };
2969
+ return { ok: true, value: { schema_version: FEEDBACK_SCREENSHOT_SCHEMA_VERSION, mime_type: mimeType, width, height, captured_at: capturedAt, data_base64: data } };
2970
+ }
2971
+ function parseFeedbackScreenshot(input) {
2972
+ const result = validateFeedbackScreenshot(input);
2973
+ if (result.ok) return result.value;
2974
+ throw new FeedbackValidationError("Invalid feedback screenshot", result.issues);
2975
+ }
2976
+ function assertFeedbackReceipt(input) {
2977
+ const issues = [];
2978
+ if (typeof input !== "object" || input === null || Array.isArray(input)) throw new FeedbackValidationError("Invalid feedback receipt");
2979
+ checkKeys(input, ["schema_version", "id", "submitted_at"], issues, "$", false);
2980
+ const schemaVersion = property(input, "schema_version");
2981
+ const id = property(input, "id");
2982
+ const submittedAt = property(input, "submitted_at");
2983
+ if (schemaVersion !== FEEDBACK_RECEIPT_SCHEMA_VERSION) issues.push({ path: "$.schema_version", message: "unsupported receipt schema" });
2984
+ if (!isUUID(id)) issues.push({ path: "$.id", message: "receipt ID is not a valid ID" });
2985
+ checkDateTime(submittedAt, "$.submitted_at", issues);
2986
+ if (issues.length > 0 || typeof id !== "string" || typeof submittedAt !== "string") throw new FeedbackValidationError("Invalid feedback receipt", issues);
2987
+ return { schema_version: FEEDBACK_RECEIPT_SCHEMA_VERSION, id, submitted_at: submittedAt };
2988
+ }
2989
+ function assertFeedbackMessage(message) {
2990
+ const issues = [];
2991
+ const normalized = validateMessage(message, issues);
2992
+ if (!normalized) throw new FeedbackValidationError("Invalid feedback message", issues);
2993
+ return normalized;
2994
+ }
2995
+ function validateMessage(value, issues) {
2996
+ if (typeof value !== "string") {
2997
+ issues.push({ path: "$.message", message: "message must be a string" });
2998
+ return void 0;
2999
+ }
3000
+ const message = value.trim();
3001
+ if (invalidMessageLength(message)) issues.push({ path: "$.message", message: "message must be between 1 and 8,000 UTF-8 bytes" });
3002
+ if (hasForbiddenMessageControl(message)) issues.push({ path: "$.message", message: "message contains a forbidden control character" });
3003
+ return hasMessageIssue(issues) ? void 0 : message;
3004
+ }
3005
+ function invalidMessageLength(message) {
3006
+ const bytes = encoder3.encode(message).byteLength;
3007
+ return bytes < 1 || bytes > FEEDBACK_MAX_MESSAGE_BYTES;
3008
+ }
3009
+ function hasForbiddenMessageControl(message) {
3010
+ for (const character of message) if (isForbiddenMessageControl(character)) return true;
3011
+ return false;
3012
+ }
3013
+ function isForbiddenMessageControl(character) {
3014
+ return isControl(character) && character !== "\n" && character !== "\r" && character !== " ";
3015
+ }
3016
+ function hasMessageIssue(issues) {
3017
+ return issues.some((issue) => issue.path === "$.message");
3018
+ }
3019
+ function validateApp(input, issues) {
3020
+ if (input === void 0) return void 0;
3021
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
3022
+ issues.push({ path: "$.app", message: "app must be an object" });
3023
+ return void 0;
3024
+ }
3025
+ checkKeys(input, ["name", "version", "build"], issues, "$.app", false);
3026
+ const name = metadata(property(input, "name"), "$.app.name", issues, true);
3027
+ const version = metadata(property(input, "version"), "$.app.version", issues, false);
3028
+ const build = metadata(property(input, "build"), "$.app.build", issues, false);
3029
+ return name ? { name, ...version ? { version } : {}, ...build ? { build } : {} } : void 0;
3030
+ }
3031
+ function validateSDK(input, issues) {
3032
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
3033
+ issues.push({ path: "$.sdk", message: "sdk must be an object" });
3034
+ return void 0;
3035
+ }
3036
+ checkKeys(input, ["client", "react", "react_native"], issues, "$.sdk", false);
3037
+ const client = metadata(property(input, "client"), "$.sdk.client", issues, true);
3038
+ const react = metadata(property(input, "react"), "$.sdk.react", issues, false);
3039
+ const reactNative = metadata(property(input, "react_native"), "$.sdk.react_native", issues, false);
3040
+ return client ? { client, ...react ? { react } : {}, ...reactNative ? { react_native: reactNative } : {} } : void 0;
3041
+ }
3042
+ function validatePlatform(input, issues) {
3043
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
3044
+ issues.push({ path: "$.platform", message: "platform must be an object" });
3045
+ return void 0;
3046
+ }
3047
+ checkKeys(input, ["kind", "os_name", "os_version", "browser_name", "browser_version", "device_class", "device_model"], issues, "$.platform", false);
3048
+ const kind = enumValue(property(input, "kind"), ["web", "ios", "android", "macos"], "$.platform.kind", issues);
3049
+ const deviceClassInput = property(input, "device_class");
3050
+ const deviceClass = deviceClassInput === void 0 ? void 0 : enumValue(deviceClassInput, ["phone", "tablet", "desktop"], "$.platform.device_class", issues);
3051
+ const values = {
3052
+ os_name: metadata(property(input, "os_name"), "$.platform.os_name", issues, false),
3053
+ os_version: metadata(property(input, "os_version"), "$.platform.os_version", issues, false),
3054
+ browser_name: metadata(property(input, "browser_name"), "$.platform.browser_name", issues, false),
3055
+ browser_version: metadata(property(input, "browser_version"), "$.platform.browser_version", issues, false),
3056
+ device_model: metadata(property(input, "device_model"), "$.platform.device_model", issues, false)
3057
+ };
3058
+ if (!kind) return void 0;
3059
+ return buildFeedbackPlatform(kind, values, deviceClass);
3060
+ }
3061
+ function buildFeedbackPlatform(kind, values, deviceClass) {
3062
+ return {
3063
+ kind,
3064
+ ...values.os_name ? { os_name: values.os_name } : {},
3065
+ ...values.os_version ? { os_version: values.os_version } : {},
3066
+ ...values.browser_name ? { browser_name: values.browser_name } : {},
3067
+ ...values.browser_version ? { browser_version: values.browser_version } : {},
3068
+ ...deviceClass ? { device_class: deviceClass } : {},
3069
+ ...values.device_model ? { device_model: values.device_model } : {}
3070
+ };
3071
+ }
3072
+ function validateConnection(input, issues) {
3073
+ if (input === void 0) return void 0;
3074
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
3075
+ issues.push({ path: "$.connection", message: "connection must be an object" });
3076
+ return void 0;
3077
+ }
3078
+ checkKeys(input, ["state", "error_code"], issues, "$.connection", false);
3079
+ const state = safeToken(property(input, "state"), "$.connection.state", issues, true);
3080
+ const errorCode = safeToken(property(input, "error_code"), "$.connection.error_code", issues, false);
3081
+ return state ? { state, ...errorCode ? { error_code: errorCode } : {} } : void 0;
3082
+ }
3083
+ function validateScope(input, issues) {
3084
+ if (input === void 0) return void 0;
3085
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
3086
+ issues.push({ path: "$.scope", message: "scope must be an object" });
3087
+ return void 0;
3088
+ }
3089
+ checkKeys(input, ["space_id", "episode_id", "participant_id"], issues, "$.scope", false);
3090
+ return buildFeedbackScope(optionalUUID(property(input, "space_id"), "$.scope.space_id", issues), optionalUUID(property(input, "episode_id"), "$.scope.episode_id", issues), optionalUUID(property(input, "participant_id"), "$.scope.participant_id", issues));
3091
+ }
3092
+ function buildFeedbackScope(space, episode, participant) {
3093
+ if (!space && !episode && !participant) return {};
3094
+ return {
3095
+ ...space ? { space_id: space } : {},
3096
+ ...episode ? { episode_id: episode } : {},
3097
+ ...participant ? { participant_id: participant } : {}
3098
+ };
3099
+ }
3100
+ function validateCorrelations(input, issues) {
3101
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
3102
+ issues.push({ path: "$.correlations", message: "correlations must be an object" });
3103
+ return void 0;
3104
+ }
3105
+ checkKeys(input, ["journey_id", "root_journey_id", "trace_id", "span_id", "request_id", "command_id", "diagnostic_reference"], issues, "$.correlations", false);
3106
+ const journey = optionalUUID(property(input, "journey_id"), "$.correlations.journey_id", issues);
3107
+ const root = optionalUUID(property(input, "root_journey_id"), "$.correlations.root_journey_id", issues);
3108
+ const trace = optionalHex(property(input, "trace_id"), TRACE_ID_PATTERN, "$.correlations.trace_id", issues);
3109
+ const span = optionalHex(property(input, "span_id"), SPAN_ID_PATTERN, "$.correlations.span_id", issues);
3110
+ const request = optionalUUID(property(input, "request_id"), "$.correlations.request_id", issues);
3111
+ const command = optionalUUID(property(input, "command_id"), "$.correlations.command_id", issues);
3112
+ const diagnostic = validateDiagnosticReference(property(input, "diagnostic_reference"), issues);
3113
+ return buildFeedbackCorrelations(journey, root, trace, span, request, command, diagnostic);
3114
+ }
3115
+ function validateDiagnosticReference(input, issues) {
3116
+ if (input === void 0) return void 0;
3117
+ if (typeof input !== "string" || !isDiagnosticReference(input)) {
3118
+ issues.push({ path: "$.correlations.diagnostic_reference", message: "diagnostic reference is malformed" });
3119
+ return void 0;
3120
+ }
3121
+ return input;
3122
+ }
3123
+ function buildFeedbackCorrelations(journey, root, trace, span, request, command, diagnostic) {
3124
+ return {
3125
+ ...journey ? { journey_id: journey } : {},
3126
+ ...root ? { root_journey_id: root } : {},
3127
+ ...trace ? { trace_id: trace } : {},
3128
+ ...span ? { span_id: span } : {},
3129
+ ...request ? { request_id: request } : {},
3130
+ ...command ? { command_id: command } : {},
3131
+ ...diagnostic ? { diagnostic_reference: diagnostic } : {}
3132
+ };
3133
+ }
3134
+ function validateDiagnostics(input, issues) {
3135
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
3136
+ issues.push({ path: "$.diagnostics", message: "diagnostics must be an object" });
3137
+ return void 0;
3138
+ }
3139
+ checkKeys(input, ["availability", "dropped_count", "telemetry_events", "diagnostic_events"], issues, "$.diagnostics", false);
3140
+ const availability = enumValue(property(input, "availability"), ["available", "disabled", "disposed", "unavailable"], "$.diagnostics.availability", issues);
3141
+ const dropped = boundedInteger2(property(input, "dropped_count"), 0, Number.MAX_SAFE_INTEGER, "$.diagnostics.dropped_count", issues);
3142
+ const telemetry = validateTelemetryEvents(property(input, "telemetry_events"), issues);
3143
+ const diagnostic = validateDiagnosticEvents(property(input, "diagnostic_events"), issues);
3144
+ if (!availability || dropped === void 0 || !telemetry || !diagnostic) return void 0;
3145
+ return { availability, dropped_count: dropped, telemetry_events: telemetry, diagnostic_events: diagnostic };
3146
+ }
3147
+ function validateTelemetryEvents(input, issues) {
3148
+ if (!Array.isArray(input)) {
3149
+ issues.push({ path: "$.diagnostics.telemetry_events", message: "telemetry_events must be an array" });
3150
+ return void 0;
3151
+ }
3152
+ if (input.length > FEEDBACK_MAX_TELEMETRY_EVENTS) issues.push({ path: "$.diagnostics.telemetry_events", message: "too many telemetry events" });
3153
+ const events = [];
3154
+ input.slice(0, FEEDBACK_MAX_TELEMETRY_EVENTS).forEach((event, index) => {
3155
+ const parsed = sanitizeTelemetryEvent(event, `$.diagnostics.telemetry_events[${index}]`, issues);
3156
+ if (parsed) events.push(parsed);
3157
+ });
3158
+ return events;
3159
+ }
3160
+ function validateDiagnosticEvents(input, issues) {
3161
+ if (!Array.isArray(input)) {
3162
+ issues.push({ path: "$.diagnostics.diagnostic_events", message: "diagnostic_events must be an array" });
3163
+ return void 0;
3164
+ }
3165
+ if (input.length > FEEDBACK_MAX_DIAGNOSTIC_EVENTS) issues.push({ path: "$.diagnostics.diagnostic_events", message: "too many diagnostic events" });
3166
+ const events = [];
3167
+ input.slice(0, FEEDBACK_MAX_DIAGNOSTIC_EVENTS).forEach((event, index) => {
3168
+ if (typeof event !== "object" || event === null || Array.isArray(event)) {
3169
+ issues.push({ path: `$.diagnostics.diagnostic_events[${index}]`, message: "diagnostic event must be an object" });
3170
+ return;
3171
+ }
3172
+ const attributes = redactDiagnosticAttributes(property(event, "attributes")).attributes;
3173
+ const sanitized = { ...event, ...Object.keys(attributes).length > 0 ? { attributes } : {} };
3174
+ try {
3175
+ events.push(parseDiagnosticEventDraft(sanitized));
3176
+ } catch {
3177
+ issues.push({ path: `$.diagnostics.diagnostic_events[${index}]`, message: "diagnostic event failed the diagnostics contract" });
3178
+ }
3179
+ });
3180
+ return events;
3181
+ }
3182
+ function sanitizeTelemetryEvent(input, path, issues) {
3183
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
3184
+ issues.push({ path, message: "telemetry event must be an object" });
3185
+ return void 0;
3186
+ }
3187
+ checkKeys(input, TELEMETRY_KEYS, issues, path, false);
3188
+ const identity = validateTelemetryEventIdentity(input, path, issues);
3189
+ const shape = validateTelemetryEventShape(input, path, issues);
3190
+ if (!identity || !shape) return void 0;
3191
+ const optional = validateTelemetryEventOptionalFields(input, path, issues);
3192
+ return buildSanitizedTelemetryEvent(identity, shape, optional);
3193
+ }
3194
+ function buildSanitizedTelemetryEvent(identity, shape, optional) {
3195
+ const event = {
3196
+ version: 1,
3197
+ event_id: identity.eventId,
3198
+ journey_id: identity.journeyId,
3199
+ sequence: identity.sequence,
3200
+ occurred_at: identity.occurredAt,
3201
+ name: shape.name,
3202
+ phase: shape.phase,
3203
+ state: shape.state,
3204
+ origin_kind: shape.origin,
3205
+ first_observed_layer: shape.firstObserved,
3206
+ upstream_visibility: shape.visibility,
3207
+ ...optional.parentEvent ? { parent_event_id: optional.parentEvent } : {},
3208
+ ...optional.traceId ? { trace_id: optional.traceId } : {},
3209
+ ...optional.spanId ? { span_id: optional.spanId } : {},
3210
+ ...optional.traceparent ? { traceparent: optional.traceparent } : {},
3211
+ ...optional.tracestate ? { tracestate: optional.tracestate } : {},
3212
+ ...optional.attributes ? { attributes: optional.attributes } : {}
3213
+ };
3214
+ return isSanitizedTelemetryEvent(event) ? event : void 0;
3215
+ }
3216
+ function validateTelemetryEventIdentity(input, path, issues) {
3217
+ const version = property(input, "version") === 1;
3218
+ if (!version) issues.push({ path: `${path}.version`, message: "unsupported telemetry version" });
3219
+ const eventId = requiredUUID(property(input, "event_id"), `${path}.event_id`, issues);
3220
+ const journeyId = requiredUUID(property(input, "journey_id"), `${path}.journey_id`, issues);
3221
+ const sequence = boundedInteger2(property(input, "sequence"), 0, Number.MAX_SAFE_INTEGER, `${path}.sequence`, issues);
3222
+ const occurredAt = checkDateTimeValue(property(input, "occurred_at"), `${path}.occurred_at`, issues);
3223
+ if (!version || !eventId || !journeyId || sequence === void 0 || !occurredAt) return void 0;
3224
+ return { eventId, journeyId, sequence, occurredAt };
3225
+ }
3226
+ function validateTelemetryEventShape(input, path, issues) {
3227
+ const name = enumValue(property(input, "name"), TELEMETRY_NAMES, `${path}.name`, issues);
3228
+ const phase = enumValue(property(input, "phase"), TELEMETRY_PHASES, `${path}.phase`, issues);
3229
+ const state = enumValue(property(input, "state"), TELEMETRY_STATES, `${path}.state`, issues);
3230
+ const origin = enumValue(property(input, "origin_kind"), TELEMETRY_ORIGINS, `${path}.origin_kind`, issues);
3231
+ const firstObserved = enumValue(property(input, "first_observed_layer"), TELEMETRY_ORIGINS, `${path}.first_observed_layer`, issues);
3232
+ const visibility = enumValue(property(input, "upstream_visibility"), TELEMETRY_VISIBILITY, `${path}.upstream_visibility`, issues);
3233
+ if (!name || !phase || !state || !origin || !firstObserved || !visibility) return void 0;
3234
+ return { name, phase, state, origin, firstObserved, visibility };
3235
+ }
3236
+ function validateTelemetryEventOptionalFields(input, path, issues) {
3237
+ return {
3238
+ parentEvent: optionalUUID(property(input, "parent_event_id"), `${path}.parent_event_id`, issues),
3239
+ traceId: optionalHex(property(input, "trace_id"), TRACE_ID_PATTERN, `${path}.trace_id`, issues),
3240
+ spanId: optionalHex(property(input, "span_id"), SPAN_ID_PATTERN, `${path}.span_id`, issues),
3241
+ traceparent: optionalTraceparent(property(input, "traceparent"), `${path}.traceparent`, issues),
3242
+ tracestate: optionalSafeText(property(input, "tracestate"), `${path}.tracestate`, 512, issues),
3243
+ attributes: sanitizeTelemetryAttributes(property(input, "attributes"), `${path}.attributes`, issues)
3244
+ };
3245
+ }
3246
+ function isSanitizedTelemetryEvent(value) {
3247
+ switch (value.name) {
3248
+ case "sync.frame":
3249
+ return value.origin_kind === "sync";
3250
+ case "http.request":
3251
+ return value.origin_kind === "http";
3252
+ case "rtc.summary":
3253
+ return value.origin_kind === "rtc";
3254
+ case "diagnostic.timeline":
3255
+ return value.origin_kind === "diagnostic";
3256
+ case "journey.started":
3257
+ return isJourneyStartedEvent(value);
3258
+ case "journey.phase":
3259
+ return isJourneyPhaseEvent(value);
3260
+ case "journey.terminal":
3261
+ return isJourneyTerminalEvent(value);
3262
+ case "journey.linked":
3263
+ return isJourneyLinkedEvent(value);
3264
+ }
3265
+ }
3266
+ function isJourneyStartedEvent(value) {
3267
+ return value.origin_kind === "client" && value.phase === "root" && value.state === "started";
3268
+ }
3269
+ function isJourneyPhaseEvent(value) {
3270
+ return value.origin_kind === "client" && value.phase !== "root" && value.phase !== "terminal" && (value.state === "in_progress" || value.state === "observed");
3271
+ }
3272
+ function isJourneyTerminalEvent(value) {
3273
+ return value.origin_kind === "client" && value.phase === "terminal" && (value.state === "succeeded" || value.state === "failed" || value.state === "cancelled");
3274
+ }
3275
+ function isJourneyLinkedEvent(value) {
3276
+ return value.origin_kind === "client" && value.phase === "root" && value.state === "observed" && typeof value.parent_event_id === "string";
3277
+ }
3278
+ function sanitizeTelemetryAttributes(input, path, issues) {
3279
+ if (input === void 0) return void 0;
3280
+ if (!isTelemetryAttributeObject(input)) {
3281
+ issues.push({ path, message: "telemetry attributes are not safe" });
3282
+ return void 0;
3283
+ }
3284
+ const redacted = redactDiagnosticAttributes(input).attributes;
3285
+ const normalized = normalizeTelemetryAttributes(redacted);
3286
+ if (!normalized) return void 0;
3287
+ for (const [key, value] of Object.entries(normalized)) if (typeof value === "string" && (value.length > 256 || isControlText(value) || UNSAFE_VALUE_PATTERN.test(value))) issues.push({ path: `${path}.${key}`, message: "telemetry attribute value is unsafe" });
3288
+ return normalized;
3289
+ }
3290
+ function isTelemetryAttributeObject(value) {
3291
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
3292
+ return Object.keys(value).every((key) => {
3293
+ const entry = property(value, key);
3294
+ return typeof entry === "boolean" || typeof entry === "string" || typeof entry === "number" && Number.isFinite(entry);
3295
+ });
3296
+ }
3297
+ function validateLocalState(input, issues) {
3298
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
3299
+ issues.push({ path: "$.local_state", message: "local_state must be an object" });
3300
+ return void 0;
3301
+ }
3302
+ checkKeys(input, ["registry_version", "entries"], issues, "$.local_state", false);
3303
+ if (property(input, "registry_version") !== FEEDBACK_LOCAL_STATE_REGISTRY_VERSION) issues.push({ path: "$.local_state.registry_version", message: "unsupported local state registry" });
3304
+ const entriesInput = property(input, "entries");
3305
+ if (!Array.isArray(entriesInput)) {
3306
+ issues.push({ path: "$.local_state.entries", message: "local state entries must be an array" });
3307
+ return void 0;
3308
+ }
3309
+ if (entriesInput.length > FEEDBACK_MAX_LOCAL_STATE_ENTRIES) issues.push({ path: "$.local_state.entries", message: "too many local state entries" });
3310
+ const entries = [];
3311
+ const seen = /* @__PURE__ */ new Set();
3312
+ entriesInput.slice(0, FEEDBACK_MAX_LOCAL_STATE_ENTRIES).forEach((entry, index) => {
3313
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
3314
+ issues.push({ path: `$.local_state.entries[${index}]`, message: "local state entry must be an object" });
3315
+ return;
3316
+ }
3317
+ checkKeys(entry, ["key", "value"], issues, `$.local_state.entries[${index}]`, false);
3318
+ const key = property(entry, "key");
3319
+ const entryValue = property(entry, "value");
3320
+ if (typeof key !== "string" || key.length === 0 || seen.has(key)) {
3321
+ issues.push({ path: `$.local_state.entries[${index}].key`, message: "local state key is invalid or duplicated" });
3322
+ return;
3323
+ }
3324
+ seen.add(key);
3325
+ if (!validLocalStateEntry(key, entryValue)) issues.push({ path: `$.local_state.entries[${index}]`, message: "local state entry is not allowlisted" });
3326
+ else if (isFeedbackLocalStateValue(entryValue)) entries.push({ key, value: entryValue });
3327
+ });
3328
+ return { registry_version: FEEDBACK_LOCAL_STATE_REGISTRY_VERSION, entries };
3329
+ }
3330
+ function validateCookies(input, issues) {
3331
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
3332
+ issues.push({ path: "$.cookies", message: "cookies must be an object" });
3333
+ return void 0;
3334
+ }
3335
+ checkKeys(input, ["registry_version", "entries"], issues, "$.cookies", false);
3336
+ if (property(input, "registry_version") !== FEEDBACK_COOKIE_REGISTRY_VERSION) issues.push({ path: "$.cookies.registry_version", message: "unsupported cookie registry" });
3337
+ const entriesInput = property(input, "entries");
3338
+ if (!Array.isArray(entriesInput)) {
3339
+ issues.push({ path: "$.cookies.entries", message: "cookie entries must be an array" });
3340
+ return void 0;
3341
+ }
3342
+ if (entriesInput.length > FEEDBACK_MAX_COOKIE_ENTRIES) issues.push({ path: "$.cookies.entries", message: "too many cookie entries" });
3343
+ const entries = [];
3344
+ const seen = /* @__PURE__ */ new Set();
3345
+ for (const [index, entry] of entriesInput.slice(0, FEEDBACK_MAX_COOKIE_ENTRIES).entries()) {
3346
+ const parsed = validateCookieEntry(entry, index, seen, issues);
3347
+ if (parsed) entries.push(parsed);
3348
+ }
3349
+ return { registry_version: FEEDBACK_COOKIE_REGISTRY_VERSION, entries };
3350
+ }
3351
+ function validateCookieEntry(input, index, seen, issues) {
3352
+ const path = `$.cookies.entries[${index}]`;
3353
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
3354
+ issues.push({ path, message: "cookie entry must be an object" });
3355
+ return void 0;
3356
+ }
3357
+ checkKeys(input, ["name", "present", "value"], issues, path, false);
3358
+ const name = enumValue(property(input, "name"), ["chalk_theme", "chalk_sidebar_state", "account", "csrf"], `${path}.name`, issues);
3359
+ const present = property(input, "present");
3360
+ if (!name || typeof present !== "boolean" || seen.has(name)) {
3361
+ issues.push({ path, message: "cookie entry is invalid or duplicated" });
3362
+ return void 0;
3363
+ }
3364
+ seen.add(name);
3365
+ return buildCookieEntry(name, present, property(input, "value"), path, issues);
3366
+ }
3367
+ function buildCookieEntry(name, present, input, path, issues) {
3368
+ const value = cookieValue(name, input, `${path}.value`, issues);
3369
+ if (!present && value !== void 0) issues.push({ path: `${path}.value`, message: "absent cookies cannot include values" });
3370
+ return { name, present, ...value !== void 0 ? { value } : {} };
3371
+ }
3372
+ function cookieValue(name, input, path, issues) {
3373
+ if (input === void 0) return void 0;
3374
+ if (typeof input !== "string") {
3375
+ issues.push({ path, message: "cookie value is invalid" });
3376
+ return void 0;
3377
+ }
3378
+ if (name === "account" || name === "csrf") {
3379
+ issues.push({ path, message: "account and CSRF cookie values are never collected" });
3380
+ return void 0;
3381
+ }
3382
+ if (name === "chalk_theme") {
3383
+ const value2 = ["light", "dark", "system"];
3384
+ const match2 = value2.find((candidate) => candidate === input);
3385
+ if (match2 !== void 0) return match2;
3386
+ issues.push({ path, message: "theme cookie value is invalid" });
3387
+ return void 0;
3388
+ }
3389
+ const value = ["true", "false"];
3390
+ const match = value.find((candidate) => candidate === input);
3391
+ if (match !== void 0) return match;
3392
+ issues.push({ path, message: "sidebar cookie value is invalid" });
3393
+ return void 0;
3394
+ }
3395
+ function validateScreenshotState(input, issues) {
3396
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
3397
+ issues.push({ path: "$.screenshot", message: "screenshot state must be an object" });
3398
+ return void 0;
3399
+ }
3400
+ checkKeys(input, ["state", "captured_at", "failure_code"], issues, "$.screenshot", false);
3401
+ const state = enumValue(property(input, "state"), ["captured", "partial", "removed", "unavailable"], "$.screenshot.state", issues);
3402
+ const failureInput = property(input, "failure_code");
3403
+ const failure3 = failureInput === void 0 ? void 0 : enumValue(failureInput, ["capture_failed", "unsupported", "tainted", "secure_surface", "too_large"], "$.screenshot.failure_code", issues);
3404
+ const capturedAtInput = property(input, "captured_at");
3405
+ const capturedAt = capturedAtInput === void 0 ? void 0 : checkDateTimeValue(capturedAtInput, "$.screenshot.captured_at", issues);
3406
+ validateScreenshotStateConsistency(state, capturedAt, failure3, issues);
3407
+ if (!state) return void 0;
3408
+ return { state, ...capturedAt ? { captured_at: capturedAt } : {}, ...failure3 ? { failure_code: failure3 } : {} };
3409
+ }
3410
+ function validateScreenshotStateConsistency(state, capturedAt, failure3, issues) {
3411
+ const captured = state === "captured" || state === "partial";
3412
+ if (captured && !capturedAt) issues.push({ path: "$.screenshot.captured_at", message: "captured screenshots require a timestamp" });
3413
+ if (captured && failure3) issues.push({ path: "$.screenshot.failure_code", message: "captured screenshots cannot have a failure code" });
3414
+ }
3415
+ function validLocalStateEntry(key, value) {
3416
+ if (key === "chalk.tenant-hint") return isUUID(value);
3417
+ if (key === "chalk.web.telemetry.v1" || key === "chalk.mobile.telemetry.v1") return validTelemetrySummary(value);
3418
+ if (key.startsWith("chalk.dashboard-request.")) return DASHBOARD_ACTION_PATTERN.test(key.slice("chalk.dashboard-request.".length)) && value === true;
3419
+ return false;
3420
+ }
3421
+ function isFeedbackLocalStateValue(value) {
3422
+ if (typeof value === "string" || typeof value === "boolean") return true;
3423
+ if (typeof value === "number") return Number.isFinite(value);
3424
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).every((key) => {
3425
+ const entry = property(value, key);
3426
+ return typeof entry === "number" && Number.isFinite(entry);
3427
+ });
3428
+ }
3429
+ function validTelemetrySummary(value) {
3430
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
3431
+ const keys = ["pending_count", "timeline_count", "dropped_count"];
3432
+ if (!keys.some((key) => property(value, key) !== void 0)) return false;
3433
+ return keys.every((key) => {
3434
+ const entry = property(value, key);
3435
+ return entry === void 0 || typeof entry === "number" && Number.isSafeInteger(entry) && entry >= 0 && entry <= 500;
3436
+ });
3437
+ }
3438
+ function metadata(value, path, issues, required) {
3439
+ if (value === void 0 && !required) return void 0;
3440
+ if (typeof value !== "string" || !isSafeMetadata(value)) {
3441
+ issues.push({ path, message: required ? "metadata is required and unsafe" : "metadata is unsafe" });
3442
+ return void 0;
3443
+ }
3444
+ return value;
3445
+ }
3446
+ function isSafeMetadata(value) {
3447
+ return value.length > 0 && value.length <= 128 && value.trim() === value && !isControlText(value) && !UNSAFE_VALUE_PATTERN.test(value);
3448
+ }
3449
+ function safeToken(value, path, issues, required) {
3450
+ if (value === void 0 && !required) return void 0;
3451
+ if (typeof value !== "string" || value.length === 0 || value.length > 128 || !SAFE_TOKEN_PATTERN.test(value) || UNSAFE_VALUE_PATTERN.test(value)) {
3452
+ issues.push({ path, message: required ? "token is required and unsafe" : "token is unsafe" });
3453
+ return void 0;
3454
+ }
3455
+ return value;
3456
+ }
3457
+ function optionalUUID(value, path, issues) {
3458
+ if (value === void 0) return void 0;
3459
+ return requiredUUID(value, path, issues);
3460
+ }
3461
+ function requiredUUID(value, path, issues) {
3462
+ if (typeof value !== "string" || !isUUID(value)) {
3463
+ issues.push({ path, message: "expected a non-zero UUID" });
3464
+ return void 0;
3465
+ }
3466
+ return value;
3467
+ }
3468
+ function optionalHex(value, pattern, path, issues) {
3469
+ if (value === void 0) return void 0;
3470
+ if (typeof value !== "string" || !pattern.test(value.toLowerCase()) || value !== value.toLowerCase()) {
3471
+ issues.push({ path, message: "expected a lower-case non-zero hexadecimal ID" });
3472
+ return void 0;
3473
+ }
3474
+ return value;
3475
+ }
3476
+ function optionalTraceparent(value, path, issues) {
3477
+ if (value === void 0) return void 0;
3478
+ if (typeof value !== "string" || !TRACEPARENT_PATTERN.test(value)) {
3479
+ issues.push({ path, message: "traceparent is malformed" });
3480
+ return void 0;
3481
+ }
3482
+ return value;
3483
+ }
3484
+ function optionalSafeText(value, path, length, issues) {
3485
+ if (value === void 0) return void 0;
3486
+ if (typeof value !== "string" || value.length > length || isControlText(value) || UNSAFE_VALUE_PATTERN.test(value)) {
3487
+ issues.push({ path, message: "text is unsafe" });
3488
+ return void 0;
3489
+ }
3490
+ return value;
3491
+ }
3492
+ function checkDateTime(value, path, issues) {
3493
+ checkDateTimeValue(value, path, issues);
3494
+ }
3495
+ function checkDateTimeValue(value, path, issues) {
3496
+ if (typeof value !== "string" || value.length > 64 || !Number.isFinite(Date.parse(value))) {
3497
+ issues.push({ path, message: "expected an ISO date-time" });
3498
+ return void 0;
3499
+ }
3500
+ return value;
3501
+ }
3502
+ function boundedInteger2(value, minimum, maximum, path, issues) {
3503
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) {
3504
+ issues.push({ path, message: "expected a bounded integer" });
3505
+ return void 0;
3506
+ }
3507
+ return value;
3508
+ }
3509
+ function enumValue(value, values, path, issues) {
3510
+ const match = values.find((candidate) => candidate === value);
3511
+ if (match !== void 0) return match;
3512
+ issues.push({ path, message: `expected one of: ${values.join(", ")}` });
3513
+ return void 0;
3514
+ }
3515
+ function checkKeys(value, allowed, issues, path, allowNullable) {
3516
+ const allowedSet = new Set(allowed);
3517
+ for (const key of Object.keys(value)) if (!allowedSet.has(key) && !(allowNullable && key === "value")) issues.push({ path: `${path}.${key}`, message: "unknown property" });
3518
+ }
3519
+ function failure2(path, message) {
3520
+ return { ok: false, issues: [{ path, message }] };
3521
+ }
3522
+ function property(value, key) {
3523
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
3524
+ return Object.prototype.hasOwnProperty.call(value, key) ? Reflect.get(value, key) : void 0;
3525
+ }
3526
+ function isUUID(value) {
3527
+ return typeof value === "string" && UUID_PATTERN.test(value) && value.replaceAll("0", "").replaceAll("-", "") !== "";
3528
+ }
3529
+ function validBase64(value) {
3530
+ return value.length > 0 && value.length % 4 === 0 && /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(value);
3531
+ }
3532
+ function base64ByteLength(value) {
3533
+ const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
3534
+ return Math.floor(value.length * 3 / 4) - padding;
3535
+ }
3536
+ function encodedByteLength(value) {
3537
+ return encoder3.encode(JSON.stringify(value)).byteLength;
3538
+ }
3539
+ function isControl(character) {
3540
+ return /\p{Cc}/u.test(character);
3541
+ }
3542
+ function isControlText(value) {
3543
+ for (const character of value) if (isControl(character)) return true;
3544
+ return false;
3545
+ }
3546
+
3547
+ // src/feedback/collector.ts
3548
+ import { validateDiagnosticEventDraft } from "@q9labsai/diagnostics-contracts";
3549
+
3550
+ // src/feedback/context.ts
3551
+ function safeSubject(snapshot) {
3552
+ const subject = snapshot.subject;
3553
+ if (!subject) return void 0;
3554
+ return {
3555
+ tenant_id: subject.tenantId,
3556
+ space_id: subject.spaceId,
3557
+ episode_id: subject.episodeId,
3558
+ participant_id: subject.participantId,
3559
+ participant_generation: subject.participantGeneration
3560
+ };
3561
+ }
3562
+
3563
+ // src/feedback/collector.ts
3564
+ function collectFeedbackEvidence(input, context = {}) {
3565
+ const collectedAt = new Date((context.now ?? Date.now)()).toISOString();
3566
+ const evidence = {
3567
+ schema_version: FEEDBACK_EVIDENCE_SCHEMA_VERSION,
3568
+ collected_at: collectedAt,
3569
+ ...input.app ? { app: input.app } : {},
3570
+ sdk: input.sdk,
3571
+ platform: input.platform,
3572
+ ...context.connection ? { connection: context.connection } : {},
3573
+ ...context.scope ? { scope: context.scope } : {},
3574
+ correlations: context.correlations ?? {},
3575
+ diagnostics: context.diagnostics ?? unavailableDiagnostics(),
3576
+ local_state: { registry_version: FEEDBACK_LOCAL_STATE_REGISTRY_VERSION, entries: collectLocalState(input.local_state) },
3577
+ cookies: { registry_version: FEEDBACK_COOKIE_REGISTRY_VERSION, entries: collectCookies(input.cookies) },
3578
+ screenshot: screenshotState(input.screenshot)
3579
+ };
3580
+ return parseFeedbackEvidence(evidence);
3581
+ }
3582
+ function collectFeedbackEvidenceFromContext(input, context) {
3583
+ const snapshot = context.connection();
3584
+ const subject = safeSubject(snapshot);
3585
+ const activeTelemetry = context.telemetry;
3586
+ const activeDiagnostics = context.diagnosticContext();
3587
+ const correlationContext = activeTelemetry ?? activeDiagnostics;
3588
+ const trace = parseTraceparent(correlationContext?.traceparent);
3589
+ const diagnostics = context.diagnosticSnapshot();
3590
+ const availability = context.diagnosticAvailability();
3591
+ return collectFeedbackEvidence(input, {
3592
+ now: context.now,
3593
+ connection: {
3594
+ state: snapshot.state,
3595
+ ...snapshot.failure?.code ? { error_code: snapshot.failure.code } : {}
3596
+ },
3597
+ ...subject ? {
3598
+ scope: {
3599
+ space_id: subject.space_id,
3600
+ episode_id: subject.episode_id,
3601
+ participant_id: subject.participant_id
3602
+ }
3603
+ } : {},
3604
+ correlations: {
3605
+ ...correlationContext ? { journey_id: correlationContext.journeyId } : {},
3606
+ ...activeTelemetry ? { root_journey_id: activeTelemetry.rootJourneyId } : {},
3607
+ ...trace ? { trace_id: trace.traceId, span_id: trace.spanId } : {}
3608
+ },
3609
+ diagnostics: {
3610
+ availability,
3611
+ dropped_count: diagnostics.dropped,
3612
+ telemetry_events: input.local_state?.telemetry?.events ?? [],
3613
+ diagnostic_events: diagnostics.events.filter(isDiagnosticEvent)
3614
+ }
3615
+ });
3616
+ }
3617
+ function collectLocalState(input) {
3618
+ if (!input) return [];
3619
+ const entries = [];
3620
+ if (input.telemetry) {
3621
+ const summary = telemetrySummary(input.telemetry);
3622
+ if (summary) entries.push({ key: input.telemetry.storage_key ?? "chalk.web.telemetry.v1", value: summary });
3623
+ }
3624
+ if (input.tenant_hint && validUUID(input.tenant_hint)) entries.push({ key: "chalk.tenant-hint", value: input.tenant_hint });
3625
+ for (const request of input.dashboard_requests ?? []) {
3626
+ if (!request.pending || !/^[a-z][a-z0-9_-]{0,63}$/u.test(request.action)) continue;
3627
+ entries.push({ key: `chalk.dashboard-request.${request.action}`, value: true });
3628
+ }
3629
+ return entries.slice(0, 32);
3630
+ }
3631
+ function collectCookies(input) {
3632
+ if (!input) return [];
3633
+ const entries = [];
3634
+ if (input.theme !== void 0) entries.push({ name: "chalk_theme", present: true, value: input.theme });
3635
+ if (input.sidebar_state !== void 0) entries.push({ name: "chalk_sidebar_state", present: true, value: input.sidebar_state ? "true" : "false" });
3636
+ if (input.account_present !== void 0) entries.push({ name: "account", present: input.account_present });
3637
+ if (input.csrf_present !== void 0) entries.push({ name: "csrf", present: input.csrf_present });
3638
+ return entries;
3639
+ }
3640
+ function telemetrySummary(input) {
3641
+ const summary = {
3642
+ ...input.pending_count !== void 0 && boundedCount(input.pending_count) ? { pending_count: input.pending_count } : {},
3643
+ ...input.timeline_count !== void 0 && boundedCount(input.timeline_count) ? { timeline_count: input.timeline_count } : {},
3644
+ ...input.dropped_count !== void 0 && boundedCount(input.dropped_count) ? { dropped_count: input.dropped_count } : {}
3645
+ };
3646
+ return Object.keys(summary).length > 0 ? summary : void 0;
3647
+ }
3648
+ function boundedCount(value) {
3649
+ return Number.isSafeInteger(value) && value >= 0 && value <= 500;
3650
+ }
3651
+ function screenshotState(input) {
3652
+ if (!input) return { state: "unavailable", failure_code: "unsupported" };
3653
+ if (input.state === "captured" || input.state === "partial") return { state: input.state, captured_at: input.captured_at };
3654
+ return "failure_code" in input && input.failure_code ? { state: input.state, failure_code: input.failure_code } : { state: input.state };
3655
+ }
3656
+ function unavailableDiagnostics() {
3657
+ return { availability: "unavailable", dropped_count: 0, telemetry_events: [], diagnostic_events: [] };
3658
+ }
3659
+ function isDiagnosticEvent(value) {
3660
+ return validateDiagnosticEventDraft(value).ok;
3661
+ }
3662
+ function validUUID(value) {
3663
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(value) && value.replaceAll("0", "").replaceAll("-", "") !== "";
3664
+ }
3665
+
3666
+ // src/feedback/transport.ts
3667
+ var FeedbackTransportError = class extends Error {
3668
+ code;
3669
+ status;
3670
+ recoverable;
3671
+ constructor(input) {
3672
+ super(input.message);
3673
+ this.name = "FeedbackTransportError";
3674
+ this.code = input.code;
3675
+ this.recoverable = input.recoverable;
3676
+ this.status = input.status;
3677
+ }
3678
+ };
3679
+ function createFeedbackTransport(options) {
3680
+ const fetchImplementation = options.fetch ?? globalThis.fetch;
3681
+ const baseUrl = options.baseUrl.replace(/\/+$/u, "");
3682
+ return {
3683
+ send: async ({ request, idempotencyKey }) => {
3684
+ const parsedRequest = parseFeedbackReportRequest(request);
3685
+ validateTransportBounds(parsedRequest);
3686
+ validateIdempotencyKey(idempotencyKey);
3687
+ const credential = options.credential();
3688
+ if (!credential) throw new FeedbackTransportError({ code: "unauthenticated", recoverable: false, message: "Feedback authentication is unavailable.", status: 401 });
3689
+ const headers = feedbackHeaders(idempotencyKey, credential.token, options.telemetry?.());
3690
+ const response = await sendFeedbackRequest(fetchImplementation, `${baseUrl}/v1/feedback-reports`, headers, parsedRequest);
3691
+ return parseFeedbackResponse(response);
3692
+ }
3693
+ };
3694
+ }
3695
+ function validateIdempotencyKey(idempotencyKey) {
3696
+ if (idempotencyKey.length < 16 || idempotencyKey.length > 128 || !/^[A-Za-z0-9_-]+$/u.test(idempotencyKey)) throw new FeedbackTransportError({ code: "invalid_request", recoverable: false, message: "Feedback idempotency key is invalid.", status: 400 });
3697
+ }
3698
+ function feedbackHeaders(idempotencyKey, credentialToken, telemetry) {
3699
+ const headers = new Headers({ "content-type": "application/json", authorization: `Bearer ${credentialToken}`, "idempotency-key": idempotencyKey });
3700
+ if (!telemetry) return headers;
3701
+ for (const [key, value] of Object.entries(journeyHeaders(telemetry))) headers.set(key, value);
3702
+ return headers;
3703
+ }
3704
+ async function sendFeedbackRequest(fetchImplementation, url, headers, request) {
3705
+ try {
3706
+ return await fetchImplementation(url, { method: "POST", headers, body: JSON.stringify(request) });
3707
+ } catch {
3708
+ throw new FeedbackTransportError({ code: "unavailable", recoverable: true, message: "Feedback service is unavailable." });
3709
+ }
3710
+ }
3711
+ async function parseFeedbackResponse(response) {
3712
+ if (!response.ok) throw responseError3(response.status);
3713
+ let body;
3714
+ try {
3715
+ body = await response.json();
3716
+ } catch {
3717
+ throw new FeedbackTransportError({ code: "invalid_response", recoverable: true, message: "Feedback service returned an invalid receipt.", status: response.status });
3718
+ }
3719
+ try {
3720
+ return assertFeedbackReceipt(body);
3721
+ } catch {
3722
+ throw new FeedbackTransportError({ code: "invalid_response", recoverable: true, message: "Feedback service returned an invalid receipt.", status: response.status });
3723
+ }
3724
+ }
3725
+ function validateTransportBounds(request) {
3726
+ const evidenceBytes = new TextEncoder().encode(JSON.stringify(request.evidence)).byteLength;
3727
+ const requestBytes = new TextEncoder().encode(JSON.stringify(request)).byteLength;
3728
+ if (evidenceBytes > FEEDBACK_MAX_EVIDENCE_BYTES) throw new FeedbackTransportError({ code: "invalid_request", recoverable: false, message: "Feedback evidence exceeds the size limit." });
3729
+ if (requestBytes > FEEDBACK_MAX_REQUEST_BYTES) throw new FeedbackTransportError({ code: "invalid_request", recoverable: false, message: "Feedback request exceeds the size limit." });
3730
+ }
3731
+ function responseError3(status) {
3732
+ if (status === 400) return new FeedbackTransportError({ code: "invalid_request", recoverable: false, message: "Feedback request was rejected.", status });
3733
+ if (status === 413) return new FeedbackTransportError({ code: "invalid_request", recoverable: false, message: "Feedback request exceeds the size limit.", status });
3734
+ if (status === 401) return new FeedbackTransportError({ code: "unauthenticated", recoverable: false, message: "Feedback authentication was rejected.", status });
3735
+ if (status === 403) return new FeedbackTransportError({ code: "forbidden", recoverable: false, message: "Feedback submission is not allowed.", status });
3736
+ if (status === 409) return new FeedbackTransportError({ code: "conflict", recoverable: false, message: "Feedback idempotency key conflicts with another submission.", status });
3737
+ if (status === 429) return new FeedbackTransportError({ code: "rate_limited", recoverable: true, message: "Feedback submissions are temporarily limited.", status });
3738
+ return new FeedbackTransportError({ code: "unavailable", recoverable: true, message: "Feedback service is unavailable.", status });
3739
+ }
3740
+
2731
3741
  // src/space-client/core.ts
2732
- import { Clock as Clock5, Context as Context11, Duration as Duration2, Effect as Effect10, Layer as Layer11 } from "effect";
3742
+ import { Clock as Clock5, Context as Context13, Duration as Duration2, Effect as Effect12, Layer as Layer13 } from "effect";
2733
3743
 
2734
3744
  // src/space-client/controller-effects.ts
2735
- import { Context as Context10, Effect as Effect9, Layer as Layer10 } from "effect";
3745
+ import { Context as Context12, Effect as Effect11, Layer as Layer12 } from "effect";
2736
3746
  import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
2737
3747
 
2738
3748
  // src/space-client/chat-controller.ts
@@ -2744,14 +3754,14 @@ var MAX_CHAT_PAGE_SIZE = 100;
2744
3754
  var MAX_LOADED_CHAT_MESSAGES = 500;
2745
3755
  var MAX_CHAT_TEXT_BYTES = 16384;
2746
3756
  var MAX_CHAT_TEXT_SCALARS = 4e3;
2747
- var encoder3 = new TextEncoder();
3757
+ var encoder4 = new TextEncoder();
2748
3758
  var allowedMimeTypes = new Set(CHALK_CHAT_ATTACHMENT_MIME_TYPES);
2749
3759
  function validateChatMessage(input) {
2750
3760
  const attachments = input.attachments ?? [];
2751
3761
  if (input.text.length === 0 && attachments.length === 0) return "A chat message requires text or an attachment";
2752
- if (Array.from(input.text).length > MAX_CHAT_TEXT_SCALARS || encoder3.encode(input.text).byteLength > MAX_CHAT_TEXT_BYTES) return `Chat text must not exceed ${MAX_CHAT_TEXT_SCALARS} characters or ${MAX_CHAT_TEXT_BYTES} bytes`;
3762
+ if (Array.from(input.text).length > MAX_CHAT_TEXT_SCALARS || encoder4.encode(input.text).byteLength > MAX_CHAT_TEXT_BYTES) return `Chat text must not exceed ${MAX_CHAT_TEXT_SCALARS} characters or ${MAX_CHAT_TEXT_BYTES} bytes`;
2753
3763
  if (attachments.length > CHALK_CHAT_ATTACHMENT_LIMITS.maximumPerMessage) return `A chat message supports at most ${CHALK_CHAT_ATTACHMENT_LIMITS.maximumPerMessage} attachments`;
2754
- if (new Set(attachments.map((attachment) => attachment.attachmentId)).size !== attachments.length) return "Chat attachment IDs must be unique";
3764
+ if (new Set(attachments.map((attachment2) => attachment2.attachmentId)).size !== attachments.length) return "Chat attachment IDs must be unique";
2755
3765
  return attachments.map(validateChatAttachment).find((value) => value !== null) ?? null;
2756
3766
  }
2757
3767
  function validateChatUpload(file, bytes, clientAttachmentId) {
@@ -2782,16 +3792,16 @@ function compareChatSequence(left, right) {
2782
3792
  const normalizedRight = right.replace(/^0+/u, "") || "0";
2783
3793
  return normalizedLeft.length === normalizedRight.length ? normalizedLeft < normalizedRight ? -1 : 1 : normalizedLeft.length - normalizedRight.length;
2784
3794
  }
2785
- function validateChatAttachment(attachment) {
2786
- const fileNameBytes = encoder3.encode(attachment.fileName).byteLength;
2787
- if (!attachment.attachmentId || fileNameBytes < 1 || fileNameBytes > CHALK_CHAT_ATTACHMENT_LIMITS.maximumFileNameBytes) return "Chat attachment metadata is invalid";
2788
- if (!Number.isSafeInteger(attachment.byteLength) || attachment.byteLength < 1 || attachment.byteLength > CHALK_CHAT_ATTACHMENT_LIMITS.maximumByteLength) return "Chat attachment metadata is invalid";
2789
- return allowedMimeTypes.has(attachment.mimeType) ? null : "Chat attachment MIME type is not allowed";
3795
+ function validateChatAttachment(attachment2) {
3796
+ const fileNameBytes = encoder4.encode(attachment2.fileName).byteLength;
3797
+ if (!attachment2.attachmentId || fileNameBytes < 1 || fileNameBytes > CHALK_CHAT_ATTACHMENT_LIMITS.maximumFileNameBytes) return "Chat attachment metadata is invalid";
3798
+ if (!Number.isSafeInteger(attachment2.byteLength) || attachment2.byteLength < 1 || attachment2.byteLength > CHALK_CHAT_ATTACHMENT_LIMITS.maximumByteLength) return "Chat attachment metadata is invalid";
3799
+ return allowedMimeTypes.has(attachment2.mimeType) ? null : "Chat attachment MIME type is not allowed";
2790
3800
  }
2791
3801
  function sameChatMessage(left, right) {
2792
- return left.messageId === right.messageId && left.clientMessageId === right.clientMessageId && left.sequence === right.sequence && left.participantId === right.participantId && left.displayName === right.displayName && left.text === right.text && left.createdAt === right.createdAt && left.attachments.length === right.attachments.length && left.attachments.every((attachment, index) => {
3802
+ return left.messageId === right.messageId && left.clientMessageId === right.clientMessageId && left.sequence === right.sequence && left.participantId === right.participantId && left.displayName === right.displayName && left.text === right.text && left.createdAt === right.createdAt && left.attachments.length === right.attachments.length && left.attachments.every((attachment2, index) => {
2793
3803
  const candidate = right.attachments[index];
2794
- return candidate?.attachmentId === attachment.attachmentId && candidate.fileName === attachment.fileName && candidate.mimeType === attachment.mimeType && candidate.byteLength === attachment.byteLength;
3804
+ return candidate?.attachmentId === attachment2.attachmentId && candidate.fileName === attachment2.fileName && candidate.mimeType === attachment2.mimeType && candidate.byteLength === attachment2.byteLength;
2795
3805
  });
2796
3806
  }
2797
3807
 
@@ -2807,7 +3817,7 @@ var makeChatController = (input) => Effect4.gen(function* () {
2807
3817
  const controller = yield* Effect4.sync(() => {
2808
3818
  let instance = null;
2809
3819
  const transport = input.createTransport?.({ token: () => Effect4.runPromiseWith(context)(input.connection.getSyncToken()) }) ?? null;
2810
- instance = new ChatControllerRuntime(input.connection, input.store, transport, input.apiBaseUrl ?? "https://api.chalkmeet.com", input.fetch ?? globalThis.fetch, fork, input.episodeDiagnostics);
3820
+ instance = new ChatControllerRuntime(input.connection, input.store, transport, input.apiBaseUrl ?? "https://api.chalkmeet.com", input.fetch ?? globalThis.fetch, fork, input.episodeDiagnostics, input.offline);
2811
3821
  return instance;
2812
3822
  });
2813
3823
  yield* Effect4.addFinalizer(() => Effect4.sync(() => controller.dispose()));
@@ -2821,6 +3831,7 @@ var ChatControllerRuntime = class {
2821
3831
  #fetch;
2822
3832
  #fork;
2823
3833
  #diagnostics;
3834
+ #offline;
2824
3835
  #catchUpRunning = false;
2825
3836
  #catchUpRequested = false;
2826
3837
  #hasOlder = false;
@@ -2838,7 +3849,8 @@ var ChatControllerRuntime = class {
2838
3849
  #unsubscribeConnection = null;
2839
3850
  #unsubscribeEvents = null;
2840
3851
  #unsubscribeSnapshot = null;
2841
- constructor(connection, store, transport, apiBaseUrl, fetch, fork, diagnostics) {
3852
+ constructor(connection, store, transport, apiBaseUrl, fetch, fork, diagnostics, offline) {
3853
+ this.#offline = offline;
2842
3854
  this.#apiBaseUrl = apiBaseUrl.replace(/\/+$/u, "");
2843
3855
  this.#connection = connection;
2844
3856
  this.#store = store;
@@ -2854,9 +3866,12 @@ var ChatControllerRuntime = class {
2854
3866
  Effect4.tap(() => Effect4.sync(() => operation?.observe("observed", "validation"))),
2855
3867
  Effect4.map((attachments) => ({ clientMessageId: this.#connection.createId(), attachments })),
2856
3868
  Effect4.flatMap(({ clientMessageId, attachments }) => {
2857
- this.#upsertPending(clientMessageId, input.text, attachments, "sending", null);
3869
+ const command = this.#connection.runCommand(({ sync }) => foreign(() => sync.sendChatMessage({ text: input.text, attachments, clientMessageId })));
3870
+ const sendNow = () => Effect4.sync(() => this.#upsertPending(clientMessageId, input.text, attachments, "sending", null)).pipe(Effect4.andThen(command));
3871
+ const held = this.#offline?.hold({ kind: "chat_message", text: input.text, clientMessageId, attachments }, sendNow);
3872
+ if (held) this.#upsertPending(clientMessageId, input.text, attachments, "queued", null);
2858
3873
  operation?.observe("observed", "authorization");
2859
- return this.#connection.runCommand(({ sync }) => foreign(() => sync.sendChatMessage({ text: input.text, attachments, clientMessageId }))).pipe(Effect4.tap(() => Effect4.sync(() => operation?.observe("observed", "durable_commit")))).pipe(
3874
+ return (held ?? sendNow()).pipe(Effect4.tap(() => Effect4.sync(() => operation?.observe("observed", "durable_commit")))).pipe(
2860
3875
  Effect4.map(chatMessageFor),
2861
3876
  Effect4.tap(() => Effect4.sync(() => this.#removePending(clientMessageId))),
2862
3877
  Effect4.map((message) => this.#observeMessage(message, false)),
@@ -2870,7 +3885,8 @@ var ChatControllerRuntime = class {
2870
3885
  Effect4.catch((cause) => {
2871
3886
  const error2 = normalizeClientError(cause, "chat.payload_invalid");
2872
3887
  return Effect4.sync(() => {
2873
- this.#upsertPending(clientMessageId, input.text, attachments, "failed", failureFromError(error2));
3888
+ if (error2.code === "offline.discarded") this.#removePending(clientMessageId);
3889
+ else this.#upsertPending(clientMessageId, input.text, attachments, "failed", failureFromError(error2));
2874
3890
  operation?.fail("send_failed");
2875
3891
  }).pipe(Effect4.andThen(Effect4.fail(error2)));
2876
3892
  })
@@ -2930,6 +3946,7 @@ var ChatControllerRuntime = class {
2930
3946
  });
2931
3947
  };
2932
3948
  upload = (file) => {
3949
+ const fetch = this.#fetch;
2933
3950
  const prepareOperation = this.#diagnostics?.startOperation("chat.attachment.prepare");
2934
3951
  return Effect4.suspend(() => {
2935
3952
  if (!this.#transport) {
@@ -2950,7 +3967,7 @@ var ChatControllerRuntime = class {
2950
3967
  prepareOperation?.succeed();
2951
3968
  }).pipe(
2952
3969
  Effect4.andThen(
2953
- foreign(() => this.#fetch(upload.uploadUrl, { method: upload.method, headers: upload.headers, body: bytes })).pipe(
3970
+ foreign(() => fetch(upload.uploadUrl, { method: upload.method, headers: upload.headers, body: bytes })).pipe(
2954
3971
  Effect4.flatMap((response) => {
2955
3972
  const commitOperation = this.#diagnostics?.startOperation("chat.attachment.commit");
2956
3973
  if (!response.ok) {
@@ -2988,7 +4005,11 @@ var ChatControllerRuntime = class {
2988
4005
  );
2989
4006
  });
2990
4007
  };
2991
- url = (attachment) => `${this.#apiBaseUrl}/v1/chat/attachments/${encodeURIComponent(attachment.attachmentId)}/download`;
4008
+ url = (attachment2) => `${this.#apiBaseUrl}/v1/chat/attachments/${encodeURIComponent(attachment2.attachmentId)}/download`;
4009
+ resolveUrl = (attachment2) => Effect4.suspend(() => {
4010
+ if (!this.#transport) return Effect4.fail(new SpaceClientError({ code: "collaboration.unavailable", recoverable: false, message: "Chat file download is unavailable" }));
4011
+ return this.#connection.runPortCommand(() => foreign(() => this.#transport.getDownloadUrl(attachment2.attachmentId)).pipe(Effect4.map((descriptor) => descriptor.downloadUrl))).pipe(Effect4.mapError(normalizeClientError));
4012
+ });
2992
4013
  dispose() {
2993
4014
  this.#unsubscribeConnection?.();
2994
4015
  this.#unsubscribeConnection = null;
@@ -3190,7 +4211,7 @@ function validateUpload(file, bytes, clientAttachmentId) {
3190
4211
  return upload;
3191
4212
  }
3192
4213
  function bytesFor(file) {
3193
- return "bytes" in file ? Effect4.succeed(file.bytes) : foreign(() => file.arrayBuffer());
4214
+ return "fileName" in file ? Effect4.succeed(file.bytes) : foreign(() => file.arrayBuffer());
3194
4215
  }
3195
4216
  function catchUpRequest(latestSequence, ports) {
3196
4217
  const extension = ports.sync.getCollaborationExtensionState();
@@ -3242,6 +4263,10 @@ var MediaControllerRuntime = class {
3242
4263
  #fork;
3243
4264
  #diagnostics;
3244
4265
  #tracks = /* @__PURE__ */ new Map();
4266
+ #observedPublicationIds = /* @__PURE__ */ new Map();
4267
+ #intentRevisions = /* @__PURE__ */ new Map();
4268
+ #pendingEnables = /* @__PURE__ */ new Map();
4269
+ #pendingSources = /* @__PURE__ */ new Set();
3245
4270
  #requestGenerations = /* @__PURE__ */ new Map();
3246
4271
  #intent = { microphone: true, camera: true };
3247
4272
  #requests = Object.freeze([]);
@@ -3368,161 +4393,268 @@ var MediaControllerRuntime = class {
3368
4393
  #set(source, enabled) {
3369
4394
  const operation = this.#diagnostics?.startOperation(`${source}.${enabled ? "publish" : "unpublish"}`);
3370
4395
  operation?.observe("observed", "intent");
3371
- return this.#serialize(
3372
- source,
3373
- this.#connection.runCommand((ports) => {
3374
- const previousIntent = this.#intent[source];
3375
- this.#intent[source] = enabled;
3376
- let captured = null;
3377
- let prepared = false;
3378
- const action = source === "microphone" ? "setMicrophoneEnabled" : "setCameraEnabled";
3379
- return Effect5.suspend(() => {
3380
- const acquire = enabled && !this.#tracks.has(source) ? this.#captureSource(source).pipe(
3381
- Effect5.tap(
3382
- (track) => Effect5.sync(() => {
3383
- captured = track;
3384
- try {
3385
- this.#assertActivePorts(ports, action);
3386
- } catch (cause) {
3387
- track.stop();
3388
- throw cause;
3389
- }
3390
- this.#tracks.set(source, track);
3391
- ports.media.prepareLocalTrack(source, track);
3392
- prepared = true;
3393
- operation?.observe("observed", "local_track_state");
3394
- this.#publish();
3395
- })
3396
- )
3397
- ) : Effect5.void;
3398
- return acquire.pipe(
3399
- Effect5.asVoid,
3400
- Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "local_track_state"))),
3401
- Effect5.andThen(source === "microphone" ? foreign2(() => ports.sync.setMicrophoneEnabled(enabled)) : foreign2(() => ports.sync.setCameraEnabled(enabled))),
3402
- Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "sync_commit"))),
3403
- Effect5.tap(() => Effect5.sync(() => this.#assertActivePorts(ports, action))),
3404
- Effect5.tap(
3405
- () => Effect5.sync(() => {
3406
- operation?.observe("observed", "sfu_publication");
3407
- operation?.succeed();
3408
- })
3409
- ),
3410
- Effect5.asVoid,
3411
- Effect5.catch(
3412
- (cause) => this.#rollbackCapture({
3413
- action,
3414
- cause,
3415
- fallback: Effect5.void,
3416
- media: ports.media,
3417
- permissionMessage: `${source} permission was denied`,
3418
- prepared,
3419
- restoreIntent: () => {
3420
- this.#intent[source] = previousIntent;
3421
- },
3422
- source,
3423
- track: captured
3424
- })
3425
- )
3426
- );
3427
- });
3428
- })
4396
+ let previousIntent = this.#intent[source];
4397
+ let intentRevision = 0;
4398
+ let earlyPause = null;
4399
+ const localPause = Effect5.suspend(() => {
4400
+ previousIntent = this.#intent[source];
4401
+ intentRevision = (this.#intentRevisions.get(source) ?? 0) + 1;
4402
+ this.#intentRevisions.set(source, intentRevision);
4403
+ this.#intent[source] = enabled;
4404
+ if (enabled) this.#pendingEnables.set(source, intentRevision);
4405
+ this.#ports?.media.setLocalSourceIntent?.(source, enabled);
4406
+ if (!enabled && this.#ports) earlyPause = this.#beginLocalPause(this.#ports, source);
4407
+ return Effect5.void;
4408
+ });
4409
+ return localPause.pipe(
4410
+ Effect5.andThen(
4411
+ this.#serialize(
4412
+ source,
4413
+ this.#connection.runCommand((ports) => {
4414
+ let captured = null;
4415
+ let prepared = false;
4416
+ const action = source === "microphone" ? "setMicrophoneEnabled" : "setCameraEnabled";
4417
+ return Effect5.suspend(() => {
4418
+ if (!enabled) {
4419
+ let paused = false;
4420
+ return (earlyPause ? foreign2(() => earlyPause) : this.#localTarget(ports, source, false)).pipe(
4421
+ Effect5.tap(
4422
+ () => Effect5.sync(() => {
4423
+ paused = true;
4424
+ })
4425
+ ),
4426
+ Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "local_track_state"))),
4427
+ Effect5.andThen(source === "microphone" ? foreign2(() => ports.sync.setMicrophoneEnabled(false)) : foreign2(() => ports.sync.setCameraEnabled(false))),
4428
+ Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "sync_commit"))),
4429
+ Effect5.tap(() => Effect5.sync(() => operation?.succeed())),
4430
+ Effect5.asVoid,
4431
+ Effect5.catch((cause) => {
4432
+ if (paused && !isSyncRejection(cause) || this.#intentRevisions.get(source) !== intentRevision) return Effect5.fail(cause);
4433
+ this.#intent[source] = previousIntent;
4434
+ ports.media.setLocalSourceIntent?.(source, previousIntent);
4435
+ return this.#localTarget(ports, source, previousIntent).pipe(
4436
+ Effect5.catch(() => Effect5.void),
4437
+ Effect5.andThen(Effect5.fail(cause))
4438
+ );
4439
+ })
4440
+ );
4441
+ }
4442
+ const acquire = enabled && !this.#tracks.has(source) ? this.#captureSource(source).pipe(
4443
+ Effect5.tap(
4444
+ (track) => Effect5.sync(() => {
4445
+ captured = track;
4446
+ try {
4447
+ this.#assertActivePorts(ports, action);
4448
+ } catch (cause) {
4449
+ track.stop();
4450
+ throw cause;
4451
+ }
4452
+ this.#tracks.set(source, track);
4453
+ ports.media.prepareLocalTrack(source, track);
4454
+ prepared = true;
4455
+ operation?.observe("observed", "local_track_state");
4456
+ this.#publish();
4457
+ })
4458
+ )
4459
+ ) : Effect5.void;
4460
+ return acquire.pipe(
4461
+ Effect5.asVoid,
4462
+ Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "local_track_state"))),
4463
+ Effect5.andThen(source === "microphone" ? foreign2(() => ports.sync.setMicrophoneEnabled(enabled)) : foreign2(() => ports.sync.setCameraEnabled(enabled))),
4464
+ Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "sync_commit"))),
4465
+ Effect5.tap(() => Effect5.sync(() => this.#assertActivePorts(ports, action))),
4466
+ Effect5.tap(
4467
+ () => Effect5.sync(() => {
4468
+ operation?.observe("observed", "sfu_publication");
4469
+ operation?.succeed();
4470
+ })
4471
+ ),
4472
+ Effect5.asVoid,
4473
+ Effect5.catch(
4474
+ (cause) => this.#rollbackCapture({
4475
+ action,
4476
+ cause,
4477
+ fallback: Effect5.void,
4478
+ media: ports.media,
4479
+ permissionMessage: `${source} permission was denied`,
4480
+ prepared,
4481
+ restoreIntent: () => {
4482
+ if (this.#intentRevisions.get(source) === intentRevision) this.#intent[source] = previousIntent;
4483
+ },
4484
+ source,
4485
+ track: captured
4486
+ })
4487
+ )
4488
+ );
4489
+ });
4490
+ })
4491
+ )
4492
+ )
3429
4493
  ).pipe(
3430
4494
  Effect5.mapError(normalizeClientError),
3431
- Effect5.tapError(() => Effect5.sync(() => operation?.fail("media_failed")))
4495
+ Effect5.tapError(() => Effect5.sync(() => operation?.fail("media_failed"))),
4496
+ Effect5.ensuring(
4497
+ Effect5.sync(() => {
4498
+ if (this.#pendingEnables.get(source) === intentRevision) this.#pendingEnables.delete(source);
4499
+ })
4500
+ )
3432
4501
  );
3433
4502
  }
3434
4503
  #startScreen() {
3435
4504
  const operation = this.#diagnostics?.startOperation("screen.start");
3436
- return this.#serialize(
3437
- "screen",
3438
- this.#connection.runCommand((ports) => {
3439
- let stream = null;
3440
- let track = null;
3441
- let prepared = false;
3442
- return Effect5.suspend(() => {
3443
- if (this.#tracks.has("screen")) {
3444
- operation?.notObservable("permission", "already_active");
3445
- operation?.notObservable("track_acquisition", "already_active");
3446
- operation?.notObservable("sync_commit", "already_active");
3447
- operation?.notObservable("sfu_publication", "already_active");
3448
- operation?.succeed();
3449
- return Effect5.void;
3450
- }
3451
- return foreign2(() => this.#selection.getDisplayMedia({ video: true, audio: false })).pipe(
3452
- Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "permission"))),
3453
- Effect5.tap(
3454
- (captured) => Effect5.sync(() => {
3455
- stream = captured;
3456
- this.#assertActivePorts(ports, "startScreenShare");
3457
- track = requireDisplayVideoTrack(captured);
3458
- this.#tracks.set("screen", track);
3459
- ports.media.prepareLocalTrack("screen", track);
3460
- prepared = true;
3461
- operation?.observe("observed", "track_acquisition");
3462
- track.addEventListener("ended", () => this.#handleScreenEnded(track));
3463
- this.#screenEndedPending = false;
3464
- this.#publish();
3465
- })
3466
- ),
3467
- Effect5.andThen(foreign2(() => ports.sync.setScreenShareEnabled(true))),
3468
- Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "sync_commit"))),
3469
- Effect5.tap(() => Effect5.sync(() => this.#assertActivePorts(ports, "startScreenShare"))),
3470
- Effect5.tap(
3471
- () => Effect5.sync(() => {
3472
- operation?.observe("observed", "sfu_publication");
4505
+ let intentRevision = 0;
4506
+ return Effect5.sync(() => {
4507
+ intentRevision = (this.#intentRevisions.get("screen") ?? 0) + 1;
4508
+ this.#intentRevisions.set("screen", intentRevision);
4509
+ this.#pendingEnables.set("screen", intentRevision);
4510
+ this.#ports?.media.setLocalSourceIntent?.("screen", true);
4511
+ }).pipe(
4512
+ Effect5.andThen(
4513
+ this.#serialize(
4514
+ "screen",
4515
+ this.#connection.runCommand((ports) => {
4516
+ let stream = null;
4517
+ let track = null;
4518
+ let prepared = false;
4519
+ return Effect5.suspend(() => {
4520
+ if (this.#tracks.has("screen")) {
4521
+ operation?.notObservable("permission", "already_active");
4522
+ operation?.notObservable("track_acquisition", "already_active");
4523
+ operation?.notObservable("sync_commit", "already_active");
4524
+ operation?.notObservable("sfu_publication", "already_active");
3473
4525
  operation?.succeed();
3474
- })
3475
- ),
3476
- Effect5.asVoid,
3477
- Effect5.catch((cause) => this.#rollbackCapture({ action: "startScreenShare", cause, fallback: Effect5.sync(() => stopStream(stream)), media: ports.media, permissionMessage: "Screen sharing permission was denied", prepared, source: "screen", track }))
3478
- );
3479
- });
3480
- })
4526
+ return Effect5.void;
4527
+ }
4528
+ return foreign2(() => this.#selection.getDisplayMedia({ video: true, audio: false })).pipe(
4529
+ Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "permission"))),
4530
+ Effect5.tap(
4531
+ (captured) => Effect5.sync(() => {
4532
+ stream = captured;
4533
+ this.#assertActivePorts(ports, "startScreenShare");
4534
+ track = requireDisplayVideoTrack(captured);
4535
+ this.#tracks.set("screen", track);
4536
+ ports.media.prepareLocalTrack("screen", track);
4537
+ prepared = true;
4538
+ operation?.observe("observed", "track_acquisition");
4539
+ track.addEventListener("ended", () => this.#handleScreenEnded(track));
4540
+ this.#screenEndedPending = false;
4541
+ this.#publish();
4542
+ })
4543
+ ),
4544
+ Effect5.andThen(foreign2(() => ports.sync.setScreenShareEnabled(true))),
4545
+ Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "sync_commit"))),
4546
+ Effect5.tap(() => Effect5.sync(() => this.#assertActivePorts(ports, "startScreenShare"))),
4547
+ Effect5.tap(
4548
+ () => Effect5.sync(() => {
4549
+ operation?.observe("observed", "sfu_publication");
4550
+ operation?.succeed();
4551
+ })
4552
+ ),
4553
+ Effect5.asVoid,
4554
+ Effect5.catch((cause) => this.#rollbackCapture({ action: "startScreenShare", cause, fallback: Effect5.sync(() => stopStream(stream)), media: ports.media, permissionMessage: "Screen sharing permission was denied", prepared, source: "screen", track }))
4555
+ );
4556
+ });
4557
+ })
4558
+ )
4559
+ )
3481
4560
  ).pipe(
3482
4561
  Effect5.mapError(normalizeClientError),
3483
- Effect5.tapError(() => Effect5.sync(() => operation?.fail("screen_start_failed")))
4562
+ Effect5.tapError(() => Effect5.sync(() => operation?.fail("screen_start_failed"))),
4563
+ Effect5.ensuring(
4564
+ Effect5.sync(() => {
4565
+ if (this.#pendingEnables.get("screen") === intentRevision) this.#pendingEnables.delete("screen");
4566
+ })
4567
+ )
3484
4568
  );
3485
4569
  }
3486
4570
  #stopScreen() {
3487
4571
  const operation = this.#diagnostics?.startOperation("screen.stop");
3488
- return this.#serialize(
3489
- "screen",
3490
- this.#connection.runCommand(
3491
- (ports) => Effect5.suspend(() => {
3492
- const track = this.#tracks.get("screen");
3493
- if (!track) {
3494
- operation?.notObservable("stop_confirmation", "already_stopped");
3495
- operation?.succeed();
3496
- return Effect5.void;
3497
- }
3498
- return foreign2(() => ports.sync.setScreenShareEnabled(false)).pipe(
3499
- Effect5.tap(() => Effect5.sync(() => this.#assertActivePorts(ports, "stopScreenShare"))),
3500
- Effect5.andThen(foreign2(() => ports.media.clearPreparedLocalTrack("screen"))),
3501
- Effect5.tap(
3502
- () => Effect5.sync(() => {
3503
- this.#assertActivePorts(ports, "stopScreenShare");
3504
- if (this.#tracks.get("screen") === track) this.#tracks.delete("screen");
3505
- track.stop();
3506
- this.#screenEndedPending = false;
3507
- this.#publish();
3508
- operation?.observe("observed", "stop_confirmation");
3509
- operation?.succeed();
3510
- })
3511
- ),
3512
- Effect5.asVoid
3513
- );
3514
- })
4572
+ let intentRevision = 0;
4573
+ let earlyPause = null;
4574
+ const localPause = Effect5.suspend(() => {
4575
+ intentRevision = (this.#intentRevisions.get("screen") ?? 0) + 1;
4576
+ this.#intentRevisions.set("screen", intentRevision);
4577
+ this.#ports?.media.setLocalSourceIntent?.("screen", false);
4578
+ if (this.#ports) earlyPause = this.#beginLocalPause(this.#ports, "screen");
4579
+ return Effect5.void;
4580
+ });
4581
+ return localPause.pipe(
4582
+ Effect5.andThen(
4583
+ this.#serialize(
4584
+ "screen",
4585
+ this.#connection.runCommand(
4586
+ (ports) => Effect5.suspend(() => {
4587
+ const track = this.#tracks.get("screen");
4588
+ let paused = false;
4589
+ return (earlyPause ? foreign2(() => earlyPause) : this.#localTarget(ports, "screen", false)).pipe(
4590
+ Effect5.tap(
4591
+ () => Effect5.sync(() => {
4592
+ paused = true;
4593
+ })
4594
+ ),
4595
+ Effect5.andThen(foreign2(() => ports.sync.setScreenShareEnabled(false))),
4596
+ Effect5.tap(() => Effect5.sync(() => this.#assertActivePorts(ports, "stopScreenShare"))),
4597
+ Effect5.andThen(foreign2(() => ports.media.clearPreparedLocalTrack("screen"))),
4598
+ Effect5.tap(
4599
+ () => Effect5.sync(() => {
4600
+ this.#assertActivePorts(ports, "stopScreenShare");
4601
+ if (this.#tracks.get("screen") === track) this.#tracks.delete("screen");
4602
+ track?.stop();
4603
+ this.#screenEndedPending = false;
4604
+ this.#publish();
4605
+ operation?.observe("observed", "stop_confirmation");
4606
+ operation?.succeed();
4607
+ })
4608
+ ),
4609
+ Effect5.asVoid,
4610
+ Effect5.catch(
4611
+ (cause) => paused && !isSyncRejection(cause) || this.#intentRevisions.get("screen") !== intentRevision ? Effect5.fail(cause) : Effect5.sync(() => ports.media.setLocalSourceIntent?.("screen", true)).pipe(
4612
+ Effect5.andThen(this.#localTarget(ports, "screen", true)),
4613
+ Effect5.catch(() => Effect5.void),
4614
+ Effect5.andThen(Effect5.fail(cause))
4615
+ )
4616
+ )
4617
+ );
4618
+ })
4619
+ )
4620
+ )
3515
4621
  )
3516
4622
  ).pipe(
3517
4623
  Effect5.mapError(normalizeClientError),
3518
4624
  Effect5.tapError(() => Effect5.sync(() => operation?.fail("screen_stop_failed")))
3519
4625
  );
3520
4626
  }
4627
+ #beginLocalPause(ports, source) {
4628
+ const pause = Effect5.runPromise(this.#localTarget(ports, source, false));
4629
+ void pause.catch(() => void 0);
4630
+ return pause;
4631
+ }
3521
4632
  #serialize(source, effect) {
3522
- return this.#gates.get(source).withPermit(effect);
4633
+ return this.#gates.get(source).withPermit(
4634
+ Effect5.sync(() => {
4635
+ this.#pendingSources.add(source);
4636
+ this.#publish();
4637
+ }).pipe(
4638
+ Effect5.andThen(effect),
4639
+ Effect5.ensuring(
4640
+ Effect5.sync(() => {
4641
+ this.#pendingSources.delete(source);
4642
+ this.#publish();
4643
+ })
4644
+ )
4645
+ )
4646
+ );
4647
+ }
4648
+ #localTarget(ports, source, enabled) {
4649
+ return foreign2(() => ports.media.setLocalPublicationTarget({ operationId: `privacy-${source}-${enabled ? "restore" : "off"}`, participantId: ports.sync.getSnapshot().participantId ?? "", source, enabled })).pipe(
4650
+ Effect5.flatMap(
4651
+ (result) => result.outcome === "confirmed" || result.outcome === "satisfied" || !enabled && result.errorCode === "source_unavailable" ? Effect5.void : Effect5.fail(new SpaceClientError({ code: "media.capture_failed", recoverable: true, message: `Could not ${enabled ? "restore" : "pause"} ${source}` }))
4652
+ )
4653
+ );
3523
4654
  }
3524
4655
  #captureSource(source) {
3525
4656
  return foreign2(() => this.#selection.getUserMedia({ audio: source === "microphone", video: source === "camera" })).pipe(
4657
+ Effect5.mapError((cause) => captureError(cause, source === "microphone" ? "setMicrophoneEnabled" : "setCameraEnabled", `${source} permission was denied`)),
3526
4658
  Effect5.tap(() => this.#refreshDevices().pipe(Effect5.ignore)),
3527
4659
  Effect5.flatMap(
3528
4660
  (stream) => Effect5.try({
@@ -3568,8 +4700,8 @@ var MediaControllerRuntime = class {
3568
4700
  input.restoreIntent?.();
3569
4701
  const rollback = input.track ? this.#discardCaptured(input.source, input.track, input.media, input.prepared) : input.fallback;
3570
4702
  const captureWasUnavailable = input.action === "startScreenShare" && !input.prepared && input.track === null;
3571
- const failure2 = isPermissionDenied(input.cause) || captureWasUnavailable ? captureError(input.cause, input.action, input.permissionMessage) : input.cause;
3572
- return rollback.pipe(Effect5.andThen(Effect5.sync(() => this.#publish())), Effect5.andThen(Effect5.fail(failure2)));
4703
+ const failure3 = isPermissionDenied(input.cause) || captureWasUnavailable ? captureError(input.cause, input.action, input.permissionMessage) : input.cause;
4704
+ return rollback.pipe(Effect5.andThen(Effect5.sync(() => this.#publish())), Effect5.andThen(Effect5.fail(failure3)));
3573
4705
  }
3574
4706
  #assertActivePorts(ports, action) {
3575
4707
  if (this.#connection.getSnapshot().state === "live" && this.#ports?.sync === ports.sync && this.#ports.media === ports.media) return;
@@ -3582,6 +4714,7 @@ var MediaControllerRuntime = class {
3582
4714
  this.#unsubscribeMedia = null;
3583
4715
  this.#unsubscribeRequests = null;
3584
4716
  this.#unsubscribeSync = null;
4717
+ if (this.#ports?.media !== ports?.media) this.#observedPublicationIds.clear();
3585
4718
  this.#ports = ports;
3586
4719
  if (!ports) {
3587
4720
  for (const track of this.#tracks.values()) track.stop();
@@ -3591,11 +4724,57 @@ var MediaControllerRuntime = class {
3591
4724
  this.#publish();
3592
4725
  return;
3593
4726
  }
3594
- this.#unsubscribeMedia = ports.media.subscribe(() => this.#publish());
4727
+ this.#unsubscribeMedia = ports.media.subscribe(() => {
4728
+ this.#applyRemoteMediaState(ports, ports.sync.getSnapshot());
4729
+ this.#publish();
4730
+ });
3595
4731
  this.#unsubscribeRequests = ports.sync.onDirectedRequest((request) => this.#request(request));
3596
- this.#unsubscribeSync = ports.sync.subscribe((snapshot) => this.#removeRequestsFromMissingParticipants(snapshot));
4732
+ this.#unsubscribeSync = ports.sync.subscribe((snapshot) => {
4733
+ this.#removeRequestsFromMissingParticipants(snapshot);
4734
+ this.#applyRemoteMediaState(ports, snapshot);
4735
+ this.#applyForcedMediaState(ports, snapshot);
4736
+ });
4737
+ this.#applyRemoteMediaState(ports, ports.sync.getSnapshot());
4738
+ this.#applyForcedMediaState(ports, ports.sync.getSnapshot());
3597
4739
  this.#publish();
3598
4740
  }
4741
+ #applyRemoteMediaState(ports, snapshot) {
4742
+ for (const publication of ports.media.getSnapshot().remoteTracks) {
4743
+ publication.track.enabled = snapshot.media?.items.some((item) => item.participantId === publication.participantId && item.source === publication.source && item.enabled) ?? false;
4744
+ }
4745
+ }
4746
+ #applyForcedMediaState(ports, snapshot) {
4747
+ if (!snapshot.media || !snapshot.participantId) return;
4748
+ for (const source of MEDIA_SOURCES) {
4749
+ const projected = snapshot.media.items.find((item) => item.participantId === snapshot.participantId && item.source === source);
4750
+ if (projected?.publicationId) {
4751
+ this.#observedPublicationIds.set(source, projected.publicationId);
4752
+ continue;
4753
+ }
4754
+ const previousPublicationId = this.#observedPublicationIds.get(source);
4755
+ if (!this.#observedPublicationIds.delete(source)) continue;
4756
+ const local = ports.media.getSnapshot().localTracks.find((publication) => publication.source === source);
4757
+ if (local && local.publicationId !== previousPublicationId) continue;
4758
+ this.#pauseForcedSource(ports, source);
4759
+ }
4760
+ }
4761
+ #pauseForcedSource(ports, source) {
4762
+ const pendingEnable = this.#hasPendingEnable(source);
4763
+ if (!pendingEnable) {
4764
+ this.#intentRevisions.set(source, (this.#intentRevisions.get(source) ?? 0) + 1);
4765
+ if (source !== "screen") this.#intent[source] = false;
4766
+ }
4767
+ const track = this.#tracks.get(source);
4768
+ if (track) track.enabled = false;
4769
+ ports.media.setLocalSourceIntent?.(source, false);
4770
+ if (ports.media.closeForcedLocalPublication) void ports.media.closeForcedLocalPublication(source).catch(() => void 0);
4771
+ if (pendingEnable) ports.media.setLocalSourceIntent?.(source, true);
4772
+ this.#publish();
4773
+ }
4774
+ #hasPendingEnable(source) {
4775
+ const revision = this.#pendingEnables.get(source);
4776
+ return revision !== void 0 && revision === this.#intentRevisions.get(source);
4777
+ }
3599
4778
  #request(request) {
3600
4779
  if (request.expires_at_ms <= this.#now()) return;
3601
4780
  const control = this.#ports?.sync.getSnapshot().optimisticControl ?? this.#ports?.sync.getSnapshot().control;
@@ -3665,7 +4844,11 @@ var MediaControllerRuntime = class {
3665
4844
  const current = this.#store.getSnapshot();
3666
4845
  const snapshot = this.#ports?.media.getSnapshot();
3667
4846
  const state = this.#connection.getSnapshot().state;
3668
- const local = Object.freeze({ microphone: localMedia("microphone", this.#tracks, snapshot, this.#intent.microphone, state), camera: localMedia("camera", this.#tracks, snapshot, this.#intent.camera, state), screen: localMedia("screen", this.#tracks, snapshot, false, state) });
4847
+ const local = Object.freeze({
4848
+ microphone: localMedia("microphone", this.#tracks, snapshot, this.#intent.microphone, state, this.#pendingSources.has("microphone")),
4849
+ camera: localMedia("camera", this.#tracks, snapshot, this.#intent.camera, state, this.#pendingSources.has("camera")),
4850
+ screen: localMedia("screen", this.#tracks, snapshot, false, state, this.#pendingSources.has("screen"))
4851
+ });
3669
4852
  const remote = Object.freeze((snapshot?.remoteTracks ?? []).map((track) => Object.freeze({ participantId: track.participantId, source: track.source, publicationId: track.publicationId, track: track.track })));
3670
4853
  const media = Object.freeze({ ...current.media, local, remote, screenShare: local.screen, incomingRequests: this.#requests });
3671
4854
  if (!sameMediaSlice(current.media, media)) this.#store.updateMedia(media);
@@ -3702,12 +4885,18 @@ function initialTrackEntries(microphone, camera) {
3702
4885
  if (camera) entries.push(["camera", camera]);
3703
4886
  return entries;
3704
4887
  }
3705
- function localMedia(source, tracks, snapshot, intended, connectionState) {
4888
+ function localMedia(source, tracks, snapshot, intended, connectionState, pending) {
3706
4889
  const track = tracks.get(source) ?? null;
3707
4890
  const publication = snapshot?.localTracks.find((candidate) => candidate.source === source);
4891
+ if (locallyMuted(intended, track, publication)) return Object.freeze({ source, state: "disabled", track });
4892
+ if (pending) return Object.freeze({ source, state: "requesting", track });
3708
4893
  if (publication?.enabled) return Object.freeze({ source, state: "enabled", track });
4894
+ if (publication && connectionState === "live") return Object.freeze({ source, state: "disabled", track });
3709
4895
  return Object.freeze({ source, state: localMediaState(source, track, intended, connectionState), track });
3710
4896
  }
4897
+ function locallyMuted(intended, track, publication) {
4898
+ return !intended && track?.enabled === false && publication?.enabled === false;
4899
+ }
3711
4900
  function localMediaState(source, track, intended, connectionState) {
3712
4901
  if (!mediaIsDesired(source, track, intended)) return inactiveLocalMediaState(connectionState, track);
3713
4902
  if (connectionState === "joining" || track) return "requesting";
@@ -3750,19 +4939,265 @@ function sameDeviceList(left, right) {
3750
4939
  return left.length === right.length && left.every((device, index) => device.deviceId === right[index]?.deviceId && device.label === right[index]?.label);
3751
4940
  }
3752
4941
  function captureError(cause, action, permissionMessage) {
4942
+ if (cause instanceof ConnectionError) return cause;
3753
4943
  if (isPermissionDenied(cause)) return new ConnectionError({ code: "permission_denied", action, recoverable: true, message: permissionMessage }, { cause });
3754
- return new ConnectionError({ code: "unsupported_environment", action, recoverable: false, message: action === "startScreenShare" ? "Screen sharing is unavailable in this browser." : "Media capture is unavailable" }, { cause });
4944
+ const failure3 = describeMediaCaptureError(cause);
4945
+ return new ConnectionError({ ...failure3, action, message: action === "startScreenShare" ? "Screen sharing could not start. Check screen recording permission and try again in a supported browser or app." : failure3.message }, { cause });
3755
4946
  }
3756
4947
  function isPermissionDenied(cause) {
3757
- return cause instanceof DOMException && (cause.name === "NotAllowedError" || cause.name === "SecurityError");
4948
+ return describeMediaCaptureError(cause).code === "permission_denied";
3758
4949
  }
3759
4950
  function isAccessInvalid2(cause) {
3760
4951
  return typeof cause === "object" && cause !== null && "code" in cause && (cause.code === "access.invalid" || cause.code === "invalid_access");
3761
4952
  }
4953
+ function isSyncRejection(cause) {
4954
+ return isAccessInvalid2(cause) || cause instanceof V1SyncError && cause.code === "terminal_failure";
4955
+ }
3762
4956
 
3763
- // src/space-client/participants-controller.ts
4957
+ // src/space-client/offline-actions.ts
3764
4958
  import { Context as Context7, Effect as Effect6, Layer as Layer7 } from "effect";
3765
4959
 
4960
+ // src/space-client/offline-actions-storage.ts
4961
+ import { Schema as Schema3 } from "effect";
4962
+ var OFFLINE_ACTION_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
4963
+ function browserOfflineActionsStorage() {
4964
+ if (typeof window === "undefined") return void 0;
4965
+ try {
4966
+ return globalThis.localStorage;
4967
+ } catch {
4968
+ return void 0;
4969
+ }
4970
+ }
4971
+ var attachment = Schema3.Struct({ attachmentId: Schema3.String, fileName: Schema3.String, mimeType: ChatAttachmentMimeTypeSchema, byteLength: Schema3.Number });
4972
+ var StoredOfflineAction = Schema3.Struct({
4973
+ id: Schema3.String,
4974
+ kind: Schema3.Literals(["chat_message", "hand_raise", "hand_lower"]),
4975
+ text: Schema3.NullOr(Schema3.String),
4976
+ queuedAt: Schema3.Number,
4977
+ clientMessageId: Schema3.optional(Schema3.String),
4978
+ attachments: Schema3.optional(Schema3.Array(attachment))
4979
+ });
4980
+ var StoredOfflineActions = Schema3.Struct({
4981
+ version: Schema3.Literal(1),
4982
+ subject: Schema3.Struct({ tenantId: Schema3.String, spaceId: Schema3.String, episodeId: Schema3.String, participantId: Schema3.String, participantGeneration: Schema3.Number }),
4983
+ actions: Schema3.Array(StoredOfflineAction)
4984
+ });
4985
+ var decodeOfflineActions = Schema3.decodeUnknownSync(StoredOfflineActions);
4986
+
4987
+ // src/space-client/offline-actions.ts
4988
+ var OfflineActionsService = class extends Context7.Service()("@chalk/client/OfflineActions") {
4989
+ };
4990
+ var sameSubject = (current, saved) => !!current && !!saved && current.tenantId === saved.tenantId && current.spaceId === saved.spaceId && current.episodeId === saved.episodeId && current.participantId === saved.participantId && current.participantGeneration === saved.participantGeneration;
4991
+ var terminalConnection = (snapshot) => snapshot.state === "left" || snapshot.state === "leaving" || snapshot.state === "failed" && !snapshot.failure?.recoverable;
4992
+ var validStoredAction = (action, now) => Number.isFinite(action.queuedAt) && action.queuedAt <= now && (action.kind !== "chat_message" || !!action.clientMessageId && action.text !== null && validateChatMessage({ text: action.text, attachments: action.attachments }) === null);
4993
+ var discardedError = () => new SpaceClientError({ code: "offline.discarded", recoverable: false, message: "This action was not sent." });
4994
+ var makeOfflineActions = (input) => Effect6.gen(function* () {
4995
+ const runtime = new OfflineActionsRuntime(input.connection, input.store, input.storage ?? browserOfflineActionsStorage(), input.storageKey);
4996
+ yield* Effect6.addFinalizer(() => Effect6.sync(() => runtime.dispose()));
4997
+ return runtime;
4998
+ });
4999
+ var OfflineActionsRuntime = class {
5000
+ #connection;
5001
+ #store;
5002
+ #unsubscribe;
5003
+ #waiters = [];
5004
+ #decisionNeeded = false;
5005
+ #policy = "send";
5006
+ #policyExplicit = false;
5007
+ #restored = false;
5008
+ #subject = null;
5009
+ #notice;
5010
+ #storage;
5011
+ #storageKey;
5012
+ constructor(connection, store, storage, storageKey = "chalk:offline-actions") {
5013
+ this.#connection = connection;
5014
+ this.#store = store;
5015
+ this.#storage = storage;
5016
+ this.#storageKey = storageKey;
5017
+ this.#unsubscribe = connection.subscribe(() => this.#connectionChanged());
5018
+ this.#connectionChanged();
5019
+ }
5020
+ send = () => Effect6.sync(() => this.#settle("send"));
5021
+ discard = () => Effect6.sync(() => this.#settle("discard"));
5022
+ setPolicy = (policy) => Effect6.sync(() => {
5023
+ this.#policyExplicit = true;
5024
+ this.#policy = policy;
5025
+ if (this.#decisionNeeded && policy !== "ask") this.#settle(policy);
5026
+ else this.#publish();
5027
+ });
5028
+ hold = (input, run) => {
5029
+ if (this.#connection.getSnapshot().state !== "reconnecting" && !this.#decisionNeeded) return null;
5030
+ const queuedSubject = this.#connection.getSnapshot().subject;
5031
+ const action = Object.freeze({ ...input, id: this.#connection.createId(), text: input.text ?? null, queuedAt: this.#connection.nowUnsafe() });
5032
+ return Effect6.callback((resume) => {
5033
+ if (this.#waiters.length === 0) this.#subject = queuedSubject;
5034
+ this.#notice = void 0;
5035
+ this.#waiters = [...this.#waiters, { action, resume }];
5036
+ this.#persist();
5037
+ this.#publish();
5038
+ return Effect6.sync(() => this.#drop(action.id));
5039
+ }).pipe(Effect6.andThen(Effect6.suspend(() => sameSubject(this.#connection.getSnapshot().subject, queuedSubject) ? run() : Effect6.fail(discardedError()))));
5040
+ };
5041
+ dispose() {
5042
+ this.#unsubscribe();
5043
+ const waiters = this.#waiters;
5044
+ this.#waiters = [];
5045
+ for (const waiter of waiters) waiter.resume(Effect6.fail(discardedError()));
5046
+ }
5047
+ #connectionChanged() {
5048
+ const snapshot = this.#connection.getSnapshot();
5049
+ const state = snapshot.state;
5050
+ const terminal = terminalConnection(snapshot);
5051
+ if (state === "live") this.#restore();
5052
+ if (this.#waiters.length === 0) {
5053
+ if (terminal) this.#discardSaved();
5054
+ return;
5055
+ }
5056
+ if (state === "live") {
5057
+ if (!this.#validScope()) {
5058
+ this.#notice = "Your offline actions were not sent because the Episode or participant changed.";
5059
+ this.#settle("discard");
5060
+ return;
5061
+ }
5062
+ if (this.#policy === "ask") {
5063
+ this.#decisionNeeded = true;
5064
+ this.#publish();
5065
+ } else this.#settle(this.#policy);
5066
+ } else if (terminal) {
5067
+ this.#notice = "Your offline actions were not sent because the Episode ended or the connection closed.";
5068
+ this.#settle("discard");
5069
+ this.#clearStorage();
5070
+ }
5071
+ }
5072
+ #settle(decision) {
5073
+ if (decision === "send" && this.#connection.getSnapshot().state !== "live") return;
5074
+ if (decision === "send" && !this.#validScope()) {
5075
+ this.#notice = "Your offline actions were not sent because the Episode or participant changed.";
5076
+ decision = "discard";
5077
+ }
5078
+ if (!this.#clearStorage() && this.#storage) {
5079
+ this.#notice = "Your offline actions could not be sent safely. Try again when storage is available.";
5080
+ this.#publish();
5081
+ return;
5082
+ }
5083
+ const waiters = this.#waiters;
5084
+ this.#waiters = [];
5085
+ this.#decisionNeeded = false;
5086
+ this.#publish();
5087
+ for (const waiter of waiters) this.#resumeWaiter(waiter, decision);
5088
+ this.#publish();
5089
+ }
5090
+ #drop(id) {
5091
+ const next = this.#waiters.filter((waiter) => waiter.action.id !== id);
5092
+ if (next.length === this.#waiters.length) return;
5093
+ this.#waiters = next;
5094
+ this.#persist();
5095
+ if (next.length === 0) this.#decisionNeeded = false;
5096
+ this.#publish();
5097
+ }
5098
+ #validScope() {
5099
+ const current = this.#connection.getSnapshot().subject;
5100
+ const saved = this.#subject;
5101
+ return sameSubject(current, saved);
5102
+ }
5103
+ #clearStorage() {
5104
+ try {
5105
+ this.#storage?.removeItem(this.#storageKey);
5106
+ return true;
5107
+ } catch {
5108
+ return false;
5109
+ }
5110
+ }
5111
+ #persist() {
5112
+ if (!this.#storage || !this.#subject) return;
5113
+ try {
5114
+ if (this.#waiters.length === 0) this.#storage.removeItem(this.#storageKey);
5115
+ else this.#storage.setItem(this.#storageKey, JSON.stringify({ version: 1, subject: this.#subject, actions: this.#waiters.map(({ action }) => action) }));
5116
+ } catch {
5117
+ this.#notice = "Your offline actions are held, but could not be saved. Keep this page open.";
5118
+ }
5119
+ }
5120
+ #restore() {
5121
+ if (this.#restored || !this.#connection.getSnapshot().subject) return;
5122
+ this.#restored = true;
5123
+ if (this.#waiters.length > 0) return;
5124
+ const saved = this.#readStored();
5125
+ if (!saved) return;
5126
+ this.#subject = saved.subject;
5127
+ if (!this.#validScope()) {
5128
+ this.#notice = "Your offline actions were not sent because the Episode or participant changed.";
5129
+ this.#clearStorage();
5130
+ this.#publish();
5131
+ return;
5132
+ }
5133
+ const actions = saved.actions.filter((action) => this.#connection.nowUnsafe() - action.queuedAt < OFFLINE_ACTION_MAX_AGE_MS);
5134
+ if (actions.length !== saved.actions.length) this.#notice = "Your expired offline actions were not sent. Actions are kept for up to 24 hours.";
5135
+ if (!this.#policyExplicit) this.#policy = "ask";
5136
+ const restoredSubject = saved.subject;
5137
+ this.#waiters = actions.map((action) => ({
5138
+ action,
5139
+ resume: (permission) => {
5140
+ const command = this.#connection.runCommand(
5141
+ ({ sync }) => Effect6.tryPromise({
5142
+ try: () => action.kind === "chat_message" ? sync.sendChatMessage({ text: action.text ?? "", clientMessageId: action.clientMessageId ?? action.id, attachments: action.attachments ?? [] }).then(() => void 0) : sync.setHandRaised(action.kind === "hand_raise"),
5143
+ catch: () => discardedError()
5144
+ })
5145
+ );
5146
+ void Effect6.runPromiseExit(
5147
+ permission.pipe(
5148
+ Effect6.andThen(
5149
+ Effect6.suspend(() => sameSubject(this.#connection.getSnapshot().subject, restoredSubject) ? command : Effect6.fail(discardedError())).pipe(
5150
+ Effect6.tapError(
5151
+ () => Effect6.sync(() => {
5152
+ this.#notice = "An offline action was not sent. Please try it again.";
5153
+ this.#publish();
5154
+ })
5155
+ )
5156
+ )
5157
+ )
5158
+ )
5159
+ );
5160
+ }
5161
+ }));
5162
+ this.#persist();
5163
+ this.#publish();
5164
+ }
5165
+ #resumeWaiter(waiter, decision) {
5166
+ const expired = this.#connection.nowUnsafe() - waiter.action.queuedAt >= OFFLINE_ACTION_MAX_AGE_MS;
5167
+ if (expired) this.#notice = "Your expired offline actions were not sent. Actions are kept for up to 24 hours.";
5168
+ waiter.resume(decision === "send" && !expired ? Effect6.void : Effect6.fail(discardedError()));
5169
+ }
5170
+ #discardSaved() {
5171
+ try {
5172
+ if (!this.#storage?.getItem(this.#storageKey)) return;
5173
+ this.#notice = "Your offline actions were not sent because the Episode ended or the connection closed.";
5174
+ this.#clearStorage();
5175
+ } catch {
5176
+ this.#notice = "Saved offline actions could not be read. They will not be sent.";
5177
+ }
5178
+ this.#publish();
5179
+ }
5180
+ #readStored() {
5181
+ try {
5182
+ const raw = this.#storage?.getItem(this.#storageKey);
5183
+ if (!raw) return null;
5184
+ const saved = decodeOfflineActions(JSON.parse(raw));
5185
+ if (saved.actions.some((action) => !validStoredAction(action, this.#connection.nowUnsafe()))) throw new Error("Invalid offline action");
5186
+ return saved;
5187
+ } catch {
5188
+ this.#clearStorage();
5189
+ return null;
5190
+ }
5191
+ }
5192
+ #publish() {
5193
+ const slice = Object.freeze({ pending: Object.freeze(this.#waiters.map((waiter) => waiter.action)), decisionNeeded: this.#decisionNeeded, policy: this.#policy, ...this.#notice ? { notice: this.#notice } : {} });
5194
+ this.#store.updateOffline(slice);
5195
+ }
5196
+ };
5197
+
5198
+ // src/space-client/participants-controller.ts
5199
+ import { Context as Context8, Effect as Effect7, Layer as Layer8 } from "effect";
5200
+
3766
5201
  // src/sync/v1-types.ts
3767
5202
  var V1_CAPABILITIES = [
3768
5203
  "publishAudio",
@@ -3793,19 +5228,21 @@ var V1_CAPABILITIES = [
3793
5228
  // src/space-client/participants-controller.ts
3794
5229
  var CAPABILITIES = new Set(V1_CAPABILITIES);
3795
5230
  var EMPTY_PARTICIPANTS = Object.freeze({ roster: Object.freeze([]), admissionQueue: Object.freeze([]) });
3796
- var ParticipantsControllerService = class extends Context7.Service()("@chalk/client/ParticipantsController") {
5231
+ var ParticipantsControllerService = class extends Context8.Service()("@chalk/client/ParticipantsController") {
3797
5232
  };
3798
- var makeParticipantsController = (connection, store, diagnostics) => Effect6.acquireRelease(
3799
- Effect6.sync(() => new ParticipantsControllerRuntime(connection, store, diagnostics)),
3800
- (controller) => Effect6.sync(() => controller.dispose())
5233
+ var makeParticipantsController = (connection, store, diagnostics, offline) => Effect7.acquireRelease(
5234
+ Effect7.sync(() => new ParticipantsControllerRuntime(connection, store, diagnostics, offline)),
5235
+ (controller) => Effect7.sync(() => controller.dispose())
3801
5236
  );
3802
5237
  var ParticipantsControllerRuntime = class {
3803
5238
  #connection;
3804
5239
  #store;
3805
5240
  #diagnostics;
5241
+ #offline;
3806
5242
  #unsubscribePorts = null;
3807
5243
  #unsubscribe = null;
3808
- constructor(connection, store, diagnostics) {
5244
+ constructor(connection, store, diagnostics, offline) {
5245
+ this.#offline = offline;
3809
5246
  this.#connection = connection;
3810
5247
  this.#store = store;
3811
5248
  this.#diagnostics = diagnostics;
@@ -3831,7 +5268,7 @@ var ParticipantsControllerRuntime = class {
3831
5268
  requestMedia = (participantId, kind) => this.#command("media_request.request", ["capability_decision", "command_commit", "target_result"], () => {
3832
5269
  assertIdentifier(participantId, "participant ID");
3833
5270
  if (kind !== "microphone" && kind !== "camera") throw invalid("Media requests must target a microphone or camera");
3834
- return ({ sync }) => (kind === "microphone" ? foreign3(() => sync.requestUnmute(participantId)) : foreign3(() => sync.requestStartCamera(participantId))).pipe(Effect6.map(directed));
5271
+ return ({ sync }) => (kind === "microphone" ? foreign3(() => sync.requestUnmute(participantId)) : foreign3(() => sync.requestStartCamera(participantId))).pipe(Effect7.map(directed));
3835
5272
  });
3836
5273
  remove = (participantId) => this.#command("moderation.remove", ["capability_decision", "command_commit", "target_delivery"], () => {
3837
5274
  assertIdentifier(participantId, "participant ID");
@@ -3845,16 +5282,16 @@ var ParticipantsControllerRuntime = class {
3845
5282
  assertIdentifier(requestId, "admission request ID");
3846
5283
  return ({ sync }) => foreign3(() => sync.deny(requestId));
3847
5284
  });
3848
- raiseHand = () => this.#command(
3849
- "participant.raised_hand.set",
3850
- ["membership_transition", "participant_result"],
3851
- () => ({ sync }) => foreign3(() => sync.setHandRaised(true))
3852
- );
3853
- lowerHand = () => this.#command(
3854
- "participant.raised_hand.set",
3855
- ["membership_transition", "participant_result"],
3856
- () => ({ sync }) => foreign3(() => sync.setHandRaised(false))
3857
- );
5285
+ raiseHand = () => this.#setHandRaised(true);
5286
+ lowerHand = () => this.#setHandRaised(false);
5287
+ #setHandRaised(raised) {
5288
+ const send = () => this.#command(
5289
+ "participant.raised_hand.set",
5290
+ ["membership_transition", "participant_result"],
5291
+ () => ({ sync }) => foreign3(() => sync.setHandRaised(raised))
5292
+ );
5293
+ return this.#offline?.hold({ kind: raised ? "hand_raise" : "hand_lower" }, send) ?? send();
5294
+ }
3858
5295
  renameSelf = (displayName) => this.#command("participant.rename", ["membership_transition", "participant_result"], () => {
3859
5296
  assertName(displayName, "display name");
3860
5297
  return ({ sync }) => foreign3(() => sync.setDisplayName(displayName));
@@ -3867,18 +5304,18 @@ var ParticipantsControllerRuntime = class {
3867
5304
  }
3868
5305
  #command(name, checkpoints, operation) {
3869
5306
  const diagnostic = this.#diagnostics?.startOperation(name);
3870
- return Effect6.try({ try: operation, catch: normalizeClientError }).pipe(
3871
- Effect6.tap(() => Effect6.sync(() => diagnostic?.observe("observed", checkpoints[0]))),
3872
- Effect6.flatMap((run) => this.#connection.runCommand(run)),
3873
- Effect6.mapError(normalizeClientError),
3874
- Effect6.tap(
3875
- () => Effect6.sync(() => {
5307
+ return Effect7.try({ try: operation, catch: normalizeClientError }).pipe(
5308
+ Effect7.tap(() => Effect7.sync(() => diagnostic?.observe("observed", checkpoints[0]))),
5309
+ Effect7.flatMap((run) => this.#connection.runCommand(run)),
5310
+ Effect7.mapError(normalizeClientError),
5311
+ Effect7.tap(
5312
+ () => Effect7.sync(() => {
3876
5313
  for (const checkpoint of checkpoints.slice(1)) diagnostic?.observe("observed", checkpoint);
3877
5314
  if (name.startsWith("moderation.")) diagnostic?.notObservable("target_application", "target_projection_is_conditional");
3878
5315
  diagnostic?.succeed();
3879
5316
  })
3880
5317
  ),
3881
- Effect6.tapError(() => Effect6.sync(() => diagnostic?.fail("command_failed")))
5318
+ Effect7.tapError(() => Effect7.sync(() => diagnostic?.fail("command_failed")))
3882
5319
  );
3883
5320
  }
3884
5321
  #bind(ports) {
@@ -3900,7 +5337,7 @@ var ParticipantsControllerRuntime = class {
3900
5337
  }
3901
5338
  };
3902
5339
  function foreign3(operation) {
3903
- return Effect6.tryPromise({ try: operation, catch: (cause) => cause });
5340
+ return Effect7.tryPromise({ try: operation, catch: (cause) => cause });
3904
5341
  }
3905
5342
  function assertIdentifier(value, label) {
3906
5343
  if (typeof value !== "string" || value.trim().length === 0) throw invalid(`A non-empty ${label} is required`);
@@ -4010,20 +5447,20 @@ function sameValues(left, right) {
4010
5447
  }
4011
5448
 
4012
5449
  // src/space-client/reactions-controller.ts
4013
- import { Clock as Clock4, Context as Context8, Effect as Effect7, Layer as Layer8 } from "effect";
5450
+ import { Clock as Clock4, Context as Context9, Effect as Effect8, Layer as Layer9 } from "effect";
4014
5451
  var MAX_VISIBLE_REACTIONS = 24;
4015
5452
  var REACTIONS = /* @__PURE__ */ new Set(["\u{1F44D}", "\u2764\uFE0F", "\u{1F602}", "\u{1F62E}", "\u{1F622}", "\u{1F389}"]);
4016
- var ReactionsControllerService = class extends Context8.Service()("@chalk/client/ReactionsController") {
5453
+ var ReactionsControllerService = class extends Context9.Service()("@chalk/client/ReactionsController") {
4017
5454
  };
4018
- var makeReactionsController = (connection, store, diagnostics) => Effect7.gen(function* () {
5455
+ var makeReactionsController = (connection, store, diagnostics) => Effect8.gen(function* () {
4019
5456
  const clock = yield* Clock4.Clock;
4020
- const scope = yield* Effect7.scope;
4021
- const context = yield* Effect7.context();
5457
+ const scope = yield* Effect8.scope;
5458
+ const context = yield* Effect8.context();
4022
5459
  const fork = (effect) => {
4023
- void Effect7.runForkWith(context)(Effect7.forkIn(effect, scope).pipe(Effect7.asVoid));
5460
+ void Effect8.runForkWith(context)(Effect8.forkIn(effect, scope).pipe(Effect8.asVoid));
4024
5461
  };
4025
5462
  const controller = new ReactionsControllerRuntime(connection, store, clock.currentTimeMillisUnsafe, fork, diagnostics);
4026
- yield* Effect7.addFinalizer(() => Effect7.sync(() => controller.dispose()));
5463
+ yield* Effect8.addFinalizer(() => Effect8.sync(() => controller.dispose()));
4027
5464
  return controller;
4028
5465
  });
4029
5466
  var ReactionsControllerRuntime = class {
@@ -4046,26 +5483,26 @@ var ReactionsControllerRuntime = class {
4046
5483
  }
4047
5484
  send = (emoji) => {
4048
5485
  const operation = this.#diagnostics?.startOperation("reaction.send");
4049
- return Effect7.try({
5486
+ return Effect8.try({
4050
5487
  try: () => {
4051
5488
  if (!REACTIONS.has(emoji)) throw new SpaceClientError({ code: "reaction.invalid", recoverable: false, message: "The reaction is not supported" });
4052
5489
  },
4053
5490
  catch: normalizeClientError
4054
5491
  }).pipe(
4055
- Effect7.tap(() => Effect7.sync(() => operation?.observe("observed", "authorization"))),
4056
- Effect7.andThen(this.#connection.runCommand(({ sync }) => foreign4(() => sync.sendReaction(emoji)))),
4057
- Effect7.tap(() => Effect7.sync(() => operation?.observe("observed", "accepted_commit"))),
4058
- Effect7.map(reactionFor),
4059
- Effect7.tap(
4060
- (reaction) => Effect7.sync(() => {
5492
+ Effect8.tap(() => Effect8.sync(() => operation?.observe("observed", "authorization"))),
5493
+ Effect8.andThen(this.#connection.runCommand(({ sync }) => foreign4(() => sync.sendReaction(emoji)))),
5494
+ Effect8.tap(() => Effect8.sync(() => operation?.observe("observed", "accepted_commit"))),
5495
+ Effect8.map(reactionFor),
5496
+ Effect8.tap(
5497
+ (reaction) => Effect8.sync(() => {
4061
5498
  this.#observe(reaction);
4062
5499
  operation?.observe("observed", "sender_result");
4063
5500
  operation?.notObservable("recipient_projection", "recipient_projection_is_conditional");
4064
5501
  operation?.succeed();
4065
5502
  })
4066
5503
  ),
4067
- Effect7.mapError(normalizeClientError),
4068
- Effect7.tapError(() => Effect7.sync(() => operation?.fail("send_failed")))
5504
+ Effect8.mapError(normalizeClientError),
5505
+ Effect8.tapError(() => Effect8.sync(() => operation?.fail("send_failed")))
4069
5506
  );
4070
5507
  };
4071
5508
  dispose() {
@@ -4095,9 +5532,9 @@ var ReactionsControllerRuntime = class {
4095
5532
  this.#expiryGenerations.set(reaction.eventId, generation);
4096
5533
  const delay = Math.max(0, expiresAt2 - this.#now());
4097
5534
  this.#fork(
4098
- Effect7.sleep(delay).pipe(
4099
- Effect7.andThen(
4100
- Effect7.sync(() => {
5535
+ Effect8.sleep(delay).pipe(
5536
+ Effect8.andThen(
5537
+ Effect8.sync(() => {
4101
5538
  if (this.#expiryGenerations.get(reaction.eventId) === generation) this.#remove(reaction.eventId);
4102
5539
  })
4103
5540
  )
@@ -4124,7 +5561,7 @@ var ReactionsControllerRuntime = class {
4124
5561
  }
4125
5562
  };
4126
5563
  function foreign4(operation) {
4127
- return Effect7.tryPromise({ try: operation, catch: (cause) => cause });
5564
+ return Effect8.tryPromise({ try: operation, catch: (cause) => cause });
4128
5565
  }
4129
5566
  function reactionFor(reaction) {
4130
5567
  return Object.freeze({ ...reaction });
@@ -4139,23 +5576,122 @@ function sameReactions(left, right) {
4139
5576
  });
4140
5577
  }
4141
5578
 
5579
+ // src/space-client/recording-controller.ts
5580
+ import { Context as Context10, Effect as Effect9, Layer as Layer10 } from "effect";
5581
+ var RecordingControllerService = class extends Context10.Service()("@chalk/client/RecordingController") {
5582
+ };
5583
+ var makeRecordingController = (connection, store, diagnostics) => Effect9.acquireRelease(
5584
+ Effect9.sync(() => new RecordingControllerRuntime(connection, store, diagnostics)),
5585
+ (controller) => Effect9.sync(() => controller.dispose())
5586
+ );
5587
+ var RecordingControllerRuntime = class {
5588
+ #connection;
5589
+ #store;
5590
+ #diagnostics;
5591
+ #subscriptions = { ports: null, recording: null };
5592
+ constructor(connection, store, diagnostics) {
5593
+ this.#connection = connection;
5594
+ this.#store = store;
5595
+ this.#diagnostics = diagnostics;
5596
+ this.#subscriptions.ports = connection.subscribePorts((ports) => this.#bind(ports));
5597
+ }
5598
+ start = () => this.#command(
5599
+ "recording.start",
5600
+ "start_failed",
5601
+ () => this.#authorizeStart(),
5602
+ (_, { sync }) => foreign5(() => sync.startRecording()).pipe(Effect9.map(({ recordingId }) => Object.freeze({ recordingId })))
5603
+ );
5604
+ stop = () => this.#command(
5605
+ "recording.stop",
5606
+ "stop_failed",
5607
+ () => this.#authorizeStop(),
5608
+ (recordingId, { sync }) => foreign5(() => sync.stopRecording(recordingId)).pipe(Effect9.asVoid)
5609
+ );
5610
+ dispose() {
5611
+ this.#subscriptions.ports?.();
5612
+ this.#subscriptions.recording?.();
5613
+ this.#subscriptions.ports = null;
5614
+ this.#subscriptions.recording = null;
5615
+ this.#store.updateRecording(EMPTY_RECORDING);
5616
+ }
5617
+ #authorizeStart() {
5618
+ const snapshot = this.#store.getSnapshot();
5619
+ assertConnectedAndAllowed(snapshot);
5620
+ const current = snapshot.recording.current;
5621
+ if (current && current.status !== "stopped" && current.status !== "failed") throw invalidState("A Recording is already active");
5622
+ }
5623
+ #authorizeStop() {
5624
+ const snapshot = this.#store.getSnapshot();
5625
+ assertConnectedAndAllowed(snapshot);
5626
+ const current = snapshot.recording.current;
5627
+ if (!current || current.status !== "recording") throw invalidState("No active Recording can be stopped");
5628
+ return current.recordingId;
5629
+ }
5630
+ #command(name, failureCode, authorize, execute) {
5631
+ const operation = this.#diagnostics?.startOperation(name);
5632
+ const authorized = Effect9.try({ try: authorize, catch: normalizeClientError });
5633
+ return authorized.pipe(
5634
+ Effect9.tap(() => Effect9.sync(() => operation?.observe("observed", "authorization"))),
5635
+ Effect9.flatMap((authorization) => this.#connection.runCommand((ports) => execute(authorization, ports))),
5636
+ Effect9.tap(() => Effect9.sync(() => operation?.observe("observed", "accepted_commit"))),
5637
+ Effect9.tap(() => Effect9.sync(() => operation?.succeed())),
5638
+ Effect9.mapError(normalizeClientError),
5639
+ Effect9.tapError(() => Effect9.sync(() => operation?.fail(failureCode)))
5640
+ );
5641
+ }
5642
+ #bind(ports) {
5643
+ this.#subscriptions.recording?.();
5644
+ this.#subscriptions.recording = null;
5645
+ if (!ports) {
5646
+ this.#store.updateRecording(EMPTY_RECORDING);
5647
+ return;
5648
+ }
5649
+ this.#subscriptions.recording = ports.sync.subscribe((snapshot) => this.#observe(recordingFor(snapshot)));
5650
+ }
5651
+ #observe(next) {
5652
+ const current = this.#store.getSnapshot().recording;
5653
+ if (sameRecording(current, next)) return;
5654
+ this.#store.updateRecording(next);
5655
+ }
5656
+ };
5657
+ var EMPTY_RECORDING = Object.freeze({ current: null });
5658
+ function recordingFor(snapshot) {
5659
+ const recording = (snapshot.optimisticControl ?? snapshot.control)?.recording ?? null;
5660
+ return Object.freeze({ current: recording ? Object.freeze({ ...recording }) : null });
5661
+ }
5662
+ function sameRecording(left, right) {
5663
+ if (left.current === right.current) return true;
5664
+ if (!left.current || !right.current) return false;
5665
+ return left.current.recordingId === right.current.recordingId && left.current.status === right.current.status && left.current.failureCode === right.current.failureCode;
5666
+ }
5667
+ function assertConnectedAndAllowed(snapshot) {
5668
+ if (snapshot.connection.status !== "live" || !snapshot.connection.episode) throw invalidState("Recording commands require a live Episode");
5669
+ if (!snapshot.self.can("manageRecording")) throw new SpaceClientError({ code: "command.rejected", recoverable: false, message: "Recording is not allowed for this Participant" });
5670
+ }
5671
+ function invalidState(message) {
5672
+ return new SpaceClientError({ code: "connection.invalid_state", recoverable: false, message });
5673
+ }
5674
+ function foreign5(operation) {
5675
+ return Effect9.tryPromise({ try: operation, catch: (cause) => cause });
5676
+ }
5677
+
4142
5678
  // src/space-client/whiteboard-controller.ts
4143
- import { Context as Context9, Effect as Effect8, Layer as Layer9 } from "effect";
5679
+ import { Context as Context11, Effect as Effect10, Layer as Layer11 } from "effect";
4144
5680
  var EMPTY_SUMMARY = Object.freeze({ status: "unsubscribed", sceneId: null, revision: null, capabilities: Object.freeze([]), canDraw: false, canClear: false, presenting: false, error: null });
4145
- var WhiteboardControllerService = class extends Context9.Service()("@chalk/client/WhiteboardController") {
5681
+ var WhiteboardControllerService = class extends Context11.Service()("@chalk/client/WhiteboardController") {
4146
5682
  };
4147
- var makeWhiteboardController = (connection, store, create) => Effect8.gen(function* () {
4148
- const context = yield* Effect8.context();
4149
- const controller = yield* Effect8.sync(() => {
5683
+ var makeWhiteboardController = (connection, store, create) => Effect10.gen(function* () {
5684
+ const context = yield* Effect10.context();
5685
+ const controller = yield* Effect10.sync(() => {
4150
5686
  let instance = null;
4151
5687
  const transport = create?.({
4152
- token: () => Effect8.runPromiseWith(context)(connection.getSyncToken()),
5688
+ token: () => Effect10.runPromiseWith(context)(connection.getSyncToken()),
4153
5689
  onSummary: (summary) => instance?.observe(summary)
4154
5690
  }) ?? null;
4155
5691
  instance = new WhiteboardControllerRuntime(connection, store, transport);
4156
5692
  return instance;
4157
5693
  });
4158
- yield* Effect8.addFinalizer(() => Effect8.sync(() => controller.dispose()));
5694
+ yield* Effect10.addFinalizer(() => Effect10.sync(() => controller.dispose()));
4159
5695
  return controller;
4160
5696
  });
4161
5697
  var WhiteboardControllerRuntime = class {
@@ -4217,15 +5753,17 @@ function sameError2(left, right) {
4217
5753
  }
4218
5754
 
4219
5755
  // src/space-client/controller-effects.ts
4220
- var ControllerEffectsService = class extends Context10.Service()("@chalk/client/ControllerEffects") {
5756
+ var ControllerEffectsService = class extends Context12.Service()("@chalk/client/ControllerEffects") {
4221
5757
  };
4222
- var makeControllerEffects = (input) => Effect9.gen(function* () {
5758
+ var makeControllerEffects = (input) => Effect11.gen(function* () {
5759
+ const offlineActions = yield* makeOfflineActions({ connection: input.connection, store: input.store, storage: input.offlineActionsStorage, storageKey: input.offlineActionsStorageKey });
4223
5760
  const media = yield* makeMediaController(input.connection, input.store, input.mediaDeviceSelection, input.episodeDiagnostics);
4224
- const chat = yield* makeChatController({ connection: input.connection, store: input.store, createTransport: input.featureFactories?.createChatFileTransport, apiBaseUrl: input.apiBaseUrl, fetch: input.fetch, episodeDiagnostics: input.episodeDiagnostics });
4225
- const participants = yield* makeParticipantsController(input.connection, input.store, input.episodeDiagnostics);
5761
+ const chat = yield* makeChatController({ connection: input.connection, store: input.store, createTransport: input.featureFactories?.createChatFileTransport, apiBaseUrl: input.apiBaseUrl, fetch: input.fetch, episodeDiagnostics: input.episodeDiagnostics, offline: offlineActions });
5762
+ const participants = yield* makeParticipantsController(input.connection, input.store, input.episodeDiagnostics, offlineActions);
4226
5763
  const reactions = yield* makeReactionsController(input.connection, input.store, input.episodeDiagnostics);
5764
+ const recording = yield* makeRecordingController(input.connection, input.store, input.episodeDiagnostics);
4227
5765
  const whiteboard = yield* makeWhiteboardController(input.connection, input.store, input.featureFactories?.createWhiteboardClient);
4228
- return { media, chat, participants, reactions, whiteboard };
5766
+ return { media, offline: offlineActions, chat, participants, reactions, recording, whiteboard };
4229
5767
  });
4230
5768
 
4231
5769
  // src/space-client/media-device-selection.ts
@@ -4280,7 +5818,7 @@ function requireDeviceId(deviceId) {
4280
5818
  }
4281
5819
 
4282
5820
  // src/space-client/episode-diagnostic-runtime.ts
4283
- import { encodedEventSize, getActionContract, parseDiagnosticEventDraft, redactDiagnosticAttributes } from "@q9labsai/diagnostics-contracts";
5821
+ import { encodedEventSize, getActionContract, parseDiagnosticEventDraft as parseDiagnosticEventDraft2, redactDiagnosticAttributes as redactDiagnosticAttributes2 } from "@q9labsai/diagnostics-contracts";
4284
5822
  var DEFAULT_RING_EVENTS = 256;
4285
5823
  var DEFAULT_RING_BYTES = 256 * 1024;
4286
5824
  var DEFAULT_RING_AGE_MS = 2 * 60 * 1e3;
@@ -4388,6 +5926,15 @@ var EpisodeDiagnosticRuntime = class {
4388
5926
  activeContext() {
4389
5927
  return this.#enabled() ? this.context() : void 0;
4390
5928
  }
5929
+ /** Internal Feedback seam. It returns only a short-lived verified credential to the private transport. */
5930
+ feedbackCredentialUnsafe() {
5931
+ if (!this.#enabled() || !this.#credential) return null;
5932
+ return Object.freeze({ ...this.#credential });
5933
+ }
5934
+ feedbackAvailabilityUnsafe() {
5935
+ if (this.#disposed) return "disposed";
5936
+ return this.#enabled() ? "available" : "disabled";
5937
+ }
4391
5938
  startOperation(name, attributes, parentOperationRef, producerOperationRef, correlationInput) {
4392
5939
  if (!this.#enabled()) return void 0;
4393
5940
  const credentialGeneration = this.#credential?.generation;
@@ -4453,7 +6000,7 @@ var EpisodeDiagnosticRuntime = class {
4453
6000
  });
4454
6001
  }
4455
6002
  #draft(input) {
4456
- return parseDiagnosticEventDraft(
6003
+ return parseDiagnosticEventDraft2(
4457
6004
  buildDraftInput(input, {
4458
6005
  eventId: safeId(this.#createId()),
4459
6006
  producerSequence: ++this.#sequence,
@@ -4764,7 +6311,7 @@ function buildDraftInput(input, context) {
4764
6311
  expectation: expectationFor(contract, checkpoint),
4765
6312
  correlation: optionalCorrelation(correlation),
4766
6313
  release: context.release,
4767
- attributes: redactDiagnosticAttributes(input.attributes).attributes
6314
+ attributes: redactDiagnosticAttributes2(input.attributes).attributes
4768
6315
  };
4769
6316
  }
4770
6317
  function eventReferences(input) {
@@ -4885,9 +6432,125 @@ function renderRegistry() {
4885
6432
  return target[REGISTRY];
4886
6433
  }
4887
6434
 
6435
+ // src/feedback/controller.ts
6436
+ var DEFAULT_SDK = Object.freeze({ client: "@q9labsai/chalk-client" });
6437
+ var DEFAULT_PLATFORM = Object.freeze({ kind: "web" });
6438
+ function createFeedbackController(context) {
6439
+ const transport = createFeedbackTransport({
6440
+ baseUrl: context.apiBaseUrl,
6441
+ fetch: context.fetch,
6442
+ credential: context.diagnosticCredential,
6443
+ telemetry: () => context.telemetry ?? context.diagnosticContext()
6444
+ });
6445
+ let disposed = false;
6446
+ const ensureActive = () => {
6447
+ if (disposed) throw new FeedbackTransportError({ code: "unavailable", recoverable: false, message: "Feedback has been disposed." });
6448
+ };
6449
+ const prepare = async (input = {}) => {
6450
+ ensureActive();
6451
+ const collectionInput = evidenceInput(input.evidence);
6452
+ const screenshot = await resolveScreenshot(input.screenshot_provider ?? input.screenshotProvider);
6453
+ ensureActive();
6454
+ const initialInput = { ...collectionInput, screenshot };
6455
+ const idempotencyKey = idempotencyKeyFor(context.createId);
6456
+ const preparedEvidence = collectFeedbackEvidenceFromContext(initialInput, context);
6457
+ const preparedScreenshot = capturedScreenshot(screenshot) ? screenshotPayload(screenshot).screenshot : void 0;
6458
+ let cachedRequest;
6459
+ const prepared = {
6460
+ idempotency_key: idempotencyKey,
6461
+ evidence: preparedEvidence,
6462
+ ...preparedScreenshot ? { screenshot: preparedScreenshot } : {},
6463
+ collect: (nextInput) => {
6464
+ ensureActive();
6465
+ return collectFeedbackEvidenceFromContext(nextInput ?? initialInput, context);
6466
+ },
6467
+ send: async ({ category, message }) => {
6468
+ ensureActive();
6469
+ const normalizedMessage = assertFeedbackMessage(message);
6470
+ const request = cachedRequest ?? parseFeedbackReportRequest({
6471
+ schema_version: FEEDBACK_REQUEST_SCHEMA_VERSION,
6472
+ category,
6473
+ message: normalizedMessage,
6474
+ source: input.source ?? context.source,
6475
+ evidence: preparedEvidence,
6476
+ ...preparedScreenshot ? { screenshot: preparedScreenshot } : {}
6477
+ });
6478
+ cachedRequest ??= request;
6479
+ return transport.send({ request, idempotencyKey });
6480
+ }
6481
+ };
6482
+ return prepared;
6483
+ };
6484
+ const send = async (input) => {
6485
+ ensureActive();
6486
+ const source = input.source ?? context.source;
6487
+ const evidenceInput2 = input.evidence ?? defaultEvidenceInput();
6488
+ const screenshot = input.screenshot ?? evidenceInput2.screenshot;
6489
+ return sendRequest({ category: input.category, message: input.message, source, evidence: { ...evidenceInput2, ...screenshot ? { screenshot } : {} }, idempotencyKey: input.idempotency_key ?? input.idempotencyKey ?? idempotencyKeyFor(context.createId) });
6490
+ };
6491
+ const sendRequest = async (input) => {
6492
+ ensureActive();
6493
+ const evidence = collectFeedbackEvidenceFromContext(input.evidence, context);
6494
+ const request = parseFeedbackReportRequest({ schema_version: FEEDBACK_REQUEST_SCHEMA_VERSION, category: input.category, message: assertFeedbackMessage(input.message), source: input.source ?? context.source, evidence, ...screenshotPayload(input.evidence.screenshot) });
6495
+ return transport.send({ request, idempotencyKey: input.idempotencyKey });
6496
+ };
6497
+ return {
6498
+ prepare,
6499
+ send,
6500
+ dispose: () => {
6501
+ disposed = true;
6502
+ }
6503
+ };
6504
+ }
6505
+ function evidenceInput(input) {
6506
+ const base = { sdk: input?.sdk ?? DEFAULT_SDK, platform: input?.platform ?? DEFAULT_PLATFORM };
6507
+ if (!input) return base;
6508
+ return { ...base, ...optionalEvidenceInput(input) };
6509
+ }
6510
+ function optionalEvidenceInput(input) {
6511
+ return {
6512
+ ...input.app ? { app: input.app } : {},
6513
+ ...input.local_state ? { local_state: input.local_state } : {},
6514
+ ...input.cookies ? { cookies: input.cookies } : {}
6515
+ };
6516
+ }
6517
+ function defaultEvidenceInput() {
6518
+ return { sdk: DEFAULT_SDK, platform: DEFAULT_PLATFORM };
6519
+ }
6520
+ async function resolveScreenshot(provider) {
6521
+ if (!provider) return { state: "unavailable", failure_code: "unsupported" };
6522
+ let timeout;
6523
+ try {
6524
+ return await Promise.race([
6525
+ provider(),
6526
+ new Promise((resolve) => {
6527
+ timeout = globalThis.setTimeout(() => resolve({ state: "unavailable", failure_code: "capture_failed" }), 5e3);
6528
+ })
6529
+ ]);
6530
+ } catch {
6531
+ return { state: "unavailable", failure_code: "capture_failed" };
6532
+ } finally {
6533
+ globalThis.clearTimeout(timeout);
6534
+ }
6535
+ }
6536
+ function capturedScreenshot(input) {
6537
+ return input.state === "captured" || input.state === "partial" ? input : void 0;
6538
+ }
6539
+ function screenshotPayload(input) {
6540
+ if (!input) return {};
6541
+ const screenshot = capturedScreenshot(input);
6542
+ return screenshot ? { screenshot: { schema_version: "FeedbackScreenshot/v1", mime_type: screenshot.mime_type, width: screenshot.width, height: screenshot.height, captured_at: screenshot.captured_at, data_base64: screenshot.data_base64 } } : {};
6543
+ }
6544
+ function idempotencyKeyFor(createId) {
6545
+ const generated = createId();
6546
+ if (/^[A-Za-z0-9_-]{16,128}$/u.test(generated)) return generated;
6547
+ if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
6548
+ return `feedback-${Date.now()}-${Math.random().toString(36).slice(2, 18)}`;
6549
+ }
6550
+
4888
6551
  // src/space-client/core.ts
4889
6552
  var syncClientsByDependencies = /* @__PURE__ */ new WeakMap();
4890
- var SpaceClientCoreService = class extends Context11.Service()("@chalk/client/SpaceClientCore") {
6553
+ var SpaceClientCoreService = class extends Context13.Service()("@chalk/client/SpaceClientCore") {
4891
6554
  };
4892
6555
  var makeSpaceClientCoreLayer = (options, platform = {}) => {
4893
6556
  const apiBaseUrl = normalizedBaseUrl2(platform.apiBaseUrl ?? options.baseUrl ?? "https://api.chalkmeet.com");
@@ -4900,7 +6563,7 @@ var makeSpaceClientCoreLayer = (options, platform = {}) => {
4900
6563
  now: baseDependencies.clock.now,
4901
6564
  setTimeout: baseDependencies.clock.setTimeout,
4902
6565
  clearTimeout: baseDependencies.clock.clearTimeout,
4903
- release: { id: "chalk-client@4.1.15" }
6566
+ release: { id: "chalk-client@4.1.16" }
4904
6567
  });
4905
6568
  const diagnosticSyncClients = /* @__PURE__ */ new Set();
4906
6569
  const dependencies = {
@@ -4922,6 +6585,7 @@ var makeSpaceClientCoreLayer = (options, platform = {}) => {
4922
6585
  initialCameraEnabled: platform.initialCameraEnabled,
4923
6586
  telemetry: platform.telemetry,
4924
6587
  recordRtcSummary: platform.recordRtcSummary,
6588
+ recordReconnect: platform.recordReconnect,
4925
6589
  dependencies,
4926
6590
  diagnostics: {
4927
6591
  onEvent: (event) => {
@@ -4930,30 +6594,49 @@ var makeSpaceClientCoreLayer = (options, platform = {}) => {
4930
6594
  }
4931
6595
  }
4932
6596
  });
4933
- return makeSpaceClientCoreLayerFromServices(options, platform).pipe(Layer11.provideMerge(Layer11.mergeAll(makeSpaceStoreLayer, lifecycle, makeConnectionPlatformLayer(dependencies), liveClock(dependencies))));
6597
+ return makeSpaceClientCoreLayerFromServices(options, platform).pipe(Layer13.provideMerge(Layer13.mergeAll(makeSpaceStoreLayer, lifecycle, makeConnectionPlatformLayer(dependencies), liveClock(dependencies))));
4934
6598
  };
4935
- var makeSpaceClientCoreLayerFromServices = (options, platform = {}) => Layer11.effect(
6599
+ var makeSpaceClientCoreLayerFromServices = (options, platform = {}) => Layer13.effect(
4936
6600
  SpaceClientCoreService,
4937
- Effect10.gen(function* () {
4938
- const store = yield* Effect10.service(SpaceStoreService);
4939
- const lifecycle = yield* Effect10.service(ConnectionLifecycleService);
4940
- const dependencies = yield* Effect10.service(ConnectionPlatformService);
6601
+ Effect12.gen(function* () {
6602
+ const store = yield* Effect12.service(SpaceStoreService);
6603
+ const lifecycle = yield* Effect12.service(ConnectionLifecycleService);
6604
+ const dependencies = yield* Effect12.service(ConnectionPlatformService);
4941
6605
  const space = options.space.trim();
4942
- if (space.length === 0) return yield* Effect10.die(new TypeError("A Space slug is required"));
4943
- if (typeof options.getAccess !== "function") return yield* Effect10.die(new TypeError("getAccess is required"));
6606
+ if (space.length === 0) return yield* Effect12.die(new TypeError("A Space slug is required"));
6607
+ if (typeof options.getAccess !== "function") return yield* Effect12.die(new TypeError("getAccess is required"));
4944
6608
  const apiBaseUrl = normalizedBaseUrl2(platform.apiBaseUrl ?? options.baseUrl ?? "https://api.chalkmeet.com");
4945
6609
  const selection = new MediaDeviceSelection(dependencies.mediaDevices);
4946
6610
  const episodeDiagnostics = episodeDiagnosticsForDependencies(dependencies);
4947
6611
  if (episodeDiagnostics) registerEpisodeDiagnosticConnection(lifecycle, episodeDiagnostics);
4948
- const controllers = yield* makeControllerEffects({ apiBaseUrl, connection: lifecycle, store, mediaDeviceSelection: selection, featureFactories: dependencies, fetch: platform.fetch, episodeDiagnostics });
4949
- return yield* Effect10.acquireRelease(
4950
- Effect10.sync(() => new SpaceClientCore(lifecycle, selection, store, controllers, episodeDiagnostics, dependencies, syncClientsByDependencies.get(dependencies))),
4951
- (core) => Effect10.sync(() => core.dispose())
6612
+ const controllers = yield* makeControllerEffects({
6613
+ offlineActionsStorage: options.offlineActionsStorage,
6614
+ offlineActionsStorageKey: `chalk:offline-actions:${JSON.stringify([apiBaseUrl, space])}`,
6615
+ apiBaseUrl,
6616
+ connection: lifecycle,
6617
+ store,
6618
+ mediaDeviceSelection: selection,
6619
+ featureFactories: dependencies,
6620
+ fetch: platform.fetch,
6621
+ episodeDiagnostics
6622
+ });
6623
+ return yield* Effect12.acquireRelease(
6624
+ Effect12.sync(
6625
+ () => new SpaceClientCore(lifecycle, selection, store, controllers, episodeDiagnostics, dependencies, syncClientsByDependencies.get(dependencies), {
6626
+ apiBaseUrl,
6627
+ fetch: platform.fetch,
6628
+ createId: platform.randomUUID ?? dependencies.createId ?? lifecycle.createId,
6629
+ source: platform.feedbackSource ?? "embedded",
6630
+ telemetry: platform.telemetry
6631
+ })
6632
+ ),
6633
+ (core) => Effect12.sync(() => core.dispose())
4952
6634
  );
4953
6635
  })
4954
6636
  );
4955
6637
  var SpaceClientCore = class {
4956
6638
  controllers;
6639
+ feedback;
4957
6640
  #connection;
4958
6641
  #store;
4959
6642
  #episodeDiagnostics;
@@ -4968,13 +6651,29 @@ var SpaceClientCore = class {
4968
6651
  #leaveOperation;
4969
6652
  #participantJoinOperation;
4970
6653
  #syncConnectOperation;
4971
- constructor(connection, _selection, store, controllers, episodeDiagnostics, episodeDiagnosticDependencies, episodeDiagnosticSyncClients) {
6654
+ constructor(connection, _selection, store, controllers, episodeDiagnostics, episodeDiagnosticDependencies, episodeDiagnosticSyncClients, feedback) {
4972
6655
  this.#connection = connection;
4973
6656
  this.#store = store;
4974
6657
  this.controllers = controllers;
4975
6658
  this.#episodeDiagnostics = episodeDiagnostics;
4976
6659
  this.#episodeDiagnosticDependencies = episodeDiagnosticDependencies;
4977
6660
  this.#episodeDiagnosticSyncClients = episodeDiagnosticSyncClients;
6661
+ this.feedback = createFeedbackController({
6662
+ apiBaseUrl: feedback?.apiBaseUrl ?? "https://api.chalkmeet.com",
6663
+ fetch: feedback?.fetch,
6664
+ createId: feedback?.createId ?? connection.createId,
6665
+ now: connection.nowUnsafe,
6666
+ source: feedback?.source ?? "embedded",
6667
+ telemetry: feedback?.telemetry,
6668
+ diagnosticContext: () => episodeDiagnostics?.activeContext(),
6669
+ connection: connection.getSnapshot,
6670
+ diagnosticCredential: () => episodeDiagnostics?.feedbackCredentialUnsafe() ?? null,
6671
+ diagnosticAvailability: () => episodeDiagnostics?.feedbackAvailabilityUnsafe() ?? "unavailable",
6672
+ diagnosticSnapshot: () => {
6673
+ const inspected = episodeDiagnostics?.inspect();
6674
+ return { dropped: inspected?.dropped ?? 0, events: inspected?.ring ?? [] };
6675
+ }
6676
+ });
4978
6677
  this.#store.updateConnection(connection.getSnapshot());
4979
6678
  this.#previous = this.#store.getSnapshot();
4980
6679
  this.#unsubscribeStore = this.#store.subscribe(() => this.#publishStore());
@@ -4992,47 +6691,47 @@ var SpaceClientCore = class {
4992
6691
  join(options = {}) {
4993
6692
  this.#participantJoinOperation = this.#episodeDiagnostics?.startOperation("participant.join");
4994
6693
  this.#syncConnectOperation = this.#episodeDiagnostics?.startOperation("sync.connect");
4995
- return Effect10.sync(() => this.#requireActive()).pipe(
4996
- Effect10.tap(() => Effect10.sync(() => this.controllers.media.configure(options))),
4997
- Effect10.andThen(this.#connection.configureJoin(options)),
4998
- Effect10.andThen(this.#connection.join()),
4999
- Effect10.andThen(options.displayName === void 0 ? Effect10.void : this.controllers.participants.renameSelf(options.displayName)),
5000
- Effect10.tap(
5001
- () => Effect10.sync(() => {
6694
+ return Effect12.sync(() => this.#requireActive()).pipe(
6695
+ Effect12.tap(() => Effect12.sync(() => this.controllers.media.configure(options))),
6696
+ Effect12.andThen(this.#connection.configureJoin(options)),
6697
+ Effect12.andThen(this.#connection.join()),
6698
+ Effect12.andThen(options.displayName === void 0 ? Effect12.void : this.controllers.participants.renameSelf(options.displayName)),
6699
+ Effect12.tap(
6700
+ () => Effect12.sync(() => {
5002
6701
  this.#participantJoinOperation?.succeed();
5003
6702
  this.#syncConnectOperation?.succeed();
5004
6703
  })
5005
6704
  ),
5006
- Effect10.tapError(
5007
- () => Effect10.sync(() => {
6705
+ Effect12.tapError(
6706
+ () => Effect12.sync(() => {
5008
6707
  this.#participantJoinOperation?.fail("join_failed");
5009
6708
  this.#syncConnectOperation?.fail("connect_failed");
5010
6709
  })
5011
6710
  ),
5012
- Effect10.mapError(normalizeClientError)
6711
+ Effect12.mapError(normalizeClientError)
5013
6712
  );
5014
6713
  }
5015
6714
  leave() {
5016
6715
  this.#leaveOperation = this.#episodeDiagnostics?.startOperation("participant.leave");
5017
6716
  return this.#connection.leave().pipe(
5018
- Effect10.tap(() => Effect10.sync(() => this.#leaveOperation?.succeed())),
5019
- Effect10.tapError(() => Effect10.sync(() => this.#leaveOperation?.fail("leave_failed"))),
5020
- Effect10.mapError(normalizeClientError)
6717
+ Effect12.tap(() => Effect12.sync(() => this.#leaveOperation?.succeed())),
6718
+ Effect12.tapError(() => Effect12.sync(() => this.#leaveOperation?.fail("leave_failed"))),
6719
+ Effect12.mapError(normalizeClientError)
5021
6720
  );
5022
6721
  }
5023
6722
  endEpisode() {
5024
6723
  return trackedEffect(
5025
6724
  this.#episodeDiagnostics,
5026
6725
  "episode.end.authorized",
5027
- this.#connection.runCommand(({ sync }) => foreign5(() => sync.endEpisode()).pipe(Effect10.tap(() => this.#connection.confirmEpisodeEnded)))
5028
- ).pipe(Effect10.asVoid, Effect10.mapError(normalizeClientError));
6726
+ this.#connection.runCommand(({ sync }) => foreign6(() => sync.endEpisode()).pipe(Effect12.tap(() => this.#connection.confirmEpisodeEnded)))
6727
+ ).pipe(Effect12.asVoid, Effect12.mapError(normalizeClientError));
5029
6728
  }
5030
6729
  extendEpisode(minutes) {
5031
6730
  return trackedEffect(
5032
6731
  this.#episodeDiagnostics,
5033
6732
  "episode.deadline.extend",
5034
- this.#connection.runCommand(({ sync }) => foreign5(() => sync.extendEpisode(minutes)))
5035
- ).pipe(Effect10.asVoid, Effect10.mapError(normalizeClientError));
6733
+ this.#connection.runCommand(({ sync }) => foreign6(() => sync.extendEpisode(minutes)))
6734
+ ).pipe(Effect12.asVoid, Effect12.mapError(normalizeClientError));
5036
6735
  }
5037
6736
  on(event, handler) {
5038
6737
  this.#handlers[event].add(handler);
@@ -5045,6 +6744,7 @@ var SpaceClientCore = class {
5045
6744
  this.#unsubscribeStore?.();
5046
6745
  this.#unsubscribeConnection = null;
5047
6746
  this.#unsubscribeStore = null;
6747
+ this.feedback.dispose();
5048
6748
  this.#disposeDiagnostics();
5049
6749
  this.#clearHandlers();
5050
6750
  }
@@ -5146,25 +6846,25 @@ var SpaceClientCore = class {
5146
6846
  }
5147
6847
  };
5148
6848
  function liveClock(dependencies) {
5149
- return Layer11.succeed(Clock5.Clock, {
6849
+ return Layer13.succeed(Clock5.Clock, {
5150
6850
  currentTimeMillisUnsafe: dependencies.clock.now,
5151
- currentTimeMillis: Effect10.sync(dependencies.clock.now),
6851
+ currentTimeMillis: Effect12.sync(dependencies.clock.now),
5152
6852
  currentTimeNanosUnsafe: () => BigInt(dependencies.clock.now()) * 1000000n,
5153
- currentTimeNanos: Effect10.sync(() => BigInt(dependencies.clock.now()) * 1000000n),
5154
- sleep: (duration) => Effect10.callback((resume) => {
5155
- const handle = dependencies.clock.setTimeout(() => resume(Effect10.void), Duration2.toMillis(duration));
5156
- return Effect10.sync(() => dependencies.clock.clearTimeout(handle));
6853
+ currentTimeNanos: Effect12.sync(() => BigInt(dependencies.clock.now()) * 1000000n),
6854
+ sleep: (duration) => Effect12.callback((resume) => {
6855
+ const handle = dependencies.clock.setTimeout(() => resume(Effect12.void), Duration2.toMillis(duration));
6856
+ return Effect12.sync(() => dependencies.clock.clearTimeout(handle));
5157
6857
  })
5158
6858
  });
5159
6859
  }
5160
- function foreign5(operation) {
5161
- return Effect10.tryPromise({ try: operation, catch: (cause) => cause });
6860
+ function foreign6(operation) {
6861
+ return Effect12.tryPromise({ try: operation, catch: (cause) => cause });
5162
6862
  }
5163
6863
  function trackedEffect(diagnostics, name, effect) {
5164
6864
  const operation = diagnostics?.startOperation(name);
5165
6865
  return effect.pipe(
5166
- Effect10.tap(() => Effect10.sync(() => operation?.succeed())),
5167
- Effect10.tapError(() => Effect10.sync(() => operation?.fail("operation_failed")))
6866
+ Effect12.tap(() => Effect12.sync(() => operation?.succeed())),
6867
+ Effect12.tapError(() => Effect12.sync(() => operation?.fail("operation_failed")))
5168
6868
  );
5169
6869
  }
5170
6870
  async function resolveAccessGrant(input) {
@@ -5186,7 +6886,7 @@ function diagnosticAccessOperation(request) {
5186
6886
  }
5187
6887
  async function fetchAccessGrant2(input) {
5188
6888
  if (input.platform.connectionAccess) return input.platform.connectionAccess(input.request);
5189
- return input.options.getAccess({ space: input.options.space.trim(), reason: accessReason(input.request?.reason) });
6889
+ return input.options.getAccess({ space: input.options.space.trim(), reason: accessReason(input.request?.reason), signal: input.request?.signal });
5190
6890
  }
5191
6891
  function accessReason(reason) {
5192
6892
  if (reason === void 0 || reason === "join") return "join";
@@ -5225,17 +6925,17 @@ function sameDiagnosticTrack(left, right) {
5225
6925
  }
5226
6926
 
5227
6927
  // src/space-client/space-client.ts
5228
- import { Effect as Effect12, ManagedRuntime } from "effect";
6928
+ import { Effect as Effect14, ManagedRuntime } from "effect";
5229
6929
 
5230
6930
  // src/space-client/promise-facade.ts
5231
- import { Effect as Effect11 } from "effect";
6931
+ import { Effect as Effect13 } from "effect";
5232
6932
  function toPromiseController(runtime, controller) {
5233
6933
  const projected = {};
5234
6934
  for (const [name, command] of Object.entries(controller)) {
5235
6935
  if (name === "configure" || name === "dispose") continue;
5236
6936
  projected[name] = (...args) => {
5237
6937
  const result = command(...args);
5238
- return Effect11.isEffect(result) ? runtime.runPromise(result) : result;
6938
+ return Effect13.isEffect(result) ? runtime.runPromise(result) : result;
5239
6939
  };
5240
6940
  }
5241
6941
  return projected;
@@ -5243,24 +6943,30 @@ function toPromiseController(runtime, controller) {
5243
6943
 
5244
6944
  // src/space-client/space-client.ts
5245
6945
  var PromiseSpaceClient = class {
6946
+ feedback;
5246
6947
  media;
5247
6948
  chat;
5248
6949
  participants;
6950
+ offline;
5249
6951
  reactions;
6952
+ recording;
5250
6953
  whiteboard;
5251
6954
  #core;
5252
6955
  #runtime;
5253
6956
  #disposed = false;
5254
6957
  constructor(options, platform) {
5255
6958
  this.#runtime = ManagedRuntime.make(makeSpaceClientCoreLayer(options, platform));
5256
- this.#core = this.#runtime.runSync(Effect12.service(SpaceClientCoreService));
6959
+ this.#core = this.#runtime.runSync(Effect14.service(SpaceClientCoreService));
5257
6960
  const controllers = this.#core.controllers;
6961
+ this.feedback = this.#core.feedback;
5258
6962
  this.media = toPromiseController(this.#runtime, controllers.media);
5259
6963
  const chat = toPromiseController(this.#runtime, controllers.chat);
5260
- const { upload, url, ...chatCommands } = chat;
5261
- this.chat = { ...chatCommands, files: { upload, url } };
6964
+ const { upload, url, resolveUrl, ...chatCommands } = chat;
6965
+ this.chat = { ...chatCommands, files: { upload, url, resolveUrl } };
6966
+ this.offline = toPromiseController(this.#runtime, controllers.offline);
5262
6967
  this.participants = toPromiseController(this.#runtime, controllers.participants);
5263
6968
  this.reactions = toPromiseController(this.#runtime, controllers.reactions);
6969
+ this.recording = toPromiseController(this.#runtime, controllers.recording);
5264
6970
  this.whiteboard = toPromiseController(this.#runtime, controllers.whiteboard);
5265
6971
  }
5266
6972
  join = (options) => this.#runtime.runPromise(this.#core.join(options));
@@ -5312,6 +7018,35 @@ export {
5312
7018
  SpaceStoreService,
5313
7019
  makeSpaceStoreLayer,
5314
7020
  makeFakeSpaceStoreLayer,
7021
+ FEEDBACK_EVIDENCE_SCHEMA_VERSION,
7022
+ FEEDBACK_REQUEST_SCHEMA_VERSION,
7023
+ FEEDBACK_RECEIPT_SCHEMA_VERSION,
7024
+ FEEDBACK_SCREENSHOT_SCHEMA_VERSION,
7025
+ FEEDBACK_LOCAL_STATE_REGISTRY_VERSION,
7026
+ FEEDBACK_COOKIE_REGISTRY_VERSION,
7027
+ FEEDBACK_MAX_MESSAGE_BYTES,
7028
+ FEEDBACK_MAX_EVIDENCE_BYTES,
7029
+ FEEDBACK_MAX_REQUEST_BYTES,
7030
+ FEEDBACK_MAX_SCREENSHOT_BYTES,
7031
+ FEEDBACK_MAX_SCREENSHOT_WIDTH,
7032
+ FEEDBACK_MAX_SCREENSHOT_HEIGHT,
7033
+ FEEDBACK_MAX_TELEMETRY_EVENTS,
7034
+ FEEDBACK_MAX_DIAGNOSTIC_EVENTS,
7035
+ FEEDBACK_MAX_LOCAL_STATE_ENTRIES,
7036
+ FEEDBACK_MAX_COOKIE_ENTRIES,
7037
+ FeedbackValidationError,
7038
+ validateFeedbackReportRequest,
7039
+ parseFeedbackReportRequest,
7040
+ validateFeedbackEvidence,
7041
+ parseFeedbackEvidence,
7042
+ validateFeedbackScreenshot,
7043
+ parseFeedbackScreenshot,
7044
+ assertFeedbackReceipt,
7045
+ assertFeedbackMessage,
7046
+ collectFeedbackEvidence,
7047
+ collectLocalState,
7048
+ collectCookies,
7049
+ FeedbackTransportError,
5315
7050
  SpaceClientCoreService,
5316
7051
  makeSpaceClientCoreLayer,
5317
7052
  makeSpaceClientCoreLayerFromServices,