@package-broker/cli 0.19.8 → 2.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/LICENSE +661 -0
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/bin.d.ts +3 -0
  4. package/dist/bin.d.ts.map +1 -0
  5. package/dist/bin.js +12 -0
  6. package/dist/bin.js.map +1 -0
  7. package/dist/build-artifacts.d.ts +27 -0
  8. package/dist/build-artifacts.d.ts.map +1 -0
  9. package/dist/build-artifacts.js +169 -0
  10. package/dist/build-artifacts.js.map +1 -0
  11. package/dist/build-assets.d.ts +36 -0
  12. package/dist/build-assets.d.ts.map +1 -0
  13. package/dist/build-assets.js +224 -0
  14. package/dist/build-assets.js.map +1 -0
  15. package/dist/build.d.ts +31 -0
  16. package/dist/build.d.ts.map +1 -0
  17. package/dist/build.js +277 -0
  18. package/dist/build.js.map +1 -0
  19. package/dist/cli.d.ts +2 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +828 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/cloudflare/d1-http-executor.d.ts +30 -0
  24. package/dist/cloudflare/d1-http-executor.d.ts.map +1 -0
  25. package/dist/cloudflare/d1-http-executor.js +230 -0
  26. package/dist/cloudflare/d1-http-executor.js.map +1 -0
  27. package/dist/cloudflare/d1-http-response.d.ts +4 -0
  28. package/dist/cloudflare/d1-http-response.d.ts.map +1 -0
  29. package/dist/cloudflare/d1-http-response.js +113 -0
  30. package/dist/cloudflare/d1-http-response.js.map +1 -0
  31. package/dist/commands/admin.d.ts +47 -0
  32. package/dist/commands/admin.d.ts.map +1 -0
  33. package/dist/commands/admin.js +447 -0
  34. package/dist/commands/admin.js.map +1 -0
  35. package/dist/commands/built-manifest.d.ts +13 -0
  36. package/dist/commands/built-manifest.d.ts.map +1 -0
  37. package/dist/commands/built-manifest.js +197 -0
  38. package/dist/commands/built-manifest.js.map +1 -0
  39. package/dist/commands/cloudflare/api.d.ts +132 -0
  40. package/dist/commands/cloudflare/api.d.ts.map +1 -0
  41. package/dist/commands/cloudflare/api.js +502 -0
  42. package/dist/commands/cloudflare/api.js.map +1 -0
  43. package/dist/commands/cloudflare/deploy.d.ts +111 -0
  44. package/dist/commands/cloudflare/deploy.d.ts.map +1 -0
  45. package/dist/commands/cloudflare/deploy.js +2170 -0
  46. package/dist/commands/cloudflare/deploy.js.map +1 -0
  47. package/dist/commands/cloudflare/inventory.d.ts +257 -0
  48. package/dist/commands/cloudflare/inventory.d.ts.map +1 -0
  49. package/dist/commands/cloudflare/inventory.js +842 -0
  50. package/dist/commands/cloudflare/inventory.js.map +1 -0
  51. package/dist/commands/cloudflare/janitor.d.ts +10 -0
  52. package/dist/commands/cloudflare/janitor.d.ts.map +1 -0
  53. package/dist/commands/cloudflare/janitor.js +84 -0
  54. package/dist/commands/cloudflare/janitor.js.map +1 -0
  55. package/dist/commands/cloudflare/migrations-check.d.ts +5 -0
  56. package/dist/commands/cloudflare/migrations-check.d.ts.map +1 -0
  57. package/dist/commands/cloudflare/migrations-check.js +10 -0
  58. package/dist/commands/cloudflare/migrations-check.js.map +1 -0
  59. package/dist/commands/cloudflare/providers-file.d.ts +17 -0
  60. package/dist/commands/cloudflare/providers-file.d.ts.map +1 -0
  61. package/dist/commands/cloudflare/providers-file.js +155 -0
  62. package/dist/commands/cloudflare/providers-file.js.map +1 -0
  63. package/dist/commands/cloudflare/provision.d.ts +79 -0
  64. package/dist/commands/cloudflare/provision.d.ts.map +1 -0
  65. package/dist/commands/cloudflare/provision.js +961 -0
  66. package/dist/commands/cloudflare/provision.js.map +1 -0
  67. package/dist/commands/cloudflare/queue-consumer.d.ts +4 -0
  68. package/dist/commands/cloudflare/queue-consumer.d.ts.map +1 -0
  69. package/dist/commands/cloudflare/queue-consumer.js +57 -0
  70. package/dist/commands/cloudflare/queue-consumer.js.map +1 -0
  71. package/dist/commands/cloudflare/routes.d.ts +3 -0
  72. package/dist/commands/cloudflare/routes.d.ts.map +1 -0
  73. package/dist/commands/cloudflare/routes.js +26 -0
  74. package/dist/commands/cloudflare/routes.js.map +1 -0
  75. package/dist/commands/cloudflare/setup.d.ts +50 -0
  76. package/dist/commands/cloudflare/setup.d.ts.map +1 -0
  77. package/dist/commands/cloudflare/setup.js +836 -0
  78. package/dist/commands/cloudflare/setup.js.map +1 -0
  79. package/dist/commands/cloudflare/smoke.d.ts +29 -0
  80. package/dist/commands/cloudflare/smoke.d.ts.map +1 -0
  81. package/dist/commands/cloudflare/smoke.js +701 -0
  82. package/dist/commands/cloudflare/smoke.js.map +1 -0
  83. package/dist/commands/cloudflare/teardown.d.ts +65 -0
  84. package/dist/commands/cloudflare/teardown.d.ts.map +1 -0
  85. package/dist/commands/cloudflare/teardown.js +984 -0
  86. package/dist/commands/cloudflare/teardown.js.map +1 -0
  87. package/dist/commands/cloudflare/worker-settings.d.ts +4 -0
  88. package/dist/commands/cloudflare/worker-settings.d.ts.map +1 -0
  89. package/dist/commands/cloudflare/worker-settings.js +93 -0
  90. package/dist/commands/cloudflare/worker-settings.js.map +1 -0
  91. package/dist/commands/cloudflare/zone.d.ts +3 -0
  92. package/dist/commands/cloudflare/zone.d.ts.map +1 -0
  93. package/dist/commands/cloudflare/zone.js +73 -0
  94. package/dist/commands/cloudflare/zone.js.map +1 -0
  95. package/dist/commands/cloudflare.d.ts +55 -0
  96. package/dist/commands/cloudflare.d.ts.map +1 -0
  97. package/dist/commands/cloudflare.js +393 -0
  98. package/dist/commands/cloudflare.js.map +1 -0
  99. package/dist/commands/import-cutover.d.ts +5 -0
  100. package/dist/commands/import-cutover.d.ts.map +1 -0
  101. package/dist/commands/import-cutover.js +25 -0
  102. package/dist/commands/import-cutover.js.map +1 -0
  103. package/dist/commands/import-private-packagist.d.ts +38 -0
  104. package/dist/commands/import-private-packagist.d.ts.map +1 -0
  105. package/dist/commands/import-private-packagist.js +232 -0
  106. package/dist/commands/import-private-packagist.js.map +1 -0
  107. package/dist/commands/import-satis.d.ts +11 -0
  108. package/dist/commands/import-satis.d.ts.map +1 -0
  109. package/dist/commands/import-satis.js +112 -0
  110. package/dist/commands/import-satis.js.map +1 -0
  111. package/dist/commands/import.d.ts +22 -0
  112. package/dist/commands/import.d.ts.map +1 -0
  113. package/dist/commands/import.js +343 -0
  114. package/dist/commands/import.js.map +1 -0
  115. package/dist/commands/openapi-specs.d.ts +4 -0
  116. package/dist/commands/openapi-specs.d.ts.map +1 -0
  117. package/dist/commands/openapi-specs.js +94 -0
  118. package/dist/commands/openapi-specs.js.map +1 -0
  119. package/dist/commands/openapi.d.ts +12 -0
  120. package/dist/commands/openapi.d.ts.map +1 -0
  121. package/dist/commands/openapi.js +44 -0
  122. package/dist/commands/openapi.js.map +1 -0
  123. package/dist/commands/source.d.ts +8 -0
  124. package/dist/commands/source.d.ts.map +1 -0
  125. package/dist/commands/source.js +61 -0
  126. package/dist/commands/source.js.map +1 -0
  127. package/dist/commands/sqlite.d.ts +11 -0
  128. package/dist/commands/sqlite.d.ts.map +1 -0
  129. package/dist/commands/sqlite.js +76 -0
  130. package/dist/commands/sqlite.js.map +1 -0
  131. package/dist/import/client.d.ts +71 -0
  132. package/dist/import/client.d.ts.map +1 -0
  133. package/dist/import/client.js +348 -0
  134. package/dist/import/client.js.map +1 -0
  135. package/dist/import/credential-refs.d.ts +2 -0
  136. package/dist/import/credential-refs.d.ts.map +1 -0
  137. package/dist/import/credential-refs.js +22 -0
  138. package/dist/import/credential-refs.js.map +1 -0
  139. package/dist/import/cutover.d.ts +64 -0
  140. package/dist/import/cutover.d.ts.map +1 -0
  141. package/dist/import/cutover.js +544 -0
  142. package/dist/import/cutover.js.map +1 -0
  143. package/dist/import/private-packagist/api.d.ts +57 -0
  144. package/dist/import/private-packagist/api.d.ts.map +1 -0
  145. package/dist/import/private-packagist/api.js +246 -0
  146. package/dist/import/private-packagist/api.js.map +1 -0
  147. package/dist/import/private-packagist/dates.d.ts +30 -0
  148. package/dist/import/private-packagist/dates.d.ts.map +1 -0
  149. package/dist/import/private-packagist/dates.js +189 -0
  150. package/dist/import/private-packagist/dates.js.map +1 -0
  151. package/dist/import/private-packagist/map.d.ts +48 -0
  152. package/dist/import/private-packagist/map.d.ts.map +1 -0
  153. package/dist/import/private-packagist/map.js +162 -0
  154. package/dist/import/private-packagist/map.js.map +1 -0
  155. package/dist/import/private-packagist/plan.d.ts +61 -0
  156. package/dist/import/private-packagist/plan.d.ts.map +1 -0
  157. package/dist/import/private-packagist/plan.js +936 -0
  158. package/dist/import/private-packagist/plan.js.map +1 -0
  159. package/dist/import/private-packagist/snapshot.d.ts +31 -0
  160. package/dist/import/private-packagist/snapshot.d.ts.map +1 -0
  161. package/dist/import/private-packagist/snapshot.js +406 -0
  162. package/dist/import/private-packagist/snapshot.js.map +1 -0
  163. package/dist/import/satis.d.ts +93 -0
  164. package/dist/import/satis.d.ts.map +1 -0
  165. package/dist/import/satis.js +873 -0
  166. package/dist/import/satis.js.map +1 -0
  167. package/dist/index.d.ts +3 -2
  168. package/dist/index.d.ts.map +1 -1
  169. package/dist/index.js +3 -96
  170. package/dist/index.js.map +1 -1
  171. package/dist/manifest.d.ts +56 -0
  172. package/dist/manifest.d.ts.map +1 -0
  173. package/dist/manifest.js +137 -0
  174. package/dist/manifest.js.map +1 -0
  175. package/dist/migrations-collect.d.ts +6 -0
  176. package/dist/migrations-collect.d.ts.map +1 -0
  177. package/dist/migrations-collect.js +39 -0
  178. package/dist/migrations-collect.js.map +1 -0
  179. package/dist/prebuilt-expected.json +7 -0
  180. package/dist/prebuilt.d.ts +27 -0
  181. package/dist/prebuilt.d.ts.map +1 -0
  182. package/dist/prebuilt.js +391 -0
  183. package/dist/prebuilt.js.map +1 -0
  184. package/package.json +22 -25
  185. package/README.md +0 -29
  186. package/dist/__tests__/migrate.test.d.ts +0 -2
  187. package/dist/__tests__/migrate.test.d.ts.map +0 -1
  188. package/dist/__tests__/migrate.test.js +0 -116
  189. package/dist/__tests__/migrate.test.js.map +0 -1
  190. package/dist/migrate.d.ts +0 -40
  191. package/dist/migrate.d.ts.map +0 -1
  192. package/dist/migrate.js +0 -335
  193. package/dist/migrate.js.map +0 -1
