zyrk 0.4.9 → 0.5.0
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 +7 -5
- package/VERSION +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +347 -14
- package/dist/commands/native-export.d.ts +2 -0
- package/dist/commands/native-export.d.ts.map +1 -0
- package/dist/commands/native-export.js +192 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -144,8 +144,7 @@ and repair have workspace fixtures. Builds that reach live workspace sources are
|
|
|
144
144
|
blocked before execution. Existing npm v2/v3 workspace `package-lock.json` selections can be adopted with
|
|
145
145
|
`zyrk import --from npm`, preserving exact versions, aliases and live members.
|
|
146
146
|
Adoption validates the complete graph before project changes; npm shrinkwrap
|
|
147
|
-
precedence and unsupported bundled/external links fail explicitly.
|
|
148
|
-
workspace export still needs its adapter. Recursive execution supports
|
|
147
|
+
precedence and unsupported bundled/external links fail explicitly. Workspace-root production exports include live member closures and assets. Recursive execution supports
|
|
149
148
|
member scripts; filters are exact names or paths, not arbitrary glob expressions.
|
|
150
149
|
|
|
151
150
|
### Run a one-off tool
|
|
@@ -233,9 +232,12 @@ node ./dist-runtime/dist/server.js
|
|
|
233
232
|
|
|
234
233
|
The exporter validates that links stay inside the output and excludes the host
|
|
235
234
|
store. Generic export supports shared production graphs and relocatable private
|
|
236
|
-
JavaScript outputs, including selected patches and generated files.
|
|
237
|
-
|
|
238
|
-
|
|
235
|
+
JavaScript outputs, including selected patches and generated files. Workspace-root exports include live production members, peer contexts, cycles
|
|
236
|
+
and assets. A restricted Linux x64 ELF64 N-API copy recipe supports dependency-free
|
|
237
|
+
addons using `napi_create_int32` and `napi_set_named_property` with matching
|
|
238
|
+
Node/ABI/build profiles. Native bytes are copied unchanged after loader validation.
|
|
239
|
+
Other native formats/API imports, external libraries, loader paths, host-path-dependent
|
|
240
|
+
outputs and arbitrary shell startup adapters remain unsupported. Exports validate active generation/profile metadata and reject
|
|
239
241
|
credentials or escaping links. Structural validation does not prove application health. Run an
|
|
240
242
|
application smoke test from the exported directory with host-store access removed.
|
|
241
243
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v0.
|
|
1
|
+
v0.5.0
|
|
@@ -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":"AAmEA,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,CA0ZjB"}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
/** Self-contained
|
|
1
|
+
/** Self-contained runtime deployment and explicit local Docker commands. */
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
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
|
+
import { discoverWorkspaces, findWorkspaceRoot, } from "../install/workspaces.js";
|
|
6
|
+
import { readWorkspaceLock } from "../install/workspace-lock.js";
|
|
7
|
+
import { validateNativeExport } from "./native-export.js";
|
|
8
|
+
import { workspaceRegistryOccurrences, workspaceContextId, } from "../install/workspace-graph.js";
|
|
5
9
|
import { findProjectBoundary, resolveStoreRoot } from "../config/paths.js";
|
|
6
10
|
import { ZyrkError } from "../errors.js";
|
|
7
11
|
import { lockedGraph } from "../install/lock.js";
|
|
@@ -81,7 +85,8 @@ function output(options, result, human) {
|
|
|
81
85
|
: human + "\n");
|
|
82
86
|
}
|
|
83
87
|
export async function runDeployCommand(options) {
|
|
84
|
-
const boundary = await
|
|
88
|
+
const boundary = (await findWorkspaceRoot(options.cwd)) ??
|
|
89
|
+
(await findProjectBoundary(options.cwd));
|
|
85
90
|
if (!boundary)
|
|
86
91
|
throw error("ZYRK_MANIFEST_INVALID", "Cannot find a project to deploy.");
|
|
87
92
|
const project = await realpath(boundary);
|
|
@@ -125,7 +130,7 @@ export async function runDeployCommand(options) {
|
|
|
125
130
|
const rawLock = await readJson(path.join(project, "package-lock.json"));
|
|
126
131
|
if (manifest.workspaces !== undefined ||
|
|
127
132
|
(object(rawLock.zyrk) && rawLock.zyrk.version === 2))
|
|
128
|
-
|
|
133
|
+
return await exportWorkspace(options, project, manifest, state, out, parent, files, entry);
|
|
129
134
|
const doc = validateLockDoc(rawLock, "project package-lock.json");
|
|
130
135
|
const resolved = lockedGraph(doc);
|
|
131
136
|
const privateGraph = state.linker === "private";
|
|
@@ -168,6 +173,8 @@ export async function runDeployCommand(options) {
|
|
|
168
173
|
root: resolveStoreRoot({ env: options.env }).root,
|
|
169
174
|
});
|
|
170
175
|
store.requireInitialized();
|
|
176
|
+
if (within(store.root, out))
|
|
177
|
+
throw error("ZYRK_DEPLOY_ESCAPE", "Deployment output must be outside the managed store.");
|
|
171
178
|
const barrier = await acquireLock(path.join(store.paths.locksDir, "liveness.lock"), { name: "portable deployment retention" });
|
|
172
179
|
try {
|
|
173
180
|
const installedKeys = new Set(Array.isArray(state.contexts)
|
|
@@ -261,9 +268,7 @@ export async function runDeployCommand(options) {
|
|
|
261
268
|
await symlink(e.target, target);
|
|
262
269
|
}
|
|
263
270
|
else {
|
|
264
|
-
|
|
265
|
-
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Native addon runtime compatibility requires a validated deployment adapter.");
|
|
266
|
-
await copyFile(path.join(sourceDir, e.path), target);
|
|
271
|
+
await scanOrCopyPrivateFile(path.join(sourceDir, e.path), target, [project, store.root], privateBudget);
|
|
267
272
|
await chmod(target, e.mode === "exec" ? 0o755 : 0o644);
|
|
268
273
|
}
|
|
269
274
|
}
|
|
@@ -311,8 +316,7 @@ export async function runDeployCommand(options) {
|
|
|
311
316
|
const entryStat = await lstat(path.join(stage, entry)).catch(() => null);
|
|
312
317
|
if (!entryStat?.isFile())
|
|
313
318
|
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "The selected runtime Node entry is missing.");
|
|
314
|
-
|
|
315
|
-
await scanPrivateExport(stage, [project, store.root], privateBudget);
|
|
319
|
+
await scanPrivateExport(stage, [project, store.root], privateBudget);
|
|
316
320
|
await validateLinks(stage, stage);
|
|
317
321
|
await writeFile(path.join(stage, "zyrk-deploy.json"), JSON.stringify({
|
|
318
322
|
schemaVersion: 1,
|
|
@@ -473,7 +477,7 @@ async function docker(options, project, manifest) {
|
|
|
473
477
|
buildScript: build ?? null,
|
|
474
478
|
port: Number(portText),
|
|
475
479
|
}, null, 2) + "\n",
|
|
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
|
|
480
|
+
"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. Live workspace runtime closures and a restricted Linux ELF64 N-API native copy recipe are supported. The native v1 recipe is limited to Linux x64 ELF64 addons importing napi_create_int32 and napi_set_named_property (plus validated weak toolchain hooks), with exact installed Node/ABI/profile checks. It copies bytes unchanged and runs no package code during export. Native addons with external libraries, loader search paths, unknown formats or API imports, embedded absolute paths, and arbitrary shell start commands require another validated adapter and are rejected by this exporter. Workspace member payloads include their live files and assets except node_modules; credential filenames and links outside each source root are rejected.\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`,
|
|
477
481
|
};
|
|
478
482
|
for (const name of Object.keys(contents))
|
|
479
483
|
if (await lstat(path.join(project, name)).catch(() => null))
|
|
@@ -667,8 +671,23 @@ async function scanOrCopyPrivateFile(source, target, forbidden, budget) {
|
|
|
667
671
|
const metadata = await input.stat();
|
|
668
672
|
if (!metadata.isFile())
|
|
669
673
|
throw error("ZYRK_DEPLOY_ESCAPE", "Private payload contains a special filesystem entry.");
|
|
670
|
-
|
|
671
|
-
|
|
674
|
+
let nativeSnapshot;
|
|
675
|
+
if (/\.node$/i.test(source)) {
|
|
676
|
+
if (metadata.size > 64 * 1024 * 1024)
|
|
677
|
+
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Native addon exceeds the validated export size budget.");
|
|
678
|
+
const bytes = Buffer.alloc(metadata.size);
|
|
679
|
+
let offset = 0;
|
|
680
|
+
while (offset < bytes.length) {
|
|
681
|
+
const read = await input.read(bytes, offset, bytes.length - offset, offset);
|
|
682
|
+
if (!read.bytesRead)
|
|
683
|
+
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Native addon changed during export validation.");
|
|
684
|
+
offset += read.bytesRead;
|
|
685
|
+
}
|
|
686
|
+
validateNativeExport(bytes);
|
|
687
|
+
// Scan and copy these exact validated bytes. Private source files remain
|
|
688
|
+
// mutable; reopening or rereading them would discard the validation.
|
|
689
|
+
nativeSnapshot = bytes;
|
|
690
|
+
}
|
|
672
691
|
if (target !== undefined)
|
|
673
692
|
output = await open(target, "wx", metadata.mode & 0o111 ? 0o755 : 0o644);
|
|
674
693
|
const needles = [
|
|
@@ -684,16 +703,23 @@ async function scanOrCopyPrivateFile(source, target, forbidden, budget) {
|
|
|
684
703
|
let fileBytes = 0;
|
|
685
704
|
const chunk = Buffer.allocUnsafe(1024 * 1024);
|
|
686
705
|
for (;;) {
|
|
687
|
-
|
|
706
|
+
let bytes;
|
|
707
|
+
if (nativeSnapshot) {
|
|
708
|
+
bytes = nativeSnapshot.subarray(fileBytes, fileBytes + chunk.length);
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
const read = await input.read(chunk, 0, chunk.length, null);
|
|
712
|
+
bytes = chunk.subarray(0, read.bytesRead);
|
|
713
|
+
}
|
|
714
|
+
const bytesRead = bytes.length;
|
|
688
715
|
if (!bytesRead)
|
|
689
716
|
break;
|
|
690
|
-
const bytes = chunk.subarray(0, bytesRead);
|
|
691
717
|
fileBytes += bytesRead;
|
|
692
718
|
budget.bytes += bytesRead;
|
|
693
719
|
checkPrivateBudget(budget);
|
|
694
720
|
if (fileBytes > 512 * 1024 * 1024)
|
|
695
721
|
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "A private generated file exceeds the deployment byte budget.");
|
|
696
|
-
if (first && nativeMagic(bytes))
|
|
722
|
+
if (first && nativeMagic(bytes) && !/\.node$/i.test(source))
|
|
697
723
|
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Native dependency outputs require a validated export adapter.");
|
|
698
724
|
first = false;
|
|
699
725
|
const inspect = Buffer.concat([previous, bytes]);
|
|
@@ -759,3 +785,310 @@ async function scanPrivateExport(root, forbidden, budget) {
|
|
|
759
785
|
await scanOrCopyPrivateFile(file, undefined, forbidden, budget);
|
|
760
786
|
}
|
|
761
787
|
}
|
|
788
|
+
/** Export the selected importer closure; live files are copied, never entered into CAS. */
|
|
789
|
+
async function exportWorkspace(options, project, manifest, state, out, parent, files, entry) {
|
|
790
|
+
const workspace = await discoverWorkspaces(project);
|
|
791
|
+
const locked = await readWorkspaceLock(project, workspace);
|
|
792
|
+
if (!locked ||
|
|
793
|
+
locked.doc.zyrk?.manifest !== workspace.manifestFingerprint ||
|
|
794
|
+
state.lockDigest !== sha512Hex(canonicalJson(locked.doc)))
|
|
795
|
+
throw error("ZYRK_LOCK_MISMATCH", "Workspace deployment manifests, lock and installed generation disagree.");
|
|
796
|
+
if (state.readiness !== "ready")
|
|
797
|
+
throw error("ZYRK_SCRIPT_BLOCKED", "Cannot export an unready workspace generation.");
|
|
798
|
+
if (state.platform !== process.platform || state.arch !== process.arch)
|
|
799
|
+
throw error("ZYRK_PLATFORM_UNSUPPORTED", "Workspace export requires the recorded target platform and architecture.");
|
|
800
|
+
const policy = object(manifest.zyrk) ? manifest.zyrk : {};
|
|
801
|
+
if (locked.doc.zyrk.scripts !== scriptPolicyFingerprint(policy.scripts))
|
|
802
|
+
throw error("ZYRK_LOCK_MISMATCH", "Workspace deployment build policy changed after installation.");
|
|
803
|
+
const origins = new Set(locked.graph.nodes
|
|
804
|
+
.filter((n) => n.kind === "registry")
|
|
805
|
+
.map((n) => n.origin));
|
|
806
|
+
if (origins.size > 1)
|
|
807
|
+
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Mixed-origin workspace export requires a validated registry adapter.");
|
|
808
|
+
if (origins.size === 1) {
|
|
809
|
+
const patches = await loadProjectPatches(project);
|
|
810
|
+
const expected = configFingerprint({
|
|
811
|
+
registry: [...origins][0],
|
|
812
|
+
linker: "shared",
|
|
813
|
+
platform: currentPlatform(),
|
|
814
|
+
...(Array.isArray(policy.mutablePackages)
|
|
815
|
+
? { mutablePackages: policy.mutablePackages }
|
|
816
|
+
: {}),
|
|
817
|
+
...(patches.length
|
|
818
|
+
? {
|
|
819
|
+
patches: sha512Hex(canonicalJson(patches.map((p) => ({
|
|
820
|
+
sourceId: p.sourceId,
|
|
821
|
+
digest: p.digest,
|
|
822
|
+
})))),
|
|
823
|
+
}
|
|
824
|
+
: {}),
|
|
825
|
+
});
|
|
826
|
+
if (locked.doc.zyrk.config !== expected)
|
|
827
|
+
throw error("ZYRK_LOCK_MISMATCH", "Workspace deployment configuration or patches changed after installation.");
|
|
828
|
+
}
|
|
829
|
+
const store = await Store.open({
|
|
830
|
+
root: resolveStoreRoot({ env: options.env }).root,
|
|
831
|
+
});
|
|
832
|
+
store.requireInitialized();
|
|
833
|
+
if (within(store.root, out))
|
|
834
|
+
throw error("ZYRK_DEPLOY_ESCAPE", "Deployment output must be outside the managed store.");
|
|
835
|
+
const barrier = await acquireLock(path.join(store.paths.locksDir, "liveness.lock"), { name: "workspace deployment retention" });
|
|
836
|
+
try {
|
|
837
|
+
const graph = locked.graph, byKey = new Map(graph.nodes.map((n) => [n.key, n]));
|
|
838
|
+
const selected = new Set();
|
|
839
|
+
const visit = (key) => {
|
|
840
|
+
if (selected.has(key))
|
|
841
|
+
return;
|
|
842
|
+
const n = byKey.get(key);
|
|
843
|
+
if (!n)
|
|
844
|
+
throw error("ZYRK_OFFLINE_MISSING", "Workspace runtime edge is missing.");
|
|
845
|
+
selected.add(key);
|
|
846
|
+
for (const e of n.edges)
|
|
847
|
+
if (!(n.kind === "local" && e.kind === "dev"))
|
|
848
|
+
visit(e.target);
|
|
849
|
+
};
|
|
850
|
+
const rootKey = graph.importers["."];
|
|
851
|
+
if (!rootKey)
|
|
852
|
+
throw error("ZYRK_LOCK_MISMATCH", "Workspace root importer is missing.");
|
|
853
|
+
visit(rootKey);
|
|
854
|
+
const directories = new Map();
|
|
855
|
+
for (const key of selected) {
|
|
856
|
+
const n = byKey.get(key);
|
|
857
|
+
if (n.kind === "local")
|
|
858
|
+
directories.set(key, workspace.importers.find((i) => i.id === n.importer).directory);
|
|
859
|
+
}
|
|
860
|
+
// Resolve recorded projection edges rather than enumerating arbitrary node_modules entries.
|
|
861
|
+
const queue = [...directories.keys()], checked = new Set();
|
|
862
|
+
while (queue.length) {
|
|
863
|
+
const key = queue.shift();
|
|
864
|
+
if (checked.has(key))
|
|
865
|
+
continue;
|
|
866
|
+
checked.add(key);
|
|
867
|
+
const n = byKey.get(key), directory = directories.get(key);
|
|
868
|
+
for (const e of n.edges) {
|
|
869
|
+
if (!selected.has(e.target) || (n.kind === "local" && e.kind === "dev"))
|
|
870
|
+
continue;
|
|
871
|
+
const slot = n.kind === "local" || e.name === n.name
|
|
872
|
+
? path.join(directory, "node_modules", e.name)
|
|
873
|
+
: path.join(directory, n.name.startsWith("@") ? "../.." : "..", e.name);
|
|
874
|
+
const actual = await realpath(slot).catch(() => null), meta = await lstat(slot).catch(() => null);
|
|
875
|
+
if (!actual ||
|
|
876
|
+
!meta?.isSymbolicLink() ||
|
|
877
|
+
(directories.has(e.target) && directories.get(e.target) !== actual))
|
|
878
|
+
throw error("ZYRK_DEPLOY_ESCAPE", "Workspace dependency projection disagrees with its recorded context.");
|
|
879
|
+
directories.set(e.target, actual);
|
|
880
|
+
queue.push(e.target);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
for (const [key, source] of directories)
|
|
884
|
+
if (key !== rootKey && (within(source, out) || within(out, source)))
|
|
885
|
+
throw error("ZYRK_DEPLOY_ESCAPE", "Deployment output overlaps a selected workspace runtime source.");
|
|
886
|
+
const registryNodes = workspaceRegistryOccurrences(graph);
|
|
887
|
+
const registryKeys = new Set(registryNodes.map((n) => n.key));
|
|
888
|
+
const privateKeys = new Set();
|
|
889
|
+
for (let changed = true; changed;) {
|
|
890
|
+
changed = false;
|
|
891
|
+
for (const n of registryNodes)
|
|
892
|
+
if (!privateKeys.has(n.key) &&
|
|
893
|
+
n.edges.some((e) => !registryKeys.has(e.target) || privateKeys.has(e.target))) {
|
|
894
|
+
privateKeys.add(n.key);
|
|
895
|
+
changed = true;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
const sourceGraph = createGraphDoc(registryNodes.map((n) => ({
|
|
899
|
+
...n,
|
|
900
|
+
deps: n.edges.filter((e) => registryKeys.has(e.target)),
|
|
901
|
+
})));
|
|
902
|
+
const pureGraph = createGraphDoc(registryNodes
|
|
903
|
+
.filter((n) => !privateKeys.has(n.key))
|
|
904
|
+
.map((n) => ({ ...n, deps: n.edges })));
|
|
905
|
+
const sharedDirectories = selectRealizationDirs(store, pureGraph);
|
|
906
|
+
const productionGraph = {
|
|
907
|
+
...graph,
|
|
908
|
+
nodes: graph.nodes.map((n) => ({
|
|
909
|
+
...n,
|
|
910
|
+
edges: n.edges.filter((e) => !(n.kind === "local" && e.kind === "dev")),
|
|
911
|
+
})),
|
|
912
|
+
};
|
|
913
|
+
const db = openStoreDb(store.paths.dbFile);
|
|
914
|
+
const registered = new Set();
|
|
915
|
+
try {
|
|
916
|
+
if (typeof state.generationId !== "string" ||
|
|
917
|
+
!/^gen1_[a-z2-7]+$/.test(state.generationId) ||
|
|
918
|
+
state.projectId !== `project1_${sha512Hex(project).slice(0, 32)}` ||
|
|
919
|
+
state.storeRoot !== store.root ||
|
|
920
|
+
state.workspaceRoot !== project)
|
|
921
|
+
throw error("ZYRK_STORE_CORRUPT", "Workspace generation identity is invalid.");
|
|
922
|
+
const generation = db
|
|
923
|
+
.prepare("SELECT readiness,state,graph_digest FROM generations WHERE generation_id=? AND project_id=?")
|
|
924
|
+
.get(state.generationId, state.projectId);
|
|
925
|
+
if (generation?.readiness !== "ready" ||
|
|
926
|
+
generation.state !== "active" ||
|
|
927
|
+
generation.graph_digest !== state.graphDigest)
|
|
928
|
+
throw error("ZYRK_STORE_CORRUPT", "Workspace deployment requires its active ready generation.");
|
|
929
|
+
const rows = db
|
|
930
|
+
.prepare("SELECT ri.path,ri.instance_id FROM generation_instances gi JOIN realization_instances ri ON gi.instance_id=ri.instance_id WHERE gi.generation_id=? AND ri.verification_state='verified'")
|
|
931
|
+
.all(state.generationId);
|
|
932
|
+
if (!Array.isArray(state.instances) ||
|
|
933
|
+
rows.length !== state.instances.length ||
|
|
934
|
+
rows.some((row) => !state.instances.includes(row.instance_id)))
|
|
935
|
+
throw error("ZYRK_STORE_CORRUPT", "Workspace instance metadata disagrees with its selected generation.");
|
|
936
|
+
rows.forEach((row) => registered.add(row.path));
|
|
937
|
+
}
|
|
938
|
+
finally {
|
|
939
|
+
db.close();
|
|
940
|
+
}
|
|
941
|
+
if (state.linker === "private" && !object(state.buildProfile))
|
|
942
|
+
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Workspace private generation has no supported build profile.");
|
|
943
|
+
const nativeProfile = state.buildProfile;
|
|
944
|
+
if (object(nativeProfile) &&
|
|
945
|
+
(typeof nativeProfile.candidate !== "string" ||
|
|
946
|
+
!/^[0-9a-f]{128}$/.test(nativeProfile.candidate) ||
|
|
947
|
+
state.privateDir !==
|
|
948
|
+
path.join(project, ".zyrk-local/generations", String(state.generationId))))
|
|
949
|
+
throw error("ZYRK_STORE_CORRUPT", "Workspace private generation profile or stable directory is invalid.");
|
|
950
|
+
if (object(nativeProfile) &&
|
|
951
|
+
(nativeProfile.node !== process.version ||
|
|
952
|
+
nativeProfile.abi !== process.versions.modules ||
|
|
953
|
+
nativeProfile.platform !== process.platform ||
|
|
954
|
+
nativeProfile.arch !== process.arch))
|
|
955
|
+
throw error("ZYRK_PLATFORM_UNSUPPORTED", "Workspace private artifacts require their exact recorded Node ABI and profile.");
|
|
956
|
+
for (const key of selected) {
|
|
957
|
+
const n = byKey.get(key), directory = directories.get(key);
|
|
958
|
+
if (!directory)
|
|
959
|
+
throw error("ZYRK_OFFLINE_MISSING", "A required workspace runtime package is not installed.");
|
|
960
|
+
if (n.kind === "registry") {
|
|
961
|
+
const context = n.name.startsWith("@")
|
|
962
|
+
? path.resolve(directory, "../../..")
|
|
963
|
+
: path.resolve(directory, "../..");
|
|
964
|
+
if (!registered.has(context) ||
|
|
965
|
+
(!within(store.paths.realizationsDir, context) &&
|
|
966
|
+
!within(path.join(project, ".zyrk-local/generations", String(state.generationId)), context)))
|
|
967
|
+
throw error("ZYRK_DEPLOY_ESCAPE", "Workspace registry dependency is outside its registered realization.");
|
|
968
|
+
if (within(store.paths.realizationsDir, context)) {
|
|
969
|
+
if (privateKeys.has(key) ||
|
|
970
|
+
packageDirIn(sharedDirectories.get(key), n.name) !== directory)
|
|
971
|
+
throw error("ZYRK_DEPLOY_ESCAPE", "Shared workspace registry dependency selects an unexpected context.");
|
|
972
|
+
await verifyRealization({
|
|
973
|
+
store,
|
|
974
|
+
graph: pureGraph,
|
|
975
|
+
rootKey: key,
|
|
976
|
+
contextId: contextIdFor(pureGraph, key),
|
|
977
|
+
realizationDirs: sharedDirectories,
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
else {
|
|
981
|
+
const expectedContexts = privateKeys.has(key)
|
|
982
|
+
? [
|
|
983
|
+
workspaceContextId(graph, key, String(state.projectId)),
|
|
984
|
+
workspaceContextId(productionGraph, key, String(state.projectId)),
|
|
985
|
+
]
|
|
986
|
+
: [contextIdFor(sourceGraph, key)];
|
|
987
|
+
if (!expectedContexts.some((id) => path.join(project, ".zyrk-local/generations", String(state.generationId), id) === context))
|
|
988
|
+
throw error("ZYRK_DEPLOY_ESCAPE", "Private workspace dependency selects an unexpected live context.");
|
|
989
|
+
}
|
|
990
|
+
const m = await readJson(path.join(directory, "package.json"));
|
|
991
|
+
if (m.name !== n.name || m.version !== n.version)
|
|
992
|
+
throw error("ZYRK_STORE_CORRUPT", "Workspace runtime package identity disagrees with its lock.");
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
const stage = await mkdtemp(path.join(parent, ".zyrk-deploy-"));
|
|
996
|
+
try {
|
|
997
|
+
const budget = { entries: 0, bytes: 0, started: Date.now() }, targets = new Map([[rootKey, stage]]);
|
|
998
|
+
for (const key of selected)
|
|
999
|
+
if (key !== rootKey)
|
|
1000
|
+
targets.set(key, packageDirIn(path.join(stage, "node_modules/.zyrk/portable", sha512Hex(key).slice(0, 40)), byKey.get(key).name));
|
|
1001
|
+
for (const file of files)
|
|
1002
|
+
await copyApplication(path.join(project, file), path.join(stage, file), project);
|
|
1003
|
+
for (const key of selected) {
|
|
1004
|
+
const n = byKey.get(key), target = targets.get(key), source = directories.get(key);
|
|
1005
|
+
if (key !== rootKey) {
|
|
1006
|
+
const skip = new Set();
|
|
1007
|
+
if (n.kind === "local")
|
|
1008
|
+
skip.add("node_modules");
|
|
1009
|
+
else
|
|
1010
|
+
for (const e of n.edges)
|
|
1011
|
+
if (e.name === n.name)
|
|
1012
|
+
skip.add(path.join("node_modules", ...e.name.split("/")));
|
|
1013
|
+
await copyPrivatePayload(source, target, source, target, skip, [project, store.root], budget);
|
|
1014
|
+
}
|
|
1015
|
+
const sourceManifest = n.kind === "local"
|
|
1016
|
+
? workspace.importers.find((i) => i.id === n.importer).manifest
|
|
1017
|
+
: await readJson(path.join(source, "package.json"));
|
|
1018
|
+
const exported = { ...sourceManifest };
|
|
1019
|
+
delete exported.devDependencies;
|
|
1020
|
+
delete exported.zyrk;
|
|
1021
|
+
delete exported.packageManager;
|
|
1022
|
+
delete exported.workspaces;
|
|
1023
|
+
await writeFile(path.join(target, "package.json"), JSON.stringify(exported, null, 2) + "\n");
|
|
1024
|
+
}
|
|
1025
|
+
for (const key of selected) {
|
|
1026
|
+
const n = byKey.get(key);
|
|
1027
|
+
for (const e of n.edges)
|
|
1028
|
+
if (selected.has(e.target) &&
|
|
1029
|
+
!(n.kind === "local" && e.kind === "dev")) {
|
|
1030
|
+
await relativeLink(path.join(targets.get(key), "node_modules", e.name), targets.get(e.target));
|
|
1031
|
+
const child = byKey.get(e.target);
|
|
1032
|
+
const rawBin = child.kind === "registry"
|
|
1033
|
+
? child.bin
|
|
1034
|
+
: workspace.importers.find((i) => i.id === child.importer)
|
|
1035
|
+
.manifest.bin;
|
|
1036
|
+
const bins = typeof rawBin === "string"
|
|
1037
|
+
? { [child.name.split("/").at(-1)]: rawBin }
|
|
1038
|
+
: object(rawBin)
|
|
1039
|
+
? rawBin
|
|
1040
|
+
: {};
|
|
1041
|
+
for (const [name, relative] of Object.entries(bins)) {
|
|
1042
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name) ||
|
|
1043
|
+
typeof relative !== "string")
|
|
1044
|
+
throw error("ZYRK_DEPLOY_ESCAPE", "Workspace executable declaration is unsafe.");
|
|
1045
|
+
const pkg = targets.get(e.target), file = path.resolve(pkg, relative);
|
|
1046
|
+
if (!within(pkg, file))
|
|
1047
|
+
throw error("ZYRK_DEPLOY_ESCAPE", "Workspace executable escapes its runtime package.");
|
|
1048
|
+
const metadata = await lstat(file).catch(() => null);
|
|
1049
|
+
if (!metadata?.isFile())
|
|
1050
|
+
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "A required workspace executable is missing.");
|
|
1051
|
+
const binDir = path.join(targets.get(key), "node_modules/.bin");
|
|
1052
|
+
await mkdir(binDir, { recursive: true });
|
|
1053
|
+
if (await lstat(path.join(binDir, name)).catch(() => null))
|
|
1054
|
+
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "Workspace runtime executable names conflict.");
|
|
1055
|
+
if (metadata.mode & 0o111)
|
|
1056
|
+
await relativeLink(path.join(binDir, name), file);
|
|
1057
|
+
else
|
|
1058
|
+
await writeFile(path.join(binDir, name), '#!/bin/sh\nexec node "$(dirname "$0")"/' +
|
|
1059
|
+
shellQuote(path.relative(binDir, file).split(path.sep).join("/")) +
|
|
1060
|
+
' "$@"\n', { mode: 0o755 });
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
if (!(await lstat(path.join(stage, entry)).catch(() => null))?.isFile())
|
|
1065
|
+
throw error("ZYRK_DEPLOY_NONRELOCATABLE", "The selected runtime Node entry is missing.");
|
|
1066
|
+
await scanPrivateExport(stage, [project, store.root], budget);
|
|
1067
|
+
await validateLinks(stage, stage);
|
|
1068
|
+
await writeFile(path.join(stage, "zyrk-deploy.json"), JSON.stringify({
|
|
1069
|
+
schemaVersion: 1,
|
|
1070
|
+
platform: process.platform,
|
|
1071
|
+
arch: process.arch,
|
|
1072
|
+
node: process.version,
|
|
1073
|
+
entry: ["node", entry],
|
|
1074
|
+
requirements: ["Node.js " + process.versions.node.split(".")[0]],
|
|
1075
|
+
packages: selected.size - 1,
|
|
1076
|
+
files,
|
|
1077
|
+
validation: "structural; application smoke test not run",
|
|
1078
|
+
}, null, 2) + "\n");
|
|
1079
|
+
await rename(stage, out);
|
|
1080
|
+
output(options, {
|
|
1081
|
+
packages: selected.size - 1,
|
|
1082
|
+
entry: ["node", entry],
|
|
1083
|
+
validation: "structural",
|
|
1084
|
+
}, `Exported ${selected.size - 1} workspace runtime packages. Start with: node ${entry}. Structural validation passed; no application smoke test ran.`);
|
|
1085
|
+
return 0;
|
|
1086
|
+
}
|
|
1087
|
+
finally {
|
|
1088
|
+
await rm(stage, { recursive: true, force: true });
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
finally {
|
|
1092
|
+
await barrier.release();
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-export.d.ts","sourceRoot":"","sources":["../../src/commands/native-export.ts"],"names":[],"mappings":"AAYA,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CA4MxD"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validated Linux ELF64 N-API copy recipe (§12.3.1).
|
|
3
|
+
*
|
|
4
|
+
* Only position-independent addons with no external dynamic-library imports,
|
|
5
|
+
* loader search paths or text relocations qualify. Undefined symbols are Node's
|
|
6
|
+
* tested integer/property entry points and the weak ELF toolchain hooks below.
|
|
7
|
+
* This recipe
|
|
8
|
+
* copies bytes unchanged; it never executes package code or rewrites binaries.
|
|
9
|
+
* The caller checks the exact build profile and scans host paths/credentials.
|
|
10
|
+
* Other native formats and dependency recipes require dedicated adapters.
|
|
11
|
+
*/
|
|
12
|
+
import { ZyrkError } from "../errors.js";
|
|
13
|
+
export function validateNativeExport(bytes) {
|
|
14
|
+
const fail = () => {
|
|
15
|
+
throw new ZyrkError("ZYRK_DEPLOY_NONRELOCATABLE", "Native artifact is outside the supported Linux ELF64 N-API export recipe.", {
|
|
16
|
+
remedy: "Use a position-independent N-API addon without external dynamic libraries or loader paths, or supply a validated target rebuild adapter.",
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
const range = (offset, size) => {
|
|
20
|
+
if (!Number.isSafeInteger(offset) ||
|
|
21
|
+
!Number.isSafeInteger(size) ||
|
|
22
|
+
offset < 0 ||
|
|
23
|
+
size < 0 ||
|
|
24
|
+
offset + size > bytes.length)
|
|
25
|
+
fail();
|
|
26
|
+
};
|
|
27
|
+
if (process.platform !== "linux" ||
|
|
28
|
+
process.arch !== "x64" ||
|
|
29
|
+
bytes.length < 64 ||
|
|
30
|
+
bytes.length > 64 * 1024 * 1024 ||
|
|
31
|
+
bytes.subarray(0, 4).toString("hex") !== "7f454c46" ||
|
|
32
|
+
bytes[4] !== 2 ||
|
|
33
|
+
bytes[5] !== 1 ||
|
|
34
|
+
bytes[6] !== 1)
|
|
35
|
+
fail();
|
|
36
|
+
if (bytes.readUInt16LE(16) !== 3 ||
|
|
37
|
+
bytes.readUInt16LE(18) !== 62 ||
|
|
38
|
+
bytes.readUInt16LE(52) !== 64 ||
|
|
39
|
+
bytes.readUInt32LE(20) !== 1)
|
|
40
|
+
fail();
|
|
41
|
+
const programs = Number(bytes.readBigUInt64LE(32)), programSize = bytes.readUInt16LE(54), programCount = bytes.readUInt16LE(56);
|
|
42
|
+
if (programSize !== 56 || programCount === 0 || programCount > 1024)
|
|
43
|
+
fail();
|
|
44
|
+
range(programs, programSize * programCount);
|
|
45
|
+
let loaderDynamic;
|
|
46
|
+
const segments = [];
|
|
47
|
+
for (let i = 0; i < programCount; i++) {
|
|
48
|
+
const p = programs + i * programSize, type = bytes.readUInt32LE(p), offset = Number(bytes.readBigUInt64LE(p + 8)), fileSize = Number(bytes.readBigUInt64LE(p + 32)), memorySize = Number(bytes.readBigUInt64LE(p + 40)), address = Number(bytes.readBigUInt64LE(p + 16));
|
|
49
|
+
if (!Number.isSafeInteger(address) ||
|
|
50
|
+
!Number.isSafeInteger(memorySize) ||
|
|
51
|
+
fileSize > memorySize)
|
|
52
|
+
fail();
|
|
53
|
+
range(offset, fileSize);
|
|
54
|
+
if (type === 3)
|
|
55
|
+
fail();
|
|
56
|
+
if (type === 1)
|
|
57
|
+
segments.push({ offset, size: fileSize, address });
|
|
58
|
+
if (type === 2) {
|
|
59
|
+
if (loaderDynamic)
|
|
60
|
+
fail();
|
|
61
|
+
if (fileSize !== memorySize)
|
|
62
|
+
fail();
|
|
63
|
+
loaderDynamic = { offset, size: fileSize, address };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const table = Number(bytes.readBigUInt64LE(40)), size = bytes.readUInt16LE(58), count = bytes.readUInt16LE(60);
|
|
67
|
+
if (size !== 64 || count === 0 || count > 4096)
|
|
68
|
+
fail();
|
|
69
|
+
range(table, size * count);
|
|
70
|
+
const section = (index) => {
|
|
71
|
+
if (index < 0 || index >= count)
|
|
72
|
+
fail();
|
|
73
|
+
const p = table + index * size;
|
|
74
|
+
return {
|
|
75
|
+
type: bytes.readUInt32LE(p + 4),
|
|
76
|
+
address: Number(bytes.readBigUInt64LE(p + 16)),
|
|
77
|
+
offset: Number(bytes.readBigUInt64LE(p + 24)),
|
|
78
|
+
size: Number(bytes.readBigUInt64LE(p + 32)),
|
|
79
|
+
link: bytes.readUInt32LE(p + 40),
|
|
80
|
+
entry: Number(bytes.readBigUInt64LE(p + 56)),
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
const mapped = (region) => {
|
|
84
|
+
if (!segments.some((segment) => region.offset >= segment.offset &&
|
|
85
|
+
region.offset + region.size <= segment.offset + segment.size &&
|
|
86
|
+
region.address === segment.address + region.offset - segment.offset))
|
|
87
|
+
fail();
|
|
88
|
+
};
|
|
89
|
+
const tags = new Map();
|
|
90
|
+
let symbols, strings;
|
|
91
|
+
let registration = false, api = false, dynamic = false;
|
|
92
|
+
const validatedApi = new Set([
|
|
93
|
+
"napi_create_int32",
|
|
94
|
+
"napi_set_named_property",
|
|
95
|
+
]);
|
|
96
|
+
const weakHooks = new Set([
|
|
97
|
+
"__cxa_finalize",
|
|
98
|
+
"__gmon_start__",
|
|
99
|
+
"_ITM_registerTMCloneTable",
|
|
100
|
+
"_ITM_deregisterTMCloneTable",
|
|
101
|
+
]);
|
|
102
|
+
for (let i = 0; i < count; i++) {
|
|
103
|
+
const s = section(i);
|
|
104
|
+
if (s.type !== 6 && s.type !== 11)
|
|
105
|
+
continue;
|
|
106
|
+
range(s.offset, s.size);
|
|
107
|
+
if (s.type === 6) {
|
|
108
|
+
if (dynamic ||
|
|
109
|
+
!loaderDynamic ||
|
|
110
|
+
loaderDynamic.offset !== s.offset ||
|
|
111
|
+
loaderDynamic.size !== s.size ||
|
|
112
|
+
loaderDynamic.address !== s.address)
|
|
113
|
+
fail();
|
|
114
|
+
mapped(s);
|
|
115
|
+
dynamic = true;
|
|
116
|
+
if (s.entry !== 16 || s.size % 16 !== 0)
|
|
117
|
+
fail();
|
|
118
|
+
let terminated = false;
|
|
119
|
+
for (let p = s.offset; p < s.offset + s.size; p += 16) {
|
|
120
|
+
const tag = bytes.readBigInt64LE(p), value = bytes.readBigUInt64LE(p + 8);
|
|
121
|
+
if (tag === 0n) {
|
|
122
|
+
terminated = true;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
if ([5n, 6n, 10n, 11n].includes(tag) && tags.has(tag))
|
|
126
|
+
fail();
|
|
127
|
+
tags.set(tag, value);
|
|
128
|
+
if ([
|
|
129
|
+
1n,
|
|
130
|
+
15n,
|
|
131
|
+
22n,
|
|
132
|
+
29n,
|
|
133
|
+
0x7fffffffn,
|
|
134
|
+
0x7ffffffdn,
|
|
135
|
+
0x6ffffefbn,
|
|
136
|
+
0x6ffffefcn,
|
|
137
|
+
].includes(tag) ||
|
|
138
|
+
(tag === 30n && (value & 4n) !== 0n))
|
|
139
|
+
fail();
|
|
140
|
+
}
|
|
141
|
+
if (!terminated)
|
|
142
|
+
fail();
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
if (symbols)
|
|
146
|
+
fail();
|
|
147
|
+
symbols = s;
|
|
148
|
+
mapped(s);
|
|
149
|
+
if (s.entry !== 24 || s.size % 24 !== 0)
|
|
150
|
+
fail();
|
|
151
|
+
const names = section(s.link);
|
|
152
|
+
strings = names;
|
|
153
|
+
mapped(names);
|
|
154
|
+
if (names.type !== 3)
|
|
155
|
+
fail();
|
|
156
|
+
range(names.offset, names.size);
|
|
157
|
+
for (let p = s.offset; p < s.offset + s.size; p += 24) {
|
|
158
|
+
const nameOffset = bytes.readUInt32LE(p), info = bytes[p + 4], index = bytes.readUInt16LE(p + 6);
|
|
159
|
+
if (nameOffset >= names.size)
|
|
160
|
+
fail();
|
|
161
|
+
const start = names.offset + nameOffset, end = bytes.indexOf(0, start);
|
|
162
|
+
if (end < start || end >= names.offset + names.size)
|
|
163
|
+
fail();
|
|
164
|
+
const name = bytes.subarray(start, end).toString("utf8");
|
|
165
|
+
if (index === 0 && name) {
|
|
166
|
+
if (validatedApi.has(name))
|
|
167
|
+
api = true;
|
|
168
|
+
else if (!(info >> 4 === 2 && weakHooks.has(name)))
|
|
169
|
+
fail();
|
|
170
|
+
}
|
|
171
|
+
if (name === "napi_register_module_v1" &&
|
|
172
|
+
index !== 0 &&
|
|
173
|
+
(info & 15) === 2)
|
|
174
|
+
registration = true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (!dynamic || !registration || !api || !symbols || !strings)
|
|
179
|
+
return fail();
|
|
180
|
+
if (tags.get(6n) !== BigInt(symbols.address) ||
|
|
181
|
+
tags.get(11n) !== 24n ||
|
|
182
|
+
tags.get(5n) !== BigInt(strings.address) ||
|
|
183
|
+
tags.get(10n) !== BigInt(strings.size))
|
|
184
|
+
fail();
|
|
185
|
+
// The dependency-free recipe has no legitimate external filesystem prerequisite.
|
|
186
|
+
// Reject literal absolute path strings; encoded paths require another adapter.
|
|
187
|
+
for (const text of bytes.toString("latin1").split("\0")) {
|
|
188
|
+
if (/^[\x20-\x7e]{4,}$/.test(text) &&
|
|
189
|
+
/(?:^|[\s"'=])(?:\/[A-Za-z0-9_.~-]|[A-Za-z]:[\\/])/.test(text))
|
|
190
|
+
fail();
|
|
191
|
+
}
|
|
192
|
+
}
|