@agentxm/extension-content 0.38.0 → 0.39.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/dist/src/content/frontmatter.js +9 -22
- package/dist/src/content/skill-content.d.ts +14 -0
- package/dist/src/content/skill-content.js +24 -0
- package/dist/src/content/subagent-content.d.ts +70 -46
- package/dist/src/content/subagent-content.js +273 -85
- package/dist/src/index.d.ts +4 -4
- package/dist/src/index.js +4 -4
- package/dist/src/knowledge/package-inspection.d.ts +7 -7
- package/dist/src/lint/catalog/hook/entrypoint-exists.js +18 -15
- package/dist/src/lint/catalog/hook/envelope.d.ts +14 -1
- package/dist/src/lint/catalog/hook/envelope.js +25 -2
- package/dist/src/lint/catalog/hook/native-bindings-supported.d.ts +4 -0
- package/dist/src/lint/catalog/hook/native-bindings-supported.js +39 -0
- package/dist/src/lint/catalog/hook.js +2 -4
- package/dist/src/lint/catalog/skill-accessor/contexts.d.ts +1 -0
- package/dist/src/lint/catalog/skill-accessor/contexts.js +3 -0
- package/dist/src/lint/catalog/skill.d.ts +1 -1
- package/dist/src/lint/catalog/skill.js +20 -8
- package/dist/src/lint/catalog/subagent/envelope.js +1 -1
- package/dist/src/lint/catalog-metadata.js +3 -9
- package/dist/src/lint/context.d.ts +2 -0
- package/dist/src/packaging/archive-guardrails.d.ts +1 -0
- package/dist/src/packaging/archive-guardrails.js +205 -11
- package/dist/src/packaging/filtered-package-validation.js +24 -31
- package/dist/src/packaging/input-normalization.d.ts +0 -1
- package/dist/src/packaging/input-normalization.js +1 -77
- package/dist/src/packaging/manifest-file.js +1 -1
- package/dist/src/packaging/manifest-policy.d.ts +239 -109
- package/dist/src/packaging/manifest-policy.js +1 -1
- package/package.json +4 -2
- package/dist/src/lint/catalog/hook/decision-portability.d.ts +0 -4
- package/dist/src/lint/catalog/hook/decision-portability.js +0 -40
- package/dist/src/lint/catalog/hook/matcher-raw-portability.d.ts +0 -4
- package/dist/src/lint/catalog/hook/matcher-raw-portability.js +0 -51
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { inflateRawSync } from "node:zlib";
|
|
1
2
|
import * as Data from "effect/Data";
|
|
2
3
|
import * as Effect from "effect/Effect";
|
|
3
4
|
export class ArchiveGuardrailError extends Data.TaggedError("ArchiveGuardrailError") {
|
|
@@ -8,6 +9,8 @@ export const ZIP_LOCAL_SIGNATURE = 0x04034b50;
|
|
|
8
9
|
const COMPRESSION_STORE = 0;
|
|
9
10
|
const COMPRESSION_DEFLATE = 8;
|
|
10
11
|
const SUPPORTED_COMPRESSION = new Set([COMPRESSION_STORE, COMPRESSION_DEFLATE]);
|
|
12
|
+
// Bound target decoding and segment allocation separately from ordinary payload files.
|
|
13
|
+
const MAX_LINK_TARGET_BYTES = 64 * 1024;
|
|
11
14
|
const S_IFLNK = 0xa000;
|
|
12
15
|
const S_IFMT = 0xf000;
|
|
13
16
|
const textDecoder = new TextDecoder();
|
|
@@ -98,10 +101,92 @@ export const parseZipCentralDirectory = (buf) => Effect.gen(function* () {
|
|
|
98
101
|
}
|
|
99
102
|
return entries;
|
|
100
103
|
});
|
|
104
|
+
const LOCAL_FILE_HEADER_SIZE = 30;
|
|
105
|
+
const INFLATE_CHUNK_SIZE = 16 * 1024;
|
|
106
|
+
export const defaultReadEntry = (archiveBytes, entry) => Effect.gen(function* () {
|
|
107
|
+
const { localHeaderOffset, compressedSize, compressionMethod, fileName } = entry;
|
|
108
|
+
if (localHeaderOffset + LOCAL_FILE_HEADER_SIZE > archiveBytes.length) {
|
|
109
|
+
return yield* new ArchiveGuardrailError({
|
|
110
|
+
code: "malformed_archive",
|
|
111
|
+
message: `Local file header for entry "${fileName}" exceeds archive bounds.`,
|
|
112
|
+
entry: fileName,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const view = new DataView(archiveBytes.buffer, archiveBytes.byteOffset, archiveBytes.byteLength);
|
|
116
|
+
const signature = view.getUint32(localHeaderOffset, true);
|
|
117
|
+
if (signature !== ZIP_LOCAL_SIGNATURE) {
|
|
118
|
+
return yield* new ArchiveGuardrailError({
|
|
119
|
+
code: "malformed_archive",
|
|
120
|
+
message: `Invalid local file header signature for entry "${fileName}".`,
|
|
121
|
+
entry: fileName,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
const fileNameLength = view.getUint16(localHeaderOffset + 26, true);
|
|
125
|
+
const extraFieldLength = view.getUint16(localHeaderOffset + 28, true);
|
|
126
|
+
const dataStart = localHeaderOffset + LOCAL_FILE_HEADER_SIZE + fileNameLength + extraFieldLength;
|
|
127
|
+
if (dataStart + compressedSize > archiveBytes.length) {
|
|
128
|
+
return yield* new ArchiveGuardrailError({
|
|
129
|
+
code: "malformed_archive",
|
|
130
|
+
message: `Compressed data for entry "${fileName}" exceeds archive bounds.`,
|
|
131
|
+
entry: fileName,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
const compressedData = archiveBytes.slice(dataStart, dataStart + compressedSize);
|
|
135
|
+
if (compressionMethod === 0) {
|
|
136
|
+
if (compressedData.byteLength !== entry.uncompressedSize) {
|
|
137
|
+
return yield* new ArchiveGuardrailError({
|
|
138
|
+
code: "malformed_archive",
|
|
139
|
+
message: `Stored entry size does not match its declaration: "${fileName}".`,
|
|
140
|
+
entry: fileName,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return compressedData;
|
|
144
|
+
}
|
|
145
|
+
if (compressionMethod === 8) {
|
|
146
|
+
const expansionFailure = () => new ArchiveGuardrailError({
|
|
147
|
+
code: "decompression_limit_exceeded",
|
|
148
|
+
message: `Entry "${fileName}" decompresses beyond its declared size of ${entry.uncompressedSize} bytes.`,
|
|
149
|
+
entry: fileName,
|
|
150
|
+
});
|
|
151
|
+
// workerd grows its bounded zlib buffer by whole chunks, including the
|
|
152
|
+
// final partial chunk. Reserve one chunk of codec headroom, then enforce
|
|
153
|
+
// the exact declared size below. Expansion remains bounded during decode.
|
|
154
|
+
const result = yield* Effect.try({
|
|
155
|
+
try: () => inflateRawSync(compressedData, {
|
|
156
|
+
chunkSize: INFLATE_CHUNK_SIZE,
|
|
157
|
+
maxOutputLength: entry.uncompressedSize + INFLATE_CHUNK_SIZE,
|
|
158
|
+
}),
|
|
159
|
+
catch: (error) => {
|
|
160
|
+
const errorCode = typeof error === "object" &&
|
|
161
|
+
error !== null &&
|
|
162
|
+
"code" in error &&
|
|
163
|
+
typeof error.code === "string"
|
|
164
|
+
? error.code
|
|
165
|
+
: "";
|
|
166
|
+
return errorCode === "ERR_BUFFER_TOO_LARGE" ||
|
|
167
|
+
(error instanceof RangeError && error.message === "Memory limit exceeded")
|
|
168
|
+
? expansionFailure()
|
|
169
|
+
: new ArchiveGuardrailError({
|
|
170
|
+
code: "malformed_archive",
|
|
171
|
+
message: `Failed to decompress entry "${fileName}".`,
|
|
172
|
+
entry: fileName,
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
if (result.byteLength > entry.uncompressedSize)
|
|
177
|
+
return yield* expansionFailure();
|
|
178
|
+
return new Uint8Array(result.buffer, result.byteOffset, result.byteLength);
|
|
179
|
+
}
|
|
180
|
+
return yield* new ArchiveGuardrailError({
|
|
181
|
+
code: "unsupported_compression",
|
|
182
|
+
message: `Unsupported compression method ${compressionMethod} for entry "${fileName}".`,
|
|
183
|
+
entry: fileName,
|
|
184
|
+
});
|
|
185
|
+
});
|
|
101
186
|
const checkPathTraversal = (entries) => {
|
|
102
187
|
for (const entry of entries) {
|
|
103
188
|
const normalized = entry.fileName.replace(/\\/g, "/");
|
|
104
|
-
if (normalized.
|
|
189
|
+
if (normalized.split("/").includes("..")) {
|
|
105
190
|
return Effect.fail(new ArchiveGuardrailError({
|
|
106
191
|
code: "path_traversal",
|
|
107
192
|
message: `Archive entry contains path traversal: "${entry.fileName}".`,
|
|
@@ -127,7 +212,7 @@ const checkAbsolutePaths = (entries) => {
|
|
|
127
212
|
const checkDuplicateEntries = (entries) => {
|
|
128
213
|
const seen = new Set();
|
|
129
214
|
for (const entry of entries) {
|
|
130
|
-
const normalized = entry.fileName.toLowerCase();
|
|
215
|
+
const normalized = entry.fileName.replace(/\/$/, "").normalize("NFC").toLowerCase();
|
|
131
216
|
if (seen.has(normalized)) {
|
|
132
217
|
return Effect.fail(new ArchiveGuardrailError({
|
|
133
218
|
code: "duplicate_entry",
|
|
@@ -139,19 +224,127 @@ const checkDuplicateEntries = (entries) => {
|
|
|
139
224
|
}
|
|
140
225
|
return Effect.void;
|
|
141
226
|
};
|
|
142
|
-
|
|
227
|
+
// Normalize only for portable identity comparisons; preserve original archive bytes.
|
|
228
|
+
const memberKey = (name) => name.normalize("NFC").toLowerCase();
|
|
229
|
+
const checkMemberTopology = (entries) => Effect.gen(function* () {
|
|
230
|
+
const members = new Map();
|
|
143
231
|
for (const entry of entries) {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
232
|
+
const name = entry.fileName.replace(/\/$/, "");
|
|
233
|
+
const kind = (entry.externalAttributes >>> 16) & S_IFMT;
|
|
234
|
+
if (name.length === 0 ||
|
|
235
|
+
/[\\\0:]/.test(name) ||
|
|
236
|
+
name.split("/").some((part) => part === "" || part === "." || part === "..") ||
|
|
237
|
+
![0, 0x8000, 0x4000, S_IFLNK].includes(kind) ||
|
|
238
|
+
(entry.fileName.endsWith("/") && kind !== 0 && kind !== 0x4000) ||
|
|
239
|
+
(kind === 0x4000 && !entry.fileName.endsWith("/"))) {
|
|
240
|
+
return yield* new ArchiveGuardrailError({
|
|
241
|
+
code: "malformed_archive",
|
|
242
|
+
message: `Archive entry has an ambiguous path or unsupported file kind: "${entry.fileName}".`,
|
|
243
|
+
entry: entry.fileName,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
members.set(memberKey(name), entry);
|
|
247
|
+
}
|
|
248
|
+
for (const entry of entries) {
|
|
249
|
+
const parts = entry.fileName.replace(/\/$/, "").split("/");
|
|
250
|
+
parts.pop();
|
|
251
|
+
while (parts.length > 0) {
|
|
252
|
+
const ancestor = members.get(memberKey(parts.join("/")));
|
|
253
|
+
if (ancestor !== undefined &&
|
|
254
|
+
!ancestor.fileName.endsWith("/") &&
|
|
255
|
+
((ancestor.externalAttributes >>> 16) & S_IFMT) !== 0x4000) {
|
|
256
|
+
return yield* new ArchiveGuardrailError({
|
|
257
|
+
code: "malformed_archive",
|
|
258
|
+
message: `Archive entry is nested beneath a file or link: "${entry.fileName}".`,
|
|
259
|
+
entry: entry.fileName,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
parts.pop();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
const checkSymlinks = (archiveBytes, entries) => Effect.gen(function* () {
|
|
267
|
+
const targets = new Map();
|
|
268
|
+
for (const entry of entries) {
|
|
269
|
+
if (((entry.externalAttributes >>> 16) & S_IFMT) !== S_IFLNK)
|
|
270
|
+
continue;
|
|
271
|
+
if (entry.uncompressedSize > MAX_LINK_TARGET_BYTES) {
|
|
272
|
+
return yield* new ArchiveGuardrailError({
|
|
147
273
|
code: "symlink_entry",
|
|
148
|
-
message: `Archive contains symlink entry: "${entry.fileName}".`,
|
|
149
274
|
entry: entry.fileName,
|
|
150
|
-
|
|
275
|
+
message: `Archive link target exceeds the ${MAX_LINK_TARGET_BYTES}-byte validation limit: "${entry.fileName}".`,
|
|
276
|
+
});
|
|
151
277
|
}
|
|
278
|
+
const bytes = yield* defaultReadEntry(archiveBytes, entry);
|
|
279
|
+
const target = yield* Effect.try({
|
|
280
|
+
try: () => new TextDecoder("utf-8", { fatal: true }).decode(bytes),
|
|
281
|
+
catch: () => new ArchiveGuardrailError({
|
|
282
|
+
code: "symlink_entry",
|
|
283
|
+
message: `Archive link target is not UTF-8: "${entry.fileName}".`,
|
|
284
|
+
entry: entry.fileName,
|
|
285
|
+
}),
|
|
286
|
+
});
|
|
287
|
+
if (target.length === 0 || target.startsWith("/") || /[\\\0:]/.test(target)) {
|
|
288
|
+
return yield* new ArchiveGuardrailError({
|
|
289
|
+
code: "symlink_entry",
|
|
290
|
+
message: `Archive link target must be a relative contained path: "${entry.fileName}".`,
|
|
291
|
+
entry: entry.fileName,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
targets.set(memberKey(entry.fileName), target);
|
|
152
295
|
}
|
|
153
|
-
|
|
154
|
-
|
|
296
|
+
// Expand links before processing subsequent '..' segments, as a filesystem
|
|
297
|
+
// does. Exit markers distinguish a cycle from a repeated finite traversal.
|
|
298
|
+
// Bound total interpretation work independently of decompressed byte limits.
|
|
299
|
+
let remainingSteps = 1_000_000;
|
|
300
|
+
for (const [name, target] of targets) {
|
|
301
|
+
const resolved = name.split("/").slice(0, -1);
|
|
302
|
+
const pending = target.split("/").reverse();
|
|
303
|
+
const active = new Set([name]);
|
|
304
|
+
while (pending.length > 0) {
|
|
305
|
+
if (--remainingSteps < 0) {
|
|
306
|
+
return yield* new ArchiveGuardrailError({
|
|
307
|
+
code: "symlink_entry",
|
|
308
|
+
message: "Archive link resolution exceeds the bounded validation budget.",
|
|
309
|
+
entry: name,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
const part = pending.pop();
|
|
313
|
+
if (part === undefined)
|
|
314
|
+
break;
|
|
315
|
+
if (typeof part !== "string") {
|
|
316
|
+
active.delete(part.leave);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (part === "" || part === ".")
|
|
320
|
+
continue;
|
|
321
|
+
if (part === "..") {
|
|
322
|
+
if (resolved.length === 0) {
|
|
323
|
+
return yield* new ArchiveGuardrailError({
|
|
324
|
+
code: "symlink_entry",
|
|
325
|
+
message: `Archive link escapes its package: "${name}".`,
|
|
326
|
+
entry: name,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
resolved.pop();
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const next = memberKey([...resolved, part].join("/"));
|
|
333
|
+
const nested = targets.get(next);
|
|
334
|
+
if (nested === undefined) {
|
|
335
|
+
resolved.push(part);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
// A cycle remains inert payload; a filesystem cannot traverse beyond it.
|
|
339
|
+
if (active.has(next))
|
|
340
|
+
break;
|
|
341
|
+
active.add(next);
|
|
342
|
+
pending.push({ leave: next });
|
|
343
|
+
for (const segment of nested.split("/").reverse())
|
|
344
|
+
pending.push(segment);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
});
|
|
155
348
|
const checkCompressionMethods = (entries) => {
|
|
156
349
|
for (const entry of entries) {
|
|
157
350
|
if (!SUPPORTED_COMPRESSION.has(entry.compressionMethod)) {
|
|
@@ -240,11 +433,12 @@ export const validateArchive = (archiveBytes, limits) => Effect.gen(function* ()
|
|
|
240
433
|
yield* checkPathTraversal(entries);
|
|
241
434
|
yield* checkAbsolutePaths(entries);
|
|
242
435
|
yield* checkDuplicateEntries(entries);
|
|
243
|
-
yield* checkSymlinks(entries);
|
|
244
436
|
yield* checkCompressionMethods(entries);
|
|
245
437
|
yield* checkEntryCount(entries, appliedLimits);
|
|
246
438
|
yield* checkDecompressedSize(entries, appliedLimits);
|
|
247
439
|
yield* checkCompressionRatio(entries, appliedLimits);
|
|
440
|
+
yield* checkMemberTopology(entries);
|
|
441
|
+
yield* checkSymlinks(archiveBytes, entries);
|
|
248
442
|
return entries;
|
|
249
443
|
});
|
|
250
444
|
//# sourceMappingURL=archive-guardrails.js.map
|
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
import * as Data from "effect/Data";
|
|
2
2
|
import * as Effect from "effect/Effect";
|
|
3
|
-
import * as
|
|
3
|
+
import * as Schema from "effect/Schema";
|
|
4
|
+
import { HookManifestSchema, requiredHookRuntimeFiles, } from "@agentxm/extension-model/unstable/hooks/manifest-schema";
|
|
4
5
|
import { parseFrontmatterEffect } from "../content/frontmatter.js";
|
|
5
6
|
import { inspectKnowledgeEntries } from "../knowledge/okf.js";
|
|
6
|
-
import {
|
|
7
|
-
import { parseSubagentMd } from "../content/subagent-content.js";
|
|
7
|
+
import { readSubagentPackage } from "../content/subagent-content.js";
|
|
8
8
|
/** A type-specific package invariant failed against the filtered archive. */
|
|
9
9
|
export class FilteredPackageError extends Data.TaggedError("FilteredPackageError") {
|
|
10
10
|
}
|
|
11
11
|
const rawField = (raw, field) => typeof raw === "object" && raw !== null && !Array.isArray(raw)
|
|
12
12
|
? Reflect.get(raw, field)
|
|
13
13
|
: undefined;
|
|
14
|
-
const safeArchivePath = (value) => value.length > 0 &&
|
|
15
|
-
!value.startsWith("/") &&
|
|
16
|
-
!value.includes("\\") &&
|
|
17
|
-
!value.split("/").some((segment) => segment === "" || segment === "." || segment === "..");
|
|
18
14
|
const requireEntry = (entries, path) => {
|
|
19
15
|
const found = entries.find((entry) => entry.fileName === path);
|
|
20
16
|
return found === undefined
|
|
@@ -36,28 +32,25 @@ const readText = (args, path) => Effect.gen(function* () {
|
|
|
36
32
|
});
|
|
37
33
|
/** Validate the complete type-specific package after Registry-only filtering. */
|
|
38
34
|
export const validateFilteredPackage = (args) => Effect.gen(function* () {
|
|
39
|
-
const name = args.manifest.identity.name;
|
|
40
35
|
switch (args.type) {
|
|
41
36
|
case "skill": {
|
|
42
37
|
const path = "src/SKILL.md";
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
code: "content_invalid",
|
|
47
|
-
detail: `Filtered "${path}" must contain valid Agent Skill frontmatter whose name is "${name}".`,
|
|
48
|
-
path,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
38
|
+
// The envelope owns Registry identity. Authoring lint owns the chosen
|
|
39
|
+
// frontmatter convention; ingest must not rewrite or reject upstream metadata.
|
|
40
|
+
yield* readText(args, path);
|
|
51
41
|
return;
|
|
52
42
|
}
|
|
53
43
|
case "subagent": {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
44
|
+
yield* readSubagentPackage({
|
|
45
|
+
manifest: args.manifest.raw,
|
|
46
|
+
readFile: (path) => readText(args, path),
|
|
47
|
+
}).pipe(Effect.mapError((error) => error instanceof FilteredPackageError
|
|
48
|
+
? error
|
|
49
|
+
: new FilteredPackageError({
|
|
50
|
+
code: error.reason === "reference-invalid" ? "reference_invalid" : "content_invalid",
|
|
51
|
+
detail: error.detail,
|
|
52
|
+
...(error.source === undefined ? {} : { path: error.source }),
|
|
53
|
+
})));
|
|
61
54
|
return;
|
|
62
55
|
}
|
|
63
56
|
case "rule": {
|
|
@@ -71,14 +64,14 @@ export const validateFilteredPackage = (args) => Effect.gen(function* () {
|
|
|
71
64
|
return;
|
|
72
65
|
}
|
|
73
66
|
case "hook": {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
67
|
+
const manifest = yield* Schema.decodeUnknownEffect(HookManifestSchema)(args.manifest.raw, {
|
|
68
|
+
onExcessProperty: "error",
|
|
69
|
+
}).pipe(Effect.mapError(() => new FilteredPackageError({
|
|
70
|
+
code: "reference_invalid",
|
|
71
|
+
detail: "Filtered Hook manifest must declare valid native implementations and safe package-relative references.",
|
|
72
|
+
})));
|
|
73
|
+
for (const file of requiredHookRuntimeFiles(manifest))
|
|
74
|
+
yield* requireEntry(args.entries, file);
|
|
82
75
|
return;
|
|
83
76
|
}
|
|
84
77
|
case "knowledge": {
|
|
@@ -35,6 +35,5 @@ export interface PublishInput {
|
|
|
35
35
|
readonly clientIntegrity?: string;
|
|
36
36
|
readonly digestHeader?: string;
|
|
37
37
|
}
|
|
38
|
-
export declare const defaultReadEntry: (archiveBytes: Uint8Array, entry: ZipEntry) => Effect.Effect<Uint8Array, ArchiveGuardrailError>;
|
|
39
38
|
export declare const normalizePublishInput: (args: NormalizePublishInputArgs) => Effect.Effect<PublishInput, IngestLimitError | IngestUnsupportedContentTypeError | ArchiveGuardrailError | ManifestError | FilteredPackageError>;
|
|
40
39
|
//# sourceMappingURL=input-normalization.d.ts.map
|
|
@@ -1,84 +1,8 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
|
-
import {
|
|
3
|
-
import { ArchiveGuardrailError, ZIP_LOCAL_SIGNATURE, validateArchive, } from "./archive-guardrails.js";
|
|
2
|
+
import { ArchiveGuardrailError, defaultReadEntry, validateArchive, } from "./archive-guardrails.js";
|
|
4
3
|
import { enforceArchiveContentType, enforceArchiveSizeLimit, } from "./ingest-limits.js";
|
|
5
4
|
import { resolveManifest, validateDeclaredManifestAlignment, } from "./manifest-policy.js";
|
|
6
5
|
import { FilteredPackageError, validateFilteredPackage } from "./filtered-package-validation.js";
|
|
7
|
-
const LOCAL_FILE_HEADER_SIZE = 30;
|
|
8
|
-
const INFLATE_CHUNK_SIZE = 16 * 1024;
|
|
9
|
-
export const defaultReadEntry = (archiveBytes, entry) => Effect.gen(function* () {
|
|
10
|
-
const { localHeaderOffset, compressedSize, compressionMethod, fileName } = entry;
|
|
11
|
-
if (localHeaderOffset + LOCAL_FILE_HEADER_SIZE > archiveBytes.length) {
|
|
12
|
-
return yield* new ArchiveGuardrailError({
|
|
13
|
-
code: "malformed_archive",
|
|
14
|
-
message: `Local file header for entry "${fileName}" exceeds archive bounds.`,
|
|
15
|
-
entry: fileName,
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
const view = new DataView(archiveBytes.buffer, archiveBytes.byteOffset, archiveBytes.byteLength);
|
|
19
|
-
const signature = view.getUint32(localHeaderOffset, true);
|
|
20
|
-
if (signature !== ZIP_LOCAL_SIGNATURE) {
|
|
21
|
-
return yield* new ArchiveGuardrailError({
|
|
22
|
-
code: "malformed_archive",
|
|
23
|
-
message: `Invalid local file header signature for entry "${fileName}".`,
|
|
24
|
-
entry: fileName,
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
const fileNameLength = view.getUint16(localHeaderOffset + 26, true);
|
|
28
|
-
const extraFieldLength = view.getUint16(localHeaderOffset + 28, true);
|
|
29
|
-
const dataStart = localHeaderOffset + LOCAL_FILE_HEADER_SIZE + fileNameLength + extraFieldLength;
|
|
30
|
-
if (dataStart + compressedSize > archiveBytes.length) {
|
|
31
|
-
return yield* new ArchiveGuardrailError({
|
|
32
|
-
code: "malformed_archive",
|
|
33
|
-
message: `Compressed data for entry "${fileName}" exceeds archive bounds.`,
|
|
34
|
-
entry: fileName,
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
const compressedData = archiveBytes.slice(dataStart, dataStart + compressedSize);
|
|
38
|
-
if (compressionMethod === 0) {
|
|
39
|
-
return compressedData;
|
|
40
|
-
}
|
|
41
|
-
if (compressionMethod === 8) {
|
|
42
|
-
const expansionFailure = () => new ArchiveGuardrailError({
|
|
43
|
-
code: "decompression_limit_exceeded",
|
|
44
|
-
message: `Entry "${fileName}" decompresses beyond its declared size of ${entry.uncompressedSize} bytes.`,
|
|
45
|
-
entry: fileName,
|
|
46
|
-
});
|
|
47
|
-
// workerd grows its bounded zlib buffer by whole chunks, including the
|
|
48
|
-
// final partial chunk. Reserve one chunk of codec headroom, then enforce
|
|
49
|
-
// the exact declared size below. Expansion remains bounded during decode.
|
|
50
|
-
const result = yield* Effect.try({
|
|
51
|
-
try: () => inflateRawSync(compressedData, {
|
|
52
|
-
chunkSize: INFLATE_CHUNK_SIZE,
|
|
53
|
-
maxOutputLength: entry.uncompressedSize + INFLATE_CHUNK_SIZE,
|
|
54
|
-
}),
|
|
55
|
-
catch: (error) => {
|
|
56
|
-
const errorCode = typeof error === "object" &&
|
|
57
|
-
error !== null &&
|
|
58
|
-
"code" in error &&
|
|
59
|
-
typeof error.code === "string"
|
|
60
|
-
? error.code
|
|
61
|
-
: "";
|
|
62
|
-
return errorCode === "ERR_BUFFER_TOO_LARGE" ||
|
|
63
|
-
(error instanceof RangeError && error.message === "Memory limit exceeded")
|
|
64
|
-
? expansionFailure()
|
|
65
|
-
: new ArchiveGuardrailError({
|
|
66
|
-
code: "malformed_archive",
|
|
67
|
-
message: `Failed to decompress entry "${fileName}".`,
|
|
68
|
-
entry: fileName,
|
|
69
|
-
});
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
if (result.byteLength > entry.uncompressedSize)
|
|
73
|
-
return yield* expansionFailure();
|
|
74
|
-
return new Uint8Array(result.buffer, result.byteOffset, result.byteLength);
|
|
75
|
-
}
|
|
76
|
-
return yield* new ArchiveGuardrailError({
|
|
77
|
-
code: "unsupported_compression",
|
|
78
|
-
message: `Unsupported compression method ${compressionMethod} for entry "${fileName}".`,
|
|
79
|
-
entry: fileName,
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
6
|
export const normalizePublishInput = (args) => Effect.gen(function* () {
|
|
83
7
|
const { declaredIdentity, archive, digestHeader, guardrailLimits } = args;
|
|
84
8
|
const entryReader = args.readEntry ?? defaultReadEntry;
|
|
@@ -39,7 +39,7 @@ export const readExtensionManifest = (directory, type, options) => Effect.gen(fu
|
|
|
39
39
|
cause,
|
|
40
40
|
})));
|
|
41
41
|
const raw = withDefaultOwner(parsed, options?.defaultOwner);
|
|
42
|
-
const manifest = yield* Schema.decodeUnknownEffect(manifestSchemaForType(type))(raw).pipe(Effect.mapError((cause) => new ManifestError({
|
|
42
|
+
const manifest = yield* Schema.decodeUnknownEffect(manifestSchemaForType(type))(raw, type === "hook" ? { onExcessProperty: "error" } : undefined).pipe(Effect.mapError((cause) => new ManifestError({
|
|
43
43
|
code: "manifest_schema_invalid",
|
|
44
44
|
detail: `Manifest file "${fileName}" does not conform to the ${type} manifest schema.`,
|
|
45
45
|
details: SchemaIssue.makeFormatterDefault()(cause.issue),
|