@rightkit/release 0.4.0 → 0.5.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 +59 -7
- package/adhoc-entitlements.mjs +8 -0
- package/adoption-evidence.mjs +650 -0
- package/advisory-exceptions.mjs +371 -0
- package/apple-profile-create.mjs +113 -10
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/rightkit-gate.mjs +1079 -0
- package/binary-license-notices.mjs +161 -0
- package/cargo-license-notices.mjs +111 -0
- package/cli/right-release.mjs +39 -0
- package/deterministic-scans.mjs +966 -0
- package/development-signing.mjs +2 -1
- package/host-upload-refusal.mjs +90 -0
- package/local-publish.mjs +142 -19
- package/local-qualify.mjs +728 -0
- package/local-runtime.mjs +319 -0
- package/lock-graph.mjs +51 -0
- package/managed-cargo.mjs +4 -1
- package/ownership-approval.mjs +23 -6
- package/ownership-dependency-allowlist.json +2 -0
- package/ownership-mandatory-set.json +158 -68
- package/ownership-mandatory-set.mjs +82 -6
- package/package.json +6 -2
- 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-receipts.mjs +302 -0
- package/pipeline-scan.mjs +285 -0
- package/pipeline-tools.mjs +171 -0
- package/production-mode.mjs +42 -0
- package/publication-gate.mjs +94 -32
- package/publish-cargo.mjs +24 -7
- package/publish-swift.mjs +12 -3
- package/qa-control-scan.mjs +4 -0
- package/release-config.mjs +330 -0
- package/release-switch-scan.mjs +44 -5
- package/release.mjs +14 -4
- package/rightkit-ownership.json +57 -0
- package/rightkit-sync.mjs +8 -0
- package/rightkit-versions.json +191 -131
- package/runtime-budget-classes.json +8 -0
- package/runtime-budget-crates.mjs +14 -0
- package/runtime-budgets.mjs +128 -31
- package/scan-digest.mjs +107 -0
- package/sign-qa-bundle.mjs +210 -0
- package/stage-sidecar.mjs +99 -15
- package/swap-helper-bundle.mjs +7 -3
- package/workflow-yaml.mjs +177 -0
- package/bin/darwin/rightkit-swap.sign.json +0 -6
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// Advisory exception gate. ci/advisory-exceptions.json is the single source; deny.toml
|
|
2
|
+
// [advisories].ignore and .cargo/audit.toml must match it exactly, every entry needs an owner,
|
|
3
|
+
// evidence and an unexpired absolute reviewBy date, and the entry's versions must match what
|
|
4
|
+
// Cargo.lock actually holds, `dependents` must equal the exact set of direct reverse
|
|
5
|
+
// dependencies of the affected versions in Cargo.lock, and every hop of `path` must be a real
|
|
6
|
+
// dependency edge (scope drift fails in both directions). Policy: Astra 2026-10-22.
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
export const EXCEPTIONS_FILE = 'ci/advisory-exceptions.json';
|
|
12
|
+
export const DENY_FILE = 'deny.toml';
|
|
13
|
+
export const AUDIT_FILE = '.cargo/audit.toml';
|
|
14
|
+
export const LOCK_FILE = 'Cargo.lock';
|
|
15
|
+
|
|
16
|
+
const REQUIRED = ['id', 'crate', 'path', 'owner', 'rationale', 'evidence', 'reviewBy', 'approvedBy'];
|
|
17
|
+
const ID_RE = /^RUSTSEC-\d{4}-\d{4}$/;
|
|
18
|
+
const DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
19
|
+
|
|
20
|
+
// ---- minimal TOML subset: tables, key = value, strings, arrays, inline tables, bare scalars ----
|
|
21
|
+
|
|
22
|
+
export function parseToml(text) {
|
|
23
|
+
let i = 0;
|
|
24
|
+
const n = text.length;
|
|
25
|
+
const root = {};
|
|
26
|
+
let table = root;
|
|
27
|
+
const fail = msg => { throw new Error(`TOML parse error at offset ${i}: ${msg}`); };
|
|
28
|
+
const skipWs = (newlines) => {
|
|
29
|
+
for (;;) {
|
|
30
|
+
const c = text[i];
|
|
31
|
+
if (c === ' ' || c === '\t' || c === '\r') i++;
|
|
32
|
+
else if (c === '\n' && newlines) i++;
|
|
33
|
+
else if (c === '#') { while (i < n && text[i] !== '\n') i++; }
|
|
34
|
+
else break;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const parseString = () => {
|
|
38
|
+
const q = text[i++];
|
|
39
|
+
let out = '';
|
|
40
|
+
if (q === "'") {
|
|
41
|
+
while (i < n && text[i] !== "'") { if (text[i] === '\n') fail('newline in string'); out += text[i++]; }
|
|
42
|
+
if (i >= n) fail('unterminated string');
|
|
43
|
+
i++;
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
if (text.startsWith('""', i)) fail('multiline strings are not supported');
|
|
47
|
+
while (i < n && text[i] !== '"') {
|
|
48
|
+
const c = text[i++];
|
|
49
|
+
if (c === '\n') fail('newline in string');
|
|
50
|
+
if (c !== '\\') { out += c; continue; }
|
|
51
|
+
const e = text[i++];
|
|
52
|
+
const map = { n: '\n', t: '\t', r: '\r', '"': '"', '\\': '\\', b: '\b', f: '\f' };
|
|
53
|
+
if (e in map) out += map[e];
|
|
54
|
+
else if (e === 'u' || e === 'U') {
|
|
55
|
+
const len = e === 'u' ? 4 : 8;
|
|
56
|
+
const hex = text.slice(i, i + len);
|
|
57
|
+
if (!/^[0-9a-fA-F]+$/.test(hex) || hex.length !== len) fail('bad unicode escape');
|
|
58
|
+
out += String.fromCodePoint(parseInt(hex, 16));
|
|
59
|
+
i += len;
|
|
60
|
+
} else fail(`bad escape \\${e}`);
|
|
61
|
+
}
|
|
62
|
+
if (i >= n) fail('unterminated string');
|
|
63
|
+
i++;
|
|
64
|
+
return out;
|
|
65
|
+
};
|
|
66
|
+
const parseKey = () => {
|
|
67
|
+
if (text[i] === '"' || text[i] === "'") return parseString();
|
|
68
|
+
const m = /^[A-Za-z0-9_-]+/.exec(text.slice(i, i + 200));
|
|
69
|
+
if (!m) fail('bad key');
|
|
70
|
+
i += m[0].length;
|
|
71
|
+
return m[0];
|
|
72
|
+
};
|
|
73
|
+
const parseValue = () => {
|
|
74
|
+
skipWs(false);
|
|
75
|
+
const c = text[i];
|
|
76
|
+
if (c === '"' || c === "'") return parseString();
|
|
77
|
+
if (c === '[') {
|
|
78
|
+
i++;
|
|
79
|
+
const arr = [];
|
|
80
|
+
for (;;) {
|
|
81
|
+
skipWs(true);
|
|
82
|
+
if (text[i] === ']') { i++; return arr; }
|
|
83
|
+
arr.push(parseValue());
|
|
84
|
+
skipWs(true);
|
|
85
|
+
if (text[i] === ',') i++;
|
|
86
|
+
else if (text[i] !== ']') fail('expected , or ] in array');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (c === '{') {
|
|
90
|
+
i++;
|
|
91
|
+
const obj = {};
|
|
92
|
+
for (;;) {
|
|
93
|
+
skipWs(false);
|
|
94
|
+
if (text[i] === '}') { i++; return obj; }
|
|
95
|
+
const k = parseKey();
|
|
96
|
+
skipWs(false);
|
|
97
|
+
if (text[i++] !== '=') fail('expected = in inline table');
|
|
98
|
+
obj[k] = parseValue();
|
|
99
|
+
skipWs(false);
|
|
100
|
+
if (text[i] === ',') i++;
|
|
101
|
+
else if (text[i] !== '}') fail('expected , or } in inline table');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const m = /^[^\s,\]}#]+/.exec(text.slice(i, i + 200));
|
|
105
|
+
if (!m) fail('bad value');
|
|
106
|
+
i += m[0].length;
|
|
107
|
+
const raw = m[0];
|
|
108
|
+
if (raw === 'true') return true;
|
|
109
|
+
if (raw === 'false') return false;
|
|
110
|
+
if (/^[+-]?\d[\d_]*(\.\d+)?$/.test(raw)) return Number(raw.replace(/_/g, ''));
|
|
111
|
+
return fail(`unsupported value ${raw}`);
|
|
112
|
+
};
|
|
113
|
+
for (;;) {
|
|
114
|
+
skipWs(true);
|
|
115
|
+
if (i >= n) break;
|
|
116
|
+
if (text[i] === '[') {
|
|
117
|
+
const array = text[i + 1] === '[';
|
|
118
|
+
i += array ? 2 : 1;
|
|
119
|
+
const parts = [];
|
|
120
|
+
for (;;) {
|
|
121
|
+
skipWs(false);
|
|
122
|
+
parts.push(parseKey());
|
|
123
|
+
skipWs(false);
|
|
124
|
+
if (text[i] === '.') { i++; continue; }
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
const close = array ? ']]' : ']';
|
|
128
|
+
if (!text.startsWith(close, i)) fail('bad table header');
|
|
129
|
+
i += close.length;
|
|
130
|
+
let t = root;
|
|
131
|
+
parts.forEach((p, idx) => {
|
|
132
|
+
const last = idx === parts.length - 1;
|
|
133
|
+
if (last && array) { (t[p] ||= []).push({}); t = t[p][t[p].length - 1]; }
|
|
134
|
+
else { const next = t[p] ||= {}; t = Array.isArray(next) ? next[next.length - 1] : next; }
|
|
135
|
+
});
|
|
136
|
+
table = t;
|
|
137
|
+
} else {
|
|
138
|
+
const k = parseKey();
|
|
139
|
+
skipWs(false);
|
|
140
|
+
if (text[i++] !== '=') fail('expected =');
|
|
141
|
+
if (Object.hasOwn(table, k)) fail(`duplicate key ${k}`);
|
|
142
|
+
table[k] = parseValue();
|
|
143
|
+
}
|
|
144
|
+
skipWs(false);
|
|
145
|
+
if (i < n && text[i] !== '\n') fail('unexpected trailing characters');
|
|
146
|
+
}
|
|
147
|
+
return root;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---- Cargo.lock ----
|
|
151
|
+
|
|
152
|
+
export function lockedVersions(lockText) {
|
|
153
|
+
const map = new Map();
|
|
154
|
+
for (const block of lockText.split('[[package]]').slice(1)) {
|
|
155
|
+
const name = /^name = "([^"]+)"/m.exec(block)?.[1];
|
|
156
|
+
const version = /^version = "([^"]+)"/m.exec(block)?.[1];
|
|
157
|
+
if (!name || !version) continue;
|
|
158
|
+
if (!map.has(name)) map.set(name, new Set());
|
|
159
|
+
map.get(name).add(version);
|
|
160
|
+
}
|
|
161
|
+
return map;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Full package graph: Map "name version" -> { name, version, deps: Set of "name version" }.
|
|
165
|
+
// A dependency ref is "name" (unambiguous) or "name version" [(source)]; an ambiguous bare
|
|
166
|
+
// name or an unknown ref is recorded in `unresolved` so the check fails closed.
|
|
167
|
+
export function lockedGraph(lockText) {
|
|
168
|
+
const pkgs = [];
|
|
169
|
+
for (const block of lockText.split('[[package]]').slice(1)) {
|
|
170
|
+
const name = /^name = "([^"]+)"/m.exec(block)?.[1];
|
|
171
|
+
const version = /^version = "([^"]+)"/m.exec(block)?.[1];
|
|
172
|
+
if (!name || !version) continue;
|
|
173
|
+
const dm = /^dependencies = \[([\s\S]*?)^\]/m.exec(block);
|
|
174
|
+
const refs = dm ? [...dm[1].matchAll(/"([^"]+)"/g)].map(m => m[1]) : [];
|
|
175
|
+
pkgs.push({ name, version, refs });
|
|
176
|
+
}
|
|
177
|
+
const byName = new Map();
|
|
178
|
+
for (const p of pkgs) {
|
|
179
|
+
if (!byName.has(p.name)) byName.set(p.name, []);
|
|
180
|
+
byName.get(p.name).push(p.version);
|
|
181
|
+
}
|
|
182
|
+
const graph = new Map();
|
|
183
|
+
const unresolved = [];
|
|
184
|
+
for (const p of pkgs) {
|
|
185
|
+
const deps = new Set();
|
|
186
|
+
for (const ref of p.refs) {
|
|
187
|
+
const [dn, dv] = ref.split(' ');
|
|
188
|
+
const versions = byName.get(dn) ?? [];
|
|
189
|
+
const v = dv ?? (versions.length === 1 ? versions[0] : undefined);
|
|
190
|
+
if (!v || !versions.includes(v)) { unresolved.push(`${p.name} ${p.version} -> ${ref}`); continue; }
|
|
191
|
+
deps.add(`${dn} ${v}`);
|
|
192
|
+
}
|
|
193
|
+
graph.set(`${p.name} ${p.version}`, { name: p.name, version: p.version, deps });
|
|
194
|
+
}
|
|
195
|
+
return { graph, unresolved };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// A path hop is "name" or "name version"; returns the matching graph node ids.
|
|
199
|
+
function hopNodes(graph, hop) {
|
|
200
|
+
const [n, v] = hop.trim().split(/\s+/);
|
|
201
|
+
return [...graph.keys()].filter(id => {
|
|
202
|
+
const node = graph.get(id);
|
|
203
|
+
return node.name === n && (v === undefined || node.version === v);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---- the check ----
|
|
208
|
+
|
|
209
|
+
const isNonEmptyString = v => typeof v === 'string' && v.trim() !== '';
|
|
210
|
+
const isStringArray = v => Array.isArray(v) && v.length > 0 && v.every(isNonEmptyString);
|
|
211
|
+
|
|
212
|
+
function dateProblem(entryLabel, value, now) {
|
|
213
|
+
const m = DATE_RE.exec(value);
|
|
214
|
+
if (!m) return `${entryLabel}: reviewBy ${JSON.stringify(value)} is not an absolute YYYY-MM-DD date`;
|
|
215
|
+
const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
216
|
+
const dt = new Date(Date.UTC(y, mo - 1, d));
|
|
217
|
+
if (dt.getUTCFullYear() !== y || dt.getUTCMonth() !== mo - 1 || dt.getUTCDate() !== d) {
|
|
218
|
+
return `${entryLabel}: reviewBy ${value} is not a real calendar date`;
|
|
219
|
+
}
|
|
220
|
+
const today = new Date(now);
|
|
221
|
+
if (Number.isNaN(today.getTime())) return `${entryLabel}: the gate clock is invalid`;
|
|
222
|
+
const todayUtc = Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate());
|
|
223
|
+
if (todayUtc > dt.getTime()) return `${entryLabel}: reviewBy ${value} has expired; renew only with fresh evidence`;
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function checkAdvisoryExceptions({ root = process.cwd(), now = new Date(), readFile, allowPending = false } = {}) {
|
|
228
|
+
const read = readFile ?? (p => fs.readFileSync(path.join(root, p), 'utf8'));
|
|
229
|
+
const problems = [];
|
|
230
|
+
const load = (file, parse) => {
|
|
231
|
+
try { return parse(read(file)); } catch (err) { problems.push(`${file}: cannot read or parse (${err.message})`); return null; }
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const doc = load(EXCEPTIONS_FILE, JSON.parse);
|
|
235
|
+
const deny = load(DENY_FILE, parseToml);
|
|
236
|
+
const audit = load(AUDIT_FILE, parseToml);
|
|
237
|
+
const lockPair = load(LOCK_FILE, t => ({ versions: lockedVersions(t), ...lockedGraph(t) }));
|
|
238
|
+
if (!doc || !deny || !audit || !lockPair) return problems;
|
|
239
|
+
const lock = lockPair.versions;
|
|
240
|
+
const { graph } = lockPair;
|
|
241
|
+
for (const u of lockPair.unresolved) problems.push(`${LOCK_FILE}: dependency ${u} does not resolve to exactly one locked package`);
|
|
242
|
+
|
|
243
|
+
if (doc.schema !== 1) problems.push(`${EXCEPTIONS_FILE}: schema must be 1`);
|
|
244
|
+
if (!Array.isArray(doc.exceptions)) {
|
|
245
|
+
problems.push(`${EXCEPTIONS_FILE}: exceptions must be an array`);
|
|
246
|
+
return problems;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Entries.
|
|
250
|
+
const byId = new Map();
|
|
251
|
+
const valid = [];
|
|
252
|
+
doc.exceptions.forEach((e, idx) => {
|
|
253
|
+
const label = `${EXCEPTIONS_FILE} entry ${e?.id ?? `#${idx}`}`;
|
|
254
|
+
if (!e || typeof e !== 'object' || Array.isArray(e)) { problems.push(`${label}: not an object`); return; }
|
|
255
|
+
let ok = true;
|
|
256
|
+
for (const f of REQUIRED) {
|
|
257
|
+
if (!isNonEmptyString(e[f])) { problems.push(`${label}: missing field ${f}`); ok = false; }
|
|
258
|
+
}
|
|
259
|
+
if (!isStringArray(e.affectedVersions)) { problems.push(`${label}: missing field affectedVersions (non-empty array of exact versions)`); ok = false; }
|
|
260
|
+
if (e.unaffectedVersions !== undefined && !isStringArray(e.unaffectedVersions)) { problems.push(`${label}: unaffectedVersions must be a non-empty array when present`); ok = false; }
|
|
261
|
+
if (isNonEmptyString(e.id)) {
|
|
262
|
+
if (!ID_RE.test(e.id)) { problems.push(`${label}: id is not a RUSTSEC id`); ok = false; }
|
|
263
|
+
if (byId.has(e.id)) { problems.push(`${label}: duplicate id`); ok = false; } else byId.set(e.id, e);
|
|
264
|
+
}
|
|
265
|
+
if (isNonEmptyString(e.reviewBy)) {
|
|
266
|
+
const p = dateProblem(label, e.reviewBy, now);
|
|
267
|
+
if (p) { problems.push(p); ok = false; }
|
|
268
|
+
}
|
|
269
|
+
if (isNonEmptyString(e.approvedBy) && !allowPending && /pending/i.test(e.approvedBy)) {
|
|
270
|
+
problems.push(`${label}: approval is pending (${e.approvedBy})`);
|
|
271
|
+
ok = false;
|
|
272
|
+
}
|
|
273
|
+
if (ok) valid.push(e);
|
|
274
|
+
// Consumer scope: the exact reverse-dependency set, machine-checked from Cargo.lock.
|
|
275
|
+
if (isNonEmptyString(e.crate) && Array.isArray(e.affectedVersions)) {
|
|
276
|
+
if (!isStringArray(e.dependents)) {
|
|
277
|
+
problems.push(`${label}: missing field dependents (non-empty array of "name version" direct reverse dependencies)`);
|
|
278
|
+
} else {
|
|
279
|
+
const affected = new Set(e.affectedVersions.map(v => `${e.crate} ${v}`));
|
|
280
|
+
const actual = new Set();
|
|
281
|
+
for (const [id, node] of graph) for (const d of node.deps) if (affected.has(d)) actual.add(id);
|
|
282
|
+
const declared = new Set(e.dependents);
|
|
283
|
+
if (declared.size !== e.dependents.length) problems.push(`${label}: dependents has duplicates`);
|
|
284
|
+
for (const id of actual) if (!declared.has(id)) problems.push(`${label}: ${id} depends on ${e.crate} in ${LOCK_FILE} but is not in dependents (new consumer widens the suppression; needs fresh approval)`);
|
|
285
|
+
for (const id of declared) if (!actual.has(id)) problems.push(`${label}: dependents lists ${id}, which does not depend on an affected ${e.crate} in ${LOCK_FILE} (stale)`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Path documentation: every hop must be a real dependency edge, ending at the advisory crate.
|
|
289
|
+
if (isNonEmptyString(e.path) && isNonEmptyString(e.crate)) {
|
|
290
|
+
const hops = e.path.split('->').map(s => s.trim());
|
|
291
|
+
if (hops.length < 2 || hops.some(h => h === '')) problems.push(`${label}: path must have at least two hops separated by "->"`);
|
|
292
|
+
else {
|
|
293
|
+
if (hops[hops.length - 1].split(/\s+/)[0] !== e.crate) problems.push(`${label}: path must end at ${e.crate}`);
|
|
294
|
+
for (let h = 0; h + 1 < hops.length; h++) {
|
|
295
|
+
const from = hopNodes(graph, hops[h]);
|
|
296
|
+
const to = new Set(hopNodes(graph, hops[h + 1]));
|
|
297
|
+
if (!from.some(id => [...graph.get(id).deps].some(d => to.has(d)))) {
|
|
298
|
+
problems.push(`${label}: path hop "${hops[h]} -> ${hops[h + 1]}" is not a dependency edge in ${LOCK_FILE}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Version scope applies even to an entry that failed another rule.
|
|
304
|
+
if (isNonEmptyString(e.crate) && Array.isArray(e.affectedVersions)) {
|
|
305
|
+
const locked = lock.get(e.crate);
|
|
306
|
+
const listed = new Set([...e.affectedVersions, ...(Array.isArray(e.unaffectedVersions) ? e.unaffectedVersions : [])]);
|
|
307
|
+
for (const v of listed) {
|
|
308
|
+
if (!locked || !locked.has(v)) problems.push(`${label}: version ${e.crate} ${v} is not in ${LOCK_FILE} (scope drift)`);
|
|
309
|
+
}
|
|
310
|
+
for (const v of locked ?? []) {
|
|
311
|
+
if (!listed.has(v)) problems.push(`${label}: ${LOCK_FILE} holds ${e.crate} ${v}, which the entry does not list (scope drift; the advisory may apply to it)`);
|
|
312
|
+
}
|
|
313
|
+
for (const v of e.affectedVersions) {
|
|
314
|
+
if ((e.unaffectedVersions ?? []).includes(v)) problems.push(`${label}: version ${v} is both affected and unaffected`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// deny.toml.
|
|
320
|
+
const adv = deny.advisories;
|
|
321
|
+
const denyIgnore = adv && Array.isArray(adv.ignore) ? adv.ignore : null;
|
|
322
|
+
if (!denyIgnore) problems.push(`${DENY_FILE}: [advisories].ignore is missing`);
|
|
323
|
+
else {
|
|
324
|
+
const seen = new Set();
|
|
325
|
+
for (const item of denyIgnore) {
|
|
326
|
+
const id = typeof item === 'string' ? item : item?.id;
|
|
327
|
+
const reason = typeof item === 'string' ? undefined : item?.reason;
|
|
328
|
+
if (!isNonEmptyString(id)) { problems.push(`${DENY_FILE}: ignore item without an id`); continue; }
|
|
329
|
+
if (seen.has(id)) problems.push(`${DENY_FILE}: duplicate ignore ${id}`);
|
|
330
|
+
seen.add(id);
|
|
331
|
+
const entry = byId.get(id);
|
|
332
|
+
if (!entry) { problems.push(`${DENY_FILE}: ignore ${id} has no entry in ${EXCEPTIONS_FILE}`); continue; }
|
|
333
|
+
if (reason !== entry.rationale) problems.push(`${DENY_FILE}: ignore ${id} reason differs from the rationale in ${EXCEPTIONS_FILE}`);
|
|
334
|
+
}
|
|
335
|
+
for (const id of byId.keys()) if (!seen.has(id)) problems.push(`${EXCEPTIONS_FILE}: entry ${id} has no ignore in ${DENY_FILE}`);
|
|
336
|
+
}
|
|
337
|
+
if (adv) {
|
|
338
|
+
if (adv.yanked !== 'deny') problems.push(`${DENY_FILE}: [advisories] yanked must be "deny"`);
|
|
339
|
+
if (adv.unmaintained !== 'workspace') problems.push(`${DENY_FILE}: [advisories] unmaintained must be "workspace"`);
|
|
340
|
+
} else problems.push(`${DENY_FILE}: [advisories] section is missing`);
|
|
341
|
+
|
|
342
|
+
// audit.toml.
|
|
343
|
+
const auditIgnore = audit.advisories?.ignore;
|
|
344
|
+
if (!Array.isArray(auditIgnore) || !auditIgnore.every(x => typeof x === 'string')) {
|
|
345
|
+
problems.push(`${AUDIT_FILE}: [advisories].ignore must be an array of string ids`);
|
|
346
|
+
} else {
|
|
347
|
+
const set = new Set();
|
|
348
|
+
for (const id of auditIgnore) {
|
|
349
|
+
if (set.has(id)) problems.push(`${AUDIT_FILE}: duplicate ignore ${id}`);
|
|
350
|
+
set.add(id);
|
|
351
|
+
if (!byId.has(id)) problems.push(`${AUDIT_FILE}: ignore ${id} has no entry in ${EXCEPTIONS_FILE}`);
|
|
352
|
+
}
|
|
353
|
+
for (const id of byId.keys()) if (!set.has(id)) problems.push(`${AUDIT_FILE}: entry ${id} from ${EXCEPTIONS_FILE} is not ignored here (sets differ)`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return problems;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ---- CLI ----
|
|
360
|
+
|
|
361
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
362
|
+
const args = process.argv.slice(2);
|
|
363
|
+
const root = path.resolve(import.meta.dirname, '..', '..');
|
|
364
|
+
const problems = checkAdvisoryExceptions({ root, allowPending: args.includes('--allow-pending') });
|
|
365
|
+
if (problems.length) {
|
|
366
|
+
for (const p of problems) console.error(`advisory-exceptions: ${p}`);
|
|
367
|
+
console.error(`advisory-exceptions: ${problems.length} problem(s)`);
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
370
|
+
console.log('advisory-exceptions: ok');
|
|
371
|
+
}
|
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/bin/darwin/rightkit-swap
CHANGED
|
Binary file
|