@massu/core 1.10.7 → 1.12.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 (177) hide show
  1. package/CHANGELOG.md +1303 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +559 -400
  4. package/dist/detect/adapters/.bundle-shasums.json +4 -7
  5. package/dist/detect/adapters/rails.js +6 -4
  6. package/dist/detect/adapters/spring.js +6 -4
  7. package/dist/detect/adapters/tree-sitter-loader.js +6 -4
  8. package/dist/hooks/auto-learning-pipeline.js +24 -1
  9. package/dist/hooks/classify-failure.js +32 -3
  10. package/dist/hooks/cost-tracker.js +24 -0
  11. package/dist/hooks/fix-detector.js +97 -1
  12. package/dist/hooks/incident-pipeline.js +30 -1
  13. package/dist/hooks/intent-suggester.js +6 -3
  14. package/dist/hooks/post-edit-context.js +24 -1
  15. package/dist/hooks/post-tool-use.js +66 -18
  16. package/dist/hooks/pre-compact.js +27 -2
  17. package/dist/hooks/pre-delete-check.js +48 -22
  18. package/dist/hooks/pre-tool-use-gate.js +962 -0
  19. package/dist/hooks/quality-event.js +24 -0
  20. package/dist/hooks/rule-enforcement-pipeline.js +24 -1
  21. package/dist/hooks/security-gate.js +29 -16
  22. package/dist/hooks/session-end.js +24 -0
  23. package/dist/hooks/session-start.js +8710 -8460
  24. package/dist/hooks/user-prompt.js +24 -0
  25. package/package.json +15 -18
  26. package/commands/README.md +0 -134
  27. package/src/adapter.ts +0 -121
  28. package/src/adr-generator.ts +0 -292
  29. package/src/analytics.ts +0 -373
  30. package/src/audit-trail.ts +0 -450
  31. package/src/backfill-sessions.ts +0 -182
  32. package/src/changelog-generator.ts +0 -178
  33. package/src/cli.ts +0 -246
  34. package/src/cloud-sync.ts +0 -234
  35. package/src/commands/adapters.ts +0 -824
  36. package/src/commands/changelog.ts +0 -165
  37. package/src/commands/config-check-drift.ts +0 -133
  38. package/src/commands/config-refresh.ts +0 -396
  39. package/src/commands/config-upgrade.ts +0 -127
  40. package/src/commands/doctor.ts +0 -485
  41. package/src/commands/hook-runner.ts +0 -145
  42. package/src/commands/init.ts +0 -1823
  43. package/src/commands/install-commands.ts +0 -692
  44. package/src/commands/install-hooks.ts +0 -27
  45. package/src/commands/permissions.ts +0 -150
  46. package/src/commands/refresh-log.ts +0 -37
  47. package/src/commands/show-template.ts +0 -65
  48. package/src/commands/template-engine.ts +0 -301
  49. package/src/commands/watch.ts +0 -430
  50. package/src/config.ts +0 -765
  51. package/src/cost-tracker.ts +0 -355
  52. package/src/db.ts +0 -370
  53. package/src/dependency-scorer.ts +0 -337
  54. package/src/detect/adapters/aspnet.ts +0 -4
  55. package/src/detect/adapters/discover.ts +0 -469
  56. package/src/detect/adapters/file-sampler.ts +0 -225
  57. package/src/detect/adapters/go-chi.ts +0 -4
  58. package/src/detect/adapters/index.ts +0 -51
  59. package/src/detect/adapters/nextjs-trpc.ts +0 -166
  60. package/src/detect/adapters/parse-guard.ts +0 -133
  61. package/src/detect/adapters/phoenix.ts +0 -4
  62. package/src/detect/adapters/python-django.ts +0 -208
  63. package/src/detect/adapters/python-fastapi.ts +0 -223
  64. package/src/detect/adapters/python-flask.ts +0 -235
  65. package/src/detect/adapters/query-helpers.ts +0 -170
  66. package/src/detect/adapters/rails.ts +0 -4
  67. package/src/detect/adapters/runner.ts +0 -284
  68. package/src/detect/adapters/spring.ts +0 -4
  69. package/src/detect/adapters/swift-swiftui.ts +0 -171
  70. package/src/detect/adapters/tree-sitter-loader.ts +0 -517
  71. package/src/detect/adapters/types.ts +0 -191
  72. package/src/detect/codebase-introspector.ts +0 -210
  73. package/src/detect/domain-inferrer.ts +0 -197
  74. package/src/detect/drift.ts +0 -199
  75. package/src/detect/framework-detector.ts +0 -307
  76. package/src/detect/index.ts +0 -209
  77. package/src/detect/manifest-registry.ts +0 -261
  78. package/src/detect/migrate.ts +0 -362
  79. package/src/detect/monorepo-detector.ts +0 -348
  80. package/src/detect/package-detector.ts +0 -828
  81. package/src/detect/passthrough.ts +0 -108
  82. package/src/detect/regex-fallback.ts +0 -449
  83. package/src/detect/source-dir-detector.ts +0 -297
  84. package/src/detect/vr-command-map.ts +0 -167
  85. package/src/docs-map.json +0 -100
  86. package/src/docs-tools.ts +0 -519
  87. package/src/domains.ts +0 -182
  88. package/src/hooks/auto-learning-pipeline.ts +0 -228
  89. package/src/hooks/classify-failure.ts +0 -259
  90. package/src/hooks/cost-tracker.ts +0 -66
  91. package/src/hooks/fix-detector.ts +0 -186
  92. package/src/hooks/incident-pipeline.ts +0 -190
  93. package/src/hooks/intent-suggester.ts +0 -131
  94. package/src/hooks/post-edit-context.ts +0 -91
  95. package/src/hooks/post-tool-use.ts +0 -413
  96. package/src/hooks/pre-compact.ts +0 -168
  97. package/src/hooks/pre-delete-check.ts +0 -243
  98. package/src/hooks/quality-event.ts +0 -127
  99. package/src/hooks/rule-enforcement-pipeline.ts +0 -159
  100. package/src/hooks/security-gate.ts +0 -153
  101. package/src/hooks/session-end.ts +0 -467
  102. package/src/hooks/session-start.ts +0 -436
  103. package/src/hooks/user-prompt.ts +0 -157
  104. package/src/import-resolver.ts +0 -226
  105. package/src/knowledge-db.ts +0 -176
  106. package/src/knowledge-indexer.ts +0 -708
  107. package/src/knowledge-tools.ts +0 -1414
  108. package/src/lib/fileLock.ts +0 -203
  109. package/src/lib/gitToplevel.ts +0 -22
  110. package/src/lib/hook-registry.ts +0 -43
  111. package/src/lib/installLock.ts +0 -66
  112. package/src/lib/memory-path.ts +0 -49
  113. package/src/lib/pidLiveness.ts +0 -67
  114. package/src/lib/settings-local.ts +0 -110
  115. package/src/lib/sql-table-names.ts +0 -71
  116. package/src/license.ts +0 -547
  117. package/src/lsp/auto-detect.ts +0 -98
  118. package/src/lsp/client.ts +0 -776
  119. package/src/lsp/enrich.ts +0 -127
  120. package/src/lsp/types.ts +0 -221
  121. package/src/memory-db.ts +0 -1614
  122. package/src/memory-file-ingest.ts +0 -128
  123. package/src/memory-tools.ts +0 -424
  124. package/src/middleware-tree.ts +0 -71
  125. package/src/observability-tools.ts +0 -343
  126. package/src/observation-extractor.ts +0 -418
  127. package/src/page-deps.ts +0 -285
  128. package/src/permissions.ts +0 -422
  129. package/src/prompt-analyzer.ts +0 -332
  130. package/src/python/coupling-detector.ts +0 -125
  131. package/src/python/domain-enforcer.ts +0 -84
  132. package/src/python/impact-analyzer.ts +0 -96
  133. package/src/python/import-parser.ts +0 -244
  134. package/src/python/import-resolver.ts +0 -136
  135. package/src/python/migration-indexer.ts +0 -116
  136. package/src/python/migration-parser.ts +0 -332
  137. package/src/python/model-indexer.ts +0 -71
  138. package/src/python/model-parser.ts +0 -279
  139. package/src/python/route-indexer.ts +0 -59
  140. package/src/python/route-parser.ts +0 -317
  141. package/src/python-tools.ts +0 -630
  142. package/src/regression-detector.ts +0 -319
  143. package/src/rules.ts +0 -57
  144. package/src/schema-mapper.ts +0 -232
  145. package/src/security/adapter-origin.ts +0 -130
  146. package/src/security/adapter-verifier.ts +0 -319
  147. package/src/security/atomic-write.ts +0 -164
  148. package/src/security/fetcher.ts +0 -200
  149. package/src/security/install-tracking.ts +0 -319
  150. package/src/security/license-pubkey.generated.ts +0 -30
  151. package/src/security/license-response-verifier.ts +0 -149
  152. package/src/security/local-fingerprint.ts +0 -225
  153. package/src/security/manifest-cache.ts +0 -333
  154. package/src/security/manifest-schema.ts +0 -129
  155. package/src/security/registry-pubkey.generated.ts +0 -35
  156. package/src/security/telemetry.ts +0 -320
  157. package/src/security-scorer.ts +0 -405
  158. package/src/security-utils.ts +0 -133
  159. package/src/sentinel-db.ts +0 -580
  160. package/src/sentinel-scanner.ts +0 -406
  161. package/src/sentinel-tools.ts +0 -513
  162. package/src/sentinel-types.ts +0 -140
  163. package/src/server-dispatch.ts +0 -225
  164. package/src/server.ts +0 -107
  165. package/src/session-archiver.ts +0 -111
  166. package/src/session-state-generator.ts +0 -174
  167. package/src/team-knowledge.ts +0 -407
  168. package/src/tool-db-needs.ts +0 -232
  169. package/src/tools.ts +0 -1205
  170. package/src/transcript-parser.ts +0 -458
  171. package/src/trpc-index.ts +0 -215
  172. package/src/validate-features-runner.ts +0 -107
  173. package/src/validation-engine.ts +0 -358
  174. package/src/watch/daemon.ts +0 -385
  175. package/src/watch/lockfile-detector.ts +0 -65
  176. package/src/watch/paths.ts +0 -279
  177. package/src/watch/state.ts +0 -178
