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.
Files changed (79) hide show
  1. package/CLAUDE.md +0 -7
  2. package/MOE_TRAINING_PIPELINE.md +216 -12
  3. package/moe-training/DEPLOY_CENTRAL_COMMAND.md +413 -0
  4. package/moe-training/client/consent.js +96 -0
  5. package/moe-training/client/envelope-builder.js +56 -0
  6. package/moe-training/client/index.js +10 -0
  7. package/moe-training/client/parsers/claude-code.js +110 -0
  8. package/moe-training/client/parsers/codex.js +80 -0
  9. package/moe-training/client/parsers/gemini.js +80 -0
  10. package/moe-training/client/parsers/grok.js +16 -0
  11. package/moe-training/client/parsers/index.js +20 -0
  12. package/moe-training/client/scrubber.js +126 -0
  13. package/moe-training/client/session-attestation.js +114 -0
  14. package/moe-training/client/step-classifier.js +51 -0
  15. package/moe-training/client/trajectory-capture.js +227 -0
  16. package/moe-training/client/transmission-queue.js +93 -0
  17. package/moe-training/package-lock.json +1266 -0
  18. package/moe-training/package.json +20 -0
  19. package/moe-training/server/enrichment.js +24 -0
  20. package/moe-training/server/index.js +119 -0
  21. package/moe-training/server/ledger.js +110 -0
  22. package/moe-training/server/routes/ingest.js +96 -0
  23. package/moe-training/server/routes/sessions.js +43 -0
  24. package/moe-training/server/routes/stats.js +31 -0
  25. package/moe-training/server/scoring.js +63 -0
  26. package/moe-training/server/session-registry.js +156 -0
  27. package/moe-training/server/stats.js +129 -0
  28. package/moe-training/server/stitcher.js +69 -0
  29. package/moe-training/server/storage.js +147 -0
  30. package/moe-training/server/verifier.js +102 -0
  31. package/moe-training/shared/constants.js +30 -0
  32. package/moe-training/shared/crypto.js +45 -0
  33. package/moe-training/shared/envelope-schema.js +220 -0
  34. package/moe-training/test/client/consent.test.js +121 -0
  35. package/moe-training/test/client/envelope-builder.test.js +107 -0
  36. package/moe-training/test/client/parsers/claude-code.test.js +119 -0
  37. package/moe-training/test/client/parsers/codex.test.js +83 -0
  38. package/moe-training/test/client/parsers/gemini.test.js +99 -0
  39. package/moe-training/test/client/scrubber.test.js +133 -0
  40. package/moe-training/test/client/session-attestation-security.test.js +95 -0
  41. package/moe-training/test/client/step-classifier.test.js +88 -0
  42. package/moe-training/test/integration/handshake.test.js +260 -0
  43. package/moe-training/test/server/ingest-security.test.js +166 -0
  44. package/moe-training/test/server/ledger.test.js +131 -0
  45. package/moe-training/test/server/scoring.test.js +242 -0
  46. package/moe-training/test/server/session-registry.test.js +125 -0
  47. package/moe-training/test/server/stitcher.test.js +157 -0
  48. package/moe-training/test/server/verifier.test.js +232 -0
  49. package/moe-training/test/shared/crypto.test.js +87 -0
  50. package/moe-training/test/shared/envelope-schema.test.js +351 -0
  51. package/node_modules/@groove-dev/cli/package.json +1 -1
  52. package/node_modules/@groove-dev/daemon/package.json +1 -1
  53. package/node_modules/@groove-dev/daemon/src/agent-loop.js +48 -5
  54. package/node_modules/@groove-dev/daemon/src/api.js +77 -0
  55. package/node_modules/@groove-dev/daemon/src/index.js +61 -0
  56. package/node_modules/@groove-dev/daemon/src/journalist.js +64 -21
  57. package/node_modules/@groove-dev/daemon/src/process.js +199 -0
  58. package/node_modules/@groove-dev/daemon/src/providers/grok.js +15 -0
  59. package/node_modules/@groove-dev/daemon/src/state.js +20 -1
  60. package/node_modules/@groove-dev/gui/dist/assets/{index-BbmPDhuW.js → index-BJgEJ9lZ.js} +1677 -1677
  61. package/node_modules/@groove-dev/gui/dist/index.html +1 -1
  62. package/node_modules/@groove-dev/gui/package.json +1 -1
  63. package/node_modules/@groove-dev/gui/src/stores/groove.js +32 -0
  64. package/node_modules/@groove-dev/gui/src/views/settings.jsx +167 -1
  65. package/package.json +1 -1
  66. package/packages/cli/package.json +1 -1
  67. package/packages/daemon/package.json +1 -1
  68. package/packages/daemon/src/agent-loop.js +48 -5
  69. package/packages/daemon/src/api.js +77 -0
  70. package/packages/daemon/src/index.js +61 -0
  71. package/packages/daemon/src/journalist.js +64 -21
  72. package/packages/daemon/src/process.js +199 -0
  73. package/packages/daemon/src/providers/grok.js +15 -0
  74. package/packages/daemon/src/state.js +20 -1
  75. package/packages/gui/dist/assets/{index-BbmPDhuW.js → index-BJgEJ9lZ.js} +1677 -1677
  76. package/packages/gui/dist/index.html +1 -1
  77. package/packages/gui/package.json +1 -1
  78. package/packages/gui/src/stores/groove.js +32 -0
  79. package/packages/gui/src/views/settings.jsx +167 -1
