@platless/axom-cli 0.6.36 → 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 +7 -1
- package/dist/axom-patch/cli/README.md +31 -2
- package/dist/axom-patch/cli/account-limits/generate.py +2 -2
- package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
- package/dist/axom-patch/cli/codex-terminal-output.json +2 -2
- package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
- package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +1 -1
- 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 +137 -41
- package/dist/axom-patch/cli/qoder/releases/1.1.75.json +5 -88
- package/dist/axom-patch/cli/qoder-native-acp.json +23 -23
- package/dist/axom-patch/cli/runtime.json +3 -1
- 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 +7 -7
- package/dist/axom-patch/cli/zcode-env-account.json +58 -0
- package/dist/axom-patch/cli/zcode-models.json +9 -9
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -10
- package/dist/cli.js +9 -11
- package/dist/commands/auth.js +4 -1
- package/dist/commands/update.js +68 -7
- package/dist/commands/workstation.js +94 -223
- 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 +2 -2
- package/dist/keepers/terminal/keeper.mjs +1089 -673
- package/dist/keepers/terminal/manifest.json +4 -4
- package/dist/native/darwin-x64/axom-quic +0 -0
- package/dist/workstation/acp/acpAgents.js +11 -4
- package/dist/workstation/acp/outbox.js +12 -5
- package/dist/workstation/acp/permission.js +2 -2
- package/dist/workstation/acp/processor/checkpoint.js +4 -3
- package/dist/workstation/acp/processor/control.js +2 -2
- package/dist/workstation/acp/processor/jsonrpc.js +8 -0
- package/dist/workstation/acp/processor/session.js +3 -2
- package/dist/workstation/acp/processor/state.js +1 -0
- package/dist/workstation/acp/processor/transport/local.js +19 -3
- package/dist/workstation/acp/processor/updates.js +25 -0
- package/dist/workstation/acp/processorSession.js +12 -1
- package/dist/workstation/acp/registry/catalog.js +6 -6
- package/dist/workstation/acp/test/keeperWorld.js +4 -3
- package/dist/workstation/agentSessions.js +76 -23
- package/dist/workstation/channels/agent.js +9 -1
- package/dist/workstation/channels/computer.js +27 -4
- package/dist/workstation/channels/management.js +36 -5
- package/dist/workstation/config.js +18 -6
- package/dist/workstation/control/bindingStore.js +4 -0
- package/dist/workstation/control/commands.js +1 -0
- package/dist/workstation/control/daemonIdentity.js +90 -0
- package/dist/workstation/control/deviceCommands.js +191 -0
- package/dist/workstation/control/deviceProtocol.js +68 -0
- package/dist/workstation/control/locks.js +51 -4
- package/dist/workstation/control/rootFormat.js +16 -0
- package/dist/workstation/control/runtimeClient.js +60 -5
- package/dist/workstation/control/runtimeHost.js +96 -9
- package/dist/workstation/control/serverIdentity.js +2 -2
- package/dist/workstation/control/updateTransition.js +91 -0
- package/dist/workstation/daemon.js +262 -360
- package/dist/workstation/daemonctl.js +150 -12
- 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/automationPreparation.js +1 -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 +2 -1
- package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
- package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
- package/dist/workstation/desktopControl/deviceSockets.js +2 -2
- package/dist/workstation/desktopControl/helperProcess.js +250 -1
- package/dist/workstation/desktopControl/hostGrant.js +65 -0
- package/dist/workstation/desktopControl/lobby.js +4 -2
- package/dist/workstation/desktopControl/macProvider.js +41 -40
- package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
- package/dist/workstation/desktopControl/nativeProvider.js +31 -5
- package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
- package/dist/workstation/desktopControl/protocol.js +15 -6
- package/dist/workstation/desktopControl/supervisor.js +110 -17
- package/dist/workstation/desktopControl/wallpaper.js +26 -6
- package/dist/workstation/exec/policy.js +5 -0
- package/dist/workstation/files/client.js +10 -1
- package/dist/workstation/files/host.js +9 -2
- 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/client.js +2 -2
- package/dist/workstation/keepers/kit/launcher.js +80 -4
- 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/oauth/deviceAuthorization.js +31 -17
- package/dist/workstation/protocol/management.js +23 -4
- package/dist/workstation/rd/artifact.js +4 -3
- package/dist/workstation/rd/control.js +3 -5
- package/dist/workstation/rd/host.js +213 -0
- package/dist/workstation/rd/migrationReadiness.js +46 -0
- package/dist/workstation/rd/provider.js +44 -10
- package/dist/workstation/rd/state.js +14 -2
- package/dist/workstation/rd/update.js +5 -8
- package/dist/workstation/rd/upkeep.js +44 -2
- 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 +93 -1
- package/dist/workstation/service/launchd.js +1 -0
- package/dist/workstation/service/systemd.js +1 -0
- package/dist/workstation/systemMetrics.js +3 -0
- 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/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 +80 -10
- package/dist/workstation/update/legacyBridge.js +414 -0
- package/dist/workstation/update/legacyOwner.js +39 -0
- package/dist/workstation/update/npmInstaller.js +2 -2
- package/dist/workstation/update/privateDurability.js +61 -0
- package/dist/workstation/update/recoveryPlan.js +124 -0
- package/dist/workstation/update/runUpdate.js +7 -1
- package/dist/workstation/update/taskRegistry.js +161 -3
- package/dist/workstation/update/watchdog.js +27427 -14410
- package/dist/workstation/update/watchdogProcess.js +72 -0
- package/package.json +7 -3
- package/dist/native/darwin-arm64/axom-quic +0 -0
|
@@ -1,16 +1,47 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
export class RdUpkeep {
|
|
2
3
|
options;
|
|
3
4
|
pending;
|
|
4
5
|
running = false;
|
|
5
6
|
stopped = false;
|
|
7
|
+
original;
|
|
8
|
+
completion;
|
|
6
9
|
constructor(options) {
|
|
7
10
|
this.options = options;
|
|
8
11
|
}
|
|
9
12
|
async tick() {
|
|
10
13
|
if (this.running || this.stopped)
|
|
11
14
|
return;
|
|
15
|
+
if (this.options.device && (this.options.platform ?? process.platform) !== 'linux')
|
|
16
|
+
return;
|
|
12
17
|
this.running = true;
|
|
18
|
+
this.completion = this.run();
|
|
19
|
+
try {
|
|
20
|
+
await this.completion;
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
this.running = false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async run() {
|
|
27
|
+
if (this.original) {
|
|
28
|
+
try {
|
|
29
|
+
await this.original.release();
|
|
30
|
+
this.original = undefined;
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
this.options.log?.(`[rd] original maintenance restoration remains pending: ${error instanceof Error ? error.message : 'unknown'}`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
13
37
|
try {
|
|
38
|
+
if (this.options.device) {
|
|
39
|
+
this.original = this.options.device.acquireMaintenance({ transactionId: `rd-upkeep:${randomUUID()}` });
|
|
40
|
+
await this.original.ready;
|
|
41
|
+
if ((await this.original.observe()).status !== 'candidate')
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
await this.options.adopt?.();
|
|
14
45
|
this.pending ??= await this.options.synchronize();
|
|
15
46
|
if (this.stopped || !this.pending)
|
|
16
47
|
return;
|
|
@@ -31,8 +62,19 @@ export class RdUpkeep {
|
|
|
31
62
|
this.options.log?.(`[rd] automatic update failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
32
63
|
}
|
|
33
64
|
finally {
|
|
34
|
-
this.
|
|
65
|
+
if (this.original) {
|
|
66
|
+
try {
|
|
67
|
+
await this.original.release();
|
|
68
|
+
this.original = undefined;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
this.options.log?.(`[rd] original maintenance restoration remains pending: ${error instanceof Error ? error.message : 'unknown'}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
35
74
|
}
|
|
36
75
|
}
|
|
37
|
-
stop() { this.stopped = true;
|
|
76
|
+
async stop() { this.stopped = true; await this.completion; if (this.original) {
|
|
77
|
+
await this.original.release();
|
|
78
|
+
this.original = undefined;
|
|
79
|
+
} }
|
|
38
80
|
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { LocalControlError } from '../control/protocol.js';
|
|
6
|
+
import { assertLockOwnership, withWorkstationLock } from '../control/locks.js';
|
|
7
|
+
import { normalizeWorkstationServer, workstationEndpoint } from '../control/serverIdentity.js';
|
|
8
|
+
import { deriveSessionKey, generateKeyPair } from '../crypto.js';
|
|
9
|
+
import { osInfo } from '../osinfo.js';
|
|
10
|
+
import { RegistrationStore } from './store.js';
|
|
11
|
+
import { registrationKey, registrationSchema } from './model.js';
|
|
12
|
+
import { assertRegistrationEnrollmentAdmission } from './migration.js';
|
|
13
|
+
import { EnrollmentPendingStore, enrollmentDirectory, enrollmentStorageError, keySchema, operationIdSchema, registrationEnrollmentInputSchema, requestDigest, requestSchema } from './enrollmentPending.js';
|
|
14
|
+
const inputSchema = registrationEnrollmentInputSchema;
|
|
15
|
+
async function acquisitionMatches(store, pending) {
|
|
16
|
+
const latest = (await store.completedTombstones()).filter(entry => entry.server === pending.server && entry.ownerUserId === pending.ownerUserId).at(-1);
|
|
17
|
+
return latest ? pending.request.acquisition.kind === 'reattach' && pending.request.proposedWorkstationId === latest.workstationId : pending.request.acquisition.kind === 'create';
|
|
18
|
+
}
|
|
19
|
+
function recoveryProof(store) {
|
|
20
|
+
return {
|
|
21
|
+
committed: async (pending, completed) => {
|
|
22
|
+
try {
|
|
23
|
+
const identity = await store.read(pending.registrationId);
|
|
24
|
+
const registration = identity.registration;
|
|
25
|
+
return registration.id === completed.registrationId && registration.revision === completed.registrationRevision && registration.server === pending.server && registration.ownerUserId === pending.ownerUserId && registration.workstationId === pending.request.proposedWorkstationId && registration.workstationId === completed.workstationId && identity.clientKey.toString('base64') === pending.clientPrivateKey;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
removed: async (completed, candidate) => {
|
|
32
|
+
if (!await store.hasCompletedRemoval({ id: completed.registrationId, server: completed.server, ownerUserId: completed.ownerUserId, workstationId: completed.workstationId }))
|
|
33
|
+
return false;
|
|
34
|
+
if (!candidate)
|
|
35
|
+
return true;
|
|
36
|
+
return acquisitionMatches(store, candidate);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export async function retireRegistrationEnrollment(root, registration) {
|
|
41
|
+
try {
|
|
42
|
+
assertLockOwnership();
|
|
43
|
+
root = path.resolve(root);
|
|
44
|
+
const parsed = registrationSchema.parse(registration);
|
|
45
|
+
await enrollmentDirectory(root, root, false);
|
|
46
|
+
const store = new RegistrationStore(root);
|
|
47
|
+
const inspection = await store.inspect();
|
|
48
|
+
if (!inspection.known)
|
|
49
|
+
throw enrollmentStorageError();
|
|
50
|
+
const entry = inspection.entries.find(candidate => candidate.id === parsed.id);
|
|
51
|
+
const target = entry?.state === 'ready' ? entry.registration : entry?.target;
|
|
52
|
+
if (target) {
|
|
53
|
+
if (target.server !== parsed.server || target.ownerUserId !== parsed.ownerUserId || target.workstationId !== parsed.workstationId)
|
|
54
|
+
throw enrollmentStorageError();
|
|
55
|
+
if (target.revision !== parsed.revision)
|
|
56
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Registration changed before enrollment cleanup');
|
|
57
|
+
}
|
|
58
|
+
else if (!await store.hasCompletedRemoval({ id: parsed.id, server: parsed.server, ownerUserId: parsed.ownerUserId, workstationId: parsed.workstationId }))
|
|
59
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Registration removal could not be verified');
|
|
60
|
+
await new EnrollmentPendingStore(root, parsed.server, parsed.ownerUserId, recoveryProof(store)).purge(parsed.id, parsed.workstationId);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error instanceof LocalControlError)
|
|
64
|
+
throw error;
|
|
65
|
+
throw enrollmentStorageError();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const identitySchema = z.object({ ownerUserId: z.string().min(1).max(1024).refine(value => !/[\x00-\x1f\x7f-\x9f]/.test(value)) }).strict();
|
|
69
|
+
const lookupSchema = identitySchema.extend({ workstationId: z.string().uuid(), version: z.number().int().min(1).max(2147483646) }).strict();
|
|
70
|
+
const responseSchema = identitySchema.extend({ protocol: z.literal(2), operationId: operationIdSchema, workstationId: z.string().uuid(), serverPub: keySchema }).strict();
|
|
71
|
+
async function requestJson(input, endpoint, body, mutation = false, historical = false) {
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch(workstationEndpoint(input.server, endpoint), {
|
|
74
|
+
method: body === undefined ? 'GET' : 'POST', headers: { Authorization: `Bearer ${input.accessToken}`, ...(body === undefined ? {} : { 'Content-Type': 'application/json' }) },
|
|
75
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }), redirect: 'manual', signal: AbortSignal.timeout(10_000),
|
|
76
|
+
});
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
await response.body?.cancel();
|
|
79
|
+
if ([401, 403].includes(response.status))
|
|
80
|
+
throw new LocalControlError('AUTHORIZATION_REQUIRED', 'Workstation enrollment requires current workstation authority');
|
|
81
|
+
if (response.status === 409 || (historical && response.status === 404))
|
|
82
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Historical workstation could not be verified or changed; inspect before retrying');
|
|
83
|
+
if ([404, 405, 501].includes(response.status))
|
|
84
|
+
throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'This server requires an upgrade for workstation enrollment');
|
|
85
|
+
if (mutation && response.status >= 500)
|
|
86
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Enrollment outcome is unknown; retry with the saved request');
|
|
87
|
+
throw new LocalControlError('NETWORK_FAILED', 'The workstation enrollment request was refused');
|
|
88
|
+
}
|
|
89
|
+
const reader = response.body?.getReader();
|
|
90
|
+
if (!reader)
|
|
91
|
+
throw Error('Missing response');
|
|
92
|
+
const chunks = [];
|
|
93
|
+
let size = 0;
|
|
94
|
+
try {
|
|
95
|
+
while (true) {
|
|
96
|
+
const chunk = await reader.read();
|
|
97
|
+
if (chunk.done)
|
|
98
|
+
break;
|
|
99
|
+
size += chunk.value.length;
|
|
100
|
+
if (size > 65_536)
|
|
101
|
+
throw Error('Response limit');
|
|
102
|
+
chunks.push(chunk.value);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
await reader.cancel().catch(() => undefined);
|
|
107
|
+
}
|
|
108
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
if (error instanceof LocalControlError)
|
|
112
|
+
throw error;
|
|
113
|
+
throw new LocalControlError(mutation ? 'OUTCOME_UNKNOWN' : 'NETWORK_FAILED', mutation ? 'Enrollment outcome is unknown; retry with the saved request' : 'Workstation authority could not be verified');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export async function verifyRegistrationAuthority(input) {
|
|
117
|
+
const scopes = z.object({ results: z.array(z.object({ scope: z.string(), allowed: z.boolean() }).strict()).max(64) }).strict().safeParse(await requestJson(input, '/rest/auth/scopes/check', { scopes: ['workstation'] }));
|
|
118
|
+
if (!scopes.success || scopes.data.results.length !== 1 || scopes.data.results[0]?.scope !== 'workstation')
|
|
119
|
+
throw new LocalControlError('NETWORK_FAILED', 'Workstation permission response is invalid');
|
|
120
|
+
if (!scopes.data.results[0].allowed)
|
|
121
|
+
throw new LocalControlError('AUTHORIZATION_REQUIRED', 'Workstation permission is required');
|
|
122
|
+
const identity = identitySchema.safeParse(await requestJson(input, '/workstation/enroll/identity'));
|
|
123
|
+
if (!identity.success)
|
|
124
|
+
throw new LocalControlError('NETWORK_FAILED', 'Workstation owner response is invalid');
|
|
125
|
+
if (input.expectedOwnerUserId !== undefined && input.expectedOwnerUserId !== identity.data.ownerUserId)
|
|
126
|
+
throw new LocalControlError('OWNER_MISMATCH', 'The authenticated workstation owner differs from the expected owner');
|
|
127
|
+
return identity.data.ownerUserId;
|
|
128
|
+
}
|
|
129
|
+
async function enrollLocked(input, root, options) {
|
|
130
|
+
await options.beforeEnrollment?.();
|
|
131
|
+
await assertRegistrationEnrollmentAdmission(root);
|
|
132
|
+
const ownerUserId = await verifyRegistrationAuthority(input);
|
|
133
|
+
const store = new RegistrationStore(root, { afterStage: stage => options.afterStage?.('store-' + stage) ?? Promise.resolve() });
|
|
134
|
+
await store.assertEnrollmentAllowed(input.server, ownerUserId);
|
|
135
|
+
const inspection = await store.inspect();
|
|
136
|
+
if (!inspection.known)
|
|
137
|
+
throw enrollmentStorageError();
|
|
138
|
+
const key = registrationKey(input.server, ownerUserId);
|
|
139
|
+
const existing = inspection.entries.find(entry => registrationKey(entry.state === 'ready' ? entry.registration.server : entry.target.server, entry.state === 'ready' ? entry.registration.ownerUserId : entry.target.ownerUserId) === key);
|
|
140
|
+
if (existing?.state === 'corrupt')
|
|
141
|
+
throw new LocalControlError('CORRUPT_IDENTITY', 'The matching workstation registration needs repair');
|
|
142
|
+
const proof = recoveryProof(store);
|
|
143
|
+
const pendingStore = new EnrollmentPendingStore(root, input.server, ownerUserId, proof, options.afterStage);
|
|
144
|
+
let envelope = await pendingStore.read();
|
|
145
|
+
if (existing?.state === 'ready') {
|
|
146
|
+
if (envelope?.state === 'pending' && envelope.registrationId === existing.id) {
|
|
147
|
+
const identity = await store.read(existing.id);
|
|
148
|
+
if (identity.registration.workstationId !== envelope.request.proposedWorkstationId || identity.clientKey.toString('base64') !== envelope.clientPrivateKey)
|
|
149
|
+
throw enrollmentStorageError();
|
|
150
|
+
try {
|
|
151
|
+
await pendingStore.complete(envelope, existing.registration.revision);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Local enrollment recovery completion is uncertain; retry with the saved identity');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { operationId: input.operationId, registration: existing.registration, kind: 'reused' };
|
|
158
|
+
}
|
|
159
|
+
if (envelope?.state === 'completed' && !await proof.removed(envelope))
|
|
160
|
+
throw enrollmentStorageError();
|
|
161
|
+
if (!envelope || envelope.state === 'completed') {
|
|
162
|
+
const historical = (await store.completedTombstones()).filter(entry => registrationKey(entry.server, entry.ownerUserId) === key).at(-1);
|
|
163
|
+
let acquisition = { kind: 'create' };
|
|
164
|
+
let proposedWorkstationId = randomUUID();
|
|
165
|
+
if (historical) {
|
|
166
|
+
let metadata;
|
|
167
|
+
try {
|
|
168
|
+
metadata = lookupSchema.parse(await requestJson(input, '/workstation/enroll/' + historical.workstationId, undefined, false, true));
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (error instanceof LocalControlError)
|
|
172
|
+
throw error;
|
|
173
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Historical workstation response could not be verified');
|
|
174
|
+
}
|
|
175
|
+
if (metadata.workstationId !== historical.workstationId || metadata.ownerUserId !== ownerUserId)
|
|
176
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Historical workstation owner or identity differs');
|
|
177
|
+
proposedWorkstationId = metadata.workstationId;
|
|
178
|
+
acquisition = { kind: 'reattach', expectedVersion: metadata.version };
|
|
179
|
+
}
|
|
180
|
+
const keys = generateKeyPair();
|
|
181
|
+
const request = requestSchema.parse({ protocol: 2, operationId: randomUUID(), proposedWorkstationId, clientPublicKey: keys.publicKey.toString('base64'), name: input.defaults.name, os: osInfo(), shell: input.defaults.shell, ...(input.defaults.workspace ? { defaultWorkspace: input.defaults.workspace } : {}), acquisition });
|
|
182
|
+
envelope = { schemaVersion: 1, state: 'pending', server: input.server, ownerUserId, registrationId: randomUUID(), requestDigest: requestDigest(request), request, clientPrivateKey: keys.privateKey.toString('base64') };
|
|
183
|
+
await pendingStore.write(envelope);
|
|
184
|
+
}
|
|
185
|
+
const pending = envelope;
|
|
186
|
+
try {
|
|
187
|
+
await fs.lstat(path.join(root, 'registrations', pending.registrationId, 'retired.json'));
|
|
188
|
+
throw new LocalControlError('BINDING_CONFLICT', 'The pending local identity was removed; verification is required');
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (error.code !== 'ENOENT')
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
if (!await acquisitionMatches(store, pending))
|
|
195
|
+
throw enrollmentStorageError();
|
|
196
|
+
await options.afterStage?.('pending-persisted');
|
|
197
|
+
const response = responseSchema.safeParse(await requestJson(input, '/workstation/enroll', pending.request, true));
|
|
198
|
+
if (!response.success || response.data.operationId !== pending.request.operationId || response.data.workstationId !== pending.request.proposedWorkstationId || response.data.ownerUserId !== ownerUserId)
|
|
199
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Enrollment response could not be verified; retry with the saved request');
|
|
200
|
+
try {
|
|
201
|
+
deriveSessionKey(Buffer.from(pending.clientPrivateKey, 'base64'), Buffer.from(response.data.serverPub, 'base64'), Buffer.alloc(32), Buffer.alloc(32));
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Enrollment server key could not be verified; retry with the saved request');
|
|
205
|
+
}
|
|
206
|
+
assertLockOwnership();
|
|
207
|
+
const dataDirectory = path.join(root, 'registrations', pending.registrationId, 'data');
|
|
208
|
+
let registration;
|
|
209
|
+
try {
|
|
210
|
+
registration = await store.commit({ registration: { id: pending.registrationId, server: input.server, ownerUserId, workstationId: response.data.workstationId, dataDirectory, keeperConfigDirectory: dataDirectory }, clientKey: Buffer.from(pending.clientPrivateKey, 'base64'), serverPub: Buffer.from(response.data.serverPub, 'base64'), expectedRevision: null, configuration: { server: input.server, ownerUserId, workstationId: response.data.workstationId, workspaces: pending.request.defaultWorkspace ? [{ name: pending.request.defaultWorkspace.name, cwd: pending.request.defaultWorkspace.cwd }] : [] } });
|
|
211
|
+
await options.afterStage?.('after-local-commit');
|
|
212
|
+
await pendingStore.complete(pending, registration.revision);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Local enrollment commit needs recovery; retry with the saved request');
|
|
216
|
+
}
|
|
217
|
+
return { operationId: input.operationId, registration, kind: pending.request.acquisition.kind === 'create' ? 'created' : 'reattached' };
|
|
218
|
+
}
|
|
219
|
+
export async function enrollRegistration(input, root, options = {}) {
|
|
220
|
+
let parsed;
|
|
221
|
+
try {
|
|
222
|
+
parsed = inputSchema.parse(input);
|
|
223
|
+
parsed.server = normalizeWorkstationServer(parsed.server);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Workstation enrollment inputs are invalid');
|
|
227
|
+
}
|
|
228
|
+
root = path.resolve(root);
|
|
229
|
+
try {
|
|
230
|
+
await enrollmentDirectory(root);
|
|
231
|
+
await enrollmentDirectory(root, path.join(root, 'control'));
|
|
232
|
+
try {
|
|
233
|
+
// The live lock owner publishes this file in place; its contents may still be growing.
|
|
234
|
+
const owner = await fs.lstat(path.join(root, 'control/operation.owner.json'));
|
|
235
|
+
if (!owner.isFile() || owner.nlink !== 1 || owner.size > 4096 || (owner.mode & 0o077) !== 0
|
|
236
|
+
|| (process.getuid && owner.uid !== process.getuid()))
|
|
237
|
+
throw enrollmentStorageError();
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
if (error.code !== 'ENOENT')
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
return await withWorkstationLock(root, 'operation', () => enrollLocked(parsed, root, options));
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
if (error instanceof LocalControlError)
|
|
247
|
+
throw error;
|
|
248
|
+
throw enrollmentStorageError();
|
|
249
|
+
}
|
|
250
|
+
}
|