@rynx-ai/daemon 0.1.0 → 0.1.10-beta.2

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.
Files changed (109) hide show
  1. package/dist/app-browser-host-supervisor.d.ts +97 -0
  2. package/dist/app-browser-host-supervisor.js +529 -0
  3. package/dist/browser-artifact-management.d.ts +20 -0
  4. package/dist/browser-artifact-management.js +61 -0
  5. package/dist/browser-cli-args.d.ts +20 -0
  6. package/dist/browser-cli-args.js +100 -0
  7. package/dist/browser-runner-session-context.d.ts +24 -0
  8. package/dist/browser-runner-session-context.js +113 -0
  9. package/dist/cdp-keyboard-input.d.ts +6 -0
  10. package/dist/cdp-keyboard-input.js +209 -0
  11. package/dist/channel-store.d.ts +0 -6
  12. package/dist/channel-store.js +2 -18
  13. package/dist/chrome-for-testing-release-resolver.d.ts +34 -0
  14. package/dist/chrome-for-testing-release-resolver.js +214 -0
  15. package/dist/chrome-for-testing-store.d.ts +85 -0
  16. package/dist/chrome-for-testing-store.js +836 -0
  17. package/dist/chrome-inspection-gateway.d.ts +58 -0
  18. package/dist/chrome-inspection-gateway.js +806 -0
  19. package/dist/chrome-inspection-manager.d.ts +94 -0
  20. package/dist/chrome-inspection-manager.js +573 -0
  21. package/dist/cli.js +478 -30
  22. package/dist/control-client.d.ts +122 -0
  23. package/dist/control-client.js +855 -0
  24. package/dist/control-deps.js +37 -6
  25. package/dist/control-endpoint.d.ts +14 -0
  26. package/dist/control-endpoint.js +124 -0
  27. package/dist/control-link-store.d.ts +16 -0
  28. package/dist/control-link-store.js +53 -0
  29. package/dist/daemon-build-status.d.ts +8 -0
  30. package/dist/daemon-build-status.js +18 -0
  31. package/dist/daemon-owner.d.ts +14 -0
  32. package/dist/daemon-owner.js +104 -0
  33. package/dist/daemon-server.d.ts +29 -2
  34. package/dist/daemon-server.js +1113 -26
  35. package/dist/db.js +196 -10
  36. package/dist/desktop-browser-host-client.d.ts +35 -0
  37. package/dist/desktop-browser-host-client.js +415 -0
  38. package/dist/direct-runtime-authenticator.d.ts +13 -0
  39. package/dist/direct-runtime-authenticator.js +132 -0
  40. package/dist/direct-runtime-config.d.ts +20 -0
  41. package/dist/direct-runtime-config.js +101 -0
  42. package/dist/embedded-browser-profile-cleanup.d.ts +58 -0
  43. package/dist/embedded-browser-profile-cleanup.js +338 -0
  44. package/dist/headless-browser-host.d.ts +14 -0
  45. package/dist/headless-browser-host.js +2173 -0
  46. package/dist/index-daemon.js +55 -5
  47. package/dist/installation-id.d.ts +33 -0
  48. package/dist/installation-id.js +692 -0
  49. package/dist/maintenance-management.d.ts +11 -0
  50. package/dist/maintenance-management.js +13 -0
  51. package/dist/migrations/cleanup-legacy-sessions.d.ts +0 -3
  52. package/dist/migrations/cleanup-legacy-sessions.js +10 -28
  53. package/dist/plugin-cli.d.ts +30 -0
  54. package/dist/plugin-cli.js +270 -0
  55. package/dist/plugin-host-rpc.d.ts +85 -0
  56. package/dist/plugin-host-rpc.js +878 -0
  57. package/dist/plugin-install-restart.d.ts +13 -0
  58. package/dist/plugin-install-restart.js +24 -0
  59. package/dist/plugin-installer.d.ts +71 -15
  60. package/dist/plugin-installer.js +521 -155
  61. package/dist/plugin-management-service.d.ts +58 -0
  62. package/dist/plugin-management-service.js +240 -0
  63. package/dist/plugin-package.d.ts +61 -0
  64. package/dist/plugin-package.js +732 -0
  65. package/dist/plugin-runtime-control.d.ts +2 -0
  66. package/dist/plugin-runtime-control.js +37 -0
  67. package/dist/plugin-store.d.ts +37 -8
  68. package/dist/plugin-store.js +80 -15
  69. package/dist/plugin-supervisor.d.ts +160 -0
  70. package/dist/plugin-supervisor.js +1143 -0
  71. package/dist/pm2.d.ts +3 -1
  72. package/dist/pm2.js +92 -18
  73. package/dist/provider-cli-service.d.ts +46 -0
  74. package/dist/provider-cli-service.js +510 -0
  75. package/dist/registry.d.ts +6 -19
  76. package/dist/registry.js +22 -87
  77. package/dist/remote-runtime-access-store.d.ts +62 -0
  78. package/dist/remote-runtime-access-store.js +393 -0
  79. package/dist/remote-runtime-admin.d.ts +35 -0
  80. package/dist/remote-runtime-admin.js +110 -0
  81. package/dist/remote-runtime-connection-manager.d.ts +103 -0
  82. package/dist/remote-runtime-connection-manager.js +1575 -0
  83. package/dist/remote-runtime-credential-store.d.ts +45 -0
  84. package/dist/remote-runtime-credential-store.js +639 -0
  85. package/dist/remote-runtime-target-control.d.ts +51 -0
  86. package/dist/remote-runtime-target-control.js +488 -0
  87. package/dist/remote-runtime-target-store.d.ts +56 -0
  88. package/dist/remote-runtime-target-store.js +326 -0
  89. package/dist/runtime-share-addresses.d.ts +21 -0
  90. package/dist/runtime-share-addresses.js +50 -0
  91. package/dist/session-browser-capability-store.d.ts +13 -0
  92. package/dist/session-browser-capability-store.js +58 -0
  93. package/dist/session-emulator-binding-store.d.ts +10 -0
  94. package/dist/session-emulator-binding-store.js +61 -0
  95. package/dist/session-launch-operations.d.ts +41 -0
  96. package/dist/session-launch-operations.js +124 -0
  97. package/dist/session-log-store.js +29 -0
  98. package/dist/session-meta-store.d.ts +3 -2
  99. package/dist/session-meta-store.js +59 -7
  100. package/dist/session-pending-message-store.d.ts +2 -0
  101. package/dist/session-pending-message-store.js +41 -0
  102. package/dist/session-resource-store.d.ts +32 -0
  103. package/dist/session-resource-store.js +700 -0
  104. package/dist/setup.d.ts +16 -0
  105. package/dist/setup.js +144 -5
  106. package/dist/skills-catalog.js +8 -4
  107. package/dist/update.d.ts +11 -2
  108. package/dist/update.js +0 -27
  109. package/package.json +23 -10
