@withtruing/companion 0.13.3 → 0.14.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.
Files changed (56) hide show
  1. package/README.md +24 -25
  2. package/dist/anthropic-output-schema.js +41 -0
  3. package/dist/api-key-caller.js +9 -2
  4. package/dist/claude-cli-caller.js +107 -8
  5. package/dist/cli.js +836 -206
  6. package/dist/codex-cli-caller.js +85 -14
  7. package/dist/config.js +177 -2
  8. package/dist/git-range.js +88 -46
  9. package/dist/http-client.js +95 -10
  10. package/dist/investigation-reader.js +12 -6
  11. package/dist/managed-repo.js +132 -0
  12. package/dist/observed-sources.js +132 -6
  13. package/dist/reader-result-contract.js +296 -2
  14. package/dist/repository-provider.js +608 -0
  15. package/dist/schema-entities.js +18 -18
  16. package/dist/service.js +7 -2
  17. package/dist/surface-mapper.js +53 -11
  18. package/dist/text-model.js +91 -8
  19. package/dist/whole-read/archive.js +196 -0
  20. package/dist/whole-read/checks.js +297 -0
  21. package/dist/whole-read/claims.js +196 -0
  22. package/dist/whole-read/coverage-ledger.js +51 -0
  23. package/dist/whole-read/decode.js +234 -0
  24. package/dist/whole-read/drafting.js +502 -0
  25. package/dist/whole-read/generated-rules.js +68 -0
  26. package/dist/whole-read/go-proof.js +255 -0
  27. package/dist/whole-read/grouping-prompt.js +19 -0
  28. package/dist/whole-read/grouping.js +247 -0
  29. package/dist/whole-read/identity.js +103 -0
  30. package/dist/whole-read/import-graph.js +293 -0
  31. package/dist/whole-read/inventory-collector.js +55 -0
  32. package/dist/whole-read/inventory-rules.js +141 -0
  33. package/dist/whole-read/lockfiles.js +947 -0
  34. package/dist/whole-read/notes.js +216 -0
  35. package/dist/whole-read/npm-proof.js +272 -0
  36. package/dist/whole-read/owner-decision-detectors.js +270 -0
  37. package/dist/whole-read/packing.js +612 -0
  38. package/dist/whole-read/pages.js +429 -0
  39. package/dist/whole-read/phase-policy.js +54 -0
  40. package/dist/whole-read/prd-build.js +279 -0
  41. package/dist/whole-read/prd-fidelity.js +14 -0
  42. package/dist/whole-read/python-proof.js +489 -0
  43. package/dist/whole-read/read-prompt.js +67 -0
  44. package/dist/whole-read/redaction.js +411 -0
  45. package/dist/whole-read/relationship-mapping.js +98 -0
  46. package/dist/whole-read/sanitize.js +266 -0
  47. package/dist/whole-read/secret-rules.js +26 -0
  48. package/dist/whole-read/structure.js +300 -0
  49. package/dist/whole-read/types.js +2 -0
  50. package/dist/whole-read-claims.js +12 -0
  51. package/dist/whole-read-git.js +340 -0
  52. package/dist/whole-read-pool.js +80 -0
  53. package/dist/whole-read-runner.js +945 -0
  54. package/dist/whole-read-shared-source.js +194 -0
  55. package/dist/whole-read-temp.js +479 -0
  56. package/package.json +8 -2
