@synopackageland/cli 0.1.0 → 0.2.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/dist/bin.js +0 -0
- package/dist/build-spk/broker-glue.d.ts +16 -0
- package/dist/build-spk/broker-glue.js +40 -0
- package/dist/build-spk/delete-app-data.d.ts +11 -0
- package/dist/build-spk/delete-app-data.js +56 -0
- package/dist/build-spk/generator.js +123 -70
- package/dist/build-spk/native-lifecycle.d.ts +51 -0
- package/dist/build-spk/native-lifecycle.js +205 -0
- package/dist/build-spk/native-payload.d.ts +15 -0
- package/dist/build-spk/native-payload.js +65 -39
- package/dist/cli.js +26 -3
- package/dist/deploy/deploy.d.ts +3 -0
- package/dist/deploy/deploy.js +13 -0
- package/dist/discovery.d.ts +2 -0
- package/dist/discovery.js +2 -0
- package/dist/host/digests.js +9 -1
- package/dist/host/dsm-client.d.ts +7 -0
- package/dist/host/dsm-client.js +25 -0
- package/dist/host/types.d.ts +6 -0
- package/dist/init.js +1 -0
- package/dist/native/load.d.ts +8 -0
- package/dist/native/load.js +11 -0
- package/dist/native/types.d.ts +28 -0
- package/dist/native/types.js +3 -0
- package/dist/native/validate.d.ts +6 -0
- package/dist/native/validate.js +144 -0
- package/dist/publish/curated-release.js +277 -91
- package/dist/templates/native/native.yaml +16 -0
- package/dist/templates/native/synology-app.yaml +7 -0
- package/dist/types.d.ts +1 -1
- package/dist/validate.js +37 -4
- package/package.json +8 -8
- package/skills/reference/diagnose.md +15 -1
- package/templates/native/native.yaml +16 -0
- package/templates/native/synology-app.yaml +7 -0
- package/dist/deploy/package-source.d.ts +0 -7
- package/dist/deploy/package-source.js +0 -17
- package/dist/deploy/spk-info.d.ts +0 -12
- package/dist/deploy/spk-info.js +0 -41
- package/dist/templates/hello-files/com.synology.hello.files.dev.spk +0 -0
- package/dist/templates/hello-web-page/com.synology.hello.web.page.dev.spk +0 -0
- package/templates/hello-files/com.synology.hello.files.dev.spk +0 -0
- package/templates/hello-web-page/com.synology.hello.web.page.dev.spk +0 -0
|
@@ -10,6 +10,8 @@ import { digestAppPackageClaims, digestCuratedReleaseClaims, normalizeCapabiliti
|
|
|
10
10
|
import { CuratedTrustRootError, loadCuratedTrustRoot, resolveCuratedSigningTrust, } from "../attestation/keyring.js";
|
|
11
11
|
import { loadContract } from "../contract/load.js";
|
|
12
12
|
import { loadCompose } from "../compose/analyze.js";
|
|
13
|
+
import { loadNative } from "../native/load.js";
|
|
14
|
+
import { infoArchValue, isNativeArchFamily } from "../build-spk/dsm-arch.js";
|
|
13
15
|
import { EMBEDDED_ATTESTATION_RELATIVE_PATH, buildSpk, extractSpk, } from "../build-spk/generator.js";
|
|
14
16
|
import { findProjectRoot } from "../discovery.js";
|
|
15
17
|
import { validateProject } from "../validate.js";
|
|
@@ -112,95 +114,172 @@ export async function releaseCurated(options) {
|
|
|
112
114
|
const workspace = mkdtempSync(join(tmpdir(), "synopkgland-curated-release-"));
|
|
113
115
|
try {
|
|
114
116
|
const releaseProject = copyReleaseProject(options.project, workspace, packageIdentity);
|
|
115
|
-
const outputPath = join(workspace, `${packageIdentity}.spk`);
|
|
116
117
|
const buildFn = options.buildSpk ?? buildSpk;
|
|
117
|
-
const built = await buildFn({
|
|
118
|
-
project: releaseProject,
|
|
119
|
-
outputPath,
|
|
120
|
-
packageIdentity,
|
|
121
|
-
displayname: prepared.displayname,
|
|
122
|
-
spkVersion: prepared.version,
|
|
123
|
-
embeddedAttestationToken: embedded.token,
|
|
124
|
-
});
|
|
125
|
-
if (!built.ok || !built.outputPath) {
|
|
126
|
-
throw new CuratedReleaseError("CURATED_BUILD_FAILED");
|
|
127
|
-
}
|
|
128
|
-
await assertEmbeddedAttestationInSpk(built.outputPath, embedded.token, embeddedInput, options.signer);
|
|
129
|
-
const artifact = digestArtifact(built.outputPath);
|
|
130
118
|
if (!options.signer.signRelease) {
|
|
131
119
|
throw new CuratedReleaseError("CURATED_RELEASE_SIGNER_MISSING");
|
|
132
120
|
}
|
|
133
|
-
const detachedInput = {
|
|
134
|
-
packageIdentity,
|
|
135
|
-
packageVersion: prepared.version,
|
|
136
|
-
packageBuild: options.build,
|
|
137
|
-
artifactDigest: artifact.sha256,
|
|
138
|
-
embeddedAttestationDigest: embedded.attestationDigest,
|
|
139
|
-
};
|
|
140
|
-
let detached;
|
|
141
|
-
try {
|
|
142
|
-
detached = await options.signer.signRelease(detachedInput);
|
|
143
|
-
}
|
|
144
|
-
catch (error) {
|
|
145
|
-
if (error instanceof CuratedReleaseError) {
|
|
146
|
-
throw error;
|
|
147
|
-
}
|
|
148
|
-
throw new CuratedReleaseError("CURATED_RELEASE_ATTESTATION_BINDING_FAILED");
|
|
149
|
-
}
|
|
150
|
-
assertDetachedAttestation(detached, detachedInput);
|
|
151
|
-
await verifySignedToken(options.signer, detached.token, "release");
|
|
152
|
-
const artifactKey = buildImmutableSpkKey({
|
|
153
|
-
package: packageIdentity,
|
|
154
|
-
version: prepared.version,
|
|
155
|
-
sha256: artifact.sha256,
|
|
156
|
-
});
|
|
157
|
-
const attestationKey = buildImmutableAttestationKey({
|
|
158
|
-
package: packageIdentity,
|
|
159
|
-
version: prepared.version,
|
|
160
|
-
artifactDigest: artifact.sha256,
|
|
161
|
-
});
|
|
162
|
-
const tokenSha256 = digestBytes(Buffer.from(detached.token, "utf8"));
|
|
163
121
|
if (!options.signer.verificationJwk) {
|
|
164
122
|
throw new CuratedReleaseError("CURATED_ATTESTATION_SIGNATURE_INVALID");
|
|
165
123
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
124
|
+
if (!prepared.isNative) {
|
|
125
|
+
// Compose single-artifact release
|
|
126
|
+
const outputPath = join(workspace, `${packageIdentity}.spk`);
|
|
127
|
+
const built = await buildFn({
|
|
128
|
+
project: releaseProject,
|
|
129
|
+
outputPath,
|
|
130
|
+
packageIdentity,
|
|
131
|
+
displayname: prepared.displayname,
|
|
132
|
+
spkVersion: prepared.version,
|
|
133
|
+
embeddedAttestationToken: embedded.token,
|
|
134
|
+
});
|
|
135
|
+
if (!built.ok || !built.outputPath) {
|
|
136
|
+
throw new CuratedReleaseError("CURATED_BUILD_FAILED");
|
|
137
|
+
}
|
|
138
|
+
await assertEmbeddedAttestationInSpk(built.outputPath, embedded.token, embeddedInput, options.signer);
|
|
139
|
+
const artifact = digestArtifact(built.outputPath);
|
|
140
|
+
const detachedInput = {
|
|
141
|
+
packageIdentity,
|
|
142
|
+
packageVersion: prepared.version,
|
|
143
|
+
packageBuild: options.build,
|
|
144
|
+
artifactDigest: artifact.sha256,
|
|
145
|
+
embeddedAttestationDigest: embedded.attestationDigest,
|
|
146
|
+
};
|
|
147
|
+
let detached;
|
|
148
|
+
try {
|
|
149
|
+
detached = await options.signer.signRelease(detachedInput);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (error instanceof CuratedReleaseError) {
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
throw new CuratedReleaseError("CURATED_RELEASE_ATTESTATION_BINDING_FAILED");
|
|
156
|
+
}
|
|
157
|
+
assertDetachedAttestation(detached, detachedInput);
|
|
158
|
+
await verifySignedToken(options.signer, detached.token, "release");
|
|
159
|
+
const artifactKey = buildImmutableSpkKey({
|
|
160
|
+
package: packageIdentity,
|
|
161
|
+
version: prepared.version,
|
|
189
162
|
sha256: artifact.sha256,
|
|
190
|
-
}
|
|
191
|
-
|
|
163
|
+
});
|
|
164
|
+
const attestationKey = buildImmutableAttestationKey({
|
|
165
|
+
package: packageIdentity,
|
|
166
|
+
version: prepared.version,
|
|
167
|
+
artifactDigest: artifact.sha256,
|
|
168
|
+
});
|
|
169
|
+
const tokenSha256 = digestBytes(Buffer.from(detached.token, "utf8"));
|
|
170
|
+
const attestation = {
|
|
171
|
+
packageId: packageIdentity,
|
|
172
|
+
version: prepared.version,
|
|
173
|
+
packageBuild: options.build,
|
|
174
|
+
artifactDigest: artifact.sha256,
|
|
175
|
+
embeddedAttestationDigest: embedded.attestationDigest,
|
|
192
176
|
key: attestationKey,
|
|
177
|
+
token: detached.token,
|
|
193
178
|
digest: detached.attestationDigest,
|
|
179
|
+
tokenSha256,
|
|
180
|
+
verificationJwk: options.signer.verificationJwk,
|
|
181
|
+
};
|
|
182
|
+
const index = {
|
|
183
|
+
packageId: packageIdentity,
|
|
184
|
+
version: prepared.version,
|
|
185
|
+
displayname: prepared.displayname,
|
|
186
|
+
description: prepared.description,
|
|
187
|
+
...(prepared.maintainer ? { maintainer: prepared.maintainer } : {}),
|
|
188
|
+
osMinVer: prepared.osMinVer,
|
|
189
|
+
artifact: {
|
|
190
|
+
key: artifactKey,
|
|
191
|
+
size: artifact.size,
|
|
192
|
+
md5: artifact.md5,
|
|
193
|
+
sha256: artifact.sha256,
|
|
194
|
+
},
|
|
195
|
+
attestation: {
|
|
196
|
+
key: attestationKey,
|
|
197
|
+
digest: detached.attestationDigest,
|
|
198
|
+
packageBuild: options.build,
|
|
199
|
+
artifactDigest: artifact.sha256,
|
|
200
|
+
embeddedAttestationDigest: embedded.attestationDigest,
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
// This is the only transition to external publication. Both tokens have
|
|
204
|
+
// been claim-checked, signature-checked when the signer supplies a key,
|
|
205
|
+
// and the embedded token was read back from the final SPK first.
|
|
206
|
+
await publishCuratedRelease({
|
|
207
|
+
artifact: {
|
|
208
|
+
packageId: packageIdentity,
|
|
209
|
+
version: prepared.version,
|
|
210
|
+
path: built.outputPath,
|
|
211
|
+
key: artifactKey,
|
|
212
|
+
size: artifact.size,
|
|
213
|
+
md5: artifact.md5,
|
|
214
|
+
sha256: artifact.sha256,
|
|
215
|
+
},
|
|
216
|
+
attestation,
|
|
217
|
+
index,
|
|
218
|
+
}, options.publisher);
|
|
219
|
+
return {
|
|
220
|
+
packageIdentity,
|
|
194
221
|
packageBuild: options.build,
|
|
222
|
+
version: prepared.version,
|
|
223
|
+
artifactKey,
|
|
195
224
|
artifactDigest: artifact.sha256,
|
|
225
|
+
attestationKey,
|
|
196
226
|
embeddedAttestationDigest: embedded.attestationDigest,
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
227
|
+
attestationDigest: detached.attestationDigest,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
// Native multi-arch release (1 Build -> N artifacts)
|
|
231
|
+
const targetFamilies = Object.keys(prepared.native?.targets ?? {}).filter(isNativeArchFamily);
|
|
232
|
+
if (targetFamilies.length === 0) {
|
|
233
|
+
throw new CuratedReleaseError("CURATED_NATIVE_TARGETS_EMPTY");
|
|
234
|
+
}
|
|
235
|
+
const builtArches = [];
|
|
236
|
+
for (const family of targetFamilies) {
|
|
237
|
+
const outputPath = join(workspace, `${packageIdentity}-${family}.spk`);
|
|
238
|
+
const built = await buildFn({
|
|
239
|
+
project: releaseProject,
|
|
240
|
+
outputPath,
|
|
241
|
+
packageIdentity,
|
|
242
|
+
displayname: prepared.displayname,
|
|
243
|
+
spkVersion: prepared.version,
|
|
244
|
+
embeddedAttestationToken: embedded.token,
|
|
245
|
+
archFamily: family,
|
|
246
|
+
});
|
|
247
|
+
if (!built.ok || !built.outputPath) {
|
|
248
|
+
throw new CuratedReleaseError("CURATED_BUILD_FAILED");
|
|
249
|
+
}
|
|
250
|
+
await assertEmbeddedAttestationInSpk(built.outputPath, embedded.token, embeddedInput, options.signer);
|
|
251
|
+
const artifact = digestArtifact(built.outputPath);
|
|
252
|
+
const detachedInput = {
|
|
253
|
+
packageIdentity,
|
|
254
|
+
packageVersion: prepared.version,
|
|
255
|
+
packageBuild: options.build,
|
|
256
|
+
artifactDigest: artifact.sha256,
|
|
257
|
+
embeddedAttestationDigest: embedded.attestationDigest,
|
|
258
|
+
};
|
|
259
|
+
let detached;
|
|
260
|
+
try {
|
|
261
|
+
detached = await options.signer.signRelease(detachedInput);
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
if (error instanceof CuratedReleaseError) {
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
throw new CuratedReleaseError("CURATED_RELEASE_ATTESTATION_BINDING_FAILED");
|
|
268
|
+
}
|
|
269
|
+
assertDetachedAttestation(detached, detachedInput);
|
|
270
|
+
await verifySignedToken(options.signer, detached.token, "release");
|
|
271
|
+
const artifactKey = buildImmutableSpkKey({
|
|
272
|
+
package: packageIdentity,
|
|
273
|
+
version: prepared.version,
|
|
274
|
+
sha256: artifact.sha256,
|
|
275
|
+
});
|
|
276
|
+
const attestationKey = buildImmutableAttestationKey({
|
|
277
|
+
package: packageIdentity,
|
|
278
|
+
version: prepared.version,
|
|
279
|
+
artifactDigest: artifact.sha256,
|
|
280
|
+
});
|
|
281
|
+
const tokenSha256 = digestBytes(Buffer.from(detached.token, "utf8"));
|
|
282
|
+
const releaseArtifact = {
|
|
204
283
|
packageId: packageIdentity,
|
|
205
284
|
version: prepared.version,
|
|
206
285
|
path: built.outputPath,
|
|
@@ -208,19 +287,89 @@ export async function releaseCurated(options) {
|
|
|
208
287
|
size: artifact.size,
|
|
209
288
|
md5: artifact.md5,
|
|
210
289
|
sha256: artifact.sha256,
|
|
290
|
+
};
|
|
291
|
+
const releaseAttestation = {
|
|
292
|
+
packageId: packageIdentity,
|
|
293
|
+
version: prepared.version,
|
|
294
|
+
packageBuild: options.build,
|
|
295
|
+
artifactDigest: artifact.sha256,
|
|
296
|
+
embeddedAttestationDigest: embedded.attestationDigest,
|
|
297
|
+
key: attestationKey,
|
|
298
|
+
token: detached.token,
|
|
299
|
+
digest: detached.attestationDigest,
|
|
300
|
+
tokenSha256,
|
|
301
|
+
verificationJwk: options.signer.verificationJwk,
|
|
302
|
+
};
|
|
303
|
+
const archPlatforms = infoArchValue(family).split(" ");
|
|
304
|
+
const archArtifact = {
|
|
305
|
+
key: artifactKey,
|
|
306
|
+
size: artifact.size,
|
|
307
|
+
md5: artifact.md5,
|
|
308
|
+
sha256: artifact.sha256,
|
|
309
|
+
arch: archPlatforms,
|
|
310
|
+
attestation: {
|
|
311
|
+
key: attestationKey,
|
|
312
|
+
digest: detached.attestationDigest,
|
|
313
|
+
packageBuild: options.build,
|
|
314
|
+
artifactDigest: artifact.sha256,
|
|
315
|
+
embeddedAttestationDigest: embedded.attestationDigest,
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
builtArches.push({
|
|
319
|
+
family,
|
|
320
|
+
outputPath: built.outputPath,
|
|
321
|
+
artifact,
|
|
322
|
+
artifactKey,
|
|
323
|
+
attestationKey,
|
|
324
|
+
detached,
|
|
325
|
+
tokenSha256,
|
|
326
|
+
releaseArtifact,
|
|
327
|
+
releaseAttestation,
|
|
328
|
+
archArtifact,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
const primary = builtArches[0];
|
|
332
|
+
const allArchs = targetFamilies.flatMap((family) => infoArchValue(family).split(" "));
|
|
333
|
+
const indexPayload = {
|
|
334
|
+
packageId: packageIdentity,
|
|
335
|
+
version: prepared.version,
|
|
336
|
+
displayname: prepared.displayname,
|
|
337
|
+
description: prepared.description,
|
|
338
|
+
...(prepared.maintainer ? { maintainer: prepared.maintainer } : {}),
|
|
339
|
+
osMinVer: prepared.osMinVer,
|
|
340
|
+
arch: allArchs,
|
|
341
|
+
artifact: {
|
|
342
|
+
key: primary.artifactKey,
|
|
343
|
+
size: primary.artifact.size,
|
|
344
|
+
md5: primary.artifact.md5,
|
|
345
|
+
sha256: primary.artifact.sha256,
|
|
346
|
+
},
|
|
347
|
+
attestation: {
|
|
348
|
+
key: primary.attestationKey,
|
|
349
|
+
digest: primary.detached.attestationDigest,
|
|
350
|
+
packageBuild: options.build,
|
|
351
|
+
artifactDigest: primary.artifact.sha256,
|
|
352
|
+
embeddedAttestationDigest: embedded.attestationDigest,
|
|
211
353
|
},
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
354
|
+
artifacts: builtArches.map((item) => item.archArtifact),
|
|
355
|
+
};
|
|
356
|
+
// 5. Publication order: all artifacts -> all detached attestations -> index
|
|
357
|
+
for (const item of builtArches) {
|
|
358
|
+
await options.publisher.uploadArtifact(item.releaseArtifact);
|
|
359
|
+
}
|
|
360
|
+
for (const item of builtArches) {
|
|
361
|
+
await options.publisher.uploadAttestation(item.releaseAttestation);
|
|
362
|
+
}
|
|
363
|
+
await options.publisher.updateIndex(indexPayload);
|
|
215
364
|
return {
|
|
216
365
|
packageIdentity,
|
|
217
366
|
packageBuild: options.build,
|
|
218
367
|
version: prepared.version,
|
|
219
|
-
artifactKey,
|
|
220
|
-
artifactDigest: artifact.sha256,
|
|
221
|
-
attestationKey,
|
|
368
|
+
artifactKey: primary.artifactKey,
|
|
369
|
+
artifactDigest: primary.artifact.sha256,
|
|
370
|
+
attestationKey: primary.attestationKey,
|
|
222
371
|
embeddedAttestationDigest: embedded.attestationDigest,
|
|
223
|
-
attestationDigest: detached.attestationDigest,
|
|
372
|
+
attestationDigest: primary.detached.attestationDigest,
|
|
224
373
|
};
|
|
225
374
|
}
|
|
226
375
|
finally {
|
|
@@ -352,6 +501,25 @@ function prepareRelease(project, build) {
|
|
|
352
501
|
if (!validation.ok) {
|
|
353
502
|
throw new CuratedReleaseError("CURATED_VALIDATION_FAILED", validation.errors.map((error) => error.code).join(","));
|
|
354
503
|
}
|
|
504
|
+
const nativePath = project.nativePath ?? join(project.root, "native.yaml");
|
|
505
|
+
const isNative = existsSync(nativePath);
|
|
506
|
+
if (isNative) {
|
|
507
|
+
const parsedNative = loadNative(nativePath);
|
|
508
|
+
if (!parsedNative) {
|
|
509
|
+
throw new CuratedReleaseError("CURATED_NATIVE_MISSING");
|
|
510
|
+
}
|
|
511
|
+
return {
|
|
512
|
+
isNative: true,
|
|
513
|
+
contract,
|
|
514
|
+
native: parsedNative.native,
|
|
515
|
+
packageIdentity,
|
|
516
|
+
version,
|
|
517
|
+
displayname: String(contract.displayname),
|
|
518
|
+
description: String(contract.description),
|
|
519
|
+
maintainer: typeof contract.maintainer === "string" ? contract.maintainer : undefined,
|
|
520
|
+
osMinVer: String(contract.os_min_ver),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
355
523
|
const parsedCompose = loadCompose(project.composePath);
|
|
356
524
|
if (!parsedCompose) {
|
|
357
525
|
throw new CuratedReleaseError("CURATED_COMPOSE_MISSING");
|
|
@@ -361,6 +529,7 @@ function prepareRelease(project, build) {
|
|
|
361
529
|
throw new CuratedReleaseError("CURATED_COMPOSE_GATE_FAILED", composeGate.errors.join(","));
|
|
362
530
|
}
|
|
363
531
|
return {
|
|
532
|
+
isNative: false,
|
|
364
533
|
contract,
|
|
365
534
|
compose: parsedCompose.compose,
|
|
366
535
|
packageIdentity,
|
|
@@ -376,15 +545,22 @@ function copyReleaseProject(source, workspace, packageIdentity) {
|
|
|
376
545
|
mkdirSync(root, { recursive: true });
|
|
377
546
|
const contractPath = join(root, "synology-app.yaml");
|
|
378
547
|
const composePath = join(root, "compose.yaml");
|
|
548
|
+
const nativePath = join(root, "native.yaml");
|
|
379
549
|
writeFileSync(contractPath, readFileSync(source.contractPath));
|
|
380
|
-
|
|
550
|
+
if (existsSync(source.composePath)) {
|
|
551
|
+
writeFileSync(composePath, readFileSync(source.composePath));
|
|
552
|
+
}
|
|
553
|
+
const srcNative = source.nativePath ?? join(source.root, "native.yaml");
|
|
554
|
+
if (existsSync(srcNative)) {
|
|
555
|
+
writeFileSync(nativePath, readFileSync(srcNative));
|
|
556
|
+
}
|
|
381
557
|
for (const name of ["icon.png", "README.md", "html"]) {
|
|
382
558
|
const sourcePath = join(source.root, name);
|
|
383
559
|
if (existsSync(sourcePath)) {
|
|
384
560
|
cpSync(sourcePath, join(root, name), { recursive: true });
|
|
385
561
|
}
|
|
386
562
|
}
|
|
387
|
-
return { root, contractPath, composePath };
|
|
563
|
+
return { root, contractPath, composePath, nativePath };
|
|
388
564
|
}
|
|
389
565
|
function digestArtifact(path) {
|
|
390
566
|
const bytes = readFileSync(path);
|
|
@@ -403,11 +579,21 @@ function digestFile(path) {
|
|
|
403
579
|
*/
|
|
404
580
|
export function digestReleaseBundle(project) {
|
|
405
581
|
const files = [];
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
582
|
+
if (!existsSync(project.contractPath) || !statSync(project.contractPath).isFile()) {
|
|
583
|
+
throw new CuratedReleaseError("CURATED_BUNDLE_FILE_MISSING", "synology-app.yaml");
|
|
584
|
+
}
|
|
585
|
+
files.push("synology-app.yaml");
|
|
586
|
+
const hasCompose = existsSync(project.composePath) && statSync(project.composePath).isFile();
|
|
587
|
+
const nativePath = project.nativePath ?? join(project.root, "native.yaml");
|
|
588
|
+
const hasNative = existsSync(nativePath) && statSync(nativePath).isFile();
|
|
589
|
+
if (!hasCompose && !hasNative) {
|
|
590
|
+
throw new CuratedReleaseError("CURATED_BUNDLE_FILE_MISSING", "compose.yaml");
|
|
591
|
+
}
|
|
592
|
+
if (hasCompose) {
|
|
593
|
+
files.push("compose.yaml");
|
|
594
|
+
}
|
|
595
|
+
if (hasNative) {
|
|
596
|
+
files.push("native.yaml");
|
|
411
597
|
}
|
|
412
598
|
const htmlRoot = join(project.root, "html");
|
|
413
599
|
if (existsSync(htmlRoot)) {
|
|
@@ -453,7 +639,7 @@ function collectCapabilities(contract, compose) {
|
|
|
453
639
|
capabilities.push(`install.${name}:${parameter.type}`);
|
|
454
640
|
}
|
|
455
641
|
}
|
|
456
|
-
if (Object.values(compose.services ?? {}).some((service) => service.privileged === true)) {
|
|
642
|
+
if (compose && Object.values(compose.services ?? {}).some((service) => service.privileged === true)) {
|
|
457
643
|
capabilities.push("compose.privileged");
|
|
458
644
|
}
|
|
459
645
|
return normalizeCapabilities(capabilities);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
schema: 1
|
|
2
|
+
|
|
3
|
+
targets:
|
|
4
|
+
x86_64:
|
|
5
|
+
path: ./dist/app-x86_64.tar.gz
|
|
6
|
+
strip_components: 1
|
|
7
|
+
aarch64:
|
|
8
|
+
path: ./dist/app-aarch64.tar.gz
|
|
9
|
+
strip_components: 1
|
|
10
|
+
|
|
11
|
+
service:
|
|
12
|
+
exec: bin/app
|
|
13
|
+
args:
|
|
14
|
+
- "-data=${SYNOPKG_PKGVAR}"
|
|
15
|
+
env:
|
|
16
|
+
TMPDIR: ${SYNOPKG_PKGVAR}/tmp
|
package/dist/types.d.ts
CHANGED
|
@@ -56,4 +56,4 @@ export interface ComposeDocument {
|
|
|
56
56
|
secrets?: Record<string, unknown>;
|
|
57
57
|
[key: string]: unknown;
|
|
58
58
|
}
|
|
59
|
-
export type TemplateName = "hello-web" | "hello-web-page" | "hello-files" | "compose-import";
|
|
59
|
+
export type TemplateName = "hello-web" | "hello-web-page" | "hello-files" | "compose-import" | "native";
|
package/dist/validate.js
CHANGED
|
@@ -1,25 +1,58 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
2
3
|
import { collectInstallParameters, loadContract } from "./contract/load.js";
|
|
3
4
|
import { validateContract } from "./contract/validate.js";
|
|
4
5
|
import { collectComposeInterpolations, collectVolumeSourceInterpolations, loadCompose, validateAdminPortMapping, validateComposeHostBinds, validateComposeRunAs, validateComposeSecurity, } from "./compose/analyze.js";
|
|
5
|
-
import {
|
|
6
|
+
import { loadNative } from "./native/load.js";
|
|
7
|
+
import { validateNative } from "./native/validate.js";
|
|
8
|
+
import { COMPOSE_FILENAME, CONTRACT_FILENAME, NATIVE_FILENAME, } from "./discovery.js";
|
|
6
9
|
import { declaredPrivilegeFlags } from "./compose/privilege.js";
|
|
7
10
|
export function validateProject(project, options = {}) {
|
|
8
11
|
const profile = options.profile ?? "personal";
|
|
9
12
|
const errors = [];
|
|
10
13
|
const contractFile = CONTRACT_FILENAME;
|
|
11
14
|
const composeFile = COMPOSE_FILENAME;
|
|
15
|
+
const nativeFile = NATIVE_FILENAME;
|
|
12
16
|
const parsed = loadContract(project.contractPath);
|
|
13
17
|
errors.push(...validateContract(parsed, contractFile));
|
|
14
|
-
|
|
18
|
+
const hasCompose = existsSync(project.composePath);
|
|
19
|
+
const nativePath = project.nativePath ?? join(project.root, NATIVE_FILENAME);
|
|
20
|
+
const hasNative = existsSync(nativePath);
|
|
21
|
+
// 2. native.yaml 與 compose.yaml 互斥:兩個都有 → 拒絕;兩個都沒有 → 拒絕。
|
|
22
|
+
if (hasCompose && hasNative) {
|
|
23
|
+
errors.push({
|
|
24
|
+
code: "PROJECT_RUNTIME_EXCLUSIVE",
|
|
25
|
+
span: { file: nativeFile, line: 1, column: 1 },
|
|
26
|
+
message: "專案同時存在 compose.yaml 與 native.yaml,兩者互斥。",
|
|
27
|
+
suggestion: "請選擇使用 compose.yaml(容器 App)或 native.yaml(原生二進位 App),並刪除另一個檔案。",
|
|
28
|
+
});
|
|
29
|
+
return { ok: false, errors };
|
|
30
|
+
}
|
|
31
|
+
if (!hasCompose && !hasNative) {
|
|
15
32
|
errors.push({
|
|
16
33
|
code: "COMPOSE_MISSING",
|
|
17
34
|
span: { file: composeFile, line: 1, column: 1 },
|
|
18
|
-
message: "找不到 compose.yaml。",
|
|
19
|
-
suggestion: "請在專案根目錄建立 compose.yaml。",
|
|
35
|
+
message: "找不到 compose.yaml 或 native.yaml。",
|
|
36
|
+
suggestion: "請在專案根目錄建立 compose.yaml 或 native.yaml。",
|
|
20
37
|
});
|
|
21
38
|
return { ok: false, errors };
|
|
22
39
|
}
|
|
40
|
+
if (hasNative) {
|
|
41
|
+
const parsedNative = loadNative(nativePath);
|
|
42
|
+
if (!parsedNative) {
|
|
43
|
+
errors.push({
|
|
44
|
+
code: "NATIVE_LOAD_FAILED",
|
|
45
|
+
span: { file: nativeFile, line: 1, column: 1 },
|
|
46
|
+
message: "無法讀取 native.yaml。",
|
|
47
|
+
suggestion: "請確認 native.yaml 存在且格式為合法 YAML。",
|
|
48
|
+
});
|
|
49
|
+
return { ok: false, errors };
|
|
50
|
+
}
|
|
51
|
+
errors.push(...validateNative(parsedNative, nativeFile, { profile }));
|
|
52
|
+
const warnings = errors.filter((item) => item.severity === "warning");
|
|
53
|
+
const hardErrors = errors.filter((item) => item.severity !== "warning");
|
|
54
|
+
return { ok: hardErrors.length === 0, errors: hardErrors, warnings };
|
|
55
|
+
}
|
|
23
56
|
const parsedCompose = loadCompose(project.composePath);
|
|
24
57
|
if (!parsedCompose) {
|
|
25
58
|
return { ok: false, errors };
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Syno Package Land Developer Kit CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"synopkgland": "dist/bin.js"
|
|
7
|
+
"synopkgland": "./dist/bin.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
@@ -17,13 +17,9 @@
|
|
|
17
17
|
"test": "npm run build && node --import tsx --test test/**/*.test.ts",
|
|
18
18
|
"prepublishOnly": "npm run build"
|
|
19
19
|
},
|
|
20
|
-
"publishConfig": {
|
|
21
|
-
"access": "public",
|
|
22
|
-
"registry": "https://registry.npmjs.org"
|
|
23
|
-
},
|
|
24
20
|
"dependencies": {
|
|
25
|
-
"@synopackageland/dev-sdk": "0.
|
|
26
|
-
"@synopackageland/package-source": "0.
|
|
21
|
+
"@synopackageland/dev-sdk": "^0.2.0",
|
|
22
|
+
"@synopackageland/package-source": "^0.2.0",
|
|
27
23
|
"ajv": "^8.17.1",
|
|
28
24
|
"commander": "^13.1.0",
|
|
29
25
|
"jose": "^6.0.10",
|
|
@@ -34,5 +30,9 @@
|
|
|
34
30
|
"@types/node": "^22.13.10",
|
|
35
31
|
"tsx": "^4.19.3",
|
|
36
32
|
"typescript": "^5.8.2"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public",
|
|
36
|
+
"registry": "https://registry.npmjs.org"
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -10,7 +10,21 @@
|
|
|
10
10
|
| `CONTRACT_SCHEMA_UNSUPPORTED` | `schema` 必須為整數 `1` |
|
|
11
11
|
| `CONTRACT_INVALID_STRING`/`CONTRACT_ADMINPORT_INVALID` | 修正字串或 `adminport` 型別 |
|
|
12
12
|
| `INSTALL_STRING_*`/`INSTALL_PARAMETER_UNREFERENCED` | 修正 install 預設值;在 compose 加 `${參數名}` |
|
|
13
|
-
| `COMPOSE_MISSING` | 建立 `compose.yaml` |
|
|
13
|
+
| `COMPOSE_MISSING` | 建立 `compose.yaml`(容器 App)或 `native.yaml`(原生 binary App) |
|
|
14
|
+
| `PROJECT_RUNTIME_EXCLUSIVE` | `compose.yaml` 與 `native.yaml` 互斥,刪掉其中一個 |
|
|
15
|
+
| `NATIVE_SCHEMA_INVALID` | `native.yaml` 的 `schema` 只接受 `1` |
|
|
16
|
+
| `NATIVE_TARGETS_REQUIRED` / `NATIVE_TARGETS_EMPTY` | 在 `targets` 底下至少宣告一個 `x86_64` 或 `aarch64` |
|
|
17
|
+
| `NATIVE_ARCH_UNKNOWN` | `targets` 的 key 只接受 `x86_64`、`aarch64` |
|
|
18
|
+
| `NATIVE_TARGET_SOURCE_EXCLUSIVE` | 每個 target 只能有 `path:` 或 `url:` 其中一個 |
|
|
19
|
+
| `NATIVE_PATH_FORBIDDEN_IN_LISTING` | Curated 上架只接受 `url:` + `sha256:`;本機開發才可以用 `path:` |
|
|
20
|
+
| `NATIVE_SHA256_REQUIRED_IN_LISTING` | 補上 `sha256:`,CI 要能自己重抓比對 |
|
|
21
|
+
| `NATIVE_SHA256_INVALID` | `sha256` 必須是 64 個 hex 字元 |
|
|
22
|
+
| `NATIVE_STRIP_COMPONENTS_INVALID` | `strip_components` 必須是 >= 0 的整數 |
|
|
23
|
+
| `NATIVE_EXEC_REQUIRED` / `NATIVE_EXEC_INVALID_PATH` | `service.exec` 必填,且必須是相對 `SYNOPKG_PKGDEST` 的路徑,不可為絕對路徑或含 `..` |
|
|
24
|
+
| `NATIVE_PKGVAR_REQUIRED` | `service.args` 或 `service.env` 至少要用一次 `${SYNOPKG_PKGVAR}`,否則升級時資料會被連同 `target/` 一起換掉 |
|
|
25
|
+
| `NATIVE_SERVICE_USER_FORBIDDEN` | 拿掉 `service.user`:DSM 自己建 package user,`SYNOPKG_PKGVAR` 已經是對的 owner |
|
|
26
|
+
| `NATIVE_SERVICE_SHELL_FIELD_FORBIDDEN` | 拿掉 `command:` / `pre_start:` 這類 shell 欄位;`args` 是陣列,逐項傳給 exec,不經 shell |
|
|
27
|
+
| `NATIVE_ARGS_INVALID` / `NATIVE_ENV_INVALID` | `args` 必須是字串陣列,`env` 必須是 string→string |
|
|
14
28
|
| `COMPOSE_PRIVILEGED_FORBIDDEN` 等安全碼 | 移除 privileged、host 網路、socket 掛載等 |
|
|
15
29
|
| `COMPOSE_SERVICE_NOT_FOUND` | 修正 `adminport` 對應的 service |
|
|
16
30
|
| `CONTRACT_ADMINPORT_UNMAPPED` | 在 compose 發布與 `adminport` 相同的 host port |
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
schema: 1
|
|
2
|
+
|
|
3
|
+
targets:
|
|
4
|
+
x86_64:
|
|
5
|
+
path: ./dist/app-x86_64.tar.gz
|
|
6
|
+
strip_components: 1
|
|
7
|
+
aarch64:
|
|
8
|
+
path: ./dist/app-aarch64.tar.gz
|
|
9
|
+
strip_components: 1
|
|
10
|
+
|
|
11
|
+
service:
|
|
12
|
+
exec: bin/app
|
|
13
|
+
args:
|
|
14
|
+
- "-data=${SYNOPKG_PKGVAR}"
|
|
15
|
+
env:
|
|
16
|
+
TMPDIR: ${SYNOPKG_PKGVAR}/tmp
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_PACKAGE_SOURCE_ORIGIN, fetchPackageSourceManifest, publishSpkToSource, resolvePublishTarget, type PublishTarget, type PublishSpkResult } from "@synology/package-source/publish-spk";
|
|
2
|
-
import { resolveNextSpkVersion } from "@synology/package-source/next-version";
|
|
3
|
-
import type { IndexManifest } from "@synology/package-source/index-manifest";
|
|
4
|
-
export { DEFAULT_PACKAGE_SOURCE_ORIGIN, fetchPackageSourceManifest, publishSpkToSource, resolvePublishTarget, resolveNextSpkVersion, };
|
|
5
|
-
export type { PublishTarget, PublishSpkResult, IndexManifest };
|
|
6
|
-
export declare function resolvePackageSourceManifest(origin?: string): Promise<IndexManifest | null>;
|
|
7
|
-
export declare function publishBuiltSpk(spkPath: string, target?: PublishTarget): Promise<PublishSpkResult>;
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_PACKAGE_SOURCE_ORIGIN, fetchPackageSourceManifest, publishSpkToSource, resolvePublishTarget, } from "@synology/package-source/publish-spk";
|
|
2
|
-
import { resolveNextSpkVersion } from "@synology/package-source/next-version";
|
|
3
|
-
export { DEFAULT_PACKAGE_SOURCE_ORIGIN, fetchPackageSourceManifest, publishSpkToSource, resolvePublishTarget, resolveNextSpkVersion, };
|
|
4
|
-
const LIVE_TEST_FALLBACK_ORIGIN = "https://syno-package-source-throwaway.bidacumen.workers.dev";
|
|
5
|
-
export async function resolvePackageSourceManifest(origin = resolvePublishTarget().origin) {
|
|
6
|
-
const manifest = await fetchPackageSourceManifest(origin);
|
|
7
|
-
if (manifest) {
|
|
8
|
-
return manifest;
|
|
9
|
-
}
|
|
10
|
-
if (origin !== LIVE_TEST_FALLBACK_ORIGIN) {
|
|
11
|
-
return fetchPackageSourceManifest(LIVE_TEST_FALLBACK_ORIGIN);
|
|
12
|
-
}
|
|
13
|
-
return null;
|
|
14
|
-
}
|
|
15
|
-
export async function publishBuiltSpk(spkPath, target = resolvePublishTarget()) {
|
|
16
|
-
return publishSpkToSource(spkPath, target);
|
|
17
|
-
}
|