@adea-ai/plugins 4.0.1 → 4.0.6

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 (142) hide show
  1. package/dist/agent-plugins-catalog.d.ts +2 -4
  2. package/dist/agent-plugins-catalog.js +42 -50
  3. package/dist/agent-plugins-catalog.js.map +1 -1
  4. package/dist/agent-plugins-schema.d.ts +8 -0
  5. package/dist/agent-plugins-schema.js +10 -1
  6. package/dist/agent-plugins-schema.js.map +1 -1
  7. package/dist/artifacts.d.ts +57 -0
  8. package/dist/artifacts.js +159 -0
  9. package/dist/artifacts.js.map +1 -0
  10. package/dist/build.d.ts +20 -0
  11. package/dist/build.js +103 -0
  12. package/dist/build.js.map +1 -0
  13. package/dist/capabilities.d.ts +7 -0
  14. package/dist/capabilities.js +116 -0
  15. package/dist/capabilities.js.map +1 -0
  16. package/dist/catalog-store.d.ts +0 -5
  17. package/dist/catalog-store.js +1 -25
  18. package/dist/catalog-store.js.map +1 -1
  19. package/dist/concurrency.d.ts +1 -0
  20. package/dist/concurrency.js +15 -0
  21. package/dist/concurrency.js.map +1 -0
  22. package/dist/consumer-index.d.ts +6 -28
  23. package/dist/consumer-index.js +7 -8
  24. package/dist/consumer-index.js.map +1 -1
  25. package/dist/digest.d.ts +13 -0
  26. package/dist/digest.js +47 -0
  27. package/dist/digest.js.map +1 -0
  28. package/dist/fs-atomic.d.ts +7 -0
  29. package/dist/fs-atomic.js +30 -0
  30. package/dist/fs-atomic.js.map +1 -0
  31. package/dist/harness.js +2 -1
  32. package/dist/harness.js.map +1 -1
  33. package/dist/icon-mirror.d.ts +3 -2
  34. package/dist/icon-mirror.js +51 -63
  35. package/dist/icon-mirror.js.map +1 -1
  36. package/dist/icons.d.ts +2 -2
  37. package/dist/icons.js +4 -12
  38. package/dist/icons.js.map +1 -1
  39. package/dist/index.d.ts +16 -315
  40. package/dist/index.js +12 -1905
  41. package/dist/index.js.map +1 -1
  42. package/dist/integrity-manifest.d.ts +43 -13
  43. package/dist/integrity-manifest.js +28 -17
  44. package/dist/integrity-manifest.js.map +1 -1
  45. package/dist/json-values.d.ts +3 -0
  46. package/dist/json-values.js +10 -0
  47. package/dist/json-values.js.map +1 -0
  48. package/dist/plugin-release.d.ts +38 -0
  49. package/dist/plugin-release.js +321 -0
  50. package/dist/plugin-release.js.map +1 -0
  51. package/dist/plugin-skips.d.ts +19 -0
  52. package/dist/plugin-skips.js +65 -0
  53. package/dist/plugin-skips.js.map +1 -0
  54. package/dist/policy.d.ts +13 -0
  55. package/dist/policy.js +26 -0
  56. package/dist/policy.js.map +1 -0
  57. package/dist/portable-catalog.d.ts +4 -4
  58. package/dist/portable-catalog.js +14 -126
  59. package/dist/portable-catalog.js.map +1 -1
  60. package/dist/portable-verify.d.ts +2 -0
  61. package/dist/portable-verify.js +68 -0
  62. package/dist/portable-verify.js.map +1 -0
  63. package/dist/publication.d.ts +6 -3
  64. package/dist/publication.js +25 -12
  65. package/dist/publication.js.map +1 -1
  66. package/dist/published-output.d.ts +17 -0
  67. package/dist/published-output.js +49 -0
  68. package/dist/published-output.js.map +1 -0
  69. package/dist/site-icons.d.ts +2 -3
  70. package/dist/site-icons.js +4 -3
  71. package/dist/site-icons.js.map +1 -1
  72. package/dist/snapshots.d.ts +39 -0
  73. package/dist/snapshots.js +238 -0
  74. package/dist/snapshots.js.map +1 -0
  75. package/dist/source-resolution.d.ts +28 -0
  76. package/dist/source-resolution.js +158 -0
  77. package/dist/source-resolution.js.map +1 -0
  78. package/dist/sources.d.ts +5 -1
  79. package/dist/sources.js +21 -27
  80. package/dist/sources.js.map +1 -1
  81. package/dist/synchronize.d.ts +44 -0
  82. package/dist/synchronize.js +118 -0
  83. package/dist/synchronize.js.map +1 -0
  84. package/dist/taxonomy.d.ts +23 -0
  85. package/dist/taxonomy.js +62 -0
  86. package/dist/taxonomy.js.map +1 -0
  87. package/dist/upstream.d.ts +42 -0
  88. package/dist/upstream.js +179 -0
  89. package/dist/upstream.js.map +1 -0
  90. package/dist/verify-artifacts.d.ts +36 -0
  91. package/dist/verify-artifacts.js +286 -0
  92. package/dist/verify-artifacts.js.map +1 -0
  93. package/native/build-provenance.txt +5 -5
  94. package/native/staged-reader-win32-arm64.node +0 -0
  95. package/native/staged-reader-win32-x64.node +0 -0
  96. package/package.json +2 -4
  97. package/src/agent-plugins-catalog.test.ts +2 -2
  98. package/src/agent-plugins-catalog.ts +46 -49
  99. package/src/agent-plugins-schema.ts +11 -1
  100. package/src/artifacts.ts +234 -0
  101. package/src/build.ts +131 -0
  102. package/src/capabilities.ts +153 -0
  103. package/src/catalog-store.ts +1 -28
  104. package/src/concurrency.ts +19 -0
  105. package/src/consumer-index.test.ts +27 -21
  106. package/src/consumer-index.ts +12 -35
  107. package/src/digest.ts +55 -0
  108. package/src/fs-atomic.ts +32 -0
  109. package/src/harness.test.ts +4 -32
  110. package/src/harness.ts +2 -1
  111. package/src/icon-mirror.ts +57 -64
  112. package/src/icons.test.ts +0 -4
  113. package/src/icons.ts +4 -4
  114. package/src/index.test.ts +52 -48
  115. package/src/index.ts +50 -2583
  116. package/src/integrity-manifest.ts +41 -21
  117. package/src/json-values.ts +11 -0
  118. package/src/plugin-release.ts +457 -0
  119. package/src/plugin-skips.ts +110 -0
  120. package/src/policy.ts +39 -0
  121. package/src/portable-catalog.test.ts +0 -5
  122. package/src/portable-catalog.ts +16 -143
  123. package/src/portable-verify.ts +71 -0
  124. package/src/publication.test.ts +29 -1
  125. package/src/publication.ts +32 -13
  126. package/src/published-output.ts +60 -0
  127. package/src/site-icons.test.ts +0 -1
  128. package/src/site-icons.ts +4 -4
  129. package/src/snapshots.ts +287 -0
  130. package/src/source-digest.test.ts +3 -7
  131. package/src/source-resolution.ts +233 -0
  132. package/src/sources-json.test.ts +0 -1
  133. package/src/sources.ts +27 -33
  134. package/src/synchronize.ts +213 -0
  135. package/src/taxonomy.ts +87 -0
  136. package/src/upstream.ts +194 -0
  137. package/src/verify-artifacts.ts +365 -0
  138. package/src/write-artifacts.test.ts +0 -1
  139. package/dist/source-digest.d.ts +0 -2
  140. package/dist/source-digest.js +0 -12
  141. package/dist/source-digest.js.map +0 -1
  142. package/src/source-digest.ts +0 -14
