@synopackageland/cli 0.1.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.
Files changed (170) hide show
  1. package/README.md +13 -0
  2. package/dist/attestation/app-package.d.ts +86 -0
  3. package/dist/attestation/app-package.js +199 -0
  4. package/dist/attestation/install-on-host.d.ts +14 -0
  5. package/dist/attestation/install-on-host.js +52 -0
  6. package/dist/attestation/keyring.d.ts +21 -0
  7. package/dist/attestation/keyring.js +168 -0
  8. package/dist/bin.d.ts +2 -0
  9. package/dist/bin.js +14 -0
  10. package/dist/build-spk/dsm-arch.d.ts +4 -0
  11. package/dist/build-spk/dsm-arch.js +32 -0
  12. package/dist/build-spk/dsm-icons.d.ts +2 -0
  13. package/dist/build-spk/dsm-icons.js +67 -0
  14. package/dist/build-spk/generator.d.ts +43 -0
  15. package/dist/build-spk/generator.js +653 -0
  16. package/dist/build-spk/native-payload.d.ts +22 -0
  17. package/dist/build-spk/native-payload.js +654 -0
  18. package/dist/build-spk/reproducible-archive.d.ts +8 -0
  19. package/dist/build-spk/reproducible-archive.js +34 -0
  20. package/dist/cli.d.ts +1 -0
  21. package/dist/cli.js +345 -0
  22. package/dist/compose/analyze.d.ts +25 -0
  23. package/dist/compose/analyze.js +346 -0
  24. package/dist/compose/env.d.ts +7 -0
  25. package/dist/compose/env.js +36 -0
  26. package/dist/compose/privilege.d.ts +14 -0
  27. package/dist/compose/privilege.js +102 -0
  28. package/dist/compose/runner.d.ts +32 -0
  29. package/dist/compose/runner.js +84 -0
  30. package/dist/contract/load.d.ts +25 -0
  31. package/dist/contract/load.js +87 -0
  32. package/dist/contract/schema-validate.d.ts +3 -0
  33. package/dist/contract/schema-validate.js +50 -0
  34. package/dist/contract/validate.d.ts +3 -0
  35. package/dist/contract/validate.js +80 -0
  36. package/dist/contract/web-launch.d.ts +19 -0
  37. package/dist/contract/web-launch.js +94 -0
  38. package/dist/deploy/catalog-publish.d.ts +3 -0
  39. package/dist/deploy/catalog-publish.js +7 -0
  40. package/dist/deploy/deploy.d.ts +22 -0
  41. package/dist/deploy/deploy.js +136 -0
  42. package/dist/deploy/identity-shared.d.ts +3 -0
  43. package/dist/deploy/identity-shared.js +14 -0
  44. package/dist/deploy/identity.d.ts +7 -0
  45. package/dist/deploy/identity.js +20 -0
  46. package/dist/deploy/package-source.d.ts +7 -0
  47. package/dist/deploy/package-source.js +17 -0
  48. package/dist/deploy/run-as-values.d.ts +18 -0
  49. package/dist/deploy/run-as-values.js +48 -0
  50. package/dist/deploy/share-values.d.ts +20 -0
  51. package/dist/deploy/share-values.js +60 -0
  52. package/dist/deploy/spk-info.d.ts +12 -0
  53. package/dist/deploy/spk-info.js +41 -0
  54. package/dist/deploy/spk-version.d.ts +4 -0
  55. package/dist/deploy/spk-version.js +64 -0
  56. package/dist/dev.d.ts +13 -0
  57. package/dist/dev.js +31 -0
  58. package/dist/discovery.d.ts +8 -0
  59. package/dist/discovery.js +22 -0
  60. package/dist/errors.d.ts +6 -0
  61. package/dist/errors.js +18 -0
  62. package/dist/host/broker-paths.d.ts +1 -0
  63. package/dist/host/broker-paths.js +1 -0
  64. package/dist/host/catalog-install.d.ts +59 -0
  65. package/dist/host/catalog-install.js +326 -0
  66. package/dist/host/credentials.d.ts +2 -0
  67. package/dist/host/credentials.js +37 -0
  68. package/dist/host/digests.d.ts +5 -0
  69. package/dist/host/digests.js +55 -0
  70. package/dist/host/dsm-client.d.ts +6 -0
  71. package/dist/host/dsm-client.js +290 -0
  72. package/dist/host/dsm-http.d.ts +1 -0
  73. package/dist/host/dsm-http.js +87 -0
  74. package/dist/host/fake-nas.d.ts +16 -0
  75. package/dist/host/fake-nas.js +116 -0
  76. package/dist/host/open-url.d.ts +5 -0
  77. package/dist/host/open-url.js +13 -0
  78. package/dist/host/share-lookup.d.ts +3 -0
  79. package/dist/host/share-lookup.js +144 -0
  80. package/dist/host/types.d.ts +67 -0
  81. package/dist/host/types.js +1 -0
  82. package/dist/index.d.ts +19 -0
  83. package/dist/index.js +14 -0
  84. package/dist/info.d.ts +17 -0
  85. package/dist/info.js +34 -0
  86. package/dist/init.d.ts +4 -0
  87. package/dist/init.js +31 -0
  88. package/dist/inspect.d.ts +13 -0
  89. package/dist/inspect.js +102 -0
  90. package/dist/load-env.d.ts +1 -0
  91. package/dist/load-env.js +31 -0
  92. package/dist/log.d.ts +13 -0
  93. package/dist/log.js +20 -0
  94. package/dist/mock-broker/bootstrap.d.ts +2 -0
  95. package/dist/mock-broker/bootstrap.js +8 -0
  96. package/dist/mock-broker/files.d.ts +60 -0
  97. package/dist/mock-broker/files.js +408 -0
  98. package/dist/mock-broker/oidc.d.ts +31 -0
  99. package/dist/mock-broker/oidc.js +161 -0
  100. package/dist/mock-broker/path.d.ts +9 -0
  101. package/dist/mock-broker/path.js +50 -0
  102. package/dist/mock-broker/server.d.ts +19 -0
  103. package/dist/mock-broker/server.js +350 -0
  104. package/dist/publish/build-ledger.d.ts +22 -0
  105. package/dist/publish/build-ledger.js +26 -0
  106. package/dist/publish/community-path.d.ts +6 -0
  107. package/dist/publish/community-path.js +41 -0
  108. package/dist/publish/curated-release.d.ts +78 -0
  109. package/dist/publish/curated-release.js +693 -0
  110. package/dist/publish/file-build-ledger.d.ts +18 -0
  111. package/dist/publish/file-build-ledger.js +268 -0
  112. package/dist/publish/github-pr.d.ts +25 -0
  113. package/dist/publish/github-pr.js +85 -0
  114. package/dist/publish/github-repo.d.ts +8 -0
  115. package/dist/publish/github-repo.js +21 -0
  116. package/dist/publish/publish.d.ts +23 -0
  117. package/dist/publish/publish.js +128 -0
  118. package/dist/publish/sync-definition.d.ts +4 -0
  119. package/dist/publish/sync-definition.js +48 -0
  120. package/dist/skill.d.ts +10 -0
  121. package/dist/skill.js +50 -0
  122. package/dist/templates/compose-import/compose.yaml +11 -0
  123. package/dist/templates/compose-import/synology-app.yaml +15 -0
  124. package/dist/templates/hello-files/com.synology.hello.files.dev.spk +0 -0
  125. package/dist/templates/hello-files/compose.yaml +8 -0
  126. package/dist/templates/hello-files/synology-app.yaml +13 -0
  127. package/dist/templates/hello-web/compose.yaml +7 -0
  128. package/dist/templates/hello-web/html/app.js +23 -0
  129. package/dist/templates/hello-web/html/index.html +15 -0
  130. package/dist/templates/hello-web/html/sdk.js +68 -0
  131. package/dist/templates/hello-web/synology-app.yaml +13 -0
  132. package/dist/templates/hello-web-page/com.synology.hello.web.page.dev.spk +0 -0
  133. package/dist/templates/hello-web-page/compose.yaml +7 -0
  134. package/dist/templates/hello-web-page/html/app.js +23 -0
  135. package/dist/templates/hello-web-page/html/index.html +15 -0
  136. package/dist/templates/hello-web-page/html/sdk.js +68 -0
  137. package/dist/templates/hello-web-page/synology-app.yaml +13 -0
  138. package/dist/test-command.d.ts +13 -0
  139. package/dist/test-command.js +26 -0
  140. package/dist/test-host.d.ts +13 -0
  141. package/dist/test-host.js +24 -0
  142. package/dist/types.d.ts +59 -0
  143. package/dist/types.js +1 -0
  144. package/dist/validate.d.ts +8 -0
  145. package/dist/validate.js +103 -0
  146. package/package.json +38 -0
  147. package/schema/synology-app.schema.json +137 -0
  148. package/skills/SKILL.md +51 -0
  149. package/skills/reference/create.md +34 -0
  150. package/skills/reference/deploy.md +41 -0
  151. package/skills/reference/diagnose.md +42 -0
  152. package/skills/reference/files.md +37 -0
  153. package/skills/reference/identity.md +31 -0
  154. package/skills/reference/wrap.md +35 -0
  155. package/templates/compose-import/compose.yaml +11 -0
  156. package/templates/compose-import/synology-app.yaml +15 -0
  157. package/templates/hello-files/com.synology.hello.files.dev.spk +0 -0
  158. package/templates/hello-files/compose.yaml +8 -0
  159. package/templates/hello-files/synology-app.yaml +13 -0
  160. package/templates/hello-web/compose.yaml +7 -0
  161. package/templates/hello-web/html/app.js +23 -0
  162. package/templates/hello-web/html/index.html +15 -0
  163. package/templates/hello-web/html/sdk.js +68 -0
  164. package/templates/hello-web/synology-app.yaml +13 -0
  165. package/templates/hello-web-page/com.synology.hello.web.page.dev.spk +0 -0
  166. package/templates/hello-web-page/compose.yaml +7 -0
  167. package/templates/hello-web-page/html/app.js +23 -0
  168. package/templates/hello-web-page/html/index.html +15 -0
  169. package/templates/hello-web-page/html/sdk.js +68 -0
  170. package/templates/hello-web-page/synology-app.yaml +13 -0
