codex-plugin-doctor 1.58.0 → 1.60.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.
@@ -0,0 +1,61 @@
1
+ const sources = Object.freeze([
2
+ "https://developers.openai.com/plugins/build/plugins",
3
+ "https://developers.openai.com/plugins/deploy/submission-errors"
4
+ ]);
5
+ const portalRules = Object.freeze([
6
+ Object.freeze({ id: "plugin.submission.archive.plugin_name_mismatch", status: "unavailable", reason: "Requires the previously published plugin identity.", source: "submission-errors" }),
7
+ Object.freeze({ id: "plugin.submission.archive.plugin_version_unchanged", status: "unavailable", reason: "Requires the previously published plugin version.", source: "submission-errors" }),
8
+ Object.freeze({ id: "plugin.submission.archive.manifest_normalized", status: "unavailable", reason: "The portal owns exact manifest normalization.", source: "submission-errors" }),
9
+ Object.freeze({ id: "plugin.submission.archive.developer_name_defaulted", status: "unavailable", reason: "Requires the selected verified developer identity.", source: "submission-errors" }),
10
+ Object.freeze({ id: "plugin.submission.archive.path_length", status: "unavailable", reason: "The public reference does not publish a numeric path-length limit.", source: "submission-errors" }),
11
+ Object.freeze({ id: "plugin.submission.archive.normalization_algorithm", status: "unavailable", reason: "The portal does not publish its case and Unicode collision algorithm.", source: "submission-errors" }),
12
+ Object.freeze({ id: "plugin.submission.archive.claude_format_normalized", status: "manual", reason: "The portal owns Claude-format normalization details.", source: "submission-errors" })
13
+ ]);
14
+ export const submissionArchiveRuleset = Object.freeze({
15
+ version: "openai-directory-archive-2026-08-23",
16
+ reviewedAt: "2026-08-23",
17
+ sources,
18
+ limits: Object.freeze({
19
+ compressedBytes: 100 * 1000 * 1000,
20
+ entries: 5_000,
21
+ memberBytes: 100 * 1024 * 1024,
22
+ totalBytes: 512 * 1024 * 1024
23
+ }),
24
+ structures: Object.freeze({
25
+ eocdComments: "automatic",
26
+ zip64: "automatic",
27
+ dataDescriptors: "automatic"
28
+ }),
29
+ compression: Object.freeze({
30
+ stored: "automatic",
31
+ deflate: "automatic"
32
+ }),
33
+ collisionAlgorithm: "NFKC + toLowerCase per segment",
34
+ automaticChecks: Object.freeze([
35
+ "plugin.submission.archive.invalid_file",
36
+ "plugin.submission.archive.invalid_zip",
37
+ "plugin.submission.archive.too_large",
38
+ "plugin.submission.archive.entry_count",
39
+ "plugin.submission.archive.member_too_large",
40
+ "plugin.submission.archive.total_too_large",
41
+ "plugin.submission.archive.encrypted",
42
+ "plugin.submission.archive.header_mismatch",
43
+ "plugin.submission.archive.descriptor_invalid",
44
+ "plugin.submission.archive.range_invalid",
45
+ "plugin.submission.archive.crc_mismatch",
46
+ "plugin.submission.archive.path_invalid",
47
+ "plugin.submission.archive.path_duplicate",
48
+ "plugin.submission.archive.path_conflict",
49
+ "plugin.submission.archive.type_unsupported",
50
+ "plugin.submission.archive.normalization_collision",
51
+ "plugin.submission.archive.root_missing",
52
+ "plugin.submission.archive.root_ambiguous",
53
+ "plugin.submission.archive.root_siblings",
54
+ "plugin.submission.archive.manifest_missing",
55
+ "plugin.submission.archive.skill_missing",
56
+ "plugin.submission.archive.mcp_excluded",
57
+ "plugin.submission.archive.app_excluded",
58
+ "plugin.submission.archive.screenshot_excluded"
59
+ ]),
60
+ portalRules
61
+ });
@@ -0,0 +1,8 @@
1
+ import type { DiscoveredPackage, PluginManifest } from "../domain/types.js";
2
+ import { type SubmissionPackageReader } from "./submission-package-reader.js";
3
+ import type { SubmissionFinding } from "./submission-preflight.js";
4
+ export interface SubmissionAssetResult {
5
+ findings: SubmissionFinding[];
6
+ }
7
+ export declare function validateSubmissionAssetsFromReader(manifest: PluginManifest, reader: SubmissionPackageReader): Promise<SubmissionAssetResult>;
8
+ export declare function validateSubmissionAssets(discoveredPackage: DiscoveredPackage): Promise<SubmissionAssetResult>;
@@ -0,0 +1,451 @@
1
+ import path from "node:path";
2
+ import { inflateSync } from "node:zlib";
3
+ import { XMLParser, XMLValidator } from "fast-xml-parser";
4
+ import { createDirectorySubmissionPackageReader } from "./submission-package-reader.js";
5
+ const maxAssetBytes = 5 * 1024 * 1024;
6
+ const MAX_DECODED_PNG_BYTES = 72 * 1024 * 1024;
7
+ const minimumDimension = 48;
8
+ const maximumDimension = 4096;
9
+ const assetFields = ["logo", "composerIcon"];
10
+ const extensions = new Map([[".png", "png"], [".jpg", "jpeg"], [".jpeg", "jpeg"], [".webp", "webp"], [".svg", "svg"]]);
11
+ const sofMarkers = new Set([0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf]);
12
+ const numeric = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
13
+ function finding(id, message, evidence) {
14
+ return { id, severity: "fail", message, evidence };
15
+ }
16
+ function assetEvidence(field, packagePath, format, dimensions) {
17
+ return {
18
+ field,
19
+ ...(packagePath === undefined ? {} : { path: packagePath }),
20
+ ...(format === undefined ? {} : { format }),
21
+ ...(dimensions === undefined ? {} : dimensions)
22
+ };
23
+ }
24
+ function crc32(buffer) {
25
+ let crc = 0xffffffff;
26
+ for (const byte of buffer) {
27
+ crc ^= byte;
28
+ for (let bit = 0; bit < 8; bit += 1) {
29
+ crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
30
+ }
31
+ }
32
+ return (crc ^ 0xffffffff) >>> 0;
33
+ }
34
+ function pngScanlines(width, height, bitDepth, channels, interlace) {
35
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1
36
+ || width > maximumDimension || height > maximumDimension) {
37
+ return null;
38
+ }
39
+ const bytesPerRow = (pixels) => Math.ceil((pixels * bitDepth * channels) / 8);
40
+ if (interlace === 0)
41
+ return Array.from({ length: height }, () => bytesPerRow(width));
42
+ if (interlace !== 1)
43
+ return null;
44
+ const passes = [[0, 0, 8, 8], [4, 0, 8, 8], [0, 4, 4, 8], [2, 0, 4, 4], [0, 2, 2, 4], [1, 0, 2, 2], [0, 1, 1, 2]];
45
+ const rows = [];
46
+ for (const [left, top, horizontal, vertical] of passes) {
47
+ const passWidth = width <= left ? 0 : Math.ceil((width - left) / horizontal);
48
+ const passHeight = height <= top ? 0 : Math.ceil((height - top) / vertical);
49
+ for (let row = 0; row < passHeight; row += 1)
50
+ rows.push(bytesPerRow(passWidth));
51
+ }
52
+ return rows;
53
+ }
54
+ function validPngEncoding(bitDepth, colorType, compression, filter, interlace) {
55
+ const supportedBitDepths = {
56
+ 0: [1, 2, 4, 8, 16], 2: [8, 16], 3: [1, 2, 4, 8], 4: [8, 16], 6: [8, 16]
57
+ };
58
+ return compression === 0 && filter === 0 && (interlace === 0 || interlace === 1)
59
+ && supportedBitDepths[colorType]?.includes(bitDepth) === true;
60
+ }
61
+ function readPng(buffer) {
62
+ if (buffer.length < 33 || ![137, 80, 78, 71, 13, 10, 26, 10].every((byte, index) => buffer[index] === byte)) {
63
+ return null;
64
+ }
65
+ const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
66
+ let dimensions = null;
67
+ let scanlines = null;
68
+ const idatParts = [];
69
+ let idatBytes = 0;
70
+ let offset = 8;
71
+ for (let chunks = 0; chunks < 128 && offset + 12 <= buffer.length; chunks += 1) {
72
+ const length = view.getUint32(offset);
73
+ const typeOffset = offset + 4;
74
+ const dataOffset = offset + 8;
75
+ if (length > buffer.length - dataOffset - 4)
76
+ return null;
77
+ const dataEnd = dataOffset + length;
78
+ if (crc32(buffer.slice(typeOffset, dataEnd)) !== view.getUint32(dataEnd))
79
+ return null;
80
+ const type = fourCc(buffer, typeOffset);
81
+ if (offset === 8) {
82
+ if (type !== "IHDR" || length !== 13)
83
+ return null;
84
+ dimensions = { width: view.getUint32(dataOffset), height: view.getUint32(dataOffset + 4) };
85
+ const bitDepth = buffer[dataOffset + 8];
86
+ const colorType = buffer[dataOffset + 9];
87
+ if (!validPngEncoding(bitDepth, colorType, buffer[dataOffset + 10], buffer[dataOffset + 11], buffer[dataOffset + 12]))
88
+ return null;
89
+ const channels = colorType === 0 || colorType === 3 ? 1 : colorType === 2 ? 3 : colorType === 4 ? 2 : 4;
90
+ scanlines = pngScanlines(dimensions.width, dimensions.height, bitDepth, channels, buffer[dataOffset + 12]);
91
+ if (scanlines === null)
92
+ return null;
93
+ }
94
+ else if (type === "IDAT") {
95
+ idatParts.push(buffer.slice(dataOffset, dataEnd));
96
+ idatBytes += length;
97
+ }
98
+ else if (type === "IEND") {
99
+ if (length !== 0 || dimensions === null || scanlines === null || idatBytes === 0 || dataEnd + 4 !== buffer.length)
100
+ return null;
101
+ const expectedBytes = scanlines.reduce((total, rowBytes) => total + rowBytes + 1, 0);
102
+ if (expectedBytes > MAX_DECODED_PNG_BYTES)
103
+ return null;
104
+ const idat = new Uint8Array(idatBytes);
105
+ let idatOffset = 0;
106
+ for (const part of idatParts) {
107
+ idat.set(part, idatOffset);
108
+ idatOffset += part.length;
109
+ }
110
+ try {
111
+ const output = inflateSync(idat, { maxOutputLength: expectedBytes });
112
+ if (output.length !== expectedBytes)
113
+ return null;
114
+ let outputOffset = 0;
115
+ for (const rowBytes of scanlines) {
116
+ if (output[outputOffset] > 4)
117
+ return null;
118
+ outputOffset += rowBytes + 1;
119
+ }
120
+ return outputOffset === output.length ? dimensions : null;
121
+ }
122
+ catch {
123
+ return null;
124
+ }
125
+ }
126
+ offset = dataEnd + 4;
127
+ }
128
+ return null;
129
+ }
130
+ function readJpeg(buffer) {
131
+ if (buffer.length < 4 || buffer[0] !== 0xff || buffer[1] !== 0xd8) {
132
+ return null;
133
+ }
134
+ const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
135
+ let dimensions = null;
136
+ let scans = 0;
137
+ let offset = 2;
138
+ for (let steps = 0; steps < 512 && offset < buffer.length; steps += 1) {
139
+ if (buffer[offset] !== 0xff)
140
+ return null;
141
+ while (offset < buffer.length && buffer[offset] === 0xff) {
142
+ offset += 1;
143
+ }
144
+ if (offset >= buffer.length) {
145
+ return null;
146
+ }
147
+ const marker = buffer[offset++];
148
+ if (marker === 0xd9) {
149
+ return dimensions !== null && scans > 0 && offset === buffer.length ? dimensions : null;
150
+ }
151
+ if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) {
152
+ continue;
153
+ }
154
+ if (offset + 2 > buffer.length) {
155
+ return null;
156
+ }
157
+ const length = view.getUint16(offset);
158
+ if (length < 2 || offset + length > buffer.length) {
159
+ return null;
160
+ }
161
+ if (sofMarkers.has(marker)) {
162
+ const start = offset + 2;
163
+ if (start + 5 > offset + length) {
164
+ return null;
165
+ }
166
+ dimensions = { height: view.getUint16(start + 1), width: view.getUint16(start + 3) };
167
+ }
168
+ if (marker === 0xda) {
169
+ if (dimensions === null)
170
+ return null;
171
+ const componentCount = buffer[offset + 2];
172
+ if (componentCount < 1 || length !== 6 + (componentCount * 2))
173
+ return null;
174
+ scans += 1;
175
+ offset += length;
176
+ for (let entropySteps = 0; entropySteps < buffer.length && offset < buffer.length; entropySteps += 1) {
177
+ if (buffer[offset] !== 0xff) {
178
+ offset += 1;
179
+ continue;
180
+ }
181
+ if (offset + 1 >= buffer.length)
182
+ return null;
183
+ const entropyMarker = buffer[offset + 1];
184
+ if (entropyMarker === 0x00 || (entropyMarker >= 0xd0 && entropyMarker <= 0xd7)) {
185
+ offset += 2;
186
+ continue;
187
+ }
188
+ break;
189
+ }
190
+ continue;
191
+ }
192
+ offset += length;
193
+ }
194
+ return null;
195
+ }
196
+ function fourCc(buffer, offset) {
197
+ return String.fromCharCode(...buffer.slice(offset, offset + 4));
198
+ }
199
+ function readWebp(buffer) {
200
+ if (buffer.length < 20 || fourCc(buffer, 0) !== "RIFF" || fourCc(buffer, 8) !== "WEBP") {
201
+ return null;
202
+ }
203
+ const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
204
+ const riffSize = view.getUint32(4, true);
205
+ if (riffSize !== buffer.length - 8) {
206
+ return null;
207
+ }
208
+ let dimensions = null;
209
+ let offset = 12;
210
+ for (let chunks = 0; chunks < 128 && offset + 8 <= buffer.length; chunks += 1) {
211
+ const chunkType = fourCc(buffer, offset);
212
+ const chunkLength = view.getUint32(offset + 4, true);
213
+ const payload = offset + 8;
214
+ const paddedLength = chunkLength + (chunkLength % 2);
215
+ if (paddedLength > buffer.length - payload)
216
+ return null;
217
+ let detected = null;
218
+ if (chunkType === "VP8X") {
219
+ if (chunkLength < 10)
220
+ return null;
221
+ detected = {
222
+ width: 1 + buffer[payload + 4] + (buffer[payload + 5] << 8) + (buffer[payload + 6] << 16),
223
+ height: 1 + buffer[payload + 7] + (buffer[payload + 8] << 8) + (buffer[payload + 9] << 16)
224
+ };
225
+ }
226
+ else if (chunkType === "VP8L") {
227
+ if (chunkLength < 5 || buffer[payload] !== 0x2f)
228
+ return null;
229
+ const packed = view.getUint32(payload + 1, true);
230
+ detected = { width: (packed & 0x3fff) + 1, height: ((packed >>> 14) & 0x3fff) + 1 };
231
+ }
232
+ else if (chunkType === "VP8 ") {
233
+ if (chunkLength < 10 || buffer[payload + 3] !== 0x9d || buffer[payload + 4] !== 0x01 || buffer[payload + 5] !== 0x2a)
234
+ return null;
235
+ detected = { width: view.getUint16(payload + 6, true) & 0x3fff, height: view.getUint16(payload + 8, true) & 0x3fff };
236
+ }
237
+ dimensions ??= detected;
238
+ offset = payload + paddedLength;
239
+ }
240
+ return offset === buffer.length ? dimensions : null;
241
+ }
242
+ function rasterAsset(buffer) {
243
+ const candidates = [
244
+ ["png", readPng], ["jpeg", readJpeg], ["webp", readWebp]
245
+ ];
246
+ for (const [format, reader] of candidates) {
247
+ const dimensions = reader(buffer);
248
+ if (dimensions !== null)
249
+ return { format, dimensions };
250
+ }
251
+ return null;
252
+ }
253
+ function isRecord(value) {
254
+ return typeof value === "object" && value !== null && !Array.isArray(value);
255
+ }
256
+ function numberFrom(value) {
257
+ if (typeof value !== "string" || !numeric.test(value))
258
+ return null;
259
+ const number = Number(value);
260
+ return Number.isFinite(number) ? number : null;
261
+ }
262
+ function normalizeCssEscapes(source) {
263
+ if (source.length > maxAssetBytes)
264
+ return null;
265
+ let normalized = "";
266
+ for (let index = 0; index < source.length; index += 1) {
267
+ if (source[index] !== "\\") {
268
+ normalized += source[index];
269
+ continue;
270
+ }
271
+ index += 1;
272
+ if (index >= source.length)
273
+ return null;
274
+ if (/[0-9A-Fa-f]/u.test(source[index])) {
275
+ let digits = "";
276
+ while (digits.length < 6 && index < source.length && /[0-9A-Fa-f]/u.test(source[index])) {
277
+ digits += source[index++];
278
+ }
279
+ const codePoint = Number.parseInt(digits, 16);
280
+ if (codePoint === 0 || codePoint > 0x10ffff || (codePoint >= 0xd800 && codePoint <= 0xdfff))
281
+ return null;
282
+ normalized += String.fromCodePoint(codePoint);
283
+ if (source[index] === "\r" && source[index + 1] === "\n")
284
+ index += 1;
285
+ else if (/[ \t\r\n\f]/u.test(source[index] ?? ""))
286
+ index += 1;
287
+ index -= 1;
288
+ continue;
289
+ }
290
+ normalized += source[index];
291
+ }
292
+ return normalized;
293
+ }
294
+ function isSafeCss(source) {
295
+ const css = normalizeCssEscapes(source);
296
+ if (css === null || /@\s*import\b/iu.test(css))
297
+ return false;
298
+ for (const match of css.matchAll(/url\s*\(\s*(?:(["'])(.*?)\1|([^)]*))\s*\)/giu)) {
299
+ if (!/^#[^\s]*$/u.test((match[2] ?? match[3] ?? "").trim()))
300
+ return false;
301
+ }
302
+ return true;
303
+ }
304
+ function inspectSvgValue(value, context = undefined) {
305
+ if (typeof value === "string") {
306
+ if (context === "css")
307
+ return isSafeCss(value);
308
+ return context !== "href" || value.trim() === "" || /^#[^\s]*$/u.test(value.trim());
309
+ }
310
+ if (Array.isArray(value))
311
+ return value.every((item) => inspectSvgValue(item, context));
312
+ if (!isRecord(value))
313
+ return true;
314
+ return Object.entries(value).every(([key, item]) => {
315
+ const childContext = context === "css" || key === "style" || key === "@_style"
316
+ ? "css"
317
+ : key === "@_href" || key === "@_xlink:href" ? "href" : undefined;
318
+ return inspectSvgValue(item, childContext);
319
+ });
320
+ }
321
+ function svgDimensions(content) {
322
+ if (/<!DOCTYPE|<!ENTITY/iu.test(content)) {
323
+ return null;
324
+ }
325
+ if (XMLValidator.validate(content) !== true)
326
+ return null;
327
+ try {
328
+ const parsed = new XMLParser({ ignoreAttributes: false, processEntities: false }).parse(content);
329
+ if (!isRecord(parsed) || !isRecord(parsed.svg))
330
+ return null;
331
+ const root = parsed.svg;
332
+ if (!inspectSvgValue(root))
333
+ return null;
334
+ const viewBox = root["@_viewBox"];
335
+ if (typeof viewBox === "string") {
336
+ const parts = viewBox.trim().split(/[\s,]+/u).map(numberFrom);
337
+ if (parts.length !== 4 || parts.some((part) => part === null))
338
+ return null;
339
+ return { width: parts[2], height: parts[3] };
340
+ }
341
+ const width = numberFrom(root["@_width"]);
342
+ const height = numberFrom(root["@_height"]);
343
+ return width === null || height === null ? null : { width, height };
344
+ }
345
+ catch {
346
+ return null;
347
+ }
348
+ }
349
+ function dimensionFinding(field, packagePath, format, dimensions) {
350
+ const evidence = assetEvidence(field, packagePath, format, dimensions);
351
+ if (!Number.isInteger(dimensions.width) || !Number.isInteger(dimensions.height) || dimensions.width <= 0 || dimensions.height <= 0) {
352
+ return finding("plugin.submission.asset.dimensions", "Asset dimensions must be positive integers.", evidence);
353
+ }
354
+ if (dimensions.width !== dimensions.height) {
355
+ return finding("plugin.submission.asset.not_square", "Asset dimensions must be square.", evidence);
356
+ }
357
+ if (dimensions.width < minimumDimension || dimensions.width > maximumDimension) {
358
+ return finding("plugin.submission.asset.dimensions", "Asset dimensions are outside the allowed range.", { ...evidence, limit: minimumDimension });
359
+ }
360
+ return null;
361
+ }
362
+ function assetPackagePath(value) {
363
+ if (!value.startsWith("./"))
364
+ return null;
365
+ const packagePath = value.slice(2);
366
+ if (packagePath === ""
367
+ || packagePath.startsWith("/")
368
+ || /^[a-zA-Z]:/u.test(packagePath)
369
+ || packagePath.includes("\\")
370
+ || /[\u0000-\u001F\u007F]/u.test(packagePath)) {
371
+ return null;
372
+ }
373
+ const normalizedPackagePath = path.posix.normalize(packagePath);
374
+ if (normalizedPackagePath === "."
375
+ || normalizedPackagePath === ""
376
+ || normalizedPackagePath === ".."
377
+ || normalizedPackagePath.startsWith("../")
378
+ || path.posix.isAbsolute(normalizedPackagePath))
379
+ return null;
380
+ return normalizedPackagePath;
381
+ }
382
+ async function validateAsset(reader, field, value) {
383
+ if (value === undefined) {
384
+ return [finding("plugin.submission.asset.required", "Branding asset is required.", assetEvidence(field))];
385
+ }
386
+ if (typeof value !== "string") {
387
+ return [finding("plugin.submission.asset.invalid_path", "Branding asset path is invalid.", assetEvidence(field))];
388
+ }
389
+ const packagePath = assetPackagePath(value);
390
+ if (packagePath === null) {
391
+ return [finding("plugin.submission.asset.invalid_path", "Branding asset path is invalid.", assetEvidence(field))];
392
+ }
393
+ const extension = extensions.get(path.extname(packagePath).toLowerCase());
394
+ if (extension === undefined) {
395
+ return [finding("plugin.submission.asset.unsupported_format", "Branding asset format is unsupported.", assetEvidence(field, packagePath))];
396
+ }
397
+ const details = await reader.stat(packagePath).catch(() => null);
398
+ if (details === null) {
399
+ return [finding("plugin.submission.asset.missing", "Branding asset is missing.", assetEvidence(field, packagePath, extension))];
400
+ }
401
+ if (details.safeResolution === "outside") {
402
+ return [finding("plugin.submission.asset.invalid_path", "Branding asset path is invalid.", assetEvidence(field))];
403
+ }
404
+ if (details.safeResolution !== "safe") {
405
+ return [finding("plugin.submission.asset.missing", "Branding asset is missing.", assetEvidence(field, packagePath, extension))];
406
+ }
407
+ if (details.resolvedKind !== "file") {
408
+ return [finding("plugin.submission.asset.unsupported_format", "Branding asset must be a regular file.", assetEvidence(field, packagePath, extension))];
409
+ }
410
+ if (details.size > maxAssetBytes) {
411
+ return [finding("plugin.submission.asset.too_large", "Branding asset exceeds the size limit.", { ...assetEvidence(field, packagePath, extension), limit: maxAssetBytes })];
412
+ }
413
+ const content = await reader.read(packagePath, maxAssetBytes).catch(() => null);
414
+ if (content === null) {
415
+ return [finding("plugin.submission.asset.missing", "Branding asset cannot be read.", assetEvidence(field, packagePath, extension))];
416
+ }
417
+ if (extension === "svg") {
418
+ let source;
419
+ try {
420
+ source = new TextDecoder("utf-8", { fatal: true }).decode(content);
421
+ }
422
+ catch {
423
+ return [finding("plugin.submission.asset.unsafe_svg", "SVG must be valid UTF-8.", assetEvidence(field, packagePath, extension))];
424
+ }
425
+ const dimensions = svgDimensions(source);
426
+ if (dimensions === null) {
427
+ return [finding("plugin.submission.asset.unsafe_svg", "SVG is unsafe or lacks valid dimensions.", assetEvidence(field, packagePath, extension))];
428
+ }
429
+ const invalidDimensions = dimensionFinding(field, packagePath, extension, dimensions);
430
+ return invalidDimensions === null ? [] : [invalidDimensions];
431
+ }
432
+ const decoded = rasterAsset(content);
433
+ if (decoded === null) {
434
+ return [finding("plugin.submission.asset.decode_failed", "Branding asset could not be decoded.", assetEvidence(field, packagePath, extension))];
435
+ }
436
+ if (decoded.format !== extension) {
437
+ return [finding("plugin.submission.asset.extension_mismatch", "Branding asset extension does not match its content.", assetEvidence(field, packagePath, decoded.format, decoded.dimensions))];
438
+ }
439
+ const invalidDimensions = dimensionFinding(field, packagePath, decoded.format, decoded.dimensions);
440
+ return invalidDimensions === null ? [] : [invalidDimensions];
441
+ }
442
+ export async function validateSubmissionAssetsFromReader(manifest, reader) {
443
+ const interfaceValues = isRecord(manifest.interface) ? manifest.interface : {};
444
+ const findings = [];
445
+ for (const field of assetFields)
446
+ findings.push(...await validateAsset(reader, field, interfaceValues[field]));
447
+ return { findings };
448
+ }
449
+ export async function validateSubmissionAssets(discoveredPackage) {
450
+ return validateSubmissionAssetsFromReader(discoveredPackage.manifest, createDirectorySubmissionPackageReader(discoveredPackage.rootPath));
451
+ }
@@ -0,0 +1,15 @@
1
+ export type SubmissionPackageEntryKind = "file" | "directory" | "symlink" | "other";
2
+ export interface SubmissionPackageEntry {
3
+ packagePath: string;
4
+ kind: SubmissionPackageEntryKind;
5
+ resolvedKind: Exclude<SubmissionPackageEntryKind, "symlink"> | null;
6
+ resolvedPackagePath?: string;
7
+ size: number;
8
+ safeResolution: "safe" | "outside" | "unavailable";
9
+ }
10
+ export interface SubmissionPackageReader {
11
+ list(directory: string): Promise<readonly SubmissionPackageEntry[]>;
12
+ stat(packagePath: string): Promise<SubmissionPackageEntry | null>;
13
+ read(packagePath: string, maxBytes: number): Promise<Uint8Array | null>;
14
+ }
15
+ export declare function createDirectorySubmissionPackageReader(rootPath: string): SubmissionPackageReader;