@@ -0,0 +1,216 @@
1
+ import { createHash } from 'node:crypto';
2
+ export const READ_CARDS_SCHEMA_VERSION = 'whole-read-cards@1';
3
+ export const READ_NOTES_SCHEMA_VERSION = READ_CARDS_SCHEMA_VERSION;
4
+ export class ReadNoteValidationError extends Error {
5
+ code;
6
+ constructor(code) {
7
+ super(code);
8
+ this.code = code;
9
+ this.name = 'ReadNoteValidationError';
10
+ }
11
+ }
12
+ const textSchema = { type: 'string', minLength: 1 };
13
+ const cardSchema = {
14
+ type: 'object', additionalProperties: false, required: ['item', 'summary', 'kind'],
15
+ properties: {
16
+ item: { type: 'integer', minimum: 1 },
17
+ summary: { type: 'string', minLength: 1, maxLength: 300 },
18
+ kind: { type: 'string', enum: ['product', 'test', 'tooling', 'config', 'docs', 'infrastructure'] },
19
+ usesHint: { type: 'array', maxItems: 20, items: { ...textSchema, maxLength: 120 } },
20
+ },
21
+ };
22
+ /** Strict model-output schema. Cards contain no evidence, identity, or provenance fields. */
23
+ export const readCardsSchema = {
24
+ type: 'object', additionalProperties: false, required: ['cards'],
25
+ properties: { cards: { type: 'array', items: cardSchema } },
26
+ };
27
+ export const readNotesSchema = readCardsSchema;
28
+ const CARD_KEYS = new Set(['item', 'summary', 'kind', 'usesHint']);
29
+ const KINDS = new Set(['product', 'test', 'tooling', 'config', 'docs', 'infrastructure']);
30
+ const fail = (code) => { throw new ReadNoteValidationError(code); };
31
+ const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
32
+ const nonBlank = (value) => typeof value === 'string' && value.trim().length > 0;
33
+ const positive = (value) => Number.isSafeInteger(value) && value >= 1;
34
+ function parseResult(value) {
35
+ if (!record(value) || Object.keys(value).length !== 1 || !Array.isArray(value.cards)) {
36
+ return fail('note_schema_invalid');
37
+ }
38
+ for (const card of value.cards) {
39
+ if (!record(card) || Object.keys(card).some((key) => !CARD_KEYS.has(key))
40
+ || !positive(card.item) || !nonBlank(card.summary) || Array.from(card.summary).length > 300
41
+ || !KINDS.has(card.kind)
42
+ || (Object.hasOwn(card, 'usesHint') && (!Array.isArray(card.usesHint) || card.usesHint.length > 20
43
+ || card.usesHint.some((hint) => !nonBlank(hint) || Array.from(hint).length > 120)))) {
44
+ return fail('note_schema_invalid');
45
+ }
46
+ if (!['item', 'summary', 'kind'].every((key) => Object.hasOwn(card, key)))
47
+ fail('note_schema_invalid');
48
+ }
49
+ return value.cards;
50
+ }
51
+ const extension = (path) => {
52
+ const name = path.replaceAll('\\', '/').split('/').at(-1).toLowerCase();
53
+ const dot = name.lastIndexOf('.');
54
+ return dot < 0 ? '' : name.slice(dot);
55
+ };
56
+ /** Returns only mechanically obvious path classifications; everything else stays model-authored. */
57
+ export function kindFromPath(path) {
58
+ const normalized = path.replaceAll('\\', '/').replace(/^\.\//, '').toLowerCase();
59
+ const parts = normalized.split('/').filter(Boolean);
60
+ const name = parts.at(-1) ?? '';
61
+ const ext = extension(normalized);
62
+ if (parts.some((part) => ['test', 'tests', '__tests__', 'spec', 'specs'].includes(part))
63
+ || /(?:^|\.)(?:test|spec)\.[^.]+$/.test(name))
64
+ return { kind: 'test', rule: 'test path or filename' };
65
+ if (parts.some((part) => ['docs', 'doc', 'documentation'].includes(part))
66
+ || ['.md', '.mdx', '.rst', '.adoc', '.asciidoc'].includes(ext)
67
+ || /^(readme|changelog|contributing|license)(?:\.|$)/.test(name)) {
68
+ return { kind: 'docs', rule: 'documentation path or extension' };
69
+ }
70
+ if (normalized.startsWith('.github/workflows/') || parts.some((part) => ['terraform', 'kubernetes', 'k8s', 'infrastructure', 'infra', 'deploy'].includes(part))
71
+ || ['.tf', '.tfvars'].includes(ext) || /^dockerfile(?:\.|$)/.test(name)
72
+ || /^(docker-compose|compose)\.(?:ya?ml)$/.test(name)) {
73
+ return { kind: 'infrastructure', rule: 'infrastructure path or extension' };
74
+ }
75
+ const genericDataExtension = ['.json', '.jsonc', '.yaml', '.yml', '.toml'].includes(ext);
76
+ const configurationFilename = /(?:^|\.)(?:config|rc)\.[^.]+$/.test(name)
77
+ || /^(package|tsconfig|jsconfig|eslint|prettier|babel|vite|vitest|jest|playwright)\./.test(name)
78
+ || /^\.[a-z0-9_-]+rc(?:\.[a-z0-9]+)?$/.test(name);
79
+ if (['.ini', '.cfg', '.conf'].includes(ext) || configurationFilename
80
+ || (genericDataExtension && (parts.length === 1
81
+ || parts.some((part) => ['config', 'configs', '.config', 'settings', 'conf'].includes(part))))) {
82
+ return { kind: 'config', rule: 'configuration filename or extension' };
83
+ }
84
+ if (parts.some((part) => ['scripts', 'script', 'tools', 'tooling'].includes(part))
85
+ || ['.sh', '.bash', '.zsh', '.fish', '.ps1', '.bat', '.cmd'].includes(ext)
86
+ || /^(makefile|justfile|rakefile)$/.test(name)) {
87
+ return { kind: 'tooling', rule: 'tooling path or extension' };
88
+ }
89
+ return null;
90
+ }
91
+ export function importResolverCoversPath(path) {
92
+ return ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.mts', '.cts'].includes(extension(path));
93
+ }
94
+ function noteId(item) {
95
+ const identity = `${item.item.entryId}\0${item.item.inventoryRevision}\0${item.item.path}`
96
+ + `\0${item.item.primarySpan.startByte}:${item.item.primarySpan.endByte}`;
97
+ return `note_${createHash('sha256').update(identity, 'utf8').digest('hex').slice(0, 24)}`;
98
+ }
99
+ function displaySpan(span, expected) {
100
+ const primary = expected.item.primarySpan;
101
+ if (!Number.isSafeInteger(span.startByte) || !Number.isSafeInteger(span.endByte)
102
+ || span.startByte < primary.startByte || span.endByte > primary.endByte
103
+ || span.endByte <= span.startByte)
104
+ return fail('note_span_invalid');
105
+ const start = expected.decoded.mapping.lines.find((line) => span.startByte >= line.startByte && span.startByte < line.endByte);
106
+ const end = expected.decoded.mapping.lines.find((line) => span.endByte - 1 >= line.startByte && span.endByte - 1 < line.endByte);
107
+ if (!start || !end)
108
+ return fail('note_span_invalid');
109
+ return { ...span, startLine: start.lineNumber, endLine: end.lineNumber };
110
+ }
111
+ /** Validates strict cards, exact sent-item membership, path kinds, and Truing-derived uses. */
112
+ export function validateReadNotes(input) {
113
+ const cards = parseResult(input.result);
114
+ const expected = new Map();
115
+ const identities = new Set();
116
+ input.items.forEach((item, index) => {
117
+ const identity = `${item.item.entryId}\0${item.item.primarySpan.startByte}:${item.item.primarySpan.endByte}`;
118
+ if (!item.item.path || identities.has(identity))
119
+ throw new TypeError('note_validation_input_invalid');
120
+ identities.add(identity);
121
+ expected.set(index + 1, item);
122
+ });
123
+ const supplied = new Map();
124
+ for (const card of cards) {
125
+ if (supplied.has(card.item))
126
+ fail('note_membership_invalid');
127
+ supplied.set(card.item, card);
128
+ }
129
+ if (supplied.size !== expected.size || [...supplied.keys()].some((key) => !expected.has(key))) {
130
+ fail('note_membership_invalid');
131
+ }
132
+ const targetByPath = new Map();
133
+ const pathByTarget = new Map();
134
+ for (const target of input.inventory) {
135
+ if (!target.path || !target.entryId || targetByPath.has(target.path) || pathByTarget.has(target.entryId)) {
136
+ throw new TypeError('note_validation_input_invalid');
137
+ }
138
+ targetByPath.set(target.path, target.entryId);
139
+ pathByTarget.set(target.entryId, target.path);
140
+ }
141
+ return { notes: [...expected].map(([key, item]) => {
142
+ const card = supplied.get(key);
143
+ const pathRule = kindFromPath(item.item.path);
144
+ if (card.usesHint !== undefined && importResolverCoversPath(item.item.path))
145
+ fail('note_schema_invalid');
146
+ const kind = pathRule?.kind ?? card.kind;
147
+ const kindSource = pathRule
148
+ ? { source: 'path_rule', rule: pathRule.rule } : { source: 'index_card', rule: null };
149
+ const fullSpan = displaySpan(item.item.primarySpan, item);
150
+ const uses = input.importEdges
151
+ .filter((edge) => edge.sourceEntryId === item.item.entryId
152
+ && edge.sourceSpan.startByte >= item.item.primarySpan.startByte
153
+ && edge.sourceSpan.endByte <= item.item.primarySpan.endByte
154
+ && pathByTarget.has(edge.targetEntryId))
155
+ .map((edge) => ({
156
+ target: pathByTarget.get(edge.targetEntryId), span: displaySpan(edge.sourceSpan, item),
157
+ classification: 'established_import', targetEntryId: edge.targetEntryId, dependency: null,
158
+ }));
159
+ for (const hint of card.usesHint ?? []) {
160
+ const targetEntryId = targetByPath.get(hint) ?? null;
161
+ uses.push({ target: hint, span: null, classification: targetEntryId ? 'candidate' : 'unvalidated',
162
+ targetEntryId, dependency: null });
163
+ }
164
+ return { path: item.item.path, blobId: item.item.blobId, range: { ...item.item.primarySpan },
165
+ summary: card.summary, kind, kindSource,
166
+ kindReason: { text: pathRule ? `path rule: ${pathRule.rule}` : 'from index card', span: fullSpan },
167
+ facts: [], exposes: [], uses, noteId: noteId(item), entryId: item.item.entryId,
168
+ inventoryRevision: item.item.inventoryRevision };
169
+ }) };
170
+ }
171
+ /** Removes no child data; ordinal is scoped by note id and child kind. */
172
+ export function normalizeReadNotes(value) {
173
+ const notes = [];
174
+ const children = [];
175
+ for (const { facts, exposes, uses, ...note } of value.notes) {
176
+ notes.push(note);
177
+ facts.forEach((fact, ordinal) => children.push({ noteId: note.noteId, childKind: 'fact', ordinal, ...fact }));
178
+ exposes.forEach((expose, ordinal) => children.push({ noteId: note.noteId, childKind: 'expose', ordinal, ...expose }));
179
+ uses.forEach((use, ordinal) => children.push({ noteId: note.noteId, childKind: 'use', ordinal, ...use }));
180
+ }
181
+ return { notes, children };
182
+ }
183
+ /** Inverse of normalizeReadNotes; rejects missing, duplicate, or non-contiguous child ordinals. */
184
+ export function reassembleReadNotes(value) {
185
+ const ids = new Set(value.notes.map((note) => note.noteId));
186
+ if (ids.size !== value.notes.length)
187
+ throw new TypeError('normalized_notes_invalid');
188
+ const grouped = new Map();
189
+ for (const child of value.children) {
190
+ if (!ids.has(child.noteId) || !['fact', 'expose', 'use'].includes(child.childKind)
191
+ || !Number.isSafeInteger(child.ordinal) || child.ordinal < 0
192
+ || (child.childKind === 'use' && child.classification.startsWith('established_') && !child.span)) {
193
+ throw new TypeError('normalized_notes_invalid');
194
+ }
195
+ const key = `${child.noteId}\0${child.childKind}`;
196
+ const found = grouped.get(key) ?? [];
197
+ if (found.some((candidate) => candidate.ordinal === child.ordinal))
198
+ throw new TypeError('normalized_notes_invalid');
199
+ found.push(child);
200
+ grouped.set(key, found);
201
+ }
202
+ const children = (noteId, kind) => {
203
+ const found = (grouped.get(`${noteId}\0${kind}`) ?? []).sort((left, right) => left.ordinal - right.ordinal);
204
+ if (found.some((child, ordinal) => child.ordinal !== ordinal))
205
+ throw new TypeError('normalized_notes_invalid');
206
+ return found;
207
+ };
208
+ return { notes: value.notes.map((note) => ({ ...note,
209
+ facts: children(note.noteId, 'fact')
210
+ .map(({ noteId: _id, childKind: _kind, ordinal: _ordinal, ...fact }) => fact),
211
+ exposes: children(note.noteId, 'expose')
212
+ .map(({ noteId: _id, childKind: _kind, ordinal: _ordinal, ...expose }) => expose),
213
+ uses: children(note.noteId, 'use')
214
+ .map(({ noteId: _id, childKind: _kind, ordinal: _ordinal, ...use }) => use),
215
+ })) };
216
+ }
@@ -0,0 +1,272 @@
1
+ import { createHash, timingSafeEqual } from 'node:crypto';
2
+ import { createRequire } from 'node:module';
3
+ import { Readable, Transform } from 'node:stream';
4
+ import { createGunzip } from 'node:zlib';
5
+ const require = createRequire(import.meta.url);
6
+ const tar = require('tar-stream');
7
+ const REGISTRY_ORIGIN = 'https://registry.npmjs.org';
8
+ const COMPRESSED_LIMIT = 64 * 1024 * 1024;
9
+ const INSPECTED_LIMIT = 256 * 1024 * 1024;
10
+ const MEMBER_LIMIT = 100_000;
11
+ const RULES_VERSION = 'npm-proof@1';
12
+ const PACKAGE_NAME = /^(?:@[^/@\s]+\/)?[^/@\s]+$/;
13
+ const SEMVER = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
14
+ const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
15
+ const readResult = (reason) => ({ kind: 'read', reason });
16
+ function validIdentity(name, version) {
17
+ return typeof name === 'string' && typeof version === 'string'
18
+ && PACKAGE_NAME.test(name) && SEMVER.test(version);
19
+ }
20
+ function strictUtf8(bytes) {
21
+ try {
22
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes).replace(/^\uFEFF/, '');
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ function parsePackageJson(bytes) {
29
+ const text = strictUtf8(bytes);
30
+ if (text === null)
31
+ return null;
32
+ try {
33
+ const parsed = JSON.parse(text);
34
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
35
+ return null;
36
+ const record = parsed;
37
+ return validIdentity(record.name, record.version)
38
+ ? { name: record.name, version: record.version }
39
+ : null;
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ function safeRepositoryPath(path) {
46
+ return path.length > 0 && !path.startsWith('/') && !path.includes('\\')
47
+ && path.split('/').every((part) => part.length > 0 && part !== '.' && part !== '..');
48
+ }
49
+ function packageIdentity(input) {
50
+ try {
51
+ if (!safeRepositoryPath(input.path))
52
+ return null;
53
+ const parts = input.path.split('/');
54
+ for (let length = parts.length; length > 0; length -= 1) {
55
+ const root = parts.slice(0, length - 1).join('/');
56
+ const manifestPath = root.length === 0 ? 'package.json' : `${root}/package.json`;
57
+ const manifest = manifestPath === input.path ? input.bytes : input.getFile(manifestPath);
58
+ if (manifest === null)
59
+ continue;
60
+ const parsed = parsePackageJson(manifest);
61
+ if (parsed === null)
62
+ return null;
63
+ return { ...parsed, root };
64
+ }
65
+ return null;
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ function relativePackagePath(path, root) {
72
+ const relative = root.length === 0 ? path : path.slice(root.length + 1);
73
+ return safeRepositoryPath(relative) ? relative : null;
74
+ }
75
+ function metadataUrl(name, version) {
76
+ return `${REGISTRY_ORIGIN}/${encodeURIComponent(name)}/${encodeURIComponent(version)}`;
77
+ }
78
+ function expectedTarballUrl(name, version) {
79
+ const leaf = name.includes('/') ? name.slice(name.lastIndexOf('/') + 1) : name;
80
+ return `${REGISTRY_ORIGIN}/${name}/-/${leaf}-${version}.tgz`;
81
+ }
82
+ function verifiedIntegrity(integrity, bytes) {
83
+ if (typeof integrity !== 'string')
84
+ return false;
85
+ const match = /^sha(256|384|512)-([A-Za-z0-9+/]+={0,2})$/.exec(integrity);
86
+ if (match === null)
87
+ return false;
88
+ const expected = Buffer.from(match[2], 'base64');
89
+ const actual = createHash(`sha${match[1]}`).update(bytes).digest();
90
+ return expected.length === actual.length && timingSafeEqual(expected, actual);
91
+ }
92
+ async function boundedFetch(transport, url) {
93
+ try {
94
+ const bytes = await transport.fetchRegistry(url);
95
+ return bytes instanceof Uint8Array && bytes.byteLength <= COMPRESSED_LIMIT ? bytes : null;
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ }
101
+ async function fetchArtifact(name, version, transport) {
102
+ const metadataBytes = await boundedFetch(transport, metadataUrl(name, version));
103
+ const metadataText = metadataBytes === null ? null : strictUtf8(metadataBytes);
104
+ if (metadataText === null)
105
+ return null;
106
+ try {
107
+ const metadata = JSON.parse(metadataText);
108
+ if (metadata === null || typeof metadata !== 'object' || Array.isArray(metadata))
109
+ return null;
110
+ const record = metadata;
111
+ const dist = record.dist;
112
+ if (!validIdentity(record.name, record.version) || record.name !== name || record.version !== version
113
+ || dist === null || typeof dist !== 'object' || Array.isArray(dist))
114
+ return null;
115
+ const distRecord = dist;
116
+ if (distRecord.tarball !== expectedTarballUrl(name, version))
117
+ return null;
118
+ const bytes = await boundedFetch(transport, distRecord.tarball);
119
+ if (bytes === null || !verifiedIntegrity(distRecord.integrity, bytes))
120
+ return null;
121
+ return {
122
+ bytes,
123
+ integrity: distRecord.integrity,
124
+ name,
125
+ tarballSha256: sha256(bytes),
126
+ version,
127
+ };
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ }
133
+ function safeArchiveMemberPath(path) {
134
+ if (!path.startsWith('package/') || path.includes('\\') || path.includes('\0'))
135
+ return false;
136
+ return path.split('/').every((part, index, parts) => (index === parts.length - 1 && part.length === 0) || (part.length > 0 && part !== '.' && part !== '..'));
137
+ }
138
+ async function inspectArtifact(bytes) {
139
+ return new Promise((resolve) => {
140
+ const source = Readable.from([Buffer.from(bytes)]);
141
+ const gunzip = createGunzip();
142
+ const extractor = tar.extract();
143
+ const members = [];
144
+ const seen = new Set();
145
+ let inspected = 0;
146
+ let memberCount = 0;
147
+ let settled = false;
148
+ const finish = (result) => {
149
+ if (settled)
150
+ return;
151
+ settled = true;
152
+ source.destroy();
153
+ gunzip.destroy();
154
+ extractor.destroy();
155
+ resolve(result);
156
+ };
157
+ const fail = (reason) => finish({ ok: false, reason });
158
+ const counter = new Transform({
159
+ transform(chunk, _encoding, callback) {
160
+ inspected += chunk.length;
161
+ callback(inspected > INSPECTED_LIMIT ? new Error('uncompressed_limit') : null, chunk);
162
+ },
163
+ });
164
+ extractor.on('entry', (header, stream, next) => {
165
+ memberCount += 1;
166
+ if (memberCount > MEMBER_LIMIT) {
167
+ stream.resume();
168
+ next(new Error('member_limit'));
169
+ return;
170
+ }
171
+ if (!safeArchiveMemberPath(header.name) || seen.has(header.name)
172
+ || (header.type !== 'file' && header.type !== 'directory')) {
173
+ stream.resume();
174
+ next(new Error('unsafe_archive'));
175
+ return;
176
+ }
177
+ seen.add(header.name);
178
+ const hash = createHash('sha256');
179
+ stream.on('data', (chunk) => hash.update(chunk));
180
+ stream.once('error', () => fail('unsafe_archive'));
181
+ stream.once('end', () => {
182
+ if (header.type === 'file') {
183
+ members.push({ path: header.name.slice('package/'.length), sha256: hash.digest('hex') });
184
+ }
185
+ next();
186
+ });
187
+ });
188
+ extractor.on('finish', () => finish({ ok: true, members }));
189
+ extractor.on('error', (error) => fail(error.message));
190
+ gunzip.on('error', () => fail('corrupt_artifact'));
191
+ counter.on('error', (error) => fail(error.message));
192
+ source.on('error', () => fail('corrupt_artifact'));
193
+ source.pipe(gunzip).pipe(counter).pipe(extractor);
194
+ });
195
+ }
196
+ function proof(artifact, pathInPackage, localSha256, registrySha256) {
197
+ return {
198
+ ecosystem: 'npm',
199
+ registry: REGISTRY_ORIGIN,
200
+ packageName: artifact.name,
201
+ version: artifact.version,
202
+ pathInPackage,
203
+ localSha256,
204
+ registrySha256,
205
+ tarballSha256: artifact.tarballSha256,
206
+ tarballIntegrity: artifact.integrity,
207
+ rulesVersion: RULES_VERSION,
208
+ };
209
+ }
210
+ export async function proveNpmEntry(input, registry) {
211
+ const identity = packageIdentity(input);
212
+ if (identity === null)
213
+ return readResult('npm_package_root_unproven');
214
+ const pathInPackage = relativePackagePath(input.path, identity.root);
215
+ if (pathInPackage === null)
216
+ return readResult('npm_package_path_unproven');
217
+ const artifact = await fetchArtifact(identity.name, identity.version, registry);
218
+ if (artifact === null)
219
+ return readResult(`npm_artifact_unproven:${identity.name}@${identity.version}`);
220
+ let inspection;
221
+ try {
222
+ inspection = await inspectArtifact(artifact.bytes);
223
+ }
224
+ catch {
225
+ return readResult(`npm_artifact_unproven:${identity.name}@${identity.version}`);
226
+ }
227
+ if (!inspection.ok)
228
+ return readResult(`npm_artifact_unproven:${inspection.reason}`);
229
+ const member = inspection.members.find((entry) => entry.path === pathInPackage);
230
+ if (member === undefined)
231
+ return readResult(`npm_artifact_member_missing:${identity.name}@${identity.version}`);
232
+ const localSha256 = sha256(input.bytes);
233
+ if (localSha256 !== member.sha256)
234
+ return readResult(`modified copy of ${identity.name}@${identity.version}`);
235
+ return { kind: 'vendored_identical', proof: proof(artifact, pathInPackage, localSha256, member.sha256) };
236
+ }
237
+ function bannerIdentity(bytes) {
238
+ const text = strictUtf8(bytes);
239
+ if (text === null)
240
+ return null;
241
+ const match = /(?:\/\*!\s+|@license\s+)(@?[^\s]+)\s+v([^\s*]+)/.exec(text);
242
+ return match !== null && validIdentity(match[1], match[2]) ? { name: match[1], version: match[2] } : null;
243
+ }
244
+ export async function proveNpmBanner(input, registry) {
245
+ if (!safeRepositoryPath(input.path))
246
+ return readResult('npm_banner_path_unproven');
247
+ const identity = bannerIdentity(input.bytes);
248
+ if (identity === null)
249
+ return readResult('npm_banner_not_applicable');
250
+ const artifact = await fetchArtifact(identity.name, identity.version, registry);
251
+ if (artifact === null)
252
+ return readResult(`npm_banner_artifact_unproven:${identity.name}@${identity.version}`);
253
+ let inspection;
254
+ try {
255
+ inspection = await inspectArtifact(artifact.bytes);
256
+ }
257
+ catch {
258
+ return readResult(`npm_banner_artifact_unproven:${identity.name}@${identity.version}`);
259
+ }
260
+ if (!inspection.ok)
261
+ return readResult(`npm_banner_artifact_unproven:${inspection.reason}`);
262
+ const localSha256 = sha256(input.bytes);
263
+ const matches = inspection.members.filter((entry) => entry.sha256 === localSha256);
264
+ if (matches.length !== 1)
265
+ return readResult(matches.length === 0
266
+ ? `npm_banner_not_identical:${identity.name}@${identity.version}`
267
+ : `npm_banner_ambiguous_artifact:${identity.name}@${identity.version}`);
268
+ return {
269
+ kind: 'library_banner_identical',
270
+ proof: proof(artifact, matches[0].path, localSha256, matches[0].sha256),
271
+ };
272
+ }