@brandzap/kit-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @brandzap/kit-core
2
+
3
+ Shared runtime contract for published BrandZap Brand Kits. It provides schema-v1 manifest and asset validation, immutable resource maps, active-kit reading, and component and asset lookup helpers used by the Kit service and CLI.
4
+
5
+ This package does not contain publishing credentials or network clients.
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@brandzap/kit-core",
3
+ "version": "0.1.0",
4
+ "description": "Shared published Brand Kit schemas, validation, readers, and search helpers.",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.js",
8
+ "./package.json": "./package.json"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=20"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ }
20
+ }
package/src/assets.js ADDED
@@ -0,0 +1,12 @@
1
+ import { kitSchemaVersion } from "./resource-map.js";
2
+
3
+ export function createEmptyAssetsDocument() {
4
+ return {
5
+ schemaVersion: kitSchemaVersion,
6
+ assets: []
7
+ };
8
+ }
9
+
10
+ export function serializeAssetsDocument(document = createEmptyAssetsDocument()) {
11
+ return `${JSON.stringify(document, null, 2)}\n`;
12
+ }
@@ -0,0 +1,71 @@
1
+ export function listComponentSummaries(components = []) {
2
+ return components.map((component) => ({
3
+ id: component.id,
4
+ name: component.name,
5
+ category: component.category ?? "",
6
+ group: component.group ?? "",
7
+ family: component.family ?? "",
8
+ description: component.description ?? "",
9
+ tags: Array.isArray(component.tags) ? component.tags : []
10
+ }));
11
+ }
12
+
13
+ export function getComponentById(components = [], componentId) {
14
+ return components.find((component) => component.id === componentId) ?? null;
15
+ }
16
+
17
+ export function searchComponents(components = [], query = "", limit = 20) {
18
+ const needle = String(query).trim().toLowerCase();
19
+ const safeLimit = Math.min(50, Math.max(1, Number(limit) || 20));
20
+ if (!needle) return listComponentSummaries(components).slice(0, safeLimit);
21
+ return components
22
+ .filter((component) => [
23
+ component.id,
24
+ component.name,
25
+ component.category,
26
+ component.group,
27
+ component.family,
28
+ component.description,
29
+ component.usageGuidance,
30
+ component.agentNotes,
31
+ ...(component.tags ?? [])
32
+ ].join(" ").toLowerCase().includes(needle))
33
+ .map((component) => listComponentSummaries([component])[0])
34
+ .slice(0, safeLimit);
35
+ }
36
+
37
+ export function listAssetSummaries(assetsDocument = { assets: [] }) {
38
+ return (assetsDocument.assets ?? []).map((asset) => ({
39
+ id: asset.id,
40
+ name: asset.name,
41
+ type: asset.type,
42
+ mediaType: asset.mediaType,
43
+ path: asset.path ?? null,
44
+ description: asset.description ?? "",
45
+ usageGuidance: asset.usageGuidance ?? "",
46
+ theme: asset.theme ?? null,
47
+ preferredFor: asset.preferredFor ?? [],
48
+ tags: asset.tags ?? []
49
+ }));
50
+ }
51
+
52
+ export function getAssetById(assetsDocument = { assets: [] }, assetId) {
53
+ return (assetsDocument.assets ?? []).find((asset) => asset.id === assetId) ?? null;
54
+ }
55
+
56
+ export function searchAssets(assetsDocument = { assets: [] }, query = "", limit = 20) {
57
+ const needle = String(query).trim().toLowerCase();
58
+ const safeLimit = Math.min(50, Math.max(1, Number(limit) || 20));
59
+ const assets = assetsDocument.assets ?? [];
60
+ if (!needle) return listAssetSummaries(assetsDocument).slice(0, safeLimit);
61
+ return assets.filter((asset) => [
62
+ asset.id,
63
+ asset.name,
64
+ asset.type,
65
+ asset.description,
66
+ asset.usageGuidance,
67
+ ...(asset.tags ?? [])
68
+ ].join(" ").toLowerCase().includes(needle))
69
+ .map((asset) => listAssetSummaries({ assets: [asset] })[0])
70
+ .slice(0, safeLimit);
71
+ }
package/src/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./assets.js";
2
+ export * from "./component-search.js";
3
+ export * from "./kit-reader.js";
4
+ export * from "./manifest.js";
5
+ export * from "./resource-map.js";
6
+ export * from "./schemas.js";
@@ -0,0 +1,51 @@
1
+ import { KitNotFoundError, validateBrandSlug, validateLatestPointer, validateManifest } from "./schemas.js";
2
+
3
+ export class KitReader {
4
+ constructor(driver) {
5
+ this.driver = driver;
6
+ }
7
+
8
+ async resolveActive(brandSlug) {
9
+ const slug = validateBrandSlug(brandSlug);
10
+ const pointer = await this.driver.readActivePointer(slug);
11
+ if (!pointer) throw new KitNotFoundError();
12
+ validateLatestPointer(pointer);
13
+ if (pointer.status !== "published" || !pointer.version) throw new KitNotFoundError();
14
+ const manifestText = await this.driver.readVersionResource(slug, pointer.version, "manifest.json");
15
+ if (manifestText === null) throw new KitNotFoundError();
16
+ let manifest;
17
+ try {
18
+ manifest = JSON.parse(manifestText);
19
+ } catch {
20
+ throw new KitNotFoundError();
21
+ }
22
+ validateManifest(manifest, { expectedSlug: slug });
23
+ if (manifest.version !== pointer.version) throw new KitNotFoundError();
24
+ return { slug, version: pointer.version, pointer, manifest, manifestText };
25
+ }
26
+
27
+ async readResource(snapshot, resourcePath) {
28
+ if (resourcePath === "manifest.json") return snapshot.manifestText;
29
+ const content = await this.driver.readVersionResource(snapshot.slug, snapshot.version, resourcePath);
30
+ if (content === null) throw new KitNotFoundError("Brand kit resource not found.");
31
+ return content;
32
+ }
33
+
34
+ async readJsonResource(snapshot, resourcePath) {
35
+ const content = await this.readResource(snapshot, resourcePath);
36
+ try {
37
+ return JSON.parse(content);
38
+ } catch {
39
+ throw new KitNotFoundError("Brand kit resource is invalid.");
40
+ }
41
+ }
42
+
43
+ async readResourceBuffer(snapshot, resourcePath) {
44
+ if (resourcePath === "manifest.json") return Buffer.from(snapshot.manifestText, "utf8");
45
+ const content = typeof this.driver.readVersionResourceBuffer === "function"
46
+ ? await this.driver.readVersionResourceBuffer(snapshot.slug, snapshot.version, resourcePath)
47
+ : Buffer.from(await this.driver.readVersionResource(snapshot.slug, snapshot.version, resourcePath), "utf8");
48
+ if (content === null) throw new KitNotFoundError("Brand kit resource not found.");
49
+ return Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8");
50
+ }
51
+ }
@@ -0,0 +1,31 @@
1
+ import { kitSchemaVersion, legacyKitSchemaVersion, legacyManifestResources, manifestResources } from "./resource-map.js";
2
+ import { validateManifest } from "./schemas.js";
3
+
4
+ export function createPublishedManifest({
5
+ kitId,
6
+ publishId,
7
+ brandId,
8
+ brandSlug,
9
+ name,
10
+ version,
11
+ publishedAt,
12
+ baseUrl,
13
+ contentDigest,
14
+ schemaVersion = kitSchemaVersion
15
+ }) {
16
+ const canonicalUrl = `${String(baseUrl).replace(/\/$/, "")}/${brandSlug}`;
17
+ return validateManifest({
18
+ schemaVersion,
19
+ kitId,
20
+ publishId,
21
+ brandId,
22
+ brandSlug,
23
+ name,
24
+ version,
25
+ publishedAt,
26
+ canonicalUrl,
27
+ mcpUrl: `${canonicalUrl}/mcp`,
28
+ contentDigest,
29
+ resources: schemaVersion === legacyKitSchemaVersion ? legacyManifestResources : manifestResources
30
+ }, { expectedSlug: brandSlug });
31
+ }
@@ -0,0 +1,92 @@
1
+ export const legacyKitSchemaVersion = "1";
2
+ export const kitSchemaVersion = "2";
3
+
4
+ export const manifestResources = Object.freeze({
5
+ agentGuide: "./agent.md",
6
+ tokens: "./tokens.json",
7
+ components: "./components.json",
8
+ stylesheet: "./styles.css",
9
+ brandVoice: "./brand-voice.json",
10
+ assets: "./assets.json",
11
+ adapters: Object.freeze({
12
+ agents: "./adapters/AGENTS.md",
13
+ claude: "./adapters/CLAUDE.md",
14
+ cursor: "./adapters/cursor-rules.md"
15
+ })
16
+ });
17
+
18
+ export const legacyManifestResources = Object.freeze({
19
+ agentGuide: "./agent.md",
20
+ tokens: "./tokens.json",
21
+ components: "./components.json",
22
+ stylesheet: "./styles.css",
23
+ assets: "./assets.json",
24
+ adapters: Object.freeze({
25
+ agents: "./adapters/AGENTS.md",
26
+ claude: "./adapters/CLAUDE.md",
27
+ cursor: "./adapters/cursor-rules.md"
28
+ })
29
+ });
30
+
31
+ export const publishedResourcePaths = Object.freeze([
32
+ "agent.md",
33
+ "tokens.json",
34
+ "components.json",
35
+ "styles.css",
36
+ "brand-voice.json",
37
+ "assets.json",
38
+ "adapters/AGENTS.md",
39
+ "adapters/CLAUDE.md",
40
+ "adapters/cursor-rules.md"
41
+ ]);
42
+
43
+ export const legacyPublishedResourcePaths = Object.freeze([
44
+ "agent.md",
45
+ "tokens.json",
46
+ "components.json",
47
+ "styles.css",
48
+ "assets.json",
49
+ "adapters/AGENTS.md",
50
+ "adapters/CLAUDE.md",
51
+ "adapters/cursor-rules.md"
52
+ ]);
53
+
54
+ export const publicResourceRoutes = Object.freeze({
55
+ "manifest.json": "manifest.json",
56
+ "agent.md": "agent.md",
57
+ "tokens.json": "tokens.json",
58
+ "components.json": "components.json",
59
+ "styles.css": "styles.css",
60
+ "brand-voice.json": "brand-voice.json",
61
+ "assets.json": "assets.json",
62
+ "adapters/AGENTS.md": "adapters/AGENTS.md",
63
+ "adapters/CLAUDE.md": "adapters/CLAUDE.md",
64
+ "adapters/cursor-rules.md": "adapters/cursor-rules.md"
65
+ });
66
+
67
+ export const contentTypesByPath = Object.freeze({
68
+ "manifest.json": "application/json; charset=utf-8",
69
+ "tokens.json": "application/json; charset=utf-8",
70
+ "components.json": "application/json; charset=utf-8",
71
+ "assets.json": "application/json; charset=utf-8",
72
+ "brand-voice.json": "application/json; charset=utf-8",
73
+ "agent.md": "text/markdown; charset=utf-8",
74
+ "styles.css": "text/css; charset=utf-8",
75
+ "adapters/AGENTS.md": "text/markdown; charset=utf-8",
76
+ "adapters/CLAUDE.md": "text/markdown; charset=utf-8",
77
+ "adapters/cursor-rules.md": "text/markdown; charset=utf-8"
78
+ });
79
+
80
+ export function flattenManifestResourcePaths(resources = manifestResources) {
81
+ return [
82
+ resources.agentGuide,
83
+ resources.tokens,
84
+ resources.components,
85
+ resources.stylesheet,
86
+ resources.brandVoice,
87
+ resources.assets,
88
+ resources.adapters?.agents,
89
+ resources.adapters?.claude,
90
+ resources.adapters?.cursor
91
+ ];
92
+ }
package/src/schemas.js ADDED
@@ -0,0 +1,318 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ kitSchemaVersion,
4
+ legacyKitSchemaVersion,
5
+ legacyManifestResources,
6
+ legacyPublishedResourcePaths,
7
+ manifestResources,
8
+ publishedResourcePaths
9
+ } from "./resource-map.js";
10
+
11
+ const slugPattern = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
12
+ const semverPattern = /^1\.0\.(0|[1-9]\d*)$/;
13
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
14
+ const reservedSlugs = new Set(["health", "internal", "static", "mcp", "guide"]);
15
+
16
+ export class KitValidationError extends Error {
17
+ constructor(message, { statusCode = 400, code = "KIT_VALIDATION_ERROR" } = {}) {
18
+ super(message);
19
+ this.name = "KitValidationError";
20
+ this.statusCode = statusCode;
21
+ this.code = code;
22
+ }
23
+ }
24
+
25
+ export class KitNotFoundError extends Error {
26
+ constructor(message = "Brand kit not found.") {
27
+ super(message);
28
+ this.name = "KitNotFoundError";
29
+ this.statusCode = 404;
30
+ this.code = "KIT_NOT_FOUND";
31
+ }
32
+ }
33
+
34
+ export function validateBrandSlug(value) {
35
+ const slug = String(value ?? "").trim();
36
+ if (!slugPattern.test(slug) || reservedSlugs.has(slug)) {
37
+ throw new KitValidationError(
38
+ "Brand slug must be 1-63 lowercase letters, numbers, or internal hyphens and may not be reserved."
39
+ );
40
+ }
41
+ return slug;
42
+ }
43
+
44
+ export function isPublishedVersion(value) {
45
+ return semverPattern.test(String(value ?? ""));
46
+ }
47
+
48
+ export function validatePublishedVersion(value) {
49
+ const version = String(value ?? "");
50
+ if (!isPublishedVersion(version)) {
51
+ throw new KitValidationError("Published version must use the 1.0.x format.");
52
+ }
53
+ return version;
54
+ }
55
+
56
+ export function validateUuid(value, fieldName) {
57
+ const normalized = String(value ?? "");
58
+ if (!uuidPattern.test(normalized)) {
59
+ throw new KitValidationError(`${fieldName} must be a UUID.`);
60
+ }
61
+ return normalized;
62
+ }
63
+
64
+ export function validateAssetsDocument(input) {
65
+ assertPlainObject(input, "assets.json must contain an object.");
66
+ if (!isSupportedSchemaVersion(input.schemaVersion) || !Array.isArray(input.assets)) {
67
+ throw new KitValidationError('assets.json must use schemaVersion "1" or "2" and contain an assets array.');
68
+ }
69
+ if ("usageDescription" in input) {
70
+ requiredString(input.usageDescription, "Assets usageDescription", { allowEmpty: true });
71
+ }
72
+ const ids = new Set();
73
+ const preferredLogoModes = new Set();
74
+ const allowedTypes = new Set(["logo", "icon", "illustration", "brand-flair", "decorative-graphic", "image", "download", "background"]);
75
+ for (const asset of input.assets) {
76
+ assertPlainObject(asset, "Each asset must be an object.");
77
+ const id = requiredString(asset.id, "Asset id");
78
+ if (ids.has(id)) throw new KitValidationError(`Duplicate asset id: ${id}.`);
79
+ ids.add(id);
80
+ requiredString(asset.name, `Asset ${id} name`);
81
+ if (!allowedTypes.has(asset.type)) throw new KitValidationError(`Asset ${id} has an unsupported type.`);
82
+ requiredString(asset.mediaType, `Asset ${id} mediaType`);
83
+ if (asset.type === "background") {
84
+ requiredString(asset.cssValue, `Asset ${id} cssValue`);
85
+ } else {
86
+ validateRelativeAssetPath(asset.path, id);
87
+ }
88
+ if (!Array.isArray(asset.tags) || asset.tags.some((tag) => typeof tag !== "string")) {
89
+ throw new KitValidationError(`Asset ${id} tags must be an array of strings.`);
90
+ }
91
+ requiredString(asset.usageGuidance, `Asset ${id} usageGuidance`, { allowEmpty: true });
92
+ if (asset.theme !== undefined && !["light", "dark", "both"].includes(asset.theme)) {
93
+ throw new KitValidationError(`Asset ${id} theme must be light, dark, or both.`);
94
+ }
95
+ if (asset.preferredFor !== undefined) {
96
+ if (asset.type !== "logo" || !Array.isArray(asset.preferredFor)) {
97
+ throw new KitValidationError(`Asset ${id} preferredFor is supported only for logos and must be an array.`);
98
+ }
99
+ if (new Set(asset.preferredFor).size !== asset.preferredFor.length
100
+ || asset.preferredFor.some((mode) => !["light", "dark"].includes(mode))) {
101
+ throw new KitValidationError(`Asset ${id} preferredFor must contain unique light and/or dark modes.`);
102
+ }
103
+ for (const mode of asset.preferredFor) {
104
+ if (asset.theme !== "both" && asset.theme !== mode) {
105
+ throw new KitValidationError(`Asset ${id} cannot be preferred for ${mode} mode when its theme is ${asset.theme}.`);
106
+ }
107
+ if (preferredLogoModes.has(mode)) {
108
+ throw new KitValidationError(`Only one logo may be preferred for ${mode} mode.`);
109
+ }
110
+ preferredLogoModes.add(mode);
111
+ }
112
+ }
113
+ }
114
+ return input;
115
+ }
116
+
117
+ export function validateManifest(input, { expectedSlug } = {}) {
118
+ assertPlainObject(input, "manifest.json must contain an object.");
119
+ if (!isSupportedSchemaVersion(input.schemaVersion)) throw new KitValidationError("Unsupported manifest schemaVersion.");
120
+ validateUuid(input.kitId, "kitId");
121
+ validateUuid(input.publishId, "publishId");
122
+ requiredString(input.brandId, "brandId");
123
+ const slug = validateBrandSlug(input.brandSlug);
124
+ if (expectedSlug && slug !== expectedSlug) throw new KitValidationError("Manifest brandSlug does not match the requested kit.");
125
+ requiredString(input.name, "name");
126
+ validatePublishedVersion(input.version);
127
+ validateIsoTimestamp(input.publishedAt, "publishedAt");
128
+ validateCanonicalUrls(input, slug);
129
+ validateManifestResources(input.resources, input.schemaVersion);
130
+ if (input.contentDigest !== undefined && !/^sha256:[0-9a-f]{64}$/.test(input.contentDigest)) {
131
+ throw new KitValidationError("contentDigest must be a sha256 digest.");
132
+ }
133
+ return input;
134
+ }
135
+
136
+ export function validatePublishEnvelope(input, expectedSlug) {
137
+ assertPlainObject(input, "Publish request must contain an object.");
138
+ if (!isSupportedSchemaVersion(input.schemaVersion)) throw new KitValidationError("Unsupported publish schemaVersion.");
139
+ const publishId = validateUuid(input.publishId, "publishId");
140
+ const brandId = requiredString(input.brandId, "brandId");
141
+ const name = requiredString(input.name, "name");
142
+ const brandSlug = validateBrandSlug(expectedSlug);
143
+ if (input.visibility !== undefined && !["public", "restricted"].includes(input.visibility)) {
144
+ throw new KitValidationError("visibility must be public or restricted.");
145
+ }
146
+ const remoteKitId = input.remoteKitId ? validateUuid(input.remoteKitId, "remoteKitId") : null;
147
+ assertPlainObject(input.files, "Publish request files must be an object.");
148
+ const keys = Object.keys(input.files).sort();
149
+ const requiredPaths = input.schemaVersion === legacyKitSchemaVersion
150
+ ? legacyPublishedResourcePaths
151
+ : publishedResourcePaths;
152
+ const expectedKeys = [...requiredPaths].sort();
153
+ if (input.schemaVersion === legacyKitSchemaVersion && JSON.stringify(keys) !== JSON.stringify(expectedKeys)) {
154
+ throw new KitValidationError(`Publish request must contain exactly: ${expectedKeys.join(", ")}.`);
155
+ }
156
+ const missing = expectedKeys.filter((resourcePath) => !keys.includes(resourcePath));
157
+ if (missing.length) {
158
+ throw new KitValidationError(`Publish request is missing: ${missing.join(", ")}.`);
159
+ }
160
+ const files = {};
161
+ for (const resourcePath of keys) {
162
+ if (!requiredPaths.includes(resourcePath) && !isSafePublishedAssetPath(resourcePath)) {
163
+ throw new KitValidationError(`${resourcePath} is not an allowed published resource path.`);
164
+ }
165
+ const content = input.files[resourcePath];
166
+ if (requiredPaths.includes(resourcePath) && typeof content !== "string") {
167
+ throw new KitValidationError(`${resourcePath} must be a UTF-8 string.`);
168
+ }
169
+ files[resourcePath] = normalizePublishedFile(content, resourcePath);
170
+ }
171
+ parseJsonResource(files["tokens.json"], "tokens.json", (value) => assertPlainObject(value, "tokens.json must contain an object."));
172
+ parseJsonResource(files["components.json"], "components.json", (value) => {
173
+ if (!Array.isArray(value)) throw new KitValidationError("components.json must contain an array.");
174
+ const ids = new Set();
175
+ for (const component of value) {
176
+ assertPlainObject(component, "Each component must be an object.");
177
+ const id = requiredString(component.id, "Component id");
178
+ if (ids.has(id)) throw new KitValidationError(`Duplicate component id: ${id}.`);
179
+ ids.add(id);
180
+ }
181
+ });
182
+ const assetsDocument = parseJsonResource(files["assets.json"], "assets.json", validateAssetsDocument);
183
+ if (input.schemaVersion === kitSchemaVersion) {
184
+ for (const asset of assetsDocument.assets) {
185
+ if (!asset.path) continue;
186
+ const resourcePath = asset.path.replace(/^\.\//, "");
187
+ if (!(resourcePath in files)) {
188
+ throw new KitValidationError(`Published asset ${asset.id} is missing file ${resourcePath}.`);
189
+ }
190
+ }
191
+ }
192
+
193
+ return {
194
+ schemaVersion: input.schemaVersion,
195
+ publishId,
196
+ remoteKitId,
197
+ brandId,
198
+ brandSlug,
199
+ name,
200
+ visibility: input.visibility === "public" ? "public" : "restricted",
201
+ files,
202
+ contentDigest: digestPublishedFiles(files)
203
+ };
204
+ }
205
+
206
+ export function digestPublishedFiles(files) {
207
+ const hash = createHash("sha256");
208
+ for (const resourcePath of Object.keys(files).sort()) {
209
+ hash.update(resourcePath);
210
+ hash.update("\0");
211
+ hash.update(publishedFileBytes(files[resourcePath]));
212
+ hash.update("\0");
213
+ }
214
+ return `sha256:${hash.digest("hex")}`;
215
+ }
216
+
217
+ export function validateLatestPointer(input) {
218
+ assertPlainObject(input, "latest.json must contain an object.");
219
+ if (!isSupportedSchemaVersion(input.schemaVersion)) throw new KitValidationError("Unsupported latest pointer schemaVersion.");
220
+ if (input.status !== "published" && input.status !== "unpublished") {
221
+ throw new KitValidationError("Latest pointer status is invalid.");
222
+ }
223
+ if (input.version !== null) validatePublishedVersion(input.version);
224
+ validateIsoTimestamp(input.activatedAt, "activatedAt");
225
+ return input;
226
+ }
227
+
228
+ function validateManifestResources(resources, schemaVersion) {
229
+ assertPlainObject(resources, "Manifest resources must be an object.");
230
+ const normalized = JSON.stringify(resources);
231
+ const expected = schemaVersion === legacyKitSchemaVersion ? legacyManifestResources : manifestResources;
232
+ if (normalized !== JSON.stringify(expected)) {
233
+ throw new KitValidationError("Manifest resources do not match the published-kit resource contract.");
234
+ }
235
+ }
236
+
237
+ function validateCanonicalUrls(manifest, slug) {
238
+ let canonicalUrl;
239
+ let mcpUrl;
240
+ try {
241
+ canonicalUrl = new URL(manifest.canonicalUrl);
242
+ mcpUrl = new URL(manifest.mcpUrl);
243
+ } catch {
244
+ throw new KitValidationError("Manifest canonicalUrl and mcpUrl must be valid absolute URLs.");
245
+ }
246
+ const canonicalPath = canonicalUrl.pathname.replace(/\/$/, "");
247
+ if (canonicalPath !== `/${slug}` || canonicalUrl.search || canonicalUrl.hash) {
248
+ throw new KitValidationError("Manifest canonicalUrl does not match brandSlug.");
249
+ }
250
+ if (mcpUrl.origin !== canonicalUrl.origin || mcpUrl.pathname !== `/${slug}/mcp` || mcpUrl.search || mcpUrl.hash) {
251
+ throw new KitValidationError("Manifest mcpUrl does not match canonicalUrl.");
252
+ }
253
+ }
254
+
255
+ function validateRelativeAssetPath(value, id) {
256
+ const assetPath = requiredString(value, `Asset ${id} path`);
257
+ if (!assetPath.startsWith("./assets/") || assetPath.includes("..") || assetPath.includes("\\") || assetPath.includes("//")) {
258
+ throw new KitValidationError(`Asset ${id} path must be a safe version-relative assets path.`);
259
+ }
260
+ }
261
+
262
+ function parseJsonResource(content, name, validate) {
263
+ let parsed;
264
+ try {
265
+ parsed = JSON.parse(content);
266
+ } catch {
267
+ throw new KitValidationError(`${name} must contain valid JSON.`);
268
+ }
269
+ return validate(parsed);
270
+ }
271
+
272
+ function isSupportedSchemaVersion(value) {
273
+ return value === legacyKitSchemaVersion || value === kitSchemaVersion;
274
+ }
275
+
276
+ function isSafePublishedAssetPath(resourcePath) {
277
+ return typeof resourcePath === "string"
278
+ && /^assets\/[a-zA-Z0-9][a-zA-Z0-9._/-]*$/.test(resourcePath)
279
+ && !resourcePath.includes("..")
280
+ && !resourcePath.includes("//");
281
+ }
282
+
283
+ function normalizePublishedFile(content, resourcePath) {
284
+ if (typeof content === "string") return content;
285
+ assertPlainObject(content, `${resourcePath} must be a UTF-8 string or base64 file descriptor.`);
286
+ if (content.encoding !== "base64" || typeof content.content !== "string" || !isCanonicalBase64(content.content)) {
287
+ throw new KitValidationError(`${resourcePath} must use a valid base64 file descriptor.`);
288
+ }
289
+ const mediaType = requiredString(content.mediaType, `${resourcePath} mediaType`);
290
+ if (!["image/png", "image/svg+xml", "application/octet-stream"].includes(mediaType)) {
291
+ throw new KitValidationError(`${resourcePath} has an unsupported binary media type.`);
292
+ }
293
+ return { encoding: "base64", content: content.content, mediaType };
294
+ }
295
+
296
+ function isCanonicalBase64(value) {
297
+ if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) return false;
298
+ return Buffer.from(value, "base64").toString("base64") === value;
299
+ }
300
+
301
+ function publishedFileBytes(value) {
302
+ return typeof value === "string" ? Buffer.from(value, "utf8") : Buffer.from(value.content, "base64");
303
+ }
304
+
305
+ function assertPlainObject(value, message) {
306
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new KitValidationError(message);
307
+ }
308
+
309
+ function requiredString(value, label, { allowEmpty = false } = {}) {
310
+ if (typeof value !== "string" || (!allowEmpty && !value.trim())) throw new KitValidationError(`${label} is required.`);
311
+ return value.trim();
312
+ }
313
+
314
+ function validateIsoTimestamp(value, label) {
315
+ if (typeof value !== "string" || !value || Number.isNaN(Date.parse(value))) {
316
+ throw new KitValidationError(`${label} must be an ISO timestamp.`);
317
+ }
318
+ }