codehere 1.0.1 → 1.0.3
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/README.md +35 -193
- package/dist/application/use-cases/check-claimed-tests.js +81 -0
- package/dist/application/use-cases/detect-test-weakening.js +237 -0
- package/dist/application/use-cases/rerun-tests.js +110 -7
- package/dist/index.js +38 -1212
- package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -323
- package/dist/presentation/cli/commands/check-command.js +178 -23
- package/dist/presentation/cli/commands/help-command.js +21 -285
- package/dist/ui.js +5 -236
- package/dist/utils/version.js +0 -6
- package/package.json +9 -38
- package/dist/application/services/verdict-service.js +0 -154
- package/dist/application/use-cases/assess-cross-vendor.js +0 -126
- package/dist/application/use-cases/build-cross-vendor-verdict.js +0 -118
- package/dist/application/use-cases/domain-trust.js +0 -110
- package/dist/application/use-cases/export-aat.js +0 -107
- package/dist/application/use-cases/generate-acceptance-criteria.js +0 -223
- package/dist/application/use-cases/generate-final-report.js +0 -259
- package/dist/application/use-cases/generate-narration.js +0 -130
- package/dist/application/use-cases/generate-thread-title.js +0 -181
- package/dist/application/use-cases/narrator-prompt-template.js +0 -158
- package/dist/application/use-cases/plan-auto-review.js +0 -57
- package/dist/application/use-cases/reconcile-audit.js +0 -100
- package/dist/application/use-cases/search-sessions.js +0 -70
- package/dist/application/use-cases/should-auto-review.js +0 -71
- package/dist/application/use-cases/thread-export.js +0 -245
- package/dist/application/use-cases/verify-claim.js +0 -456
- package/dist/application/use-cases/write-pass.js +0 -277
- package/dist/domain/audit.js +0 -11
- package/dist/domain/services/codebase-detector.js +0 -85
- package/dist/domain/task.js +0 -91
- package/dist/error-handler.js +0 -139
- package/dist/infrastructure/completion/bash-completion.js +0 -29
- package/dist/infrastructure/completion/fish-completion.js +0 -17
- package/dist/infrastructure/completion/zsh-completion.js +0 -25
- package/dist/infrastructure/config/config-manager.js +0 -174
- package/dist/infrastructure/config/product-context.js +0 -49
- package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
- package/dist/infrastructure/context/adapters/figma-source.js +0 -198
- package/dist/infrastructure/context/adapters/github-issue-source.js +0 -160
- package/dist/infrastructure/context/adapters/linear-source.js +0 -190
- package/dist/infrastructure/context/context-resolver.js +0 -144
- package/dist/infrastructure/context/context-source.js +0 -114
- package/dist/infrastructure/context/github-api.js +0 -110
- package/dist/infrastructure/context/github-device-flow.js +0 -65
- package/dist/infrastructure/context/oauth-flow.js +0 -207
- package/dist/infrastructure/context/oauth-store.js +0 -122
- package/dist/infrastructure/context/registry-bootstrap.js +0 -95
- package/dist/infrastructure/context/write-pass-resolver.js +0 -106
- package/dist/infrastructure/cost/budget-config.js +0 -114
- package/dist/infrastructure/cost/cost-router.js +0 -97
- package/dist/infrastructure/cost/spend-aggregator.js +0 -222
- package/dist/infrastructure/cost/team-ledger.js +0 -355
- package/dist/infrastructure/discovery/discovery-log.js +0 -85
- package/dist/infrastructure/hooks/audit-tip-signature.js +0 -336
- package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -1018
- package/dist/infrastructure/hooks/hook-mode.js +0 -87
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -49
- package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
- package/dist/infrastructure/hooks/session-analyzer.js +0 -177
- package/dist/infrastructure/observability/execution-tracer.js +0 -227
- package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
- package/dist/infrastructure/orchestration/agent-installer.js +0 -101
- package/dist/infrastructure/orchestration/agents-config-seed.js +0 -68
- package/dist/infrastructure/orchestration/aider-adapter.js +0 -611
- package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
- package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -592
- package/dist/infrastructure/orchestration/codex-adapter.js +0 -574
- package/dist/infrastructure/orchestration/conductor-adapter.js +0 -416
- package/dist/infrastructure/orchestration/context-handoff.js +0 -162
- package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
- package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
- package/dist/infrastructure/orchestration/detect-ollama.js +0 -75
- package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
- package/dist/infrastructure/orchestration/failover-config.js +0 -67
- package/dist/infrastructure/orchestration/failover-policy.js +0 -106
- package/dist/infrastructure/orchestration/gemini-adapter.js +0 -663
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -689
- package/dist/infrastructure/orchestration/graphify-context.js +0 -209
- package/dist/infrastructure/orchestration/memory-injector.js +0 -380
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -388
- package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
- package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
- package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
- package/dist/infrastructure/orchestration/opencode-adapter.js +0 -580
- package/dist/infrastructure/orchestration/progress-aggregator.js +0 -906
- package/dist/infrastructure/orchestration/readiness.js +0 -103
- package/dist/infrastructure/orchestration/session-manager.js +0 -1590
- package/dist/infrastructure/orchestration/session-persistence.js +0 -394
- package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -38
- package/dist/infrastructure/protocols/cloud-sync.js +0 -207
- package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
- package/dist/infrastructure/protocols/mcp-server-impl.js +0 -3935
- package/dist/infrastructure/protocols/mcp-tools.js +0 -401
- package/dist/infrastructure/protocols/probe-port.js +0 -36
- package/dist/infrastructure/protocols/thread-phase.js +0 -105
- package/dist/infrastructure/protocols/ui/app-script-artifacts.js +0 -121
- package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +0 -44
- package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +0 -56
- package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +0 -588
- package/dist/infrastructure/protocols/ui/app-script-context-sources.js +0 -168
- package/dist/infrastructure/protocols/ui/app-script-conversation.js +0 -67
- package/dist/infrastructure/protocols/ui/app-script-drawer.js +0 -115
- package/dist/infrastructure/protocols/ui/app-script-failover.js +0 -133
- package/dist/infrastructure/protocols/ui/app-script-github.js +0 -113
- package/dist/infrastructure/protocols/ui/app-script-observer.js +0 -104
- package/dist/infrastructure/protocols/ui/app-script-onboarding.js +0 -80
- package/dist/infrastructure/protocols/ui/app-script-overlay.js +0 -203
- package/dist/infrastructure/protocols/ui/app-script-recovery.js +0 -136
- package/dist/infrastructure/protocols/ui/app-script-render.js +0 -166
- package/dist/infrastructure/protocols/ui/app-script-share.js +0 -78
- package/dist/infrastructure/protocols/ui/app-script-sidebar.js +0 -194
- package/dist/infrastructure/protocols/ui/app-script-tour.js +0 -78
- package/dist/infrastructure/protocols/ui/app-script-wpass.js +0 -312
- package/dist/infrastructure/protocols/ui/app-script.js +0 -2998
- package/dist/infrastructure/protocols/ui/font-faces.js +0 -7
- package/dist/infrastructure/protocols/ui/styles.js +0 -2147
- package/dist/infrastructure/protocols/ui/tokens.js +0 -96
- package/dist/infrastructure/protocols/ui-page-v2.js +0 -320
- package/dist/infrastructure/protocols/verify-page.js +0 -1677
- package/dist/infrastructure/security/file-collection.js +0 -118
- package/dist/infrastructure/security/fix-code.js +0 -144
- package/dist/infrastructure/security/fix-secret.js +0 -89
- package/dist/infrastructure/security/permissions-config.js +0 -235
- package/dist/infrastructure/security/scan-types.js +0 -10
- package/dist/infrastructure/security/ship-scan.js +0 -195
- package/dist/infrastructure/security/trust-scorer.js +0 -724
- package/dist/infrastructure/security/vibe-checks.js +0 -99
- package/dist/infrastructure/storage/atomic-write.js +0 -86
- package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
- package/dist/infrastructure/storage/project-thread-migration.js +0 -237
- package/dist/infrastructure/storage/project-thread-mirror.js +0 -671
- package/dist/infrastructure/storage/project-thread-reader.js +0 -409
- package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
- package/dist/monitoring.js +0 -212
- package/dist/presentation/cli/commands/audit-command.js +0 -201
- package/dist/presentation/cli/commands/budget-command.js +0 -79
- package/dist/presentation/cli/commands/config-command.js +0 -125
- package/dist/presentation/cli/commands/context-source-command.js +0 -217
- package/dist/presentation/cli/commands/hook-command.js +0 -265
- package/dist/presentation/cli/commands/init-command.js +0 -590
- package/dist/presentation/cli/commands/memory-command.js +0 -1031
- package/dist/presentation/cli/commands/narrator-command.js +0 -101
- package/dist/presentation/cli/commands/permission-command.js +0 -105
- package/dist/presentation/cli/commands/protocols-builder.js +0 -254
- package/dist/presentation/cli/commands/protocols-command.js +0 -84
- package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
- package/dist/presentation/cli/commands/report-command.js +0 -403
- package/dist/presentation/cli/commands/scan-command.js +0 -52
- package/dist/presentation/cli/commands/scan-tool-command.js +0 -110
- package/dist/presentation/cli/commands/search-command.js +0 -51
- package/dist/presentation/cli/commands/setup-command.js +0 -330
- package/dist/presentation/cli/commands/status-command.js +0 -614
- package/dist/presentation/cli/commands/team-command.js +0 -256
- package/dist/presentation/cli/commands/thread-command.js +0 -370
- package/dist/presentation/cli/commands/trust-command.js +0 -328
- package/dist/presentation/cli/commands/verify-command.js +0 -298
- package/dist/presentation/cli/error-display.js +0 -328
- package/dist/presentation/cli/repl.js +0 -805
- package/dist/presentation/cli/task-deliverable.js +0 -153
- package/dist/testing/cassette.js +0 -125
- package/dist/testing/replay-adapter.js +0 -147
|
@@ -1,336 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* R2 · sign the live audit-chain TIP with the machine's persisted ed25519 key.
|
|
3
|
-
*
|
|
4
|
-
* The hash chain catches a NAIVE edit, but a determined attacker can edit a
|
|
5
|
-
* mid-chain entry and RE-WALK every later hash to produce a chain that verifies
|
|
6
|
-
* green (`computeEntryHash` is public). Signing the tip closes that for anyone
|
|
7
|
-
* who verifies with the public key: a re-walk changes the hash of the SIGNED
|
|
8
|
-
* entry, so the stored signature — made with a key the tamperer doesn't hold —
|
|
9
|
-
* no longer matches. Truncation below the signed point likewise fails.
|
|
10
|
-
*
|
|
11
|
-
* VERIFICATION IS AN ANCESTOR CHECK, not a tip-equality check (devil's-advocate
|
|
12
|
-
* audit 2026-06-09 caught the original tip-equality version false-alarming
|
|
13
|
-
* "tampered" on honest chains): the chain appends CONTINUOUSLY (every tool call,
|
|
14
|
-
* spawns, overrides) while signing happens at session completion — so the signed
|
|
15
|
-
* entry is usually an ANCESTOR of the live tip, and that is honest. Verification
|
|
16
|
-
* asserts the signed (seq, entryHash) still exists UNMODIFIED at that seq.
|
|
17
|
-
* Entries appended after the most recent signing are not yet covered — they're
|
|
18
|
-
* covered by the next completion's signature. The protected prefix only ever
|
|
19
|
-
* grows.
|
|
20
|
-
*
|
|
21
|
-
* `rm <audit>.tipsig` bypass (same audit): once a chain has EVER been signed, a
|
|
22
|
-
* missing signature file must BLOCK — otherwise the forger just deletes the
|
|
23
|
-
* sidecar. Sign-state lives in the keystore dir (`keys/audit-sign-state.json`),
|
|
24
|
-
* separate from the audit file, so an attacker who deletes the sidecar but not
|
|
25
|
-
* the keystore is caught. NOTE: sign-state is LOCAL-ONLY and path-keyed — it does
|
|
26
|
-
* NOT travel with an exported chain, so deleted-sidecar detection only works on
|
|
27
|
-
* the machine that signed; an exported/shared chain carries its `.tipsig` but a
|
|
28
|
-
* recipient can't tell it was ever signed if the sidecar is stripped. (An
|
|
29
|
-
* owner-level attacker who can also rewrite the keystore defeats this entirely —
|
|
30
|
-
* see HONEST SCOPE; the real fix is R1's daemon-held key + R3's external anchor.)
|
|
31
|
-
*
|
|
32
|
-
* R1 DOES NOT CLOSE THE OWNER GAP — evaluated 2026-08-08. A daemon-held key is
|
|
33
|
-
* still reachable by the owner, because the daemon runs as the owner. R1 raises
|
|
34
|
-
* the cost of the attack; it does not change who can defeat it. Only an EXTERNAL
|
|
35
|
-
* anchor does, and that collides head-on with local-first + zero telemetry: an
|
|
36
|
-
* anchor has to be outside the machine to be an anchor. Those two values are in
|
|
37
|
-
* direct tension and no amount of local cryptography resolves it.
|
|
38
|
-
*
|
|
39
|
-
* THE ANCHOR THAT FITS, and it needs no service of ours: the user's own git
|
|
40
|
-
* remote. `git notes --ref=codehere-audit` on HEAD carries the signed tip, and
|
|
41
|
-
* nothing leaves the machine until the USER pushes — which they were doing
|
|
42
|
-
* anyway. Proven end-to-end 2026-08-08 in a throwaway repo:
|
|
43
|
-
* · the commit SHA is unchanged and the working tree stays clean (non-invasive)
|
|
44
|
-
* · a history rewrite ORPHANS the note, so the rewrite is visible
|
|
45
|
-
* · after the owner deletes the note locally, the REMOTE still holds it and a
|
|
46
|
-
* reviewer can fetch it back and read the original tip hash
|
|
47
|
-
* · erasing the remote copy needs an explicit destructive push, which shows up
|
|
48
|
-
* in host audit logs / branch protection / anyone who already fetched
|
|
49
|
-
* So it converts "silently forgeable by the owner" into "forgeable only by a
|
|
50
|
-
* visible act on a third party the owner does not solely control" — the same
|
|
51
|
-
* standard a signed git commit gets, no more and no less. It is NOT
|
|
52
|
-
* cryptographic owner-proofing; nothing local-first can be. Known costs:
|
|
53
|
-
* refs/notes are not pushed by default (needs a refspec), and a user who never
|
|
54
|
-
* pushes has no witness. It also lands exactly on the PR surface.
|
|
55
|
-
* NOT BUILT — this is the evaluation, not the feature.
|
|
56
|
-
*
|
|
57
|
-
* HONEST SCOPE: this defends against a 3RD PARTY (or the owner sharing an export)
|
|
58
|
-
* editing the chain after it was signed. It does NOT defend against the agent or
|
|
59
|
-
* owner who CAN reach the signing key + keystore and re-sign / rewrite sign-state
|
|
60
|
-
* — that needs a daemon-held key the agent can't reach (R1) + an external anchor
|
|
61
|
-
* (R3). SIGNATURE-ORACLE limit (R1.3): a signature attests the daemon
|
|
62
|
-
* RECORDED the entry — never that the described tool call actually happened
|
|
63
|
-
* (any local process can POST an honest-shaped entry). Surface wording must
|
|
64
|
-
* stay "recorded-by-daemon"-shaped. The tip signature also TRANSITIVELY
|
|
65
|
-
* notarizes the whole prefix — including hook-direct entries and any
|
|
66
|
-
* well-chained same-uid direct append (even one carrying a forged
|
|
67
|
-
* writer:'daemon'; the endpoint's rejection binds only endpoint callers).
|
|
68
|
-
* Until R3 the honest words remain
|
|
69
|
-
* "integrity-checked" + "signed at
|
|
70
|
-
* write-time".
|
|
71
|
-
*
|
|
72
|
-
* Deliberately standalone (reads the chain from the file itself) so the audit
|
|
73
|
-
* hot-path in claude-sdk-hook.ts never has to import the signing stack — keeps
|
|
74
|
-
* the dependency graph acyclic.
|
|
75
|
-
*/
|
|
76
|
-
import { existsSync, readFileSync, writeFileSync, chmodSync, mkdirSync, openSync, fstatSync, readSync, closeSync, renameSync, } from 'fs';
|
|
77
|
-
import { join } from 'path';
|
|
78
|
-
import { sign as edSign, verify as edVerify } from 'crypto';
|
|
79
|
-
import { loadOrCreateKeypair, keysDir } from '../orchestration/thread-key-store.js';
|
|
80
|
-
import { exportPublicPem, importPublicPem, publicKeyToBase64, } from '../../application/use-cases/thread-export.js';
|
|
81
|
-
/** Read the audit chain's current tip (last real line's seq + entryHash).
|
|
82
|
-
* TAIL-READ (last 64KB), not a full-file read: sign-at-write (R1.3) calls
|
|
83
|
-
* this per daemon append on a file with no rotation — a full read is O(N)
|
|
84
|
-
* on the daemon event loop, measured 4-5ms on a 6.4MB chain vs 0.2ms for
|
|
85
|
-
* the tail (refuter B2). The tip is by definition in the tail. */
|
|
86
|
-
export function readAuditTip(auditFile) {
|
|
87
|
-
if (!existsSync(auditFile))
|
|
88
|
-
return null;
|
|
89
|
-
try {
|
|
90
|
-
const fd = openSync(auditFile, 'r');
|
|
91
|
-
let text;
|
|
92
|
-
try {
|
|
93
|
-
const size = fstatSync(fd).size;
|
|
94
|
-
const start = Math.max(0, size - 65536);
|
|
95
|
-
const buf = Buffer.alloc(size - start);
|
|
96
|
-
readSync(fd, buf, 0, buf.length, start);
|
|
97
|
-
text = buf.toString('utf-8');
|
|
98
|
-
if (start > 0)
|
|
99
|
-
text = text.slice(text.indexOf('\n') + 1); // drop partial first line
|
|
100
|
-
}
|
|
101
|
-
finally {
|
|
102
|
-
closeSync(fd);
|
|
103
|
-
}
|
|
104
|
-
const lines = text.trimEnd().split('\n');
|
|
105
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
106
|
-
const line = lines[i].trim();
|
|
107
|
-
if (!line)
|
|
108
|
-
continue;
|
|
109
|
-
const e = JSON.parse(line);
|
|
110
|
-
if (typeof e.seq === 'number' && typeof e.entryHash === 'string') {
|
|
111
|
-
return { seq: e.seq, entryHash: e.entryHash };
|
|
112
|
-
}
|
|
113
|
-
return null; // last real line is a pre-chain legacy entry → no tip to sign
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
/* unreadable / malformed → no tip */
|
|
118
|
-
}
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
/** Read the entry hash recorded at a given seq, or null if no such entry. */
|
|
122
|
-
function readEntryHashAtSeq(auditFile, seq) {
|
|
123
|
-
if (!existsSync(auditFile))
|
|
124
|
-
return null;
|
|
125
|
-
try {
|
|
126
|
-
for (const line of readFileSync(auditFile, 'utf-8').split('\n')) {
|
|
127
|
-
const t = line.trim();
|
|
128
|
-
if (!t)
|
|
129
|
-
continue;
|
|
130
|
-
const e = JSON.parse(t);
|
|
131
|
-
if (e.seq === seq && typeof e.entryHash === 'string')
|
|
132
|
-
return e.entryHash;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
/* malformed line mid-file → treat as not found (the chain verifier reports it) */
|
|
137
|
-
}
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
function tipSigPath(auditFile) {
|
|
141
|
-
return auditFile + '.tipsig';
|
|
142
|
-
}
|
|
143
|
-
function tipMessage(tip) {
|
|
144
|
-
return Buffer.from(`${tip.seq}:${tip.entryHash}`, 'utf-8');
|
|
145
|
-
}
|
|
146
|
-
function signStatePath() {
|
|
147
|
-
return join(keysDir(), 'audit-sign-state.json');
|
|
148
|
-
}
|
|
149
|
-
function loadSignState() {
|
|
150
|
-
try {
|
|
151
|
-
const raw = JSON.parse(readFileSync(signStatePath(), 'utf-8'));
|
|
152
|
-
if (raw && raw.schema === 1 && typeof raw.files === 'object')
|
|
153
|
-
return raw;
|
|
154
|
-
}
|
|
155
|
-
catch {
|
|
156
|
-
/* missing/corrupt → empty */
|
|
157
|
-
}
|
|
158
|
-
return { schema: 1, files: {} };
|
|
159
|
-
}
|
|
160
|
-
function recordSignState(auditFile, seq, signedAt) {
|
|
161
|
-
try {
|
|
162
|
-
mkdirSync(keysDir(), { recursive: true });
|
|
163
|
-
const state = loadSignState();
|
|
164
|
-
state.files[auditFile] = { seq, signedAt };
|
|
165
|
-
{
|
|
166
|
-
const tmpState = signStatePath() + '.tmp';
|
|
167
|
-
writeFileSync(tmpState, JSON.stringify(state, null, 2) + '\n', { mode: 0o600 });
|
|
168
|
-
renameSync(tmpState, signStatePath());
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
catch {
|
|
172
|
-
/* best-effort — sign-state is a detection aid, never blocks signing */
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
/** Was this audit file ever signed (per the keystore's sign-state)? */
|
|
176
|
-
export function chainWasSigned(auditFile) {
|
|
177
|
-
return !!loadSignState().files[auditFile];
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* S9 · forget a chain's sign-state entry. ONLY for the mock practice chain:
|
|
181
|
-
* the daemon wipes the mock chain at boot, and without pruning, the stale
|
|
182
|
-
* sign-state key would flag the fresh chain as an rm-bypass (a false tamper
|
|
183
|
-
* alarm in practice mode) while dead keys accumulate in the REAL keystore.
|
|
184
|
-
* Never call this for a real chain — losing sign-state is losing detection.
|
|
185
|
-
*/
|
|
186
|
-
export function pruneSignState(auditFile) {
|
|
187
|
-
try {
|
|
188
|
-
const state = loadSignState();
|
|
189
|
-
if (!state.files[auditFile])
|
|
190
|
-
return;
|
|
191
|
-
delete state.files[auditFile];
|
|
192
|
-
const tmpState = signStatePath() + '.tmp';
|
|
193
|
-
writeFileSync(tmpState, JSON.stringify(state, null, 2) + '\n', { mode: 0o600 });
|
|
194
|
-
renameSync(tmpState, signStatePath());
|
|
195
|
-
}
|
|
196
|
-
catch {
|
|
197
|
-
/* best-effort — same contract as recordSignState */
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
// Cache the keypair per-process — loadOrCreateKeypair does disk I/O each call,
|
|
201
|
-
// and signing happens repeatedly in a long-lived daemon.
|
|
202
|
-
let cachedKeys = null;
|
|
203
|
-
function keys() {
|
|
204
|
-
return (cachedKeys ??= loadOrCreateKeypair());
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Sign the current chain tip, writing `<auditFile>.tipsig` + recording
|
|
208
|
-
* sign-state in the keystore. Best-effort: returns null (no-op) when there's
|
|
209
|
-
* no chain yet. Never throws.
|
|
210
|
-
*/
|
|
211
|
-
export function signAuditTip(auditFile, now = Date.now()) {
|
|
212
|
-
const tip = readAuditTip(auditFile);
|
|
213
|
-
if (!tip)
|
|
214
|
-
return null;
|
|
215
|
-
try {
|
|
216
|
-
const k = keys();
|
|
217
|
-
const sig = edSign(null, tipMessage(tip), k.privateKey).toString('base64');
|
|
218
|
-
const rec = {
|
|
219
|
-
schema: 1,
|
|
220
|
-
alg: 'ed25519',
|
|
221
|
-
seq: tip.seq,
|
|
222
|
-
entryHash: tip.entryHash,
|
|
223
|
-
sig,
|
|
224
|
-
pubPem: exportPublicPem(k.publicKey),
|
|
225
|
-
signedAt: now,
|
|
226
|
-
};
|
|
227
|
-
// Atomic (tmp+rename): per-write signing multiplies rewrite frequency
|
|
228
|
-
// ~1000x vs per-session — a crash mid-write must not leave a corrupt
|
|
229
|
-
// .tipsig that fails honest verification (refuter N1).
|
|
230
|
-
const tmpSig = tipSigPath(auditFile) + '.tmp';
|
|
231
|
-
writeFileSync(tmpSig, JSON.stringify(rec) + '\n', { mode: 0o600 });
|
|
232
|
-
renameSync(tmpSig, tipSigPath(auditFile));
|
|
233
|
-
try {
|
|
234
|
-
chmodSync(tipSigPath(auditFile), 0o600);
|
|
235
|
-
}
|
|
236
|
-
catch {
|
|
237
|
-
/* perms best-effort */
|
|
238
|
-
}
|
|
239
|
-
recordSignState(auditFile, tip.seq, now);
|
|
240
|
-
return rec;
|
|
241
|
-
}
|
|
242
|
-
catch {
|
|
243
|
-
return null; // signing must never break the path that called it
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* Verify the tip signature against the chain. The signed (seq, entryHash) must
|
|
248
|
-
* still be an UNMODIFIED ancestor of the current chain — appends since signing
|
|
249
|
-
* are honest and expected (signing happens at session completion; the chain
|
|
250
|
-
* grows continuously between completions). Pass `trustedKeys` to also check the
|
|
251
|
-
* signer is known.
|
|
252
|
-
*/
|
|
253
|
-
export function verifyAuditTip(auditFile, trustedKeys) {
|
|
254
|
-
const base = {
|
|
255
|
-
present: false,
|
|
256
|
-
expectedButMissing: false,
|
|
257
|
-
signatureValid: false,
|
|
258
|
-
ancestorIntact: false,
|
|
259
|
-
matchesTip: false,
|
|
260
|
-
trusted: false,
|
|
261
|
-
signerKeyB64: null,
|
|
262
|
-
reason: '',
|
|
263
|
-
};
|
|
264
|
-
if (!existsSync(tipSigPath(auditFile))) {
|
|
265
|
-
if (chainWasSigned(auditFile)) {
|
|
266
|
-
return {
|
|
267
|
-
...base,
|
|
268
|
-
expectedButMissing: true,
|
|
269
|
-
reason: 'this chain was previously signed but the signature file is GONE — it may have been deleted to hide an edit',
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
return { ...base, reason: 'no tip signature on this chain' };
|
|
273
|
-
}
|
|
274
|
-
let rec;
|
|
275
|
-
try {
|
|
276
|
-
rec = JSON.parse(readFileSync(tipSigPath(auditFile), 'utf-8'));
|
|
277
|
-
}
|
|
278
|
-
catch {
|
|
279
|
-
return { ...base, present: true, reason: 'tip signature file is unreadable or corrupt' };
|
|
280
|
-
}
|
|
281
|
-
if (rec.alg !== 'ed25519' || typeof rec.entryHash !== 'string' || typeof rec.seq !== 'number') {
|
|
282
|
-
return { ...base, present: true, reason: 'tip signature is malformed' };
|
|
283
|
-
}
|
|
284
|
-
base.present = true;
|
|
285
|
-
let pub;
|
|
286
|
-
try {
|
|
287
|
-
pub = importPublicPem(rec.pubPem);
|
|
288
|
-
}
|
|
289
|
-
catch {
|
|
290
|
-
return { ...base, reason: 'tip signature carries an invalid public key' };
|
|
291
|
-
}
|
|
292
|
-
base.signerKeyB64 = publicKeyToBase64(pub);
|
|
293
|
-
base.trusted = !trustedKeys || trustedKeys.has(base.signerKeyB64);
|
|
294
|
-
try {
|
|
295
|
-
base.signatureValid = edVerify(null, tipMessage({ seq: rec.seq, entryHash: rec.entryHash }), pub, Buffer.from(rec.sig, 'base64'));
|
|
296
|
-
}
|
|
297
|
-
catch {
|
|
298
|
-
base.signatureValid = false;
|
|
299
|
-
}
|
|
300
|
-
const hashAtSeq = readEntryHashAtSeq(auditFile, rec.seq);
|
|
301
|
-
base.ancestorIntact = hashAtSeq !== null && hashAtSeq === rec.entryHash;
|
|
302
|
-
const tip = readAuditTip(auditFile);
|
|
303
|
-
base.matchesTip = !!tip && tip.seq === rec.seq && tip.entryHash === rec.entryHash;
|
|
304
|
-
if (!base.signatureValid)
|
|
305
|
-
base.reason = 'tip signature is invalid — forged or corrupt';
|
|
306
|
-
else if (!base.ancestorIntact)
|
|
307
|
-
base.reason =
|
|
308
|
-
'the signed entry is no longer intact in the chain — it was edited (re-walked) or truncated after signing';
|
|
309
|
-
else if (!base.trusted)
|
|
310
|
-
base.reason = 'chain is signed and intact, but by an untrusted key';
|
|
311
|
-
else if (base.matchesTip)
|
|
312
|
-
base.reason = "chain tip is signed and matches — no edits by anyone without this machine's key";
|
|
313
|
-
else
|
|
314
|
-
base.reason = `signed at seq ${rec.seq} · the chain has grown since · the signed prefix is intact`;
|
|
315
|
-
return base;
|
|
316
|
-
}
|
|
317
|
-
/**
|
|
318
|
-
* Does the tip-signature state BLOCK verification? Blocks when:
|
|
319
|
-
* - sign-state says the chain was signed but the .tipsig is GONE (the
|
|
320
|
-
* delete-the-sidecar bypass), or
|
|
321
|
-
* - a signature is present but invalid, its signed entry is no longer an
|
|
322
|
-
* intact ancestor (edit/re-walk/truncation), or the signer is untrusted
|
|
323
|
-
* (an attacker re-signing a forgery with their OWN key would otherwise pass).
|
|
324
|
-
* A never-signed chain does not block (pre-R2 chains stay valid). Kept pure —
|
|
325
|
-
* this is the verdict the `audit verify` exit code depends on.
|
|
326
|
-
*/
|
|
327
|
-
export function tipBlocksVerification(tip) {
|
|
328
|
-
if (tip.expectedButMissing)
|
|
329
|
-
return true;
|
|
330
|
-
return tip.present && (!tip.signatureValid || !tip.ancestorIntact || !tip.trusted);
|
|
331
|
-
}
|
|
332
|
-
/** Reset the per-process key cache (tests). */
|
|
333
|
-
export function __resetTipKeyCache() {
|
|
334
|
-
cachedKeys = null;
|
|
335
|
-
}
|
|
336
|
-
//# sourceMappingURL=audit-tip-signature.js.map
|