@@ -0,0 +1,654 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { chmodSync, closeSync, copyFileSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readlinkSync, readdirSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync, } from "node:fs";
4
+ import { dirname, join, posix, resolve } from "node:path";
5
+ import { inflateRawSync } from "node:zlib";
6
+ import * as tar from "tar";
7
+ import { collectArchiveEntries, createReproducibleTar } from "./reproducible-archive.js";
8
+ class NativePayloadError extends Error {
9
+ code;
10
+ constructor(code, message) {
11
+ super(`${code}: ${message}`);
12
+ this.name = "NativePayloadError";
13
+ this.code = code;
14
+ }
15
+ }
16
+ export function archiveKindFor(nameOrUrl) {
17
+ const pathname = nameOrUrlForSuffix(nameOrUrl).toLowerCase();
18
+ if (pathname.endsWith(".tar.gz")) {
19
+ return "tar.gz";
20
+ }
21
+ if (pathname.endsWith(".tar.xz")) {
22
+ return "tar.xz";
23
+ }
24
+ if (pathname.endsWith(".zip")) {
25
+ return "zip";
26
+ }
27
+ return "raw";
28
+ }
29
+ export async function buildNativePayload(options) {
30
+ const stripComponents = readStripComponents(options.source.strip_components);
31
+ const outputPath = resolve(options.outputTgz);
32
+ const outputParent = dirname(outputPath);
33
+ mkdirSync(outputParent, { recursive: true });
34
+ rmSync(outputPath, { force: true });
35
+ const workDir = mkdtempSync(join(outputParent, ".syno-native-payload-"));
36
+ let committed = false;
37
+ try {
38
+ const sourceName = options.source.path ?? options.source.url;
39
+ if (sourceName === undefined) {
40
+ throw new NativePayloadError("NATIVE_PAYLOAD_SOURCE_REQUIRED", "source.path or source.url is required");
41
+ }
42
+ const kind = archiveKindFor(sourceName);
43
+ const sourceBytes = await readSourceBytes(options);
44
+ const sourceSha256 = digestBytes(sourceBytes);
45
+ if (options.source.sha256 !== undefined &&
46
+ options.source.sha256.trim().toLowerCase() !== sourceSha256) {
47
+ throw new NativePayloadError("NATIVE_PAYLOAD_SHA256_MISMATCH", `expected ${options.source.sha256}, got ${sourceSha256}`);
48
+ }
49
+ const sourcePath = join(workDir, "source.archive");
50
+ writeFileSync(sourcePath, sourceBytes, { mode: 0o600 });
51
+ // The payload root IS SYNOPKG_PKGDEST (/var/packages/<pkg>/target): DSM
52
+ // extracts package.tgz straight into it. Nesting a literal `target/`
53
+ // directory here would put the binary at $SYNOPKG_PKGDEST/target/<exec>.
54
+ // Same layout the Broker SPK ships (packages/broker/src/spk/build.ts).
55
+ const targetDir = join(workDir, "package");
56
+ mkdirSync(targetDir, { recursive: true, mode: 0o755 });
57
+ chmodSync(targetDir, 0o755);
58
+ if (kind === "raw") {
59
+ materializeRaw(sourceBytes, targetDir, rawSourceName(sourceName));
60
+ }
61
+ else if (kind === "zip") {
62
+ const rawDir = join(workDir, "zip-tree");
63
+ mkdirSync(rawDir, { mode: 0o755 });
64
+ const entries = parseZipEntries(sourceBytes, stripComponents);
65
+ materializeZipEntries(entries, rawDir);
66
+ copyStrippedTree(rawDir, targetDir, stripComponents);
67
+ }
68
+ else {
69
+ const tarPath = kind === "tar.xz" ? decompressXz(sourcePath, join(workDir, "source.tar")) : sourcePath;
70
+ const descriptors = await inspectTarEntries(tarPath, stripComponents);
71
+ const rawDir = join(workDir, "tar-tree");
72
+ mkdirSync(rawDir, { mode: 0o755 });
73
+ await tar.x({
74
+ file: tarPath,
75
+ cwd: rawDir,
76
+ strict: true,
77
+ preservePaths: false,
78
+ unlink: true,
79
+ preserveOwner: false,
80
+ });
81
+ validateExtractedTarTree(rawDir, descriptors, stripComponents);
82
+ copyStrippedTree(rawDir, targetDir, stripComponents);
83
+ }
84
+ const entries = collectArchiveEntries(targetDir);
85
+ const temporaryOutput = join(workDir, "package.tgz");
86
+ await createReproducibleTar(targetDir, temporaryOutput, { gzip: true });
87
+ const packageSha256 = digestFile(temporaryOutput);
88
+ renameSync(temporaryOutput, outputPath);
89
+ committed = true;
90
+ return {
91
+ outputTgz: options.outputTgz,
92
+ sha256: packageSha256,
93
+ sourceSha256,
94
+ entries,
95
+ };
96
+ }
97
+ finally {
98
+ rmSync(workDir, { recursive: true, force: true });
99
+ if (!committed) {
100
+ rmSync(outputPath, { force: true });
101
+ }
102
+ }
103
+ }
104
+ async function readSourceBytes(options) {
105
+ if (options.source.path !== undefined && options.source.url !== undefined) {
106
+ throw new NativePayloadError("NATIVE_PAYLOAD_SOURCE_AMBIGUOUS", "source.path and source.url cannot both be set");
107
+ }
108
+ if (options.source.path !== undefined) {
109
+ return readFileSync(resolve(options.projectRoot, options.source.path));
110
+ }
111
+ if (options.source.url === undefined) {
112
+ throw new NativePayloadError("NATIVE_PAYLOAD_SOURCE_REQUIRED", "source.path or source.url is required");
113
+ }
114
+ const download = options.download ?? defaultDownload;
115
+ const bytes = await download(options.source.url);
116
+ if (!Buffer.isBuffer(bytes)) {
117
+ throw new NativePayloadError("NATIVE_PAYLOAD_DOWNLOAD_INVALID", "download must return a Buffer");
118
+ }
119
+ return bytes;
120
+ }
121
+ async function defaultDownload(url) {
122
+ const response = await fetch(url);
123
+ if (!response.ok) {
124
+ throw new NativePayloadError("NATIVE_PAYLOAD_DOWNLOAD_FAILED", `HTTP ${response.status} while downloading ${url}`);
125
+ }
126
+ return Buffer.from(await response.arrayBuffer());
127
+ }
128
+ function readStripComponents(value) {
129
+ const stripComponents = value ?? 0;
130
+ if (!Number.isInteger(stripComponents) || stripComponents < 0) {
131
+ throw new NativePayloadError("NATIVE_PAYLOAD_INVALID_STRIP_COMPONENTS", "strip_components must be a non-negative integer");
132
+ }
133
+ return stripComponents;
134
+ }
135
+ function nameOrUrlForSuffix(nameOrUrl) {
136
+ try {
137
+ const parsed = new URL(nameOrUrl);
138
+ return decodeURIComponent(parsed.pathname);
139
+ }
140
+ catch {
141
+ return nameOrUrl.split(/[?#]/u, 1)[0];
142
+ }
143
+ }
144
+ function rawSourceName(nameOrUrl) {
145
+ const candidate = posix.basename(nameOrUrlForSuffix(nameOrUrl));
146
+ if (candidate === "" || candidate === "." || candidate === ".." || candidate.includes("\0")) {
147
+ return "native-binary";
148
+ }
149
+ return candidate;
150
+ }
151
+ function digestBytes(bytes) {
152
+ return createHash("sha256").update(bytes).digest("hex");
153
+ }
154
+ function digestFile(path) {
155
+ return digestBytes(readFileSync(path));
156
+ }
157
+ function normalizeArchivePath(pathValue) {
158
+ if (pathValue.length === 0) {
159
+ return "";
160
+ }
161
+ if (pathValue.includes("\0") ||
162
+ pathValue.startsWith("/") ||
163
+ pathValue.startsWith("\\") ||
164
+ /^[A-Za-z]:[\\/]/u.test(pathValue)) {
165
+ throw new NativePayloadError("NATIVE_PAYLOAD_UNSAFE_PATH", `archive entry is absolute: ${pathValue}`);
166
+ }
167
+ const normalized = posix.normalize(pathValue).replace(/^\.\//u, "");
168
+ if (normalized === "." || normalized === "") {
169
+ return "";
170
+ }
171
+ if (normalized === ".." || normalized.startsWith("../")) {
172
+ throw new NativePayloadError("NATIVE_PAYLOAD_UNSAFE_PATH", `archive entry escapes its root: ${pathValue}`);
173
+ }
174
+ return normalized;
175
+ }
176
+ function stripArchivePath(pathValue, stripComponents) {
177
+ const normalized = normalizeArchivePath(pathValue);
178
+ if (normalized === "") {
179
+ return undefined;
180
+ }
181
+ const parts = normalized.split("/");
182
+ if (parts.length <= stripComponents) {
183
+ return undefined;
184
+ }
185
+ return parts.slice(stripComponents).join("/");
186
+ }
187
+ async function inspectTarEntries(archivePath, stripComponents) {
188
+ const descriptors = [];
189
+ let validationError;
190
+ await tar.t({
191
+ file: archivePath,
192
+ strict: true,
193
+ onReadEntry: (entry) => {
194
+ if (validationError !== undefined) {
195
+ return;
196
+ }
197
+ try {
198
+ const rawPath = normalizeArchivePath(entry.path);
199
+ const outputPath = stripArchivePath(rawPath, stripComponents);
200
+ if (outputPath === undefined) {
201
+ return;
202
+ }
203
+ descriptors.push({
204
+ rawPath,
205
+ outputPath,
206
+ kind: tarEntryKind(entry.type),
207
+ mode: entry.mode ?? 0o644,
208
+ linkpath: entry.linkpath,
209
+ });
210
+ }
211
+ catch (error) {
212
+ validationError = error;
213
+ }
214
+ },
215
+ });
216
+ if (validationError !== undefined) {
217
+ throw validationError;
218
+ }
219
+ validateArchiveDescriptors(descriptors, stripComponents);
220
+ return descriptors;
221
+ }
222
+ function tarEntryKind(type) {
223
+ if (type === "File" || type === "OldFile") {
224
+ return "file";
225
+ }
226
+ if (type === "Directory") {
227
+ return "directory";
228
+ }
229
+ if (type === "SymbolicLink") {
230
+ return "symlink";
231
+ }
232
+ if (type === "Link") {
233
+ return "hardlink";
234
+ }
235
+ throw new NativePayloadError("NATIVE_PAYLOAD_UNSUPPORTED_ENTRY", `unsupported tar entry type: ${type}`);
236
+ }
237
+ function validateArchiveDescriptors(descriptors, stripComponents) {
238
+ const byOutputPath = new Map();
239
+ const symlinks = new Map();
240
+ for (const descriptor of descriptors) {
241
+ const outputPath = descriptor.outputPath;
242
+ if (outputPath === undefined) {
243
+ continue;
244
+ }
245
+ const previous = byOutputPath.get(outputPath);
246
+ if (previous !== undefined) {
247
+ if (previous.kind === "directory" && descriptor.kind === "directory") {
248
+ continue;
249
+ }
250
+ throw new NativePayloadError("NATIVE_PAYLOAD_DUPLICATE_ENTRY", `multiple archive entries map to ${outputPath}`);
251
+ }
252
+ byOutputPath.set(outputPath, descriptor);
253
+ if (descriptor.kind === "symlink") {
254
+ if (descriptor.linkpath === undefined) {
255
+ throw new NativePayloadError("NATIVE_PAYLOAD_INVALID_SYMLINK", `symlink ${descriptor.rawPath} has no target`);
256
+ }
257
+ symlinks.set(outputPath, descriptor.linkpath);
258
+ }
259
+ }
260
+ for (const descriptor of descriptors) {
261
+ const outputPath = descriptor.outputPath;
262
+ if (outputPath === undefined) {
263
+ continue;
264
+ }
265
+ assertOutputParentsAreRealEntries(outputPath, byOutputPath);
266
+ if (descriptor.kind === "symlink") {
267
+ resolveSymlinkTarget(outputPath, symlinks, new Set());
268
+ }
269
+ else if (descriptor.kind === "hardlink") {
270
+ const linkpath = descriptor.linkpath;
271
+ if (linkpath === undefined) {
272
+ throw new NativePayloadError("NATIVE_PAYLOAD_INVALID_HARDLINK", `hardlink ${descriptor.rawPath} has no target`);
273
+ }
274
+ const targetRawPath = normalizeArchivePath(linkpath);
275
+ const targetOutputPath = stripArchivePath(targetRawPath, stripComponents);
276
+ if (targetOutputPath === undefined) {
277
+ throw new NativePayloadError("NATIVE_PAYLOAD_UNSAFE_HARDLINK", `hardlink ${descriptor.rawPath} targets a stripped entry`);
278
+ }
279
+ if (targetOutputPath === outputPath) {
280
+ throw new NativePayloadError("NATIVE_PAYLOAD_INVALID_HARDLINK", `hardlink ${descriptor.rawPath} targets itself`);
281
+ }
282
+ assertNoSymlinkPathComponent(targetOutputPath, symlinks);
283
+ }
284
+ }
285
+ }
286
+ function assertOutputParentsAreRealEntries(outputPath, byOutputPath) {
287
+ const parts = outputPath.split("/");
288
+ for (let index = 1; index < parts.length; index += 1) {
289
+ const parent = parts.slice(0, index).join("/");
290
+ const entry = byOutputPath.get(parent);
291
+ if (entry !== undefined && entry.kind !== "directory") {
292
+ throw new NativePayloadError("NATIVE_PAYLOAD_UNSAFE_PATH", `archive entry ${outputPath} traverses ${entry.kind} ${parent}`);
293
+ }
294
+ }
295
+ }
296
+ function resolveSymlinkTarget(pathValue, symlinks, visiting) {
297
+ const linkpath = symlinks.get(pathValue);
298
+ if (linkpath === undefined) {
299
+ return pathValue;
300
+ }
301
+ if (linkpath.length === 0 || posix.isAbsolute(linkpath) || /^[A-Za-z]:[\\/]/u.test(linkpath)) {
302
+ throw new NativePayloadError("NATIVE_PAYLOAD_UNSAFE_SYMLINK", `symlink ${pathValue} points outside target: ${linkpath}`);
303
+ }
304
+ if (visiting.has(pathValue)) {
305
+ throw new NativePayloadError("NATIVE_PAYLOAD_SYMLINK_CYCLE", `symlink cycle includes ${pathValue}`);
306
+ }
307
+ const target = normalizePayloadTarget(posix.dirname(pathValue), linkpath, pathValue);
308
+ assertNoSymlinkPathComponent(target, symlinks);
309
+ visiting.add(pathValue);
310
+ const resolved = symlinks.has(target)
311
+ ? resolveSymlinkTarget(target, symlinks, visiting)
312
+ : target;
313
+ visiting.delete(pathValue);
314
+ return resolved;
315
+ }
316
+ function normalizePayloadTarget(parent, linkpath, sourcePath) {
317
+ const target = posix.normalize(posix.join(parent, linkpath));
318
+ if (target === ".." || target.startsWith("../") || posix.isAbsolute(target)) {
319
+ throw new NativePayloadError("NATIVE_PAYLOAD_UNSAFE_SYMLINK", `symlink ${sourcePath} points outside target: ${linkpath}`);
320
+ }
321
+ return target === "." ? "" : target;
322
+ }
323
+ function assertNoSymlinkPathComponent(pathValue, symlinks) {
324
+ const parts = pathValue === "" ? [] : pathValue.split("/");
325
+ for (let index = 1; index < parts.length; index += 1) {
326
+ const parent = parts.slice(0, index).join("/");
327
+ if (symlinks.has(parent)) {
328
+ throw new NativePayloadError("NATIVE_PAYLOAD_UNSAFE_SYMLINK", `path ${pathValue} traverses symlink ${parent}`);
329
+ }
330
+ }
331
+ }
332
+ function decompressXz(inputPath, outputPath) {
333
+ let outputFd;
334
+ try {
335
+ outputFd = openSync(outputPath, "w", 0o600);
336
+ execFileSync("xz", ["--decompress", "--stdout", inputPath], {
337
+ stdio: ["ignore", outputFd, "pipe"],
338
+ });
339
+ return outputPath;
340
+ }
341
+ catch (error) {
342
+ const code = error instanceof Error && "code" in error ? error.code : undefined;
343
+ if (code === "ENOENT") {
344
+ throw new NativePayloadError("NATIVE_PAYLOAD_XZ_UNAVAILABLE", "the xz executable is required to read .tar.xz sources");
345
+ }
346
+ const detail = error instanceof Error ? error.message : String(error);
347
+ throw new NativePayloadError("NATIVE_PAYLOAD_XZ_FAILED", detail);
348
+ }
349
+ finally {
350
+ if (outputFd !== undefined) {
351
+ closeSync(outputFd);
352
+ }
353
+ }
354
+ }
355
+ function validateExtractedTarTree(rawDir, descriptors, stripComponents) {
356
+ for (const descriptor of descriptors) {
357
+ const path = safePathInside(rawDir, descriptor.rawPath);
358
+ let stats;
359
+ try {
360
+ stats = lstatSync(path);
361
+ }
362
+ catch (error) {
363
+ const detail = error instanceof Error ? error.message : String(error);
364
+ throw new NativePayloadError("NATIVE_PAYLOAD_EXTRACTION_FAILED", `cannot inspect ${descriptor.rawPath}: ${detail}`);
365
+ }
366
+ if (descriptor.kind === "symlink") {
367
+ if (!stats.isSymbolicLink()) {
368
+ throw new NativePayloadError("NATIVE_PAYLOAD_EXTRACTION_FAILED", `${descriptor.rawPath} was not extracted as a symlink`);
369
+ }
370
+ const linkpath = readlinkSync(path);
371
+ const outputPath = stripArchivePath(descriptor.rawPath, stripComponents);
372
+ if (outputPath !== undefined) {
373
+ normalizePayloadTarget(posix.dirname(outputPath), linkpath, outputPath);
374
+ }
375
+ try {
376
+ const targetStats = statSync(resolve(dirname(path), linkpath));
377
+ if (targetStats.isDirectory()) {
378
+ throw new NativePayloadError("NATIVE_PAYLOAD_UNSUPPORTED_ENTRY", `directory symlink is not supported: ${descriptor.rawPath}`);
379
+ }
380
+ }
381
+ catch (error) {
382
+ if (error instanceof NativePayloadError) {
383
+ throw error;
384
+ }
385
+ throw new NativePayloadError("NATIVE_PAYLOAD_EXTRACTION_FAILED", `symlink target is unavailable: ${descriptor.rawPath}`);
386
+ }
387
+ }
388
+ }
389
+ }
390
+ function materializeRaw(bytes, targetDir, name) {
391
+ const targetPath = safePathInside(targetDir, name);
392
+ ensureOutputParents(targetDir, name);
393
+ writeFileSync(targetPath, bytes, { mode: 0o755 });
394
+ chmodSync(targetPath, 0o755);
395
+ }
396
+ function materializeZipEntries(entries, rawDir) {
397
+ const directories = entries
398
+ .filter((entry) => entry.kind === "directory")
399
+ .sort((left, right) => pathDepth(left.rawPath) - pathDepth(right.rawPath));
400
+ for (const entry of directories) {
401
+ ensureOutputDirectory(rawDir, entry.rawPath, entry.mode);
402
+ }
403
+ for (const entry of entries) {
404
+ if (entry.kind === "directory") {
405
+ continue;
406
+ }
407
+ const outputPath = safePathInside(rawDir, entry.rawPath);
408
+ ensureOutputParents(rawDir, entry.rawPath);
409
+ if (lstatIfPresent(outputPath) !== undefined) {
410
+ throw new NativePayloadError("NATIVE_PAYLOAD_DUPLICATE_ENTRY", `multiple archive entries map to ${entry.rawPath}`);
411
+ }
412
+ if (entry.kind === "symlink") {
413
+ symlinkSync(entry.linkpath, outputPath);
414
+ }
415
+ else if (entry.kind === "file") {
416
+ writeFileSync(outputPath, entry.data ?? Buffer.alloc(0), { mode: normalizedMode(entry.mode) });
417
+ chmodSync(outputPath, normalizedMode(entry.mode));
418
+ }
419
+ else {
420
+ throw new NativePayloadError("NATIVE_PAYLOAD_UNSUPPORTED_ENTRY", `unsupported zip entry kind: ${entry.kind}`);
421
+ }
422
+ }
423
+ }
424
+ function copyStrippedTree(sourceDir, targetDir, stripComponents) {
425
+ walkTree(sourceDir, "", targetDir, stripComponents);
426
+ }
427
+ function walkTree(currentDir, relativeDir, targetDir, stripComponents) {
428
+ const children = readdirSync(currentDir, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
429
+ for (const child of children) {
430
+ const rawPath = relativeDir ? `${relativeDir}/${child.name}` : child.name;
431
+ const outputPath = stripArchivePath(rawPath, stripComponents);
432
+ const sourcePath = join(currentDir, child.name);
433
+ if (child.isDirectory()) {
434
+ if (outputPath !== undefined) {
435
+ ensureOutputDirectory(targetDir, outputPath, lstatSync(sourcePath).mode);
436
+ }
437
+ walkTree(sourcePath, rawPath, targetDir, stripComponents);
438
+ continue;
439
+ }
440
+ if (outputPath === undefined) {
441
+ continue;
442
+ }
443
+ ensureOutputParents(targetDir, outputPath);
444
+ const targetPath = safePathInside(targetDir, outputPath);
445
+ if (lstatIfPresent(targetPath) !== undefined) {
446
+ throw new NativePayloadError("NATIVE_PAYLOAD_DUPLICATE_ENTRY", `multiple archive entries map to ${outputPath}`);
447
+ }
448
+ if (child.isSymbolicLink()) {
449
+ const linkpath = readlinkSync(sourcePath);
450
+ normalizePayloadTarget(posix.dirname(outputPath), linkpath, outputPath);
451
+ let targetStats;
452
+ try {
453
+ targetStats = statSync(sourcePath);
454
+ }
455
+ catch {
456
+ throw new NativePayloadError("NATIVE_PAYLOAD_EXTRACTION_FAILED", `symlink target is unavailable: ${rawPath}`);
457
+ }
458
+ if (targetStats.isDirectory()) {
459
+ throw new NativePayloadError("NATIVE_PAYLOAD_UNSUPPORTED_ENTRY", `directory symlink is not supported: ${rawPath}`);
460
+ }
461
+ symlinkSync(linkpath, targetPath);
462
+ continue;
463
+ }
464
+ if (!child.isFile()) {
465
+ throw new NativePayloadError("NATIVE_PAYLOAD_UNSUPPORTED_ENTRY", `unsupported filesystem entry: ${rawPath}`);
466
+ }
467
+ const mode = lstatSync(sourcePath).mode;
468
+ copyFileSync(sourcePath, targetPath);
469
+ chmodSync(targetPath, normalizedMode(mode));
470
+ }
471
+ }
472
+ function normalizedMode(mode) {
473
+ return mode & 0o7777;
474
+ }
475
+ function pathDepth(pathValue) {
476
+ return pathValue === "" ? 0 : pathValue.split("/").length;
477
+ }
478
+ function safePathInside(root, relativePath) {
479
+ const rootPath = resolve(root);
480
+ const candidate = resolve(rootPath, ...relativePath.split("/"));
481
+ if (candidate !== rootPath && !candidate.startsWith(`${rootPath}/`)) {
482
+ throw new NativePayloadError("NATIVE_PAYLOAD_UNSAFE_PATH", `path escapes target: ${relativePath}`);
483
+ }
484
+ return candidate;
485
+ }
486
+ function ensureOutputParents(root, relativePath) {
487
+ const parts = relativePath.split("/");
488
+ if (parts.length <= 1) {
489
+ return;
490
+ }
491
+ ensureOutputDirectory(root, parts.slice(0, -1).join("/"), 0o755);
492
+ }
493
+ function ensureOutputDirectory(root, relativePath, mode) {
494
+ if (relativePath === "") {
495
+ return;
496
+ }
497
+ const parts = relativePath.split("/");
498
+ let current = resolve(root);
499
+ for (const part of parts) {
500
+ current = safePathInside(current, part);
501
+ const existing = lstatIfPresent(current);
502
+ if (existing !== undefined) {
503
+ if (!existing.isDirectory() || existing.isSymbolicLink()) {
504
+ throw new NativePayloadError("NATIVE_PAYLOAD_UNSAFE_PATH", `path component is not a real directory: ${part}`);
505
+ }
506
+ continue;
507
+ }
508
+ mkdirSync(current, { mode: 0o755 });
509
+ }
510
+ chmodSync(current, normalizedMode(mode) || 0o755);
511
+ }
512
+ function lstatIfPresent(path) {
513
+ try {
514
+ return lstatSync(path);
515
+ }
516
+ catch (error) {
517
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
518
+ return undefined;
519
+ }
520
+ throw error;
521
+ }
522
+ }
523
+ function parseZipEntries(bytes, stripComponents) {
524
+ const directory = findZipCentralDirectory(bytes);
525
+ const descriptors = [];
526
+ let cursor = directory.offset;
527
+ for (let index = 0; index < directory.count; index += 1) {
528
+ assertBytes(bytes, cursor, 46, "zip central directory header");
529
+ if (bytes.readUInt32LE(cursor) !== 0x02014b50) {
530
+ throw new NativePayloadError("NATIVE_PAYLOAD_INVALID_ZIP", "invalid central directory header");
531
+ }
532
+ const flags = bytes.readUInt16LE(cursor + 8);
533
+ const method = bytes.readUInt16LE(cursor + 10);
534
+ const compressedSize = bytes.readUInt32LE(cursor + 20);
535
+ const uncompressedSize = bytes.readUInt32LE(cursor + 24);
536
+ const nameLength = bytes.readUInt16LE(cursor + 28);
537
+ const extraLength = bytes.readUInt16LE(cursor + 30);
538
+ const commentLength = bytes.readUInt16LE(cursor + 32);
539
+ const externalAttributes = bytes.readUInt32LE(cursor + 38);
540
+ const localHeaderOffset = bytes.readUInt32LE(cursor + 42);
541
+ const recordLength = 46 + nameLength + extraLength + commentLength;
542
+ assertBytes(bytes, cursor, recordLength, "zip central directory record");
543
+ const nameBytes = bytes.subarray(cursor + 46, cursor + 46 + nameLength);
544
+ const archiveName = decodeZipName(nameBytes, flags);
545
+ const rawPath = normalizeArchivePath(archiveName);
546
+ if (rawPath === "") {
547
+ throw new NativePayloadError("NATIVE_PAYLOAD_INVALID_ZIP", "zip entry has an empty name");
548
+ }
549
+ const data = readZipEntryData(bytes, localHeaderOffset, compressedSize, uncompressedSize, method, flags);
550
+ const mode = (externalAttributes >>> 16) & 0xffff;
551
+ const directoryEntry = archiveName.endsWith("/") || (externalAttributes & 0x10) !== 0 || (mode & 0o170000) === 0o040000;
552
+ const symlinkEntry = !directoryEntry && (mode & 0o170000) === 0o120000;
553
+ const kind = directoryEntry
554
+ ? "directory"
555
+ : symlinkEntry
556
+ ? "symlink"
557
+ : "file";
558
+ const outputPath = stripArchivePath(rawPath, stripComponents);
559
+ const descriptor = {
560
+ rawPath,
561
+ outputPath,
562
+ kind,
563
+ mode: mode === 0 ? (directoryEntry ? 0o755 : 0o644) : mode,
564
+ data: kind === "file" ? data : undefined,
565
+ linkpath: kind === "symlink" ? data.toString("utf8") : undefined,
566
+ };
567
+ descriptors.push(descriptor);
568
+ cursor += recordLength;
569
+ }
570
+ if (cursor !== directory.endOffset) {
571
+ throw new NativePayloadError("NATIVE_PAYLOAD_INVALID_ZIP", "central directory length does not match");
572
+ }
573
+ validateArchiveDescriptors(descriptors, stripComponents);
574
+ return descriptors;
575
+ }
576
+ function findZipCentralDirectory(bytes) {
577
+ const minimumOffset = Math.max(0, bytes.length - 0xffff - 22);
578
+ for (let cursor = bytes.length - 22; cursor >= minimumOffset; cursor -= 1) {
579
+ if (cursor < 0 || bytes.readUInt32LE(cursor) !== 0x06054b50) {
580
+ continue;
581
+ }
582
+ assertBytes(bytes, cursor, 22, "zip end record");
583
+ const disk = bytes.readUInt16LE(cursor + 4);
584
+ const centralDisk = bytes.readUInt16LE(cursor + 6);
585
+ const entriesOnDisk = bytes.readUInt16LE(cursor + 8);
586
+ const entryCount = bytes.readUInt16LE(cursor + 10);
587
+ const centralSize = bytes.readUInt32LE(cursor + 12);
588
+ const centralOffset = bytes.readUInt32LE(cursor + 16);
589
+ const commentLength = bytes.readUInt16LE(cursor + 20);
590
+ if (disk !== 0 || centralDisk !== 0 || entriesOnDisk !== entryCount) {
591
+ throw new NativePayloadError("NATIVE_PAYLOAD_UNSUPPORTED_ZIP", "multi-disk zip archives are unsupported");
592
+ }
593
+ if (centralSize === 0xffffffff || centralOffset === 0xffffffff) {
594
+ throw new NativePayloadError("NATIVE_PAYLOAD_UNSUPPORTED_ZIP", "ZIP64 archives are unsupported");
595
+ }
596
+ assertBytes(bytes, cursor, 22 + commentLength, "zip end record");
597
+ assertBytes(bytes, centralOffset, centralSize, "zip central directory");
598
+ return {
599
+ offset: centralOffset,
600
+ size: centralSize,
601
+ count: entryCount,
602
+ endOffset: centralOffset + centralSize,
603
+ };
604
+ }
605
+ throw new NativePayloadError("NATIVE_PAYLOAD_INVALID_ZIP", "end of central directory was not found");
606
+ }
607
+ function decodeZipName(bytes, flags) {
608
+ if ((flags & 0x1) !== 0) {
609
+ throw new NativePayloadError("NATIVE_PAYLOAD_UNSUPPORTED_ZIP", "encrypted zip entries are unsupported");
610
+ }
611
+ return bytes.toString("utf8");
612
+ }
613
+ function readZipEntryData(bytes, localHeaderOffset, compressedSize, uncompressedSize, method, flags) {
614
+ assertBytes(bytes, localHeaderOffset, 30, "zip local header");
615
+ if (bytes.readUInt32LE(localHeaderOffset) !== 0x04034b50) {
616
+ throw new NativePayloadError("NATIVE_PAYLOAD_INVALID_ZIP", "invalid local file header");
617
+ }
618
+ const localNameLength = bytes.readUInt16LE(localHeaderOffset + 26);
619
+ const localExtraLength = bytes.readUInt16LE(localHeaderOffset + 28);
620
+ const dataOffset = localHeaderOffset + 30 + localNameLength + localExtraLength;
621
+ assertBytes(bytes, dataOffset, compressedSize, "zip entry data");
622
+ const compressed = bytes.subarray(dataOffset, dataOffset + compressedSize);
623
+ let data;
624
+ try {
625
+ if (method === 0) {
626
+ data = Buffer.from(compressed);
627
+ }
628
+ else if (method === 8) {
629
+ data = inflateRawSync(compressed);
630
+ }
631
+ else {
632
+ throw new NativePayloadError("NATIVE_PAYLOAD_UNSUPPORTED_ZIP", `compression method ${method} is unsupported`);
633
+ }
634
+ }
635
+ catch (error) {
636
+ if (error instanceof NativePayloadError) {
637
+ throw error;
638
+ }
639
+ const detail = error instanceof Error ? error.message : String(error);
640
+ throw new NativePayloadError("NATIVE_PAYLOAD_INVALID_ZIP", `cannot inflate entry: ${detail}`);
641
+ }
642
+ if (data.length !== uncompressedSize) {
643
+ throw new NativePayloadError("NATIVE_PAYLOAD_INVALID_ZIP", "zip entry size does not match its header");
644
+ }
645
+ if ((flags & 0x1) !== 0) {
646
+ throw new NativePayloadError("NATIVE_PAYLOAD_UNSUPPORTED_ZIP", "encrypted zip entries are unsupported");
647
+ }
648
+ return data;
649
+ }
650
+ function assertBytes(bytes, offset, length, what) {
651
+ if (offset < 0 || length < 0 || offset > bytes.length - length) {
652
+ throw new NativePayloadError("NATIVE_PAYLOAD_INVALID_ZIP", `${what} is truncated`);
653
+ }
654
+ }
@@ -0,0 +1,8 @@
1
+ /** Every archive this CLI writes uses the same timestamp so builds are byte-identical. */
2
+ export declare const FIXED_MTIME: Date;
3
+ /** Relative paths of every file under `root`, depth-first and sorted for a stable archive order. */
4
+ export declare function collectArchiveEntries(root: string): string[];
5
+ /** Writes `sourceDir` to `outputPath` as a tar whose bytes depend only on the file contents. */
6
+ export declare function createReproducibleTar(sourceDir: string, outputPath: string, options?: {
7
+ gzip?: boolean;
8
+ }): Promise<void>;