@frockbot/catalog-core 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/index.test.ts +141 -0
- package/src/index.ts +156 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/catalog-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@frockbot/kernel-composition": "0.
|
|
14
|
+
"@frockbot/kernel-composition": "0.2.0"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"@types/bun": "1.3.6",
|
package/src/index.test.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import {
|
|
3
3
|
MAX_CATALOG_SKILL_BODY_BYTES_V1,
|
|
4
|
+
assertCatalogPackageBundleV1,
|
|
4
5
|
assertCatalogEntryMatchesIndexV1,
|
|
5
6
|
catalogContentHashV1,
|
|
6
7
|
catalogEntryKeyV1,
|
|
8
|
+
catalogPackageArtifactKeyV1,
|
|
9
|
+
catalogPackageSourceKeyV1,
|
|
10
|
+
catalogPackageUiArtifactKeyV1,
|
|
7
11
|
catalogSetupFieldKeyV1,
|
|
8
12
|
catalogIndexKeyV1,
|
|
9
13
|
CATALOG_POINTER_KEY_V1,
|
|
@@ -20,6 +24,28 @@ import {
|
|
|
20
24
|
|
|
21
25
|
const MANIFEST_HASH = "a".repeat(64);
|
|
22
26
|
|
|
27
|
+
function isolateManifest() {
|
|
28
|
+
return {
|
|
29
|
+
schemaVersion: 3 as const,
|
|
30
|
+
id: "parcel-tracking",
|
|
31
|
+
displayName: "Parcel tracking",
|
|
32
|
+
version: "0.0.1",
|
|
33
|
+
compatibility: { frockbot: ">=0.0.1" },
|
|
34
|
+
dependencies: {},
|
|
35
|
+
contributions: {
|
|
36
|
+
runtime: { entry: "./package.js", host: "bot-isolate" as const },
|
|
37
|
+
},
|
|
38
|
+
tools: [
|
|
39
|
+
{
|
|
40
|
+
name: "track_parcel",
|
|
41
|
+
description: "Tracks a parcel.",
|
|
42
|
+
inputSchema: { type: "object" },
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
permissions: [],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
23
49
|
function indexEntry(
|
|
24
50
|
overrides: Partial<CatalogIndexEntryV1> = {},
|
|
25
51
|
): CatalogIndexEntryV1 {
|
|
@@ -228,6 +254,112 @@ describe("catalog entry decoding", () => {
|
|
|
228
254
|
decodeCatalogEntryV1({ ...entryDetail(), installs: 12 }),
|
|
229
255
|
).toThrow('unknown field "installs"');
|
|
230
256
|
});
|
|
257
|
+
|
|
258
|
+
test("decodes an immutable Bot-isolate bundle with the authored manifest shape", async () => {
|
|
259
|
+
const manifest = isolateManifest();
|
|
260
|
+
const manifestHash = await catalogContentHashV1(
|
|
261
|
+
JSON.stringify(manifest, Object.keys(manifest).sort()),
|
|
262
|
+
);
|
|
263
|
+
const entry = decodeCatalogEntryV1({
|
|
264
|
+
...entryDetail({
|
|
265
|
+
catalogId: "parcel-tracking",
|
|
266
|
+
packageId: "parcel-tracking",
|
|
267
|
+
displayName: "Parcel tracking",
|
|
268
|
+
description: "Tracks deliveries.",
|
|
269
|
+
kind: "package",
|
|
270
|
+
manifestHash,
|
|
271
|
+
}),
|
|
272
|
+
tags: ["shipping", "parcels"],
|
|
273
|
+
bundle: {
|
|
274
|
+
contentHash: "b".repeat(64),
|
|
275
|
+
size: 512,
|
|
276
|
+
mediaType: "application/javascript",
|
|
277
|
+
bundlerVersion: "catalog-test@1",
|
|
278
|
+
manifest,
|
|
279
|
+
sourceHash: "c".repeat(64),
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
expect(entry.bundle?.manifest.tools?.[0]?.name).toBe("track_parcel");
|
|
284
|
+
expect(entry.tags).toEqual(["shipping", "parcels"]);
|
|
285
|
+
// The decoder validates shape synchronously; the reader additionally
|
|
286
|
+
// verifies the exact manifest bytes against the entry's hash.
|
|
287
|
+
await expect(assertCatalogPackageBundleV1(entry)).rejects.toThrow(
|
|
288
|
+
"manifest failed content hash verification",
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("keeps an iframe artifact reference in the bundle's verified manifest", () => {
|
|
293
|
+
const uiArtifact = {
|
|
294
|
+
contentHash: "d".repeat(64),
|
|
295
|
+
size: 42,
|
|
296
|
+
mediaType: "text/html" as const,
|
|
297
|
+
bundlerVersion: "frockbot-inline-html@1",
|
|
298
|
+
};
|
|
299
|
+
const manifest = isolateManifest();
|
|
300
|
+
const decoded = decodeCatalogEntryV1({
|
|
301
|
+
...entryDetail({
|
|
302
|
+
catalogId: "parcel-tracking",
|
|
303
|
+
packageId: "parcel-tracking",
|
|
304
|
+
displayName: "Parcel tracking",
|
|
305
|
+
description: "Tracks deliveries.",
|
|
306
|
+
kind: "package",
|
|
307
|
+
}),
|
|
308
|
+
bundle: {
|
|
309
|
+
contentHash: "b".repeat(64),
|
|
310
|
+
size: 512,
|
|
311
|
+
mediaType: "application/javascript",
|
|
312
|
+
bundlerVersion: "catalog-test@1",
|
|
313
|
+
manifest: {
|
|
314
|
+
...manifest,
|
|
315
|
+
contributions: {
|
|
316
|
+
...manifest.contributions,
|
|
317
|
+
client: {
|
|
318
|
+
kind: "iframe",
|
|
319
|
+
artifact: uiArtifact,
|
|
320
|
+
mounts: [{ slot: "frockbot.tool-result:track_parcel" }],
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
expect(decoded.bundle?.manifest.contributions.client).toEqual({
|
|
328
|
+
kind: "iframe",
|
|
329
|
+
artifact: uiArtifact,
|
|
330
|
+
mounts: [{ slot: "frockbot.tool-result:track_parcel" }],
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("refuses code on a connector or a manifest that does not target the Bot isolate", () => {
|
|
335
|
+
const bundle = {
|
|
336
|
+
contentHash: "b".repeat(64),
|
|
337
|
+
size: 512,
|
|
338
|
+
mediaType: "application/javascript" as const,
|
|
339
|
+
bundlerVersion: "catalog-test@1",
|
|
340
|
+
manifest: isolateManifest(),
|
|
341
|
+
};
|
|
342
|
+
expect(() => decodeCatalogEntryV1({ ...entryDetail(), bundle })).toThrow(
|
|
343
|
+
"only a package Catalog entry may carry a bundle",
|
|
344
|
+
);
|
|
345
|
+
expect(() =>
|
|
346
|
+
decodeCatalogEntryV1({
|
|
347
|
+
...entryDetail({
|
|
348
|
+
catalogId: "parcel-tracking",
|
|
349
|
+
packageId: "parcel-tracking",
|
|
350
|
+
kind: "package",
|
|
351
|
+
}),
|
|
352
|
+
bundle: {
|
|
353
|
+
...bundle,
|
|
354
|
+
manifest: {
|
|
355
|
+
...isolateManifest(),
|
|
356
|
+
contributions: { runtime: { entry: "./runtime.js" } },
|
|
357
|
+
tools: undefined,
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
}),
|
|
361
|
+
).toThrow("must declare a Bot isolate runtime");
|
|
362
|
+
});
|
|
231
363
|
});
|
|
232
364
|
|
|
233
365
|
describe("content addressing", () => {
|
|
@@ -278,6 +410,15 @@ describe("object layout", () => {
|
|
|
278
410
|
"catalog/gen-0001/entry/mcp-weather.json",
|
|
279
411
|
);
|
|
280
412
|
expect(CATALOG_POINTER_KEY_V1).toBe("catalog/current");
|
|
413
|
+
expect(catalogPackageArtifactKeyV1("b".repeat(64))).toBe(
|
|
414
|
+
`packages/${"b".repeat(64)}.mjs`,
|
|
415
|
+
);
|
|
416
|
+
expect(catalogPackageSourceKeyV1("c".repeat(64))).toBe(
|
|
417
|
+
`packages/${"c".repeat(64)}.ts`,
|
|
418
|
+
);
|
|
419
|
+
expect(catalogPackageUiArtifactKeyV1("d".repeat(64))).toBe(
|
|
420
|
+
`packages/${"d".repeat(64)}.html`,
|
|
421
|
+
);
|
|
281
422
|
});
|
|
282
423
|
|
|
283
424
|
test("refuses a generation or catalogId that could escape its prefix", () => {
|
package/src/index.ts
CHANGED
|
@@ -23,9 +23,12 @@
|
|
|
23
23
|
* manifest could not declare.
|
|
24
24
|
*/
|
|
25
25
|
import {
|
|
26
|
+
decodeFrockBotManifest,
|
|
26
27
|
decodePackageSettingSchemaV1,
|
|
28
|
+
type FrockBotManifest,
|
|
27
29
|
type PackageSettingSchema,
|
|
28
30
|
} from "@frockbot/kernel-composition";
|
|
31
|
+
import { canonicalJson } from "@frockbot/kernel-composition/compiler";
|
|
29
32
|
|
|
30
33
|
export class CatalogDecodeError extends Error {
|
|
31
34
|
constructor(message: string, options?: ErrorOptions) {
|
|
@@ -41,6 +44,7 @@ export const MAX_CATALOG_ENTRIES_V1 = 512;
|
|
|
41
44
|
export const MAX_CATALOG_SERVERS_V1 = 16;
|
|
42
45
|
export const MAX_CATALOG_SETUP_FIELDS_V1 = 32;
|
|
43
46
|
export const MAX_CATALOG_SKILLS_V1 = 64;
|
|
47
|
+
export const MAX_CATALOG_TAGS_V1 = 32;
|
|
44
48
|
/**
|
|
45
49
|
* Longest Skill body one Catalog entry may carry.
|
|
46
50
|
*
|
|
@@ -65,6 +69,9 @@ export interface CatalogIndexEntryV1 {
|
|
|
65
69
|
version: string;
|
|
66
70
|
manifestHash: string;
|
|
67
71
|
kind: CatalogEntryKindV1;
|
|
72
|
+
/** Present when this row names code loaded in a Bot isolate. */
|
|
73
|
+
contentHash?: string;
|
|
74
|
+
tags?: string[];
|
|
68
75
|
logo?: string;
|
|
69
76
|
homepage?: string;
|
|
70
77
|
}
|
|
@@ -93,6 +100,25 @@ export interface CatalogSkillV1 {
|
|
|
93
100
|
body?: string;
|
|
94
101
|
}
|
|
95
102
|
|
|
103
|
+
/**
|
|
104
|
+
* One immutable non-first-party bundle named by a Package Catalog entry.
|
|
105
|
+
*
|
|
106
|
+
* The manifest is decoded with the same kernel decoder used for a stored
|
|
107
|
+
* Bot-authored manifest. `sourceHash` is optional because source publication
|
|
108
|
+
* is optional; when present it addresses the retained `.ts` object in the
|
|
109
|
+
* shared Package artifact store. A sandboxed page needs no duplicate bundle
|
|
110
|
+
* field: its canonical artifact descriptor is the decoded manifest's iframe
|
|
111
|
+
* client Contribution.
|
|
112
|
+
*/
|
|
113
|
+
export interface CatalogPackageBundleV1 {
|
|
114
|
+
contentHash: string;
|
|
115
|
+
size: number;
|
|
116
|
+
mediaType: "application/javascript";
|
|
117
|
+
bundlerVersion: string;
|
|
118
|
+
manifest: FrockBotManifest;
|
|
119
|
+
sourceHash?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
96
122
|
export interface CatalogEntryV1 {
|
|
97
123
|
schemaVersion: 1;
|
|
98
124
|
catalogId: string;
|
|
@@ -102,11 +128,14 @@ export interface CatalogEntryV1 {
|
|
|
102
128
|
version: string;
|
|
103
129
|
kind: CatalogEntryKindV1;
|
|
104
130
|
manifestHash: string;
|
|
131
|
+
tags?: string[];
|
|
105
132
|
logo?: string;
|
|
106
133
|
homepage?: string;
|
|
107
134
|
servers: CatalogServerV1[];
|
|
108
135
|
setupFields: PackageSettingSchema[];
|
|
109
136
|
skills: CatalogSkillV1[];
|
|
137
|
+
/** Absent means the existing reviewed, compiled-in first-party form. */
|
|
138
|
+
bundle?: CatalogPackageBundleV1;
|
|
110
139
|
}
|
|
111
140
|
|
|
112
141
|
/**
|
|
@@ -232,6 +261,20 @@ function entryKind(value: unknown, label: string): CatalogEntryKindV1 {
|
|
|
232
261
|
return value;
|
|
233
262
|
}
|
|
234
263
|
|
|
264
|
+
function tags(value: unknown, label: string): string[] | undefined {
|
|
265
|
+
if (value === undefined) return undefined;
|
|
266
|
+
const decoded = boundedArray(value, label, MAX_CATALOG_TAGS_V1).map(
|
|
267
|
+
(tag, index) => text(tag, `${label}[${index}]`, 64),
|
|
268
|
+
);
|
|
269
|
+
if (
|
|
270
|
+
new Set(decoded.map((tag) => tag.toLocaleLowerCase())).size !==
|
|
271
|
+
decoded.length
|
|
272
|
+
) {
|
|
273
|
+
throw new CatalogDecodeError(`${label} repeats a tag`);
|
|
274
|
+
}
|
|
275
|
+
return decoded;
|
|
276
|
+
}
|
|
277
|
+
|
|
235
278
|
/**
|
|
236
279
|
* A Catalog logo and homepage are rendered by the browser, so only an absolute
|
|
237
280
|
* `https:` URL is admitted: a `javascript:` or `data:` value in a document the
|
|
@@ -295,9 +338,9 @@ export function decodeCatalogIndexEntryV1(value: unknown): CatalogIndexEntryV1 {
|
|
|
295
338
|
"manifestHash",
|
|
296
339
|
"kind",
|
|
297
340
|
],
|
|
298
|
-
["logo", "homepage"],
|
|
341
|
+
["contentHash", "tags", "logo", "homepage"],
|
|
299
342
|
);
|
|
300
|
-
|
|
343
|
+
const decoded = withOptional(
|
|
301
344
|
{
|
|
302
345
|
catalogId: decodeCatalogIdV1(entry.catalogId),
|
|
303
346
|
packageId: pattern(entry.packageId, "packageId", PACKAGE_ID_PATTERN, 64),
|
|
@@ -313,10 +356,18 @@ export function decodeCatalogIndexEntryV1(value: unknown): CatalogIndexEntryV1 {
|
|
|
313
356
|
kind: entryKind(entry.kind, "kind"),
|
|
314
357
|
},
|
|
315
358
|
{
|
|
359
|
+
contentHash:
|
|
360
|
+
entry.contentHash === undefined
|
|
361
|
+
? undefined
|
|
362
|
+
: decodeCatalogContentHashV1(entry.contentHash, "contentHash"),
|
|
316
363
|
logo: optionalHttpsUrl(entry.logo, "logo"),
|
|
317
364
|
homepage: optionalHttpsUrl(entry.homepage, "homepage"),
|
|
318
365
|
},
|
|
319
366
|
);
|
|
367
|
+
const decodedTags = tags(entry.tags, "catalog tags");
|
|
368
|
+
return decodedTags === undefined
|
|
369
|
+
? decoded
|
|
370
|
+
: { ...decoded, tags: decodedTags };
|
|
320
371
|
}
|
|
321
372
|
|
|
322
373
|
export function decodeCatalogIndexV1(input: unknown): CatalogIndexV1 {
|
|
@@ -391,6 +442,49 @@ function decodeCatalogSkillV1(value: unknown): CatalogSkillV1 {
|
|
|
391
442
|
);
|
|
392
443
|
}
|
|
393
444
|
|
|
445
|
+
function decodeCatalogPackageBundleV1(input: unknown): CatalogPackageBundleV1 {
|
|
446
|
+
const bundle = exactRecord(
|
|
447
|
+
input,
|
|
448
|
+
"catalog package bundle",
|
|
449
|
+
["contentHash", "size", "mediaType", "bundlerVersion", "manifest"],
|
|
450
|
+
["sourceHash"],
|
|
451
|
+
);
|
|
452
|
+
if (!Number.isSafeInteger(bundle.size) || (bundle.size as number) < 0) {
|
|
453
|
+
throw new CatalogDecodeError("catalog package bundle size is invalid");
|
|
454
|
+
}
|
|
455
|
+
if (bundle.mediaType !== "application/javascript") {
|
|
456
|
+
throw new CatalogDecodeError("catalog package bundle mediaType is invalid");
|
|
457
|
+
}
|
|
458
|
+
const manifest = decodeFrockBotManifest(bundle.manifest);
|
|
459
|
+
if (manifest.contributions.runtime?.host !== "bot-isolate") {
|
|
460
|
+
throw new CatalogDecodeError(
|
|
461
|
+
"catalog package bundle manifest must declare a Bot isolate runtime",
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
contentHash: decodeCatalogContentHashV1(
|
|
466
|
+
bundle.contentHash,
|
|
467
|
+
"catalog package bundle contentHash",
|
|
468
|
+
),
|
|
469
|
+
size: bundle.size as number,
|
|
470
|
+
mediaType: "application/javascript",
|
|
471
|
+
bundlerVersion: text(
|
|
472
|
+
bundle.bundlerVersion,
|
|
473
|
+
"catalog package bundle bundlerVersion",
|
|
474
|
+
128,
|
|
475
|
+
),
|
|
476
|
+
manifest,
|
|
477
|
+
...(bundle.sourceHash === undefined
|
|
478
|
+
? {}
|
|
479
|
+
: {
|
|
480
|
+
sourceHash: decodeCatalogContentHashV1(
|
|
481
|
+
bundle.sourceHash,
|
|
482
|
+
"catalog package bundle sourceHash",
|
|
483
|
+
),
|
|
484
|
+
}),
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
394
488
|
export function decodeCatalogEntryV1(input: unknown): CatalogEntryV1 {
|
|
395
489
|
const value = exactRecord(
|
|
396
490
|
input,
|
|
@@ -408,10 +502,10 @@ export function decodeCatalogEntryV1(input: unknown): CatalogEntryV1 {
|
|
|
408
502
|
"setupFields",
|
|
409
503
|
"skills",
|
|
410
504
|
],
|
|
411
|
-
["logo", "homepage"],
|
|
505
|
+
["tags", "logo", "homepage", "bundle"],
|
|
412
506
|
);
|
|
413
507
|
schemaVersion(value, "catalog entry detail");
|
|
414
|
-
|
|
508
|
+
const decoded = withOptional(
|
|
415
509
|
{
|
|
416
510
|
schemaVersion: 1 as const,
|
|
417
511
|
catalogId: decodeCatalogIdV1(value.catalogId),
|
|
@@ -447,6 +541,45 @@ export function decodeCatalogEntryV1(input: unknown): CatalogEntryV1 {
|
|
|
447
541
|
homepage: optionalHttpsUrl(value.homepage, "homepage"),
|
|
448
542
|
},
|
|
449
543
|
);
|
|
544
|
+
const decodedTags = tags(value.tags, "catalog tags");
|
|
545
|
+
const bundle =
|
|
546
|
+
value.bundle === undefined
|
|
547
|
+
? undefined
|
|
548
|
+
: decodeCatalogPackageBundleV1(value.bundle);
|
|
549
|
+
if (bundle && decoded.kind !== "package") {
|
|
550
|
+
throw new CatalogDecodeError(
|
|
551
|
+
"only a package Catalog entry may carry a bundle",
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
if (
|
|
555
|
+
bundle &&
|
|
556
|
+
(bundle.manifest.id !== decoded.packageId ||
|
|
557
|
+
bundle.manifest.version !== decoded.version)
|
|
558
|
+
) {
|
|
559
|
+
throw new CatalogDecodeError(
|
|
560
|
+
"catalog package bundle manifest does not match its entry",
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
return {
|
|
564
|
+
...decoded,
|
|
565
|
+
...(decodedTags === undefined ? {} : { tags: decodedTags }),
|
|
566
|
+
...(bundle === undefined ? {} : { bundle }),
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** Verify the nested manifest against the entry's immutable manifest hash. */
|
|
571
|
+
export async function assertCatalogPackageBundleV1(
|
|
572
|
+
entry: CatalogEntryV1,
|
|
573
|
+
): Promise<void> {
|
|
574
|
+
if (!entry.bundle) return;
|
|
575
|
+
const actual = await catalogContentHashV1(
|
|
576
|
+
canonicalJson(entry.bundle.manifest),
|
|
577
|
+
);
|
|
578
|
+
if (actual !== entry.manifestHash) {
|
|
579
|
+
throw new CatalogDecodeError(
|
|
580
|
+
`catalog entry "${entry.catalogId}" bundle manifest failed content hash verification`,
|
|
581
|
+
);
|
|
582
|
+
}
|
|
450
583
|
}
|
|
451
584
|
|
|
452
585
|
export function decodeCatalogPointerV1(input: unknown): CatalogPointerV1 {
|
|
@@ -474,6 +607,20 @@ export function catalogEntryKeyV1(
|
|
|
474
607
|
return `catalog/${decodeCatalogGenerationIdV1(generation)}/entry/${decodeCatalogIdV1(catalogId)}.json`;
|
|
475
608
|
}
|
|
476
609
|
|
|
610
|
+
/** Package artifacts share the same immutable store and layout as authored code. */
|
|
611
|
+
export function catalogPackageArtifactKeyV1(contentHash: string): string {
|
|
612
|
+
return `packages/${decodeCatalogContentHashV1(contentHash)}.mjs`;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export function catalogPackageSourceKeyV1(sourceHash: string): string {
|
|
616
|
+
return `packages/${decodeCatalogContentHashV1(sourceHash)}.ts`;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** Sandboxed UI artifacts share the authored Package artifact layout too. */
|
|
620
|
+
export function catalogPackageUiArtifactKeyV1(contentHash: string): string {
|
|
621
|
+
return `packages/${decodeCatalogContentHashV1(contentHash)}.html`;
|
|
622
|
+
}
|
|
623
|
+
|
|
477
624
|
/**
|
|
478
625
|
* The content hash of one Catalog document: SHA-256 over the exact UTF-8 bytes
|
|
479
626
|
* the bucket holds. Hashing the bytes rather than a re-serialization is what
|
|
@@ -535,7 +682,9 @@ export async function decodeCatalogEntryDocumentV1(
|
|
|
535
682
|
expectedHash: string,
|
|
536
683
|
): Promise<CatalogEntryV1> {
|
|
537
684
|
await assertContentHash(document, expectedHash, "catalog entry");
|
|
538
|
-
|
|
685
|
+
const entry = decodeCatalogEntryV1(parseDocument(document, "catalog entry"));
|
|
686
|
+
await assertCatalogPackageBundleV1(entry);
|
|
687
|
+
return entry;
|
|
539
688
|
}
|
|
540
689
|
|
|
541
690
|
/** Parse an index document whose hash the caller has not pinned yet. */
|
|
@@ -561,7 +710,8 @@ export function assertCatalogEntryMatchesIndexV1(
|
|
|
561
710
|
entry.packageId !== indexEntry.packageId ||
|
|
562
711
|
entry.version !== indexEntry.version ||
|
|
563
712
|
entry.kind !== indexEntry.kind ||
|
|
564
|
-
entry.manifestHash !== indexEntry.manifestHash
|
|
713
|
+
entry.manifestHash !== indexEntry.manifestHash ||
|
|
714
|
+
entry.bundle?.contentHash !== indexEntry.contentHash
|
|
565
715
|
) {
|
|
566
716
|
throw new CatalogDecodeError(
|
|
567
717
|
`catalog entry "${indexEntry.catalogId}" does not match its index row`,
|