@platless/axom-cli 0.5.9 → 0.6.52
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 +25 -6
- package/dist/axom-patch/cli/README.md +129 -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/claude-replay-lifecycle.json +16 -0
- 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/claude-replay-lifecycle.mjs +31 -0
- 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/probes/zcode-env-account.mjs +81 -0
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
- package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1687 -0
- package/dist/axom-patch/cli/qoder/releases/1.1.75.json +277 -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 +19 -19
- package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
- package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
- package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
- package/dist/axom-patch/cli/zcode-env-account.json +58 -0
- package/dist/axom-patch/cli/zcode-models.json +9 -10
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -11
- package/dist/cli.js +11 -45
- package/dist/commands/auth.js +4 -1
- package/dist/commands/knowledge.js +47 -0
- package/dist/commands/update.js +126 -3
- package/dist/commands/workstation.js +101 -164
- package/dist/commands/workstationRegistrations.js +116 -0
- package/dist/credentialBroker.js +13 -2
- package/dist/credentials.js +16 -0
- package/dist/install/claude/commands.js +15 -15
- package/dist/keepers/terminal/keeper.mjs +1089 -673
- package/dist/keepers/terminal/manifest.json +4 -4
- package/dist/mcp.js +1 -1
- package/dist/native/darwin-x64/axom-quic +0 -0
- package/dist/workstation/acp/acpAgents.js +113 -102
- 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/outbox.js +12 -5
- package/dist/workstation/acp/patchAssets.js +40 -0
- package/dist/workstation/acp/permission.js +2 -2
- package/dist/workstation/acp/processor/checkpoint.js +26 -34
- package/dist/workstation/acp/processor/control.js +7 -80
- package/dist/workstation/acp/processor/jsonrpc.js +8 -0
- package/dist/workstation/acp/processor/runtime.js +5 -81
- package/dist/workstation/acp/processor/session.js +54 -82
- package/dist/workstation/acp/processor/state.js +7 -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/transport/local.js +19 -3
- package/dist/workstation/acp/processor/updates.js +156 -105
- package/dist/workstation/acp/processorSession.js +20 -173
- 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 +32 -3
- 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 +114 -166
- package/dist/workstation/channels/agent.js +9 -1
- package/dist/workstation/channels/computer.js +27 -4
- package/dist/workstation/channels/management.js +130 -15
- package/dist/workstation/channels/url.js +1 -1
- package/dist/workstation/config.js +33 -9
- package/dist/workstation/connection/encryptedChannel.js +4 -1
- package/dist/workstation/control/bindingStore.js +204 -0
- package/dist/workstation/control/commands.js +125 -0
- package/dist/workstation/control/daemonIdentity.js +143 -0
- package/dist/workstation/control/deviceCommands.js +191 -0
- package/dist/workstation/control/deviceProtocol.js +68 -0
- package/dist/workstation/control/enrollment.js +106 -0
- package/dist/workstation/control/inspect.js +71 -0
- package/dist/workstation/control/locks.js +110 -0
- package/dist/workstation/control/protocol.js +40 -0
- package/dist/workstation/control/restartGuard.js +46 -0
- package/dist/workstation/control/rootFormat.js +16 -0
- package/dist/workstation/control/runtimeClient.js +152 -0
- package/dist/workstation/control/runtimeHost.js +235 -0
- package/dist/workstation/control/serverIdentity.js +35 -0
- package/dist/workstation/control/updateTransition.js +91 -0
- package/dist/workstation/daemon.js +266 -264
- package/dist/workstation/daemonctl.js +226 -25
- package/dist/workstation/desktopControl/audio.js +99 -0
- package/dist/workstation/desktopControl/automationDevice.js +219 -0
- package/dist/workstation/desktopControl/automationJson.js +74 -0
- package/dist/workstation/desktopControl/automationLocalState.js +23 -0
- package/dist/workstation/desktopControl/automationProtocol.js +253 -0
- package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
- package/dist/workstation/desktopControl/automationSession.js +81 -0
- package/dist/workstation/desktopControl/automationTransport.js +138 -0
- package/dist/workstation/desktopControl/capabilities.js +93 -0
- package/dist/workstation/desktopControl/captureModes.js +9 -0
- package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
- package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
- package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
- package/dist/workstation/desktopControl/deviceSockets.js +246 -0
- package/dist/workstation/desktopControl/helperProcess.js +602 -0
- package/dist/workstation/desktopControl/hostGrant.js +65 -0
- package/dist/workstation/desktopControl/lobby.js +131 -0
- package/dist/workstation/desktopControl/macProvider.js +156 -0
- package/dist/workstation/desktopControl/media.js +331 -0
- package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
- package/dist/workstation/desktopControl/nativeProvider.js +1044 -0
- package/dist/workstation/desktopControl/networkEvidence.js +43 -0
- package/dist/workstation/desktopControl/overview.js +58 -0
- package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
- package/dist/workstation/desktopControl/protocol.js +311 -0
- package/dist/workstation/desktopControl/provider.js +1 -0
- package/dist/workstation/desktopControl/quicMedia.js +240 -0
- package/dist/workstation/desktopControl/rateController.js +226 -0
- package/dist/workstation/desktopControl/supervisor.js +1774 -0
- package/dist/workstation/desktopControl/transport.js +27 -0
- package/dist/workstation/desktopControl/wallpaper.js +98 -0
- package/dist/workstation/exec/policy.js +5 -0
- package/dist/workstation/files/client.js +10 -1
- package/dist/workstation/files/credentials.js +142 -0
- package/dist/workstation/files/host.js +9 -2
- package/dist/workstation/files/text.js +42 -8
- package/dist/workstation/journal.js +5 -2
- package/dist/workstation/keepers/acp/client.js +38 -2
- package/dist/workstation/keepers/kit/buildVersion.js +35 -11
- package/dist/workstation/keepers/kit/bundle.js +21 -17
- package/dist/workstation/keepers/kit/client.js +2 -2
- package/dist/workstation/keepers/kit/launcher.js +123 -7
- package/dist/workstation/keepers/kit/paths.js +2 -1
- package/dist/workstation/keepers/legacy.js +16 -2
- package/dist/workstation/keepers/policy.js +5 -34
- package/dist/workstation/keepers/terminal/client.js +115 -23
- package/dist/workstation/keepers/terminal/exec.js +6 -3
- package/dist/workstation/keepers/terminal/frames.js +73 -0
- package/dist/workstation/keepers/terminal/generations.js +5 -8
- package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
- package/dist/workstation/keepers/terminal/input.js +146 -0
- package/dist/workstation/keepers/terminal/keeper.js +22 -8
- package/dist/workstation/keepers/terminal/main.js +1 -1
- package/dist/workstation/keepers/terminal/metadata.js +138 -0
- package/dist/workstation/keepers/terminal/protocol.js +4 -28
- package/dist/workstation/keepers/terminal/pty.js +1 -330
- package/dist/workstation/keepers/terminal/screen.js +246 -0
- package/dist/workstation/keepers/terminal/sessions.js +526 -0
- package/dist/workstation/managedInstall.js +57 -0
- package/dist/workstation/oauth/deviceAuthorization.js +31 -17
- package/dist/workstation/protocol/agent.js +2 -15
- package/dist/workstation/protocol/management.js +113 -5
- package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
- package/dist/workstation/rd/artifact.js +120 -0
- package/dist/workstation/rd/control.js +117 -0
- package/dist/workstation/rd/display.js +29 -0
- package/dist/workstation/rd/host.js +213 -0
- package/dist/workstation/rd/migrationReadiness.js +46 -0
- package/dist/workstation/rd/provider.js +256 -0
- package/dist/workstation/rd/sessions.js +391 -0
- package/dist/workstation/rd/state.js +57 -0
- package/dist/workstation/rd/update.js +168 -0
- package/dist/workstation/rd/upkeep.js +80 -0
- package/dist/workstation/register.js +27 -37
- package/dist/workstation/registrations/enrollment.js +250 -0
- package/dist/workstation/registrations/enrollmentPending.js +361 -0
- package/dist/workstation/registrations/migration.js +356 -0
- package/dist/workstation/registrations/model.js +34 -0
- package/dist/workstation/registrations/removal.js +222 -0
- package/dist/workstation/registrations/store.js +737 -0
- package/dist/workstation/runtime/desktopBinding.js +20 -0
- package/dist/workstation/runtime/deviceDesktop.js +193 -0
- package/dist/workstation/runtime/deviceRuntime.js +568 -0
- package/dist/workstation/runtime/deviceServices.js +117 -0
- package/dist/workstation/runtime/events.js +20 -0
- package/dist/workstation/runtime/ownership.js +100 -0
- package/dist/workstation/runtime/registrationRuntime.js +575 -0
- package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
- package/dist/workstation/runtime/removalActivity.js +184 -0
- package/dist/workstation/service/installer.js +197 -14
- package/dist/workstation/service/launchd.js +2 -0
- package/dist/workstation/service/systemd.js +1 -0
- package/dist/workstation/storageMetrics.js +118 -0
- package/dist/workstation/systemMetrics.js +47 -92
- package/dist/workstation/toolchain.js +94 -0
- package/dist/workstation/tools/browser.js +28 -4
- package/dist/workstation/tools/client.js +8 -27
- package/dist/workstation/tools/info.js +6 -13
- package/dist/workstation/tools/terminalRouter.js +17 -172
- package/dist/workstation/tools/validation.js +55 -1
- package/dist/workstation/update/archiveEvidence.js +206 -0
- package/dist/workstation/update/artifact.js +2 -1
- package/dist/workstation/update/coordinator.js +34 -13
- package/dist/workstation/update/deviceCompatibility.js +119 -0
- package/dist/workstation/update/deviceCoordinator.js +369 -0
- package/dist/workstation/update/deviceHealth.js +104 -0
- package/dist/workstation/update/deviceHealthProtocol.js +26 -0
- package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
- package/dist/workstation/update/deviceRollback.js +78 -0
- package/dist/workstation/update/globalInstall.js +186 -0
- package/dist/workstation/update/legacyBridge.js +414 -0
- package/dist/workstation/update/legacyOwner.js +39 -0
- package/dist/workstation/update/npmInstaller.js +51 -25
- package/dist/workstation/update/privateDurability.js +61 -0
- package/dist/workstation/update/rdRollback.js +145 -0
- package/dist/workstation/update/recoveryPlan.js +124 -0
- package/dist/workstation/update/runUpdate.js +10 -3
- package/dist/workstation/update/taskRegistry.js +161 -3
- package/dist/workstation/update/watchdog.js +29806 -333
- package/dist/workstation/update/watchdogProcess.js +72 -0
- package/package.json +11 -7
- 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/automationPreparation.js} +0 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { constants, promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash, createPrivateKey, createPublicKey, randomUUID } from 'node:crypto';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { assertLockOwnership } from '../control/locks.js';
|
|
6
|
+
import { LocalControlError } from '../control/protocol.js';
|
|
7
|
+
import { registrationKey, registrationServerSchema } from './model.js';
|
|
8
|
+
const bounded = (maximum, empty = false) => z.string().max(maximum).refine(value => !/[\x00-\x1f\x7f-\x9f]/.test(value) && (empty || value.trim().length > 0));
|
|
9
|
+
const name = bounded(128).regex(/^[A-Za-z0-9._-]+$/).refine(value => value !== '.' && value !== '..');
|
|
10
|
+
export const keySchema = z.string().regex(/^[A-Za-z0-9+/]{43}=$/).refine(value => Buffer.from(value, 'base64').length === 32 && Buffer.from(value, 'base64').toString('base64') === value);
|
|
11
|
+
export const workspaceSchema = z.object({ name, cwd: bounded(4096).refine(value => path.isAbsolute(value)), shell: bounded(4096) }).strict();
|
|
12
|
+
export const operationIdSchema = bounded(128);
|
|
13
|
+
export const requestSchema = z.object({
|
|
14
|
+
protocol: z.literal(2), operationId: operationIdSchema, proposedWorkstationId: z.string().uuid(), clientPublicKey: keySchema,
|
|
15
|
+
name, os: z.object({ name: bounded(1024), version: bounded(1024, true), arch: bounded(1024), hostname: bounded(1024, true) }).strict(),
|
|
16
|
+
defaultWorkspace: workspaceSchema.optional(), shell: bounded(4096),
|
|
17
|
+
acquisition: z.discriminatedUnion('kind', [z.object({ kind: z.literal('create') }).strict(), z.object({ kind: z.literal('reattach'), expectedVersion: z.number().int().min(1).max(2147483646) }).strict()]),
|
|
18
|
+
}).strict();
|
|
19
|
+
export const registrationEnrollmentInputSchema = z.object({ server: z.string(), accessToken: z.string().min(1).max(32_768).refine(value => !/[\r\n]/.test(value)), expectedOwnerUserId: z.string().min(1).max(1024).optional(), operationId: operationIdSchema, defaults: z.object({ name: requestSchema.shape.name, shell: requestSchema.shape.shell, workspace: workspaceSchema.optional() }).strict() }).strict();
|
|
20
|
+
const targetSchema = z.object({ schemaVersion: z.literal(1), server: registrationServerSchema, ownerUserId: bounded(1024), registrationId: z.string().uuid(), requestDigest: z.string().regex(/^[a-f0-9]{64}$/) }).strict();
|
|
21
|
+
const pendingSchema = targetSchema.extend({ state: z.literal('pending'), request: requestSchema, clientPrivateKey: keySchema }).strict();
|
|
22
|
+
const completedSchema = targetSchema.extend({ state: z.literal('completed'), registrationRevision: z.string().regex(/^[a-f0-9]{64}$/), workstationId: z.string().uuid() }).strict();
|
|
23
|
+
const envelopeSchema = z.discriminatedUnion('state', [pendingSchema, completedSchema]);
|
|
24
|
+
export const enrollmentStorageError = () => new LocalControlError('CORRUPT_IDENTITY', 'Enrollment recovery storage needs verification');
|
|
25
|
+
export const requestDigest = (request) => createHash('sha256').update(JSON.stringify(requestSchema.parse(request))).digest('hex');
|
|
26
|
+
const missing = (error) => error.code === 'ENOENT';
|
|
27
|
+
export async function enrollmentDirectory(root, directory = root, create = true) {
|
|
28
|
+
const relative = path.relative(root, directory);
|
|
29
|
+
if (relative.startsWith('..') || path.isAbsolute(relative))
|
|
30
|
+
throw enrollmentStorageError();
|
|
31
|
+
let current = path.parse(root).root;
|
|
32
|
+
for (const component of directory.slice(current.length).split(path.sep).filter(Boolean)) {
|
|
33
|
+
current = path.join(current, component);
|
|
34
|
+
const privatePath = current === root || current.startsWith(root + path.sep);
|
|
35
|
+
if (create) {
|
|
36
|
+
try {
|
|
37
|
+
await fs.mkdir(current, { mode: 0o700 });
|
|
38
|
+
const parent = await fs.open(path.dirname(current), constants.O_RDONLY);
|
|
39
|
+
try {
|
|
40
|
+
await parent.sync();
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await parent.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error.code !== 'EEXIST')
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const stat = await fs.lstat(current);
|
|
52
|
+
const trustedSystemLink = !privatePath && stat.isSymbolicLink() && stat.uid === 0;
|
|
53
|
+
if ((!stat.isDirectory() && !trustedSystemLink) || (privatePath && stat.isSymbolicLink()) || (process.getuid && stat.uid !== process.getuid() && (privatePath || stat.uid !== 0)))
|
|
54
|
+
throw enrollmentStorageError();
|
|
55
|
+
if (privatePath ? (stat.mode & 0o077) !== 0 : !trustedSystemLink && (stat.mode & 0o022) !== 0 && !(stat.uid === 0 && (stat.mode & 0o1000) !== 0))
|
|
56
|
+
throw enrollmentStorageError();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export async function enrollmentFile(root, file, limit = 65_536) {
|
|
60
|
+
await enrollmentDirectory(root, path.dirname(file), false);
|
|
61
|
+
const handle = await fs.open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
62
|
+
try {
|
|
63
|
+
const stat = await handle.stat();
|
|
64
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size > limit || (stat.mode & 0o077) !== 0 || (process.getuid && stat.uid !== process.getuid()))
|
|
65
|
+
throw enrollmentStorageError();
|
|
66
|
+
const buffer = Buffer.alloc(stat.size + 1);
|
|
67
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
68
|
+
if (bytesRead !== stat.size)
|
|
69
|
+
throw enrollmentStorageError();
|
|
70
|
+
return buffer.subarray(0, bytesRead);
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await handle.close();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function validate(value, server, ownerUserId) {
|
|
77
|
+
const envelope = envelopeSchema.parse(value);
|
|
78
|
+
if (envelope.server !== server || envelope.ownerUserId !== ownerUserId)
|
|
79
|
+
throw enrollmentStorageError();
|
|
80
|
+
if (envelope.state === 'pending') {
|
|
81
|
+
if (requestDigest(envelope.request) !== envelope.requestDigest)
|
|
82
|
+
throw enrollmentStorageError();
|
|
83
|
+
const privateKey = createPrivateKey({ key: Buffer.concat([Buffer.from('302e020100300506032b656e04220420', 'hex'), Buffer.from(envelope.clientPrivateKey, 'base64')]), format: 'der', type: 'pkcs8' });
|
|
84
|
+
const publicKey = createPublicKey(privateKey).export({ format: 'der', type: 'spki' }).subarray(-32).toString('base64');
|
|
85
|
+
if (publicKey !== envelope.request.clientPublicKey)
|
|
86
|
+
throw enrollmentStorageError();
|
|
87
|
+
}
|
|
88
|
+
return envelope;
|
|
89
|
+
}
|
|
90
|
+
function publicationTransition(previous, next) {
|
|
91
|
+
if (JSON.stringify(previous) === JSON.stringify(next))
|
|
92
|
+
return 'same';
|
|
93
|
+
if (!previous)
|
|
94
|
+
return next.state === 'pending' ? 'create' : null;
|
|
95
|
+
if (previous.state === 'pending' && next.state === 'completed' && previous.registrationId === next.registrationId && previous.requestDigest === next.requestDigest && previous.request.proposedWorkstationId === next.workstationId)
|
|
96
|
+
return 'committed';
|
|
97
|
+
if (previous.state === 'completed' && next.state === 'pending' && previous.registrationId !== next.registrationId)
|
|
98
|
+
return 'removed';
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
function publicationStem(name) {
|
|
102
|
+
const final = /^([a-f0-9]{64})\.json$/.exec(name);
|
|
103
|
+
if (final)
|
|
104
|
+
return final[1];
|
|
105
|
+
const candidate = /^([a-f0-9]{64})\.write-(.+)$/.exec(name);
|
|
106
|
+
if (!candidate || !z.string().uuid().safeParse(candidate[2]).success)
|
|
107
|
+
throw enrollmentStorageError();
|
|
108
|
+
return candidate[1];
|
|
109
|
+
}
|
|
110
|
+
async function readPublications(root, stem, names, target) {
|
|
111
|
+
const directory = path.join(root, 'enrollment-pending');
|
|
112
|
+
const parse = (value) => {
|
|
113
|
+
const parsed = envelopeSchema.parse(value);
|
|
114
|
+
const envelope = validate(parsed, target?.server ?? parsed.server, target?.ownerUserId ?? parsed.ownerUserId);
|
|
115
|
+
if (createHash('sha256').update(registrationKey(envelope.server, envelope.ownerUserId)).digest('hex') !== stem)
|
|
116
|
+
throw enrollmentStorageError();
|
|
117
|
+
return envelope;
|
|
118
|
+
};
|
|
119
|
+
let final = null;
|
|
120
|
+
try {
|
|
121
|
+
final = parse(JSON.parse((await enrollmentFile(root, path.join(directory, stem + '.json'))).toString()));
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (!missing(error))
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
const candidates = names.filter(name => name.startsWith(stem + '.write-'));
|
|
128
|
+
if (candidates.length > 16)
|
|
129
|
+
throw enrollmentStorageError();
|
|
130
|
+
const valid = [], incomplete = [];
|
|
131
|
+
for (const name of candidates) {
|
|
132
|
+
if (publicationStem(name) !== stem)
|
|
133
|
+
throw enrollmentStorageError();
|
|
134
|
+
const file = path.join(directory, name), buffer = await enrollmentFile(root, file);
|
|
135
|
+
let value;
|
|
136
|
+
try {
|
|
137
|
+
value = JSON.parse(buffer.toString());
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (!(error instanceof SyntaxError))
|
|
141
|
+
throw error;
|
|
142
|
+
incomplete.push(file);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
valid.push({ file, envelope: parse(value) });
|
|
146
|
+
}
|
|
147
|
+
if (valid.some(candidate => JSON.stringify(candidate.envelope) !== JSON.stringify(valid[0].envelope)))
|
|
148
|
+
throw enrollmentStorageError();
|
|
149
|
+
if (valid[0] && !publicationTransition(final, valid[0].envelope))
|
|
150
|
+
throw enrollmentStorageError();
|
|
151
|
+
return { final, valid, incomplete };
|
|
152
|
+
}
|
|
153
|
+
export async function inspectPendingEnrollments(root) {
|
|
154
|
+
const directory = path.join(root, 'enrollment-pending');
|
|
155
|
+
try {
|
|
156
|
+
try {
|
|
157
|
+
await enrollmentDirectory(root, directory, false);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
if (missing(error))
|
|
161
|
+
return [];
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
const names = await fs.readdir(directory);
|
|
165
|
+
if (names.length > 1024)
|
|
166
|
+
throw enrollmentStorageError();
|
|
167
|
+
const pending = [];
|
|
168
|
+
for (const stem of new Set(names.map(publicationStem))) {
|
|
169
|
+
const publications = await readPublications(root, stem, names);
|
|
170
|
+
const envelope = publications.final?.state === 'pending' ? publications.final : publications.valid[0]?.envelope;
|
|
171
|
+
if (envelope?.state === 'pending')
|
|
172
|
+
pending.push({ operationId: envelope.request.operationId, registrationId: envelope.registrationId, target: { server: envelope.server, ownerUserId: envelope.ownerUserId } });
|
|
173
|
+
else if (publications.incomplete.length)
|
|
174
|
+
pending.push({ operationId: stem, registrationId: null, target: null });
|
|
175
|
+
}
|
|
176
|
+
return pending;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
throw enrollmentStorageError();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
export class EnrollmentPendingStore {
|
|
183
|
+
root;
|
|
184
|
+
server;
|
|
185
|
+
ownerUserId;
|
|
186
|
+
proof;
|
|
187
|
+
afterStage;
|
|
188
|
+
directory;
|
|
189
|
+
file;
|
|
190
|
+
stem;
|
|
191
|
+
constructor(root, server, ownerUserId, proof, afterStage) {
|
|
192
|
+
this.root = root;
|
|
193
|
+
this.server = server;
|
|
194
|
+
this.ownerUserId = ownerUserId;
|
|
195
|
+
this.proof = proof;
|
|
196
|
+
this.afterStage = afterStage;
|
|
197
|
+
this.directory = path.join(root, 'enrollment-pending');
|
|
198
|
+
this.stem = createHash('sha256').update(registrationKey(server, ownerUserId)).digest('hex');
|
|
199
|
+
this.file = path.join(this.directory, this.stem + '.json');
|
|
200
|
+
}
|
|
201
|
+
async sync() {
|
|
202
|
+
const directory = await fs.open(this.directory, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
203
|
+
try {
|
|
204
|
+
await directory.sync();
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
await directory.close();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async transition(previous, next) {
|
|
211
|
+
const transition = publicationTransition(previous, next);
|
|
212
|
+
if (transition === 'same' || transition === 'create')
|
|
213
|
+
return true;
|
|
214
|
+
if (transition === 'committed')
|
|
215
|
+
return this.proof.committed(previous, next);
|
|
216
|
+
if (transition === 'removed')
|
|
217
|
+
return this.proof.removed(previous, next);
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
async read() {
|
|
221
|
+
try {
|
|
222
|
+
let names;
|
|
223
|
+
try {
|
|
224
|
+
await enrollmentDirectory(this.root, this.directory, false);
|
|
225
|
+
names = await fs.readdir(this.directory);
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
if (missing(error))
|
|
229
|
+
return null;
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
const publications = await readPublications(this.root, this.stem, names, { server: this.server, ownerUserId: this.ownerUserId });
|
|
233
|
+
let final = publications.final;
|
|
234
|
+
const valid = publications.valid;
|
|
235
|
+
for (const file of publications.incomplete) {
|
|
236
|
+
assertLockOwnership();
|
|
237
|
+
await fs.unlink(file);
|
|
238
|
+
await this.sync();
|
|
239
|
+
}
|
|
240
|
+
const candidate = valid[0];
|
|
241
|
+
if (candidate) {
|
|
242
|
+
const next = candidate.envelope;
|
|
243
|
+
const promote = JSON.stringify(final) !== JSON.stringify(next);
|
|
244
|
+
if (!await this.transition(final, next))
|
|
245
|
+
throw enrollmentStorageError();
|
|
246
|
+
if (promote) {
|
|
247
|
+
assertLockOwnership();
|
|
248
|
+
await fs.rename(candidate.file, this.file);
|
|
249
|
+
await this.sync();
|
|
250
|
+
final = next;
|
|
251
|
+
}
|
|
252
|
+
for (const orphan of valid) {
|
|
253
|
+
if (promote && orphan.file === candidate.file)
|
|
254
|
+
continue;
|
|
255
|
+
assertLockOwnership();
|
|
256
|
+
await fs.unlink(orphan.file);
|
|
257
|
+
}
|
|
258
|
+
await this.sync();
|
|
259
|
+
}
|
|
260
|
+
return final;
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
throw enrollmentStorageError();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async write(value) {
|
|
267
|
+
const text = JSON.stringify(validate(value, this.server, this.ownerUserId));
|
|
268
|
+
if (Buffer.byteLength(text) > 65_536)
|
|
269
|
+
throw enrollmentStorageError();
|
|
270
|
+
assertLockOwnership();
|
|
271
|
+
await enrollmentDirectory(this.root, this.directory);
|
|
272
|
+
if (!await this.transition(await this.read(), value))
|
|
273
|
+
throw enrollmentStorageError();
|
|
274
|
+
const temporary = path.join(this.directory, this.stem + '.write-' + randomUUID());
|
|
275
|
+
try {
|
|
276
|
+
const handle = await fs.open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
|
|
277
|
+
try {
|
|
278
|
+
await handle.writeFile(text);
|
|
279
|
+
await handle.sync();
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
await handle.close();
|
|
283
|
+
}
|
|
284
|
+
await this.afterStage?.('enrollment-' + value.state + '-file-synced');
|
|
285
|
+
assertLockOwnership();
|
|
286
|
+
await fs.rename(temporary, this.file);
|
|
287
|
+
await this.sync();
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
assertLockOwnership();
|
|
291
|
+
await fs.rm(temporary, { force: true });
|
|
292
|
+
await this.sync();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async complete(pending, registrationRevision) {
|
|
296
|
+
await this.write({ schemaVersion: 1, state: 'completed', server: pending.server, ownerUserId: pending.ownerUserId, registrationId: pending.registrationId, requestDigest: pending.requestDigest, registrationRevision, workstationId: pending.request.proposedWorkstationId });
|
|
297
|
+
}
|
|
298
|
+
async purge(registrationId, workstationId) {
|
|
299
|
+
try {
|
|
300
|
+
let final = null;
|
|
301
|
+
try {
|
|
302
|
+
final = validate(JSON.parse((await enrollmentFile(this.root, this.file)).toString()), this.server, this.ownerUserId);
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
if (!missing(error))
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
const matches = (envelope) => {
|
|
309
|
+
if (envelope.registrationId !== registrationId)
|
|
310
|
+
return false;
|
|
311
|
+
if ((envelope.state === 'pending' ? envelope.request.proposedWorkstationId : envelope.workstationId) !== workstationId)
|
|
312
|
+
throw enrollmentStorageError();
|
|
313
|
+
return true;
|
|
314
|
+
};
|
|
315
|
+
const finalMatches = final !== null && matches(final);
|
|
316
|
+
let names;
|
|
317
|
+
try {
|
|
318
|
+
await enrollmentDirectory(this.root, this.directory, false);
|
|
319
|
+
names = await fs.readdir(this.directory);
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
if (missing(error) && !final)
|
|
323
|
+
return;
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
326
|
+
const candidates = names.filter(name => name.startsWith(this.stem + '.write-'));
|
|
327
|
+
if (candidates.length > 16)
|
|
328
|
+
throw enrollmentStorageError();
|
|
329
|
+
const targets = [];
|
|
330
|
+
for (const name of candidates) {
|
|
331
|
+
if (!z.string().uuid().safeParse(name.slice((this.stem + '.write-').length)).success)
|
|
332
|
+
throw enrollmentStorageError();
|
|
333
|
+
const file = path.join(this.directory, name);
|
|
334
|
+
const buffer = await enrollmentFile(this.root, file);
|
|
335
|
+
let value;
|
|
336
|
+
try {
|
|
337
|
+
value = JSON.parse(buffer.toString());
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
if (!(error instanceof SyntaxError) || !finalMatches)
|
|
341
|
+
throw error;
|
|
342
|
+
targets.push(file);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (matches(validate(value, this.server, this.ownerUserId)))
|
|
346
|
+
targets.push(file);
|
|
347
|
+
}
|
|
348
|
+
if (finalMatches && final?.state === 'pending')
|
|
349
|
+
targets.push(this.file);
|
|
350
|
+
for (const file of targets) {
|
|
351
|
+
assertLockOwnership();
|
|
352
|
+
await fs.unlink(file);
|
|
353
|
+
}
|
|
354
|
+
if (targets.length)
|
|
355
|
+
await this.sync();
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
throw enrollmentStorageError();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|