@q9labsai/chalk-client 4.1.14 → 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 (132) hide show
  1. package/README.md +56 -2
  2. package/dist/access/grant.d.ts +1 -0
  3. package/dist/access/grant.d.ts.map +1 -1
  4. package/dist/access/manager.d.ts.map +1 -1
  5. package/dist/{chunk-JCNUF3DP.js → chunk-BD7FRQ67.js} +2082 -328
  6. package/dist/{chunk-2NUTY2VP.js → chunk-E4OK5K6N.js} +7 -3
  7. package/dist/{chunk-NKOKY2AE.js → chunk-EWTITNJ3.js} +15 -101
  8. package/dist/chunk-HW5TTZL2.js +99 -0
  9. package/dist/{chunk-HE6CLPZM.js → chunk-M3S7HNQF.js} +1663 -516
  10. package/dist/{chunk-S5OUHFGC.js → chunk-O7WTAXAY.js} +670 -73
  11. package/dist/{chunk-PWUU6T3R.js → chunk-OKIL2ELF.js} +1185 -874
  12. package/dist/chunk-RCDXIDCN.js +33 -0
  13. package/dist/client.d.ts +1420 -463
  14. package/dist/client.d.ts.map +1 -1
  15. package/dist/connection/dependencies.d.ts +10 -0
  16. package/dist/connection/dependencies.d.ts.map +1 -1
  17. package/dist/connection/index.d.ts +2 -0
  18. package/dist/connection/index.d.ts.map +1 -1
  19. package/dist/connection/lifecycle.d.ts.map +1 -1
  20. package/dist/connection/production.d.ts +4 -1
  21. package/dist/connection/production.d.ts.map +1 -1
  22. package/dist/connection/types.d.ts +2 -2
  23. package/dist/connection/types.d.ts.map +1 -1
  24. package/dist/effect.js +343 -130
  25. package/dist/feedback/collector.d.ts +14 -0
  26. package/dist/feedback/collector.d.ts.map +1 -0
  27. package/dist/feedback/context.d.ts +38 -0
  28. package/dist/feedback/context.d.ts.map +1 -0
  29. package/dist/feedback/controller.d.ts +4 -0
  30. package/dist/feedback/controller.d.ts.map +1 -0
  31. package/dist/feedback/index.d.ts +7 -0
  32. package/dist/feedback/index.d.ts.map +1 -0
  33. package/dist/feedback/transport.d.ts +29 -0
  34. package/dist/feedback/transport.d.ts.map +1 -0
  35. package/dist/feedback/types.d.ts +202 -0
  36. package/dist/feedback/types.d.ts.map +1 -0
  37. package/dist/feedback/validation.d.ts +30 -0
  38. package/dist/feedback/validation.d.ts.map +1 -0
  39. package/dist/generated/http-api.d.ts +1409 -462
  40. package/dist/generated/http-api.d.ts.map +1 -1
  41. package/dist/generated/schemas.d.ts +2613 -858
  42. package/dist/generated/schemas.d.ts.map +1 -1
  43. package/dist/index.d.ts +1 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +130 -15
  46. package/dist/media/camera-uplink.d.ts +13 -0
  47. package/dist/media/camera-uplink.d.ts.map +1 -0
  48. package/dist/media/capture-error.d.ts +27 -0
  49. package/dist/media/capture-error.d.ts.map +1 -0
  50. package/dist/media/client.d.ts +4 -0
  51. package/dist/media/client.d.ts.map +1 -1
  52. package/dist/media/index.d.ts +1 -0
  53. package/dist/media/index.d.ts.map +1 -1
  54. package/dist/media/index.js +5 -1
  55. package/dist/media/plane.d.ts +4 -0
  56. package/dist/media/plane.d.ts.map +1 -1
  57. package/dist/media/remote-track-response.d.ts +6 -0
  58. package/dist/media/remote-track-response.d.ts.map +1 -0
  59. package/dist/media/transport.d.ts.map +1 -1
  60. package/dist/media/types.d.ts +20 -2
  61. package/dist/media/types.d.ts.map +1 -1
  62. package/dist/public-invites/client.d.ts.map +1 -1
  63. package/dist/public-invites/index.js +4 -3
  64. package/dist/public-invites/types.d.ts +4 -2
  65. package/dist/public-invites/types.d.ts.map +1 -1
  66. package/dist/server/access-refresh.d.cts +8 -0
  67. package/dist/server/access-refresh.d.ts +9 -0
  68. package/dist/server/access-refresh.d.ts.map +1 -0
  69. package/dist/server/client.d.ts.map +1 -1
  70. package/dist/server/index.cjs +189 -20
  71. package/dist/server/index.d.cts +2 -1
  72. package/dist/server/index.d.ts +2 -1
  73. package/dist/server/index.d.ts.map +1 -1
  74. package/dist/server/index.js +187 -19
  75. package/dist/server/types.d.cts +74 -0
  76. package/dist/server/types.d.ts +74 -0
  77. package/dist/server/types.d.ts.map +1 -1
  78. package/dist/space-client/chat-controller.d.ts +3 -0
  79. package/dist/space-client/chat-controller.d.ts.map +1 -1
  80. package/dist/space-client/controller-effects.d.ts +6 -0
  81. package/dist/space-client/controller-effects.d.ts.map +1 -1
  82. package/dist/space-client/core.d.ts +12 -1
  83. package/dist/space-client/core.d.ts.map +1 -1
  84. package/dist/space-client/core.test.helpers.d.ts +1 -0
  85. package/dist/space-client/core.test.helpers.d.ts.map +1 -1
  86. package/dist/space-client/effect.d.ts +5 -0
  87. package/dist/space-client/effect.d.ts.map +1 -1
  88. package/dist/space-client/episode-diagnostic-runtime.d.ts +3 -0
  89. package/dist/space-client/episode-diagnostic-runtime.d.ts.map +1 -1
  90. package/dist/space-client/errors.d.ts.map +1 -1
  91. package/dist/space-client/index.d.ts +4 -1
  92. package/dist/space-client/index.d.ts.map +1 -1
  93. package/dist/space-client/media-controller.d.ts.map +1 -1
  94. package/dist/space-client/offline-actions-storage.d.ts +66 -0
  95. package/dist/space-client/offline-actions-storage.d.ts.map +1 -0
  96. package/dist/space-client/offline-actions.d.ts +36 -0
  97. package/dist/space-client/offline-actions.d.ts.map +1 -0
  98. package/dist/space-client/participants-controller.d.ts +2 -1
  99. package/dist/space-client/participants-controller.d.ts.map +1 -1
  100. package/dist/space-client/recording-controller.d.ts +20 -0
  101. package/dist/space-client/recording-controller.d.ts.map +1 -0
  102. package/dist/space-client/reload-rejoin.d.ts +24 -0
  103. package/dist/space-client/reload-rejoin.d.ts.map +1 -0
  104. package/dist/space-client/space-client.d.ts.map +1 -1
  105. package/dist/space-client/store.d.ts +3 -1
  106. package/dist/space-client/store.d.ts.map +1 -1
  107. package/dist/space-client/types.d.ts +40 -4
  108. package/dist/space-client/types.d.ts.map +1 -1
  109. package/dist/sync/index.js +2 -1
  110. package/dist/sync/v1-client.d.ts +1 -5
  111. package/dist/sync/v1-client.d.ts.map +1 -1
  112. package/dist/sync/v1-collaboration-state.d.ts +65 -0
  113. package/dist/sync/v1-collaboration-state.d.ts.map +1 -0
  114. package/dist/sync/v1-command-scheduler.d.ts +54 -0
  115. package/dist/sync/v1-command-scheduler.d.ts.map +1 -0
  116. package/dist/sync/v1-deferred.d.ts +8 -0
  117. package/dist/sync/v1-deferred.d.ts.map +1 -0
  118. package/dist/sync/v1-error.d.ts +5 -0
  119. package/dist/sync/v1-error.d.ts.map +1 -0
  120. package/dist/sync/v1-frame-signature.d.ts +2 -0
  121. package/dist/sync/v1-frame-signature.d.ts.map +1 -0
  122. package/dist/sync/v1-live-target-coordinator.d.ts +32 -0
  123. package/dist/sync/v1-live-target-coordinator.d.ts.map +1 -0
  124. package/dist/sync/v1-types.d.ts +3 -0
  125. package/dist/sync/v1-types.d.ts.map +1 -1
  126. package/dist/telemetry/index.js +9 -7
  127. package/dist/telemetry/reconnect.d.ts +7 -0
  128. package/dist/telemetry/reconnect.d.ts.map +1 -0
  129. package/dist/telemetry/trace.d.ts +1 -1
  130. package/dist/telemetry/trace.d.ts.map +1 -1
  131. package/dist/whiteboard/v1-client.d.ts.map +1 -1
  132. package/package.json +1 -1
