zyrk 0.3.7 → 0.3.9
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/VERSION +1 -1
- package/bin/zyrk.js +1 -1
- package/dist/cli/main.d.ts +54 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +473 -0
- package/dist/cli/npm-delegate.d.ts +38 -0
- package/dist/cli/npm-delegate.d.ts.map +1 -0
- package/{src/cli/npm-delegate.ts → dist/cli/npm-delegate.js} +26 -55
- package/dist/cli/parse.d.ts +40 -0
- package/dist/cli/parse.d.ts.map +1 -0
- package/dist/cli/parse.js +78 -0
- package/dist/commands/dependencies.d.ts +12 -0
- package/dist/commands/dependencies.d.ts.map +1 -0
- package/dist/commands/dependencies.js +118 -0
- package/dist/commands/deploy.d.ts +15 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +506 -0
- package/dist/commands/maintenance.d.ts +27 -0
- package/dist/commands/maintenance.d.ts.map +1 -0
- package/dist/commands/maintenance.js +198 -0
- package/dist/commands/patch.d.ts +38 -0
- package/dist/commands/patch.d.ts.map +1 -0
- package/dist/commands/patch.js +336 -0
- package/dist/commands/project.d.ts +15 -0
- package/dist/commands/project.d.ts.map +1 -0
- package/dist/commands/project.js +396 -0
- package/dist/commands/store.d.ts +10 -0
- package/dist/commands/store.d.ts.map +1 -0
- package/dist/commands/store.js +551 -0
- package/dist/config/paths.d.ts +37 -0
- package/dist/config/paths.d.ts.map +1 -0
- package/dist/config/paths.js +66 -0
- package/dist/dlx/create.d.ts +23 -0
- package/dist/dlx/create.d.ts.map +1 -0
- package/dist/dlx/create.js +59 -0
- package/dist/dlx/run.d.ts +54 -0
- package/dist/dlx/run.d.ts.map +1 -0
- package/dist/dlx/run.js +274 -0
- package/dist/errors.d.ts +68 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +111 -0
- package/dist/extract/tarball.d.ts +82 -0
- package/dist/extract/tarball.d.ts.map +1 -0
- package/dist/extract/tarball.js +645 -0
- package/dist/fetch/all.d.ts +48 -0
- package/dist/fetch/all.d.ts.map +1 -0
- package/dist/fetch/all.js +127 -0
- package/dist/fetch/package.d.ts +45 -0
- package/dist/fetch/package.d.ts.map +1 -0
- package/dist/fetch/package.js +124 -0
- package/dist/fetch/registry.d.ts +109 -0
- package/dist/fetch/registry.d.ts.map +1 -0
- package/dist/fetch/registry.js +418 -0
- package/dist/install/import-npm.d.ts +4 -0
- package/dist/install/import-npm.d.ts.map +1 -0
- package/dist/install/import-npm.js +147 -0
- package/dist/install/lock.d.ts +8 -0
- package/dist/install/lock.d.ts.map +1 -0
- package/dist/install/lock.js +146 -0
- package/dist/install/metadata.d.ts +19 -0
- package/dist/install/metadata.d.ts.map +1 -0
- package/dist/install/metadata.js +79 -0
- package/dist/install/registry.d.ts +8 -0
- package/dist/install/registry.d.ts.map +1 -0
- package/dist/install/registry.js +59 -0
- package/dist/install/run.d.ts +32 -0
- package/dist/install/run.d.ts.map +1 -0
- package/dist/install/run.js +545 -0
- package/dist/lockfile/fingerprints.d.ts +35 -0
- package/dist/lockfile/fingerprints.d.ts.map +1 -0
- package/dist/lockfile/fingerprints.js +56 -0
- package/dist/lockfile/read.d.ts +27 -0
- package/dist/lockfile/read.d.ts.map +1 -0
- package/dist/lockfile/read.js +126 -0
- package/dist/lockfile/types.d.ts +74 -0
- package/dist/lockfile/types.d.ts.map +1 -0
- package/dist/lockfile/types.js +17 -0
- package/dist/lockfile/write.d.ts +39 -0
- package/dist/lockfile/write.d.ts.map +1 -0
- package/dist/lockfile/write.js +180 -0
- package/dist/log.d.ts +21 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +79 -0
- package/dist/project/projection.d.ts +50 -0
- package/dist/project/projection.d.ts.map +1 -0
- package/dist/project/projection.js +276 -0
- package/dist/resolve/resolver.d.ts +75 -0
- package/dist/resolve/resolver.d.ts.map +1 -0
- package/dist/resolve/resolver.js +533 -0
- package/dist/resolve/spec.d.ts +26 -0
- package/dist/resolve/spec.d.ts.map +1 -0
- package/dist/resolve/spec.js +87 -0
- package/dist/store/db.d.ts +49 -0
- package/dist/store/db.d.ts.map +1 -0
- package/{src/store/db.ts → dist/store/db.js} +106 -206
- package/dist/store/graph.d.ts +60 -0
- package/dist/store/graph.d.ts.map +1 -0
- package/dist/store/graph.js +115 -0
- package/dist/store/layout.d.ts +71 -0
- package/dist/store/layout.d.ts.map +1 -0
- package/dist/store/layout.js +282 -0
- package/dist/store/manifests.d.ts +78 -0
- package/dist/store/manifests.d.ts.map +1 -0
- package/dist/store/manifests.js +238 -0
- package/dist/store/objects.d.ts +56 -0
- package/dist/store/objects.d.ts.map +1 -0
- package/dist/store/objects.js +157 -0
- package/dist/store/realize.d.ts +38 -0
- package/dist/store/realize.d.ts.map +1 -0
- package/dist/store/realize.js +227 -0
- package/dist/store/source.d.ts +42 -0
- package/dist/store/source.d.ts.map +1 -0
- package/dist/store/source.js +169 -0
- package/dist/store/txn.d.ts +37 -0
- package/dist/store/txn.d.ts.map +1 -0
- package/dist/store/txn.js +97 -0
- package/dist/store/verify.d.ts +40 -0
- package/dist/store/verify.d.ts.map +1 -0
- package/dist/store/verify.js +396 -0
- package/dist/util/fsx.d.ts +54 -0
- package/dist/util/fsx.d.ts.map +1 -0
- package/dist/util/fsx.js +259 -0
- package/dist/util/hash.d.ts +48 -0
- package/dist/util/hash.d.ts.map +1 -0
- package/dist/util/hash.js +80 -0
- package/dist/util/ids.d.ts +19 -0
- package/dist/util/ids.d.ts.map +1 -0
- package/dist/util/ids.js +38 -0
- package/dist/util/integrity.d.ts +97 -0
- package/dist/util/integrity.d.ts.map +1 -0
- package/dist/util/integrity.js +192 -0
- package/dist/util/json.d.ts +14 -0
- package/dist/util/json.d.ts.map +1 -0
- package/dist/util/json.js +78 -0
- package/dist/util/locks.d.ts +40 -0
- package/dist/util/locks.d.ts.map +1 -0
- package/dist/util/locks.js +186 -0
- package/dist/util/proc.d.ts +32 -0
- package/dist/util/proc.d.ts.map +1 -0
- package/dist/util/proc.js +84 -0
- package/dist/version.d.ts +27 -0
- package/dist/version.d.ts.map +1 -0
- package/{src/version.ts → dist/version.js} +0 -8
- package/package.json +5 -3
- package/src/cli/main.ts +0 -468
- package/src/cli/parse.ts +0 -106
- package/src/commands/dependencies.ts +0 -154
- package/src/commands/deploy.ts +0 -777
- package/src/commands/maintenance.ts +0 -260
- package/src/commands/patch.ts +0 -408
- package/src/commands/project.ts +0 -598
- package/src/commands/store.ts +0 -689
- package/src/config/paths.ts +0 -119
- package/src/dlx/create.ts +0 -69
- package/src/dlx/run.ts +0 -389
- package/src/errors.ts +0 -142
- package/src/extract/tarball.ts +0 -868
- package/src/fetch/all.ts +0 -206
- package/src/fetch/package.ts +0 -181
- package/src/fetch/registry.ts +0 -615
- package/src/install/import-npm.ts +0 -177
- package/src/install/lock.ts +0 -176
- package/src/install/metadata.ts +0 -122
- package/src/install/registry.ts +0 -88
- package/src/install/run.ts +0 -760
- package/src/lockfile/fingerprints.ts +0 -74
- package/src/lockfile/read.ts +0 -198
- package/src/lockfile/types.ts +0 -75
- package/src/lockfile/write.ts +0 -257
- package/src/log.ts +0 -100
- package/src/project/projection.ts +0 -390
- package/src/resolve/resolver.ts +0 -757
- package/src/resolve/spec.ts +0 -131
- package/src/store/graph.ts +0 -175
- package/src/store/layout.ts +0 -431
- package/src/store/manifests.ts +0 -347
- package/src/store/objects.ts +0 -229
- package/src/store/realize.ts +0 -322
- package/src/store/source.ts +0 -249
- package/src/store/txn.ts +0 -118
- package/src/store/verify.ts +0 -433
- package/src/types/vendor.d.ts +0 -120
- package/src/util/fsx.ts +0 -289
- package/src/util/hash.ts +0 -117
- package/src/util/ids.ts +0 -42
- package/src/util/integrity.ts +0 -269
- package/src/util/json.ts +0 -89
- package/src/util/locks.ts +0 -266
- package/src/util/proc.ts +0 -108
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/** Read-only advisory reporting and verified source prefetch. */
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { json as fetchJson } from "npm-registry-fetch";
|
|
5
|
+
import { satisfies, validRange } from "semver";
|
|
6
|
+
import { ZyrkError } from "../errors.js";
|
|
7
|
+
import { installProject } from "../install/run.js";
|
|
8
|
+
import { readLockfile, assertFrozenCompatible } from "../lockfile/read.js";
|
|
9
|
+
import { manifestFingerprint, configFingerprint, scriptPolicyFingerprint, } from "../lockfile/fingerprints.js";
|
|
10
|
+
import { lockedGraph, lockMismatch } from "../install/lock.js";
|
|
11
|
+
const severities = ["info", "low", "moderate", "high", "critical"];
|
|
12
|
+
function object(value) {
|
|
13
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
function usage(reason) {
|
|
16
|
+
return new ZyrkError("ZYRK_USAGE", `Cannot run maintenance: ${reason}.`, {
|
|
17
|
+
remedy: "Use audit [--audit-level <severity>] [--offline], or fetch --frozen-lockfile [--offline].",
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export async function runMaintenanceCommand(options) {
|
|
21
|
+
let frozen = false, offline = false;
|
|
22
|
+
let threshold;
|
|
23
|
+
for (let i = 0; i < options.args.length; i++) {
|
|
24
|
+
const arg = options.args[i];
|
|
25
|
+
if (arg === "--offline")
|
|
26
|
+
offline = true;
|
|
27
|
+
else if (arg === "--frozen-lockfile" && options.command === "fetch")
|
|
28
|
+
frozen = true;
|
|
29
|
+
else if (arg === "--audit-level" && options.command === "audit") {
|
|
30
|
+
threshold = options.args[++i];
|
|
31
|
+
if (threshold === undefined)
|
|
32
|
+
throw usage("--audit-level requires a severity");
|
|
33
|
+
}
|
|
34
|
+
else if (arg.startsWith("--audit-level=") && options.command === "audit")
|
|
35
|
+
threshold = arg.slice(14);
|
|
36
|
+
else
|
|
37
|
+
throw usage("an unsupported option or argument was supplied");
|
|
38
|
+
}
|
|
39
|
+
if (options.command === "repair")
|
|
40
|
+
throw new ZyrkError("ZYRK_STORE_CORRUPT", "Cannot repair shared content in place while other projects may reference it.", {
|
|
41
|
+
remedy: "Use store verify to identify affected sources; immutable replacement and reverse-closure repair are not available yet.",
|
|
42
|
+
});
|
|
43
|
+
if (options.command === "fetch") {
|
|
44
|
+
if (!frozen)
|
|
45
|
+
throw usage("fetch requires --frozen-lockfile");
|
|
46
|
+
return installProject({
|
|
47
|
+
projectDir: options.projectDir,
|
|
48
|
+
store: options.store,
|
|
49
|
+
registry: options.registry,
|
|
50
|
+
frozen: true,
|
|
51
|
+
offline,
|
|
52
|
+
fetchOnly: true,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
let manifest;
|
|
56
|
+
try {
|
|
57
|
+
manifest = JSON.parse(await readFile(path.join(options.projectDir, "package.json"), "utf8"));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
throw new ZyrkError("ZYRK_MANIFEST_INVALID", "Cannot audit dependencies without a readable package.json.", { remedy: "Restore the project manifest and retry." });
|
|
61
|
+
}
|
|
62
|
+
if (!object(manifest))
|
|
63
|
+
throw usage("package.json must be an object");
|
|
64
|
+
const config = object(manifest.zyrk) ? manifest.zyrk.audit : undefined;
|
|
65
|
+
if (config !== undefined &&
|
|
66
|
+
(!object(config) ||
|
|
67
|
+
Object.keys(config).some((key) => !["enabled", "failOn"].includes(key)) ||
|
|
68
|
+
(config.enabled !== undefined && typeof config.enabled !== "boolean") ||
|
|
69
|
+
(config.failOn !== undefined && typeof config.failOn !== "string")))
|
|
70
|
+
throw new ZyrkError("ZYRK_POLICY_DENIED", "Cannot audit dependencies with unsupported audit policy settings.", {
|
|
71
|
+
remedy: "Set package.json zyrk.audit to enabled (boolean) and failOn (none, info, low, moderate, high, or critical).",
|
|
72
|
+
});
|
|
73
|
+
const policy = object(config) ? config : {};
|
|
74
|
+
threshold ??= typeof policy.failOn === "string" ? policy.failOn : "none";
|
|
75
|
+
if (threshold !== "none" && !severities.includes(threshold))
|
|
76
|
+
throw usage("audit severity threshold is invalid");
|
|
77
|
+
const base = {
|
|
78
|
+
kind: "audit",
|
|
79
|
+
status: "complete",
|
|
80
|
+
advisories: [],
|
|
81
|
+
thresholdExceeded: false,
|
|
82
|
+
warnings: [],
|
|
83
|
+
};
|
|
84
|
+
if (policy.enabled === false)
|
|
85
|
+
return {
|
|
86
|
+
...base,
|
|
87
|
+
status: "disabled",
|
|
88
|
+
warnings: [
|
|
89
|
+
"Audit is disabled by project policy; no dependency information was sent.",
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
if (offline)
|
|
93
|
+
return {
|
|
94
|
+
...base,
|
|
95
|
+
status: "unavailable",
|
|
96
|
+
warnings: [
|
|
97
|
+
"Offline audit has no cached advisory data; vulnerability status is unknown.",
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
const lock = await readLockfile(path.join(options.projectDir, "package-lock.json"));
|
|
101
|
+
if (!lock?.doc.zyrk)
|
|
102
|
+
throw lockMismatch("audit requires an exact zyrk lockfile");
|
|
103
|
+
assertFrozenCompatible(lock.doc, {
|
|
104
|
+
manifest: manifestFingerprint(manifest),
|
|
105
|
+
scripts: scriptPolicyFingerprint(object(manifest.zyrk) ? manifest.zyrk.scripts : undefined),
|
|
106
|
+
config: configFingerprint({
|
|
107
|
+
registry: options.registry.origin,
|
|
108
|
+
linker: "shared",
|
|
109
|
+
}),
|
|
110
|
+
});
|
|
111
|
+
const graph = lockedGraph(lock.doc);
|
|
112
|
+
const packages = Object.create(null);
|
|
113
|
+
for (const node of graph.nodes) {
|
|
114
|
+
if (node.origin !== options.registry.origin)
|
|
115
|
+
throw new ZyrkError("ZYRK_POLICY_DENIED", "Cannot audit a graph from a different registry origin.", {
|
|
116
|
+
remedy: "Select the registry recorded by the lockfile before sending dependency names.",
|
|
117
|
+
});
|
|
118
|
+
packages[node.name] ??= [];
|
|
119
|
+
if (!packages[node.name].includes(node.version))
|
|
120
|
+
packages[node.name].push(node.version);
|
|
121
|
+
}
|
|
122
|
+
if (!graph.nodes.length)
|
|
123
|
+
return base;
|
|
124
|
+
let response;
|
|
125
|
+
try {
|
|
126
|
+
response = await fetchJson(new URL("-/npm/v1/security/advisories/bulk", options.registry.registry)
|
|
127
|
+
.href, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
body: packages,
|
|
130
|
+
registry: options.registry.registry,
|
|
131
|
+
retry: false,
|
|
132
|
+
timeout: 15_000,
|
|
133
|
+
headers: { "content-type": "application/json" },
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return {
|
|
138
|
+
...base,
|
|
139
|
+
status: "unavailable",
|
|
140
|
+
warnings: [
|
|
141
|
+
"The configured registry advisory service is unavailable; vulnerability status is unknown.",
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const unavailable = () => ({
|
|
146
|
+
...base,
|
|
147
|
+
status: "unavailable",
|
|
148
|
+
warnings: [
|
|
149
|
+
"The configured advisory service returned unsupported data; vulnerability status is unknown.",
|
|
150
|
+
],
|
|
151
|
+
});
|
|
152
|
+
if (!object(response))
|
|
153
|
+
return unavailable();
|
|
154
|
+
for (const [name, entries] of Object.entries(response)) {
|
|
155
|
+
if (!Object.hasOwn(packages, name) || !Array.isArray(entries))
|
|
156
|
+
return unavailable();
|
|
157
|
+
for (const entry of entries) {
|
|
158
|
+
if (!object(entry) ||
|
|
159
|
+
typeof entry.severity !== "string" ||
|
|
160
|
+
!severities.includes(entry.severity) ||
|
|
161
|
+
typeof entry.vulnerable_versions !== "string" ||
|
|
162
|
+
validRange(entry.vulnerable_versions) === null ||
|
|
163
|
+
typeof entry.title !== "string" ||
|
|
164
|
+
typeof entry.url !== "string")
|
|
165
|
+
return unavailable();
|
|
166
|
+
let url;
|
|
167
|
+
try {
|
|
168
|
+
url = new URL(entry.url);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return unavailable();
|
|
172
|
+
}
|
|
173
|
+
if (!["https:", "http:"].includes(url.protocol) ||
|
|
174
|
+
url.username ||
|
|
175
|
+
url.password)
|
|
176
|
+
return unavailable();
|
|
177
|
+
for (const version of packages[name])
|
|
178
|
+
if (satisfies(version, entry.vulnerable_versions))
|
|
179
|
+
base.advisories.push({
|
|
180
|
+
name,
|
|
181
|
+
version,
|
|
182
|
+
severity: entry.severity,
|
|
183
|
+
title: entry.title
|
|
184
|
+
.replace(/[\u0000-\u001f\u007f]/g, " ")
|
|
185
|
+
.slice(0, 500),
|
|
186
|
+
url: url.href,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
base.advisories.sort((a, b) => a.name.localeCompare(b.name) ||
|
|
191
|
+
a.version.localeCompare(b.version) ||
|
|
192
|
+
a.title.localeCompare(b.title));
|
|
193
|
+
base.thresholdExceeded =
|
|
194
|
+
threshold !== "none" &&
|
|
195
|
+
base.advisories.some((advisory) => severities.indexOf(advisory.severity) >=
|
|
196
|
+
severities.indexOf(threshold));
|
|
197
|
+
return base;
|
|
198
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Store } from "../store/layout.ts";
|
|
2
|
+
type Change = {
|
|
3
|
+
path: string;
|
|
4
|
+
before: string | null;
|
|
5
|
+
content: string | null;
|
|
6
|
+
executable: boolean;
|
|
7
|
+
};
|
|
8
|
+
export type ProjectPatch = {
|
|
9
|
+
schemaVersion: 1;
|
|
10
|
+
sourceId: string;
|
|
11
|
+
manifestHash: string;
|
|
12
|
+
changes: Change[];
|
|
13
|
+
};
|
|
14
|
+
export type LoadedProjectPatch = ProjectPatch & {
|
|
15
|
+
digest: string;
|
|
16
|
+
artifact: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function loadProjectPatches(project: string): Promise<LoadedProjectPatch[]>;
|
|
19
|
+
export declare function hasProjectPatches(project: string): Promise<boolean>;
|
|
20
|
+
/** Caller supplies the complete private generation, copied/reflinked without store hardlinks. */
|
|
21
|
+
export declare function applyProjectPatches(options: {
|
|
22
|
+
project: string;
|
|
23
|
+
packages: ReadonlyMap<string, {
|
|
24
|
+
sourceId: string;
|
|
25
|
+
directory: string;
|
|
26
|
+
}>;
|
|
27
|
+
}): Promise<{
|
|
28
|
+
digest: string;
|
|
29
|
+
count: number;
|
|
30
|
+
}>;
|
|
31
|
+
export declare function runPatchCommand(options: {
|
|
32
|
+
command: "patch" | "patch-commit";
|
|
33
|
+
args: readonly string[];
|
|
34
|
+
store: Store;
|
|
35
|
+
cwd: string;
|
|
36
|
+
}): Promise<unknown>;
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=patch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patch.d.ts","sourceRoot":"","sources":["../../src/commands/patch.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAS3C,KAAK,MAAM,GAAG;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,OAAO,CAAC;CACrB,CAAC;AACF,MAAM,MAAM,YAAY,GAAG;IACzB,aAAa,EAAE,CAAC,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AACF,MAAM,MAAM,kBAAkB,GAAG,YAAY,GAAG;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AA+DF,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,kBAAkB,EAAE,CAAC,CA+C/B;AACD,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAEzE;AAED,iGAAiG;AACjG,wBAAsB,mBAAmB,CAAC,OAAO,EAAE;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACxE,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAgD7C;AAED,wBAAsB,eAAe,CAAC,OAAO,EAAE;IAC7C,OAAO,EAAE,OAAO,GAAG,cAAc,CAAC;IAClC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,KAAK,EAAE,KAAK,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,OAAO,CAAC,OAAO,CAAC,CAsBnB"}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { ZyrkError } from "../errors.js";
|
|
4
|
+
import { lockedGraph } from "../install/lock.js";
|
|
5
|
+
import { Store } from "../store/layout.js";
|
|
6
|
+
import { readSourceManifest } from "../store/manifests.js";
|
|
7
|
+
import { objectPathFor, verifyObjectPayload } from "../store/objects.js";
|
|
8
|
+
import { isErrno, writeFileAtomic } from "../util/fsx.js";
|
|
9
|
+
import { modeClassOf, sha512Hex } from "../util/hash.js";
|
|
10
|
+
import { canonicalJson } from "../util/json.js";
|
|
11
|
+
import { randomId } from "../util/ids.js";
|
|
12
|
+
import { acquireLock } from "../util/locks.js";
|
|
13
|
+
function fail(why) {
|
|
14
|
+
throw new ZyrkError("ZYRK_MANIFEST_INVALID", `Cannot process dependency patch: ${why}.`, {
|
|
15
|
+
remedy: "Use a patch editing area created by zyrk, keep changes inside the intended package, and reinstall after committing the patch.",
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function relative(value) {
|
|
19
|
+
if (!value ||
|
|
20
|
+
value.includes("\\") ||
|
|
21
|
+
path.posix.isAbsolute(value) ||
|
|
22
|
+
value.split("/").some((part) => !part || part === "." || part === ".."))
|
|
23
|
+
fail("unsafe package-relative path");
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
async function readJson(file) {
|
|
27
|
+
if (!(await fs.lstat(file)).isFile())
|
|
28
|
+
fail("metadata is not a regular file");
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(await fs.readFile(file, "utf8"));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return fail("invalid JSON metadata");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function manifest(project) {
|
|
37
|
+
return (await readJson(path.join(project, "package.json")));
|
|
38
|
+
}
|
|
39
|
+
async function safeParent(root, relativePath, create) {
|
|
40
|
+
relative(relativePath);
|
|
41
|
+
let current = root;
|
|
42
|
+
if (!(await fs.lstat(root)).isDirectory())
|
|
43
|
+
fail("package root is not a real directory");
|
|
44
|
+
for (const segment of relativePath.split("/").slice(0, -1)) {
|
|
45
|
+
current = path.join(current, segment);
|
|
46
|
+
try {
|
|
47
|
+
if (!(await fs.lstat(current)).isDirectory())
|
|
48
|
+
fail("path crosses a link or file");
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (!create || !isErrno(error, "ENOENT"))
|
|
52
|
+
throw error;
|
|
53
|
+
await fs.mkdir(current, { mode: 0o700 });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return path.join(root, ...relativePath.split("/"));
|
|
57
|
+
}
|
|
58
|
+
function scanSecrets(bytes) {
|
|
59
|
+
const text = bytes.toString("utf8");
|
|
60
|
+
if (/-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----|\bnpm_[A-Za-z0-9]{20,}|\bgh[pousr]_[A-Za-z0-9]{20,}|https?:\/\/[^\s/@]+:[^\s/@]+@/.test(text))
|
|
61
|
+
fail("patch contains credential-like content");
|
|
62
|
+
}
|
|
63
|
+
export async function loadProjectPatches(project) {
|
|
64
|
+
const config = (await manifest(project)).zyrk?.patches;
|
|
65
|
+
if (config === undefined)
|
|
66
|
+
return [];
|
|
67
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
68
|
+
fail("zyrk.patches must map source identities to artifact paths");
|
|
69
|
+
const result = [];
|
|
70
|
+
for (const sourceId of Object.keys(config).sort()) {
|
|
71
|
+
const artifact = config[sourceId];
|
|
72
|
+
if (typeof artifact !== "string" || !artifact.startsWith(".zyrk/patches/"))
|
|
73
|
+
fail("patch artifact must live in .zyrk/patches");
|
|
74
|
+
const file = await safeParent(project, artifact, false);
|
|
75
|
+
const raw = (await readJson(file));
|
|
76
|
+
if (!raw ||
|
|
77
|
+
raw.schemaVersion !== 1 ||
|
|
78
|
+
raw.sourceId !== sourceId ||
|
|
79
|
+
typeof raw.manifestHash !== "string" ||
|
|
80
|
+
!/^[0-9a-f]{128}$/.test(raw.manifestHash) ||
|
|
81
|
+
!Array.isArray(raw.changes))
|
|
82
|
+
fail("patch source binding is invalid");
|
|
83
|
+
const seen = new Set();
|
|
84
|
+
for (const change of raw.changes) {
|
|
85
|
+
if (!change || typeof change.path !== "string")
|
|
86
|
+
fail("invalid change");
|
|
87
|
+
relative(change.path);
|
|
88
|
+
if (seen.has(change.path))
|
|
89
|
+
fail("duplicate change");
|
|
90
|
+
seen.add(change.path);
|
|
91
|
+
if (change.before !== null &&
|
|
92
|
+
(typeof change.before !== "string" ||
|
|
93
|
+
!/^[0-9a-f]{128}$/.test(change.before)))
|
|
94
|
+
fail("invalid original file digest");
|
|
95
|
+
if (typeof change.executable !== "boolean" ||
|
|
96
|
+
(change.content !== null &&
|
|
97
|
+
(typeof change.content !== "string" ||
|
|
98
|
+
Buffer.from(change.content, "base64").toString("base64") !==
|
|
99
|
+
change.content)))
|
|
100
|
+
fail("invalid file delta");
|
|
101
|
+
if (change.content !== null)
|
|
102
|
+
scanSecrets(Buffer.from(change.content, "base64"));
|
|
103
|
+
}
|
|
104
|
+
result.push({ ...raw, artifact, digest: sha512Hex(canonicalJson(raw)) });
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
export async function hasProjectPatches(project) {
|
|
109
|
+
return (await loadProjectPatches(project)).length > 0;
|
|
110
|
+
}
|
|
111
|
+
/** Caller supplies the complete private generation, copied/reflinked without store hardlinks. */
|
|
112
|
+
export async function applyProjectPatches(options) {
|
|
113
|
+
const project = await fs.realpath(options.project);
|
|
114
|
+
const patches = await loadProjectPatches(project);
|
|
115
|
+
let count = 0;
|
|
116
|
+
for (const patch of patches) {
|
|
117
|
+
const targets = [...options.packages.values()].filter((pkg) => pkg.sourceId === patch.sourceId);
|
|
118
|
+
if (!targets.length)
|
|
119
|
+
fail("patched source is absent from the selected graph");
|
|
120
|
+
for (const pkg of targets) {
|
|
121
|
+
const root = await fs.realpath(pkg.directory);
|
|
122
|
+
if (!root.startsWith(`${project}${path.sep}`) ||
|
|
123
|
+
!root.startsWith(path.join(project, ".zyrk") + path.sep))
|
|
124
|
+
fail("patch destination is outside the private generation");
|
|
125
|
+
for (const change of patch.changes) {
|
|
126
|
+
const target = await safeParent(root, change.path, true);
|
|
127
|
+
let before = null;
|
|
128
|
+
try {
|
|
129
|
+
const stat = await fs.lstat(target);
|
|
130
|
+
if (!stat.isFile() || stat.nlink > 1)
|
|
131
|
+
fail("patch target is a link or shares an inode");
|
|
132
|
+
before = await fs.readFile(target);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (!isErrno(error, "ENOENT"))
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
if ((before === null ? null : sha512Hex(before)) !== change.before)
|
|
139
|
+
fail("original file digest differs from patch");
|
|
140
|
+
if (change.content === null) {
|
|
141
|
+
if (before !== null)
|
|
142
|
+
await fs.unlink(target);
|
|
143
|
+
}
|
|
144
|
+
else
|
|
145
|
+
await writeFileAtomic(target, Buffer.from(change.content, "base64"), {
|
|
146
|
+
mode: change.executable ? 0o755 : 0o644,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
count++;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
digest: sha512Hex(canonicalJson(patches.map(({ sourceId, digest }) => ({ sourceId, digest })))),
|
|
154
|
+
count,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
export async function runPatchCommand(options) {
|
|
158
|
+
if (options.args.length !== 1 || !options.args[0])
|
|
159
|
+
fail("expected one package selector or editing-area path");
|
|
160
|
+
const project = await fs.realpath(options.cwd);
|
|
161
|
+
const control = path.join(project, ".zyrk");
|
|
162
|
+
try {
|
|
163
|
+
if (!(await fs.lstat(control)).isDirectory())
|
|
164
|
+
fail("project control directory is not a real directory");
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (!isErrno(error, "ENOENT"))
|
|
168
|
+
throw error;
|
|
169
|
+
await fs.mkdir(control, { mode: 0o700 });
|
|
170
|
+
}
|
|
171
|
+
const held = await acquireLock(path.join(control, "project.lock"), {
|
|
172
|
+
name: "project patch",
|
|
173
|
+
});
|
|
174
|
+
try {
|
|
175
|
+
return options.command === "patch"
|
|
176
|
+
? await createEditingArea(project, options.args[0], options.store)
|
|
177
|
+
: await commitPatch(project, options.args[0], options.store);
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
await held.release();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async function createEditingArea(project, selector, store) {
|
|
184
|
+
const graph = lockedGraph((await readJson(path.join(project, "package-lock.json"))));
|
|
185
|
+
const matches = graph.nodes.filter((node) => node.name === selector ||
|
|
186
|
+
`${node.name}@${node.version}` === selector ||
|
|
187
|
+
node.key === selector);
|
|
188
|
+
const ids = [...new Set(matches.map((node) => node.sourceId))];
|
|
189
|
+
if (ids.length !== 1 || !ids[0])
|
|
190
|
+
fail("selector must identify exactly one installed source");
|
|
191
|
+
const sourceId = ids[0];
|
|
192
|
+
const doc = await readSourceManifest(store, sourceId);
|
|
193
|
+
const base = path.join(project, ".zyrk", "patch-work");
|
|
194
|
+
await safeParent(project, ".zyrk/patch-work/placeholder", true);
|
|
195
|
+
const editingArea = path.join(base, randomId("edit1_"));
|
|
196
|
+
await fs.mkdir(editingArea, { mode: 0o700 });
|
|
197
|
+
const packageRoot = path.join(editingArea, "package");
|
|
198
|
+
await fs.mkdir(packageRoot, { mode: 0o700 });
|
|
199
|
+
for (const entry of doc.manifest.entries) {
|
|
200
|
+
const target = await safeParent(packageRoot, entry.path, true);
|
|
201
|
+
if (entry.type === "dir")
|
|
202
|
+
await fs.mkdir(target, { recursive: true, mode: 0o755 });
|
|
203
|
+
else if (entry.type === "symlink")
|
|
204
|
+
await fs.symlink(entry.target, target);
|
|
205
|
+
else {
|
|
206
|
+
const object = objectPathFor(store, entry.objectId);
|
|
207
|
+
if (!(await fs.lstat(object)).isFile())
|
|
208
|
+
fail("source object is not a regular file");
|
|
209
|
+
const verified = await verifyObjectPayload(store, {
|
|
210
|
+
objectId: entry.objectId,
|
|
211
|
+
innerDigestHex: entry.innerDigest,
|
|
212
|
+
modeClass: entry.mode,
|
|
213
|
+
bytes: entry.size,
|
|
214
|
+
});
|
|
215
|
+
if (!verified.ok)
|
|
216
|
+
fail("source payload verification failed");
|
|
217
|
+
await fs.copyFile(object, target);
|
|
218
|
+
await fs.chmod(target, entry.mode === "exec" ? 0o755 : 0o644);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
await writeFileAtomic(path.join(editingArea, "edit.json"), JSON.stringify({
|
|
222
|
+
schemaVersion: 1,
|
|
223
|
+
sourceId,
|
|
224
|
+
manifestHash: doc.manifestHash,
|
|
225
|
+
}), { mode: 0o600 });
|
|
226
|
+
return {
|
|
227
|
+
editingArea: packageRoot,
|
|
228
|
+
sourceId,
|
|
229
|
+
next: "Edit these private files, then run zyrk patch-commit <editing-area>.",
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
async function commitPatch(project, argument, store) {
|
|
233
|
+
let area = await fs.realpath(path.resolve(project, argument));
|
|
234
|
+
if (path.basename(area) === "package")
|
|
235
|
+
area = path.dirname(area);
|
|
236
|
+
const base = path.join(project, ".zyrk", "patch-work");
|
|
237
|
+
if (path.dirname(area) !== base)
|
|
238
|
+
fail("editing area is outside this project");
|
|
239
|
+
const info = (await readJson(path.join(area, "edit.json")));
|
|
240
|
+
if (!info ||
|
|
241
|
+
typeof info.sourceId !== "string" ||
|
|
242
|
+
!/^[a-z0-9_]+$/.test(info.sourceId))
|
|
243
|
+
fail("invalid editing source identity");
|
|
244
|
+
const original = await readSourceManifest(store, info.sourceId);
|
|
245
|
+
if (original.manifestHash !== info.manifestHash)
|
|
246
|
+
fail("source manifest changed");
|
|
247
|
+
const edited = new Map();
|
|
248
|
+
const originalEntries = new Map(original.manifest.entries.map((entry) => [entry.path, entry]));
|
|
249
|
+
const walk = async (directory, prefix) => {
|
|
250
|
+
if (!(await fs.lstat(directory)).isDirectory())
|
|
251
|
+
fail("editing root is not a real directory");
|
|
252
|
+
for (const name of (await fs.readdir(directory)).sort()) {
|
|
253
|
+
const rel = prefix ? `${prefix}/${name}` : name;
|
|
254
|
+
const full = path.join(directory, name);
|
|
255
|
+
const stat = await fs.lstat(full);
|
|
256
|
+
if (stat.isDirectory()) {
|
|
257
|
+
await walk(full, rel);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (stat.isSymbolicLink()) {
|
|
261
|
+
const old = originalEntries.get(rel);
|
|
262
|
+
if (!old ||
|
|
263
|
+
old.type !== "symlink" ||
|
|
264
|
+
(await fs.readlink(full)) !== old.target)
|
|
265
|
+
fail("symlink edits are unsupported");
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (!stat.isFile() || stat.size > 16 * 1024 * 1024)
|
|
269
|
+
fail("unsupported or oversized edited file");
|
|
270
|
+
const bytes = await fs.readFile(full);
|
|
271
|
+
scanSecrets(bytes);
|
|
272
|
+
edited.set(relative(rel), {
|
|
273
|
+
bytes,
|
|
274
|
+
executable: modeClassOf(stat.mode) === "exec",
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
await walk(path.join(area, "package"), "");
|
|
279
|
+
const changes = [];
|
|
280
|
+
for (const [name, old] of originalEntries) {
|
|
281
|
+
if (old.type === "symlink") {
|
|
282
|
+
try {
|
|
283
|
+
if (!(await fs.lstat(path.join(area, "package", ...name.split("/")))).isSymbolicLink())
|
|
284
|
+
fail("symlink edits are unsupported");
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
fail("symlink edits are unsupported");
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const files = new Set([
|
|
292
|
+
...original.manifest.entries
|
|
293
|
+
.filter((entry) => entry.type === "file")
|
|
294
|
+
.map((entry) => entry.path),
|
|
295
|
+
...edited.keys(),
|
|
296
|
+
]);
|
|
297
|
+
for (const name of [...files].sort()) {
|
|
298
|
+
const old = originalEntries.get(name);
|
|
299
|
+
if (old && old.type !== "file")
|
|
300
|
+
fail("entry type changes are unsupported");
|
|
301
|
+
const next = edited.get(name);
|
|
302
|
+
const digest = next ? sha512Hex(next.bytes) : null;
|
|
303
|
+
if (old?.innerDigest === digest &&
|
|
304
|
+
(old.mode === "exec") === next?.executable)
|
|
305
|
+
continue;
|
|
306
|
+
changes.push({
|
|
307
|
+
path: name,
|
|
308
|
+
before: old?.innerDigest ?? null,
|
|
309
|
+
content: next?.bytes.toString("base64") ?? null,
|
|
310
|
+
executable: next?.executable ?? false,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
const patch = {
|
|
314
|
+
schemaVersion: 1,
|
|
315
|
+
sourceId: info.sourceId,
|
|
316
|
+
manifestHash: info.manifestHash,
|
|
317
|
+
changes,
|
|
318
|
+
};
|
|
319
|
+
const digest = sha512Hex(canonicalJson(patch));
|
|
320
|
+
const artifact = `.zyrk/patches/${digest}.json`;
|
|
321
|
+
const file = await safeParent(project, artifact, true);
|
|
322
|
+
await writeFileAtomic(file, `${canonicalJson(patch)}\n`, { mode: 0o644 });
|
|
323
|
+
const config = await manifest(project);
|
|
324
|
+
config.zyrk = {
|
|
325
|
+
...config.zyrk,
|
|
326
|
+
patches: { ...config.zyrk?.patches, [info.sourceId]: artifact },
|
|
327
|
+
};
|
|
328
|
+
await writeFileAtomic(path.join(project, "package.json"), `${JSON.stringify(config, null, 2)}\n`, { mode: 0o644 });
|
|
329
|
+
return {
|
|
330
|
+
artifact,
|
|
331
|
+
digest,
|
|
332
|
+
sourceId: info.sourceId,
|
|
333
|
+
changedFiles: changes.length,
|
|
334
|
+
next: "Commit the artifact and package.json, then reinstall to apply it to a private project generation.",
|
|
335
|
+
};
|
|
336
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type ProjectCommandOptions = {
|
|
2
|
+
command: string;
|
|
3
|
+
args: readonly string[];
|
|
4
|
+
cwd: string;
|
|
5
|
+
env: NodeJS.ProcessEnv;
|
|
6
|
+
stdout: {
|
|
7
|
+
write(chunk: string): unknown;
|
|
8
|
+
};
|
|
9
|
+
stderr: {
|
|
10
|
+
write(chunk: string): unknown;
|
|
11
|
+
};
|
|
12
|
+
json?: boolean;
|
|
13
|
+
};
|
|
14
|
+
export declare function runProjectCommand(options: ProjectCommandOptions): Promise<number>;
|
|
15
|
+
//# sourceMappingURL=project.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../src/commands/project.ts"],"names":[],"mappings":"AAiDA,MAAM,MAAM,qBAAqB,GAAG;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,MAAM,EAAE;QAAE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IAC1C,MAAM,EAAE;QAAE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IAC1C,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AA+EF,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,MAAM,CAAC,CA8KjB"}
|