@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.
- package/README.md +13 -0
- package/dist/attestation/app-package.d.ts +86 -0
- package/dist/attestation/app-package.js +199 -0
- package/dist/attestation/install-on-host.d.ts +14 -0
- package/dist/attestation/install-on-host.js +52 -0
- package/dist/attestation/keyring.d.ts +21 -0
- package/dist/attestation/keyring.js +168 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +14 -0
- package/dist/build-spk/dsm-arch.d.ts +4 -0
- package/dist/build-spk/dsm-arch.js +32 -0
- package/dist/build-spk/dsm-icons.d.ts +2 -0
- package/dist/build-spk/dsm-icons.js +67 -0
- package/dist/build-spk/generator.d.ts +43 -0
- package/dist/build-spk/generator.js +653 -0
- package/dist/build-spk/native-payload.d.ts +22 -0
- package/dist/build-spk/native-payload.js +654 -0
- package/dist/build-spk/reproducible-archive.d.ts +8 -0
- package/dist/build-spk/reproducible-archive.js +34 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +345 -0
- package/dist/compose/analyze.d.ts +25 -0
- package/dist/compose/analyze.js +346 -0
- package/dist/compose/env.d.ts +7 -0
- package/dist/compose/env.js +36 -0
- package/dist/compose/privilege.d.ts +14 -0
- package/dist/compose/privilege.js +102 -0
- package/dist/compose/runner.d.ts +32 -0
- package/dist/compose/runner.js +84 -0
- package/dist/contract/load.d.ts +25 -0
- package/dist/contract/load.js +87 -0
- package/dist/contract/schema-validate.d.ts +3 -0
- package/dist/contract/schema-validate.js +50 -0
- package/dist/contract/validate.d.ts +3 -0
- package/dist/contract/validate.js +80 -0
- package/dist/contract/web-launch.d.ts +19 -0
- package/dist/contract/web-launch.js +94 -0
- package/dist/deploy/catalog-publish.d.ts +3 -0
- package/dist/deploy/catalog-publish.js +7 -0
- package/dist/deploy/deploy.d.ts +22 -0
- package/dist/deploy/deploy.js +136 -0
- package/dist/deploy/identity-shared.d.ts +3 -0
- package/dist/deploy/identity-shared.js +14 -0
- package/dist/deploy/identity.d.ts +7 -0
- package/dist/deploy/identity.js +20 -0
- package/dist/deploy/package-source.d.ts +7 -0
- package/dist/deploy/package-source.js +17 -0
- package/dist/deploy/run-as-values.d.ts +18 -0
- package/dist/deploy/run-as-values.js +48 -0
- package/dist/deploy/share-values.d.ts +20 -0
- package/dist/deploy/share-values.js +60 -0
- package/dist/deploy/spk-info.d.ts +12 -0
- package/dist/deploy/spk-info.js +41 -0
- package/dist/deploy/spk-version.d.ts +4 -0
- package/dist/deploy/spk-version.js +64 -0
- package/dist/dev.d.ts +13 -0
- package/dist/dev.js +31 -0
- package/dist/discovery.d.ts +8 -0
- package/dist/discovery.js +22 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +18 -0
- package/dist/host/broker-paths.d.ts +1 -0
- package/dist/host/broker-paths.js +1 -0
- package/dist/host/catalog-install.d.ts +59 -0
- package/dist/host/catalog-install.js +326 -0
- package/dist/host/credentials.d.ts +2 -0
- package/dist/host/credentials.js +37 -0
- package/dist/host/digests.d.ts +5 -0
- package/dist/host/digests.js +55 -0
- package/dist/host/dsm-client.d.ts +6 -0
- package/dist/host/dsm-client.js +290 -0
- package/dist/host/dsm-http.d.ts +1 -0
- package/dist/host/dsm-http.js +87 -0
- package/dist/host/fake-nas.d.ts +16 -0
- package/dist/host/fake-nas.js +116 -0
- package/dist/host/open-url.d.ts +5 -0
- package/dist/host/open-url.js +13 -0
- package/dist/host/share-lookup.d.ts +3 -0
- package/dist/host/share-lookup.js +144 -0
- package/dist/host/types.d.ts +67 -0
- package/dist/host/types.js +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +14 -0
- package/dist/info.d.ts +17 -0
- package/dist/info.js +34 -0
- package/dist/init.d.ts +4 -0
- package/dist/init.js +31 -0
- package/dist/inspect.d.ts +13 -0
- package/dist/inspect.js +102 -0
- package/dist/load-env.d.ts +1 -0
- package/dist/load-env.js +31 -0
- package/dist/log.d.ts +13 -0
- package/dist/log.js +20 -0
- package/dist/mock-broker/bootstrap.d.ts +2 -0
- package/dist/mock-broker/bootstrap.js +8 -0
- package/dist/mock-broker/files.d.ts +60 -0
- package/dist/mock-broker/files.js +408 -0
- package/dist/mock-broker/oidc.d.ts +31 -0
- package/dist/mock-broker/oidc.js +161 -0
- package/dist/mock-broker/path.d.ts +9 -0
- package/dist/mock-broker/path.js +50 -0
- package/dist/mock-broker/server.d.ts +19 -0
- package/dist/mock-broker/server.js +350 -0
- package/dist/publish/build-ledger.d.ts +22 -0
- package/dist/publish/build-ledger.js +26 -0
- package/dist/publish/community-path.d.ts +6 -0
- package/dist/publish/community-path.js +41 -0
- package/dist/publish/curated-release.d.ts +78 -0
- package/dist/publish/curated-release.js +693 -0
- package/dist/publish/file-build-ledger.d.ts +18 -0
- package/dist/publish/file-build-ledger.js +268 -0
- package/dist/publish/github-pr.d.ts +25 -0
- package/dist/publish/github-pr.js +85 -0
- package/dist/publish/github-repo.d.ts +8 -0
- package/dist/publish/github-repo.js +21 -0
- package/dist/publish/publish.d.ts +23 -0
- package/dist/publish/publish.js +128 -0
- package/dist/publish/sync-definition.d.ts +4 -0
- package/dist/publish/sync-definition.js +48 -0
- package/dist/skill.d.ts +10 -0
- package/dist/skill.js +50 -0
- package/dist/templates/compose-import/compose.yaml +11 -0
- package/dist/templates/compose-import/synology-app.yaml +15 -0
- package/dist/templates/hello-files/com.synology.hello.files.dev.spk +0 -0
- package/dist/templates/hello-files/compose.yaml +8 -0
- package/dist/templates/hello-files/synology-app.yaml +13 -0
- package/dist/templates/hello-web/compose.yaml +7 -0
- package/dist/templates/hello-web/html/app.js +23 -0
- package/dist/templates/hello-web/html/index.html +15 -0
- package/dist/templates/hello-web/html/sdk.js +68 -0
- package/dist/templates/hello-web/synology-app.yaml +13 -0
- package/dist/templates/hello-web-page/com.synology.hello.web.page.dev.spk +0 -0
- package/dist/templates/hello-web-page/compose.yaml +7 -0
- package/dist/templates/hello-web-page/html/app.js +23 -0
- package/dist/templates/hello-web-page/html/index.html +15 -0
- package/dist/templates/hello-web-page/html/sdk.js +68 -0
- package/dist/templates/hello-web-page/synology-app.yaml +13 -0
- package/dist/test-command.d.ts +13 -0
- package/dist/test-command.js +26 -0
- package/dist/test-host.d.ts +13 -0
- package/dist/test-host.js +24 -0
- package/dist/types.d.ts +59 -0
- package/dist/types.js +1 -0
- package/dist/validate.d.ts +8 -0
- package/dist/validate.js +103 -0
- package/package.json +38 -0
- package/schema/synology-app.schema.json +137 -0
- package/skills/SKILL.md +51 -0
- package/skills/reference/create.md +34 -0
- package/skills/reference/deploy.md +41 -0
- package/skills/reference/diagnose.md +42 -0
- package/skills/reference/files.md +37 -0
- package/skills/reference/identity.md +31 -0
- package/skills/reference/wrap.md +35 -0
- package/templates/compose-import/compose.yaml +11 -0
- package/templates/compose-import/synology-app.yaml +15 -0
- package/templates/hello-files/com.synology.hello.files.dev.spk +0 -0
- package/templates/hello-files/compose.yaml +8 -0
- package/templates/hello-files/synology-app.yaml +13 -0
- package/templates/hello-web/compose.yaml +7 -0
- package/templates/hello-web/html/app.js +23 -0
- package/templates/hello-web/html/index.html +15 -0
- package/templates/hello-web/html/sdk.js +68 -0
- package/templates/hello-web/synology-app.yaml +13 -0
- package/templates/hello-web-page/com.synology.hello.web.page.dev.spk +0 -0
- package/templates/hello-web-page/compose.yaml +7 -0
- package/templates/hello-web-page/html/app.js +23 -0
- package/templates/hello-web-page/html/index.html +15 -0
- package/templates/hello-web-page/html/sdk.js +68 -0
- package/templates/hello-web-page/synology-app.yaml +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @synopackageland/cli
|
|
2
|
+
|
|
3
|
+
Syno Package Land Developer Kit CLI (`synopkgland`).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
# from repository root
|
|
7
|
+
npm install
|
|
8
|
+
npm run build
|
|
9
|
+
npx synopkgland validate --project packages/app-cli/templates/hello-web
|
|
10
|
+
npx synopkgland init -t hello-web ./my-app
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Day-to-day is `synopkgland init`. GitHub template mirrors (not product repos): [`synopkgland-hello-web`](https://github.com/synopackageland/synopkgland-hello-web) (`dsm-window`) and [`synopkgland-hello-web-page`](https://github.com/synopackageland/synopkgland-hello-web-page) (`new-page`). Curated Packaging Definitions go to [`synopkgland-community-apps`](https://github.com/synopackageland/synopkgland-community-apps).
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { type JWK } from "jose";
|
|
2
|
+
export declare const PLATFORM_ATTESTATION_ISSUER = "synopkgland-curated";
|
|
3
|
+
export declare const APP_PACKAGE_ATTESTATION_TYP = "app-package-attestation";
|
|
4
|
+
export declare const APP_PACKAGE_ATTESTATION_VERSION: 2;
|
|
5
|
+
export declare const CURATED_RELEASE_ATTESTATION_TYP = "curated-release-attestation";
|
|
6
|
+
export declare const CURATED_RELEASE_ATTESTATION_VERSION: 1;
|
|
7
|
+
export interface AppPackageSigningKey {
|
|
8
|
+
kid: string;
|
|
9
|
+
privateKey: CryptoKey;
|
|
10
|
+
}
|
|
11
|
+
export interface LabAttestationKeys extends AppPackageSigningKey {
|
|
12
|
+
publicKey: CryptoKey;
|
|
13
|
+
publicJwk: JWK;
|
|
14
|
+
}
|
|
15
|
+
/** Inputs for the token embedded in the generated SPK. */
|
|
16
|
+
export interface CuratedAppPackageAttestationInput {
|
|
17
|
+
packageIdentity: string;
|
|
18
|
+
packageVersion: string;
|
|
19
|
+
packageBuild: number;
|
|
20
|
+
contractDigest: string;
|
|
21
|
+
bundleDigest: string;
|
|
22
|
+
capabilities: string[];
|
|
23
|
+
}
|
|
24
|
+
/** Inputs for the detached token published beside the final SPK. */
|
|
25
|
+
export interface CuratedReleaseAttestationInput {
|
|
26
|
+
packageIdentity: string;
|
|
27
|
+
packageVersion: string;
|
|
28
|
+
packageBuild: number;
|
|
29
|
+
artifactDigest: string;
|
|
30
|
+
embeddedAttestationDigest: string;
|
|
31
|
+
}
|
|
32
|
+
export interface AppPackageAttestationClaims {
|
|
33
|
+
typ: typeof APP_PACKAGE_ATTESTATION_TYP;
|
|
34
|
+
version: typeof APP_PACKAGE_ATTESTATION_VERSION;
|
|
35
|
+
packageIdentity: string;
|
|
36
|
+
packageVersion: string;
|
|
37
|
+
packageBuild: number;
|
|
38
|
+
contractDigest: string;
|
|
39
|
+
bundleDigest: string;
|
|
40
|
+
capabilities: string[];
|
|
41
|
+
issuedAt: string;
|
|
42
|
+
}
|
|
43
|
+
export interface CuratedReleaseAttestationClaims {
|
|
44
|
+
typ: typeof CURATED_RELEASE_ATTESTATION_TYP;
|
|
45
|
+
version: typeof CURATED_RELEASE_ATTESTATION_VERSION;
|
|
46
|
+
packageIdentity: string;
|
|
47
|
+
packageVersion: string;
|
|
48
|
+
packageBuild: number;
|
|
49
|
+
artifactDigest: string;
|
|
50
|
+
embeddedAttestationDigest: string;
|
|
51
|
+
issuedAt: string;
|
|
52
|
+
}
|
|
53
|
+
export declare function loadOrCreateLabAttestationKeys(filePath: string): Promise<LabAttestationKeys>;
|
|
54
|
+
/**
|
|
55
|
+
* Sign the local development token used by the inner-loop flow.
|
|
56
|
+
* Curated releases must use the v2 function below and never this helper.
|
|
57
|
+
*/
|
|
58
|
+
export declare function signAppPackageAttestationToken(input: {
|
|
59
|
+
packageIdentity: string;
|
|
60
|
+
contractDigest: string;
|
|
61
|
+
bundleDigest: string;
|
|
62
|
+
capabilities: string[];
|
|
63
|
+
}, keys: LabAttestationKeys): Promise<{
|
|
64
|
+
token: string;
|
|
65
|
+
attestationDigest: string;
|
|
66
|
+
}>;
|
|
67
|
+
/** Sign the v2 App Package Attestation embedded in a Curated SPK. */
|
|
68
|
+
export declare function signCuratedAppPackageAttestationToken(input: CuratedAppPackageAttestationInput, key: AppPackageSigningKey): Promise<{
|
|
69
|
+
token: string;
|
|
70
|
+
attestationDigest: string;
|
|
71
|
+
claims: AppPackageAttestationClaims;
|
|
72
|
+
}>;
|
|
73
|
+
/** Sign the v1 detached release attestation for a final SPK archive. */
|
|
74
|
+
export declare function signCuratedReleaseAttestationToken(input: CuratedReleaseAttestationInput, key: AppPackageSigningKey): Promise<{
|
|
75
|
+
token: string;
|
|
76
|
+
attestationDigest: string;
|
|
77
|
+
claims: CuratedReleaseAttestationClaims;
|
|
78
|
+
}>;
|
|
79
|
+
/** Stable JSON representation used by the embedded token's jti binding. */
|
|
80
|
+
export declare function canonicalAppPackageClaims(claims: AppPackageAttestationClaims): string;
|
|
81
|
+
export declare function digestAppPackageClaims(claims: AppPackageAttestationClaims): string;
|
|
82
|
+
/** Stable JSON representation used by the detached token's jti binding. */
|
|
83
|
+
export declare function canonicalCuratedReleaseClaims(claims: CuratedReleaseAttestationClaims): string;
|
|
84
|
+
export declare function digestCuratedReleaseClaims(claims: CuratedReleaseAttestationClaims): string;
|
|
85
|
+
export declare function normalizeCapabilities(capabilities: string[]): string[];
|
|
86
|
+
export declare function assertPackageVersionBinding(packageVersion: string, packageBuild: number): void;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { SignJWT, exportJWK, generateKeyPair, importJWK } from "jose";
|
|
5
|
+
export const PLATFORM_ATTESTATION_ISSUER = "synopkgland-curated";
|
|
6
|
+
export const APP_PACKAGE_ATTESTATION_TYP = "app-package-attestation";
|
|
7
|
+
export const APP_PACKAGE_ATTESTATION_VERSION = 2;
|
|
8
|
+
export const CURATED_RELEASE_ATTESTATION_TYP = "curated-release-attestation";
|
|
9
|
+
export const CURATED_RELEASE_ATTESTATION_VERSION = 1;
|
|
10
|
+
export async function loadOrCreateLabAttestationKeys(filePath) {
|
|
11
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
12
|
+
if (existsSync(filePath)) {
|
|
13
|
+
const parsed = JSON.parse(readFileSync(filePath, "utf8"));
|
|
14
|
+
return {
|
|
15
|
+
kid: parsed.kid,
|
|
16
|
+
privateKey: (await importJWK(parsed.privateJwk, "RS256")),
|
|
17
|
+
publicKey: (await importJWK(parsed.publicJwk, "RS256")),
|
|
18
|
+
publicJwk: parsed.publicJwk,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const { publicKey, privateKey } = await generateKeyPair("RS256", { extractable: true });
|
|
22
|
+
const publicJwk = await exportJWK(publicKey);
|
|
23
|
+
const privateJwk = await exportJWK(privateKey);
|
|
24
|
+
const kid = createHash("sha256").update(JSON.stringify(publicJwk)).digest("hex").slice(0, 16);
|
|
25
|
+
publicJwk.kid = kid;
|
|
26
|
+
privateJwk.kid = kid;
|
|
27
|
+
writeFileSync(filePath, `${JSON.stringify({ kid, privateJwk, publicJwk }, null, 2)}\n`, { mode: 0o600 });
|
|
28
|
+
return { kid, privateKey, publicKey, publicJwk };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Sign the local development token used by the inner-loop flow.
|
|
32
|
+
* Curated releases must use the v2 function below and never this helper.
|
|
33
|
+
*/
|
|
34
|
+
export async function signAppPackageAttestationToken(input, keys) {
|
|
35
|
+
const claims = {
|
|
36
|
+
typ: APP_PACKAGE_ATTESTATION_TYP,
|
|
37
|
+
version: 1,
|
|
38
|
+
packageIdentity: input.packageIdentity,
|
|
39
|
+
contractDigest: input.contractDigest,
|
|
40
|
+
bundleDigest: input.bundleDigest,
|
|
41
|
+
capabilities: normalizeCapabilities(input.capabilities),
|
|
42
|
+
issuedAt: new Date().toISOString(),
|
|
43
|
+
};
|
|
44
|
+
const canonical = JSON.stringify({
|
|
45
|
+
artifactDigest: null,
|
|
46
|
+
bundleDigest: claims.bundleDigest,
|
|
47
|
+
capabilities: claims.capabilities,
|
|
48
|
+
contractDigest: claims.contractDigest,
|
|
49
|
+
issuedAt: claims.issuedAt,
|
|
50
|
+
packageBuild: null,
|
|
51
|
+
packageIdentity: claims.packageIdentity,
|
|
52
|
+
typ: claims.typ,
|
|
53
|
+
version: claims.version,
|
|
54
|
+
});
|
|
55
|
+
const attestationDigest = sha256(canonical);
|
|
56
|
+
const token = await signClaims(claims, keys, attestationDigest);
|
|
57
|
+
return { token, attestationDigest };
|
|
58
|
+
}
|
|
59
|
+
/** Sign the v2 App Package Attestation embedded in a Curated SPK. */
|
|
60
|
+
export async function signCuratedAppPackageAttestationToken(input, key) {
|
|
61
|
+
assertCuratedAppPackageInput(input);
|
|
62
|
+
const claims = {
|
|
63
|
+
typ: APP_PACKAGE_ATTESTATION_TYP,
|
|
64
|
+
version: APP_PACKAGE_ATTESTATION_VERSION,
|
|
65
|
+
packageIdentity: input.packageIdentity,
|
|
66
|
+
packageVersion: input.packageVersion,
|
|
67
|
+
packageBuild: input.packageBuild,
|
|
68
|
+
contractDigest: input.contractDigest,
|
|
69
|
+
bundleDigest: input.bundleDigest,
|
|
70
|
+
capabilities: normalizeCapabilities(input.capabilities),
|
|
71
|
+
issuedAt: new Date().toISOString(),
|
|
72
|
+
};
|
|
73
|
+
const attestationDigest = digestAppPackageClaims(claims);
|
|
74
|
+
const token = await signClaims(claims, key, attestationDigest);
|
|
75
|
+
return { token, attestationDigest, claims };
|
|
76
|
+
}
|
|
77
|
+
/** Sign the v1 detached release attestation for a final SPK archive. */
|
|
78
|
+
export async function signCuratedReleaseAttestationToken(input, key) {
|
|
79
|
+
assertCuratedReleaseInput(input);
|
|
80
|
+
const claims = {
|
|
81
|
+
typ: CURATED_RELEASE_ATTESTATION_TYP,
|
|
82
|
+
version: CURATED_RELEASE_ATTESTATION_VERSION,
|
|
83
|
+
packageIdentity: input.packageIdentity,
|
|
84
|
+
packageVersion: input.packageVersion,
|
|
85
|
+
packageBuild: input.packageBuild,
|
|
86
|
+
artifactDigest: input.artifactDigest,
|
|
87
|
+
embeddedAttestationDigest: input.embeddedAttestationDigest,
|
|
88
|
+
issuedAt: new Date().toISOString(),
|
|
89
|
+
};
|
|
90
|
+
const attestationDigest = digestCuratedReleaseClaims(claims);
|
|
91
|
+
const token = await signClaims(claims, key, attestationDigest);
|
|
92
|
+
return { token, attestationDigest, claims };
|
|
93
|
+
}
|
|
94
|
+
/** Stable JSON representation used by the embedded token's jti binding. */
|
|
95
|
+
export function canonicalAppPackageClaims(claims) {
|
|
96
|
+
return JSON.stringify({
|
|
97
|
+
bundleDigest: claims.bundleDigest,
|
|
98
|
+
capabilities: normalizeCapabilities(claims.capabilities),
|
|
99
|
+
contractDigest: claims.contractDigest,
|
|
100
|
+
issuedAt: claims.issuedAt,
|
|
101
|
+
packageBuild: claims.packageBuild,
|
|
102
|
+
packageIdentity: claims.packageIdentity,
|
|
103
|
+
packageVersion: claims.packageVersion,
|
|
104
|
+
typ: claims.typ,
|
|
105
|
+
version: claims.version,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
export function digestAppPackageClaims(claims) {
|
|
109
|
+
return sha256(canonicalAppPackageClaims(claims));
|
|
110
|
+
}
|
|
111
|
+
/** Stable JSON representation used by the detached token's jti binding. */
|
|
112
|
+
export function canonicalCuratedReleaseClaims(claims) {
|
|
113
|
+
return JSON.stringify({
|
|
114
|
+
artifactDigest: claims.artifactDigest,
|
|
115
|
+
embeddedAttestationDigest: claims.embeddedAttestationDigest,
|
|
116
|
+
issuedAt: claims.issuedAt,
|
|
117
|
+
packageBuild: claims.packageBuild,
|
|
118
|
+
packageIdentity: claims.packageIdentity,
|
|
119
|
+
packageVersion: claims.packageVersion,
|
|
120
|
+
typ: claims.typ,
|
|
121
|
+
version: claims.version,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
export function digestCuratedReleaseClaims(claims) {
|
|
125
|
+
return sha256(canonicalCuratedReleaseClaims(claims));
|
|
126
|
+
}
|
|
127
|
+
export function normalizeCapabilities(capabilities) {
|
|
128
|
+
if (!Array.isArray(capabilities)) {
|
|
129
|
+
throw new Error("capabilities must be an array");
|
|
130
|
+
}
|
|
131
|
+
const normalized = new Set();
|
|
132
|
+
for (const capability of capabilities) {
|
|
133
|
+
if (typeof capability !== "string" || capability.length === 0) {
|
|
134
|
+
throw new Error("capabilities must contain non-empty strings");
|
|
135
|
+
}
|
|
136
|
+
normalized.add(capability);
|
|
137
|
+
}
|
|
138
|
+
return [...normalized].sort();
|
|
139
|
+
}
|
|
140
|
+
export function assertPackageVersionBinding(packageVersion, packageBuild) {
|
|
141
|
+
if (typeof packageVersion !== "string" || packageVersion.length === 0) {
|
|
142
|
+
throw new Error("packageVersion must be a non-empty string");
|
|
143
|
+
}
|
|
144
|
+
const match = /-(\d+)$/.exec(packageVersion);
|
|
145
|
+
if (!match || Number(match[1]) !== packageBuild) {
|
|
146
|
+
throw new Error("packageVersion and packageBuild must agree");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function signClaims(claims, key, attestationDigest) {
|
|
150
|
+
if (typeof key.kid !== "string" || key.kid.length === 0) {
|
|
151
|
+
throw new Error("attestation signing key kid is required");
|
|
152
|
+
}
|
|
153
|
+
const packageIdentity = claims.packageIdentity;
|
|
154
|
+
if (typeof packageIdentity !== "string") {
|
|
155
|
+
throw new Error("attestation package identity is required");
|
|
156
|
+
}
|
|
157
|
+
return new SignJWT({ ...claims })
|
|
158
|
+
.setProtectedHeader({ alg: "RS256", kid: key.kid, typ: "JWT" })
|
|
159
|
+
.setIssuer(PLATFORM_ATTESTATION_ISSUER)
|
|
160
|
+
.setSubject(packageIdentity)
|
|
161
|
+
.setJti(attestationDigest)
|
|
162
|
+
.sign(key.privateKey);
|
|
163
|
+
}
|
|
164
|
+
function assertCuratedAppPackageInput(input) {
|
|
165
|
+
assertIdentityAndVersion(input.packageIdentity, input.packageVersion);
|
|
166
|
+
assertPackageBuild(input.packageBuild);
|
|
167
|
+
assertPackageVersionBinding(input.packageVersion, input.packageBuild);
|
|
168
|
+
assertSha256Digest(input.contractDigest, "contractDigest");
|
|
169
|
+
assertSha256Digest(input.bundleDigest, "bundleDigest");
|
|
170
|
+
normalizeCapabilities(input.capabilities);
|
|
171
|
+
}
|
|
172
|
+
function assertCuratedReleaseInput(input) {
|
|
173
|
+
assertIdentityAndVersion(input.packageIdentity, input.packageVersion);
|
|
174
|
+
assertPackageBuild(input.packageBuild);
|
|
175
|
+
assertPackageVersionBinding(input.packageVersion, input.packageBuild);
|
|
176
|
+
assertSha256Digest(input.artifactDigest, "artifactDigest");
|
|
177
|
+
assertSha256Digest(input.embeddedAttestationDigest, "embeddedAttestationDigest");
|
|
178
|
+
}
|
|
179
|
+
function assertIdentityAndVersion(packageIdentity, packageVersion) {
|
|
180
|
+
if (typeof packageIdentity !== "string" || packageIdentity.trim().length === 0) {
|
|
181
|
+
throw new Error("packageIdentity must be a non-empty string");
|
|
182
|
+
}
|
|
183
|
+
if (typeof packageVersion !== "string" || packageVersion.trim().length === 0) {
|
|
184
|
+
throw new Error("packageVersion must be a non-empty string");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function assertPackageBuild(packageBuild) {
|
|
188
|
+
if (!Number.isSafeInteger(packageBuild) || packageBuild < 1) {
|
|
189
|
+
throw new Error("packageBuild must be a positive safe integer");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function assertSha256Digest(value, name) {
|
|
193
|
+
if (!/^[a-f0-9]{64}$/.test(value)) {
|
|
194
|
+
throw new Error(`${name} must be a lowercase SHA-256 digest`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function sha256(value) {
|
|
198
|
+
return createHash("sha256").update(value).digest("hex");
|
|
199
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { JWK } from "jose";
|
|
2
|
+
import type { HostCredentials } from "../host/types.js";
|
|
3
|
+
export declare function loginDsmCookie(credentials: HostCredentials): Promise<string>;
|
|
4
|
+
export declare function installAppPackageAttestationOnNas(options: {
|
|
5
|
+
credentials: HostCredentials;
|
|
6
|
+
token: string;
|
|
7
|
+
publicJwk: JWK;
|
|
8
|
+
}): Promise<{
|
|
9
|
+
ok: boolean;
|
|
10
|
+
status: number;
|
|
11
|
+
code?: string;
|
|
12
|
+
message?: string;
|
|
13
|
+
kind?: string;
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { IDENTITY_CGI_PATH } from "../host/broker-paths.js";
|
|
2
|
+
export async function loginDsmCookie(credentials) {
|
|
3
|
+
const dsmOrigin = credentials.dsmOrigin.replace(/\/$/, "");
|
|
4
|
+
const query = new URLSearchParams({
|
|
5
|
+
api: "SYNO.API.Auth",
|
|
6
|
+
version: "3",
|
|
7
|
+
method: "login",
|
|
8
|
+
account: credentials.username,
|
|
9
|
+
passwd: credentials.password,
|
|
10
|
+
session: "FileStation",
|
|
11
|
+
format: "cookie",
|
|
12
|
+
});
|
|
13
|
+
const response = await fetch(`${dsmOrigin}/webapi/entry.cgi?${query.toString()}`);
|
|
14
|
+
const setCookie = typeof response.headers.getSetCookie === "function"
|
|
15
|
+
? response.headers.getSetCookie()
|
|
16
|
+
: response.headers.get("set-cookie")
|
|
17
|
+
? [response.headers.get("set-cookie")]
|
|
18
|
+
: [];
|
|
19
|
+
return setCookie
|
|
20
|
+
.map((line) => line.split(";")[0]?.trim())
|
|
21
|
+
.filter((pair) => Boolean(pair))
|
|
22
|
+
.join("; ");
|
|
23
|
+
}
|
|
24
|
+
export async function installAppPackageAttestationOnNas(options) {
|
|
25
|
+
const dsmOrigin = options.credentials.dsmOrigin.replace(/\/$/, "");
|
|
26
|
+
const cookie = await loginDsmCookie(options.credentials);
|
|
27
|
+
const response = await fetch(`${dsmOrigin}${IDENTITY_CGI_PATH}/api/v1/inner-loop/accept-app-package`, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: {
|
|
30
|
+
cookie,
|
|
31
|
+
"content-type": "application/json",
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify({ token: options.token, publicJwk: options.publicJwk }),
|
|
34
|
+
});
|
|
35
|
+
const text = await response.text();
|
|
36
|
+
let body;
|
|
37
|
+
try {
|
|
38
|
+
body = JSON.parse(text);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return { ok: false, status: response.status, code: "INNER_LOOP_API_MISSING", message: text.slice(0, 200) };
|
|
42
|
+
}
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
status: response.status,
|
|
47
|
+
code: body.error?.code,
|
|
48
|
+
message: body.error?.message,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return { ok: true, status: response.status, kind: body.kind };
|
|
52
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type JWK } from "jose";
|
|
2
|
+
export declare const CURATED_ATTESTATION_KEYRING_PATH_ENV = "SYNOPKGLAND_CURATED_ATTESTATION_KEYRING_PATH";
|
|
3
|
+
export declare const CURATED_ATTESTATION_KEYRING_ENV = "SYNOPKGLAND_CURATED_ATTESTATION_KEYRING";
|
|
4
|
+
export type CuratedTrustRootStatus = "active" | "verify-only" | "revoked";
|
|
5
|
+
export type CuratedTrustRootErrorCode = "CURATED_TRUST_ROOT_MISSING" | "CURATED_TRUST_ROOT_INVALID" | "CURATED_SIGNING_KEY_UNKNOWN" | "CURATED_SIGNING_KEY_REVOKED" | "CURATED_SIGNING_KEY_NOT_ACTIVE" | "CURATED_SIGNING_KEY_MISMATCH";
|
|
6
|
+
export interface CuratedTrustRootEntry {
|
|
7
|
+
kid: string;
|
|
8
|
+
status: CuratedTrustRootStatus;
|
|
9
|
+
publicJwk: JWK;
|
|
10
|
+
}
|
|
11
|
+
export interface CuratedTrustRoot {
|
|
12
|
+
version: 1;
|
|
13
|
+
keys: CuratedTrustRootEntry[];
|
|
14
|
+
}
|
|
15
|
+
export declare class CuratedTrustRootError extends Error {
|
|
16
|
+
readonly code: CuratedTrustRootErrorCode;
|
|
17
|
+
constructor(code: CuratedTrustRootErrorCode, message?: string);
|
|
18
|
+
}
|
|
19
|
+
export declare function loadCuratedTrustRoot(env?: NodeJS.Dict<string>): Promise<CuratedTrustRoot>;
|
|
20
|
+
export declare function parseCuratedTrustRoot(value: unknown): CuratedTrustRoot;
|
|
21
|
+
export declare function resolveCuratedSigningTrust(root: CuratedTrustRoot, kid: string, signingPublicJwk: JWK): Promise<CryptoKey>;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { importJWK } from "jose";
|
|
4
|
+
export const CURATED_ATTESTATION_KEYRING_PATH_ENV = "SYNOPKGLAND_CURATED_ATTESTATION_KEYRING_PATH";
|
|
5
|
+
export const CURATED_ATTESTATION_KEYRING_ENV = "SYNOPKGLAND_CURATED_ATTESTATION_KEYRING";
|
|
6
|
+
const TRUST_ROOT_FIELDS = new Set(["version", "keys"]);
|
|
7
|
+
const TRUST_ROOT_ENTRY_FIELDS = new Set(["kid", "status", "publicJwk"]);
|
|
8
|
+
const PUBLIC_JWK_FIELDS = new Set([
|
|
9
|
+
"kty",
|
|
10
|
+
"n",
|
|
11
|
+
"e",
|
|
12
|
+
"alg",
|
|
13
|
+
"use",
|
|
14
|
+
"key_ops",
|
|
15
|
+
"kid",
|
|
16
|
+
"x5c",
|
|
17
|
+
"x5t",
|
|
18
|
+
"x5t#S256",
|
|
19
|
+
]);
|
|
20
|
+
export class CuratedTrustRootError extends Error {
|
|
21
|
+
code;
|
|
22
|
+
constructor(code, message = code) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.name = "CuratedTrustRootError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export async function loadCuratedTrustRoot(env = process.env) {
|
|
29
|
+
const rawFromEnv = env[CURATED_ATTESTATION_KEYRING_ENV]?.trim();
|
|
30
|
+
const pathFromEnv = env[CURATED_ATTESTATION_KEYRING_PATH_ENV]?.trim();
|
|
31
|
+
if (rawFromEnv && pathFromEnv) {
|
|
32
|
+
throw trustRootError("CURATED_TRUST_ROOT_INVALID", "Configure either the curated public keyring JSON or its path, not both.");
|
|
33
|
+
}
|
|
34
|
+
if (!rawFromEnv && !pathFromEnv) {
|
|
35
|
+
throw trustRootError("CURATED_TRUST_ROOT_MISSING", "The release signing custody contract requires a curated public keyring.");
|
|
36
|
+
}
|
|
37
|
+
let raw;
|
|
38
|
+
try {
|
|
39
|
+
raw = rawFromEnv ?? readFileSync(resolve(pathFromEnv), "utf8");
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw trustRootError("CURATED_TRUST_ROOT_INVALID", "Unable to read the curated public keyring.");
|
|
43
|
+
}
|
|
44
|
+
let parsed;
|
|
45
|
+
try {
|
|
46
|
+
parsed = JSON.parse(raw);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
throw trustRootError("CURATED_TRUST_ROOT_INVALID", "The curated public keyring is not valid JSON.");
|
|
50
|
+
}
|
|
51
|
+
return parseCuratedTrustRoot(parsed);
|
|
52
|
+
}
|
|
53
|
+
export function parseCuratedTrustRoot(value) {
|
|
54
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
55
|
+
throw trustRootError("CURATED_TRUST_ROOT_INVALID", "The curated public keyring must be an object.");
|
|
56
|
+
}
|
|
57
|
+
const record = value;
|
|
58
|
+
if (containsPrivateJwkField(record)) {
|
|
59
|
+
throw trustRootError("CURATED_TRUST_ROOT_INVALID", "The curated public keyring contains private RSA material.");
|
|
60
|
+
}
|
|
61
|
+
assertKnownFields(record, TRUST_ROOT_FIELDS, "The curated public keyring");
|
|
62
|
+
if (record.version !== 1 || !Array.isArray(record.keys)) {
|
|
63
|
+
throw trustRootError("CURATED_TRUST_ROOT_INVALID", "The curated public keyring must use schema version 1.");
|
|
64
|
+
}
|
|
65
|
+
const seen = new Set();
|
|
66
|
+
const keys = [];
|
|
67
|
+
for (const rawEntry of record.keys) {
|
|
68
|
+
if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
|
|
69
|
+
throw trustRootError("CURATED_TRUST_ROOT_INVALID", "The curated public keyring contains a malformed entry.");
|
|
70
|
+
}
|
|
71
|
+
const entry = rawEntry;
|
|
72
|
+
assertKnownFields(entry, TRUST_ROOT_ENTRY_FIELDS, "The curated public keyring entry");
|
|
73
|
+
if (typeof entry.kid !== "string" ||
|
|
74
|
+
entry.kid.length === 0 ||
|
|
75
|
+
seen.has(entry.kid) ||
|
|
76
|
+
(entry.status !== "active" && entry.status !== "verify-only" && entry.status !== "revoked") ||
|
|
77
|
+
!entry.publicJwk ||
|
|
78
|
+
typeof entry.publicJwk !== "object" ||
|
|
79
|
+
Array.isArray(entry.publicJwk)) {
|
|
80
|
+
throw trustRootError("CURATED_TRUST_ROOT_INVALID", "The curated public keyring contains an invalid entry.");
|
|
81
|
+
}
|
|
82
|
+
seen.add(entry.kid);
|
|
83
|
+
const publicJwk = entry.publicJwk;
|
|
84
|
+
validatePublicJwk(publicJwk, entry.kid);
|
|
85
|
+
keys.push({ kid: entry.kid, status: entry.status, publicJwk });
|
|
86
|
+
}
|
|
87
|
+
return { version: 1, keys };
|
|
88
|
+
}
|
|
89
|
+
export async function resolveCuratedSigningTrust(root, kid, signingPublicJwk) {
|
|
90
|
+
if (root.keys.length === 0) {
|
|
91
|
+
throw trustRootError("CURATED_TRUST_ROOT_MISSING", "The curated public keyring contains no trusted keys.");
|
|
92
|
+
}
|
|
93
|
+
const entry = root.keys.find((candidate) => candidate.kid === kid);
|
|
94
|
+
if (!entry) {
|
|
95
|
+
throw trustRootError("CURATED_SIGNING_KEY_UNKNOWN", `The signing kid is not in the curated keyring: ${kid}`);
|
|
96
|
+
}
|
|
97
|
+
if (entry.status === "revoked") {
|
|
98
|
+
throw trustRootError("CURATED_SIGNING_KEY_REVOKED", `The signing kid is revoked: ${kid}`);
|
|
99
|
+
}
|
|
100
|
+
if (entry.status !== "active") {
|
|
101
|
+
throw trustRootError("CURATED_SIGNING_KEY_NOT_ACTIVE", `The signing kid is not active: ${kid}`);
|
|
102
|
+
}
|
|
103
|
+
if (signingPublicJwk.kty !== entry.publicJwk.kty ||
|
|
104
|
+
signingPublicJwk.n !== entry.publicJwk.n ||
|
|
105
|
+
signingPublicJwk.e !== entry.publicJwk.e) {
|
|
106
|
+
throw trustRootError("CURATED_SIGNING_KEY_MISMATCH", `The signing material does not match curated kid: ${kid}`);
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
return (await importJWK(entry.publicJwk, "RS256"));
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
throw trustRootError("CURATED_TRUST_ROOT_INVALID", "The curated public keyring key could not be imported.");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function validatePublicJwk(jwk, expectedKid) {
|
|
116
|
+
if (!jwk || typeof jwk !== "object") {
|
|
117
|
+
throw trustRootError("CURATED_TRUST_ROOT_INVALID", "The curated key is not a JWK object.");
|
|
118
|
+
}
|
|
119
|
+
const record = jwk;
|
|
120
|
+
if (containsPrivateJwkField(record)) {
|
|
121
|
+
throw trustRootError("CURATED_TRUST_ROOT_INVALID", "The curated keyring contains private RSA material.");
|
|
122
|
+
}
|
|
123
|
+
assertKnownFields(record, PUBLIC_JWK_FIELDS, "Curated trust-root JWK");
|
|
124
|
+
if (record.kty !== "RSA" ||
|
|
125
|
+
typeof record.n !== "string" ||
|
|
126
|
+
typeof record.e !== "string" ||
|
|
127
|
+
record.alg !== "RS256" ||
|
|
128
|
+
record.kid !== expectedKid) {
|
|
129
|
+
throw trustRootError("CURATED_TRUST_ROOT_INVALID", "Curated trust-root keys must be public RS256 RSA JWKs with matching kid.");
|
|
130
|
+
}
|
|
131
|
+
if (record.use !== undefined && record.use !== "sig") {
|
|
132
|
+
throw trustRootError("CURATED_TRUST_ROOT_INVALID", "Curated trust-root keys must use sig.");
|
|
133
|
+
}
|
|
134
|
+
if (record.key_ops !== undefined &&
|
|
135
|
+
(!Array.isArray(record.key_ops) ||
|
|
136
|
+
record.key_ops.length === 0 ||
|
|
137
|
+
new Set(record.key_ops).size !== record.key_ops.length ||
|
|
138
|
+
record.key_ops.some((operation) => operation !== "verify"))) {
|
|
139
|
+
throw trustRootError("CURATED_TRUST_ROOT_INVALID", "Curated trust-root keys may only verify signatures.");
|
|
140
|
+
}
|
|
141
|
+
if (record.x5c !== undefined &&
|
|
142
|
+
(!Array.isArray(record.x5c) || record.x5c.some((certificate) => typeof certificate !== "string"))) {
|
|
143
|
+
throw trustRootError("CURATED_TRUST_ROOT_INVALID", "Curated trust-root x5c must be an array of strings.");
|
|
144
|
+
}
|
|
145
|
+
if ((record.x5t !== undefined && typeof record.x5t !== "string") ||
|
|
146
|
+
(record["x5t#S256"] !== undefined && typeof record["x5t#S256"] !== "string")) {
|
|
147
|
+
throw trustRootError("CURATED_TRUST_ROOT_INVALID", "Curated trust-root thumbprints must be strings.");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function containsPrivateJwkField(value) {
|
|
151
|
+
if (!value || typeof value !== "object") {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
if (Array.isArray(value)) {
|
|
155
|
+
return value.some((item) => containsPrivateJwkField(item));
|
|
156
|
+
}
|
|
157
|
+
const record = value;
|
|
158
|
+
return ["d", "p", "q", "dp", "dq", "qi", "oth"].some((field) => Object.prototype.hasOwnProperty.call(record, field)) || Object.values(record).some((nested) => containsPrivateJwkField(nested));
|
|
159
|
+
}
|
|
160
|
+
function assertKnownFields(record, allowed, label) {
|
|
161
|
+
const unknown = Object.keys(record).find((key) => !allowed.has(key));
|
|
162
|
+
if (unknown) {
|
|
163
|
+
throw trustRootError("CURATED_TRUST_ROOT_INVALID", `${label} contains unknown field: ${unknown}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function trustRootError(code, message) {
|
|
167
|
+
return new CuratedTrustRootError(code, message);
|
|
168
|
+
}
|
package/dist/bin.d.ts
ADDED
package/dist/bin.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { loadDotEnv } from "./load-env.js";
|
|
3
|
+
import { runCli } from "./cli.js";
|
|
4
|
+
import { runCuratedReleaseCli } from "./publish/curated-release.js";
|
|
5
|
+
loadDotEnv();
|
|
6
|
+
const execution = process.argv[2] === "release"
|
|
7
|
+
? runCuratedReleaseCli(process.argv)
|
|
8
|
+
: runCli(process.argv).then(() => 0);
|
|
9
|
+
execution.then((status) => {
|
|
10
|
+
process.exitCode = status;
|
|
11
|
+
}).catch((error) => {
|
|
12
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
13
|
+
process.exit(1);
|
|
14
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type NativeArchFamily = "x86_64" | "aarch64";
|
|
2
|
+
export declare const NATIVE_ARCH_PLATFORMS: Record<NativeArchFamily, readonly string[]>;
|
|
3
|
+
export declare function isNativeArchFamily(value: string): value is NativeArchFamily;
|
|
4
|
+
export declare function infoArchValue(family: NativeArchFamily): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const NATIVE_ARCH_PLATFORMS = {
|
|
2
|
+
x86_64: [
|
|
3
|
+
"apollolake",
|
|
4
|
+
"avoton",
|
|
5
|
+
"braswell",
|
|
6
|
+
"broadwell",
|
|
7
|
+
"broadwellnk",
|
|
8
|
+
"broadwellnkv2",
|
|
9
|
+
"broadwellntbap",
|
|
10
|
+
"bromolow",
|
|
11
|
+
"cedarview",
|
|
12
|
+
"denverton",
|
|
13
|
+
"epyc7002",
|
|
14
|
+
"geminilake",
|
|
15
|
+
"geminilakenk",
|
|
16
|
+
"grantley",
|
|
17
|
+
"icelaked",
|
|
18
|
+
"kvmx64",
|
|
19
|
+
"purley",
|
|
20
|
+
"r1000",
|
|
21
|
+
"r1000nk",
|
|
22
|
+
"v1000",
|
|
23
|
+
"v1000nk",
|
|
24
|
+
],
|
|
25
|
+
aarch64: ["armada37xx", "rtd1296", "rtd1619b"],
|
|
26
|
+
};
|
|
27
|
+
export function isNativeArchFamily(value) {
|
|
28
|
+
return value === "x86_64" || value === "aarch64";
|
|
29
|
+
}
|
|
30
|
+
export function infoArchValue(family) {
|
|
31
|
+
return NATIVE_ARCH_PLATFORMS[family].join(" ");
|
|
32
|
+
}
|