@jsonstudio/zterm-daemon-darwin-arm64 0.1.3 → 0.1.4

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.
@@ -2228,7 +2228,7 @@ var require_websocket = __commonJS({
2228
2228
  var http = require("http");
2229
2229
  var net = require("net");
2230
2230
  var tls = require("tls");
2231
- var { randomBytes, createHash: createHash4 } = require("crypto");
2231
+ var { randomBytes, createHash: createHash5 } = require("crypto");
2232
2232
  var { Duplex, Readable } = require("stream");
2233
2233
  var { URL: URL2 } = require("url");
2234
2234
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -2888,7 +2888,7 @@ var require_websocket = __commonJS({
2888
2888
  abortHandshake(websocket, socket, "Invalid Upgrade header");
2889
2889
  return;
2890
2890
  }
2891
- const digest = createHash4("sha1").update(key + GUID).digest("base64");
2891
+ const digest = createHash5("sha1").update(key + GUID).digest("base64");
2892
2892
  if (res.headers["sec-websocket-accept"] !== digest) {
2893
2893
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
2894
2894
  return;
@@ -3167,7 +3167,7 @@ var require_stream = __commonJS({
3167
3167
  };
3168
3168
  duplex._final = function(callback) {
3169
3169
  if (ws.readyState === ws.CONNECTING) {
3170
- ws.once("open", function open() {
3170
+ ws.once("open", function open2() {
3171
3171
  duplex._final(callback);
3172
3172
  });
3173
3173
  return;
@@ -3188,7 +3188,7 @@ var require_stream = __commonJS({
3188
3188
  };
3189
3189
  duplex._write = function(chunk, encoding, callback) {
3190
3190
  if (ws.readyState === ws.CONNECTING) {
3191
- ws.once("open", function open() {
3191
+ ws.once("open", function open2() {
3192
3192
  duplex._write(chunk, encoding, callback);
3193
3193
  });
3194
3194
  return;
@@ -3255,7 +3255,7 @@ var require_websocket_server = __commonJS({
3255
3255
  var EventEmitter = require("events");
3256
3256
  var http = require("http");
3257
3257
  var { Duplex } = require("stream");
3258
- var { createHash: createHash4 } = require("crypto");
3258
+ var { createHash: createHash5 } = require("crypto");
3259
3259
  var extension2 = require_extension();
3260
3260
  var PerMessageDeflate2 = require_permessage_deflate();
3261
3261
  var subprotocol2 = require_subprotocol();
@@ -3556,7 +3556,7 @@ var require_websocket_server = __commonJS({
3556
3556
  );
3557
3557
  }
3558
3558
  if (this._state > RUNNING) return abortHandshake(socket, 503);
3559
- const digest = createHash4("sha1").update(key + GUID).digest("base64");
3559
+ const digest = createHash5("sha1").update(key + GUID).digest("base64");
3560
3560
  const headers = [
3561
3561
  "HTTP/1.1 101 Switching Protocols",
3562
3562
  "Upgrade: websocket",
@@ -5040,6 +5040,7 @@ var import_sender = __toESM(require_sender(), 1);
5040
5040
  var import_subprotocol = __toESM(require_subprotocol(), 1);
5041
5041
  var import_websocket = __toESM(require_websocket(), 1);
5042
5042
  var import_websocket_server = __toESM(require_websocket_server(), 1);
5043
+ var wrapper_default = import_websocket.default;
5043
5044
 
5044
5045
  // src/server/server.ts
5045
5046
  var import_http = require("http");
@@ -5184,7 +5185,8 @@ function resolveDaemonRuntimeConfig(options) {
5184
5185
  const relayPassword = asString(env.ZTERM_TRAVERSAL_PASSWORD) || asString(relayConfig.password);
5185
5186
  const relayHostId = asString(env.ZTERM_TRAVERSAL_HOST_ID) || asString(relayConfig.hostId);
5186
5187
  const relayDefaultHostName = (0, import_os.hostname)().trim();
5187
- const relayDeviceId = asString(env.ZTERM_TRAVERSAL_DEVICE_ID) || asString(relayConfig.deviceId) || relayDefaultHostName;
5188
+ const configuredDeviceId = asString(env.ZTERM_TRAVERSAL_DEVICE_ID) || asString(relayConfig.deviceId);
5189
+ const relayDeviceId = configuredDeviceId || relayDefaultHostName;
5188
5190
  const relayDeviceName = asString(env.ZTERM_TRAVERSAL_DEVICE_NAME) || asString(relayConfig.deviceName) || relayDefaultHostName;
5189
5191
  const relayPlatform = asString(env.ZTERM_TRAVERSAL_PLATFORM) || asString(relayConfig.platform) || process.platform;
5190
5192
  const relayAppVersion = asString(env.ZTERM_TRAVERSAL_APP_VERSION) || asString(env.ZTERM_APP_VERSION) || asString(relayConfig.appVersion);
@@ -5207,6 +5209,7 @@ function resolveDaemonRuntimeConfig(options) {
5207
5209
  authToken,
5208
5210
  terminalCacheLines,
5209
5211
  sessionName,
5212
+ deviceId: configuredDeviceId,
5210
5213
  daemonHostId,
5211
5214
  configPath: path,
5212
5215
  configFound: found,
@@ -7505,9 +7508,11 @@ function v4(options, buf, offset) {
7505
7508
  }
7506
7509
  var v4_default = v4;
7507
7510
 
7508
- // src/server/terminal-transport-runtime.ts
7511
+ // src/server/terminal-runtime-types.ts
7509
7512
  var TRANSPORT_BACKPRESSURE_BUFFERED_BYTES = 128e3;
7510
7513
  var TRANSPORT_BACKPRESSURE_LOW_WATER_BYTES = 64e3;
7514
+
7515
+ // src/server/terminal-transport-runtime.ts
7511
7516
  var TERMINAL_TRANSPORT_STALE_INBOUND_MS = 19e4;
7512
7517
  function estimateTransportMessageBytes(text) {
7513
7518
  return Buffer.byteLength(text, "utf8");
@@ -7517,6 +7522,7 @@ function readTerminalTransportBackpressureSnapshot(transport) {
7517
7522
  return null;
7518
7523
  }
7519
7524
  const bufferedBytes = Math.max(0, Math.floor(transport.bufferedAmount || 0));
7525
+ const flowUnknown = !!transport.flowUnknown;
7520
7526
  const backpressure = bufferedBytes >= TRANSPORT_BACKPRESSURE_BUFFERED_BYTES;
7521
7527
  if (backpressure) {
7522
7528
  transport.backpressureCount = Math.max(0, Math.floor(transport.backpressureCount || 0)) + 1;
@@ -7529,6 +7535,7 @@ function readTerminalTransportBackpressureSnapshot(transport) {
7529
7535
  bufferedBytes,
7530
7536
  backpressure,
7531
7537
  lowWaterDrained: bufferedBytes <= TRANSPORT_BACKPRESSURE_LOW_WATER_BYTES,
7538
+ flowUnknown,
7532
7539
  backpressureCount: Math.max(0, Math.floor(transport.backpressureCount || 0)),
7533
7540
  lastSendBytes: Math.max(0, Math.floor(transport.lastSendBytes || 0)),
7534
7541
  totalSendBytes: Math.max(0, Math.floor(transport.totalSendBytes || 0)),
@@ -7850,6 +7857,9 @@ function createDaemonBufferPublisherRuntime(deps) {
7850
7857
  if (!snapshot?.ready) {
7851
7858
  return true;
7852
7859
  }
7860
+ if (snapshot.flowUnknown) {
7861
+ return true;
7862
+ }
7853
7863
  if (snapshot.backpressure) {
7854
7864
  state.highWaterActive = true;
7855
7865
  if (!state.highWaterEnteredAt) {
@@ -7918,6 +7928,9 @@ function createDaemonBufferPublisherRuntime(deps) {
7918
7928
  const start = Math.max(0, item.nextChunkIndex || 0);
7919
7929
  const end = Math.min(messages.length, start + Math.max(1, Math.floor(maxChunks)));
7920
7930
  for (let index = start; index < end; index += 1) {
7931
+ if (shouldHoldPendingForBackpressure(state, session)) {
7932
+ return sentChunks > 0 ? { status: "sent", sentChunks } : { status: "backpressured", sentChunks: 0 };
7933
+ }
7921
7934
  deps.sendText(session.transport, messages[index].text);
7922
7935
  item.nextChunkIndex = index + 1;
7923
7936
  sentChunks += 1;
@@ -8011,6 +8024,9 @@ function createDaemonBufferPublisherRuntime(deps) {
8011
8024
  try {
8012
8025
  deps.ensureSessionReady(session, mirror);
8013
8026
  for (let index = start; index < end; index += 1) {
8027
+ if (shouldHoldPendingForBackpressure(state, session)) {
8028
+ return sentChunks > 0 ? { status: "sent", sentChunks } : { status: "backpressured", sentChunks: 0 };
8029
+ }
8014
8030
  const message = messages[index];
8015
8031
  deps.recordPerformanceTrace?.({
8016
8032
  ...traceBase,
@@ -10414,6 +10430,7 @@ function logFileTransferRuntimeError(scope, error, context) {
10414
10430
  var MAX_REMOTE_DIRECTORY_CACHE_ENTRIES = 32;
10415
10431
  function createTerminalFileTransferListRuntime(deps) {
10416
10432
  const directoryCache = /* @__PURE__ */ new Map();
10433
+ const cancelActiveDownloads = /* @__PURE__ */ new Set();
10417
10434
  function readDirectorySnapshot(resolvedPath) {
10418
10435
  const entries = (0, import_fs4.readdirSync)(resolvedPath, { withFileTypes: true });
10419
10436
  const fileEntries = [];
@@ -10568,6 +10585,10 @@ function createTerminalFileTransferListRuntime(deps) {
10568
10585
  }
10569
10586
  }
10570
10587
  function dispose() {
10588
+ for (const cancel of cancelActiveDownloads) {
10589
+ cancel();
10590
+ }
10591
+ cancelActiveDownloads.clear();
10571
10592
  for (const entry of directoryCache.values()) {
10572
10593
  closeDirectoryCacheEntry(entry);
10573
10594
  }
@@ -10578,32 +10599,99 @@ function createTerminalFileTransferListRuntime(deps) {
10578
10599
  }
10579
10600
  function sendFileDownloadBuffer(session, requestId, fileName, fileBuffer) {
10580
10601
  const totalChunks = Math.ceil(fileBuffer.length / FILE_CHUNK_SIZE);
10602
+ const requestTransportId = session.transportId;
10581
10603
  let index = 0;
10582
- function sendNextChunk() {
10583
- if (index >= totalChunks) {
10584
- deps.sendMessage(session, {
10585
- type: "file-download-complete",
10586
- payload: { requestId, fileName, totalBytes: fileBuffer.length }
10587
- });
10588
- return;
10604
+ let cancelled = false;
10605
+ let waitTimer = null;
10606
+ let draining = false;
10607
+ function stop() {
10608
+ cancelled = true;
10609
+ if (waitTimer) {
10610
+ clearTimeout(waitTimer);
10611
+ waitTimer = null;
10589
10612
  }
10590
- const start = index * FILE_CHUNK_SIZE;
10591
- const end = Math.min(start + FILE_CHUNK_SIZE, fileBuffer.length);
10592
- const chunk = fileBuffer.subarray(start, end);
10613
+ cancelActiveDownloads.delete(stop);
10614
+ }
10615
+ function fail(error) {
10616
+ stop();
10593
10617
  deps.sendMessage(session, {
10594
- type: "file-download-chunk",
10618
+ type: "file-download-error",
10595
10619
  payload: {
10596
10620
  requestId,
10597
- chunkIndex: index,
10598
- totalChunks,
10599
- fileName,
10600
- dataBase64: chunk.toString("base64")
10621
+ error: error instanceof Error ? error.message : String(error)
10601
10622
  }
10602
10623
  });
10624
+ }
10625
+ function pump() {
10626
+ if (cancelled) {
10627
+ return;
10628
+ }
10629
+ if (session.transportId !== requestTransportId) {
10630
+ stop();
10631
+ return;
10632
+ }
10633
+ const transport = session.transport;
10634
+ if (transport) {
10635
+ if (transport.readyState !== import_websocket.default.OPEN) {
10636
+ stop();
10637
+ return;
10638
+ }
10639
+ if (transport.flowUnknown) {
10640
+ draining = true;
10641
+ waitTimer = setTimeout(() => {
10642
+ waitTimer = null;
10643
+ pump();
10644
+ }, 20);
10645
+ return;
10646
+ }
10647
+ const bufferedBytes = Math.max(0, Math.floor(transport.bufferedAmount || 0));
10648
+ const holding = draining ? bufferedBytes > TRANSPORT_BACKPRESSURE_LOW_WATER_BYTES : bufferedBytes >= TRANSPORT_BACKPRESSURE_BUFFERED_BYTES;
10649
+ if (holding) {
10650
+ draining = true;
10651
+ waitTimer = setTimeout(() => {
10652
+ waitTimer = null;
10653
+ pump();
10654
+ }, 20);
10655
+ return;
10656
+ }
10657
+ draining = false;
10658
+ }
10659
+ if (index >= totalChunks) {
10660
+ try {
10661
+ deps.sendMessage(session, {
10662
+ type: "file-download-complete",
10663
+ payload: { requestId, fileName, totalBytes: fileBuffer.length }
10664
+ });
10665
+ } catch (error) {
10666
+ fail(error);
10667
+ return;
10668
+ }
10669
+ stop();
10670
+ return;
10671
+ }
10672
+ const start = index * FILE_CHUNK_SIZE;
10673
+ const end = Math.min(start + FILE_CHUNK_SIZE, fileBuffer.length);
10674
+ const chunk = fileBuffer.subarray(start, end);
10675
+ try {
10676
+ deps.sendMessage(session, {
10677
+ type: "file-download-chunk",
10678
+ payload: {
10679
+ requestId,
10680
+ chunkIndex: index,
10681
+ totalChunks,
10682
+ fileName,
10683
+ dataBase64: chunk.toString("base64")
10684
+ }
10685
+ });
10686
+ } catch (error) {
10687
+ fail(error);
10688
+ return;
10689
+ }
10603
10690
  index += 1;
10604
- setImmediate(sendNextChunk);
10691
+ setImmediate(pump);
10605
10692
  }
10606
- sendNextChunk();
10693
+ cancelActiveDownloads.add(stop);
10694
+ pump();
10607
10695
  }
10608
10696
  function buildRemoteScreenshotFileName(prefix = "remote-screenshot") {
10609
10697
  const now = /* @__PURE__ */ new Date();
@@ -19823,199 +19911,2709 @@ function createTerminalAttachTokenRuntime() {
19823
19911
  };
19824
19912
  }
19825
19913
 
19826
- // src/server/remote-screenshot-daemon.ts
19827
- var import_node_child_process6 = require("node:child_process");
19828
- var import_node_os2 = require("node:os");
19829
- var import_node_path5 = require("node:path");
19830
- async function captureRemoteScreenshotWithDaemon(options) {
19831
- const gate = runPhase3Screenshot({
19832
- execution_id: `remote-screenshot:${Date.now()}`,
19833
- attempt_id: "1",
19834
- inputs: {
19835
- "arc.screenshot_request": {
19836
- sessionId: options.sessionId || "daemon-screenshot",
19837
- outputPath: options.outputPath
19838
- },
19839
- "arc.screenshot_permission": { allowScreenshot: true }
19840
- }
19841
- });
19842
- if (!gate.ok) {
19843
- throw new Error(`dagpipe screenshot gate rejected capture: ${gate.error}`);
19914
+ // src/server/daemon-app-messaging-runtime.ts
19915
+ var import_node_crypto3 = require("node:crypto");
19916
+
19917
+ // ../packages/shared/src/connection/app-protocol.ts
19918
+ var APP_PROTOCOL_VERSION = 1;
19919
+ var APP_MAX_MESSAGE_BYTES = 1048576;
19920
+ var APP_MAX_FILE_BYTES = 268435456;
19921
+ var APP_FILE_CHUNK_BYTES = 16384;
19922
+ var APP_MAX_SAFE_FILE_NAME_BYTES = 255;
19923
+ var APP_MAX_PATH_IDENTITY_BYTES = 191;
19924
+ var APP_MAX_INBOX_RECORDS = 512;
19925
+ var APP_MAX_INBOX_BYTES = 2 * 1024 * 1024 * 1024;
19926
+ var APP_STREAM_OUTBOUND_QUEUE_BYTES = 4 * 1024 * 1024;
19927
+ var APP_ACKED_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
19928
+ var APP_SERVER_NAME = "zterm.app";
19929
+ var AppProtocolError = class extends Error {
19930
+ constructor(options) {
19931
+ super(options.message);
19932
+ this.name = "AppProtocolError";
19933
+ this.code = options.code;
19934
+ this.stage = options.stage;
19935
+ this.requestId = options.requestId;
19936
+ this.messageId = options.messageId;
19937
+ this.transferId = options.transferId;
19844
19938
  }
19845
- return await new Promise((resolve4, reject) => {
19846
- const daemonBinary = (process.env.ZTERM_DAEMON_NATIVE || "").trim() || (0, import_node_path5.join)((0, import_node_os2.homedir)(), ".zterm", "bin", "zterm-daemon");
19847
- const args = ["capture-screen", options.outputPath];
19848
- const windowId = options.windowId?.trim();
19849
- if (windowId) {
19850
- args.push("--window-id", windowId);
19851
- }
19852
- if (options.rect) {
19853
- args.push(
19854
- "--rect",
19855
- [
19856
- Math.round(options.rect.x),
19857
- Math.round(options.rect.y),
19858
- Math.round(options.rect.width),
19859
- Math.round(options.rect.height)
19860
- ].join(",")
19861
- );
19862
- }
19863
- (0, import_node_child_process6.execFile)(daemonBinary, args, {
19864
- timeout: options.timeoutMs,
19865
- windowsHide: true
19866
- }, (error, stdout, stderr) => {
19867
- if (error) {
19868
- const details = [error.message, stderr, stdout].filter(Boolean).join("\n");
19869
- reject(new Error(details || "daemon screenshot capture failed"));
19870
- return;
19871
- }
19872
- resolve4({ outputPath: options.outputPath });
19939
+ };
19940
+ var APP_CAPABILITIES = {
19941
+ serverName: APP_SERVER_NAME,
19942
+ protocolVersion: APP_PROTOCOL_VERSION,
19943
+ messages: true,
19944
+ files: true,
19945
+ durableInbox: true,
19946
+ push: true,
19947
+ ackStates: ["delivered", "consumed"],
19948
+ resumeFileTransfer: true,
19949
+ maxMessageBytes: APP_MAX_MESSAGE_BYTES,
19950
+ maxFileBytes: APP_MAX_FILE_BYTES,
19951
+ fileChunkBytes: APP_FILE_CHUNK_BYTES
19952
+ };
19953
+ function isRecord2(value) {
19954
+ return value !== null && typeof value === "object" && !Array.isArray(value);
19955
+ }
19956
+ function requiredString(value, field, stage) {
19957
+ if (typeof value !== "string" || !value.trim()) {
19958
+ throw new AppProtocolError({
19959
+ code: "invalid_frame",
19960
+ message: `${field} must be a non-empty string`,
19961
+ stage
19873
19962
  });
19874
- });
19875
- }
19876
-
19877
- // src/server/remote-window-stream-daemon.ts
19878
- var import_wrtc2 = __toESM(require_lib2(), 1);
19879
-
19880
- // src/server/remote-window-stream-daemon-helpers.ts
19881
- function cloneRemoteWindowTargetCatalogResponse(response, requestId) {
19882
- return {
19883
- requestId,
19884
- targets: response.targets.slice(),
19885
- ...response.errors ? {
19886
- errors: response.errors.map((error) => ({
19887
- ...error,
19888
- requestId
19889
- }))
19890
- } : {}
19891
- };
19963
+ }
19964
+ return value;
19892
19965
  }
19893
- function cloneRemoteWindowTargetCatalogResult(result, requestId) {
19894
- if ("targets" in result) {
19895
- return cloneRemoteWindowTargetCatalogResponse(result, requestId);
19966
+ function stringValue(value, field, stage) {
19967
+ if (typeof value !== "string") {
19968
+ throw new AppProtocolError({
19969
+ code: "invalid_frame",
19970
+ message: `${field} must be a string`,
19971
+ stage
19972
+ });
19896
19973
  }
19897
- return {
19898
- ...result,
19899
- requestId
19900
- };
19974
+ return value;
19901
19975
  }
19902
- function normalizeRtcDescription(description, expectedType) {
19903
- if (!description || description.type !== expectedType || typeof description.sdp !== "string") {
19904
- throw new Error(`remote window daemon expected ${expectedType} description`);
19976
+ function requiredInteger(value, field, stage, min) {
19977
+ if (typeof value !== "number" || !Number.isInteger(value) || value < min) {
19978
+ throw new AppProtocolError({
19979
+ code: "invalid_frame",
19980
+ message: `${field} must be an integer >= ${min}`,
19981
+ stage
19982
+ });
19905
19983
  }
19906
- return {
19907
- type: expectedType,
19908
- sdp: description.sdp
19909
- };
19984
+ return value;
19910
19985
  }
19911
- function normalizeIceCandidate(candidate) {
19912
- const candidateLike = typeof candidate.toJSON === "function" ? candidate.toJSON() : candidate;
19913
- return {
19914
- candidate: String(candidateLike.candidate || ""),
19915
- sdpMid: candidateLike.sdpMid ?? null,
19916
- sdpMLineIndex: candidateLike.sdpMLineIndex ?? null,
19917
- usernameFragment: candidateLike.usernameFragment ?? null
19918
- };
19986
+ function requiredBoolean(value, field, stage) {
19987
+ if (typeof value !== "boolean") {
19988
+ throw new AppProtocolError({
19989
+ code: "invalid_frame",
19990
+ message: `${field} must be a boolean`,
19991
+ stage
19992
+ });
19993
+ }
19994
+ return value;
19919
19995
  }
19920
- function normalizeRemoteWindowVideoProfile(input) {
19921
- if (!input) {
19922
- return null;
19996
+ function requiredFiniteNumber(value, field, stage) {
19997
+ if (typeof value !== "number" || !Number.isFinite(value)) {
19998
+ throw new AppProtocolError({
19999
+ code: "invalid_frame",
20000
+ message: `${field} must be a finite number`,
20001
+ stage
20002
+ });
19923
20003
  }
19924
- if (input.preference !== "smooth" && input.preference !== "quality") {
19925
- throw new Error(`remote window video preference is invalid: ${String(input.preference)}`);
20004
+ return value;
20005
+ }
20006
+ function optionalIntegerValue(value, field, stage, min) {
20007
+ return value === void 0 ? void 0 : requiredInteger(value, field, stage, min);
20008
+ }
20009
+ function optionalString(value) {
20010
+ return typeof value === "string" && value.trim() ? value : void 0;
20011
+ }
20012
+ function optionalStringValue(value) {
20013
+ return typeof value === "string" ? value : void 0;
20014
+ }
20015
+ var APP_RECORD_STATES = ["received", "delivered", "consumed", "rejected"];
20016
+ var APP_SEND_STATUSES = [
20017
+ "received",
20018
+ "delivered",
20019
+ "consumed",
20020
+ "rejected",
20021
+ "resultUnknown",
20022
+ "unreachable",
20023
+ "receiving"
20024
+ ];
20025
+ var APP_SEND_STAGES = [
20026
+ "prepared",
20027
+ "sent",
20028
+ "received",
20029
+ "result_unknown"
20030
+ ];
20031
+ var APP_ERROR_CODES = [
20032
+ "invalid_version",
20033
+ "invalid_frame",
20034
+ "missing_identity",
20035
+ "unauthorized",
20036
+ "forbidden_role",
20037
+ "message_id_conflict",
20038
+ "message_too_large",
20039
+ "file_too_large",
20040
+ "invalid_file_name",
20041
+ "invalid_chunk",
20042
+ "chunk_conflict",
20043
+ "transfer_conflict",
20044
+ "transfer_incomplete",
20045
+ "integrity_mismatch",
20046
+ "inbox_full",
20047
+ "not_found",
20048
+ "ambiguous_message_id",
20049
+ "invalid_state",
20050
+ "slow_consumer",
20051
+ "unreachable",
20052
+ "result_unknown",
20053
+ "source_changed",
20054
+ "storage_error",
20055
+ "cancelled",
20056
+ "internal_error"
20057
+ ];
20058
+ function oneOf(value, allowed, field, stage) {
20059
+ if (typeof value !== "string" || !allowed.includes(value)) {
20060
+ throw new AppProtocolError({
20061
+ code: "invalid_frame",
20062
+ message: `${field} must be one of ${allowed.join(", ")}`,
20063
+ stage
20064
+ });
19926
20065
  }
19927
- const fields = [
19928
- ["maxBitrateBps", input.maxBitrateBps, 5e5, 25e6],
19929
- ["maxFrameRateFps", input.maxFrameRateFps, 1, 60],
19930
- ["maxCaptureWidth", input.maxCaptureWidth, 320, 3840],
19931
- ["maxCaptureHeight", input.maxCaptureHeight, 240, 2400],
19932
- ["maxFrameAgeMs", input.maxFrameAgeMs, 40, 1e3],
19933
- ["overviewMaxBitrateBps", input.overviewMaxBitrateBps, 0, 2e6],
19934
- ["overviewMaxFrameRateFps", input.overviewMaxFrameRateFps, 0, 8]
19935
- ];
19936
- for (const [name, value, minimum, maximum] of fields) {
19937
- if (!Number.isFinite(value) || value < minimum || value > maximum) {
19938
- throw new Error(`remote window video profile ${name} is out of range`);
19939
- }
20066
+ return value;
20067
+ }
20068
+ function assertAppProtocolVersion(value) {
20069
+ if (value !== APP_PROTOCOL_VERSION) {
20070
+ throw new AppProtocolError({
20071
+ code: "invalid_version",
20072
+ message: `unsupported protocol version: ${String(value)}`,
20073
+ stage: "admission"
20074
+ });
19940
20075
  }
19941
- if (input.overviewMaxBitrateBps >= input.maxBitrateBps) {
19942
- throw new Error("remote window overview bitrate must remain below the total bitrate");
20076
+ return APP_PROTOCOL_VERSION;
20077
+ }
20078
+ function validateAppHello(value) {
20079
+ if (!isRecord2(value)) {
20080
+ throw new AppProtocolError({ code: "invalid_frame", message: "hello must be an object", stage: "admission" });
19943
20081
  }
19944
- if (input.overviewMaxFrameRateFps > input.maxFrameRateFps) {
19945
- throw new Error("remote window overview frame rate must not exceed focus frame rate");
20082
+ const requestId = requiredString(value.requestId, "requestId", "admission");
20083
+ const role = value.role;
20084
+ if (role !== "local" && role !== "peer") {
20085
+ throw new AppProtocolError({
20086
+ code: "invalid_frame",
20087
+ message: "role must be local or peer",
20088
+ stage: "admission",
20089
+ requestId
20090
+ });
19946
20091
  }
20092
+ assertAppProtocolVersion(value.protocolVersion);
19947
20093
  return {
19948
- preference: input.preference,
19949
- maxBitrateBps: Math.floor(input.maxBitrateBps),
19950
- maxFrameRateFps: Math.floor(input.maxFrameRateFps),
19951
- maxCaptureWidth: Math.floor(input.maxCaptureWidth),
19952
- maxCaptureHeight: Math.floor(input.maxCaptureHeight),
19953
- maxFrameAgeMs: Math.floor(input.maxFrameAgeMs),
19954
- interactionActive: input.interactionActive === true,
19955
- overviewMaxBitrateBps: Math.floor(input.overviewMaxBitrateBps),
19956
- overviewMaxFrameRateFps: Math.floor(input.overviewMaxFrameRateFps)
20094
+ protocolVersion: APP_PROTOCOL_VERSION,
20095
+ requestId,
20096
+ role,
20097
+ clientInstanceId: requiredString(value.clientInstanceId, "clientInstanceId", "admission"),
20098
+ deviceId: requiredString(value.deviceId, "deviceId", "admission"),
20099
+ daemonHostId: requiredString(value.daemonHostId, "daemonHostId", "admission"),
20100
+ capabilities: isRecord2(value.capabilities) ? value.capabilities : {}
19957
20101
  };
19958
20102
  }
19959
- function formatRemoteWindowVideoProfileError(error) {
19960
- if (error instanceof Error) {
19961
- return error.message || error.name || "remote window video bitrate could not be applied";
20103
+ function validateAppTarget(value, stage) {
20104
+ if (!isRecord2(value)) {
20105
+ throw new AppProtocolError({ code: "invalid_frame", message: "target must be an object", stage });
19962
20106
  }
19963
- const message = String(error || "").trim();
19964
- return message || "remote window video bitrate could not be applied";
19965
- }
19966
- function convertRgbaToI420Frame(frame, convert) {
19967
- const width = Math.max(1, Math.floor(frame.width));
19968
- const height = Math.max(1, Math.floor(frame.height));
19969
- const chromaWidth = Math.ceil(width / 2);
19970
- const chromaHeight = Math.ceil(height / 2);
19971
- const i420 = {
19972
- width,
19973
- height,
19974
- data: new Uint8Array(width * height + chromaWidth * chromaHeight * 2)
20107
+ return {
20108
+ deviceId: requiredString(value.deviceId, "target.deviceId", stage),
20109
+ daemonHostId: requiredString(value.daemonHostId, "target.daemonHostId", stage),
20110
+ wsUrl: requiredString(value.wsUrl, "target.wsUrl", stage),
20111
+ authToken: typeof value.authToken === "string" ? value.authToken : ""
19975
20112
  };
19976
- convert({
19977
- width,
19978
- height,
19979
- data: frame.rgba
19980
- }, i420);
19981
- return i420;
19982
20113
  }
19983
- var REMOTE_WINDOW_LATENCY_SAMPLE_CAPACITY = 240;
19984
- function pushBounded(samples, value) {
19985
- if (!Number.isFinite(value) || value < 0) {
19986
- return;
19987
- }
19988
- samples.push(value);
19989
- if (samples.length > REMOTE_WINDOW_LATENCY_SAMPLE_CAPACITY) {
19990
- samples.splice(0, samples.length - REMOTE_WINDOW_LATENCY_SAMPLE_CAPACITY);
19991
- }
20114
+ function optionalAppTarget(value, stage) {
20115
+ return value === void 0 ? void 0 : validateAppTarget(value, stage);
19992
20116
  }
19993
- function nearestRankPercentile(samples, percentile) {
19994
- if (samples.length === 0) {
19995
- return null;
20117
+ function validateAppClientFrame(value) {
20118
+ if (!isRecord2(value) || typeof value.type !== "string" || !isRecord2(value.payload)) {
20119
+ throw new AppProtocolError({ code: "invalid_frame", message: "frame must have a type and payload object", stage: "frame" });
19996
20120
  }
19997
- const sorted = [...samples].sort((left, right) => left - right);
19998
- const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(percentile * sorted.length) - 1));
19999
- return sorted[index];
20000
- }
20001
- function readStatsNumber(entry, key) {
20002
- const value = entry[key];
20003
- return typeof value === "number" && Number.isFinite(value) ? value : null;
20004
- }
20005
- function createRemoteWindowLatencyAccumulator() {
20006
- const captureToEncodeSamples = [];
20007
- const captureArrivalTimestamps = [];
20008
- const dispatchTimestamps = [];
20009
- const inputInjectionSamples = [];
20010
- let previousOutbound = null;
20011
- let encodeMsPerFrame = null;
20012
- let packetSendDelayMs = null;
20013
- let encodedFps = null;
20014
- let sentBitrateBps = null;
20015
- let codecMimeType = null;
20016
- let networkRttMs = null;
20017
- let networkJitterMs = null;
20018
- let packetLossRatio = null;
20121
+ const payload = value.payload;
20122
+ switch (value.type) {
20123
+ case "app-hello":
20124
+ return { type: "app-hello", payload: validateAppHello(payload) };
20125
+ case "app-send-message": {
20126
+ const body = validateMessageBody(payload.body);
20127
+ return {
20128
+ type: "app-send-message",
20129
+ payload: {
20130
+ requestId: requiredString(payload.requestId, "requestId", "send-message"),
20131
+ messageId: requiredString(payload.messageId, "messageId", "send-message"),
20132
+ subject: requiredString(payload.subject, "subject", "send-message"),
20133
+ body,
20134
+ target: optionalAppTarget(payload.target, "send-message")
20135
+ }
20136
+ };
20137
+ }
20138
+ case "app-send-file-start":
20139
+ return {
20140
+ type: "app-send-file-start",
20141
+ payload: {
20142
+ requestId: requiredString(payload.requestId, "requestId", "file-start"),
20143
+ messageId: requiredString(payload.messageId, "messageId", "file-start"),
20144
+ subject: stringValue(payload.subject, "subject", "file-start"),
20145
+ file: validateAppFileMetadata(payload.file, "file-start"),
20146
+ target: optionalAppTarget(payload.target, "file-start")
20147
+ }
20148
+ };
20149
+ case "app-send-file-chunk":
20150
+ return {
20151
+ type: "app-send-file-chunk",
20152
+ payload: {
20153
+ requestId: requiredString(payload.requestId, "requestId", "file-chunk"),
20154
+ messageId: requiredString(payload.messageId, "messageId", "file-chunk"),
20155
+ transferId: requiredString(payload.transferId, "transferId", "file-chunk"),
20156
+ offset: validateOffset(payload.offset, "file-chunk"),
20157
+ chunkIndex: validateChunkIndex(payload.chunkIndex, "file-chunk"),
20158
+ dataBase64: requiredString(payload.dataBase64, "dataBase64", "file-chunk")
20159
+ }
20160
+ };
20161
+ case "app-send-file-commit":
20162
+ return {
20163
+ type: "app-send-file-commit",
20164
+ payload: {
20165
+ requestId: requiredString(payload.requestId, "requestId", "file-commit"),
20166
+ messageId: requiredString(payload.messageId, "messageId", "file-commit"),
20167
+ transferId: requiredString(payload.transferId, "transferId", "file-commit")
20168
+ }
20169
+ };
20170
+ case "app-ack":
20171
+ return {
20172
+ type: "app-ack",
20173
+ payload: {
20174
+ requestId: requiredString(payload.requestId, "requestId", "ack"),
20175
+ messageId: requiredString(payload.messageId, "messageId", "ack"),
20176
+ senderDaemonHostId: optionalString(payload.senderDaemonHostId)
20177
+ }
20178
+ };
20179
+ case "app-consume":
20180
+ return {
20181
+ type: "app-consume",
20182
+ payload: {
20183
+ requestId: requiredString(payload.requestId, "requestId", "consume"),
20184
+ messageId: requiredString(payload.messageId, "messageId", "consume"),
20185
+ senderDaemonHostId: optionalString(payload.senderDaemonHostId)
20186
+ }
20187
+ };
20188
+ case "app-status":
20189
+ return {
20190
+ type: "app-status",
20191
+ payload: {
20192
+ requestId: requiredString(payload.requestId, "requestId", "status"),
20193
+ messageId: requiredString(payload.messageId, "messageId", "status"),
20194
+ senderDaemonHostId: optionalString(payload.senderDaemonHostId),
20195
+ remote: payload.remote === true,
20196
+ target: optionalAppTarget(payload.target, "status")
20197
+ }
20198
+ };
20199
+ case "app-ping": {
20200
+ const sentAt = payload.sentAt;
20201
+ if (typeof sentAt !== "number" || !Number.isFinite(sentAt)) {
20202
+ throw new AppProtocolError({ code: "invalid_frame", message: "sentAt must be a finite number", stage: "ping" });
20203
+ }
20204
+ return {
20205
+ type: "app-ping",
20206
+ payload: {
20207
+ requestId: requiredString(payload.requestId, "requestId", "ping"),
20208
+ sentAt
20209
+ }
20210
+ };
20211
+ }
20212
+ default:
20213
+ throw new AppProtocolError({ code: "invalid_frame", message: `unsupported frame: ${value.type}`, stage: "frame" });
20214
+ }
20215
+ }
20216
+ function validateAppServerFrame(value) {
20217
+ if (!isRecord2(value) || typeof value.type !== "string" || !isRecord2(value.payload)) {
20218
+ throw new AppProtocolError({
20219
+ code: "invalid_frame",
20220
+ message: "server frame must have a type and payload object",
20221
+ stage: "frame"
20222
+ });
20223
+ }
20224
+ const payload = value.payload;
20225
+ switch (value.type) {
20226
+ case "app-ready": {
20227
+ const requestId = requiredString(payload.requestId, "requestId", "ready");
20228
+ assertAppProtocolVersion(payload.protocolVersion);
20229
+ return {
20230
+ type: "app-ready",
20231
+ payload: {
20232
+ requestId,
20233
+ protocolVersion: APP_PROTOCOL_VERSION,
20234
+ deviceId: requiredString(payload.deviceId, "deviceId", "ready"),
20235
+ daemonHostId: requiredString(payload.daemonHostId, "daemonHostId", "ready"),
20236
+ capabilities: isRecord2(payload.capabilities) ? payload.capabilities : (() => {
20237
+ throw new AppProtocolError({
20238
+ code: "invalid_frame",
20239
+ message: "capabilities must be an object",
20240
+ stage: "ready",
20241
+ requestId
20242
+ });
20243
+ })()
20244
+ }
20245
+ };
20246
+ }
20247
+ case "app-inbound-message":
20248
+ return {
20249
+ type: "app-inbound-message",
20250
+ payload: {
20251
+ messageId: requiredString(payload.messageId, "messageId", "inbound"),
20252
+ senderDeviceId: requiredString(payload.senderDeviceId, "senderDeviceId", "inbound"),
20253
+ senderDaemonHostId: requiredString(payload.senderDaemonHostId, "senderDaemonHostId", "inbound"),
20254
+ receiverDeviceId: requiredString(payload.receiverDeviceId, "receiverDeviceId", "inbound"),
20255
+ receiverDaemonHostId: requiredString(payload.receiverDaemonHostId, "receiverDaemonHostId", "inbound"),
20256
+ subject: stringValue(payload.subject, "subject", "inbound"),
20257
+ body: stringValue(payload.body, "body", "inbound"),
20258
+ state: oneOf(payload.state, APP_RECORD_STATES, "state", "inbound"),
20259
+ receivedAt: requiredString(payload.receivedAt, "receivedAt", "inbound")
20260
+ }
20261
+ };
20262
+ case "app-inbound-file":
20263
+ return {
20264
+ type: "app-inbound-file",
20265
+ payload: {
20266
+ messageId: requiredString(payload.messageId, "messageId", "inbound"),
20267
+ transferId: requiredString(payload.transferId, "transferId", "inbound"),
20268
+ senderDeviceId: requiredString(payload.senderDeviceId, "senderDeviceId", "inbound"),
20269
+ senderDaemonHostId: requiredString(payload.senderDaemonHostId, "senderDaemonHostId", "inbound"),
20270
+ receiverDeviceId: requiredString(payload.receiverDeviceId, "receiverDeviceId", "inbound"),
20271
+ receiverDaemonHostId: requiredString(payload.receiverDaemonHostId, "receiverDaemonHostId", "inbound"),
20272
+ subject: stringValue(payload.subject, "subject", "inbound"),
20273
+ fileName: validateSafeFileName(payload.fileName, "inbound"),
20274
+ totalBytes: requiredInteger(payload.totalBytes, "totalBytes", "inbound", 0),
20275
+ sha256: requiredString(payload.sha256, "sha256", "inbound"),
20276
+ absolutePath: requiredString(payload.absolutePath, "absolutePath", "inbound"),
20277
+ state: oneOf(payload.state, APP_RECORD_STATES, "state", "inbound"),
20278
+ receivedAt: requiredString(payload.receivedAt, "receivedAt", "inbound")
20279
+ }
20280
+ };
20281
+ case "app-send-result": {
20282
+ const requestId = requiredString(payload.requestId, "requestId", "send-result");
20283
+ return {
20284
+ type: "app-send-result",
20285
+ payload: {
20286
+ requestId,
20287
+ messageId: requiredString(payload.messageId, "messageId", "send-result"),
20288
+ transferId: optionalStringValue(payload.transferId),
20289
+ absolutePath: optionalStringValue(payload.absolutePath),
20290
+ status: oneOf(payload.status, APP_SEND_STATUSES, "status", "send-result"),
20291
+ stage: oneOf(payload.stage, APP_SEND_STAGES, "stage", "send-result"),
20292
+ persistedOffset: optionalIntegerValue(payload.persistedOffset, "persistedOffset", "send-result", 0)
20293
+ }
20294
+ };
20295
+ }
20296
+ case "app-status-result": {
20297
+ const requestId = requiredString(payload.requestId, "requestId", "status-result");
20298
+ return {
20299
+ type: "app-status-result",
20300
+ payload: {
20301
+ requestId,
20302
+ messageId: requiredString(payload.messageId, "messageId", "status-result"),
20303
+ found: requiredBoolean(payload.found, "found", "status-result"),
20304
+ kind: payload.kind === void 0 ? void 0 : oneOf(payload.kind, ["inbox", "outbox"], "kind", "status-result"),
20305
+ senderDaemonHostId: optionalStringValue(payload.senderDaemonHostId),
20306
+ targetDaemonHostId: optionalStringValue(payload.targetDaemonHostId),
20307
+ state: payload.state === void 0 ? void 0 : oneOf(payload.state, APP_RECORD_STATES, "state", "status-result"),
20308
+ transferId: optionalStringValue(payload.transferId),
20309
+ absolutePath: optionalStringValue(payload.absolutePath),
20310
+ outboxState: payload.outboxState === void 0 ? void 0 : oneOf(payload.outboxState, ["pending", "sent", "received", "result_unknown", "failed"], "outboxState", "status-result"),
20311
+ ambiguous: payload.ambiguous === void 0 ? void 0 : requiredBoolean(payload.ambiguous, "ambiguous", "status-result")
20312
+ }
20313
+ };
20314
+ }
20315
+ case "app-connection-state": {
20316
+ const requestId = requiredString(payload.requestId, "requestId", "connection-state");
20317
+ return {
20318
+ type: "app-connection-state",
20319
+ payload: {
20320
+ requestId,
20321
+ state: oneOf(
20322
+ payload.state,
20323
+ ["connected", "disconnected", "peer-connected", "peer-disconnected"],
20324
+ "state",
20325
+ "connection-state"
20326
+ ),
20327
+ daemonHostId: optionalStringValue(payload.daemonHostId),
20328
+ code: optionalIntegerValue(payload.code, "code", "connection-state", 1e3),
20329
+ reason: optionalStringValue(payload.reason)
20330
+ }
20331
+ };
20332
+ }
20333
+ case "app-error": {
20334
+ const requestId = optionalStringValue(payload.requestId);
20335
+ return {
20336
+ type: "app-error",
20337
+ payload: {
20338
+ requestId,
20339
+ messageId: optionalStringValue(payload.messageId),
20340
+ transferId: optionalStringValue(payload.transferId),
20341
+ code: oneOf(payload.code, APP_ERROR_CODES, "code", "error"),
20342
+ stage: stringValue(payload.stage, "stage", "error"),
20343
+ message: stringValue(payload.message, "message", "error")
20344
+ }
20345
+ };
20346
+ }
20347
+ case "app-pong": {
20348
+ const requestId = requiredString(payload.requestId, "requestId", "pong");
20349
+ return {
20350
+ type: "app-pong",
20351
+ payload: {
20352
+ requestId,
20353
+ sentAt: requiredFiniteNumber(payload.sentAt, "sentAt", "pong"),
20354
+ receivedAt: requiredFiniteNumber(payload.receivedAt, "receivedAt", "pong")
20355
+ }
20356
+ };
20357
+ }
20358
+ default:
20359
+ throw new AppProtocolError({ code: "invalid_frame", message: `unsupported server frame: ${value.type}`, stage: "frame" });
20360
+ }
20361
+ }
20362
+ function validateMessageBody(value) {
20363
+ if (typeof value !== "string") {
20364
+ throw new AppProtocolError({ code: "invalid_frame", message: "body must be a string", stage: "validate" });
20365
+ }
20366
+ const bytes = new TextEncoder().encode(value).length;
20367
+ if (bytes > APP_MAX_MESSAGE_BYTES) {
20368
+ throw new AppProtocolError({ code: "message_too_large", message: "message body exceeds 1 MiB", stage: "validate" });
20369
+ }
20370
+ return value;
20371
+ }
20372
+ function validateAppFileMetadata(value, stage) {
20373
+ if (!isRecord2(value)) {
20374
+ throw new AppProtocolError({ code: "invalid_frame", message: "file metadata must be an object", stage });
20375
+ }
20376
+ const transferId = requiredString(value.transferId, "transferId", stage);
20377
+ const fileName = validateSafeFileName(value.fileName, stage);
20378
+ const totalBytes = requiredInteger(value.totalBytes, "totalBytes", stage, 0);
20379
+ const chunkCount = requiredInteger(value.chunkCount, "chunkCount", stage, 0);
20380
+ const sha256 = requiredString(value.sha256, "sha256", stage);
20381
+ if (totalBytes > APP_MAX_FILE_BYTES) {
20382
+ throw new AppProtocolError({ code: "file_too_large", message: "file exceeds 256 MiB", stage, transferId });
20383
+ }
20384
+ const expectedChunks = computeAppChunkCount(totalBytes);
20385
+ if (chunkCount !== expectedChunks) {
20386
+ throw new AppProtocolError({
20387
+ code: "invalid_frame",
20388
+ message: `chunkCount ${chunkCount} does not match ${expectedChunks} for ${totalBytes} bytes`,
20389
+ stage,
20390
+ transferId
20391
+ });
20392
+ }
20393
+ if (!/^[0-9a-f]{64}$/.test(sha256)) {
20394
+ throw new AppProtocolError({ code: "invalid_frame", message: "sha256 must be a 64 hex digest", stage, transferId });
20395
+ }
20396
+ return { transferId, fileName, totalBytes, chunkCount, sha256 };
20397
+ }
20398
+ function computeAppChunkCount(totalBytes) {
20399
+ if (totalBytes <= 0) return 0;
20400
+ return Math.ceil(totalBytes / APP_FILE_CHUNK_BYTES);
20401
+ }
20402
+ function validateSafeFileName(value, stage) {
20403
+ if (typeof value !== "string") {
20404
+ throw new AppProtocolError({ code: "invalid_file_name", message: "fileName must be a string", stage });
20405
+ }
20406
+ if (value.length === 0 || value === "." || value === "..") {
20407
+ throw new AppProtocolError({ code: "invalid_file_name", message: "fileName is not a valid basename", stage });
20408
+ }
20409
+ if (value.includes("/") || value.includes("\\") || value.includes("\0")) {
20410
+ throw new AppProtocolError({ code: "invalid_file_name", message: "fileName must be a single basename", stage });
20411
+ }
20412
+ const bytes = new TextEncoder().encode(value).length;
20413
+ if (bytes > APP_MAX_SAFE_FILE_NAME_BYTES) {
20414
+ throw new AppProtocolError({ code: "invalid_file_name", message: "fileName exceeds 255 UTF-8 bytes", stage });
20415
+ }
20416
+ return value;
20417
+ }
20418
+ function validateChunkIndex(value, stage) {
20419
+ return requiredInteger(value, "chunkIndex", stage, 0);
20420
+ }
20421
+ function validateOffset(value, stage) {
20422
+ return requiredInteger(value, "offset", stage, 0);
20423
+ }
20424
+ function encodeAppPathSegment(value) {
20425
+ if (value.length === 0) {
20426
+ throw new AppProtocolError({ code: "invalid_frame", message: "identity segment is empty", stage: "storage" });
20427
+ }
20428
+ if (new TextEncoder().encode(value).length > APP_MAX_PATH_IDENTITY_BYTES) {
20429
+ throw new AppProtocolError({ code: "invalid_frame", message: "identity segment exceeds 191 UTF-8 bytes", stage: "storage" });
20430
+ }
20431
+ const bytes = new TextEncoder().encode(value);
20432
+ let binary = "";
20433
+ for (const byte of bytes) binary += String.fromCharCode(byte);
20434
+ const base64 = globalThis.btoa?.(binary) ?? Buffer.from(bytes).toString("base64");
20435
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
20436
+ }
20437
+ function appErrorPayload(options) {
20438
+ return {
20439
+ requestId: options.requestId,
20440
+ messageId: options.messageId,
20441
+ transferId: options.transferId,
20442
+ code: options.code,
20443
+ stage: options.stage,
20444
+ message: options.message
20445
+ };
20446
+ }
20447
+
20448
+ // src/server/daemon-app-messaging-store.ts
20449
+ var import_node_crypto2 = require("node:crypto");
20450
+ var import_promises2 = require("node:fs/promises");
20451
+ var import_node_path5 = require("node:path");
20452
+ var AppStoreError = class extends Error {
20453
+ code;
20454
+ constructor(code, message, options) {
20455
+ super(message);
20456
+ this.name = "AppStoreError";
20457
+ this.code = code;
20458
+ if (options?.cause !== void 0) {
20459
+ Object.defineProperty(this, "cause", { value: options.cause });
20460
+ }
20461
+ }
20462
+ };
20463
+ function assertSegment(value) {
20464
+ if (value.length === 0 || new TextEncoder().encode(value).length > APP_MAX_PATH_IDENTITY_BYTES) {
20465
+ throw new AppStoreError("storage_error", "identity segment is empty or longer than 191 UTF-8 bytes");
20466
+ }
20467
+ return value;
20468
+ }
20469
+ function sameStagedFile(record, meta) {
20470
+ return record.kind === "file" && record.file?.transferId === meta.transferId && record.file.fileName === meta.fileName && record.file.totalBytes === meta.totalBytes && record.file.chunkCount === meta.chunkCount && record.file.sha256 === meta.sha256;
20471
+ }
20472
+ async function fsyncDirectory(directory) {
20473
+ try {
20474
+ const handle = await (0, import_promises2.open)(directory, "r");
20475
+ try {
20476
+ await handle.sync();
20477
+ } finally {
20478
+ await handle.close();
20479
+ }
20480
+ } catch {
20481
+ }
20482
+ }
20483
+ async function atomicWriteJson(filePath, value) {
20484
+ const directory = (0, import_node_path5.dirname)(filePath);
20485
+ await (0, import_promises2.mkdir)(directory, { recursive: true });
20486
+ const tmpPath = (0, import_node_path5.join)(directory, `.tmp-${(0, import_node_crypto2.randomUUID)()}.json`);
20487
+ const handle = await (0, import_promises2.open)(tmpPath, "wx");
20488
+ try {
20489
+ await handle.writeFile(JSON.stringify(value), "utf8");
20490
+ await handle.sync();
20491
+ } finally {
20492
+ await handle.close();
20493
+ }
20494
+ await (0, import_promises2.rename)(tmpPath, filePath);
20495
+ await fsyncDirectory(directory);
20496
+ }
20497
+ async function readJson(filePath) {
20498
+ try {
20499
+ const raw = await (0, import_promises2.readFile)(filePath, "utf8");
20500
+ return JSON.parse(raw);
20501
+ } catch (error) {
20502
+ if (error && typeof error === "object" && error.code === "ENOENT") {
20503
+ return null;
20504
+ }
20505
+ throw error;
20506
+ }
20507
+ }
20508
+ var DaemonAppMessagingStore = class {
20509
+ inboxRoot;
20510
+ outboxRoot;
20511
+ maxInboxRecords;
20512
+ maxInboxBytes;
20513
+ queue = Promise.resolve();
20514
+ constructor(options) {
20515
+ this.inboxRoot = (0, import_node_path5.join)(options.appRootDir, "app-inbox");
20516
+ this.outboxRoot = (0, import_node_path5.join)(options.appRootDir, "app-outbox");
20517
+ this.maxInboxRecords = options.maxInboxRecords ?? APP_MAX_INBOX_RECORDS;
20518
+ this.maxInboxBytes = options.maxInboxBytes ?? APP_MAX_INBOX_BYTES;
20519
+ }
20520
+ async initialize() {
20521
+ await (0, import_promises2.mkdir)(this.inboxRoot, { recursive: true });
20522
+ await (0, import_promises2.mkdir)(this.outboxRoot, { recursive: true });
20523
+ }
20524
+ runExclusive(operation) {
20525
+ const next = this.queue.then(operation, operation);
20526
+ this.queue = next.catch(() => void 0);
20527
+ return next;
20528
+ }
20529
+ recordDir(partition) {
20530
+ return (0, import_node_path5.join)(this.inboxRoot, "records", encodeAppPathSegment(assertSegment(partition)));
20531
+ }
20532
+ filesDir(partition, messageId) {
20533
+ return (0, import_node_path5.join)(this.inboxRoot, "files", encodeAppPathSegment(assertSegment(partition)), encodeAppPathSegment(assertSegment(messageId)));
20534
+ }
20535
+ stagingDir(partition) {
20536
+ return (0, import_node_path5.join)(this.inboxRoot, "staging", encodeAppPathSegment(assertSegment(partition)));
20537
+ }
20538
+ outboxDir(partition) {
20539
+ return (0, import_node_path5.join)(this.outboxRoot, "records", encodeAppPathSegment(assertSegment(partition)));
20540
+ }
20541
+ // An encoded identity is the longest path component (at most 255 ASCII bytes
20542
+ // for the 191-byte protocol limit), so every durable file name is a fixed
20543
+ // short name inside a directory owned by that identity. Appending `.json` or
20544
+ // `.part` to the encoded identity would exceed the 255-byte NAME_MAX.
20545
+ inboxRecordPath(senderDaemonHostId, messageId) {
20546
+ return (0, import_node_path5.join)(this.recordDir(senderDaemonHostId), encodeAppPathSegment(assertSegment(messageId)), "record.json");
20547
+ }
20548
+ outboxRecordPath(targetDaemonHostId, messageId) {
20549
+ return (0, import_node_path5.join)(this.outboxDir(targetDaemonHostId), encodeAppPathSegment(assertSegment(messageId)), "record.json");
20550
+ }
20551
+ stagingPartPath(senderDaemonHostId, transferId) {
20552
+ return (0, import_node_path5.join)(this.stagingDir(senderDaemonHostId), encodeAppPathSegment(assertSegment(transferId)), "data.part");
20553
+ }
20554
+ stagingMetaPath(senderDaemonHostId, transferId) {
20555
+ return (0, import_node_path5.join)(this.stagingDir(senderDaemonHostId), encodeAppPathSegment(assertSegment(transferId)), "meta.json");
20556
+ }
20557
+ stagingEntryDir(senderDaemonHostId, transferId) {
20558
+ return (0, import_node_path5.join)(this.stagingDir(senderDaemonHostId), encodeAppPathSegment(assertSegment(transferId)));
20559
+ }
20560
+ async removeStagingEntry(senderDaemonHostId, transferId) {
20561
+ await (0, import_promises2.rm)(this.stagingEntryDir(senderDaemonHostId, transferId), { recursive: true, force: true });
20562
+ }
20563
+ finalFilePath(senderDaemonHostId, messageId, safeFileName) {
20564
+ return (0, import_node_path5.join)(this.filesDir(senderDaemonHostId, messageId), safeFileName);
20565
+ }
20566
+ async writeInboxRecord(record) {
20567
+ await atomicWriteJson(this.inboxRecordPath(record.senderDaemonHostId, record.messageId), record);
20568
+ }
20569
+ async readInboxRecord(senderDaemonHostId, messageId) {
20570
+ return readJson(this.inboxRecordPath(senderDaemonHostId, messageId));
20571
+ }
20572
+ async listInboxRecords() {
20573
+ const records = [];
20574
+ await this.walkJson(this.recordRoot(), (record) => {
20575
+ records.push(record);
20576
+ });
20577
+ return records;
20578
+ }
20579
+ async writeOutboxRecord(record) {
20580
+ await atomicWriteJson(this.outboxRecordPath(record.targetDaemonHostId, record.messageId), record);
20581
+ }
20582
+ async readOutboxRecord(targetDaemonHostId, messageId) {
20583
+ return readJson(this.outboxRecordPath(targetDaemonHostId, messageId));
20584
+ }
20585
+ async listOutboxRecords() {
20586
+ const records = [];
20587
+ await this.walkJson((0, import_node_path5.join)(this.outboxRoot, "records"), (record) => {
20588
+ records.push(record);
20589
+ });
20590
+ return records;
20591
+ }
20592
+ async listStagingMetadata() {
20593
+ const metadata = [];
20594
+ const stagingRoot = (0, import_node_path5.join)(this.inboxRoot, "staging");
20595
+ await this.walkJson(stagingRoot, (record) => {
20596
+ if (record && record.schemaVersion === 1) {
20597
+ metadata.push(record);
20598
+ }
20599
+ });
20600
+ return metadata;
20601
+ }
20602
+ async writeStagingMetadata(metadata) {
20603
+ await atomicWriteJson(this.stagingMetaPath(metadata.senderDaemonHostId, metadata.transferId), metadata);
20604
+ }
20605
+ async readStagingMetadata(senderDaemonHostId, transferId) {
20606
+ return readJson(this.stagingMetaPath(senderDaemonHostId, transferId));
20607
+ }
20608
+ async hasStagingKey(senderDaemonHostId, messageId) {
20609
+ const all = await this.listStagingMetadata();
20610
+ return all.some(
20611
+ (meta) => meta.senderDaemonHostId === senderDaemonHostId && meta.messageId === messageId
20612
+ );
20613
+ }
20614
+ async findStagingKey(senderDaemonHostId, messageId) {
20615
+ const all = await this.listStagingMetadata();
20616
+ return all.find(
20617
+ (meta) => meta.senderDaemonHostId === senderDaemonHostId && meta.messageId === messageId
20618
+ ) ?? null;
20619
+ }
20620
+ async openStagingPart(metadata) {
20621
+ await (0, import_promises2.mkdir)(this.stagingDir(metadata.senderDaemonHostId), { recursive: true });
20622
+ await (0, import_promises2.mkdir)(this.filesDir(metadata.senderDaemonHostId, metadata.messageId), { recursive: true });
20623
+ await this.writeStagingMetadata({ ...metadata, phase: "open" });
20624
+ const partPath = this.stagingPartPath(metadata.senderDaemonHostId, metadata.transferId);
20625
+ const handle = await (0, import_promises2.open)(partPath, "w");
20626
+ await handle.sync();
20627
+ await handle.close();
20628
+ await fsyncDirectory((0, import_node_path5.dirname)(partPath));
20629
+ }
20630
+ async appendPart(senderDaemonHostId, transferId, expectedOffset, data) {
20631
+ const partPath = this.stagingPartPath(senderDaemonHostId, transferId);
20632
+ const handle = await (0, import_promises2.open)(partPath, "r+");
20633
+ try {
20634
+ const currentSize = (await handle.stat()).size;
20635
+ if (currentSize !== expectedOffset) {
20636
+ throw new AppStoreError("chunk_conflict", `offset ${expectedOffset} does not match persisted part size ${currentSize}`);
20637
+ }
20638
+ await handle.write(data, 0, data.length, expectedOffset);
20639
+ await handle.sync();
20640
+ return expectedOffset + data.length;
20641
+ } finally {
20642
+ await handle.close();
20643
+ }
20644
+ }
20645
+ async statPart(senderDaemonHostId, transferId) {
20646
+ try {
20647
+ return (await (0, import_promises2.stat)(this.stagingPartPath(senderDaemonHostId, transferId))).size;
20648
+ } catch (error) {
20649
+ if (error && typeof error === "object" && error.code === "ENOENT") {
20650
+ return null;
20651
+ }
20652
+ throw error;
20653
+ }
20654
+ }
20655
+ async truncatePart(senderDaemonHostId, transferId, size) {
20656
+ const handle = await (0, import_promises2.open)(this.stagingPartPath(senderDaemonHostId, transferId), "r+");
20657
+ try {
20658
+ await handle.truncate(size);
20659
+ await handle.sync();
20660
+ } finally {
20661
+ await handle.close();
20662
+ }
20663
+ }
20664
+ async readPartRange(senderDaemonHostId, transferId, offset, length) {
20665
+ const handle = await (0, import_promises2.open)(this.stagingPartPath(senderDaemonHostId, transferId), "r");
20666
+ try {
20667
+ const buffer = Buffer.alloc(length);
20668
+ const { bytesRead } = await handle.read(buffer, 0, length, offset);
20669
+ return bytesRead === length ? buffer : buffer.subarray(0, bytesRead);
20670
+ } finally {
20671
+ await handle.close();
20672
+ }
20673
+ }
20674
+ async removePart(senderDaemonHostId, transferId) {
20675
+ await (0, import_promises2.rm)(this.stagingPartPath(senderDaemonHostId, transferId), { force: true });
20676
+ }
20677
+ async renamePartToFinal(senderDaemonHostId, messageId, safeFileName, transferId) {
20678
+ const target = this.finalFilePath(senderDaemonHostId, messageId, safeFileName);
20679
+ await (0, import_promises2.mkdir)((0, import_node_path5.dirname)(target), { recursive: true });
20680
+ await (0, import_promises2.rename)(this.stagingPartPath(senderDaemonHostId, transferId), target);
20681
+ await fsyncDirectory((0, import_node_path5.dirname)(target));
20682
+ return target;
20683
+ }
20684
+ async computeFileSha256(filePath) {
20685
+ const hash2 = (0, import_node_crypto2.createHash)("sha256");
20686
+ const handle = await (0, import_promises2.open)(filePath, "r");
20687
+ try {
20688
+ const buffer = Buffer.alloc(64 * 1024);
20689
+ let position = 0;
20690
+ while (true) {
20691
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
20692
+ if (bytesRead <= 0) break;
20693
+ hash2.update(buffer.subarray(0, bytesRead));
20694
+ position += bytesRead;
20695
+ }
20696
+ } finally {
20697
+ await handle.close();
20698
+ }
20699
+ return hash2.digest("hex");
20700
+ }
20701
+ async computeInboxUsage(excludeStaging) {
20702
+ let records = 0;
20703
+ let bytes = 0;
20704
+ await this.walkJson(this.recordRoot(), (record) => {
20705
+ records += 1;
20706
+ const inbox = record;
20707
+ if (inbox.kind === "message" && typeof inbox.body === "string") {
20708
+ bytes += Buffer.byteLength(inbox.body, "utf8");
20709
+ }
20710
+ });
20711
+ await this.walkFiles((0, import_node_path5.join)(this.inboxRoot, "files"), (_filePath, size) => {
20712
+ bytes += size;
20713
+ });
20714
+ const staging = await this.listStagingMetadata();
20715
+ for (const meta of staging) {
20716
+ if (excludeStaging && meta.senderDaemonHostId === excludeStaging.senderDaemonHostId && meta.transferId === excludeStaging.transferId) {
20717
+ continue;
20718
+ }
20719
+ records += 1;
20720
+ bytes += meta.reservedBytes;
20721
+ }
20722
+ return { records, bytes };
20723
+ }
20724
+ async admitInboxRecord(record, reservedBytes, options) {
20725
+ const existing = await this.readInboxRecord(record.senderDaemonHostId, record.messageId);
20726
+ const isNew = existing === null;
20727
+ if (isNew) {
20728
+ const usage = await this.computeInboxUsage(options?.staging);
20729
+ if (usage.records + 1 > this.maxInboxRecords) {
20730
+ throw new AppStoreError("inbox_full", "durable inbox record limit reached");
20731
+ }
20732
+ if (usage.bytes + reservedBytes > this.maxInboxBytes) {
20733
+ throw new AppStoreError("inbox_full", "durable inbox byte limit reached");
20734
+ }
20735
+ }
20736
+ await this.writeInboxRecord(record);
20737
+ return isNew;
20738
+ }
20739
+ async removeInboxRecord(record) {
20740
+ await (0, import_promises2.rm)(this.inboxRecordPath(record.senderDaemonHostId, record.messageId), { force: true });
20741
+ if (record.file) {
20742
+ await (0, import_promises2.rm)((0, import_node_path5.dirname)(record.file.absolutePath), { recursive: true, force: true });
20743
+ }
20744
+ }
20745
+ async cleanupRetention(now, retentionMs) {
20746
+ const records = await this.listInboxRecords();
20747
+ for (const record of records) {
20748
+ if (record.state !== "delivered" && record.state !== "consumed") continue;
20749
+ const settledAt = record.consumedAt ?? record.deliveredAt ?? record.receivedAt;
20750
+ const settled = Date.parse(settledAt);
20751
+ if (!Number.isFinite(settled) || now - settled < retentionMs) continue;
20752
+ await this.removeInboxRecord(record);
20753
+ }
20754
+ }
20755
+ async recoverStaging() {
20756
+ const recovered = [];
20757
+ for (const meta of await this.listStagingMetadata()) {
20758
+ const partSize = await this.statPart(meta.senderDaemonHostId, meta.transferId);
20759
+ if (meta.phase === "verified") {
20760
+ const absolutePath = meta.absolutePath ?? this.finalFilePath(meta.senderDaemonHostId, meta.messageId, meta.fileName);
20761
+ if (partSize === null) {
20762
+ const finalSize = await (0, import_promises2.stat)(absolutePath).then((s) => s.size).catch(() => -1);
20763
+ if (finalSize !== meta.totalBytes) {
20764
+ throw new AppStoreError("storage_inconsistent", `verified transfer ${meta.transferId} has neither a complete part nor a final file`);
20765
+ }
20766
+ } else if (partSize !== meta.totalBytes) {
20767
+ throw new AppStoreError("storage_inconsistent", `verified transfer ${meta.transferId} part size ${partSize} != ${meta.totalBytes}`);
20768
+ } else {
20769
+ await this.renamePartToFinal(meta.senderDaemonHostId, meta.messageId, meta.fileName, meta.transferId);
20770
+ }
20771
+ const record = await this.finishRecoveredFile(meta, absolutePath);
20772
+ if (record) recovered.push(record);
20773
+ continue;
20774
+ }
20775
+ const openPartSize = partSize ?? 0;
20776
+ if (openPartSize < meta.persistedOffset) {
20777
+ throw new AppStoreError("storage_inconsistent", `open transfer ${meta.transferId} part ${openPartSize} < persisted offset ${meta.persistedOffset}`);
20778
+ }
20779
+ if (openPartSize > meta.persistedOffset) {
20780
+ await this.truncatePart(meta.senderDaemonHostId, meta.transferId, meta.persistedOffset);
20781
+ }
20782
+ }
20783
+ return recovered;
20784
+ }
20785
+ async finishRecoveredFile(meta, absolutePath) {
20786
+ const existing = await this.readInboxRecord(meta.senderDaemonHostId, meta.messageId);
20787
+ if (existing) {
20788
+ if (!sameStagedFile(existing, meta)) {
20789
+ throw new AppStoreError(
20790
+ "storage_inconsistent",
20791
+ `inbox record ${meta.messageId} does not match verified transfer ${meta.transferId}`
20792
+ );
20793
+ }
20794
+ await this.removeStagingEntry(meta.senderDaemonHostId, meta.transferId);
20795
+ return null;
20796
+ }
20797
+ const receivedAt = (/* @__PURE__ */ new Date()).toISOString();
20798
+ const record = {
20799
+ schemaVersion: 1,
20800
+ senderDaemonHostId: meta.senderDaemonHostId,
20801
+ senderDeviceId: meta.senderDeviceId,
20802
+ receiverDeviceId: meta.receiverDeviceId,
20803
+ receiverDaemonHostId: meta.receiverDaemonHostId,
20804
+ messageId: meta.messageId,
20805
+ kind: "file",
20806
+ subject: meta.subject,
20807
+ file: {
20808
+ transferId: meta.transferId,
20809
+ fileName: meta.fileName,
20810
+ totalBytes: meta.totalBytes,
20811
+ chunkCount: meta.chunkCount,
20812
+ sha256: meta.sha256,
20813
+ absolutePath
20814
+ },
20815
+ state: "received",
20816
+ receivedAt
20817
+ };
20818
+ await this.writeInboxRecord(record);
20819
+ await this.removeStagingEntry(meta.senderDaemonHostId, meta.transferId);
20820
+ return record;
20821
+ }
20822
+ recordRoot() {
20823
+ return (0, import_node_path5.join)(this.inboxRoot, "records");
20824
+ }
20825
+ async walkJson(root, visit) {
20826
+ let entries = [];
20827
+ try {
20828
+ entries = await (0, import_promises2.readdir)(root, { withFileTypes: true });
20829
+ } catch (error) {
20830
+ if (error && typeof error === "object" && error.code === "ENOENT") {
20831
+ return;
20832
+ }
20833
+ throw error;
20834
+ }
20835
+ for (const entry of entries) {
20836
+ const next = (0, import_node_path5.join)(root, entry.name);
20837
+ if (entry.isDirectory()) {
20838
+ await this.walkJson(next, visit);
20839
+ } else if (entry.isFile() && (entry.name === "record.json" || entry.name === "meta.json")) {
20840
+ const value = await readJson(next);
20841
+ if (value !== null) await visit(value);
20842
+ }
20843
+ }
20844
+ }
20845
+ async walkFiles(root, visit) {
20846
+ let entries = [];
20847
+ try {
20848
+ entries = await (0, import_promises2.readdir)(root, { withFileTypes: true });
20849
+ } catch (error) {
20850
+ if (error && typeof error === "object" && error.code === "ENOENT") {
20851
+ return;
20852
+ }
20853
+ throw error;
20854
+ }
20855
+ for (const entry of entries) {
20856
+ const next = (0, import_node_path5.join)(root, entry.name);
20857
+ if (entry.isDirectory()) {
20858
+ await this.walkFiles(next, visit);
20859
+ } else if (entry.isFile()) {
20860
+ const size = (await (0, import_promises2.stat)(next)).size;
20861
+ await visit(next, size);
20862
+ }
20863
+ }
20864
+ }
20865
+ };
20866
+
20867
+ // src/server/daemon-app-messaging-runtime.ts
20868
+ var PeerLinkError = class extends Error {
20869
+ code;
20870
+ afterSend;
20871
+ constructor(code, message, afterSend) {
20872
+ super(message);
20873
+ this.name = "PeerLinkError";
20874
+ this.code = code;
20875
+ this.afterSend = afterSend;
20876
+ }
20877
+ };
20878
+ function nowIso(now) {
20879
+ return new Date(now()).toISOString();
20880
+ }
20881
+ function decodeStrictBase64(value) {
20882
+ if (typeof value !== "string" || value.length === 0) {
20883
+ throw new AppProtocolError({ code: "invalid_chunk", message: "chunk data is empty", stage: "file-chunk" });
20884
+ }
20885
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) {
20886
+ throw new AppProtocolError({ code: "invalid_chunk", message: "chunk data is not strict base64", stage: "file-chunk" });
20887
+ }
20888
+ const buffer = Buffer.from(value, "base64");
20889
+ if (buffer.toString("base64") !== value) {
20890
+ throw new AppProtocolError({ code: "invalid_chunk", message: "chunk data decoded lossily", stage: "file-chunk" });
20891
+ }
20892
+ return buffer;
20893
+ }
20894
+ function sameMessageContent(record, payload) {
20895
+ return record.kind === "message" && record.subject === payload.subject && record.body === payload.body;
20896
+ }
20897
+ function sameFileContent(record, payload) {
20898
+ return record.kind === "file" && record.subject === payload.subject && record.file?.transferId === payload.file.transferId && record.file.fileName === payload.file.fileName && record.file.totalBytes === payload.file.totalBytes && record.file.chunkCount === payload.file.chunkCount && record.file.sha256 === payload.file.sha256;
20899
+ }
20900
+ function sameOutboxMessage(record, payload) {
20901
+ return record.kind === "message" && record.subject === payload.subject && record.body === payload.body;
20902
+ }
20903
+ function sameOutboxFile(record, payload) {
20904
+ return record.kind === "file" && record.subject === payload.subject && record.file?.transferId === payload.file.transferId && record.file.fileName === payload.file.fileName && record.file.totalBytes === payload.file.totalBytes && record.file.chunkCount === payload.file.chunkCount && record.file.sha256 === payload.file.sha256;
20905
+ }
20906
+ function inboundMessageFrame(record) {
20907
+ const payload = {
20908
+ messageId: record.messageId,
20909
+ senderDeviceId: record.senderDeviceId,
20910
+ senderDaemonHostId: record.senderDaemonHostId,
20911
+ receiverDeviceId: record.receiverDeviceId,
20912
+ receiverDaemonHostId: record.receiverDaemonHostId,
20913
+ subject: record.subject,
20914
+ body: record.body ?? "",
20915
+ state: record.state,
20916
+ receivedAt: record.receivedAt
20917
+ };
20918
+ return { type: "app-inbound-message", payload };
20919
+ }
20920
+ function inboundFileFrame(record) {
20921
+ if (!record.file) return null;
20922
+ const payload = {
20923
+ messageId: record.messageId,
20924
+ transferId: record.file.transferId,
20925
+ senderDeviceId: record.senderDeviceId,
20926
+ senderDaemonHostId: record.senderDaemonHostId,
20927
+ receiverDeviceId: record.receiverDeviceId,
20928
+ receiverDaemonHostId: record.receiverDaemonHostId,
20929
+ subject: record.subject,
20930
+ fileName: record.file.fileName,
20931
+ totalBytes: record.file.totalBytes,
20932
+ sha256: record.file.sha256,
20933
+ absolutePath: record.file.absolutePath,
20934
+ state: record.state,
20935
+ receivedAt: record.receivedAt
20936
+ };
20937
+ return { type: "app-inbound-file", payload };
20938
+ }
20939
+ function recordStatusPayload(record, requestId) {
20940
+ return {
20941
+ requestId,
20942
+ messageId: record.messageId,
20943
+ found: true,
20944
+ kind: "inbox",
20945
+ senderDaemonHostId: record.senderDaemonHostId,
20946
+ state: record.state,
20947
+ transferId: record.file?.transferId,
20948
+ absolutePath: record.file?.absolutePath
20949
+ };
20950
+ }
20951
+ function outboxStatusPayload(record, requestId) {
20952
+ return {
20953
+ requestId,
20954
+ messageId: record.messageId,
20955
+ found: true,
20956
+ kind: "outbox",
20957
+ targetDaemonHostId: record.targetDaemonHostId,
20958
+ transferId: record.file?.transferId,
20959
+ state: record.state === "received" ? "received" : "rejected",
20960
+ outboxState: record.state
20961
+ };
20962
+ }
20963
+ function frameRequestId(frame) {
20964
+ return "requestId" in frame.payload && typeof frame.payload.requestId === "string" ? frame.payload.requestId : void 0;
20965
+ }
20966
+ function createDaemonAppMessagingRuntime(options) {
20967
+ const now = options.now ?? (() => Date.now());
20968
+ const retentionMs = options.retentionMs ?? APP_ACKED_RETENTION_MS;
20969
+ const peerRequestTimeoutMs = options.peerRequestTimeoutMs ?? 15e3;
20970
+ const store = new DaemonAppMessagingStore({
20971
+ appRootDir: options.appRootDir,
20972
+ maxInboxRecords: options.maxInboxRecords ?? APP_MAX_INBOX_RECORDS,
20973
+ maxInboxBytes: options.maxInboxBytes ?? APP_MAX_INBOX_BYTES
20974
+ });
20975
+ const subscribers = /* @__PURE__ */ new Map();
20976
+ const peerLinks = /* @__PURE__ */ new Map();
20977
+ const activeFileSends = /* @__PURE__ */ new Map();
20978
+ let initialized = false;
20979
+ let initializationError = null;
20980
+ let retentionTimer = null;
20981
+ const createSocket = options.createWebSocket ?? ((url, authToken) => new wrapper_default(url, {
20982
+ headers: authToken ? { Authorization: `Bearer ${authToken}` } : {}
20983
+ }));
20984
+ function sendError(link, error, requestId, messageId, transferId) {
20985
+ const code = error instanceof AppProtocolError || error instanceof PeerLinkError ? error.code : error instanceof AppStoreError ? error.code === "storage_inconsistent" ? "storage_error" : error.code : "internal_error";
20986
+ const stage = error instanceof AppProtocolError ? error.stage : "execute";
20987
+ const message = error instanceof Error ? error.message : String(error);
20988
+ link.send({
20989
+ type: "app-error",
20990
+ payload: appErrorPayload({
20991
+ code,
20992
+ stage,
20993
+ message,
20994
+ requestId,
20995
+ messageId,
20996
+ transferId
20997
+ })
20998
+ });
20999
+ }
21000
+ function notifySubscribers(frame) {
21001
+ for (const [id, subscriber] of [...subscribers]) {
21002
+ if (!subscriber.send(frame)) {
21003
+ subscribers.delete(id);
21004
+ subscriber.close("slow_consumer", 4008);
21005
+ }
21006
+ }
21007
+ }
21008
+ function fileSendKey(linkId, transferId) {
21009
+ return `${linkId}\0${transferId}`;
21010
+ }
21011
+ function reserveFileSend(link, file, messageId, target) {
21012
+ const key = fileSendKey(link.id, file.transferId);
21013
+ const current = activeFileSends.get(key);
21014
+ if (current) {
21015
+ if (current.messageId !== messageId || current.target.daemonHostId !== target.daemonHostId) {
21016
+ throw new AppProtocolError({
21017
+ code: "transfer_conflict",
21018
+ message: "transferId is already bound to another message or target on this connection",
21019
+ stage: "file-start",
21020
+ messageId,
21021
+ transferId: file.transferId
21022
+ });
21023
+ }
21024
+ current.peer = void 0;
21025
+ current.revision += 1;
21026
+ return { active: current, token: current.revision };
21027
+ }
21028
+ const reserved = { linkId: link.id, transferId: file.transferId, messageId, target, revision: 1 };
21029
+ activeFileSends.set(key, reserved);
21030
+ return { active: reserved, token: 1 };
21031
+ }
21032
+ function bindFileSendPeer(link, reservation, peer) {
21033
+ const key = fileSendKey(link.id, reservation.active.transferId);
21034
+ const current = activeFileSends.get(key);
21035
+ if (current === reservation.active && current.revision === reservation.token) {
21036
+ current.peer = peer;
21037
+ }
21038
+ }
21039
+ function clearFileSend(link, transferId, active, token) {
21040
+ const key = fileSendKey(link.id, transferId);
21041
+ const current = activeFileSends.get(key);
21042
+ if (current === active && current.revision === token) activeFileSends.delete(key);
21043
+ }
21044
+ function resolveActiveFileSend(link, messageId, transferId, stage) {
21045
+ const active = activeFileSends.get(fileSendKey(link.id, transferId));
21046
+ if (!active) {
21047
+ throw new AppProtocolError({
21048
+ code: "invalid_state",
21049
+ message: "file transfer is not active; restart with file-start",
21050
+ stage,
21051
+ transferId
21052
+ });
21053
+ }
21054
+ if (active.messageId !== messageId) {
21055
+ throw new AppProtocolError({
21056
+ code: "transfer_conflict",
21057
+ message: "transferId is bound to another messageId on this connection",
21058
+ stage,
21059
+ messageId,
21060
+ transferId
21061
+ });
21062
+ }
21063
+ return active;
21064
+ }
21065
+ function publishRecord(record) {
21066
+ const frame = record.kind === "file" ? inboundFileFrame(record) : inboundMessageFrame(record);
21067
+ if (frame) notifySubscribers(frame);
21068
+ }
21069
+ function notifyPeerState(state, daemonHostId) {
21070
+ notifySubscribers({
21071
+ type: "app-connection-state",
21072
+ payload: {
21073
+ requestId: `event-${(0, import_node_crypto3.randomUUID)()}`,
21074
+ state,
21075
+ daemonHostId
21076
+ }
21077
+ });
21078
+ }
21079
+ async function closePeerLink(peer, reason) {
21080
+ if (peer.closed) return;
21081
+ peer.closed = true;
21082
+ peerLinks.delete(peer.key);
21083
+ for (const pending of peer.pending.values()) {
21084
+ clearTimeout(pending.timer);
21085
+ pending.reject(new PeerLinkError(pending.settlement.code, reason, pending.settlement.afterSend));
21086
+ }
21087
+ peer.pending.clear();
21088
+ try {
21089
+ peer.ws.close(1e3, reason);
21090
+ } catch {
21091
+ }
21092
+ notifyPeerState("peer-disconnected", peer.target.daemonHostId);
21093
+ }
21094
+ async function connectPeer(target) {
21095
+ const key = `${target.daemonHostId}\0${target.wsUrl}`;
21096
+ const existing = peerLinks.get(key);
21097
+ if (existing && !existing.closed && existing.ws.readyState === wrapper_default.OPEN && existing.ready) {
21098
+ if (await probePeerLink(existing)) {
21099
+ return existing;
21100
+ }
21101
+ await closePeerLink(existing, "peer liveness probe failed");
21102
+ }
21103
+ const ws = createSocket(target.wsUrl, target.authToken);
21104
+ const peer = {
21105
+ key,
21106
+ target,
21107
+ ws,
21108
+ ready: false,
21109
+ closed: false,
21110
+ pending: /* @__PURE__ */ new Map()
21111
+ };
21112
+ peerLinks.set(key, peer);
21113
+ ws.on("message", (raw) => {
21114
+ try {
21115
+ const parsed = JSON.parse(typeof raw === "string" ? raw : Buffer.from(raw).toString("utf8"));
21116
+ const frame = validateAppServerFrame(parsed);
21117
+ handlePeerFrame(peer, frame);
21118
+ } catch (error) {
21119
+ const requestId = resolvePeerRequestIdFromRaw(raw);
21120
+ for (const [id, pending] of [...peer.pending]) {
21121
+ if (requestId === void 0 || id === requestId) {
21122
+ clearTimeout(pending.timer);
21123
+ peer.pending.delete(id);
21124
+ pending.reject(new PeerLinkError(
21125
+ "invalid_frame",
21126
+ `peer returned an invalid frame: ${error instanceof Error ? error.message : String(error)}`,
21127
+ false
21128
+ ));
21129
+ }
21130
+ }
21131
+ }
21132
+ });
21133
+ ws.on("close", () => {
21134
+ void closePeerLink(peer, "peer closed");
21135
+ });
21136
+ ws.on("error", (error) => {
21137
+ void closePeerLink(peer, error.message);
21138
+ });
21139
+ const helloRequestId = `hello-${(0, import_node_crypto3.randomUUID)()}`;
21140
+ const ready = await new Promise((resolve4, reject) => {
21141
+ const timer = setTimeout(() => {
21142
+ peer.pending.delete(helloRequestId);
21143
+ reject(new PeerLinkError("unreachable", "peer handshake timed out", false));
21144
+ }, peerRequestTimeoutMs);
21145
+ peer.pending.set(helloRequestId, {
21146
+ resolve: resolve4,
21147
+ reject,
21148
+ timer,
21149
+ settlement: { code: "unreachable", afterSend: false }
21150
+ });
21151
+ ws.once("open", () => {
21152
+ ws.send(JSON.stringify({
21153
+ type: "app-hello",
21154
+ payload: {
21155
+ protocolVersion: APP_PROTOCOL_VERSION,
21156
+ requestId: helloRequestId,
21157
+ role: "peer",
21158
+ clientInstanceId: `daemon-${(0, import_node_crypto3.randomUUID)()}`,
21159
+ deviceId: options.deviceId,
21160
+ daemonHostId: options.daemonHostId,
21161
+ capabilities: {}
21162
+ }
21163
+ }));
21164
+ });
21165
+ ws.once("unexpected-response", (_request, response) => {
21166
+ clearTimeout(timer);
21167
+ peer.pending.delete(helloRequestId);
21168
+ reject(new PeerLinkError(
21169
+ response.statusCode === 401 ? "unauthorized" : "unreachable",
21170
+ `peer upgrade rejected with HTTP ${response.statusCode}`,
21171
+ false
21172
+ ));
21173
+ });
21174
+ ws.once("error", (error) => {
21175
+ clearTimeout(timer);
21176
+ peer.pending.delete(helloRequestId);
21177
+ reject(new PeerLinkError("unreachable", error.message, false));
21178
+ });
21179
+ }).catch(async (error) => {
21180
+ await closePeerLink(peer, error instanceof Error ? error.message : "peer handshake failed");
21181
+ throw error;
21182
+ });
21183
+ if (ready.type !== "app-ready" || ready.payload.protocolVersion !== APP_PROTOCOL_VERSION || ready.payload.daemonHostId !== target.daemonHostId || ready.payload.deviceId !== target.deviceId) {
21184
+ await closePeerLink(peer, "peer identity or version mismatch");
21185
+ throw new PeerLinkError("unreachable", "peer returned a different daemon identity or protocol version", false);
21186
+ }
21187
+ notifyPeerState("peer-connected", target.daemonHostId);
21188
+ return peer;
21189
+ }
21190
+ function resolvePeerRequestIdFromRaw(raw) {
21191
+ try {
21192
+ const parsed = JSON.parse(typeof raw === "string" ? raw : Buffer.from(raw).toString("utf8"));
21193
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
21194
+ const payload = parsed.payload;
21195
+ if (payload && typeof payload === "object") {
21196
+ return payload.requestId;
21197
+ }
21198
+ }
21199
+ return void 0;
21200
+ } catch {
21201
+ return void 0;
21202
+ }
21203
+ }
21204
+ function handlePeerFrame(peer, frame) {
21205
+ const requestId = "payload" in frame && frame.payload && typeof frame.payload === "object" ? frame.payload.requestId : void 0;
21206
+ if (frame.type === "app-ready" && typeof requestId === "string") {
21207
+ const pending2 = peer.pending.get(requestId);
21208
+ if (pending2) {
21209
+ clearTimeout(pending2.timer);
21210
+ peer.pending.delete(requestId);
21211
+ peer.ready = true;
21212
+ pending2.resolve(frame);
21213
+ }
21214
+ return;
21215
+ }
21216
+ if (typeof requestId !== "string") return;
21217
+ const pending = peer.pending.get(requestId);
21218
+ if (!pending) return;
21219
+ clearTimeout(pending.timer);
21220
+ peer.pending.delete(requestId);
21221
+ pending.resolve(frame);
21222
+ }
21223
+ async function probePeerLink(peer) {
21224
+ if (peer.closed || peer.ws.readyState !== wrapper_default.OPEN || !peer.ready) return false;
21225
+ const requestId = `probe-${(0, import_node_crypto3.randomUUID)()}`;
21226
+ try {
21227
+ await new Promise((resolve4, reject) => {
21228
+ const timer = setTimeout(() => {
21229
+ peer.pending.delete(requestId);
21230
+ reject(new PeerLinkError("unreachable", "peer liveness probe timed out", false));
21231
+ }, peerRequestTimeoutMs);
21232
+ const pending = {
21233
+ resolve: (frame) => {
21234
+ if (frame.type === "app-pong" && frame.payload.requestId === requestId) {
21235
+ resolve4();
21236
+ return;
21237
+ }
21238
+ reject(new PeerLinkError("unreachable", "peer liveness probe returned an unexpected frame", false));
21239
+ },
21240
+ reject,
21241
+ timer,
21242
+ settlement: { code: "unreachable", afterSend: false }
21243
+ };
21244
+ peer.pending.set(requestId, pending);
21245
+ try {
21246
+ peer.ws.send(JSON.stringify({
21247
+ type: "app-ping",
21248
+ payload: { requestId, sentAt: now() }
21249
+ }));
21250
+ } catch (error) {
21251
+ clearTimeout(timer);
21252
+ peer.pending.delete(requestId);
21253
+ reject(new PeerLinkError("unreachable", error instanceof Error ? error.message : "peer liveness probe send failed", false));
21254
+ }
21255
+ });
21256
+ return true;
21257
+ } catch {
21258
+ return false;
21259
+ }
21260
+ }
21261
+ async function peerRequest(peer, frame) {
21262
+ if (peer.closed || peer.ws.readyState !== wrapper_default.OPEN) {
21263
+ throw new PeerLinkError("unreachable", "peer transport is closed", false);
21264
+ }
21265
+ const requestId = frameRequestId(frame);
21266
+ if (!requestId) {
21267
+ throw new AppProtocolError({ code: "invalid_frame", message: "requestId is required", stage: "peer" });
21268
+ }
21269
+ return new Promise((resolve4, reject) => {
21270
+ const timer = setTimeout(() => {
21271
+ peer.pending.delete(requestId);
21272
+ reject(new PeerLinkError("result_unknown", "peer receipt timed out", true));
21273
+ }, peerRequestTimeoutMs);
21274
+ peer.pending.set(requestId, {
21275
+ resolve: resolve4,
21276
+ reject,
21277
+ timer,
21278
+ settlement: { code: "result_unknown", afterSend: true }
21279
+ });
21280
+ try {
21281
+ peer.ws.send(JSON.stringify(frame));
21282
+ } catch (error) {
21283
+ clearTimeout(timer);
21284
+ peer.pending.delete(requestId);
21285
+ reject(new PeerLinkError("result_unknown", error instanceof Error ? error.message : "peer send failed", true));
21286
+ }
21287
+ });
21288
+ }
21289
+ function peerResultFrame(requestId, messageId, status, stage, extra) {
21290
+ return {
21291
+ type: "app-send-result",
21292
+ payload: {
21293
+ requestId,
21294
+ messageId,
21295
+ ...extra?.transferId ? { transferId: extra.transferId } : {},
21296
+ status,
21297
+ stage,
21298
+ ...extra?.persistedOffset !== void 0 ? { persistedOffset: extra.persistedOffset } : {}
21299
+ }
21300
+ };
21301
+ }
21302
+ async function prepareMessageOutbox(payload) {
21303
+ const target = validateAppTarget(payload.target, "send-message");
21304
+ const record = {
21305
+ schemaVersion: 1,
21306
+ targetDeviceId: target.deviceId,
21307
+ targetDaemonHostId: target.daemonHostId,
21308
+ messageId: payload.messageId,
21309
+ kind: "message",
21310
+ subject: payload.subject,
21311
+ body: payload.body,
21312
+ state: "pending",
21313
+ updatedAt: nowIso(now)
21314
+ };
21315
+ await store.runExclusive(async () => {
21316
+ const existing = await store.readOutboxRecord(target.daemonHostId, payload.messageId);
21317
+ if (existing && existing.kind !== "message") {
21318
+ throw new AppProtocolError({ code: "message_id_conflict", message: "messageId collision", stage: "outbox", messageId: payload.messageId });
21319
+ }
21320
+ if (existing && !sameOutboxMessage(existing, payload)) {
21321
+ throw new AppProtocolError({ code: "message_id_conflict", message: "messageId content conflict", stage: "outbox", messageId: payload.messageId });
21322
+ }
21323
+ if (existing?.state === "received") return;
21324
+ await store.writeOutboxRecord(record);
21325
+ });
21326
+ return store.readOutboxRecord(target.daemonHostId, payload.messageId);
21327
+ }
21328
+ async function prepareFileOutbox(payload) {
21329
+ const target = validateAppTarget(payload.target, "file-start");
21330
+ const record = {
21331
+ schemaVersion: 1,
21332
+ targetDeviceId: target.deviceId,
21333
+ targetDaemonHostId: target.daemonHostId,
21334
+ messageId: payload.messageId,
21335
+ kind: "file",
21336
+ subject: payload.subject,
21337
+ file: payload.file,
21338
+ state: "pending",
21339
+ updatedAt: nowIso(now)
21340
+ };
21341
+ await store.runExclusive(async () => {
21342
+ const existing = await store.readOutboxRecord(target.daemonHostId, payload.messageId);
21343
+ if (existing && existing.kind !== "file") {
21344
+ throw new AppProtocolError({ code: "message_id_conflict", message: "messageId collision", stage: "outbox", messageId: payload.messageId });
21345
+ }
21346
+ if (existing && !sameOutboxFile(existing, payload)) {
21347
+ throw new AppProtocolError({ code: "message_id_conflict", message: "messageId content conflict", stage: "outbox", messageId: payload.messageId });
21348
+ }
21349
+ if (existing?.state === "received") return;
21350
+ await store.writeOutboxRecord(record);
21351
+ });
21352
+ return store.readOutboxRecord(target.daemonHostId, payload.messageId);
21353
+ }
21354
+ async function updateOutbox(targetDaemonHostId, messageId, state, lastError) {
21355
+ await store.runExclusive(async () => {
21356
+ const existing = await store.readOutboxRecord(targetDaemonHostId, messageId);
21357
+ if (!existing) return;
21358
+ await store.writeOutboxRecord({
21359
+ ...existing,
21360
+ state,
21361
+ ...lastError ? { lastError } : { lastError: void 0 },
21362
+ updatedAt: nowIso(now)
21363
+ });
21364
+ });
21365
+ }
21366
+ async function sendMessageFromLocal(link, payload) {
21367
+ const target = validateAppTarget(payload.target, "send-message");
21368
+ const body = validateMessageBody(payload.body);
21369
+ await prepareMessageOutbox({ ...payload, target, body });
21370
+ const outbox = await store.readOutboxRecord(target.daemonHostId, payload.messageId);
21371
+ if (outbox?.state === "received") {
21372
+ link.send(peerResultFrame(payload.requestId, payload.messageId, "received", "received"));
21373
+ return;
21374
+ }
21375
+ try {
21376
+ if (target.daemonHostId === options.daemonHostId) {
21377
+ const result = await receiveMessage(
21378
+ { deviceId: options.deviceId, daemonHostId: options.daemonHostId },
21379
+ { ...payload, body }
21380
+ );
21381
+ await updateOutbox(target.daemonHostId, payload.messageId, result === "received" || result === "delivered" || result === "consumed" ? "received" : "failed");
21382
+ link.send(peerResultFrame(payload.requestId, payload.messageId, result === "rejected" ? "rejected" : "received", "received"));
21383
+ return;
21384
+ }
21385
+ const peer = await connectPeer(target);
21386
+ const response = await peerRequest(peer, {
21387
+ type: "app-send-message",
21388
+ payload: {
21389
+ requestId: payload.requestId,
21390
+ messageId: payload.messageId,
21391
+ subject: payload.subject,
21392
+ body
21393
+ }
21394
+ });
21395
+ if (response.type === "app-error") {
21396
+ await updateOutbox(target.daemonHostId, payload.messageId, "failed", response.payload.code);
21397
+ link.send(response);
21398
+ return;
21399
+ }
21400
+ if (response.type !== "app-send-result") {
21401
+ await updateOutbox(target.daemonHostId, payload.messageId, "result_unknown", "unexpected peer response");
21402
+ link.send(peerResultFrame(payload.requestId, payload.messageId, "resultUnknown", "result_unknown"));
21403
+ return;
21404
+ }
21405
+ const state = response.payload.status === "received" || response.payload.status === "delivered" || response.payload.status === "consumed" ? "received" : response.payload.status === "rejected" ? "failed" : "result_unknown";
21406
+ await updateOutbox(target.daemonHostId, payload.messageId, state);
21407
+ link.send({ ...response, payload: { ...response.payload, requestId: payload.requestId } });
21408
+ } catch (error) {
21409
+ if (error instanceof PeerLinkError) {
21410
+ const state = error.afterSend ? "result_unknown" : "failed";
21411
+ await updateOutbox(target.daemonHostId, payload.messageId, state, error.code);
21412
+ if (error.code === "result_unknown") {
21413
+ link.send(peerResultFrame(payload.requestId, payload.messageId, "resultUnknown", "result_unknown"));
21414
+ } else {
21415
+ sendError(link, error, payload.requestId, payload.messageId);
21416
+ }
21417
+ return;
21418
+ }
21419
+ await updateOutbox(target.daemonHostId, payload.messageId, "failed", error instanceof Error ? error.message : "send failed");
21420
+ sendError(link, error, payload.requestId, payload.messageId);
21421
+ }
21422
+ }
21423
+ async function sendFileStartFromLocal(link, payload) {
21424
+ const target = validateAppTarget(payload.target, "file-start");
21425
+ const file = validateAppFileMetadata(payload.file, "file-start");
21426
+ await prepareFileOutbox({ ...payload, target, file });
21427
+ const outbox = await store.readOutboxRecord(target.daemonHostId, payload.messageId);
21428
+ if (outbox?.state === "received") {
21429
+ link.send(peerResultFrame(payload.requestId, payload.messageId, "received", "received", { transferId: file.transferId }));
21430
+ return;
21431
+ }
21432
+ let reservation = null;
21433
+ try {
21434
+ reservation = await store.runExclusive(async () => {
21435
+ return reserveFileSend(link, file, payload.messageId, target);
21436
+ });
21437
+ if (target.daemonHostId === options.daemonHostId) {
21438
+ const result = await receiveFileStart(
21439
+ { deviceId: options.deviceId, daemonHostId: options.daemonHostId },
21440
+ { ...payload, file }
21441
+ );
21442
+ if (result.type !== "app-send-result" || result.payload.status !== "receiving") {
21443
+ clearFileSend(link, file.transferId, reservation.active, reservation.token);
21444
+ }
21445
+ link.send(result);
21446
+ return;
21447
+ }
21448
+ const peer = await connectPeer(target);
21449
+ bindFileSendPeer(link, reservation, peer);
21450
+ const response = await peerRequest(peer, {
21451
+ type: "app-send-file-start",
21452
+ payload: {
21453
+ requestId: payload.requestId,
21454
+ messageId: payload.messageId,
21455
+ subject: payload.subject,
21456
+ file
21457
+ }
21458
+ });
21459
+ if (response.type === "app-error") {
21460
+ clearFileSend(link, file.transferId, reservation.active, reservation.token);
21461
+ await updateOutbox(target.daemonHostId, payload.messageId, "failed", response.payload.code);
21462
+ }
21463
+ link.send(response);
21464
+ } catch (error) {
21465
+ if (reservation) clearFileSend(link, file.transferId, reservation.active, reservation.token);
21466
+ if (error instanceof PeerLinkError) {
21467
+ const state = error.afterSend ? "result_unknown" : "failed";
21468
+ await updateOutbox(target.daemonHostId, payload.messageId, state, error.code);
21469
+ if (error.code === "result_unknown") {
21470
+ link.send(peerResultFrame(payload.requestId, payload.messageId, "resultUnknown", "result_unknown", { transferId: file.transferId }));
21471
+ } else {
21472
+ sendError(link, error, payload.requestId, payload.messageId, file.transferId);
21473
+ }
21474
+ return;
21475
+ }
21476
+ await updateOutbox(target.daemonHostId, payload.messageId, "failed", error instanceof Error ? error.message : "file start failed");
21477
+ sendError(link, error, payload.requestId, payload.messageId, file.transferId);
21478
+ }
21479
+ }
21480
+ async function forwardFileChunk(link, payload) {
21481
+ const active = resolveActiveFileSend(link, payload.messageId, payload.transferId, "file-chunk");
21482
+ try {
21483
+ if (active.target.daemonHostId === options.daemonHostId) {
21484
+ const response2 = await receiveFileChunk(
21485
+ { deviceId: options.deviceId, daemonHostId: options.daemonHostId },
21486
+ payload
21487
+ );
21488
+ link.send(response2);
21489
+ return;
21490
+ }
21491
+ if (!active.peer) {
21492
+ throw new AppProtocolError({ code: "invalid_state", message: "peer transport is not active", stage: "file-chunk", transferId: payload.transferId });
21493
+ }
21494
+ const response = await peerRequest(active.peer, { type: "app-send-file-chunk", payload });
21495
+ if (response.type === "app-error") {
21496
+ await updateOutbox(active.target.daemonHostId, payload.messageId, "failed", response.payload.code);
21497
+ }
21498
+ link.send(response);
21499
+ } catch (error) {
21500
+ if (error instanceof PeerLinkError) {
21501
+ const state = error.afterSend ? "result_unknown" : "failed";
21502
+ await updateOutbox(active.target.daemonHostId, payload.messageId, state, error.code);
21503
+ if (error.code === "result_unknown") {
21504
+ link.send(peerResultFrame(payload.requestId, payload.messageId, "resultUnknown", "result_unknown", { transferId: payload.transferId }));
21505
+ } else {
21506
+ sendError(link, error, payload.requestId, payload.messageId, payload.transferId);
21507
+ }
21508
+ return;
21509
+ }
21510
+ await updateOutbox(active.target.daemonHostId, payload.messageId, "failed", error instanceof Error ? error.message : "file chunk failed");
21511
+ sendError(link, error, payload.requestId, payload.messageId, payload.transferId);
21512
+ }
21513
+ }
21514
+ async function forwardFileCommit(link, payload) {
21515
+ const active = resolveActiveFileSend(link, payload.messageId, payload.transferId, "file-commit");
21516
+ const commitToken = active.revision;
21517
+ try {
21518
+ if (active.target.daemonHostId === options.daemonHostId) {
21519
+ const response2 = await receiveFileCommit(
21520
+ { deviceId: options.deviceId, daemonHostId: options.daemonHostId },
21521
+ payload
21522
+ );
21523
+ if (response2.type === "app-send-result" && (response2.payload.status === "received" || response2.payload.status === "delivered" || response2.payload.status === "consumed")) {
21524
+ await updateOutbox(active.target.daemonHostId, payload.messageId, "received");
21525
+ clearFileSend(link, payload.transferId, active, commitToken);
21526
+ }
21527
+ link.send(response2);
21528
+ return;
21529
+ }
21530
+ if (!active.peer) {
21531
+ throw new AppProtocolError({ code: "invalid_state", message: "peer transport is not active", stage: "file-commit", transferId: payload.transferId });
21532
+ }
21533
+ const response = await peerRequest(active.peer, { type: "app-send-file-commit", payload });
21534
+ if (response.type === "app-error") {
21535
+ await updateOutbox(active.target.daemonHostId, payload.messageId, "failed", response.payload.code);
21536
+ link.send(response);
21537
+ return;
21538
+ }
21539
+ if (response.type === "app-send-result" && (response.payload.status === "received" || response.payload.status === "delivered" || response.payload.status === "consumed")) {
21540
+ await updateOutbox(active.target.daemonHostId, payload.messageId, "received");
21541
+ clearFileSend(link, payload.transferId, active, commitToken);
21542
+ }
21543
+ link.send(response);
21544
+ } catch (error) {
21545
+ if (error instanceof PeerLinkError) {
21546
+ const state = error.afterSend ? "result_unknown" : "failed";
21547
+ await updateOutbox(active.target.daemonHostId, payload.messageId, state, error.code);
21548
+ if (error.code === "result_unknown") {
21549
+ link.send(peerResultFrame(payload.requestId, payload.messageId, "resultUnknown", "result_unknown", { transferId: payload.transferId }));
21550
+ } else {
21551
+ sendError(link, error, payload.requestId, payload.messageId, payload.transferId);
21552
+ }
21553
+ return;
21554
+ }
21555
+ await updateOutbox(active.target.daemonHostId, payload.messageId, "failed", error instanceof Error ? error.message : "file commit failed");
21556
+ sendError(link, error, payload.requestId, payload.messageId, payload.transferId);
21557
+ }
21558
+ }
21559
+ async function receiveMessage(sender, payload) {
21560
+ const body = validateMessageBody(payload.body);
21561
+ return store.runExclusive(async () => {
21562
+ const existing = await store.readInboxRecord(sender.daemonHostId, payload.messageId);
21563
+ if (existing) {
21564
+ if (!sameMessageContent(existing, { ...payload, body })) {
21565
+ throw new AppProtocolError({
21566
+ code: "message_id_conflict",
21567
+ message: "messageId already exists with different content",
21568
+ stage: "inbound",
21569
+ messageId: payload.messageId
21570
+ });
21571
+ }
21572
+ return existing.state;
21573
+ }
21574
+ if (await store.hasStagingKey(sender.daemonHostId, payload.messageId)) {
21575
+ throw new AppProtocolError({
21576
+ code: "message_id_conflict",
21577
+ message: "messageId is already being received as a file transfer",
21578
+ stage: "inbound",
21579
+ messageId: payload.messageId
21580
+ });
21581
+ }
21582
+ const record = {
21583
+ schemaVersion: 1,
21584
+ senderDeviceId: sender.deviceId,
21585
+ senderDaemonHostId: sender.daemonHostId,
21586
+ receiverDeviceId: options.deviceId,
21587
+ receiverDaemonHostId: options.daemonHostId,
21588
+ messageId: payload.messageId,
21589
+ kind: "message",
21590
+ subject: payload.subject,
21591
+ body,
21592
+ state: "received",
21593
+ receivedAt: nowIso(now)
21594
+ };
21595
+ await store.admitInboxRecord(record, Buffer.byteLength(body, "utf8"));
21596
+ publishRecord(record);
21597
+ return record.state;
21598
+ });
21599
+ }
21600
+ async function receiveFileStart(sender, payload) {
21601
+ const file = validateAppFileMetadata(payload.file, "file-start");
21602
+ return store.runExclusive(async () => {
21603
+ const existingRecord = await store.readInboxRecord(sender.daemonHostId, payload.messageId);
21604
+ if (existingRecord) {
21605
+ if (!sameFileContent(existingRecord, { ...payload, file })) {
21606
+ throw new AppProtocolError({
21607
+ code: "message_id_conflict",
21608
+ message: "messageId already exists with a different file",
21609
+ stage: "inbound-file",
21610
+ messageId: payload.messageId
21611
+ });
21612
+ }
21613
+ return peerResultFrame(payload.requestId, payload.messageId, existingRecord.state === "received" ? "received" : existingRecord.state === "rejected" ? "rejected" : "received", "received", {
21614
+ transferId: file.transferId
21615
+ });
21616
+ }
21617
+ const sameKeyStaging = await store.findStagingKey(sender.daemonHostId, payload.messageId);
21618
+ if (sameKeyStaging && sameKeyStaging.transferId !== file.transferId) {
21619
+ throw new AppProtocolError({
21620
+ code: "message_id_conflict",
21621
+ message: "messageId is already being received as a different transfer",
21622
+ stage: "inbound-file",
21623
+ messageId: payload.messageId
21624
+ });
21625
+ }
21626
+ const existingStaging = await store.readStagingMetadata(sender.daemonHostId, file.transferId);
21627
+ if (existingStaging) {
21628
+ const matches = existingStaging.messageId === payload.messageId && existingStaging.fileName === file.fileName && existingStaging.totalBytes === file.totalBytes && existingStaging.chunkCount === file.chunkCount && existingStaging.sha256 === file.sha256;
21629
+ if (!matches) {
21630
+ throw new AppProtocolError({
21631
+ code: "transfer_conflict",
21632
+ message: "transferId already exists with different metadata",
21633
+ stage: "inbound-file",
21634
+ transferId: file.transferId
21635
+ });
21636
+ }
21637
+ return peerResultFrame(payload.requestId, payload.messageId, "receiving", "prepared", {
21638
+ transferId: file.transferId,
21639
+ persistedOffset: existingStaging.persistedOffset
21640
+ });
21641
+ }
21642
+ const usage = await store.computeInboxUsage();
21643
+ if (usage.records + 1 > (options.maxInboxRecords ?? APP_MAX_INBOX_RECORDS) || usage.bytes + file.totalBytes > (options.maxInboxBytes ?? APP_MAX_INBOX_BYTES)) {
21644
+ throw new AppStoreError("inbox_full", "durable inbox capacity would be exceeded by this file");
21645
+ }
21646
+ const metadata = {
21647
+ schemaVersion: 1,
21648
+ senderDaemonHostId: sender.daemonHostId,
21649
+ senderDeviceId: sender.deviceId,
21650
+ receiverDeviceId: options.deviceId,
21651
+ receiverDaemonHostId: options.daemonHostId,
21652
+ transferId: file.transferId,
21653
+ messageId: payload.messageId,
21654
+ subject: payload.subject,
21655
+ fileName: file.fileName,
21656
+ totalBytes: file.totalBytes,
21657
+ chunkCount: file.chunkCount,
21658
+ sha256: file.sha256,
21659
+ reservedBytes: file.totalBytes,
21660
+ persistedOffset: 0,
21661
+ phase: "open"
21662
+ };
21663
+ await store.openStagingPart(metadata);
21664
+ return peerResultFrame(payload.requestId, payload.messageId, "receiving", "prepared", {
21665
+ transferId: file.transferId,
21666
+ persistedOffset: 0
21667
+ });
21668
+ });
21669
+ }
21670
+ async function receiveFileChunk(sender, payload) {
21671
+ const data = decodeStrictBase64(payload.dataBase64);
21672
+ return store.runExclusive(async () => {
21673
+ const metadata = await store.readStagingMetadata(sender.daemonHostId, payload.transferId);
21674
+ if (!metadata) {
21675
+ throw new AppProtocolError({
21676
+ code: "not_found",
21677
+ message: "transfer staging record not found",
21678
+ stage: "inbound-file",
21679
+ transferId: payload.transferId
21680
+ });
21681
+ }
21682
+ if (metadata.messageId !== payload.messageId) {
21683
+ throw new AppProtocolError({
21684
+ code: "transfer_conflict",
21685
+ message: "transferId belongs to another message",
21686
+ stage: "inbound-file",
21687
+ transferId: payload.transferId
21688
+ });
21689
+ }
21690
+ const expectedOffset = payload.chunkIndex * APP_FILE_CHUNK_BYTES;
21691
+ const isLast = payload.chunkIndex === metadata.chunkCount - 1;
21692
+ const expectedLength = isLast ? metadata.totalBytes - expectedOffset : APP_FILE_CHUNK_BYTES;
21693
+ if (payload.offset !== expectedOffset || payload.chunkIndex < 0 || payload.chunkIndex >= metadata.chunkCount || data.length !== expectedLength) {
21694
+ throw new AppProtocolError({
21695
+ code: "invalid_chunk",
21696
+ message: `chunk ${payload.chunkIndex} offset/length does not match the transfer`,
21697
+ stage: "inbound-file",
21698
+ transferId: payload.transferId
21699
+ });
21700
+ }
21701
+ if (payload.offset < metadata.persistedOffset) {
21702
+ if (payload.offset + data.length > metadata.persistedOffset) {
21703
+ throw new AppProtocolError({ code: "chunk_conflict", message: "chunk overlaps persisted data", stage: "inbound-file", transferId: payload.transferId });
21704
+ }
21705
+ const existing = await store.readPartRange(sender.daemonHostId, payload.transferId, payload.offset, data.length);
21706
+ if (!existing.equals(data)) {
21707
+ throw new AppProtocolError({ code: "chunk_conflict", message: "duplicate chunk bytes differ", stage: "inbound-file", transferId: payload.transferId });
21708
+ }
21709
+ return peerResultFrame(payload.requestId, payload.messageId, "receiving", "prepared", {
21710
+ transferId: payload.transferId,
21711
+ persistedOffset: metadata.persistedOffset
21712
+ });
21713
+ }
21714
+ if (payload.offset > metadata.persistedOffset) {
21715
+ throw new AppProtocolError({ code: "invalid_chunk", message: "chunk offset is ahead of persisted offset", stage: "inbound-file", transferId: payload.transferId });
21716
+ }
21717
+ const nextOffset = await store.appendPart(sender.daemonHostId, payload.transferId, metadata.persistedOffset, data);
21718
+ const updated = { ...metadata, persistedOffset: nextOffset };
21719
+ await store.writeStagingMetadata(updated);
21720
+ return peerResultFrame(payload.requestId, payload.messageId, "receiving", "prepared", {
21721
+ transferId: payload.transferId,
21722
+ persistedOffset: nextOffset
21723
+ });
21724
+ });
21725
+ }
21726
+ async function receiveFileCommit(sender, payload) {
21727
+ return store.runExclusive(async () => {
21728
+ const existing = await store.readInboxRecord(sender.daemonHostId, payload.messageId);
21729
+ if (existing?.kind === "file" && existing.file?.transferId === payload.transferId) {
21730
+ return peerResultFrame(payload.requestId, payload.messageId, "received", "received", {
21731
+ transferId: payload.transferId,
21732
+ absolutePath: existing.file.absolutePath
21733
+ });
21734
+ }
21735
+ if (existing) {
21736
+ throw new AppProtocolError({
21737
+ code: "message_id_conflict",
21738
+ message: "messageId already exists for a different transfer",
21739
+ stage: "file-commit",
21740
+ messageId: payload.messageId,
21741
+ transferId: payload.transferId
21742
+ });
21743
+ }
21744
+ const metadata = await store.readStagingMetadata(sender.daemonHostId, payload.transferId);
21745
+ if (!metadata || metadata.messageId !== payload.messageId) {
21746
+ throw new AppProtocolError({ code: "not_found", message: "transfer staging record not found", stage: "file-commit", transferId: payload.transferId });
21747
+ }
21748
+ const partSize = await store.statPart(sender.daemonHostId, payload.transferId);
21749
+ if (partSize !== metadata.totalBytes || metadata.persistedOffset !== metadata.totalBytes) {
21750
+ throw new AppProtocolError({
21751
+ code: "transfer_incomplete",
21752
+ message: `transfer has ${partSize ?? 0}/${metadata.totalBytes} persisted bytes`,
21753
+ stage: "file-commit",
21754
+ transferId: payload.transferId
21755
+ });
21756
+ }
21757
+ const actualSha = await store.computeFileSha256(store.stagingPartPath(sender.daemonHostId, payload.transferId));
21758
+ if (actualSha !== metadata.sha256) {
21759
+ throw new AppProtocolError({
21760
+ code: "integrity_mismatch",
21761
+ message: "transfer SHA-256 does not match the declared digest",
21762
+ stage: "file-commit",
21763
+ transferId: payload.transferId
21764
+ });
21765
+ }
21766
+ const finalPath = store.finalFilePath(sender.daemonHostId, payload.messageId, metadata.fileName);
21767
+ const verified = { ...metadata, phase: "verified", absolutePath: finalPath };
21768
+ await store.writeStagingMetadata(verified);
21769
+ await store.renamePartToFinal(sender.daemonHostId, payload.messageId, metadata.fileName, payload.transferId);
21770
+ const record = {
21771
+ schemaVersion: 1,
21772
+ senderDeviceId: sender.deviceId,
21773
+ senderDaemonHostId: sender.daemonHostId,
21774
+ receiverDeviceId: options.deviceId,
21775
+ receiverDaemonHostId: options.daemonHostId,
21776
+ messageId: payload.messageId,
21777
+ kind: "file",
21778
+ subject: metadata.subject,
21779
+ file: {
21780
+ transferId: metadata.transferId,
21781
+ fileName: metadata.fileName,
21782
+ totalBytes: metadata.totalBytes,
21783
+ chunkCount: metadata.chunkCount,
21784
+ sha256: metadata.sha256,
21785
+ absolutePath: finalPath
21786
+ },
21787
+ state: "received",
21788
+ receivedAt: nowIso(now)
21789
+ };
21790
+ await store.admitInboxRecord(record, 0, {
21791
+ staging: { senderDaemonHostId: sender.daemonHostId, transferId: payload.transferId }
21792
+ });
21793
+ await store.removeStagingEntry(sender.daemonHostId, payload.transferId);
21794
+ publishRecord(record);
21795
+ return peerResultFrame(payload.requestId, payload.messageId, "received", "received", {
21796
+ transferId: payload.transferId,
21797
+ absolutePath: finalPath
21798
+ });
21799
+ });
21800
+ }
21801
+ async function resolveInboxRecord(messageId, senderDaemonHostId) {
21802
+ if (senderDaemonHostId) {
21803
+ const record = await store.readInboxRecord(senderDaemonHostId, messageId);
21804
+ if (!record) {
21805
+ throw new AppProtocolError({ code: "not_found", message: "message not found", stage: "status", messageId });
21806
+ }
21807
+ return record;
21808
+ }
21809
+ const matches = (await store.listInboxRecords()).filter((record) => record.messageId === messageId);
21810
+ if (matches.length === 1) return matches[0];
21811
+ if (matches.length === 0) {
21812
+ throw new AppProtocolError({ code: "not_found", message: "message not found", stage: "status", messageId });
21813
+ }
21814
+ throw new AppProtocolError({ code: "ambiguous_message_id", message: "messageId matches multiple senders", stage: "status", messageId });
21815
+ }
21816
+ async function settleLocalState(messageId, senderDaemonHostId, targetState) {
21817
+ return store.runExclusive(async () => {
21818
+ const record = await resolveInboxRecord(messageId, senderDaemonHostId);
21819
+ if (record.state === "rejected") {
21820
+ throw new AppProtocolError({ code: "invalid_state", message: "rejected messages cannot advance", stage: "ack", messageId });
21821
+ }
21822
+ if (targetState === "delivered") {
21823
+ if (record.state !== "received") return { changed: false, record };
21824
+ const next2 = { ...record, state: "delivered", deliveredAt: nowIso(now) };
21825
+ await store.writeInboxRecord(next2);
21826
+ return { changed: true, record: next2 };
21827
+ }
21828
+ if (record.state === "received") {
21829
+ throw new AppProtocolError({ code: "invalid_state", message: "consume requires delivered state", stage: "consume", messageId });
21830
+ }
21831
+ if (record.state === "consumed") return { changed: false, record };
21832
+ const next = { ...record, state: "consumed", consumedAt: nowIso(now) };
21833
+ await store.writeInboxRecord(next);
21834
+ return { changed: true, record: next };
21835
+ });
21836
+ }
21837
+ async function localStatus(link, requestId, messageId, senderDaemonHostId) {
21838
+ try {
21839
+ const record = await resolveInboxRecord(messageId, senderDaemonHostId);
21840
+ link.send({ type: "app-status-result", payload: recordStatusPayload(record, requestId) });
21841
+ return;
21842
+ } catch (error) {
21843
+ if (!(error instanceof AppProtocolError) || error.code !== "not_found") {
21844
+ sendError(link, error, requestId, messageId);
21845
+ return;
21846
+ }
21847
+ }
21848
+ const outbox = (await store.listOutboxRecords()).filter((record) => record.messageId === messageId);
21849
+ if (outbox.length === 1) {
21850
+ link.send({ type: "app-status-result", payload: outboxStatusPayload(outbox[0], requestId) });
21851
+ return;
21852
+ }
21853
+ if (outbox.length > 1) {
21854
+ link.send({
21855
+ type: "app-status-result",
21856
+ payload: { requestId, messageId, found: false, ambiguous: true }
21857
+ });
21858
+ return;
21859
+ }
21860
+ link.send({ type: "app-status-result", payload: { requestId, messageId, found: false } });
21861
+ }
21862
+ async function remoteStatus(link, requestId, messageId, target) {
21863
+ try {
21864
+ const peer = await connectPeer(target);
21865
+ const response = await peerRequest(peer, {
21866
+ type: "app-status",
21867
+ payload: { requestId, messageId }
21868
+ });
21869
+ link.send(response);
21870
+ } catch (error) {
21871
+ if (error instanceof PeerLinkError && error.code === "result_unknown") {
21872
+ sendError(link, error, requestId, messageId);
21873
+ return;
21874
+ }
21875
+ sendError(link, error, requestId, messageId);
21876
+ }
21877
+ }
21878
+ async function peerStatus(link, requestId, messageId, senderDaemonHostId) {
21879
+ if (senderDaemonHostId && senderDaemonHostId !== link.daemonHostId) {
21880
+ throw new AppProtocolError({ code: "not_found", message: "peer may only query its own sender scope", stage: "status", messageId });
21881
+ }
21882
+ const record = await store.readInboxRecord(link.daemonHostId, messageId);
21883
+ if (!record) {
21884
+ link.send({ type: "app-status-result", payload: { requestId, messageId, found: false } });
21885
+ return;
21886
+ }
21887
+ link.send({ type: "app-status-result", payload: recordStatusPayload(record, requestId) });
21888
+ }
21889
+ async function recoverOutbox() {
21890
+ await store.runExclusive(async () => {
21891
+ for (const record of await store.listOutboxRecords()) {
21892
+ if (record.state !== "pending" && record.state !== "sent") continue;
21893
+ await store.writeOutboxRecord({ ...record, state: "result_unknown", updatedAt: nowIso(now) });
21894
+ }
21895
+ });
21896
+ }
21897
+ return {
21898
+ async initialize() {
21899
+ if (initialized) return;
21900
+ try {
21901
+ await store.initialize();
21902
+ const recovered = await store.runExclusive(() => store.recoverStaging());
21903
+ await recoverOutbox();
21904
+ await store.runExclusive(() => store.cleanupRetention(now(), retentionMs));
21905
+ for (const record of recovered) publishRecord(record);
21906
+ retentionTimer = setInterval(() => {
21907
+ void store.runExclusive(() => store.cleanupRetention(now(), retentionMs)).catch(() => void 0);
21908
+ }, options.retentionIntervalMs ?? 60 * 60 * 1e3);
21909
+ retentionTimer.unref?.();
21910
+ initialized = true;
21911
+ initializationError = null;
21912
+ } catch (error) {
21913
+ initializationError = error instanceof Error ? error : new Error(String(error));
21914
+ }
21915
+ },
21916
+ dispose() {
21917
+ if (retentionTimer) {
21918
+ clearInterval(retentionTimer);
21919
+ retentionTimer = null;
21920
+ }
21921
+ for (const peer of [...peerLinks.values()]) {
21922
+ void closePeerLink(peer, "daemon dispose");
21923
+ }
21924
+ for (const [id, subscriber] of subscribers) {
21925
+ subscriber.close("daemon dispose", 1001);
21926
+ subscribers.delete(id);
21927
+ }
21928
+ activeFileSends.clear();
21929
+ initialized = false;
21930
+ initializationError = null;
21931
+ },
21932
+ getInitializationError() {
21933
+ return initializationError;
21934
+ },
21935
+ getCapabilities() {
21936
+ return APP_CAPABILITIES;
21937
+ },
21938
+ getIdentity() {
21939
+ return { deviceId: options.deviceId, daemonHostId: options.daemonHostId };
21940
+ },
21941
+ async subscribeLocal(link) {
21942
+ await store.runExclusive(async () => {
21943
+ subscribers.set(link.id, link);
21944
+ for (const record of await store.listInboxRecords()) {
21945
+ if (record.state !== "received") continue;
21946
+ const frame = record.kind === "file" ? inboundFileFrame(record) : inboundMessageFrame(record);
21947
+ if (frame && !link.send(frame)) {
21948
+ subscribers.delete(link.id);
21949
+ link.close("slow_consumer", 4008);
21950
+ break;
21951
+ }
21952
+ }
21953
+ });
21954
+ },
21955
+ revokeLink(linkId) {
21956
+ subscribers.delete(linkId);
21957
+ for (const [key, active] of [...activeFileSends]) {
21958
+ if (active.linkId === linkId) activeFileSends.delete(key);
21959
+ }
21960
+ },
21961
+ async handleFrame(link, frame) {
21962
+ const requestId = frameRequestId(frame);
21963
+ const messageId = "messageId" in frame.payload && typeof frame.payload.messageId === "string" ? frame.payload.messageId : void 0;
21964
+ const transferId = "transferId" in frame.payload && typeof frame.payload.transferId === "string" ? frame.payload.transferId : void 0;
21965
+ try {
21966
+ if (initializationError) {
21967
+ throw new AppProtocolError({
21968
+ code: "storage_error",
21969
+ message: `app messaging is unavailable: ${initializationError.message}`,
21970
+ stage: "admission",
21971
+ requestId
21972
+ });
21973
+ }
21974
+ if (frame.type === "app-ack" || frame.type === "app-consume") {
21975
+ if (link.role !== "local") {
21976
+ throw new AppProtocolError({ code: "forbidden_role", message: "peer connections cannot ack or consume", stage: "admission" });
21977
+ }
21978
+ const state = frame.type === "app-ack" ? "delivered" : "consumed";
21979
+ const settled = await settleLocalState(frame.payload.messageId, frame.payload.senderDaemonHostId, state);
21980
+ link.send({ type: "app-status-result", payload: recordStatusPayload(settled.record, frame.payload.requestId) });
21981
+ if (settled.changed) {
21982
+ return { settled: true, messageId: settled.record.messageId, senderDaemonHostId: settled.record.senderDaemonHostId };
21983
+ }
21984
+ return { settled: false };
21985
+ }
21986
+ switch (frame.type) {
21987
+ case "app-send-message":
21988
+ if (link.role === "local") {
21989
+ await sendMessageFromLocal(link, frame.payload);
21990
+ } else {
21991
+ const state = await receiveMessage(link, frame.payload);
21992
+ link.send(peerResultFrame(frame.payload.requestId, frame.payload.messageId, state === "rejected" ? "rejected" : "received", "received"));
21993
+ }
21994
+ return { settled: false };
21995
+ case "app-send-file-start":
21996
+ if (link.role === "local") {
21997
+ await sendFileStartFromLocal(link, frame.payload);
21998
+ } else {
21999
+ link.send(await receiveFileStart(link, frame.payload));
22000
+ }
22001
+ return { settled: false };
22002
+ case "app-send-file-chunk":
22003
+ if (link.role === "local") {
22004
+ await forwardFileChunk(link, frame.payload);
22005
+ } else {
22006
+ link.send(await receiveFileChunk(link, frame.payload));
22007
+ }
22008
+ return { settled: false };
22009
+ case "app-send-file-commit":
22010
+ if (link.role === "local") {
22011
+ await forwardFileCommit(link, frame.payload);
22012
+ } else {
22013
+ link.send(await receiveFileCommit(link, frame.payload));
22014
+ }
22015
+ return { settled: false };
22016
+ case "app-status":
22017
+ if (link.role === "peer") {
22018
+ await peerStatus(link, frame.payload.requestId, frame.payload.messageId, frame.payload.senderDaemonHostId);
22019
+ return { settled: false };
22020
+ }
22021
+ if (frame.payload.remote) {
22022
+ const target = validateAppTarget(frame.payload.target, "status");
22023
+ await remoteStatus(link, frame.payload.requestId, frame.payload.messageId, target);
22024
+ return { settled: false };
22025
+ }
22026
+ await localStatus(link, frame.payload.requestId, frame.payload.messageId, frame.payload.senderDaemonHostId);
22027
+ return { settled: false };
22028
+ case "app-ping":
22029
+ link.send({
22030
+ type: "app-pong",
22031
+ payload: {
22032
+ requestId: frame.payload.requestId,
22033
+ sentAt: frame.payload.sentAt,
22034
+ receivedAt: now()
22035
+ }
22036
+ });
22037
+ return { settled: false };
22038
+ default:
22039
+ throw new AppProtocolError({ code: "invalid_frame", message: `unsupported frame: ${frame.type}`, stage: "dispatch" });
22040
+ }
22041
+ } catch (error) {
22042
+ sendError(link, error, requestId, messageId, transferId);
22043
+ return { settled: false };
22044
+ }
22045
+ return { settled: false };
22046
+ }
22047
+ };
22048
+ }
22049
+
22050
+ // src/server/daemon-app-websocket-runtime.ts
22051
+ var import_node_crypto4 = require("node:crypto");
22052
+ function assertDaemonAppHelloAdmission(input) {
22053
+ if (!input.deviceId || !input.daemonHostId) {
22054
+ throw new AppProtocolError({
22055
+ code: "missing_identity",
22056
+ message: "daemon device identity is not configured; set zterm.android.relay.deviceId",
22057
+ stage: "admission",
22058
+ requestId: input.hello.requestId
22059
+ });
22060
+ }
22061
+ if (input.hello.role === "local") {
22062
+ if (!input.remoteLoopback) {
22063
+ throw new AppProtocolError({ code: "forbidden_role", message: "local role requires a loopback connection", stage: "admission", requestId: input.hello.requestId });
22064
+ }
22065
+ if (input.hello.daemonHostId !== input.daemonHostId || input.hello.deviceId !== input.deviceId) {
22066
+ throw new AppProtocolError({ code: "forbidden_role", message: "local role identity does not match this daemon", stage: "admission", requestId: input.hello.requestId });
22067
+ }
22068
+ }
22069
+ }
22070
+ var APP_PATH = "/app";
22071
+ var HANDSHAKE_TIMEOUT_MS = 15e3;
22072
+ var SLOW_CONSUMER_GRACE_MS = 2e4;
22073
+ function isLoopbackAddress(address) {
22074
+ if (!address) return false;
22075
+ return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
22076
+ }
22077
+ function bearerToken(request) {
22078
+ const authorization = request.headers.authorization?.trim() || "";
22079
+ return authorization.toLowerCase().startsWith("bearer ") ? authorization.slice(7).trim() : "";
22080
+ }
22081
+ function tokensEqual(left, right) {
22082
+ if (!left || !right) return false;
22083
+ const leftBytes = Buffer.from(left);
22084
+ const rightBytes = Buffer.from(right);
22085
+ return leftBytes.length === rightBytes.length && (0, import_node_crypto4.timingSafeEqual)(leftBytes, rightBytes);
22086
+ }
22087
+ function rejectUpgrade(socket, status, message) {
22088
+ socket.write(
22089
+ `HTTP/1.1 ${status} ${message}\r
22090
+ Connection: close\r
22091
+ Content-Length: 0\r
22092
+ \r
22093
+ `
22094
+ );
22095
+ socket.destroy();
22096
+ }
22097
+ function rawText(rawData) {
22098
+ if (typeof rawData === "string") return rawData;
22099
+ if (Buffer.isBuffer(rawData)) return rawData.toString("utf8");
22100
+ if (Array.isArray(rawData)) return Buffer.concat(rawData).toString("utf8");
22101
+ return Buffer.from(rawData).toString("utf8");
22102
+ }
22103
+ function createDaemonAppGateway(options) {
22104
+ const wss2 = new import_websocket_server.default({ noServer: true });
22105
+ const connections2 = /* @__PURE__ */ new Set();
22106
+ const log = options.log ?? (() => void 0);
22107
+ function sendRawError(ws, error, requestId) {
22108
+ if (ws.readyState !== wrapper_default.OPEN) return;
22109
+ ws.send(JSON.stringify({
22110
+ type: "app-error",
22111
+ payload: appErrorPayload({
22112
+ code: error.code,
22113
+ stage: error.stage,
22114
+ message: error.message,
22115
+ requestId: error.requestId ?? requestId
22116
+ })
22117
+ }));
22118
+ }
22119
+ function handleConnection(ws, request) {
22120
+ connections2.add(ws);
22121
+ let link = null;
22122
+ let helloSeen = false;
22123
+ let pendingBytes = 0;
22124
+ let overLimitSince = null;
22125
+ let lastAckProgressAt = null;
22126
+ let slowConsumerTimer = null;
22127
+ let slowConsumerClosed = false;
22128
+ const pendingMessages = /* @__PURE__ */ new Map();
22129
+ const pendingWrite = (frame, bytes) => {
22130
+ if (frame.type === "app-connection-state" || frame.type === "app-error" && frame.payload.requestId === void 0) {
22131
+ if (ws.readyState === wrapper_default.OPEN) {
22132
+ ws.send(JSON.stringify(frame));
22133
+ }
22134
+ return;
22135
+ }
22136
+ if (frame.type === "app-inbound-message" || frame.type === "app-inbound-file") {
22137
+ trackPendingInbound(frame, bytes);
22138
+ }
22139
+ const text = JSON.stringify(frame);
22140
+ ws.send(text, (error) => {
22141
+ if (error) {
22142
+ ws.close(1011, "send failed");
22143
+ }
22144
+ });
22145
+ };
22146
+ const slowConsumerGraceMs = options.slowConsumerGraceMs ?? SLOW_CONSUMER_GRACE_MS;
22147
+ const remoteLoopback = isLoopbackAddress(request.socket.remoteAddress);
22148
+ const handshakeTimer = setTimeout(() => {
22149
+ if (helloSeen || ws.readyState !== wrapper_default.OPEN) return;
22150
+ sendRawError(ws, new AppProtocolError({ code: "invalid_frame", message: "app handshake timed out", stage: "admission" }));
22151
+ ws.close(4003, "handshake timeout");
22152
+ }, HANDSHAKE_TIMEOUT_MS);
22153
+ handshakeTimer.unref?.();
22154
+ function clearSlowConsumerTimer() {
22155
+ if (slowConsumerTimer) {
22156
+ clearTimeout(slowConsumerTimer);
22157
+ slowConsumerTimer = null;
22158
+ }
22159
+ }
22160
+ function oldestPendingMessage() {
22161
+ let oldest = null;
22162
+ for (const pending of pendingMessages.values()) {
22163
+ if (!oldest || pending.enqueuedAt < oldest.enqueuedAt) oldest = pending;
22164
+ }
22165
+ return oldest;
22166
+ }
22167
+ function scheduleSlowConsumerCheck() {
22168
+ clearSlowConsumerTimer();
22169
+ if (slowConsumerClosed || ws.readyState !== wrapper_default.OPEN) return;
22170
+ if (pendingBytes <= APP_STREAM_OUTBOUND_QUEUE_BYTES) {
22171
+ overLimitSince = null;
22172
+ return;
22173
+ }
22174
+ const oldest = oldestPendingMessage();
22175
+ if (!oldest) {
22176
+ overLimitSince = null;
22177
+ return;
22178
+ }
22179
+ const currentTime = Date.now();
22180
+ if (overLimitSince === null) overLimitSince = currentTime;
22181
+ const eligibleAt = Math.max(
22182
+ oldest.enqueuedAt,
22183
+ overLimitSince,
22184
+ lastAckProgressAt ?? 0
22185
+ );
22186
+ const delayMs = Math.max(0, eligibleAt + slowConsumerGraceMs - currentTime);
22187
+ slowConsumerTimer = setTimeout(checkSlowConsumer, delayMs);
22188
+ slowConsumerTimer.unref?.();
22189
+ }
22190
+ function checkSlowConsumer() {
22191
+ slowConsumerTimer = null;
22192
+ if (slowConsumerClosed || ws.readyState !== wrapper_default.OPEN) return;
22193
+ if (pendingBytes <= APP_STREAM_OUTBOUND_QUEUE_BYTES) {
22194
+ overLimitSince = null;
22195
+ return;
22196
+ }
22197
+ const oldest = oldestPendingMessage();
22198
+ if (!oldest) {
22199
+ overLimitSince = null;
22200
+ return;
22201
+ }
22202
+ const currentTime = Date.now();
22203
+ if (overLimitSince === null) overLimitSince = currentTime;
22204
+ const eligibleAt = Math.max(
22205
+ oldest.enqueuedAt,
22206
+ overLimitSince,
22207
+ lastAckProgressAt ?? 0
22208
+ );
22209
+ if (currentTime - eligibleAt < slowConsumerGraceMs) {
22210
+ scheduleSlowConsumerCheck();
22211
+ return;
22212
+ }
22213
+ slowConsumerClosed = true;
22214
+ clearSlowConsumerTimer();
22215
+ sendRawError(ws, new AppProtocolError({
22216
+ code: "slow_consumer",
22217
+ stage: "publish",
22218
+ message: "local app stream has unacknowledged inbound data beyond the slow-consumer grace period"
22219
+ }));
22220
+ ws.close(4008, "slow_consumer");
22221
+ }
22222
+ function inboundPendingKey(frame) {
22223
+ if (frame.type === "app-inbound-message") {
22224
+ return `${frame.payload.senderDaemonHostId}\0${frame.payload.messageId}`;
22225
+ }
22226
+ if (frame.type === "app-inbound-file") {
22227
+ return `${frame.payload.senderDaemonHostId}\0${frame.payload.messageId}`;
22228
+ }
22229
+ return null;
22230
+ }
22231
+ function trackPendingInbound(frame, bytes) {
22232
+ if (!link || link.role !== "local") return;
22233
+ const key = inboundPendingKey(frame);
22234
+ if (!key) return;
22235
+ if (pendingMessages.has(key)) return;
22236
+ const currentTime = Date.now();
22237
+ pendingMessages.set(key, { bytes, enqueuedAt: currentTime });
22238
+ pendingBytes += bytes;
22239
+ if (pendingBytes > APP_STREAM_OUTBOUND_QUEUE_BYTES && overLimitSince === null) {
22240
+ overLimitSince = currentTime;
22241
+ }
22242
+ if (pendingBytes > APP_STREAM_OUTBOUND_QUEUE_BYTES) scheduleSlowConsumerCheck();
22243
+ }
22244
+ function acknowledgePending(senderDaemonHostId, messageId) {
22245
+ let key = null;
22246
+ if (senderDaemonHostId) {
22247
+ const candidate = `${senderDaemonHostId}\0${messageId}`;
22248
+ if (pendingMessages.has(candidate)) key = candidate;
22249
+ } else {
22250
+ const matches = [...pendingMessages.keys()].filter((entry) => entry.endsWith(`\0${messageId}`));
22251
+ if (matches.length === 1) key = matches[0];
22252
+ }
22253
+ if (key === null) return;
22254
+ const pending = pendingMessages.get(key);
22255
+ if (!pending) return;
22256
+ pendingMessages.delete(key);
22257
+ pendingBytes = Math.max(0, pendingBytes - pending.bytes);
22258
+ lastAckProgressAt = Date.now();
22259
+ if (pendingBytes <= APP_STREAM_OUTBOUND_QUEUE_BYTES) {
22260
+ overLimitSince = null;
22261
+ clearSlowConsumerTimer();
22262
+ return;
22263
+ }
22264
+ overLimitSince = lastAckProgressAt;
22265
+ scheduleSlowConsumerCheck();
22266
+ }
22267
+ function enqueue(frame) {
22268
+ if (ws.readyState !== wrapper_default.OPEN) return false;
22269
+ const text = JSON.stringify(frame);
22270
+ const bytes = Buffer.byteLength(text, "utf8");
22271
+ pendingWrite(frame, bytes);
22272
+ return true;
22273
+ }
22274
+ function closeLink(reason, code = 1e3) {
22275
+ if (ws.readyState === wrapper_default.OPEN || ws.readyState === wrapper_default.CONNECTING) {
22276
+ ws.close(code, reason);
22277
+ }
22278
+ }
22279
+ async function acceptHello(hello) {
22280
+ if (helloSeen) {
22281
+ throw new AppProtocolError({ code: "invalid_frame", message: "app-hello may only be sent once", stage: "admission", requestId: hello.requestId });
22282
+ }
22283
+ assertDaemonAppHelloAdmission({
22284
+ hello,
22285
+ deviceId: options.deviceId,
22286
+ daemonHostId: options.daemonHostId,
22287
+ remoteLoopback
22288
+ });
22289
+ const initError = options.messaging.getInitializationError();
22290
+ if (initError) {
22291
+ throw new AppProtocolError({
22292
+ code: "storage_error",
22293
+ message: `app messaging is unavailable: ${initError.message}`,
22294
+ stage: "admission",
22295
+ requestId: hello.requestId
22296
+ });
22297
+ }
22298
+ helloSeen = true;
22299
+ clearTimeout(handshakeTimer);
22300
+ const connectionId = `app-${(0, import_node_crypto4.randomUUID)()}`;
22301
+ link = {
22302
+ id: connectionId,
22303
+ role: hello.role,
22304
+ deviceId: hello.deviceId,
22305
+ daemonHostId: hello.daemonHostId,
22306
+ clientInstanceId: hello.clientInstanceId,
22307
+ send: enqueue,
22308
+ close: closeLink
22309
+ };
22310
+ const ready = {
22311
+ requestId: hello.requestId,
22312
+ protocolVersion: APP_PROTOCOL_VERSION,
22313
+ deviceId: options.deviceId,
22314
+ daemonHostId: options.daemonHostId,
22315
+ capabilities: APP_CAPABILITIES
22316
+ };
22317
+ ws.send(JSON.stringify({ type: "app-ready", payload: ready }));
22318
+ if (hello.role === "local") {
22319
+ await options.messaging.subscribeLocal(link);
22320
+ enqueue({
22321
+ type: "app-connection-state",
22322
+ payload: {
22323
+ requestId: `event-${(0, import_node_crypto4.randomUUID)()}`,
22324
+ state: "connected",
22325
+ daemonHostId: options.daemonHostId
22326
+ }
22327
+ });
22328
+ }
22329
+ }
22330
+ ws.on("message", (rawData) => {
22331
+ let parsed;
22332
+ try {
22333
+ parsed = JSON.parse(rawText(rawData));
22334
+ } catch {
22335
+ sendRawError(ws, new AppProtocolError({ code: "invalid_frame", message: "frame is not valid JSON", stage: "frame" }));
22336
+ ws.close(4003, "invalid frame");
22337
+ return;
22338
+ }
22339
+ let frame;
22340
+ try {
22341
+ frame = validateAppClientFrame(parsed);
22342
+ } catch (error) {
22343
+ if (error instanceof AppProtocolError) {
22344
+ sendRawError(ws, error);
22345
+ if (error.code === "invalid_frame" || error.code === "invalid_version") {
22346
+ ws.close(4003, error.code);
22347
+ }
22348
+ return;
22349
+ }
22350
+ sendRawError(ws, new AppProtocolError({ code: "invalid_frame", message: "frame validation failed", stage: "frame" }));
22351
+ ws.close(4003, "invalid frame");
22352
+ return;
22353
+ }
22354
+ if (frame.type === "app-hello") {
22355
+ void acceptHello(frame.payload).catch((error) => {
22356
+ if (error instanceof AppProtocolError) {
22357
+ sendRawError(ws, error, frame.payload.requestId);
22358
+ }
22359
+ ws.close(4003, "admission rejected");
22360
+ });
22361
+ return;
22362
+ }
22363
+ if (!link || !helloSeen) {
22364
+ sendRawError(ws, new AppProtocolError({ code: "invalid_frame", message: "app-hello must be the first frame", stage: "admission" }));
22365
+ ws.close(4003, "hello required");
22366
+ return;
22367
+ }
22368
+ void options.messaging.handleFrame(link, frame).then((result) => {
22369
+ if (result.settled && result.messageId) {
22370
+ acknowledgePending(result.senderDaemonHostId, result.messageId);
22371
+ }
22372
+ }).catch((error) => {
22373
+ log(`app frame handling failed: ${error instanceof Error ? error.message : String(error)}`);
22374
+ });
22375
+ });
22376
+ ws.on("close", () => {
22377
+ clearTimeout(handshakeTimer);
22378
+ clearSlowConsumerTimer();
22379
+ if (link) options.messaging.revokeLink(link.id);
22380
+ connections2.delete(ws);
22381
+ });
22382
+ ws.on("error", (error) => {
22383
+ clearTimeout(handshakeTimer);
22384
+ clearSlowConsumerTimer();
22385
+ if (link) options.messaging.revokeLink(link.id);
22386
+ connections2.delete(ws);
22387
+ log(`app websocket error: ${error.message}`);
22388
+ });
22389
+ }
22390
+ return {
22391
+ handleServerUpgrade(request, socket, head) {
22392
+ let pathname;
22393
+ try {
22394
+ pathname = new URL(request.url || "/", "ws://localhost").pathname;
22395
+ } catch {
22396
+ rejectUpgrade(socket, 404, "Not Found");
22397
+ return true;
22398
+ }
22399
+ if (pathname !== APP_PATH) return false;
22400
+ if (!options.requiredAuthToken) {
22401
+ rejectUpgrade(socket, 401, "Unauthorized");
22402
+ return true;
22403
+ }
22404
+ if (!tokensEqual(bearerToken(request), options.requiredAuthToken)) {
22405
+ rejectUpgrade(socket, 401, "Unauthorized");
22406
+ return true;
22407
+ }
22408
+ wss2.handleUpgrade(request, socket, head, (ws) => handleConnection(ws, request));
22409
+ return true;
22410
+ },
22411
+ dispose() {
22412
+ for (const ws of [...connections2]) {
22413
+ try {
22414
+ ws.close(1001, "daemon dispose");
22415
+ } catch {
22416
+ }
22417
+ }
22418
+ connections2.clear();
22419
+ wss2.close();
22420
+ }
22421
+ };
22422
+ }
22423
+
22424
+ // src/server/remote-screenshot-daemon.ts
22425
+ var import_node_child_process6 = require("node:child_process");
22426
+ var import_node_os2 = require("node:os");
22427
+ var import_node_path6 = require("node:path");
22428
+ async function captureRemoteScreenshotWithDaemon(options) {
22429
+ const gate = runPhase3Screenshot({
22430
+ execution_id: `remote-screenshot:${Date.now()}`,
22431
+ attempt_id: "1",
22432
+ inputs: {
22433
+ "arc.screenshot_request": {
22434
+ sessionId: options.sessionId || "daemon-screenshot",
22435
+ outputPath: options.outputPath
22436
+ },
22437
+ "arc.screenshot_permission": { allowScreenshot: true }
22438
+ }
22439
+ });
22440
+ if (!gate.ok) {
22441
+ throw new Error(`dagpipe screenshot gate rejected capture: ${gate.error}`);
22442
+ }
22443
+ return await new Promise((resolve4, reject) => {
22444
+ const daemonBinary = (process.env.ZTERM_DAEMON_NATIVE || "").trim() || (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".zterm", "bin", "zterm-daemon");
22445
+ const args = ["capture-screen", options.outputPath];
22446
+ const windowId = options.windowId?.trim();
22447
+ if (windowId) {
22448
+ args.push("--window-id", windowId);
22449
+ }
22450
+ if (options.rect) {
22451
+ args.push(
22452
+ "--rect",
22453
+ [
22454
+ Math.round(options.rect.x),
22455
+ Math.round(options.rect.y),
22456
+ Math.round(options.rect.width),
22457
+ Math.round(options.rect.height)
22458
+ ].join(",")
22459
+ );
22460
+ }
22461
+ (0, import_node_child_process6.execFile)(daemonBinary, args, {
22462
+ timeout: options.timeoutMs,
22463
+ windowsHide: true
22464
+ }, (error, stdout, stderr) => {
22465
+ if (error) {
22466
+ const details = [error.message, stderr, stdout].filter(Boolean).join("\n");
22467
+ reject(new Error(details || "daemon screenshot capture failed"));
22468
+ return;
22469
+ }
22470
+ resolve4({ outputPath: options.outputPath });
22471
+ });
22472
+ });
22473
+ }
22474
+
22475
+ // src/server/remote-window-stream-daemon.ts
22476
+ var import_wrtc2 = __toESM(require_lib2(), 1);
22477
+
22478
+ // src/server/remote-window-stream-daemon-helpers.ts
22479
+ function cloneRemoteWindowTargetCatalogResponse(response, requestId) {
22480
+ return {
22481
+ requestId,
22482
+ targets: response.targets.slice(),
22483
+ ...response.errors ? {
22484
+ errors: response.errors.map((error) => ({
22485
+ ...error,
22486
+ requestId
22487
+ }))
22488
+ } : {}
22489
+ };
22490
+ }
22491
+ function cloneRemoteWindowTargetCatalogResult(result, requestId) {
22492
+ if ("targets" in result) {
22493
+ return cloneRemoteWindowTargetCatalogResponse(result, requestId);
22494
+ }
22495
+ return {
22496
+ ...result,
22497
+ requestId
22498
+ };
22499
+ }
22500
+ function normalizeRtcDescription(description, expectedType) {
22501
+ if (!description || description.type !== expectedType || typeof description.sdp !== "string") {
22502
+ throw new Error(`remote window daemon expected ${expectedType} description`);
22503
+ }
22504
+ return {
22505
+ type: expectedType,
22506
+ sdp: description.sdp
22507
+ };
22508
+ }
22509
+ function normalizeIceCandidate(candidate) {
22510
+ const candidateLike = typeof candidate.toJSON === "function" ? candidate.toJSON() : candidate;
22511
+ return {
22512
+ candidate: String(candidateLike.candidate || ""),
22513
+ sdpMid: candidateLike.sdpMid ?? null,
22514
+ sdpMLineIndex: candidateLike.sdpMLineIndex ?? null,
22515
+ usernameFragment: candidateLike.usernameFragment ?? null
22516
+ };
22517
+ }
22518
+ function normalizeRemoteWindowVideoProfile(input) {
22519
+ if (!input) {
22520
+ return null;
22521
+ }
22522
+ if (input.preference !== "smooth" && input.preference !== "quality") {
22523
+ throw new Error(`remote window video preference is invalid: ${String(input.preference)}`);
22524
+ }
22525
+ const fields = [
22526
+ ["maxBitrateBps", input.maxBitrateBps, 5e5, 25e6],
22527
+ ["maxFrameRateFps", input.maxFrameRateFps, 1, 60],
22528
+ ["maxCaptureWidth", input.maxCaptureWidth, 320, 3840],
22529
+ ["maxCaptureHeight", input.maxCaptureHeight, 240, 2400],
22530
+ ["maxFrameAgeMs", input.maxFrameAgeMs, 40, 1e3],
22531
+ ["overviewMaxBitrateBps", input.overviewMaxBitrateBps, 0, 2e6],
22532
+ ["overviewMaxFrameRateFps", input.overviewMaxFrameRateFps, 0, 8]
22533
+ ];
22534
+ for (const [name, value, minimum, maximum] of fields) {
22535
+ if (!Number.isFinite(value) || value < minimum || value > maximum) {
22536
+ throw new Error(`remote window video profile ${name} is out of range`);
22537
+ }
22538
+ }
22539
+ if (input.overviewMaxBitrateBps >= input.maxBitrateBps) {
22540
+ throw new Error("remote window overview bitrate must remain below the total bitrate");
22541
+ }
22542
+ if (input.overviewMaxFrameRateFps > input.maxFrameRateFps) {
22543
+ throw new Error("remote window overview frame rate must not exceed focus frame rate");
22544
+ }
22545
+ return {
22546
+ preference: input.preference,
22547
+ maxBitrateBps: Math.floor(input.maxBitrateBps),
22548
+ maxFrameRateFps: Math.floor(input.maxFrameRateFps),
22549
+ maxCaptureWidth: Math.floor(input.maxCaptureWidth),
22550
+ maxCaptureHeight: Math.floor(input.maxCaptureHeight),
22551
+ maxFrameAgeMs: Math.floor(input.maxFrameAgeMs),
22552
+ interactionActive: input.interactionActive === true,
22553
+ overviewMaxBitrateBps: Math.floor(input.overviewMaxBitrateBps),
22554
+ overviewMaxFrameRateFps: Math.floor(input.overviewMaxFrameRateFps)
22555
+ };
22556
+ }
22557
+ function formatRemoteWindowVideoProfileError(error) {
22558
+ if (error instanceof Error) {
22559
+ return error.message || error.name || "remote window video bitrate could not be applied";
22560
+ }
22561
+ const message = String(error || "").trim();
22562
+ return message || "remote window video bitrate could not be applied";
22563
+ }
22564
+ function convertRgbaToI420Frame(frame, convert) {
22565
+ const width = Math.max(1, Math.floor(frame.width));
22566
+ const height = Math.max(1, Math.floor(frame.height));
22567
+ const chromaWidth = Math.ceil(width / 2);
22568
+ const chromaHeight = Math.ceil(height / 2);
22569
+ const i420 = {
22570
+ width,
22571
+ height,
22572
+ data: new Uint8Array(width * height + chromaWidth * chromaHeight * 2)
22573
+ };
22574
+ convert({
22575
+ width,
22576
+ height,
22577
+ data: frame.rgba
22578
+ }, i420);
22579
+ return i420;
22580
+ }
22581
+ var REMOTE_WINDOW_LATENCY_SAMPLE_CAPACITY = 240;
22582
+ function pushBounded(samples, value) {
22583
+ if (!Number.isFinite(value) || value < 0) {
22584
+ return;
22585
+ }
22586
+ samples.push(value);
22587
+ if (samples.length > REMOTE_WINDOW_LATENCY_SAMPLE_CAPACITY) {
22588
+ samples.splice(0, samples.length - REMOTE_WINDOW_LATENCY_SAMPLE_CAPACITY);
22589
+ }
22590
+ }
22591
+ function nearestRankPercentile(samples, percentile) {
22592
+ if (samples.length === 0) {
22593
+ return null;
22594
+ }
22595
+ const sorted = [...samples].sort((left, right) => left - right);
22596
+ const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(percentile * sorted.length) - 1));
22597
+ return sorted[index];
22598
+ }
22599
+ function readStatsNumber(entry, key) {
22600
+ const value = entry[key];
22601
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
22602
+ }
22603
+ function createRemoteWindowLatencyAccumulator() {
22604
+ const captureToEncodeSamples = [];
22605
+ const captureArrivalTimestamps = [];
22606
+ const dispatchTimestamps = [];
22607
+ const inputInjectionSamples = [];
22608
+ let previousOutbound = null;
22609
+ let encodeMsPerFrame = null;
22610
+ let packetSendDelayMs = null;
22611
+ let encodedFps = null;
22612
+ let sentBitrateBps = null;
22613
+ let codecMimeType = null;
22614
+ let networkRttMs = null;
22615
+ let networkJitterMs = null;
22616
+ let packetLossRatio = null;
20019
22617
  let captureWidth = null;
20020
22618
  let captureHeight = null;
20021
22619
  return {
@@ -20541,23 +23139,23 @@ function rectWithOffset(rect, offset) {
20541
23139
  var import_child_process3 = require("child_process");
20542
23140
 
20543
23141
  // src/server/macos-swift-script.ts
20544
- var import_node_crypto2 = require("node:crypto");
23142
+ var import_node_crypto5 = require("node:crypto");
20545
23143
  var import_node_child_process7 = require("node:child_process");
20546
23144
  var import_node_fs4 = require("node:fs");
20547
23145
  var import_node_os3 = require("node:os");
20548
- var import_node_path6 = require("node:path");
23146
+ var import_node_path7 = require("node:path");
20549
23147
  var import_node_util = require("node:util");
20550
23148
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process7.execFile);
20551
23149
  var SWIFT_SCRIPT_COMPILE_TIMEOUT_MS = 12e4;
20552
23150
  function cacheRoot() {
20553
- return process.env.ZTERM_SWIFT_SCRIPT_CACHE_DIR || (0, import_node_path6.join)((0, import_node_os3.homedir)(), ".zterm", "cache", "swift-scripts");
23151
+ return process.env.ZTERM_SWIFT_SCRIPT_CACHE_DIR || (0, import_node_path7.join)((0, import_node_os3.homedir)(), ".zterm", "cache", "swift-scripts");
20554
23152
  }
20555
23153
  function swiftScriptCacheKey(source, swiftVersion) {
20556
- return (0, import_node_crypto2.createHash)("sha256").update(`${swiftVersion}
23154
+ return (0, import_node_crypto5.createHash)("sha256").update(`${swiftVersion}
20557
23155
  ${source}`).digest("hex").slice(0, 32);
20558
23156
  }
20559
23157
  function swiftScriptCachePath(source, swiftVersion) {
20560
- return (0, import_node_path6.join)(cacheRoot(), `zterm-swift-${swiftScriptCacheKey(source, swiftVersion)}`);
23158
+ return (0, import_node_path7.join)(cacheRoot(), `zterm-swift-${swiftScriptCacheKey(source, swiftVersion)}`);
20561
23159
  }
20562
23160
  function resolveCachedSwiftScript(source, swiftVersion) {
20563
23161
  const binaryPath = swiftScriptCachePath(source, swiftVersion);
@@ -20575,11 +23173,11 @@ async function prepareSwiftScript(options) {
20575
23173
  if (cached) {
20576
23174
  return cached;
20577
23175
  }
20578
- const stagingDir = (0, import_node_fs4.mkdtempSync)((0, import_node_path6.join)((0, import_node_os3.tmpdir)(), "zterm-swift-"));
23176
+ const stagingDir = (0, import_node_fs4.mkdtempSync)((0, import_node_path7.join)((0, import_node_os3.tmpdir)(), "zterm-swift-"));
20579
23177
  try {
20580
23178
  (0, import_node_fs4.mkdirSync)(cacheRoot(), { recursive: true });
20581
- const sourcePath = (0, import_node_path6.join)(stagingDir, "script.swift");
20582
- const binaryPath = (0, import_node_path6.join)(stagingDir, "script");
23179
+ const sourcePath = (0, import_node_path7.join)(stagingDir, "script.swift");
23180
+ const binaryPath = (0, import_node_path7.join)(stagingDir, "script");
20583
23181
  (0, import_node_fs4.writeFileSync)(sourcePath, options.source);
20584
23182
  await execFileAsync(`${options.swiftBinary}c`, ["-swift-version", swiftVersion, "-O", "-o", binaryPath, sourcePath], {
20585
23183
  timeout: options.timeoutMs || SWIFT_SCRIPT_COMPILE_TIMEOUT_MS
@@ -27115,6 +29713,48 @@ relayHostClient = createTraversalRelayHostClient({
27115
29713
  }),
27116
29714
  listTerminalSessionCatalog
27117
29715
  });
29716
+ async function startAppMessagingGateway() {
29717
+ const appMessagingRuntime = createDaemonAppMessagingRuntime({
29718
+ appRootDir: WTERM_HOME_DIR,
29719
+ deviceId: DAEMON_CONFIG.deviceId,
29720
+ daemonHostId: DAEMON_CONFIG.daemonHostId
29721
+ });
29722
+ await appMessagingRuntime.initialize().catch((error) => {
29723
+ console.error(
29724
+ `[${logTimePrefix()}] app messaging initialization failed: ${error instanceof Error ? error.message : String(error)}`
29725
+ );
29726
+ });
29727
+ let appGateway = null;
29728
+ try {
29729
+ appGateway = createDaemonAppGateway({
29730
+ requiredAuthToken: REQUIRED_AUTH_TOKEN,
29731
+ daemonHostId: DAEMON_CONFIG.daemonHostId,
29732
+ deviceId: DAEMON_CONFIG.deviceId,
29733
+ messaging: appMessagingRuntime,
29734
+ log: (message) => console.warn(`[${logTimePrefix()}] ${message}`)
29735
+ });
29736
+ } catch (error) {
29737
+ console.error(
29738
+ `[${logTimePrefix()}] app messaging gateway unavailable: ${error instanceof Error ? error.message : String(error)}`
29739
+ );
29740
+ }
29741
+ server.on("upgrade", (request, socket, head) => {
29742
+ if (appGateway?.handleServerUpgrade(request, socket, head)) return;
29743
+ handleServerUpgrade(request, socket, head);
29744
+ });
29745
+ server.on("close", () => {
29746
+ appGateway?.dispose();
29747
+ appMessagingRuntime.dispose();
29748
+ });
29749
+ server.listen(PORT, HOST, () => {
29750
+ handleDaemonServerListening();
29751
+ });
29752
+ }
29753
+ void startAppMessagingGateway().catch((error) => {
29754
+ console.error(
29755
+ `[${logTimePrefix()}] app messaging gateway startup failed: ${error instanceof Error ? error.message : String(error)}`
29756
+ );
29757
+ });
27118
29758
  wss.on("connection", handleWebSocketConnection);
27119
29759
  startHeartbeatLoop();
27120
29760
  startMemoryGuardLoop();
@@ -27133,10 +29773,6 @@ wss.on("close", () => {
27133
29773
  server.on("error", (error) => {
27134
29774
  handleDaemonServerError(error);
27135
29775
  });
27136
- server.on("upgrade", handleServerUpgrade);
27137
- server.listen(PORT, HOST, () => {
27138
- handleDaemonServerListening();
27139
- });
27140
29776
  process.on("SIGINT", () => shutdownDaemon("SIGINT", 0));
27141
29777
  process.on("SIGTERM", () => shutdownDaemon("SIGTERM", 0));
27142
29778
  process.on("SIGHUP", () => shutdownDaemon("SIGHUP", 0));