@platless/axom-cli 0.1.0 → 0.5.9
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 +1 -1
- package/dist/authScopes.js +19 -0
- package/dist/axom-patch/cli/README.md +132 -0
- package/dist/axom-patch/cli/account-limits/README.md +27 -0
- package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
- package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
- package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
- package/dist/axom-patch/cli/account-limits/generate.py +120 -0
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
- package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
- package/dist/axom-patch/cli/claude-account-limits.json +37 -0
- package/dist/axom-patch/cli/codex-account-limits.json +37 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
- package/dist/axom-patch/cli/nightly-task.md +152 -0
- package/dist/axom-patch/cli/pi-steering.json +37 -0
- package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
- package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +38 -0
- package/dist/axom-patch/cli/probes/pi-steering.mjs +80 -0
- package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +89 -0
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +61 -0
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +82 -0
- package/dist/axom-patch/cli/qoder-foreground-output.md +42 -0
- package/dist/axom-patch/cli/qoder-steering.json +52 -0
- package/dist/axom-patch/cli/qoder-steering.md +30 -0
- package/dist/axom-patch/cli/qoder-subagent-origin.json +73 -0
- package/dist/axom-patch/cli/qoder-subagent-subwork.md +73 -0
- package/dist/axom-patch/cli/runtime.json +63 -0
- package/dist/axom-patch/cli/steering/claude-steering-js.patch +89 -0
- package/dist/axom-patch/cli/steering/claude-steering-types.patch +28 -0
- package/dist/axom-patch/cli/steering/codex-steering.patch +57 -0
- package/dist/axom-patch/cli/steering/pi-steering.js +79 -0
- package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
- package/dist/axom-patch/cli/zcode-models.json +38 -0
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
- package/dist/cli.js +36 -1
- package/dist/commands/auth.js +4 -13
- package/dist/commands/execute.js +4 -4
- package/dist/commands/record.js +6 -6
- package/dist/commands/struct.js +4 -4
- package/dist/commands/workstation.js +2 -2
- package/dist/credentials.js +2 -2
- package/dist/install/claude/commands.js +13 -13
- package/dist/keepers/acp/keeper.mjs +3083 -0
- package/dist/keepers/acp/manifest.json +9 -0
- package/dist/keepers/terminal/keeper.mjs +8469 -0
- package/dist/keepers/terminal/manifest.json +9 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
- package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
- package/dist/mcp.js +32 -14
- package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
- package/dist/runtime-deps/qoder-sdk/package.json +8 -0
- package/dist/test/keeperKit.js +67 -0
- package/dist/workstation/acp/acpAgents.js +2514 -681
- package/dist/workstation/acp/acpClient.js +95 -318
- package/dist/workstation/acp/activityBusy.js +43 -0
- package/dist/workstation/acp/activityReporting.js +54 -0
- package/dist/workstation/acp/adoption.js +28 -356
- package/dist/workstation/acp/authUrlSniffer.js +36 -0
- package/dist/workstation/acp/binaryInstall.js +100 -27
- package/dist/workstation/acp/builds/gc.js +245 -0
- package/dist/workstation/acp/builds/install/binary.js +28 -0
- package/dist/workstation/acp/builds/install/npm.js +235 -0
- package/dist/workstation/acp/builds/install/uv.js +254 -0
- package/dist/workstation/acp/builds/migrate.js +195 -0
- package/dist/workstation/acp/builds/model.js +128 -0
- package/dist/workstation/acp/builds/nodeGate.js +100 -0
- package/dist/workstation/acp/builds/store.js +352 -0
- package/dist/workstation/acp/commandTableProbe.js +68 -0
- package/dist/workstation/acp/configOptions.js +103 -1
- package/dist/workstation/acp/delegationMcp.js +24 -0
- package/dist/workstation/acp/envSecrets.js +27 -0
- package/dist/workstation/acp/initializeResponse.js +285 -0
- package/dist/workstation/acp/installErrors.js +12 -2
- package/dist/workstation/acp/installProbe.js +1 -1
- package/dist/workstation/acp/managedPatches.js +144 -0
- package/dist/workstation/acp/outbox.js +330 -0
- package/dist/workstation/acp/patchAssets.js +80 -0
- package/dist/workstation/acp/permission.js +335 -37
- package/dist/workstation/acp/processor/checkpoint.js +87 -0
- package/dist/workstation/acp/processor/commands.js +35 -0
- package/dist/workstation/acp/processor/control.js +355 -0
- package/dist/workstation/acp/processor/emit.js +26 -0
- package/dist/workstation/acp/processor/jsonrpc.js +172 -0
- package/dist/workstation/acp/processor/memoryLog.js +49 -0
- package/dist/workstation/acp/processor/origins.js +48 -0
- package/dist/workstation/acp/processor/output.js +67 -0
- package/dist/workstation/acp/processor/projection.js +42 -0
- package/dist/workstation/acp/processor/record.js +128 -0
- package/dist/workstation/acp/processor/runtime.js +181 -0
- package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
- package/dist/workstation/acp/processor/session.js +525 -0
- package/dist/workstation/acp/processor/sideChannel.js +80 -0
- package/dist/workstation/acp/processor/state.js +74 -0
- package/dist/workstation/acp/processor/step.js +459 -0
- package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
- package/dist/workstation/acp/processor/test/harness.js +120 -0
- package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
- package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
- package/dist/workstation/acp/processor/transport/generations.js +76 -0
- package/dist/workstation/acp/processor/transport/keeper.js +130 -0
- package/dist/workstation/acp/processor/transport/local.js +269 -0
- package/dist/workstation/acp/processor/transport/types.js +78 -0
- package/dist/workstation/acp/processor/updates.js +364 -0
- package/dist/workstation/acp/processorSession.js +1512 -0
- package/dist/workstation/acp/qoderStartup.js +45 -0
- package/dist/workstation/acp/registry/catalog.js +385 -4
- package/dist/workstation/acp/registry/detect.js +55 -0
- package/dist/workstation/acp/registry/launch.js +61 -119
- package/dist/workstation/acp/sessionUpdateMapper.js +56 -27
- package/dist/workstation/acp/steering.js +58 -0
- package/dist/workstation/acp/switching.js +114 -0
- package/dist/workstation/acp/terminalLogin.js +171 -0
- package/dist/workstation/acp/terminalLoginSpec.js +41 -0
- package/dist/workstation/acp/test/adapterFixtures.js +256 -0
- package/dist/workstation/acp/test/daemonLife.js +106 -0
- package/dist/workstation/acp/test/fakeAgent.js +185 -9
- package/dist/workstation/acp/test/keeperWorld.js +267 -0
- package/dist/workstation/acp/test/memoryOutbox.js +55 -0
- package/dist/workstation/acp/test/raw/fixtures.js +196 -0
- package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
- package/dist/workstation/acp/test/raw/sanitize.js +344 -0
- package/dist/workstation/acp/test/raw/sources.js +249 -0
- package/dist/workstation/acp/test/realAdapter.js +377 -0
- package/dist/workstation/acp/test/scriptAgent.js +206 -0
- package/dist/workstation/acp/test/sessionFrames.js +42 -0
- package/dist/workstation/acp/timedCommand.js +4 -2
- package/dist/workstation/acp/uvxInstall.js +72 -122
- package/dist/workstation/acp/validate.js +33 -5
- package/dist/workstation/agentFlow.js +103 -0
- package/dist/workstation/agentSessions.js +1686 -348
- package/dist/workstation/channels/agent.js +14 -6
- package/dist/workstation/channels/computer.js +203 -7
- package/dist/workstation/channels/computerExecs.js +200 -0
- package/dist/workstation/channels/management.js +86 -6
- package/dist/workstation/config.js +3 -1
- package/dist/workstation/connection/encryptedChannel.js +88 -21
- package/dist/workstation/daemon.js +309 -6
- package/dist/workstation/daemonctl.js +28 -2
- package/dist/workstation/desktop/binding.js +36 -0
- package/dist/workstation/desktop/bridge.js +212 -0
- package/dist/workstation/desktop/flow-control.js +52 -0
- package/dist/workstation/desktop/focused-text.js +130 -0
- package/dist/workstation/desktop/protocol.js +35 -0
- package/dist/workstation/exec/attach.js +126 -0
- package/dist/workstation/exec/command.js +40 -0
- package/dist/workstation/exec/durable.js +517 -0
- package/dist/workstation/exec/fallback.js +106 -0
- package/dist/workstation/exec/policy.js +837 -0
- package/dist/workstation/exec/shaping.js +211 -0
- package/dist/workstation/exec.js +3 -112
- package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
- package/dist/workstation/files/adapters/toolRequests.js +121 -0
- package/dist/workstation/files/client.js +455 -0
- package/dist/workstation/files/errors.js +28 -0
- package/dist/workstation/files/fence.js +350 -0
- package/dist/workstation/files/find/fileIndex.js +153 -0
- package/dist/workstation/files/find/fuzzy.js +155 -0
- package/dist/workstation/files/host.js +122 -0
- package/dist/workstation/files/hostProtocol.js +2 -0
- package/dist/workstation/files/media/cache.js +620 -0
- package/dist/workstation/files/media/encoding.js +308 -0
- package/dist/workstation/files/media/estimator.js +100 -0
- package/dist/workstation/files/media/media.js +325 -0
- package/dist/workstation/files/media/packedAudio.js +23 -0
- package/dist/workstation/files/media/scheduler.js +551 -0
- package/dist/workstation/files/media/types.js +5 -0
- package/dist/workstation/files/scope.js +45 -0
- package/dist/workstation/files/structured.js +284 -0
- package/dist/workstation/files/text.js +356 -0
- package/dist/workstation/files/uploads.js +351 -0
- package/dist/workstation/journal.js +65 -236
- package/dist/workstation/keepers/acp/client.js +101 -0
- package/dist/workstation/keepers/acp/envelope.js +62 -0
- package/dist/workstation/keepers/acp/host.js +419 -0
- package/dist/workstation/keepers/acp/main.js +18 -0
- package/dist/workstation/keepers/acp/memoryStream.js +121 -0
- package/dist/workstation/keepers/acp/params.js +137 -0
- package/dist/workstation/keepers/acp/protocol.js +66 -0
- package/dist/workstation/keepers/acp/recover.js +120 -0
- package/dist/workstation/keepers/acp/session.js +584 -0
- package/dist/workstation/keepers/acp/stderr.js +154 -0
- package/dist/workstation/keepers/kit/activity.js +103 -0
- package/dist/workstation/keepers/kit/bootstrap.js +164 -0
- package/dist/workstation/keepers/kit/buildVersion.js +51 -0
- package/dist/workstation/keepers/kit/bundle.js +120 -0
- package/dist/workstation/keepers/kit/client.js +432 -0
- package/dist/workstation/keepers/kit/identity.js +21 -0
- package/dist/workstation/keepers/kit/ipc.js +112 -0
- package/dist/workstation/keepers/kit/launchConfig.js +8 -0
- package/dist/workstation/keepers/kit/launcher.js +249 -0
- package/dist/workstation/keepers/kit/lock.js +123 -0
- package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
- package/dist/workstation/keepers/kit/paths.js +75 -0
- package/dist/workstation/keepers/kit/protocol.js +43 -0
- package/dist/workstation/keepers/kit/server.js +518 -0
- package/dist/workstation/keepers/kit/streamLog.js +697 -0
- package/dist/workstation/keepers/legacy.js +296 -0
- package/dist/workstation/keepers/policy.js +576 -0
- package/dist/workstation/keepers/stop.js +34 -0
- package/dist/workstation/keepers/terminal/client.js +213 -0
- package/dist/workstation/keepers/terminal/exec.js +981 -0
- package/dist/workstation/keepers/terminal/generations.js +129 -0
- package/dist/workstation/keepers/terminal/keeper.js +62 -0
- package/dist/workstation/keepers/terminal/main.js +43 -0
- package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
- package/dist/workstation/keepers/terminal/params.js +27 -0
- package/dist/workstation/keepers/terminal/protocol.js +56 -0
- package/dist/workstation/keepers/terminal/pty.js +330 -0
- package/dist/workstation/oauth/deviceAuthorization.js +21 -12
- package/dist/workstation/protocol/accountLimits.js +47 -0
- package/dist/workstation/protocol/agent.js +201 -8
- package/dist/workstation/protocol/common.js +36 -0
- package/dist/workstation/protocol/computer.js +107 -7
- package/dist/workstation/protocol/management.js +50 -2
- package/dist/workstation/qoderAdapter/backgroundProcess.js +118 -0
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +193 -0
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
- package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
- package/dist/workstation/qoderAdapter/completedContinuation.js +139 -0
- package/dist/workstation/qoderAdapter/contract.js +78 -0
- package/dist/workstation/qoderAdapter/foregroundOutput.js +36 -0
- package/dist/workstation/qoderAdapter/history.js +85 -0
- package/dist/workstation/qoderAdapter/identityIndex.js +191 -0
- package/dist/workstation/qoderAdapter/inputQueue.js +42 -0
- package/dist/workstation/qoderAdapter/main.js +95 -0
- package/dist/workstation/qoderAdapter/mapper.js +117 -0
- package/dist/workstation/qoderAdapter/observerPlugin.js +245 -0
- package/dist/workstation/qoderAdapter/observerReceiver.js +187 -0
- package/dist/workstation/qoderAdapter/permissions.js +61 -0
- package/dist/workstation/qoderAdapter/profile.js +93 -0
- package/dist/workstation/qoderAdapter/sdkDriver.js +48 -0
- package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
- package/dist/workstation/qoderAdapter/session.js +319 -0
- package/dist/workstation/qoderAdapter/subagentMeta.js +28 -0
- package/dist/workstation/qoderAdapter/subwork.js +85 -0
- package/dist/workstation/qoderAdapter/taskControl.js +38 -0
- package/dist/workstation/qoderAdapter/taskLedger.js +163 -0
- package/dist/workstation/qoderAdapter/types.js +1 -0
- package/dist/workstation/qoderAdapter/usage.js +37 -0
- package/dist/workstation/register.js +4 -0
- package/dist/workstation/systemMetrics.js +110 -1
- package/dist/workstation/tools/browser.js +394 -0
- package/dist/workstation/tools/client.js +277 -0
- package/dist/workstation/tools/info.js +50 -0
- package/dist/workstation/tools/keeperProcess.js +124 -0
- package/dist/workstation/tools/router.js +55 -0
- package/dist/workstation/tools/terminalRouter.js +222 -0
- package/dist/workstation/tools/validation.js +86 -0
- package/dist/workstation/update/coordinator.js +32 -0
- package/dist/workstation/update/npmInstaller.js +2 -0
- package/dist/workstation/update/ptyPreflight.js +45 -0
- package/dist/workstation/update/taskRegistry.js +141 -23
- package/dist/workstation/update/watchdog.js +66 -0
- package/package.json +20 -7
- package/skills/axom/SKILL.md +55 -17
- package/skills/axom-definition/SKILL.md +32 -32
- package/skills/axom-platform/SKILL.md +39 -39
- package/dist/workstation/acp/acpSession.js +0 -1047
- package/dist/workstation/acp/contract.js +0 -494
- package/dist/workstation/acp/draftInterceptor.js +0 -154
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
// Daemon-side half of the kit. Exempt from the keeper-side import guard (it may be imported by daemon modules).
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { promises as fs } from 'node:fs';
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
import { encodeFrame, LineDecoder, parseClientBound } from './ipc.js';
|
|
6
|
+
import { frameLimitsOf, inProtocolWindow, WINDOW_EXEMPT_METHODS, } from './protocol.js';
|
|
7
|
+
/** The keeper answered with `ok:false`. */
|
|
8
|
+
export class KeeperRequestError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
constructor(code, message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.name = 'KeeperRequestError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** The connection broke while the request was in flight. The request MAY have been executed: requests are never retried, reconcile by the keeper's own state. */
|
|
17
|
+
export class KeeperConnectionLostError extends Error {
|
|
18
|
+
code = 'connection_lost';
|
|
19
|
+
constructor(message = 'Keeper connection lost') { super(message); this.name = 'KeeperConnectionLostError'; }
|
|
20
|
+
}
|
|
21
|
+
export class KeeperRequestTimeoutError extends Error {
|
|
22
|
+
code = 'timeout';
|
|
23
|
+
constructor(method) { super(`Keeper request ${method} timed out`); this.name = 'KeeperRequestTimeoutError'; }
|
|
24
|
+
}
|
|
25
|
+
export class KeeperNotConnectedError extends Error {
|
|
26
|
+
code = 'not_connected';
|
|
27
|
+
constructor(message = 'Keeper is not connected') { super(message); this.name = 'KeeperNotConnectedError'; }
|
|
28
|
+
}
|
|
29
|
+
export class KeeperClosedError extends Error {
|
|
30
|
+
code = 'closed';
|
|
31
|
+
constructor() { super('Keeper client is closed'); this.name = 'KeeperClosedError'; }
|
|
32
|
+
}
|
|
33
|
+
export class KeeperFrameTooLargeError extends Error {
|
|
34
|
+
code = 'frame_too_large';
|
|
35
|
+
constructor(size, limit) { super(`Request frame of ${size} bytes exceeds the ${limit} byte limit`); this.name = 'KeeperFrameTooLargeError'; }
|
|
36
|
+
}
|
|
37
|
+
/** The keeper's protocol is outside the daemon's `[N-1, N]` window: it has to be replaced. Only the `keeper.*` management methods still work. */
|
|
38
|
+
export class KeeperReplacementRequiredError extends Error {
|
|
39
|
+
hello;
|
|
40
|
+
daemonProtocol;
|
|
41
|
+
code = 'replacement_required';
|
|
42
|
+
constructor(hello, daemonProtocol) {
|
|
43
|
+
super(`Keeper protocol ${hello.protocolVersion} is outside the supported window [${daemonProtocol[0]}, ${daemonProtocol[1]}]`);
|
|
44
|
+
this.hello = hello;
|
|
45
|
+
this.daemonProtocol = daemonProtocol;
|
|
46
|
+
this.name = 'KeeperReplacementRequiredError';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Reads the connection token. The file must be a regular file (never a symlink), owned by this user and private
|
|
51
|
+
* (no group/other bits): a token anyone else could have planted or read authenticates nothing.
|
|
52
|
+
*/
|
|
53
|
+
export async function readKeeperToken(tokenFile) {
|
|
54
|
+
const stat = await fs.lstat(tokenFile);
|
|
55
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
56
|
+
throw new Error('Unsafe keeper token file: not a regular file');
|
|
57
|
+
if (process.getuid && stat.uid !== process.getuid())
|
|
58
|
+
throw new Error('Unsafe keeper token file: owned by another user');
|
|
59
|
+
if ((stat.mode & 0o077) !== 0)
|
|
60
|
+
throw new Error('Unsafe keeper token file: readable by group or others');
|
|
61
|
+
const token = (await fs.readFile(tokenFile, 'utf8')).trim();
|
|
62
|
+
if (!token)
|
|
63
|
+
throw new Error('Empty keeper token');
|
|
64
|
+
return token;
|
|
65
|
+
}
|
|
66
|
+
/** One socket: request/response correlation plus an `evt` hook. Never reused after it closes. */
|
|
67
|
+
class Link {
|
|
68
|
+
socket;
|
|
69
|
+
limits;
|
|
70
|
+
onEvt;
|
|
71
|
+
onClose;
|
|
72
|
+
nextId = 1;
|
|
73
|
+
pending = new Map();
|
|
74
|
+
decoder;
|
|
75
|
+
dead = false;
|
|
76
|
+
constructor(socket, limits, onEvt, onClose) {
|
|
77
|
+
this.socket = socket;
|
|
78
|
+
this.limits = limits;
|
|
79
|
+
this.onEvt = onEvt;
|
|
80
|
+
this.onClose = onClose;
|
|
81
|
+
this.decoder = new LineDecoder(limits.out); // per socket: a torn line is never completed by another connection's bytes
|
|
82
|
+
socket.on('data', (chunk) => this.onData(chunk));
|
|
83
|
+
socket.on('error', (error) => this.destroy(new KeeperConnectionLostError(error.message)));
|
|
84
|
+
socket.on('close', () => this.destroy(new KeeperConnectionLostError('Keeper closed the connection')));
|
|
85
|
+
}
|
|
86
|
+
get isDead() { return this.dead; }
|
|
87
|
+
onData(chunk) {
|
|
88
|
+
let frames;
|
|
89
|
+
try {
|
|
90
|
+
frames = this.decoder.push(chunk).map(parseClientBound);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
this.destroy(new KeeperConnectionLostError(`Invalid keeper frame: ${error.message}`));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
for (const frame of frames) {
|
|
97
|
+
if (this.dead)
|
|
98
|
+
return;
|
|
99
|
+
if (frame.t === 'evt') {
|
|
100
|
+
this.onEvt(frame);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const entry = this.pending.get(frame.id);
|
|
104
|
+
if (!entry)
|
|
105
|
+
continue; // a late answer to a timed-out request
|
|
106
|
+
this.pending.delete(frame.id);
|
|
107
|
+
clearTimeout(entry.timer);
|
|
108
|
+
if (frame.ok)
|
|
109
|
+
entry.resolve(frame.result ?? {});
|
|
110
|
+
else {
|
|
111
|
+
const e = frame.error;
|
|
112
|
+
entry.reject(typeof e === 'object' && e !== null ? new KeeperRequestError(e.code, e.message) : new KeeperRequestError('error', typeof e === 'string' ? e : 'Keeper request failed'));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
request(method, params, timeoutMs) {
|
|
117
|
+
if (this.dead)
|
|
118
|
+
return Promise.reject(new KeeperConnectionLostError());
|
|
119
|
+
const id = this.nextId++;
|
|
120
|
+
let encoded;
|
|
121
|
+
try {
|
|
122
|
+
encoded = encodeFrame({ t: 'req', id, method, params });
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
return Promise.reject(error);
|
|
126
|
+
}
|
|
127
|
+
if (encoded.length > this.limits.in)
|
|
128
|
+
return Promise.reject(new KeeperFrameTooLargeError(encoded.length, this.limits.in));
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
const timer = setTimeout(() => { this.pending.delete(id); reject(new KeeperRequestTimeoutError(method)); }, timeoutMs);
|
|
131
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
132
|
+
this.socket.write(encoded.data);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
sendFrame(frame) { if (!this.dead)
|
|
136
|
+
this.socket.write(encodeFrame(frame).data); }
|
|
137
|
+
destroy(error) {
|
|
138
|
+
if (this.dead)
|
|
139
|
+
return;
|
|
140
|
+
this.dead = true;
|
|
141
|
+
for (const entry of this.pending.values()) {
|
|
142
|
+
clearTimeout(entry.timer);
|
|
143
|
+
entry.reject(error);
|
|
144
|
+
}
|
|
145
|
+
this.pending.clear();
|
|
146
|
+
this.socket.destroy();
|
|
147
|
+
this.onClose(error);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function connectLink(socketPath, limits, timeoutMs, onEvt, onClose) {
|
|
151
|
+
return new Promise((resolve, reject) => {
|
|
152
|
+
const socket = net.createConnection(socketPath);
|
|
153
|
+
let settled = false;
|
|
154
|
+
const timer = setTimeout(() => fail(new KeeperNotConnectedError('Timed out connecting to the keeper')), timeoutMs);
|
|
155
|
+
const fail = (error) => { if (settled)
|
|
156
|
+
return; settled = true; clearTimeout(timer); socket.destroy(); reject(error); };
|
|
157
|
+
socket.once('error', (error) => fail(new KeeperNotConnectedError(error.message)));
|
|
158
|
+
socket.once('connect', () => {
|
|
159
|
+
if (settled)
|
|
160
|
+
return;
|
|
161
|
+
settled = true;
|
|
162
|
+
clearTimeout(timer);
|
|
163
|
+
socket.removeAllListeners('error');
|
|
164
|
+
resolve(new Link(socket, limits, onEvt, onClose));
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* One request on a fresh connection (probe, diagnostics, graceful stop). `keeper.ping` needs no token; anything else
|
|
170
|
+
* passes `options.token`. This is the "one connection, one request" form the spec keeps for legacy probes.
|
|
171
|
+
*/
|
|
172
|
+
export async function keeperOneShot(socketPath, method, params = {}, timeoutMs = 2000, options = {}) {
|
|
173
|
+
const deadline = Date.now() + timeoutMs;
|
|
174
|
+
const link = await connectLink(socketPath, frameLimitsOf(options.kind), timeoutMs, () => { }, () => { });
|
|
175
|
+
try {
|
|
176
|
+
if (options.token !== undefined)
|
|
177
|
+
await link.request('keeper.hello', { token: options.token, daemonProtocol: options.daemonProtocol ?? [0, 0xffff] }, Math.max(1, deadline - Date.now()));
|
|
178
|
+
return await link.request(method, params, Math.max(1, deadline - Date.now()));
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
link.destroy(new KeeperClosedError());
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const subscribeParams = (sub) => ({ stream: sub.stream, from: sub.next, credit: sub.window, ...(sub.windowBytes !== undefined ? { creditBytes: sub.windowBytes } : {}) });
|
|
185
|
+
const DEFAULT_WINDOW = 256;
|
|
186
|
+
/**
|
|
187
|
+
* One long-lived duplex connection to one keeper generation. Reconnects on its own after a drop; requests are never
|
|
188
|
+
* retried (an in-flight one fails with `KeeperConnectionLostError`); stream subscriptions resume from the offset after the
|
|
189
|
+
* last record delivered. Emits `state` (KeeperClientState), `connected` (KeeperHello) and `disconnected` (Error).
|
|
190
|
+
*/
|
|
191
|
+
export class KeeperClient extends EventEmitter {
|
|
192
|
+
options;
|
|
193
|
+
link;
|
|
194
|
+
epoch = 0;
|
|
195
|
+
current = 'idle';
|
|
196
|
+
helloValue;
|
|
197
|
+
opening;
|
|
198
|
+
reconnectTimer;
|
|
199
|
+
backoff;
|
|
200
|
+
everConnected = false;
|
|
201
|
+
subs = new Map();
|
|
202
|
+
waiters = new Set();
|
|
203
|
+
constructor(options) {
|
|
204
|
+
super();
|
|
205
|
+
this.options = options;
|
|
206
|
+
this.backoff = options.reconnectMinMs ?? 50;
|
|
207
|
+
}
|
|
208
|
+
get state() { return this.current; }
|
|
209
|
+
get hello() { return this.helloValue; }
|
|
210
|
+
/** `'required'` when the connected keeper's protocol is outside the daemon's window. */
|
|
211
|
+
get replacement() { return this.current === 'replacement-required' ? 'required' : undefined; }
|
|
212
|
+
setState(state) { if (this.current !== state) {
|
|
213
|
+
this.current = state;
|
|
214
|
+
this.emit('state', state);
|
|
215
|
+
} }
|
|
216
|
+
get requestTimeout() { return this.options.requestTimeoutMs ?? 30_000; }
|
|
217
|
+
get connectTimeout() { return this.options.connectTimeoutMs ?? 3000; }
|
|
218
|
+
/** Connects and says hello once; rejects on failure. After a later drop the client reconnects by itself. */
|
|
219
|
+
connect() {
|
|
220
|
+
if (this.current === 'closed')
|
|
221
|
+
return Promise.reject(new KeeperClosedError());
|
|
222
|
+
if (this.link && !this.link.isDead && this.helloValue)
|
|
223
|
+
return Promise.resolve(this.resultOf(this.helloValue));
|
|
224
|
+
if (!this.opening) {
|
|
225
|
+
this.opening = this.openOnce().finally(() => { this.opening = undefined; });
|
|
226
|
+
this.opening.catch(() => { if (this.everConnected && this.current !== 'closed')
|
|
227
|
+
this.scheduleReconnect(); });
|
|
228
|
+
}
|
|
229
|
+
return this.opening;
|
|
230
|
+
}
|
|
231
|
+
resultOf(hello) {
|
|
232
|
+
return inProtocolWindow(this.options.daemonProtocol, hello.protocolVersion)
|
|
233
|
+
? { status: 'ready', hello }
|
|
234
|
+
: { status: 'replacement-required', replacement: 'required', hello, daemonProtocol: this.options.daemonProtocol };
|
|
235
|
+
}
|
|
236
|
+
async openOnce() {
|
|
237
|
+
this.setState('connecting');
|
|
238
|
+
let link;
|
|
239
|
+
try {
|
|
240
|
+
const token = await readKeeperToken(this.options.tokenFile);
|
|
241
|
+
const epoch = ++this.epoch;
|
|
242
|
+
link = await connectLink(this.options.socket, frameLimitsOf(this.options.kind), this.connectTimeout, (frame) => this.onEvt(epoch, frame), (error) => this.onLinkClosed(epoch, error));
|
|
243
|
+
this.link = link;
|
|
244
|
+
const hello = await link.request('keeper.hello', { token, daemonProtocol: [...this.options.daemonProtocol] }, this.connectTimeout);
|
|
245
|
+
if (hello.kind !== this.options.kind || (this.options.gen !== undefined && hello.gen !== this.options.gen) || !Number.isSafeInteger(hello.protocolVersion))
|
|
246
|
+
throw new KeeperRequestError('identity_mismatch', 'The keeper at this socket is not the expected kind or generation');
|
|
247
|
+
this.helloValue = hello;
|
|
248
|
+
this.everConnected = true;
|
|
249
|
+
this.backoff = this.options.reconnectMinMs ?? 50;
|
|
250
|
+
const result = this.resultOf(hello);
|
|
251
|
+
this.setState(result.status === 'ready' ? 'ready' : 'replacement-required');
|
|
252
|
+
this.emit('connected', hello);
|
|
253
|
+
this.flushWaiters();
|
|
254
|
+
if (result.status === 'ready')
|
|
255
|
+
this.resubscribeAll(link, epoch);
|
|
256
|
+
else
|
|
257
|
+
this.failSubscriptions(new KeeperReplacementRequiredError(hello, this.options.daemonProtocol));
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
if (link) {
|
|
262
|
+
this.link = undefined;
|
|
263
|
+
link.destroy(error instanceof Error ? error : new Error(String(error)));
|
|
264
|
+
}
|
|
265
|
+
this.helloValue = undefined;
|
|
266
|
+
if (this.current !== 'closed')
|
|
267
|
+
this.setState(this.everConnected ? 'connecting' : 'idle');
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
onLinkClosed(epoch, error) {
|
|
272
|
+
if (epoch !== this.epoch || this.current === 'closed')
|
|
273
|
+
return;
|
|
274
|
+
if (this.link) { // an established connection dropped (a failing hello is handled by openOnce)
|
|
275
|
+
this.link = undefined;
|
|
276
|
+
this.helloValue = undefined;
|
|
277
|
+
if (this.opening)
|
|
278
|
+
return;
|
|
279
|
+
this.setState('connecting');
|
|
280
|
+
this.emit('disconnected', error);
|
|
281
|
+
this.scheduleReconnect();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
scheduleReconnect() {
|
|
285
|
+
if (this.reconnectTimer || this.current === 'closed')
|
|
286
|
+
return;
|
|
287
|
+
const delay = this.backoff;
|
|
288
|
+
this.backoff = Math.min(this.options.reconnectMaxMs ?? 2000, this.backoff * 2);
|
|
289
|
+
this.reconnectTimer = setTimeout(() => {
|
|
290
|
+
this.reconnectTimer = undefined;
|
|
291
|
+
this.connect().catch(() => { });
|
|
292
|
+
}, delay);
|
|
293
|
+
this.reconnectTimer.unref();
|
|
294
|
+
}
|
|
295
|
+
flushWaiters() { for (const w of this.waiters) {
|
|
296
|
+
clearTimeout(w.timer);
|
|
297
|
+
w.resolve();
|
|
298
|
+
} this.waiters.clear(); }
|
|
299
|
+
async ready() {
|
|
300
|
+
if (this.current === 'closed')
|
|
301
|
+
throw new KeeperClosedError();
|
|
302
|
+
if (this.current === 'idle')
|
|
303
|
+
await this.connect();
|
|
304
|
+
else if (this.current === 'connecting') {
|
|
305
|
+
await new Promise((resolve, reject) => {
|
|
306
|
+
const waiter = { resolve, reject, timer: setTimeout(() => { this.waiters.delete(waiter); reject(new KeeperNotConnectedError('Keeper is reconnecting')); }, this.connectTimeout) };
|
|
307
|
+
this.waiters.add(waiter);
|
|
308
|
+
this.connect().catch(() => { }); // joins the attempt in flight or starts one
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
if (!this.link || this.link.isDead)
|
|
312
|
+
throw new KeeperNotConnectedError();
|
|
313
|
+
return this.link;
|
|
314
|
+
}
|
|
315
|
+
/** One request. Never retried: if the connection breaks while it is in flight it fails with `KeeperConnectionLostError`. */
|
|
316
|
+
async request(method, params = {}, options = {}) {
|
|
317
|
+
const link = await this.ready();
|
|
318
|
+
if (this.current === 'replacement-required' && !WINDOW_EXEMPT_METHODS.has(method))
|
|
319
|
+
throw new KeeperReplacementRequiredError(this.helloValue, this.options.daemonProtocol);
|
|
320
|
+
return await link.request(method, params, options.timeoutMs ?? this.requestTimeout);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Streams `stream` into `handler`, in order, with credit-based backpressure: the keeper pushes at most `window` records
|
|
324
|
+
* ahead of what `handler` has finished. After a reconnect the subscription continues from the offset after the last
|
|
325
|
+
* record delivered; nothing is delivered twice.
|
|
326
|
+
*/
|
|
327
|
+
async subscribe(stream, handler, options) {
|
|
328
|
+
const link = await this.ready();
|
|
329
|
+
if (this.current === 'replacement-required')
|
|
330
|
+
throw new KeeperReplacementRequiredError(this.helloValue, this.options.daemonProtocol);
|
|
331
|
+
const sub = {
|
|
332
|
+
stream, handler, onClosed: options.onClosed, window: options.window ?? DEFAULT_WINDOW,
|
|
333
|
+
...(options.windowBytes !== undefined ? { windowBytes: options.windowBytes } : {}),
|
|
334
|
+
next: options.from, chain: Promise.resolve(), cancelled: false, epoch: this.epoch,
|
|
335
|
+
};
|
|
336
|
+
const previous = this.subs.get(stream);
|
|
337
|
+
if (previous)
|
|
338
|
+
previous.cancelled = true;
|
|
339
|
+
this.subs.set(stream, sub); // before the request: records may arrive right behind the reply
|
|
340
|
+
try {
|
|
341
|
+
await link.request('keeper.subscribe', subscribeParams(sub), this.requestTimeout);
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
if (this.subs.get(stream) === sub)
|
|
345
|
+
this.subs.delete(stream);
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
const owner = this;
|
|
349
|
+
return {
|
|
350
|
+
stream,
|
|
351
|
+
get nextOffset() { return sub.next; },
|
|
352
|
+
async unsubscribe() {
|
|
353
|
+
sub.cancelled = true;
|
|
354
|
+
if (owner.subs.get(stream) === sub)
|
|
355
|
+
owner.subs.delete(stream);
|
|
356
|
+
if (owner.link && !owner.link.isDead)
|
|
357
|
+
await owner.link.request('keeper.unsubscribe', { stream }, owner.requestTimeout).catch(() => { });
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
onEvt(epoch, frame) {
|
|
362
|
+
if (epoch !== this.epoch)
|
|
363
|
+
return;
|
|
364
|
+
const sub = this.subs.get(frame.stream);
|
|
365
|
+
if (!sub || sub.cancelled)
|
|
366
|
+
return;
|
|
367
|
+
if (frame.closed !== undefined) {
|
|
368
|
+
this.subs.delete(frame.stream);
|
|
369
|
+
sub.cancelled = true;
|
|
370
|
+
sub.onClosed?.(new KeeperRequestError(frame.closed, `Keeper ended stream ${frame.stream}: ${frame.closed}`));
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const received = frame.records.length;
|
|
374
|
+
const fresh = frame.records.filter((r) => r.o >= sub.next);
|
|
375
|
+
if (fresh.length > 0)
|
|
376
|
+
sub.next = fresh[fresh.length - 1].o + 1;
|
|
377
|
+
sub.chain = sub.chain.then(async () => {
|
|
378
|
+
if (sub.cancelled)
|
|
379
|
+
return;
|
|
380
|
+
if (fresh.length > 0)
|
|
381
|
+
await sub.handler(fresh);
|
|
382
|
+
if (!sub.cancelled && this.epoch === epoch) {
|
|
383
|
+
this.link?.sendFrame({ t: 'credit', stream: sub.stream, n: received, ...(typeof frame.bytes === 'number' ? { bytes: frame.bytes } : {}) });
|
|
384
|
+
}
|
|
385
|
+
}).catch((error) => {
|
|
386
|
+
this.subs.delete(sub.stream);
|
|
387
|
+
sub.cancelled = true;
|
|
388
|
+
sub.onClosed?.(error instanceof Error ? error : new Error(String(error)));
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
/** Live subscriptions cannot continue on a keeper that now needs replacing: tell their owners instead of leaving them silent. */
|
|
392
|
+
failSubscriptions(error) {
|
|
393
|
+
for (const sub of [...this.subs.values()]) {
|
|
394
|
+
sub.cancelled = true;
|
|
395
|
+
sub.onClosed?.(error);
|
|
396
|
+
}
|
|
397
|
+
this.subs.clear();
|
|
398
|
+
}
|
|
399
|
+
resubscribeAll(link, epoch) {
|
|
400
|
+
for (const sub of [...this.subs.values()]) {
|
|
401
|
+
sub.epoch = epoch;
|
|
402
|
+
link.request('keeper.subscribe', subscribeParams(sub), this.requestTimeout).catch((error) => {
|
|
403
|
+
if (this.subs.get(sub.stream) !== sub || this.epoch !== epoch)
|
|
404
|
+
return; // superseded, or the link died again (the next reconnect resumes it)
|
|
405
|
+
if (error instanceof KeeperConnectionLostError || error instanceof KeeperRequestTimeoutError)
|
|
406
|
+
return;
|
|
407
|
+
this.subs.delete(sub.stream);
|
|
408
|
+
sub.cancelled = true;
|
|
409
|
+
sub.onClosed?.(error instanceof Error ? error : new Error(String(error)));
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
close() {
|
|
414
|
+
if (this.current === 'closed')
|
|
415
|
+
return;
|
|
416
|
+
this.setState('closed');
|
|
417
|
+
clearTimeout(this.reconnectTimer);
|
|
418
|
+
this.reconnectTimer = undefined;
|
|
419
|
+
for (const sub of this.subs.values())
|
|
420
|
+
sub.cancelled = true;
|
|
421
|
+
this.subs.clear();
|
|
422
|
+
for (const w of this.waiters) {
|
|
423
|
+
clearTimeout(w.timer);
|
|
424
|
+
w.reject(new KeeperClosedError());
|
|
425
|
+
}
|
|
426
|
+
this.waiters.clear();
|
|
427
|
+
const link = this.link;
|
|
428
|
+
this.link = undefined;
|
|
429
|
+
this.helloValue = undefined;
|
|
430
|
+
link?.destroy(new KeeperClosedError());
|
|
431
|
+
}
|
|
432
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export const KEEPER_KINDS = ['terminal', 'acp'];
|
|
4
|
+
export const KEEPER_NAMESPACE = 'keeper/1';
|
|
5
|
+
/** sha256(JSON[resolve(configDir), workstationId, server, 'keeper/1', kind])[:32]. Spec §3.1. */
|
|
6
|
+
export function keeperIdentity(input, kind) {
|
|
7
|
+
return createHash('sha256')
|
|
8
|
+
.update(JSON.stringify([path.resolve(input.configDir), input.workstationId, input.server, KEEPER_NAMESPACE, kind]))
|
|
9
|
+
.digest('hex')
|
|
10
|
+
.slice(0, 32);
|
|
11
|
+
}
|
|
12
|
+
/** A "generation" is the first 8 hex digits of the keeper build; two generations of one kind may coexist. */
|
|
13
|
+
export function keeperGen(buildVersion) {
|
|
14
|
+
if (!/^[0-9a-f]{8,}$/.test(buildVersion))
|
|
15
|
+
throw new Error('Invalid keeper buildVersion');
|
|
16
|
+
return buildVersion.slice(0, 8);
|
|
17
|
+
}
|
|
18
|
+
/** systemd unit name (Linux) and the human-readable id of a keeper generation. */
|
|
19
|
+
export function keeperUnit(kind, identity, gen) {
|
|
20
|
+
return `axom-keeper-${kind}-${identity}-${gen}`;
|
|
21
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export class FrameError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(message, code) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.name = 'FrameError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
const NEWLINE = 0x0a;
|
|
10
|
+
/**
|
|
11
|
+
* Incremental line splitter for ONE socket. It must never be shared between connections: a torn line from
|
|
12
|
+
* one peer would otherwise be completed by bytes of another. Chunks are only concatenated once a full line
|
|
13
|
+
* is present, and a line longer than `maxBytes` (newline excluded) is rejected as soon as it is seen, even
|
|
14
|
+
* before its newline arrives, so a hostile peer cannot make the buffer grow without bound.
|
|
15
|
+
*/
|
|
16
|
+
export class LineDecoder {
|
|
17
|
+
maxBytes;
|
|
18
|
+
chunks = [];
|
|
19
|
+
pending = 0;
|
|
20
|
+
constructor(maxBytes) {
|
|
21
|
+
this.maxBytes = maxBytes;
|
|
22
|
+
}
|
|
23
|
+
/** Changes the cap for lines not yet completed (used once a peer has authenticated). */
|
|
24
|
+
setMax(maxBytes) { this.maxBytes = maxBytes; }
|
|
25
|
+
/** Bytes of an incomplete trailing line currently buffered. */
|
|
26
|
+
get buffered() { return this.pending; }
|
|
27
|
+
push(chunk) {
|
|
28
|
+
const lines = [];
|
|
29
|
+
let start = 0;
|
|
30
|
+
for (;;) {
|
|
31
|
+
const newline = chunk.indexOf(NEWLINE, start);
|
|
32
|
+
if (newline < 0)
|
|
33
|
+
break;
|
|
34
|
+
const tail = chunk.subarray(start, newline);
|
|
35
|
+
const length = this.pending + tail.length;
|
|
36
|
+
if (length > this.maxBytes)
|
|
37
|
+
throw new FrameError(`Frame exceeds ${this.maxBytes} bytes`, 'too_large');
|
|
38
|
+
lines.push(this.chunks.length === 0 ? Buffer.from(tail) : Buffer.concat([...this.chunks, tail], length));
|
|
39
|
+
this.chunks = [];
|
|
40
|
+
this.pending = 0;
|
|
41
|
+
start = newline + 1;
|
|
42
|
+
}
|
|
43
|
+
if (start < chunk.length) {
|
|
44
|
+
const rest = chunk.subarray(start);
|
|
45
|
+
if (this.pending + rest.length > this.maxBytes)
|
|
46
|
+
throw new FrameError(`Frame exceeds ${this.maxBytes} bytes`, 'too_large');
|
|
47
|
+
this.chunks.push(Buffer.from(rest));
|
|
48
|
+
this.pending += rest.length;
|
|
49
|
+
}
|
|
50
|
+
return lines;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** A frame as bytes including the terminating newline. `length` is the line length without it (what the cap applies to). */
|
|
54
|
+
export function encodeFrame(frame) {
|
|
55
|
+
const body = Buffer.from(JSON.stringify(frame), 'utf8');
|
|
56
|
+
return { data: Buffer.concat([body, Buffer.from('\n')]), length: body.length };
|
|
57
|
+
}
|
|
58
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
59
|
+
const isId = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
60
|
+
const malformed = (what) => { throw new FrameError(`Malformed frame: ${what}`, 'malformed'); };
|
|
61
|
+
function parseJsonObject(line) {
|
|
62
|
+
let value;
|
|
63
|
+
try {
|
|
64
|
+
value = JSON.parse(line.toString('utf8'));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return malformed('not JSON');
|
|
68
|
+
}
|
|
69
|
+
return isRecord(value) ? value : malformed('not an object');
|
|
70
|
+
}
|
|
71
|
+
/** A frame the keeper may receive. Throws `FrameError` for anything else; the caller destroys that connection. */
|
|
72
|
+
export function parseServerBound(line) {
|
|
73
|
+
const frame = parseJsonObject(line);
|
|
74
|
+
if (frame.t === 'req') {
|
|
75
|
+
if (!isId(frame.id))
|
|
76
|
+
malformed('req id');
|
|
77
|
+
if (typeof frame.method !== 'string' || frame.method.length === 0 || frame.method.length > 128)
|
|
78
|
+
malformed('req method');
|
|
79
|
+
if (frame.params !== undefined && !isRecord(frame.params))
|
|
80
|
+
malformed('req params');
|
|
81
|
+
return frame;
|
|
82
|
+
}
|
|
83
|
+
if (frame.t === 'credit') {
|
|
84
|
+
if (typeof frame.stream !== 'string' || frame.stream.length === 0)
|
|
85
|
+
malformed('credit stream');
|
|
86
|
+
if (!Number.isSafeInteger(frame.n) || frame.n <= 0)
|
|
87
|
+
malformed('credit n');
|
|
88
|
+
if (frame.bytes !== undefined && (!Number.isSafeInteger(frame.bytes) || frame.bytes < 0))
|
|
89
|
+
malformed('credit bytes');
|
|
90
|
+
return frame;
|
|
91
|
+
}
|
|
92
|
+
return malformed('unexpected frame type');
|
|
93
|
+
}
|
|
94
|
+
/** A frame the daemon may receive. */
|
|
95
|
+
export function parseClientBound(line) {
|
|
96
|
+
const frame = parseJsonObject(line);
|
|
97
|
+
if (frame.t === 'res') {
|
|
98
|
+
if (!isId(frame.id))
|
|
99
|
+
malformed('res id');
|
|
100
|
+
if (typeof frame.ok !== 'boolean')
|
|
101
|
+
malformed('res ok');
|
|
102
|
+
return frame;
|
|
103
|
+
}
|
|
104
|
+
if (frame.t === 'evt') {
|
|
105
|
+
if (typeof frame.stream !== 'string')
|
|
106
|
+
malformed('evt stream');
|
|
107
|
+
if (!Array.isArray(frame.records) || !frame.records.every((r) => isRecord(r) && Number.isSafeInteger(r.o)))
|
|
108
|
+
malformed('evt records');
|
|
109
|
+
return frame;
|
|
110
|
+
}
|
|
111
|
+
return malformed('unexpected frame type');
|
|
112
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function launchConfigOf(paths, buildVersion, bundleDir, umask) {
|
|
2
|
+
return {
|
|
3
|
+
kind: paths.kind, identity: paths.identity, gen: paths.gen, unit: paths.unit, buildVersion, bundleDir,
|
|
4
|
+
runDir: paths.runDir, dataDir: paths.dataDir, socket: paths.socket, pidFile: paths.pidFile, tokenFile: paths.tokenFile, umask,
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export const encodeLaunchConfig = (config) => Buffer.from(JSON.stringify(config)).toString('base64');
|
|
8
|
+
export const decodeLaunchConfig = (encoded) => JSON.parse(Buffer.from(encoded, 'base64').toString('utf8'));
|