@adea-ai/plugins 4.0.1 → 4.0.7
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/dist/agent-plugins-catalog.d.ts +2 -4
- package/dist/agent-plugins-catalog.js +42 -50
- package/dist/agent-plugins-catalog.js.map +1 -1
- package/dist/agent-plugins-schema.d.ts +8 -0
- package/dist/agent-plugins-schema.js +10 -1
- package/dist/agent-plugins-schema.js.map +1 -1
- package/dist/artifacts.d.ts +57 -0
- package/dist/artifacts.js +159 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/build.d.ts +20 -0
- package/dist/build.js +103 -0
- package/dist/build.js.map +1 -0
- package/dist/capabilities.d.ts +7 -0
- package/dist/capabilities.js +116 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/catalog-store.d.ts +0 -5
- package/dist/catalog-store.js +1 -25
- package/dist/catalog-store.js.map +1 -1
- package/dist/concurrency.d.ts +1 -0
- package/dist/concurrency.js +15 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/consumer-index.d.ts +6 -28
- package/dist/consumer-index.js +7 -8
- package/dist/consumer-index.js.map +1 -1
- package/dist/digest.d.ts +13 -0
- package/dist/digest.js +47 -0
- package/dist/digest.js.map +1 -0
- package/dist/fs-atomic.d.ts +7 -0
- package/dist/fs-atomic.js +30 -0
- package/dist/fs-atomic.js.map +1 -0
- package/dist/harness.js +2 -1
- package/dist/harness.js.map +1 -1
- package/dist/icon-mirror.d.ts +3 -2
- package/dist/icon-mirror.js +51 -63
- package/dist/icon-mirror.js.map +1 -1
- package/dist/icons.d.ts +2 -2
- package/dist/icons.js +4 -12
- package/dist/icons.js.map +1 -1
- package/dist/index.d.ts +16 -315
- package/dist/index.js +12 -1905
- package/dist/index.js.map +1 -1
- package/dist/integrity-manifest.d.ts +43 -13
- package/dist/integrity-manifest.js +28 -17
- package/dist/integrity-manifest.js.map +1 -1
- package/dist/json-values.d.ts +3 -0
- package/dist/json-values.js +10 -0
- package/dist/json-values.js.map +1 -0
- package/dist/plugin-release.d.ts +38 -0
- package/dist/plugin-release.js +321 -0
- package/dist/plugin-release.js.map +1 -0
- package/dist/plugin-skips.d.ts +19 -0
- package/dist/plugin-skips.js +65 -0
- package/dist/plugin-skips.js.map +1 -0
- package/dist/policy.d.ts +13 -0
- package/dist/policy.js +26 -0
- package/dist/policy.js.map +1 -0
- package/dist/portable-catalog.d.ts +4 -4
- package/dist/portable-catalog.js +14 -126
- package/dist/portable-catalog.js.map +1 -1
- package/dist/portable-verify.d.ts +2 -0
- package/dist/portable-verify.js +68 -0
- package/dist/portable-verify.js.map +1 -0
- package/dist/publication.d.ts +6 -3
- package/dist/publication.js +25 -12
- package/dist/publication.js.map +1 -1
- package/dist/published-output.d.ts +17 -0
- package/dist/published-output.js +49 -0
- package/dist/published-output.js.map +1 -0
- package/dist/site-icons.d.ts +2 -3
- package/dist/site-icons.js +4 -3
- package/dist/site-icons.js.map +1 -1
- package/dist/snapshots.d.ts +39 -0
- package/dist/snapshots.js +238 -0
- package/dist/snapshots.js.map +1 -0
- package/dist/source-resolution.d.ts +28 -0
- package/dist/source-resolution.js +158 -0
- package/dist/source-resolution.js.map +1 -0
- package/dist/sources.d.ts +5 -1
- package/dist/sources.js +21 -27
- package/dist/sources.js.map +1 -1
- package/dist/synchronize.d.ts +44 -0
- package/dist/synchronize.js +118 -0
- package/dist/synchronize.js.map +1 -0
- package/dist/taxonomy.d.ts +23 -0
- package/dist/taxonomy.js +62 -0
- package/dist/taxonomy.js.map +1 -0
- package/dist/upstream.d.ts +42 -0
- package/dist/upstream.js +179 -0
- package/dist/upstream.js.map +1 -0
- package/dist/verify-artifacts.d.ts +36 -0
- package/dist/verify-artifacts.js +286 -0
- package/dist/verify-artifacts.js.map +1 -0
- package/native/build-provenance.txt +5 -5
- package/native/staged-reader-win32-arm64.node +0 -0
- package/native/staged-reader-win32-x64.node +0 -0
- package/package.json +2 -4
- package/src/agent-plugins-catalog.test.ts +2 -2
- package/src/agent-plugins-catalog.ts +46 -49
- package/src/agent-plugins-schema.ts +11 -1
- package/src/artifacts.ts +234 -0
- package/src/build.ts +131 -0
- package/src/capabilities.ts +153 -0
- package/src/catalog-store.ts +1 -28
- package/src/concurrency.ts +19 -0
- package/src/consumer-index.test.ts +27 -21
- package/src/consumer-index.ts +12 -35
- package/src/digest.ts +55 -0
- package/src/fs-atomic.ts +32 -0
- package/src/harness.test.ts +4 -32
- package/src/harness.ts +2 -1
- package/src/icon-mirror.ts +57 -64
- package/src/icons.test.ts +0 -4
- package/src/icons.ts +4 -4
- package/src/index.test.ts +52 -48
- package/src/index.ts +50 -2583
- package/src/integrity-manifest.ts +41 -21
- package/src/json-values.ts +11 -0
- package/src/plugin-release.ts +457 -0
- package/src/plugin-skips.ts +110 -0
- package/src/policy.ts +39 -0
- package/src/portable-catalog.test.ts +0 -5
- package/src/portable-catalog.ts +16 -143
- package/src/portable-verify.ts +71 -0
- package/src/publication.test.ts +29 -1
- package/src/publication.ts +32 -13
- package/src/published-output.ts +60 -0
- package/src/site-icons.test.ts +0 -1
- package/src/site-icons.ts +4 -4
- package/src/snapshots.ts +287 -0
- package/src/source-digest.test.ts +3 -7
- package/src/source-resolution.ts +233 -0
- package/src/sources-json.test.ts +0 -1
- package/src/sources.ts +27 -33
- package/src/synchronize.ts +213 -0
- package/src/taxonomy.ts +87 -0
- package/src/upstream.ts +194 -0
- package/src/verify-artifacts.ts +365 -0
- package/src/write-artifacts.test.ts +0 -1
- package/dist/source-digest.d.ts +0 -2
- package/dist/source-digest.js +0 -12
- package/dist/source-digest.js.map +0 -1
- 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', () =>
|
package/dist/source-digest.d.ts
DELETED
package/dist/source-digest.js
DELETED
|
@@ -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"}
|
package/src/source-digest.ts
DELETED
|
@@ -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
|
-
}
|