@linzumi/cli 0.0.4-beta → 0.0.6-beta
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 +197 -85
- package/package.json +17 -11
- package/src/authResolution.ts +2 -0
- package/src/boundedCache.ts +57 -0
- package/src/channelSession.ts +907 -453
- package/src/codexRuntimeOptions.ts +80 -0
- package/src/dependencyStatus.ts +198 -0
- package/src/forwardTunnel.ts +834 -0
- package/src/forwardTunnelProtocol.ts +324 -0
- package/src/index.ts +414 -30
- package/src/kandanTls.ts +86 -0
- package/src/localCapabilities.ts +130 -0
- package/src/localCodexMessageState.ts +135 -0
- package/src/localCodexTurnState.ts +108 -0
- package/src/localEditor.ts +963 -0
- package/src/localEditorRuntime.ts +603 -0
- package/src/localForwarding.ts +500 -0
- package/src/oauth.ts +135 -4
- package/src/pendingKandanMessageQueue.ts +109 -0
- package/src/phoenix.ts +25 -1
- package/src/portForwardApproval.ts +181 -0
- package/src/portForwardWatcher.ts +404 -0
- package/src/protocol.ts +97 -3
- package/src/runner.ts +413 -28
- package/src/streamDeltaCoalescing.ts +129 -0
- package/src/streamDeltaQueue.ts +102 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
/*
|
|
2
|
+
- Date: 2026-04-29
|
|
3
|
+
Spec: kandan/server_v2/plans/2026-04-29-local-runner-editor-runtime-distribution-note.md
|
|
4
|
+
Relationship: Keeps local editor launches on a Kandan-approved,
|
|
5
|
+
checksummed runtime distribution instead of relying on host package-manager
|
|
6
|
+
code-server layouts.
|
|
7
|
+
*/
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import {
|
|
11
|
+
createReadStream,
|
|
12
|
+
createWriteStream,
|
|
13
|
+
existsSync,
|
|
14
|
+
mkdirSync,
|
|
15
|
+
mkdtempSync,
|
|
16
|
+
readFileSync,
|
|
17
|
+
renameSync,
|
|
18
|
+
rmSync,
|
|
19
|
+
writeFileSync,
|
|
20
|
+
} from "node:fs";
|
|
21
|
+
import { homedir, tmpdir } from "node:os";
|
|
22
|
+
import { dirname, join, resolve } from "node:path";
|
|
23
|
+
import { Readable } from "node:stream";
|
|
24
|
+
import { pipeline } from "node:stream/promises";
|
|
25
|
+
import { kandanHttpBaseUrl } from "./oauth";
|
|
26
|
+
import { isJsonObject, type JsonObject } from "./protocol";
|
|
27
|
+
|
|
28
|
+
export type EditorRuntimeManifest = {
|
|
29
|
+
readonly version: string;
|
|
30
|
+
readonly platform: string;
|
|
31
|
+
readonly archiveUrl: string;
|
|
32
|
+
readonly archiveSha256: string;
|
|
33
|
+
readonly codeServerVersion: string;
|
|
34
|
+
readonly codeServerBinPath: string;
|
|
35
|
+
readonly manifestPath: string;
|
|
36
|
+
readonly assets: readonly JsonObject[];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type EditorRuntimeStatus =
|
|
40
|
+
| {
|
|
41
|
+
readonly status: "ready";
|
|
42
|
+
readonly mode: "server_managed";
|
|
43
|
+
readonly version: string;
|
|
44
|
+
readonly platform: string;
|
|
45
|
+
readonly archiveSha256: string;
|
|
46
|
+
readonly codeServerVersion: string;
|
|
47
|
+
readonly codeServerBin: string;
|
|
48
|
+
readonly updated: boolean;
|
|
49
|
+
}
|
|
50
|
+
| {
|
|
51
|
+
readonly status: "custom";
|
|
52
|
+
readonly mode: "custom";
|
|
53
|
+
readonly codeServerBin: string;
|
|
54
|
+
}
|
|
55
|
+
| {
|
|
56
|
+
readonly status: "unavailable";
|
|
57
|
+
readonly mode: "server_managed";
|
|
58
|
+
readonly reason:
|
|
59
|
+
| "unsupported_platform"
|
|
60
|
+
| "manifest_unavailable"
|
|
61
|
+
| "download_failed"
|
|
62
|
+
| "checksum_mismatch"
|
|
63
|
+
| "archive_extract_failed"
|
|
64
|
+
| "invalid_archive"
|
|
65
|
+
| "install_failed";
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type InstalledEditorRuntime = {
|
|
69
|
+
readonly mode: "server_managed";
|
|
70
|
+
readonly root: string;
|
|
71
|
+
readonly codeServerBin: string;
|
|
72
|
+
readonly assets: {
|
|
73
|
+
readonly collaborationExtensionTarball: string;
|
|
74
|
+
readonly collaborationServerTarball: string;
|
|
75
|
+
readonly documentStateExtensionDir: string;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type ResolveEditorRuntimeOptions = {
|
|
80
|
+
readonly kandanUrl: string;
|
|
81
|
+
readonly token: string;
|
|
82
|
+
readonly customCodeServerBin?: string | undefined;
|
|
83
|
+
readonly cacheRoot?: string | undefined;
|
|
84
|
+
readonly platform?: NodeJS.Platform | undefined;
|
|
85
|
+
readonly arch?: NodeJS.Architecture | undefined;
|
|
86
|
+
readonly fetchImpl?: typeof fetch | undefined;
|
|
87
|
+
readonly extractArchive?: ((archivePath: string, destination: string) => Promise<boolean>) | undefined;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export type ResolveEditorRuntimeResult = {
|
|
91
|
+
readonly status: EditorRuntimeStatus;
|
|
92
|
+
readonly codeServerBin?: string | undefined;
|
|
93
|
+
readonly runtime?: InstalledEditorRuntime | undefined;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export async function resolveEditorRuntime(
|
|
97
|
+
options: ResolveEditorRuntimeOptions,
|
|
98
|
+
): Promise<ResolveEditorRuntimeResult> {
|
|
99
|
+
if (options.customCodeServerBin !== undefined) {
|
|
100
|
+
return {
|
|
101
|
+
codeServerBin: options.customCodeServerBin,
|
|
102
|
+
status: {
|
|
103
|
+
status: "custom",
|
|
104
|
+
mode: "custom",
|
|
105
|
+
codeServerBin: options.customCodeServerBin,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const platform = editorRuntimePlatform(
|
|
111
|
+
options.platform ?? process.platform,
|
|
112
|
+
options.arch ?? process.arch,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (platform === undefined) {
|
|
116
|
+
return unavailable("unsupported_platform");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const manifest = await fetchApprovedManifest({
|
|
120
|
+
kandanUrl: options.kandanUrl,
|
|
121
|
+
token: options.token,
|
|
122
|
+
platform,
|
|
123
|
+
fetchImpl: options.fetchImpl ?? fetch,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (!manifest.ok) {
|
|
127
|
+
return unavailable("manifest_unavailable");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const cacheRoot = options.cacheRoot ?? defaultEditorRuntimeCacheRoot();
|
|
131
|
+
const installed = installedRuntime(cacheRoot, manifest.manifest);
|
|
132
|
+
|
|
133
|
+
if (installed.ok) {
|
|
134
|
+
return ready(manifest.manifest, installed.runtime, false);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const installResult = await installRuntime({
|
|
138
|
+
kandanUrl: options.kandanUrl,
|
|
139
|
+
token: options.token,
|
|
140
|
+
manifest: manifest.manifest,
|
|
141
|
+
cacheRoot,
|
|
142
|
+
fetchImpl: options.fetchImpl ?? fetch,
|
|
143
|
+
extractArchive: options.extractArchive ?? extractTarGz,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (!installResult.ok) {
|
|
147
|
+
return unavailable(installResult.reason);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return ready(manifest.manifest, installResult.runtime, true);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function editorRuntimeStatusPayload(status: EditorRuntimeStatus): JsonObject {
|
|
154
|
+
switch (status.status) {
|
|
155
|
+
case "ready":
|
|
156
|
+
return {
|
|
157
|
+
status: "ready",
|
|
158
|
+
mode: status.mode,
|
|
159
|
+
version: status.version,
|
|
160
|
+
platform: status.platform,
|
|
161
|
+
archiveSha256: status.archiveSha256,
|
|
162
|
+
codeServerVersion: status.codeServerVersion,
|
|
163
|
+
updated: status.updated,
|
|
164
|
+
};
|
|
165
|
+
case "custom":
|
|
166
|
+
return {
|
|
167
|
+
status: "custom",
|
|
168
|
+
mode: status.mode,
|
|
169
|
+
codeServerBin: status.codeServerBin,
|
|
170
|
+
};
|
|
171
|
+
case "unavailable":
|
|
172
|
+
return {
|
|
173
|
+
status: "unavailable",
|
|
174
|
+
mode: status.mode,
|
|
175
|
+
reason: status.reason,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function editorRuntimePlatform(
|
|
181
|
+
platform: NodeJS.Platform,
|
|
182
|
+
arch: NodeJS.Architecture,
|
|
183
|
+
): string | undefined {
|
|
184
|
+
switch (`${platform}-${arch}`) {
|
|
185
|
+
case "darwin-arm64":
|
|
186
|
+
return "darwin-arm64";
|
|
187
|
+
case "darwin-x64":
|
|
188
|
+
return "darwin-x64";
|
|
189
|
+
default:
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function unavailable(reason: Extract<EditorRuntimeStatus, { status: "unavailable" }>["reason"]): ResolveEditorRuntimeResult {
|
|
195
|
+
return {
|
|
196
|
+
status: {
|
|
197
|
+
status: "unavailable",
|
|
198
|
+
mode: "server_managed",
|
|
199
|
+
reason,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function ready(
|
|
205
|
+
manifest: EditorRuntimeManifest,
|
|
206
|
+
runtime: InstalledEditorRuntime,
|
|
207
|
+
updated: boolean,
|
|
208
|
+
): ResolveEditorRuntimeResult {
|
|
209
|
+
return {
|
|
210
|
+
codeServerBin: runtime.codeServerBin,
|
|
211
|
+
runtime,
|
|
212
|
+
status: {
|
|
213
|
+
status: "ready",
|
|
214
|
+
mode: "server_managed",
|
|
215
|
+
version: manifest.version,
|
|
216
|
+
platform: manifest.platform,
|
|
217
|
+
archiveSha256: manifest.archiveSha256,
|
|
218
|
+
codeServerVersion: manifest.codeServerVersion,
|
|
219
|
+
codeServerBin: runtime.codeServerBin,
|
|
220
|
+
updated,
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function fetchApprovedManifest(args: {
|
|
226
|
+
readonly kandanUrl: string;
|
|
227
|
+
readonly token: string;
|
|
228
|
+
readonly platform: string;
|
|
229
|
+
readonly fetchImpl: typeof fetch;
|
|
230
|
+
}): Promise<
|
|
231
|
+
| { readonly ok: true; readonly manifest: EditorRuntimeManifest }
|
|
232
|
+
| { readonly ok: false }
|
|
233
|
+
> {
|
|
234
|
+
const url = new URL(
|
|
235
|
+
`/api/v2/local-codex-runner/editor-runtime/${encodeURIComponent(args.platform)}/manifest`,
|
|
236
|
+
kandanHttpBaseUrl(args.kandanUrl),
|
|
237
|
+
);
|
|
238
|
+
const response = await args.fetchImpl(url, {
|
|
239
|
+
headers: { authorization: `Bearer ${args.token}` },
|
|
240
|
+
}).catch(() => undefined);
|
|
241
|
+
|
|
242
|
+
if (response === undefined) {
|
|
243
|
+
return { ok: false };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (response.status !== 200) {
|
|
247
|
+
return { ok: false };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const body: unknown = await response.json();
|
|
251
|
+
|
|
252
|
+
if (!isJsonObject(body) || body.ok !== true || !isJsonObject(body.runtime)) {
|
|
253
|
+
return { ok: false };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return normalizeManifest(body.runtime);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function normalizeManifest(value: JsonObject):
|
|
260
|
+
| { readonly ok: true; readonly manifest: EditorRuntimeManifest }
|
|
261
|
+
| { readonly ok: false } {
|
|
262
|
+
const version = nonEmptyString(value.version);
|
|
263
|
+
const platform = nonEmptyString(value.platform);
|
|
264
|
+
const archiveUrl = nonEmptyString(value.archiveUrl);
|
|
265
|
+
const archiveSha256 = sha256String(value.archiveSha256);
|
|
266
|
+
const codeServerVersion = nonEmptyString(value.codeServerVersion);
|
|
267
|
+
const codeServerBinPath = nonEmptyString(value.codeServerBinPath) ?? "bin/code-server";
|
|
268
|
+
const manifestPath = nonEmptyString(value.manifestPath) ?? "linzumi-editor-runtime.json";
|
|
269
|
+
const assets = Array.isArray(value.assets) && value.assets.every(isJsonObject)
|
|
270
|
+
? value.assets
|
|
271
|
+
: [];
|
|
272
|
+
|
|
273
|
+
if (
|
|
274
|
+
version === undefined ||
|
|
275
|
+
platform === undefined ||
|
|
276
|
+
archiveUrl === undefined ||
|
|
277
|
+
archiveSha256 === undefined ||
|
|
278
|
+
codeServerVersion === undefined
|
|
279
|
+
) {
|
|
280
|
+
return { ok: false };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
ok: true,
|
|
285
|
+
manifest: {
|
|
286
|
+
version,
|
|
287
|
+
platform,
|
|
288
|
+
archiveUrl,
|
|
289
|
+
archiveSha256,
|
|
290
|
+
codeServerVersion,
|
|
291
|
+
codeServerBinPath,
|
|
292
|
+
manifestPath,
|
|
293
|
+
assets,
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function installedRuntime(
|
|
299
|
+
cacheRoot: string,
|
|
300
|
+
manifest: EditorRuntimeManifest,
|
|
301
|
+
): { readonly ok: true; readonly runtime: InstalledEditorRuntime } | { readonly ok: false } {
|
|
302
|
+
const runtimeRoot = runtimeInstallRoot(cacheRoot, manifest);
|
|
303
|
+
const manifestPath = join(runtimeRoot, manifest.manifestPath);
|
|
304
|
+
const codeServerBin = join(runtimeRoot, manifest.codeServerBinPath);
|
|
305
|
+
const assets = verifiedRuntimeAssetPaths(runtimeRoot, manifest);
|
|
306
|
+
|
|
307
|
+
if (!existsSync(manifestPath) || !existsSync(codeServerBin) || assets === undefined) {
|
|
308
|
+
return { ok: false };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const installed: unknown = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
313
|
+
|
|
314
|
+
if (
|
|
315
|
+
isJsonObject(installed) &&
|
|
316
|
+
installed.version === manifest.version &&
|
|
317
|
+
installed.platform === manifest.platform &&
|
|
318
|
+
(installed.archiveSha256 === undefined ||
|
|
319
|
+
installed.archiveSha256 === manifest.archiveSha256)
|
|
320
|
+
) {
|
|
321
|
+
return {
|
|
322
|
+
ok: true,
|
|
323
|
+
runtime: {
|
|
324
|
+
mode: "server_managed",
|
|
325
|
+
root: runtimeRoot,
|
|
326
|
+
codeServerBin,
|
|
327
|
+
assets,
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
} catch (_error) {
|
|
332
|
+
return { ok: false };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return { ok: false };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function installRuntime(args: {
|
|
339
|
+
readonly kandanUrl: string;
|
|
340
|
+
readonly token: string;
|
|
341
|
+
readonly manifest: EditorRuntimeManifest;
|
|
342
|
+
readonly cacheRoot: string;
|
|
343
|
+
readonly fetchImpl: typeof fetch;
|
|
344
|
+
readonly extractArchive: (archivePath: string, destination: string) => Promise<boolean>;
|
|
345
|
+
}): Promise<
|
|
346
|
+
| { readonly ok: true; readonly runtime: InstalledEditorRuntime }
|
|
347
|
+
| { readonly ok: false; readonly reason: Extract<EditorRuntimeStatus, { status: "unavailable" }>["reason"] }
|
|
348
|
+
> {
|
|
349
|
+
mkdirSync(args.cacheRoot, { recursive: true });
|
|
350
|
+
|
|
351
|
+
const tempRoot = mkdtempSync(join(tmpdir(), "linzumi-editor-runtime-install-"));
|
|
352
|
+
const archivePath = join(tempRoot, "runtime.tar.gz");
|
|
353
|
+
const extractRoot = join(tempRoot, "runtime");
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
const downloaded = await downloadArchive({
|
|
357
|
+
kandanUrl: args.kandanUrl,
|
|
358
|
+
token: args.token,
|
|
359
|
+
manifest: args.manifest,
|
|
360
|
+
archivePath,
|
|
361
|
+
fetchImpl: args.fetchImpl,
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
if (!downloaded.ok) {
|
|
365
|
+
return downloaded;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
mkdirSync(extractRoot, { recursive: true });
|
|
369
|
+
|
|
370
|
+
if (!(await args.extractArchive(archivePath, extractRoot))) {
|
|
371
|
+
return { ok: false, reason: "archive_extract_failed" };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const manifestPath = join(extractRoot, args.manifest.manifestPath);
|
|
375
|
+
const codeServerBin = join(extractRoot, args.manifest.codeServerBinPath);
|
|
376
|
+
const assets = verifiedRuntimeAssetPaths(extractRoot, args.manifest);
|
|
377
|
+
|
|
378
|
+
if (!existsSync(manifestPath) || !existsSync(codeServerBin) || assets === undefined) {
|
|
379
|
+
return { ok: false, reason: "invalid_archive" };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const installed: unknown = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
383
|
+
|
|
384
|
+
if (
|
|
385
|
+
!isJsonObject(installed) ||
|
|
386
|
+
installed.version !== args.manifest.version ||
|
|
387
|
+
installed.platform !== args.manifest.platform ||
|
|
388
|
+
(installed.archiveSha256 !== undefined &&
|
|
389
|
+
installed.archiveSha256 !== args.manifest.archiveSha256)
|
|
390
|
+
) {
|
|
391
|
+
return { ok: false, reason: "invalid_archive" };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
writeFileSync(
|
|
395
|
+
manifestPath,
|
|
396
|
+
JSON.stringify(
|
|
397
|
+
{
|
|
398
|
+
...installed,
|
|
399
|
+
archiveSha256: args.manifest.archiveSha256,
|
|
400
|
+
codeServerVersion: args.manifest.codeServerVersion,
|
|
401
|
+
},
|
|
402
|
+
null,
|
|
403
|
+
2,
|
|
404
|
+
),
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
const targetRoot = runtimeInstallRoot(args.cacheRoot, args.manifest);
|
|
408
|
+
rmSync(targetRoot, { recursive: true, force: true });
|
|
409
|
+
mkdirSync(dirname(targetRoot), { recursive: true });
|
|
410
|
+
renameSync(extractRoot, targetRoot);
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
ok: true,
|
|
414
|
+
runtime: {
|
|
415
|
+
mode: "server_managed",
|
|
416
|
+
root: targetRoot,
|
|
417
|
+
codeServerBin: join(targetRoot, args.manifest.codeServerBinPath),
|
|
418
|
+
assets: {
|
|
419
|
+
collaborationExtensionTarball: join(
|
|
420
|
+
targetRoot,
|
|
421
|
+
"kandan",
|
|
422
|
+
"editor_extensions",
|
|
423
|
+
"typefox.open-collaboration-tools.tar.gz",
|
|
424
|
+
),
|
|
425
|
+
collaborationServerTarball: join(
|
|
426
|
+
targetRoot,
|
|
427
|
+
"kandan",
|
|
428
|
+
"editor_servers",
|
|
429
|
+
"open-collaboration-server.tar.gz",
|
|
430
|
+
),
|
|
431
|
+
documentStateExtensionDir: join(
|
|
432
|
+
targetRoot,
|
|
433
|
+
"kandan",
|
|
434
|
+
"editor_extensions",
|
|
435
|
+
"kandan.document-state-telemetry",
|
|
436
|
+
),
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
} catch (_error) {
|
|
441
|
+
return { ok: false, reason: "install_failed" };
|
|
442
|
+
} finally {
|
|
443
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function downloadArchive(args: {
|
|
448
|
+
readonly kandanUrl: string;
|
|
449
|
+
readonly token: string;
|
|
450
|
+
readonly manifest: EditorRuntimeManifest;
|
|
451
|
+
readonly archivePath: string;
|
|
452
|
+
readonly fetchImpl: typeof fetch;
|
|
453
|
+
}): Promise<
|
|
454
|
+
| { readonly ok: true }
|
|
455
|
+
| { readonly ok: false; readonly reason: "download_failed" | "checksum_mismatch" }
|
|
456
|
+
> {
|
|
457
|
+
const url = new URL(args.manifest.archiveUrl, kandanHttpBaseUrl(args.kandanUrl));
|
|
458
|
+
const response = await args.fetchImpl(url, {
|
|
459
|
+
headers: { authorization: `Bearer ${args.token}` },
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
if (response.status !== 200 || response.body === null) {
|
|
463
|
+
return { ok: false, reason: "download_failed" };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
await pipeline(
|
|
467
|
+
Readable.fromWeb(response.body),
|
|
468
|
+
createWriteStream(args.archivePath),
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
const sha256 = await fileSha256(args.archivePath);
|
|
472
|
+
|
|
473
|
+
if (sha256 !== args.manifest.archiveSha256) {
|
|
474
|
+
return { ok: false, reason: "checksum_mismatch" };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return { ok: true };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function extractTarGz(archivePath: string, destination: string): Promise<boolean> {
|
|
481
|
+
return new Promise((resolveExtract) => {
|
|
482
|
+
const child = spawn("tar", ["-xzf", archivePath, "-C", destination], {
|
|
483
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
child.on("error", () => resolveExtract(false));
|
|
487
|
+
child.on("exit", (code) => resolveExtract(code === 0));
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function fileSha256(path: string): Promise<string> {
|
|
492
|
+
return new Promise((resolveHash, rejectHash) => {
|
|
493
|
+
const hash = createHash("sha256");
|
|
494
|
+
const stream = createReadStream(path);
|
|
495
|
+
|
|
496
|
+
stream.on("error", rejectHash);
|
|
497
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
498
|
+
stream.on("end", () => resolveHash(hash.digest("hex")));
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function runtimeInstallRoot(
|
|
503
|
+
cacheRoot: string,
|
|
504
|
+
manifest: EditorRuntimeManifest,
|
|
505
|
+
): string {
|
|
506
|
+
return resolve(cacheRoot, manifest.platform, manifest.archiveSha256);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function verifiedRuntimeAssetPaths(
|
|
510
|
+
runtimeRoot: string,
|
|
511
|
+
manifest: EditorRuntimeManifest,
|
|
512
|
+
): InstalledEditorRuntime["assets"] | undefined {
|
|
513
|
+
const collaborationExtensionTarball = join(
|
|
514
|
+
runtimeRoot,
|
|
515
|
+
"kandan",
|
|
516
|
+
"editor_extensions",
|
|
517
|
+
"typefox.open-collaboration-tools.tar.gz",
|
|
518
|
+
);
|
|
519
|
+
const collaborationServerTarball = join(
|
|
520
|
+
runtimeRoot,
|
|
521
|
+
"kandan",
|
|
522
|
+
"editor_servers",
|
|
523
|
+
"open-collaboration-server.tar.gz",
|
|
524
|
+
);
|
|
525
|
+
const documentStateExtensionDir = join(
|
|
526
|
+
runtimeRoot,
|
|
527
|
+
"kandan",
|
|
528
|
+
"editor_extensions",
|
|
529
|
+
"kandan.document-state-telemetry",
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
const requiredPaths = [
|
|
533
|
+
manifest.codeServerBinPath,
|
|
534
|
+
"lib/vscode/node_modules/vsda/rust/web/vsda.js",
|
|
535
|
+
"lib/vscode/node_modules/vsda/rust/web/vsda_bg.wasm",
|
|
536
|
+
"kandan/editor_extensions/typefox.open-collaboration-tools.tar.gz",
|
|
537
|
+
"kandan/editor_servers/open-collaboration-server.tar.gz",
|
|
538
|
+
"kandan/editor_extensions/kandan.document-state-telemetry/package.json",
|
|
539
|
+
"kandan/editor_extensions/kandan.document-state-telemetry/out/extension.js",
|
|
540
|
+
];
|
|
541
|
+
const assetChecksums = manifestAssetChecksums(manifest.assets);
|
|
542
|
+
|
|
543
|
+
for (const relativePath of requiredPaths) {
|
|
544
|
+
const expectedSha256 = assetChecksums.get(relativePath);
|
|
545
|
+
|
|
546
|
+
if (expectedSha256 === undefined) {
|
|
547
|
+
return undefined;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const absolutePath = join(runtimeRoot, relativePath);
|
|
551
|
+
|
|
552
|
+
if (!existsSync(absolutePath) || fileSha256Sync(absolutePath) !== expectedSha256) {
|
|
553
|
+
return undefined;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (!existsSync(collaborationExtensionTarball) || !existsSync(collaborationServerTarball)) {
|
|
558
|
+
return undefined;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
collaborationExtensionTarball,
|
|
563
|
+
collaborationServerTarball,
|
|
564
|
+
documentStateExtensionDir,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function manifestAssetChecksums(assets: readonly JsonObject[]): Map<string, string> {
|
|
569
|
+
const checksums = new Map<string, string>();
|
|
570
|
+
|
|
571
|
+
for (const asset of assets) {
|
|
572
|
+
const path = nonEmptyString(asset.path);
|
|
573
|
+
const sha256 = sha256String(asset.sha256);
|
|
574
|
+
|
|
575
|
+
if (path !== undefined && sha256 !== undefined) {
|
|
576
|
+
checksums.set(path, sha256);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return checksums;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function fileSha256Sync(path: string): string {
|
|
584
|
+
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function defaultEditorRuntimeCacheRoot(): string {
|
|
588
|
+
return join(homedir(), ".linzumi", "editor-runtimes");
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function nonEmptyString(value: unknown): string | undefined {
|
|
592
|
+
return typeof value === "string" && value.trim() !== ""
|
|
593
|
+
? value.trim()
|
|
594
|
+
: undefined;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function sha256String(value: unknown): string | undefined {
|
|
598
|
+
const normalized = nonEmptyString(value);
|
|
599
|
+
|
|
600
|
+
return normalized !== undefined && /^[a-f0-9]{64}$/i.test(normalized)
|
|
601
|
+
? normalized.toLowerCase()
|
|
602
|
+
: undefined;
|
|
603
|
+
}
|