@platless/axom-cli 0.5.9 → 0.6.36
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/README.md +18 -5
- package/dist/axom-patch/cli/README.md +100 -12
- package/dist/axom-patch/cli/account-limits/generate.py +15 -12
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +20 -3
- package/dist/axom-patch/cli/claude-account-limits.json +9 -9
- package/dist/axom-patch/cli/codex-account-limits.json +11 -11
- package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
- package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
- package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
- package/dist/axom-patch/cli/nightly-task.md +55 -13
- package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
- package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
- package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1591 -0
- package/dist/axom-patch/cli/qoder/releases/1.1.75.json +360 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
- package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
- package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
- package/dist/axom-patch/cli/qoder-steering.md +20 -25
- package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
- package/dist/axom-patch/cli/runtime.json +16 -18
- package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
- package/dist/axom-patch/cli/zcode-models.json +1 -2
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +9 -10
- package/dist/cli.js +4 -36
- package/dist/commands/knowledge.js +47 -0
- package/dist/commands/update.js +65 -3
- package/dist/commands/workstation.js +73 -7
- package/dist/install/claude/commands.js +13 -13
- package/dist/mcp.js +1 -1
- package/dist/native/darwin-arm64/axom-quic +0 -0
- package/dist/workstation/acp/acpAgents.js +102 -98
- package/dist/workstation/acp/acpClient.js +26 -0
- package/dist/workstation/acp/builds/install/npm.js +99 -10
- package/dist/workstation/acp/initializeResponse.js +6 -1
- package/dist/workstation/acp/managedPatches.js +189 -25
- package/dist/workstation/acp/managedRuntime.js +314 -0
- package/dist/workstation/acp/patchAssets.js +40 -0
- package/dist/workstation/acp/processor/checkpoint.js +25 -34
- package/dist/workstation/acp/processor/control.js +5 -78
- package/dist/workstation/acp/processor/runtime.js +5 -81
- package/dist/workstation/acp/processor/session.js +51 -80
- package/dist/workstation/acp/processor/state.js +6 -13
- package/dist/workstation/acp/processor/step.js +3 -20
- package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
- package/dist/workstation/acp/processor/updates.js +131 -105
- package/dist/workstation/acp/processorSession.js +8 -172
- package/dist/workstation/acp/registry/catalog.js +65 -39
- package/dist/workstation/acp/registry/launch.js +16 -1
- package/dist/workstation/acp/switching.js +0 -3
- package/dist/workstation/acp/test/fakeAgent.js +13 -0
- package/dist/workstation/acp/test/keeperWorld.js +28 -0
- package/dist/workstation/acp/test/raw/sources.js +16 -130
- package/dist/workstation/acp/test/realAdapter.js +1 -47
- package/dist/workstation/acp/test/scriptAgent.js +11 -3
- package/dist/workstation/agentFlow.js +1 -1
- package/dist/workstation/agentSessions.js +38 -143
- package/dist/workstation/channels/management.js +95 -11
- package/dist/workstation/channels/url.js +1 -1
- package/dist/workstation/config.js +16 -4
- package/dist/workstation/connection/encryptedChannel.js +4 -1
- package/dist/workstation/control/bindingStore.js +200 -0
- package/dist/workstation/control/commands.js +124 -0
- package/dist/workstation/control/daemonIdentity.js +53 -0
- package/dist/workstation/control/enrollment.js +106 -0
- package/dist/workstation/control/inspect.js +71 -0
- package/dist/workstation/control/locks.js +63 -0
- package/dist/workstation/control/protocol.js +40 -0
- package/dist/workstation/control/restartGuard.js +46 -0
- package/dist/workstation/control/runtimeClient.js +97 -0
- package/dist/workstation/control/runtimeHost.js +148 -0
- package/dist/workstation/control/serverIdentity.js +35 -0
- package/dist/workstation/daemon.js +106 -6
- package/dist/workstation/daemonctl.js +77 -14
- package/dist/workstation/desktopControl/audio.js +99 -0
- package/dist/workstation/desktopControl/capabilities.js +92 -0
- package/dist/workstation/desktopControl/captureModes.js +9 -0
- package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
- package/dist/workstation/desktopControl/deviceSockets.js +246 -0
- package/dist/workstation/desktopControl/helperProcess.js +353 -0
- package/dist/workstation/desktopControl/lobby.js +129 -0
- package/dist/workstation/desktopControl/macProvider.js +155 -0
- package/dist/workstation/desktopControl/media.js +331 -0
- package/dist/workstation/desktopControl/nativeProvider.js +1018 -0
- package/dist/workstation/desktopControl/networkEvidence.js +43 -0
- package/dist/workstation/desktopControl/overview.js +58 -0
- package/dist/workstation/desktopControl/protocol.js +302 -0
- package/dist/workstation/desktopControl/quicMedia.js +240 -0
- package/dist/workstation/desktopControl/rateController.js +226 -0
- package/dist/workstation/desktopControl/supervisor.js +1681 -0
- package/dist/workstation/desktopControl/transport.js +27 -0
- package/dist/workstation/desktopControl/wallpaper.js +78 -0
- package/dist/workstation/files/credentials.js +142 -0
- package/dist/workstation/files/text.js +42 -8
- package/dist/workstation/keepers/kit/bundle.js +21 -17
- package/dist/workstation/keepers/kit/launcher.js +47 -7
- package/dist/workstation/keepers/kit/paths.js +2 -1
- package/dist/workstation/managedInstall.js +57 -0
- package/dist/workstation/protocol/agent.js +2 -15
- package/dist/workstation/protocol/management.js +93 -4
- package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
- package/dist/workstation/rd/artifact.js +119 -0
- package/dist/workstation/rd/control.js +119 -0
- package/dist/workstation/rd/display.js +29 -0
- package/dist/workstation/rd/provider.js +222 -0
- package/dist/workstation/rd/sessions.js +391 -0
- package/dist/workstation/rd/state.js +45 -0
- package/dist/workstation/rd/update.js +171 -0
- package/dist/workstation/rd/upkeep.js +38 -0
- package/dist/workstation/register.js +27 -37
- package/dist/workstation/service/installer.js +104 -13
- package/dist/workstation/service/launchd.js +1 -0
- package/dist/workstation/storageMetrics.js +118 -0
- package/dist/workstation/systemMetrics.js +44 -92
- package/dist/workstation/toolchain.js +94 -0
- package/dist/workstation/tools/browser.js +28 -4
- package/dist/workstation/update/artifact.js +2 -1
- package/dist/workstation/update/globalInstall.js +116 -0
- package/dist/workstation/update/npmInstaller.js +49 -23
- package/dist/workstation/update/rdRollback.js +145 -0
- package/dist/workstation/update/runUpdate.js +3 -2
- package/dist/workstation/update/watchdog.js +16790 -334
- package/package.json +5 -5
- package/skills/axom/SKILL.md +11 -11
- package/skills/axom-definition/SKILL.md +3 -3
- package/skills/axom-platform/SKILL.md +3 -3
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
- package/dist/axom-patch/cli/qoder-steering.json +0 -52
- package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
- package/dist/commands/guide.js +0 -38
- package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
- package/dist/runtime-deps/qoder-sdk/package.json +0 -8
- package/dist/workstation/acp/delegationMcp.js +0 -24
- package/dist/workstation/acp/processor/sideChannel.js +0 -80
- package/dist/workstation/acp/qoderStartup.js +0 -45
- package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
- package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
- package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
- package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
- package/dist/workstation/qoderAdapter/contract.js +0 -78
- package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
- package/dist/workstation/qoderAdapter/history.js +0 -85
- package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
- package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
- package/dist/workstation/qoderAdapter/main.js +0 -95
- package/dist/workstation/qoderAdapter/mapper.js +0 -117
- package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
- package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
- package/dist/workstation/qoderAdapter/permissions.js +0 -61
- package/dist/workstation/qoderAdapter/profile.js +0 -93
- package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
- package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
- package/dist/workstation/qoderAdapter/session.js +0 -319
- package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
- package/dist/workstation/qoderAdapter/subwork.js +0 -85
- package/dist/workstation/qoderAdapter/taskControl.js +0 -38
- package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
- package/dist/workstation/qoderAdapter/usage.js +0 -37
- /package/dist/workstation/{qoderAdapter/types.js → desktopControl/provider.js} +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const counter = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
|
|
3
|
+
const pathSchema = z.strictObject({
|
|
4
|
+
sequence: counter.min(1), pathGeneration: counter.min(1),
|
|
5
|
+
rttUs: counter, minRttUs: counter, bandwidthBps: counter.nullable(),
|
|
6
|
+
cwndBytes: counter, sentPackets: counter, lostPackets: counter,
|
|
7
|
+
congestionEvents: counter, udpTxBytes: counter,
|
|
8
|
+
});
|
|
9
|
+
export function decodeQuicPathStats(bytes, observedAtMs) {
|
|
10
|
+
if (bytes.length > 4096 || !Number.isFinite(observedAtMs) || observedAtMs < 0)
|
|
11
|
+
throw new Error('invalid path stats');
|
|
12
|
+
const stats = pathSchema.parse(JSON.parse(bytes.toString('utf8')));
|
|
13
|
+
if (stats.lostPackets > stats.sentPackets)
|
|
14
|
+
throw new Error('invalid path counters');
|
|
15
|
+
return { ...stats, observedAtMs };
|
|
16
|
+
}
|
|
17
|
+
export class DesktopNetworkEvidence {
|
|
18
|
+
previous;
|
|
19
|
+
state = 'unknown';
|
|
20
|
+
queueMs = 0;
|
|
21
|
+
sample(nowMs, stats, demandBps) {
|
|
22
|
+
if (!stats || stats.observedAtMs > nowMs || nowMs - stats.observedAtMs > Math.max(1500, stats.rttUs / 1000 * 3)) {
|
|
23
|
+
return { state: 'unknown', fresh: false, queueMs: 0 };
|
|
24
|
+
}
|
|
25
|
+
const previous = this.previous;
|
|
26
|
+
const samePath = previous?.id === stats.id && previous.pathGeneration === stats.pathGeneration;
|
|
27
|
+
if (samePath && stats.sequence <= previous.sequence) {
|
|
28
|
+
return { state: this.state, fresh: false, queueMs: this.queueMs, bandwidthBps: previous.bandwidthBps ?? undefined };
|
|
29
|
+
}
|
|
30
|
+
this.previous = stats;
|
|
31
|
+
this.queueMs = Math.max(0, (stats.rttUs - stats.minRttUs) / 1000);
|
|
32
|
+
const queueThresholdMs = Math.max(10, stats.minRttUs / 1000 * 0.1);
|
|
33
|
+
const countersAdvance = samePath && stats.sentPackets >= previous.sentPackets &&
|
|
34
|
+
stats.lostPackets >= previous.lostPackets && stats.congestionEvents >= previous.congestionEvents &&
|
|
35
|
+
stats.udpTxBytes > previous.udpTxBytes;
|
|
36
|
+
const congestion = countersAdvance && stats.congestionEvents > previous.congestionEvents;
|
|
37
|
+
const bandwidth = stats.bandwidthBps ?? undefined;
|
|
38
|
+
const loaded = countersAdvance && bandwidth !== undefined && bandwidth > 0 && demandBps >= bandwidth * 0.85;
|
|
39
|
+
this.state = this.queueMs > queueThresholdMs && (congestion || loaded) ? 'congested'
|
|
40
|
+
: this.queueMs <= queueThresholdMs ? 'usable' : 'unknown';
|
|
41
|
+
return { state: this.state, fresh: true, queueMs: this.queueMs, bandwidthBps: bandwidth };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mediaRequire } from './media.js';
|
|
3
|
+
/** A single initial image, then <=2% of focus target and <=32kbps, including wire headers. */
|
|
4
|
+
export class DesktopOverviewBudget {
|
|
5
|
+
tokens = 32808;
|
|
6
|
+
at = 0;
|
|
7
|
+
lastSent = -Infinity;
|
|
8
|
+
digest;
|
|
9
|
+
initialized = false;
|
|
10
|
+
canCapture(now, targetBps, pressure) {
|
|
11
|
+
this.refill(now, targetBps);
|
|
12
|
+
return !pressure && now - this.lastSent >= 2000 && this.tokens >= 1024;
|
|
13
|
+
}
|
|
14
|
+
get maxSide() { return this.tokens < 8192 ? 320 : 640; }
|
|
15
|
+
eligible(bytes, now, targetBps) {
|
|
16
|
+
this.refill(now, targetBps);
|
|
17
|
+
const digest = createHash('sha256').update(bytes.subarray(16, 28)).update(bytes.subarray(40)).digest('hex');
|
|
18
|
+
if (digest === this.digest || bytes.length > this.tokens || now - this.lastSent < 2000)
|
|
19
|
+
return false;
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
admit(bytes, now, targetBps) {
|
|
23
|
+
if (!this.eligible(bytes, now, targetBps))
|
|
24
|
+
return false;
|
|
25
|
+
this.tokens -= bytes.length;
|
|
26
|
+
this.lastSent = now;
|
|
27
|
+
this.digest = createHash('sha256').update(bytes.subarray(16, 28)).update(bytes.subarray(40)).digest('hex');
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
disconnected() { this.digest = undefined; }
|
|
31
|
+
refill(now, targetBps) {
|
|
32
|
+
mediaRequire(Number.isFinite(now) && Number.isFinite(targetBps) && targetBps >= 0);
|
|
33
|
+
if (!this.initialized) {
|
|
34
|
+
this.at = now;
|
|
35
|
+
this.initialized = true;
|
|
36
|
+
}
|
|
37
|
+
this.tokens = Math.min(32808, this.tokens + Math.max(0, now - this.at) / 1000 * Math.min(4000, targetBps / 400));
|
|
38
|
+
this.at = now;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function overviewPacket(image, sequence, generation) {
|
|
42
|
+
const { width: w, height: h, sourceWidth: sw, sourceHeight: sh, jpeg } = image;
|
|
43
|
+
mediaRequire([w, h, sw, sh, sequence, generation].every(Number.isSafeInteger) && sequence > 0 && generation > 0 &&
|
|
44
|
+
w >= 2 && h >= 2 && w <= 640 && h <= 640 && sw >= w && sh >= h && sw <= 16384 && sh <= 16384 &&
|
|
45
|
+
Math.abs(w / h - sw / sh) <= 2 / h && jpeg.length >= 4 && jpeg.length <= 32768 && jpeg.readUInt16BE(0) === 0xffd8 && jpeg.readUInt16BE(jpeg.length - 2) === 0xffd9);
|
|
46
|
+
const bytes = Buffer.alloc(40 + jpeg.length);
|
|
47
|
+
bytes.write('AXOV');
|
|
48
|
+
bytes[4] = 1;
|
|
49
|
+
bytes.writeBigUInt64BE(BigInt(sequence), 8);
|
|
50
|
+
bytes.writeUInt32BE(sw, 16);
|
|
51
|
+
bytes.writeUInt32BE(sh, 20);
|
|
52
|
+
bytes.writeUInt16BE(w, 24);
|
|
53
|
+
bytes.writeUInt16BE(h, 26);
|
|
54
|
+
bytes.writeUInt32BE(jpeg.length, 28);
|
|
55
|
+
bytes.writeBigUInt64BE(BigInt(generation), 32);
|
|
56
|
+
jpeg.copy(bytes, 40);
|
|
57
|
+
return bytes;
|
|
58
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { audioConfiguration, audioMode } from './audio.js';
|
|
3
|
+
import { lobbyRequestSchema, lobbyPlanSchema } from './lobby.js';
|
|
4
|
+
export const DESKTOP_MAX_JSON_BYTES = 48 * 1024;
|
|
5
|
+
export class DesktopProtocolError extends Error {
|
|
6
|
+
code = 'invalid_request';
|
|
7
|
+
}
|
|
8
|
+
const invalid = () => { throw new DesktopProtocolError('Invalid desktop protocol value'); };
|
|
9
|
+
const int = (min = 0, max = Number.MAX_SAFE_INTEGER) => z.number().int().min(min).max(max);
|
|
10
|
+
const positive = int(1);
|
|
11
|
+
const u32 = int(1, 0xffffffff);
|
|
12
|
+
const id = z.string().regex(/^[0-9a-f]{32}$/);
|
|
13
|
+
const bounded = z.string().min(1).refine(s => Buffer.byteLength(s) <= 128 && !/[\x00-\x1f\x7f]/.test(s));
|
|
14
|
+
const code = z.string().regex(/^[a-z0-9_]{1,128}$/);
|
|
15
|
+
const sourceAxis = int(2, 4096);
|
|
16
|
+
const axis = sourceAxis.refine(n => n % 2 === 0);
|
|
17
|
+
const fps = z.union([z.literal(15), z.literal(30), z.literal(60)]);
|
|
18
|
+
const dpi = int(64, 384);
|
|
19
|
+
const format = z.literal('h264-420-v1');
|
|
20
|
+
const quality = z.enum(['auto', 'text', 'motion']);
|
|
21
|
+
const distinct = (items) => new Set(items.map(i => canonicalDesktopJson(i))).size === items.length;
|
|
22
|
+
const formats = z.array(format).min(1).max(1).refine(distinct);
|
|
23
|
+
const protocols = z.strictObject({ min: int(1, 2147483647), max: int(1, 2147483647) }).refine(p => p.min <= p.max);
|
|
24
|
+
const mode = z.strictObject({ width: axis, height: axis });
|
|
25
|
+
const resizeRange = z.strictObject({ minWidth: axis, minHeight: axis, maxWidth: axis, maxHeight: axis })
|
|
26
|
+
.refine(r => r.minWidth <= r.maxWidth && r.minHeight <= r.maxHeight);
|
|
27
|
+
const display = z.strictObject({ id, label: bounded, width: sourceAxis, height: sourceAxis, revision: u32,
|
|
28
|
+
resize: z.enum(['none', 'modes', 'dynamic']), modes: z.array(mode).max(32).refine(distinct),
|
|
29
|
+
kind: z.enum(['display', 'window']).optional(),
|
|
30
|
+
resizeRange: resizeRange.nullable().optional(), captureViewport: z.boolean().optional(), overview: z.boolean().optional(), capturePixelScale: z.number().min(1).max(16).optional(),
|
|
31
|
+
dpi: dpi.optional(), maxDpi: dpi.optional(), appearanceSync: z.boolean().optional(),
|
|
32
|
+
}).refine(d => (d.capturePixelScale == null || d.captureViewport === true) && (d.resize !== 'modes' || d.modes.length > 0) &&
|
|
33
|
+
(d.dpi == null) === (d.maxDpi == null) && (d.dpi == null || d.dpi <= d.maxDpi) &&
|
|
34
|
+
(d.resize === 'dynamic') === (d.resizeRange != null) &&
|
|
35
|
+
(d.resizeRange == null || d.modes.every(m => m.width >= d.resizeRange.minWidth && m.width <= d.resizeRange.maxWidth &&
|
|
36
|
+
m.height >= d.resizeRange.minHeight && m.height <= d.resizeRange.maxHeight)));
|
|
37
|
+
const encoderFormat = z.strictObject({ format, maxWidth: axis, maxHeight: axis, maxPixels: int(4, 16777216), maxFps: fps, cappedRate: z.boolean() });
|
|
38
|
+
const backend = z.strictObject({ id: bounded, kind: z.enum(['software', 'hardware']),
|
|
39
|
+
formats: z.array(encoderFormat).min(1).max(2).refine(a => distinct(a.map(f => f.format))),
|
|
40
|
+
});
|
|
41
|
+
export const desktopCapabilitiesSchema = z.strictObject({
|
|
42
|
+
cursorEmbedded: z.boolean().optional(), cursorVisibility: z.boolean().optional(), lobby: z.boolean().optional(),
|
|
43
|
+
protocols, providerId: bounded, providerVersion: bounded, configured: z.boolean(),
|
|
44
|
+
permission: z.enum(['granted', 'required', 'unknown', 'not_applicable']), health: z.enum(['ready', 'unavailable']), reason: code.nullable(),
|
|
45
|
+
formats, encoderBackends: z.array(backend),
|
|
46
|
+
input: z.strictObject({ pointer: z.boolean(), physicalKeyboard: z.boolean(), unicodeText: z.boolean(), clipboard: z.boolean().optional(), textPaste: z.boolean().optional() }),
|
|
47
|
+
displays: z.array(display).min(1).max(128).refine(a => distinct(a.map(d => d.id))),
|
|
48
|
+
}).refine(c => distinct(c.encoderBackends.map(b => b.id)) && c.encoderBackends.every(b => b.formats.every(f => c.formats.includes(f.format))));
|
|
49
|
+
export const capabilityReportSchema = z.strictObject({ generation: bounded, revision: positive, desktop: desktopCapabilitiesSchema.nullable() });
|
|
50
|
+
export const captureRegion = z.strictObject({ x: int(), y: int(), width: positive, height: positive,
|
|
51
|
+
sourceWidth: sourceAxis, sourceHeight: sourceAxis }).refine(r => r.x + r.width <= r.sourceWidth && r.y + r.height <= r.sourceHeight);
|
|
52
|
+
const focus = z.strictObject({ revision: positive, region: captureRegion });
|
|
53
|
+
const focusRequest = z.strictObject({ commandId: id, controlGeneration: positive, mediaEpoch: u32, displayRevision: u32, focus });
|
|
54
|
+
const selection = z.strictObject({ commandId: id, format, width: axis, height: axis, fpsCeiling: fps, quality, dpi: dpi.optional(), captureRegion: captureRegion.optional(), cursorVisible: z.boolean().optional() });
|
|
55
|
+
const configured = z.strictObject({ commandId: id, deviceGeneration: bounded, controlGeneration: positive, mediaEpoch: u32,
|
|
56
|
+
displayRevision: u32, format, width: axis, height: axis, fpsCeiling: fps, quality, encoderBackend: bounded, rateTargetBps: positive, dpi: dpi.optional(), captureRegion: captureRegion.optional() });
|
|
57
|
+
const ready = z.strictObject({ controlGeneration: positive, mediaEpoch: u32, displayRevision: u32 });
|
|
58
|
+
const command = z.strictObject({ commandId: id });
|
|
59
|
+
const inputBase = { controlGeneration: positive, displayRevision: u32, sequence: positive };
|
|
60
|
+
const point = { x: int(0, 4095), y: int(0, 4095) };
|
|
61
|
+
const text = z.string().refine(s => {
|
|
62
|
+
if (Buffer.byteLength(s) > 32768)
|
|
63
|
+
return false;
|
|
64
|
+
let scalars = 0;
|
|
65
|
+
for (const c of s) {
|
|
66
|
+
const n = c.codePointAt(0);
|
|
67
|
+
if (++scalars > 8192 || n >= 0xd800 && n <= 0xdfff || n < 32 && n !== 9 && n !== 10 || n >= 127 && n <= 159)
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
});
|
|
72
|
+
const input = z.discriminatedUnion('action', [
|
|
73
|
+
z.strictObject({ ...inputBase, action: z.literal('pointer'), ...point }),
|
|
74
|
+
z.strictObject({ ...inputBase, action: z.literal('button'), ...point, button: int(1, 3), down: z.boolean() }),
|
|
75
|
+
z.strictObject({ ...inputBase, action: z.literal('wheel'), ...point, wheelX: int(-120, 120), wheelY: int(-120, 120) }),
|
|
76
|
+
z.strictObject({ ...inputBase, action: z.literal('key'), usage: int(4, 231), down: z.boolean() }),
|
|
77
|
+
z.strictObject({ ...inputBase, action: z.literal('release_all') }),
|
|
78
|
+
z.strictObject({ ...inputBase, action: z.literal('text'), operationId: id, paste: z.boolean().optional(), text }),
|
|
79
|
+
]);
|
|
80
|
+
const held = z.strictObject({ controlGeneration: positive, displayRevision: u32, sequence: int(),
|
|
81
|
+
keys: z.array(int(4, 231)).max(228).refine(distinct), buttons: z.array(int(1, 3)).max(3).refine(distinct) });
|
|
82
|
+
const inputResult = z.strictObject({ sequence: positive, outcome: z.enum(['injected', 'rejected', 'unknown']), operationId: id.nullable().optional() })
|
|
83
|
+
.refine(r => r.outcome !== 'unknown' || r.operationId != null);
|
|
84
|
+
const finite = z.number().finite().min(0);
|
|
85
|
+
const feedback = z.strictObject({ epoch: u32, receivedId: int(), decodedId: int(), presentedId: int(), receivedTotalBytes: int(),
|
|
86
|
+
queueDepth: int(0, 2), queueAgeMs: finite, decodeMs: finite, presentMs: finite, exactPresentedSequence: int() }).refine(f => f.receivedId >= f.decodedId && f.decodedId >= f.presentedId);
|
|
87
|
+
const resizeResult = z.strictObject({ commandId: id, width: axis, height: axis, displayRevision: u32, outcome: z.enum(['applied', 'rolled_back', 'failed']), dpi: dpi.optional(), captureRegion: captureRegion.optional() });
|
|
88
|
+
export const desktopErrorCodeSchema = z.enum(['unavailable', 'unknown_capability', 'unsupported_protocol', 'unsupported_client',
|
|
89
|
+
'permission_required', 'busy', 'conflict', 'invalid_request', 'rate_limited', 'startup_timeout', 'encoder_failed', 'decoder_failed',
|
|
90
|
+
'resize_failed', 'transport_failed', 'authorization_lost', 'takeover_lost']);
|
|
91
|
+
const state = z.enum(['REQUESTED', 'PREPARING', 'NEGOTIATING', 'STARTING', 'ACTIVE', 'RECONFIGURING', 'SUSPENDED', 'RECONNECTING', 'FAILED', 'CLOSED']);
|
|
92
|
+
const clipboardChunk = { controlGeneration: positive, requestId: id, revision: int().default(0), offset: int(0, 1048576).default(0),
|
|
93
|
+
total: int(0, 1048576).default(0), data: z.string().max(10924).default('') };
|
|
94
|
+
const validClipboardChunk = (v) => {
|
|
95
|
+
const bytes = Buffer.from(v.data, 'base64');
|
|
96
|
+
return bytes.length <= 8192 && v.offset + bytes.length <= v.total && bytes.toString('base64') === v.data;
|
|
97
|
+
};
|
|
98
|
+
const clipboardRequest = z.strictObject({ ...clipboardChunk, action: z.enum(['read', 'write', 'paste']) })
|
|
99
|
+
.refine(v => validClipboardChunk(v) && (v.action === 'write' || v.data === '' && (v.action !== 'paste' || v.offset === 0 && v.total === 0)));
|
|
100
|
+
const clipboardResult = z.strictObject({ ...clipboardChunk, outcome: z.enum(['ok', 'pending', 'conflict', 'unavailable']) })
|
|
101
|
+
.refine(validClipboardChunk);
|
|
102
|
+
const cacheDependency = z.strictObject({ commandId: id, controlGeneration: positive, mediaEpoch: u32, displayRevision: u32,
|
|
103
|
+
frameId: positive, exactSequence: positive, cacheId: positive, width: int(1, 32), height: int(1, 32) });
|
|
104
|
+
const cacheDependencyResult = z.strictObject({ request: cacheDependency, pixels: z.string().max(5464) }).refine(v => {
|
|
105
|
+
const pixels = Buffer.from(v.pixels, 'base64');
|
|
106
|
+
return pixels.toString('base64') === v.pixels && pixels.length === v.request.width * v.request.height * 4 &&
|
|
107
|
+
pixels.every((byte, i) => i % 4 !== 3 || byte === 255);
|
|
108
|
+
});
|
|
109
|
+
const motionCut = { commandId: id, controlGeneration: positive, mediaEpoch: u32, displayRevision: u32 };
|
|
110
|
+
const motionCutPrepared = { ...motionCut, cutId: positive, pictureThrough: int(0, 0xffffffff), frameThrough: positive };
|
|
111
|
+
const payloadSchemas = {
|
|
112
|
+
audio_select: z.strictObject({ commandId: id, mode: audioMode }),
|
|
113
|
+
audio_ready: z.strictObject({ commandId: id, audioEpoch: u32 }),
|
|
114
|
+
audio_configured: audioConfiguration,
|
|
115
|
+
audio_state: z.strictObject({ commandId: id, state: z.enum(['off', 'buffering', 'playing', 'unavailable', 'suspended']), reason: z.string().regex(/^[a-z0-9_]{1,128}$/).nullable().default(null) }),
|
|
116
|
+
audio_feedback: z.strictObject({ audioEpoch: u32, receivedPackets: int(), latePackets: int(), lostPackets: int(), playedSample: int(), queueAgeMs: z.number().finite().min(0).max(10000), underruns: int() }),
|
|
117
|
+
focus: focusRequest,
|
|
118
|
+
cache_dependency: cacheDependency, cache_dependency_result: cacheDependencyResult,
|
|
119
|
+
focus_result: focusRequest.extend({ outcome: z.enum(['applied', 'failed']) }),
|
|
120
|
+
motion_cut_barrier: z.strictObject(motionCutPrepared),
|
|
121
|
+
motion_cut_request: z.strictObject(motionCut),
|
|
122
|
+
motion_cut_prepare: z.strictObject({ ...motionCut, cutId: positive }),
|
|
123
|
+
motion_cut_prepared: z.strictObject(motionCutPrepared),
|
|
124
|
+
motion_cut_fence: z.strictObject({ ...motionCutPrepared, motionCommittedBytes: int() }),
|
|
125
|
+
motion_cut_ack: z.strictObject({ ...motionCut, cutId: positive }),
|
|
126
|
+
motion_cut_commit: z.strictObject({ ...motionCut, cutId: positive }),
|
|
127
|
+
clipboard_request: clipboardRequest, clipboard_result: clipboardResult,
|
|
128
|
+
offer: desktopCapabilitiesSchema, select: selection, configured, viewer_ready: ready, decoder_ready: ready,
|
|
129
|
+
cursor_visibility: z.strictObject({ commandId: id, visible: z.boolean() }),
|
|
130
|
+
cursor_visibility_result: z.strictObject({ commandId: id, visible: z.boolean(), outcome: z.enum(['applied', 'failed']), mediaEpoch: int().default(0), afterFrameId: int().default(0) }),
|
|
131
|
+
appearance: z.strictObject({ commandId: id, colorScheme: z.enum(['light', 'dark']) }),
|
|
132
|
+
appearance_result: z.strictObject({ commandId: id, colorScheme: z.enum(['light', 'dark']), outcome: z.enum(['applied', 'failed']) }),
|
|
133
|
+
start: command, request_refresh: command, suspend: command, resume: command, stop: command, media_open: command,
|
|
134
|
+
input, held_state: held, input_result: inputResult, feedback,
|
|
135
|
+
credit: z.strictObject({ epoch: u32, motionConsumedBytes: int(), cursorConsumedBytes: int(), exactConsumedBytes: int(), windowBytes: int(196608, 524288) }).refine(v => Number.isSafeInteger(v.motionConsumedBytes + v.cursorConsumedBytes + v.exactConsumedBytes)),
|
|
136
|
+
ping: z.strictObject({ challenge: bounded }), pong: z.strictObject({ challenge: bounded }),
|
|
137
|
+
resize: z.strictObject({ commandId: id, expectedDisplayRevision: u32, width: axis, height: axis, dpi: dpi.optional(), captureRegion: captureRegion.optional() }), resize_result: resizeResult,
|
|
138
|
+
state: z.strictObject({ state, code: desktopErrorCodeSchema.nullable().optional() }),
|
|
139
|
+
error: z.strictObject({ code: desktopErrorCodeSchema, retryable: z.boolean() }),
|
|
140
|
+
};
|
|
141
|
+
export function supportsDesktopMode(display, width, height) {
|
|
142
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 2 || height < 2 || width > 4096 || height > 4096 || width % 2 || height % 2)
|
|
143
|
+
return false;
|
|
144
|
+
if (width === display.width && height === display.height)
|
|
145
|
+
return true;
|
|
146
|
+
const range = display.resizeRange;
|
|
147
|
+
if (display.resize === 'dynamic')
|
|
148
|
+
return range != null && width >= range.minWidth && width <= range.maxWidth && height >= range.minHeight && height <= range.maxHeight;
|
|
149
|
+
return display.resize === 'modes' && display.modes.some(m => m.width === width && m.height === height);
|
|
150
|
+
}
|
|
151
|
+
const viewer = z.strictObject({ protocols, formats, maxWidth: axis, maxHeight: axis, maxPixels: int(4, 16777216), maxFps: fps, maxDpi: dpi.optional(), appearanceSync: z.boolean().optional(), clipboard: z.boolean().optional(), overview: z.boolean().optional(), cursorControl: z.boolean().optional() });
|
|
152
|
+
const sessionRequest = z.strictObject({ requestId: id, displayId: id, viewer,
|
|
153
|
+
viewport: z.strictObject({ widthDp: z.number().positive().max(16384), heightDp: z.number().positive().max(16384),
|
|
154
|
+
deviceScale: z.number().min(0.5).max(8), orientation: z.enum(['landscape', 'portrait']) }),
|
|
155
|
+
resolution: z.discriminatedUnion('mode', [z.strictObject({ mode: z.literal('fit') }), z.strictObject({ mode: z.literal('fixed'), width: axis, height: axis })]),
|
|
156
|
+
quality, takeover: z.boolean().default(false), lobbyId: id.optional(), planId: id.optional(), mediaTransport: z.enum(['quic', 'wss']).optional() })
|
|
157
|
+
.refine(request => (request.lobbyId == null) === (request.planId == null) && (request.lobbyId == null) === (request.mediaTransport == null));
|
|
158
|
+
const desktopManagementSchema = z.discriminatedUnion('t', [
|
|
159
|
+
z.strictObject({ t: z.literal('desktop_lobby'), commandId: id, lobbyId: id, deviceGeneration: bounded, request: z.lazy(() => lobbyRequestSchema),
|
|
160
|
+
wssProbeGrant: z.string().regex(/^[A-Za-z0-9_-]{43}$/).nullish(), quicProbeGrant: z.string().regex(/^[A-Za-z0-9_-]{43}$/).nullish(),
|
|
161
|
+
quic: z.strictObject({ port: int(1, 65535), certificate: z.string().min(64).max(2048) }).nullish() }),
|
|
162
|
+
z.strictObject({ t: z.literal('desktop_background'), commandId: id, lobbyId: id, deviceGeneration: bounded,
|
|
163
|
+
planId: id, revision: positive, targetId: id, grant: z.string().regex(/^[A-Za-z0-9_-]{43}$/),
|
|
164
|
+
cachedVersion: z.string().regex(/^[A-Za-z0-9_-]{1,128}$/).nullish() }),
|
|
165
|
+
z.strictObject({ t: z.literal('desktop_lobby_result'), commandId: id, lobbyId: id, deviceGeneration: bounded,
|
|
166
|
+
plan: z.lazy(() => lobbyPlanSchema).nullable(), code: desktopErrorCodeSchema.nullable() }),
|
|
167
|
+
z.strictObject({ t: z.literal('workstation_capabilities'), ...capabilityReportSchema.shape }),
|
|
168
|
+
z.strictObject({ t: z.literal('workstation_capabilities_request'), generation: bounded }),
|
|
169
|
+
z.strictObject({ t: z.literal('desktop_prepare'), commandId: id, sessionId: id, displayId: id,
|
|
170
|
+
deviceGeneration: bounded, controlGeneration: positive, controlGrant: z.string().regex(/^[A-Za-z0-9_-]{43}$/),
|
|
171
|
+
candidate: z.boolean().optional(), mediaTransport: z.enum(['quic', 'wss']).optional() }),
|
|
172
|
+
z.strictObject({ t: z.literal('desktop_prepare_result'), commandId: id, sessionId: id,
|
|
173
|
+
state: z.enum(['bound', 'failed']), code: desktopErrorCodeSchema.nullable() }),
|
|
174
|
+
z.strictObject({ t: z.literal('desktop_cancel'), commandId: id, sessionId: id, controlGeneration: positive, code: desktopErrorCodeSchema }),
|
|
175
|
+
z.strictObject({ t: z.literal('desktop_session_state'), sessionId: id, controlGeneration: positive, state, code: desktopErrorCodeSchema.nullable() }),
|
|
176
|
+
]);
|
|
177
|
+
export function decodeDesktopManagementFrame(source, from) {
|
|
178
|
+
const frame = checked(desktopManagementSchema, source);
|
|
179
|
+
const sender = ['desktop_lobby_result', 'workstation_capabilities', 'desktop_prepare_result', 'desktop_session_state'].includes(frame.t) ? 'device' : 'server';
|
|
180
|
+
if (from && from !== sender)
|
|
181
|
+
return invalid();
|
|
182
|
+
return frame;
|
|
183
|
+
}
|
|
184
|
+
const authors = {
|
|
185
|
+
motion_cut_barrier: ['device', 'server'], motion_cut_request: ['viewer', 'device'], motion_cut_prepare: ['server'], motion_cut_prepared: ['device'], motion_cut_fence: ['server'], motion_cut_ack: ['viewer'], motion_cut_commit: ['server'],
|
|
186
|
+
cursor_visibility: ['viewer'], cursor_visibility_result: ['device'], appearance: ['viewer'], appearance_result: ['device'],
|
|
187
|
+
audio_select: ['viewer'], audio_ready: ['viewer'], audio_feedback: ['viewer'], audio_configured: ['device'], audio_state: ['device'],
|
|
188
|
+
clipboard_result: ['device'], clipboard_request: ['viewer'],
|
|
189
|
+
cache_dependency: ['viewer'], cache_dependency_result: ['device'], focus_result: ['device'], offer: ['device'], configured: ['device'], input_result: ['device'], resize_result: ['device'], media_open: ['device'],
|
|
190
|
+
select: ['viewer'], viewer_ready: ['viewer'], decoder_ready: ['viewer'], input: ['viewer'], held_state: ['viewer'],
|
|
191
|
+
focus: ['viewer'], feedback: ['viewer'], resize: ['viewer'], request_refresh: ['viewer'], suspend: ['viewer'], resume: ['viewer'], stop: ['viewer'],
|
|
192
|
+
start: ['server'], state: ['device', 'server'], error: ['device', 'server'],
|
|
193
|
+
};
|
|
194
|
+
/** JSON.parse runs only after a lexical pass checks duplicate decoded keys and exact integer tokens. */
|
|
195
|
+
export function parseDesktopJson(source, limit = DESKTOP_MAX_JSON_BYTES, integerFields) {
|
|
196
|
+
if (typeof source !== 'string' && source.byteLength > limit)
|
|
197
|
+
invalid();
|
|
198
|
+
const value = typeof source === 'string' ? source : new TextDecoder('utf-8', { fatal: true }).decode(source);
|
|
199
|
+
if (Buffer.byteLength(value) > limit)
|
|
200
|
+
invalid();
|
|
201
|
+
let offset = 0;
|
|
202
|
+
const space = () => { while (/[ \t\r\n]/.test(value[offset] ?? '\0'))
|
|
203
|
+
offset++; };
|
|
204
|
+
const take = (c) => { space(); if (value[offset] !== c)
|
|
205
|
+
return false; offset++; return true; };
|
|
206
|
+
const string = () => {
|
|
207
|
+
space();
|
|
208
|
+
const start = offset;
|
|
209
|
+
if (!take('"'))
|
|
210
|
+
return invalid();
|
|
211
|
+
while (offset < value.length) {
|
|
212
|
+
const c = value[offset++];
|
|
213
|
+
if (c === '\\')
|
|
214
|
+
offset++;
|
|
215
|
+
else if (c === '"')
|
|
216
|
+
return JSON.parse(value.slice(start, offset));
|
|
217
|
+
}
|
|
218
|
+
return invalid();
|
|
219
|
+
};
|
|
220
|
+
const floatFields = new Set(['widthDp', 'heightDp', 'deviceScale', 'capturePixelScale', 'queueAgeMs', 'decodeMs', 'presentMs']);
|
|
221
|
+
const visit = (depth, field = '') => {
|
|
222
|
+
if (depth > 32)
|
|
223
|
+
invalid();
|
|
224
|
+
space();
|
|
225
|
+
if (take('{')) {
|
|
226
|
+
const keys = new Set();
|
|
227
|
+
if (take('}'))
|
|
228
|
+
return;
|
|
229
|
+
do {
|
|
230
|
+
const key = string();
|
|
231
|
+
if (keys.has(key) || !take(':'))
|
|
232
|
+
invalid();
|
|
233
|
+
keys.add(key);
|
|
234
|
+
visit(depth + 1, key);
|
|
235
|
+
} while (take(','));
|
|
236
|
+
if (!take('}'))
|
|
237
|
+
invalid();
|
|
238
|
+
}
|
|
239
|
+
else if (take('[')) {
|
|
240
|
+
if (take(']'))
|
|
241
|
+
return;
|
|
242
|
+
do {
|
|
243
|
+
visit(depth + 1, field);
|
|
244
|
+
} while (take(','));
|
|
245
|
+
if (!take(']'))
|
|
246
|
+
invalid();
|
|
247
|
+
}
|
|
248
|
+
else if (value[offset] === '"') {
|
|
249
|
+
string();
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
const start = offset;
|
|
253
|
+
while (offset < value.length && !/[,\]} \t\r\n]/.test(value[offset]))
|
|
254
|
+
offset++;
|
|
255
|
+
const token = value.slice(start, offset);
|
|
256
|
+
if (/^-?[0-9]/.test(token) && (integerFields ? integerFields.has(field) : !floatFields.has(field))) {
|
|
257
|
+
if (!/^-?(0|[1-9][0-9]*)$/.test(token) || !Number.isSafeInteger(Number(token)))
|
|
258
|
+
invalid();
|
|
259
|
+
}
|
|
260
|
+
if (!token)
|
|
261
|
+
invalid();
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
visit(0);
|
|
265
|
+
space();
|
|
266
|
+
if (offset !== value.length)
|
|
267
|
+
invalid();
|
|
268
|
+
return JSON.parse(value);
|
|
269
|
+
}
|
|
270
|
+
function checked(schema, source) {
|
|
271
|
+
try {
|
|
272
|
+
return schema.parse(parseDesktopJson(source));
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return invalid();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
export function decodeCapabilityReport(source) { return checked(capabilityReportSchema, source); }
|
|
279
|
+
export function decodeDesktopSessionRequest(source) { return checked(sessionRequest, source); }
|
|
280
|
+
export function decodeDesktopControl(source, from) {
|
|
281
|
+
try {
|
|
282
|
+
const envelope = z.strictObject({ v: z.literal(1), type: z.string(), sessionId: id, payload: z.unknown() }).parse(parseDesktopJson(source));
|
|
283
|
+
if (!Object.hasOwn(payloadSchemas, envelope.type))
|
|
284
|
+
return invalid();
|
|
285
|
+
const type = envelope.type;
|
|
286
|
+
if (from && authors[type] && !authors[type].includes(from))
|
|
287
|
+
return invalid();
|
|
288
|
+
return { ...envelope, type, payload: payloadSchemas[type].parse(envelope.payload) };
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return invalid();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
export function encodeDesktopControl(message) {
|
|
295
|
+
const text = JSON.stringify(message);
|
|
296
|
+
decodeDesktopControl(text);
|
|
297
|
+
return text;
|
|
298
|
+
}
|
|
299
|
+
export function canonicalDesktopJson(value) {
|
|
300
|
+
return JSON.stringify(value, (_key, item) => item && typeof item === 'object' && !Array.isArray(item)
|
|
301
|
+
? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a.localeCompare(b))) : item);
|
|
302
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { access } from 'node:fs/promises';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { once } from 'node:events';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { bounded, DesktopQueue, NativeCommandError } from './helperProcess.js';
|
|
8
|
+
import { mediaRequire } from './media.js';
|
|
9
|
+
import { decodeQuicPathStats } from './networkEvidence.js';
|
|
10
|
+
const safeU64 = (b, offset) => { const n = b.readBigUInt64BE(offset); mediaRequire(n <= BigInt(Number.MAX_SAFE_INTEGER)); return Number(n); };
|
|
11
|
+
export function decodeQuicCutEvent(bytes) {
|
|
12
|
+
mediaRequire(bytes.length === 48 && bytes[0] === 1 && bytes[1] >= 1 && bytes[1] <= 6 && bytes[2] <= 7 && bytes[3] === 0 && bytes.readUInt32BE(20) === 0);
|
|
13
|
+
const value = { action: bytes[1], reason: bytes[2], epoch: bytes.readUInt32BE(4), cutId: safeU64(bytes, 8), pictureThrough: bytes.readUInt32BE(16), frameThrough: safeU64(bytes, 24), wireThrough: safeU64(bytes, 32), motionCommittedBytes: safeU64(bytes, 40) };
|
|
14
|
+
mediaRequire(value.epoch > 0 && (value.action === 1 || value.cutId > 0));
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
export class DesktopMediaAdmissionCancelled extends NativeCommandError {
|
|
18
|
+
cutId;
|
|
19
|
+
epoch;
|
|
20
|
+
pictureSequence;
|
|
21
|
+
constructor(cutId, epoch, pictureSequence) {
|
|
22
|
+
super('transport_failed');
|
|
23
|
+
this.cutId = cutId;
|
|
24
|
+
this.epoch = epoch;
|
|
25
|
+
this.pictureSequence = pictureSequence;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export const isQuicMediaQueueEvent = (kind) => kind === 1 || kind === 2 || kind === 3 || kind === 4;
|
|
29
|
+
/** At most one unresolved ticket per lane, sharing the caller's existing bounded reservation. */
|
|
30
|
+
export class DesktopQuicAdmission {
|
|
31
|
+
peer;
|
|
32
|
+
write;
|
|
33
|
+
pending = new Map();
|
|
34
|
+
nextId = 0n;
|
|
35
|
+
totals = new Map([[0, [0, 0, 0]]]);
|
|
36
|
+
epoch = 0;
|
|
37
|
+
dead = false;
|
|
38
|
+
cut;
|
|
39
|
+
constructor(peer, write) {
|
|
40
|
+
this.peer = peer;
|
|
41
|
+
this.write = write;
|
|
42
|
+
}
|
|
43
|
+
ownCut(id, epoch, pictureThrough) { mediaRequire(Number.isSafeInteger(id) && id > 0 && epoch > 0 && !this.cut); this.cut = { id, epoch, pictureThrough }; }
|
|
44
|
+
finishCut(id) { mediaRequire(this.cut?.id === id); this.cut = undefined; }
|
|
45
|
+
async send(kind, bytes) {
|
|
46
|
+
mediaRequire(!this.dead && (kind === 2 || kind === 3) && bytes.length > 0 && bytes.length <= 65600);
|
|
47
|
+
let lane = 255, epoch = 0, sequence = 0, payloadBytes = 0;
|
|
48
|
+
if (kind === 3) {
|
|
49
|
+
mediaRequire(bytes.length >= 65 && bytes.subarray(0, 5).equals(Buffer.from('AXDS\x01')));
|
|
50
|
+
lane = bytes[5] <= 2 ? 0 : bytes[5] <= 4 ? 1 : 2;
|
|
51
|
+
epoch = bytes.readUInt32BE(12);
|
|
52
|
+
sequence = bytes.readUInt32BE(20);
|
|
53
|
+
payloadBytes = bytes.length - 64;
|
|
54
|
+
mediaRequire(bytes[5] >= 1 && bytes[5] <= 5 && epoch > 0 && sequence > 0);
|
|
55
|
+
if (bytes[5] === 1) {
|
|
56
|
+
mediaRequire(epoch > this.epoch);
|
|
57
|
+
const oldest = [...this.totals.keys()].filter(e => e !== 0 && e !== this.epoch);
|
|
58
|
+
for (const old of oldest) {
|
|
59
|
+
mediaRequire(![...this.pending.values()].some(p => p.epoch === old));
|
|
60
|
+
this.totals.delete(old);
|
|
61
|
+
}
|
|
62
|
+
this.epoch = epoch;
|
|
63
|
+
if (!this.totals.has(epoch))
|
|
64
|
+
this.totals.set(epoch, [0, 0, 0]);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
mediaRequire(epoch === this.epoch || this.epoch === 0);
|
|
68
|
+
if (!this.totals.has(epoch))
|
|
69
|
+
this.totals.set(epoch, [0, 0, 0]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
mediaRequire(![...this.pending.values()].some(p => p.lane === lane));
|
|
73
|
+
const id = ++this.nextId;
|
|
74
|
+
mediaRequire(id <= BigInt(Number.MAX_SAFE_INTEGER));
|
|
75
|
+
const header = Buffer.alloc(21);
|
|
76
|
+
header.writeUInt32BE(bytes.length + 17);
|
|
77
|
+
header[4] = kind;
|
|
78
|
+
header.writeBigUInt64BE(this.peer, 5);
|
|
79
|
+
header.writeBigUInt64BE(id, 13);
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
const item = { id, kind, lane, epoch, sequence, payloadBytes, frame: Buffer.concat([header, bytes]), resolve, reject, busy: false, deadline: setTimeout(() => { this.close(new NativeCommandError('transport_failed')); }, 5000) };
|
|
82
|
+
this.pending.set(id, item);
|
|
83
|
+
void this.write(item.frame).catch(e => this.close(e));
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
accept(peer, bytes) {
|
|
87
|
+
mediaRequire(peer === this.peer && bytes.length === 32 && bytes.readUInt16BE(10) === 0 && bytes.readUInt32BE(20) === 0);
|
|
88
|
+
const id = bytes.readBigUInt64BE(), p = this.pending.get(id);
|
|
89
|
+
mediaRequire(p && bytes[8] >= 1 && bytes[8] <= 3 && bytes[9] === p.lane && bytes.readUInt32BE(12) === p.epoch && bytes.readUInt32BE(16) === p.sequence);
|
|
90
|
+
const total = safeU64(bytes, 24), previous = p.lane === 255 ? 0 : this.totals.get(p.epoch)[p.lane];
|
|
91
|
+
mediaRequire(total === previous + (bytes[8] === 1 ? p.payloadBytes : 0));
|
|
92
|
+
if (bytes[8] === 3) {
|
|
93
|
+
mediaRequire(!p.busy);
|
|
94
|
+
p.busy = true;
|
|
95
|
+
p.timer = setTimeout(() => { p.busy = false; if (this.pending.get(id) === p)
|
|
96
|
+
void this.write(p.frame).catch(e => this.close(e)); }, 10);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (bytes[8] === 2)
|
|
100
|
+
mediaRequire(p.lane === 0 && this.cut && p.epoch === this.cut.epoch && p.sequence <= this.cut.pictureThrough);
|
|
101
|
+
this.pending.delete(id);
|
|
102
|
+
clearTimeout(p.deadline);
|
|
103
|
+
clearTimeout(p.timer);
|
|
104
|
+
if (bytes[8] === 1) {
|
|
105
|
+
if (p.lane !== 255)
|
|
106
|
+
this.totals.get(p.epoch)[p.lane] = total;
|
|
107
|
+
p.resolve();
|
|
108
|
+
}
|
|
109
|
+
else
|
|
110
|
+
p.reject(new DesktopMediaAdmissionCancelled(this.cut.id, p.epoch, p.sequence));
|
|
111
|
+
}
|
|
112
|
+
close(error = new NativeCommandError('transport_failed')) { this.dead = true; for (const p of this.pending.values()) {
|
|
113
|
+
clearTimeout(p.deadline);
|
|
114
|
+
clearTimeout(p.timer);
|
|
115
|
+
p.reject(error);
|
|
116
|
+
} this.pending.clear(); }
|
|
117
|
+
}
|
|
118
|
+
/** The subprocess is a platform build of the same Rust engine embedded by native viewers. */
|
|
119
|
+
export async function openQuicMedia(server, offer, signal, current) {
|
|
120
|
+
const packaged = fileURLToPath(new URL(`../../native/${process.platform}-${process.arch}/axom-quic`, import.meta.url));
|
|
121
|
+
const candidates = [...(process.env.AXOM_QUIC_BINARY ? [process.env.AXOM_QUIC_BINARY] : []), packaged, path.join(homedir(), '.axom/workstation/quic', `${process.platform}-${process.arch}`, 'axom-quic'), '/usr/local/bin/axom-quic'];
|
|
122
|
+
const executable = await (async () => { for (const file of candidates) {
|
|
123
|
+
try {
|
|
124
|
+
await access(file);
|
|
125
|
+
return file;
|
|
126
|
+
}
|
|
127
|
+
catch { }
|
|
128
|
+
} return undefined; })();
|
|
129
|
+
if (!executable)
|
|
130
|
+
return undefined;
|
|
131
|
+
const child = spawn(executable, [], { stdio: ['pipe', 'pipe', 'ignore'] });
|
|
132
|
+
const queue = new DesktopQueue();
|
|
133
|
+
const events = queue[Symbol.asyncIterator]();
|
|
134
|
+
let buffer = Buffer.alloc(0), peer = 0n, dead = false, transportBytes = 0;
|
|
135
|
+
let pathStats;
|
|
136
|
+
let admission;
|
|
137
|
+
const cuts = new DesktopQueue(16);
|
|
138
|
+
const close = () => { if (dead)
|
|
139
|
+
return; dead = true; child.stdin.destroy(); child.kill(); admission?.close(); queue.close(); cuts.close(); signal.removeEventListener('abort', close); };
|
|
140
|
+
const closed = new Promise(resolve => child.once('close', () => { close(); resolve(); }));
|
|
141
|
+
signal.addEventListener('abort', close, { once: true });
|
|
142
|
+
child.once('error', close);
|
|
143
|
+
child.stdin.on('error', close);
|
|
144
|
+
if (signal.aborted)
|
|
145
|
+
close();
|
|
146
|
+
child.stdout.on('data', (bytes) => {
|
|
147
|
+
try {
|
|
148
|
+
mediaRequire(buffer.length + bytes.length <= 2 * 65613);
|
|
149
|
+
buffer = Buffer.concat([buffer, bytes]);
|
|
150
|
+
while (buffer.length >= 4) {
|
|
151
|
+
const length = buffer.readUInt32BE();
|
|
152
|
+
mediaRequire(length >= 9 && length <= 65609);
|
|
153
|
+
if (buffer.length < length + 4)
|
|
154
|
+
break;
|
|
155
|
+
const frame = buffer.subarray(4, 4 + length);
|
|
156
|
+
buffer = buffer.subarray(4 + length);
|
|
157
|
+
const kind = frame[0], eventPeer = frame.readBigUInt64BE(1), payload = Buffer.from(frame.subarray(9));
|
|
158
|
+
if (kind === 1) {
|
|
159
|
+
mediaRequire(peer === 0n && eventPeer > 0n);
|
|
160
|
+
peer = eventPeer;
|
|
161
|
+
}
|
|
162
|
+
if (kind === 9) {
|
|
163
|
+
mediaRequire(eventPeer === peer && payload.length === 8);
|
|
164
|
+
const total = safeU64(payload, 0);
|
|
165
|
+
mediaRequire(total >= transportBytes);
|
|
166
|
+
transportBytes = total;
|
|
167
|
+
}
|
|
168
|
+
else if (kind === 10) {
|
|
169
|
+
mediaRequire(eventPeer === peer);
|
|
170
|
+
const next = decodeQuicPathStats(payload, performance.now());
|
|
171
|
+
mediaRequire(!pathStats || next.sequence > pathStats.sequence && next.pathGeneration >= pathStats.pathGeneration);
|
|
172
|
+
pathStats = next;
|
|
173
|
+
}
|
|
174
|
+
else if (kind === 8) {
|
|
175
|
+
mediaRequire(admission);
|
|
176
|
+
admission.accept(eventPeer, payload);
|
|
177
|
+
}
|
|
178
|
+
else if (kind === 7) {
|
|
179
|
+
mediaRequire(eventPeer === peer);
|
|
180
|
+
cuts.push(decodeQuicCutEvent(payload));
|
|
181
|
+
}
|
|
182
|
+
else if (isQuicMediaQueueEvent(kind))
|
|
183
|
+
queue.push({ kind, peer: eventPeer, bytes: payload });
|
|
184
|
+
// Native event extensions belong to their own lanes. Never let an
|
|
185
|
+
// unrecognized event become an AXDS media packet and retire the RD session.
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
close();
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
const write = async (bytes) => { mediaRequire(current() && !signal.aborted && !dead); if (!child.stdin.write(bytes))
|
|
193
|
+
await bounded(once(child.stdin, 'drain'), 2000); };
|
|
194
|
+
try {
|
|
195
|
+
const config = Buffer.from(JSON.stringify({ mode: 'client', host: new URL(server).hostname.replace(/^\[|\]$/g, ''), port: offer.port, certificate: offer.certificate }));
|
|
196
|
+
const header = Buffer.alloc(4);
|
|
197
|
+
header.writeUInt32BE(config.length);
|
|
198
|
+
await write(Buffer.concat([header, config]));
|
|
199
|
+
const ready = await bounded(events.next(), 1800);
|
|
200
|
+
mediaRequire(!ready.done && ready.value.kind === 1);
|
|
201
|
+
peer = ready.value.peer;
|
|
202
|
+
admission = new DesktopQuicAdmission(peer, write);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
close();
|
|
206
|
+
if (signal.aborted)
|
|
207
|
+
throw error;
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
const send = (kind, bytes) => admission.send(kind, bytes);
|
|
211
|
+
const sendAudio = async (bytes) => {
|
|
212
|
+
mediaRequire(bytes.length >= 41 && bytes.length <= 1040 && bytes.subarray(0, 4).equals(Buffer.from('AXDA')));
|
|
213
|
+
if (!current() || signal.aborted || dead || child.stdin.writableLength > 4096)
|
|
214
|
+
return false;
|
|
215
|
+
const frame = Buffer.alloc(21);
|
|
216
|
+
frame.writeUInt32BE(17 + bytes.length);
|
|
217
|
+
frame[4] = 12;
|
|
218
|
+
frame.writeBigUInt64BE(peer, 5);
|
|
219
|
+
child.stdin.write(Buffer.concat([frame, bytes]));
|
|
220
|
+
return true;
|
|
221
|
+
};
|
|
222
|
+
const cutCommand = async (kind, cutId, epoch, pictureThrough = 0, frameThrough = 0) => {
|
|
223
|
+
mediaRequire(Number.isSafeInteger(cutId) && cutId > 0 && Number.isInteger(epoch) && epoch > 0);
|
|
224
|
+
const payload = Buffer.alloc(kind === 9 ? 16 : 4);
|
|
225
|
+
payload.writeUInt32BE(epoch);
|
|
226
|
+
if (kind === 9) {
|
|
227
|
+
payload.writeUInt32BE(pictureThrough, 4);
|
|
228
|
+
payload.writeBigUInt64BE(BigInt(frameThrough), 8);
|
|
229
|
+
admission.ownCut(cutId, epoch, pictureThrough);
|
|
230
|
+
}
|
|
231
|
+
const frame = Buffer.alloc(21);
|
|
232
|
+
frame.writeUInt32BE(17 + payload.length);
|
|
233
|
+
frame[4] = kind;
|
|
234
|
+
frame.writeBigUInt64BE(peer, 5);
|
|
235
|
+
frame.writeBigUInt64BE(BigInt(cutId), 13);
|
|
236
|
+
await write(Buffer.concat([frame, payload]));
|
|
237
|
+
};
|
|
238
|
+
return { send, sendAudio, close, closed, transportCost: () => transportBytes, pathStats: () => pathStats, cutEvents: cuts, cutCommand, finishCut: (id) => admission.finishCut(id), receive: async (timeoutMs = 0) => { const event = timeoutMs ? await bounded(events.next(), timeoutMs) : await events.next(); if (event.done || event.value.kind === 4)
|
|
239
|
+
throw new NativeCommandError('transport_failed'); return event.value; } };
|
|
240
|
+
}
|