@@ -1,200 +0,0 @@
1
- /**
2
- * HTTPS fetch helper with strict host allowlist + bounded timeout.
3
- *
4
- * Plan 3c Phase 5 deliverable (gap-3 / Phase 5 deps line 114). The Phase 5
5
- * adapter-verifier is the only consumer; fetching from arbitrary URLs is
6
- * forbidden. Allowlist:
7
- * - registry.massu.ai (manifest fetch)
8
- * - telemetry.massu.ai (adapter-discovery telemetry)
9
- *
10
- * Why an allowlist: an unrestricted fetch primitive in @massu/core would
11
- * be a supply-chain liability — a future bug or compromised adapter could
12
- * exfiltrate data to attacker-controlled hosts. The allowlist lives in
13
- * THIS module's source code (single source of truth), not in
14
- * massu.config.yaml — operators cannot widen it without auditing this
15
- * file.
16
- *
17
- * Why this module instead of a third-party http lib: Node 18+ has a built-in
18
- * fetch (per Plan 3c gap-3 audit-fact line 46 — "no deps for `node-fetch`").
19
- * This module wraps it with the allowlist + timeout + JSON parsing in a
20
- * single typed surface. The verifier doesn't need redirects, retry, or
21
- * streaming — those features are attack surface we don't want.
22
- */
23
-
24
- const DEFAULT_TIMEOUT_MS = 10_000;
25
- /**
26
- * CR-9 audit H3 fix: hard cap on response body size. Without this, a
27
- * compromised CDN (or TLS-MITM) could serve a multi-GB body that exceeds
28
- * available heap before the timeout fires. The manifest is on the order
29
- * of KB; even with 1000 adapter entries it should fit comfortably under
30
- * 1 MB. The 10 MB ceiling here is generous enough to never affect
31
- * legitimate manifests AND tight enough to bound damage from a malicious
32
- * upstream.
33
- */
34
- const DEFAULT_MAX_BODY_BYTES = 10 * 1024 * 1024;
35
-
36
- /**
37
- * Allowlist of hosts the fetcher will GET from. Exported for unit testing
38
- * but intentionally read-only (`as const`) — runtime mutation is impossible.
39
- * To widen the allowlist for a new use case, edit this constant in source
40
- * and ship a `@massu/core` minor release.
41
- */
42
- export const ALLOWED_HOSTS = [
43
- 'registry.massu.ai',
44
- 'telemetry.massu.ai',
45
- ] as const;
46
-
47
- export interface FetchUrlOptions {
48
- /** Maximum time the request may take, in milliseconds. Defaults to 10s. */
49
- timeoutMs?: number;
50
- /**
51
- * Override the allowlist for tests. Production code MUST NOT pass this —
52
- * the default ALLOWED_HOSTS is the production contract. Test-only.
53
- */
54
- allowedHosts?: readonly string[];
55
- /**
56
- * Override the response body size cap (test-only). Defaults to 10 MB.
57
- * CR-9 audit H3 fix: bounds OOM exposure from a malicious upstream.
58
- */
59
- maxBodyBytes?: number;
60
- }
61
-
62
- export interface FetchUrlResult {
63
- status: number;
64
- /** Response body as UTF-8 string. */
65
- body: string;
66
- }
67
-
68
- export class FetchAllowlistError extends Error {
69
- constructor(public readonly url: string, public readonly host: string) {
70
- super(
71
- `Refusing to fetch ${url}: host '${host}' is not in the @massu/core ` +
72
- `fetcher allowlist. Allowed hosts: ${ALLOWED_HOSTS.join(', ')}. ` +
73
- `Widening the allowlist requires editing packages/core/src/security/fetcher.ts ` +
74
- `at the source level.`,
75
- );
76
- this.name = 'FetchAllowlistError';
77
- }
78
- }
79
-
80
- export class FetchTimeoutError extends Error {
81
- constructor(public readonly url: string, public readonly timeoutMs: number) {
82
- super(`Fetch of ${url} timed out after ${timeoutMs}ms`);
83
- this.name = 'FetchTimeoutError';
84
- }
85
- }
86
-
87
- export class FetchBodySizeError extends Error {
88
- constructor(public readonly url: string, public readonly maxBodyBytes: number) {
89
- super(
90
- `Fetch of ${url} exceeded body size cap of ${maxBodyBytes} bytes. ` +
91
- `This indicates either a malicious upstream OR an unexpectedly large ` +
92
- `legitimate response. Refusing to load.`,
93
- );
94
- this.name = 'FetchBodySizeError';
95
- }
96
- }
97
-
98
- /**
99
- * GET an HTTPS URL with allowlist enforcement + timeout. Returns the response
100
- * body as a string (caller is responsible for JSON.parse + schema validation).
101
- *
102
- * Throws:
103
- * - FetchAllowlistError if the URL's host is not in ALLOWED_HOSTS (or the
104
- * test override). Includes the host name in the error so operators can
105
- * debug misconfigurations.
106
- * - FetchTimeoutError if the request exceeds the timeout.
107
- * - TypeError if the URL does not parse as a valid URL.
108
- * - Error (with .cause set to the underlying network error) for other failures.
109
- */
110
- export async function fetchUrl(url: string, opts: FetchUrlOptions = {}): Promise<FetchUrlResult> {
111
- const allowedHosts = opts.allowedHosts ?? ALLOWED_HOSTS;
112
- const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
113
- const maxBodyBytes = opts.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
114
-
115
- let parsed: URL;
116
- try {
117
- parsed = new URL(url);
118
- } catch (err) {
119
- throw new TypeError(`fetchUrl: invalid URL: ${url}`);
120
- }
121
-
122
- if (parsed.protocol !== 'https:') {
123
- throw new FetchAllowlistError(url, parsed.host);
124
- }
125
-
126
- if (!allowedHosts.includes(parsed.host as (typeof ALLOWED_HOSTS)[number])) {
127
- throw new FetchAllowlistError(url, parsed.host);
128
- }
129
-
130
- const controller = new AbortController();
131
- const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
132
-
133
- try {
134
- const response = await fetch(url, {
135
- method: 'GET',
136
- signal: controller.signal,
137
- redirect: 'error', // refuse redirects — would defeat the allowlist
138
- });
139
-
140
- // CR-9 audit H3 fix: stream the body with a hard byte cap. Without this,
141
- // a malicious upstream could OOM the host with a multi-GB response
142
- // before the wall-clock timeout fires. content-length header check is
143
- // a fast pre-filter; the streaming reader is the actual enforcement.
144
- // Headers can be absent in test mocks; treat missing as "no pre-filter".
145
- if (response.headers && typeof response.headers.get === 'function') {
146
- const contentLengthHeader = response.headers.get('content-length');
147
- if (contentLengthHeader !== null && contentLengthHeader !== undefined) {
148
- const declared = Number.parseInt(contentLengthHeader, 10);
149
- if (Number.isFinite(declared) && declared > maxBodyBytes) {
150
- throw new FetchBodySizeError(url, maxBodyBytes);
151
- }
152
- }
153
- }
154
-
155
- const body = response.body;
156
- if (body && typeof body.getReader === 'function') {
157
- // Streaming path: bound the read by maxBodyBytes. Used in production
158
- // (Node 18+ fetch returns a ReadableStream).
159
- const reader = body.getReader();
160
- const chunks: Uint8Array[] = [];
161
- let received = 0;
162
- while (true) {
163
- const { value, done } = await reader.read();
164
- if (done) break;
165
- if (value) {
166
- received += value.byteLength;
167
- if (received > maxBodyBytes) {
168
- try { await reader.cancel(); } catch { /* best effort */ }
169
- throw new FetchBodySizeError(url, maxBodyBytes);
170
- }
171
- chunks.push(value);
172
- }
173
- }
174
- const merged = new Uint8Array(received);
175
- let offset = 0;
176
- for (const chunk of chunks) {
177
- merged.set(chunk, offset);
178
- offset += chunk.byteLength;
179
- }
180
- return { status: response.status, body: new TextDecoder().decode(merged) };
181
- }
182
-
183
- // Fallback when response.body is not a ReadableStream (e.g. test
184
- // mocks that only stub `text()`). The size cap is still enforced
185
- // post-read because the text length is the byte length for ASCII +
186
- // a strict upper bound for UTF-8 content.
187
- const text = await response.text();
188
- if (text.length > maxBodyBytes) {
189
- throw new FetchBodySizeError(url, maxBodyBytes);
190
- }
191
- return { status: response.status, body: text };
192
- } catch (err) {
193
- if (err instanceof Error && err.name === 'AbortError') {
194
- throw new FetchTimeoutError(url, timeoutMs);
195
- }
196
- throw err;
197
- } finally {
198
- clearTimeout(timeoutHandle);
199
- }
200
- }
@@ -1,319 +0,0 @@
1
- /**
2
- * Install-time + load-time sha256 verification of REGISTRY-VERIFIED adapter
3
- * package contents (Plan 3c gap-37).
4
- *
5
- * Two checks defend against post-install tampering:
6
- *
7
- * 1. INSTALL-time: when the operator runs `npx massu adapters install <pkg>`,
8
- * this module walks the installed package's directory, computes a content-
9
- * addressed sha256, AND verifies it equals the sha256 the signed registry
10
- * manifest entry pinned. On match → record in
11
- * ~/.massu/adapter-manifest-installed.json with the version + timestamp.
12
- * Mismatch → refuse to register; the operator should `npm uninstall` the
13
- * suspicious package.
14
- *
15
- * 2. LOAD-time: discovery (detect/adapters/discover.ts) re-computes the
16
- * package's sha256 on every startup, compares to the install-time recorded
17
- * hash from the sidecar file. Mismatch → REFUSE to load (post-install
18
- * tampering of unpacked files in node_modules). The load-time check
19
- * compares to the LOCAL sidecar, NOT the live registry — re-fetching
20
- * from the registry every startup would be a network dependency on the
21
- * boot path (per Plan 3c gap-3 deliverable: NO network on boot).
22
- *
23
- * Why content-addressed (not tarball sha256):
24
- * The published tarball's sha256 is a moving target — npm tarballs include
25
- * timestamps + permission bits that vary across operating systems. The
26
- * SAME tarball extracted to two different machines produces different
27
- * tarball hashes but identical FILE CONTENT hashes. Content-addressed
28
- * hashing (sha256 of canonical-stringified path:fileSha pairs, sorted by
29
- * path) is stable across machines + filesystems.
30
- *
31
- * Per CR-46 / Rule 0 single-source-of-truth: this is the ONLY recursive
32
- * directory hashing in @massu/core. Future modules that need to hash
33
- * directories MUST consume sha256OfDir from here, not re-implement.
34
- */
35
- import { readFileSync, readdirSync, lstatSync, existsSync } from 'node:fs';
36
- import { join, relative, sep } from 'node:path';
37
- import { homedir } from 'node:os';
38
- import { resolve } from 'node:path';
39
- import { createHash } from 'node:crypto';
40
- import { z } from 'zod';
41
- import { atomicWrite } from './atomic-write.js';
42
-
43
- export const INSTALLED_MANIFEST_PATH = resolve(homedir(), '.massu', 'adapter-manifest-installed.json');
44
-
45
- /**
46
- * Compute a content-addressed sha256 of a directory recursively. Stable
47
- * across machines + filesystems: sorts files by relative POSIX path,
48
- * hashes each file's bytes, concatenates `<path>\0<sha256-hex>\n` per
49
- * file, then sha256s the whole concatenation.
50
- *
51
- * Symlinks are NOT followed (they're hashed as their target path content).
52
- * Directory entries (themselves) are NOT hashed (their content is implied
53
- * by the files inside). Files exceeding `maxFileBytes` (default 64 MB)
54
- * abort with an error — adapter packages should not ship huge binary
55
- * blobs; the cap is a sanity ceiling against accidental misuse.
56
- *
57
- * Excluded patterns (NEVER hashed; they are install-time artifacts that
58
- * vary across machines):
59
- * - .git/
60
- * - node_modules/ (transitive deps; their integrity is npm's concern)
61
- * - any path containing /.cache/ or /.tmp/
62
- */
63
- const DEFAULT_MAX_FILE_BYTES = 64 * 1024 * 1024;
64
- /**
65
- * Directory names that sha256OfDir EXCLUDES from hashing — these are
66
- * install-time artifacts that vary across machines (.git history,
67
- * transitive deps, build caches, scratch dirs). Their CONTENT is not
68
- * part of the adapter package's content-addressable hash.
69
- *
70
- * CR-9 audit M5 + iter-2 audit MED-NEW-1/-2 enforcement: a published
71
- * npm tarball MUST NOT ship these directories. Any package that does
72
- * is refused at install + resign + load-time discovery via
73
- * `containsHiddenDirs()` below. Without these refusals, a malicious
74
- * tarball could smuggle payload files under `.git/payload.js` (excluded
75
- * from the hash) and have them require()'d by the legitimate adapter
76
- * at runtime — hash matches, payload runs.
77
- */
78
- export const EXCLUDED_DIR_NAMES: ReadonlySet<string> = new Set(['.git', 'node_modules', '.cache', '.tmp']);
79
-
80
- /**
81
- * Returns the first hidden-dir name found in `packageDir`, or null if
82
- * none are present. Caller (install / resign / discovery) refuses the
83
- * package on non-null return.
84
- */
85
- export function containsHiddenDirs(packageDir: string): string | null {
86
- for (const hidden of EXCLUDED_DIR_NAMES) {
87
- if (existsSync(`${packageDir}/${hidden}`)) {
88
- return hidden;
89
- }
90
- }
91
- return null;
92
- }
93
-
94
- export interface Sha256OfDirOpts {
95
- /** Override the file-size cap (test-only). */
96
- maxFileBytes?: number;
97
- }
98
-
99
- export function sha256OfDir(dir: string, opts: Sha256OfDirOpts = {}): string {
100
- const maxFileBytes = opts.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
101
- const files: Array<{ relativePath: string; absPath: string }> = [];
102
-
103
- function walk(currentDir: string): void {
104
- let entries: string[];
105
- try {
106
- entries = readdirSync(currentDir);
107
- } catch {
108
- return;
109
- }
110
- for (const entry of entries.sort()) {
111
- const absPath = join(currentDir, entry);
112
- let lst;
113
- try {
114
- // CR-9 audit H1 fix: lstatSync (not statSync) so symlinks are
115
- // detected + skipped without following. Following symlinks would
116
- // expose a read-anywhere primitive (a malicious dist/x.js -> /etc/shadow
117
- // would have the target's content captured into the hash) AND would
118
- // cause readFileSync to block on named-pipe targets.
119
- lst = lstatSync(absPath);
120
- } catch {
121
- continue;
122
- }
123
- if (lst.isSymbolicLink()) {
124
- // Skip symlinks entirely — they're not part of the package's
125
- // content-addressable hash. A package author who legitimately wants
126
- // to ship a symlink (rare) must replace it with a real file at
127
- // publish time.
128
- continue;
129
- }
130
- if (lst.isDirectory()) {
131
- if (EXCLUDED_DIR_NAMES.has(entry)) continue;
132
- walk(absPath);
133
- continue;
134
- }
135
- if (!lst.isFile()) continue;
136
- if (lst.size > maxFileBytes) {
137
- throw new Error(
138
- `sha256OfDir: file ${absPath} exceeds maxFileBytes (${lst.size} > ${maxFileBytes}); ` +
139
- `adapter packages should not ship files this large.`,
140
- );
141
- }
142
- const rel = relative(dir, absPath).split(sep).join('/');
143
- files.push({ relativePath: rel, absPath });
144
- }
145
- }
146
- walk(dir);
147
-
148
- // Sort by relative POSIX path so the hash is stable regardless of
149
- // readdir order (some filesystems return entries in inode order).
150
- files.sort((a, b) => (a.relativePath < b.relativePath ? -1 : a.relativePath > b.relativePath ? 1 : 0));
151
-
152
- const top = createHash('sha256');
153
- for (const f of files) {
154
- const fileHash = createHash('sha256').update(readFileSync(f.absPath)).digest('hex');
155
- top.update(f.relativePath, 'utf-8');
156
- top.update('\0', 'utf-8');
157
- top.update(fileHash, 'utf-8');
158
- top.update('\n', 'utf-8');
159
- }
160
- return top.digest('hex');
161
- }
162
-
163
- /**
164
- * Per-package install record. Keyed by package name in the sidecar's
165
- * top-level object.
166
- */
167
- const InstallEntrySchema = z.object({
168
- // CR-9 iter-3 audit LOW-NEW3-1 fix: reject control characters in version.
169
- // Without this, a same-user attacker writing the install-tracking
170
- // sidecar could embed ANSI escapes (or other control chars) in version,
171
- // log-injecting via runAdaptersResign's `${name}@${entry.version}`
172
- // stderr emits. The regex permits printable ASCII (0x20-0x7e) plus
173
- // common semver characters; control chars (0x00-0x1f, 0x7f) are
174
- // rejected at parse time. Schema-level validation closes the vector
175
- // at every callsite that reads the sidecar — no per-callsite escaping
176
- // gymnastics needed.
177
- version: z.string().min(1).regex(/^[\x20-\x7e]+$/, 'version must be printable ASCII (no control characters)'),
178
- installed_sha256: z.string().regex(/^[0-9a-f]{64}$/),
179
- manifest_sha256: z.string().regex(/^[0-9a-f]{64}$/),
180
- ts: z.string().min(1).regex(/^[\x20-\x7e]+$/, 'ts must be printable ASCII (no control characters)'),
181
- }).strict();
182
- export type InstallEntry = z.infer<typeof InstallEntrySchema>;
183
-
184
- const InstalledManifestSchema = z.record(z.string(), InstallEntrySchema);
185
- export type InstalledManifest = z.infer<typeof InstalledManifestSchema>;
186
-
187
- /**
188
- * Read the install-tracking sidecar at ~/.massu/adapter-manifest-installed.json.
189
- * Returns an empty object when the file is absent OR fails parse / schema —
190
- * caller should treat absent + corrupt the same way (no install records, so
191
- * REGISTRY-VERIFIED adapters cannot satisfy the load-time check + are refused
192
- * until reinstalled via `npx massu adapters install`).
193
- */
194
- export function readInstalledManifest(path: string = INSTALLED_MANIFEST_PATH): InstalledManifest {
195
- if (!existsSync(path)) return {};
196
- let raw: unknown;
197
- try {
198
- raw = JSON.parse(readFileSync(path, 'utf-8'));
199
- } catch {
200
- return {};
201
- }
202
- const parsed = InstalledManifestSchema.safeParse(raw);
203
- return parsed.success ? parsed.data : {};
204
- }
205
-
206
- export type InstallTrackingWriteResult = { written: true } | { written: false; error: string };
207
-
208
- /**
209
- * Write a single package's install entry to the sidecar (additive). Reads
210
- * the current sidecar, updates the named key, writes back atomically with
211
- * mode 0o600. Concurrent calls to this function for DIFFERENT package
212
- * names are NOT serialized — the manifest cache lock at
213
- * ~/.massu/.adapter-manifest.lock applies only to the cache, not this
214
- * sidecar. Since the install flow runs interactively (one CLI invocation
215
- * at a time per shell), racing writes are not a practical concern.
216
- */
217
- export function writeInstalledManifestEntry(
218
- packageName: string,
219
- entry: InstallEntry,
220
- path: string = INSTALLED_MANIFEST_PATH,
221
- ): InstallTrackingWriteResult {
222
- // CR-9 iter-4 audit LOW-NEW4-1 fix: validate entry against the schema
223
- // BEFORE writing. TypeScript types do NOT enforce Zod regex constraints
224
- // at runtime; without this .parse(), a caller passing a control-char-
225
- // contaminated entry (e.g. version from an attacker-controlled
226
- // node_modules package.json) would write the malformed entry to disk,
227
- // and the corresponding stderr emit at the install/resign callsite
228
- // would log-inject before the next read-time validation could catch it.
229
- const validated = InstallEntrySchema.safeParse(entry);
230
- if (!validated.success) {
231
- const issues = validated.error.issues
232
- .map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`)
233
- .join('; ');
234
- return { written: false, error: `install-tracking entry shape invalid: ${issues}` };
235
- }
236
- const current = readInstalledManifest(path);
237
- current[packageName] = validated.data;
238
- const result = atomicWrite(path, JSON.stringify(current, null, 2), {
239
- mode: 0o600,
240
- ensureParentDirMode: 0o700,
241
- });
242
- if (!result.written) {
243
- return { written: false, error: result.error ?? 'unknown atomicWrite error' };
244
- }
245
- return { written: true };
246
- }
247
-
248
- /**
249
- * Remove a single package's install entry from the sidecar. Used by
250
- * `npx massu adapters resign --uninstall` (or by future cleanup tooling).
251
- * No-op if the entry is absent; sidecar file persists.
252
- */
253
- export function removeInstalledManifestEntry(
254
- packageName: string,
255
- path: string = INSTALLED_MANIFEST_PATH,
256
- ): InstallTrackingWriteResult {
257
- const current = readInstalledManifest(path);
258
- if (!(packageName in current)) {
259
- return { written: true };
260
- }
261
- delete current[packageName];
262
- const result = atomicWrite(path, JSON.stringify(current, null, 2), {
263
- mode: 0o600,
264
- ensureParentDirMode: 0o700,
265
- });
266
- if (!result.written) {
267
- return { written: false, error: result.error ?? 'unknown atomicWrite error' };
268
- }
269
- return { written: true };
270
- }
271
-
272
- export type IntegrityCheckResult =
273
- | { kind: 'ok'; entry: InstallEntry }
274
- | { kind: 'no-entry'; reason: string }
275
- | { kind: 'drift'; entry: InstallEntry; computedSha: string; reason: string };
276
-
277
- /**
278
- * Load-time integrity check for an installed REGISTRY-VERIFIED adapter
279
- * package. Compares the package directory's CURRENT sha256OfDir output
280
- * to the install-time hash recorded in the sidecar. Mismatch → drift,
281
- * caller refuses to load.
282
- *
283
- * Caller (typically discoverAdapters) interprets:
284
- * - 'ok' → load is safe
285
- * - 'no-entry' → package is in node_modules but never registered via
286
- * `massu adapters install`; refuse + tell operator to run install
287
- * - 'drift' → contents tampered after install; REFUSE; tell operator
288
- * to `npm uninstall` + `npm install` + `massu adapters install`
289
- */
290
- export function verifyInstalledIntegrity(
291
- packageName: string,
292
- packageDir: string,
293
- sidecarPath: string = INSTALLED_MANIFEST_PATH,
294
- ): IntegrityCheckResult {
295
- const installed = readInstalledManifest(sidecarPath);
296
- const entry = installed[packageName];
297
- if (!entry) {
298
- return {
299
- kind: 'no-entry',
300
- reason:
301
- `${packageName} is in node_modules but has no install-tracking entry in ~/.massu/adapter-manifest-installed.json. ` +
302
- `Run \`npx massu adapters install ${packageName}\` to register it.`,
303
- };
304
- }
305
- const computedSha = sha256OfDir(packageDir);
306
- if (computedSha !== entry.installed_sha256) {
307
- return {
308
- kind: 'drift',
309
- entry,
310
- computedSha,
311
- reason:
312
- `${packageName}@${entry.version} contents changed after install: ` +
313
- `expected sha256 ${entry.installed_sha256.slice(0, 16)}..., got ${computedSha.slice(0, 16)}.... ` +
314
- `This indicates post-install tampering of the package files. Recover by running ` +
315
- `\`npm uninstall ${packageName} && npm install ${packageName} && npx massu adapters install ${packageName}\`.`,
316
- };
317
- }
318
- return { kind: 'ok', entry };
319
- }
@@ -1,30 +0,0 @@
1
- // AUTO-GENERATED by scripts/bundle-license-pubkey.mjs at 2026-05-17T15:51:32.373Z.
2
- // Source pem: packages/core/security/license-pubkey.pem
3
- // RAW-bytes sha256: 18a63d64fdec9e5a368fc45feaa49bed6ced815967e582bc7b8af534f22a9475
4
- // DO NOT EDIT — regenerate via `node scripts/bundle-license-pubkey.mjs` or
5
- // the prepublishOnly hook (which runs on every npm publish).
6
-
7
- /**
8
- * Ed25519 public key for verifying signed /validate-key responses from
9
- * massu.ai. Used by packages/core/src/security/license-response-verifier.ts
10
- * to reject MITM / malicious-cloud-endpoint responses.
11
- *
12
- * P-H019 (plan-stage-c-high-batch / 1.10.5).
13
- *
14
- * To rotate: regenerate the keypair via Node crypto.generateKeyPairSync,
15
- * update packages/core/security/license-pubkey.pem with the new public key,
16
- * append the new RAW-bytes sha256 to KNOWN_PUBKEY_FINGERPRINTS in
17
- * scripts/bundle-license-pubkey.mjs (DO NOT delete the old entry during
18
- * the rotation grace window), then run `node scripts/bundle-license-pubkey.mjs`.
19
- * Operator then updates the Supabase Edge Function env var
20
- * `LICENSE_RESPONSE_SIGNING_PRIVATE_KEY_B64` with the new private key.
21
- */
22
- export const LICENSE_PUBKEY_ED25519: Uint8Array = new Uint8Array([39, 136, 108, 146, 85, 233, 119, 252, 223, 226, 123, 155, 234, 168, 200, 150, 36, 249, 174, 6, 130, 146, 125, 196, 136, 224, 202, 150, 53, 228, 114, 15]);
23
-
24
- /** Hex sha256 of LICENSE_PUBKEY_ED25519 (raw 32 bytes). */
25
- export const LICENSE_PUBKEY_FINGERPRINT_HEX = '18a63d64fdec9e5a368fc45feaa49bed6ced815967e582bc7b8af534f22a9475';
26
-
27
- /** Allowlist of historically-trusted license pubkey fingerprints. */
28
- export const KNOWN_LICENSE_PUBKEY_FINGERPRINTS: ReadonlySet<string> = new Set([
29
- "18a63d64fdec9e5a368fc45feaa49bed6ced815967e582bc7b8af534f22a9475"
30
- ]);