groove-dev 0.27.77 → 0.27.78
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/CLAUDE.md +0 -7
- package/MOE_TRAINING_PIPELINE.md +216 -12
- package/moe-training/DEPLOY_CENTRAL_COMMAND.md +413 -0
- package/moe-training/client/consent.js +96 -0
- package/moe-training/client/envelope-builder.js +56 -0
- package/moe-training/client/index.js +10 -0
- package/moe-training/client/parsers/claude-code.js +110 -0
- package/moe-training/client/parsers/codex.js +80 -0
- package/moe-training/client/parsers/gemini.js +80 -0
- package/moe-training/client/parsers/grok.js +16 -0
- package/moe-training/client/parsers/index.js +20 -0
- package/moe-training/client/scrubber.js +126 -0
- package/moe-training/client/session-attestation.js +114 -0
- package/moe-training/client/step-classifier.js +51 -0
- package/moe-training/client/trajectory-capture.js +227 -0
- package/moe-training/client/transmission-queue.js +93 -0
- package/moe-training/package-lock.json +1266 -0
- package/moe-training/package.json +20 -0
- package/moe-training/server/enrichment.js +24 -0
- package/moe-training/server/index.js +119 -0
- package/moe-training/server/ledger.js +110 -0
- package/moe-training/server/routes/ingest.js +96 -0
- package/moe-training/server/routes/sessions.js +43 -0
- package/moe-training/server/routes/stats.js +31 -0
- package/moe-training/server/scoring.js +63 -0
- package/moe-training/server/session-registry.js +156 -0
- package/moe-training/server/stats.js +129 -0
- package/moe-training/server/stitcher.js +69 -0
- package/moe-training/server/storage.js +147 -0
- package/moe-training/server/verifier.js +102 -0
- package/moe-training/shared/constants.js +30 -0
- package/moe-training/shared/crypto.js +45 -0
- package/moe-training/shared/envelope-schema.js +220 -0
- package/moe-training/test/client/consent.test.js +121 -0
- package/moe-training/test/client/envelope-builder.test.js +107 -0
- package/moe-training/test/client/parsers/claude-code.test.js +119 -0
- package/moe-training/test/client/parsers/codex.test.js +83 -0
- package/moe-training/test/client/parsers/gemini.test.js +99 -0
- package/moe-training/test/client/scrubber.test.js +133 -0
- package/moe-training/test/client/session-attestation-security.test.js +95 -0
- package/moe-training/test/client/step-classifier.test.js +88 -0
- package/moe-training/test/integration/handshake.test.js +260 -0
- package/moe-training/test/server/ingest-security.test.js +166 -0
- package/moe-training/test/server/ledger.test.js +131 -0
- package/moe-training/test/server/scoring.test.js +242 -0
- package/moe-training/test/server/session-registry.test.js +125 -0
- package/moe-training/test/server/stitcher.test.js +157 -0
- package/moe-training/test/server/verifier.test.js +232 -0
- package/moe-training/test/shared/crypto.test.js +87 -0
- package/moe-training/test/shared/envelope-schema.test.js +351 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/agent-loop.js +48 -5
- package/node_modules/@groove-dev/daemon/src/api.js +77 -0
- package/node_modules/@groove-dev/daemon/src/index.js +61 -0
- package/node_modules/@groove-dev/daemon/src/journalist.js +64 -21
- package/node_modules/@groove-dev/daemon/src/process.js +199 -0
- package/node_modules/@groove-dev/daemon/src/providers/grok.js +15 -0
- package/node_modules/@groove-dev/daemon/src/state.js +20 -1
- package/node_modules/@groove-dev/gui/dist/assets/{index-BbmPDhuW.js → index-BJgEJ9lZ.js} +1677 -1677
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +32 -0
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +167 -1
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/agent-loop.js +48 -5
- package/packages/daemon/src/api.js +77 -0
- package/packages/daemon/src/index.js +61 -0
- package/packages/daemon/src/journalist.js +64 -21
- package/packages/daemon/src/process.js +199 -0
- package/packages/daemon/src/providers/grok.js +15 -0
- package/packages/daemon/src/state.js +20 -1
- package/packages/gui/dist/assets/{index-BbmPDhuW.js → index-BJgEJ9lZ.js} +1677 -1677
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/stores/groove.js +32 -0
- package/packages/gui/src/views/settings.jsx +167 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
|
|
3
|
+
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { EnvelopeStorage } from '../../server/storage.js';
|
|
9
|
+
import { TrajectoryStitcher } from '../../server/stitcher.js';
|
|
10
|
+
|
|
11
|
+
describe('TrajectoryStitcher', () => {
|
|
12
|
+
let storage;
|
|
13
|
+
let stitcher;
|
|
14
|
+
let tmpDir;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'stitch-test-'));
|
|
18
|
+
storage = new EnvelopeStorage(join(tmpDir, 'envelopes'));
|
|
19
|
+
stitcher = new TrajectoryStitcher(storage);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('stitches 3 chunks into correct order', () => {
|
|
27
|
+
const sessionId = 'sess_stitch_001';
|
|
28
|
+
|
|
29
|
+
storage.store({
|
|
30
|
+
session_id: sessionId, chunk_sequence: 2, contributor_id: 'contrib_1',
|
|
31
|
+
metadata: { model_engine: 'claude-opus-4-6', provider: 'claude-code' },
|
|
32
|
+
trajectory_log: [
|
|
33
|
+
{ step: 3, type: 'action', tool: 'Edit', token_count: 20 },
|
|
34
|
+
{ step: 4, type: 'observation', content: 'done', token_count: 5 },
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
storage.store({
|
|
38
|
+
session_id: sessionId, chunk_sequence: 0, contributor_id: 'contrib_1',
|
|
39
|
+
metadata: { model_engine: 'claude-opus-4-6', provider: 'claude-code' },
|
|
40
|
+
trajectory_log: [
|
|
41
|
+
{ step: 1, type: 'thought', content: 'plan', token_count: 10 },
|
|
42
|
+
],
|
|
43
|
+
});
|
|
44
|
+
storage.store({
|
|
45
|
+
session_id: sessionId, chunk_sequence: 1, contributor_id: 'contrib_1',
|
|
46
|
+
metadata: { model_engine: 'claude-opus-4-6', provider: 'claude-code' },
|
|
47
|
+
trajectory_log: [
|
|
48
|
+
{ step: 2, type: 'action', tool: 'Grep', token_count: 15 },
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const result = stitcher.stitch(sessionId);
|
|
53
|
+
|
|
54
|
+
assert.ok(result);
|
|
55
|
+
assert.equal(result.session_id, sessionId);
|
|
56
|
+
assert.equal(result.total_steps, 4);
|
|
57
|
+
assert.equal(result.total_chunks, 3);
|
|
58
|
+
assert.deepEqual(result.trajectory_log.map(s => s.step), [1, 2, 3, 4]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('has all steps present and ordered by step number', () => {
|
|
62
|
+
const sessionId = 'sess_stitch_002';
|
|
63
|
+
|
|
64
|
+
storage.store({
|
|
65
|
+
session_id: sessionId, chunk_sequence: 0, contributor_id: 'c1',
|
|
66
|
+
metadata: {},
|
|
67
|
+
trajectory_log: [
|
|
68
|
+
{ step: 1, type: 'thought', token_count: 5 },
|
|
69
|
+
{ step: 2, type: 'action', tool: 'Read', token_count: 3 },
|
|
70
|
+
],
|
|
71
|
+
});
|
|
72
|
+
storage.store({
|
|
73
|
+
session_id: sessionId, chunk_sequence: 1, contributor_id: 'c1',
|
|
74
|
+
metadata: {},
|
|
75
|
+
trajectory_log: [
|
|
76
|
+
{ step: 3, type: 'observation', token_count: 8 },
|
|
77
|
+
],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const result = stitcher.stitch(sessionId);
|
|
81
|
+
assert.equal(result.total_steps, 3);
|
|
82
|
+
assert.equal(result.total_tokens, 16);
|
|
83
|
+
|
|
84
|
+
for (let i = 0; i < result.trajectory_log.length - 1; i++) {
|
|
85
|
+
assert.ok(result.trajectory_log[i].step < result.trajectory_log[i + 1].step);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('computes step_type_distribution correctly', () => {
|
|
90
|
+
const sessionId = 'sess_stitch_003';
|
|
91
|
+
|
|
92
|
+
storage.store({
|
|
93
|
+
session_id: sessionId, chunk_sequence: 0, contributor_id: 'c1',
|
|
94
|
+
metadata: {},
|
|
95
|
+
trajectory_log: [
|
|
96
|
+
{ step: 1, type: 'thought', token_count: 5 },
|
|
97
|
+
{ step: 2, type: 'action', tool: 'Read', token_count: 3 },
|
|
98
|
+
{ step: 3, type: 'action', tool: 'Edit', token_count: 4 },
|
|
99
|
+
{ step: 4, type: 'observation', token_count: 2 },
|
|
100
|
+
{ step: 5, type: 'error', token_count: 1 },
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const result = stitcher.stitch(sessionId);
|
|
105
|
+
assert.deepEqual(result.step_type_distribution, {
|
|
106
|
+
thought: 1,
|
|
107
|
+
action: 2,
|
|
108
|
+
observation: 1,
|
|
109
|
+
error: 1,
|
|
110
|
+
});
|
|
111
|
+
assert.deepEqual(result.unique_tools_used.sort(), ['Edit', 'Read']);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('returns null for unknown session', () => {
|
|
115
|
+
const result = stitcher.stitch('sess_nonexistent');
|
|
116
|
+
assert.equal(result, null);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('includes outcome from SESSION_CLOSE envelope', () => {
|
|
120
|
+
const sessionId = 'sess_stitch_004';
|
|
121
|
+
|
|
122
|
+
storage.store({
|
|
123
|
+
session_id: sessionId, chunk_sequence: 0, contributor_id: 'c1',
|
|
124
|
+
metadata: {},
|
|
125
|
+
trajectory_log: [{ step: 1, type: 'thought', token_count: 5 }],
|
|
126
|
+
});
|
|
127
|
+
storage.store({
|
|
128
|
+
session_id: sessionId, type: 'SESSION_CLOSE',
|
|
129
|
+
outcome: { status: 'SUCCESS', total_steps: 1, user_interventions: 0 },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const result = stitcher.stitch(sessionId);
|
|
133
|
+
assert.ok(result.outcome);
|
|
134
|
+
assert.equal(result.outcome.status, 'SUCCESS');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('links coordination steps', () => {
|
|
138
|
+
const sessionId = 'sess_stitch_005';
|
|
139
|
+
|
|
140
|
+
storage.store({
|
|
141
|
+
session_id: sessionId, chunk_sequence: 0, contributor_id: 'c1',
|
|
142
|
+
metadata: {},
|
|
143
|
+
trajectory_log: [
|
|
144
|
+
{ step: 1, type: 'coordination', coordination_id: 'coord_1', direction: 'outbound', target_agent: 'backend-1', token_count: 5 },
|
|
145
|
+
{ step: 2, type: 'thought', token_count: 3 },
|
|
146
|
+
],
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const result = stitcher.stitch(sessionId);
|
|
150
|
+
const enriched = stitcher.linkCoordination(result);
|
|
151
|
+
|
|
152
|
+
const coordStep = enriched.trajectory_log.find(s => s.type === 'coordination');
|
|
153
|
+
assert.ok(coordStep.coordination_partner);
|
|
154
|
+
assert.equal(coordStep.coordination_partner.coordination_id, 'coord_1');
|
|
155
|
+
assert.equal(coordStep.coordination_partner.linked, true);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
|
|
3
|
+
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { generateECDHKeypair, deriveSharedSecret, signEnvelope } from '../../shared/crypto.js';
|
|
9
|
+
import { SessionRegistry } from '../../server/session-registry.js';
|
|
10
|
+
import { EnvelopeVerifier } from '../../server/verifier.js';
|
|
11
|
+
|
|
12
|
+
const VALID_CONTRIBUTOR = 'c'.repeat(32);
|
|
13
|
+
const VALID_APP_HASH = 'b'.repeat(64);
|
|
14
|
+
|
|
15
|
+
function makeSignedEnvelope(sessionId, sequence, sharedSecret, extra = {}) {
|
|
16
|
+
const envelope = {
|
|
17
|
+
envelope_id: `env_test_${sequence}`,
|
|
18
|
+
session_id: sessionId,
|
|
19
|
+
chunk_sequence: sequence,
|
|
20
|
+
contributor_id: VALID_CONTRIBUTOR,
|
|
21
|
+
metadata: { model_engine: 'claude-opus-4-6', provider: 'claude-code', agent_role: 'frontend', agent_id: 'frontend-1' },
|
|
22
|
+
trajectory_log: [{ step: 1, type: 'thought', timestamp: Date.now() / 1000, content: 'test', token_count: 10 }],
|
|
23
|
+
...extra,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const forHmac = { ...envelope };
|
|
27
|
+
const envelopeBytes = JSON.stringify(forHmac);
|
|
28
|
+
const hmac = signEnvelope(sharedSecret, envelopeBytes, sequence);
|
|
29
|
+
|
|
30
|
+
envelope.attestation = {
|
|
31
|
+
session_hmac: hmac,
|
|
32
|
+
sequence,
|
|
33
|
+
app_version_hash: VALID_APP_HASH,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return envelope;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makeSignedCloseEnvelope(sessionId, sequence, sharedSecret) {
|
|
40
|
+
const envelope = {
|
|
41
|
+
envelope_id: `env_close_${sequence}`,
|
|
42
|
+
session_id: sessionId,
|
|
43
|
+
type: 'SESSION_CLOSE',
|
|
44
|
+
outcome: {
|
|
45
|
+
status: 'SUCCESS',
|
|
46
|
+
total_steps: 10,
|
|
47
|
+
total_chunks: 1,
|
|
48
|
+
user_interventions: 0,
|
|
49
|
+
total_tokens: 500,
|
|
50
|
+
duration_seconds: 60,
|
|
51
|
+
files_modified: 1,
|
|
52
|
+
errors_encountered: 0,
|
|
53
|
+
errors_recovered: 0,
|
|
54
|
+
coordination_events: 0,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const forHmac = { ...envelope };
|
|
59
|
+
const envelopeBytes = JSON.stringify(forHmac);
|
|
60
|
+
const hmac = signEnvelope(sharedSecret, envelopeBytes, sequence);
|
|
61
|
+
|
|
62
|
+
envelope.attestation = {
|
|
63
|
+
session_hmac: hmac,
|
|
64
|
+
sequence,
|
|
65
|
+
app_version_hash: VALID_APP_HASH,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return envelope;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe('EnvelopeVerifier', () => {
|
|
72
|
+
let registry;
|
|
73
|
+
let verifier;
|
|
74
|
+
let tmpDir;
|
|
75
|
+
let sharedSecret;
|
|
76
|
+
const sessionId = 'sess_verify_001';
|
|
77
|
+
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'verify-test-'));
|
|
80
|
+
registry = new SessionRegistry(join(tmpDir, 'sessions.db'));
|
|
81
|
+
verifier = new EnvelopeVerifier(registry);
|
|
82
|
+
|
|
83
|
+
const clientKeypair = generateECDHKeypair();
|
|
84
|
+
const result = registry.openSession(
|
|
85
|
+
sessionId, clientKeypair.publicKey, 'claude-code', 'claude-opus-4-6',
|
|
86
|
+
'fp_verify', 'hash_verify', '0.27.77'
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const session = registry.getSession(sessionId);
|
|
90
|
+
sharedSecret = session.shared_secret;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
afterEach(() => {
|
|
94
|
+
registry.close();
|
|
95
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('accepts a valid envelope with correct HMAC', () => {
|
|
99
|
+
const envelope = makeSignedEnvelope(sessionId, 0, sharedSecret);
|
|
100
|
+
const result = verifier.verify(envelope);
|
|
101
|
+
assert.equal(result.valid, true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('rejects a tampered envelope (HMAC fails)', () => {
|
|
105
|
+
const envelope = makeSignedEnvelope(sessionId, 0, sharedSecret);
|
|
106
|
+
envelope.trajectory_log.push({ step: 2, type: 'action', content: 'injected' });
|
|
107
|
+
const result = verifier.verify(envelope);
|
|
108
|
+
assert.equal(result.valid, false);
|
|
109
|
+
assert.ok(result.reason.includes('HMAC'));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('rejects wrong sequence number', () => {
|
|
113
|
+
const envelope = makeSignedEnvelope(sessionId, 5, sharedSecret);
|
|
114
|
+
const result = verifier.verify(envelope);
|
|
115
|
+
assert.equal(result.valid, false);
|
|
116
|
+
assert.ok(result.reason.includes('sequence'));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('rejects unknown session_id', () => {
|
|
120
|
+
const envelope = makeSignedEnvelope('sess_nonexistent', 0, sharedSecret);
|
|
121
|
+
const result = verifier.verify(envelope);
|
|
122
|
+
assert.equal(result.valid, false);
|
|
123
|
+
assert.ok(result.reason.includes('unknown session_id'));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('rejects envelope for closed session', () => {
|
|
127
|
+
registry.closeSession(sessionId);
|
|
128
|
+
const envelope = makeSignedEnvelope(sessionId, 0, sharedSecret);
|
|
129
|
+
const result = verifier.verify(envelope);
|
|
130
|
+
assert.equal(result.valid, false);
|
|
131
|
+
assert.ok(result.reason.includes('closed'));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('rejects envelope missing attestation', () => {
|
|
135
|
+
const envelope = {
|
|
136
|
+
session_id: sessionId,
|
|
137
|
+
envelope_id: 'env_no_att',
|
|
138
|
+
trajectory_log: [],
|
|
139
|
+
};
|
|
140
|
+
const result = verifier.verify(envelope);
|
|
141
|
+
assert.equal(result.valid, false);
|
|
142
|
+
assert.ok(result.reason.includes('attestation'));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('increments sequence after successful verification', () => {
|
|
146
|
+
const env0 = makeSignedEnvelope(sessionId, 0, sharedSecret);
|
|
147
|
+
const r0 = verifier.verify(env0);
|
|
148
|
+
assert.equal(r0.valid, true);
|
|
149
|
+
|
|
150
|
+
const env1 = makeSignedEnvelope(sessionId, 1, sharedSecret);
|
|
151
|
+
const r1 = verifier.verify(env1);
|
|
152
|
+
assert.equal(r1.valid, true);
|
|
153
|
+
|
|
154
|
+
const session = registry.getSession(sessionId);
|
|
155
|
+
assert.equal(session.expected_sequence, 2);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// --- New security tests ---
|
|
159
|
+
|
|
160
|
+
it('rejects empty HMAC string', () => {
|
|
161
|
+
const envelope = makeSignedEnvelope(sessionId, 0, sharedSecret);
|
|
162
|
+
envelope.attestation.session_hmac = '';
|
|
163
|
+
const result = verifier.verify(envelope);
|
|
164
|
+
assert.equal(result.valid, false);
|
|
165
|
+
assert.ok(result.reason.includes('HMAC'));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('rejects missing HMAC field', () => {
|
|
169
|
+
const envelope = makeSignedEnvelope(sessionId, 0, sharedSecret);
|
|
170
|
+
delete envelope.attestation.session_hmac;
|
|
171
|
+
const result = verifier.verify(envelope);
|
|
172
|
+
assert.equal(result.valid, false);
|
|
173
|
+
assert.ok(result.reason.includes('HMAC'));
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('atomic sequence prevents duplicate sequence acceptance', () => {
|
|
177
|
+
const env0 = makeSignedEnvelope(sessionId, 0, sharedSecret);
|
|
178
|
+
const r0 = verifier.verify(env0);
|
|
179
|
+
assert.equal(r0.valid, true);
|
|
180
|
+
|
|
181
|
+
// Re-sign with sequence 0 again (replay attempt)
|
|
182
|
+
const env0replay = makeSignedEnvelope(sessionId, 0, sharedSecret);
|
|
183
|
+
const rReplay = verifier.verify(env0replay);
|
|
184
|
+
assert.equal(rReplay.valid, false);
|
|
185
|
+
assert.ok(rReplay.reason.includes('sequence'));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('verifyClose checks sequence number', () => {
|
|
189
|
+
// Send one regular envelope first (sequence 0)
|
|
190
|
+
const env0 = makeSignedEnvelope(sessionId, 0, sharedSecret);
|
|
191
|
+
verifier.verify(env0);
|
|
192
|
+
|
|
193
|
+
// Close with wrong sequence
|
|
194
|
+
const closeWrong = makeSignedCloseEnvelope(sessionId, 0, sharedSecret);
|
|
195
|
+
const resultWrong = verifyClose(verifier, closeWrong);
|
|
196
|
+
assert.equal(resultWrong.valid, false);
|
|
197
|
+
assert.ok(resultWrong.reason.includes('sequence'));
|
|
198
|
+
|
|
199
|
+
// Close with correct sequence
|
|
200
|
+
const closeRight = makeSignedCloseEnvelope(sessionId, 1, sharedSecret);
|
|
201
|
+
const resultRight = verifyClose(verifier, closeRight);
|
|
202
|
+
assert.equal(resultRight.valid, true);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('verifyClose rejects empty HMAC', () => {
|
|
206
|
+
const closeEnv = makeSignedCloseEnvelope(sessionId, 0, sharedSecret);
|
|
207
|
+
closeEnv.attestation.session_hmac = '';
|
|
208
|
+
const result = verifier.verifyClose(closeEnv);
|
|
209
|
+
assert.equal(result.valid, false);
|
|
210
|
+
assert.ok(result.reason.includes('HMAC'));
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('rejects OFFLINE HMAC marker from offline client', () => {
|
|
214
|
+
const envelope = makeSignedEnvelope(sessionId, 0, sharedSecret);
|
|
215
|
+
envelope.attestation.session_hmac = 'OFFLINE';
|
|
216
|
+
const result = verifier.verify(envelope);
|
|
217
|
+
assert.equal(result.valid, false);
|
|
218
|
+
assert.ok(result.reason.includes('HMAC'));
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('rejects mega-length HMAC string', () => {
|
|
222
|
+
const envelope = makeSignedEnvelope(sessionId, 0, sharedSecret);
|
|
223
|
+
envelope.attestation.session_hmac = 'a'.repeat(1_000_000);
|
|
224
|
+
const result = verifier.verify(envelope);
|
|
225
|
+
assert.equal(result.valid, false);
|
|
226
|
+
assert.ok(result.reason.includes('HMAC'));
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
function verifyClose(verifier, envelope) {
|
|
231
|
+
return verifier.verifyClose(envelope);
|
|
232
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
|
|
3
|
+
import { describe, it } from 'node:test';
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import { writeFileSync, mkdtempSync, rmSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import {
|
|
9
|
+
generateECDHKeypair,
|
|
10
|
+
deriveSharedSecret,
|
|
11
|
+
signEnvelope,
|
|
12
|
+
verifyEnvelope,
|
|
13
|
+
computeAppHash,
|
|
14
|
+
} from '../../shared/crypto.js';
|
|
15
|
+
|
|
16
|
+
describe('crypto', () => {
|
|
17
|
+
it('generates ECDH keypairs with base64 encoded keys', () => {
|
|
18
|
+
const kp = generateECDHKeypair();
|
|
19
|
+
assert.ok(kp.publicKey);
|
|
20
|
+
assert.ok(kp.privateKey);
|
|
21
|
+
assert.ok(Buffer.from(kp.publicKey, 'base64').length > 0);
|
|
22
|
+
assert.ok(Buffer.from(kp.privateKey, 'base64').length > 0);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('derives the same shared secret from both sides', () => {
|
|
26
|
+
const alice = generateECDHKeypair();
|
|
27
|
+
const bob = generateECDHKeypair();
|
|
28
|
+
|
|
29
|
+
const secretA = deriveSharedSecret(alice.privateKey, bob.publicKey);
|
|
30
|
+
const secretB = deriveSharedSecret(bob.privateKey, alice.publicKey);
|
|
31
|
+
|
|
32
|
+
assert.equal(secretA, secretB);
|
|
33
|
+
assert.ok(Buffer.from(secretA, 'base64').length > 0);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('HMAC sign and verify round-trip', () => {
|
|
37
|
+
const alice = generateECDHKeypair();
|
|
38
|
+
const bob = generateECDHKeypair();
|
|
39
|
+
const secret = deriveSharedSecret(alice.privateKey, bob.publicKey);
|
|
40
|
+
|
|
41
|
+
const payload = JSON.stringify({ data: 'test envelope content' });
|
|
42
|
+
const seq = 1;
|
|
43
|
+
const hmac = signEnvelope(secret, payload, seq);
|
|
44
|
+
|
|
45
|
+
assert.ok(typeof hmac === 'string');
|
|
46
|
+
assert.ok(hmac.length > 0);
|
|
47
|
+
assert.ok(verifyEnvelope(secret, payload, seq, hmac));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('verification fails with tampered payload', () => {
|
|
51
|
+
const alice = generateECDHKeypair();
|
|
52
|
+
const bob = generateECDHKeypair();
|
|
53
|
+
const secret = deriveSharedSecret(alice.privateKey, bob.publicKey);
|
|
54
|
+
|
|
55
|
+
const payload = JSON.stringify({ data: 'original' });
|
|
56
|
+
const hmac = signEnvelope(secret, payload, 1);
|
|
57
|
+
|
|
58
|
+
const tampered = JSON.stringify({ data: 'tampered' });
|
|
59
|
+
assert.equal(verifyEnvelope(secret, tampered, 1, hmac), false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('verification fails with wrong sequence number', () => {
|
|
63
|
+
const alice = generateECDHKeypair();
|
|
64
|
+
const bob = generateECDHKeypair();
|
|
65
|
+
const secret = deriveSharedSecret(alice.privateKey, bob.publicKey);
|
|
66
|
+
|
|
67
|
+
const payload = JSON.stringify({ data: 'test' });
|
|
68
|
+
const hmac = signEnvelope(secret, payload, 1);
|
|
69
|
+
|
|
70
|
+
assert.equal(verifyEnvelope(secret, payload, 2, hmac), false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('computeAppHash returns SHA256 of file contents', () => {
|
|
74
|
+
const dir = mkdtempSync(join(tmpdir(), 'crypto-test-'));
|
|
75
|
+
const filePath = join(dir, 'test-file.txt');
|
|
76
|
+
writeFileSync(filePath, 'hello world');
|
|
77
|
+
|
|
78
|
+
const hash = computeAppHash(filePath);
|
|
79
|
+
assert.ok(typeof hash === 'string');
|
|
80
|
+
assert.equal(hash.length, 64);
|
|
81
|
+
|
|
82
|
+
const hash2 = computeAppHash(filePath);
|
|
83
|
+
assert.equal(hash, hash2);
|
|
84
|
+
|
|
85
|
+
rmSync(dir, { recursive: true });
|
|
86
|
+
});
|
|
87
|
+
});
|