@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,186 @@
1
+ function repository(value) {
2
+ const input = typeof value === "string" ? value : value?.url;
3
+ if (
4
+ typeof input !== "string" ||
5
+ input.length > 512 ||
6
+ /[\s%\\?#\0]/u.test(input)
7
+ )
8
+ return null;
9
+ let pair;
10
+ if (input.startsWith("github:")) pair = input.slice(7);
11
+ else if (input.startsWith("git@github.com:"))
12
+ pair = input.slice("git@github.com:".length);
13
+ else {
14
+ const match =
15
+ /^(https|git\+https|git|ssh|git\+ssh):\/\/([^/]+)\/(.+)$/u.exec(input);
16
+ if (!match) return null;
17
+ const ssh = ["ssh", "git+ssh"].includes(match[1]);
18
+ if (match[2] !== (ssh ? "git@github.com" : "github.com")) return null;
19
+ pair = match[3];
20
+ }
21
+ if (pair.endsWith(".git")) pair = pair.slice(0, -4);
22
+ const match =
23
+ /^([a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?)\/([a-z0-9_.-]{1,100})$/iu.exec(
24
+ pair,
25
+ );
26
+ if (!match || match[1].includes("--") || [".", ".."].includes(match[2]))
27
+ return null;
28
+ return {
29
+ owner: match[1].toLowerCase(),
30
+ repo: match[2].toLowerCase(),
31
+ repository: `https://github.com/${match[1].toLowerCase()}/${match[2].toLowerCase()}`,
32
+ };
33
+ }
34
+ function directory(value) {
35
+ if (value === undefined || value === "") return "";
36
+ if (
37
+ typeof value !== "string" ||
38
+ value.length > 4096 ||
39
+ /[\0\\]/u.test(value) ||
40
+ value.startsWith("/") ||
41
+ /^[a-z]:/iu.test(value) ||
42
+ value.split("/").some((p) => !p || p === "." || p === "..")
43
+ )
44
+ return null;
45
+ return value;
46
+ }
47
+ function candidate(repo, commit, dir, method) {
48
+ if (
49
+ !repo ||
50
+ typeof commit !== "string" ||
51
+ !/^[a-f0-9]{40}$/iu.test(commit) ||
52
+ dir === null
53
+ )
54
+ return null;
55
+ const sha = commit.toLowerCase();
56
+ return {
57
+ repository: repo.repository,
58
+ commit: sha,
59
+ directory: dir,
60
+ method,
61
+ archiveUrl: `https://codeload.github.com/${repo.owner}/${repo.repo}/tar.gz/${sha}`,
62
+ sourceRoot: `${repo.repo}-${sha}`,
63
+ };
64
+ }
65
+ function same(a, b) {
66
+ return a.repository === b.repository && a.commit === b.commit;
67
+ }
68
+ function identity(value) {
69
+ if (typeof value === "string") {
70
+ const at = value.lastIndexOf("@");
71
+ return at > 0
72
+ ? { name: value.slice(0, at), version: value.slice(at + 1) }
73
+ : null;
74
+ }
75
+ return value &&
76
+ typeof value.name === "string" &&
77
+ typeof value.version === "string"
78
+ ? { name: value.name, version: value.version }
79
+ : null;
80
+ }
81
+ export function resolveSource({
82
+ claim,
83
+ artifactManifest,
84
+ registryDoc,
85
+ explicitSource,
86
+ commit,
87
+ sourceDir,
88
+ } = {}) {
89
+ const result = {
90
+ status: "unresolved",
91
+ method: "unresolved",
92
+ candidate: null,
93
+ alternatives: [],
94
+ provenance: { status: "not-assessed" },
95
+ conflicts: [],
96
+ };
97
+ const requested = identity(claim),
98
+ artifact = identity(artifactManifest),
99
+ registry = identity(registryDoc);
100
+ if (
101
+ (requested &&
102
+ artifact &&
103
+ (requested.name !== artifact.name ||
104
+ requested.version !== artifact.version)) ||
105
+ (requested &&
106
+ registry &&
107
+ (requested.name !== registry.name ||
108
+ requested.version !== registry.version))
109
+ )
110
+ result.conflicts.push({
111
+ code: "identity-claim-conflict",
112
+ claim: requested,
113
+ artifact,
114
+ registry,
115
+ });
116
+ const registryRepo = repository(registryDoc?.repository);
117
+ const artifactRepo = repository(artifactManifest?.repository);
118
+ if (
119
+ registryRepo &&
120
+ artifactRepo &&
121
+ registryRepo.repository !== artifactRepo.repository
122
+ )
123
+ result.conflicts.push({
124
+ code: "repository-claim-conflict",
125
+ artifact: artifactRepo.repository,
126
+ registry: registryRepo.repository,
127
+ });
128
+ const publisher = candidate(
129
+ registryRepo,
130
+ registryDoc?.gitHead,
131
+ directory(registryDoc?.repository?.directory),
132
+ "publisher-gitHead",
133
+ );
134
+ if (publisher) result.alternatives.push(publisher);
135
+ const explicitRepo =
136
+ typeof explicitSource === "string" ? repository(explicitSource) : null;
137
+ if (explicitSource !== undefined && !explicitRepo) {
138
+ result.reason = "unsupported-explicit-repository";
139
+ return result;
140
+ }
141
+ const dir = sourceDir === undefined ? undefined : directory(sourceDir);
142
+ if (dir === null) {
143
+ result.reason = "invalid-source-directory";
144
+ return result;
145
+ }
146
+ if (commit !== undefined) {
147
+ const pinned = candidate(
148
+ explicitRepo ?? registryRepo ?? artifactRepo,
149
+ commit,
150
+ dir ?? "",
151
+ "user-full-SHA",
152
+ );
153
+ if (!pinned) {
154
+ result.reason = "invalid-user-pin";
155
+ return result;
156
+ }
157
+ if (publisher && !same(publisher, pinned))
158
+ result.conflicts.push({
159
+ code: "source-claim-conflict",
160
+ alternatives: [publisher, pinned],
161
+ });
162
+ result.alternatives.push(pinned);
163
+ return {
164
+ ...result,
165
+ status: "selected",
166
+ method: pinned.method,
167
+ candidate: pinned,
168
+ };
169
+ }
170
+ if (!publisher) {
171
+ result.reason = "no-full-sha-source-candidate";
172
+ return result;
173
+ }
174
+ if (explicitRepo && explicitRepo.repository !== publisher.repository) {
175
+ result.reason = "explicit-repository-disagrees";
176
+ return result;
177
+ }
178
+ const selected =
179
+ dir === undefined ? publisher : { ...publisher, directory: dir };
180
+ return {
181
+ ...result,
182
+ status: "selected",
183
+ method: selected.method,
184
+ candidate: selected,
185
+ };
186
+ }
@@ -0,0 +1,106 @@
1
+ function unresolved(reason, directory = null) {
2
+ return {
3
+ status: "unresolved",
4
+ directory,
5
+ entries: [],
6
+ manifest: null,
7
+ reason,
8
+ };
9
+ }
10
+ function directoryPath(value) {
11
+ if (
12
+ typeof value !== "string" ||
13
+ /[\0\\]/u.test(value) ||
14
+ value.startsWith("/") ||
15
+ /^[a-z]:/iu.test(value)
16
+ )
17
+ return null;
18
+ if (value === "") return "";
19
+ const path = value.replace(/\/+$/u, "");
20
+ return path.split("/").some((p) => !p || p === "." || p === "..")
21
+ ? null
22
+ : path;
23
+ }
24
+ function manifest(entry) {
25
+ if (entry?.type !== "file" || !Buffer.isBuffer(entry.content)) return null;
26
+ try {
27
+ const value = JSON.parse(entry.content.toString("utf8"));
28
+ return value && typeof value === "object" && !Array.isArray(value)
29
+ ? value
30
+ : null;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+ function blocked(entries, directory) {
36
+ return entries.some(
37
+ (e) =>
38
+ e.path !== null &&
39
+ e.path !== undefined &&
40
+ e.type !== "directory" &&
41
+ (e.path === directory || directory.startsWith(e.path + "/")),
42
+ );
43
+ }
44
+ export function selectSourcePackage(
45
+ inventory,
46
+ { directory, packageName } = {},
47
+ ) {
48
+ const entries = inventory.entries;
49
+ let selected = directory;
50
+ if (selected !== undefined) {
51
+ selected = directoryPath(selected);
52
+ if (selected === null) return unresolved("invalid-directory");
53
+ } else {
54
+ if (!inventory.complete) return unresolved("incomplete-inventory");
55
+ const candidates = entries.filter(
56
+ (e) =>
57
+ (e.path === "package.json" || e.path?.endsWith("/package.json")) &&
58
+ typeof packageName === "string" &&
59
+ manifest(e)?.name === packageName,
60
+ );
61
+ if (candidates.length !== 1)
62
+ return unresolved(
63
+ candidates.length ? "ambiguous-manifest" : "manifest-not-found",
64
+ );
65
+ selected = candidates[0].path
66
+ .slice(0, -"package.json".length)
67
+ .replace(/\/$/u, "");
68
+ }
69
+ if (selected && blocked(entries, selected))
70
+ return unresolved("nonregular-package-root", selected);
71
+ const prefix = selected ? selected + "/" : "";
72
+ const selectedEntries = entries
73
+ .filter(
74
+ (e) =>
75
+ typeof e.path === "string" &&
76
+ e.path.startsWith(prefix) &&
77
+ e.path !== selected,
78
+ )
79
+ .map((e) => ({ ...e, path: e.path.slice(prefix.length) }))
80
+ .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
81
+ const existingDirectory = entries.some(
82
+ (e) =>
83
+ e.type === "directory" &&
84
+ (e.path === selected || (selected === "" && e.path === null)),
85
+ );
86
+ if (!selectedEntries.length && !existingDirectory)
87
+ return unresolved("directory-not-found", selected);
88
+ if (selectedEntries.some((e) => directoryPath(e.path) === null))
89
+ return unresolved("unsafe-mapped-path", selected);
90
+ const manifestEntry = selectedEntries.find((e) => e.path === "package.json");
91
+ const parsed = manifest(manifestEntry);
92
+ const reason = !manifestEntry
93
+ ? "missing"
94
+ : manifestEntry.type !== "file"
95
+ ? "nonregular-manifest"
96
+ : !Buffer.isBuffer(manifestEntry.content)
97
+ ? manifestEntry.inspection
98
+ : "invalid-json";
99
+ return {
100
+ status: "selected",
101
+ directory: selected,
102
+ entries: selectedEntries,
103
+ manifest: parsed,
104
+ ...(!parsed ? { reason } : {}),
105
+ };
106
+ }
@@ -0,0 +1,340 @@
1
+ import { parse } from "acorn";
2
+ import { createHash } from "node:crypto";
3
+ import { validateReport } from "./report.mjs";
4
+
5
+ export const SYNTAX_LIMITS = Object.freeze({
6
+ fileBytes: 2097152,
7
+ totalBytesPerSide: 8388608,
8
+ tokens: 200000,
9
+ cues: 1000,
10
+ sourceLiterals: 10000,
11
+ });
12
+ const hash = (value) => createHash("sha256").update(value).digest("hex");
13
+ function offsets(text) {
14
+ const result = new Uint32Array(text.length + 1);
15
+ let index = 0,
16
+ bytes = 0;
17
+ for (const character of text) {
18
+ result[index] = bytes;
19
+ if (character.length === 2) result[index + 1] = bytes;
20
+ index += character.length;
21
+ bytes += Buffer.byteLength(character);
22
+ result[index] = bytes;
23
+ }
24
+ return result;
25
+ }
26
+ function evidenceFor(side, inventory, entry, node, positions) {
27
+ const range = [positions[node.start], positions[node.end]];
28
+ const identity = [
29
+ side,
30
+ inventory.digest.sha256,
31
+ entry.path ?? entry.originalPath,
32
+ range,
33
+ ];
34
+ const evidence = {
35
+ id: `ev-${hash(JSON.stringify(identity))}`,
36
+ side,
37
+ blobDigest: inventory.digest.sha256,
38
+ path: entry.path,
39
+ originalPath: entry.originalPath,
40
+ fileSha256: entry.sha256,
41
+ size: entry.size,
42
+ range,
43
+ };
44
+ let end = Math.min(range[1], range[0] + 512);
45
+ while (end > range[0]) {
46
+ try {
47
+ evidence.excerpt = new TextDecoder("utf-8", {
48
+ fatal: true,
49
+ ignoreBOM: true,
50
+ }).decode(entry.content.subarray(range[0], end));
51
+ evidence.excerptRange = [range[0], end];
52
+ break;
53
+ } catch {
54
+ end--;
55
+ }
56
+ }
57
+ return evidence;
58
+ }
59
+ function nameOf(node) {
60
+ const parts = [];
61
+ for (let depth = 0; node && depth < 4; depth++) {
62
+ if (node.type === "Identifier") return [node.name, ...parts].join(".");
63
+ if (
64
+ node.type === "CallExpression" &&
65
+ node.callee?.type === "Identifier" &&
66
+ node.callee.name === "require" &&
67
+ node.arguments.length === 1 &&
68
+ node.arguments[0].type === "Literal" &&
69
+ typeof node.arguments[0].value === "string"
70
+ ) {
71
+ const module = node.arguments[0].value.replace(/^node:/, "");
72
+ return ["child_process", "http", "https", "net"].includes(module)
73
+ ? [module, ...parts].join(".")
74
+ : "";
75
+ }
76
+ if (node.type !== "MemberExpression" || node.computed) return "";
77
+ parts.unshift(node.property.name);
78
+ node = node.object;
79
+ }
80
+ return "";
81
+ }
82
+ function capability(node) {
83
+ const name = nameOf(node.callee);
84
+ if (
85
+ node.type === "CallExpression" &&
86
+ /^(fetch|globalThis.fetch|https?\.(request|get)|net\.connect)$/.test(name)
87
+ )
88
+ return "network-call";
89
+ if (
90
+ node.type === "CallExpression" &&
91
+ /^(child_process\.)?(exec|execSync|execFile|execFileSync|spawn|spawnSync)$/.test(
92
+ name,
93
+ )
94
+ )
95
+ return "process-execution";
96
+ if (
97
+ ["CallExpression", "NewExpression"].includes(node.type) &&
98
+ /^(eval|Function)$/.test(name)
99
+ )
100
+ return "dynamic-code";
101
+ if (
102
+ node.type === "MemberExpression" &&
103
+ nameOf(node).startsWith("process.env.")
104
+ )
105
+ return "environment-access";
106
+ return null;
107
+ }
108
+ function nodes(ast) {
109
+ const stack = [ast],
110
+ result = [];
111
+ while (stack.length) {
112
+ const node = stack.pop();
113
+ result.push(node);
114
+ for (const value of Object.values(node)) {
115
+ if (Array.isArray(value)) {
116
+ for (let i = value.length - 1; i >= 0; i--)
117
+ if (value[i]?.type) stack.push(value[i]);
118
+ } else if (value?.type) stack.push(value);
119
+ }
120
+ }
121
+ return result;
122
+ }
123
+
124
+ export function inspectJavaScript(artifact, source, overrides = {}) {
125
+ const limits = { ...SYNTAX_LIMITS };
126
+ for (const [key, value] of Object.entries(overrides)) {
127
+ if (
128
+ !(key in limits) ||
129
+ !Number.isSafeInteger(value) ||
130
+ value < 0 ||
131
+ value > limits[key]
132
+ )
133
+ throw new TypeError("Syntax limits may only lower defaults");
134
+ limits[key] = value;
135
+ }
136
+ const result = {
137
+ cues: [],
138
+ evidence: [],
139
+ coverage: { files: 0, bytes: 0 },
140
+ sourceCoverage: { files: 0, bytes: 0 },
141
+ annotations: [],
142
+ limits,
143
+ };
144
+ const literals = new Map(),
145
+ candidates = [],
146
+ evidence = new Map();
147
+ let sourceLiteralTruncated = false;
148
+ for (const [side, inventory] of [
149
+ ["artifact", artifact],
150
+ ["source", source],
151
+ ]) {
152
+ let consumed = 0;
153
+ const entries = (inventory?.entries || [])
154
+ .filter((entry) => entry.type === "file")
155
+ .sort((a, b) =>
156
+ a.originalPath < b.originalPath
157
+ ? -1
158
+ : a.originalPath > b.originalPath
159
+ ? 1
160
+ : 0,
161
+ );
162
+ for (const entry of entries) {
163
+ const skip = (reason) =>
164
+ result.annotations.push({
165
+ type: "syntax-not-inspected",
166
+ side,
167
+ path: entry.path,
168
+ originalPath: entry.originalPath,
169
+ reason,
170
+ });
171
+ if (!/\.(?:mjs|cjs|js)$/.test(entry.path || entry.originalPath)) {
172
+ skip("unsupported-syntax");
173
+ continue;
174
+ }
175
+ if (!Buffer.isBuffer(entry.content)) {
176
+ skip("not-retained");
177
+ continue;
178
+ }
179
+ if (entry.size > limits.fileBytes) {
180
+ skip("syntax-file-cap");
181
+ continue;
182
+ }
183
+ if (consumed + entry.size > limits.totalBytesPerSide) {
184
+ skip("syntax-total-cap");
185
+ continue;
186
+ }
187
+ consumed += entry.size;
188
+ let text, ast;
189
+ try {
190
+ text = new TextDecoder("utf-8", {
191
+ fatal: true,
192
+ ignoreBOM: true,
193
+ }).decode(entry.content);
194
+ const parseAs = (sourceType) => {
195
+ let tokens = 0;
196
+ return parse(text, {
197
+ ecmaVersion: 2025,
198
+ sourceType,
199
+ allowHashBang: true,
200
+ onToken() {
201
+ if (++tokens > limits.tokens) throw new Error("syntax-token-cap");
202
+ },
203
+ });
204
+ };
205
+ try {
206
+ ast = parseAs("module");
207
+ } catch (error) {
208
+ if (!(error instanceof SyntaxError)) throw error;
209
+ ast = parseAs("commonjs");
210
+ }
211
+ } catch (error) {
212
+ skip(
213
+ error.message === "syntax-token-cap" ? error.message : "parse-failed",
214
+ );
215
+ continue;
216
+ }
217
+ const coverage =
218
+ side === "artifact" ? result.coverage : result.sourceCoverage;
219
+ coverage.files++;
220
+ coverage.bytes += entry.size;
221
+ const positions = offsets(text);
222
+ for (const node of nodes(ast)) {
223
+ if (side === "source") {
224
+ if (
225
+ node.type === "Literal" &&
226
+ typeof node.value === "string" &&
227
+ !literals.has(node.value)
228
+ ) {
229
+ if (literals.size < limits.sourceLiterals)
230
+ literals.set(
231
+ node.value,
232
+ evidenceFor(side, inventory, entry, node, positions),
233
+ );
234
+ else sourceLiteralTruncated = true;
235
+ }
236
+ continue;
237
+ }
238
+ const kind = capability(node);
239
+ if (!kind) continue;
240
+ if (candidates.length >= limits.cues) {
241
+ skip("syntax-cue-cap");
242
+ break;
243
+ }
244
+ candidates.push({
245
+ kind,
246
+ evidence: evidenceFor(side, inventory, entry, node, positions),
247
+ literal:
248
+ node.arguments?.[0]?.type === "Literal" &&
249
+ typeof node.arguments[0].value === "string"
250
+ ? node.arguments[0].value
251
+ : null,
252
+ });
253
+ }
254
+ }
255
+ }
256
+ const stem = (value) => value.replace(/\.[cm]?[jt]sx?$/, "");
257
+ const unknownSource = new Set(
258
+ result.annotations
259
+ .filter(
260
+ (item) =>
261
+ item.type === "syntax-not-inspected" && item.side === "source",
262
+ )
263
+ .map((item) => stem(item.path || item.originalPath)),
264
+ );
265
+ for (const candidate of candidates) {
266
+ const ids = [candidate.evidence.id];
267
+ evidence.set(candidate.evidence.id, candidate.evidence);
268
+ const match = candidate.literal !== null && literals.get(candidate.literal);
269
+ if (match) {
270
+ evidence.set(match.id, match);
271
+ ids.push(match.id);
272
+ }
273
+ result.cues.push({
274
+ ruleId: `syntax-${candidate.kind}`,
275
+ priority: "review",
276
+ summary:
277
+ "Syntactic capability reference; execution and binding are not proven.",
278
+ literalStatus:
279
+ candidate.literal === null
280
+ ? "no-static-literal"
281
+ : match
282
+ ? "found-in-inspected-source"
283
+ : source
284
+ ? result.sourceCoverage.files === 0 ||
285
+ sourceLiteralTruncated ||
286
+ unknownSource.has(
287
+ stem(
288
+ candidate.evidence.path || candidate.evidence.originalPath,
289
+ ),
290
+ )
291
+ ? "not-assessed"
292
+ : "not-found-in-inspected-source"
293
+ : "source-unavailable",
294
+ evidenceIds: ids,
295
+ });
296
+ }
297
+ result.evidence = [...evidence.values()];
298
+ result.annotations.push({
299
+ type: "syntax-scope",
300
+ parser: "acorn@8.19.0",
301
+ sourceCoverage: result.sourceCoverage,
302
+ sourceLiteralCount: literals.size,
303
+ sourceLiteralTruncated,
304
+ limitation:
305
+ "Literal matches are not semantic equivalence; missing literals refer only to inspected JavaScript. TypeScript, JSX, source maps, computed aliases and dynamic targets are not resolved.",
306
+ });
307
+ return result;
308
+ }
309
+
310
+ export function addSyntaxInspection(report, inspection) {
311
+ const result = structuredClone(report);
312
+ const existing = new Set(result.evidence.map((item) => item.id));
313
+ for (const item of inspection.evidence)
314
+ if (!existing.has(item.id)) {
315
+ result.evidence.push(item);
316
+ existing.add(item.id);
317
+ }
318
+ const byFile = new Map(result.files.map((file) => [file.originalPath, file]));
319
+ for (const item of inspection.evidence) {
320
+ const file = item.side === "artifact" && byFile.get(item.originalPath);
321
+ if (file && !file.evidenceIds.includes(item.id))
322
+ file.evidenceIds.push(item.id);
323
+ }
324
+ const evidenceById = new Map(
325
+ inspection.evidence.map((item) => [item.id, item]),
326
+ );
327
+ for (const cue of inspection.cues) {
328
+ const item = evidenceById.get(cue.evidenceIds[0]);
329
+ const file = byFile.get(item.originalPath);
330
+ const unchanged = file?.state === "byte-identical";
331
+ if (unchanged)
332
+ result.annotations.push({ type: "unchanged-capability", ...cue });
333
+ else result.cues.push(cue);
334
+ }
335
+ result.annotations.push(...inspection.annotations);
336
+ result.coverage.syntaxInspected = inspection.coverage;
337
+ result.inputs.limits = { ...result.inputs.limits, syntax: inspection.limits };
338
+ result.inputs.syntaxParser = "acorn@8.19.0";
339
+ return validateReport(result);
340
+ }