@@ -0,0 +1,936 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { DEFAULT_ROLES, PERMISSIONS } from '@package-broker/core/authz/permissions';
3
+ import { PROJECT_SLUG_PATTERN } from '@package-broker/core/authz/project-path';
4
+ import { packageKey } from '@package-broker/core/catalogue/names';
5
+ import { MAX_PATTERN_BYTES, matchesPattern } from '@package-broker/core/catalogue/patterns';
6
+ import { canonicalManifest, MAX_MANIFEST_BYTES, MAX_MANIFEST_ITEMS, parseManifest, planHash, } from '@package-broker/core/import/manifest';
7
+ import { isCanonicalUrl } from '@package-broker/core/import/source-config';
8
+ import { normalizeEmail } from '@package-broker/core/identity/policy';
9
+ import { MapError, object, parsePackagistMap, } from './map.js';
10
+ import { canonical } from './snapshot.js';
11
+ const GRANT_ROLE_RANK = {
12
+ project_developer: 1,
13
+ project_owner: 2,
14
+ organization_admin: 3,
15
+ };
16
+ const compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
17
+ const sorted = (v) => [...v].sort(compare);
18
+ const hash = (...tuple) => createHash('sha256').update(JSON.stringify(tuple)).digest('hex');
19
+ const ref = (kind, ...parts) => `pp:${kind}:${hash(`pp-${kind}-v1`, ...parts)}`;
20
+ function json(v, code = 'invalid_snapshot') {
21
+ try {
22
+ return typeof v === 'string' ? JSON.parse(v) : v;
23
+ }
24
+ catch {
25
+ return refuse(code);
26
+ }
27
+ }
28
+ const own = (table, key) => Object.hasOwn(table, key) ? table[key] : undefined;
29
+ const stable = (v) => {
30
+ if (Array.isArray(v))
31
+ return `[${v.map(stable).join(',')}]`;
32
+ if (object(v))
33
+ return `{${Object.keys(v)
34
+ .sort(compare)
35
+ .map((k) => `${JSON.stringify(k)}:${stable(v[k])}`)
36
+ .join(',')}}`;
37
+ return JSON.stringify(v);
38
+ };
39
+ export class Refusal extends Error {
40
+ code;
41
+ pointer;
42
+ cap;
43
+ detail;
44
+ constructor(code, pointer, cap, detail) {
45
+ super(code);
46
+ this.code = code;
47
+ this.pointer = pointer;
48
+ this.cap = cap;
49
+ this.detail = detail;
50
+ }
51
+ }
52
+ function refuse(code, pointer = '', cap, detail) {
53
+ throw new Refusal(code, pointer, cap, detail);
54
+ }
55
+ export function repository(value, pointer, vcs = false) {
56
+ if (typeof value !== 'string' || /[\s\p{Cc}\p{Cf}]/u.test(value))
57
+ return refuse('invalid_repository_url', pointer);
58
+ let url = value;
59
+ if (/^[a-z0-9._-]+@[^:]+:.+$/i.test(url))
60
+ url = `ssh://${url.replace(':', '/')}`;
61
+ const ssh = url.startsWith('ssh://');
62
+ if (!ssh && !url.startsWith('https://'))
63
+ refuse('invalid_repository_url', pointer);
64
+ let parsed;
65
+ try {
66
+ parsed = new URL(url);
67
+ }
68
+ catch {
69
+ return refuse('invalid_repository_url', pointer);
70
+ }
71
+ if ((ssh ? !/^[a-z0-9._-]{1,32}$/.test(parsed.username) : parsed.username) ||
72
+ parsed.password ||
73
+ /[?#]/.test(url) ||
74
+ !parsed.hostname)
75
+ refuse('invalid_repository_url', pointer);
76
+ const github = parsed.hostname.toLowerCase() === 'github.com';
77
+ if (github)
78
+ parsed.hostname = 'github.com';
79
+ url = parsed.href;
80
+ if (!ssh && !isCanonicalUrl(url))
81
+ refuse('invalid_repository_url', pointer);
82
+ if ((!ssh && vcs) || github) {
83
+ let path = parsed.pathname.replace(/\/+$/, '').replace(/\.git$/, '');
84
+ if (github) {
85
+ path = path.toLowerCase();
86
+ if (!/^\/[a-z0-9-]+\/[a-z0-9._-]+$/.test(path))
87
+ refuse('invalid_repository_url', pointer);
88
+ }
89
+ parsed.pathname = path || '/';
90
+ url = parsed.href;
91
+ }
92
+ return url;
93
+ }
94
+ function readMembers(text) {
95
+ const result = new Map();
96
+ if (text === undefined)
97
+ return result;
98
+ const lines = text
99
+ .replace(/^\uFEFF/, '')
100
+ .replace(/(?:\r?\n)+$/, '')
101
+ .split(/\r?\n/);
102
+ const fieldsOf = (line, pointer) => {
103
+ const match = /^("(?:[^"]|"")*"|[^",]*),("(?:[^"]|"")*"|[^",]*)$/.exec(line);
104
+ if (!match)
105
+ return refuse('invalid_members', pointer);
106
+ return match
107
+ .slice(1)
108
+ .map((field) => (field.startsWith('"') ? field.slice(1, -1).replaceAll('""', '"') : field));
109
+ };
110
+ if (stable(fieldsOf(lines.shift() ?? '', '/members')) !== stable(['username', 'email']))
111
+ refuse('invalid_members', '/members');
112
+ for (const [i, line] of lines.entries()) {
113
+ const fields = fieldsOf(line, `/members/${i}`);
114
+ const username = fields[0];
115
+ const email = normalizeEmail(fields[1] ?? '');
116
+ if (fields.length !== 2 ||
117
+ !username ||
118
+ /[\p{Cc}]/u.test(username) ||
119
+ email === null ||
120
+ result.has(username))
121
+ refuse('invalid_members', `/members/${i}`);
122
+ result.set(username, email);
123
+ }
124
+ return result;
125
+ }
126
+ function rowsOf(input) {
127
+ const value = json(input);
128
+ if (!object(value) ||
129
+ value.version !== 1 ||
130
+ !object(value.objects) ||
131
+ typeof value.org !== 'string' ||
132
+ typeof value.versions !== 'boolean' ||
133
+ typeof value.fetchedAt !== 'string')
134
+ refuse('invalid_snapshot');
135
+ const result = {};
136
+ for (const [kind, entries] of Object.entries(value.objects)) {
137
+ if (!Array.isArray(entries))
138
+ refuse('invalid_snapshot');
139
+ const unique = new Map();
140
+ for (const entry of entries) {
141
+ if (!object(entry) ||
142
+ !object(entry.data) ||
143
+ entry.sha256 !== createHash('sha256').update(canonical(entry.data)).digest('hex'))
144
+ refuse('invalid_snapshot');
145
+ unique.set(stable(entry.data), entry.data);
146
+ }
147
+ result[kind] = [...unique.entries()].sort(([a], [b]) => compare(a, b)).map(([, v]) => v);
148
+ }
149
+ return result;
150
+ }
151
+ function credential(source, map, summary) {
152
+ const bindings = new Set();
153
+ const matches = map.credentials
154
+ .flatMap((rule, index) => {
155
+ const m = rule.match;
156
+ let score = -1;
157
+ if ('package' in m && [...source.names].some((n) => matchesPattern(m.package, n)))
158
+ score = m.package.includes('*') ? 4_000_000 : 5_000_000;
159
+ if ('url_prefix' in m &&
160
+ (source.identity === m.url_prefix ||
161
+ source.identity.startsWith(m.url_prefix.endsWith('/') ? m.url_prefix : `${m.url_prefix}/`)))
162
+ score = 3_000_000 + m.url_prefix.length;
163
+ if ('packagist_credential' in m && source.credentialIds.has(m.packagist_credential))
164
+ score = 2_000_000;
165
+ if ('host' in m && new URL(source.identity).hostname === m.host)
166
+ score = 1_000_000;
167
+ return score < 0 ? [] : [{ rule, index, score }];
168
+ })
169
+ .sort((a, b) => b.score - a.score)
170
+ .filter(({ rule }) => {
171
+ const key = stable([rule.kind, rule.ref]);
172
+ if (bindings.has(key))
173
+ return false;
174
+ bindings.add(key);
175
+ return true;
176
+ });
177
+ if (matches.length === 0)
178
+ return undefined;
179
+ if (matches[1]?.score === matches[0].score)
180
+ refuse('ambiguous_credential', source.pointer);
181
+ const { rule, index } = matches[0];
182
+ if ('host' in rule.match) {
183
+ const pointer = `/credentials/${index}`;
184
+ const entry = summary.warning.find((e) => e.code === 'host_credential_coverage' && e.pointer === pointer);
185
+ if (entry)
186
+ summary.warning[summary.warning.indexOf(entry)] = { ...entry, count: entry.count + 1 };
187
+ else
188
+ summary.warning.push({ code: 'host_credential_coverage', pointer, count: 1 });
189
+ }
190
+ const provider = source.item.source_type === 'vcs'
191
+ ? source.item.config.provider
192
+ : null;
193
+ if (rule.ref !== null &&
194
+ ((provider === 'github' && !['github_app', 'http_basic'].includes(rule.kind)) ||
195
+ (provider === 'git' && rule.kind !== 'http_basic') ||
196
+ (provider === 'ssh' && rule.kind !== 'ssh_key') ||
197
+ (provider === null && !['http_basic', 'bearer'].includes(rule.kind))))
198
+ refuse('invalid_credential_kind', `/credentials/${index}`);
199
+ return rule;
200
+ }
201
+ export async function planPrivatePackagistImport(input) {
202
+ const summary = {
203
+ create: [],
204
+ existing: [],
205
+ warning: [],
206
+ needsInput: [],
207
+ unsupported: [],
208
+ };
209
+ try {
210
+ const result = await compile(input, summary);
211
+ for (const entries of Object.values(summary))
212
+ entries.sort((a, b) => compare(stable(a), stable(b)));
213
+ return { ...result, summary };
214
+ }
215
+ catch (error) {
216
+ const diagnostic = error instanceof Refusal
217
+ ? {
218
+ code: error.code,
219
+ pointer: error.pointer,
220
+ ...(error.cap === undefined ? {} : { cap: error.cap }),
221
+ ...(error.detail === undefined ? {} : { detail: error.detail }),
222
+ }
223
+ : error instanceof MapError
224
+ ? {
225
+ code: 'invalid_map',
226
+ pointer: error.pointer,
227
+ ...(error.cap === undefined ? {} : { cap: error.cap }),
228
+ }
229
+ : { code: 'invalid_snapshot', pointer: '' };
230
+ return { ok: false, diagnostics: [diagnostic], summary };
231
+ }
232
+ }
233
+ async function compile(input, summary) {
234
+ const options = input.options ?? {};
235
+ if (options.mode === 'override')
236
+ refuse('override_mode_unsupported', '/options/mode');
237
+ for (const [index, vendor] of (options.compactVendors ?? []).entries())
238
+ if (!/^[a-z0-9](?:[a-z0-9_.-]*[a-z0-9])?$/.test(vendor))
239
+ refuse('invalid_compact_vendor', `/options/compactVendors/${index}`);
240
+ const compactVendors = new Set(options.compactVendors ?? []);
241
+ const map = parsePackagistMap(input.map);
242
+ const rows = rowsOf(input.snapshot);
243
+ for (const [i, rule] of map.credentials.entries())
244
+ if ('host' in rule.match)
245
+ summary.warning.push({
246
+ code: 'host_credential_coverage',
247
+ pointer: `/credentials/${i}`,
248
+ count: 0,
249
+ });
250
+ const members = readMembers(input.members);
251
+ const baseline = input.baseline === undefined ? null : parseManifest(json(input.baseline, 'invalid_baseline'));
252
+ if (baseline !== null && (!baseline.ok || baseline.data.source_system !== 'private-packagist'))
253
+ refuse('invalid_baseline', '/baseline');
254
+ const retained = baseline?.ok ? baseline.data : null;
255
+ const retainedVcsPrecedence = new Map();
256
+ if (retained) {
257
+ const oldProjects = new Map(retained.items.flatMap((item) => item.type === 'project' ? [[item.external_ref, item.slug]] : []));
258
+ const oldSources = new Map(retained.items.flatMap((item) => item.type === 'source' ? [[item.external_ref, item]] : []));
259
+ for (const item of retained.items) {
260
+ if (item.type !== 'attachment' ||
261
+ !('ref' in item.source) ||
262
+ oldSources.get(item.source.ref)?.source_type !== 'vcs')
263
+ continue;
264
+ const slug = 'existing' in item.project ? item.project.existing : oldProjects.get(item.project.ref);
265
+ if (slug !== undefined)
266
+ retainedVcsPrecedence.set(`${slug}\0${item.source.ref}`, item.precedence);
267
+ }
268
+ }
269
+ const items = [];
270
+ const widening = [];
271
+ const projects = new Map();
272
+ for (const [i, row] of (rows.suborganizations ?? []).entries()) {
273
+ const pointer = `/objects/suborganizations/${i}`;
274
+ if (typeof row.id !== 'number' ||
275
+ typeof row.urlName !== 'string' ||
276
+ typeof row.name !== 'string' ||
277
+ projects.has(row.id))
278
+ refuse('invalid_snapshot', pointer);
279
+ const mapping = own(map.projects, row.urlName);
280
+ const slug = mapping?.existing ?? mapping?.slug ?? row.urlName;
281
+ if (!PROJECT_SLUG_PATTERN.test(slug) || /[\p{Cc}]/u.test(slug))
282
+ refuse('project_slug_required', pointer);
283
+ const external_ref = ref('project', row.id);
284
+ const reference = mapping?.existing === undefined ? { ref: external_ref } : { existing: slug };
285
+ projects.set(row.id, { id: row.id, slug, reference, pointer });
286
+ if ('ref' in reference)
287
+ items.push({ type: 'project', external_ref, slug, name: row.name });
288
+ else
289
+ summary.existing.push({ code: 'project', project: slug });
290
+ }
291
+ const projectSlugs = new Set([...projects.values()].map((p) => p.slug));
292
+ for (const [index, slug] of (options.acceptProjectWidening ?? []).entries())
293
+ if (!projectSlugs.has(slug))
294
+ refuse('unknown_project_acknowledgement', `/options/acceptProjectWidening/${index}`);
295
+ for (const kind of [
296
+ 'suborganizationPackages',
297
+ 'suborganizationTeams',
298
+ 'suborganizationMirroredRepositories',
299
+ 'suborganizationTokens',
300
+ ])
301
+ for (const [index, row] of (rows[kind] ?? []).entries())
302
+ if (!projects.has(row.suborganizationId))
303
+ refuse('invalid_snapshot', `/objects/${kind}/${index}`);
304
+ const sources = new Map();
305
+ const sourceNames = new Map();
306
+ function source(identity, type, url, config, pointer) {
307
+ const external_ref = ref('source', identity);
308
+ const name = `pp-${external_ref.split(':')[2].slice(0, 61)}`;
309
+ if (sourceNames.has(name) && sourceNames.get(name) !== external_ref)
310
+ refuse('source_name_collision', pointer);
311
+ sourceNames.set(name, external_ref);
312
+ const previous = sources.get(external_ref);
313
+ if (previous) {
314
+ if (stable(previous.item.config) !== stable(config))
315
+ refuse('conflicting_source', pointer);
316
+ return previous;
317
+ }
318
+ const item = {
319
+ type: 'source',
320
+ external_ref,
321
+ name,
322
+ ecosystem: 'composer',
323
+ project: null,
324
+ source_type: type,
325
+ url,
326
+ credential_ref: null,
327
+ config,
328
+ };
329
+ const result = {
330
+ item,
331
+ identity: Array.isArray(identity) ? identity[0] : identity,
332
+ names: new Set(),
333
+ credentialIds: new Set(),
334
+ mirrorIds: new Set(),
335
+ pointer,
336
+ patterns: [],
337
+ reference: { ref: external_ref },
338
+ };
339
+ sources.set(external_ref, result);
340
+ return result;
341
+ }
342
+ const mirrors = new Map();
343
+ const mirrorCredentials = new Map();
344
+ for (const [collection, index, row] of [
345
+ 'mirroredRepositories',
346
+ 'suborganizationMirroredRepositories',
347
+ ].flatMap((collection) => (rows[collection] ?? []).map((row, index) => [collection, index, row]))) {
348
+ if (typeof row.id !== 'number' || typeof row.url !== 'string')
349
+ refuse('invalid_snapshot');
350
+ if (row.mirroringBehavior !== undefined &&
351
+ (typeof row.mirroringBehavior !== 'string' ||
352
+ !['manual', 'add_on_use', 'admin_managed', 'disabled'].includes(row.mirroringBehavior)))
353
+ refuse('unsupported_mirroring_behavior', `/objects/${collection}/${index}`);
354
+ const previous = mirrors.get(row.id);
355
+ if (previous && previous.url !== row.url)
356
+ refuse('conflicting_source');
357
+ if (!previous)
358
+ mirrors.set(row.id, row);
359
+ const ids = mirrorCredentials.get(row.id) ?? new Set();
360
+ if (typeof row.credentials === 'number')
361
+ ids.add(row.credentials);
362
+ mirrorCredentials.set(row.id, ids);
363
+ }
364
+ for (const [index, id] of map.mirrorOrder.entries())
365
+ if (!mirrors.has(id))
366
+ summary.warning.push({ code: 'stale_mirror_order', pointer: `/mirrorOrder/${index}` });
367
+ const packageSources = new Map();
368
+ const packageRows = [...(rows.packages ?? []), ...(rows.suborganizationPackages ?? [])];
369
+ const reported = new Set();
370
+ for (const [i, pkg] of packageRows.entries()) {
371
+ const pointer = `/packages/${i}`;
372
+ if (typeof pkg.name !== 'string' || !object(pkg.config) || typeof pkg.id !== 'number')
373
+ refuse('invalid_snapshot', pointer);
374
+ const name = pkg.name;
375
+ try {
376
+ if (packageKey(name, { ecosystem: 'composer' }) !== name)
377
+ refuse('invalid_package', pointer);
378
+ }
379
+ catch {
380
+ refuse('invalid_package', pointer);
381
+ }
382
+ if (Buffer.byteLength(name) > MAX_PATTERN_BYTES)
383
+ refuse('single_run_limit_exceeded', pointer, 'MAX_PATTERN_BYTES');
384
+ const c = pkg.config;
385
+ if (c.defaultSuborganizationAccess !== undefined &&
386
+ (typeof c.defaultSuborganizationAccess !== 'string' ||
387
+ !['active', 'no-access'].includes(c.defaultSuborganizationAccess)))
388
+ refuse('unsupported_default_suborganization_access', pointer);
389
+ if (c.customJson !== undefined && c.customJson !== null && c.customJson !== '') {
390
+ summary.unsupported.push({ code: 'inline_package_unsupported', pointer });
391
+ refuse('inline_package_unsupported', pointer);
392
+ }
393
+ if (!reported.has(`pkg:${pkg.id}`)) {
394
+ reported.add(`pkg:${pkg.id}`);
395
+ if (pkg.abandoned)
396
+ summary.warning.push({ code: 'abandoned', pointer });
397
+ if (pkg.replacementPackage)
398
+ summary.warning.push({ code: 'replacement_package', pointer });
399
+ if (c.defaultSuborganizationAccess === 'active')
400
+ summary.warning.push({ code: 'auto_visible_to_new_projects', pointer });
401
+ }
402
+ if (c.type === 'artifact' || (Array.isArray(c.artifactIds) && c.artifactIds.length > 0)) {
403
+ if (!reported.has(`artifact:${pkg.id}`)) {
404
+ reported.add(`artifact:${pkg.id}`);
405
+ const artifacts = (rows.artifacts ?? []).filter((a) => a.packageId === pkg.id);
406
+ summary.needsInput.push({
407
+ code: 'needs-upload',
408
+ pointer,
409
+ count: artifacts.length,
410
+ detail: artifacts
411
+ .map((a) => {
412
+ if (typeof a.originalFileName !== 'string' ||
413
+ /[\p{Cc}]|[^\s@]+@[^\s@]+\.[^\s@]+/u.test(a.originalFileName))
414
+ return refuse('invalid_snapshot', pointer);
415
+ return a.originalFileName;
416
+ })
417
+ .sort(compare)
418
+ .join(', '),
419
+ });
420
+ }
421
+ continue;
422
+ }
423
+ let s;
424
+ if (['public-mirror', 'public-proxy'].includes(String(pkg.origin))) {
425
+ s = source('packagist', 'packagist_proxy', 'https://repo.packagist.org', { githubToken: false }, pointer);
426
+ if (options.packagist !== undefined) {
427
+ if (!/^existing:[a-z0-9][a-z0-9._-]{0,63}$/.test(options.packagist))
428
+ refuse('invalid_packagist', '/options/packagist');
429
+ s.reference = { existing: { project: null, name: options.packagist.slice(9) } };
430
+ }
431
+ }
432
+ else if (['private-mirror', 'private-proxy'].includes(String(pkg.origin))) {
433
+ const mirror = mirrors.get(c.mirroredRepositoryId);
434
+ if (!mirror)
435
+ refuse('mirror_missing', pointer);
436
+ const url = repository(mirror.url, pointer);
437
+ if (url.startsWith('ssh://'))
438
+ refuse('ssh_requires_node', pointer);
439
+ s = source(url, 'composer_repository', url, {
440
+ selection: { packages: [], patterns: [], expandPatterns: false },
441
+ distHosts: [],
442
+ mirrorsFallback: false,
443
+ }, pointer);
444
+ s.mirrorIds.add(mirror.id);
445
+ for (const id of mirrorCredentials.get(mirror.id) ?? [])
446
+ s.credentialIds.add(id);
447
+ }
448
+ else if (pkg.origin === 'private') {
449
+ if (c.type !== undefined &&
450
+ !['vcs', 'git', 'github', 'gitlab', 'bitbucket'].includes(String(c.type)))
451
+ refuse('unsupported_package_type', pointer);
452
+ let url = repository(own(map.packages, name)?.url ?? c.url, pointer, true);
453
+ const parsed = new URL(url);
454
+ const installationId = parsed.hostname === 'github.com'
455
+ ? own(map.github.installations, parsed.pathname.split('/')[1])
456
+ : undefined;
457
+ if (url.startsWith('ssh://')) {
458
+ if (installationId !== undefined)
459
+ url = `https://github.com${parsed.pathname}`;
460
+ else if (options.runtime !== 'node')
461
+ refuse('ssh_requires_node', pointer);
462
+ }
463
+ const config = {
464
+ provider: installationId !== undefined
465
+ ? 'github'
466
+ : url.startsWith('ssh://')
467
+ ? 'ssh'
468
+ : 'git',
469
+ repository: installationId !== undefined ? parsed.pathname.slice(1) : url,
470
+ installationId: installationId ?? null,
471
+ packageName: name,
472
+ refs: { tags: true, branches: ['*'] },
473
+ };
474
+ s = source([url, name], 'vcs', null, config, pointer);
475
+ }
476
+ else
477
+ refuse('unsupported_package_origin', pointer);
478
+ if (typeof c.credentialsId === 'number')
479
+ s.credentialIds.add(c.credentialsId);
480
+ s.names.add(name);
481
+ packageSources.set(pkg, s);
482
+ }
483
+ for (const [index, [id, mirror]] of [...mirrors].entries()) {
484
+ const used = [...sources.values()].some((s) => s.mirrorIds.has(id));
485
+ summary.warning.push({
486
+ code: !used && mirror.mirroringBehavior === 'disabled'
487
+ ? 'disabled_mirror_skipped'
488
+ : 'mirroring_behavior',
489
+ pointer: `/mirrors/${index}`,
490
+ detail: ['manual', 'add_on_use', 'admin_managed', 'disabled'].includes(String(mirror.mirroringBehavior))
491
+ ? String(mirror.mirroringBehavior)
492
+ : 'unspecified',
493
+ count: used ? 1 : 0,
494
+ });
495
+ }
496
+ const allSources = [...sources.values()].sort((a, b) => compare(a.item.external_ref, b.item.external_ref));
497
+ const credentialOutput = [];
498
+ for (const s of allSources) {
499
+ if (s.item.source_type === 'packagist_proxy')
500
+ continue;
501
+ const binding = credential(s, map, summary);
502
+ if (binding === undefined) {
503
+ const suggestions = {
504
+ magento: 'http_basic',
505
+ 'http-basic': 'http_basic',
506
+ 'bitbucket-app-pw': 'http_basic',
507
+ 'github-oauth': 'http_basic or github_app',
508
+ 'gitlab-token': 'http_basic or github_app',
509
+ 'gitlab-oauth': 'http_basic or github_app',
510
+ 'bitbucket-api': 'http_basic or github_app',
511
+ 'bearer-token': 'bearer',
512
+ 'ssh-key': 'ssh_key',
513
+ };
514
+ const kinds = sorted(new Set((rows.credentials ?? [])
515
+ .filter((c) => s.credentialIds.has(c.id) || c.domain === new URL(s.identity).hostname)
516
+ .flatMap((c) => typeof c.type === 'string' && Object.hasOwn(suggestions, c.type)
517
+ ? [suggestions[c.type]]
518
+ : [])));
519
+ summary.needsInput.push({
520
+ code: 'unbound_credential',
521
+ source: s.item.name,
522
+ ...(kinds.length ? { detail: kinds.join(', ') } : {}),
523
+ });
524
+ }
525
+ else {
526
+ s.item = { ...s.item, credential_ref: binding.ref };
527
+ if (binding.ref !== null)
528
+ credentialOutput.push({
529
+ external_ref: s.item.external_ref,
530
+ credential_ref: binding.ref,
531
+ kind: binding.kind,
532
+ });
533
+ }
534
+ if (s.item.source_type !== 'composer_repository')
535
+ continue;
536
+ let packages = sorted(s.names);
537
+ const vendors = new Map();
538
+ const proxyVendors = new Set();
539
+ const unapprovedVendors = new Set();
540
+ for (const n of packages) {
541
+ const vendor = n.split('/')[0];
542
+ if (!compactVendors.has(vendor))
543
+ unapprovedVendors.add(vendor);
544
+ vendors.set(vendor, [...(vendors.get(vendor) ?? []), n]);
545
+ }
546
+ if (packages.length > 64 && options.compactSelection) {
547
+ const candidates = [...vendors].sort(([a, av], [b, bv]) => bv.length - av.length || compare(a, b));
548
+ for (const [vendor, names] of candidates) {
549
+ if (packages.length + s.patterns.length <= 64)
550
+ break;
551
+ if (names.length < 2)
552
+ continue;
553
+ if (!compactVendors.has(vendor))
554
+ continue;
555
+ const shared = allSources.filter((other) => other !== s && [...other.names].some((n) => n.startsWith(`${vendor}/`)));
556
+ if (shared.length) {
557
+ if (shared.some((other) => other.item.source_type === 'packagist_proxy'))
558
+ proxyVendors.add(vendor);
559
+ continue;
560
+ }
561
+ packages = packages.filter((n) => !n.startsWith(`${vendor}/`));
562
+ const pattern = `${vendor}/*`;
563
+ if (Buffer.byteLength(pattern) > MAX_PATTERN_BYTES)
564
+ refuse('single_run_limit_exceeded', s.pointer, 'MAX_PATTERN_BYTES');
565
+ s.patterns.push(pattern);
566
+ summary.warning.push({
567
+ code: 'selection_compacted',
568
+ source: s.item.name,
569
+ pattern,
570
+ count: names.length,
571
+ detail: 'Vendor opt-in accepts residual risk: the snapshot cannot reveal unused or future public packages; this wildcard may serve them from the mirror and prevent proxy fallback.',
572
+ });
573
+ }
574
+ }
575
+ if (packages.length + s.patterns.length > 64)
576
+ refuse('single_run_limit_exceeded', s.pointer, 'SOURCE_CONFIG_LIST_LIMIT', [
577
+ ...(unapprovedVendors.size
578
+ ? [
579
+ `Vendors requiring --compact-vendor opt-in: ${sorted(unapprovedVendors).join(', ')}.`,
580
+ ]
581
+ : []),
582
+ ...(proxyVendors.size
583
+ ? [`Proxy-served vendors: ${sorted(proxyVendors).join(', ')}.`]
584
+ : []),
585
+ 'See owner question A (project_package).',
586
+ ].join(' '));
587
+ const hosts = sorted(new Set([...s.mirrorIds].flatMap((id) => own(map.distHosts, String(id)) ?? own(map.distHosts, s.identity) ?? [])));
588
+ if (hosts.length > 64)
589
+ refuse('single_run_limit_exceeded', s.pointer, 'SOURCE_CONFIG_LIST_LIMIT');
590
+ s.item = {
591
+ ...s.item,
592
+ config: {
593
+ selection: { packages, patterns: sorted(s.patterns), expandPatterns: false },
594
+ distHosts: hosts,
595
+ mirrorsFallback: false,
596
+ },
597
+ };
598
+ }
599
+ const nameOwners = new Map();
600
+ for (const s of allSources) {
601
+ if ('ref' in s.reference)
602
+ items.push(s.item);
603
+ else
604
+ summary.existing.push({ code: 'source', source: s.reference.existing.name });
605
+ if (s.item.source_type !== 'packagist_proxy')
606
+ for (const n of sorted(s.names))
607
+ if (!nameOwners.has(n))
608
+ nameOwners.set(n, s);
609
+ }
610
+ for (const [pattern, s] of nameOwners)
611
+ items.push({
612
+ type: 'name',
613
+ external_ref: ref('name', pattern),
614
+ source: s.reference,
615
+ ecosystem: 'composer',
616
+ pattern,
617
+ });
618
+ const vcs = allSources.filter((s) => s.item.source_type === 'vcs');
619
+ if (vcs.length > 8999)
620
+ refuse('single_run_limit_exceeded', '', 'MAX_VCS_SOURCES');
621
+ const mirrorSources = allSources.filter((s) => s.item.source_type === 'composer_repository');
622
+ if (mirrorSources.length > 89)
623
+ refuse('single_run_limit_exceeded', '', 'MAX_MIRRORS');
624
+ if (mirrorSources.some((s) => ![...s.mirrorIds].some((id) => map.mirrorOrder.includes(id))))
625
+ refuse('mirror_order_required', '/mirrorOrder');
626
+ const mirrorRank = (s) => Math.min(...[...s.mirrorIds].map((id) => map.mirrorOrder.indexOf(id)).filter((n) => n >= 0));
627
+ const mirrorSorted = [...mirrorSources].sort((a, b) => mirrorRank(a) - mirrorRank(b) || compare(a.item.external_ref, b.item.external_ref));
628
+ for (const project of projects.values()) {
629
+ const listed = (rows.suborganizationPackages ?? []).filter((pkg) => pkg.suborganizationId === project.id);
630
+ const wanted = listed.flatMap((pkg) => {
631
+ const s = packageSources.get(pkg);
632
+ return s ? [{ name: pkg.name, s }] : [];
633
+ });
634
+ const attached = [...new Set(wanted.map((w) => w.s))];
635
+ const conflict = (conflicting) => {
636
+ for (const s of conflicting)
637
+ summary.warning.push({
638
+ code: 'cross_source_conflict',
639
+ project: project.slug,
640
+ source: s.item.name,
641
+ });
642
+ return refuse('cross_source_conflict', project.pointer);
643
+ };
644
+ const edges = new Map(attached.map((s) => [s, new Set()]));
645
+ for (const { name, s } of wanted)
646
+ for (const other of attached) {
647
+ if (other !== s && other.item.source_type !== 'packagist_proxy' && other.names.has(name))
648
+ edges.get(s).add(other);
649
+ }
650
+ const ordered = [];
651
+ const pending = new Set(attached);
652
+ const projectVcs = attached
653
+ .filter((s) => s.item.source_type === 'vcs')
654
+ .sort((a, b) => compare(a.item.external_ref, b.item.external_ref));
655
+ const vcsPositions = new Map();
656
+ const occupied = new Set();
657
+ for (const s of projectVcs) {
658
+ const position = retainedVcsPrecedence.get(`${project.slug}\0${s.item.external_ref}`);
659
+ if (position !== undefined) {
660
+ vcsPositions.set(s, position);
661
+ occupied.add(position);
662
+ }
663
+ }
664
+ let nextVcsPosition = 1000;
665
+ for (const s of projectVcs) {
666
+ if (vcsPositions.has(s))
667
+ continue;
668
+ while (occupied.has(nextVcsPosition))
669
+ nextVcsPosition++;
670
+ if (nextVcsPosition > 9998)
671
+ refuse('single_run_limit_exceeded', project.pointer, 'MAX_VCS_SOURCES');
672
+ vcsPositions.set(s, nextVcsPosition);
673
+ occupied.add(nextVcsPosition++);
674
+ }
675
+ const priority = (s) => s.item.source_type === 'vcs'
676
+ ? vcsPositions.get(s)
677
+ : s.item.source_type === 'composer_repository'
678
+ ? 10000 + 100 * mirrorRank(s)
679
+ : 99999;
680
+ while (pending.size) {
681
+ const ready = [...pending]
682
+ .filter((s) => ![...pending].some((other) => edges.get(other).has(s)))
683
+ .sort((a, b) => priority(a) - priority(b) || compare(a.item.external_ref, b.item.external_ref));
684
+ if (!ready.length)
685
+ conflict(pending);
686
+ const next = ready[0];
687
+ ordered.push(next);
688
+ pending.delete(next);
689
+ }
690
+ const localMirrors = ordered.filter((s) => s.item.source_type === 'composer_repository');
691
+ const precedence = (s) => s.item.source_type === 'composer_repository'
692
+ ? 10000 +
693
+ 100 *
694
+ mirrorRank(mirrorSorted.filter((m) => attached.includes(m))[localMirrors.indexOf(s)])
695
+ : priority(s);
696
+ const conflicting = new Set();
697
+ for (const s of ordered)
698
+ for (const other of edges.get(s))
699
+ if (precedence(s) >= precedence(other)) {
700
+ conflicting.add(s);
701
+ conflicting.add(other);
702
+ }
703
+ if (conflicting.size)
704
+ conflict(conflicting);
705
+ const projectNames = new Set(listed.map((pkg) => pkg.name));
706
+ for (const s of attached) {
707
+ if (s.item.source_type === 'composer_repository') {
708
+ const extra = [...s.names].filter((n) => !projectNames.has(n)).length;
709
+ if (extra > 0 || s.patterns.length > 0) {
710
+ summary.warning.push({
711
+ code: 'project_widening',
712
+ project: project.slug,
713
+ source: s.item.name,
714
+ count: extra,
715
+ });
716
+ if (!options.acceptProjectWidening?.includes(project.slug))
717
+ refuse('project_widening', project.pointer);
718
+ }
719
+ }
720
+ if (s.item.source_type === 'packagist_proxy')
721
+ summary.warning.push({
722
+ code: 'public_proxy_exposure',
723
+ project: project.slug,
724
+ source: s.item.name,
725
+ });
726
+ items.push({
727
+ type: 'attachment',
728
+ external_ref: ref('attachment', project.id, s.item.external_ref),
729
+ project: project.reference,
730
+ source: s.reference,
731
+ precedence: precedence(s),
732
+ });
733
+ }
734
+ }
735
+ addGrants(rows, map, members, projects, options.includeOrgAdmins === true, items, widening, summary);
736
+ for (const kind of ['customers', 'vendorBundles'])
737
+ if (rows[kind]?.length)
738
+ summary.unsupported.push({ code: kind, count: rows[kind].length });
739
+ for (const kind of ['synchronizations', 'tokens', 'suborganizationTokens'])
740
+ if (rows[kind]?.length)
741
+ summary.warning.push({ code: `report_only_${kind}`, count: rows[kind].length });
742
+ summary.warning.push({
743
+ code: 'manual_cutover',
744
+ detail: 'Issue project tokens; configure SSO, monitoring, collaborators and webhooks; verify historical dists with cutover-check.',
745
+ });
746
+ let delta = items;
747
+ let deltaWidening = widening;
748
+ if (retained !== null) {
749
+ const previous = retained;
750
+ for (const item of previous.items) {
751
+ if ('source' in item &&
752
+ 'existing' in item.source &&
753
+ !(options.packagist === `existing:${item.source.existing.name}`))
754
+ refuse('incomplete_baseline', '/baseline');
755
+ }
756
+ const planned = new Map(items.map((item) => [item.external_ref, item]));
757
+ const prior = new Map(previous.items.map((item) => [item.external_ref, item]));
758
+ for (const old of [...previous.items].sort((a, b) => Number(b.type === 'source') - Number(a.type === 'source'))) {
759
+ const current = planned.get(old.external_ref);
760
+ if (!current) {
761
+ if (old.type === 'source' &&
762
+ previous.items.some((n) => n.type === 'name' &&
763
+ 'ref' in n.source &&
764
+ n.source.ref === old.external_ref &&
765
+ planned.has(n.external_ref)))
766
+ refuse('existing_source_change', '/baseline');
767
+ if (old.type === 'source' &&
768
+ old.source_type === 'vcs' &&
769
+ items.some((item) => item.type === 'source' &&
770
+ item.source_type === 'vcs' &&
771
+ item.config.packageName ===
772
+ old.config.packageName))
773
+ refuse('existing_source_change', '/baseline');
774
+ refuse('destructive_delta_unsupported', '/baseline');
775
+ }
776
+ if (stable(old) !== stable(current))
777
+ refuse(old.type === 'source' ? 'existing_source_change' : 'destructive_delta_unsupported', '/baseline');
778
+ }
779
+ const projectRefs = new Map(previous.items.flatMap((item) => item.type === 'project' ? [[item.external_ref, item.slug]] : []));
780
+ const sourceRefs = new Map(previous.items.flatMap((item) => item.type === 'source' ? [[item.external_ref, item.name]] : []));
781
+ delta = items
782
+ .filter((item) => !prior.has(item.external_ref))
783
+ .map((item) => {
784
+ const copy = { ...item };
785
+ if ('project' in copy &&
786
+ copy.project !== null &&
787
+ 'ref' in copy.project &&
788
+ projectRefs.has(copy.project.ref))
789
+ copy.project = { existing: projectRefs.get(copy.project.ref) };
790
+ if ('source' in copy && 'ref' in copy.source && sourceRefs.has(copy.source.ref))
791
+ copy.source = { existing: { project: null, name: sourceRefs.get(copy.source.ref) } };
792
+ return copy;
793
+ });
794
+ deltaWidening = widening.filter((w) => delta.some((item) => item.external_ref === w.external_ref));
795
+ for (const old of previous.items)
796
+ summary.existing.push({ code: old.type, count: 1 });
797
+ }
798
+ if (delta.length > MAX_MANIFEST_ITEMS)
799
+ refuse('single_run_limit_exceeded', '', 'MAX_MANIFEST_ITEMS');
800
+ const parsed = parseManifest({
801
+ version: 1,
802
+ source_system: 'private-packagist',
803
+ items: delta,
804
+ widening: deltaWidening,
805
+ });
806
+ if (!parsed.ok)
807
+ refuse('manifest_invalid', parsed.details[0]?.path ?? '');
808
+ const bytes = canonicalManifest(parsed.data);
809
+ if (Buffer.byteLength(bytes) > MAX_MANIFEST_BYTES)
810
+ refuse('single_run_limit_exceeded', '', 'MAX_MANIFEST_BYTES');
811
+ for (const item of delta)
812
+ summary.create.push({
813
+ code: item.type,
814
+ count: 1,
815
+ ...(item.type === 'source' ? { source: item.name, detail: item.source_type } : {}),
816
+ ...(item.type === 'project' ? { project: item.slug } : {}),
817
+ ...(item.type === 'name' ? { pattern: item.pattern } : {}),
818
+ });
819
+ const emittedSources = new Set(delta.filter((item) => item.type === 'source').map((item) => item.external_ref));
820
+ return {
821
+ ok: true,
822
+ diagnostics: [],
823
+ manifest: parsed.data,
824
+ canonical: bytes,
825
+ hash: await planHash(parsed.data),
826
+ credentialsOut: JSON.stringify({
827
+ version: 1,
828
+ sources: credentialOutput.filter((e) => emittedSources.has(e.external_ref)),
829
+ }),
830
+ };
831
+ }
832
+ function addGrants(rows, map, members, projects, includeAdmins, items, widening, summary) {
833
+ const grants = new Map();
834
+ for (const [i, team] of [...(rows.teams ?? []), ...(rows.suborganizationTeams ?? [])].entries()) {
835
+ const pointer = `/teams/${i}`;
836
+ if (typeof team.name !== 'string' ||
837
+ typeof team.id !== 'number' ||
838
+ typeof team.permission !== 'string')
839
+ refuse('invalid_snapshot', pointer);
840
+ const permission = team.permission;
841
+ const org = team.suborganizationId === undefined;
842
+ if (!(org ? ['owner', 'admin', 'edit', 'view', 'billing'] : ['owner', 'view']).includes(permission))
843
+ refuse('unsupported_team_permission', pointer);
844
+ if (['Owners', 'Admins', 'Billing', 'All Organization Members'].includes(team.name)) {
845
+ summary.warning.push({ code: 'built_in_team_skipped', pointer });
846
+ continue;
847
+ }
848
+ const mapping = own(map.teams, team.name);
849
+ if (permission === 'billing') {
850
+ summary.warning.push({ code: 'billing_team', pointer });
851
+ continue;
852
+ }
853
+ if (!mapping ||
854
+ (org && ['edit', 'view'].includes(permission) && !mapping.projects?.length) ||
855
+ (org && ['owner', 'admin'].includes(permission) && !includeAdmins)) {
856
+ summary.warning.push({
857
+ code: 'unlisted_team',
858
+ pointer,
859
+ count: Array.isArray(team.members) ? team.members.length : 0,
860
+ });
861
+ continue;
862
+ }
863
+ const targets = org && ['owner', 'admin'].includes(permission)
864
+ ? [null]
865
+ : org
866
+ ? (mapping.projects ?? []).map((slug) => {
867
+ const project = [...projects.values()].find((p) => p.slug === slug);
868
+ if (!project)
869
+ return refuse('unknown_team_project', pointer);
870
+ return project;
871
+ })
872
+ : [
873
+ projects.get(team.suborganizationId) ??
874
+ refuse('unknown_team_project', pointer),
875
+ ];
876
+ const subjects = [];
877
+ if (mapping.group)
878
+ subjects.push({ type: 'group', ...mapping.group });
879
+ else {
880
+ let missing = 0;
881
+ for (const member of Array.isArray(team.members) ? team.members : []) {
882
+ if (!object(member) || typeof member.username !== 'string')
883
+ refuse('invalid_snapshot', pointer);
884
+ const email = members.get(member.username);
885
+ if (email)
886
+ subjects.push({ type: 'email', value: email });
887
+ else
888
+ missing++;
889
+ }
890
+ if (missing)
891
+ summary.needsInput.push({ code: 'unmapped_members', pointer, count: missing });
892
+ }
893
+ for (const project of targets)
894
+ for (const subject of subjects) {
895
+ const role = project === null
896
+ ? 'organization_admin'
897
+ : permission === 'owner' || (permission === 'edit' && mapping.role === 'project_owner')
898
+ ? 'project_owner'
899
+ : 'project_developer';
900
+ const key = stable([project?.reference ?? null, subject]);
901
+ const grant = {
902
+ type: 'grant',
903
+ external_ref: ref('grant', project?.id ?? null, team.id, JSON.parse(stable(subject))),
904
+ scope: project === null ? 'organization' : 'project',
905
+ project: project?.reference ?? null,
906
+ role: { existing: role },
907
+ subject,
908
+ };
909
+ const previous = grants.get(key);
910
+ if (previous && 'existing' in previous.role && previous.role.existing !== role) {
911
+ summary.warning.push({ code: 'grant_merged', pointer, count: 1 });
912
+ const stronger = GRANT_ROLE_RANK[previous.role.existing] > GRANT_ROLE_RANK[role]
913
+ ? previous.role
914
+ : grant.role;
915
+ const chosen = compare(grant.external_ref, previous.external_ref) < 0 ? grant : previous;
916
+ grants.set(key, { ...chosen, role: stronger });
917
+ }
918
+ else if (!previous || compare(grant.external_ref, previous.external_ref) < 0)
919
+ grants.set(key, grant);
920
+ }
921
+ }
922
+ for (const grant of grants.values()) {
923
+ items.push(grant);
924
+ const role = DEFAULT_ROLES.find((r) => 'existing' in grant.role && r.key === grant.role.existing);
925
+ if (role?.permissions.some((p) => PERMISSIONS.some((info) => info.name === p && info.sessionOnly))) {
926
+ widening.push({ type: 'grant', external_ref: grant.external_ref });
927
+ summary.warning.push({
928
+ code: grant.scope === 'organization'
929
+ ? 'organization_admin_widening'
930
+ : 'project_grant_session_permissions',
931
+ count: 1,
932
+ });
933
+ }
934
+ }
935
+ }
936
+ //# sourceMappingURL=plan.js.map