zyrk 0.3.9 → 0.4.8
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 +143 -34
- package/SECURITY.md +42 -0
- package/VERSION +1 -1
- package/bin/zyrk.js +9 -1
- package/dist/cli/main.d.ts +0 -14
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +157 -25
- package/dist/cli/npm-delegate.d.ts.map +1 -1
- package/dist/cli/npm-delegate.js +11 -16
- package/dist/commands/dependencies.d.ts +1 -0
- package/dist/commands/dependencies.d.ts.map +1 -1
- package/dist/commands/dependencies.js +1 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +294 -39
- package/dist/commands/maintenance.d.ts +1 -0
- package/dist/commands/maintenance.d.ts.map +1 -1
- package/dist/commands/maintenance.js +44 -17
- package/dist/commands/patch.d.ts +1 -0
- package/dist/commands/patch.d.ts.map +1 -1
- package/dist/commands/patch.js +24 -7
- package/dist/commands/project.d.ts +2 -0
- package/dist/commands/project.d.ts.map +1 -1
- package/dist/commands/project.js +178 -55
- package/dist/commands/store.js +84 -25
- package/dist/dlx/run.d.ts +3 -4
- package/dist/dlx/run.d.ts.map +1 -1
- package/dist/dlx/run.js +166 -45
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +9 -4
- package/dist/extract/tarball.d.ts +4 -0
- package/dist/extract/tarball.d.ts.map +1 -1
- package/dist/extract/tarball.js +86 -4
- package/dist/fetch/all.d.ts +10 -4
- package/dist/fetch/all.d.ts.map +1 -1
- package/dist/fetch/all.js +118 -77
- package/dist/fetch/package.d.ts +20 -0
- package/dist/fetch/package.d.ts.map +1 -1
- package/dist/fetch/package.js +127 -54
- package/dist/fetch/registry.d.ts +23 -7
- package/dist/fetch/registry.d.ts.map +1 -1
- package/dist/fetch/registry.js +309 -53
- package/dist/install/import-npm.d.ts.map +1 -1
- package/dist/install/import-npm.js +16 -7
- package/dist/install/lock.d.ts +2 -0
- package/dist/install/lock.d.ts.map +1 -1
- package/dist/install/lock.js +98 -5
- package/dist/install/metadata.d.ts +6 -0
- package/dist/install/metadata.d.ts.map +1 -1
- package/dist/install/metadata.js +15 -2
- package/dist/install/private.d.ts +46 -0
- package/dist/install/private.d.ts.map +1 -0
- package/dist/install/private.js +304 -0
- package/dist/install/registry.d.ts.map +1 -1
- package/dist/install/registry.js +47 -8
- package/dist/install/repair.d.ts +22 -0
- package/dist/install/repair.d.ts.map +1 -0
- package/dist/install/repair.js +115 -0
- package/dist/install/run.d.ts +14 -1
- package/dist/install/run.d.ts.map +1 -1
- package/dist/install/run.js +225 -54
- package/dist/install/source-policy.d.ts +11 -0
- package/dist/install/source-policy.d.ts.map +1 -0
- package/dist/install/source-policy.js +8 -0
- package/dist/install/workspace-graph.d.ts +62 -0
- package/dist/install/workspace-graph.d.ts.map +1 -0
- package/dist/install/workspace-graph.js +365 -0
- package/dist/install/workspace-install.d.ts +3 -0
- package/dist/install/workspace-install.d.ts.map +1 -0
- package/dist/install/workspace-install.js +527 -0
- package/dist/install/workspace-lock.d.ts +16 -0
- package/dist/install/workspace-lock.d.ts.map +1 -0
- package/dist/install/workspace-lock.js +428 -0
- package/dist/install/workspace-mutation.d.ts +10 -0
- package/dist/install/workspace-mutation.d.ts.map +1 -0
- package/dist/install/workspace-mutation.js +154 -0
- package/dist/install/workspace-transaction.d.ts +26 -0
- package/dist/install/workspace-transaction.d.ts.map +1 -0
- package/dist/install/workspace-transaction.js +191 -0
- package/dist/install/workspaces.d.ts +66 -0
- package/dist/install/workspaces.d.ts.map +1 -0
- package/dist/install/workspaces.js +343 -0
- package/dist/lockfile/fingerprints.d.ts +10 -0
- package/dist/lockfile/fingerprints.d.ts.map +1 -1
- package/dist/lockfile/fingerprints.js +10 -1
- package/dist/lockfile/read.d.ts.map +1 -1
- package/dist/lockfile/read.js +10 -0
- package/dist/lockfile/types.d.ts +6 -3
- package/dist/lockfile/types.d.ts.map +1 -1
- package/dist/lockfile/types.js +3 -2
- package/dist/lockfile/write.d.ts +7 -10
- package/dist/lockfile/write.d.ts.map +1 -1
- package/dist/lockfile/write.js +83 -52
- package/dist/log.d.ts +3 -0
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +53 -6
- package/dist/project/projection.d.ts.map +1 -1
- package/dist/project/projection.js +20 -4
- package/dist/resolve/resolver.d.ts +25 -1
- package/dist/resolve/resolver.d.ts.map +1 -1
- package/dist/resolve/resolver.js +167 -31
- package/dist/store/objects.d.ts +4 -1
- package/dist/store/objects.d.ts.map +1 -1
- package/dist/store/objects.js +51 -14
- package/dist/store/realize.d.ts +6 -0
- package/dist/store/realize.d.ts.map +1 -1
- package/dist/store/realize.js +78 -3
- package/dist/store/verify.d.ts +1 -0
- package/dist/store/verify.d.ts.map +1 -1
- package/dist/store/verify.js +15 -6
- package/dist/util/integrity.d.ts +14 -4
- package/dist/util/integrity.d.ts.map +1 -1
- package/dist/util/integrity.js +44 -9
- package/dist/util/locks.d.ts.map +1 -1
- package/dist/util/locks.js +94 -61
- package/dist/util/proc.d.ts +21 -0
- package/dist/util/proc.d.ts.map +1 -1
- package/dist/util/proc.js +105 -0
- package/package.json +5 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"AAyDA,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC7B,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;AAwEF,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,MAAM,CAAC,CA8YjB"}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
/** Self-contained pure-package deployment and explicit local Docker commands. */
|
|
2
|
-
import {
|
|
3
|
-
import { copyFile, chmod, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rename, rm, symlink, writeFile, } from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { open, copyFile, chmod, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rename, rm, symlink, writeFile, } from "node:fs/promises";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { findProjectBoundary, resolveStoreRoot } from "../config/paths.js";
|
|
6
6
|
import { ZyrkError } from "../errors.js";
|
|
7
7
|
import { lockedGraph } from "../install/lock.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { loadProjectPatches } from "./patch.js";
|
|
9
|
+
import { currentPlatform } from "../resolve/resolver.js";
|
|
10
|
+
import { openStoreDb } from "../store/db.js";
|
|
11
|
+
import { validateLockDoc, assertFrozenCompatible } from "../lockfile/read.js";
|
|
12
|
+
import { manifestFingerprint, scriptPolicyFingerprint, configFingerprint, } from "../lockfile/fingerprints.js";
|
|
13
|
+
import { createGraphDoc, contextIdFor, graphHash, } from "../store/graph.js";
|
|
11
14
|
import { Store } from "../store/layout.js";
|
|
12
15
|
import { readSourceManifest } from "../store/manifests.js";
|
|
13
|
-
import { packageDirIn,
|
|
16
|
+
import { packageDirIn, selectRealizationDirs } from "../store/realize.js";
|
|
14
17
|
import { verifyRealization } from "../store/verify.js";
|
|
15
18
|
import { sha512Hex, sha512HexOfFile } from "../util/hash.js";
|
|
16
19
|
import { canonicalJson } from "../util/json.js";
|
|
17
20
|
import { acquireLock } from "../util/locks.js";
|
|
18
|
-
import {
|
|
21
|
+
import { superviseChild } from "../util/proc.js";
|
|
19
22
|
function object(v) {
|
|
20
23
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
21
24
|
}
|
|
@@ -119,14 +122,46 @@ export async function runDeployCommand(options) {
|
|
|
119
122
|
const projectLock = await acquireLock(path.join(project, ".zyrk-local/project.lock"), { name: "portable deployment" });
|
|
120
123
|
try {
|
|
121
124
|
const state = await readJson(path.join(project, "node_modules/.zyrk/install-state.json"));
|
|
122
|
-
const
|
|
125
|
+
const rawLock = await readJson(path.join(project, "package-lock.json"));
|
|
126
|
+
if (manifest.workspaces !== undefined ||
|
|
127
|
+
(object(rawLock.zyrk) && rawLock.zyrk.version === 2))
|
|
128
|
+
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Workspace runtime exports require a validated live-importer deployment adapter.");
|
|
129
|
+
const doc = validateLockDoc(rawLock, "project package-lock.json");
|
|
123
130
|
const resolved = lockedGraph(doc);
|
|
131
|
+
const privateGraph = state.linker === "private";
|
|
124
132
|
if (state.readiness !== "ready")
|
|
125
133
|
throw error("ZYRK_SCRIPT_BLOCKED", "Cannot export an unready or intentionally unbuilt generation.");
|
|
126
134
|
if (state.lockDigest !== sha512Hex(canonicalJson(doc)) ||
|
|
127
135
|
doc.zyrk?.manifest !==
|
|
128
136
|
manifestFingerprint(manifest))
|
|
129
137
|
throw error("ZYRK_LOCK_MISMATCH", "Deployment manifest, lock, and installed generation disagree.");
|
|
138
|
+
if (resolved.nodes.length) {
|
|
139
|
+
const origins = new Set(resolved.nodes.map((node) => node.origin));
|
|
140
|
+
if (origins.size !== 1)
|
|
141
|
+
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Mixed-origin deployment requires a validated registry adapter.");
|
|
142
|
+
const patches = await loadProjectPatches(project);
|
|
143
|
+
const policy = object(manifest.zyrk) ? manifest.zyrk : {};
|
|
144
|
+
assertFrozenCompatible(doc, {
|
|
145
|
+
manifest: manifestFingerprint(manifest),
|
|
146
|
+
scripts: scriptPolicyFingerprint(policy.scripts),
|
|
147
|
+
config: configFingerprint({
|
|
148
|
+
registry: [...origins][0],
|
|
149
|
+
linker: "shared",
|
|
150
|
+
platform: currentPlatform(),
|
|
151
|
+
...(Array.isArray(policy.mutablePackages)
|
|
152
|
+
? { mutablePackages: policy.mutablePackages }
|
|
153
|
+
: {}),
|
|
154
|
+
...(patches.length
|
|
155
|
+
? {
|
|
156
|
+
patches: sha512Hex(canonicalJson(patches.map((p) => ({
|
|
157
|
+
sourceId: p.sourceId,
|
|
158
|
+
digest: p.digest,
|
|
159
|
+
})))),
|
|
160
|
+
}
|
|
161
|
+
: {}),
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
130
165
|
if (state.platform !== process.platform || state.arch !== process.arch)
|
|
131
166
|
throw error("ZYRK_PLATFORM_UNSUPPORTED", "Deployment requires artifacts for the current platform and architecture.");
|
|
132
167
|
const store = await Store.open({
|
|
@@ -140,6 +175,9 @@ export async function runDeployCommand(options) {
|
|
|
140
175
|
: []);
|
|
141
176
|
const installedNodes = resolved.nodes.filter((n) => installedKeys.has(n.key));
|
|
142
177
|
const graph = createGraphDoc(installedNodes.map((n) => ({ ...n, deps: n.edges })));
|
|
178
|
+
const directories = privateGraph
|
|
179
|
+
? await privateDirectories(project, store, graph, state)
|
|
180
|
+
: selectRealizationDirs(store, graph);
|
|
143
181
|
const selectedRoots = {};
|
|
144
182
|
const declarations = {
|
|
145
183
|
...(object(manifest.dependencies) ? manifest.dependencies : {}),
|
|
@@ -168,13 +206,17 @@ export async function runDeployCommand(options) {
|
|
|
168
206
|
visit(edge.target);
|
|
169
207
|
};
|
|
170
208
|
Object.values(selectedRoots).forEach(visit);
|
|
171
|
-
|
|
172
|
-
await
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
209
|
+
if (privateGraph)
|
|
210
|
+
await validatePrivateProjection(project, graph, directories, selected, selectedRoots);
|
|
211
|
+
else
|
|
212
|
+
for (const key of selected)
|
|
213
|
+
await verifyRealization({
|
|
214
|
+
store,
|
|
215
|
+
graph,
|
|
216
|
+
rootKey: key,
|
|
217
|
+
contextId: contextIdFor(graph, key),
|
|
218
|
+
realizationDirs: directories,
|
|
219
|
+
});
|
|
178
220
|
const stage = await mkdtemp(path.join(parent, ".zyrk-deploy-"));
|
|
179
221
|
try {
|
|
180
222
|
for (const file of files)
|
|
@@ -185,13 +227,25 @@ export async function runDeployCommand(options) {
|
|
|
185
227
|
delete deployedManifest.packageManager;
|
|
186
228
|
await writeFile(path.join(stage, "package.json"), JSON.stringify(deployedManifest, null, 2) + "\n");
|
|
187
229
|
const packageDirs = new Map();
|
|
230
|
+
const privateBudget = { entries: 0, bytes: 0, started: Date.now() };
|
|
188
231
|
for (const key of selected) {
|
|
189
232
|
const node = byKey.get(key);
|
|
190
233
|
const context = contextIdFor(graph, key);
|
|
191
|
-
const sourceDir = packageDirIn(
|
|
234
|
+
const sourceDir = packageDirIn(directories.get(key), node.name);
|
|
192
235
|
const targetDir = packageDirIn(path.join(stage, "node_modules/.zyrk/portable", context), node.name);
|
|
193
236
|
packageDirs.set(key, targetDir);
|
|
194
237
|
await mkdir(targetDir, { recursive: true });
|
|
238
|
+
if (privateGraph) {
|
|
239
|
+
const nestedSlots = new Set();
|
|
240
|
+
const names = new Set([node.name]);
|
|
241
|
+
for (const edge of node.edges) {
|
|
242
|
+
if (names.has(edge.name))
|
|
243
|
+
nestedSlots.add(path.join("node_modules", ...edge.name.split("/")));
|
|
244
|
+
names.add(edge.name);
|
|
245
|
+
}
|
|
246
|
+
await copyPrivatePayload(sourceDir, targetDir, sourceDir, targetDir, nestedSlots, [project, store.root], privateBudget);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
195
249
|
const source = await readSourceManifest(store, node.sourceId);
|
|
196
250
|
for (const e of source.manifest.entries) {
|
|
197
251
|
const target = path.join(targetDir, e.path);
|
|
@@ -257,6 +311,8 @@ export async function runDeployCommand(options) {
|
|
|
257
311
|
const entryStat = await lstat(path.join(stage, entry)).catch(() => null);
|
|
258
312
|
if (!entryStat?.isFile())
|
|
259
313
|
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "The selected runtime Node entry is missing.");
|
|
314
|
+
if (privateGraph)
|
|
315
|
+
await scanPrivateExport(stage, [project, store.root], privateBudget);
|
|
260
316
|
await validateLinks(stage, stage);
|
|
261
317
|
await writeFile(path.join(stage, "zyrk-deploy.json"), JSON.stringify({
|
|
262
318
|
schemaVersion: 1,
|
|
@@ -313,8 +369,9 @@ async function copyApplication(source, target, project) {
|
|
|
313
369
|
const stat = await lstat(source).catch(() => null);
|
|
314
370
|
if (!stat)
|
|
315
371
|
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "A selected runtime file is missing.");
|
|
316
|
-
if (stat.isSymbolicLink()
|
|
317
|
-
|
|
372
|
+
if (stat.isSymbolicLink() ||
|
|
373
|
+
(await realpath(source).catch(() => null)) !== source)
|
|
374
|
+
throw error("ZYRK_DEPLOY_ESCAPE", "Application runtime selections must contain real files and directories without linked ancestors.");
|
|
318
375
|
if (stat.isDirectory()) {
|
|
319
376
|
await mkdir(target, { recursive: true });
|
|
320
377
|
for (const name of await readdir(source))
|
|
@@ -416,7 +473,7 @@ async function docker(options, project, manifest) {
|
|
|
416
473
|
buildScript: build ?? null,
|
|
417
474
|
port: Number(portText),
|
|
418
475
|
}, null, 2) + "\n",
|
|
419
|
-
"ZYRK_DOCKER.md": `Review Dockerfile and .dockerignore before building. Runtime files: ${files.join(", ")}. Runtime entry: node ${entry}. Build script: ${build ?? "none"}. Container port: ${portText}.\n\nThe pinned CLI tarball must remain at its declared path. Dependencies are fetched with an optional BuildKit npmrc secret, then installed, built, and exported without networking or registry secrets. No host node_modules or store enters the runtime image. Adapt build inputs and credential exclusions to your project; generated files do not prove application health.
|
|
476
|
+
"ZYRK_DOCKER.md": `Review Dockerfile and .dockerignore before building. Runtime files: ${files.join(", ")}. Runtime entry: node ${entry}. Build script: ${build ?? "none"}. Container port: ${portText}.\n\nThe pinned CLI tarball must remain at its declared path. Dependencies are fetched with an optional BuildKit npmrc secret, then installed, built, and exported without networking or registry secrets. No host node_modules or store enters the runtime image. Adapt build inputs and credential exclusions to your project; generated files do not prove application health. Relocatable JavaScript output from ready private generations is supported, including patches and approved build output. Native outputs, embedded host paths, live workspace exports, and arbitrary shell start commands require a validated adapter and are rejected by this generic exporter.\n\nBuild: zyrk docker build --tag your-app:local\nRun: zyrk docker run --tag your-app:local --port ${portText}\nSupply private registry credentials to Docker BuildKit directly with docker build --secret id=npmrc,src=<credential-file> when required; zyrk never embeds them.\n`,
|
|
420
477
|
};
|
|
421
478
|
for (const name of Object.keys(contents))
|
|
422
479
|
if (await lstat(path.join(project, name)).catch(() => null))
|
|
@@ -480,27 +537,225 @@ async function docker(options, project, manifest) {
|
|
|
480
537
|
return code;
|
|
481
538
|
}
|
|
482
539
|
async function subprocess(options, command, args, cwd, env) {
|
|
483
|
-
|
|
484
|
-
const
|
|
540
|
+
try {
|
|
541
|
+
const result = await superviseChild(command, args, {
|
|
485
542
|
cwd,
|
|
486
543
|
env,
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
child.stdout?.on("data", (c) => options.stdout.write(c.toString()));
|
|
490
|
-
child.stderr?.on("data", (c) => options.stderr.write(c.toString()));
|
|
491
|
-
const signals = ["SIGTERM", "SIGINT", "SIGHUP"];
|
|
492
|
-
const listeners = signals.map((sig) => {
|
|
493
|
-
const listener = () => {
|
|
494
|
-
child.kill(sig);
|
|
495
|
-
};
|
|
496
|
-
process.on(sig, listener);
|
|
497
|
-
return [sig, listener];
|
|
544
|
+
stdout: options.stdout,
|
|
545
|
+
stderr: options.stderr,
|
|
498
546
|
});
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
547
|
+
return result.exitCode;
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
throw error("ZYRK_BUILD_FAILED", "Cannot launch Docker; install the Docker CLI and enable a reachable BuildKit-capable daemon.");
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
async function privateDirectories(project, store, graph, state) {
|
|
554
|
+
const profile = state.buildProfile;
|
|
555
|
+
if (!object(profile) ||
|
|
556
|
+
typeof state.generationId !== "string" ||
|
|
557
|
+
!/^gen1_[a-z2-7]+$/.test(state.generationId) ||
|
|
558
|
+
typeof state.projectId !== "string")
|
|
559
|
+
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Private generation has no supported build profile or selected identity.");
|
|
560
|
+
if (profile.node !== process.version ||
|
|
561
|
+
profile.abi !== process.versions.modules ||
|
|
562
|
+
profile.platform !== process.platform ||
|
|
563
|
+
profile.arch !== process.arch)
|
|
564
|
+
throw error("ZYRK_PLATFORM_UNSUPPORTED", "Private export requires its exact recorded Node, ABI, platform and architecture.");
|
|
565
|
+
if (typeof profile.candidate !== "string" ||
|
|
566
|
+
!/^[0-9a-f]{128}$/.test(profile.candidate) ||
|
|
567
|
+
state.graphDigest !== graphHash(graph) ||
|
|
568
|
+
state.storeRoot !== store.root)
|
|
569
|
+
throw error("ZYRK_STORE_CORRUPT", "Private generation profile does not match its selected graph and store.");
|
|
570
|
+
const base = path.join(project, ".zyrk-local", "generations", state.generationId);
|
|
571
|
+
if (state.privateDir !== base ||
|
|
572
|
+
!Array.isArray(state.instances) ||
|
|
573
|
+
state.instances.some((id) => typeof id !== "string"))
|
|
574
|
+
throw error("ZYRK_STORE_CORRUPT", "Private generation instance selection is invalid.");
|
|
575
|
+
const db = openStoreDb(store.paths.dbFile);
|
|
576
|
+
const directories = new Map();
|
|
577
|
+
try {
|
|
578
|
+
const generation = db
|
|
579
|
+
.prepare("SELECT graph_digest,readiness,state FROM generations WHERE generation_id=? AND project_id=?")
|
|
580
|
+
.get(state.generationId, state.projectId);
|
|
581
|
+
if (!generation ||
|
|
582
|
+
generation.graph_digest !== graphHash(graph) ||
|
|
583
|
+
generation.readiness !== "ready" ||
|
|
584
|
+
generation.state !== "active")
|
|
585
|
+
throw error("ZYRK_STORE_CORRUPT", "Private deployment requires the active ready generation recorded by the project.");
|
|
586
|
+
const rows = db
|
|
587
|
+
.prepare("SELECT gi.context_id,ri.instance_id,ri.path FROM generation_instances gi JOIN realization_instances ri ON gi.instance_id=ri.instance_id WHERE gi.generation_id=? AND ri.verification_state='verified'")
|
|
588
|
+
.all(state.generationId);
|
|
589
|
+
const ids = new Set(state.instances);
|
|
590
|
+
if (rows.length !== ids.size ||
|
|
591
|
+
rows.some((row) => !ids.has(row.instance_id)))
|
|
592
|
+
throw error("ZYRK_STORE_CORRUPT", "Private generation instance metadata disagrees with the selected projection.");
|
|
593
|
+
const byContext = new Map(rows.map((row) => [row.context_id, row.path]));
|
|
594
|
+
for (const node of graph.nodes) {
|
|
595
|
+
const directory = path.join(base, contextIdFor(graph, node.key));
|
|
596
|
+
if (byContext.get(contextIdFor(graph, node.key)) !== directory)
|
|
597
|
+
throw error("ZYRK_DEPLOY_ESCAPE", "Private context path is outside its exact selected generation.");
|
|
598
|
+
directories.set(node.key, directory);
|
|
599
|
+
const pkg = packageDirIn(directory, node.name);
|
|
600
|
+
const ancestors = [
|
|
601
|
+
path.join(project, ".zyrk-local"),
|
|
602
|
+
path.join(project, ".zyrk-local/generations"),
|
|
603
|
+
base,
|
|
604
|
+
directory,
|
|
605
|
+
path.join(directory, "node_modules"),
|
|
606
|
+
...(node.name.startsWith("@") ? [path.dirname(pkg)] : []),
|
|
607
|
+
pkg,
|
|
608
|
+
];
|
|
609
|
+
for (const ancestor of ancestors) {
|
|
610
|
+
const metadata = await lstat(ancestor).catch(() => null);
|
|
611
|
+
if (!metadata?.isDirectory() || metadata.isSymbolicLink())
|
|
612
|
+
throw error("ZYRK_DEPLOY_ESCAPE", "Private generation passes through an unexpected link or missing directory.");
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
finally {
|
|
617
|
+
db.close();
|
|
618
|
+
}
|
|
619
|
+
return directories;
|
|
620
|
+
}
|
|
621
|
+
async function validatePrivateProjection(project, graph, directories, selected, roots) {
|
|
622
|
+
const nodes = new Map(graph.nodes.map((node) => [node.key, node]));
|
|
623
|
+
const check = async (slot, target) => {
|
|
624
|
+
const metadata = await lstat(slot).catch(() => null);
|
|
625
|
+
if (!metadata?.isSymbolicLink() ||
|
|
626
|
+
(await realpath(slot).catch(() => null)) !== target)
|
|
627
|
+
throw error("ZYRK_DEPLOY_ESCAPE", "Private dependency projection does not select its recorded context.");
|
|
628
|
+
};
|
|
629
|
+
for (const [name, key] of Object.entries(roots))
|
|
630
|
+
await check(path.join(project, "node_modules", name), packageDirIn(directories.get(key), nodes.get(key).name));
|
|
631
|
+
for (const key of selected) {
|
|
632
|
+
const node = nodes.get(key);
|
|
633
|
+
const pkg = packageDirIn(directories.get(key), node.name);
|
|
634
|
+
const names = new Set([node.name]);
|
|
635
|
+
for (const edge of node.edges) {
|
|
636
|
+
const target = nodes.get(edge.target);
|
|
637
|
+
const slot = names.has(edge.name)
|
|
638
|
+
? path.join(pkg, "node_modules", ...edge.name.split("/"))
|
|
639
|
+
: path.join(directories.get(key), "node_modules", ...edge.name.split("/"));
|
|
640
|
+
await check(slot, packageDirIn(directories.get(target.key), target.name));
|
|
641
|
+
names.add(edge.name);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
function checkPrivateBudget(budget) {
|
|
646
|
+
if (budget.entries > 200_000 ||
|
|
647
|
+
budget.bytes > 4 * 1024 * 1024 * 1024 ||
|
|
648
|
+
Date.now() - budget.started > 120_000)
|
|
649
|
+
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Private deployment exceeds its file, byte or duration budget.");
|
|
650
|
+
}
|
|
651
|
+
function nativeMagic(buffer) {
|
|
652
|
+
const magic = buffer.subarray(0, 4).toString("hex");
|
|
653
|
+
return ([
|
|
654
|
+
"7f454c46",
|
|
655
|
+
"feedface",
|
|
656
|
+
"cefaedfe",
|
|
657
|
+
"feedfacf",
|
|
658
|
+
"cffaedfe",
|
|
659
|
+
"cafebabe",
|
|
660
|
+
"bebafeca",
|
|
661
|
+
].includes(magic) || buffer.subarray(0, 2).toString("ascii") === "MZ");
|
|
662
|
+
}
|
|
663
|
+
async function scanOrCopyPrivateFile(source, target, forbidden, budget) {
|
|
664
|
+
const input = await open(source, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
665
|
+
let output;
|
|
666
|
+
try {
|
|
667
|
+
const metadata = await input.stat();
|
|
668
|
+
if (!metadata.isFile())
|
|
669
|
+
throw error("ZYRK_DEPLOY_ESCAPE", "Private payload contains a special filesystem entry.");
|
|
670
|
+
if (/\.node$/i.test(source))
|
|
671
|
+
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Native dependency outputs require a validated export adapter.");
|
|
672
|
+
if (target !== undefined)
|
|
673
|
+
output = await open(target, "wx", metadata.mode & 0o111 ? 0o755 : 0o644);
|
|
674
|
+
const needles = [
|
|
675
|
+
...new Set(forbidden.flatMap((value) => [
|
|
676
|
+
value,
|
|
677
|
+
JSON.stringify(value).slice(1, -1),
|
|
678
|
+
])),
|
|
679
|
+
].map((value) => Buffer.from(value));
|
|
680
|
+
// Keep enough overlap to detect credential markers split across read chunks.
|
|
681
|
+
const overlap = Math.max(4096, ...needles.map((value) => value.length));
|
|
682
|
+
let previous = Buffer.alloc(0);
|
|
683
|
+
let first = true;
|
|
684
|
+
let fileBytes = 0;
|
|
685
|
+
const chunk = Buffer.allocUnsafe(1024 * 1024);
|
|
686
|
+
for (;;) {
|
|
687
|
+
const { bytesRead } = await input.read(chunk, 0, chunk.length, null);
|
|
688
|
+
if (!bytesRead)
|
|
689
|
+
break;
|
|
690
|
+
const bytes = chunk.subarray(0, bytesRead);
|
|
691
|
+
fileBytes += bytesRead;
|
|
692
|
+
budget.bytes += bytesRead;
|
|
693
|
+
checkPrivateBudget(budget);
|
|
694
|
+
if (fileBytes > 512 * 1024 * 1024)
|
|
695
|
+
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "A private generated file exceeds the deployment byte budget.");
|
|
696
|
+
if (first && nativeMagic(bytes))
|
|
697
|
+
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Native dependency outputs require a validated export adapter.");
|
|
698
|
+
first = false;
|
|
699
|
+
const inspect = Buffer.concat([previous, bytes]);
|
|
700
|
+
if (needles.some((needle) => inspect.includes(needle)))
|
|
701
|
+
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Private output embeds an absolute host project or store path.");
|
|
702
|
+
if (/-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----|\bnpm_[A-Za-z0-9]{20,}|\bgh[pousr]_[A-Za-z0-9]{20,}|https?:\/\/[^\s/@]+:[^\s/@]+@|["']?_(?:authToken|auth|password)["']?\s*[:=]\s*["']?[^\s"';,}]+/i.test(inspect.toString("utf8")))
|
|
703
|
+
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Private output contains credential-like content.");
|
|
704
|
+
previous = Buffer.from(inspect.subarray(Math.max(0, inspect.length - overlap)));
|
|
705
|
+
if (output) {
|
|
706
|
+
let offset = 0;
|
|
707
|
+
while (offset < bytes.length) {
|
|
708
|
+
const written = await output.write(bytes, offset, bytes.length - offset);
|
|
709
|
+
if (!written.bytesWritten)
|
|
710
|
+
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Cannot finish copying private dependency output.");
|
|
711
|
+
offset += written.bytesWritten;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
finally {
|
|
717
|
+
await input.close();
|
|
718
|
+
await output?.close();
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
async function copyPrivatePayload(source, target, sourceRoot, targetRoot, skip, forbidden, budget) {
|
|
722
|
+
const metadata = await lstat(source);
|
|
723
|
+
budget.entries++;
|
|
724
|
+
checkPrivateBudget(budget);
|
|
725
|
+
const relative = path.relative(sourceRoot, source);
|
|
726
|
+
if (skip.has(relative))
|
|
727
|
+
return;
|
|
728
|
+
const name = path.basename(source);
|
|
729
|
+
if (relative && name !== "node_modules" && sensitive(name))
|
|
730
|
+
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Private dependency output contains a credential or host configuration filename.");
|
|
731
|
+
if (metadata.isSymbolicLink()) {
|
|
732
|
+
const actual = await realpath(source).catch(() => null);
|
|
733
|
+
if (actual === null || !within(sourceRoot, actual))
|
|
734
|
+
throw error("ZYRK_DEPLOY_ESCAPE", "Private payload link escapes its package or has no stable target.");
|
|
735
|
+
const mapped = path.join(targetRoot, path.relative(sourceRoot, actual));
|
|
736
|
+
await symlink(path.relative(path.dirname(target), mapped) || ".", target);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (metadata.isDirectory()) {
|
|
740
|
+
await mkdir(target, { recursive: true });
|
|
741
|
+
for (const name of (await readdir(source)).sort())
|
|
742
|
+
await copyPrivatePayload(path.join(source, name), path.join(target, name), sourceRoot, targetRoot, skip, forbidden, budget);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if (!metadata.isFile())
|
|
746
|
+
throw error("ZYRK_DEPLOY_ESCAPE", "Private payload contains a special filesystem entry.");
|
|
747
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
748
|
+
await scanOrCopyPrivateFile(source, target, forbidden, budget);
|
|
749
|
+
}
|
|
750
|
+
async function scanPrivateExport(root, forbidden, budget) {
|
|
751
|
+
for (const name of await readdir(root)) {
|
|
752
|
+
const file = path.join(root, name);
|
|
753
|
+
const metadata = await lstat(file);
|
|
754
|
+
budget.entries++;
|
|
755
|
+
checkPrivateBudget(budget);
|
|
756
|
+
if (metadata.isDirectory())
|
|
757
|
+
await scanPrivateExport(file, forbidden, budget);
|
|
758
|
+
else if (metadata.isFile())
|
|
759
|
+
await scanOrCopyPrivateFile(file, undefined, forbidden, budget);
|
|
760
|
+
}
|
|
506
761
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"maintenance.d.ts","sourceRoot":"","sources":["../../src/commands/maintenance.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"maintenance.d.ts","sourceRoot":"","sources":["../../src/commands/maintenance.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AASvE,QAAA,MAAM,UAAU,YAAI,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,CAAU,CAAC;AAC5E,KAAK,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAC5C,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,UAAU,GAAG,UAAU,GAAG,aAAa,CAAC;IAChD,UAAU,EAAE,KAAK,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,QAAQ,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;KACb,CAAC,CAAC;IACH,iBAAiB,EAAE,OAAO,CAAC;IAC3B,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAUF,wBAAsB,qBAAqB,CAAC,OAAO,EAAE;IACnD,OAAO,EAAE,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAC;IACtC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,cAAc,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB,GAAG,OAAO,CAAC,aAAa,GAAG,WAAW,CAAC,CAwOvC"}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import { findWorkspaceRoot, discoverWorkspaces, } from "../install/workspaces.js";
|
|
2
|
+
import { readWorkspaceLock } from "../install/workspace-lock.js";
|
|
3
|
+
import { currentPlatform } from "../resolve/resolver.js";
|
|
4
|
+
import { loadProjectPatches } from "./patch.js";
|
|
5
|
+
import { sha512Hex } from "../util/hash.js";
|
|
6
|
+
import { canonicalJson } from "../util/json.js";
|
|
1
7
|
/** Read-only advisory reporting and verified source prefetch. */
|
|
2
8
|
import { readFile } from "node:fs/promises";
|
|
3
9
|
import * as path from "node:path";
|
|
4
|
-
import { json as fetchJson } from "npm-registry-fetch";
|
|
5
10
|
import { satisfies, validRange } from "semver";
|
|
6
11
|
import { ZyrkError } from "../errors.js";
|
|
7
12
|
import { installProject } from "../install/run.js";
|
|
@@ -18,6 +23,9 @@ function usage(reason) {
|
|
|
18
23
|
});
|
|
19
24
|
}
|
|
20
25
|
export async function runMaintenanceCommand(options) {
|
|
26
|
+
const workspaceRoot = await findWorkspaceRoot(options.projectDir);
|
|
27
|
+
if (workspaceRoot)
|
|
28
|
+
options = { ...options, projectDir: workspaceRoot };
|
|
21
29
|
let frozen = false, offline = false;
|
|
22
30
|
let threshold;
|
|
23
31
|
for (let i = 0; i < options.args.length; i++) {
|
|
@@ -36,10 +44,19 @@ export async function runMaintenanceCommand(options) {
|
|
|
36
44
|
else
|
|
37
45
|
throw usage("an unsupported option or argument was supplied");
|
|
38
46
|
}
|
|
39
|
-
if (options.command === "repair")
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
if (options.command === "repair") {
|
|
48
|
+
if (options.args.some((arg) => arg !== "--offline"))
|
|
49
|
+
throw usage("repair accepts only --offline");
|
|
50
|
+
return installProject({
|
|
51
|
+
projectDir: options.projectDir,
|
|
52
|
+
store: options.store,
|
|
53
|
+
registry: options.registry,
|
|
54
|
+
frozen: true,
|
|
55
|
+
repair: true,
|
|
56
|
+
offline,
|
|
57
|
+
...(options.env ? { env: options.env } : {}),
|
|
42
58
|
});
|
|
59
|
+
}
|
|
43
60
|
if (options.command === "fetch") {
|
|
44
61
|
if (!frozen)
|
|
45
62
|
throw usage("fetch requires --frozen-lockfile");
|
|
@@ -50,6 +67,7 @@ export async function runMaintenanceCommand(options) {
|
|
|
50
67
|
frozen: true,
|
|
51
68
|
offline,
|
|
52
69
|
fetchOnly: true,
|
|
70
|
+
...(options.env ? { env: options.env } : {}),
|
|
53
71
|
});
|
|
54
72
|
}
|
|
55
73
|
let manifest;
|
|
@@ -100,17 +118,34 @@ export async function runMaintenanceCommand(options) {
|
|
|
100
118
|
const lock = await readLockfile(path.join(options.projectDir, "package-lock.json"));
|
|
101
119
|
if (!lock?.doc.zyrk)
|
|
102
120
|
throw lockMismatch("audit requires an exact zyrk lockfile");
|
|
121
|
+
const patches = await loadProjectPatches(options.projectDir);
|
|
103
122
|
assertFrozenCompatible(lock.doc, {
|
|
104
|
-
manifest:
|
|
123
|
+
manifest: workspaceRoot
|
|
124
|
+
? (await discoverWorkspaces(workspaceRoot)).manifestFingerprint
|
|
125
|
+
: manifestFingerprint(manifest),
|
|
105
126
|
scripts: scriptPolicyFingerprint(object(manifest.zyrk) ? manifest.zyrk.scripts : undefined),
|
|
106
127
|
config: configFingerprint({
|
|
107
128
|
registry: options.registry.origin,
|
|
108
129
|
linker: "shared",
|
|
130
|
+
platform: currentPlatform(),
|
|
131
|
+
...(object(manifest.zyrk) && Array.isArray(manifest.zyrk.mutablePackages)
|
|
132
|
+
? { mutablePackages: manifest.zyrk.mutablePackages }
|
|
133
|
+
: {}),
|
|
134
|
+
...(patches.length
|
|
135
|
+
? {
|
|
136
|
+
patches: sha512Hex(canonicalJson(patches.map((p) => ({
|
|
137
|
+
sourceId: p.sourceId,
|
|
138
|
+
digest: p.digest,
|
|
139
|
+
})))),
|
|
140
|
+
}
|
|
141
|
+
: {}),
|
|
109
142
|
}),
|
|
110
143
|
});
|
|
111
|
-
const
|
|
144
|
+
const graphNodes = workspaceRoot
|
|
145
|
+
? (await readWorkspaceLock(workspaceRoot, await discoverWorkspaces(workspaceRoot))).graph.nodes.filter((node) => node.kind === "registry")
|
|
146
|
+
: lockedGraph(lock.doc).nodes;
|
|
112
147
|
const packages = Object.create(null);
|
|
113
|
-
for (const node of
|
|
148
|
+
for (const node of graphNodes) {
|
|
114
149
|
if (node.origin !== options.registry.origin)
|
|
115
150
|
throw new ZyrkError("ZYRK_POLICY_DENIED", "Cannot audit a graph from a different registry origin.", {
|
|
116
151
|
remedy: "Select the registry recorded by the lockfile before sending dependency names.",
|
|
@@ -119,19 +154,11 @@ export async function runMaintenanceCommand(options) {
|
|
|
119
154
|
if (!packages[node.name].includes(node.version))
|
|
120
155
|
packages[node.name].push(node.version);
|
|
121
156
|
}
|
|
122
|
-
if (!
|
|
157
|
+
if (!graphNodes.length)
|
|
123
158
|
return base;
|
|
124
159
|
let response;
|
|
125
160
|
try {
|
|
126
|
-
response = await
|
|
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
|
-
});
|
|
161
|
+
response = await options.registry.auditAdvisories(packages);
|
|
135
162
|
}
|
|
136
163
|
catch {
|
|
137
164
|
return {
|
package/dist/commands/patch.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export declare function hasProjectPatches(project: string): Promise<boolean>;
|
|
|
20
20
|
/** Caller supplies the complete private generation, copied/reflinked without store hardlinks. */
|
|
21
21
|
export declare function applyProjectPatches(options: {
|
|
22
22
|
project: string;
|
|
23
|
+
store: Store;
|
|
23
24
|
packages: ReadonlyMap<string, {
|
|
24
25
|
sourceId: string;
|
|
25
26
|
directory: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"patch.d.ts","sourceRoot":"","sources":["../../src/commands/patch.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"patch.d.ts","sourceRoot":"","sources":["../../src/commands/patch.ts"],"names":[],"mappings":"AAQA,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,KAAK,EAAE,KAAK,CAAC;IACb,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,CA4D7C;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,CAwBnB"}
|