@@ -0,0 +1,129 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ import { readFileSync, statSync } from 'node:fs';
4
+
5
+ const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
6
+ const MAX_LINES_PER_FILE = 100_000;
7
+
8
+ export class CentralStats {
9
+ constructor(storage, ledger, sessionRegistry) {
10
+ this.storage = storage;
11
+ this.ledger = ledger;
12
+ this.sessionRegistry = sessionRegistry;
13
+ }
14
+
15
+ summary() {
16
+ const storageStats = this.storage.getTotalStats();
17
+ const activeSessions = this.sessionRegistry.getActiveSessions().length;
18
+ const totalPointsAwarded = this.ledger.getTotalPointsAwarded();
19
+
20
+ return {
21
+ totalEnvelopes: storageStats.totalEnvelopes,
22
+ totalSteps: storageStats.totalSteps,
23
+ totalSessions: storageStats.uniqueSessions,
24
+ activeSessions,
25
+ uniqueContributors: storageStats.uniqueContributors,
26
+ storageSizeMb: storageStats.storageSizeMb,
27
+ totalPointsAwarded,
28
+ };
29
+ }
30
+
31
+ dailyGrowth(days = 7) {
32
+ days = Math.max(1, Math.min(365, Number(days) || 7));
33
+ const storageDaily = this.storage.getDailyStats(days);
34
+ const creditDaily = this.ledger.getDailyCredits(days);
35
+
36
+ const creditMap = {};
37
+ for (const c of creditDaily) {
38
+ creditMap[c.date] = { points: c.totalPoints, sessions: c.totalSessions };
39
+ }
40
+
41
+ return storageDaily.map(d => ({
42
+ date: d.date,
43
+ envelopes: d.envelopeCount,
44
+ steps: d.stepCount,
45
+ sessions: creditMap[d.date]?.sessions || 0,
46
+ points: creditMap[d.date]?.points || 0,
47
+ }));
48
+ }
49
+
50
+ modelBreakdown() {
51
+ const files = this.storage._listFiles();
52
+ const models = {};
53
+
54
+ for (const file of files) {
55
+ try {
56
+ const stat = statSync(file);
57
+ if (stat.size > MAX_FILE_SIZE) continue;
58
+ } catch { continue; }
59
+
60
+ try {
61
+ const lines = readFileSync(file, 'utf-8').split('\n').filter(Boolean);
62
+ const capped = lines.slice(0, MAX_LINES_PER_FILE);
63
+ for (const line of capped) {
64
+ try {
65
+ const env = JSON.parse(line);
66
+ const model = env.metadata?.model_engine || 'unknown';
67
+ if (!models[model]) models[model] = { sessions: new Set(), steps: 0, points: 0 };
68
+ models[model].sessions.add(env.session_id);
69
+ models[model].steps += env.trajectory_log?.length || 0;
70
+ } catch { /* skip */ }
71
+ }
72
+ } catch { /* skip file */ }
73
+ }
74
+
75
+ const total = Object.values(models).reduce((sum, m) => sum + m.steps, 0) || 1;
76
+ const result = {};
77
+ for (const [model, data] of Object.entries(models)) {
78
+ result[model] = {
79
+ sessions: data.sessions.size,
80
+ steps: data.steps,
81
+ points: data.points,
82
+ percentage: +((data.steps / total) * 100).toFixed(1),
83
+ };
84
+ }
85
+ return result;
86
+ }
87
+
88
+ providerBreakdown() {
89
+ const files = this.storage._listFiles();
90
+ const providers = {};
91
+
92
+ for (const file of files) {
93
+ try {
94
+ const stat = statSync(file);
95
+ if (stat.size > MAX_FILE_SIZE) continue;
96
+ } catch { continue; }
97
+
98
+ try {
99
+ const lines = readFileSync(file, 'utf-8').split('\n').filter(Boolean);
100
+ const capped = lines.slice(0, MAX_LINES_PER_FILE);
101
+ for (const line of capped) {
102
+ try {
103
+ const env = JSON.parse(line);
104
+ const provider = env.metadata?.provider || 'unknown';
105
+ if (!providers[provider]) providers[provider] = { sessions: new Set(), steps: 0, points: 0 };
106
+ providers[provider].sessions.add(env.session_id);
107
+ providers[provider].steps += env.trajectory_log?.length || 0;
108
+ } catch { /* skip */ }
109
+ }
110
+ } catch { /* skip file */ }
111
+ }
112
+
113
+ const result = {};
114
+ for (const [provider, data] of Object.entries(providers)) {
115
+ result[provider] = { sessions: data.sessions.size, steps: data.steps, points: data.points };
116
+ }
117
+ return result;
118
+ }
119
+
120
+ topContributors(limit = 10) {
121
+ limit = Math.max(1, Math.min(1000, Number(limit) || 10));
122
+ const leaders = this.ledger.getLeaderboard(limit);
123
+ return leaders.map(l => ({
124
+ contributor_id: l.contributor_id.slice(0, 8) + '...',
125
+ total_points: l.total_points,
126
+ total_sessions: l.total_sessions,
127
+ }));
128
+ }
129
+ }
@@ -0,0 +1,69 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ export class TrajectoryStitcher {
4
+ constructor(storage) {
5
+ this.storage = storage;
6
+ }
7
+
8
+ stitch(sessionId) {
9
+ const envelopes = this.storage.getSessionEnvelopes(sessionId);
10
+ if (!envelopes.length) return null;
11
+
12
+ const chunks = envelopes
13
+ .filter(e => e.type !== 'SESSION_CLOSE')
14
+ .sort((a, b) => (a.chunk_sequence ?? 0) - (b.chunk_sequence ?? 0));
15
+
16
+ const closeEnvelope = envelopes.find(e => e.type === 'SESSION_CLOSE');
17
+ const firstChunk = chunks[0];
18
+ if (!firstChunk) return null;
19
+
20
+ const allSteps = [];
21
+ for (const chunk of chunks) {
22
+ if (Array.isArray(chunk.trajectory_log)) {
23
+ allSteps.push(...chunk.trajectory_log);
24
+ }
25
+ }
26
+
27
+ allSteps.sort((a, b) => a.step - b.step);
28
+
29
+ const toolsUsed = new Set();
30
+ const stepTypeCounts = {};
31
+ let totalTokens = 0;
32
+
33
+ for (const step of allSteps) {
34
+ if (step.tool) toolsUsed.add(step.tool);
35
+ stepTypeCounts[step.type] = (stepTypeCounts[step.type] || 0) + 1;
36
+ totalTokens += step.token_count || 0;
37
+ }
38
+
39
+ return {
40
+ session_id: sessionId,
41
+ contributor_id: firstChunk.contributor_id,
42
+ metadata: firstChunk.metadata || {},
43
+ trajectory_log: allSteps,
44
+ outcome: closeEnvelope?.outcome || null,
45
+ total_steps: allSteps.length,
46
+ total_tokens: totalTokens,
47
+ unique_tools_used: [...toolsUsed],
48
+ step_type_distribution: stepTypeCounts,
49
+ total_chunks: chunks.length,
50
+ };
51
+ }
52
+
53
+ linkCoordination(trajectory) {
54
+ if (!trajectory || !trajectory.trajectory_log) return trajectory;
55
+
56
+ const coordSteps = trajectory.trajectory_log.filter(s => s.type === 'coordination' && s.coordination_id);
57
+
58
+ for (const step of coordSteps) {
59
+ step.coordination_partner = {
60
+ coordination_id: step.coordination_id,
61
+ direction: step.direction,
62
+ target_agent: step.target_agent || null,
63
+ linked: true,
64
+ };
65
+ }
66
+
67
+ return trajectory;
68
+ }
69
+ }
@@ -0,0 +1,147 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ import { mkdirSync, appendFileSync, readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+
6
+ const QUOTA_BYTES = 50 * 1024 * 1024 * 1024; // 50GB
7
+ const WARN_BYTES = 40 * 1024 * 1024 * 1024; // 40GB
8
+
9
+ export class EnvelopeStorage {
10
+ constructor(basePath = './data/envelopes') {
11
+ this.basePath = basePath;
12
+ if (!existsSync(basePath)) mkdirSync(basePath, { recursive: true, mode: 0o700 });
13
+ }
14
+
15
+ checkQuota() {
16
+ const files = this._listFiles();
17
+ let total = 0;
18
+ for (const file of files) {
19
+ try { total += statSync(file).size; } catch { /* skip */ }
20
+ }
21
+ if (total > WARN_BYTES && total <= QUOTA_BYTES) {
22
+ console.warn(`[storage] WARNING: storage at ${(total / (1024 * 1024 * 1024)).toFixed(1)}GB, approaching 50GB quota`);
23
+ }
24
+ return total < QUOTA_BYTES;
25
+ }
26
+
27
+ store(envelope) {
28
+ if (!this.checkQuota()) {
29
+ throw new Error('STORAGE_QUOTA_EXCEEDED');
30
+ }
31
+
32
+ const date = new Date().toISOString().slice(0, 10);
33
+ const filePath = join(this.basePath, `${date}.jsonl`);
34
+
35
+ if (!existsSync(this.basePath)) {
36
+ mkdirSync(this.basePath, { recursive: true, mode: 0o700 });
37
+ }
38
+
39
+ const line = JSON.stringify(envelope) + '\n';
40
+ appendFileSync(filePath, line, { mode: 0o600 });
41
+ }
42
+
43
+ getSessionEnvelopes(sessionId) {
44
+ const envelopes = [];
45
+ const files = this._listFiles();
46
+
47
+ for (const file of files) {
48
+ const lines = readFileSync(file, 'utf-8').split('\n').filter(Boolean);
49
+ for (const line of lines) {
50
+ try {
51
+ const env = JSON.parse(line);
52
+ if (env.session_id === sessionId) envelopes.push(env);
53
+ } catch { /* skip malformed lines */ }
54
+ }
55
+ }
56
+
57
+ return envelopes.sort((a, b) => (a.chunk_sequence ?? 0) - (b.chunk_sequence ?? 0));
58
+ }
59
+
60
+ getDateRange(startDate, endDate) {
61
+ const envelopes = [];
62
+ const files = this._listFiles();
63
+
64
+ for (const file of files) {
65
+ const fileName = file.split('/').pop().replace('.jsonl', '');
66
+ if (fileName < startDate || fileName > endDate) continue;
67
+
68
+ const lines = readFileSync(file, 'utf-8').split('\n').filter(Boolean);
69
+ for (const line of lines) {
70
+ try {
71
+ envelopes.push(JSON.parse(line));
72
+ } catch { /* skip */ }
73
+ }
74
+ }
75
+
76
+ return envelopes;
77
+ }
78
+
79
+ getDailyStats(days = 7) {
80
+ const stats = [];
81
+ const files = this._listFiles();
82
+
83
+ const cutoff = new Date();
84
+ cutoff.setDate(cutoff.getDate() - days);
85
+ const cutoffStr = cutoff.toISOString().slice(0, 10);
86
+
87
+ for (const file of files) {
88
+ const date = file.split('/').pop().replace('.jsonl', '');
89
+ if (date < cutoffStr) continue;
90
+
91
+ const lines = readFileSync(file, 'utf-8').split('\n').filter(Boolean);
92
+ let stepCount = 0;
93
+ for (const line of lines) {
94
+ try {
95
+ const env = JSON.parse(line);
96
+ stepCount += env.trajectory_log?.length ?? 0;
97
+ } catch { /* skip */ }
98
+ }
99
+
100
+ stats.push({ date, envelopeCount: lines.length, stepCount });
101
+ }
102
+
103
+ return stats.sort((a, b) => a.date.localeCompare(b.date));
104
+ }
105
+
106
+ getTotalStats() {
107
+ const files = this._listFiles();
108
+ let totalEnvelopes = 0;
109
+ let totalSteps = 0;
110
+ let storageSizeBytes = 0;
111
+ const contributors = new Set();
112
+ const sessions = new Set();
113
+
114
+ for (const file of files) {
115
+ const stat = statSync(file);
116
+ storageSizeBytes += stat.size;
117
+
118
+ const lines = readFileSync(file, 'utf-8').split('\n').filter(Boolean);
119
+ totalEnvelopes += lines.length;
120
+
121
+ for (const line of lines) {
122
+ try {
123
+ const env = JSON.parse(line);
124
+ totalSteps += env.trajectory_log?.length ?? 0;
125
+ if (env.contributor_id) contributors.add(env.contributor_id);
126
+ if (env.session_id) sessions.add(env.session_id);
127
+ } catch { /* skip */ }
128
+ }
129
+ }
130
+
131
+ return {
132
+ totalEnvelopes,
133
+ totalSteps,
134
+ storageSizeMb: +(storageSizeBytes / 1_048_576).toFixed(2),
135
+ uniqueContributors: contributors.size,
136
+ uniqueSessions: sessions.size,
137
+ };
138
+ }
139
+
140
+ _listFiles() {
141
+ if (!existsSync(this.basePath)) return [];
142
+ return readdirSync(this.basePath)
143
+ .filter(f => f.endsWith('.jsonl'))
144
+ .map(f => join(this.basePath, f))
145
+ .sort();
146
+ }
147
+ }
@@ -0,0 +1,102 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ import { verifyEnvelope } from '../shared/crypto.js';
4
+
5
+ let validateEnvelope;
6
+ try {
7
+ const schema = await import('../shared/envelope-schema.js');
8
+ validateEnvelope = schema.validateEnvelope;
9
+ } catch {
10
+ validateEnvelope = () => ({ valid: true });
11
+ }
12
+
13
+ export class EnvelopeVerifier {
14
+ constructor(sessionRegistry) {
15
+ this.sessionRegistry = sessionRegistry;
16
+ }
17
+
18
+ verify(envelope) {
19
+ const sessionId = envelope.session_id;
20
+ if (!sessionId) return { valid: false, reason: 'missing session_id' };
21
+
22
+ const session = this.sessionRegistry.getSession(sessionId);
23
+ if (!session) return { valid: false, reason: 'unknown session_id' };
24
+ if (session.status !== 'active') return { valid: false, reason: 'session is closed' };
25
+
26
+ const attestation = envelope.attestation;
27
+ if (!attestation) return { valid: false, reason: 'missing attestation' };
28
+
29
+ if (!attestation.session_hmac || typeof attestation.session_hmac !== 'string' || attestation.session_hmac.length === 0) {
30
+ return { valid: false, reason: 'empty or missing HMAC' };
31
+ }
32
+
33
+ // HMAC covers the entire envelope EXCEPT the attestation block
34
+ const envelopeForHmac = { ...envelope };
35
+ delete envelopeForHmac.attestation;
36
+ const envelopeBytes = JSON.stringify(envelopeForHmac);
37
+
38
+ const hmacValid = verifyEnvelope(
39
+ session.shared_secret,
40
+ envelopeBytes,
41
+ attestation.sequence,
42
+ attestation.session_hmac
43
+ );
44
+ if (!hmacValid) return { valid: false, reason: 'HMAC verification failed' };
45
+
46
+ // Atomic sequence check + increment (prevents race conditions)
47
+ const seqResult = this.sessionRegistry.checkAndIncrementSequence(sessionId, attestation.sequence);
48
+ if (!seqResult.valid) {
49
+ return { valid: false, reason: seqResult.reason };
50
+ }
51
+
52
+ const schemaResult = validateEnvelope(envelope);
53
+ if (!schemaResult.valid) {
54
+ return { valid: false, reason: `schema validation failed: ${schemaResult.reason || schemaResult.errors?.join(', ')}` };
55
+ }
56
+
57
+ return { valid: true };
58
+ }
59
+
60
+ verifyClose(envelope) {
61
+ const sessionId = envelope.session_id;
62
+ if (!sessionId) return { valid: false, reason: 'missing session_id' };
63
+
64
+ const session = this.sessionRegistry.getSession(sessionId);
65
+ if (!session) return { valid: false, reason: 'unknown session_id' };
66
+ if (session.status !== 'active') return { valid: false, reason: 'session already closed' };
67
+
68
+ const attestation = envelope.attestation;
69
+ if (!attestation) return { valid: false, reason: 'missing attestation' };
70
+
71
+ if (!attestation.session_hmac || typeof attestation.session_hmac !== 'string' || attestation.session_hmac.length === 0) {
72
+ return { valid: false, reason: 'empty or missing HMAC' };
73
+ }
74
+
75
+ // HMAC covers the entire envelope EXCEPT the attestation block
76
+ const envelopeForHmac = { ...envelope };
77
+ delete envelopeForHmac.attestation;
78
+ const envelopeBytes = JSON.stringify(envelopeForHmac);
79
+
80
+ const hmacValid = verifyEnvelope(
81
+ session.shared_secret,
82
+ envelopeBytes,
83
+ attestation.sequence,
84
+ attestation.session_hmac
85
+ );
86
+ if (!hmacValid) return { valid: false, reason: 'HMAC verification failed' };
87
+
88
+ // Sequence check for close envelopes too
89
+ const seqResult = this.sessionRegistry.checkAndIncrementSequence(sessionId, attestation.sequence);
90
+ if (!seqResult.valid) {
91
+ return { valid: false, reason: seqResult.reason };
92
+ }
93
+
94
+ const schemaResult = validateEnvelope(envelope);
95
+ if (!schemaResult.valid) {
96
+ return { valid: false, reason: `schema validation failed: ${schemaResult.reason || schemaResult.errors?.join(', ')}` };
97
+ }
98
+
99
+ this.sessionRegistry.closeSession(sessionId);
100
+ return { valid: true };
101
+ }
102
+ }
@@ -0,0 +1,30 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ export const CURRENT_CONSENT_VERSION = '1.0';
4
+ export const CHUNK_SIZE = 200;
5
+ export const CHUNK_TIMEOUT_MS = 300_000;
6
+ export const MAX_QUEUE_SIZE = 10_000;
7
+ export const OBSERVATION_TRUNCATE_HEAD = 50;
8
+ export const OBSERVATION_TRUNCATE_TAIL = 20;
9
+ export const SUPPORTED_PROVIDERS = ['claude-code', 'codex', 'gemini'];
10
+
11
+ export const MODEL_TIERS = {
12
+ 'claude-opus-4-6': 5,
13
+ 'claude-opus-4-7': 5,
14
+ 'claude-sonnet-4-6': 3,
15
+ 'gpt-4.5': 5,
16
+ 'o3': 5,
17
+ 'o4-mini': 2,
18
+ 'gemini-2.5-pro': 3,
19
+ 'gemini-2.5-flash': 1.5,
20
+ };
21
+
22
+ export const QUALITY_MULTIPLIERS = {
23
+ correction: 10,
24
+ coordination: 5,
25
+ errorRecovery: 3,
26
+ heavyTask: 2,
27
+ highQuality: 1.5,
28
+ };
29
+
30
+ export const CENTRAL_COMMAND_URL = process.env.GROOVE_CENTRAL_URL || 'https://api.groovedev.ai';
@@ -0,0 +1,45 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ import { createECDH, createHmac, createHash, randomBytes } from 'node:crypto';
4
+ import { readFileSync } from 'node:fs';
5
+
6
+ export function generateECDHKeypair() {
7
+ const ecdh = createECDH('prime256v1');
8
+ ecdh.generateKeys();
9
+ return {
10
+ publicKey: ecdh.getPublicKey('base64'),
11
+ privateKey: ecdh.getPrivateKey('base64'),
12
+ };
13
+ }
14
+
15
+ export function deriveSharedSecret(privateKeyB64, otherPublicKeyB64) {
16
+ const ecdh = createECDH('prime256v1');
17
+ ecdh.setPrivateKey(Buffer.from(privateKeyB64, 'base64'));
18
+ const secret = ecdh.computeSecret(Buffer.from(otherPublicKeyB64, 'base64'));
19
+ return secret.toString('base64');
20
+ }
21
+
22
+ // envelopeBytes MUST contain ALL envelope fields EXCEPT attestation (which holds the HMAC itself).
23
+ // Callers must strip attestation before JSON.stringify to produce envelopeBytes.
24
+ export function signEnvelope(sharedSecretB64, envelopeBytes, sequenceNumber) {
25
+ const key = Buffer.from(sharedSecretB64, 'base64');
26
+ const seqBuf = Buffer.alloc(4);
27
+ seqBuf.writeUInt32BE(sequenceNumber);
28
+ const payload = Buffer.concat([seqBuf, Buffer.from(envelopeBytes)]);
29
+ return createHmac('sha256', key).update(payload).digest('hex');
30
+ }
31
+
32
+ export function verifyEnvelope(sharedSecretB64, envelopeBytes, sequenceNumber, hmac) {
33
+ const expected = signEnvelope(sharedSecretB64, envelopeBytes, sequenceNumber);
34
+ if (expected.length !== hmac.length) return false;
35
+ let diff = 0;
36
+ for (let i = 0; i < expected.length; i++) {
37
+ diff |= expected.charCodeAt(i) ^ hmac.charCodeAt(i);
38
+ }
39
+ return diff === 0;
40
+ }
41
+
42
+ export function computeAppHash(filePath) {
43
+ const content = readFileSync(filePath);
44
+ return createHash('sha256').update(content).digest('hex');
45
+ }