@massu/core 1.12.1 → 1.12.2

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 CHANGED
@@ -4,6 +4,37 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [1.12.2] - 2026-05-18
8
+
9
+ **Security MEDIUM sweep (`plan-2026-05-18-security-medium-sweep`)**. Closes the 5 MEDIUM findings from the post-1.12.0 `massu-security-reviewer` audit-loop with structural drift-guards for each. M-1 IP_HASH_PEPPER consistency (silent-fallback vs throw class), M-2 webhook POST schema symmetry, M-3 Lemon Squeezy webhook idempotency parity with Stripe, M-4 v1 audit UUID echo redaction, M-5 SSO IdP host allowlist (precondition for SSO re-enable). New CR-51 + VR-PEPPER-GUARD + pattern-scanner Check 27.
10
+
11
+ ### Added
12
+
13
+ - **M-1 / CR-51 / VR-PEPPER-GUARD** — `website/src/lib/ip/pepper-guard.ts` exporting `requireIpHashPepper()` + `hashIpWithPepper(ip, { length: 16 | 32 })` as the SOLE allowed reader of the IP hash pepper env var. Production fail-closed via `MissingPepperError`; documented test override via `MASSU_TEST_ALLOW_EMPTY_PEPPER=1`. Closes the structural drift class where `evidence/[id]/download/route.ts:21` silent-fallback diverged from `license/activate/route.ts:34-40` throw.
14
+ - **M-2 / VR-SCHEMA-SYMMETRY** — `webhookCreateSchema` in `src/lib/validations.ts`, symmetric to `webhookUpdateSchema`. `description` capped at 500 chars on BOTH schemas (CR-9 bypass-prevention). `route-schema-symmetry.test.ts` drift-guard scans every `app/api/v1/**/route.ts` POST/PATCH/PUT handler for `*Schema.safeParse` invocation.
15
+ - **M-3 / VR-LS-IDEMPOTENCY** — migration `042_lemon_squeezy_webhook_events.sql` + `lemon_squeezy_event_apply(p_event_id, p_event_type, p_payload, ...)` RPC returning TEXT `processing_status` (`processed_ok` | `duplicate_ignored` | `permanent_failure` | `transient_failure`). Route handler maps each status to HTTP 200/200/422/500 — 422 stops LS retries on permanent failures (Stripe's BOOLEAN return can't carry this contract). Down migration shipped alongside (`.down.sql`) per §6.5 Rollback Plan.
16
+ - **M-4 / VR-V1-NO-UUID-LEAK** — `v1-error-response-no-uuid-leak.test.ts` drift-guard scanning every `app/api/v1/**/route.ts` source for caller-controlled UUID interpolation in error/note response surfaces (`apiError`, `note =`, etc.). `${auth.orgId}` self-echo explicitly permitted (caller's auth context already binds them).
17
+ - **M-5 / VR-SSO-IDP-ALLOWLIST** — `website/src/lib/sso/idp-allowlist.ts` with `SSO_IDP_HOSTS_BY_PROVIDER` constant + `validateSsoUrlAgainstProvider(provider, ssoUrl)` + `inferSsoProviderFromUrl(ssoUrl)`. TLD-aware wildcard host match (`*.okta.com` does NOT match `evil-okta.com`); HTTPS-only scheme enforcement. Covers Okta, Auth0, Azure AD (microsoftonline.com + .us), Google Workspace. Generic OIDC deferred to `plan-B.3-followup` SSO re-enable.
18
+ - **`MASSU_SSO_ALLOWLIST_LOG_ONLY` env-flag** for the M-5 staged rollout — `isSsoAllowlistLogOnly()` in `sso-flag.ts`. Operator runs 24h log-only post-deploy to monitor false-positives before flipping to enforcing.
19
+ - **`scripts/massu-pattern-scanner.sh` Check 27** — bash drift-guard for security-sensitive env-var patterns. 27a flags raw `process.env.<NAME>_PEPPER` outside `lib/<purpose>/<purpose>-guard.ts`; 27b flags any `process.env.<NAME>_(PEPPER|SECRET|KEY) ?? <literal>` silent-fallback adjacent to such reads (framework-platform keys exempt).
20
+ - **CR-51** canonical rule in `.claude/CLAUDE.md`: security-sensitive env vars must use a dedicated guard helper. Three-layer enforcement: pattern-scanner Check 27 + vitest drift-guard + the guard helper is the ONLY allowed reader.
21
+ - 5 new vitest drift-guards: `ip-pepper-guard-usage.test.ts`, `route-schema-symmetry.test.ts`, `lemon-squeezy-event-atomic.test.ts`, `v1-error-response-no-uuid-leak.test.ts`, `sso-idp-allowlist.test.ts` (+23 happy/negative/integration cases for M-5 alone).
22
+
23
+ ### Changed
24
+
25
+ - **Lemon Squeezy webhook route (`src/app/api/lemon-squeezy/webhook/route.ts`)** — 4 handlers (`handleOrderCreated`, `handleLicenseKeyCreated`, `handleOrderRefunded`, `handleSubscriptionCancelled`) refactored to dispatch through `lemon_squeezy_event_apply` RPC. Direct `from('book_purchases').insert` / `from('organizations').update` calls REMOVED from the route. Pre-RPC test-mode mismatch rejection now returns `permanent_failure` (HTTP 422) instead of throwing.
26
+ - **`src/app/api/v1/audit/route.ts:66`** — `${actor}` UUID echo dropped from non-empty-org-no-rows note. New message: "No audit entries for the actor filter in this org. Verify the user is a member of org ${auth.orgId}." Eliminates the UUID-existence-probe side-channel for authenticated API-key holders.
27
+ - **`src/app/api/sso/route.ts`** — invokes `inferSsoProviderFromUrl(ssoConfig.sso_url)` before constructing `redirect_url`. Non-allowlisted host returns 502 with `X-SSO-Validation-Failure: idp-host-not-allowlisted` (or logs-only under `MASSU_SSO_ALLOWLIST_LOG_ONLY=1`).
28
+ - **`src/app/api/sso/callback/route.ts:exchangeOidcCode`** — same allowlist guard applied BEFORE any outbound `fetch()` to the IdP token endpoint. Closes the SSRF-to-IdP vector at the callback layer (P5-007).
29
+ - **`scripts/massu-security-scanner.sh` Check 6** — RLS coverage scanner now excludes `*.down.sql` rollback scripts (CR-46 structural fix; the literal "CREATE TABLE" only appears in their explanatory comments).
30
+ - **`website/src/data/stats.ts`** — Database Tables 44 → 45 (new `lemon_squeezy_webhook_events`), Canonical Rules 16 → 17 (new CR-51). Marketing count drift-guard covers both.
31
+ - **`website/src/__tests__/changelog-parse.test.ts:EXPECTED_COUNT`** bumped 40 → 41.
32
+ - **`website/src/__tests__/migration-grant-discipline.test.ts:SERVICE_ROLE_ONLY_TABLES`** adds `lemon_squeezy_webhook_events: '042_lemon_squeezy_webhook_events.sql'`.
33
+
34
+ ### Fixed
35
+
36
+ - Pre-existing `plan-token-changelog-coverage.test.ts` PTCC-03 failure surfaced during the loop — 3 post-tag chore-commit plan-tokens (`plan-stage-d-medium-sweep`, `plan-stage-e-low-info-sweep`, `plan-2026-05-16-prelaunch-audit`) added to the documented-divergence allowlist with the same rationale pattern as the existing `plan-stage-c-high-batch` entry.
37
+
7
38
  ## [1.12.1] - 2026-05-18
8
39
 
9
40
  **Pre-push ↔ CI parity (`plan-2026-05-18-pre-push-ci-parity`)**. Closes the structural drift class surfaced 2026-05-18 when SHA `b26fbb1` passed `pre-push-light` locally but failed CI on 4 distinct gates spread across 3 separate workflow YAMLs (`@massu/types/dist` missing → `ci.yml`, `eslint-rules/` missing from `PUBLIC_DIRS` → `sync-check.yml`, `diff-commands-vs-docs.sh` hardcoded path → `sync-check.yml`, Tarball E2E adapter list stale → `ci.yml`). The two operator-pushed fix attempts `8c6b843` + `d3164f7` fixed the specific instances but left the bug class wide open. This release closes the CLASS via three-layer enforcement.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massu/core",
3
- "version": "1.12.1",
3
+ "version": "1.12.2",
4
4
  "type": "module",
5
5
  "description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 73 total), 59 workflow commands, 11 agents, 20+ patterns",
6
6
  "main": "./dist/cli.js",
@@ -1,577 +0,0 @@
1
- // ../adapter-aspnet/dist/index.js
2
- import { Parser as Parser2 } from "web-tree-sitter";
3
-
4
- // src/detect/adapters/query-helpers.ts
5
- import { Query } from "web-tree-sitter";
6
- var InvalidQueryError = class extends Error {
7
- queryName;
8
- querySource;
9
- cause;
10
- constructor(queryName, querySource, cause) {
11
- const causeMsg = cause instanceof Error ? cause.message : String(cause);
12
- super(
13
- `[query-helpers] Invalid Tree-sitter query "${queryName}": ${causeMsg}
14
- Query source:
15
- ${querySource}`
16
- );
17
- this.name = "InvalidQueryError";
18
- this.queryName = queryName;
19
- this.querySource = querySource;
20
- this.cause = cause;
21
- }
22
- };
23
- var queryCache = /* @__PURE__ */ new WeakMap();
24
- function compileQuery(language, source, queryName) {
25
- let perLang = queryCache.get(language);
26
- if (!perLang) {
27
- perLang = /* @__PURE__ */ new Map();
28
- queryCache.set(language, perLang);
29
- }
30
- const cached = perLang.get(source);
31
- if (cached) return cached;
32
- let q;
33
- try {
34
- q = new Query(language, source);
35
- } catch (e) {
36
- throw new InvalidQueryError(queryName, source, e);
37
- }
38
- perLang.set(source, q);
39
- return q;
40
- }
41
- function runQuery(parser, source, queryText, queryName, filePath) {
42
- const language = parser.language;
43
- if (!language) {
44
- throw new InvalidQueryError(
45
- queryName,
46
- queryText,
47
- new Error("Parser has no language assigned")
48
- );
49
- }
50
- const query = compileQuery(language, queryText, queryName);
51
- const tree = parser.parse(source);
52
- if (!tree) return [];
53
- let matches;
54
- try {
55
- matches = query.matches(tree.rootNode);
56
- } catch (e) {
57
- throw new InvalidQueryError(queryName, queryText, e);
58
- }
59
- const out = [];
60
- for (const match of matches) {
61
- if (!match.captures || match.captures.length === 0) continue;
62
- const captures = {};
63
- let earliestLine = Number.POSITIVE_INFINITY;
64
- for (const cap of match.captures) {
65
- const node = cap.node;
66
- captures[cap.name] = node.text;
67
- if (node.startPosition.row + 1 < earliestLine) {
68
- earliestLine = node.startPosition.row + 1;
69
- }
70
- }
71
- out.push({
72
- captures,
73
- file: filePath,
74
- line: Number.isFinite(earliestLine) ? earliestLine : 1,
75
- queryName
76
- });
77
- }
78
- try {
79
- tree.delete();
80
- } catch {
81
- }
82
- return out;
83
- }
84
-
85
- // src/detect/adapters/tree-sitter-loader.ts
86
- import { createHash } from "crypto";
87
- import {
88
- mkdirSync,
89
- readdirSync,
90
- readFileSync,
91
- writeFileSync,
92
- renameSync,
93
- unlinkSync,
94
- lstatSync,
95
- chmodSync,
96
- utimesSync
97
- } from "fs";
98
- import { homedir } from "os";
99
- import { dirname, join } from "path";
100
- import { Language, Parser } from "web-tree-sitter";
101
- var GrammarSHAMismatchError = class extends Error {
102
- language;
103
- expected;
104
- actual;
105
- constructor(language, expected, actual) {
106
- super(
107
- `[tree-sitter-loader] SHA-256 mismatch for grammar "${language}". Expected ${expected}, got ${actual}. REFUSING to load \u2014 see Phase 3.5 audit attack vector #3.`
108
- );
109
- this.name = "GrammarSHAMismatchError";
110
- this.language = language;
111
- this.expected = expected;
112
- this.actual = actual;
113
- }
114
- };
115
- var GrammarUnavailableError = class extends Error {
116
- language;
117
- cause;
118
- constructor(language, cause) {
119
- const causeMsg = cause instanceof Error ? cause.message : cause ? String(cause) : "no cached grammar and download failed";
120
- super(
121
- `[tree-sitter-loader] Grammar for "${language}" is unavailable: ${causeMsg}. Falling back to regex introspection for files in ${language}.`
122
- );
123
- this.name = "GrammarUnavailableError";
124
- this.language = language;
125
- this.cause = cause;
126
- }
127
- };
128
- var GrammarCacheSymlinkError = class extends Error {
129
- cachePath;
130
- constructor(cachePath) {
131
- super(
132
- `[tree-sitter-loader] Refusing to load grammar \u2014 cache path "${cachePath}" is a symlink or non-regular file. (Phase 3.5 finding #3 \u2014 symlink attack vector.)`
133
- );
134
- this.name = "GrammarCacheSymlinkError";
135
- this.cachePath = cachePath;
136
- }
137
- };
138
- var GrammarUrlNotHttpsError = class extends Error {
139
- url;
140
- constructor(url) {
141
- super(
142
- `[tree-sitter-loader] Refusing to download grammar from non-HTTPS URL: ${url}. Only https:// URLs are accepted. (Phase 3.5 finding #3.)`
143
- );
144
- this.name = "GrammarUrlNotHttpsError";
145
- this.url = url;
146
- }
147
- };
148
- var GRAMMAR_MANIFEST = {
149
- python: {
150
- url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-python.wasm",
151
- sha256: "9056d0fb0c337810d019fae350e8167786119da98f0f282aceae7ab89ee8253b",
152
- version: "0.1.13"
153
- },
154
- typescript: {
155
- url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-typescript.wasm",
156
- sha256: "8515404dceed38e1ed86aa34b09fcf3379fff1b4ff9dd3967bcd6d1eb5ac3d8f",
157
- version: "0.1.13"
158
- },
159
- javascript: {
160
- url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-javascript.wasm",
161
- sha256: "63812b9e275d26851264734868d27a1656bd44a2ef6eb3e85e6b03728c595ab5",
162
- version: "0.1.13"
163
- },
164
- swift: {
165
- url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-swift.wasm",
166
- sha256: "41c4fdb2249a3aa6d87eed0d383081ff09725c2248b4977043a43825980ffcc7",
167
- version: "0.1.13"
168
- },
169
- // ----------------------------------------------------------------
170
- // Plan 3c Phase 7 expansion (2026-05-07):
171
- //
172
- // Six additional grammars to support the registry-verified framework
173
- // adapters (go-chi, rails, aspnet, spring, ktor, phoenix) plus the
174
- // bundled adapters in the same language families (gin/echo/fiber,
175
- // sinatra, etc.). All entries use the SAME pinned tree-sitter-wasms
176
- // version (0.1.13) as the v1 four to keep the dependency surface
177
- // single-source.
178
- //
179
- // SHA-256s computed 2026-05-07 via:
180
- // curl -fsSL <url> | shasum -a 256
181
- //
182
- // The unpkg filename for C# uses an underscore (`c_sharp`) while the
183
- // TreeSitterLanguage identifier uses no separator (`csharp`); the map
184
- // key is the type identifier, the URL is the storage path — they do
185
- // NOT need to match, the same as how `python` maps to `tree-sitter-
186
- // python.wasm`. This is intentional and validated by the manifest
187
- // shape test in tree-sitter-loader-manifest.test.ts.
188
- // ----------------------------------------------------------------
189
- go: {
190
- url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-go.wasm",
191
- sha256: "9963ca89b616eaf04b08a43bc1fb0f07b85395bec313330851f1f1ead2f755b6",
192
- version: "0.1.13"
193
- },
194
- ruby: {
195
- url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-ruby.wasm",
196
- sha256: "93a5022855314cdb45458c7bb026a24a0ebc3a5ff6439e542e881f14dfa13a39",
197
- version: "0.1.13"
198
- },
199
- csharp: {
200
- url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-c_sharp.wasm",
201
- sha256: "6266a7e32d68a3459104d994dc848df15d5672b0ea8e86d327274b694f8e6991",
202
- version: "0.1.13"
203
- },
204
- java: {
205
- url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-java.wasm",
206
- sha256: "637aac4415fb39a211a4f4292d63c66b5ce9c32fa2cd35464af4f681d91b9a1f",
207
- version: "0.1.13"
208
- },
209
- kotlin: {
210
- url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-kotlin.wasm",
211
- sha256: "b5cb00c8d06ed0f10f1dbe497205b437809d7e87db1f638721a8cfb30e044449",
212
- version: "0.1.13"
213
- },
214
- elixir: {
215
- url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-elixir.wasm",
216
- sha256: "82e91b9759ddca30d8978ebbfa8e347b4451b64c931f9ae62112e6db9b8fac20",
217
- version: "0.1.13"
218
- }
219
- };
220
- function getCacheDir() {
221
- return process.env.MASSU_WASM_CACHE_DIR ?? join(homedir(), ".massu", "wasm-cache");
222
- }
223
- function getCachedPath(language, sha) {
224
- return join(getCacheDir(), `${language}-${sha}.wasm`);
225
- }
226
- var DEFAULT_CACHE_RETAIN_COUNT = 16;
227
- function getCacheRetainCount() {
228
- const env = process.env.MASSU_WASM_CACHE_RETAIN;
229
- if (env) {
230
- const n = Number(env);
231
- if (Number.isFinite(n) && n >= 1 && n <= 1024) return Math.floor(n);
232
- }
233
- return DEFAULT_CACHE_RETAIN_COUNT;
234
- }
235
- function touchCacheFile(path) {
236
- try {
237
- const now = /* @__PURE__ */ new Date();
238
- utimesSync(path, now, now);
239
- } catch {
240
- }
241
- }
242
- function evictBeyondRetainCount(retain = getCacheRetainCount()) {
243
- const dir = getCacheDir();
244
- let entries;
245
- try {
246
- entries = readdirSync(dir);
247
- } catch {
248
- return;
249
- }
250
- const candidates = [];
251
- for (const name of entries) {
252
- if (!name.endsWith(".wasm")) continue;
253
- const path = join(dir, name);
254
- let stat;
255
- try {
256
- stat = lstatSync(path);
257
- } catch {
258
- continue;
259
- }
260
- if (stat.isSymbolicLink() || !stat.isFile()) {
261
- console.error(
262
- `[tree-sitter-loader] cache eviction skipped non-regular file: ${path} (possible symlink attack \u2014 see Phase 3.5 finding F-008).`
263
- );
264
- continue;
265
- }
266
- candidates.push({ path, mtimeMs: stat.mtimeMs });
267
- }
268
- if (candidates.length <= retain) return;
269
- candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
270
- for (const victim of candidates.slice(retain)) {
271
- try {
272
- unlinkSync(victim.path);
273
- } catch {
274
- }
275
- }
276
- }
277
- function sha256(bytes) {
278
- return createHash("sha256").update(bytes).digest("hex");
279
- }
280
- var parserInitPromise = null;
281
- async function ensureParserInitialized() {
282
- if (parserInitPromise) return parserInitPromise;
283
- parserInitPromise = Parser.init();
284
- return parserInitPromise;
285
- }
286
- var loadedGrammars = /* @__PURE__ */ new Map();
287
- async function loadGrammar(language, options = {}) {
288
- await ensureParserInitialized();
289
- const cached = loadedGrammars.get(language);
290
- if (cached) return cached;
291
- const manifest = options.manifestOverride?.[language] ?? GRAMMAR_MANIFEST[language];
292
- if (!manifest) {
293
- throw new GrammarUnavailableError(
294
- language,
295
- new Error(`No manifest entry for language "${language}". v1 supports: ${Object.keys(GRAMMAR_MANIFEST).join(", ")}.`)
296
- );
297
- }
298
- const cachePath = getCachedPath(language, manifest.sha256);
299
- let cacheLstat;
300
- try {
301
- cacheLstat = lstatSync(cachePath);
302
- } catch {
303
- cacheLstat = null;
304
- }
305
- if (cacheLstat) {
306
- if (cacheLstat.isSymbolicLink() || !cacheLstat.isFile()) {
307
- throw new GrammarCacheSymlinkError(cachePath);
308
- }
309
- let bytes;
310
- try {
311
- bytes = readFileSync(cachePath);
312
- } catch (e) {
313
- bytes = new Uint8Array(0);
314
- }
315
- if (bytes.byteLength > 0) {
316
- const actualSha = sha256(bytes);
317
- if (actualSha !== manifest.sha256) {
318
- throw new GrammarSHAMismatchError(language, manifest.sha256, actualSha);
319
- }
320
- const lang2 = await Language.load(bytes);
321
- loadedGrammars.set(language, lang2);
322
- touchCacheFile(cachePath);
323
- return lang2;
324
- }
325
- }
326
- if (!/^https:\/\//i.test(manifest.url)) {
327
- throw new GrammarUrlNotHttpsError(manifest.url);
328
- }
329
- const fetchImpl = options.fetchImpl ?? globalThis.fetch;
330
- if (!fetchImpl) {
331
- throw new GrammarUnavailableError(
332
- language,
333
- new Error("No fetch implementation available (Node < 18?)")
334
- );
335
- }
336
- let body;
337
- try {
338
- const res = await fetchImpl(manifest.url);
339
- if (!res.ok) {
340
- throw new Error(`HTTP ${res.status ?? "unknown"} from ${manifest.url}`);
341
- }
342
- body = new Uint8Array(await res.arrayBuffer());
343
- } catch (e) {
344
- throw new GrammarUnavailableError(language, e);
345
- }
346
- const downloadedSha = sha256(body);
347
- if (downloadedSha !== manifest.sha256) {
348
- throw new GrammarSHAMismatchError(language, manifest.sha256, downloadedSha);
349
- }
350
- try {
351
- mkdirSync(dirname(cachePath), { recursive: true, mode: 448 });
352
- try {
353
- chmodSync(dirname(cachePath), 448);
354
- } catch {
355
- }
356
- const tmpPath = `${cachePath}.tmp.${process.pid}`;
357
- writeFileSync(tmpPath, body, { mode: 384 });
358
- try {
359
- chmodSync(tmpPath, 384);
360
- } catch {
361
- }
362
- try {
363
- renameSync(tmpPath, cachePath);
364
- try {
365
- chmodSync(cachePath, 384);
366
- } catch {
367
- }
368
- } catch (e) {
369
- try {
370
- unlinkSync(tmpPath);
371
- } catch {
372
- }
373
- throw e;
374
- }
375
- evictBeyondRetainCount();
376
- } catch (e) {
377
- console.error(
378
- `[tree-sitter-loader] cache write failed for ${language}: ${e instanceof Error ? e.message : String(e)} \u2014 loading directly from memory.`
379
- );
380
- }
381
- const lang = await Language.load(body);
382
- loadedGrammars.set(language, lang);
383
- return lang;
384
- }
385
-
386
- // src/detect/adapters/parse-guard.ts
387
- var MAX_AST_FILE_BYTES = 1 * 1024 * 1024;
388
- var MAX_AST_PARSE_DEPTH = 5e3;
389
- function isParsableSource(source, sizeBytes) {
390
- const bytes = sizeBytes ?? Buffer.byteLength(source, "utf-8");
391
- if (bytes > MAX_AST_FILE_BYTES) {
392
- return {
393
- reason: "size-cap",
394
- detail: `${bytes} bytes > ${MAX_AST_FILE_BYTES} cap`
395
- };
396
- }
397
- let depth = 0;
398
- let maxDepth = 0;
399
- for (let i = 0; i < source.length; i++) {
400
- const c = source.charCodeAt(i);
401
- if (c === 0) {
402
- return { reason: "control-bytes", detail: "NUL byte at offset " + i };
403
- }
404
- if (c === 40 || c === 91 || c === 123) {
405
- depth++;
406
- if (depth > maxDepth) maxDepth = depth;
407
- if (depth > MAX_AST_PARSE_DEPTH) {
408
- return {
409
- reason: "depth-cap",
410
- detail: `nesting depth exceeded ${MAX_AST_PARSE_DEPTH}`
411
- };
412
- }
413
- } else if (c === 41 || c === 93 || c === 125) {
414
- depth = depth > 0 ? depth - 1 : 0;
415
- }
416
- }
417
- return null;
418
- }
419
-
420
- // ../adapter-aspnet/dist/index.js
421
- var MAP_VERB_QUERY = `
422
- (invocation_expression
423
- function: (member_access_expression
424
- name: (identifier) @method (#match? @method "^Map(Get|Post|Put|Patch|Delete|Head|Options)$"))
425
- arguments: (argument_list
426
- .
427
- (argument (string_literal) @route_path)))
428
- `;
429
- var HTTP_ATTR_QUERY = `
430
- (attribute
431
- name: (identifier) @attr_name (#match? @attr_name "^Http(Get|Post|Put|Patch|Delete|Head|Options)$"))
432
- `;
433
- var ROUTE_ATTR_QUERY = `
434
- (attribute
435
- name: (identifier) @_attr_name (#eq? @_attr_name "Route")
436
- (attribute_argument_list
437
- (attribute_argument (string_literal) @route_template)))
438
- `;
439
- var CONTROLLER_CLASS_QUERY = `
440
- (class_declaration
441
- name: (identifier) @class_name (#match? @class_name "Controller$"))
442
- `;
443
- var aspnetAdapter = {
444
- id: "aspnet",
445
- languages: ["csharp"],
446
- matches(signals) {
447
- if (!signals.csproj)
448
- return false;
449
- if (/Sdk\s*=\s*["']Microsoft\.NET\.Sdk\.Web["']/i.test(signals.csproj))
450
- return true;
451
- if (/Microsoft\.AspNetCore\.App/i.test(signals.csproj))
452
- return true;
453
- return false;
454
- },
455
- async introspect(files, _rootDir) {
456
- if (files.length === 0) {
457
- return { conventions: {}, provenance: [], confidence: "none" };
458
- }
459
- let language;
460
- try {
461
- language = await loadGrammar("csharp");
462
- } catch (e) {
463
- return { conventions: {}, provenance: [], confidence: "none" };
464
- }
465
- const parser = new Parser2();
466
- parser.setLanguage(language);
467
- const routeMethods = /* @__PURE__ */ new Map();
468
- const prefixBases = /* @__PURE__ */ new Map();
469
- const controllerClasses = /* @__PURE__ */ new Map();
470
- try {
471
- for (const file of files) {
472
- const skip = isParsableSource(file.content, file.size);
473
- if (skip) {
474
- process.stderr.write(`[massu/ast] WARN: aspnet skipping ${file.path}: ${skip.reason} (${skip.detail}). Cap=${MAX_AST_FILE_BYTES}. (Phase 3.5 mitigation)
475
- `);
476
- continue;
477
- }
478
- try {
479
- for (const hit of runQuery(parser, file.content, MAP_VERB_QUERY, "aspnet-map-verb", file.path)) {
480
- const methodRaw = hit.captures.method;
481
- if (!methodRaw)
482
- continue;
483
- const verb = methodRaw.replace(/^Map/, "");
484
- if (!routeMethods.has(verb)) {
485
- routeMethods.set(verb, { line: hit.line, file: file.path });
486
- }
487
- const pathRaw = hit.captures.route_path;
488
- if (pathRaw) {
489
- const literal = pathRaw.replace(/^["']/, "").replace(/["']$/, "");
490
- const base = extractPrefixBase(literal);
491
- if (base && !prefixBases.has(base)) {
492
- prefixBases.set(base, { line: hit.line, file: file.path });
493
- }
494
- }
495
- }
496
- for (const hit of runQuery(parser, file.content, HTTP_ATTR_QUERY, "aspnet-http-attr", file.path)) {
497
- const attrRaw = hit.captures.attr_name;
498
- if (!attrRaw)
499
- continue;
500
- const verb = attrRaw.replace(/^Http/, "");
501
- if (!routeMethods.has(verb)) {
502
- routeMethods.set(verb, { line: hit.line, file: file.path });
503
- }
504
- }
505
- for (const hit of runQuery(parser, file.content, ROUTE_ATTR_QUERY, "aspnet-route-attr", file.path)) {
506
- const tplRaw = hit.captures.route_template;
507
- if (!tplRaw)
508
- continue;
509
- const literal = tplRaw.replace(/^["']/, "").replace(/["']$/, "");
510
- const base = extractPrefixBase(literal);
511
- if (base && !prefixBases.has(base)) {
512
- prefixBases.set(base, { line: hit.line, file: file.path });
513
- }
514
- }
515
- for (const hit of runQuery(parser, file.content, CONTROLLER_CLASS_QUERY, "aspnet-controller-class", file.path)) {
516
- const name = hit.captures.class_name;
517
- if (name && !controllerClasses.has(name)) {
518
- controllerClasses.set(name, { line: hit.line, file: file.path });
519
- }
520
- }
521
- } catch (e) {
522
- if (e instanceof InvalidQueryError) {
523
- throw e;
524
- }
525
- continue;
526
- }
527
- }
528
- } finally {
529
- try {
530
- parser.delete();
531
- } catch {
532
- }
533
- }
534
- const conventions = {};
535
- const provenance = [];
536
- if (routeMethods.size === 1) {
537
- const [name, { line, file }] = routeMethods.entries().next().value;
538
- conventions.route_method = name;
539
- provenance.push({ field: "route_method", sourceFile: file, line, query: "aspnet-map-verb" });
540
- } else if (routeMethods.size >= 2) {
541
- const [name, { line, file }] = routeMethods.entries().next().value;
542
- conventions.route_method = name;
543
- provenance.push({ field: "route_method", sourceFile: file, line, query: "aspnet-map-verb" });
544
- }
545
- if (prefixBases.size >= 1) {
546
- const [base, { line, file }] = prefixBases.entries().next().value;
547
- conventions.route_prefix_base = base;
548
- provenance.push({ field: "route_prefix_base", sourceFile: file, line, query: "aspnet-route-prefix" });
549
- }
550
- if (controllerClasses.size >= 1) {
551
- const [name, { line, file }] = controllerClasses.entries().next().value;
552
- conventions.controller_class = name;
553
- provenance.push({ field: "controller_class", sourceFile: file, line, query: "aspnet-controller-class" });
554
- }
555
- let confidence;
556
- if (Object.keys(conventions).length === 0) {
557
- confidence = "none";
558
- } else if (routeMethods.size === 1) {
559
- confidence = "high";
560
- } else if (routeMethods.size >= 2) {
561
- confidence = "low";
562
- } else {
563
- confidence = "medium";
564
- }
565
- return { conventions, provenance, confidence };
566
- }
567
- };
568
- function extractPrefixBase(prefix) {
569
- const stripped = prefix.replace(/^\/+/, "");
570
- const firstSeg = stripped.split("/")[0];
571
- if (!firstSeg)
572
- return null;
573
- return "/" + firstSeg;
574
- }
575
- export {
576
- aspnetAdapter
577
- };