@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,149 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* P-H019 (plan-stage-c-high-batch / 1.10.5): Ed25519 verifier for signed
|
|
6
|
-
* /validate-key responses from massu.ai.
|
|
7
|
-
*
|
|
8
|
-
* **Bug class closed**: pre-fix, `packages/core/src/license.ts:280-318`
|
|
9
|
-
* accepted ANY `{ valid: true, plan: 'enterprise' }` JSON over HTTPS from
|
|
10
|
-
* whatever endpoint `config.cloud?.endpoint` pointed to. MITM, malicious
|
|
11
|
-
* cloud.endpoint, or local edit of the cached `license_cache` SQLite row
|
|
12
|
-
* could grant arbitrary tier.
|
|
13
|
-
*
|
|
14
|
-
* **Structural fix**: the server (validate-key Supabase Edge Function)
|
|
15
|
-
* signs every response with Ed25519. The client (this module, called
|
|
16
|
-
* from `license.ts`) verifies the signature against the bundled public
|
|
17
|
-
* key in `license-pubkey.generated.ts`. Invalid / missing signature →
|
|
18
|
-
* reject under strict mode, warn-and-accept under transition mode.
|
|
19
|
-
*
|
|
20
|
-
* **Transition mode** (default): when `MASSU_REQUIRE_SIGNED_LICENSE` env
|
|
21
|
-
* var is NOT `'true'`, unsigned/invalid-sig responses are accepted but
|
|
22
|
-
* a one-shot stderr warning is emitted. Lets existing customers keep
|
|
23
|
-
* working while operators provision the signing key.
|
|
24
|
-
*
|
|
25
|
-
* **Strict mode** (post-cutover): when `MASSU_REQUIRE_SIGNED_LICENSE=true`,
|
|
26
|
-
* unsigned/invalid-sig responses are REJECTED (caller drops to grace-period
|
|
27
|
-
* cached or 'free' tier). Operator flips this after Supabase Edge Function
|
|
28
|
-
* env var `LICENSE_RESPONSE_SIGNING_PRIVATE_KEY_B64` is provisioned and
|
|
29
|
-
* cutover smoke test passes.
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
import { createVerify, createPublicKey } from 'crypto';
|
|
33
|
-
import {
|
|
34
|
-
LICENSE_PUBKEY_ED25519,
|
|
35
|
-
LICENSE_PUBKEY_FINGERPRINT_HEX,
|
|
36
|
-
KNOWN_LICENSE_PUBKEY_FINGERPRINTS,
|
|
37
|
-
} from './license-pubkey.generated.ts';
|
|
38
|
-
|
|
39
|
-
export interface SignedLicenseResponse {
|
|
40
|
-
/** Base64-encoded Ed25519 signature over the canonical-serialized payload. */
|
|
41
|
-
_signature?: string;
|
|
42
|
-
/** Signature algorithm tag (currently always `'ed25519'`). */
|
|
43
|
-
_signature_alg?: string;
|
|
44
|
-
/**
|
|
45
|
-
* Sorted list of top-level keys (excluding the `_signature*` triplet) that
|
|
46
|
-
* were included in the signed payload. Lets the client reconstruct the
|
|
47
|
-
* canonical-serialization input deterministically.
|
|
48
|
-
*/
|
|
49
|
-
_signature_payload_keys?: readonly string[];
|
|
50
|
-
/** Hex sha256 of the LICENSE pubkey used for signing (so client can detect rotation). */
|
|
51
|
-
_signature_pubkey_fingerprint?: string;
|
|
52
|
-
// Plus the actual fields (plan, tier, validUntil, features, etc.)
|
|
53
|
-
[key: string]: unknown;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export type VerificationResult =
|
|
57
|
-
| { kind: 'valid' }
|
|
58
|
-
| { kind: 'missing_signature' }
|
|
59
|
-
| { kind: 'bad_signature' }
|
|
60
|
-
| { kind: 'unknown_pubkey'; got: string }
|
|
61
|
-
| { kind: 'error'; reason: string };
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Verify a server-signed /validate-key response. Returns a tagged union
|
|
65
|
-
* describing the verification outcome. Caller decides whether to accept
|
|
66
|
-
* based on strict/transition mode.
|
|
67
|
-
*/
|
|
68
|
-
export function verifyLicenseResponse(payload: SignedLicenseResponse): VerificationResult {
|
|
69
|
-
// Self-check: confirm the bundled key fingerprint matches the allowlist.
|
|
70
|
-
// CR-9 audit L2 pattern: a future build that swaps the pem to an unknown
|
|
71
|
-
// key refuses to load even if compiled in.
|
|
72
|
-
if (!KNOWN_LICENSE_PUBKEY_FINGERPRINTS.has(LICENSE_PUBKEY_FINGERPRINT_HEX)) {
|
|
73
|
-
return {
|
|
74
|
-
kind: 'error',
|
|
75
|
-
reason: `Bundled license pubkey fingerprint ${LICENSE_PUBKEY_FINGERPRINT_HEX} is not in the trusted allowlist. Possible build-time tamper.`,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const sig = payload._signature;
|
|
80
|
-
const alg = payload._signature_alg;
|
|
81
|
-
const payloadKeys = payload._signature_payload_keys;
|
|
82
|
-
const sigPubkey = payload._signature_pubkey_fingerprint;
|
|
83
|
-
|
|
84
|
-
if (typeof sig !== 'string' || sig.length === 0) {
|
|
85
|
-
return { kind: 'missing_signature' };
|
|
86
|
-
}
|
|
87
|
-
if (alg !== 'ed25519') {
|
|
88
|
-
return { kind: 'error', reason: `Unsupported signature algorithm: ${alg}` };
|
|
89
|
-
}
|
|
90
|
-
if (!Array.isArray(payloadKeys) || payloadKeys.length === 0) {
|
|
91
|
-
return { kind: 'error', reason: 'Missing _signature_payload_keys' };
|
|
92
|
-
}
|
|
93
|
-
if (typeof sigPubkey === 'string' && sigPubkey !== LICENSE_PUBKEY_FINGERPRINT_HEX) {
|
|
94
|
-
// Server signed with a different pubkey than we have bundled — rotation
|
|
95
|
-
// mismatch. Caller should fail-open under transition mode (operator
|
|
96
|
-
// hasn't published the new bundle yet) but reject under strict mode.
|
|
97
|
-
return { kind: 'unknown_pubkey', got: sigPubkey };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Reconstruct the canonical serialization the server signed:
|
|
101
|
-
// JSON.stringify({ k1: payload[k1], k2: payload[k2], ... }, payloadKeysSorted)
|
|
102
|
-
// where payloadKeysSorted is the sorted list of NON-underscore-prefixed keys.
|
|
103
|
-
const canonicalObj: Record<string, unknown> = {};
|
|
104
|
-
for (const k of payloadKeys) {
|
|
105
|
-
if (typeof k !== 'string') continue;
|
|
106
|
-
canonicalObj[k] = payload[k];
|
|
107
|
-
}
|
|
108
|
-
const canonical = JSON.stringify(canonicalObj, [...payloadKeys].sort());
|
|
109
|
-
|
|
110
|
-
// Verify signature.
|
|
111
|
-
try {
|
|
112
|
-
// Construct a node KeyObject from the bundled raw Ed25519 pubkey bytes.
|
|
113
|
-
// We use createPublicKey with SubjectPublicKeyInfo DER encoding (44 bytes:
|
|
114
|
-
// 12-byte algorithm OID prefix + 32-byte raw key).
|
|
115
|
-
const spkiPrefix = Buffer.from([
|
|
116
|
-
0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65,
|
|
117
|
-
0x70, 0x03, 0x21, 0x00,
|
|
118
|
-
]);
|
|
119
|
-
const der = Buffer.concat([spkiPrefix, Buffer.from(LICENSE_PUBKEY_ED25519)]);
|
|
120
|
-
const pubkey = createPublicKey({ key: der, format: 'der', type: 'spki' });
|
|
121
|
-
|
|
122
|
-
// Ed25519 in node uses `verify` directly (no createVerify needed for ed25519).
|
|
123
|
-
// crypto.verify(algorithm, data, key, signature) — algorithm MUST be null for Ed25519.
|
|
124
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
125
|
-
const { verify } = require('crypto');
|
|
126
|
-
const ok = verify(
|
|
127
|
-
null,
|
|
128
|
-
Buffer.from(canonical, 'utf-8'),
|
|
129
|
-
pubkey,
|
|
130
|
-
Buffer.from(sig, 'base64'),
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
return ok ? { kind: 'valid' } : { kind: 'bad_signature' };
|
|
134
|
-
} catch (err) {
|
|
135
|
-
return {
|
|
136
|
-
kind: 'error',
|
|
137
|
-
reason: `Signature verification threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* P-H019 strict-mode gate. Operator sets `MASSU_REQUIRE_SIGNED_LICENSE=true`
|
|
144
|
-
* AFTER the validate-key Edge Function is provisioned with
|
|
145
|
-
* `LICENSE_RESPONSE_SIGNING_PRIVATE_KEY_B64` and cutover smoke test passes.
|
|
146
|
-
*/
|
|
147
|
-
export function isLicenseSignatureRequired(): boolean {
|
|
148
|
-
return process.env.MASSU_REQUIRE_SIGNED_LICENSE === 'true';
|
|
149
|
-
}
|
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* adapters.local postinstall-poisoning fingerprint (Plan 3c gap-32).
|
|
3
|
-
*
|
|
4
|
-
* Threat model: a malicious npm package's `postinstall` script could mutate
|
|
5
|
-
* `massu.config.yaml > adapters.local` to add a path pointing at attacker-
|
|
6
|
-
* controlled code. Local adapters bypass the registry-signed allowlist
|
|
7
|
-
* entirely (operator opt-in per-path), so the only defense is verifying
|
|
8
|
-
* that the operator INTENDED the mutation. The mechanism:
|
|
9
|
-
*
|
|
10
|
-
* 1. Every time the operator runs `massu adapters add-local <path>`,
|
|
11
|
-
* `massu adapters remove-local <path>`, or `massu adapters resync-local-
|
|
12
|
-
* fingerprint`, this module writes a sentinel file at
|
|
13
|
-
* `~/.massu/adapters-local-fingerprint.json` recording:
|
|
14
|
-
* { fingerprint: <sha256-hex of canonical-stringified sorted array>,
|
|
15
|
-
* source: "cli" | "cli-resync",
|
|
16
|
-
* ts: ISO8601-string }
|
|
17
|
-
*
|
|
18
|
-
* 2. At loader startup, discoverAdapters compares the CURRENT
|
|
19
|
-
* massu.config.yaml.adapters.local content's fingerprint against the
|
|
20
|
-
* sentinel. If they differ, the loader REFUSES to load any local
|
|
21
|
-
* adapter and emits a stderr warning naming the additions/removals
|
|
22
|
-
* that diverged from the last operator-acknowledged state.
|
|
23
|
-
*
|
|
24
|
-
* 3. Operators can re-acknowledge the current state by running
|
|
25
|
-
* `massu adapters resync-local-fingerprint` — which recomputes the
|
|
26
|
-
* fingerprint over whatever adapters.local currently holds (regardless
|
|
27
|
-
* of how it got there) and writes the sentinel anew. This is
|
|
28
|
-
* explicitly a "trust me, I know what I edited" CLI escape hatch.
|
|
29
|
-
*
|
|
30
|
-
* Why a SEPARATE file (not stored inside the cache, not stored in the
|
|
31
|
-
* yaml itself):
|
|
32
|
-
* - In yaml: a postinstall script could update both the entry AND the
|
|
33
|
-
* fingerprint, defeating detection.
|
|
34
|
-
* - In ~/.massu/adapter-manifest.json (signed cache): the cache wraps
|
|
35
|
-
* registry data, mixing operator-trust state into it would conflate
|
|
36
|
-
* two different security domains.
|
|
37
|
-
* - Standalone: the file's path is well-known + stable; CLI-only
|
|
38
|
-
* writes are the explicit acknowledgment signal.
|
|
39
|
-
*
|
|
40
|
-
* File mode: 0o600 (gap-37 — security-relevant cache files are owner-only).
|
|
41
|
-
*
|
|
42
|
-
* Drift-prevention (CR-46 / Rule 0 self-attest #2): the SAME canonical-
|
|
43
|
-
* fingerprint computation is used at write time AND at check time —
|
|
44
|
-
* `computeLocalFingerprint` is the single source of truth. A future
|
|
45
|
-
* caller that hashes locally-different bytes would silently drift; this
|
|
46
|
-
* module's API makes that impossible by exposing only the high-level
|
|
47
|
-
* write/check primitives, not the raw sha256 step.
|
|
48
|
-
*/
|
|
49
|
-
import { existsSync, readFileSync, lstatSync } from 'node:fs';
|
|
50
|
-
import { homedir } from 'node:os';
|
|
51
|
-
import { resolve, isAbsolute } from 'node:path';
|
|
52
|
-
import { createHash } from 'node:crypto';
|
|
53
|
-
import { z } from 'zod';
|
|
54
|
-
import { atomicWrite } from './atomic-write.js';
|
|
55
|
-
import { PrintableAsciiStringSchema } from './manifest-schema.js';
|
|
56
|
-
|
|
57
|
-
export const FINGERPRINT_PATH = resolve(homedir(), '.massu', 'adapters-local-fingerprint.json');
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Canonical fingerprint: sha256 hex of `[path, sha256(content)]` tuples for
|
|
61
|
-
* each adapters.local entry, sorted by path. CR-9 audit C3 fix: hashing path
|
|
62
|
-
* STRINGS only (the prior shape) let a postinstall script swap the FILE at
|
|
63
|
-
* an already-acknowledged path with full bypass — fingerprint matched even
|
|
64
|
-
* though the file content changed. This implementation hashes BOTH the path
|
|
65
|
-
* AND the current file content, so swapping the file (even with a same-
|
|
66
|
-
* length payload) triggers drift on the very next discovery + the loader
|
|
67
|
-
* refuses until the operator runs `massu adapters resync-local-fingerprint`.
|
|
68
|
-
*
|
|
69
|
-
* Path inputs MUST be the AdapterLocalPathSchema-validated + POSIX-normalized
|
|
70
|
-
* content from getConfig().adapters?.local. `projectRoot` is the absolute
|
|
71
|
-
* path to the project so we can resolve relative entries to disk reads.
|
|
72
|
-
*
|
|
73
|
-
* Missing files: the fingerprint includes a sentinel string `<missing>` for
|
|
74
|
-
* any adapters.local entry that does not resolve to a regular file at
|
|
75
|
-
* fingerprint time. This means an absent file is part of the fingerprint
|
|
76
|
-
* — adding the file later is a drift event the operator must explicitly
|
|
77
|
-
* acknowledge. Symbolic links are detected via lstatSync + treated as
|
|
78
|
-
* `<symlink>` (also a sentinel; the link target is NEVER followed for
|
|
79
|
-
* hashing — a malicious symlink to /etc/shadow does not exfiltrate that
|
|
80
|
-
* file's content into the fingerprint).
|
|
81
|
-
*/
|
|
82
|
-
export function computeLocalFingerprint(
|
|
83
|
-
localPaths: ReadonlyArray<string>,
|
|
84
|
-
projectRoot: string,
|
|
85
|
-
): string {
|
|
86
|
-
const tuples: Array<{ path: string; contentTag: string }> = [];
|
|
87
|
-
for (const p of localPaths) {
|
|
88
|
-
const abs = isAbsolute(p) ? p : resolve(projectRoot, p);
|
|
89
|
-
let contentTag: string;
|
|
90
|
-
try {
|
|
91
|
-
const lst = lstatSync(abs);
|
|
92
|
-
if (lst.isSymbolicLink()) {
|
|
93
|
-
contentTag = '<symlink>';
|
|
94
|
-
} else if (!lst.isFile()) {
|
|
95
|
-
contentTag = '<not-a-file>';
|
|
96
|
-
} else {
|
|
97
|
-
contentTag = createHash('sha256').update(readFileSync(abs)).digest('hex');
|
|
98
|
-
}
|
|
99
|
-
} catch {
|
|
100
|
-
contentTag = '<missing>';
|
|
101
|
-
}
|
|
102
|
-
tuples.push({ path: p, contentTag });
|
|
103
|
-
}
|
|
104
|
-
tuples.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
105
|
-
const canonical = JSON.stringify(tuples);
|
|
106
|
-
return createHash('sha256').update(canonical).digest('hex');
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const FingerprintSentinelSchema = z.object({
|
|
110
|
-
fingerprint: z.string().regex(/^[0-9a-f]{64}$/),
|
|
111
|
-
source: z.enum(['cli', 'cli-resync']),
|
|
112
|
-
// CR-9 iter-5 audit LOW-NEW5-1 fix: ts is rendered to stderr in the
|
|
113
|
-
// drift warning at line 214 (`${sentinel.source} at ${sentinel.ts}`);
|
|
114
|
-
// a same-user attacker writing the sentinel file with control chars
|
|
115
|
-
// in ts would log-inject. Same vector iter-3 closed for
|
|
116
|
-
// InstallEntrySchema.ts; closing the parallel sentinel schema for
|
|
117
|
-
// single-source-of-truth consistency.
|
|
118
|
-
ts: PrintableAsciiStringSchema,
|
|
119
|
-
}).strict();
|
|
120
|
-
export type FingerprintSentinel = z.infer<typeof FingerprintSentinelSchema>;
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Read the on-disk sentinel. Returns null when:
|
|
124
|
-
* - file is absent (no operator action has ever been recorded)
|
|
125
|
-
* - file is present but unparseable (treat as absent — caller should
|
|
126
|
-
* surface a stderr warning about the corrupt sentinel)
|
|
127
|
-
* - file's shape doesn't match the strict schema
|
|
128
|
-
*
|
|
129
|
-
* Caller decides what to do with `null` (typically: refuse all
|
|
130
|
-
* LOCAL-EXPLICIT loading until `massu adapters resync-local-fingerprint`
|
|
131
|
-
* is run).
|
|
132
|
-
*/
|
|
133
|
-
export function readFingerprintSentinel(path: string = FINGERPRINT_PATH): FingerprintSentinel | null {
|
|
134
|
-
if (!existsSync(path)) return null;
|
|
135
|
-
let raw: unknown;
|
|
136
|
-
try {
|
|
137
|
-
raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
138
|
-
} catch {
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
const parsed = FingerprintSentinelSchema.safeParse(raw);
|
|
142
|
-
if (!parsed.success) return null;
|
|
143
|
-
return parsed.data;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export type FingerprintWriteResult = { written: true } | { written: false; error: string };
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Atomically write the sentinel with the given paths' fingerprint + source
|
|
150
|
-
* tag. Mode 0o600; parent dir 0o700. Uses the shared atomicWrite primitive
|
|
151
|
-
* so torn writes are impossible.
|
|
152
|
-
*/
|
|
153
|
-
export function writeFingerprintSentinel(
|
|
154
|
-
localPaths: ReadonlyArray<string>,
|
|
155
|
-
source: FingerprintSentinel['source'],
|
|
156
|
-
projectRoot: string,
|
|
157
|
-
path: string = FINGERPRINT_PATH,
|
|
158
|
-
): FingerprintWriteResult {
|
|
159
|
-
const sentinel: FingerprintSentinel = {
|
|
160
|
-
fingerprint: computeLocalFingerprint(localPaths, projectRoot),
|
|
161
|
-
source,
|
|
162
|
-
ts: new Date().toISOString(),
|
|
163
|
-
};
|
|
164
|
-
const result = atomicWrite(path, JSON.stringify(sentinel, null, 2), {
|
|
165
|
-
mode: 0o600,
|
|
166
|
-
ensureParentDirMode: 0o700,
|
|
167
|
-
});
|
|
168
|
-
if (!result.written) {
|
|
169
|
-
return { written: false, error: result.error ?? 'unknown atomicWrite error' };
|
|
170
|
-
}
|
|
171
|
-
return { written: true };
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export type FingerprintCheckResult =
|
|
175
|
-
| { kind: 'match'; sentinel: FingerprintSentinel }
|
|
176
|
-
| { kind: 'no-sentinel'; reason: string }
|
|
177
|
-
| { kind: 'drift'; sentinel: FingerprintSentinel; currentFingerprint: string; reason: string };
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Compare the current adapters.local fingerprint to the on-disk sentinel.
|
|
181
|
-
* Caller (typically discoverAdapters) interprets the result:
|
|
182
|
-
* - 'match' — proceed to load LOCAL-EXPLICIT adapters
|
|
183
|
-
* - 'no-sentinel' — refuse all LOCAL-EXPLICIT; tell operator to run
|
|
184
|
-
* `massu adapters resync-local-fingerprint` once
|
|
185
|
-
* - 'drift' — refuse all LOCAL-EXPLICIT; surface the additions/
|
|
186
|
-
* removals so the operator can audit + ack via
|
|
187
|
-
* `massu adapters resync-local-fingerprint`
|
|
188
|
-
*
|
|
189
|
-
* Note: `localPaths` MUST be the AdapterLocalPathSchema-validated +
|
|
190
|
-
* POSIX-normalized content from getConfig() — passing a non-normalized
|
|
191
|
-
* array will produce a fingerprint that doesn't match what the CLI
|
|
192
|
-
* wrote. Drift-prevention from the schema side: AdapterLocalPathSchema
|
|
193
|
-
* runs at config-parse time, so any code path reading
|
|
194
|
-
* cfg.adapters.local always sees the canonical form.
|
|
195
|
-
*/
|
|
196
|
-
export function checkFingerprintDrift(
|
|
197
|
-
localPaths: ReadonlyArray<string>,
|
|
198
|
-
projectRoot: string,
|
|
199
|
-
path: string = FINGERPRINT_PATH,
|
|
200
|
-
): FingerprintCheckResult {
|
|
201
|
-
const sentinel = readFingerprintSentinel(path);
|
|
202
|
-
if (!sentinel) {
|
|
203
|
-
return {
|
|
204
|
-
kind: 'no-sentinel',
|
|
205
|
-
reason:
|
|
206
|
-
`no adapters-local-fingerprint sentinel at ${path}. ` +
|
|
207
|
-
`If you have entries in adapters.local, run \`massu adapters resync-local-fingerprint\` ` +
|
|
208
|
-
`to acknowledge them once.`,
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
const currentFingerprint = computeLocalFingerprint(localPaths, projectRoot);
|
|
212
|
-
if (currentFingerprint === sentinel.fingerprint) {
|
|
213
|
-
return { kind: 'match', sentinel };
|
|
214
|
-
}
|
|
215
|
-
return {
|
|
216
|
-
kind: 'drift',
|
|
217
|
-
sentinel,
|
|
218
|
-
currentFingerprint,
|
|
219
|
-
reason:
|
|
220
|
-
`adapters.local fingerprint drift: sentinel was ${sentinel.fingerprint.slice(0, 16)}... ` +
|
|
221
|
-
`(written by ${sentinel.source} at ${sentinel.ts}); current is ${currentFingerprint.slice(0, 16)}.... ` +
|
|
222
|
-
`If you edited massu.config.yaml directly OR a postinstall script may have mutated adapters.local, ` +
|
|
223
|
-
`audit the diff and run \`massu adapters resync-local-fingerprint\` to acknowledge.`,
|
|
224
|
-
};
|
|
225
|
-
}
|