@jsonstudio/zterm-daemon-win32-x64 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.
- package/package.json +1 -1
- package/runtime/dagpipe.node +0 -0
- package/runtime/server.cjs +2845 -209
- package/zterm-package.json +1 -1
package/runtime/server.cjs
CHANGED
|
@@ -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:
|
|
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 =
|
|
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
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
|
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-
|
|
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
|
-
|
|
10583
|
-
|
|
10584
|
-
|
|
10585
|
-
|
|
10586
|
-
|
|
10587
|
-
|
|
10588
|
-
|
|
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
|
-
|
|
10591
|
-
|
|
10592
|
-
|
|
10613
|
+
cancelActiveDownloads.delete(stop);
|
|
10614
|
+
}
|
|
10615
|
+
function fail(error) {
|
|
10616
|
+
stop();
|
|
10593
10617
|
deps.sendMessage(session, {
|
|
10594
|
-
type: "file-download-
|
|
10618
|
+
type: "file-download-error",
|
|
10595
10619
|
payload: {
|
|
10596
10620
|
requestId,
|
|
10597
|
-
|
|
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(
|
|
10691
|
+
setImmediate(pump);
|
|
10605
10692
|
}
|
|
10606
|
-
|
|
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/
|
|
19827
|
-
var
|
|
19828
|
-
|
|
19829
|
-
|
|
19830
|
-
|
|
19831
|
-
|
|
19832
|
-
|
|
19833
|
-
|
|
19834
|
-
|
|
19835
|
-
|
|
19836
|
-
|
|
19837
|
-
|
|
19838
|
-
|
|
19839
|
-
|
|
19840
|
-
|
|
19841
|
-
|
|
19842
|
-
|
|
19843
|
-
|
|
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
|
-
|
|
19846
|
-
|
|
19847
|
-
|
|
19848
|
-
|
|
19849
|
-
|
|
19850
|
-
|
|
19851
|
-
|
|
19852
|
-
|
|
19853
|
-
|
|
19854
|
-
|
|
19855
|
-
|
|
19856
|
-
|
|
19857
|
-
|
|
19858
|
-
|
|
19859
|
-
|
|
19860
|
-
|
|
19861
|
-
|
|
19862
|
-
|
|
19863
|
-
|
|
19864
|
-
|
|
19865
|
-
|
|
19866
|
-
|
|
19867
|
-
|
|
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
|
|
19894
|
-
if ("
|
|
19895
|
-
|
|
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
|
|
19903
|
-
if (
|
|
19904
|
-
throw new
|
|
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
|
|
19912
|
-
|
|
19913
|
-
|
|
19914
|
-
|
|
19915
|
-
|
|
19916
|
-
|
|
19917
|
-
|
|
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
|
|
19921
|
-
if (!
|
|
19922
|
-
|
|
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
|
-
|
|
19925
|
-
|
|
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
|
-
|
|
19928
|
-
|
|
19929
|
-
|
|
19930
|
-
|
|
19931
|
-
|
|
19932
|
-
|
|
19933
|
-
|
|
19934
|
-
|
|
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
|
-
|
|
19942
|
-
|
|
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
|
-
|
|
19945
|
-
|
|
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
|
-
|
|
19949
|
-
|
|
19950
|
-
|
|
19951
|
-
|
|
19952
|
-
|
|
19953
|
-
|
|
19954
|
-
|
|
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
|
|
19960
|
-
if (
|
|
19961
|
-
|
|
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
|
-
|
|
19964
|
-
|
|
19965
|
-
|
|
19966
|
-
|
|
19967
|
-
|
|
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
|
-
|
|
19984
|
-
|
|
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
|
|
19994
|
-
if (
|
|
19995
|
-
|
|
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
|
|
19998
|
-
|
|
19999
|
-
|
|
20000
|
-
}
|
|
20001
|
-
|
|
20002
|
-
|
|
20003
|
-
|
|
20004
|
-
|
|
20005
|
-
|
|
20006
|
-
|
|
20007
|
-
|
|
20008
|
-
|
|
20009
|
-
|
|
20010
|
-
|
|
20011
|
-
|
|
20012
|
-
|
|
20013
|
-
|
|
20014
|
-
|
|
20015
|
-
|
|
20016
|
-
|
|
20017
|
-
|
|
20018
|
-
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
20582
|
-
const binaryPath = (0,
|
|
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));
|