@@ -1,26 +1,38 @@
1
1
  import {
2
2
  CloudflareRTKClient,
3
3
  CloudflareSFUClient,
4
- createCloudflareSFUHTTPTransport
5
- } from "./chunk-S5OUHFGC.js";
4
+ createCloudflareSFUHTTPTransport,
5
+ describeMediaCaptureError
6
+ } from "./chunk-O7WTAXAY.js";
6
7
  import {
7
8
  AccessGrantError,
8
9
  requireParsedAccessGrant,
9
10
  validEpisodeDiagnosticCredential
10
- } from "./chunk-HE6CLPZM.js";
11
+ } from "./chunk-M3S7HNQF.js";
12
+ import {
13
+ normalizeTelemetryAttributes
14
+ } from "./chunk-EWTITNJ3.js";
11
15
  import {
12
16
  createTraceContext,
17
+ journeyHeaders,
13
18
  parseTraceparent
14
- } from "./chunk-NKOKY2AE.js";
19
+ } from "./chunk-HW5TTZL2.js";
15
20
  import {
21
+ ChatAttachmentMimeTypeSchema,
22
+ V1SyncError,
16
23
  createBrowserSyncLifecycle,
17
24
  createBrowserWebSocketFactory,
18
25
  createV1SyncClient
19
- } from "./chunk-PWUU6T3R.js";
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,9 +2055,9 @@ 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
- const publish = () => Effect2.sync(() => snapshotFor(model, access.currentUnsafe()?.subject ?? null)).pipe(
2060
+ const publish = () => Effect2.sync(() => snapshotFor(model, access.currentUnsafe())).pipe(
2029
2061
  Effect2.tap((snapshot) => SubscriptionRef.set(snapshotRef, snapshot)),
2030
2062
  Effect2.asVoid,
2031
2063
  Effect2.tap(
@@ -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;
@@ -2496,16 +2557,28 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2496
2557
  function idleSnapshot() {
2497
2558
  return Object.freeze({ state: "idle", subject: null, episode: null, connection: Object.freeze({ sync: "idle", media: "idle" }), failure: null });
2498
2559
  }
2499
- function snapshotFor(model, subject) {
2560
+ function snapshotFor(model, access) {
2500
2561
  const control = model.syncSnapshot?.optimisticControl ?? model.syncSnapshot?.control;
2501
2562
  return Object.freeze({
2502
2563
  state: model.state,
2503
- subject: subject ? Object.freeze({ ...subject }) : null,
2504
- episode: subject ? Object.freeze({ id: subject.episodeId, startedAt: null, deadline: control ? new Date(control.deadlineAtMs).toISOString() : null }) : null,
2505
- connection: Object.freeze({ sync: syncPhase(model.syncSnapshot?.connection.phase), media: mediaPhase(model.mediaSnapshot?.connection.phase) }),
2564
+ subject: subjectFor(access),
2565
+ episode: episodeFor(access, control),
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
  }
2570
+ function subjectFor(access) {
2571
+ if (!access) return null;
2572
+ return Object.freeze({ ...access.subject });
2573
+ }
2574
+ function episodeFor(access, control) {
2575
+ if (!access) return null;
2576
+ return Object.freeze({
2577
+ id: access.subject.episodeId,
2578
+ startedAt: access.episodeStartedAt ?? null,
2579
+ deadline: control ? new Date(control.deadlineAtMs).toISOString() : null
2580
+ });
2581
+ }
2509
2582
  function portsFor(model) {
2510
2583
  return model.sync && model.media ? { sync: model.sync, media: model.media } : null;
2511
2584
  }
@@ -2566,6 +2639,10 @@ var CONNECTION_ERROR_CODE_MAP = {
2566
2639
  invalid_access: "access.invalid",
2567
2640
  access_unavailable: "access.unavailable",
2568
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",
2569
2646
  sync_start_failed: "connection.sync_start_failed",
2570
2647
  media_start_failed: "connection.media_start_failed",
2571
2648
  join_cleanup_unconfirmed: "connection.join_cleanup_unconfirmed",
@@ -2598,6 +2675,9 @@ function normalizeClientError(cause, fallback = "client.internal_error") {
2598
2675
  message: cause.message
2599
2676
  });
2600
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
+ }
2601
2681
  if (isAccessInvalid(cause)) {
2602
2682
  return new SpaceClientError({ code: "access.invalid", recoverable: true, message: "The access grant was rejected" });
2603
2683
  }
@@ -2621,6 +2701,7 @@ function connectionErrorCode(code, action) {
2621
2701
 
2622
2702
  // src/space-client/store.ts
2623
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"]);
2624
2705
  var empty = (value) => Object.freeze(value);
2625
2706
  var EMPTY = empty({
2626
2707
  connection: empty({ status: "idle", episode: null, lastError: null }),
@@ -2636,6 +2717,8 @@ var EMPTY = empty({
2636
2717
  }),
2637
2718
  chat: empty({ status: "idle", messages: empty([]), pendingSends: empty([]), readReceipts: empty([]), unreadCount: 0, pagination: empty({ cursor: null, hasOlder: false, historyTruncated: false }), lastError: null }),
2638
2719
  reactions: empty({ active: empty([]) }),
2720
+ recording: empty({ current: null }),
2721
+ offline: empty({ pending: empty([]), decisionNeeded: false, policy: "send" }),
2639
2722
  whiteboard: empty({ open: false, engine: empty({ status: "unsubscribed", sceneId: null, revision: null, presenting: false, error: null }) })
2640
2723
  });
2641
2724
  var SpaceStore = class _SpaceStore {
@@ -2660,8 +2743,9 @@ var SpaceStore = class _SpaceStore {
2660
2743
  updateConnection(snapshot) {
2661
2744
  const current = this.getSnapshot().connection;
2662
2745
  const error2 = snapshot.failure ? empty({ code: mapCode(snapshot.failure.code), recoverable: snapshot.failure.recoverable, message: snapshot.failure.message }) : null;
2663
- if (current.status === snapshot.state && sameEpisode(current.episode, snapshot.episode) && sameError(current.lastError, error2)) return;
2664
- 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 }));
2665
2749
  }
2666
2750
  updateSelf(value) {
2667
2751
  this.#replace("self", value);
@@ -2678,6 +2762,12 @@ var SpaceStore = class _SpaceStore {
2678
2762
  updateReactions(value) {
2679
2763
  this.#replace("reactions", value);
2680
2764
  }
2765
+ updateRecording(value) {
2766
+ this.#replace("recording", value);
2767
+ }
2768
+ updateOffline(value) {
2769
+ this.#replace("offline", value);
2770
+ }
2681
2771
  updateWhiteboard(value) {
2682
2772
  this.#replace("whiteboard", value);
2683
2773
  }
@@ -2716,11 +2806,943 @@ function sameError(left, right) {
2716
2806
  return left.code === right.code && left.recoverable === right.recoverable && left.message === right.message;
2717
2807
  }
2718
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
+
2719
3741
  // src/space-client/core.ts
2720
- 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";
2721
3743
 
2722
3744
  // src/space-client/controller-effects.ts
2723
- 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";
2724
3746
  import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
2725
3747
 
2726
3748
  // src/space-client/chat-controller.ts
@@ -2732,14 +3754,14 @@ var MAX_CHAT_PAGE_SIZE = 100;
2732
3754
  var MAX_LOADED_CHAT_MESSAGES = 500;
2733
3755
  var MAX_CHAT_TEXT_BYTES = 16384;
2734
3756
  var MAX_CHAT_TEXT_SCALARS = 4e3;
2735
- var encoder3 = new TextEncoder();
3757
+ var encoder4 = new TextEncoder();
2736
3758
  var allowedMimeTypes = new Set(CHALK_CHAT_ATTACHMENT_MIME_TYPES);
2737
3759
  function validateChatMessage(input) {
2738
3760
  const attachments = input.attachments ?? [];
2739
3761
  if (input.text.length === 0 && attachments.length === 0) return "A chat message requires text or an attachment";
2740
- 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`;
2741
3763
  if (attachments.length > CHALK_CHAT_ATTACHMENT_LIMITS.maximumPerMessage) return `A chat message supports at most ${CHALK_CHAT_ATTACHMENT_LIMITS.maximumPerMessage} attachments`;
2742
- 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";
2743
3765
  return attachments.map(validateChatAttachment).find((value) => value !== null) ?? null;
2744
3766
  }
2745
3767
  function validateChatUpload(file, bytes, clientAttachmentId) {
@@ -2770,16 +3792,16 @@ function compareChatSequence(left, right) {
2770
3792
  const normalizedRight = right.replace(/^0+/u, "") || "0";
2771
3793
  return normalizedLeft.length === normalizedRight.length ? normalizedLeft < normalizedRight ? -1 : 1 : normalizedLeft.length - normalizedRight.length;
2772
3794
  }
2773
- function validateChatAttachment(attachment) {
2774
- const fileNameBytes = encoder3.encode(attachment.fileName).byteLength;
2775
- if (!attachment.attachmentId || fileNameBytes < 1 || fileNameBytes > CHALK_CHAT_ATTACHMENT_LIMITS.maximumFileNameBytes) return "Chat attachment metadata is invalid";
2776
- if (!Number.isSafeInteger(attachment.byteLength) || attachment.byteLength < 1 || attachment.byteLength > CHALK_CHAT_ATTACHMENT_LIMITS.maximumByteLength) return "Chat attachment metadata is invalid";
2777
- 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";
2778
3800
  }
2779
3801
  function sameChatMessage(left, right) {
2780
- 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) => {
2781
3803
  const candidate = right.attachments[index];
2782
- 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;
2783
3805
  });
2784
3806
  }
2785
3807
 
@@ -2795,7 +3817,7 @@ var makeChatController = (input) => Effect4.gen(function* () {
2795
3817
  const controller = yield* Effect4.sync(() => {
2796
3818
  let instance = null;
2797
3819
  const transport = input.createTransport?.({ token: () => Effect4.runPromiseWith(context)(input.connection.getSyncToken()) }) ?? null;
2798
- 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);
2799
3821
  return instance;
2800
3822
  });
2801
3823
  yield* Effect4.addFinalizer(() => Effect4.sync(() => controller.dispose()));
@@ -2809,6 +3831,7 @@ var ChatControllerRuntime = class {
2809
3831
  #fetch;
2810
3832
  #fork;
2811
3833
  #diagnostics;
3834
+ #offline;
2812
3835
  #catchUpRunning = false;
2813
3836
  #catchUpRequested = false;
2814
3837
  #hasOlder = false;
@@ -2826,7 +3849,8 @@ var ChatControllerRuntime = class {
2826
3849
  #unsubscribeConnection = null;
2827
3850
  #unsubscribeEvents = null;
2828
3851
  #unsubscribeSnapshot = null;
2829
- constructor(connection, store, transport, apiBaseUrl, fetch, fork, diagnostics) {
3852
+ constructor(connection, store, transport, apiBaseUrl, fetch, fork, diagnostics, offline) {
3853
+ this.#offline = offline;
2830
3854
  this.#apiBaseUrl = apiBaseUrl.replace(/\/+$/u, "");
2831
3855
  this.#connection = connection;
2832
3856
  this.#store = store;
@@ -2842,9 +3866,12 @@ var ChatControllerRuntime = class {
2842
3866
  Effect4.tap(() => Effect4.sync(() => operation?.observe("observed", "validation"))),
2843
3867
  Effect4.map((attachments) => ({ clientMessageId: this.#connection.createId(), attachments })),
2844
3868
  Effect4.flatMap(({ clientMessageId, attachments }) => {
2845
- 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);
2846
3873
  operation?.observe("observed", "authorization");
2847
- 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(
2848
3875
  Effect4.map(chatMessageFor),
2849
3876
  Effect4.tap(() => Effect4.sync(() => this.#removePending(clientMessageId))),
2850
3877
  Effect4.map((message) => this.#observeMessage(message, false)),
@@ -2858,7 +3885,8 @@ var ChatControllerRuntime = class {
2858
3885
  Effect4.catch((cause) => {
2859
3886
  const error2 = normalizeClientError(cause, "chat.payload_invalid");
2860
3887
  return Effect4.sync(() => {
2861
- 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));
2862
3890
  operation?.fail("send_failed");
2863
3891
  }).pipe(Effect4.andThen(Effect4.fail(error2)));
2864
3892
  })
@@ -2918,6 +3946,7 @@ var ChatControllerRuntime = class {
2918
3946
  });
2919
3947
  };
2920
3948
  upload = (file) => {
3949
+ const fetch = this.#fetch;
2921
3950
  const prepareOperation = this.#diagnostics?.startOperation("chat.attachment.prepare");
2922
3951
  return Effect4.suspend(() => {
2923
3952
  if (!this.#transport) {
@@ -2938,7 +3967,7 @@ var ChatControllerRuntime = class {
2938
3967
  prepareOperation?.succeed();
2939
3968
  }).pipe(
2940
3969
  Effect4.andThen(
2941
- 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(
2942
3971
  Effect4.flatMap((response) => {
2943
3972
  const commitOperation = this.#diagnostics?.startOperation("chat.attachment.commit");
2944
3973
  if (!response.ok) {
@@ -2976,7 +4005,11 @@ var ChatControllerRuntime = class {
2976
4005
  );
2977
4006
  });
2978
4007
  };
2979
- 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
+ });
2980
4013
  dispose() {
2981
4014
  this.#unsubscribeConnection?.();
2982
4015
  this.#unsubscribeConnection = null;
@@ -3178,7 +4211,7 @@ function validateUpload(file, bytes, clientAttachmentId) {
3178
4211
  return upload;
3179
4212
  }
3180
4213
  function bytesFor(file) {
3181
- return "bytes" in file ? Effect4.succeed(file.bytes) : foreign(() => file.arrayBuffer());
4214
+ return "fileName" in file ? Effect4.succeed(file.bytes) : foreign(() => file.arrayBuffer());
3182
4215
  }
3183
4216
  function catchUpRequest(latestSequence, ports) {
3184
4217
  const extension = ports.sync.getCollaborationExtensionState();
@@ -3230,6 +4263,10 @@ var MediaControllerRuntime = class {
3230
4263
  #fork;
3231
4264
  #diagnostics;
3232
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();
3233
4270
  #requestGenerations = /* @__PURE__ */ new Map();
3234
4271
  #intent = { microphone: true, camera: true };
3235
4272
  #requests = Object.freeze([]);
@@ -3356,161 +4393,268 @@ var MediaControllerRuntime = class {
3356
4393
  #set(source, enabled) {
3357
4394
  const operation = this.#diagnostics?.startOperation(`${source}.${enabled ? "publish" : "unpublish"}`);
3358
4395
  operation?.observe("observed", "intent");
3359
- return this.#serialize(
3360
- source,
3361
- this.#connection.runCommand((ports) => {
3362
- const previousIntent = this.#intent[source];
3363
- this.#intent[source] = enabled;
3364
- let captured = null;
3365
- let prepared = false;
3366
- const action = source === "microphone" ? "setMicrophoneEnabled" : "setCameraEnabled";
3367
- return Effect5.suspend(() => {
3368
- const acquire = enabled && !this.#tracks.has(source) ? this.#captureSource(source).pipe(
3369
- Effect5.tap(
3370
- (track) => Effect5.sync(() => {
3371
- captured = track;
3372
- try {
3373
- this.#assertActivePorts(ports, action);
3374
- } catch (cause) {
3375
- track.stop();
3376
- throw cause;
3377
- }
3378
- this.#tracks.set(source, track);
3379
- ports.media.prepareLocalTrack(source, track);
3380
- prepared = true;
3381
- operation?.observe("observed", "local_track_state");
3382
- this.#publish();
3383
- })
3384
- )
3385
- ) : Effect5.void;
3386
- return acquire.pipe(
3387
- Effect5.asVoid,
3388
- Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "local_track_state"))),
3389
- Effect5.andThen(source === "microphone" ? foreign2(() => ports.sync.setMicrophoneEnabled(enabled)) : foreign2(() => ports.sync.setCameraEnabled(enabled))),
3390
- Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "sync_commit"))),
3391
- Effect5.tap(() => Effect5.sync(() => this.#assertActivePorts(ports, action))),
3392
- Effect5.tap(
3393
- () => Effect5.sync(() => {
3394
- operation?.observe("observed", "sfu_publication");
3395
- operation?.succeed();
3396
- })
3397
- ),
3398
- Effect5.asVoid,
3399
- Effect5.catch(
3400
- (cause) => this.#rollbackCapture({
3401
- action,
3402
- cause,
3403
- fallback: Effect5.void,
3404
- media: ports.media,
3405
- permissionMessage: `${source} permission was denied`,
3406
- prepared,
3407
- restoreIntent: () => {
3408
- this.#intent[source] = previousIntent;
3409
- },
3410
- source,
3411
- track: captured
3412
- })
3413
- )
3414
- );
3415
- });
3416
- })
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
+ )
3417
4493
  ).pipe(
3418
4494
  Effect5.mapError(normalizeClientError),
3419
- 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
+ )
3420
4501
  );
3421
4502
  }
3422
4503
  #startScreen() {
3423
4504
  const operation = this.#diagnostics?.startOperation("screen.start");
3424
- return this.#serialize(
3425
- "screen",
3426
- this.#connection.runCommand((ports) => {
3427
- let stream = null;
3428
- let track = null;
3429
- let prepared = false;
3430
- return Effect5.suspend(() => {
3431
- if (this.#tracks.has("screen")) {
3432
- operation?.notObservable("permission", "already_active");
3433
- operation?.notObservable("track_acquisition", "already_active");
3434
- operation?.notObservable("sync_commit", "already_active");
3435
- operation?.notObservable("sfu_publication", "already_active");
3436
- operation?.succeed();
3437
- return Effect5.void;
3438
- }
3439
- return foreign2(() => this.#selection.getDisplayMedia({ video: true, audio: false })).pipe(
3440
- Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "permission"))),
3441
- Effect5.tap(
3442
- (captured) => Effect5.sync(() => {
3443
- stream = captured;
3444
- this.#assertActivePorts(ports, "startScreenShare");
3445
- track = requireDisplayVideoTrack(captured);
3446
- this.#tracks.set("screen", track);
3447
- ports.media.prepareLocalTrack("screen", track);
3448
- prepared = true;
3449
- operation?.observe("observed", "track_acquisition");
3450
- track.addEventListener("ended", () => this.#handleScreenEnded(track));
3451
- this.#screenEndedPending = false;
3452
- this.#publish();
3453
- })
3454
- ),
3455
- Effect5.andThen(foreign2(() => ports.sync.setScreenShareEnabled(true))),
3456
- Effect5.tap(() => Effect5.sync(() => operation?.observe("observed", "sync_commit"))),
3457
- Effect5.tap(() => Effect5.sync(() => this.#assertActivePorts(ports, "startScreenShare"))),
3458
- Effect5.tap(
3459
- () => Effect5.sync(() => {
3460
- 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");
3461
4525
  operation?.succeed();
3462
- })
3463
- ),
3464
- Effect5.asVoid,
3465
- 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 }))
3466
- );
3467
- });
3468
- })
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
+ )
3469
4560
  ).pipe(
3470
4561
  Effect5.mapError(normalizeClientError),
3471
- 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
+ )
3472
4568
  );
3473
4569
  }
3474
4570
  #stopScreen() {
3475
4571
  const operation = this.#diagnostics?.startOperation("screen.stop");
3476
- return this.#serialize(
3477
- "screen",
3478
- this.#connection.runCommand(
3479
- (ports) => Effect5.suspend(() => {
3480
- const track = this.#tracks.get("screen");
3481
- if (!track) {
3482
- operation?.notObservable("stop_confirmation", "already_stopped");
3483
- operation?.succeed();
3484
- return Effect5.void;
3485
- }
3486
- return foreign2(() => ports.sync.setScreenShareEnabled(false)).pipe(
3487
- Effect5.tap(() => Effect5.sync(() => this.#assertActivePorts(ports, "stopScreenShare"))),
3488
- Effect5.andThen(foreign2(() => ports.media.clearPreparedLocalTrack("screen"))),
3489
- Effect5.tap(
3490
- () => Effect5.sync(() => {
3491
- this.#assertActivePorts(ports, "stopScreenShare");
3492
- if (this.#tracks.get("screen") === track) this.#tracks.delete("screen");
3493
- track.stop();
3494
- this.#screenEndedPending = false;
3495
- this.#publish();
3496
- operation?.observe("observed", "stop_confirmation");
3497
- operation?.succeed();
3498
- })
3499
- ),
3500
- Effect5.asVoid
3501
- );
3502
- })
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
+ )
3503
4621
  )
3504
4622
  ).pipe(
3505
4623
  Effect5.mapError(normalizeClientError),
3506
4624
  Effect5.tapError(() => Effect5.sync(() => operation?.fail("screen_stop_failed")))
3507
4625
  );
3508
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
+ }
3509
4632
  #serialize(source, effect) {
3510
- 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
+ );
3511
4654
  }
3512
4655
  #captureSource(source) {
3513
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`)),
3514
4658
  Effect5.tap(() => this.#refreshDevices().pipe(Effect5.ignore)),
3515
4659
  Effect5.flatMap(
3516
4660
  (stream) => Effect5.try({
@@ -3556,8 +4700,8 @@ var MediaControllerRuntime = class {
3556
4700
  input.restoreIntent?.();
3557
4701
  const rollback = input.track ? this.#discardCaptured(input.source, input.track, input.media, input.prepared) : input.fallback;
3558
4702
  const captureWasUnavailable = input.action === "startScreenShare" && !input.prepared && input.track === null;
3559
- const failure2 = isPermissionDenied(input.cause) || captureWasUnavailable ? captureError(input.cause, input.action, input.permissionMessage) : input.cause;
3560
- 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)));
3561
4705
  }
3562
4706
  #assertActivePorts(ports, action) {
3563
4707
  if (this.#connection.getSnapshot().state === "live" && this.#ports?.sync === ports.sync && this.#ports.media === ports.media) return;
@@ -3570,6 +4714,7 @@ var MediaControllerRuntime = class {
3570
4714
  this.#unsubscribeMedia = null;
3571
4715
  this.#unsubscribeRequests = null;
3572
4716
  this.#unsubscribeSync = null;
4717
+ if (this.#ports?.media !== ports?.media) this.#observedPublicationIds.clear();
3573
4718
  this.#ports = ports;
3574
4719
  if (!ports) {
3575
4720
  for (const track of this.#tracks.values()) track.stop();
@@ -3579,11 +4724,57 @@ var MediaControllerRuntime = class {
3579
4724
  this.#publish();
3580
4725
  return;
3581
4726
  }
3582
- this.#unsubscribeMedia = ports.media.subscribe(() => this.#publish());
4727
+ this.#unsubscribeMedia = ports.media.subscribe(() => {
4728
+ this.#applyRemoteMediaState(ports, ports.sync.getSnapshot());
4729
+ this.#publish();
4730
+ });
3583
4731
  this.#unsubscribeRequests = ports.sync.onDirectedRequest((request) => this.#request(request));
3584
- 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());
3585
4739
  this.#publish();
3586
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
+ }
3587
4778
  #request(request) {
3588
4779
  if (request.expires_at_ms <= this.#now()) return;
3589
4780
  const control = this.#ports?.sync.getSnapshot().optimisticControl ?? this.#ports?.sync.getSnapshot().control;
@@ -3653,7 +4844,11 @@ var MediaControllerRuntime = class {
3653
4844
  const current = this.#store.getSnapshot();
3654
4845
  const snapshot = this.#ports?.media.getSnapshot();
3655
4846
  const state = this.#connection.getSnapshot().state;
3656
- 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
+ });
3657
4852
  const remote = Object.freeze((snapshot?.remoteTracks ?? []).map((track) => Object.freeze({ participantId: track.participantId, source: track.source, publicationId: track.publicationId, track: track.track })));
3658
4853
  const media = Object.freeze({ ...current.media, local, remote, screenShare: local.screen, incomingRequests: this.#requests });
3659
4854
  if (!sameMediaSlice(current.media, media)) this.#store.updateMedia(media);
@@ -3690,12 +4885,18 @@ function initialTrackEntries(microphone, camera) {
3690
4885
  if (camera) entries.push(["camera", camera]);
3691
4886
  return entries;
3692
4887
  }
3693
- function localMedia(source, tracks, snapshot, intended, connectionState) {
4888
+ function localMedia(source, tracks, snapshot, intended, connectionState, pending) {
3694
4889
  const track = tracks.get(source) ?? null;
3695
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 });
3696
4893
  if (publication?.enabled) return Object.freeze({ source, state: "enabled", track });
4894
+ if (publication && connectionState === "live") return Object.freeze({ source, state: "disabled", track });
3697
4895
  return Object.freeze({ source, state: localMediaState(source, track, intended, connectionState), track });
3698
4896
  }
4897
+ function locallyMuted(intended, track, publication) {
4898
+ return !intended && track?.enabled === false && publication?.enabled === false;
4899
+ }
3699
4900
  function localMediaState(source, track, intended, connectionState) {
3700
4901
  if (!mediaIsDesired(source, track, intended)) return inactiveLocalMediaState(connectionState, track);
3701
4902
  if (connectionState === "joining" || track) return "requesting";
@@ -3738,19 +4939,265 @@ function sameDeviceList(left, right) {
3738
4939
  return left.length === right.length && left.every((device, index) => device.deviceId === right[index]?.deviceId && device.label === right[index]?.label);
3739
4940
  }
3740
4941
  function captureError(cause, action, permissionMessage) {
4942
+ if (cause instanceof ConnectionError) return cause;
3741
4943
  if (isPermissionDenied(cause)) return new ConnectionError({ code: "permission_denied", action, recoverable: true, message: permissionMessage }, { cause });
3742
- 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 });
3743
4946
  }
3744
4947
  function isPermissionDenied(cause) {
3745
- return cause instanceof DOMException && (cause.name === "NotAllowedError" || cause.name === "SecurityError");
4948
+ return describeMediaCaptureError(cause).code === "permission_denied";
3746
4949
  }
3747
4950
  function isAccessInvalid2(cause) {
3748
4951
  return typeof cause === "object" && cause !== null && "code" in cause && (cause.code === "access.invalid" || cause.code === "invalid_access");
3749
4952
  }
4953
+ function isSyncRejection(cause) {
4954
+ return isAccessInvalid2(cause) || cause instanceof V1SyncError && cause.code === "terminal_failure";
4955
+ }
3750
4956
 
3751
- // src/space-client/participants-controller.ts
4957
+ // src/space-client/offline-actions.ts
3752
4958
  import { Context as Context7, Effect as Effect6, Layer as Layer7 } from "effect";
3753
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
+
3754
5201
  // src/sync/v1-types.ts
3755
5202
  var V1_CAPABILITIES = [
3756
5203
  "publishAudio",
@@ -3781,19 +5228,21 @@ var V1_CAPABILITIES = [
3781
5228
  // src/space-client/participants-controller.ts
3782
5229
  var CAPABILITIES = new Set(V1_CAPABILITIES);
3783
5230
  var EMPTY_PARTICIPANTS = Object.freeze({ roster: Object.freeze([]), admissionQueue: Object.freeze([]) });
3784
- var ParticipantsControllerService = class extends Context7.Service()("@chalk/client/ParticipantsController") {
5231
+ var ParticipantsControllerService = class extends Context8.Service()("@chalk/client/ParticipantsController") {
3785
5232
  };
3786
- var makeParticipantsController = (connection, store, diagnostics) => Effect6.acquireRelease(
3787
- Effect6.sync(() => new ParticipantsControllerRuntime(connection, store, diagnostics)),
3788
- (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())
3789
5236
  );
3790
5237
  var ParticipantsControllerRuntime = class {
3791
5238
  #connection;
3792
5239
  #store;
3793
5240
  #diagnostics;
5241
+ #offline;
3794
5242
  #unsubscribePorts = null;
3795
5243
  #unsubscribe = null;
3796
- constructor(connection, store, diagnostics) {
5244
+ constructor(connection, store, diagnostics, offline) {
5245
+ this.#offline = offline;
3797
5246
  this.#connection = connection;
3798
5247
  this.#store = store;
3799
5248
  this.#diagnostics = diagnostics;
@@ -3819,7 +5268,7 @@ var ParticipantsControllerRuntime = class {
3819
5268
  requestMedia = (participantId, kind) => this.#command("media_request.request", ["capability_decision", "command_commit", "target_result"], () => {
3820
5269
  assertIdentifier(participantId, "participant ID");
3821
5270
  if (kind !== "microphone" && kind !== "camera") throw invalid("Media requests must target a microphone or camera");
3822
- 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));
3823
5272
  });
3824
5273
  remove = (participantId) => this.#command("moderation.remove", ["capability_decision", "command_commit", "target_delivery"], () => {
3825
5274
  assertIdentifier(participantId, "participant ID");
@@ -3833,16 +5282,16 @@ var ParticipantsControllerRuntime = class {
3833
5282
  assertIdentifier(requestId, "admission request ID");
3834
5283
  return ({ sync }) => foreign3(() => sync.deny(requestId));
3835
5284
  });
3836
- raiseHand = () => this.#command(
3837
- "participant.raised_hand.set",
3838
- ["membership_transition", "participant_result"],
3839
- () => ({ sync }) => foreign3(() => sync.setHandRaised(true))
3840
- );
3841
- lowerHand = () => this.#command(
3842
- "participant.raised_hand.set",
3843
- ["membership_transition", "participant_result"],
3844
- () => ({ sync }) => foreign3(() => sync.setHandRaised(false))
3845
- );
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
+ }
3846
5295
  renameSelf = (displayName) => this.#command("participant.rename", ["membership_transition", "participant_result"], () => {
3847
5296
  assertName(displayName, "display name");
3848
5297
  return ({ sync }) => foreign3(() => sync.setDisplayName(displayName));
@@ -3855,18 +5304,18 @@ var ParticipantsControllerRuntime = class {
3855
5304
  }
3856
5305
  #command(name, checkpoints, operation) {
3857
5306
  const diagnostic = this.#diagnostics?.startOperation(name);
3858
- return Effect6.try({ try: operation, catch: normalizeClientError }).pipe(
3859
- Effect6.tap(() => Effect6.sync(() => diagnostic?.observe("observed", checkpoints[0]))),
3860
- Effect6.flatMap((run) => this.#connection.runCommand(run)),
3861
- Effect6.mapError(normalizeClientError),
3862
- Effect6.tap(
3863
- () => 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(() => {
3864
5313
  for (const checkpoint of checkpoints.slice(1)) diagnostic?.observe("observed", checkpoint);
3865
5314
  if (name.startsWith("moderation.")) diagnostic?.notObservable("target_application", "target_projection_is_conditional");
3866
5315
  diagnostic?.succeed();
3867
5316
  })
3868
5317
  ),
3869
- Effect6.tapError(() => Effect6.sync(() => diagnostic?.fail("command_failed")))
5318
+ Effect7.tapError(() => Effect7.sync(() => diagnostic?.fail("command_failed")))
3870
5319
  );
3871
5320
  }
3872
5321
  #bind(ports) {
@@ -3888,7 +5337,7 @@ var ParticipantsControllerRuntime = class {
3888
5337
  }
3889
5338
  };
3890
5339
  function foreign3(operation) {
3891
- return Effect6.tryPromise({ try: operation, catch: (cause) => cause });
5340
+ return Effect7.tryPromise({ try: operation, catch: (cause) => cause });
3892
5341
  }
3893
5342
  function assertIdentifier(value, label) {
3894
5343
  if (typeof value !== "string" || value.trim().length === 0) throw invalid(`A non-empty ${label} is required`);
@@ -3998,20 +5447,20 @@ function sameValues(left, right) {
3998
5447
  }
3999
5448
 
4000
5449
  // src/space-client/reactions-controller.ts
4001
- 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";
4002
5451
  var MAX_VISIBLE_REACTIONS = 24;
4003
5452
  var REACTIONS = /* @__PURE__ */ new Set(["\u{1F44D}", "\u2764\uFE0F", "\u{1F602}", "\u{1F62E}", "\u{1F622}", "\u{1F389}"]);
4004
- var ReactionsControllerService = class extends Context8.Service()("@chalk/client/ReactionsController") {
5453
+ var ReactionsControllerService = class extends Context9.Service()("@chalk/client/ReactionsController") {
4005
5454
  };
4006
- var makeReactionsController = (connection, store, diagnostics) => Effect7.gen(function* () {
5455
+ var makeReactionsController = (connection, store, diagnostics) => Effect8.gen(function* () {
4007
5456
  const clock = yield* Clock4.Clock;
4008
- const scope = yield* Effect7.scope;
4009
- const context = yield* Effect7.context();
5457
+ const scope = yield* Effect8.scope;
5458
+ const context = yield* Effect8.context();
4010
5459
  const fork = (effect) => {
4011
- void Effect7.runForkWith(context)(Effect7.forkIn(effect, scope).pipe(Effect7.asVoid));
5460
+ void Effect8.runForkWith(context)(Effect8.forkIn(effect, scope).pipe(Effect8.asVoid));
4012
5461
  };
4013
5462
  const controller = new ReactionsControllerRuntime(connection, store, clock.currentTimeMillisUnsafe, fork, diagnostics);
4014
- yield* Effect7.addFinalizer(() => Effect7.sync(() => controller.dispose()));
5463
+ yield* Effect8.addFinalizer(() => Effect8.sync(() => controller.dispose()));
4015
5464
  return controller;
4016
5465
  });
4017
5466
  var ReactionsControllerRuntime = class {
@@ -4034,26 +5483,26 @@ var ReactionsControllerRuntime = class {
4034
5483
  }
4035
5484
  send = (emoji) => {
4036
5485
  const operation = this.#diagnostics?.startOperation("reaction.send");
4037
- return Effect7.try({
5486
+ return Effect8.try({
4038
5487
  try: () => {
4039
5488
  if (!REACTIONS.has(emoji)) throw new SpaceClientError({ code: "reaction.invalid", recoverable: false, message: "The reaction is not supported" });
4040
5489
  },
4041
5490
  catch: normalizeClientError
4042
5491
  }).pipe(
4043
- Effect7.tap(() => Effect7.sync(() => operation?.observe("observed", "authorization"))),
4044
- Effect7.andThen(this.#connection.runCommand(({ sync }) => foreign4(() => sync.sendReaction(emoji)))),
4045
- Effect7.tap(() => Effect7.sync(() => operation?.observe("observed", "accepted_commit"))),
4046
- Effect7.map(reactionFor),
4047
- Effect7.tap(
4048
- (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(() => {
4049
5498
  this.#observe(reaction);
4050
5499
  operation?.observe("observed", "sender_result");
4051
5500
  operation?.notObservable("recipient_projection", "recipient_projection_is_conditional");
4052
5501
  operation?.succeed();
4053
5502
  })
4054
5503
  ),
4055
- Effect7.mapError(normalizeClientError),
4056
- Effect7.tapError(() => Effect7.sync(() => operation?.fail("send_failed")))
5504
+ Effect8.mapError(normalizeClientError),
5505
+ Effect8.tapError(() => Effect8.sync(() => operation?.fail("send_failed")))
4057
5506
  );
4058
5507
  };
4059
5508
  dispose() {
@@ -4083,9 +5532,9 @@ var ReactionsControllerRuntime = class {
4083
5532
  this.#expiryGenerations.set(reaction.eventId, generation);
4084
5533
  const delay = Math.max(0, expiresAt2 - this.#now());
4085
5534
  this.#fork(
4086
- Effect7.sleep(delay).pipe(
4087
- Effect7.andThen(
4088
- Effect7.sync(() => {
5535
+ Effect8.sleep(delay).pipe(
5536
+ Effect8.andThen(
5537
+ Effect8.sync(() => {
4089
5538
  if (this.#expiryGenerations.get(reaction.eventId) === generation) this.#remove(reaction.eventId);
4090
5539
  })
4091
5540
  )
@@ -4112,7 +5561,7 @@ var ReactionsControllerRuntime = class {
4112
5561
  }
4113
5562
  };
4114
5563
  function foreign4(operation) {
4115
- return Effect7.tryPromise({ try: operation, catch: (cause) => cause });
5564
+ return Effect8.tryPromise({ try: operation, catch: (cause) => cause });
4116
5565
  }
4117
5566
  function reactionFor(reaction) {
4118
5567
  return Object.freeze({ ...reaction });
@@ -4127,23 +5576,122 @@ function sameReactions(left, right) {
4127
5576
  });
4128
5577
  }
4129
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
+
4130
5678
  // src/space-client/whiteboard-controller.ts
4131
- 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";
4132
5680
  var EMPTY_SUMMARY = Object.freeze({ status: "unsubscribed", sceneId: null, revision: null, capabilities: Object.freeze([]), canDraw: false, canClear: false, presenting: false, error: null });
4133
- var WhiteboardControllerService = class extends Context9.Service()("@chalk/client/WhiteboardController") {
5681
+ var WhiteboardControllerService = class extends Context11.Service()("@chalk/client/WhiteboardController") {
4134
5682
  };
4135
- var makeWhiteboardController = (connection, store, create) => Effect8.gen(function* () {
4136
- const context = yield* Effect8.context();
4137
- 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(() => {
4138
5686
  let instance = null;
4139
5687
  const transport = create?.({
4140
- token: () => Effect8.runPromiseWith(context)(connection.getSyncToken()),
5688
+ token: () => Effect10.runPromiseWith(context)(connection.getSyncToken()),
4141
5689
  onSummary: (summary) => instance?.observe(summary)
4142
5690
  }) ?? null;
4143
5691
  instance = new WhiteboardControllerRuntime(connection, store, transport);
4144
5692
  return instance;
4145
5693
  });
4146
- yield* Effect8.addFinalizer(() => Effect8.sync(() => controller.dispose()));
5694
+ yield* Effect10.addFinalizer(() => Effect10.sync(() => controller.dispose()));
4147
5695
  return controller;
4148
5696
  });
4149
5697
  var WhiteboardControllerRuntime = class {
@@ -4205,15 +5753,17 @@ function sameError2(left, right) {
4205
5753
  }
4206
5754
 
4207
5755
  // src/space-client/controller-effects.ts
4208
- var ControllerEffectsService = class extends Context10.Service()("@chalk/client/ControllerEffects") {
5756
+ var ControllerEffectsService = class extends Context12.Service()("@chalk/client/ControllerEffects") {
4209
5757
  };
4210
- 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 });
4211
5760
  const media = yield* makeMediaController(input.connection, input.store, input.mediaDeviceSelection, input.episodeDiagnostics);
4212
- const chat = yield* makeChatController({ connection: input.connection, store: input.store, createTransport: input.featureFactories?.createChatFileTransport, apiBaseUrl: input.apiBaseUrl, fetch: input.fetch, episodeDiagnostics: input.episodeDiagnostics });
4213
- 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);
4214
5763
  const reactions = yield* makeReactionsController(input.connection, input.store, input.episodeDiagnostics);
5764
+ const recording = yield* makeRecordingController(input.connection, input.store, input.episodeDiagnostics);
4215
5765
  const whiteboard = yield* makeWhiteboardController(input.connection, input.store, input.featureFactories?.createWhiteboardClient);
4216
- return { media, chat, participants, reactions, whiteboard };
5766
+ return { media, offline: offlineActions, chat, participants, reactions, recording, whiteboard };
4217
5767
  });
4218
5768
 
4219
5769
  // src/space-client/media-device-selection.ts
@@ -4268,7 +5818,7 @@ function requireDeviceId(deviceId) {
4268
5818
  }
4269
5819
 
4270
5820
  // src/space-client/episode-diagnostic-runtime.ts
4271
- import { encodedEventSize, getActionContract, parseDiagnosticEventDraft, redactDiagnosticAttributes } from "@q9labsai/diagnostics-contracts";
5821
+ import { encodedEventSize, getActionContract, parseDiagnosticEventDraft as parseDiagnosticEventDraft2, redactDiagnosticAttributes as redactDiagnosticAttributes2 } from "@q9labsai/diagnostics-contracts";
4272
5822
  var DEFAULT_RING_EVENTS = 256;
4273
5823
  var DEFAULT_RING_BYTES = 256 * 1024;
4274
5824
  var DEFAULT_RING_AGE_MS = 2 * 60 * 1e3;
@@ -4376,6 +5926,15 @@ var EpisodeDiagnosticRuntime = class {
4376
5926
  activeContext() {
4377
5927
  return this.#enabled() ? this.context() : void 0;
4378
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
+ }
4379
5938
  startOperation(name, attributes, parentOperationRef, producerOperationRef, correlationInput) {
4380
5939
  if (!this.#enabled()) return void 0;
4381
5940
  const credentialGeneration = this.#credential?.generation;
@@ -4441,7 +6000,7 @@ var EpisodeDiagnosticRuntime = class {
4441
6000
  });
4442
6001
  }
4443
6002
  #draft(input) {
4444
- return parseDiagnosticEventDraft(
6003
+ return parseDiagnosticEventDraft2(
4445
6004
  buildDraftInput(input, {
4446
6005
  eventId: safeId(this.#createId()),
4447
6006
  producerSequence: ++this.#sequence,
@@ -4752,7 +6311,7 @@ function buildDraftInput(input, context) {
4752
6311
  expectation: expectationFor(contract, checkpoint),
4753
6312
  correlation: optionalCorrelation(correlation),
4754
6313
  release: context.release,
4755
- attributes: redactDiagnosticAttributes(input.attributes).attributes
6314
+ attributes: redactDiagnosticAttributes2(input.attributes).attributes
4756
6315
  };
4757
6316
  }
4758
6317
  function eventReferences(input) {
@@ -4776,8 +6335,15 @@ function stripTrailingUrlSlash(value) {
4776
6335
  }
4777
6336
  function intakeResponse(value) {
4778
6337
  if (!isRecord3(value) || typeof value.diagnosticReference !== "string" || !Number.isSafeInteger(value.committedCursor)) return null;
4779
- if (!Array.isArray(value.accepted) || !Array.isArray(value.duplicates) || !Array.isArray(value.conflicts)) return null;
4780
- return { accepted: value.accepted, duplicates: value.duplicates, conflicts: value.conflicts };
6338
+ const accepted = legacyEmptyAcknowledgement(value.accepted);
6339
+ const duplicates = legacyEmptyAcknowledgement(value.duplicates);
6340
+ const conflicts = legacyEmptyAcknowledgement(value.conflicts);
6341
+ if (accepted === null || duplicates === null || conflicts === null) return null;
6342
+ return { accepted, duplicates, conflicts };
6343
+ }
6344
+ function legacyEmptyAcknowledgement(value) {
6345
+ if (value === null) return [];
6346
+ return Array.isArray(value) ? value : null;
4781
6347
  }
4782
6348
  function completeDelivery(delivery, expected) {
4783
6349
  const all = [...delivery.accepted, ...delivery.duplicates, ...delivery.conflicts];
@@ -4866,9 +6432,125 @@ function renderRegistry() {
4866
6432
  return target[REGISTRY];
4867
6433
  }
4868
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
+
4869
6551
  // src/space-client/core.ts
4870
6552
  var syncClientsByDependencies = /* @__PURE__ */ new WeakMap();
4871
- var SpaceClientCoreService = class extends Context11.Service()("@chalk/client/SpaceClientCore") {
6553
+ var SpaceClientCoreService = class extends Context13.Service()("@chalk/client/SpaceClientCore") {
4872
6554
  };
4873
6555
  var makeSpaceClientCoreLayer = (options, platform = {}) => {
4874
6556
  const apiBaseUrl = normalizedBaseUrl2(platform.apiBaseUrl ?? options.baseUrl ?? "https://api.chalkmeet.com");
@@ -4881,7 +6563,7 @@ var makeSpaceClientCoreLayer = (options, platform = {}) => {
4881
6563
  now: baseDependencies.clock.now,
4882
6564
  setTimeout: baseDependencies.clock.setTimeout,
4883
6565
  clearTimeout: baseDependencies.clock.clearTimeout,
4884
- release: { id: "chalk-client@4.1.14" }
6566
+ release: { id: "chalk-client@4.1.16" }
4885
6567
  });
4886
6568
  const diagnosticSyncClients = /* @__PURE__ */ new Set();
4887
6569
  const dependencies = {
@@ -4903,6 +6585,7 @@ var makeSpaceClientCoreLayer = (options, platform = {}) => {
4903
6585
  initialCameraEnabled: platform.initialCameraEnabled,
4904
6586
  telemetry: platform.telemetry,
4905
6587
  recordRtcSummary: platform.recordRtcSummary,
6588
+ recordReconnect: platform.recordReconnect,
4906
6589
  dependencies,
4907
6590
  diagnostics: {
4908
6591
  onEvent: (event) => {
@@ -4911,30 +6594,49 @@ var makeSpaceClientCoreLayer = (options, platform = {}) => {
4911
6594
  }
4912
6595
  }
4913
6596
  });
4914
- 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))));
4915
6598
  };
4916
- var makeSpaceClientCoreLayerFromServices = (options, platform = {}) => Layer11.effect(
6599
+ var makeSpaceClientCoreLayerFromServices = (options, platform = {}) => Layer13.effect(
4917
6600
  SpaceClientCoreService,
4918
- Effect10.gen(function* () {
4919
- const store = yield* Effect10.service(SpaceStoreService);
4920
- const lifecycle = yield* Effect10.service(ConnectionLifecycleService);
4921
- 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);
4922
6605
  const space = options.space.trim();
4923
- if (space.length === 0) return yield* Effect10.die(new TypeError("A Space slug is required"));
4924
- 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"));
4925
6608
  const apiBaseUrl = normalizedBaseUrl2(platform.apiBaseUrl ?? options.baseUrl ?? "https://api.chalkmeet.com");
4926
6609
  const selection = new MediaDeviceSelection(dependencies.mediaDevices);
4927
6610
  const episodeDiagnostics = episodeDiagnosticsForDependencies(dependencies);
4928
6611
  if (episodeDiagnostics) registerEpisodeDiagnosticConnection(lifecycle, episodeDiagnostics);
4929
- const controllers = yield* makeControllerEffects({ apiBaseUrl, connection: lifecycle, store, mediaDeviceSelection: selection, featureFactories: dependencies, fetch: platform.fetch, episodeDiagnostics });
4930
- return yield* Effect10.acquireRelease(
4931
- Effect10.sync(() => new SpaceClientCore(lifecycle, selection, store, controllers, episodeDiagnostics, dependencies, syncClientsByDependencies.get(dependencies))),
4932
- (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())
4933
6634
  );
4934
6635
  })
4935
6636
  );
4936
6637
  var SpaceClientCore = class {
4937
6638
  controllers;
6639
+ feedback;
4938
6640
  #connection;
4939
6641
  #store;
4940
6642
  #episodeDiagnostics;
@@ -4949,13 +6651,29 @@ var SpaceClientCore = class {
4949
6651
  #leaveOperation;
4950
6652
  #participantJoinOperation;
4951
6653
  #syncConnectOperation;
4952
- constructor(connection, _selection, store, controllers, episodeDiagnostics, episodeDiagnosticDependencies, episodeDiagnosticSyncClients) {
6654
+ constructor(connection, _selection, store, controllers, episodeDiagnostics, episodeDiagnosticDependencies, episodeDiagnosticSyncClients, feedback) {
4953
6655
  this.#connection = connection;
4954
6656
  this.#store = store;
4955
6657
  this.controllers = controllers;
4956
6658
  this.#episodeDiagnostics = episodeDiagnostics;
4957
6659
  this.#episodeDiagnosticDependencies = episodeDiagnosticDependencies;
4958
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
+ });
4959
6677
  this.#store.updateConnection(connection.getSnapshot());
4960
6678
  this.#previous = this.#store.getSnapshot();
4961
6679
  this.#unsubscribeStore = this.#store.subscribe(() => this.#publishStore());
@@ -4973,47 +6691,47 @@ var SpaceClientCore = class {
4973
6691
  join(options = {}) {
4974
6692
  this.#participantJoinOperation = this.#episodeDiagnostics?.startOperation("participant.join");
4975
6693
  this.#syncConnectOperation = this.#episodeDiagnostics?.startOperation("sync.connect");
4976
- return Effect10.sync(() => this.#requireActive()).pipe(
4977
- Effect10.tap(() => Effect10.sync(() => this.controllers.media.configure(options))),
4978
- Effect10.andThen(this.#connection.configureJoin(options)),
4979
- Effect10.andThen(this.#connection.join()),
4980
- Effect10.andThen(options.displayName === void 0 ? Effect10.void : this.controllers.participants.renameSelf(options.displayName)),
4981
- Effect10.tap(
4982
- () => 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(() => {
4983
6701
  this.#participantJoinOperation?.succeed();
4984
6702
  this.#syncConnectOperation?.succeed();
4985
6703
  })
4986
6704
  ),
4987
- Effect10.tapError(
4988
- () => Effect10.sync(() => {
6705
+ Effect12.tapError(
6706
+ () => Effect12.sync(() => {
4989
6707
  this.#participantJoinOperation?.fail("join_failed");
4990
6708
  this.#syncConnectOperation?.fail("connect_failed");
4991
6709
  })
4992
6710
  ),
4993
- Effect10.mapError(normalizeClientError)
6711
+ Effect12.mapError(normalizeClientError)
4994
6712
  );
4995
6713
  }
4996
6714
  leave() {
4997
6715
  this.#leaveOperation = this.#episodeDiagnostics?.startOperation("participant.leave");
4998
6716
  return this.#connection.leave().pipe(
4999
- Effect10.tap(() => Effect10.sync(() => this.#leaveOperation?.succeed())),
5000
- Effect10.tapError(() => Effect10.sync(() => this.#leaveOperation?.fail("leave_failed"))),
5001
- 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)
5002
6720
  );
5003
6721
  }
5004
6722
  endEpisode() {
5005
6723
  return trackedEffect(
5006
6724
  this.#episodeDiagnostics,
5007
6725
  "episode.end.authorized",
5008
- this.#connection.runCommand(({ sync }) => foreign5(() => sync.endEpisode()).pipe(Effect10.tap(() => this.#connection.confirmEpisodeEnded)))
5009
- ).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));
5010
6728
  }
5011
6729
  extendEpisode(minutes) {
5012
6730
  return trackedEffect(
5013
6731
  this.#episodeDiagnostics,
5014
6732
  "episode.deadline.extend",
5015
- this.#connection.runCommand(({ sync }) => foreign5(() => sync.extendEpisode(minutes)))
5016
- ).pipe(Effect10.asVoid, Effect10.mapError(normalizeClientError));
6733
+ this.#connection.runCommand(({ sync }) => foreign6(() => sync.extendEpisode(minutes)))
6734
+ ).pipe(Effect12.asVoid, Effect12.mapError(normalizeClientError));
5017
6735
  }
5018
6736
  on(event, handler) {
5019
6737
  this.#handlers[event].add(handler);
@@ -5026,6 +6744,7 @@ var SpaceClientCore = class {
5026
6744
  this.#unsubscribeStore?.();
5027
6745
  this.#unsubscribeConnection = null;
5028
6746
  this.#unsubscribeStore = null;
6747
+ this.feedback.dispose();
5029
6748
  this.#disposeDiagnostics();
5030
6749
  this.#clearHandlers();
5031
6750
  }
@@ -5127,25 +6846,25 @@ var SpaceClientCore = class {
5127
6846
  }
5128
6847
  };
5129
6848
  function liveClock(dependencies) {
5130
- return Layer11.succeed(Clock5.Clock, {
6849
+ return Layer13.succeed(Clock5.Clock, {
5131
6850
  currentTimeMillisUnsafe: dependencies.clock.now,
5132
- currentTimeMillis: Effect10.sync(dependencies.clock.now),
6851
+ currentTimeMillis: Effect12.sync(dependencies.clock.now),
5133
6852
  currentTimeNanosUnsafe: () => BigInt(dependencies.clock.now()) * 1000000n,
5134
- currentTimeNanos: Effect10.sync(() => BigInt(dependencies.clock.now()) * 1000000n),
5135
- sleep: (duration) => Effect10.callback((resume) => {
5136
- const handle = dependencies.clock.setTimeout(() => resume(Effect10.void), Duration2.toMillis(duration));
5137
- 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));
5138
6857
  })
5139
6858
  });
5140
6859
  }
5141
- function foreign5(operation) {
5142
- return Effect10.tryPromise({ try: operation, catch: (cause) => cause });
6860
+ function foreign6(operation) {
6861
+ return Effect12.tryPromise({ try: operation, catch: (cause) => cause });
5143
6862
  }
5144
6863
  function trackedEffect(diagnostics, name, effect) {
5145
6864
  const operation = diagnostics?.startOperation(name);
5146
6865
  return effect.pipe(
5147
- Effect10.tap(() => Effect10.sync(() => operation?.succeed())),
5148
- Effect10.tapError(() => Effect10.sync(() => operation?.fail("operation_failed")))
6866
+ Effect12.tap(() => Effect12.sync(() => operation?.succeed())),
6867
+ Effect12.tapError(() => Effect12.sync(() => operation?.fail("operation_failed")))
5149
6868
  );
5150
6869
  }
5151
6870
  async function resolveAccessGrant(input) {
@@ -5167,7 +6886,7 @@ function diagnosticAccessOperation(request) {
5167
6886
  }
5168
6887
  async function fetchAccessGrant2(input) {
5169
6888
  if (input.platform.connectionAccess) return input.platform.connectionAccess(input.request);
5170
- 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 });
5171
6890
  }
5172
6891
  function accessReason(reason) {
5173
6892
  if (reason === void 0 || reason === "join") return "join";
@@ -5206,17 +6925,17 @@ function sameDiagnosticTrack(left, right) {
5206
6925
  }
5207
6926
 
5208
6927
  // src/space-client/space-client.ts
5209
- import { Effect as Effect12, ManagedRuntime } from "effect";
6928
+ import { Effect as Effect14, ManagedRuntime } from "effect";
5210
6929
 
5211
6930
  // src/space-client/promise-facade.ts
5212
- import { Effect as Effect11 } from "effect";
6931
+ import { Effect as Effect13 } from "effect";
5213
6932
  function toPromiseController(runtime, controller) {
5214
6933
  const projected = {};
5215
6934
  for (const [name, command] of Object.entries(controller)) {
5216
6935
  if (name === "configure" || name === "dispose") continue;
5217
6936
  projected[name] = (...args) => {
5218
6937
  const result = command(...args);
5219
- return Effect11.isEffect(result) ? runtime.runPromise(result) : result;
6938
+ return Effect13.isEffect(result) ? runtime.runPromise(result) : result;
5220
6939
  };
5221
6940
  }
5222
6941
  return projected;
@@ -5224,24 +6943,30 @@ function toPromiseController(runtime, controller) {
5224
6943
 
5225
6944
  // src/space-client/space-client.ts
5226
6945
  var PromiseSpaceClient = class {
6946
+ feedback;
5227
6947
  media;
5228
6948
  chat;
5229
6949
  participants;
6950
+ offline;
5230
6951
  reactions;
6952
+ recording;
5231
6953
  whiteboard;
5232
6954
  #core;
5233
6955
  #runtime;
5234
6956
  #disposed = false;
5235
6957
  constructor(options, platform) {
5236
6958
  this.#runtime = ManagedRuntime.make(makeSpaceClientCoreLayer(options, platform));
5237
- this.#core = this.#runtime.runSync(Effect12.service(SpaceClientCoreService));
6959
+ this.#core = this.#runtime.runSync(Effect14.service(SpaceClientCoreService));
5238
6960
  const controllers = this.#core.controllers;
6961
+ this.feedback = this.#core.feedback;
5239
6962
  this.media = toPromiseController(this.#runtime, controllers.media);
5240
6963
  const chat = toPromiseController(this.#runtime, controllers.chat);
5241
- const { upload, url, ...chatCommands } = chat;
5242
- 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);
5243
6967
  this.participants = toPromiseController(this.#runtime, controllers.participants);
5244
6968
  this.reactions = toPromiseController(this.#runtime, controllers.reactions);
6969
+ this.recording = toPromiseController(this.#runtime, controllers.recording);
5245
6970
  this.whiteboard = toPromiseController(this.#runtime, controllers.whiteboard);
5246
6971
  }
5247
6972
  join = (options) => this.#runtime.runPromise(this.#core.join(options));
@@ -5293,6 +7018,35 @@ export {
5293
7018
  SpaceStoreService,
5294
7019
  makeSpaceStoreLayer,
5295
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,
5296
7050
  SpaceClientCoreService,
5297
7051
  makeSpaceClientCoreLayer,
5298
7052
  makeSpaceClientCoreLayerFromServices,