@rightkit/release 0.3.0 → 0.4.1
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 +74 -10
- package/addon-command.mjs +3 -4
- package/adoption-evidence.mjs +650 -0
- package/android-play-upload.mjs +2 -1
- package/apple-profile-create.mjs +113 -10
- package/apple-profile-renew.mjs +11 -3
- package/artifact-storage.mjs +35 -2
- package/bin/darwin/rightkit-swap +0 -0
- package/binary-license-notices.mjs +161 -0
- package/build-release.mjs +138 -47
- package/cargo-license-notices.mjs +111 -0
- package/cli/right-release.mjs +21 -2
- package/codesign-environment.mjs +2 -2
- package/content-digest.mjs +209 -0
- package/development.mjs +17 -6
- package/direct-bootstrap.mjs +5 -6
- package/disk-command.mjs +1 -0
- package/disk-policy.json +6 -2
- package/disk-stable-root.mjs +13 -9
- package/github-updater-release.mjs +70 -7
- package/in-place-install.mjs +17 -14
- package/install-app-windows.mjs +24 -7
- package/install-app.mjs +21 -40
- package/ios-asc-credentials.mjs +163 -0
- package/ios-build-context.mjs +129 -0
- package/ios-build-upload.mjs +282 -0
- package/ios-credentials-command.mjs +70 -0
- package/ios-dev.mjs +248 -0
- package/ios-host.mjs +76 -0
- package/ios-lock.mjs +75 -0
- package/ios-package-recipe.mjs +707 -0
- package/ios-testflight.mjs +266 -0
- package/ios-upload.mjs +313 -0
- package/local-publish.mjs +222 -53
- package/managed-cargo.mjs +8 -0
- package/mirror-root-artifact.mjs +5 -1
- package/native-cargo-layout.mjs +47 -4
- package/ownership-command.mjs +2 -1
- package/ownership-dependency-allowlist.json +3 -1
- package/ownership-evidence.mjs +30 -18
- package/ownership-key-command.mjs +69 -2
- package/ownership-mandatory-set.json +117 -67
- package/ownership-mandatory-set.mjs +141 -21
- package/ownership-repositories.json +1 -1
- package/ownership-scope.mjs +94 -0
- package/ownership.mjs +49 -7
- package/package-recipe-targets.mjs +41 -1
- package/package.json +1 -1
- package/pipeline-admission.mjs +375 -0
- package/pipeline-config.mjs +100 -0
- package/pipeline-generators.json +4 -0
- package/pipeline-generators.mjs +137 -0
- package/pipeline-scan.mjs +285 -0
- package/pipeline-tools.mjs +171 -0
- package/preflight.mjs +14 -7
- package/production-mode.mjs +42 -0
- package/project-type.mjs +3 -1
- package/protected-credentials.mjs +28 -1
- package/publication-artifacts.mjs +30 -5
- package/publication-gate.mjs +94 -32
- package/publish-cargo.mjs +117 -25
- package/publish-swift.mjs +12 -3
- package/publish-update.mjs +15 -2
- package/qa-build-features.mjs +16 -13
- package/qa-contract.mjs +30 -2
- package/qa-control-scan.mjs +8 -0
- package/release-config.mjs +330 -0
- package/release-invocation.mjs +118 -8
- package/release-switch-registry.json +126 -27
- package/release-switch-scan.mjs +88 -21
- package/release.mjs +82 -20
- package/rightkit-ownership.json +1 -1
- package/rightkit-sync.mjs +8 -0
- package/rightkit-versions.json +428 -194
- package/runtime-budget-crates.mjs +14 -0
- package/runtime-budgets.mjs +248 -47
- package/sign-updater.mjs +56 -59
- package/source-gate.mjs +8 -1
- package/stage-sidecar.mjs +207 -40
- package/swap-helper-bundle.mjs +7 -3
- package/target-distribution.mjs +5 -0
- package/upload-release.mjs +7 -2
- package/bin/darwin/rightkit-swap.sign.json +0 -6
package/android-play-upload.mjs
CHANGED
|
@@ -27,6 +27,7 @@ import { pathToFileURL } from "node:url";
|
|
|
27
27
|
import { spawnSync } from "./background-process.mjs";
|
|
28
28
|
import { describeAabProblems, scanAabBuffer } from "./android-aab-scan.mjs";
|
|
29
29
|
import { sealedManifestAllowsContentDevtools } from "./devtools-policy.mjs";
|
|
30
|
+
import { targetDistribution } from "./target-distribution.mjs";
|
|
30
31
|
import { AndroidReleaseError, createAndroidCredentialStore, resolveAndroidSecret, validateSecretSource } from "./android-release.mjs";
|
|
31
32
|
import { assertArtifactUnchanged, digestBytes, verifyPublicationRelease } from "./publication-artifacts.mjs";
|
|
32
33
|
import { assertPrimaryReleaseCheckout } from "./release-invocation.mjs";
|
|
@@ -199,7 +200,7 @@ export async function authorizePlayUpload({ releaseId, configName = "right-relea
|
|
|
199
200
|
if (!existsSync(configPath)) refuse("PLAY_CONFIG_INVALID", `release config is required: ${configName}`);
|
|
200
201
|
// The config is a module that runs on import: loaded only after the tree is proven clean.
|
|
201
202
|
const config = (await import(`${pathToFileURL(configPath).href}?upload=${Date.now()}`)).default;
|
|
202
|
-
const distribution = validatePlayDistribution(config
|
|
203
|
+
const distribution = validatePlayDistribution(targetDistribution(config, "android"));
|
|
203
204
|
const sealedDir = path.join(repoRoot, ".right-release", "sealed", releaseId, "android");
|
|
204
205
|
const sealed = verifyPublicationRelease({ sealedDir, repoRoot, appRoot: path.dirname(configPath), commit, app: config.app, version: config.version, platform: "android", configPath, releaseId });
|
|
205
206
|
const entries = sealed.manifest.files.filter((entry) => entry.role === "aab");
|
package/apple-profile-create.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
6
|
import { spawnSync } from "./background-process.mjs";
|
|
7
|
-
import { appleProfileAuth, decodeAppleProfile, signAppleApiJwt } from "./apple-profile-renew.mjs";
|
|
7
|
+
import { appleProfileAuth, decodeAppleProfile, findAppleSigningCertificate, signAppleApiJwt } from "./apple-profile-renew.mjs";
|
|
8
8
|
|
|
9
9
|
// right-release apple-profile-create: issue a Mac Development provisioning profile
|
|
10
10
|
// (MAC_APP_DEVELOPMENT) for one explicit bundle id with the ICLOUD/CloudKit capability,
|
|
@@ -14,6 +14,7 @@ import { appleProfileAuth, decodeAppleProfile, signAppleApiJwt } from "./apple-p
|
|
|
14
14
|
|
|
15
15
|
const ASC = "https://api.appstoreconnect.apple.com";
|
|
16
16
|
const PROFILE_TYPE = "MAC_APP_DEVELOPMENT";
|
|
17
|
+
const DIRECT_TYPE = "MAC_APP_DIRECT";
|
|
17
18
|
export class AppleProfileCreateError extends Error {
|
|
18
19
|
constructor(code, message) { super(message); this.name = "AppleProfileCreateError"; this.code = code; }
|
|
19
20
|
}
|
|
@@ -56,7 +57,7 @@ const UDID_PATTERN = /^[A-Fa-f0-9-]{8,64}$/;
|
|
|
56
57
|
|
|
57
58
|
export function parseCreateArgs(args) {
|
|
58
59
|
const out = { configPath: undefined, bundleId: "", name: "", containers: [], type: PROFILE_TYPE, certificate: "create", out: "", dryRun: true, apply: false, registerDevices: [], keyOut: "" };
|
|
59
|
-
const values = { "--config": "configPath", "--bundle-id": "bundleId", "--name": "name", "--type": "type", "--certificate": "certificate", "--out": "out", "--key-out": "keyOut" };
|
|
60
|
+
const values = { "--config": "configPath", "--bundle-id": "bundleId", "--name": "name", "--type": "type", "--certificate": "certificate", "--out": "out", "--key-out": "keyOut", "--expected-team": "expectedTeam", "--expected-keychain-group": "expectedKeychainGroup" };
|
|
60
61
|
for (let i = 0; i < args.length; i++) {
|
|
61
62
|
const arg = args[i];
|
|
62
63
|
if (arg === "--dry-run") out.dryRun = true;
|
|
@@ -71,10 +72,10 @@ export function parseCreateArgs(args) {
|
|
|
71
72
|
}
|
|
72
73
|
if (out.apply && args.includes("--dry-run")) fail("APPLE_PROFILE_USAGE", "--dry-run and --apply are mutually exclusive.");
|
|
73
74
|
out.dryRun = !out.apply;
|
|
74
|
-
|
|
75
|
+
validateProfileMode(out);
|
|
75
76
|
if (!BUNDLE_PATTERN.test(out.bundleId)) fail("APPLE_PROFILE_USAGE", "--bundle-id must be an explicit bundle identifier (no wildcard).");
|
|
76
77
|
if (!out.name.trim()) fail("APPLE_PROFILE_USAGE", "--name is required.");
|
|
77
|
-
if (!out.containers.length) fail("APPLE_PROFILE_USAGE", "At least one --icloud-container is required.");
|
|
78
|
+
if (out.type === PROFILE_TYPE && !out.containers.length) fail("APPLE_PROFILE_USAGE", "At least one --icloud-container is required.");
|
|
78
79
|
for (const container of out.containers) if (!CONTAINER_PATTERN.test(container)) fail("APPLE_PROFILE_USAGE", `Invalid iCloud container identifier: ${container}`);
|
|
79
80
|
for (const udid of out.registerDevices) if (!UDID_PATTERN.test(udid)) fail("APPLE_PROFILE_USAGE", `Invalid device UDID: ${udid}`);
|
|
80
81
|
if (!out.out) fail("APPLE_PROFILE_USAGE", "--out <profile path> is required.");
|
|
@@ -82,6 +83,15 @@ export function parseCreateArgs(args) {
|
|
|
82
83
|
return out;
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
function validateProfileMode({ type, certificate, containers = [], registerDevices = [], keyOut, expectedTeam, expectedKeychainGroup }) {
|
|
87
|
+
if (![PROFILE_TYPE, DIRECT_TYPE].includes(type)) fail("APPLE_PROFILE_USAGE", `--type must be ${PROFILE_TYPE} or ${DIRECT_TYPE}.`);
|
|
88
|
+
if (type !== DIRECT_TYPE) return;
|
|
89
|
+
if (!/^[A-Z0-9]{10}$/.test(expectedTeam ?? "")) fail("APPLE_PROFILE_USAGE", "Direct profiles require --expected-team <10-character team id>.");
|
|
90
|
+
if (typeof expectedKeychainGroup !== "string" || !expectedKeychainGroup.startsWith(`${expectedTeam}.`) || !BUNDLE_PATTERN.test(expectedKeychainGroup.slice(expectedTeam.length + 1))) fail("APPLE_PROFILE_USAGE", "Direct profiles require one exact --expected-keychain-group prefixed by expected team (no wildcard).");
|
|
91
|
+
if (!/^[A-Za-z0-9]{4,40}$/.test(certificate ?? "") || certificate === "create") fail("APPLE_PROFILE_USAGE", "Direct profiles require --certificate <existing ASC certificate id>.");
|
|
92
|
+
if (containers.length || registerDevices.length || keyOut) fail("APPLE_PROFILE_USAGE", "Direct profiles refuse iCloud containers, device registration & key output.");
|
|
93
|
+
}
|
|
94
|
+
|
|
85
95
|
// Key material must never ride in config, the environment or arguments. Unlike the
|
|
86
96
|
// renew command this refusal applies on every host.
|
|
87
97
|
export function assertNoInlinePem(env, config, extra = []) {
|
|
@@ -113,10 +123,11 @@ function localDevelopmentIdentities(run, now, { tolerant }) {
|
|
|
113
123
|
|
|
114
124
|
// ---- main flow ---------------------------------------------------------------------
|
|
115
125
|
|
|
116
|
-
export async function createAppleProfile({ root, config = {}, bundleId, name, containers, type = PROFILE_TYPE, certificate = "create", out, dryRun = true, registerDevices = [], keyOut = "", env = process.env, run = spawnSync, fetch: fetchApi = globalThis.fetch, jwtSigner = signAppleApiJwt, now = new Date(), log = console.log, platform = process.platform, home = os.homedir() }) {
|
|
117
|
-
|
|
126
|
+
export async function createAppleProfile({ root, config = {}, bundleId, name, containers = [], type = PROFILE_TYPE, certificate = "create", out, dryRun = true, registerDevices = [], keyOut = "", expectedTeam, expectedKeychainGroup, env = process.env, run = spawnSync, fetch: fetchApi = globalThis.fetch, jwtSigner = signAppleApiJwt, now = new Date(), log = console.log, platform = process.platform, home = os.homedir() }) {
|
|
127
|
+
validateProfileMode({ type, certificate, containers, registerDevices, keyOut, expectedTeam, expectedKeychainGroup });
|
|
118
128
|
if (!BUNDLE_PATTERN.test(bundleId ?? "")) fail("APPLE_PROFILE_USAGE", "bundleId must be an explicit bundle identifier.");
|
|
119
|
-
if (!
|
|
129
|
+
if (!name?.trim() || !out) fail("APPLE_PROFILE_USAGE", "Profile name & output path are required.");
|
|
130
|
+
if (type === PROFILE_TYPE && !containers?.length) fail("APPLE_PROFILE_USAGE", "At least one iCloud container is required.");
|
|
120
131
|
assertNoInlinePem(env, config, [["--certificate", certificate], ["--name", name]]);
|
|
121
132
|
if (!dryRun && platform !== "darwin") fail("APPLE_PROFILE_PLATFORM", "A real run decodes the issued profile and uses the Keychain; run it on the macOS host.");
|
|
122
133
|
const outFile = path.resolve(root, out);
|
|
@@ -155,12 +166,14 @@ export async function createAppleProfile({ root, config = {}, bundleId, name, co
|
|
|
155
166
|
}
|
|
156
167
|
return items;
|
|
157
168
|
};
|
|
158
|
-
const plan = { dryRun, command: "apple-profile-create", profileType:
|
|
169
|
+
const plan = { dryRun, command: "apple-profile-create", profileType: type, out: outFile, name, calls, planned };
|
|
159
170
|
const mutate = async (method, endpoint, body) => {
|
|
160
171
|
if (dryRun) { planned.push({ method, path: endpoint, ...(body ? { body } : {}) }); log(`PLANNED ${method} ${endpoint}${body ? ` ${JSON.stringify(body)}` : ""}`); return null; }
|
|
161
172
|
return request(method, endpoint, body);
|
|
162
173
|
};
|
|
163
174
|
|
|
175
|
+
if (type === DIRECT_TYPE) return createDirectProfile({ bundleId, name, certificate, expectedTeam, expectedKeychainGroup, outFile, dryRun, env, run, now, log, request, list, mutate, plan });
|
|
176
|
+
|
|
164
177
|
// (1) bundle id
|
|
165
178
|
const bundles = await list(query("/v1/bundleIds", { "filter[identifier]": bundleId, limit: "200" }));
|
|
166
179
|
let bundle = bundles.find((item) => item.attributes?.identifier === bundleId && ["MAC_OS", "UNIVERSAL"].includes(item.attributes?.platform));
|
|
@@ -290,6 +303,96 @@ export async function createAppleProfile({ root, config = {}, bundleId, name, co
|
|
|
290
303
|
return { ...plan, profileId: profile.id, identitySha1: certificateSha1 };
|
|
291
304
|
}
|
|
292
305
|
|
|
306
|
+
// Direct profiles authorize one selected existing Developer ID identity. No device,
|
|
307
|
+
// capability, certificate or key creation is reachable from this branch.
|
|
308
|
+
async function createDirectProfile({ bundleId, name, certificate, expectedTeam, expectedKeychainGroup, outFile, dryRun, env, run, now, log, request, list, mutate, plan }) {
|
|
309
|
+
const ascCert = (await request("GET", `/v1/certificates/${enc(certificate)}`)).data;
|
|
310
|
+
if (!ascCert?.id || ascCert.id !== certificate) fail("APPLE_PROFILE_CERT_NOT_IN_ASC", "Selected certificate is absent from ASC.");
|
|
311
|
+
if (!["DEVELOPER_ID_APPLICATION", "DEVELOPER_ID_APPLICATION_G2"].includes(ascCert.attributes?.certificateType)) fail("APPLE_PROFILE_CERT_TYPE", "Direct profiles require an existing DEVELOPER_ID_APPLICATION or DEVELOPER_ID_APPLICATION_G2 certificate.");
|
|
312
|
+
if (!(Date.parse(ascCert.attributes?.expirationDate) > now.getTime())) fail("APPLE_PROFILE_CERT_EXPIRED", "Selected ASC certificate is expired.");
|
|
313
|
+
let leaf;
|
|
314
|
+
try { leaf = new X509Certificate(Buffer.from(ascCert.attributes?.certificateContent ?? "", "base64")); }
|
|
315
|
+
catch { fail("APPLE_PROFILE_API_INVALID", "Selected ASC certificate has no valid public DER certificate."); }
|
|
316
|
+
const certificateSha1 = sha1(leaf.raw);
|
|
317
|
+
const identity = findAppleSigningCertificate({ type: DIRECT_TYPE, team: expectedTeam }, { env: { ...env, APPLE_SIGNING_IDENTITY: certificateSha1 }, run, now });
|
|
318
|
+
if (identity.sha1 !== certificateSha1 || serial(identity.serialNumber) !== serial(ascCert.attributes?.serialNumber)) fail("APPLE_PROFILE_CERT_MISMATCH", "Selected ASC certificate does not match local Developer ID identity.");
|
|
319
|
+
plan.teamIdentifier = expectedTeam;
|
|
320
|
+
plan.expectedKeychainGroup = expectedKeychainGroup;
|
|
321
|
+
plan.certificate = { ...identity, id: ascCert.id, mode: "explicit", privateKeyInKeychain: true };
|
|
322
|
+
|
|
323
|
+
const bundles = await list(query("/v1/bundleIds", { "filter[identifier]": bundleId, limit: "200" }));
|
|
324
|
+
const matching = bundles.filter((item) => item.attributes?.identifier === bundleId);
|
|
325
|
+
if (matching.length > 1 || (matching.length && !["MAC_OS", "UNIVERSAL"].includes(matching[0].attributes?.platform))) fail("APPLE_PROFILE_BUNDLE_CONFLICT", "Existing bundle identifier has conflicting platform or duplicate resources.");
|
|
326
|
+
let bundle = matching[0];
|
|
327
|
+
// Look across types so a dedicated name is never silently duplicated.
|
|
328
|
+
const profiles = (await list(query("/v1/profiles", { "filter[name]": name, limit: "200" }))).filter((item) => item.attributes?.name === name);
|
|
329
|
+
if (profiles.length > 1) fail("APPLE_PROFILE_NAME_CONFLICT", "More than one profile already uses this dedicated name.");
|
|
330
|
+
let profile = profiles[0];
|
|
331
|
+
if (profile) {
|
|
332
|
+
const [bundleRel, certRel] = await Promise.all([request("GET", `/v1/profiles/${enc(profile.id)}/bundleId`), list(`/v1/profiles/${enc(profile.id)}/certificates`)]);
|
|
333
|
+
if (!bundle?.id || profile.attributes?.profileType !== DIRECT_TYPE || profile.attributes?.profileState !== "ACTIVE" || !(Date.parse(profile.attributes?.expirationDate) > now.getTime())
|
|
334
|
+
|| bundleRel.data?.id !== bundle.id || certRel.length !== 1 || certRel[0].id !== ascCert.id) fail("APPLE_PROFILE_NAME_CONFLICT", "Existing named profile does not match direct type, bundle & selected certificate.");
|
|
335
|
+
}
|
|
336
|
+
plan.bundleId = { identifier: bundleId, id: bundle?.id ?? null, action: bundle ? "reuse" : "create" };
|
|
337
|
+
if (!bundle) {
|
|
338
|
+
const result = await mutate("POST", "/v1/bundleIds", { data: { type: "bundleIds", attributes: { identifier: bundleId, name: name.replace(/[^A-Za-z0-9 ]/g, " ").trim() || bundleId, platform: "MAC_OS" } } });
|
|
339
|
+
if (result) {
|
|
340
|
+
bundle = result.data;
|
|
341
|
+
if (!bundle?.id || bundle.attributes?.identifier !== bundleId || !["MAC_OS", "UNIVERSAL"].includes(bundle.attributes?.platform)) fail("APPLE_PROFILE_API_INVALID", "ASC returned an unexpected created bundle identifier.");
|
|
342
|
+
plan.bundleId.id = bundle.id;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
plan.profile = { id: profile?.id ?? null, action: profile ? "reuse" : "create" };
|
|
346
|
+
if (!profile) {
|
|
347
|
+
const body = { data: { type: "profiles", attributes: { name, profileType: DIRECT_TYPE }, relationships: {
|
|
348
|
+
bundleId: { data: { type: "bundleIds", id: bundle?.id ?? "<new bundleId>" } }, certificates: { data: [{ type: "certificates", id: ascCert.id }] } } } };
|
|
349
|
+
const result = await mutate("POST", "/v1/profiles", body);
|
|
350
|
+
if (result) {
|
|
351
|
+
profile = result.data;
|
|
352
|
+
if (!profile?.id) fail("APPLE_PROFILE_API_INVALID", "ASC did not return a created profile id.");
|
|
353
|
+
plan.profile.id = profile.id;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
plan.verification = "Exact team, application identifier, OSX, expiry, all devices, debugging disabled, selected DER fingerprint & exact keychain group authorization; app must request only its exact group.";
|
|
357
|
+
log(JSON.stringify({ ...plan, calls: undefined }, null, 2));
|
|
358
|
+
if (dryRun) { plan.writes = `would write ${outFile} (0600)`; return plan; }
|
|
359
|
+
const content = profile.attributes?.profileContent ?? (await request("GET", `/v1/profiles/${enc(profile.id)}`)).data?.attributes?.profileContent;
|
|
360
|
+
if (typeof content !== "string" || !content || !/^[A-Za-z0-9+/]+={0,2}$/.test(content)) fail("APPLE_PROFILE_API_INVALID", "ASC profileContent is missing or invalid.");
|
|
361
|
+
mkdirSync(path.dirname(outFile), { recursive: true });
|
|
362
|
+
const temporary = `${outFile}.create-${randomUUID()}.tmp`;
|
|
363
|
+
try {
|
|
364
|
+
writeFileSync(temporary, Buffer.from(content, "base64"), { flag: "wx", mode: 0o600 });
|
|
365
|
+
verifyDirectProfile(decodeAppleProfile(temporary, { run }), { bundleId, name, expectedTeam, expectedKeychainGroup, certificateSha1, now });
|
|
366
|
+
if (existsSync(outFile)) copyFileSync(outFile, `${outFile}.backup-${now.toISOString().replace(/[:.]/g, "-")}-${randomUUID()}`, constants.COPYFILE_EXCL);
|
|
367
|
+
renameSync(temporary, outFile);
|
|
368
|
+
chmodSync(outFile, 0o600);
|
|
369
|
+
} finally { rmSync(temporary, { force: true }); }
|
|
370
|
+
log(`Wrote ${outFile}; Developer ID identity SHA-1 ${certificateSha1}`);
|
|
371
|
+
return { ...plan, profileId: profile.id, identitySha1: certificateSha1 };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function verifyDirectProfile(decoded, { bundleId, name, expectedTeam, expectedKeychainGroup, certificateSha1, now }) {
|
|
375
|
+
const problems = [], entitlements = decoded.Entitlements ?? {};
|
|
376
|
+
if (decoded.Name !== name) problems.push("profile name mismatch");
|
|
377
|
+
if (!Array.isArray(decoded.TeamIdentifier) || decoded.TeamIdentifier.length !== 1 || decoded.TeamIdentifier[0] !== expectedTeam) problems.push("team mismatch");
|
|
378
|
+
if (entitlements["com.apple.developer.team-identifier"] !== expectedTeam) problems.push("entitlement team mismatch");
|
|
379
|
+
const appKeys = ["com.apple.application-identifier", "application-identifier"].filter((key) => Object.hasOwn(entitlements, key));
|
|
380
|
+
if (!appKeys.length || appKeys.some((key) => entitlements[key] !== `${expectedTeam}.${bundleId}`)) problems.push("application identifier mismatch");
|
|
381
|
+
if (!Array.isArray(decoded.Platform) || !decoded.Platform.includes("OSX")) problems.push("not a macOS profile");
|
|
382
|
+
if (!(Date.parse(decoded.ExpirationDate) > now.getTime())) problems.push("expired or missing expiration");
|
|
383
|
+
if (decoded.ProvisionsAllDevices !== true) problems.push("not an all-devices direct profile");
|
|
384
|
+
const debugKeys = ["get-task-allow", "com.apple.security.get-task-allow"].filter((key) => Object.hasOwn(entitlements, key));
|
|
385
|
+
// Profiles carry an entitlement allowlist (Apple TN3125). Direct profiles may
|
|
386
|
+
// omit debugging entirely; every present alias must still be boolean false.
|
|
387
|
+
if (debugKeys.some((key) => entitlements[key] !== false)) problems.push("debugging is not disabled");
|
|
388
|
+
if (!certificateSha1 || !(decoded.DeveloperCertificates ?? []).some((data) => sha1(Buffer.from(data, "base64")) === certificateSha1)) problems.push("selected certificate not authorized");
|
|
389
|
+
const groups = entitlements["keychain-access-groups"];
|
|
390
|
+
// Apple-issued terminal wildcards may authorize an exact app group. This never
|
|
391
|
+
// changes the app's requested entitlements or permits wildcard CLI input.
|
|
392
|
+
if (!Array.isArray(groups) || !groups.some((group) => group === expectedKeychainGroup || (typeof group === "string" && group.startsWith(`${expectedTeam}.`) && /^[^*]+\*$/u.test(group) && expectedKeychainGroup.startsWith(group.slice(0, -1))))) problems.push("keychain group not authorized");
|
|
393
|
+
if (problems.length) fail("APPLE_PROFILE_VERIFY_FAILED", `Direct profile failed verification: ${problems.join("; ")}.`);
|
|
394
|
+
}
|
|
395
|
+
|
|
293
396
|
export function verifyIssuedProfile(decoded, { bundleId, containers, certificateSha1, deviceUdids, now }) {
|
|
294
397
|
const other = [], missing = [];
|
|
295
398
|
const entitlements = decoded.Entitlements ?? {};
|
|
@@ -345,7 +448,7 @@ async function createCertificate({ commonName, request, run, keyOut, home, log }
|
|
|
345
448
|
|
|
346
449
|
export async function main(args = process.argv.slice(2), options = {}) {
|
|
347
450
|
if (args.includes("--help") || args.includes("-h")) {
|
|
348
|
-
(options.log ?? console.log)("right-release apple-profile-create --bundle-id <id> --name <profile name> --
|
|
451
|
+
(options.log ?? console.log)("right-release apple-profile-create --bundle-id <id> --name <profile name> --out <profile path>\n [--config <file>] [--type MAC_APP_DEVELOPMENT|MAC_APP_DIRECT] [--certificate <asc cert id>|create] [--dry-run | --apply]\nDevelopment: --icloud-container <iCloud.x>... [--register-device <UDID>]... [--key-out <file>]\nDirect: --certificate <existing ASC Developer ID cert id> --expected-team <team id> --expected-keychain-group <exact group>; refuses iCloud, devices & key output.\nDefault is --dry-run (GET discovery only). Uses APPLE_API_ISSUER, APPLE_API_KEY & APPLE_API_KEY_PATH or appStoreConnect.* in the config; inline PEM is refused.\nThe ASC API cannot create or assign iCloud containers; the issued profile is verified and the command prints manual steps if the container is missing.");
|
|
349
452
|
return;
|
|
350
453
|
}
|
|
351
454
|
const parsed = parseCreateArgs(args);
|
|
@@ -354,7 +457,7 @@ export async function main(args = process.argv.slice(2), options = {}) {
|
|
|
354
457
|
let config = {};
|
|
355
458
|
if (existsSync(absolute)) { const loaded = await import(pathToFileURL(absolute)); config = loaded.default ?? loaded.config ?? loaded; }
|
|
356
459
|
else if (parsed.configPath) fail("APPLE_PROFILE_USAGE", `Config file not found: ${parsed.configPath}`);
|
|
357
|
-
return createAppleProfile({ ...options, root: path.dirname(absolute), config,
|
|
460
|
+
return createAppleProfile({ ...options, root: path.dirname(absolute), config, ...parsed });
|
|
358
461
|
}
|
|
359
462
|
|
|
360
463
|
const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
|
package/apple-profile-renew.mjs
CHANGED
|
@@ -43,12 +43,20 @@ export function appleProfileAuth(env = process.env, { config = {}, root = proces
|
|
|
43
43
|
// Only this in-process signer reads key material. Never send it to a subprocess,
|
|
44
44
|
// include it in diagnostics, or expose it in returned renewal plans.
|
|
45
45
|
export function signAppleApiJwt({ keyPath, keyId, issuer }, now = new Date()) {
|
|
46
|
+
let key;
|
|
47
|
+
try { key = createPrivateKey(readFileSync(keyPath)); } catch { fail("APPLE_PROFILE_KEY_INVALID", "Unable to sign API JWT with configured .p8 key."); }
|
|
48
|
+
return signAppleApiJwtWithKey(key, { keyId, issuer }, now);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The same ES256 App Store Connect JWT from a key that is already in memory (a KeyObject, for example one read from the
|
|
52
|
+
// macOS Keychain by ios-asc-credentials.mjs). No file is read. lifetimeSeconds is at most Apple's 1200 second limit.
|
|
53
|
+
export function signAppleApiJwtWithKey(key, { keyId, issuer }, now = new Date(), { lifetimeSeconds = 600 } = {}) {
|
|
46
54
|
try {
|
|
47
|
-
|
|
48
|
-
if (key
|
|
55
|
+
if (!Number.isInteger(lifetimeSeconds) || lifetimeSeconds < 1 || lifetimeSeconds > 1200) throw new Error();
|
|
56
|
+
if (key?.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") throw new Error();
|
|
49
57
|
const iat = Math.floor(now.getTime() / 1000);
|
|
50
58
|
const encoded = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
51
|
-
const payload = `${encoded({ alg: "ES256", kid: keyId, typ: "JWT" })}.${encoded({ iss: issuer, iat, exp: iat +
|
|
59
|
+
const payload = `${encoded({ alg: "ES256", kid: keyId, typ: "JWT" })}.${encoded({ iss: issuer, iat, exp: iat + lifetimeSeconds, aud: "appstoreconnect-v1" })}`;
|
|
52
60
|
return `${payload}.${sign("sha256", Buffer.from(payload), { key, dsaEncoding: "ieee-p1363" }).toString("base64url")}`;
|
|
53
61
|
} catch { fail("APPLE_PROFILE_KEY_INVALID", "Unable to sign API JWT with configured .p8 key."); }
|
|
54
62
|
}
|
package/artifact-storage.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
mkdirSync,
|
|
7
7
|
readFileSync,
|
|
8
8
|
readdirSync,
|
|
9
|
+
realpathSync,
|
|
9
10
|
renameSync,
|
|
10
11
|
rmSync,
|
|
11
12
|
statSync,
|
|
@@ -20,6 +21,21 @@ export const DEFAULT_MANAGED_ROOT = resolveManagedRoots().dataRoot;
|
|
|
20
21
|
|
|
21
22
|
const SAFE_COMPONENT = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,159}$/;
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The configured root, canonicalised once: an owner may move the managed root to another volume and leave a symlink at the
|
|
26
|
+
* configured path (2026-10-18: /Volumes/D/.rightkit-managed -> /Volumes/E/.rightkit-managed). Every path below it is still checked
|
|
27
|
+
* for symlinks by its consumer (ensurePrivateDirectory refuses symlink ancestors); only the root itself is resolved here.
|
|
28
|
+
* Used only where a strict ancestor walk needs it (codesign-environment.mjs): resolveManagedRoot stays lexical, because the
|
|
29
|
+
* Mac signing agent's allowed roots are recorded in that lexical spelling.
|
|
30
|
+
*/
|
|
31
|
+
export function canonicalManagedRoot(root, { realpath = realpathSync.native } = {}) {
|
|
32
|
+
const resolved = path.resolve(root);
|
|
33
|
+
try { return realpath(resolved); } catch (error) {
|
|
34
|
+
if (error?.code === "ENOENT") return resolved;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
export function resolveManagedRoot({ env = process.env, exists = existsSync } = {}) {
|
|
24
40
|
if (env.RIGHTKIT_DATA_ROOT || env.RIGHTKIT_MANAGED_ROOT || env.RIGHTKIT_EXTERNAL_ROOT) return path.resolve(resolveManagedRoots({ env }).dataRoot);
|
|
25
41
|
// Workstation default only when its drive exists; hosted CI runners fall back to RUNNER_TEMP, then the system temp dir.
|
|
@@ -310,15 +326,32 @@ function ensureDirectory(directory) {
|
|
|
310
326
|
mkdirSync(directory, { recursive: true });
|
|
311
327
|
}
|
|
312
328
|
|
|
313
|
-
|
|
329
|
+
/**
|
|
330
|
+
* The managed roots an owner configures (env overrides and the workstation default), as spelled. Only these exact paths may
|
|
331
|
+
* themselves be symlinks (2026-10-18: /Volumes/D/.rightkit-managed -> /Volumes/E/.rightkit-managed); every path below them,
|
|
332
|
+
* and every other ancestor, is still refused when it is a symlink.
|
|
333
|
+
*/
|
|
334
|
+
export function configuredManagedRootSpellings({ env = process.env } = {}) {
|
|
335
|
+
const spellings = [DEFAULT_MANAGED_ROOT, env.RIGHTKIT_DATA_ROOT, env.RIGHTKIT_MANAGED_ROOT, env.RIGHTKIT_EXTERNAL_ROOT]
|
|
336
|
+
.filter((value) => typeof value === "string" && value.length > 0)
|
|
337
|
+
.map((value) => path.resolve(value));
|
|
338
|
+
return new Set(process.platform === "win32" ? spellings.map((value) => value.toLowerCase()) : spellings);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function isConfiguredManagedRoot(candidate, roots) {
|
|
342
|
+
return roots.has(process.platform === "win32" ? path.resolve(candidate).toLowerCase() : path.resolve(candidate));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function assertSafeMutation(target, { env = process.env } = {}) {
|
|
314
346
|
const absolute = path.resolve(target);
|
|
315
347
|
const root = path.parse(absolute).root;
|
|
316
348
|
const relative = path.relative(root, absolute);
|
|
349
|
+
const managedRoots = configuredManagedRootSpellings({ env });
|
|
317
350
|
let current = root;
|
|
318
351
|
for (const segment of relative ? relative.split(path.sep) : []) {
|
|
319
352
|
current = path.join(current, segment);
|
|
320
353
|
try {
|
|
321
|
-
if (lstatSync(current).isSymbolicLink()) throw new Error(`refusing filesystem mutation through symlink: ${current}`);
|
|
354
|
+
if (lstatSync(current).isSymbolicLink() && !isConfiguredManagedRoot(current, managedRoots)) throw new Error(`refusing filesystem mutation through symlink: ${current}`);
|
|
322
355
|
} catch (error) {
|
|
323
356
|
if (error.code === "ENOENT") continue;
|
|
324
357
|
throw error;
|
package/bin/darwin/rightkit-swap
CHANGED
|
Binary file
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `right-release license-notices binaries [--config <file>] [--output <file>] [--product-name <name>] [--receipt <path>] [--check]`:
|
|
3
|
+
// writes the notice appendix (one HTML file) for the binaries an app ships that no RightKit catalog row covers (a downloaded yt-dlp, a
|
|
4
|
+
// vendored tool, a command-built helper). Catalog-backed sidecars (ffmpeg, onnxruntime, pdfium, whisper) write their own notices when
|
|
5
|
+
// they are staged; everything else must be declared here, with its licence text, or the build has no right to ship it.
|
|
6
|
+
//
|
|
7
|
+
// legal: { binaryNotices: { output: "legal/THIRD_PARTY_BINARIES.html", productName: "ScrapeRight", receipt: "src-tauri/binaries/stage-sidecar.json",
|
|
8
|
+
// binaries: [{ name: "yt-dlp", version: "2026.03.17" | "self-updating", license: "Unlicense", sourceUrl: "https://github.com/yt-dlp/yt-dlp",
|
|
9
|
+
// files: ["legal/texts/yt-dlp-UNLICENSE.txt"], covers?: ["yt-dlp-helper"] }] } }
|
|
10
|
+
//
|
|
11
|
+
// Every `files` entry is an app-relative text file that must exist; no licence text is ever invented. With a `receipt` (the
|
|
12
|
+
// stage-sidecar receipt, also settable with `--receipt`) every staged file of kind remote, command, cmake or prebuilt must belong to a
|
|
13
|
+
// declared binary - by its entry name, or a name listed in `covers` - or the run fails naming the uncovered ones. The page records the
|
|
14
|
+
// receipt-independent inputs only (declared fields and the sha256 of each text), so the same declaration always produces the same bytes.
|
|
15
|
+
// `--check` writes nothing and exits 1 when the file on disk differs.
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { pathToFileURL } from "node:url";
|
|
20
|
+
import { CommandSpecError, loadAppConfig, relativeInside } from "./command-spec.mjs";
|
|
21
|
+
import { escapeHtml } from "./npm-license-notices.mjs";
|
|
22
|
+
|
|
23
|
+
const fail = (message) => { throw new CommandSpecError(message); };
|
|
24
|
+
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
25
|
+
const KEYS = ["output", "productName", "receipt", "binaries"];
|
|
26
|
+
const BINARY_KEYS = ["name", "version", "license", "sourceUrl", "files", "covers"];
|
|
27
|
+
/** Receipt kinds with no catalog row behind them. */
|
|
28
|
+
export const NON_CATALOG_KINDS = Object.freeze(["remote", "command", "cmake", "prebuilt"]);
|
|
29
|
+
|
|
30
|
+
/** Resolve and validate `legal.binaryNotices` plus flags. */
|
|
31
|
+
export function resolveBinaryNoticeOptions(config, flags = {}) {
|
|
32
|
+
const declared = config?.legal?.binaryNotices;
|
|
33
|
+
if (!isObject(declared)) fail("legal.binaryNotices must be an object");
|
|
34
|
+
const unknown = Object.keys(declared).filter((key) => !KEYS.includes(key));
|
|
35
|
+
if (unknown.length) fail(`legal.binaryNotices has unknown key(s): ${unknown.join(", ")} (allowed: ${KEYS.join(", ")})`);
|
|
36
|
+
const output = flags.output ?? declared.output;
|
|
37
|
+
if (output === undefined) fail("no output file: set legal.binaryNotices.output or pass --output");
|
|
38
|
+
relativeInside(output, "legal.binaryNotices.output");
|
|
39
|
+
const productName = flags.productName ?? declared.productName;
|
|
40
|
+
if (typeof productName !== "string" || !productName.trim()) fail("no product name: set legal.binaryNotices.productName or pass --product-name");
|
|
41
|
+
const receipt = flags.receipt ?? declared.receipt;
|
|
42
|
+
if (receipt !== undefined) relativeInside(receipt, "legal.binaryNotices.receipt");
|
|
43
|
+
if (!Array.isArray(declared.binaries) || !declared.binaries.length) fail("legal.binaryNotices.binaries must be a non-empty list");
|
|
44
|
+
const names = new Set();
|
|
45
|
+
const binaries = declared.binaries.map((item, index) => {
|
|
46
|
+
const label = `legal.binaryNotices.binaries[${index}]`;
|
|
47
|
+
if (!isObject(item)) fail(`${label} must be an object`);
|
|
48
|
+
const extra = Object.keys(item).filter((key) => !BINARY_KEYS.includes(key));
|
|
49
|
+
if (extra.length) fail(`${label} has unknown key(s): ${extra.join(", ")} (allowed: ${BINARY_KEYS.join(", ")})`);
|
|
50
|
+
for (const key of ["name", "version", "license"]) if (typeof item[key] !== "string" || !item[key].trim()) fail(`${label}.${key} must be a non-empty string`);
|
|
51
|
+
if (typeof item.sourceUrl !== "string" || !/^https:\/\/[^\s/]+\/\S*$/.test(item.sourceUrl)) fail(`${label}.sourceUrl must be an https:// URL of the source or release`);
|
|
52
|
+
if (!Array.isArray(item.files) || !item.files.length) fail(`${label}.files must list at least one licence/notice text file`);
|
|
53
|
+
const files = item.files.map((file, fileIndex) => relativeInside(file, `${label}.files[${fileIndex}]`));
|
|
54
|
+
const covers = item.covers ?? [];
|
|
55
|
+
if (!Array.isArray(covers) || covers.some((name) => typeof name !== "string" || !name.trim())) fail(`${label}.covers must be a list of staged entry names`);
|
|
56
|
+
for (const name of [item.name, ...covers]) {
|
|
57
|
+
if (names.has(name)) fail(`${label}: ${name} is declared twice`);
|
|
58
|
+
names.add(name);
|
|
59
|
+
}
|
|
60
|
+
return { name: item.name, version: item.version, license: item.license, sourceUrl: item.sourceUrl, files, covers: [...covers] };
|
|
61
|
+
});
|
|
62
|
+
return { output, productName, receipt, binaries };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The names of staged non-catalog entries that no declared binary covers. */
|
|
66
|
+
export function uncoveredBinaries(receipt, binaries) {
|
|
67
|
+
const covered = new Set(binaries.flatMap((binary) => [binary.name, ...(binary.covers ?? [])]));
|
|
68
|
+
const missing = new Set();
|
|
69
|
+
for (const file of receipt?.files ?? []) {
|
|
70
|
+
if (!NON_CATALOG_KINDS.includes(file.kind)) continue;
|
|
71
|
+
const name = file.entryName ?? file.command ?? file.name;
|
|
72
|
+
if (!covered.has(name)) missing.add(name);
|
|
73
|
+
}
|
|
74
|
+
return [...missing].sort();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Render the appendix. `texts` maps an app-relative file to its text. Returns the HTML. */
|
|
78
|
+
export function renderBinaryNotices({ productName, binaries, texts }) {
|
|
79
|
+
const sections = binaries.map((binary) => {
|
|
80
|
+
const bodies = binary.files.map((file) => `<h3>${escapeHtml(path.posix.basename(file))}</h3><pre>${escapeHtml(texts.get(file))}</pre>`).join("\n");
|
|
81
|
+
const digests = binary.files.map((file) => `${escapeHtml(path.posix.basename(file))} sha256=${createHash("sha256").update(texts.get(file)).digest("hex")}`).join("; ");
|
|
82
|
+
return `<section><h2>${escapeHtml(binary.name)} ${escapeHtml(binary.version)}</h2><p>License: ${escapeHtml(binary.license)}</p><p>Source: ${escapeHtml(binary.sourceUrl)}</p><p>Texts: ${digests}</p>\n${bodies}</section>`;
|
|
83
|
+
});
|
|
84
|
+
return `<!doctype html>
|
|
85
|
+
<html lang="en"><head><meta charset="utf-8"><title>${escapeHtml(productName)} bundled program licenses</title></head><body>
|
|
86
|
+
<h1>${escapeHtml(productName)} bundled program licenses</h1>
|
|
87
|
+
<p>Programs shipped with ${escapeHtml(productName)} that are not Cargo or npm dependencies; generated-by=right-release license-notices binaries</p>
|
|
88
|
+
${sections.join("\n")}
|
|
89
|
+
</body></html>
|
|
90
|
+
`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Generate (or check) the appendix for the app at `appRoot`. File access is injectable. */
|
|
94
|
+
export function generateBinaryNotices({ appRoot, config, flags = {}, check = false, exists = existsSync, readText = (file) => readFileSync(file, "utf8"), writeText = (file, text) => { mkdirSync(path.dirname(file), { recursive: true }); writeFileSync(file, text); } }) {
|
|
95
|
+
const { output, productName, receipt, binaries } = resolveBinaryNoticeOptions(config, flags);
|
|
96
|
+
const texts = new Map();
|
|
97
|
+
for (const binary of binaries) {
|
|
98
|
+
for (const file of binary.files) {
|
|
99
|
+
const absolute = path.join(appRoot, file);
|
|
100
|
+
if (!exists(absolute)) fail(`${binary.name}: licence text ${file} does not exist; no text is invented, add the file`);
|
|
101
|
+
const text = String(readText(absolute)).replace(/\r\n/g, "\n");
|
|
102
|
+
if (!text.trim()) fail(`${binary.name}: licence text ${file} is empty`);
|
|
103
|
+
texts.set(file, text);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
let uncovered = [];
|
|
107
|
+
if (receipt !== undefined) {
|
|
108
|
+
const receiptFile = path.join(appRoot, receipt);
|
|
109
|
+
if (!exists(receiptFile)) fail(`the stage-sidecar receipt ${receipt} does not exist; run right-release stage-sidecar first`);
|
|
110
|
+
let parsed;
|
|
111
|
+
try { parsed = JSON.parse(readText(receiptFile)); } catch (error) { fail(`the stage-sidecar receipt ${receipt} is not JSON (${error.message})`); }
|
|
112
|
+
uncovered = uncoveredBinaries(parsed, binaries);
|
|
113
|
+
if (uncovered.length) fail(`staged program(s) with no declared licence notice: ${uncovered.join(", ")}; declare them in legal.binaryNotices.binaries (name or covers)`);
|
|
114
|
+
}
|
|
115
|
+
const html = renderBinaryNotices({ productName, binaries, texts });
|
|
116
|
+
const target = path.join(appRoot, output);
|
|
117
|
+
if (check) {
|
|
118
|
+
const current = exists(target) ? String(readText(target)).replace(/\r\n/g, "\n") : null;
|
|
119
|
+
return { target, binaryCount: binaries.length, uncovered, upToDate: current === html };
|
|
120
|
+
}
|
|
121
|
+
writeText(target, html);
|
|
122
|
+
return { target, binaryCount: binaries.length, uncovered, upToDate: true };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function parseBinaryNoticeArgs(argv) {
|
|
126
|
+
const opts = { config: "right-release.config.mjs", output: undefined, productName: undefined, receipt: undefined, check: false };
|
|
127
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
128
|
+
const arg = argv[i];
|
|
129
|
+
if (arg === "--config" || arg === "--output" || arg === "--product-name" || arg === "--receipt") {
|
|
130
|
+
const value = argv[i + 1];
|
|
131
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg} needs a value`);
|
|
132
|
+
opts[arg === "--product-name" ? "productName" : arg.slice(2)] = value;
|
|
133
|
+
i += 1;
|
|
134
|
+
} else if (arg === "--check") opts.check = true;
|
|
135
|
+
else throw new Error(`unknown license-notices argument: ${arg}`);
|
|
136
|
+
}
|
|
137
|
+
return opts;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
141
|
+
const opts = parseBinaryNoticeArgs(argv);
|
|
142
|
+
const { config, appRoot } = await loadAppConfig(opts.config);
|
|
143
|
+
const outcome = generateBinaryNotices({ appRoot, config, flags: { output: opts.output, productName: opts.productName, receipt: opts.receipt }, check: opts.check });
|
|
144
|
+
if (opts.check) {
|
|
145
|
+
if (!outcome.upToDate) {
|
|
146
|
+
console.error(`right-release license-notices: ${outcome.target} is out of date; run right-release license-notices binaries`);
|
|
147
|
+
return 1;
|
|
148
|
+
}
|
|
149
|
+
console.log(`${outcome.target} is up to date (${outcome.binaryCount} program notices)`);
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
console.log(`wrote ${outcome.binaryCount} program notices to ${outcome.target}`);
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
157
|
+
main().then((code) => { process.exitCode = code; }, (error) => {
|
|
158
|
+
console.error(`right-release license-notices: ${error.code ? `[${error.code}] ` : ""}${error.message}`);
|
|
159
|
+
process.exitCode = 1;
|
|
160
|
+
});
|
|
161
|
+
}
|