@jasonews/agent-squad-gateway 1.0.0
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/LICENSE +22 -0
- package/README.md +200 -0
- package/SECURITY.md +44 -0
- package/dist/bin/agent-squad-gateway.js +55 -0
- package/dist/bin/agent-squad-gateway.js.map +1 -0
- package/dist/config/config.js +124 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/paths.js +16 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/control-plane/clients.js +77 -0
- package/dist/control-plane/clients.js.map +1 -0
- package/dist/control-plane/core-connection.js +54 -0
- package/dist/control-plane/core-connection.js.map +1 -0
- package/dist/control-plane/credentials.js +134 -0
- package/dist/control-plane/credentials.js.map +1 -0
- package/dist/control-plane/db.js +33 -0
- package/dist/control-plane/db.js.map +1 -0
- package/dist/control-plane/extensions.js +59 -0
- package/dist/control-plane/extensions.js.map +1 -0
- package/dist/control-plane/grants.js +107 -0
- package/dist/control-plane/grants.js.map +1 -0
- package/dist/control-plane/idempotency.js +132 -0
- package/dist/control-plane/idempotency.js.map +1 -0
- package/dist/control-plane/migrations.js +133 -0
- package/dist/control-plane/migrations.js.map +1 -0
- package/dist/control-plane/response-sessions.js +359 -0
- package/dist/control-plane/response-sessions.js.map +1 -0
- package/dist/control-plane/retention.js +138 -0
- package/dist/control-plane/retention.js.map +1 -0
- package/dist/control-plane/runs.js +156 -0
- package/dist/control-plane/runs.js.map +1 -0
- package/dist/control-plane/targets.js +449 -0
- package/dist/control-plane/targets.js.map +1 -0
- package/dist/control-plane/types.js +2 -0
- package/dist/control-plane/types.js.map +1 -0
- package/dist/core-client/client.js +275 -0
- package/dist/core-client/client.js.map +1 -0
- package/dist/core-client/event-proxy.js +105 -0
- package/dist/core-client/event-proxy.js.map +1 -0
- package/dist/core-client/types.js +2 -0
- package/dist/core-client/types.js.map +1 -0
- package/dist/extensions/contract.js +2 -0
- package/dist/extensions/contract.js.map +1 -0
- package/dist/extensions/openai/auth.js +16 -0
- package/dist/extensions/openai/auth.js.map +1 -0
- package/dist/extensions/openai/chat.js +403 -0
- package/dist/extensions/openai/chat.js.map +1 -0
- package/dist/extensions/openai/errors.js +79 -0
- package/dist/extensions/openai/errors.js.map +1 -0
- package/dist/extensions/openai/image-input.js +79 -0
- package/dist/extensions/openai/image-input.js.map +1 -0
- package/dist/extensions/openai/models.js +40 -0
- package/dist/extensions/openai/models.js.map +1 -0
- package/dist/extensions/openai/responses.js +731 -0
- package/dist/extensions/openai/responses.js.map +1 -0
- package/dist/extensions/openai/routes.js +81 -0
- package/dist/extensions/openai/routes.js.map +1 -0
- package/dist/extensions/openai/run-attempt.js +96 -0
- package/dist/extensions/openai/run-attempt.js.map +1 -0
- package/dist/extensions/openai/schemas.js +176 -0
- package/dist/extensions/openai/schemas.js.map +1 -0
- package/dist/extensions/openai/sse.js +421 -0
- package/dist/extensions/openai/sse.js.map +1 -0
- package/dist/extensions/openai/tools.js +727 -0
- package/dist/extensions/openai/tools.js.map +1 -0
- package/dist/extensions/registry.js +30 -0
- package/dist/extensions/registry.js.map +1 -0
- package/dist/lifecycle/daemon.js +437 -0
- package/dist/lifecycle/daemon.js.map +1 -0
- package/dist/lifecycle/pidfile.js +34 -0
- package/dist/lifecycle/pidfile.js.map +1 -0
- package/dist/provider-runtime/antigravity/adapter.js +398 -0
- package/dist/provider-runtime/antigravity/adapter.js.map +1 -0
- package/dist/provider-runtime/antigravity/conversation.js +31 -0
- package/dist/provider-runtime/antigravity/conversation.js.map +1 -0
- package/dist/provider-runtime/capability-service.js +191 -0
- package/dist/provider-runtime/capability-service.js.map +1 -0
- package/dist/provider-runtime/claude/adapter.js +397 -0
- package/dist/provider-runtime/claude/adapter.js.map +1 -0
- package/dist/provider-runtime/claude/protocol.js +143 -0
- package/dist/provider-runtime/claude/protocol.js.map +1 -0
- package/dist/provider-runtime/codex/adapter.js +440 -0
- package/dist/provider-runtime/codex/adapter.js.map +1 -0
- package/dist/provider-runtime/codex/json-rpc-client.js +182 -0
- package/dist/provider-runtime/codex/json-rpc-client.js.map +1 -0
- package/dist/provider-runtime/codex/protocol.js +128 -0
- package/dist/provider-runtime/codex/protocol.js.map +1 -0
- package/dist/provider-runtime/command-discovery.js +106 -0
- package/dist/provider-runtime/command-discovery.js.map +1 -0
- package/dist/provider-runtime/cursor/adapter.js +427 -0
- package/dist/provider-runtime/cursor/adapter.js.map +1 -0
- package/dist/provider-runtime/cursor/protocol.js +81 -0
- package/dist/provider-runtime/cursor/protocol.js.map +1 -0
- package/dist/provider-runtime/fake/adapter.js +138 -0
- package/dist/provider-runtime/fake/adapter.js.map +1 -0
- package/dist/provider-runtime/image-assets.js +409 -0
- package/dist/provider-runtime/image-assets.js.map +1 -0
- package/dist/provider-runtime/image-support.js +5 -0
- package/dist/provider-runtime/image-support.js.map +1 -0
- package/dist/provider-runtime/invocation-service.js +503 -0
- package/dist/provider-runtime/invocation-service.js.map +1 -0
- package/dist/provider-runtime/kimi/adapter.js +657 -0
- package/dist/provider-runtime/kimi/adapter.js.map +1 -0
- package/dist/provider-runtime/opencode/adapter.js +610 -0
- package/dist/provider-runtime/opencode/adapter.js.map +1 -0
- package/dist/provider-runtime/opencode/server.js +46 -0
- package/dist/provider-runtime/opencode/server.js.map +1 -0
- package/dist/provider-runtime/process/line-reader.js +49 -0
- package/dist/provider-runtime/process/line-reader.js.map +1 -0
- package/dist/provider-runtime/process/managed-process.js +151 -0
- package/dist/provider-runtime/process/managed-process.js.map +1 -0
- package/dist/provider-runtime/provider-input.js +77 -0
- package/dist/provider-runtime/provider-input.js.map +1 -0
- package/dist/provider-runtime/register-providers.js +26 -0
- package/dist/provider-runtime/register-providers.js.map +1 -0
- package/dist/provider-runtime/registry.js +35 -0
- package/dist/provider-runtime/registry.js.map +1 -0
- package/dist/provider-runtime/replay-buffer.js +168 -0
- package/dist/provider-runtime/replay-buffer.js.map +1 -0
- package/dist/provider-runtime/scheduler.js +155 -0
- package/dist/provider-runtime/scheduler.js.map +1 -0
- package/dist/provider-runtime/types.js +2 -0
- package/dist/provider-runtime/types.js.map +1 -0
- package/dist/provider-runtime/workspaces.js +814 -0
- package/dist/provider-runtime/workspaces.js.map +1 -0
- package/dist/security/admin-auth.js +109 -0
- package/dist/security/admin-auth.js.map +1 -0
- package/dist/security/crypto.js +52 -0
- package/dist/security/crypto.js.map +1 -0
- package/dist/security/secret-files.js +33 -0
- package/dist/security/secret-files.js.map +1 -0
- package/dist/server/admin-routes.js +610 -0
- package/dist/server/admin-routes.js.map +1 -0
- package/dist/server/app.js +178 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/auth-hooks.js +43 -0
- package/dist/server/auth-hooks.js.map +1 -0
- package/dist/server/core-routes.js +127 -0
- package/dist/server/core-routes.js.map +1 -0
- package/dist/server/errors.js +19 -0
- package/dist/server/errors.js.map +1 -0
- package/dist/server/ui-routes.js +77 -0
- package/dist/server/ui-routes.js.map +1 -0
- package/dist/web/assets/index-BhqNoPpE.js +206 -0
- package/dist/web/assets/index-BhqNoPpE.js.map +1 -0
- package/dist/web/assets/index-C5nnaBfZ.css +1 -0
- package/dist/web/index.html +14 -0
- package/package.json +74 -0
- package/translations/README.zh-CN.md +175 -0
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
import { chmodSync, closeSync, constants, fchmodSync, fstatSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { GatewayError } from '../server/errors.js';
|
|
5
|
+
const MARKER = '.gateway-workspace';
|
|
6
|
+
const OWNER_KEY = '.gateway-owner-key';
|
|
7
|
+
const QUARANTINE = '.gateway-quarantine';
|
|
8
|
+
const STAGING = '.gateway-staging';
|
|
9
|
+
const COMPONENT = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
10
|
+
const RECOVERY_ENTRY = /^[a-f0-9]{48}$/;
|
|
11
|
+
const OWNER_KEY_BYTES = 32;
|
|
12
|
+
const NOFOLLOW = process.platform === 'win32' ? 0 : constants.O_NOFOLLOW;
|
|
13
|
+
export class WorkspaceManager {
|
|
14
|
+
configuredRoot;
|
|
15
|
+
getFixedWorkspaces;
|
|
16
|
+
testHooks;
|
|
17
|
+
rootPromise;
|
|
18
|
+
mutationTail = Promise.resolve();
|
|
19
|
+
active = new Map();
|
|
20
|
+
constructor(workspacesDir, options) {
|
|
21
|
+
this.configuredRoot = resolve(workspacesDir);
|
|
22
|
+
this.getFixedWorkspaces = options.getFixedWorkspaces;
|
|
23
|
+
this.testHooks = options.testHooks ?? {};
|
|
24
|
+
}
|
|
25
|
+
acquireChat(target, runId) {
|
|
26
|
+
return this.exclusive(async () => {
|
|
27
|
+
validateComponent('target', target.id);
|
|
28
|
+
validateComponent('run', runId);
|
|
29
|
+
if (target.fixedWorkspace !== null)
|
|
30
|
+
return this.fixedLease(target.fixedWorkspace);
|
|
31
|
+
return target.isolationLevel === 'strict'
|
|
32
|
+
? this.acquireStrict(target)
|
|
33
|
+
: this.acquireTemporary(runId);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
createResponse(target, responseId) {
|
|
37
|
+
return this.exclusive(async () => {
|
|
38
|
+
validateComponent('target', target.id);
|
|
39
|
+
const id = validateComponent('response', responseId);
|
|
40
|
+
if (target.fixedWorkspace !== null)
|
|
41
|
+
return this.fixedLease(target.fixedWorkspace);
|
|
42
|
+
const state = await this.root();
|
|
43
|
+
const responses = this.ensureDirectory(state, ['responses']).path;
|
|
44
|
+
const path = join(responses, id);
|
|
45
|
+
const identity = this.managedDirectoryIdentity(path);
|
|
46
|
+
if (identity === undefined)
|
|
47
|
+
await this.replaceDirectory(state, path, 'response', 'cleanup');
|
|
48
|
+
else
|
|
49
|
+
this.requireMarker(state, path, 'response');
|
|
50
|
+
return this.persistentLease(path);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
openResponse(path) {
|
|
54
|
+
return this.exclusive(async () => {
|
|
55
|
+
const state = await this.root();
|
|
56
|
+
const canonical = this.validateResponsePath(state, path);
|
|
57
|
+
return this.persistentLease(canonical);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
cleanupExpired(paths) {
|
|
61
|
+
return this.cleanupForRetention(paths).then(() => undefined);
|
|
62
|
+
}
|
|
63
|
+
cleanupForRetention(paths) {
|
|
64
|
+
return this.exclusive(async () => {
|
|
65
|
+
const state = await this.root();
|
|
66
|
+
try {
|
|
67
|
+
this.sweepRecovery(state);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (error instanceof FixedWorkspaceConfigurationError)
|
|
71
|
+
throw error;
|
|
72
|
+
return paths.map((path) => ({
|
|
73
|
+
path,
|
|
74
|
+
status: this.cleanupCandidate(state, path) === null ? 'unmanaged' : 'retry',
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
const outcomes = [];
|
|
78
|
+
for (const path of paths) {
|
|
79
|
+
const candidate = this.cleanupCandidate(state, path);
|
|
80
|
+
if (candidate === null) {
|
|
81
|
+
outcomes.push({ path, status: 'unmanaged' });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (this.active.has(candidate.path)) {
|
|
85
|
+
outcomes.push({ path, status: 'retry' });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
let identity;
|
|
89
|
+
try {
|
|
90
|
+
identity = this.requireMarker(state, candidate.path, candidate.kind);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
outcomes.push({
|
|
94
|
+
path,
|
|
95
|
+
status: isMissingWorkspacePath(candidate.path, error) ? 'absent' : 'retry',
|
|
96
|
+
});
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
this.sweepRecovery(state);
|
|
101
|
+
const detached = this.atomicDetach(state, candidate.path, identity, 'cleanup');
|
|
102
|
+
this.removeQuarantined(state, detached, 'cleanup');
|
|
103
|
+
outcomes.push({ path, status: 'removed' });
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (error instanceof FixedWorkspaceConfigurationError)
|
|
107
|
+
throw error;
|
|
108
|
+
outcomes.push({
|
|
109
|
+
path,
|
|
110
|
+
status: isMissingWorkspacePath(candidate.path, error) ? 'absent' : 'retry',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return outcomes;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async acquireStrict(target) {
|
|
118
|
+
const targetId = validateComponent('target', target.id);
|
|
119
|
+
if (!Number.isSafeInteger(target.maxConcurrency) || target.maxConcurrency < 1) {
|
|
120
|
+
throw new Error('invalid target maxConcurrency');
|
|
121
|
+
}
|
|
122
|
+
const state = await this.root();
|
|
123
|
+
for (let index = 0; index < target.maxConcurrency; index += 1) {
|
|
124
|
+
const slots = this.ensureDirectory(state, ['targets', targetId, 'slots']).path;
|
|
125
|
+
const path = join(slots, String(index));
|
|
126
|
+
if (this.active.has(path))
|
|
127
|
+
continue;
|
|
128
|
+
const detached = await this.replaceDirectory(state, path, 'strict', 'strict-acquire');
|
|
129
|
+
if (detached !== null)
|
|
130
|
+
this.removeQuarantined(state, detached, 'strict-acquire');
|
|
131
|
+
this.active.set(path, 1);
|
|
132
|
+
let pendingRelease;
|
|
133
|
+
return this.destructiveLease(path, async () => {
|
|
134
|
+
if (pendingRelease === undefined) {
|
|
135
|
+
pendingRelease = await this.replaceDirectory(state, path, 'strict', 'strict-release');
|
|
136
|
+
}
|
|
137
|
+
if (pendingRelease !== null)
|
|
138
|
+
this.removeQuarantined(state, pendingRelease, 'strict-release');
|
|
139
|
+
pendingRelease = null;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
throw new GatewayError(429, 'target_busy', `target ${target.id} has no free workspace slots`);
|
|
143
|
+
}
|
|
144
|
+
async acquireTemporary(runId) {
|
|
145
|
+
const id = validateComponent('run', runId);
|
|
146
|
+
const state = await this.root();
|
|
147
|
+
const temporary = this.ensureDirectory(state, ['temporary']).path;
|
|
148
|
+
const path = join(temporary, id);
|
|
149
|
+
if (this.active.has(path)) {
|
|
150
|
+
throw new GatewayError(429, 'target_busy', `run ${runId} already has a workspace lease`);
|
|
151
|
+
}
|
|
152
|
+
const detached = await this.replaceDirectory(state, path, 'temporary', 'temporary-acquire');
|
|
153
|
+
if (detached !== null)
|
|
154
|
+
this.removeQuarantined(state, detached, 'temporary-acquire');
|
|
155
|
+
this.active.set(path, 1);
|
|
156
|
+
let pendingRelease;
|
|
157
|
+
return this.destructiveLease(path, async () => {
|
|
158
|
+
if (pendingRelease === undefined) {
|
|
159
|
+
this.sweepRecovery(state);
|
|
160
|
+
pendingRelease = this.removeManagedPath(state, path, 'temporary', 'temporary-release');
|
|
161
|
+
}
|
|
162
|
+
if (pendingRelease !== null) {
|
|
163
|
+
this.removeQuarantined(state, pendingRelease, 'temporary-release');
|
|
164
|
+
}
|
|
165
|
+
pendingRelease = null;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
async fixedLease(path) {
|
|
169
|
+
const canonical = realpathSync(path);
|
|
170
|
+
if (!statSync(canonical).isDirectory())
|
|
171
|
+
throw new Error('fixed workspace must be a directory');
|
|
172
|
+
const prospectiveRoot = canonicalizeProspectivePath(this.configuredRoot);
|
|
173
|
+
const configured = this.authoritativeFixedWorkspaces(prospectiveRoot);
|
|
174
|
+
if (!configured.includes(canonical))
|
|
175
|
+
throw new Error('fixed workspace is not configured');
|
|
176
|
+
return { path: canonical, release: async () => undefined };
|
|
177
|
+
}
|
|
178
|
+
destructiveLease(path, cleanup) {
|
|
179
|
+
let released = false;
|
|
180
|
+
return {
|
|
181
|
+
path,
|
|
182
|
+
release: () => this.exclusive(async () => {
|
|
183
|
+
if (released)
|
|
184
|
+
return;
|
|
185
|
+
await cleanup();
|
|
186
|
+
this.active.delete(path);
|
|
187
|
+
released = true;
|
|
188
|
+
}),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
persistentLease(path) {
|
|
192
|
+
this.active.set(path, (this.active.get(path) ?? 0) + 1);
|
|
193
|
+
let released = false;
|
|
194
|
+
return {
|
|
195
|
+
path,
|
|
196
|
+
release: () => this.exclusive(async () => {
|
|
197
|
+
if (released)
|
|
198
|
+
return;
|
|
199
|
+
const remaining = (this.active.get(path) ?? 1) - 1;
|
|
200
|
+
if (remaining === 0)
|
|
201
|
+
this.active.delete(path);
|
|
202
|
+
else
|
|
203
|
+
this.active.set(path, remaining);
|
|
204
|
+
released = true;
|
|
205
|
+
}),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
root() {
|
|
209
|
+
if (this.rootPromise === undefined) {
|
|
210
|
+
const pending = Promise.resolve().then(() => this.initializeRoot());
|
|
211
|
+
this.rootPromise = pending;
|
|
212
|
+
void pending.catch(() => {
|
|
213
|
+
if (this.rootPromise === pending)
|
|
214
|
+
this.rootPromise = undefined;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return this.rootPromise;
|
|
218
|
+
}
|
|
219
|
+
async initializeRoot() {
|
|
220
|
+
const prospectiveRoot = canonicalizeProspectivePath(this.configuredRoot);
|
|
221
|
+
this.authoritativeFixedWorkspaces(prospectiveRoot);
|
|
222
|
+
mkdirSync(this.configuredRoot, { recursive: true, mode: 0o700 });
|
|
223
|
+
const root = realpathSync(this.configuredRoot);
|
|
224
|
+
this.authoritativeFixedWorkspaces(root);
|
|
225
|
+
chmodSync(root, 0o700);
|
|
226
|
+
const rootIdentity = directoryIdentity(root);
|
|
227
|
+
const ownerKey = loadOrCreateOwnerKey(root);
|
|
228
|
+
const quarantine = join(root, QUARANTINE);
|
|
229
|
+
ensurePrivateDirectory(quarantine);
|
|
230
|
+
const quarantineIdentity = directoryIdentity(quarantine);
|
|
231
|
+
const staging = join(root, STAGING);
|
|
232
|
+
ensurePrivateDirectory(staging);
|
|
233
|
+
const stagingIdentity = directoryIdentity(staging);
|
|
234
|
+
const state = { root, rootIdentity, quarantine, quarantineIdentity, staging, stagingIdentity, ownerKey };
|
|
235
|
+
this.sweepRecovery(state);
|
|
236
|
+
return state;
|
|
237
|
+
}
|
|
238
|
+
ensureDirectory(state, parts) {
|
|
239
|
+
this.requireRootIdentity(state);
|
|
240
|
+
let current = state.root;
|
|
241
|
+
let finalCreated = false;
|
|
242
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
243
|
+
const candidate = join(current, parts[index]);
|
|
244
|
+
let created = false;
|
|
245
|
+
try {
|
|
246
|
+
const entry = lstatSync(candidate);
|
|
247
|
+
if (entry.isSymbolicLink())
|
|
248
|
+
throw new Error(`managed workspace contains symbolic link: ${candidate}`);
|
|
249
|
+
if (!entry.isDirectory())
|
|
250
|
+
throw new Error(`managed workspace path is not a directory: ${candidate}`);
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
if (!isErrorCode(error, 'ENOENT'))
|
|
254
|
+
throw error;
|
|
255
|
+
try {
|
|
256
|
+
mkdirSync(candidate, { mode: 0o700 });
|
|
257
|
+
created = true;
|
|
258
|
+
}
|
|
259
|
+
catch (mkdirError) {
|
|
260
|
+
if (!isErrorCode(mkdirError, 'EEXIST'))
|
|
261
|
+
throw mkdirError;
|
|
262
|
+
const winner = lstatSync(candidate);
|
|
263
|
+
if (winner.isSymbolicLink() || !winner.isDirectory()) {
|
|
264
|
+
throw new Error(`unsafe managed workspace path: ${candidate}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const canonical = realpathSync(candidate);
|
|
269
|
+
assertContained(state.root, canonical, false);
|
|
270
|
+
if (canonical !== candidate)
|
|
271
|
+
throw new Error(`managed workspace path is not canonical: ${candidate}`);
|
|
272
|
+
current = canonical;
|
|
273
|
+
if (index === parts.length - 1)
|
|
274
|
+
finalCreated = created;
|
|
275
|
+
}
|
|
276
|
+
return { path: current, created: finalCreated, identity: directoryIdentity(current) };
|
|
277
|
+
}
|
|
278
|
+
async replaceDirectory(state, path, kind, reason) {
|
|
279
|
+
this.sweepRecovery(state);
|
|
280
|
+
const existingIdentity = this.managedDirectoryIdentity(path);
|
|
281
|
+
const expectedIdentity = existingIdentity === undefined ? undefined : this.requireMarker(state, path, kind);
|
|
282
|
+
const staged = join(state.staging, randomBytes(24).toString('hex'));
|
|
283
|
+
mkdirSync(staged, { mode: 0o700 });
|
|
284
|
+
const stagedIdentity = directoryIdentity(staged);
|
|
285
|
+
try {
|
|
286
|
+
this.writeMarker(state, staged, path, kind, stagedIdentity);
|
|
287
|
+
return this.publishStagedDirectory(state, staged, stagedIdentity, path, kind, reason, expectedIdentity);
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
try {
|
|
291
|
+
this.removeStaged(state, { path: staged, identity: stagedIdentity }, 'transaction-cleanup');
|
|
292
|
+
}
|
|
293
|
+
catch (cleanupError) {
|
|
294
|
+
throw combinedError(error, cleanupError, `failed to clean staged workspace for ${path}`);
|
|
295
|
+
}
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
removeManagedPath(state, path, kind, reason) {
|
|
300
|
+
let identity;
|
|
301
|
+
try {
|
|
302
|
+
identity = this.requireMarker(state, path, kind);
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
if (isErrorCode(error, 'ENOENT'))
|
|
306
|
+
return null;
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
const detached = this.atomicDetach(state, path, identity, reason);
|
|
310
|
+
return detached;
|
|
311
|
+
}
|
|
312
|
+
removeQuarantined(state, entry, reason) {
|
|
313
|
+
this.testHooks.beforeQuarantineRemove?.({ path: entry.path, reason });
|
|
314
|
+
const fixedWorkspaces = this.canonicalFixedWorkspaceSnapshot();
|
|
315
|
+
this.requireRootIdentity(state);
|
|
316
|
+
this.validateRecoveryEntry(state, state.quarantine, state.quarantineIdentity, entry);
|
|
317
|
+
this.assertFixedWorkspaceSnapshotSafe(state.root, fixedWorkspaces);
|
|
318
|
+
rmSync(entry.path, { recursive: true, force: true });
|
|
319
|
+
}
|
|
320
|
+
removeStaged(state, entry, reason) {
|
|
321
|
+
this.testHooks.beforeStagingRemove?.({ path: entry.path, reason });
|
|
322
|
+
const fixedWorkspaces = this.canonicalFixedWorkspaceSnapshot();
|
|
323
|
+
this.requireRootIdentity(state);
|
|
324
|
+
this.validateRecoveryEntry(state, state.staging, state.stagingIdentity, entry);
|
|
325
|
+
this.assertFixedWorkspaceSnapshotSafe(state.root, fixedWorkspaces);
|
|
326
|
+
rmSync(entry.path, { recursive: true, force: true });
|
|
327
|
+
}
|
|
328
|
+
sweepRecovery(state) {
|
|
329
|
+
this.sweepDirectory(state, state.quarantine, state.quarantineIdentity, (entry) => this.removeQuarantined(state, entry, 'recovery'));
|
|
330
|
+
this.sweepDirectory(state, state.staging, state.stagingIdentity, (entry) => this.removeStaged(state, entry, 'recovery'));
|
|
331
|
+
}
|
|
332
|
+
sweepDirectory(state, directory, expectedIdentity, removeEntry) {
|
|
333
|
+
this.validateRecoveryDirectory(state, directory, expectedIdentity);
|
|
334
|
+
const entries = readdirSync(directory).sort();
|
|
335
|
+
for (const name of entries) {
|
|
336
|
+
const path = join(directory, name);
|
|
337
|
+
if (!RECOVERY_ENTRY.test(name))
|
|
338
|
+
throw new Error(`unsafe recovery entry: ${path}`);
|
|
339
|
+
removeEntry({ path, identity: identityOf(lstatSync(path)) });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
atomicDetach(state, path, expectedIdentity, reason) {
|
|
343
|
+
this.testHooks.beforeAtomicDetach?.({ path, reason });
|
|
344
|
+
// Node has no dirfd/openat API. Keep the final ancestor checks and rename in
|
|
345
|
+
// one synchronous sequence so no same-process task can interleave a swap.
|
|
346
|
+
this.assertFixedWorkspacesSafe(state);
|
|
347
|
+
this.requireRootIdentity(state);
|
|
348
|
+
const parent = dirname(path);
|
|
349
|
+
const destinationParentIdentity = this.validateManagedDirectory(state, parent);
|
|
350
|
+
this.validateRecoveryDirectory(state, state.quarantine, state.quarantineIdentity);
|
|
351
|
+
if (readdirSync(state.quarantine).length !== 0)
|
|
352
|
+
throw new Error('pending quarantine cleanup');
|
|
353
|
+
const current = lstatSync(path);
|
|
354
|
+
if (!current.isSymbolicLink()) {
|
|
355
|
+
if (!current.isDirectory() || !sameIdentity(identityOf(current), expectedIdentity)) {
|
|
356
|
+
throw new Error(`managed workspace changed before detach: ${path}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const quarantined = join(state.quarantine, randomBytes(24).toString('hex'));
|
|
360
|
+
renameSync(path, quarantined);
|
|
361
|
+
return { path: quarantined, identity: identityOf(current), destinationParentIdentity };
|
|
362
|
+
}
|
|
363
|
+
publishStagedDirectory(state, staged, stagedIdentity, path, kind, reason, expectedIdentity) {
|
|
364
|
+
if (expectedIdentity === undefined) {
|
|
365
|
+
this.testHooks.beforeMarkerOperation?.({
|
|
366
|
+
markerPath: join(staged, MARKER),
|
|
367
|
+
path,
|
|
368
|
+
kind,
|
|
369
|
+
phase: 'directory-commit',
|
|
370
|
+
});
|
|
371
|
+
this.assertFixedWorkspacesSafe(state);
|
|
372
|
+
this.validateManagedDirectory(state, dirname(path));
|
|
373
|
+
if (this.managedDirectoryIdentity(path) !== undefined)
|
|
374
|
+
throw new Error(`managed workspace already exists: ${path}`);
|
|
375
|
+
if (!sameIdentity(this.validateManagedDirectory(state, staged), stagedIdentity)) {
|
|
376
|
+
throw new Error(`staged managed workspace changed before commit: ${path}`);
|
|
377
|
+
}
|
|
378
|
+
renameSync(staged, path);
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
const detached = this.atomicDetach(state, path, expectedIdentity, reason);
|
|
382
|
+
try {
|
|
383
|
+
this.testHooks.beforeMarkerOperation?.({
|
|
384
|
+
markerPath: join(staged, MARKER),
|
|
385
|
+
path,
|
|
386
|
+
kind,
|
|
387
|
+
phase: 'directory-commit',
|
|
388
|
+
});
|
|
389
|
+
this.assertFixedWorkspacesSafe(state);
|
|
390
|
+
this.validateManagedDirectory(state, dirname(path));
|
|
391
|
+
if (!sameIdentity(this.validateManagedDirectory(state, staged), stagedIdentity)) {
|
|
392
|
+
throw new Error(`staged managed workspace changed before commit: ${path}`);
|
|
393
|
+
}
|
|
394
|
+
renameSync(staged, path);
|
|
395
|
+
return detached;
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
try {
|
|
399
|
+
this.testHooks.beforeDirectoryRollback?.({ path, kind });
|
|
400
|
+
const fixedWorkspaces = this.canonicalFixedWorkspaceSnapshot();
|
|
401
|
+
this.requireRootIdentity(state);
|
|
402
|
+
this.validateRecoveryEntry(state, state.quarantine, state.quarantineIdentity, detached);
|
|
403
|
+
const destinationParentIdentity = this.validateManagedDirectory(state, dirname(path));
|
|
404
|
+
if (!sameIdentity(destinationParentIdentity, detached.destinationParentIdentity)) {
|
|
405
|
+
throw new Error(`managed workspace parent changed during rollback: ${path}`);
|
|
406
|
+
}
|
|
407
|
+
if (this.managedDirectoryIdentity(path) !== undefined) {
|
|
408
|
+
throw new Error(`managed workspace occupied during rollback: ${path}`);
|
|
409
|
+
}
|
|
410
|
+
this.assertFixedWorkspaceSnapshotSafe(state.root, fixedWorkspaces);
|
|
411
|
+
renameSync(detached.path, path);
|
|
412
|
+
}
|
|
413
|
+
catch (rollbackError) {
|
|
414
|
+
throw combinedError(error, rollbackError, `failed to roll back managed workspace ${path}`);
|
|
415
|
+
}
|
|
416
|
+
throw error;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
managedDirectoryIdentity(path) {
|
|
420
|
+
try {
|
|
421
|
+
const entry = lstatSync(path);
|
|
422
|
+
if (entry.isSymbolicLink() || !entry.isDirectory())
|
|
423
|
+
throw new Error(`unsafe managed workspace: ${path}`);
|
|
424
|
+
if (realpathSync(path) !== path)
|
|
425
|
+
throw new Error(`managed workspace is not canonical: ${path}`);
|
|
426
|
+
return identityOf(entry);
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
if (isErrorCode(error, 'ENOENT'))
|
|
430
|
+
return undefined;
|
|
431
|
+
throw error;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
validateRecoveryDirectory(state, path, expectedIdentity) {
|
|
435
|
+
const identity = this.validateManagedDirectory(state, path);
|
|
436
|
+
if (!sameIdentity(identity, expectedIdentity))
|
|
437
|
+
throw new Error(`unsafe recovery directory: ${path}`);
|
|
438
|
+
}
|
|
439
|
+
validateRecoveryEntry(state, directory, directoryIdentity, entry) {
|
|
440
|
+
this.validateRecoveryDirectory(state, directory, directoryIdentity);
|
|
441
|
+
if (dirname(entry.path) !== directory || !RECOVERY_ENTRY.test(basename(entry.path))) {
|
|
442
|
+
throw new Error(`unsafe recovery entry: ${entry.path}`);
|
|
443
|
+
}
|
|
444
|
+
const current = lstatSync(entry.path);
|
|
445
|
+
if (!sameIdentity(identityOf(current), entry.identity)) {
|
|
446
|
+
throw new Error(`recovery entry changed before removal: ${entry.path}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
validateManagedDirectory(state, path) {
|
|
450
|
+
this.requireRootIdentity(state);
|
|
451
|
+
if (!isAbsolute(path) || path !== resolve(path))
|
|
452
|
+
throw new Error(`unsafe managed workspace: ${path}`);
|
|
453
|
+
assertContained(state.root, path, true);
|
|
454
|
+
const relation = relative(state.root, path);
|
|
455
|
+
let current = state.root;
|
|
456
|
+
if (relation !== '') {
|
|
457
|
+
for (const part of relation.split(sep)) {
|
|
458
|
+
current = join(current, part);
|
|
459
|
+
const entry = lstatSync(current);
|
|
460
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
461
|
+
throw new Error(`managed workspace ancestor is unsafe: ${current}`);
|
|
462
|
+
}
|
|
463
|
+
if (realpathSync(current) !== current) {
|
|
464
|
+
throw new Error(`managed workspace ancestor is not canonical: ${current}`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return directoryIdentity(path);
|
|
469
|
+
}
|
|
470
|
+
validateResponsePath(state, path) {
|
|
471
|
+
if (!isAbsolute(path) || path !== resolve(path))
|
|
472
|
+
throw new Error('path is not a canonical managed response');
|
|
473
|
+
const candidate = resolve(path);
|
|
474
|
+
if (!isContained(state.root, candidate, false))
|
|
475
|
+
throw new Error('path is not a managed response');
|
|
476
|
+
const parts = relative(state.root, candidate).split(sep);
|
|
477
|
+
if (parts.length !== 2 || parts[0] !== 'responses')
|
|
478
|
+
throw new Error('path is not a managed response');
|
|
479
|
+
validateComponent('response', parts[1]);
|
|
480
|
+
try {
|
|
481
|
+
this.requireMarker(state, candidate, 'response');
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
throw new Error('path is not a managed response or has an invalid marker');
|
|
485
|
+
}
|
|
486
|
+
return candidate;
|
|
487
|
+
}
|
|
488
|
+
cleanupCandidate(state, path) {
|
|
489
|
+
if (!isAbsolute(path) || path !== resolve(path))
|
|
490
|
+
return null;
|
|
491
|
+
const candidate = resolve(path);
|
|
492
|
+
if (!isContained(state.root, candidate, false))
|
|
493
|
+
return null;
|
|
494
|
+
const parts = relative(state.root, candidate).split(sep);
|
|
495
|
+
try {
|
|
496
|
+
const kind = cleanupKind(parts);
|
|
497
|
+
return kind === null ? null : { path: candidate, kind };
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
writeMarker(state, directory, path, kind, expectedIdentity) {
|
|
504
|
+
const identity = this.validateManagedDirectory(state, directory);
|
|
505
|
+
if (!sameIdentity(identity, expectedIdentity))
|
|
506
|
+
throw new Error(`managed workspace changed before marking: ${path}`);
|
|
507
|
+
const markerPath = join(directory, MARKER);
|
|
508
|
+
const temporaryPath = join(directory, `.${MARKER}.${randomBytes(24).toString('hex')}.tmp`);
|
|
509
|
+
const marker = expectedMarker(state, path, kind);
|
|
510
|
+
let descriptor;
|
|
511
|
+
let failure;
|
|
512
|
+
try {
|
|
513
|
+
descriptor = openSync(temporaryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | NOFOLLOW, 0o600);
|
|
514
|
+
this.testHooks.beforeMarkerOperation?.({ markerPath, path, kind, phase: 'temporary-write' });
|
|
515
|
+
writeFileSync(descriptor, marker);
|
|
516
|
+
this.testHooks.beforeMarkerOperation?.({ markerPath, path, kind, phase: 'temporary-fsync' });
|
|
517
|
+
fsyncSync(descriptor);
|
|
518
|
+
closeSync(descriptor);
|
|
519
|
+
descriptor = undefined;
|
|
520
|
+
this.testHooks.beforeMarkerCommit?.({ markerPath, path, kind });
|
|
521
|
+
const currentIdentity = this.validateManagedDirectory(state, directory);
|
|
522
|
+
if (!sameIdentity(currentIdentity, expectedIdentity)) {
|
|
523
|
+
throw new Error(`managed workspace changed before marker commit: ${path}`);
|
|
524
|
+
}
|
|
525
|
+
this.testHooks.beforeMarkerOperation?.({ markerPath, path, kind, phase: 'temporary-rename' });
|
|
526
|
+
renameSync(temporaryPath, markerPath);
|
|
527
|
+
syncDirectory(directory);
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
failure = error;
|
|
531
|
+
}
|
|
532
|
+
finally {
|
|
533
|
+
if (descriptor !== undefined) {
|
|
534
|
+
try {
|
|
535
|
+
closeSync(descriptor);
|
|
536
|
+
}
|
|
537
|
+
catch (closeError) {
|
|
538
|
+
failure = failure === undefined
|
|
539
|
+
? closeError
|
|
540
|
+
: combinedError(failure, closeError, `failed to close marker temporary file for ${path}`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
try {
|
|
544
|
+
unlinkSync(temporaryPath);
|
|
545
|
+
}
|
|
546
|
+
catch (cleanupError) {
|
|
547
|
+
if (!isErrorCode(cleanupError, 'ENOENT')) {
|
|
548
|
+
failure = failure === undefined
|
|
549
|
+
? cleanupError
|
|
550
|
+
: combinedError(failure, cleanupError, `failed to clean marker temporary file for ${path}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (failure !== undefined)
|
|
555
|
+
throw failure;
|
|
556
|
+
}
|
|
557
|
+
requireMarker(state, path, kind) {
|
|
558
|
+
const identity = this.validateManagedDirectory(state, path);
|
|
559
|
+
const markerPath = join(path, MARKER);
|
|
560
|
+
this.testHooks.beforeMarkerOpen?.({ markerPath, path, kind });
|
|
561
|
+
const currentIdentity = this.validateManagedDirectory(state, path);
|
|
562
|
+
if (!sameIdentity(currentIdentity, identity))
|
|
563
|
+
throw new Error(`invalid managed workspace marker: ${path}`);
|
|
564
|
+
let descriptor;
|
|
565
|
+
try {
|
|
566
|
+
descriptor = openSync(markerPath, constants.O_RDONLY | NOFOLLOW);
|
|
567
|
+
const markerStat = fstatSync(descriptor);
|
|
568
|
+
if (!markerStat.isFile() || markerStat.size > 256)
|
|
569
|
+
throw new Error('invalid marker file');
|
|
570
|
+
const actual = readFileSync(descriptor);
|
|
571
|
+
const expected = expectedMarker(state, path, kind);
|
|
572
|
+
if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {
|
|
573
|
+
throw new Error('invalid marker contents');
|
|
574
|
+
}
|
|
575
|
+
return identity;
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
throw new Error(`invalid managed workspace marker: ${path}`);
|
|
579
|
+
}
|
|
580
|
+
finally {
|
|
581
|
+
if (descriptor !== undefined)
|
|
582
|
+
closeSync(descriptor);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
authoritativeFixedWorkspaces(root) {
|
|
586
|
+
const configured = this.canonicalFixedWorkspaceSnapshot();
|
|
587
|
+
this.assertFixedWorkspaceSnapshotSafe(root, configured);
|
|
588
|
+
return configured;
|
|
589
|
+
}
|
|
590
|
+
canonicalFixedWorkspaceSnapshot() {
|
|
591
|
+
try {
|
|
592
|
+
const configured = this.getFixedWorkspaces();
|
|
593
|
+
if (!Array.isArray(configured))
|
|
594
|
+
throw new Error('fixed workspace provider must return an array');
|
|
595
|
+
return Array.from(configured, (path) => {
|
|
596
|
+
const canonical = realpathSync(path);
|
|
597
|
+
if (!statSync(canonical).isDirectory())
|
|
598
|
+
throw new Error('fixed workspace must be a directory');
|
|
599
|
+
return canonical;
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
if (error instanceof FixedWorkspaceConfigurationError)
|
|
604
|
+
throw error;
|
|
605
|
+
throw new FixedWorkspaceConfigurationError('invalid fixed workspace configuration', { cause: error });
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
assertFixedWorkspaceSnapshotSafe(root, configured) {
|
|
609
|
+
for (const canonical of configured) {
|
|
610
|
+
if (pathsOverlap(root, canonical)) {
|
|
611
|
+
throw new FixedWorkspaceConfigurationError('fixed workspace overlaps managed root');
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
assertFixedWorkspacesSafe(state) {
|
|
616
|
+
this.authoritativeFixedWorkspaces(state.root);
|
|
617
|
+
}
|
|
618
|
+
requireRootIdentity(state) {
|
|
619
|
+
const entry = lstatSync(state.root);
|
|
620
|
+
if (entry.isSymbolicLink() || !entry.isDirectory() || !sameIdentity(identityOf(entry), state.rootIdentity)) {
|
|
621
|
+
throw new Error('managed workspace root changed');
|
|
622
|
+
}
|
|
623
|
+
if (realpathSync(state.root) !== state.root)
|
|
624
|
+
throw new Error('managed workspace root is not canonical');
|
|
625
|
+
}
|
|
626
|
+
exclusive(operation) {
|
|
627
|
+
const result = this.mutationTail.then(operation);
|
|
628
|
+
this.mutationTail = result.then(() => undefined, () => undefined);
|
|
629
|
+
return result;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
class FixedWorkspaceConfigurationError extends Error {
|
|
633
|
+
name = 'FixedWorkspaceConfigurationError';
|
|
634
|
+
}
|
|
635
|
+
function combinedError(primary, cleanup, message) {
|
|
636
|
+
return new AggregateError([primary, cleanup], `${message}: ${String(primary)}; cleanup: ${String(cleanup)}`, { cause: primary });
|
|
637
|
+
}
|
|
638
|
+
function loadOrCreateOwnerKey(root) {
|
|
639
|
+
const keyPath = join(root, OWNER_KEY);
|
|
640
|
+
const candidatePath = join(root, `.${OWNER_KEY}.${randomBytes(24).toString('hex')}.tmp`);
|
|
641
|
+
const candidateKey = randomBytes(OWNER_KEY_BYTES);
|
|
642
|
+
let descriptor;
|
|
643
|
+
let candidateCreated = false;
|
|
644
|
+
try {
|
|
645
|
+
descriptor = openSync(candidatePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | NOFOLLOW, 0o600);
|
|
646
|
+
candidateCreated = true;
|
|
647
|
+
writeFileSync(descriptor, candidateKey);
|
|
648
|
+
fchmodSync(descriptor, 0o600);
|
|
649
|
+
fsyncSync(descriptor);
|
|
650
|
+
closeSync(descriptor);
|
|
651
|
+
descriptor = undefined;
|
|
652
|
+
try {
|
|
653
|
+
linkSync(candidatePath, keyPath);
|
|
654
|
+
syncDirectory(root);
|
|
655
|
+
return candidateKey;
|
|
656
|
+
}
|
|
657
|
+
catch (error) {
|
|
658
|
+
if (!isErrorCode(error, 'EEXIST'))
|
|
659
|
+
throw error;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
finally {
|
|
663
|
+
if (descriptor !== undefined)
|
|
664
|
+
closeSync(descriptor);
|
|
665
|
+
if (candidateCreated) {
|
|
666
|
+
try {
|
|
667
|
+
unlinkSync(candidatePath);
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
if (!isErrorCode(error, 'ENOENT'))
|
|
671
|
+
throw error;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
try {
|
|
676
|
+
const entry = lstatSync(keyPath);
|
|
677
|
+
if (entry.isSymbolicLink() || !entry.isFile())
|
|
678
|
+
throw new Error('invalid owner key');
|
|
679
|
+
descriptor = openSync(keyPath, constants.O_RDONLY | NOFOLLOW);
|
|
680
|
+
const opened = fstatSync(descriptor);
|
|
681
|
+
if (!opened.isFile() || opened.size !== OWNER_KEY_BYTES)
|
|
682
|
+
throw new Error('invalid owner key');
|
|
683
|
+
fchmodSync(descriptor, 0o600);
|
|
684
|
+
return readFileSync(descriptor);
|
|
685
|
+
}
|
|
686
|
+
finally {
|
|
687
|
+
if (descriptor !== undefined)
|
|
688
|
+
closeSync(descriptor);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
function expectedMarker(state, path, kind) {
|
|
692
|
+
const relativePath = relative(state.root, path).split(sep).join('/');
|
|
693
|
+
const digest = createHmac('sha256', state.ownerKey)
|
|
694
|
+
.update('gateway-workspace-v1\0')
|
|
695
|
+
.update(kind)
|
|
696
|
+
.update('\0')
|
|
697
|
+
.update(relativePath)
|
|
698
|
+
.digest('hex');
|
|
699
|
+
return Buffer.from(`v1:${digest}\n`, 'utf8');
|
|
700
|
+
}
|
|
701
|
+
function ensurePrivateDirectory(path) {
|
|
702
|
+
try {
|
|
703
|
+
mkdirSync(path, { mode: 0o700 });
|
|
704
|
+
}
|
|
705
|
+
catch (error) {
|
|
706
|
+
if (!isErrorCode(error, 'EEXIST'))
|
|
707
|
+
throw error;
|
|
708
|
+
const entry = lstatSync(path);
|
|
709
|
+
if (entry.isSymbolicLink() || !entry.isDirectory())
|
|
710
|
+
throw new Error(`unsafe private directory: ${path}`);
|
|
711
|
+
}
|
|
712
|
+
chmodSync(path, 0o700);
|
|
713
|
+
if (realpathSync(path) !== path)
|
|
714
|
+
throw new Error(`private directory is not canonical: ${path}`);
|
|
715
|
+
}
|
|
716
|
+
function syncDirectory(path) {
|
|
717
|
+
let descriptor;
|
|
718
|
+
try {
|
|
719
|
+
descriptor = openSync(path, constants.O_RDONLY);
|
|
720
|
+
fsyncSync(descriptor);
|
|
721
|
+
}
|
|
722
|
+
catch (error) {
|
|
723
|
+
if (!isErrorCode(error, 'EINVAL') && !isErrorCode(error, 'ENOTSUP'))
|
|
724
|
+
throw error;
|
|
725
|
+
}
|
|
726
|
+
finally {
|
|
727
|
+
if (descriptor !== undefined)
|
|
728
|
+
closeSync(descriptor);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function canonicalizeProspectivePath(path) {
|
|
732
|
+
let current = resolve(path);
|
|
733
|
+
const missing = [];
|
|
734
|
+
while (true) {
|
|
735
|
+
try {
|
|
736
|
+
return resolve(realpathSync(current), ...missing);
|
|
737
|
+
}
|
|
738
|
+
catch (error) {
|
|
739
|
+
if (!isErrorCode(error, 'ENOENT'))
|
|
740
|
+
throw error;
|
|
741
|
+
const parent = dirname(current);
|
|
742
|
+
if (parent === current)
|
|
743
|
+
throw error;
|
|
744
|
+
missing.unshift(basename(current));
|
|
745
|
+
current = parent;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
function directoryIdentity(path) {
|
|
750
|
+
const entry = lstatSync(path);
|
|
751
|
+
if (entry.isSymbolicLink() || !entry.isDirectory())
|
|
752
|
+
throw new Error(`unsafe managed workspace: ${path}`);
|
|
753
|
+
return identityOf(entry);
|
|
754
|
+
}
|
|
755
|
+
function identityOf(entry) {
|
|
756
|
+
return { dev: entry.dev, ino: entry.ino };
|
|
757
|
+
}
|
|
758
|
+
function sameIdentity(left, right) {
|
|
759
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
760
|
+
}
|
|
761
|
+
function validateComponent(kind, value) {
|
|
762
|
+
if (!COMPONENT.test(value))
|
|
763
|
+
throw new Error(`invalid ${kind} id`);
|
|
764
|
+
return value;
|
|
765
|
+
}
|
|
766
|
+
function cleanupKind(parts) {
|
|
767
|
+
if (parts.length === 2 && parts[0] === 'responses') {
|
|
768
|
+
validateComponent('response', parts[1]);
|
|
769
|
+
return 'response';
|
|
770
|
+
}
|
|
771
|
+
if (parts.length === 2 && parts[0] === 'temporary') {
|
|
772
|
+
validateComponent('run', parts[1]);
|
|
773
|
+
return 'temporary';
|
|
774
|
+
}
|
|
775
|
+
if (parts.length === 4
|
|
776
|
+
&& parts[0] === 'targets'
|
|
777
|
+
&& parts[2] === 'slots'
|
|
778
|
+
&& /^(0|[1-9][0-9]*)$/.test(parts[3])) {
|
|
779
|
+
validateComponent('target', parts[1]);
|
|
780
|
+
return 'strict';
|
|
781
|
+
}
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
function isMissingPath(error) {
|
|
785
|
+
return error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
786
|
+
}
|
|
787
|
+
function isMissingWorkspacePath(path, error) {
|
|
788
|
+
if (!isMissingPath(error))
|
|
789
|
+
return false;
|
|
790
|
+
try {
|
|
791
|
+
lstatSync(path);
|
|
792
|
+
return false;
|
|
793
|
+
}
|
|
794
|
+
catch (pathError) {
|
|
795
|
+
return isMissingPath(pathError);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
function assertContained(root, path, allowRoot) {
|
|
799
|
+
if (!isContained(root, path, allowRoot))
|
|
800
|
+
throw new Error(`workspace path resolves outside managed root: ${path}`);
|
|
801
|
+
}
|
|
802
|
+
function pathsOverlap(left, right) {
|
|
803
|
+
return isContained(left, right, true) || isContained(right, left, true);
|
|
804
|
+
}
|
|
805
|
+
function isContained(root, path, allowRoot) {
|
|
806
|
+
const relation = relative(root, path);
|
|
807
|
+
if (relation === '')
|
|
808
|
+
return allowRoot;
|
|
809
|
+
return !isAbsolute(relation) && relation !== '..' && !relation.startsWith(`..${sep}`);
|
|
810
|
+
}
|
|
811
|
+
function isErrorCode(error, code) {
|
|
812
|
+
return error instanceof Error && 'code' in error && error.code === code;
|
|
813
|
+
}
|
|
814
|
+
//# sourceMappingURL=workspaces.js.map
|