@dsh-electron/dsh-plugin-multi-root-workspace 0.1.11 → 0.1.12
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/CHANGELOG.en.md +13 -1
- package/CHANGELOG.md +13 -1
- package/README.en.md +9 -8
- package/README.md +9 -8
- package/cordis.patch.yml +3 -2
- package/lib/auto-review.js +1 -1
- package/lib/client.js +96 -49
- package/lib/client.js.map +1 -1
- package/lib/{command-BGdz7VBH.js → command-CmLekG1M.js} +3 -2
- package/lib/command.js +1 -1
- package/lib/{compat-BiW-m_q0.js → compat-Bg2HFVMX.js} +3 -0
- package/lib/compat.js +1 -1
- package/lib/{sandbox-CXk5h-ct.js → dialects-Dr4rDR-H.js} +6 -264
- package/lib/{fs-BTXySwSG.js → fs-CfzJq2wd.js} +23 -8
- package/lib/fs.js +1 -1
- package/lib/index.js +7 -6
- package/lib/{instructions-MePoRigy.js → instructions-KsUwUFE0.js} +53 -37
- package/lib/instructions.js +1 -1
- package/lib/lsp.js +19 -16
- package/lib/native-win32-2waCRkPI.js +312 -0
- package/lib/process-fence.js +62 -12
- package/lib/protocol-4zdwV7u4.js +641 -0
- package/lib/{registry-KSsmU393.js → registry-YTUOjmVu.js} +368 -25
- package/lib/registry-operation-C2ZV3Jhw.js +116 -0
- package/lib/registry.js +3 -2
- package/lib/{review-context-AH-SXHmj.js → review-context-NDpKeMCc.js} +1 -1
- package/lib/review-context.js +1 -1
- package/lib/sandbox-C-vCJCp3.js +278 -0
- package/lib/sandbox.js +1 -1
- package/lib/scope.js +65 -9
- package/lib/skills.js +48 -29
- package/lib/types/authority/native-win32.d.ts +7 -0
- package/lib/types/authority/native.d.ts +23 -0
- package/lib/types/authority/protocol.d.ts +70 -0
- package/lib/types/compat/authority-storage.d.ts +6 -0
- package/lib/types/compat/dsh-version.d.ts +4 -4
- package/lib/types/compat/open-in-app-device.d.ts +1 -1
- package/lib/types/compat/process-entrypoints.d.ts +2 -0
- package/lib/types/compat/process-profile.d.ts +3 -0
- package/lib/types/compat/sandbox-confine.d.ts +2 -0
- package/lib/types/fs.d.ts +4 -1
- package/lib/types/instructions.d.ts +0 -47
- package/lib/types/registry-operation.d.ts +31 -0
- package/lib/types/registry.d.ts +47 -6
- package/lib/types/scope.d.ts +10 -0
- package/lib/types/skills/catalog.d.ts +3 -0
- package/lib/types/skills/pool.d.ts +1 -1
- package/lib/workspace-files.js +63 -7
- package/package.json +38 -31
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
import { closeSync, lstatSync, mkdirSync, readFileSync, realpathSync, statSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { Socket } from "node:net";
|
|
6
|
+
//#region src/authority/native.ts
|
|
7
|
+
/** Kernel-authenticated local IPC. No credential file and no inherited capability. */
|
|
8
|
+
function storeIdentity(root) {
|
|
9
|
+
mkdirSync(root, {
|
|
10
|
+
recursive: true,
|
|
11
|
+
mode: 448
|
|
12
|
+
});
|
|
13
|
+
const physical = realpathSync.native(root);
|
|
14
|
+
const info = statSync(physical, { bigint: true });
|
|
15
|
+
const id = createHash("sha256").update(`${info.dev}:${info.ino}:${process.platform === "win32" ? physical.toLowerCase() : physical}`).digest("hex");
|
|
16
|
+
if (process.platform === "win32") return {
|
|
17
|
+
root: physical,
|
|
18
|
+
id,
|
|
19
|
+
endpoint: `\\\\.\\pipe\\LOCAL\\dsh-registry-${id}`
|
|
20
|
+
};
|
|
21
|
+
const directory = `/tmp/dsh-registry-${process.getuid()}`;
|
|
22
|
+
try {
|
|
23
|
+
mkdirSync(directory, { mode: 448 });
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (error.code !== "EEXIST") throw error;
|
|
26
|
+
}
|
|
27
|
+
const entry = lstatSync(directory);
|
|
28
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || entry.uid !== process.getuid() || (entry.mode & 63) !== 0) throw new Error("unsafe registry IPC directory");
|
|
29
|
+
return {
|
|
30
|
+
root: physical,
|
|
31
|
+
id,
|
|
32
|
+
endpoint: join(directory, `${id.slice(0, 48)}.sock`)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
let posixPromise;
|
|
36
|
+
async function loadPosix() {
|
|
37
|
+
const koffi = (await import("koffi")).default;
|
|
38
|
+
const libc = koffi.load(process.platform === "darwin" ? "/usr/lib/libSystem.B.dylib" : "libc.so.6");
|
|
39
|
+
const bind = (name, ret, args) => libc.func(name, ret, args);
|
|
40
|
+
const socketpair = bind("socketpair", "int", [
|
|
41
|
+
"int",
|
|
42
|
+
"int",
|
|
43
|
+
"int",
|
|
44
|
+
"void *"
|
|
45
|
+
]);
|
|
46
|
+
const socket = bind("socket", "int", [
|
|
47
|
+
"int",
|
|
48
|
+
"int",
|
|
49
|
+
"int"
|
|
50
|
+
]);
|
|
51
|
+
const connect = bind("connect", "int", [
|
|
52
|
+
"int",
|
|
53
|
+
"void *",
|
|
54
|
+
"uint"
|
|
55
|
+
]);
|
|
56
|
+
const bindSocket = bind("bind", "int", [
|
|
57
|
+
"int",
|
|
58
|
+
"void *",
|
|
59
|
+
"uint"
|
|
60
|
+
]);
|
|
61
|
+
const listen = bind("listen", "int", ["int", "int"]);
|
|
62
|
+
const accept = bind("accept", "int", [
|
|
63
|
+
"int",
|
|
64
|
+
"void *",
|
|
65
|
+
"void *"
|
|
66
|
+
]);
|
|
67
|
+
const fcntlCall = libc.func("int fcntl(int fd, int command, ...)");
|
|
68
|
+
const fcntl = (fd, command, value) => fcntlCall(fd, command, "int", value);
|
|
69
|
+
const getsockopt = bind("getsockopt", "int", [
|
|
70
|
+
"int",
|
|
71
|
+
"int",
|
|
72
|
+
"int",
|
|
73
|
+
"void *",
|
|
74
|
+
"void *"
|
|
75
|
+
]);
|
|
76
|
+
const poll = bind("poll", "int", [
|
|
77
|
+
"void *",
|
|
78
|
+
"uint",
|
|
79
|
+
"int"
|
|
80
|
+
]);
|
|
81
|
+
let auditCheck;
|
|
82
|
+
if (process.platform === "darwin") {
|
|
83
|
+
const tokenType = koffi.struct({ val: koffi.array("uint32", 8) });
|
|
84
|
+
auditCheck = koffi.load("/usr/lib/libsandbox.dylib").func("sandbox_check_by_audit_token", "int", [
|
|
85
|
+
tokenType,
|
|
86
|
+
"void *",
|
|
87
|
+
"int"
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
90
|
+
function option(fd, level, name, size) {
|
|
91
|
+
const bytes = Buffer.alloc(size), length = Buffer.alloc(4);
|
|
92
|
+
length.writeUInt32LE(size);
|
|
93
|
+
if (getsockopt(fd, level, name, bytes, length) !== 0 || length.readUInt32LE() !== size) throw new Error("kernel peer attestation is unavailable");
|
|
94
|
+
return bytes;
|
|
95
|
+
}
|
|
96
|
+
function authenticate(fd) {
|
|
97
|
+
const uid = process.getuid();
|
|
98
|
+
if (process.platform === "darwin") {
|
|
99
|
+
const token = option(fd, 0, 6, 32);
|
|
100
|
+
if (token.readUInt32LE(4) !== uid || auditCheck({ val: [...new Uint32Array(token.buffer, token.byteOffset, 8)] }, null, 0) !== 0) throw new Error("registry IPC refuses a different user or sandboxed peer");
|
|
101
|
+
return () => {};
|
|
102
|
+
}
|
|
103
|
+
const cred = option(fd, 1, 17, 12);
|
|
104
|
+
const pidfd = option(fd, 1, 77, 4).readInt32LE();
|
|
105
|
+
try {
|
|
106
|
+
const pfd = Buffer.alloc(8);
|
|
107
|
+
pfd.writeInt32LE(pidfd);
|
|
108
|
+
pfd.writeInt16LE(1, 4);
|
|
109
|
+
const status = readFileSync(`/proc/${cred.readInt32LE()}/status`, "utf8");
|
|
110
|
+
const ids = /^Uid:\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/m.exec(status);
|
|
111
|
+
if (cred.readUInt32LE(4) !== uid || ids === null || ids.slice(1).some((value) => Number(value) !== uid) || !/^NoNewPrivs:\s+0$/m.test(status) || poll(pfd, 1, 0) !== 0) throw new Error("registry IPC refuses a different user, exited or restricted peer");
|
|
112
|
+
return () => closeSync(pidfd);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
closeSync(pidfd);
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function address(path) {
|
|
119
|
+
const data = Buffer.from(path);
|
|
120
|
+
if (data.length >= (process.platform === "darwin" ? 104 : 108)) throw new Error("registry IPC path is too long");
|
|
121
|
+
const bytes = Buffer.alloc(data.length + 3);
|
|
122
|
+
if (process.platform === "darwin") {
|
|
123
|
+
bytes[0] = bytes.length;
|
|
124
|
+
bytes[1] = 1;
|
|
125
|
+
} else bytes.writeUInt16LE(1);
|
|
126
|
+
data.copy(bytes, 2);
|
|
127
|
+
return bytes;
|
|
128
|
+
}
|
|
129
|
+
function open() {
|
|
130
|
+
const fd = socket(1, 1, 0);
|
|
131
|
+
if (fd < 0) throw new Error("cannot create registry IPC socket");
|
|
132
|
+
if (fcntl(fd, 2, 1) !== 0 || fcntl(fd, 4, process.platform === "darwin" ? 4 : 2048) !== 0) {
|
|
133
|
+
closeSync(fd);
|
|
134
|
+
throw new Error("cannot make registry socket non-inheritable/non-blocking");
|
|
135
|
+
}
|
|
136
|
+
return fd;
|
|
137
|
+
}
|
|
138
|
+
function channel(fd) {
|
|
139
|
+
let cleanup;
|
|
140
|
+
try {
|
|
141
|
+
if (fcntl(fd, 2, 1) !== 0 || fcntl(fd, 4, process.platform === "darwin" ? 4 : 2048) !== 0) throw new Error("cannot make registry connection non-inheritable/non-blocking");
|
|
142
|
+
cleanup = authenticate(fd);
|
|
143
|
+
const result = new Socket({
|
|
144
|
+
fd,
|
|
145
|
+
readable: true,
|
|
146
|
+
writable: true
|
|
147
|
+
});
|
|
148
|
+
result.on("close", cleanup);
|
|
149
|
+
result.on("error", () => {});
|
|
150
|
+
return result;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
cleanup?.();
|
|
153
|
+
closeSync(fd);
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
open,
|
|
159
|
+
channel,
|
|
160
|
+
address,
|
|
161
|
+
connect,
|
|
162
|
+
bindSocket,
|
|
163
|
+
listen,
|
|
164
|
+
accept,
|
|
165
|
+
authenticate,
|
|
166
|
+
socketpair
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function posix$1() {
|
|
170
|
+
return posixPromise ??= loadPosix();
|
|
171
|
+
}
|
|
172
|
+
async function connectNative(endpoint) {
|
|
173
|
+
if (process.platform === "win32") return (await import("./native-win32-2waCRkPI.js")).connectWindows(endpoint);
|
|
174
|
+
const api = await posix$1(), address = api.address(endpoint), fd = api.open();
|
|
175
|
+
if (api.connect(fd, address, address.length) !== 0) {
|
|
176
|
+
closeSync(fd);
|
|
177
|
+
throw new Error("registry authority is not ready");
|
|
178
|
+
}
|
|
179
|
+
return api.channel(fd);
|
|
180
|
+
}
|
|
181
|
+
/** Called only by the kernel lease holder. Never removes another epoch's socket. */
|
|
182
|
+
async function listenNative(endpoint, accepted) {
|
|
183
|
+
if (process.platform === "win32") return (await import("./native-win32-2waCRkPI.js")).listenWindows(endpoint, accepted);
|
|
184
|
+
const api = await posix$1(), address = api.address(endpoint), fd = api.open();
|
|
185
|
+
try {
|
|
186
|
+
unlinkSync(endpoint);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (error.code !== "ENOENT") {
|
|
189
|
+
closeSync(fd);
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (api.bindSocket(fd, address, address.length) !== 0 || api.listen(fd, 32) !== 0) {
|
|
194
|
+
closeSync(fd);
|
|
195
|
+
throw new Error("cannot listen for registry Hosts");
|
|
196
|
+
}
|
|
197
|
+
const inode = lstatSync(endpoint).ino;
|
|
198
|
+
let closed = false;
|
|
199
|
+
const timer = setInterval(() => {
|
|
200
|
+
for (let i = 0; i < 8 && !closed; i++) {
|
|
201
|
+
const client = api.accept(fd, null, null);
|
|
202
|
+
if (client < 0) break;
|
|
203
|
+
let channel;
|
|
204
|
+
try {
|
|
205
|
+
channel = api.channel(client);
|
|
206
|
+
accepted(channel);
|
|
207
|
+
} catch {
|
|
208
|
+
channel?.destroy();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}, 20);
|
|
212
|
+
timer.unref();
|
|
213
|
+
return { close() {
|
|
214
|
+
if (closed) return;
|
|
215
|
+
closed = true;
|
|
216
|
+
clearInterval(timer);
|
|
217
|
+
closeSync(fd);
|
|
218
|
+
try {
|
|
219
|
+
if (lstatSync(endpoint).ino === inode) unlinkSync(endpoint);
|
|
220
|
+
} catch {}
|
|
221
|
+
} };
|
|
222
|
+
}
|
|
223
|
+
/** Capability probe for native tests and diagnostics; not an authorization bypass. */
|
|
224
|
+
async function nativeHostAvailability() {
|
|
225
|
+
if (process.platform === "win32") return (await import("./native-win32-2waCRkPI.js")).windowsHostAvailability();
|
|
226
|
+
if (process.platform === "linux") {
|
|
227
|
+
if (!/^NoNewPrivs:\s+0$/m.test(readFileSync("/proc/self/status", "utf8"))) return {
|
|
228
|
+
available: false,
|
|
229
|
+
reason: "the Host itself has NoNewPrivs; shared IPC refuses restricted peers"
|
|
230
|
+
};
|
|
231
|
+
} else if (process.platform === "darwin") {
|
|
232
|
+
if ((await import("koffi")).default.load("/usr/lib/libsandbox.dylib").func("int sandbox_check(int pid, const char *operation, int filter)")(process.pid, null, 0) !== 0) return {
|
|
233
|
+
available: false,
|
|
234
|
+
reason: "the Host itself is sandboxed; shared IPC refuses sandboxed peers"
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const api = await posix$1(), pair = Buffer.alloc(8);
|
|
238
|
+
if (api.socketpair(1, 1, 0, pair) !== 0) throw new Error("cannot probe registry IPC credentials");
|
|
239
|
+
try {
|
|
240
|
+
api.authenticate(pair.readInt32LE())();
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (process.platform === "linux" && String(error).includes("kernel peer attestation is unavailable")) return {
|
|
243
|
+
available: false,
|
|
244
|
+
reason: "the kernel lacks SO_PEERPIDFD; shared IPC requires a pinned peer identity"
|
|
245
|
+
};
|
|
246
|
+
throw error;
|
|
247
|
+
} finally {
|
|
248
|
+
closeSync(pair.readInt32LE());
|
|
249
|
+
closeSync(pair.readInt32LE(4));
|
|
250
|
+
}
|
|
251
|
+
return { available: true };
|
|
252
|
+
}
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/authority/protocol.ts
|
|
255
|
+
/** Bounded, versioned requests on an already kernel-attested connection. */
|
|
256
|
+
const MAX_FRAME = 1048576;
|
|
257
|
+
const MAX_PENDING = 32;
|
|
258
|
+
const helloSchema = z.object({
|
|
259
|
+
protocol: z.literal(1),
|
|
260
|
+
store: z.string(),
|
|
261
|
+
epoch: z.string(),
|
|
262
|
+
limit: z.number().int()
|
|
263
|
+
}).strict();
|
|
264
|
+
const requestSchema = z.object({
|
|
265
|
+
protocol: z.literal(1),
|
|
266
|
+
store: z.string(),
|
|
267
|
+
id: z.string().uuid(),
|
|
268
|
+
epoch: z.string().nullable(),
|
|
269
|
+
method: z.enum([
|
|
270
|
+
"hello",
|
|
271
|
+
"snapshot",
|
|
272
|
+
"admit",
|
|
273
|
+
"add",
|
|
274
|
+
"removeAt",
|
|
275
|
+
"setAccess",
|
|
276
|
+
"setAlias",
|
|
277
|
+
"setSkillsEnabled",
|
|
278
|
+
"setAliasId",
|
|
279
|
+
"move",
|
|
280
|
+
"recheck"
|
|
281
|
+
]),
|
|
282
|
+
args: z.array(z.unknown()).max(4)
|
|
283
|
+
}).strict();
|
|
284
|
+
const rootSchema = z.object({
|
|
285
|
+
id: z.string(),
|
|
286
|
+
path: z.string(),
|
|
287
|
+
access: z.enum([
|
|
288
|
+
"inherit",
|
|
289
|
+
"read-only",
|
|
290
|
+
"read-write"
|
|
291
|
+
]),
|
|
292
|
+
addedAt: z.string(),
|
|
293
|
+
recordedPath: z.string().optional(),
|
|
294
|
+
readOnlyRecordedPath: z.string().optional(),
|
|
295
|
+
readWriteRecordedPath: z.string().optional(),
|
|
296
|
+
alias: z.string().optional(),
|
|
297
|
+
aliasId: z.unknown().optional(),
|
|
298
|
+
skillsEnabled: z.unknown().optional(),
|
|
299
|
+
state: z.enum([
|
|
300
|
+
"available",
|
|
301
|
+
"missing",
|
|
302
|
+
"redirected",
|
|
303
|
+
"invalid"
|
|
304
|
+
]),
|
|
305
|
+
detail: z.string().optional(),
|
|
306
|
+
invalidReason: z.literal("membership-conflict").optional()
|
|
307
|
+
}).strict();
|
|
308
|
+
const snapshotSchema = z.object({
|
|
309
|
+
protocol: z.literal(1),
|
|
310
|
+
store: z.string(),
|
|
311
|
+
epoch: z.string(),
|
|
312
|
+
revision: z.number().int().nonnegative().safe(),
|
|
313
|
+
primary: z.string(),
|
|
314
|
+
roots: z.array(rootSchema).max(4096),
|
|
315
|
+
available: z.boolean()
|
|
316
|
+
}).strict();
|
|
317
|
+
const replySchema = z.discriminatedUnion("type", [
|
|
318
|
+
z.object({
|
|
319
|
+
type: z.literal("reply"),
|
|
320
|
+
id: z.string().uuid(),
|
|
321
|
+
value: z.unknown()
|
|
322
|
+
}).strict(),
|
|
323
|
+
z.object({
|
|
324
|
+
type: z.literal("error"),
|
|
325
|
+
id: z.string().uuid(),
|
|
326
|
+
code: z.string(),
|
|
327
|
+
message: z.string()
|
|
328
|
+
}).strict(),
|
|
329
|
+
z.object({
|
|
330
|
+
type: z.literal("change"),
|
|
331
|
+
value: snapshotSchema
|
|
332
|
+
}).strict()
|
|
333
|
+
]);
|
|
334
|
+
var AuthorityRpcError = class extends Error {
|
|
335
|
+
code;
|
|
336
|
+
constructor(code, message) {
|
|
337
|
+
super(message);
|
|
338
|
+
this.code = code;
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
function receive(channel, body) {
|
|
342
|
+
let data = Buffer.alloc(0);
|
|
343
|
+
channel.on("data", (chunk) => {
|
|
344
|
+
try {
|
|
345
|
+
if (data.length + chunk.length > MAX_FRAME) throw new Error("registry IPC frame exceeds budget");
|
|
346
|
+
data = Buffer.concat([data, chunk]);
|
|
347
|
+
for (;;) {
|
|
348
|
+
const end = data.indexOf(10);
|
|
349
|
+
if (end < 0) break;
|
|
350
|
+
const line = data.subarray(0, end);
|
|
351
|
+
data = data.subarray(end + 1);
|
|
352
|
+
body(JSON.parse(line.toString("utf8")));
|
|
353
|
+
}
|
|
354
|
+
} catch {
|
|
355
|
+
channel.destroy();
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
function send(channel, value) {
|
|
360
|
+
const text = JSON.stringify(value) + "\n";
|
|
361
|
+
if (Buffer.byteLength(text) + (channel.writableLength ?? 0) > MAX_FRAME) {
|
|
362
|
+
channel.destroy();
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
channel.write(text);
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
var AuthorityClient = class AuthorityClient {
|
|
369
|
+
channel;
|
|
370
|
+
store;
|
|
371
|
+
limit;
|
|
372
|
+
pending = /* @__PURE__ */ new Map();
|
|
373
|
+
closed = false;
|
|
374
|
+
epoch;
|
|
375
|
+
constructor(channel, store, limit, onChange, disconnected) {
|
|
376
|
+
this.channel = channel;
|
|
377
|
+
this.store = store;
|
|
378
|
+
this.limit = limit;
|
|
379
|
+
channel.on("close", () => {
|
|
380
|
+
if (this.closed) return;
|
|
381
|
+
this.closed = true;
|
|
382
|
+
for (const request of this.pending.values()) {
|
|
383
|
+
clearTimeout(request.timer);
|
|
384
|
+
request.reject(new AuthorityRpcError("authority-disconnected", "registry request result is unknown; resynchronize before another mutation"));
|
|
385
|
+
}
|
|
386
|
+
this.pending.clear();
|
|
387
|
+
disconnected();
|
|
388
|
+
});
|
|
389
|
+
channel.on("error", () => channel.destroy());
|
|
390
|
+
receive(channel, (value) => {
|
|
391
|
+
const reply = replySchema.parse(value);
|
|
392
|
+
if (reply.type === "change") {
|
|
393
|
+
if (reply.value.store !== store || reply.value.epoch !== this.epoch) throw new Error("wrong registry epoch");
|
|
394
|
+
onChange(reply.value);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const request = this.pending.get(reply.id);
|
|
398
|
+
if (!request) return;
|
|
399
|
+
if (reply.type === "reply" && request.method === "hello") {
|
|
400
|
+
const hello = helloSchema.parse(reply.value);
|
|
401
|
+
if (hello.store !== store || hello.limit !== this.limit) throw new Error("registry configuration mismatch");
|
|
402
|
+
this.epoch = hello.epoch;
|
|
403
|
+
}
|
|
404
|
+
this.pending.delete(reply.id);
|
|
405
|
+
clearTimeout(request.timer);
|
|
406
|
+
if (reply.type === "reply") request.resolve(reply.value);
|
|
407
|
+
else request.reject(new AuthorityRpcError(reply.code, reply.message));
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
static async connect(endpoint, store, limit, onChange, disconnected) {
|
|
411
|
+
const client = new AuthorityClient(await connectNative(endpoint), store, limit, onChange, disconnected);
|
|
412
|
+
try {
|
|
413
|
+
const hello = helloSchema.parse(await client.request("hello", [limit]));
|
|
414
|
+
if (hello.store !== store || hello.limit !== limit) throw new Error("registry configuration mismatch");
|
|
415
|
+
client.epoch = hello.epoch;
|
|
416
|
+
return client;
|
|
417
|
+
} catch (error) {
|
|
418
|
+
client.close();
|
|
419
|
+
throw error;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
request(method, args) {
|
|
423
|
+
if (this.closed || this.pending.size >= MAX_PENDING) return Promise.reject(new AuthorityRpcError("authority-unavailable", "registry connection is unavailable or busy"));
|
|
424
|
+
const id = randomUUID();
|
|
425
|
+
return new Promise((resolve, reject) => {
|
|
426
|
+
const timer = setTimeout(() => {
|
|
427
|
+
this.channel.destroy();
|
|
428
|
+
}, 5e3);
|
|
429
|
+
this.pending.set(id, {
|
|
430
|
+
method,
|
|
431
|
+
resolve,
|
|
432
|
+
reject,
|
|
433
|
+
timer
|
|
434
|
+
});
|
|
435
|
+
send(this.channel, {
|
|
436
|
+
protocol: 1,
|
|
437
|
+
store: this.store,
|
|
438
|
+
epoch: this.epoch ?? null,
|
|
439
|
+
id,
|
|
440
|
+
method,
|
|
441
|
+
args
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
close() {
|
|
446
|
+
this.channel.destroy();
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
var AuthorityServer = class AuthorityServer {
|
|
450
|
+
store;
|
|
451
|
+
epoch;
|
|
452
|
+
limit;
|
|
453
|
+
dispatch;
|
|
454
|
+
listener;
|
|
455
|
+
peers = /* @__PURE__ */ new Set();
|
|
456
|
+
readyPeers = /* @__PURE__ */ new Set();
|
|
457
|
+
closed = false;
|
|
458
|
+
inFlight = 0;
|
|
459
|
+
constructor(store, epoch, limit, dispatch) {
|
|
460
|
+
this.store = store;
|
|
461
|
+
this.epoch = epoch;
|
|
462
|
+
this.limit = limit;
|
|
463
|
+
this.dispatch = dispatch;
|
|
464
|
+
}
|
|
465
|
+
static async listen(endpoint, store, epoch, limit, dispatch) {
|
|
466
|
+
const server = new AuthorityServer(store, epoch, limit, dispatch);
|
|
467
|
+
server.listener = await listenNative(endpoint, (channel) => server.accept(channel));
|
|
468
|
+
return server;
|
|
469
|
+
}
|
|
470
|
+
accept(channel) {
|
|
471
|
+
if (this.closed || this.peers.size >= 32) {
|
|
472
|
+
channel.destroy();
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
this.peers.add(channel);
|
|
476
|
+
let ready = false, pending = 0;
|
|
477
|
+
const seen = /* @__PURE__ */ new Map();
|
|
478
|
+
const timer = setTimeout(() => {
|
|
479
|
+
if (!ready) channel.destroy();
|
|
480
|
+
}, 5e3);
|
|
481
|
+
timer.unref();
|
|
482
|
+
channel.on("close", () => {
|
|
483
|
+
clearTimeout(timer);
|
|
484
|
+
this.peers.delete(channel);
|
|
485
|
+
this.readyPeers.delete(channel);
|
|
486
|
+
});
|
|
487
|
+
channel.on("error", () => channel.destroy());
|
|
488
|
+
receive(channel, (value) => {
|
|
489
|
+
const request = requestSchema.parse(value);
|
|
490
|
+
if (request.store !== this.store || pending >= MAX_PENDING) throw new Error("registry peer violated protocol");
|
|
491
|
+
if (request.method === "hello") {
|
|
492
|
+
if (ready || request.epoch !== null || request.args[0] !== this.limit) throw new Error("registry handshake mismatch");
|
|
493
|
+
ready = true;
|
|
494
|
+
this.readyPeers.add(channel);
|
|
495
|
+
clearTimeout(timer);
|
|
496
|
+
send(channel, {
|
|
497
|
+
type: "reply",
|
|
498
|
+
id: request.id,
|
|
499
|
+
value: {
|
|
500
|
+
protocol: 1,
|
|
501
|
+
store: this.store,
|
|
502
|
+
epoch: this.epoch,
|
|
503
|
+
limit: this.limit
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (!ready || request.epoch !== this.epoch) throw new Error("registry epoch mismatch");
|
|
509
|
+
if (this.inFlight >= MAX_PENDING) {
|
|
510
|
+
send(channel, {
|
|
511
|
+
type: "error",
|
|
512
|
+
id: request.id,
|
|
513
|
+
code: "authority-busy",
|
|
514
|
+
message: "registry is busy; request was not submitted"
|
|
515
|
+
});
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const mutation = request.method !== "snapshot" && request.method !== "admit";
|
|
519
|
+
if (mutation && !seen.has(request.id) && seen.size >= 2048) {
|
|
520
|
+
channel.destroy();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const fingerprint = createHash("sha256").update(JSON.stringify([request.method, request.args])).digest("hex");
|
|
524
|
+
const entry = seen.get(request.id);
|
|
525
|
+
if (entry && entry.fingerprint !== fingerprint) throw new Error("registry request ID reused with different arguments");
|
|
526
|
+
pending++;
|
|
527
|
+
this.inFlight++;
|
|
528
|
+
const run = entry ? entry.outcome.then((outcome) => {
|
|
529
|
+
if (!outcome.ok) throw new AuthorityRpcError(outcome.code, outcome.message);
|
|
530
|
+
return this.dispatch("snapshot", [request.args[0]]);
|
|
531
|
+
}) : this.dispatch(request.method, request.args);
|
|
532
|
+
if (mutation && !entry) seen.set(request.id, {
|
|
533
|
+
fingerprint,
|
|
534
|
+
outcome: run.then(() => ({ ok: true }), (error) => ({
|
|
535
|
+
ok: false,
|
|
536
|
+
code: String(error?.code ?? "registry-error").slice(0, 120),
|
|
537
|
+
message: String(error?.message ?? error).slice(0, 4096)
|
|
538
|
+
}))
|
|
539
|
+
});
|
|
540
|
+
run.then((result) => send(channel, {
|
|
541
|
+
type: "reply",
|
|
542
|
+
id: request.id,
|
|
543
|
+
value: result
|
|
544
|
+
}), (error) => send(channel, {
|
|
545
|
+
type: "error",
|
|
546
|
+
id: request.id,
|
|
547
|
+
code: String(error?.code ?? "registry-error"),
|
|
548
|
+
message: String(error?.message ?? error)
|
|
549
|
+
})).finally(() => {
|
|
550
|
+
pending--;
|
|
551
|
+
this.inFlight--;
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
changed(snapshot) {
|
|
556
|
+
if (this.closed) return;
|
|
557
|
+
for (const peer of this.readyPeers) send(peer, {
|
|
558
|
+
type: "change",
|
|
559
|
+
value: snapshot
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
close() {
|
|
563
|
+
if (this.closed) return;
|
|
564
|
+
this.closed = true;
|
|
565
|
+
this.listener?.close();
|
|
566
|
+
for (const peer of this.peers) peer.destroy();
|
|
567
|
+
this.peers.clear();
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
const ref = z.discriminatedUnion("kind", [
|
|
571
|
+
z.object({
|
|
572
|
+
kind: z.literal("id"),
|
|
573
|
+
id: z.string()
|
|
574
|
+
}).strict(),
|
|
575
|
+
z.object({
|
|
576
|
+
kind: z.literal("path"),
|
|
577
|
+
path: z.string()
|
|
578
|
+
}).strict(),
|
|
579
|
+
z.object({
|
|
580
|
+
kind: z.literal("ordinal"),
|
|
581
|
+
ordinal: z.number().int().positive()
|
|
582
|
+
}).strict(),
|
|
583
|
+
z.object({
|
|
584
|
+
kind: z.literal("entry"),
|
|
585
|
+
ordinal: z.number().int().positive(),
|
|
586
|
+
id: z.string(),
|
|
587
|
+
path: z.string(),
|
|
588
|
+
addedAt: z.string()
|
|
589
|
+
}).strict()
|
|
590
|
+
]);
|
|
591
|
+
const primary = z.string().min(1);
|
|
592
|
+
const argumentSchemas = {
|
|
593
|
+
hello: z.tuple([z.number().int()]),
|
|
594
|
+
snapshot: z.tuple([primary]),
|
|
595
|
+
admit: z.tuple([primary]),
|
|
596
|
+
recheck: z.tuple([primary]),
|
|
597
|
+
add: z.tuple([primary, z.object({
|
|
598
|
+
path: z.string(),
|
|
599
|
+
alias: z.string().optional(),
|
|
600
|
+
access: z.enum([
|
|
601
|
+
"inherit",
|
|
602
|
+
"read-only",
|
|
603
|
+
"read-write"
|
|
604
|
+
]).optional()
|
|
605
|
+
}).strict()]),
|
|
606
|
+
removeAt: z.tuple([primary, ref]),
|
|
607
|
+
setAccess: z.tuple([
|
|
608
|
+
primary,
|
|
609
|
+
ref,
|
|
610
|
+
z.enum([
|
|
611
|
+
"inherit",
|
|
612
|
+
"read-only",
|
|
613
|
+
"read-write"
|
|
614
|
+
])
|
|
615
|
+
]),
|
|
616
|
+
setAlias: z.tuple([
|
|
617
|
+
primary,
|
|
618
|
+
ref,
|
|
619
|
+
z.string().nullable()
|
|
620
|
+
]),
|
|
621
|
+
setSkillsEnabled: z.tuple([
|
|
622
|
+
primary,
|
|
623
|
+
ref,
|
|
624
|
+
z.boolean()
|
|
625
|
+
]),
|
|
626
|
+
setAliasId: z.tuple([
|
|
627
|
+
primary,
|
|
628
|
+
ref,
|
|
629
|
+
z.string()
|
|
630
|
+
]),
|
|
631
|
+
move: z.tuple([
|
|
632
|
+
primary,
|
|
633
|
+
ref,
|
|
634
|
+
ref.nullable()
|
|
635
|
+
])
|
|
636
|
+
};
|
|
637
|
+
function parseArguments(method, args) {
|
|
638
|
+
return argumentSchemas[method].parse(args);
|
|
639
|
+
}
|
|
640
|
+
//#endregion
|
|
641
|
+
export { snapshotSchema as a, parseArguments as i, AuthorityRpcError as n, nativeHostAvailability as o, AuthorityServer as r, storeIdentity as s, AuthorityClient as t };
|