@synopackageland/cli 0.3.1 → 0.3.2
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/attestation/app-package.js +8 -1
- package/dist/attestation/platform-service.d.ts +70 -0
- package/dist/attestation/platform-service.js +250 -0
- package/dist/bin.js +0 -0
- package/dist/build-spk/generator.js +202 -10
- package/dist/build-spk/resource-worker.d.ts +14 -0
- package/dist/build-spk/resource-worker.js +140 -0
- package/dist/cli.js +69 -10
- package/dist/compose/analyze.d.ts +41 -1
- package/dist/compose/analyze.js +458 -18
- package/dist/contract/load.d.ts +1 -0
- package/dist/contract/load.js +1 -1
- package/dist/contract/posix-all-shares.d.ts +16 -0
- package/dist/contract/posix-all-shares.js +63 -0
- package/dist/contract/schema-validate.js +26 -1
- package/dist/contract/validate.js +57 -0
- package/dist/contract/web-launch.d.ts +6 -0
- package/dist/contract/web-launch.js +25 -0
- package/dist/deploy/deploy.d.ts +1 -0
- package/dist/deploy/deploy.js +37 -2
- package/dist/deploy/homes-values.d.ts +19 -0
- package/dist/deploy/homes-values.js +29 -0
- package/dist/deploy/param-values.d.ts +21 -0
- package/dist/deploy/param-values.js +51 -0
- package/dist/deploy/spk-version.d.ts +5 -0
- package/dist/deploy/spk-version.js +50 -18
- package/dist/doctor.d.ts +22 -0
- package/dist/doctor.js +594 -0
- package/dist/error-collector.d.ts +2 -0
- package/dist/error-collector.js +46 -0
- package/dist/error-registry.d.ts +7 -0
- package/dist/error-registry.js +827 -0
- package/dist/host/broker-upgrade-preflight.d.ts +61 -0
- package/dist/host/broker-upgrade-preflight.js +156 -0
- package/dist/host/catalog-install.d.ts +12 -0
- package/dist/host/catalog-install.js +83 -17
- package/dist/host/credentials.js +35 -5
- package/dist/host/dsm-client.d.ts +12 -0
- package/dist/host/dsm-client.js +56 -1
- package/dist/host/dsm-compound.d.ts +37 -0
- package/dist/host/dsm-compound.js +120 -0
- package/dist/host/fake-nas.d.ts +1 -0
- package/dist/host/fake-nas.js +3 -0
- package/dist/host/types.d.ts +4 -0
- package/dist/publish/curated-release.js +3 -3
- package/dist/publish/platform-service-release.d.ts +60 -0
- package/dist/publish/platform-service-release.js +333 -0
- package/dist/publish/preflight.js +14 -1
- package/dist/release-bin.js +5 -1
- package/dist/templates/hello-files/compose.yaml +2 -0
- package/dist/templates/hello-web/compose.yaml +14 -3
- package/dist/templates/hello-web/synology-app.yaml +1 -1
- package/dist/templates/hello-web-page/compose.yaml +14 -3
- package/dist/templates/hello-web-page/synology-app.yaml +1 -1
- package/dist/test-host.d.ts +2 -0
- package/dist/test-host.js +15 -2
- package/dist/types.d.ts +1 -1
- package/dist/validate.js +14 -3
- package/package.json +2 -2
- package/schema/doctor.schema.json +38 -0
- package/schema/synology-app.schema.json +1 -1
- package/skills/reference/create.md +19 -0
- package/skills/reference/diagnose.md +35 -2
- package/skills/reference/files.md +10 -0
- package/templates/hello-files/compose.yaml +2 -0
- package/templates/hello-web/compose.yaml +14 -3
- package/templates/hello-web/synology-app.yaml +1 -1
- package/templates/hello-web-page/compose.yaml +14 -3
- package/templates/hello-web-page/synology-app.yaml +1 -1
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
import { SignJWT, exportJWK, generateKeyPair, importJWK } from "jose";
|
|
5
|
+
import { CURATED_BUILD_OFFSET } from "@synopackageland/package-source/index-manifest";
|
|
5
6
|
export const PLATFORM_ATTESTATION_ISSUER = "synopkgland-curated";
|
|
6
7
|
export const APP_PACKAGE_ATTESTATION_TYP = "app-package-attestation";
|
|
7
8
|
export const APP_PACKAGE_ATTESTATION_VERSION = 2;
|
|
@@ -141,8 +142,14 @@ export function assertPackageVersionBinding(packageVersion, packageBuild) {
|
|
|
141
142
|
if (typeof packageVersion !== "string" || packageVersion.length === 0) {
|
|
142
143
|
throw new Error("packageVersion must be a non-empty string");
|
|
143
144
|
}
|
|
145
|
+
if (!Number.isSafeInteger(packageBuild) ||
|
|
146
|
+
packageBuild < 1 ||
|
|
147
|
+
packageBuild >= CURATED_BUILD_OFFSET) {
|
|
148
|
+
throw new Error("packageBuild must be a positive safe integer below the Curated offset");
|
|
149
|
+
}
|
|
144
150
|
const match = /-(\d+)$/.exec(packageVersion);
|
|
145
|
-
|
|
151
|
+
const parsed = match ? Number(match[1]) : NaN;
|
|
152
|
+
if (parsed !== packageBuild && parsed !== packageBuild + CURATED_BUILD_OFFSET) {
|
|
146
153
|
throw new Error("packageVersion and packageBuild must agree");
|
|
147
154
|
}
|
|
148
155
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type JWK } from "jose";
|
|
2
|
+
import type { AppPackageSigningKey } from "./app-package.js";
|
|
3
|
+
/**
|
|
4
|
+
* Trust contract for releasing the platform service (the Broker SPK) itself.
|
|
5
|
+
* This is deliberately a separate issuer, typ and keyring from the curated App
|
|
6
|
+
* chain: the Broker carries the curated trust root as *data*, so that root can
|
|
7
|
+
* never double as the authority that signs the Broker's own releases.
|
|
8
|
+
*/
|
|
9
|
+
export declare const PLATFORM_ATTESTATION_ISSUER = "synopkgland-platform";
|
|
10
|
+
export declare const PLATFORM_SERVICE_RELEASE_ATTESTATION_TYP = "platform-service-release-attestation";
|
|
11
|
+
export declare const PLATFORM_SERVICE_RELEASE_ATTESTATION_VERSION: 1;
|
|
12
|
+
export declare const PLATFORM_SERVICE_PACKAGE_IDENTITY = "SynoPkgLandService";
|
|
13
|
+
export declare const PLATFORM_KEYRING_PURPOSE = "platform-service-release";
|
|
14
|
+
export declare const PLATFORM_ATTESTATION_PRIVATE_JWK_ENV = "SYNOPKGLAND_PLATFORM_ATTESTATION_PRIVATE_JWK";
|
|
15
|
+
export declare const PLATFORM_ATTESTATION_KID_ENV = "SYNOPKGLAND_PLATFORM_ATTESTATION_KID";
|
|
16
|
+
export declare const PLATFORM_ATTESTATION_KEYRING_PATH_ENV = "SYNOPKGLAND_PLATFORM_ATTESTATION_KEYRING_PATH";
|
|
17
|
+
export type PlatformServiceReleaseErrorCode = "PLATFORM_KEYRING_INVALID" | "PLATFORM_KEYRING_PURPOSE_MISMATCH" | "PLATFORM_KEYRING_PRIVATE_MATERIAL" | "PLATFORM_KEYRING_NO_ACTIVE_KEY" | "PLATFORM_SIGNING_MATERIAL_MISSING" | "PLATFORM_SIGNING_MATERIAL_INVALID" | "PLATFORM_SIGNING_KEY_UNKNOWN" | "PLATFORM_ATTESTATION_INPUT_INVALID" | "PLATFORM_ATTESTATION_INVALID";
|
|
18
|
+
export declare class PlatformServiceReleaseError extends Error {
|
|
19
|
+
readonly code: PlatformServiceReleaseErrorCode;
|
|
20
|
+
constructor(code: PlatformServiceReleaseErrorCode, detail?: string);
|
|
21
|
+
}
|
|
22
|
+
export interface PlatformServiceReleaseAttestationInput {
|
|
23
|
+
packageIdentity: string;
|
|
24
|
+
/** Source version without any Build suffix; the constants file keeps it. */
|
|
25
|
+
sourceVersion: string;
|
|
26
|
+
packageBuild: number;
|
|
27
|
+
/** `${sourceVersion}-<zero-padded Build>`; must agree with packageBuild. */
|
|
28
|
+
packageVersion: string;
|
|
29
|
+
/** SHA-256 of the final SPK bytes. */
|
|
30
|
+
artifactDigest: string;
|
|
31
|
+
/** Git commit the trusted workflow checked out and built. */
|
|
32
|
+
sourceRevision: string;
|
|
33
|
+
osMinVer: string;
|
|
34
|
+
arch: "noarch";
|
|
35
|
+
}
|
|
36
|
+
export interface PlatformServiceReleaseAttestationClaims extends PlatformServiceReleaseAttestationInput {
|
|
37
|
+
typ: typeof PLATFORM_SERVICE_RELEASE_ATTESTATION_TYP;
|
|
38
|
+
version: typeof PLATFORM_SERVICE_RELEASE_ATTESTATION_VERSION;
|
|
39
|
+
issuedAt: string;
|
|
40
|
+
}
|
|
41
|
+
export type PlatformKeyStatus = "active" | "verify-only" | "revoked";
|
|
42
|
+
export interface PlatformKeyringDocument {
|
|
43
|
+
version: 1;
|
|
44
|
+
purpose: typeof PLATFORM_KEYRING_PURPOSE;
|
|
45
|
+
keys: Array<{
|
|
46
|
+
kid: string;
|
|
47
|
+
status: PlatformKeyStatus;
|
|
48
|
+
publicJwk: JWK;
|
|
49
|
+
}>;
|
|
50
|
+
}
|
|
51
|
+
export interface PlatformAttestationSigner {
|
|
52
|
+
sign(input: PlatformServiceReleaseAttestationInput): Promise<{
|
|
53
|
+
token: string;
|
|
54
|
+
attestationDigest: string;
|
|
55
|
+
claims: PlatformServiceReleaseAttestationClaims;
|
|
56
|
+
}>;
|
|
57
|
+
verify(token: string): Promise<PlatformServiceReleaseAttestationClaims>;
|
|
58
|
+
verificationJwk: JWK;
|
|
59
|
+
}
|
|
60
|
+
export declare function parsePlatformKeyringDocument(raw: unknown): PlatformKeyringDocument;
|
|
61
|
+
export declare function readPlatformKeyringFile(path: string): PlatformKeyringDocument;
|
|
62
|
+
export declare function canonicalPlatformServiceReleaseClaims(claims: PlatformServiceReleaseAttestationClaims): string;
|
|
63
|
+
export declare function digestPlatformServiceReleaseClaims(claims: PlatformServiceReleaseAttestationClaims): string;
|
|
64
|
+
export declare function signPlatformServiceReleaseAttestationToken(input: PlatformServiceReleaseAttestationInput, key: AppPackageSigningKey): Promise<{
|
|
65
|
+
token: string;
|
|
66
|
+
attestationDigest: string;
|
|
67
|
+
claims: PlatformServiceReleaseAttestationClaims;
|
|
68
|
+
}>;
|
|
69
|
+
export declare function verifyPlatformServiceReleaseAttestationToken(token: string, keyring: PlatformKeyringDocument): Promise<PlatformServiceReleaseAttestationClaims>;
|
|
70
|
+
export declare function createPlatformAttestationSignerFromEnv(env?: NodeJS.Dict<string>): Promise<PlatformAttestationSigner>;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { SignJWT, importJWK, jwtVerify } from "jose";
|
|
4
|
+
/**
|
|
5
|
+
* Trust contract for releasing the platform service (the Broker SPK) itself.
|
|
6
|
+
* This is deliberately a separate issuer, typ and keyring from the curated App
|
|
7
|
+
* chain: the Broker carries the curated trust root as *data*, so that root can
|
|
8
|
+
* never double as the authority that signs the Broker's own releases.
|
|
9
|
+
*/
|
|
10
|
+
export const PLATFORM_ATTESTATION_ISSUER = "synopkgland-platform";
|
|
11
|
+
export const PLATFORM_SERVICE_RELEASE_ATTESTATION_TYP = "platform-service-release-attestation";
|
|
12
|
+
export const PLATFORM_SERVICE_RELEASE_ATTESTATION_VERSION = 1;
|
|
13
|
+
export const PLATFORM_SERVICE_PACKAGE_IDENTITY = "SynoPkgLandService";
|
|
14
|
+
export const PLATFORM_KEYRING_PURPOSE = "platform-service-release";
|
|
15
|
+
export const PLATFORM_ATTESTATION_PRIVATE_JWK_ENV = "SYNOPKGLAND_PLATFORM_ATTESTATION_PRIVATE_JWK";
|
|
16
|
+
export const PLATFORM_ATTESTATION_KID_ENV = "SYNOPKGLAND_PLATFORM_ATTESTATION_KID";
|
|
17
|
+
export const PLATFORM_ATTESTATION_KEYRING_PATH_ENV = "SYNOPKGLAND_PLATFORM_ATTESTATION_KEYRING_PATH";
|
|
18
|
+
export class PlatformServiceReleaseError extends Error {
|
|
19
|
+
code;
|
|
20
|
+
constructor(code, detail) {
|
|
21
|
+
super(detail ? `${code}: ${detail}` : code);
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.name = "PlatformServiceReleaseError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const PRIVATE_JWK_FIELDS = ["d", "p", "q", "dp", "dq", "qi", "oth", "k"];
|
|
27
|
+
const KEY_STATUSES = new Set(["active", "verify-only", "revoked"]);
|
|
28
|
+
export function parsePlatformKeyringDocument(raw) {
|
|
29
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
30
|
+
throw new PlatformServiceReleaseError("PLATFORM_KEYRING_INVALID", "not an object");
|
|
31
|
+
}
|
|
32
|
+
const document = raw;
|
|
33
|
+
if (document.version !== 1 || !Array.isArray(document.keys)) {
|
|
34
|
+
throw new PlatformServiceReleaseError("PLATFORM_KEYRING_INVALID", "unsupported document shape");
|
|
35
|
+
}
|
|
36
|
+
if (document.purpose !== PLATFORM_KEYRING_PURPOSE) {
|
|
37
|
+
// A curated keyring document has no purpose field at all; refusing here is
|
|
38
|
+
// what keeps the Broker's bundled trust root from signing the Broker.
|
|
39
|
+
throw new PlatformServiceReleaseError("PLATFORM_KEYRING_PURPOSE_MISMATCH");
|
|
40
|
+
}
|
|
41
|
+
for (const entry of document.keys) {
|
|
42
|
+
if (!entry ||
|
|
43
|
+
typeof entry.kid !== "string" ||
|
|
44
|
+
entry.kid.length === 0 ||
|
|
45
|
+
!KEY_STATUSES.has(entry.status) ||
|
|
46
|
+
!entry.publicJwk ||
|
|
47
|
+
typeof entry.publicJwk !== "object") {
|
|
48
|
+
throw new PlatformServiceReleaseError("PLATFORM_KEYRING_INVALID", "invalid key entry");
|
|
49
|
+
}
|
|
50
|
+
if (PRIVATE_JWK_FIELDS.some((field) => field in entry.publicJwk)) {
|
|
51
|
+
throw new PlatformServiceReleaseError("PLATFORM_KEYRING_PRIVATE_MATERIAL");
|
|
52
|
+
}
|
|
53
|
+
if (entry.publicJwk.kty !== "RSA" || typeof entry.publicJwk.n !== "string") {
|
|
54
|
+
throw new PlatformServiceReleaseError("PLATFORM_KEYRING_INVALID", "not a public RSA key");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!document.keys.some((entry) => entry.status === "active")) {
|
|
58
|
+
throw new PlatformServiceReleaseError("PLATFORM_KEYRING_NO_ACTIVE_KEY");
|
|
59
|
+
}
|
|
60
|
+
return document;
|
|
61
|
+
}
|
|
62
|
+
export function readPlatformKeyringFile(path) {
|
|
63
|
+
let raw;
|
|
64
|
+
try {
|
|
65
|
+
raw = readFileSync(path, "utf8");
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw new PlatformServiceReleaseError("PLATFORM_KEYRING_INVALID", `unable to read keyring: ${error instanceof Error ? error.message : String(error)}`);
|
|
69
|
+
}
|
|
70
|
+
let parsed;
|
|
71
|
+
try {
|
|
72
|
+
parsed = JSON.parse(raw);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
throw new PlatformServiceReleaseError("PLATFORM_KEYRING_INVALID", "keyring is not valid JSON");
|
|
76
|
+
}
|
|
77
|
+
return parsePlatformKeyringDocument(parsed);
|
|
78
|
+
}
|
|
79
|
+
export function canonicalPlatformServiceReleaseClaims(claims) {
|
|
80
|
+
return JSON.stringify({
|
|
81
|
+
arch: claims.arch,
|
|
82
|
+
artifactDigest: claims.artifactDigest,
|
|
83
|
+
issuedAt: claims.issuedAt,
|
|
84
|
+
osMinVer: claims.osMinVer,
|
|
85
|
+
packageBuild: claims.packageBuild,
|
|
86
|
+
packageIdentity: claims.packageIdentity,
|
|
87
|
+
packageVersion: claims.packageVersion,
|
|
88
|
+
sourceRevision: claims.sourceRevision,
|
|
89
|
+
sourceVersion: claims.sourceVersion,
|
|
90
|
+
typ: claims.typ,
|
|
91
|
+
version: claims.version,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
export function digestPlatformServiceReleaseClaims(claims) {
|
|
95
|
+
return createHash("sha256").update(canonicalPlatformServiceReleaseClaims(claims)).digest("hex");
|
|
96
|
+
}
|
|
97
|
+
export async function signPlatformServiceReleaseAttestationToken(input, key) {
|
|
98
|
+
assertPlatformInput(input);
|
|
99
|
+
if (typeof key.kid !== "string" || key.kid.length === 0) {
|
|
100
|
+
throw new PlatformServiceReleaseError("PLATFORM_SIGNING_MATERIAL_INVALID", "kid required");
|
|
101
|
+
}
|
|
102
|
+
const claims = {
|
|
103
|
+
typ: PLATFORM_SERVICE_RELEASE_ATTESTATION_TYP,
|
|
104
|
+
version: PLATFORM_SERVICE_RELEASE_ATTESTATION_VERSION,
|
|
105
|
+
packageIdentity: input.packageIdentity,
|
|
106
|
+
sourceVersion: input.sourceVersion,
|
|
107
|
+
packageBuild: input.packageBuild,
|
|
108
|
+
packageVersion: input.packageVersion,
|
|
109
|
+
artifactDigest: input.artifactDigest,
|
|
110
|
+
sourceRevision: input.sourceRevision,
|
|
111
|
+
osMinVer: input.osMinVer,
|
|
112
|
+
arch: input.arch,
|
|
113
|
+
issuedAt: new Date().toISOString(),
|
|
114
|
+
};
|
|
115
|
+
const attestationDigest = digestPlatformServiceReleaseClaims(claims);
|
|
116
|
+
const token = await new SignJWT({ ...claims })
|
|
117
|
+
.setProtectedHeader({ alg: "RS256", kid: key.kid, typ: "JWT" })
|
|
118
|
+
.setIssuer(PLATFORM_ATTESTATION_ISSUER)
|
|
119
|
+
.setSubject(claims.packageIdentity)
|
|
120
|
+
.setJti(attestationDigest)
|
|
121
|
+
.sign(key.privateKey);
|
|
122
|
+
return { token, attestationDigest, claims };
|
|
123
|
+
}
|
|
124
|
+
export async function verifyPlatformServiceReleaseAttestationToken(token, keyring) {
|
|
125
|
+
parsePlatformKeyringDocument(keyring);
|
|
126
|
+
try {
|
|
127
|
+
const header = decodeHeader(token);
|
|
128
|
+
const entry = keyring.keys.find((candidate) => candidate.kid === header.kid && candidate.status !== "revoked");
|
|
129
|
+
if (!entry) {
|
|
130
|
+
throw new Error("unknown or revoked kid");
|
|
131
|
+
}
|
|
132
|
+
const publicKey = (await importJWK(entry.publicJwk, "RS256"));
|
|
133
|
+
const result = await jwtVerify(token, publicKey, {
|
|
134
|
+
algorithms: ["RS256"],
|
|
135
|
+
issuer: PLATFORM_ATTESTATION_ISSUER,
|
|
136
|
+
});
|
|
137
|
+
if (result.protectedHeader.alg !== "RS256" ||
|
|
138
|
+
result.protectedHeader.typ !== "JWT" ||
|
|
139
|
+
result.protectedHeader.kid !== entry.kid) {
|
|
140
|
+
throw new Error("token header mismatch");
|
|
141
|
+
}
|
|
142
|
+
const payload = result.payload;
|
|
143
|
+
if (payload.typ !== PLATFORM_SERVICE_RELEASE_ATTESTATION_TYP ||
|
|
144
|
+
payload.version !== PLATFORM_SERVICE_RELEASE_ATTESTATION_VERSION) {
|
|
145
|
+
throw new Error("token typ mismatch");
|
|
146
|
+
}
|
|
147
|
+
const claims = {
|
|
148
|
+
typ: payload.typ,
|
|
149
|
+
version: payload.version,
|
|
150
|
+
packageIdentity: payload.packageIdentity,
|
|
151
|
+
sourceVersion: payload.sourceVersion,
|
|
152
|
+
packageBuild: payload.packageBuild,
|
|
153
|
+
packageVersion: payload.packageVersion,
|
|
154
|
+
artifactDigest: payload.artifactDigest,
|
|
155
|
+
sourceRevision: payload.sourceRevision,
|
|
156
|
+
osMinVer: payload.osMinVer,
|
|
157
|
+
arch: payload.arch,
|
|
158
|
+
issuedAt: payload.issuedAt,
|
|
159
|
+
};
|
|
160
|
+
assertPlatformInput(claims);
|
|
161
|
+
if (typeof claims.issuedAt !== "string" || Number.isNaN(Date.parse(claims.issuedAt))) {
|
|
162
|
+
throw new Error("issuedAt is not a timestamp");
|
|
163
|
+
}
|
|
164
|
+
if (result.payload.jti !== digestPlatformServiceReleaseClaims(claims)) {
|
|
165
|
+
throw new Error("jti does not bind the claims");
|
|
166
|
+
}
|
|
167
|
+
return claims;
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (error instanceof PlatformServiceReleaseError) {
|
|
171
|
+
throw new PlatformServiceReleaseError("PLATFORM_ATTESTATION_INVALID", error.message);
|
|
172
|
+
}
|
|
173
|
+
throw new PlatformServiceReleaseError("PLATFORM_ATTESTATION_INVALID", error instanceof Error ? error.message : String(error));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
export async function createPlatformAttestationSignerFromEnv(env = process.env) {
|
|
177
|
+
const jwkRaw = env[PLATFORM_ATTESTATION_PRIVATE_JWK_ENV]?.trim();
|
|
178
|
+
const kidFromEnv = env[PLATFORM_ATTESTATION_KID_ENV]?.trim();
|
|
179
|
+
const keyringPath = env[PLATFORM_ATTESTATION_KEYRING_PATH_ENV]?.trim();
|
|
180
|
+
if (!jwkRaw) {
|
|
181
|
+
throw new PlatformServiceReleaseError("PLATFORM_SIGNING_MATERIAL_MISSING");
|
|
182
|
+
}
|
|
183
|
+
if (!keyringPath) {
|
|
184
|
+
throw new PlatformServiceReleaseError("PLATFORM_KEYRING_INVALID", `${PLATFORM_ATTESTATION_KEYRING_PATH_ENV} is required`);
|
|
185
|
+
}
|
|
186
|
+
const keyring = readPlatformKeyringFile(keyringPath);
|
|
187
|
+
let privateJwk;
|
|
188
|
+
let privateKey;
|
|
189
|
+
try {
|
|
190
|
+
privateJwk = JSON.parse(jwkRaw);
|
|
191
|
+
privateKey = (await importJWK(privateJwk, "RS256"));
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
throw new PlatformServiceReleaseError("PLATFORM_SIGNING_MATERIAL_INVALID");
|
|
195
|
+
}
|
|
196
|
+
const kid = kidFromEnv || (typeof privateJwk.kid === "string" ? privateJwk.kid : "");
|
|
197
|
+
if (!kid) {
|
|
198
|
+
throw new PlatformServiceReleaseError("PLATFORM_SIGNING_MATERIAL_INVALID", "kid required");
|
|
199
|
+
}
|
|
200
|
+
const trustEntry = keyring.keys.find((entry) => entry.kid === kid && entry.status === "active");
|
|
201
|
+
if (!trustEntry) {
|
|
202
|
+
throw new PlatformServiceReleaseError("PLATFORM_SIGNING_KEY_UNKNOWN");
|
|
203
|
+
}
|
|
204
|
+
const signingKey = { kid, privateKey };
|
|
205
|
+
return {
|
|
206
|
+
sign: (input) => signPlatformServiceReleaseAttestationToken(input, signingKey),
|
|
207
|
+
verify: (token) => verifyPlatformServiceReleaseAttestationToken(token, keyring),
|
|
208
|
+
verificationJwk: trustEntry.publicJwk,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function assertPlatformInput(input) {
|
|
212
|
+
const fail = (detail) => {
|
|
213
|
+
throw new PlatformServiceReleaseError("PLATFORM_ATTESTATION_INPUT_INVALID", detail);
|
|
214
|
+
};
|
|
215
|
+
if (input.packageIdentity !== PLATFORM_SERVICE_PACKAGE_IDENTITY) {
|
|
216
|
+
fail("packageIdentity must be the platform service identity");
|
|
217
|
+
}
|
|
218
|
+
if (!/^\d+\.\d+\.\d+$/.test(input.sourceVersion)) {
|
|
219
|
+
fail("sourceVersion must be plain semver without a Build suffix");
|
|
220
|
+
}
|
|
221
|
+
if (!Number.isSafeInteger(input.packageBuild) || input.packageBuild < 1) {
|
|
222
|
+
fail("packageBuild must be a positive safe integer");
|
|
223
|
+
}
|
|
224
|
+
if (input.packageVersion !== `${input.sourceVersion}-${String(input.packageBuild).padStart(4, "0")}`) {
|
|
225
|
+
fail("packageVersion must be sourceVersion plus the zero-padded Build");
|
|
226
|
+
}
|
|
227
|
+
if (!/^[a-f0-9]{64}$/.test(input.artifactDigest)) {
|
|
228
|
+
fail("artifactDigest must be a lowercase SHA-256 digest");
|
|
229
|
+
}
|
|
230
|
+
if (!/^[a-f0-9]{40}$/.test(input.sourceRevision)) {
|
|
231
|
+
fail("sourceRevision must be a full git commit SHA");
|
|
232
|
+
}
|
|
233
|
+
if (!/^\d+\.\d+-\d+$/.test(input.osMinVer)) {
|
|
234
|
+
fail("osMinVer must look like 7.2-69057");
|
|
235
|
+
}
|
|
236
|
+
if (input.arch !== "noarch") {
|
|
237
|
+
fail("the platform service is noarch");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function decodeHeader(token) {
|
|
241
|
+
const parts = token.split(".");
|
|
242
|
+
if (parts.length !== 3 || parts.some((part) => part.length === 0)) {
|
|
243
|
+
throw new Error("not a compact JWT");
|
|
244
|
+
}
|
|
245
|
+
const header = JSON.parse(Buffer.from(parts[0], "base64url").toString("utf8"));
|
|
246
|
+
if (!header || typeof header !== "object" || Array.isArray(header)) {
|
|
247
|
+
throw new Error("invalid JWT header");
|
|
248
|
+
}
|
|
249
|
+
return header;
|
|
250
|
+
}
|
package/dist/bin.js
CHANGED
|
File without changes
|
|
@@ -5,7 +5,7 @@ import { createHash } from "node:crypto";
|
|
|
5
5
|
import * as tar from "tar";
|
|
6
6
|
import { loadContract, collectInstallParameters } from "../contract/load.js";
|
|
7
7
|
import { renderDsmWindowConfig, resolvedWebLaunch, trustedAppEntryPath, } from "../contract/web-launch.js";
|
|
8
|
-
import { loadCompose } from "../compose/analyze.js";
|
|
8
|
+
import { classifyComposeRoute, loadCompose } from "../compose/analyze.js";
|
|
9
9
|
import { scanComposePrivilege } from "../compose/privilege.js";
|
|
10
10
|
import { validateProject } from "../validate.js";
|
|
11
11
|
import { writeDsmWindowIcons, writePackageCenterIcons } from "./dsm-icons.js";
|
|
@@ -16,6 +16,7 @@ import { BROKER_INSTALLER_VARS, BROKER_SOCKET_REQUIRED_SCRIPT, renderBrokerStatu
|
|
|
16
16
|
import { loadNative } from "../native/load.js";
|
|
17
17
|
import { materializeNativePayload } from "./native-payload.js";
|
|
18
18
|
import { renderNativePrivilege, renderNativeStartStopStatus, renderNativePostuninst, } from "./native-lifecycle.js";
|
|
19
|
+
import { renderResourceConfig, renderResourceWorkerStartStopStatus } from "./resource-worker.js";
|
|
19
20
|
const FILE_MODE = 0o644;
|
|
20
21
|
const SCRIPT_MODE = 0o755;
|
|
21
22
|
const INSTALL_UIFILE = "install_uifile";
|
|
@@ -129,30 +130,56 @@ export async function buildSpk(options) {
|
|
|
129
130
|
rmSync(staging, { recursive: true, force: true });
|
|
130
131
|
}
|
|
131
132
|
}
|
|
133
|
+
const parsedCompose = loadCompose(options.project.composePath);
|
|
134
|
+
const route = parsedCompose?.compose
|
|
135
|
+
? classifyComposeRoute(contract, parsedCompose)
|
|
136
|
+
: undefined;
|
|
137
|
+
if (route && route.errors.length > 0) {
|
|
138
|
+
return { ok: false, errors: route.errors.map((error) => error.code) };
|
|
139
|
+
}
|
|
140
|
+
const rwEligibility = route?.eligibility;
|
|
141
|
+
const isDynamicRoute = route?.route === "dynamic" &&
|
|
142
|
+
rwEligibility?.adminport !== undefined &&
|
|
143
|
+
collectInstallParameters(contract).length > 0;
|
|
144
|
+
const isResourceWorkerRoute = route?.route === "static-zero-config" ||
|
|
145
|
+
isDynamicRoute;
|
|
132
146
|
const staging = mkdtempSync(join(tmpdir(), "syno-spk-"));
|
|
133
147
|
try {
|
|
134
|
-
writeFileSync(join(staging, "INFO"), renderInfo(contract, options.archFamily), "utf8");
|
|
148
|
+
writeFileSync(join(staging, "INFO"), renderInfo(contract, options.archFamily, isResourceWorkerRoute), "utf8");
|
|
135
149
|
writePackageCenterIcons(staging);
|
|
136
150
|
mkdirSync(join(staging, "conf"), { recursive: true });
|
|
137
151
|
writeFileSync(join(staging, "conf", "privilege"), renderPrivilege(), "utf8");
|
|
138
152
|
writeFileSync(join(staging, "privilege"), renderPrivilege(), "utf8");
|
|
153
|
+
if (isResourceWorkerRoute) {
|
|
154
|
+
const resourceConfig = renderResourceConfig(contract, rwEligibility);
|
|
155
|
+
writeFileSync(join(staging, "conf", "resource"), resourceConfig, "utf8");
|
|
156
|
+
chmodSync(join(staging, "conf", "resource"), FILE_MODE);
|
|
157
|
+
}
|
|
139
158
|
mkdirSync(join(staging, "WIZARD_UIFILES"), { recursive: true });
|
|
140
|
-
const parsedCompose = loadCompose(options.project.composePath);
|
|
141
159
|
writeFileSync(join(staging, "WIZARD_UIFILES", INSTALL_UIFILE), JSON.stringify(renderInstallWizard(contract, parsedCompose?.compose), null, 2), "utf8");
|
|
142
160
|
writeFileSync(join(staging, "WIZARD_UIFILES", UPGRADE_UIFILE), JSON.stringify(renderUpgradeWizard(contract, parsedCompose?.compose), null, 2), "utf8");
|
|
143
161
|
writeFileSync(join(staging, "WIZARD_UIFILES", UNINSTALL_UIFILE), JSON.stringify(renderUninstallWizard(), null, 2), "utf8");
|
|
144
162
|
mkdirSync(join(staging, "scripts"), { recursive: true });
|
|
145
163
|
for (const name of SCRIPT_NAMES) {
|
|
146
164
|
const scriptPath = join(staging, "scripts", name);
|
|
147
|
-
writeFileSync(scriptPath, renderManagedScript(name, contract), "utf8");
|
|
165
|
+
writeFileSync(scriptPath, renderManagedScript(name, contract, undefined, isResourceWorkerRoute, rwEligibility?.adminport, isDynamicRoute), "utf8");
|
|
148
166
|
chmodSync(scriptPath, SCRIPT_MODE);
|
|
149
167
|
}
|
|
150
168
|
mkdirSync(join(staging, "package"), { recursive: true });
|
|
151
169
|
copyFileSync(options.project.composePath, join(staging, "package", "compose.yaml"));
|
|
152
170
|
copyFileSync(options.project.contractPath, join(staging, "package", "synology-app.yaml"));
|
|
171
|
+
if (isResourceWorkerRoute) {
|
|
172
|
+
const resourceProjectDir = join(staging, "package", "compose");
|
|
173
|
+
mkdirSync(resourceProjectDir, { recursive: true });
|
|
174
|
+
copyFileSync(options.project.composePath, join(resourceProjectDir, "docker-compose.yml"));
|
|
175
|
+
chmodSync(join(resourceProjectDir, "docker-compose.yml"), FILE_MODE);
|
|
176
|
+
}
|
|
153
177
|
const htmlDir = join(options.project.root, "html");
|
|
154
178
|
if (existsSync(htmlDir)) {
|
|
155
179
|
cpSync(htmlDir, join(staging, "package", "html"), { recursive: true });
|
|
180
|
+
if (isResourceWorkerRoute) {
|
|
181
|
+
cpSync(htmlDir, join(staging, "package", "compose", "html"), { recursive: true });
|
|
182
|
+
}
|
|
156
183
|
}
|
|
157
184
|
if (options.embeddedAttestationToken !== undefined) {
|
|
158
185
|
const attestationPath = join(staging, "package", EMBEDDED_ATTESTATION_RELATIVE_PATH);
|
|
@@ -190,7 +217,7 @@ function quoteInfo(value) {
|
|
|
190
217
|
function spkVersion(version) {
|
|
191
218
|
return /-\d+$/.test(version) ? version : `${version}-0001`;
|
|
192
219
|
}
|
|
193
|
-
function renderInfo(contract, archFamily) {
|
|
220
|
+
function renderInfo(contract, archFamily, isResourceWorker = false) {
|
|
194
221
|
const lines = [];
|
|
195
222
|
const add = (key, value) => {
|
|
196
223
|
lines.push(`${key}=${quoteInfo(String(value))}`);
|
|
@@ -202,10 +229,22 @@ function renderInfo(contract, archFamily) {
|
|
|
202
229
|
add("maintainer", String(contract.maintainer));
|
|
203
230
|
add("arch", archFamily === undefined ? "noarch" : infoArchValue(archFamily));
|
|
204
231
|
add("os_min_ver", String(contract.os_min_ver));
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
232
|
+
if (isResourceWorker) {
|
|
233
|
+
// DSM reports Container Manager as "not ready" through the package
|
|
234
|
+
// dependency gate even while pkgctl-ContainerManager is running. The
|
|
235
|
+
// docker-project worker itself remains the authoritative live prerequisite
|
|
236
|
+
// (ContainerManager >= 1432); keep only the reliable Web Station package
|
|
237
|
+
// dependency here so a valid resource-worker SPK can reach acquisition.
|
|
238
|
+
if (contract.adminport !== undefined) {
|
|
239
|
+
add("install_dep_packages", "WebStation");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
// synopkg's Container Manager dep gate reports "not ready" even when CM is
|
|
244
|
+
// running. Depend on Broker only; Broker's adapter.probe() checks CM at
|
|
245
|
+
// App install time (CM_CAPABILITY_MISSING).
|
|
246
|
+
add("install_dep_packages", "SynoPkgLandService>=0.1.0-0016");
|
|
247
|
+
}
|
|
209
248
|
add("startable", "yes");
|
|
210
249
|
const launch = resolvedWebLaunch(contract);
|
|
211
250
|
if (contract.adminprotocol) {
|
|
@@ -243,6 +282,7 @@ function renderPrivilege() {
|
|
|
243
282
|
}, null, 2)}\n`;
|
|
244
283
|
}
|
|
245
284
|
const SHARED_FOLDER_MOUNT_CONSENT = "此 App 會用你在這台 NAS 選的資料夾;整顆應用都看得到。Shared Folder Mount 是整個 App Instance 的 container access:能打開這個 App 的使用者,容器裡的程式都看得到該 Share,即使該使用者在 File Station 看不到。這不是逐使用者 DSM ACL。要依登入者權限讀寫 NAS 檔案,必須另外核准廣域 files.* 並走 File Gateway。";
|
|
285
|
+
const HOMES_MOUNT_CONSENT = "此 App 會使用這台 NAS 已啟用的 User Home;整顆應用的容器都看得到,容器路徑由 Compose 決定。這不是逐使用者 DSM ACL;要依登入者權限讀寫 NAS 檔案,必須另外核准廣域 files.* 並走 File Gateway。";
|
|
246
286
|
const DSM_USER_RUN_AS_CONSENT = "寫進 Share 的新檔在 File Station 顯示為你選的那位 DSM 使用者。點開此 App 的人共用這個磁碟身分,不是逐登入 ACL。";
|
|
247
287
|
const DSM_USER_UPGRADE_CONSENT = "升級時更換 Container Run-as 不會 chown Share 上的舊檔;只有升級後寫入的新檔才會使用新的磁碟身分。";
|
|
248
288
|
const SHARED_FOLDER_RW_UPGRADE_CONSENT = "此版本把 Share 掛載改為讀寫;容器可讀取並寫入你選的 Share,不再是唯讀。";
|
|
@@ -328,6 +368,7 @@ function renderInstallWizard(contract, compose) {
|
|
|
328
368
|
const items = [];
|
|
329
369
|
const params = collectInstallParameters(contract);
|
|
330
370
|
const hasSharedFolder = params.some((item) => item.param.type === "shared_folder");
|
|
371
|
+
const hasHomes = params.some((item) => item.param.type === "homes");
|
|
331
372
|
const hasDsmUser = params.some((item) => item.param.type === "dsm_user");
|
|
332
373
|
const capabilityText = renderFilesCapability(contract);
|
|
333
374
|
if (compose && scanComposePrivilege(compose).length > 0) {
|
|
@@ -345,6 +386,11 @@ function renderInstallWizard(contract, compose) {
|
|
|
345
386
|
desc: SHARED_FOLDER_MOUNT_CONSENT,
|
|
346
387
|
});
|
|
347
388
|
}
|
|
389
|
+
if (hasHomes) {
|
|
390
|
+
items.push({
|
|
391
|
+
desc: HOMES_MOUNT_CONSENT,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
348
394
|
if (hasDsmUser) {
|
|
349
395
|
items.push({
|
|
350
396
|
desc: DSM_USER_RUN_AS_CONSENT,
|
|
@@ -515,7 +561,128 @@ function renderUninstallWizard() {
|
|
|
515
561
|
],
|
|
516
562
|
}];
|
|
517
563
|
}
|
|
518
|
-
function
|
|
564
|
+
function renderDynamicPreinstMaterializer(contract) {
|
|
565
|
+
const params = collectInstallParameters(contract).filter(({ param }) => param.type === "string" || param.type === "boolean");
|
|
566
|
+
const lines = [
|
|
567
|
+
"#!/bin/sh",
|
|
568
|
+
"set -eu",
|
|
569
|
+
"umask 077",
|
|
570
|
+
"# D3 package-only carrier; values are wizard answers, canonical values, or defaults.",
|
|
571
|
+
"die() { echo \"dynamic route materialization failed: $1\" >&2; exit 1; }",
|
|
572
|
+
'T="${SYNOPKG_PKGINST_TEMP_DIR:?}"',
|
|
573
|
+
'STATUS="${SYNOPKG_PKG_STATUS:?}"',
|
|
574
|
+
'PROJECT="$T/compose"',
|
|
575
|
+
'RUNTIME_ENV="$PROJECT/.env"',
|
|
576
|
+
'CANONICAL_ENV="${SYNOPKG_PKGDEST_VOL:?}/@appdata/${SYNOPKG_PKGNAME:?}/.env"',
|
|
577
|
+
'PLATFORM_PKGVAR="${SYNOPKG_PKGDEST_VOL:?}/@appdata/${SYNOPKG_PKGNAME:?}"',
|
|
578
|
+
'PLATFORM_SECRETS="${SYNOPKG_PKGDEST_VOL:?}/@apphome/${SYNOPKG_PKGNAME:?}/secrets"',
|
|
579
|
+
'[ -d "$PROJECT" ] || die "compose directory is missing"',
|
|
580
|
+
'[ ! -L "$PROJECT" ] || die "compose directory is a symlink"',
|
|
581
|
+
'[ "$(stat -c %u "$PROJECT")" = "$(id -u)" ] || die "compose directory owner is not the package user"',
|
|
582
|
+
'[ ! -L "$RUNTIME_ENV" ] || die "runtime file is a symlink"',
|
|
583
|
+
'if [ -e "$RUNTIME_ENV" ] && [ ! -f "$RUNTIME_ENV" ]; then die "runtime path is not a regular file"; fi',
|
|
584
|
+
"read_env_value() {",
|
|
585
|
+
" file=\"$1\"",
|
|
586
|
+
" key=\"$2\"",
|
|
587
|
+
" awk -F= -v key=\"$key\" '$1 == key { value=substr($0, index($0,\"=\")+1); if (value ~ /^\".*\"$/) value=substr(value,2,length(value)-2); print value; found=1; exit } END { if (!found) exit 1 }' \"$file\"",
|
|
588
|
+
"}",
|
|
589
|
+
"append_env() {",
|
|
590
|
+
" key=\"$1\"",
|
|
591
|
+
" value=\"$2\"",
|
|
592
|
+
" type=\"${3:-string}\"",
|
|
593
|
+
" if printf '%s' \"$value\" | LC_ALL=C grep -q '[[:cntrl:]]'; then",
|
|
594
|
+
" die \"control character in environment value $key\"",
|
|
595
|
+
" fi",
|
|
596
|
+
" case \"$value\" in",
|
|
597
|
+
" *[[:cntrl:]]*) die \"control character in environment value $key\" ;;",
|
|
598
|
+
" esac",
|
|
599
|
+
" if [ \"$type\" = \"boolean\" ]; then",
|
|
600
|
+
" normalized=\"$(printf '%s' \"$value\" | LC_ALL=C tr '[:upper:]' '[:lower:]')\"",
|
|
601
|
+
" case \"$normalized\" in",
|
|
602
|
+
" true|false) value=\"$normalized\" ;;",
|
|
603
|
+
" *) die \"invalid boolean value $key\" ;;",
|
|
604
|
+
" esac",
|
|
605
|
+
" fi",
|
|
606
|
+
" case \"$value\" in",
|
|
607
|
+
" *[!A-Za-z0-9_./:@-]*)",
|
|
608
|
+
" escaped=\"$(printf '%s' \"$value\" | sed 's/\\\\/\\\\\\\\/g; s/\"/\\\\\"/g')\"",
|
|
609
|
+
" printf '%s=\"%s\"\\n' \"$key\" \"$escaped\"",
|
|
610
|
+
" ;;",
|
|
611
|
+
" *) printf '%s=%s\\n' \"$key\" \"$value\" ;;",
|
|
612
|
+
" esac",
|
|
613
|
+
"}",
|
|
614
|
+
'tmp="$(mktemp "$PROJECT/.env.tmp.XXXXXX")"',
|
|
615
|
+
'chmod 600 "$tmp"',
|
|
616
|
+
'[ "$(stat -c %a "$tmp")" = "600" ] || die "temporary env mode is not 0600"',
|
|
617
|
+
'[ "$(stat -c %u "$tmp")" = "$(id -u)" ] || die "temporary env owner is not the package user"',
|
|
618
|
+
"trap 'rm -f \"$tmp\"' EXIT HUP INT TERM",
|
|
619
|
+
"{",
|
|
620
|
+
];
|
|
621
|
+
const valueVars = [];
|
|
622
|
+
for (const { name, param } of params) {
|
|
623
|
+
const type = param.type;
|
|
624
|
+
const valueVar = "D3_VALUE_" + name;
|
|
625
|
+
valueVars.push({ name, valueVar, type });
|
|
626
|
+
const fallback = param.default !== undefined
|
|
627
|
+
? String(param.default)
|
|
628
|
+
: type === "boolean"
|
|
629
|
+
? "false"
|
|
630
|
+
: "";
|
|
631
|
+
lines.push(` if [ "\${${name}+x}" = x ]; then`, ` ${valueVar}="\${${name}}"`, ' elif [ "$STATUS" = "UPGRADE" ] && [ -f "$CANONICAL_ENV" ] && [ ! -L "$CANONICAL_ENV" ] && ' +
|
|
632
|
+
`${valueVar}="$(read_env_value "$CANONICAL_ENV" "${name}")"; then`, " :", " else", " " + valueVar + "=" + shellQuote(fallback), " fi");
|
|
633
|
+
}
|
|
634
|
+
lines.push(' append_env "SYNOPKG_PKGVAR" "$PLATFORM_PKGVAR"', ' append_env "SYNO_APP_SECRETS_DIR" "$PLATFORM_SECRETS"');
|
|
635
|
+
for (const { name, valueVar, type } of valueVars) {
|
|
636
|
+
lines.push(` append_env "${name}" "$${valueVar}" "${type}"`);
|
|
637
|
+
}
|
|
638
|
+
lines.push(" } > \"$tmp\"", " mv \"$tmp\" \"$RUNTIME_ENV\"", " trap - EXIT HUP INT TERM", " [ ! -L \"$RUNTIME_ENV\" ] || die \"runtime file became a symlink\"", " [ \"$(stat -c %a \"$RUNTIME_ENV\")\" = \"600\" ] || die \"runtime env mode is not 0600\"", "exit 0");
|
|
639
|
+
return lines.join("\n") + "\n";
|
|
640
|
+
}
|
|
641
|
+
function renderDynamicCanonicalMirror() {
|
|
642
|
+
const lines = [
|
|
643
|
+
"#!/bin/sh",
|
|
644
|
+
"set -eu",
|
|
645
|
+
"umask 077",
|
|
646
|
+
"# D3 post-worker mirror; the runtime .env is the only source of truth here.",
|
|
647
|
+
"die() { echo \"dynamic route canonical mirror failed: $1\" >&2; exit 1; }",
|
|
648
|
+
'PKGVAR="${SYNOPKG_PKGVAR:?}"',
|
|
649
|
+
'PKGHOME="${SYNOPKG_PKGHOME:?}"',
|
|
650
|
+
'PKGDEST="${SYNOPKG_PKGDEST:?}"',
|
|
651
|
+
'PROJECT="${PKGDEST}/compose"',
|
|
652
|
+
'RUNTIME_ENV="${PROJECT}/.env"',
|
|
653
|
+
'CANONICAL_ENV="${PKGVAR}/.env"',
|
|
654
|
+
'SECRETS="${PKGHOME}/secrets"',
|
|
655
|
+
'mkdir -p "$PKGVAR"',
|
|
656
|
+
'[ -d "$PKGVAR" ] || die "PKGVAR is not a directory"',
|
|
657
|
+
'if [ -L "$SECRETS" ]; then die "secret root is a symlink"; fi',
|
|
658
|
+
'mkdir -p "$SECRETS"',
|
|
659
|
+
'[ -d "$SECRETS" ] || die "secret root is not a directory"',
|
|
660
|
+
'chmod 700 "$SECRETS"',
|
|
661
|
+
'[ "$(stat -c %a "$SECRETS")" = "700" ] || die "secret root mode is not 0700"',
|
|
662
|
+
'[ -d "$PROJECT" ] || die "project directory is missing"',
|
|
663
|
+
'[ ! -L "$PROJECT" ] || die "project directory is a symlink"',
|
|
664
|
+
'[ ! -L "$RUNTIME_ENV" ] || die "runtime file is a symlink"',
|
|
665
|
+
'[ -f "$RUNTIME_ENV" ] || die "runtime file is not a regular file"',
|
|
666
|
+
'[ ! -L "$CANONICAL_ENV" ] || die "canonical file is a symlink"',
|
|
667
|
+
'if [ -e "$CANONICAL_ENV" ] && [ ! -f "$CANONICAL_ENV" ]; then die "canonical path is not a regular file"; fi',
|
|
668
|
+
'tmp="$(mktemp "$PKGVAR/.env.tmp.XXXXXX")"',
|
|
669
|
+
'chmod 600 "$tmp"',
|
|
670
|
+
'[ "$(stat -c %a "$tmp")" = "600" ] || die "temporary env mode is not 0600"',
|
|
671
|
+
'trap \'rm -f "$tmp"\' EXIT HUP INT TERM',
|
|
672
|
+
'cp "$RUNTIME_ENV" "$tmp"',
|
|
673
|
+
'chmod 600 "$tmp"',
|
|
674
|
+
'mv "$tmp" "$CANONICAL_ENV"',
|
|
675
|
+
'trap - EXIT HUP INT TERM',
|
|
676
|
+
'[ ! -L "$CANONICAL_ENV" ] || die "canonical file became a symlink"',
|
|
677
|
+
'[ "$(stat -c %a "$CANONICAL_ENV")" = "600" ] || die "canonical env mode is not 0600"',
|
|
678
|
+
"exit 0",
|
|
679
|
+
];
|
|
680
|
+
return lines.join("\n") + "\n";
|
|
681
|
+
}
|
|
682
|
+
function shellQuote(value) {
|
|
683
|
+
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
684
|
+
}
|
|
685
|
+
function renderManagedScript(name, contract, nativeContext, isResourceWorker = false, resourceWorkerAdminport, isDynamicRoute = false) {
|
|
519
686
|
if (nativeContext) {
|
|
520
687
|
const lifecycleInput = {
|
|
521
688
|
packageIdentity: String(contract.package),
|
|
@@ -553,6 +720,31 @@ fi
|
|
|
553
720
|
}
|
|
554
721
|
return `#!/bin/sh\nexit 0\n`;
|
|
555
722
|
}
|
|
723
|
+
if (isResourceWorker) {
|
|
724
|
+
if (name === "start-stop-status") {
|
|
725
|
+
return renderResourceWorkerStartStopStatus(resourceWorkerAdminport);
|
|
726
|
+
}
|
|
727
|
+
if (isDynamicRoute && name === "preinst") {
|
|
728
|
+
return renderDynamicPreinstMaterializer(contract);
|
|
729
|
+
}
|
|
730
|
+
if (isDynamicRoute && (name === "postinst" || name === "postupgrade")) {
|
|
731
|
+
return renderDynamicCanonicalMirror();
|
|
732
|
+
}
|
|
733
|
+
if (name === "postuninst") {
|
|
734
|
+
const projectRoot = isDynamicRoute ? 'PROJECT="${SYNOPKG_PKGDEST}/compose"\n' : "";
|
|
735
|
+
const deleteRoots = isDynamicRoute
|
|
736
|
+
? ["PKGVAR", "PROJECT", "SECRETS"]
|
|
737
|
+
: ["PKGVAR", "SECRETS"];
|
|
738
|
+
return `#!/bin/sh
|
|
739
|
+
set -eu
|
|
740
|
+
PKGVAR="\${SYNOPKG_PKGVAR}"
|
|
741
|
+
PKGHOME="\${SYNOPKG_PKGHOME}"
|
|
742
|
+
SECRETS="\${PKGHOME}/secrets"
|
|
743
|
+
${projectRoot}${renderDeleteAppDataBlock(deleteRoots)}exit 0
|
|
744
|
+
`;
|
|
745
|
+
}
|
|
746
|
+
return `#!/bin/sh\nexit 0\n`;
|
|
747
|
+
}
|
|
556
748
|
if (name === "start-stop-status") {
|
|
557
749
|
return `#!/bin/sh
|
|
558
750
|
${BROKER_INSTALLER_VARS}
|