@xagents_in/unmask 0.0.0-stage → 0.1.1

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,283 @@
1
+ import { createHash } from "node:crypto";
2
+ import { resolveSource } from "./resolve.mjs";
3
+
4
+ const MAX_BLOB_BYTES = 50 * 1024 * 1024;
5
+ const MAX_METADATA_BYTES = 1024 * 1024;
6
+ const MAX_TIMEOUT_MS = 30000;
7
+ export class AcquisitionError extends Error {
8
+ constructor(code, message) {
9
+ super(message);
10
+ this.code = code;
11
+ }
12
+ }
13
+ function reject(code, message) {
14
+ throw new AcquisitionError(code, message);
15
+ }
16
+ export function parsePackageSpec(spec) {
17
+ if (typeof spec !== "string" || spec.length > 512)
18
+ reject("invalid-package-spec", "Expected exact package name@version");
19
+ const at = spec.lastIndexOf("@");
20
+ const name = spec.slice(0, at),
21
+ version = spec.slice(at + 1);
22
+ const part = "[a-z0-9][a-z0-9._-]*";
23
+ const validName = new RegExp(`^(?:${part}|@${part}/${part})$`, "u");
24
+ const semver =
25
+ /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/u;
26
+ const match = version.match(semver);
27
+ if (
28
+ at <= 0 ||
29
+ name.length > 214 ||
30
+ !validName.test(name) ||
31
+ !match ||
32
+ match[4]
33
+ ?.split(".")
34
+ .some((x) => /^[0-9]+$/u.test(x) && x.length > 1 && x.startsWith("0"))
35
+ )
36
+ reject("invalid-package-spec", "Expected exact package name@version");
37
+ return { name, version };
38
+ }
39
+ function approvedUrl(value) {
40
+ if (
41
+ typeof value !== "string" ||
42
+ value.length > 4096 ||
43
+ /[\s\\\0-\x1f\x7f]/u.test(value)
44
+ )
45
+ reject(
46
+ "unapproved-url",
47
+ "URL is outside the approved acquisition boundary",
48
+ );
49
+ const match =
50
+ /^https:\/\/(registry\.npmjs\.org|codeload\.github\.com)(\/[^?#]*)$/u.exec(
51
+ value,
52
+ );
53
+ if (!match)
54
+ reject(
55
+ "unapproved-url",
56
+ "URL is outside the approved acquisition boundary",
57
+ );
58
+ const url = new URL(value);
59
+ let path;
60
+ try {
61
+ path = decodeURIComponent(url.pathname);
62
+ } catch {
63
+ reject("unapproved-url", "Invalid URL path encoding");
64
+ }
65
+ if (
66
+ path.includes("%") ||
67
+ /[\0-\x20\x7f\\]/u.test(path) ||
68
+ path.split("/").some((p) => p === "." || p === "..") ||
69
+ url.href !== value
70
+ )
71
+ reject("unapproved-url", "Noncanonical acquisition URL");
72
+ if (
73
+ match[1] === "codeload.github.com" &&
74
+ !/^\/[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?\/[a-z0-9_.-]+\/tar\.gz\/[a-f0-9]{40}$/u.test(
75
+ path,
76
+ )
77
+ )
78
+ reject(
79
+ "unapproved-url",
80
+ "Source URL requires a canonical repository and full SHA",
81
+ );
82
+ if (match[1] === "codeload.github.com" && value.includes("%"))
83
+ reject("unapproved-url", "Encoded source identifiers are unsupported");
84
+ return value;
85
+ }
86
+ function digest(bytes) {
87
+ return {
88
+ sha256: createHash("sha256").update(bytes).digest("hex"),
89
+ sha512: createHash("sha512").update(bytes).digest("hex"),
90
+ };
91
+ }
92
+ export async function fetchBlob(
93
+ url,
94
+ {
95
+ fetchImpl = globalThis.fetch,
96
+ signal,
97
+ maxBytes = MAX_BLOB_BYTES,
98
+ timeoutMs = MAX_TIMEOUT_MS,
99
+ } = {},
100
+ ) {
101
+ approvedUrl(url);
102
+ if (
103
+ !Number.isSafeInteger(maxBytes) ||
104
+ maxBytes < 0 ||
105
+ maxBytes > MAX_BLOB_BYTES
106
+ )
107
+ throw new TypeError("Invalid acquisition byte limit");
108
+ if (
109
+ !Number.isSafeInteger(timeoutMs) ||
110
+ timeoutMs < 0 ||
111
+ timeoutMs > MAX_TIMEOUT_MS
112
+ )
113
+ throw new TypeError("Invalid acquisition deadline");
114
+ if (typeof fetchImpl !== "function")
115
+ throw new TypeError("fetchImpl must be a function");
116
+ const controller = new AbortController();
117
+ const abortExternal = () =>
118
+ controller.abort(
119
+ new AcquisitionError("acquisition-aborted", "Acquisition cancelled"),
120
+ );
121
+ if (signal?.aborted) abortExternal();
122
+ signal?.addEventListener("abort", abortExternal, { once: true });
123
+ const observation = { url, observedAt: new Date().toISOString(), bytes: 0 };
124
+ let reader, response, timer, rejectAbort;
125
+ const aborted = new Promise((resolve, rejectPromise) => {
126
+ rejectAbort = rejectPromise;
127
+ });
128
+ aborted.catch(() => {});
129
+ const onAbort = () => rejectAbort(controller.signal.reason);
130
+ controller.signal.addEventListener("abort", onAbort, { once: true });
131
+ try {
132
+ if (controller.signal.aborted) throw controller.signal.reason;
133
+ if (timeoutMs === 0)
134
+ reject("acquisition-timeout", "Acquisition deadline exceeded");
135
+ timer = setTimeout(
136
+ () =>
137
+ controller.abort(
138
+ new AcquisitionError(
139
+ "acquisition-timeout",
140
+ "Acquisition deadline exceeded",
141
+ ),
142
+ ),
143
+ timeoutMs,
144
+ );
145
+ const operation = (async () => {
146
+ response = await fetchImpl(url, {
147
+ redirect: "error",
148
+ credentials: "omit",
149
+ referrerPolicy: "no-referrer",
150
+ headers: { accept: "*/*", "accept-encoding": "identity" },
151
+ signal: controller.signal,
152
+ });
153
+ if (controller.signal.aborted) throw controller.signal.reason;
154
+ observation.status = response.status;
155
+ if (
156
+ response.redirected ||
157
+ (response.status >= 300 && response.status < 400) ||
158
+ (response.url && response.url !== url)
159
+ )
160
+ reject("redirect-refused", "Redirected acquisition is forbidden");
161
+ if (response.status !== 200)
162
+ reject(
163
+ "http-status",
164
+ "Acquisition did not return a complete successful response",
165
+ );
166
+ const length = response.headers.get("content-length");
167
+ if (length !== null) {
168
+ if (!/^[0-9]+$/u.test(length) || !Number.isSafeInteger(Number(length)))
169
+ reject("invalid-response", "Invalid response length");
170
+ observation.contentLength = Number(length);
171
+ if (Number(length) > maxBytes)
172
+ reject("response-cap", "Response exceeds acquisition byte limit");
173
+ }
174
+ let storage = Buffer.alloc(0);
175
+ if (response.body) {
176
+ reader = response.body.getReader();
177
+ while (true) {
178
+ const { done, value } = await reader.read();
179
+ if (controller.signal.aborted) throw controller.signal.reason;
180
+ if (done) break;
181
+ if (!(value instanceof Uint8Array))
182
+ reject("invalid-response", "Response stream must contain bytes");
183
+ if (value.byteLength > maxBytes - observation.bytes)
184
+ reject("response-cap", "Response exceeds acquisition byte limit");
185
+ const required = observation.bytes + value.byteLength;
186
+ if (required > storage.length) {
187
+ const capacity = Math.min(
188
+ maxBytes,
189
+ Math.max(required, storage.length ? storage.length * 2 : 65536),
190
+ );
191
+ const grown = Buffer.alloc(capacity);
192
+ storage.copy(grown, 0, 0, observation.bytes);
193
+ storage = grown;
194
+ }
195
+ storage.set(value, observation.bytes);
196
+ observation.bytes = required;
197
+ }
198
+ }
199
+ const bytes = storage.subarray(0, observation.bytes);
200
+ return { bytes, digest: digest(bytes), observation };
201
+ })();
202
+ return await Promise.race([operation, aborted]);
203
+ } catch (error) {
204
+ const failure = controller.signal.aborted
205
+ ? controller.signal.reason
206
+ : error instanceof AcquisitionError
207
+ ? error
208
+ : new AcquisitionError(
209
+ "network-failure",
210
+ "Acquisition transport failed",
211
+ );
212
+ failure.observation = observation;
213
+ controller.abort(failure);
214
+ try {
215
+ Promise.resolve(
216
+ reader ? reader.cancel() : response?.body?.cancel(),
217
+ ).catch(() => {});
218
+ } catch {}
219
+ throw failure;
220
+ } finally {
221
+ clearTimeout(timer);
222
+ signal?.removeEventListener("abort", abortExternal);
223
+ controller.signal.removeEventListener("abort", onAbort);
224
+ reader?.releaseLock();
225
+ }
226
+ }
227
+ export async function getRegistryRelease(spec, { fetchImpl, signal } = {}) {
228
+ const identity =
229
+ typeof spec === "string"
230
+ ? parsePackageSpec(spec)
231
+ : parsePackageSpec(`${spec?.name}@${spec?.version}`);
232
+ const url = `https://registry.npmjs.org/${encodeURIComponent(identity.name)}/${encodeURIComponent(identity.version)}`;
233
+ const blob = await fetchBlob(url, {
234
+ fetchImpl,
235
+ signal,
236
+ maxBytes: MAX_METADATA_BYTES,
237
+ });
238
+ try {
239
+ let metadata;
240
+ try {
241
+ metadata = JSON.parse(blob.bytes.toString("utf8"));
242
+ } catch {
243
+ reject("invalid-registry-document", "Registry release is not valid JSON");
244
+ }
245
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata))
246
+ reject("invalid-registry-document", "Registry release must be an object");
247
+ if (
248
+ metadata.name !== identity.name ||
249
+ metadata.version !== identity.version
250
+ )
251
+ reject(
252
+ "registry-identity-mismatch",
253
+ "Registry release identity differs from the exact requested version",
254
+ );
255
+ const tarballUrl = approvedUrl(metadata.dist?.tarball);
256
+ if (!tarballUrl.startsWith("https://registry.npmjs.org/"))
257
+ reject(
258
+ "unapproved-url",
259
+ "Registry tarball must remain on the public registry",
260
+ );
261
+ const expectedIntegrity =
262
+ typeof metadata.dist?.integrity === "string"
263
+ ? metadata.dist.integrity
264
+ : /^[a-f0-9]{40}$/iu.test(metadata.dist?.shasum ?? "")
265
+ ? `sha1-${Buffer.from(metadata.dist.shasum, "hex").toString("base64")}`
266
+ : null;
267
+ return {
268
+ metadata,
269
+ metadataBytes: blob.bytes,
270
+ metadataDigest: blob.digest,
271
+ tarballUrl,
272
+ expectedIntegrity,
273
+ sourceCandidate: resolveSource({ registryDoc: metadata }).candidate,
274
+ observation: blob.observation,
275
+ };
276
+ } catch (error) {
277
+ if (error instanceof AcquisitionError) {
278
+ error.observation = blob.observation;
279
+ error.metadataDigest = blob.digest;
280
+ }
281
+ throw error;
282
+ }
283
+ }