@@ -0,0 +1,732 @@
1
+ /**
2
+ * Static plugin-package materialization and inspection.
3
+ *
4
+ * This module deliberately never imports a plugin entry point. Package code is
5
+ * copied/downloaded with lifecycle scripts disabled, then only package.json and
6
+ * the referenced JSON manifest are parsed.
7
+ */
8
+ import { createHash } from "node:crypto";
9
+ import { spawn } from "node:child_process";
10
+ import { constants as fsConstants, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync, } from "node:fs";
11
+ import { lstat as lstatAsync, open as openFile, opendir, readlink as readlinkAsync, realpath as realpathAsync, } from "node:fs/promises";
12
+ import { homedir } from "node:os";
13
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+ import { create as createTar } from "tar";
16
+ import { parsePluginManifest, } from "@rynx-ai/plugin-sdk";
17
+ const MAX_METADATA_BYTES = 1024 * 1024;
18
+ const PACKAGE_HASH_STREAM_BYTES = 256 * 1024;
19
+ const DEFAULT_COMMAND_ABORT_GRACE_MS = 2_000;
20
+ export const DEFAULT_PLUGIN_PACKAGE_INSPECTION_LIMITS = Object.freeze({
21
+ maxFileBytes: 128 * 1024 * 1024,
22
+ maxTotalBytes: 512 * 1024 * 1024,
23
+ maxEntries: 50_000,
24
+ maxDepth: 64,
25
+ });
26
+ /** Classify provenance without fetching or executing anything. */
27
+ export function detectPluginSource(spec) {
28
+ if (looksLocal(spec))
29
+ return "local";
30
+ if (/^(?:git\+|git:|git@|ssh:|github:|gitlab:|bitbucket:)/i.test(spec) ||
31
+ /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:#.*)?$/.test(spec)) {
32
+ return "git";
33
+ }
34
+ if (/^https?:\/\//i.test(spec))
35
+ return spec.includes(".git") ? "git" : "url";
36
+ return "npm";
37
+ }
38
+ /** Stable provenance for local sources; remote specs are retained verbatim. */
39
+ export function normalizePluginSpec(spec, source) {
40
+ return source === "local" ? toLocalDir(spec) : spec;
41
+ }
42
+ /**
43
+ * Materialize a package without running package lifecycle scripts.
44
+ *
45
+ * Local workspace packages are archived with their internal dependency closure
46
+ * and then installed into a disposable npm prefix. Other local, npm, git, and
47
+ * URL specs use the same prefix flow. Every path therefore reaches inspection
48
+ * with runtime dependencies present, while lifecycle scripts remain disabled.
49
+ */
50
+ export const materializePluginPackage = async ({ spec, source, destination, onProgress, signal, }) => {
51
+ if (source === "local") {
52
+ await materializeLocalDirectory(toLocalDir(spec), destination, onProgress, signal);
53
+ return;
54
+ }
55
+ if (source === "git") {
56
+ const checkout = join(resolve(destination, ".."), "git-checkout");
57
+ await cloneGitPackage(spec, checkout, onProgress, signal);
58
+ await materializeLocalDirectory(checkout, destination, onProgress, signal);
59
+ return;
60
+ }
61
+ const archive = await acquirePublishedPackageArchive(spec, join(resolve(destination, ".."), "package-archive"), onProgress, signal);
62
+ await materializeWithNpm({
63
+ spec: archive,
64
+ prefix: join(resolve(destination, ".."), "remote-prefix"),
65
+ destination,
66
+ onProgress,
67
+ signal,
68
+ });
69
+ };
70
+ async function materializeLocalDirectory(sourceDir, destination, onProgress, signal) {
71
+ if (!existsSync(sourceDir) || !statSync(sourceDir).isDirectory()) {
72
+ throw new Error(`local plugin source is not a directory: ${sourceDir}`);
73
+ }
74
+ const sourceReal = realpathSync(sourceDir);
75
+ if (isWithin(sourceReal, resolve(destination))) {
76
+ throw new Error("plugin staging directory must not be inside the local source");
77
+ }
78
+ if (!hasProductionDependencies(sourceReal)) {
79
+ copyPackageTree(sourceReal, destination, true);
80
+ onProgress(`copied self-contained local plugin package from ${sourceReal}`);
81
+ return;
82
+ }
83
+ const workspaceRoot = findPnpmWorkspaceRoot(sourceReal);
84
+ if (workspaceRoot) {
85
+ const stagedWorkspace = await stageLocalWorkspaceClosure(sourceReal, workspaceRoot, join(resolve(destination, ".."), "workspace-packages"));
86
+ onProgress(`materializing local workspace plugin with ${stagedWorkspace.packageCount - 1} workspace dependency/dependencies`);
87
+ await materializeWithNpm({
88
+ spec: stagedWorkspace.pluginArchive,
89
+ prefix: join(resolve(destination, ".."), "local-prefix"),
90
+ destination,
91
+ onProgress,
92
+ signal,
93
+ label: "local workspace",
94
+ });
95
+ // The rewritten file: specs are a staging-only resolution aid. Keep the
96
+ // installed plugin's own package metadata identical to its source.
97
+ copyBoundedMetadataFile(join(sourceReal, "package.json"), join(destination, "package.json"), "package.json");
98
+ return;
99
+ }
100
+ const localArchiveRoot = join(resolve(destination, ".."), "local-package");
101
+ const localPackage = join(localArchiveRoot, "package");
102
+ rmSync(localArchiveRoot, { recursive: true, force: true });
103
+ copyPackageTree(sourceReal, localPackage, true);
104
+ const localArchive = join(localArchiveRoot, "package.tgz");
105
+ await createPackageArchive(localPackage, localArchive);
106
+ await materializeWithNpm({
107
+ spec: localArchive,
108
+ prefix: join(resolve(destination, ".."), "local-prefix"),
109
+ destination,
110
+ onProgress,
111
+ signal,
112
+ label: "local",
113
+ });
114
+ }
115
+ async function acquirePublishedPackageArchive(spec, archiveRoot, onProgress, signal) {
116
+ rmSync(archiveRoot, { recursive: true, force: true });
117
+ mkdirSync(archiveRoot, { recursive: true, mode: 0o700 });
118
+ await runNpm([
119
+ "pack",
120
+ "--ignore-scripts",
121
+ "--pack-destination",
122
+ archiveRoot,
123
+ "--",
124
+ spec,
125
+ ], onProgress, signal);
126
+ const archives = readdirSync(archiveRoot)
127
+ .filter((name) => name.endsWith(".tgz"))
128
+ .map((name) => join(archiveRoot, name));
129
+ if (archives.length !== 1 || !statSync(archives[0]).isFile()) {
130
+ throw new Error("package source did not produce exactly one npm archive");
131
+ }
132
+ return archives[0];
133
+ }
134
+ async function cloneGitPackage(spec, destination, onProgress, signal) {
135
+ const { url, ref } = parseGitSpec(spec);
136
+ rmSync(destination, { recursive: true, force: true });
137
+ const args = ["clone", "--no-checkout", "--filter=blob:none", "--", url, destination];
138
+ await runPluginPackageCommand("git", args, onProgress, { signal });
139
+ await runPluginPackageCommand("git", ["-C", destination, "checkout", "--detach", ref ?? "HEAD"], onProgress, { signal });
140
+ }
141
+ export function parseGitSpec(spec) {
142
+ let value = spec.trim();
143
+ let shorthand = false;
144
+ if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:#.*)?$/.test(value)) {
145
+ value = `https://github.com/${value}`;
146
+ shorthand = true;
147
+ }
148
+ else if (value.startsWith("github:")) {
149
+ value = `https://github.com/${value.slice("github:".length)}`;
150
+ shorthand = true;
151
+ }
152
+ else if (value.startsWith("gitlab:")) {
153
+ value = `https://gitlab.com/${value.slice("gitlab:".length)}`;
154
+ shorthand = true;
155
+ }
156
+ else if (value.startsWith("bitbucket:")) {
157
+ value = `https://bitbucket.org/${value.slice("bitbucket:".length)}`;
158
+ shorthand = true;
159
+ }
160
+ if (value.startsWith("git+"))
161
+ value = value.slice(4);
162
+ const hash = value.lastIndexOf("#");
163
+ const ref = hash >= 0 ? value.slice(hash + 1).trim() : undefined;
164
+ const url = hash >= 0 ? value.slice(0, hash) : value;
165
+ if (!url || url.startsWith("-") || /[\r\n\0]/.test(url)) {
166
+ throw new Error("git plugin source has an invalid URL");
167
+ }
168
+ if (ref && (ref.startsWith("-") || /[\r\n\0]/.test(ref))) {
169
+ throw new Error("git plugin source has an invalid ref");
170
+ }
171
+ const normalizedUrl = shorthand && !url.endsWith(".git") ? `${url}.git` : url;
172
+ return { url: normalizedUrl, ...(ref ? { ref } : {}) };
173
+ }
174
+ async function materializeWithNpm({ spec, prefix, destination, onProgress, signal, installLinks = false, label, }) {
175
+ mkdirSync(prefix, { recursive: true, mode: 0o700 });
176
+ writeFileSync(join(prefix, "package.json"), `${JSON.stringify({ name: "rynx-plugin-materializer", private: true }, null, 2)}\n`, { mode: 0o600 });
177
+ onProgress(`materializing ${label ?? (installLinks ? "local" : "remote")} plugin ${spec}`);
178
+ await runNpm([
179
+ "install",
180
+ "--prefix",
181
+ prefix,
182
+ "--ignore-scripts",
183
+ "--no-audit",
184
+ "--no-fund",
185
+ "--package-lock=false",
186
+ "--install-strategy=nested",
187
+ ...(installLinks ? ["--install-links=true"] : []),
188
+ "--save-exact",
189
+ "--",
190
+ spec,
191
+ ], onProgress, signal);
192
+ const materializerPackage = readJsonObject(join(prefix, "package.json"), "materializer package.json");
193
+ const dependencies = materializerPackage.dependencies;
194
+ if (!isRecord(dependencies)) {
195
+ throw new Error("npm did not record the materialized plugin package");
196
+ }
197
+ const names = Object.keys(dependencies);
198
+ if (names.length !== 1 || !isSafeNpmPackageName(names[0])) {
199
+ throw new Error("npm materialization did not resolve exactly one safe package name");
200
+ }
201
+ const packageDir = join(prefix, "node_modules", ...names[0].split("/"));
202
+ const packageReal = requireContainedFile(prefix, packageDir, "materialized package", "directory");
203
+ copyPackageTree(packageReal, destination, false);
204
+ }
205
+ /** Parse and validate static metadata, validate all executable entry paths, and hash the tree. */
206
+ export async function inspectPluginPackage(packageDir, options = {}) {
207
+ throwIfInspectionAborted(options.signal);
208
+ const packageRoot = realpathSync(packageDir);
209
+ const integrity = await hashPackageTree(packageRoot, normalizedInspectionLimits(options.limits), options.signal);
210
+ throwIfInspectionAborted(options.signal);
211
+ const packageJsonPath = requireContainedFile(packageRoot, join(packageRoot, "package.json"), "package.json", "file");
212
+ const packageJson = readJsonObject(packageJsonPath, "package.json");
213
+ if (!isRecord(packageJson.rynx) || typeof packageJson.rynx.manifest !== "string") {
214
+ throw new Error("package.json must declare `rynx.manifest` as a static JSON path");
215
+ }
216
+ const manifestPath = resolvePackageEntry(packageRoot, packageJson.rynx.manifest, "rynx.manifest");
217
+ const manifest = parsePluginManifest(readJsonObject(manifestPath, "plugin manifest"));
218
+ if (options.requireEntries !== false) {
219
+ resolvePackageEntry(packageRoot, manifest.runtimeEntry, "runtimeEntry");
220
+ if (manifest.cliEntry)
221
+ resolvePackageEntry(packageRoot, manifest.cliEntry, "cliEntry");
222
+ }
223
+ const version = typeof packageJson.version === "string" && packageJson.version.trim()
224
+ ? packageJson.version
225
+ : undefined;
226
+ return {
227
+ manifest,
228
+ ...(version ? { version } : {}),
229
+ integrity,
230
+ };
231
+ }
232
+ /** Run package/dependency build scripts only after explicit user approval. */
233
+ export async function rebuildPluginPackage(packageDir, onProgress, signal) {
234
+ await runNpm([
235
+ "rebuild",
236
+ "--prefix",
237
+ realpathSync(packageDir),
238
+ "--foreground-scripts",
239
+ "--no-audit",
240
+ "--no-fund",
241
+ ], onProgress, signal);
242
+ }
243
+ function looksLocal(spec) {
244
+ if (spec.startsWith("file:"))
245
+ return true;
246
+ if (spec === "~" || spec.startsWith("~/"))
247
+ return true;
248
+ if (spec.startsWith("./") || spec.startsWith("../") || isAbsolute(spec))
249
+ return true;
250
+ try {
251
+ return existsSync(spec) && statSync(spec).isDirectory();
252
+ }
253
+ catch {
254
+ return false;
255
+ }
256
+ }
257
+ function toLocalDir(spec) {
258
+ let path;
259
+ if (spec.startsWith("file:")) {
260
+ try {
261
+ path = fileURLToPath(spec);
262
+ }
263
+ catch {
264
+ path = spec.slice("file:".length);
265
+ }
266
+ }
267
+ else {
268
+ path = spec;
269
+ }
270
+ if (path === "~" || path.startsWith("~/"))
271
+ path = join(homedir(), path.slice(1));
272
+ return resolve(path);
273
+ }
274
+ function copyPackageTree(source, destination, excludeNodeModules) {
275
+ cpSync(source, destination, {
276
+ recursive: true,
277
+ errorOnExist: true,
278
+ force: false,
279
+ dereference: false,
280
+ filter: (entry) => {
281
+ const rel = relative(source, entry);
282
+ if (!rel)
283
+ return true;
284
+ const segments = rel.split(sep);
285
+ if (segments.includes(".git"))
286
+ return false;
287
+ // npm generates command shims as absolute symlinks into its disposable
288
+ // materialization prefix. Plugin entries resolve tools by package path,
289
+ // so these non-runtime shims must not escape into the immutable closure.
290
+ if (segments.includes(".bin"))
291
+ return false;
292
+ return !excludeNodeModules || !segments.includes("node_modules");
293
+ },
294
+ });
295
+ }
296
+ function findPnpmWorkspaceRoot(start) {
297
+ let current = start;
298
+ while (true) {
299
+ if (existsSync(join(current, "pnpm-workspace.yaml")))
300
+ return current;
301
+ const parent = dirname(current);
302
+ if (parent === current)
303
+ return undefined;
304
+ current = parent;
305
+ }
306
+ }
307
+ async function stageLocalWorkspaceClosure(pluginRoot, workspaceRoot, stagingRoot) {
308
+ const packages = new Map();
309
+ const workspaceNodeModules = existsSync(join(workspaceRoot, "node_modules"))
310
+ ? realpathSync(join(workspaceRoot, "node_modules"))
311
+ : undefined;
312
+ const visit = (source) => {
313
+ const sourceReal = realpathSync(source);
314
+ const packageJson = readJsonObject(join(sourceReal, "package.json"), "package.json");
315
+ const name = packageJson.name;
316
+ if (typeof name !== "string" || !isSafeNpmPackageName(name)) {
317
+ throw new Error(`local workspace package has an invalid name: ${String(name)}`);
318
+ }
319
+ const existing = packages.get(name);
320
+ if (existing) {
321
+ if (existing.source !== sourceReal) {
322
+ throw new Error(`local workspace contains duplicate package name: ${name}`);
323
+ }
324
+ return;
325
+ }
326
+ const key = createHash("sha256").update(name).digest("hex").slice(0, 16);
327
+ packages.set(name, {
328
+ source: sourceReal,
329
+ staged: join(stagingRoot, "packages", key),
330
+ archive: join(stagingRoot, "archives", `${key}.tgz`),
331
+ });
332
+ for (const field of ["dependencies", "optionalDependencies", "peerDependencies"]) {
333
+ const dependencies = packageJson[field];
334
+ if (!isRecord(dependencies))
335
+ continue;
336
+ for (const [dependencyName, spec] of Object.entries(dependencies)) {
337
+ const dependencyPath = join(sourceReal, "node_modules", ...dependencyName.split("/"));
338
+ if (!existsSync(dependencyPath)) {
339
+ if (typeof spec === "string" && spec.startsWith("workspace:")) {
340
+ throw new Error(`local workspace dependency is not installed: ${name} -> ${dependencyName}`);
341
+ }
342
+ continue;
343
+ }
344
+ const dependencyReal = realpathSync(dependencyPath);
345
+ if (!isWithin(workspaceRoot, dependencyReal) ||
346
+ (workspaceNodeModules && isWithin(workspaceNodeModules, dependencyReal))) {
347
+ continue;
348
+ }
349
+ const dependencyPackage = readJsonObject(join(dependencyReal, "package.json"), `${dependencyName} package.json`);
350
+ if (dependencyPackage.name !== dependencyName) {
351
+ throw new Error(`local workspace dependency identity mismatch: ${dependencyName}`);
352
+ }
353
+ visit(dependencyReal);
354
+ }
355
+ }
356
+ };
357
+ rmSync(stagingRoot, { recursive: true, force: true });
358
+ mkdirSync(stagingRoot, { recursive: true, mode: 0o700 });
359
+ visit(pluginRoot);
360
+ for (const entry of packages.values())
361
+ copyPackageTree(entry.source, entry.staged, true);
362
+ for (const entry of packages.values()) {
363
+ const file = join(entry.staged, "package.json");
364
+ const packageJson = readJsonObject(file, "staged package.json");
365
+ for (const field of ["dependencies", "optionalDependencies", "peerDependencies"]) {
366
+ const dependencies = packageJson[field];
367
+ if (!isRecord(dependencies))
368
+ continue;
369
+ for (const dependencyName of Object.keys(dependencies)) {
370
+ const target = packages.get(dependencyName);
371
+ if (target)
372
+ dependencies[dependencyName] = `file:${target.archive}`;
373
+ }
374
+ }
375
+ writeFileSync(file, `${JSON.stringify(packageJson, null, 2)}\n`, { mode: 0o600 });
376
+ }
377
+ await Promise.all([...packages.values()].map((entry) => createPackageArchive(entry.staged, entry.archive)));
378
+ const pluginName = readJsonObject(join(pluginRoot, "package.json"), "package.json").name;
379
+ const plugin = typeof pluginName === "string" ? packages.get(pluginName) : undefined;
380
+ if (!plugin)
381
+ throw new Error("local workspace plugin was not staged");
382
+ return { pluginArchive: plugin.archive, packageCount: packages.size };
383
+ }
384
+ async function createPackageArchive(packageRoot, archive) {
385
+ mkdirSync(dirname(archive), { recursive: true, mode: 0o700 });
386
+ await createTar({
387
+ cwd: packageRoot,
388
+ file: archive,
389
+ gzip: true,
390
+ portable: true,
391
+ prefix: "package",
392
+ noMtime: true,
393
+ }, readdirSync(packageRoot));
394
+ }
395
+ function hasProductionDependencies(packageRoot) {
396
+ const packageJson = readJsonObject(join(packageRoot, "package.json"), "package.json");
397
+ return ["dependencies", "optionalDependencies", "peerDependencies"].some((key) => {
398
+ const value = packageJson[key];
399
+ return isRecord(value) && Object.keys(value).length > 0;
400
+ });
401
+ }
402
+ function resolvePackageEntry(packageRoot, entry, label) {
403
+ if (!entry.startsWith("./") || entry.includes("\\") || entry.includes("\0")) {
404
+ throw new Error(`${label} must be a package-root-relative POSIX path starting with ./`);
405
+ }
406
+ const segments = entry.slice(2).split("/");
407
+ if (segments.some((segment) => !segment || segment === "." || segment === "..")) {
408
+ throw new Error(`${label} must not contain empty, . or .. segments`);
409
+ }
410
+ return requireContainedFile(packageRoot, resolve(packageRoot, entry), label, "file");
411
+ }
412
+ function requireContainedFile(root, candidate, label, kind) {
413
+ let rootReal;
414
+ let candidateReal;
415
+ try {
416
+ rootReal = realpathSync(root);
417
+ candidateReal = realpathSync(candidate);
418
+ }
419
+ catch {
420
+ throw new Error(`${label} does not exist`);
421
+ }
422
+ if (!isWithin(rootReal, candidateReal)) {
423
+ throw new Error(`${label} resolves outside the plugin package`);
424
+ }
425
+ const stat = statSync(candidateReal);
426
+ if (kind === "file" ? !stat.isFile() : !stat.isDirectory()) {
427
+ throw new Error(`${label} must be a ${kind}`);
428
+ }
429
+ return candidateReal;
430
+ }
431
+ function isWithin(root, candidate) {
432
+ const rel = relative(root, candidate);
433
+ return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel));
434
+ }
435
+ function readJsonObject(path, label) {
436
+ let raw;
437
+ try {
438
+ const stat = statSync(path);
439
+ if (!stat.isFile())
440
+ throw new Error("not a file");
441
+ if (stat.size > MAX_METADATA_BYTES)
442
+ throw new Error("metadata is too large");
443
+ raw = readFileSync(path, "utf8");
444
+ }
445
+ catch (error) {
446
+ throw new Error(`cannot read ${label}: ${messageOf(error)}`);
447
+ }
448
+ try {
449
+ const parsed = JSON.parse(raw);
450
+ if (!isRecord(parsed))
451
+ throw new Error("must contain a JSON object");
452
+ return parsed;
453
+ }
454
+ catch (error) {
455
+ throw new Error(`invalid ${label}: ${messageOf(error)}`);
456
+ }
457
+ }
458
+ function copyBoundedMetadataFile(source, destination, label) {
459
+ try {
460
+ const stat = statSync(source);
461
+ if (!stat.isFile())
462
+ throw new Error("not a file");
463
+ if (stat.size > MAX_METADATA_BYTES)
464
+ throw new Error("metadata is too large");
465
+ copyFileSync(source, destination);
466
+ }
467
+ catch (error) {
468
+ throw new Error(`cannot copy ${label}: ${messageOf(error)}`);
469
+ }
470
+ }
471
+ async function hashPackageTree(root, limits, signal) {
472
+ const hash = createHash("sha256");
473
+ hash.update("rynx-plugin-package-v1\0");
474
+ let entryCount = 0;
475
+ let totalBytes = 0;
476
+ await visit(root, "", 0);
477
+ return `sha256-${hash.digest("base64")}`;
478
+ async function visit(path, rel, depth) {
479
+ throwIfInspectionAborted(signal);
480
+ entryCount += 1;
481
+ if (entryCount > limits.maxEntries) {
482
+ throw new Error(`plugin package exceeds ${limits.maxEntries} entries`);
483
+ }
484
+ if (depth > limits.maxDepth) {
485
+ throw new Error(`plugin package exceeds maximum depth ${limits.maxDepth}`);
486
+ }
487
+ const stat = await lstatAsync(path);
488
+ const normalized = rel.split(sep).join("/");
489
+ if (stat.isDirectory()) {
490
+ hash.update(`d\0${normalized}\0`);
491
+ const names = [];
492
+ const directory = await opendir(path);
493
+ for await (const entry of directory) {
494
+ throwIfInspectionAborted(signal);
495
+ names.push(entry.name);
496
+ if (entryCount + names.length > limits.maxEntries) {
497
+ throw new Error(`plugin package exceeds ${limits.maxEntries} entries`);
498
+ }
499
+ }
500
+ for (const name of names.sort()) {
501
+ await visit(join(path, name), join(rel, name), depth + 1);
502
+ }
503
+ return;
504
+ }
505
+ if (stat.isFile()) {
506
+ if (stat.size > limits.maxFileBytes) {
507
+ throw new Error(`plugin package file exceeds ${limits.maxFileBytes} bytes: ${normalized}`);
508
+ }
509
+ if (totalBytes + stat.size > limits.maxTotalBytes) {
510
+ throw new Error(`plugin package exceeds ${limits.maxTotalBytes} total bytes`);
511
+ }
512
+ totalBytes += stat.size;
513
+ hash.update(`f\0${normalized}\0${stat.size}\0`);
514
+ const noFollow = process.platform === "win32" ? 0 : fsConstants.O_NOFOLLOW;
515
+ const file = await openFile(path, fsConstants.O_RDONLY | noFollow);
516
+ try {
517
+ const before = await file.stat();
518
+ if (!before.isFile() || before.size !== stat.size) {
519
+ throw new Error(`plugin package file changed during inspection: ${normalized}`);
520
+ }
521
+ let streamedBytes = 0;
522
+ const stream = file.createReadStream({
523
+ autoClose: false,
524
+ highWaterMark: PACKAGE_HASH_STREAM_BYTES,
525
+ ...(signal ? { signal } : {}),
526
+ });
527
+ for await (const chunk of stream) {
528
+ throwIfInspectionAborted(signal);
529
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
530
+ streamedBytes += bytes.byteLength;
531
+ if (streamedBytes > limits.maxFileBytes ||
532
+ totalBytes - stat.size + streamedBytes > limits.maxTotalBytes) {
533
+ throw new Error(`plugin package file changed during inspection: ${normalized}`);
534
+ }
535
+ hash.update(bytes);
536
+ }
537
+ const after = await file.stat();
538
+ if (streamedBytes !== before.size ||
539
+ after.size !== before.size ||
540
+ after.mtimeMs !== before.mtimeMs) {
541
+ throw new Error(`plugin package file changed during inspection: ${normalized}`);
542
+ }
543
+ }
544
+ finally {
545
+ await file.close();
546
+ }
547
+ return;
548
+ }
549
+ if (stat.isSymbolicLink()) {
550
+ let target;
551
+ try {
552
+ target = await realpathAsync(path);
553
+ }
554
+ catch {
555
+ throw new Error(`plugin package contains a broken symlink: ${normalized}`);
556
+ }
557
+ if (!isWithin(root, target)) {
558
+ throw new Error(`plugin package symlink resolves outside the package: ${normalized}`);
559
+ }
560
+ const link = await readlinkAsync(path);
561
+ hash.update(`l\0${normalized}\0${link}\0`);
562
+ return;
563
+ }
564
+ throw new Error(`unsupported package entry: ${normalized}`);
565
+ }
566
+ }
567
+ function normalizedInspectionLimits(overrides) {
568
+ const limits = {
569
+ ...DEFAULT_PLUGIN_PACKAGE_INSPECTION_LIMITS,
570
+ ...overrides,
571
+ };
572
+ for (const [name, value] of Object.entries(limits)) {
573
+ if (!Number.isSafeInteger(value) || value <= 0) {
574
+ throw new Error(`plugin package inspection ${name} must be a positive safe integer`);
575
+ }
576
+ }
577
+ return limits;
578
+ }
579
+ function throwIfInspectionAborted(signal) {
580
+ if (signal?.aborted) {
581
+ throw new Error("plugin package inspection aborted", { cause: signal.reason });
582
+ }
583
+ }
584
+ function isSafeNpmPackageName(name) {
585
+ return /^(?:@[A-Za-z0-9][A-Za-z0-9._-]*\/)?[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name);
586
+ }
587
+ function runNpm(args, onProgress, signal) {
588
+ return runPluginPackageCommand("npm", args, onProgress, { signal });
589
+ }
590
+ /**
591
+ * Run one package tool without a shell and tear down its full POSIX process
592
+ * group on cancellation. Windows cannot signal process groups through Node,
593
+ * so it safely degrades to TERM→KILL on the direct child.
594
+ */
595
+ export function runPluginPackageCommand(command, args, onProgress, options = {}) {
596
+ return new Promise((resolveDone, reject) => {
597
+ const { signal, cwd } = options;
598
+ const abortGraceMs = options.abortGraceMs ?? DEFAULT_COMMAND_ABORT_GRACE_MS;
599
+ if (!Number.isSafeInteger(abortGraceMs) || abortGraceMs <= 0 || abortGraceMs > 60_000) {
600
+ reject(new Error("plugin command abort grace must be 1-60000ms"));
601
+ return;
602
+ }
603
+ if (signal?.aborted) {
604
+ reject(new Error("plugin installation aborted"));
605
+ return;
606
+ }
607
+ const useProcessGroup = process.platform !== "win32";
608
+ const child = spawn(command, args, {
609
+ cwd,
610
+ stdio: ["ignore", "pipe", "pipe"],
611
+ detached: useProcessGroup,
612
+ });
613
+ let settled = false;
614
+ let aborting = false;
615
+ let childClosed = false;
616
+ let escalationTimer;
617
+ let hardStopTimer;
618
+ let groupPollTimer;
619
+ const settle = (callback) => {
620
+ if (settled)
621
+ return;
622
+ settled = true;
623
+ signal?.removeEventListener("abort", abort);
624
+ if (escalationTimer)
625
+ clearTimeout(escalationTimer);
626
+ if (hardStopTimer)
627
+ clearTimeout(hardStopTimer);
628
+ if (groupPollTimer)
629
+ clearInterval(groupPollTimer);
630
+ callback();
631
+ };
632
+ const rejectAborted = () => settle(() => reject(new Error("plugin installation aborted", {
633
+ cause: signal?.reason,
634
+ })));
635
+ const processGroupAlive = () => {
636
+ if (!useProcessGroup || child.pid === undefined)
637
+ return !childClosed;
638
+ try {
639
+ process.kill(-child.pid, 0);
640
+ return true;
641
+ }
642
+ catch (error) {
643
+ return error.code !== "ESRCH";
644
+ }
645
+ };
646
+ const signalProcessTree = (processSignal) => {
647
+ if (useProcessGroup && child.pid !== undefined) {
648
+ try {
649
+ process.kill(-child.pid, processSignal);
650
+ return;
651
+ }
652
+ catch (error) {
653
+ if (error.code !== "ESRCH") {
654
+ try {
655
+ child.kill(processSignal);
656
+ }
657
+ catch {
658
+ // The bounded hard-stop path below still resolves cancellation.
659
+ }
660
+ }
661
+ return;
662
+ }
663
+ }
664
+ try {
665
+ child.kill(processSignal);
666
+ }
667
+ catch {
668
+ // A concurrently exited child is already stopped.
669
+ }
670
+ };
671
+ const finishWhenStopped = () => {
672
+ if (!aborting || settled)
673
+ return;
674
+ if (childClosed && !processGroupAlive())
675
+ rejectAborted();
676
+ };
677
+ const abort = () => {
678
+ if (aborting || settled)
679
+ return;
680
+ aborting = true;
681
+ signalProcessTree("SIGTERM");
682
+ groupPollTimer = setInterval(finishWhenStopped, 25);
683
+ escalationTimer = setTimeout(() => {
684
+ signalProcessTree("SIGKILL");
685
+ finishWhenStopped();
686
+ }, abortGraceMs);
687
+ hardStopTimer = setTimeout(rejectAborted, abortGraceMs * 2);
688
+ finishWhenStopped();
689
+ };
690
+ signal?.addEventListener("abort", abort, { once: true });
691
+ if (signal?.aborted)
692
+ abort();
693
+ const onData = (buffer) => {
694
+ for (const line of buffer.toString().split(/\r?\n/)) {
695
+ if (line.trim())
696
+ onProgress(line);
697
+ }
698
+ };
699
+ child.stdout?.on("data", onData);
700
+ child.stderr?.on("data", onData);
701
+ child.on("error", (error) => {
702
+ if (aborting)
703
+ rejectAborted();
704
+ else
705
+ settle(() => reject(error));
706
+ });
707
+ child.on("close", (code) => {
708
+ childClosed = true;
709
+ if (aborting || signal?.aborted) {
710
+ if (!aborting)
711
+ abort();
712
+ if (!useProcessGroup)
713
+ rejectAborted();
714
+ else
715
+ finishWhenStopped();
716
+ return;
717
+ }
718
+ settle(() => {
719
+ if (code === 0)
720
+ resolveDone();
721
+ else
722
+ reject(new Error(`${command} exited with code ${code ?? "null"}`));
723
+ });
724
+ });
725
+ });
726
+ }
727
+ function isRecord(value) {
728
+ return typeof value === "object" && value !== null && !Array.isArray(value);
729
+ }
730
+ function messageOf(error) {
731
+ return error instanceof Error ? error.message : String(error);
732
+ }