@@ -0,0 +1,365 @@
1
+ import type { GeneratedArtifacts } from './artifacts.js'
2
+ import {
3
+ parseIntegrityManifest,
4
+ publishedAssetOf,
5
+ type PublishedAsset,
6
+ } from './integrity-manifest.js'
7
+ import {
8
+ CATALOG_INDEX_ARTIFACT,
9
+ iconCoverage,
10
+ type ConsumerCategory,
11
+ type ConsumerIndex,
12
+ type ConsumerProduct,
13
+ type LeadingDiagnostic,
14
+ } from './consumer-index.js'
15
+ import { digest } from './digest.js'
16
+ import { assertRepositoryAllowed, DEFAULT_POLICY, type CatalogPolicy } from './policy.js'
17
+ import { CatalogSchema, SourcesLockSchema, type Catalog } from './schema.js'
18
+ import { safeRelativePath } from './sources.js'
19
+
20
+ export interface ConsumerIndexVerification {
21
+ readonly hasConsumerIndex: boolean
22
+ readonly diagnostics: readonly LeadingDiagnostic[]
23
+ readonly counts?: ConsumerIndex['counts']
24
+ readonly topCount?: number
25
+ readonly products?: Readonly<Record<string, ConsumerProduct>>
26
+ readonly categories?: readonly ConsumerCategory[]
27
+ /** Shards the published integrity file set must carry for this index. */
28
+ readonly shards: readonly ShardArtifact[]
29
+ }
30
+
31
+ export interface ShardArtifact {
32
+ readonly name: string
33
+ readonly kind: 'index' | 'shelf' | 'list'
34
+ readonly category?: string
35
+ }
36
+
37
+ export function verifyArtifacts(artifacts: GeneratedArtifacts): void {
38
+ const parsedCatalog = CatalogSchema.parse(JSON.parse(artifacts['catalog.v1.json']))
39
+ verifyCatalogReferences(parsedCatalog)
40
+ const lock = SourcesLockSchema.parse(JSON.parse(artifacts['sources.lock.json']))
41
+ const summary = JSON.parse(artifacts['catalog-summary.v1.json']) as {
42
+ schemaVersion?: unknown
43
+ catalogId?: unknown
44
+ sourceCount?: unknown
45
+ pluginCount?: unknown
46
+ search?: unknown
47
+ }
48
+ const compatibility = JSON.parse(artifacts['compatibility.v1.json']) as {
49
+ schemaVersion?: unknown
50
+ catalogId?: unknown
51
+ plugins?: unknown
52
+ }
53
+ const categories = JSON.parse(artifacts['categories.v1.json']) as {
54
+ schemaVersion?: unknown
55
+ catalogId?: unknown
56
+ topCount?: unknown
57
+ counts?: { plugins?: unknown; products?: unknown; redundantPlugins?: unknown }
58
+ categories?: unknown
59
+ products?: unknown
60
+ diagnostics?: unknown
61
+ }
62
+ const integrity = parseIntegrityManifest(JSON.parse(artifacts['integrity.json']))
63
+ if (parsedCatalog.sources.length !== lock.sources.length)
64
+ throw new Error('INTEGRITY_SOURCE_COUNT_MISMATCH')
65
+ if (
66
+ summary.schemaVersion !== 1 ||
67
+ summary.catalogId !== parsedCatalog.catalogId ||
68
+ summary.sourceCount !== parsedCatalog.sources.length ||
69
+ summary.pluginCount !== parsedCatalog.plugins.length ||
70
+ !Array.isArray(summary.search) ||
71
+ compatibility.schemaVersion !== 1 ||
72
+ compatibility.catalogId !== parsedCatalog.catalogId ||
73
+ !Array.isArray(compatibility.plugins) ||
74
+ compatibility.plugins.length !== parsedCatalog.plugins.length ||
75
+ categories.schemaVersion !== 1 ||
76
+ categories.catalogId !== parsedCatalog.catalogId ||
77
+ !Array.isArray(categories.categories) ||
78
+ (categories.products !== undefined &&
79
+ (typeof categories.products !== 'object' || categories.products === null)) ||
80
+ (categories.diagnostics !== undefined && !Array.isArray(categories.diagnostics)) ||
81
+ integrity.catalogId !== parsedCatalog.catalogId
82
+ ) {
83
+ throw new Error('INTEGRITY_ARTIFACT_METADATA_MISMATCH')
84
+ }
85
+ // The published file set is exactly what integrity.json enumerates, so the
86
+ // shards can grow with the category configuration without a fixed list here.
87
+ for (const name of Object.keys(artifacts))
88
+ if (name !== 'integrity.json' && integrity.files[name] === undefined)
89
+ throw new Error(`INTEGRITY_FILE_SET_MISMATCH: ${name}`)
90
+ for (const [name, expected] of Object.entries(integrity.files)) {
91
+ const content = artifacts[name]
92
+ if (content === undefined) throw new Error(`INTEGRITY_FILE_SET_MISMATCH: ${name}`)
93
+ if (digest(content) !== expected) throw new Error(`INTEGRITY_DIGEST_MISMATCH: ${name}`)
94
+ }
95
+ const indexArtifact = artifacts[CATALOG_INDEX_ARTIFACT]
96
+ const verification = verifyConsumerIndex(
97
+ artifacts['categories.v1.json'],
98
+ parsedCatalog,
99
+ indexArtifact
100
+ )
101
+ if (verification.hasConsumerIndex && indexArtifact === undefined)
102
+ throw new Error(`SHARD_MISSING: ${CATALOG_INDEX_ARTIFACT}`)
103
+ for (const shard of verification.shards)
104
+ verifyShardArtifact(shard, artifacts[shard.name], verification, parsedCatalog.catalogId)
105
+ verifyIconAssets(integrity.assets ?? [], verification)
106
+ }
107
+
108
+ /**
109
+ * Re-checks one consumer shard against the navigation index: a shelf carries
110
+ * exactly the curated products in order, and a category list carries exactly
111
+ * the category's products in order. The expected catalog ID must come from a
112
+ * separately verified catalog, not from the shard being checked.
113
+ */
114
+ export function verifyShardArtifact(
115
+ shard: ShardArtifact,
116
+ content: string | undefined,
117
+ verification: ConsumerIndexVerification,
118
+ catalogId: string
119
+ ): void {
120
+ if (!CatalogSchema.shape.catalogId.safeParse(catalogId).success)
121
+ throw new Error('SHARD_CATALOG_ID_REQUIRED')
122
+ if (content === undefined) throw new Error(`SHARD_MISSING: ${shard.name}`)
123
+ const parsed = JSON.parse(content) as {
124
+ schemaVersion?: unknown
125
+ catalogId?: unknown
126
+ category?: unknown
127
+ topCount?: unknown
128
+ products?: unknown
129
+ }
130
+ if (
131
+ parsed.schemaVersion !== 1 ||
132
+ parsed.catalogId !== catalogId ||
133
+ (shard.kind !== 'index' && parsed.category !== shard.category)
134
+ )
135
+ throw new Error(`SHARD_IDENTITY_MISMATCH: ${shard.name}`)
136
+ const products: readonly ConsumerProduct[] =
137
+ shard.kind === 'index'
138
+ ? Object.values((parsed.products ?? {}) as Record<string, ConsumerProduct>)
139
+ : (parsed.products as readonly ConsumerProduct[])
140
+ if (!Array.isArray(products)) throw new Error(`SHARD_PRODUCTS_INVALID: ${shard.name}`)
141
+ const keys = products.map((product) => product.productKey)
142
+ if (new Set(keys).size !== keys.length) throw new Error(`SHARD_DUPLICATE_PRODUCT: ${shard.name}`)
143
+ if (shard.kind === 'index') {
144
+ const expected = Object.keys(verification.products ?? {}).toSorted()
145
+ if (keys.toSorted().join('\n') !== expected.join('\n'))
146
+ throw new Error('SHARD_INDEX_MEMBERSHIP_MISMATCH')
147
+ return
148
+ }
149
+ const category = verification.categories?.find((item) => item.category === shard.category)
150
+ if (!category) throw new Error(`SHARD_CATEGORY_UNKNOWN: ${shard.name}`)
151
+ const expected = shard.kind === 'shelf' ? category.topProductKeys : category.productKeys
152
+ if (keys.join('\n') !== expected.join('\n'))
153
+ throw new Error(`SHARD_ORDER_MISMATCH: ${shard.name}`)
154
+ for (const product of products) {
155
+ const index = verification.products?.[product.productKey]
156
+ if (!index || digest(index) !== digest(product))
157
+ throw new Error(`SHARD_PRODUCT_MISMATCH: ${shard.name}: ${product.productKey}`)
158
+ }
159
+ }
160
+
161
+ function verifyIconAssets(
162
+ declared: readonly PublishedAsset[],
163
+ verification: ConsumerIndexVerification
164
+ ): void {
165
+ const referenced = new Map<string, PublishedAsset>()
166
+ for (const product of Object.values(verification.products ?? {})) {
167
+ if (!product.icon) continue
168
+ const existing = referenced.get(product.icon.asset)
169
+ if (existing && existing.digest !== product.icon.digest)
170
+ throw new Error(`ICON_ASSET_DIGEST_CONFLICT: ${product.icon.asset}`)
171
+ referenced.set(product.icon.asset, publishedAssetOf(product.icon))
172
+ }
173
+ if (declared.length !== referenced.size) throw new Error('INTEGRITY_ASSET_SET_MISMATCH')
174
+ for (const asset of declared) {
175
+ const expected = referenced.get(asset.name)
176
+ if (!expected) throw new Error(`INTEGRITY_ASSET_UNDECLARED: ${asset.name}`)
177
+ if (
178
+ expected.digest !== asset.digest ||
179
+ expected.bytes !== asset.bytes ||
180
+ expected.contentType !== asset.contentType ||
181
+ expected.kind !== asset.kind ||
182
+ expected.origin !== asset.origin
183
+ )
184
+ throw new Error(`INTEGRITY_ASSET_MISMATCH: ${asset.name}`)
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Re-checks the published consumer index against the catalog it indexes. The
190
+ * index is what Adea renders directly, so a contradiction between the two
191
+ * artifacts must never reach a release. Snapshots published before the index
192
+ * existed carry membership only; those are still membership-checked and
193
+ * reported as `hasConsumerIndex: false`.
194
+ */
195
+ export function verifyConsumerIndex(
196
+ content: string,
197
+ catalog: Pick<Catalog, 'plugins'>,
198
+ indexContent?: string
199
+ ): ConsumerIndexVerification {
200
+ const index = JSON.parse(content) as Partial<ConsumerIndex>
201
+ const knownPlugins = new Map(catalog.plugins.map((plugin) => [plugin.pluginId, plugin]))
202
+ // Products live in the index shard; the navigation artifact only points at it.
203
+ const shard = indexContent ? (JSON.parse(indexContent) as Partial<ConsumerIndex>) : undefined
204
+ const products =
205
+ shard && typeof shard.products === 'object' && shard.products !== null
206
+ ? shard.products
207
+ : typeof index.products === 'object' && index.products !== null
208
+ ? index.products
209
+ : undefined
210
+ const diagnostics = Array.isArray(index.diagnostics) ? index.diagnostics : []
211
+ for (const category of index.categories ?? []) {
212
+ const members = catalog.plugins.filter((plugin) =>
213
+ plugin.categories.includes(category.category)
214
+ )
215
+ if (category.pluginCount !== undefined && category.pluginCount !== members.length)
216
+ throw new Error(`CONSUMER_INDEX_PLUGIN_COUNT_MISMATCH: ${category.category}`)
217
+ if (category.pluginIds.length !== members.length)
218
+ throw new Error(`CONSUMER_INDEX_PLUGIN_LIST_MISMATCH: ${category.category}`)
219
+ if (new Set(category.pluginIds).size !== category.pluginIds.length)
220
+ throw new Error(`CONSUMER_INDEX_DUPLICATE_PLUGIN: ${category.category}`)
221
+ for (const pluginId of category.pluginIds) {
222
+ const plugin = knownPlugins.get(pluginId)
223
+ if (!plugin || !plugin.categories.includes(category.category))
224
+ throw new Error(`CONSUMER_INDEX_FOREIGN_PLUGIN: ${category.category}: ${pluginId}`)
225
+ }
226
+ if (!products) continue
227
+ if (category.productKeys === undefined || category.topProductKeys === undefined)
228
+ throw new Error(`CONSUMER_INDEX_PRODUCTS_INCOMPLETE: ${category.category}`)
229
+ if (new Set(category.productKeys).size !== category.productKeys.length)
230
+ throw new Error(`CONSUMER_INDEX_DUPLICATE_PRODUCT: ${category.category}`)
231
+ for (const productKey of category.productKeys) {
232
+ const product = products[productKey]
233
+ if (!product) throw new Error(`CONSUMER_INDEX_UNKNOWN_PRODUCT: ${productKey}`)
234
+ if (!product.categories.includes(category.category))
235
+ throw new Error(`CONSUMER_INDEX_PRODUCT_CATEGORY_MISMATCH: ${productKey}`)
236
+ }
237
+ if (category.productCount !== category.productKeys.length)
238
+ throw new Error(`CONSUMER_INDEX_PRODUCT_COUNT_MISMATCH: ${category.category}`)
239
+ const topCount = index.topCount
240
+ if (topCount === undefined || !Number.isInteger(topCount) || topCount < 0)
241
+ throw new Error('CONSUMER_INDEX_TOP_COUNT_INVALID')
242
+ if (category.topProductKeys.length > topCount)
243
+ throw new Error(`CONSUMER_INDEX_TOP_OVERFLOW: ${category.category}`)
244
+ const shelved = new Set<string>()
245
+ for (const product of Object.values(products)) {
246
+ // Indexes published before the install facts existed carry no `release`,
247
+ // and are still valid; a record that does carry one must agree with the
248
+ // catalog it claims to describe.
249
+ if (product.release) {
250
+ const plugin = knownPlugins.get(product.pluginId)
251
+ const release = plugin?.availableReleases.find(
252
+ (candidate) => candidate.releaseId === plugin.currentReleaseId
253
+ )
254
+ if (
255
+ !plugin ||
256
+ !release ||
257
+ product.release.releaseId !== release.releaseId ||
258
+ product.release.canonicalContentDigest !== release.canonicalContentDigest ||
259
+ product.release.sourceRevision !== release.resolvedCommitSha ||
260
+ product.provenance?.resolvedCommitSha !== release.resolvedCommitSha
261
+ )
262
+ throw new Error(`CONSUMER_INDEX_RELEASE_MISMATCH: ${product.productKey}`)
263
+ }
264
+ const icon = product.icon
265
+ if (!icon?.assetUrl) continue
266
+ if (!icon.assetUrl.startsWith('https://') || !icon.assetUrl.endsWith(`/${icon.asset}`))
267
+ throw new Error(`CONSUMER_INDEX_ASSET_URL_INVALID: ${icon.asset}`)
268
+ }
269
+ for (const productKey of category.topProductKeys) {
270
+ if (!category.productKeys.includes(productKey))
271
+ throw new Error(`CONSUMER_INDEX_TOP_NOT_MEMBER: ${category.category}: ${productKey}`)
272
+ if (shelved.has(productKey)) throw new Error(`CONSUMER_INDEX_TOP_REPEATED: ${productKey}`)
273
+ shelved.add(productKey)
274
+ }
275
+ }
276
+ if (products) {
277
+ // A product headlines one category only, so no product is shelved twice.
278
+ const claimed = new Set<string>()
279
+ for (const category of index.categories ?? [])
280
+ for (const productKey of category.topProductKeys ?? []) {
281
+ if (claimed.has(productKey))
282
+ throw new Error(`CONSUMER_INDEX_TOP_CROSS_CATEGORY: ${productKey}`)
283
+ claimed.add(productKey)
284
+ }
285
+ for (const [productKey, product] of Object.entries(products)) {
286
+ if (productKey !== product.productKey) throw new Error('CONSUMER_INDEX_PRODUCT_KEY_MISMATCH')
287
+ if (product.variantPluginIds[0] !== product.pluginId)
288
+ throw new Error(`CONSUMER_INDEX_CANONICAL_NOT_FIRST: ${productKey}`)
289
+ const variants = new Set(
290
+ product.variantPluginIds.map((pluginId) => {
291
+ const plugin = knownPlugins.get(pluginId)
292
+ if (!plugin) throw new Error(`CONSUMER_INDEX_UNKNOWN_PLUGIN: ${pluginId}`)
293
+ if (plugin.productGroupingKey !== productKey)
294
+ throw new Error(`CONSUMER_INDEX_VARIANT_PRODUCT_MISMATCH: ${pluginId}`)
295
+ return pluginId
296
+ })
297
+ )
298
+ if (variants.size !== product.variantPluginIds.length)
299
+ throw new Error(`CONSUMER_INDEX_DUPLICATE_VARIANT: ${productKey}`)
300
+ }
301
+ const counts = shard?.counts ?? index.counts
302
+ if (counts?.plugins !== catalog.plugins.length) throw new Error('CONSUMER_INDEX_TOTAL_MISMATCH')
303
+ if (counts?.products !== Object.keys(products).length)
304
+ throw new Error('CONSUMER_INDEX_PRODUCT_TOTAL_MISMATCH')
305
+ if (counts?.redundantPlugins !== catalog.plugins.length - counts.products)
306
+ throw new Error('CONSUMER_INDEX_REDUNDANCY_MISMATCH')
307
+ if (index.counts && shard?.counts && digest(index.counts) !== digest(shard.counts))
308
+ throw new Error('CONSUMER_INDEX_COUNTS_MISMATCH')
309
+ if (shard?.topCount !== undefined && index.topCount !== shard.topCount)
310
+ throw new Error('CONSUMER_INDEX_TOP_COUNT_MISMATCH')
311
+ }
312
+ if (products && index.iconCoverage) {
313
+ const expected = iconCoverage({ products })
314
+ if (
315
+ index.iconCoverage.total !== expected.total ||
316
+ index.iconCoverage.content !== expected.content ||
317
+ index.iconCoverage.favicon !== expected.favicon ||
318
+ index.iconCoverage.monogramOnly !== expected.monogramOnly ||
319
+ index.iconCoverage.monogramProductKeys.join('\n') !== expected.monogramProductKeys.join('\n')
320
+ )
321
+ throw new Error('CONSUMER_INDEX_ICON_COVERAGE_MISMATCH')
322
+ }
323
+ const shards: ShardArtifact[] = [
324
+ ...(products ? [{ name: CATALOG_INDEX_ARTIFACT, kind: 'index' as const }] : []),
325
+ ...(index.categories ?? []).flatMap((category) =>
326
+ products && category.shelfArtifact && category.listArtifact
327
+ ? [
328
+ { name: category.shelfArtifact, kind: 'shelf' as const, category: category.category },
329
+ { name: category.listArtifact, kind: 'list' as const, category: category.category },
330
+ ]
331
+ : []
332
+ ),
333
+ ]
334
+ return {
335
+ hasConsumerIndex: products !== undefined,
336
+ diagnostics,
337
+ ...(products ? { counts: index.counts as ConsumerIndex['counts'] } : {}),
338
+ ...(products && index.topCount !== undefined ? { topCount: index.topCount } : {}),
339
+ ...(products ? { products, categories: index.categories ?? [] } : {}),
340
+ shards: products ? shards : [],
341
+ }
342
+ }
343
+
344
+ export function verifyCatalogReferences(
345
+ catalog: Catalog,
346
+ policy: CatalogPolicy = DEFAULT_POLICY
347
+ ): void {
348
+ for (const source of catalog.sources) {
349
+ assertRepositoryAllowed(source.repositoryUrl, policy)
350
+ validateCatalogPath(source.manifestPath, 'SOURCE_MANIFEST_PATH_INVALID')
351
+ }
352
+ for (const plugin of catalog.plugins) {
353
+ assertRepositoryAllowed(plugin.provenance.repositoryUrl, policy)
354
+ validateCatalogPath(plugin.provenance.pluginSubdirectory, 'PROVENANCE_PATH_INVALID')
355
+ for (const release of plugin.availableReleases) {
356
+ assertRepositoryAllowed(release.resolvedRepositoryUrl, policy)
357
+ validateCatalogPath(release.pluginSubdirectory, 'RELEASE_PATH_INVALID')
358
+ for (const path of release.fileIndex) validateCatalogPath(path, 'RELEASE_FILE_PATH_INVALID')
359
+ }
360
+ }
361
+ }
362
+
363
+ function validateCatalogPath(path: string, code: string): void {
364
+ if (path !== '.') safeRelativePath(path, code)
365
+ }
@@ -315,7 +315,6 @@ describe('crash recovery', () => {
315
315
  expect(await readFile(join(`${output}.previous`, 'recovery'), 'utf8')).toBe('saved')
316
316
  expect(await read(output)).toBe('current')
317
317
  expect(await withCatalogRead(output, () => read(output))).toBe('current')
318
- expect(await readFile(join(`${output}.previous`, 'recovery'), 'utf8')).toBe('saved')
319
318
  }))
320
319
 
321
320
  test('a failed write keeps the previous catalog and releases the lock', () =>
@@ -1,2 +0,0 @@
1
- /** Shared source-byte identity for catalog provenance and canonical package recipes. */
2
- export declare function sourceDigest(files: ReadonlyMap<string, Uint8Array>): string;
@@ -1,12 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- /** Shared source-byte identity for catalog provenance and canonical package recipes. */
3
- export function sourceDigest(files) {
4
- const hash = createHash('sha256');
5
- // English matches the compiler runtime; ordinal ties distinguish equivalent Unicode names.
6
- for (const [path, bytes] of [...files].toSorted(([a], [b]) => a.localeCompare(b, 'en') || (a < b ? -1 : a > b ? 1 : 0))) {
7
- hash.update(`${path.length}:${path}:${bytes.byteLength}:`);
8
- hash.update(bytes);
9
- }
10
- return `sha256:${hash.digest('hex')}`;
11
- }
12
- //# sourceMappingURL=source-digest.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"source-digest.js","sourceRoot":"","sources":["../src/source-digest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,wFAAwF;AACxF,MAAM,UAAU,YAAY,CAAC,KAAsC;IACjE,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAA;IACjC,2FAA2F;IAC3F,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC,QAAQ,CAC7C,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CACvE,EAAE,CAAC;QACF,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,IAAI,KAAK,CAAC,UAAU,GAAG,CAAC,CAAA;QAC1D,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IACpB,CAAC;IACD,OAAO,UAAU,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAA;AACvC,CAAC"}
@@ -1,14 +0,0 @@
1
- import { createHash } from 'node:crypto'
2
-
3
- /** Shared source-byte identity for catalog provenance and canonical package recipes. */
4
- export function sourceDigest(files: ReadonlyMap<string, Uint8Array>): string {
5
- const hash = createHash('sha256')
6
- // English matches the compiler runtime; ordinal ties distinguish equivalent Unicode names.
7
- for (const [path, bytes] of [...files].toSorted(
8
- ([a], [b]) => a.localeCompare(b, 'en') || (a < b ? -1 : a > b ? 1 : 0)
9
- )) {
10
- hash.update(`${path.length}:${path}:${bytes.byteLength}:`)
11
- hash.update(bytes)
12
- }
13
- return `sha256:${hash.digest('hex')}`
14
- }