@massu/core 1.10.8 → 1.12.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.
- package/CHANGELOG.md +1334 -0
- package/dist/adapter.js +6 -4
- package/dist/cli.js +581 -448
- package/dist/detect/adapters/.bundle-shasums.json +4 -7
- package/dist/detect/adapters/rails.js +6 -4
- package/dist/detect/adapters/spring.js +6 -4
- package/dist/detect/adapters/tree-sitter-loader.js +6 -4
- package/dist/hooks/auto-learning-pipeline.js +24 -1
- package/dist/hooks/classify-failure.js +32 -3
- package/dist/hooks/cost-tracker.js +24 -0
- package/dist/hooks/fix-detector.js +97 -1
- package/dist/hooks/incident-pipeline.js +30 -1
- package/dist/hooks/intent-suggester.js +6 -3
- package/dist/hooks/post-edit-context.js +24 -1
- package/dist/hooks/post-tool-use.js +67 -19
- package/dist/hooks/pre-compact.js +28 -3
- package/dist/hooks/pre-delete-check.js +53 -27
- package/dist/hooks/pre-tool-use-gate.js +962 -0
- package/dist/hooks/quality-event.js +24 -0
- package/dist/hooks/rule-enforcement-pipeline.js +24 -1
- package/dist/hooks/security-gate.js +29 -16
- package/dist/hooks/session-end.js +31 -6
- package/dist/hooks/session-start.js +8709 -8459
- package/dist/hooks/user-prompt.js +25 -1
- package/package.json +15 -18
- package/commands/README.md +0 -134
- package/src/adapter.ts +0 -121
- package/src/adr-generator.ts +0 -292
- package/src/analytics.ts +0 -373
- package/src/audit-trail.ts +0 -450
- package/src/backfill-sessions.ts +0 -182
- package/src/changelog-generator.ts +0 -178
- package/src/cli.ts +0 -246
- package/src/cloud-sync.ts +0 -234
- package/src/commands/adapters.ts +0 -824
- package/src/commands/changelog.ts +0 -165
- package/src/commands/config-check-drift.ts +0 -133
- package/src/commands/config-refresh.ts +0 -396
- package/src/commands/config-upgrade.ts +0 -127
- package/src/commands/doctor.ts +0 -485
- package/src/commands/hook-runner.ts +0 -145
- package/src/commands/init.ts +0 -1823
- package/src/commands/install-commands.ts +0 -692
- package/src/commands/install-hooks.ts +0 -27
- package/src/commands/permissions.ts +0 -150
- package/src/commands/refresh-log.ts +0 -37
- package/src/commands/show-template.ts +0 -65
- package/src/commands/template-engine.ts +0 -301
- package/src/commands/watch.ts +0 -430
- package/src/config.ts +0 -765
- package/src/cost-tracker.ts +0 -355
- package/src/db.ts +0 -370
- package/src/dependency-scorer.ts +0 -337
- package/src/detect/adapters/aspnet.ts +0 -4
- package/src/detect/adapters/discover.ts +0 -469
- package/src/detect/adapters/file-sampler.ts +0 -225
- package/src/detect/adapters/go-chi.ts +0 -4
- package/src/detect/adapters/index.ts +0 -51
- package/src/detect/adapters/nextjs-trpc.ts +0 -166
- package/src/detect/adapters/parse-guard.ts +0 -133
- package/src/detect/adapters/phoenix.ts +0 -4
- package/src/detect/adapters/python-django.ts +0 -208
- package/src/detect/adapters/python-fastapi.ts +0 -223
- package/src/detect/adapters/python-flask.ts +0 -235
- package/src/detect/adapters/query-helpers.ts +0 -170
- package/src/detect/adapters/rails.ts +0 -4
- package/src/detect/adapters/runner.ts +0 -284
- package/src/detect/adapters/spring.ts +0 -4
- package/src/detect/adapters/swift-swiftui.ts +0 -171
- package/src/detect/adapters/tree-sitter-loader.ts +0 -517
- package/src/detect/adapters/types.ts +0 -191
- package/src/detect/codebase-introspector.ts +0 -210
- package/src/detect/domain-inferrer.ts +0 -197
- package/src/detect/drift.ts +0 -199
- package/src/detect/framework-detector.ts +0 -307
- package/src/detect/index.ts +0 -209
- package/src/detect/manifest-registry.ts +0 -261
- package/src/detect/migrate.ts +0 -362
- package/src/detect/monorepo-detector.ts +0 -348
- package/src/detect/package-detector.ts +0 -828
- package/src/detect/passthrough.ts +0 -108
- package/src/detect/regex-fallback.ts +0 -449
- package/src/detect/source-dir-detector.ts +0 -297
- package/src/detect/vr-command-map.ts +0 -167
- package/src/docs-map.json +0 -100
- package/src/docs-tools.ts +0 -519
- package/src/domains.ts +0 -182
- package/src/hooks/auto-learning-pipeline.ts +0 -228
- package/src/hooks/classify-failure.ts +0 -259
- package/src/hooks/cost-tracker.ts +0 -66
- package/src/hooks/fix-detector.ts +0 -186
- package/src/hooks/incident-pipeline.ts +0 -190
- package/src/hooks/intent-suggester.ts +0 -131
- package/src/hooks/post-edit-context.ts +0 -91
- package/src/hooks/post-tool-use.ts +0 -413
- package/src/hooks/pre-compact.ts +0 -168
- package/src/hooks/pre-delete-check.ts +0 -243
- package/src/hooks/quality-event.ts +0 -127
- package/src/hooks/rule-enforcement-pipeline.ts +0 -159
- package/src/hooks/security-gate.ts +0 -153
- package/src/hooks/session-end.ts +0 -467
- package/src/hooks/session-start.ts +0 -436
- package/src/hooks/user-prompt.ts +0 -157
- package/src/import-resolver.ts +0 -226
- package/src/knowledge-db.ts +0 -176
- package/src/knowledge-indexer.ts +0 -708
- package/src/knowledge-tools.ts +0 -1414
- package/src/lib/fileLock.ts +0 -203
- package/src/lib/framework-supports.ts +0 -85
- package/src/lib/gitToplevel.ts +0 -22
- package/src/lib/hook-registry.ts +0 -43
- package/src/lib/installLock.ts +0 -66
- package/src/lib/memory-path.ts +0 -49
- package/src/lib/pidLiveness.ts +0 -67
- package/src/lib/settings-local.ts +0 -110
- package/src/lib/sql-table-names.ts +0 -71
- package/src/license.ts +0 -547
- package/src/lsp/auto-detect.ts +0 -98
- package/src/lsp/client.ts +0 -776
- package/src/lsp/enrich.ts +0 -127
- package/src/lsp/types.ts +0 -221
- package/src/memory-db.ts +0 -1614
- package/src/memory-file-ingest.ts +0 -128
- package/src/memory-tools.ts +0 -424
- package/src/middleware-tree.ts +0 -71
- package/src/observability-tools.ts +0 -343
- package/src/observation-extractor.ts +0 -418
- package/src/page-deps.ts +0 -285
- package/src/permissions.ts +0 -422
- package/src/prompt-analyzer.ts +0 -332
- package/src/python/coupling-detector.ts +0 -125
- package/src/python/domain-enforcer.ts +0 -84
- package/src/python/impact-analyzer.ts +0 -96
- package/src/python/import-parser.ts +0 -244
- package/src/python/import-resolver.ts +0 -136
- package/src/python/migration-indexer.ts +0 -116
- package/src/python/migration-parser.ts +0 -332
- package/src/python/model-indexer.ts +0 -71
- package/src/python/model-parser.ts +0 -279
- package/src/python/route-indexer.ts +0 -59
- package/src/python/route-parser.ts +0 -317
- package/src/python-tools.ts +0 -630
- package/src/regression-detector.ts +0 -319
- package/src/rules.ts +0 -57
- package/src/schema-mapper.ts +0 -232
- package/src/security/adapter-origin.ts +0 -130
- package/src/security/adapter-verifier.ts +0 -319
- package/src/security/atomic-write.ts +0 -164
- package/src/security/fetcher.ts +0 -200
- package/src/security/install-tracking.ts +0 -319
- package/src/security/license-pubkey.generated.ts +0 -30
- package/src/security/license-response-verifier.ts +0 -149
- package/src/security/local-fingerprint.ts +0 -225
- package/src/security/manifest-cache.ts +0 -333
- package/src/security/manifest-schema.ts +0 -129
- package/src/security/registry-pubkey.generated.ts +0 -35
- package/src/security/telemetry.ts +0 -320
- package/src/security-scorer.ts +0 -405
- package/src/security-utils.ts +0 -133
- package/src/sentinel-db.ts +0 -580
- package/src/sentinel-scanner.ts +0 -406
- package/src/sentinel-tools.ts +0 -513
- package/src/sentinel-types.ts +0 -140
- package/src/server-dispatch.ts +0 -225
- package/src/server.ts +0 -107
- package/src/session-archiver.ts +0 -111
- package/src/session-state-generator.ts +0 -174
- package/src/team-knowledge.ts +0 -407
- package/src/tool-db-needs.ts +0 -232
- package/src/tools.ts +0 -1206
- package/src/transcript-parser.ts +0 -458
- package/src/trpc-index.ts +0 -215
- package/src/validate-features-runner.ts +0 -107
- package/src/validation-engine.ts +0 -358
- package/src/watch/daemon.ts +0 -385
- package/src/watch/lockfile-detector.ts +0 -65
- package/src/watch/paths.ts +0 -279
- package/src/watch/state.ts +0 -178
|
@@ -1,517 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Plan 3b — Phase 1: Tree-sitter WASM grammar loader (Strategy A).
|
|
6
|
-
*
|
|
7
|
-
* Strategy A: grammars are NOT bundled in the npm tarball. The loader downloads
|
|
8
|
-
* each requested grammar at first use from a pinned URL, verifies SHA-256
|
|
9
|
-
* against a hardcoded manifest, caches under `~/.massu/wasm-cache/`.
|
|
10
|
-
*
|
|
11
|
-
* Security model (Phase 3.5 #3):
|
|
12
|
-
* - SHA-256 manifest hardcoded HERE — never network-fetched.
|
|
13
|
-
* - Mismatch → throw `GrammarSHAMismatchError`. NO silent fallback.
|
|
14
|
-
* - Atomic cache write: `<lang>-<sha>.wasm.tmp.<pid>` → rename → final.
|
|
15
|
-
* - Offline + no-cache → throw `GrammarUnavailableError` so the runner can
|
|
16
|
-
* translate to a regex-fallback path with a stderr note.
|
|
17
|
-
*
|
|
18
|
-
* Phase 1 ships the CODE PATH; the actual SHA-256 values for each grammar
|
|
19
|
-
* URL are placeholders pending Phase 9 release-prep (`curl <url> | shasum
|
|
20
|
-
* -a 256`). The placeholder string is intentionally non-empty so the
|
|
21
|
-
* verification logic exercises the comparison branch in tests.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import { createHash } from 'crypto';
|
|
25
|
-
import {
|
|
26
|
-
mkdirSync,
|
|
27
|
-
readdirSync,
|
|
28
|
-
readFileSync,
|
|
29
|
-
writeFileSync,
|
|
30
|
-
renameSync,
|
|
31
|
-
unlinkSync,
|
|
32
|
-
lstatSync,
|
|
33
|
-
chmodSync,
|
|
34
|
-
utimesSync,
|
|
35
|
-
} from 'fs';
|
|
36
|
-
import { homedir } from 'os';
|
|
37
|
-
import { dirname, join } from 'path';
|
|
38
|
-
import { Language, Parser } from 'web-tree-sitter';
|
|
39
|
-
import type { TreeSitterLanguage } from './types.ts';
|
|
40
|
-
|
|
41
|
-
// ============================================================
|
|
42
|
-
// Typed errors
|
|
43
|
-
// ============================================================
|
|
44
|
-
|
|
45
|
-
/** Thrown when downloaded WASM SHA-256 doesn't match the hardcoded manifest. */
|
|
46
|
-
export class GrammarSHAMismatchError extends Error {
|
|
47
|
-
public readonly language: TreeSitterLanguage;
|
|
48
|
-
public readonly expected: string;
|
|
49
|
-
public readonly actual: string;
|
|
50
|
-
constructor(language: TreeSitterLanguage, expected: string, actual: string) {
|
|
51
|
-
super(
|
|
52
|
-
`[tree-sitter-loader] SHA-256 mismatch for grammar "${language}". ` +
|
|
53
|
-
`Expected ${expected}, got ${actual}. ` +
|
|
54
|
-
`REFUSING to load — see Phase 3.5 audit attack vector #3.`,
|
|
55
|
-
);
|
|
56
|
-
this.name = 'GrammarSHAMismatchError';
|
|
57
|
-
this.language = language;
|
|
58
|
-
this.expected = expected;
|
|
59
|
-
this.actual = actual;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Thrown when a grammar can't be obtained: download failed AND cache empty. */
|
|
64
|
-
export class GrammarUnavailableError extends Error {
|
|
65
|
-
public readonly language: TreeSitterLanguage;
|
|
66
|
-
public readonly cause?: unknown;
|
|
67
|
-
constructor(language: TreeSitterLanguage, cause?: unknown) {
|
|
68
|
-
const causeMsg =
|
|
69
|
-
cause instanceof Error ? cause.message : cause ? String(cause) : 'no cached grammar and download failed';
|
|
70
|
-
super(
|
|
71
|
-
`[tree-sitter-loader] Grammar for "${language}" is unavailable: ${causeMsg}. ` +
|
|
72
|
-
`Falling back to regex introspection for files in ${language}.`,
|
|
73
|
-
);
|
|
74
|
-
this.name = 'GrammarUnavailableError';
|
|
75
|
-
this.language = language;
|
|
76
|
-
this.cause = cause;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Thrown when the cache path resolves to a symlink (or any non-regular
|
|
82
|
-
* file). Pre-creating a symlink at the expected cache path is a known
|
|
83
|
-
* vector for redirecting reads/writes elsewhere on the filesystem.
|
|
84
|
-
* (Phase 3.5 finding #3 — symlink attack on cache dir.)
|
|
85
|
-
*/
|
|
86
|
-
export class GrammarCacheSymlinkError extends Error {
|
|
87
|
-
public readonly cachePath: string;
|
|
88
|
-
constructor(cachePath: string) {
|
|
89
|
-
super(
|
|
90
|
-
`[tree-sitter-loader] Refusing to load grammar — cache path "${cachePath}" is a symlink ` +
|
|
91
|
-
`or non-regular file. (Phase 3.5 finding #3 — symlink attack vector.)`,
|
|
92
|
-
);
|
|
93
|
-
this.name = 'GrammarCacheSymlinkError';
|
|
94
|
-
this.cachePath = cachePath;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Thrown when a manifest URL is not HTTPS. The manifest is hardcoded in
|
|
100
|
-
* source, but defense in depth: any future edit that introduces an http://
|
|
101
|
-
* URL is rejected at load time, not at code review.
|
|
102
|
-
* (Phase 3.5 finding #3 — MITM on download.)
|
|
103
|
-
*/
|
|
104
|
-
export class GrammarUrlNotHttpsError extends Error {
|
|
105
|
-
public readonly url: string;
|
|
106
|
-
constructor(url: string) {
|
|
107
|
-
super(
|
|
108
|
-
`[tree-sitter-loader] Refusing to download grammar from non-HTTPS URL: ${url}. ` +
|
|
109
|
-
`Only https:// URLs are accepted. (Phase 3.5 finding #3.)`,
|
|
110
|
-
);
|
|
111
|
-
this.name = 'GrammarUrlNotHttpsError';
|
|
112
|
-
this.url = url;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ============================================================
|
|
117
|
-
// Pinned manifest
|
|
118
|
-
// ============================================================
|
|
119
|
-
|
|
120
|
-
interface ManifestEntry {
|
|
121
|
-
url: string;
|
|
122
|
-
sha256: string;
|
|
123
|
-
version: string;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Hardcoded grammar manifest. Source-code-resident; tampering requires a
|
|
128
|
-
* release.
|
|
129
|
-
*
|
|
130
|
-
* Source: `tree-sitter-wasms` npm package (https://npm.im/tree-sitter-wasms)
|
|
131
|
-
* — pre-built WASM binaries for Tree-sitter language parsers. NOT added as
|
|
132
|
-
* a dependency (per plan §Phase 0 ban on bundling); fetched from unpkg at
|
|
133
|
-
* first use. The individual `tree-sitter-<lang>` packages on npm do NOT
|
|
134
|
-
* ship `.wasm` files, only C source + native .node prebuilds — confirmed
|
|
135
|
-
* by inspecting unpkg `?meta` listings during Phase 9 release-prep.
|
|
136
|
-
*
|
|
137
|
-
* SHA-256 hashes computed 2026-04-28 via:
|
|
138
|
-
* curl -fsSL <url> | shasum -a 256
|
|
139
|
-
*
|
|
140
|
-
* The verification code path is exercised in `tree-sitter-loader.test.ts`
|
|
141
|
-
* by injecting test manifest entries that intentionally mismatch.
|
|
142
|
-
*/
|
|
143
|
-
export const GRAMMAR_MANIFEST: Partial<Record<TreeSitterLanguage, ManifestEntry>> = {
|
|
144
|
-
python: {
|
|
145
|
-
url: 'https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-python.wasm',
|
|
146
|
-
sha256: '9056d0fb0c337810d019fae350e8167786119da98f0f282aceae7ab89ee8253b',
|
|
147
|
-
version: '0.1.13',
|
|
148
|
-
},
|
|
149
|
-
typescript: {
|
|
150
|
-
url: 'https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-typescript.wasm',
|
|
151
|
-
sha256: '8515404dceed38e1ed86aa34b09fcf3379fff1b4ff9dd3967bcd6d1eb5ac3d8f',
|
|
152
|
-
version: '0.1.13',
|
|
153
|
-
},
|
|
154
|
-
javascript: {
|
|
155
|
-
url: 'https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-javascript.wasm',
|
|
156
|
-
sha256: '63812b9e275d26851264734868d27a1656bd44a2ef6eb3e85e6b03728c595ab5',
|
|
157
|
-
version: '0.1.13',
|
|
158
|
-
},
|
|
159
|
-
swift: {
|
|
160
|
-
url: 'https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-swift.wasm',
|
|
161
|
-
sha256: '41c4fdb2249a3aa6d87eed0d383081ff09725c2248b4977043a43825980ffcc7',
|
|
162
|
-
version: '0.1.13',
|
|
163
|
-
},
|
|
164
|
-
// ----------------------------------------------------------------
|
|
165
|
-
// Plan 3c Phase 7 expansion (2026-05-07):
|
|
166
|
-
//
|
|
167
|
-
// Six additional grammars to support the registry-verified framework
|
|
168
|
-
// adapters (go-chi, rails, aspnet, spring, ktor, phoenix) plus the
|
|
169
|
-
// bundled adapters in the same language families (gin/echo/fiber,
|
|
170
|
-
// sinatra, etc.). All entries use the SAME pinned tree-sitter-wasms
|
|
171
|
-
// version (0.1.13) as the v1 four to keep the dependency surface
|
|
172
|
-
// single-source.
|
|
173
|
-
//
|
|
174
|
-
// SHA-256s computed 2026-05-07 via:
|
|
175
|
-
// curl -fsSL <url> | shasum -a 256
|
|
176
|
-
//
|
|
177
|
-
// The unpkg filename for C# uses an underscore (`c_sharp`) while the
|
|
178
|
-
// TreeSitterLanguage identifier uses no separator (`csharp`); the map
|
|
179
|
-
// key is the type identifier, the URL is the storage path — they do
|
|
180
|
-
// NOT need to match, the same as how `python` maps to `tree-sitter-
|
|
181
|
-
// python.wasm`. This is intentional and validated by the manifest
|
|
182
|
-
// shape test in tree-sitter-loader-manifest.test.ts.
|
|
183
|
-
// ----------------------------------------------------------------
|
|
184
|
-
go: {
|
|
185
|
-
url: 'https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-go.wasm',
|
|
186
|
-
sha256: '9963ca89b616eaf04b08a43bc1fb0f07b85395bec313330851f1f1ead2f755b6',
|
|
187
|
-
version: '0.1.13',
|
|
188
|
-
},
|
|
189
|
-
ruby: {
|
|
190
|
-
url: 'https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-ruby.wasm',
|
|
191
|
-
sha256: '93a5022855314cdb45458c7bb026a24a0ebc3a5ff6439e542e881f14dfa13a39',
|
|
192
|
-
version: '0.1.13',
|
|
193
|
-
},
|
|
194
|
-
csharp: {
|
|
195
|
-
url: 'https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-c_sharp.wasm',
|
|
196
|
-
sha256: '6266a7e32d68a3459104d994dc848df15d5672b0ea8e86d327274b694f8e6991',
|
|
197
|
-
version: '0.1.13',
|
|
198
|
-
},
|
|
199
|
-
java: {
|
|
200
|
-
url: 'https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-java.wasm',
|
|
201
|
-
sha256: '637aac4415fb39a211a4f4292d63c66b5ce9c32fa2cd35464af4f681d91b9a1f',
|
|
202
|
-
version: '0.1.13',
|
|
203
|
-
},
|
|
204
|
-
kotlin: {
|
|
205
|
-
url: 'https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-kotlin.wasm',
|
|
206
|
-
sha256: 'b5cb00c8d06ed0f10f1dbe497205b437809d7e87db1f638721a8cfb30e044449',
|
|
207
|
-
version: '0.1.13',
|
|
208
|
-
},
|
|
209
|
-
elixir: {
|
|
210
|
-
url: 'https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-elixir.wasm',
|
|
211
|
-
sha256: '82e91b9759ddca30d8978ebbfa8e347b4451b64c931f9ae62112e6db9b8fac20',
|
|
212
|
-
version: '0.1.13',
|
|
213
|
-
},
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
// ============================================================
|
|
217
|
-
// Cache + Parser init
|
|
218
|
-
// ============================================================
|
|
219
|
-
|
|
220
|
-
function getCacheDir(): string {
|
|
221
|
-
return process.env.MASSU_WASM_CACHE_DIR ?? join(homedir(), '.massu', 'wasm-cache');
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function getCachedPath(language: TreeSitterLanguage, sha: string): string {
|
|
225
|
-
return join(getCacheDir(), `${language}-${sha}.wasm`);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// ============================================================
|
|
229
|
-
// LRU cache eviction (Phase 3.5 audit F-011 — closed 2026-05-06)
|
|
230
|
-
// ============================================================
|
|
231
|
-
//
|
|
232
|
-
// F-011 was deferred at v1 ("at ~3MB per grammar, full cache footprint is
|
|
233
|
-
// <100MB — not an attack vector"). The 2026-05-06 audit-leak retrospective
|
|
234
|
-
// elevated it: now that the cache path + naming convention are publicly
|
|
235
|
-
// known (the security audit doc was visible for 9 days), opportunistic
|
|
236
|
-
// disk-fill attacks become slightly less hypothetical, AND the cost of
|
|
237
|
-
// retrofitting LRU once Plan 3c expands the supported grammar set is
|
|
238
|
-
// strictly higher than doing it now while only 4 grammars exist.
|
|
239
|
-
//
|
|
240
|
-
// Eviction rule: keep the N most-recently-USED entries (mtime, updated by
|
|
241
|
-
// the cache-hit path on every read). Default cap = 16 — leaves headroom
|
|
242
|
-
// for Plan 3c's 31-grammar expansion plus dev-time version churn, while
|
|
243
|
-
// bounding total cache to ~50MB at 3MB/grammar.
|
|
244
|
-
|
|
245
|
-
const DEFAULT_CACHE_RETAIN_COUNT = 16;
|
|
246
|
-
|
|
247
|
-
function getCacheRetainCount(): number {
|
|
248
|
-
const env = process.env.MASSU_WASM_CACHE_RETAIN;
|
|
249
|
-
if (env) {
|
|
250
|
-
const n = Number(env);
|
|
251
|
-
if (Number.isFinite(n) && n >= 1 && n <= 1024) return Math.floor(n);
|
|
252
|
-
}
|
|
253
|
-
return DEFAULT_CACHE_RETAIN_COUNT;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Touch a cache file's mtime to mark "most recently used." Called on every
|
|
258
|
-
* cache-hit. Best-effort: any failure is silently swallowed — touching is
|
|
259
|
-
* an optimization signal for eviction, not load-bearing.
|
|
260
|
-
*
|
|
261
|
-
* Uses utimes via writeFileSync round-trip would be expensive; instead we
|
|
262
|
-
* use the same filesystem touch trick as `touch -a`: open + close. On
|
|
263
|
-
* macOS/Linux Node, `chmodSync` to the same mode does NOT update mtime,
|
|
264
|
-
* so we do a no-op write of empty content via a tmp marker file. Cheaper
|
|
265
|
-
* approach: just rely on atime if filesystem records it. Most modern
|
|
266
|
-
* filesystems are mounted with `relatime` so atime updates only when
|
|
267
|
-
* older than mtime — which means after our first eviction-relevant
|
|
268
|
-
* read, atime IS the right signal.
|
|
269
|
-
*
|
|
270
|
-
* Decision: use mtime via `utimesSync` — explicit and portable.
|
|
271
|
-
*/
|
|
272
|
-
function touchCacheFile(path: string): void {
|
|
273
|
-
try {
|
|
274
|
-
const now = new Date();
|
|
275
|
-
utimesSync(path, now, now);
|
|
276
|
-
} catch {
|
|
277
|
-
// best-effort
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Evict cache entries beyond the retain count, keeping the N most recently
|
|
283
|
-
* used (by mtime). Called after every successful cache write. Best-effort:
|
|
284
|
-
* eviction failure never blocks a load.
|
|
285
|
-
*
|
|
286
|
-
* Rejects symlinks and non-regular files via lstat — the same defense as
|
|
287
|
-
* the cache-hit path (F-008 fix). A symlink in the cache dir is logged
|
|
288
|
-
* as a security warning but not deleted (don't act on attacker-controlled
|
|
289
|
-
* paths automatically).
|
|
290
|
-
*/
|
|
291
|
-
function evictBeyondRetainCount(retain: number = getCacheRetainCount()): void {
|
|
292
|
-
const dir = getCacheDir();
|
|
293
|
-
let entries: string[];
|
|
294
|
-
try {
|
|
295
|
-
entries = readdirSync(dir);
|
|
296
|
-
} catch {
|
|
297
|
-
return; // Dir doesn't exist yet; nothing to evict.
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const candidates: { path: string; mtimeMs: number }[] = [];
|
|
301
|
-
for (const name of entries) {
|
|
302
|
-
if (!name.endsWith('.wasm')) continue; // Don't touch non-grammar files.
|
|
303
|
-
const path = join(dir, name);
|
|
304
|
-
let stat;
|
|
305
|
-
try {
|
|
306
|
-
stat = lstatSync(path);
|
|
307
|
-
} catch {
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
311
|
-
// Skip — never automatically delete what could be an attacker-placed
|
|
312
|
-
// symlink. Surface via stderr; user's cache dir is suspect.
|
|
313
|
-
console.error(
|
|
314
|
-
`[tree-sitter-loader] cache eviction skipped non-regular file: ${path} ` +
|
|
315
|
-
`(possible symlink attack — see Phase 3.5 finding F-008).`,
|
|
316
|
-
);
|
|
317
|
-
continue;
|
|
318
|
-
}
|
|
319
|
-
candidates.push({ path, mtimeMs: stat.mtimeMs });
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (candidates.length <= retain) return;
|
|
323
|
-
|
|
324
|
-
// Sort newest-first; everything beyond `retain` is evictable.
|
|
325
|
-
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
326
|
-
for (const victim of candidates.slice(retain)) {
|
|
327
|
-
try {
|
|
328
|
-
unlinkSync(victim.path);
|
|
329
|
-
} catch {
|
|
330
|
-
// best-effort
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/** Test-injection hook: lets tests force eviction without writing a new grammar. */
|
|
336
|
-
export function _evictCacheForTest(retain?: number): void {
|
|
337
|
-
evictBeyondRetainCount(retain);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function sha256(bytes: Uint8Array): string {
|
|
341
|
-
return createHash('sha256').update(bytes).digest('hex');
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
let parserInitPromise: Promise<void> | null = null;
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* `Parser.init()` is async and must be called once before any `new Parser()`.
|
|
348
|
-
* This function is idempotent — repeated calls return the same promise.
|
|
349
|
-
*
|
|
350
|
-
* Test harnesses can mock this by stubbing `Parser.init`.
|
|
351
|
-
*/
|
|
352
|
-
export async function ensureParserInitialized(): Promise<void> {
|
|
353
|
-
if (parserInitPromise) return parserInitPromise;
|
|
354
|
-
parserInitPromise = Parser.init();
|
|
355
|
-
return parserInitPromise;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// ============================================================
|
|
359
|
-
// Loader (the main entry point)
|
|
360
|
-
// ============================================================
|
|
361
|
-
|
|
362
|
-
interface LoaderOptions {
|
|
363
|
-
/**
|
|
364
|
-
* Test-injection: override the manifest entry for a language. Production
|
|
365
|
-
* callers leave this undefined; tests use it to exercise SHA-mismatch and
|
|
366
|
-
* download-failure paths.
|
|
367
|
-
*/
|
|
368
|
-
manifestOverride?: Partial<Record<TreeSitterLanguage, ManifestEntry>>;
|
|
369
|
-
/**
|
|
370
|
-
* Test-injection: override the fetch implementation. Defaults to global
|
|
371
|
-
* `fetch`. Tests pass a mock that returns a fixed body or throws.
|
|
372
|
-
*/
|
|
373
|
-
fetchImpl?: (url: string) => Promise<{ ok: boolean; arrayBuffer: () => Promise<ArrayBuffer>; status?: number }>;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const loadedGrammars = new Map<TreeSitterLanguage, Language>();
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Lazy-load a Tree-sitter grammar. Only fetches/caches the grammar for
|
|
380
|
-
* `language`; other languages are unaffected.
|
|
381
|
-
*
|
|
382
|
-
* Order:
|
|
383
|
-
* 1. In-memory cache hit → return.
|
|
384
|
-
* 2. Disk cache hit + SHA verify pass → load from disk.
|
|
385
|
-
* 3. Disk cache hit + SHA mismatch → throw GrammarSHAMismatchError.
|
|
386
|
-
* 4. Cache miss → fetch from pinned URL → SHA verify → atomic write → load.
|
|
387
|
-
* 5. Fetch fails AND no cache → throw GrammarUnavailableError.
|
|
388
|
-
*/
|
|
389
|
-
export async function loadGrammar(
|
|
390
|
-
language: TreeSitterLanguage,
|
|
391
|
-
options: LoaderOptions = {},
|
|
392
|
-
): Promise<Language> {
|
|
393
|
-
await ensureParserInitialized();
|
|
394
|
-
|
|
395
|
-
const cached = loadedGrammars.get(language);
|
|
396
|
-
if (cached) return cached;
|
|
397
|
-
|
|
398
|
-
const manifest = options.manifestOverride?.[language] ?? GRAMMAR_MANIFEST[language];
|
|
399
|
-
if (!manifest) {
|
|
400
|
-
throw new GrammarUnavailableError(
|
|
401
|
-
language,
|
|
402
|
-
new Error(`No manifest entry for language "${language}". v1 supports: ${Object.keys(GRAMMAR_MANIFEST).join(', ')}.`),
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const cachePath = getCachedPath(language, manifest.sha256);
|
|
407
|
-
|
|
408
|
-
// 2/3: disk cache check. Use lstatSync (NOT statSync) so a symlink at
|
|
409
|
-
// the cache path is detected and rejected — never followed.
|
|
410
|
-
// (Phase 3.5 finding #3 — symlink attack on cache dir.)
|
|
411
|
-
let cacheLstat;
|
|
412
|
-
try {
|
|
413
|
-
cacheLstat = lstatSync(cachePath);
|
|
414
|
-
} catch {
|
|
415
|
-
cacheLstat = null;
|
|
416
|
-
}
|
|
417
|
-
if (cacheLstat) {
|
|
418
|
-
if (cacheLstat.isSymbolicLink() || !cacheLstat.isFile()) {
|
|
419
|
-
throw new GrammarCacheSymlinkError(cachePath);
|
|
420
|
-
}
|
|
421
|
-
let bytes: Uint8Array;
|
|
422
|
-
try {
|
|
423
|
-
bytes = readFileSync(cachePath);
|
|
424
|
-
} catch (e) {
|
|
425
|
-
// Treat read failure as cache miss; fall through to download.
|
|
426
|
-
bytes = new Uint8Array(0);
|
|
427
|
-
}
|
|
428
|
-
if (bytes.byteLength > 0) {
|
|
429
|
-
const actualSha = sha256(bytes);
|
|
430
|
-
if (actualSha !== manifest.sha256) {
|
|
431
|
-
// Refuse to load. Don't silently re-download — that would mask
|
|
432
|
-
// tampering of the on-disk cache.
|
|
433
|
-
throw new GrammarSHAMismatchError(language, manifest.sha256, actualSha);
|
|
434
|
-
}
|
|
435
|
-
const lang = await Language.load(bytes);
|
|
436
|
-
loadedGrammars.set(language, lang);
|
|
437
|
-
// F-011 LRU: mark this entry as most-recently-used so it survives
|
|
438
|
-
// future evictions.
|
|
439
|
-
touchCacheFile(cachePath);
|
|
440
|
-
return lang;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// 4/5: download. Defense in depth: refuse non-HTTPS URLs.
|
|
445
|
-
if (!/^https:\/\//i.test(manifest.url)) {
|
|
446
|
-
throw new GrammarUrlNotHttpsError(manifest.url);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const fetchImpl = options.fetchImpl ?? (globalThis.fetch as LoaderOptions['fetchImpl']);
|
|
450
|
-
if (!fetchImpl) {
|
|
451
|
-
throw new GrammarUnavailableError(
|
|
452
|
-
language,
|
|
453
|
-
new Error('No fetch implementation available (Node < 18?)'),
|
|
454
|
-
);
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
let body: Uint8Array;
|
|
458
|
-
try {
|
|
459
|
-
const res = await fetchImpl(manifest.url);
|
|
460
|
-
if (!res.ok) {
|
|
461
|
-
throw new Error(`HTTP ${res.status ?? 'unknown'} from ${manifest.url}`);
|
|
462
|
-
}
|
|
463
|
-
body = new Uint8Array(await res.arrayBuffer());
|
|
464
|
-
} catch (e) {
|
|
465
|
-
throw new GrammarUnavailableError(language, e);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
const downloadedSha = sha256(body);
|
|
469
|
-
if (downloadedSha !== manifest.sha256) {
|
|
470
|
-
throw new GrammarSHAMismatchError(language, manifest.sha256, downloadedSha);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Atomic cache write. Always create the dir first.
|
|
474
|
-
// Mode 0o700 on the dir + 0o600 on files — owner-only access prevents
|
|
475
|
-
// local information disclosure of cached grammars.
|
|
476
|
-
// (Phase 3.5 finding #3 — file-mode hardening.)
|
|
477
|
-
try {
|
|
478
|
-
mkdirSync(dirname(cachePath), { recursive: true, mode: 0o700 });
|
|
479
|
-
try { chmodSync(dirname(cachePath), 0o700); } catch { /* best effort */ }
|
|
480
|
-
const tmpPath = `${cachePath}.tmp.${process.pid}`;
|
|
481
|
-
writeFileSync(tmpPath, body, { mode: 0o600 });
|
|
482
|
-
try { chmodSync(tmpPath, 0o600); } catch { /* best effort */ }
|
|
483
|
-
try {
|
|
484
|
-
renameSync(tmpPath, cachePath);
|
|
485
|
-
try { chmodSync(cachePath, 0o600); } catch { /* best effort */ }
|
|
486
|
-
} catch (e) {
|
|
487
|
-
// Try to clean up the tmp file on rename failure
|
|
488
|
-
try {
|
|
489
|
-
unlinkSync(tmpPath);
|
|
490
|
-
} catch {
|
|
491
|
-
/* ignore */
|
|
492
|
-
}
|
|
493
|
-
throw e;
|
|
494
|
-
}
|
|
495
|
-
// F-011 LRU: prune cache to retain count after every successful write.
|
|
496
|
-
// Best-effort — eviction failure never blocks a load.
|
|
497
|
-
evictBeyondRetainCount();
|
|
498
|
-
} catch (e) {
|
|
499
|
-
// Cache write failure is non-fatal — we still have `body` in memory and
|
|
500
|
-
// can load directly. Log to stderr per VR-USER-ERROR-MESSAGES style.
|
|
501
|
-
console.error(
|
|
502
|
-
`[tree-sitter-loader] cache write failed for ${language}: ${e instanceof Error ? e.message : String(e)} — loading directly from memory.`,
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
const lang = await Language.load(body);
|
|
507
|
-
loadedGrammars.set(language, lang);
|
|
508
|
-
return lang;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* Test-only: clear in-memory loaded grammar cache. Disk cache persists.
|
|
513
|
-
* Production code never needs this; the in-memory map lives for the process.
|
|
514
|
-
*/
|
|
515
|
-
export function __resetLoadedGrammars(): void {
|
|
516
|
-
loadedGrammars.clear();
|
|
517
|
-
}
|