forkit-connect 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/QUICKSTART.md +55 -0
- package/README.md +96 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +4724 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +21 -0
- package/dist/launcher.d.ts +33 -0
- package/dist/launcher.js +9344 -0
- package/dist/ps-list-loader.d.ts +5 -0
- package/dist/ps-list-loader.js +20 -0
- package/dist/v1/agent-observation.d.ts +42 -0
- package/dist/v1/agent-observation.js +499 -0
- package/dist/v1/api.d.ts +276 -0
- package/dist/v1/api.js +390 -0
- package/dist/v1/credential-store.d.ts +92 -0
- package/dist/v1/credential-store.js +797 -0
- package/dist/v1/currency.d.ts +41 -0
- package/dist/v1/currency.js +127 -0
- package/dist/v1/daemon.d.ts +50 -0
- package/dist/v1/daemon.js +265 -0
- package/dist/v1/discovery.d.ts +61 -0
- package/dist/v1/discovery.js +168 -0
- package/dist/v1/filesystem-models.d.ts +11 -0
- package/dist/v1/filesystem-models.js +261 -0
- package/dist/v1/heartbeat.d.ts +45 -0
- package/dist/v1/heartbeat.js +463 -0
- package/dist/v1/lifecycle-monitor.d.ts +78 -0
- package/dist/v1/lifecycle-monitor.js +512 -0
- package/dist/v1/lmstudio.d.ts +11 -0
- package/dist/v1/lmstudio.js +148 -0
- package/dist/v1/ollama.d.ts +19 -0
- package/dist/v1/ollama.js +164 -0
- package/dist/v1/openai-compatible.d.ts +12 -0
- package/dist/v1/openai-compatible.js +124 -0
- package/dist/v1/process-scout.d.ts +50 -0
- package/dist/v1/process-scout.js +715 -0
- package/dist/v1/providers.d.ts +50 -0
- package/dist/v1/providers.js +106 -0
- package/dist/v1/service.d.ts +680 -0
- package/dist/v1/service.js +8286 -0
- package/dist/v1/state.d.ts +87 -0
- package/dist/v1/state.js +1318 -0
- package/dist/v1/test-credential-backend.d.ts +19 -0
- package/dist/v1/test-credential-backend.js +49 -0
- package/dist/v1/types.d.ts +873 -0
- package/dist/v1/types.js +3 -0
- package/dist/v1/update.d.ts +38 -0
- package/dist/v1/update.js +184 -0
- package/dist/v1/vitality-pulse.d.ts +36 -0
- package/dist/v1/vitality-pulse.js +512 -0
- package/package.json +53 -0
package/dist/v1/state.js
ADDED
|
@@ -0,0 +1,1318 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.LocalStateStore = void 0;
|
|
7
|
+
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const C2_EVENT_RETENTION_LIMIT = 1000;
|
|
13
|
+
const C2_EVENT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
14
|
+
// Events synced more than 2 hours ago are evicted before applying the retention cap.
|
|
15
|
+
const C2_SYNCED_EVICTION_AGE_MS = 2 * 60 * 60 * 1000;
|
|
16
|
+
// After this many failures an event is moved to dead_letter to stop retry spam.
|
|
17
|
+
const C2_MAX_RETRY_COUNT = 5;
|
|
18
|
+
const PULSE_EVENT_RETENTION_LIMIT = 500;
|
|
19
|
+
// Observation events repeat on every daemon scan cycle for the same entity.
|
|
20
|
+
// Only the newest per (event_type, entity_key) matters; all prior copies are noise.
|
|
21
|
+
const C2_OBSERVATION_EVENT_TYPES = new Set([
|
|
22
|
+
'connect_runtime_detected',
|
|
23
|
+
'connect_runtime_active',
|
|
24
|
+
'connect_runtime_unavailable',
|
|
25
|
+
'connect_runtime_restored',
|
|
26
|
+
'connect_runtime_health_changed',
|
|
27
|
+
'connect_model_detected',
|
|
28
|
+
'connect_agent_detected',
|
|
29
|
+
'connect_agent_inactive',
|
|
30
|
+
'connect_pulse_green',
|
|
31
|
+
'connect_pulse_amber',
|
|
32
|
+
'connect_pulse_red',
|
|
33
|
+
'connect_daemon_started',
|
|
34
|
+
'connect_shadow_candidate_detected',
|
|
35
|
+
]);
|
|
36
|
+
function c2ObservationKey(event) {
|
|
37
|
+
const entity = event.discovery_hash ?? event.runtime_gaid ?? event.model_name ?? '';
|
|
38
|
+
return `${event.event_type}|${entity}|${event.passport_gaid ?? ''}`;
|
|
39
|
+
}
|
|
40
|
+
const DELIVERED_NOTIFICATION_RETENTION_LIMIT = 1000;
|
|
41
|
+
const AGENT_MODEL_USAGE_RETENTION_LIMIT = 5000;
|
|
42
|
+
const STATE_DATABASE_FILENAME = 'state.v1.sqlite';
|
|
43
|
+
const LEGACY_STATE_FILENAME = 'state.v1.json';
|
|
44
|
+
const STATE_SCHEMA_VERSION = 1;
|
|
45
|
+
const STATE_SECTION_NAMES = [
|
|
46
|
+
'runtime_identity',
|
|
47
|
+
'user_session_reference',
|
|
48
|
+
'workspace_binding',
|
|
49
|
+
'project_binding',
|
|
50
|
+
'effective_binding',
|
|
51
|
+
'connect_identity',
|
|
52
|
+
'connect_config',
|
|
53
|
+
'service_entitlements',
|
|
54
|
+
'daemon_status',
|
|
55
|
+
'last_c2_sync_at',
|
|
56
|
+
'last_c2_sync_error',
|
|
57
|
+
'c2_events',
|
|
58
|
+
'last_pulse_at',
|
|
59
|
+
'last_pulse_summary',
|
|
60
|
+
'last_pulse_error',
|
|
61
|
+
'pulse_events',
|
|
62
|
+
'build_sessions',
|
|
63
|
+
'detected_agents',
|
|
64
|
+
'agent_links',
|
|
65
|
+
'agent_model_usage',
|
|
66
|
+
'detected_runtimes',
|
|
67
|
+
'runtime_passports',
|
|
68
|
+
'c2_sessions',
|
|
69
|
+
'evolution_candidates',
|
|
70
|
+
'detected_models',
|
|
71
|
+
'model_bindings',
|
|
72
|
+
'delivered_notifications',
|
|
73
|
+
'evidence_events',
|
|
74
|
+
'sync_queue',
|
|
75
|
+
'pending_reviews',
|
|
76
|
+
'lifecycle_monitor_records',
|
|
77
|
+
];
|
|
78
|
+
function hashCredentialRef(value) {
|
|
79
|
+
return (0, node_crypto_1.createHash)('sha256').update(value, 'utf8').digest('hex').slice(0, 24);
|
|
80
|
+
}
|
|
81
|
+
function nowIso() {
|
|
82
|
+
return new Date().toISOString();
|
|
83
|
+
}
|
|
84
|
+
function buildDefaultConfig(now) {
|
|
85
|
+
return {
|
|
86
|
+
discovery_mode: 'advisory_only',
|
|
87
|
+
daemon_interval_seconds: 30,
|
|
88
|
+
notifications_enabled: true,
|
|
89
|
+
notification_min_interval_seconds: 300,
|
|
90
|
+
evolution_bridge_enabled: true,
|
|
91
|
+
updated_at: now,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function buildDefaultConnectIdentity() {
|
|
95
|
+
return {
|
|
96
|
+
connect_device_id: null,
|
|
97
|
+
connect_public_key: null,
|
|
98
|
+
connect_identity_status: 'uninitialized',
|
|
99
|
+
paired_at: null,
|
|
100
|
+
pairing_method: null,
|
|
101
|
+
last_identity_check_at: null,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function buildDefaultUserSessionReference(now) {
|
|
105
|
+
return {
|
|
106
|
+
credentialPresent: false,
|
|
107
|
+
credentialRef: null,
|
|
108
|
+
authStatus: 'missing',
|
|
109
|
+
lastAuthenticatedAt: null,
|
|
110
|
+
updatedAt: now,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function buildDefaultServiceEntitlements(now) {
|
|
114
|
+
return {
|
|
115
|
+
secure_device_pairing: true,
|
|
116
|
+
signed_events: false,
|
|
117
|
+
evolution_bridge: true,
|
|
118
|
+
orchestrator: false,
|
|
119
|
+
handoff: false,
|
|
120
|
+
advisory_mode: true,
|
|
121
|
+
auto_draft: false,
|
|
122
|
+
notifications: true,
|
|
123
|
+
authenticated: false,
|
|
124
|
+
workspace_bound: false,
|
|
125
|
+
project_bound: false,
|
|
126
|
+
tier: null,
|
|
127
|
+
updated_at: now,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function buildDefaultDaemonStatus() {
|
|
131
|
+
return {
|
|
132
|
+
daemon_running: false,
|
|
133
|
+
daemon_pid: null,
|
|
134
|
+
started_at: null,
|
|
135
|
+
last_scan_at: null,
|
|
136
|
+
last_scan_summary: null,
|
|
137
|
+
last_error: null,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function normalizeIntervalSeconds(value) {
|
|
141
|
+
const numeric = Number(value);
|
|
142
|
+
if (!Number.isFinite(numeric))
|
|
143
|
+
return 30;
|
|
144
|
+
return Math.max(5, Math.floor(numeric));
|
|
145
|
+
}
|
|
146
|
+
function normalizeConfig(value, now) {
|
|
147
|
+
const record = value && typeof value === 'object' ? value : {};
|
|
148
|
+
const mode = record.discovery_mode === 'auto_draft' ? 'auto_draft' : 'advisory_only';
|
|
149
|
+
const updatedAt = typeof record.updated_at === 'string' && record.updated_at.trim() ? record.updated_at : now;
|
|
150
|
+
return {
|
|
151
|
+
discovery_mode: mode,
|
|
152
|
+
daemon_interval_seconds: normalizeIntervalSeconds(record.daemon_interval_seconds),
|
|
153
|
+
notifications_enabled: record.notifications_enabled !== false,
|
|
154
|
+
notification_min_interval_seconds: normalizeIntervalSeconds(record.notification_min_interval_seconds ?? 300),
|
|
155
|
+
evolution_bridge_enabled: record.evolution_bridge_enabled !== false,
|
|
156
|
+
updated_at: updatedAt,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function normalizeConnectIdentity(value) {
|
|
160
|
+
const record = value && typeof value === 'object' ? value : {};
|
|
161
|
+
const identity = buildDefaultConnectIdentity();
|
|
162
|
+
return {
|
|
163
|
+
connect_device_id: typeof record.connect_device_id === 'string' && record.connect_device_id.trim()
|
|
164
|
+
? record.connect_device_id
|
|
165
|
+
: identity.connect_device_id,
|
|
166
|
+
connect_public_key: typeof record.connect_public_key === 'string' && record.connect_public_key.trim()
|
|
167
|
+
? record.connect_public_key
|
|
168
|
+
: identity.connect_public_key,
|
|
169
|
+
connect_identity_status: record.connect_identity_status === 'ready'
|
|
170
|
+
|| record.connect_identity_status === 'paired'
|
|
171
|
+
|| record.connect_identity_status === 'error'
|
|
172
|
+
? record.connect_identity_status
|
|
173
|
+
: identity.connect_identity_status,
|
|
174
|
+
paired_at: typeof record.paired_at === 'string' ? record.paired_at : identity.paired_at,
|
|
175
|
+
pairing_method: record.pairing_method === 'local_generated'
|
|
176
|
+
|| record.pairing_method === 'device_login'
|
|
177
|
+
|| record.pairing_method === 'restored'
|
|
178
|
+
? record.pairing_method
|
|
179
|
+
: identity.pairing_method,
|
|
180
|
+
last_identity_check_at: typeof record.last_identity_check_at === 'string'
|
|
181
|
+
? record.last_identity_check_at
|
|
182
|
+
: identity.last_identity_check_at,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function normalizeServiceEntitlements(value, now) {
|
|
186
|
+
const record = value && typeof value === 'object' ? value : {};
|
|
187
|
+
const initial = buildDefaultServiceEntitlements(now);
|
|
188
|
+
return {
|
|
189
|
+
secure_device_pairing: record.secure_device_pairing !== false,
|
|
190
|
+
signed_events: record.signed_events === true,
|
|
191
|
+
evolution_bridge: record.evolution_bridge !== false,
|
|
192
|
+
orchestrator: record.orchestrator === true,
|
|
193
|
+
handoff: record.handoff === true,
|
|
194
|
+
advisory_mode: record.advisory_mode !== false,
|
|
195
|
+
auto_draft: record.auto_draft === true,
|
|
196
|
+
notifications: record.notifications !== false,
|
|
197
|
+
authenticated: record.authenticated === true,
|
|
198
|
+
workspace_bound: record.workspace_bound === true,
|
|
199
|
+
project_bound: record.project_bound === true,
|
|
200
|
+
tier: typeof record.tier === 'string' && record.tier.trim() ? record.tier : initial.tier,
|
|
201
|
+
updated_at: typeof record.updated_at === 'string' && record.updated_at.trim() ? record.updated_at : now,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function normalizeBindingState(value) {
|
|
205
|
+
return value === 'pending_approval'
|
|
206
|
+
|| value === 'approved_needs_scope'
|
|
207
|
+
|| value === 'active'
|
|
208
|
+
|| value === 'paused'
|
|
209
|
+
|| value === 'revoked'
|
|
210
|
+
|| value === 'stale'
|
|
211
|
+
|| value === 'needs_reconsent'
|
|
212
|
+
? value
|
|
213
|
+
: 'pending_approval';
|
|
214
|
+
}
|
|
215
|
+
function normalizeBindingRole(value) {
|
|
216
|
+
return value === 'owner'
|
|
217
|
+
|| value === 'maintainer'
|
|
218
|
+
|| value === 'contributor'
|
|
219
|
+
|| value === 'viewer'
|
|
220
|
+
? value
|
|
221
|
+
: null;
|
|
222
|
+
}
|
|
223
|
+
function normalizeConsentProfile(value) {
|
|
224
|
+
const record = value && typeof value === 'object' ? value : {};
|
|
225
|
+
return {
|
|
226
|
+
allow_draft_creation: record.allow_draft_creation !== false,
|
|
227
|
+
allow_evidence_sync: record.allow_evidence_sync === true,
|
|
228
|
+
allow_runtime_session_sync: record.allow_runtime_session_sync === true,
|
|
229
|
+
allow_lineage_suggestions: record.allow_lineage_suggestions === true,
|
|
230
|
+
allow_publication_support: record.allow_publication_support === true,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function normalizeCapabilityProfile(value) {
|
|
234
|
+
const record = value && typeof value === 'object' ? value : {};
|
|
235
|
+
return {
|
|
236
|
+
binding_active: record.binding_active === true,
|
|
237
|
+
workspace_bound: record.workspace_bound === true,
|
|
238
|
+
project_bound: record.project_bound === true,
|
|
239
|
+
can_create_draft: record.can_create_draft === true,
|
|
240
|
+
can_sync_evidence: record.can_sync_evidence === true,
|
|
241
|
+
can_sync_runtime_sessions: record.can_sync_runtime_sessions === true,
|
|
242
|
+
can_emit_lineage_suggestions: record.can_emit_lineage_suggestions === true,
|
|
243
|
+
can_prepare_private_publication: record.can_prepare_private_publication === true,
|
|
244
|
+
can_prepare_public_publication: record.can_prepare_public_publication === true,
|
|
245
|
+
plan_key: typeof record.plan_key === 'string' && record.plan_key.trim() ? record.plan_key : null,
|
|
246
|
+
role: normalizeBindingRole(record.role),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function normalizeApprovedUser(value) {
|
|
250
|
+
const record = value && typeof value === 'object' ? value : null;
|
|
251
|
+
if (!record || typeof record.id !== 'string' || !record.id.trim())
|
|
252
|
+
return null;
|
|
253
|
+
const avatarUrl = typeof record.avatarUrl === 'string' && record.avatarUrl.trim()
|
|
254
|
+
? record.avatarUrl
|
|
255
|
+
: typeof record.avatar_url === 'string' && record.avatar_url.trim()
|
|
256
|
+
? record.avatar_url
|
|
257
|
+
: typeof record.picture === 'string' && record.picture.trim()
|
|
258
|
+
? record.picture
|
|
259
|
+
: typeof record.pictureUrl === 'string' && record.pictureUrl.trim()
|
|
260
|
+
? record.pictureUrl
|
|
261
|
+
: typeof record.picture_url === 'string' && record.picture_url.trim()
|
|
262
|
+
? record.picture_url
|
|
263
|
+
: typeof record.photoUrl === 'string' && record.photoUrl.trim()
|
|
264
|
+
? record.photoUrl
|
|
265
|
+
: typeof record.photo_url === 'string' && record.photo_url.trim()
|
|
266
|
+
? record.photo_url
|
|
267
|
+
: null;
|
|
268
|
+
return {
|
|
269
|
+
id: record.id,
|
|
270
|
+
name: typeof record.name === 'string' && record.name.trim() ? record.name : null,
|
|
271
|
+
email: typeof record.email === 'string' && record.email.trim() ? record.email : null,
|
|
272
|
+
goaid: typeof record.goaid === 'string' && record.goaid.trim() ? record.goaid : null,
|
|
273
|
+
avatarUrl,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function normalizeWorkspaceRef(value) {
|
|
277
|
+
const record = value && typeof value === 'object' ? value : null;
|
|
278
|
+
if (!record || typeof record.id !== 'string' || !record.id.trim())
|
|
279
|
+
return null;
|
|
280
|
+
return {
|
|
281
|
+
id: record.id,
|
|
282
|
+
name: typeof record.name === 'string' && record.name.trim() ? record.name : null,
|
|
283
|
+
ownerName: typeof record.ownerName === 'string' && record.ownerName.trim() ? record.ownerName : null,
|
|
284
|
+
passportGaid: typeof record.passportGaid === 'string' && record.passportGaid.trim() ? record.passportGaid : null,
|
|
285
|
+
passportType: typeof record.passportType === 'string' && record.passportType.trim() ? record.passportType : null,
|
|
286
|
+
description: typeof record.description === 'string' && record.description.trim() ? record.description : null,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function normalizeProjectRef(value) {
|
|
290
|
+
const record = value && typeof value === 'object' ? value : null;
|
|
291
|
+
if (!record || typeof record.id !== 'string' || !record.id.trim())
|
|
292
|
+
return null;
|
|
293
|
+
return {
|
|
294
|
+
id: record.id,
|
|
295
|
+
workspaceId: typeof record.workspaceId === 'string' && record.workspaceId.trim() ? record.workspaceId : null,
|
|
296
|
+
name: typeof record.name === 'string' && record.name.trim() ? record.name : null,
|
|
297
|
+
description: typeof record.description === 'string' && record.description.trim() ? record.description : null,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function normalizeBindingRecord(value) {
|
|
301
|
+
const record = value && typeof value === 'object' ? value : null;
|
|
302
|
+
if (!record || typeof record.bindingId !== 'string' || !record.bindingId.trim())
|
|
303
|
+
return null;
|
|
304
|
+
const consentProfile = normalizeConsentProfile(record.consentProfile);
|
|
305
|
+
const capabilityProfile = normalizeCapabilityProfile(record.capabilityProfile);
|
|
306
|
+
return {
|
|
307
|
+
bindingId: record.bindingId,
|
|
308
|
+
state: normalizeBindingState(record.state),
|
|
309
|
+
connectDeviceId: typeof record.connectDeviceId === 'string' && record.connectDeviceId.trim() ? record.connectDeviceId : '',
|
|
310
|
+
approvedUserId: typeof record.approvedUserId === 'string' && record.approvedUserId.trim() ? record.approvedUserId : null,
|
|
311
|
+
approvedUserGoadid: typeof record.approvedUserGoadid === 'string' && record.approvedUserGoadid.trim() ? record.approvedUserGoadid : null,
|
|
312
|
+
workspaceId: typeof record.workspaceId === 'string' && record.workspaceId.trim() ? record.workspaceId : null,
|
|
313
|
+
projectId: typeof record.projectId === 'string' && record.projectId.trim() ? record.projectId : null,
|
|
314
|
+
planKey: typeof record.planKey === 'string' && record.planKey.trim() ? record.planKey : capabilityProfile.plan_key,
|
|
315
|
+
role: normalizeBindingRole(record.role ?? capabilityProfile.role),
|
|
316
|
+
consentProfile,
|
|
317
|
+
capabilityProfile,
|
|
318
|
+
approvedAt: typeof record.approvedAt === 'string' && record.approvedAt.trim() ? record.approvedAt : null,
|
|
319
|
+
revokedAt: typeof record.revokedAt === 'string' && record.revokedAt.trim() ? record.revokedAt : null,
|
|
320
|
+
lastSeenAt: typeof record.lastSeenAt === 'string' && record.lastSeenAt.trim() ? record.lastSeenAt : null,
|
|
321
|
+
createdAt: typeof record.createdAt === 'string' && record.createdAt.trim() ? record.createdAt : nowIso(),
|
|
322
|
+
updatedAt: typeof record.updatedAt === 'string' && record.updatedAt.trim() ? record.updatedAt : nowIso(),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function normalizeEffectiveBinding(value) {
|
|
326
|
+
const record = value && typeof value === 'object' ? value : null;
|
|
327
|
+
if (!record)
|
|
328
|
+
return null;
|
|
329
|
+
const binding = normalizeBindingRecord(record.binding);
|
|
330
|
+
if (!binding)
|
|
331
|
+
return null;
|
|
332
|
+
return {
|
|
333
|
+
binding,
|
|
334
|
+
capability_profile: normalizeCapabilityProfile(record.capability_profile ?? binding.capabilityProfile),
|
|
335
|
+
consent_profile: normalizeConsentProfile(record.consent_profile ?? binding.consentProfile),
|
|
336
|
+
approved_user: normalizeApprovedUser(record.approved_user),
|
|
337
|
+
workspace: normalizeWorkspaceRef(record.workspace),
|
|
338
|
+
project: normalizeProjectRef(record.project),
|
|
339
|
+
fetched_at: typeof record.fetched_at === 'string' && record.fetched_at.trim() ? record.fetched_at : nowIso(),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function normalizeDaemonStatus(value) {
|
|
343
|
+
const record = value && typeof value === 'object' ? value : {};
|
|
344
|
+
return {
|
|
345
|
+
daemon_running: Boolean(record.daemon_running),
|
|
346
|
+
daemon_pid: typeof record.daemon_pid === 'number' && Number.isFinite(record.daemon_pid) ? record.daemon_pid : null,
|
|
347
|
+
started_at: typeof record.started_at === 'string' ? record.started_at : null,
|
|
348
|
+
last_scan_at: typeof record.last_scan_at === 'string' ? record.last_scan_at : null,
|
|
349
|
+
last_scan_summary: record.last_scan_summary && typeof record.last_scan_summary === 'object'
|
|
350
|
+
? record.last_scan_summary
|
|
351
|
+
: null,
|
|
352
|
+
last_error: typeof record.last_error === 'string' ? record.last_error : null,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function normalizeState(parsed) {
|
|
356
|
+
const now = nowIso();
|
|
357
|
+
const initial = buildInitialState();
|
|
358
|
+
const value = parsed && typeof parsed === 'object' ? parsed : {};
|
|
359
|
+
return {
|
|
360
|
+
runtime_identity: value.runtime_identity && typeof value.runtime_identity === 'object'
|
|
361
|
+
? { ...initial.runtime_identity, ...value.runtime_identity }
|
|
362
|
+
: initial.runtime_identity,
|
|
363
|
+
user_session_reference: value.user_session_reference && typeof value.user_session_reference === 'object'
|
|
364
|
+
? {
|
|
365
|
+
...initial.user_session_reference,
|
|
366
|
+
...value.user_session_reference,
|
|
367
|
+
credentialPresent: value.user_session_reference.credentialPresent === true
|
|
368
|
+
|| typeof value.user_session_reference.sessionRef === 'string',
|
|
369
|
+
credentialRef: typeof value.user_session_reference.credentialRef === 'string'
|
|
370
|
+
? String(value.user_session_reference.credentialRef)
|
|
371
|
+
: typeof value.user_session_reference.sessionRef === 'string'
|
|
372
|
+
? hashCredentialRef(String(value.user_session_reference.sessionRef))
|
|
373
|
+
: null,
|
|
374
|
+
authStatus: value.user_session_reference.authStatus === 'authenticated'
|
|
375
|
+
|| typeof value.user_session_reference.sessionRef === 'string'
|
|
376
|
+
? 'authenticated'
|
|
377
|
+
: 'missing',
|
|
378
|
+
lastAuthenticatedAt: typeof value.user_session_reference.lastAuthenticatedAt === 'string'
|
|
379
|
+
? String(value.user_session_reference.lastAuthenticatedAt)
|
|
380
|
+
: typeof value.user_session_reference.sessionRef === 'string'
|
|
381
|
+
? String(value.user_session_reference.updatedAt || now)
|
|
382
|
+
: null,
|
|
383
|
+
updatedAt: typeof value.user_session_reference.updatedAt === 'string'
|
|
384
|
+
? String(value.user_session_reference.updatedAt)
|
|
385
|
+
: now,
|
|
386
|
+
}
|
|
387
|
+
: initial.user_session_reference,
|
|
388
|
+
workspace_binding: value.workspace_binding && typeof value.workspace_binding === 'object'
|
|
389
|
+
? { ...initial.workspace_binding, ...value.workspace_binding }
|
|
390
|
+
: initial.workspace_binding,
|
|
391
|
+
project_binding: value.project_binding && typeof value.project_binding === 'object'
|
|
392
|
+
? { ...initial.project_binding, ...value.project_binding }
|
|
393
|
+
: initial.project_binding,
|
|
394
|
+
effective_binding: normalizeEffectiveBinding(value.effective_binding),
|
|
395
|
+
connect_identity: normalizeConnectIdentity(value.connect_identity),
|
|
396
|
+
connect_config: normalizeConfig(value.connect_config, now),
|
|
397
|
+
service_entitlements: normalizeServiceEntitlements(value.service_entitlements, now),
|
|
398
|
+
daemon_status: normalizeDaemonStatus(value.daemon_status),
|
|
399
|
+
last_c2_sync_at: typeof value.last_c2_sync_at === 'string' ? value.last_c2_sync_at : null,
|
|
400
|
+
last_c2_sync_error: typeof value.last_c2_sync_error === 'string' ? value.last_c2_sync_error : null,
|
|
401
|
+
c2_events: Array.isArray(value.c2_events) ? value.c2_events : [],
|
|
402
|
+
last_pulse_at: typeof value.last_pulse_at === 'string' ? value.last_pulse_at : null,
|
|
403
|
+
last_pulse_summary: value.last_pulse_summary && typeof value.last_pulse_summary === 'object'
|
|
404
|
+
? value.last_pulse_summary
|
|
405
|
+
: null,
|
|
406
|
+
last_pulse_error: typeof value.last_pulse_error === 'string' ? value.last_pulse_error : null,
|
|
407
|
+
pulse_events: Array.isArray(value.pulse_events) ? value.pulse_events : [],
|
|
408
|
+
build_sessions: Array.isArray(value.build_sessions) ? value.build_sessions : [],
|
|
409
|
+
detected_agents: Array.isArray(value.detected_agents) ? value.detected_agents : [],
|
|
410
|
+
agent_links: Array.isArray(value.agent_links) ? value.agent_links : [],
|
|
411
|
+
agent_model_usage: Array.isArray(value.agent_model_usage) ? value.agent_model_usage : [],
|
|
412
|
+
detected_runtimes: Array.isArray(value.detected_runtimes) ? value.detected_runtimes : [],
|
|
413
|
+
runtime_passports: Array.isArray(value.runtime_passports) ? value.runtime_passports : [],
|
|
414
|
+
c2_sessions: Array.isArray(value.c2_sessions) ? value.c2_sessions : [],
|
|
415
|
+
evolution_candidates: Array.isArray(value.evolution_candidates) ? value.evolution_candidates : [],
|
|
416
|
+
detected_models: Array.isArray(value.detected_models) ? value.detected_models : [],
|
|
417
|
+
model_bindings: Array.isArray(value.model_bindings)
|
|
418
|
+
? value.model_bindings.map((binding) => ({
|
|
419
|
+
...binding,
|
|
420
|
+
runtimeSignalKeyPresent: binding.runtimeSignalKeyPresent === true || typeof binding.runtimeSignalApiKey === 'string',
|
|
421
|
+
runtimeSignalKeyRef: typeof binding.runtimeSignalKeyRef === 'string'
|
|
422
|
+
? String(binding.runtimeSignalKeyRef)
|
|
423
|
+
: typeof binding.runtimeSignalApiKey === 'string'
|
|
424
|
+
? hashCredentialRef(String(binding.runtimeSignalApiKey))
|
|
425
|
+
: null,
|
|
426
|
+
}))
|
|
427
|
+
: [],
|
|
428
|
+
delivered_notifications: Array.isArray(value.delivered_notifications) ? value.delivered_notifications : [],
|
|
429
|
+
evidence_events: Array.isArray(value.evidence_events) ? value.evidence_events : [],
|
|
430
|
+
sync_queue: Array.isArray(value.sync_queue) ? value.sync_queue : [],
|
|
431
|
+
pending_reviews: Array.isArray(value.pending_reviews) ? value.pending_reviews : [],
|
|
432
|
+
lifecycle_monitor_records: Array.isArray(value.lifecycle_monitor_records) ? value.lifecycle_monitor_records : [],
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function buildInitialState() {
|
|
436
|
+
const now = nowIso();
|
|
437
|
+
return {
|
|
438
|
+
runtime_identity: {
|
|
439
|
+
runtimeId: (0, node_crypto_1.randomUUID)(),
|
|
440
|
+
hostname: node_os_1.default.hostname(),
|
|
441
|
+
platform: node_os_1.default.platform(),
|
|
442
|
+
arch: node_os_1.default.arch(),
|
|
443
|
+
createdAt: now,
|
|
444
|
+
},
|
|
445
|
+
user_session_reference: buildDefaultUserSessionReference(now),
|
|
446
|
+
workspace_binding: {
|
|
447
|
+
workspaceId: null,
|
|
448
|
+
updatedAt: now,
|
|
449
|
+
},
|
|
450
|
+
project_binding: {
|
|
451
|
+
projectId: null,
|
|
452
|
+
updatedAt: now,
|
|
453
|
+
},
|
|
454
|
+
effective_binding: null,
|
|
455
|
+
connect_identity: buildDefaultConnectIdentity(),
|
|
456
|
+
connect_config: buildDefaultConfig(now),
|
|
457
|
+
service_entitlements: buildDefaultServiceEntitlements(now),
|
|
458
|
+
daemon_status: buildDefaultDaemonStatus(),
|
|
459
|
+
last_c2_sync_at: null,
|
|
460
|
+
last_c2_sync_error: null,
|
|
461
|
+
c2_events: [],
|
|
462
|
+
last_pulse_at: null,
|
|
463
|
+
last_pulse_summary: null,
|
|
464
|
+
last_pulse_error: null,
|
|
465
|
+
pulse_events: [],
|
|
466
|
+
build_sessions: [],
|
|
467
|
+
detected_agents: [],
|
|
468
|
+
agent_links: [],
|
|
469
|
+
agent_model_usage: [],
|
|
470
|
+
detected_runtimes: [],
|
|
471
|
+
runtime_passports: [],
|
|
472
|
+
c2_sessions: [],
|
|
473
|
+
evolution_candidates: [],
|
|
474
|
+
detected_models: [],
|
|
475
|
+
model_bindings: [],
|
|
476
|
+
delivered_notifications: [],
|
|
477
|
+
evidence_events: [],
|
|
478
|
+
sync_queue: [],
|
|
479
|
+
pending_reviews: [],
|
|
480
|
+
lifecycle_monitor_records: [],
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function readJsonFile(filePath) {
|
|
484
|
+
try {
|
|
485
|
+
const raw = node_fs_1.default.readFileSync(filePath, 'utf8');
|
|
486
|
+
return JSON.parse(raw);
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
class LocalStateStore {
|
|
493
|
+
paths;
|
|
494
|
+
db = null;
|
|
495
|
+
constructor(baseDir) {
|
|
496
|
+
const envStateDir = String(process.env.FORKIT_CONNECT_STATE_DIR || '').trim();
|
|
497
|
+
const stateDir = baseDir ?? (envStateDir || node_path_1.default.join(node_os_1.default.homedir(), '.forkit-connect'));
|
|
498
|
+
this.paths = {
|
|
499
|
+
stateDir,
|
|
500
|
+
stateFile: node_path_1.default.join(stateDir, STATE_DATABASE_FILENAME),
|
|
501
|
+
legacyStateFile: node_path_1.default.join(stateDir, LEGACY_STATE_FILENAME),
|
|
502
|
+
identityPrivateKeyFile: node_path_1.default.join(stateDir, 'connect-device.v1.pem'),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
getPaths() {
|
|
506
|
+
return this.paths;
|
|
507
|
+
}
|
|
508
|
+
ensureInitialized() {
|
|
509
|
+
this.ensureInitializedInternal();
|
|
510
|
+
return this.readStateFromDatabase();
|
|
511
|
+
}
|
|
512
|
+
readState() {
|
|
513
|
+
this.ensureInitializedInternal();
|
|
514
|
+
return this.readStateFromDatabase();
|
|
515
|
+
}
|
|
516
|
+
readRawStateFile() {
|
|
517
|
+
const legacy = this.readLegacyStateFile();
|
|
518
|
+
if (legacy)
|
|
519
|
+
return legacy;
|
|
520
|
+
try {
|
|
521
|
+
this.ensureInitializedInternal();
|
|
522
|
+
return this.withDatabase((db) => this.loadRawStateRecord(db), true);
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
writeState(state) {
|
|
529
|
+
const normalized = normalizeState(state);
|
|
530
|
+
this.ensureInitializedInternal();
|
|
531
|
+
this.withDatabase((db) => {
|
|
532
|
+
this.persistState(db, normalized);
|
|
533
|
+
}, true);
|
|
534
|
+
}
|
|
535
|
+
updateSessionCredentialMetadata(input) {
|
|
536
|
+
return this.mutateState((state) => {
|
|
537
|
+
state.user_session_reference = {
|
|
538
|
+
credentialPresent: input.credentialPresent,
|
|
539
|
+
credentialRef: input.credentialRef,
|
|
540
|
+
authStatus: input.authStatus,
|
|
541
|
+
lastAuthenticatedAt: input.lastAuthenticatedAt,
|
|
542
|
+
updatedAt: input.updatedAt ?? nowIso(),
|
|
543
|
+
};
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
updateWorkspaceProjectBinding(workspaceId, projectId) {
|
|
547
|
+
return this.mutateState((state) => {
|
|
548
|
+
state.workspace_binding = {
|
|
549
|
+
workspaceId,
|
|
550
|
+
updatedAt: nowIso(),
|
|
551
|
+
};
|
|
552
|
+
state.project_binding = {
|
|
553
|
+
projectId,
|
|
554
|
+
updatedAt: nowIso(),
|
|
555
|
+
};
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
updateEffectiveBinding(effectiveBinding) {
|
|
559
|
+
return this.mutateState((state) => {
|
|
560
|
+
state.effective_binding = effectiveBinding;
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
updateConnectConfig(configPatch) {
|
|
564
|
+
return this.mutateState((state) => {
|
|
565
|
+
state.connect_config = {
|
|
566
|
+
...state.connect_config,
|
|
567
|
+
...configPatch,
|
|
568
|
+
daemon_interval_seconds: configPatch.daemon_interval_seconds !== undefined
|
|
569
|
+
? normalizeIntervalSeconds(configPatch.daemon_interval_seconds)
|
|
570
|
+
: state.connect_config.daemon_interval_seconds,
|
|
571
|
+
notification_min_interval_seconds: configPatch.notification_min_interval_seconds !== undefined
|
|
572
|
+
? normalizeIntervalSeconds(configPatch.notification_min_interval_seconds)
|
|
573
|
+
: state.connect_config.notification_min_interval_seconds,
|
|
574
|
+
updated_at: nowIso(),
|
|
575
|
+
};
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
updateConnectIdentity(identityPatch) {
|
|
579
|
+
return this.mutateState((state) => {
|
|
580
|
+
state.connect_identity = {
|
|
581
|
+
...state.connect_identity,
|
|
582
|
+
...identityPatch,
|
|
583
|
+
};
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
updateServiceEntitlements(entitlements) {
|
|
587
|
+
return this.mutateState((state) => {
|
|
588
|
+
state.service_entitlements = entitlements;
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
updateDaemonStatus(statusPatch) {
|
|
592
|
+
return this.mutateState((state) => {
|
|
593
|
+
state.daemon_status = {
|
|
594
|
+
...state.daemon_status,
|
|
595
|
+
...statusPatch,
|
|
596
|
+
};
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
appendC2Events(events) {
|
|
600
|
+
if (events.length === 0) {
|
|
601
|
+
return this.ensureInitialized();
|
|
602
|
+
}
|
|
603
|
+
return this.mutateState((state) => {
|
|
604
|
+
const existingIds = new Set(state.c2_events.map((event) => event.event_id));
|
|
605
|
+
// For observation events already pending, track their content keys so we
|
|
606
|
+
// skip recording a new event when nothing meaningful has changed.
|
|
607
|
+
const pendingObservationKeys = new Set(state.c2_events
|
|
608
|
+
.filter((e) => e.sync_state === 'pending' && C2_OBSERVATION_EVENT_TYPES.has(e.event_type))
|
|
609
|
+
.map(c2ObservationKey));
|
|
610
|
+
for (const event of events) {
|
|
611
|
+
if (existingIds.has(event.event_id))
|
|
612
|
+
continue;
|
|
613
|
+
if (C2_OBSERVATION_EVENT_TYPES.has(event.event_type)) {
|
|
614
|
+
const key = c2ObservationKey(event);
|
|
615
|
+
if (pendingObservationKeys.has(key))
|
|
616
|
+
continue; // already have a pending copy of this observation
|
|
617
|
+
pendingObservationKeys.add(key);
|
|
618
|
+
}
|
|
619
|
+
state.c2_events.push(event);
|
|
620
|
+
existingIds.add(event.event_id);
|
|
621
|
+
}
|
|
622
|
+
// Evict old synced events first so they don't consume the retention cap.
|
|
623
|
+
const cutoff = Date.now() - C2_SYNCED_EVICTION_AGE_MS;
|
|
624
|
+
state.c2_events = state.c2_events.filter((event) => event.sync_state !== 'synced' || !event.synced_at || new Date(event.synced_at).getTime() > cutoff);
|
|
625
|
+
if (state.c2_events.length > C2_EVENT_RETENTION_LIMIT) {
|
|
626
|
+
state.c2_events = state.c2_events.slice(-C2_EVENT_RETENTION_LIMIT);
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
// Collapses all pending observation events down to one per (event_type, entity_key),
|
|
631
|
+
// keeping the newest. Also removes any pending events older than C2_EVENT_MAX_AGE_MS.
|
|
632
|
+
// Safe to call before flushing — transition events and already-synced events are untouched.
|
|
633
|
+
pruneRedundantPendingC2Events() {
|
|
634
|
+
let pruned = 0;
|
|
635
|
+
this.mutateState((state) => {
|
|
636
|
+
const cutoff = new Date(Date.now() - C2_EVENT_MAX_AGE_MS).toISOString();
|
|
637
|
+
const keepIds = new Set();
|
|
638
|
+
const newestObservationByKey = new Map();
|
|
639
|
+
for (const event of state.c2_events) {
|
|
640
|
+
// Always keep synced events and transition events; only collapse observation events.
|
|
641
|
+
if (event.sync_state === 'synced' || !C2_OBSERVATION_EVENT_TYPES.has(event.event_type)) {
|
|
642
|
+
// Still drop stale observation-unrelated events that are beyond the age limit
|
|
643
|
+
if (event.occurred_at >= cutoff)
|
|
644
|
+
keepIds.add(event.event_id);
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
// For observation events: track the newest per logical key
|
|
648
|
+
const key = c2ObservationKey(event);
|
|
649
|
+
const existing = newestObservationByKey.get(key);
|
|
650
|
+
if (!existing || event.occurred_at > existing.occurred_at) {
|
|
651
|
+
newestObservationByKey.set(key, event);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
for (const event of newestObservationByKey.values()) {
|
|
655
|
+
keepIds.add(event.event_id);
|
|
656
|
+
}
|
|
657
|
+
const before = state.c2_events.length;
|
|
658
|
+
state.c2_events = state.c2_events.filter((e) => keepIds.has(e.event_id));
|
|
659
|
+
pruned = before - state.c2_events.length;
|
|
660
|
+
});
|
|
661
|
+
return pruned;
|
|
662
|
+
}
|
|
663
|
+
// Assigns gaid to every pending event that has none, so events recorded before
|
|
664
|
+
// a binding existed can still be synced once a binding is established.
|
|
665
|
+
// Idempotent: only touches events where passport_gaid is null.
|
|
666
|
+
backfillMissingGaidOnPendingC2Events(gaid) {
|
|
667
|
+
let count = 0;
|
|
668
|
+
this.mutateState((state) => {
|
|
669
|
+
for (let i = 0; i < state.c2_events.length; i++) {
|
|
670
|
+
const event = state.c2_events[i];
|
|
671
|
+
if (!event || event.sync_state !== 'pending' || event.passport_gaid)
|
|
672
|
+
continue;
|
|
673
|
+
state.c2_events[i] = { ...event, passport_gaid: gaid };
|
|
674
|
+
count += 1;
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
return count;
|
|
678
|
+
}
|
|
679
|
+
updateC2SyncStatus(lastSyncAt, lastSyncError) {
|
|
680
|
+
return this.mutateState((state) => {
|
|
681
|
+
state.last_c2_sync_at = lastSyncAt;
|
|
682
|
+
state.last_c2_sync_error = lastSyncError;
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Backfill pending events that have no passport_gaid with the supplied gaid.
|
|
687
|
+
* Events recorded before a model was bound lack a GAID; once binding is
|
|
688
|
+
* confirmed this promotes them to syncable without altering their content.
|
|
689
|
+
* Returns the number of events updated.
|
|
690
|
+
*/
|
|
691
|
+
backfillC2EventsWithGaid(gaid) {
|
|
692
|
+
const trimmed = String(gaid || '').trim();
|
|
693
|
+
if (!trimmed)
|
|
694
|
+
return 0;
|
|
695
|
+
let count = 0;
|
|
696
|
+
this.mutateState((state) => {
|
|
697
|
+
for (let i = 0; i < state.c2_events.length; i++) {
|
|
698
|
+
const event = state.c2_events[i];
|
|
699
|
+
if (event && event.sync_state === 'pending' && !String(event.passport_gaid || '').trim()) {
|
|
700
|
+
state.c2_events[i] = { ...event, passport_gaid: trimmed };
|
|
701
|
+
count += 1;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
return count;
|
|
706
|
+
}
|
|
707
|
+
markC2EventSynced(eventId) {
|
|
708
|
+
return this.mutateState((state) => {
|
|
709
|
+
const index = state.c2_events.findIndex((event) => event.event_id === eventId);
|
|
710
|
+
if (index < 0)
|
|
711
|
+
return;
|
|
712
|
+
const current = state.c2_events[index];
|
|
713
|
+
if (!current)
|
|
714
|
+
return;
|
|
715
|
+
state.c2_events[index] = {
|
|
716
|
+
...current,
|
|
717
|
+
sync_state: 'synced',
|
|
718
|
+
synced_at: nowIso(),
|
|
719
|
+
last_error: null,
|
|
720
|
+
};
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
markC2EventFailed(eventId, errorMessage) {
|
|
724
|
+
return this.mutateState((state) => {
|
|
725
|
+
const index = state.c2_events.findIndex((event) => event.event_id === eventId);
|
|
726
|
+
if (index < 0)
|
|
727
|
+
return;
|
|
728
|
+
const current = state.c2_events[index];
|
|
729
|
+
if (!current)
|
|
730
|
+
return;
|
|
731
|
+
const retryCount = (current.retry_count ?? 0) + 1;
|
|
732
|
+
state.c2_events[index] = {
|
|
733
|
+
...current,
|
|
734
|
+
sync_state: retryCount >= C2_MAX_RETRY_COUNT ? 'dead_letter' : 'pending',
|
|
735
|
+
synced_at: null,
|
|
736
|
+
last_error: errorMessage,
|
|
737
|
+
retry_count: retryCount,
|
|
738
|
+
};
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
discardC2Event(eventId, errorMessage) {
|
|
742
|
+
return this.mutateState((state) => {
|
|
743
|
+
const index = state.c2_events.findIndex((event) => event.event_id === eventId);
|
|
744
|
+
if (index < 0)
|
|
745
|
+
return;
|
|
746
|
+
const current = state.c2_events[index];
|
|
747
|
+
if (!current)
|
|
748
|
+
return;
|
|
749
|
+
state.c2_events[index] = {
|
|
750
|
+
...current,
|
|
751
|
+
sync_state: 'dead_letter',
|
|
752
|
+
synced_at: null,
|
|
753
|
+
last_error: errorMessage,
|
|
754
|
+
retry_count: Math.max(current.retry_count ?? 0, C2_MAX_RETRY_COUNT),
|
|
755
|
+
};
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
updatePulseState(summary, lastError, pulseEvents) {
|
|
759
|
+
return this.mutateState((state) => {
|
|
760
|
+
state.last_pulse_at = summary?.measured_at ?? state.last_pulse_at;
|
|
761
|
+
state.last_pulse_summary = summary;
|
|
762
|
+
state.last_pulse_error = lastError;
|
|
763
|
+
if (pulseEvents && pulseEvents.length > 0) {
|
|
764
|
+
state.pulse_events.push(...pulseEvents.map((event) => ({
|
|
765
|
+
event_id: (0, node_crypto_1.randomUUID)(),
|
|
766
|
+
...event,
|
|
767
|
+
})));
|
|
768
|
+
if (state.pulse_events.length > PULSE_EVENT_RETENTION_LIMIT) {
|
|
769
|
+
state.pulse_events = state.pulse_events.slice(-PULSE_EVENT_RETENTION_LIMIT);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
upsertDetectedRuntime(runtime) {
|
|
775
|
+
return this.mutateState((state) => {
|
|
776
|
+
const index = state.detected_runtimes.findIndex((item) => item.discoveryHash === runtime.discoveryHash);
|
|
777
|
+
if (index >= 0) {
|
|
778
|
+
state.detected_runtimes[index] = runtime;
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
state.detected_runtimes.push(runtime);
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
upsertRuntimePassport(runtimePassport) {
|
|
786
|
+
return this.mutateState((state) => {
|
|
787
|
+
const index = state.runtime_passports.findIndex((item) => item.runtime_gaid === runtimePassport.runtime_gaid);
|
|
788
|
+
if (index >= 0) {
|
|
789
|
+
state.runtime_passports[index] = runtimePassport;
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
state.runtime_passports.push(runtimePassport);
|
|
793
|
+
}
|
|
794
|
+
state.runtime_passports.sort((left, right) => right.last_seen_at.localeCompare(left.last_seen_at));
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
replaceRuntimePassports(runtimePassports) {
|
|
798
|
+
return this.mutateState((state) => {
|
|
799
|
+
const seen = new Map();
|
|
800
|
+
for (const runtimePassport of runtimePassports) {
|
|
801
|
+
seen.set(runtimePassport.runtime_gaid, runtimePassport);
|
|
802
|
+
}
|
|
803
|
+
state.runtime_passports = [...seen.values()].sort((left, right) => right.last_seen_at.localeCompare(left.last_seen_at));
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
upsertC2Session(session) {
|
|
807
|
+
return this.mutateState((state) => {
|
|
808
|
+
const index = state.c2_sessions.findIndex((item) => item.session_id === session.session_id && item.passport_gaid === session.passport_gaid);
|
|
809
|
+
if (index >= 0) {
|
|
810
|
+
state.c2_sessions[index] = session;
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
state.c2_sessions.push(session);
|
|
814
|
+
}
|
|
815
|
+
state.c2_sessions.sort((left, right) => String(right.last_seen_at || right.last_control_seen_at || '').localeCompare(String(left.last_seen_at || left.last_control_seen_at || '')));
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
replaceC2Sessions(sessions) {
|
|
819
|
+
return this.mutateState((state) => {
|
|
820
|
+
const seen = new Map();
|
|
821
|
+
for (const session of sessions) {
|
|
822
|
+
seen.set(`${session.passport_gaid}:${session.session_id}`, session);
|
|
823
|
+
}
|
|
824
|
+
state.c2_sessions = [...seen.values()].sort((left, right) => String(right.last_seen_at || right.last_control_seen_at || '').localeCompare(String(left.last_seen_at || left.last_control_seen_at || '')));
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
upsertBuildSession(buildSession) {
|
|
828
|
+
return this.mutateState((state) => {
|
|
829
|
+
const index = state.build_sessions.findIndex((item) => item.build_session_id === buildSession.build_session_id);
|
|
830
|
+
if (index >= 0) {
|
|
831
|
+
state.build_sessions[index] = buildSession;
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
state.build_sessions.push(buildSession);
|
|
835
|
+
}
|
|
836
|
+
state.build_sessions.sort((left, right) => right.updated_at.localeCompare(left.updated_at));
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
upsertDetectedAgent(agent) {
|
|
840
|
+
return this.mutateState((state) => {
|
|
841
|
+
const index = state.detected_agents.findIndex((item) => item.agent_id === agent.agent_id);
|
|
842
|
+
if (index >= 0) {
|
|
843
|
+
state.detected_agents[index] = agent;
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
state.detected_agents.push(agent);
|
|
847
|
+
}
|
|
848
|
+
state.detected_agents.sort((left, right) => right.last_seen_at.localeCompare(left.last_seen_at));
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
replaceDetectedAgents(agents) {
|
|
852
|
+
return this.mutateState((state) => {
|
|
853
|
+
const seen = new Map();
|
|
854
|
+
for (const agent of agents) {
|
|
855
|
+
seen.set(agent.agent_id, agent);
|
|
856
|
+
}
|
|
857
|
+
state.detected_agents = [...seen.values()].sort((left, right) => right.last_seen_at.localeCompare(left.last_seen_at));
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
upsertAgentLink(link) {
|
|
861
|
+
return this.mutateState((state) => {
|
|
862
|
+
const index = state.agent_links.findIndex((item) => item.agent_id === link.agent_id && item.discoveryHash === link.discoveryHash);
|
|
863
|
+
if (index >= 0) {
|
|
864
|
+
state.agent_links[index] = link;
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
state.agent_links.push(link);
|
|
868
|
+
}
|
|
869
|
+
state.agent_links.sort((left, right) => right.updated_at.localeCompare(left.updated_at));
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
upsertAgentModelUsage(record) {
|
|
873
|
+
return this.mutateState((state) => {
|
|
874
|
+
const index = state.agent_model_usage.findIndex((item) => item.usage_id === record.usage_id);
|
|
875
|
+
if (index >= 0) {
|
|
876
|
+
state.agent_model_usage[index] = record;
|
|
877
|
+
}
|
|
878
|
+
else {
|
|
879
|
+
state.agent_model_usage.push(record);
|
|
880
|
+
}
|
|
881
|
+
state.agent_model_usage.sort((left, right) => right.last_observed_at.localeCompare(left.last_observed_at));
|
|
882
|
+
if (state.agent_model_usage.length > AGENT_MODEL_USAGE_RETENTION_LIMIT) {
|
|
883
|
+
state.agent_model_usage = state.agent_model_usage.slice(0, AGENT_MODEL_USAGE_RETENTION_LIMIT);
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
replaceAgentModelUsage(records) {
|
|
888
|
+
return this.mutateState((state) => {
|
|
889
|
+
const seen = new Map();
|
|
890
|
+
for (const record of records) {
|
|
891
|
+
seen.set(record.usage_id, record);
|
|
892
|
+
}
|
|
893
|
+
state.agent_model_usage = [...seen.values()]
|
|
894
|
+
.sort((left, right) => right.last_observed_at.localeCompare(left.last_observed_at))
|
|
895
|
+
.slice(0, AGENT_MODEL_USAGE_RETENTION_LIMIT);
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
replaceDetectedModels(models) {
|
|
899
|
+
return this.mutateState((state) => {
|
|
900
|
+
const seen = new Map();
|
|
901
|
+
for (const model of models) {
|
|
902
|
+
const existing = state.detected_models.find((item) => item.discoveryHash === model.discoveryHash);
|
|
903
|
+
seen.set(model.discoveryHash, {
|
|
904
|
+
...model,
|
|
905
|
+
status: model.status === 'ignored' || model.status === 'bound'
|
|
906
|
+
? model.status
|
|
907
|
+
: existing?.status === 'ignored' || existing?.status === 'bound'
|
|
908
|
+
? existing.status
|
|
909
|
+
: model.status,
|
|
910
|
+
review_state: model.review_state ?? existing?.review_state ?? (existing?.status === 'ignored' ? 'ignored' : undefined),
|
|
911
|
+
review_deferred_until: Object.prototype.hasOwnProperty.call(model, 'review_deferred_until')
|
|
912
|
+
? model.review_deferred_until ?? null
|
|
913
|
+
: existing?.review_deferred_until ?? null,
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
state.detected_models = [...seen.values()];
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
replaceEvolutionCandidates(candidates) {
|
|
920
|
+
return this.mutateState((state) => {
|
|
921
|
+
const seen = new Map();
|
|
922
|
+
for (const candidate of candidates) {
|
|
923
|
+
seen.set(candidate.candidate_id, candidate);
|
|
924
|
+
}
|
|
925
|
+
state.evolution_candidates = [...seen.values()].sort((left, right) => right.last_detected_at.localeCompare(left.last_detected_at));
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
upsertModelBinding(binding) {
|
|
929
|
+
return this.mutateState((state) => {
|
|
930
|
+
const index = state.model_bindings.findIndex((item) => item.modelKey === binding.modelKey);
|
|
931
|
+
if (index >= 0) {
|
|
932
|
+
state.model_bindings[index] = binding;
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
state.model_bindings.push(binding);
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
replaceModelBindings(bindings) {
|
|
940
|
+
return this.mutateState((state) => {
|
|
941
|
+
const seen = new Map();
|
|
942
|
+
for (const binding of bindings) {
|
|
943
|
+
seen.set(binding.modelKey, binding);
|
|
944
|
+
}
|
|
945
|
+
state.model_bindings = [...seen.values()];
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
recordDeliveredNotification(notification) {
|
|
949
|
+
return this.mutateState((state) => {
|
|
950
|
+
const index = state.delivered_notifications.findIndex((item) => item.notification_type === notification.notification_type && item.target_id === notification.target_id);
|
|
951
|
+
if (index >= 0) {
|
|
952
|
+
state.delivered_notifications[index] = notification;
|
|
953
|
+
}
|
|
954
|
+
else {
|
|
955
|
+
state.delivered_notifications.push(notification);
|
|
956
|
+
}
|
|
957
|
+
state.delivered_notifications.sort((left, right) => right.delivered_at.localeCompare(left.delivered_at));
|
|
958
|
+
if (state.delivered_notifications.length > DELIVERED_NOTIFICATION_RETENTION_LIMIT) {
|
|
959
|
+
state.delivered_notifications = state.delivered_notifications.slice(0, DELIVERED_NOTIFICATION_RETENTION_LIMIT);
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
addEvidenceEvent(event) {
|
|
964
|
+
return this.mutateState((state) => {
|
|
965
|
+
const nextEvent = {
|
|
966
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
967
|
+
createdAt: nowIso(),
|
|
968
|
+
...event,
|
|
969
|
+
};
|
|
970
|
+
state.evidence_events.push(nextEvent);
|
|
971
|
+
if (state.evidence_events.length > 5000) {
|
|
972
|
+
state.evidence_events = state.evidence_events.slice(-5000);
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
enqueue(item) {
|
|
977
|
+
const state = this.ensureInitialized();
|
|
978
|
+
const queued = {
|
|
979
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
980
|
+
attempts: 0,
|
|
981
|
+
createdAt: nowIso(),
|
|
982
|
+
nextRetryAt: nowIso(),
|
|
983
|
+
lastError: null,
|
|
984
|
+
...item,
|
|
985
|
+
};
|
|
986
|
+
state.sync_queue.push(queued);
|
|
987
|
+
this.writeState(state);
|
|
988
|
+
return queued;
|
|
989
|
+
}
|
|
990
|
+
trimQueueItemsByType(type, keepLatest) {
|
|
991
|
+
const state = this.ensureInitialized();
|
|
992
|
+
const safeKeepLatest = Math.max(0, Math.floor(keepLatest));
|
|
993
|
+
const matchingIndexes = state.sync_queue
|
|
994
|
+
.map((item, index) => ({ item, index }))
|
|
995
|
+
.filter((entry) => entry.item.type === type)
|
|
996
|
+
.map((entry) => entry.index);
|
|
997
|
+
const overflow = matchingIndexes.length - safeKeepLatest;
|
|
998
|
+
if (overflow <= 0) {
|
|
999
|
+
return 0;
|
|
1000
|
+
}
|
|
1001
|
+
const indexesToRemove = new Set(matchingIndexes.slice(0, overflow));
|
|
1002
|
+
state.sync_queue = state.sync_queue.filter((_item, index) => !indexesToRemove.has(index));
|
|
1003
|
+
this.writeState(state);
|
|
1004
|
+
return overflow;
|
|
1005
|
+
}
|
|
1006
|
+
listReadyQueueItems(referenceTime = new Date()) {
|
|
1007
|
+
const state = this.ensureInitialized();
|
|
1008
|
+
return state.sync_queue.filter((item) => new Date(item.nextRetryAt).getTime() <= referenceTime.getTime());
|
|
1009
|
+
}
|
|
1010
|
+
hasPendingPassportDraftForDiscoveryHash(discoveryHash, registrationKey) {
|
|
1011
|
+
const state = this.ensureInitialized();
|
|
1012
|
+
return state.sync_queue.some((item) => {
|
|
1013
|
+
if (item.type !== 'passport-draft')
|
|
1014
|
+
return false;
|
|
1015
|
+
const metadata = item.payload?.metadata;
|
|
1016
|
+
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata))
|
|
1017
|
+
return false;
|
|
1018
|
+
const record = metadata;
|
|
1019
|
+
const queuedDiscoveryHash = String(record.discoveryHash || '').trim();
|
|
1020
|
+
const queuedRegistrationKey = String(record.registrationKey || '').trim();
|
|
1021
|
+
return queuedDiscoveryHash === discoveryHash
|
|
1022
|
+
|| (!!registrationKey && queuedRegistrationKey === registrationKey);
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
markQueueItemResult(id, ok, errorMessage) {
|
|
1026
|
+
const state = this.ensureInitialized();
|
|
1027
|
+
const index = state.sync_queue.findIndex((item) => item.id === id);
|
|
1028
|
+
if (index < 0)
|
|
1029
|
+
return;
|
|
1030
|
+
if (ok) {
|
|
1031
|
+
state.sync_queue.splice(index, 1);
|
|
1032
|
+
this.writeState(state);
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
const item = state.sync_queue[index];
|
|
1036
|
+
if (!item) {
|
|
1037
|
+
this.writeState(state);
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
const attempts = item.attempts + 1;
|
|
1041
|
+
const backoffMs = Math.min(60_000, 2 ** Math.min(10, attempts) * 1000);
|
|
1042
|
+
state.sync_queue[index] = {
|
|
1043
|
+
...item,
|
|
1044
|
+
attempts,
|
|
1045
|
+
nextRetryAt: new Date(Date.now() + backoffMs).toISOString(),
|
|
1046
|
+
lastError: errorMessage ?? 'sync_failed',
|
|
1047
|
+
};
|
|
1048
|
+
this.writeState(state);
|
|
1049
|
+
}
|
|
1050
|
+
// ─── Lifecycle Monitor ─────────────────────────────────────────────────────
|
|
1051
|
+
upsertPendingReview(review) {
|
|
1052
|
+
return this.mutateState((state) => {
|
|
1053
|
+
const index = state.pending_reviews.findIndex((r) => r.review_id === review.review_id);
|
|
1054
|
+
if (index >= 0) {
|
|
1055
|
+
state.pending_reviews[index] = review;
|
|
1056
|
+
}
|
|
1057
|
+
else {
|
|
1058
|
+
state.pending_reviews.push(review);
|
|
1059
|
+
}
|
|
1060
|
+
// Keep at most 500 reviews (oldest resolved first)
|
|
1061
|
+
if (state.pending_reviews.length > 500) {
|
|
1062
|
+
const resolved = state.pending_reviews.filter((r) => r.status !== 'pending');
|
|
1063
|
+
const pending = state.pending_reviews.filter((r) => r.status === 'pending');
|
|
1064
|
+
state.pending_reviews = [...resolved.slice(-250), ...pending].slice(-500);
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
resolvePendingReview(reviewId, status) {
|
|
1069
|
+
return this.mutateState((state) => {
|
|
1070
|
+
const review = state.pending_reviews.find((r) => r.review_id === reviewId);
|
|
1071
|
+
if (review) {
|
|
1072
|
+
review.status = status;
|
|
1073
|
+
review.reviewed_at = new Date().toISOString();
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
upsertLifecycleMonitorRecord(record) {
|
|
1078
|
+
return this.mutateState((state) => {
|
|
1079
|
+
const index = state.lifecycle_monitor_records.findIndex((r) => r.subject_id === record.subject_id && r.subject_type === record.subject_type);
|
|
1080
|
+
if (index >= 0) {
|
|
1081
|
+
state.lifecycle_monitor_records[index] = record;
|
|
1082
|
+
}
|
|
1083
|
+
else {
|
|
1084
|
+
state.lifecycle_monitor_records.push(record);
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
readIdentityPrivateKey() {
|
|
1089
|
+
if (!node_fs_1.default.existsSync(this.paths.identityPrivateKeyFile)) {
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
return node_fs_1.default.readFileSync(this.paths.identityPrivateKeyFile, 'utf8');
|
|
1093
|
+
}
|
|
1094
|
+
writeIdentityPrivateKey(privateKeyPem) {
|
|
1095
|
+
this.ensureStateDirectory();
|
|
1096
|
+
node_fs_1.default.writeFileSync(this.paths.identityPrivateKeyFile, privateKeyPem, { mode: 0o600 });
|
|
1097
|
+
}
|
|
1098
|
+
retireLegacyStateFile() {
|
|
1099
|
+
if (!node_fs_1.default.existsSync(this.paths.legacyStateFile))
|
|
1100
|
+
return;
|
|
1101
|
+
node_fs_1.default.rmSync(this.paths.legacyStateFile, { force: true });
|
|
1102
|
+
}
|
|
1103
|
+
mutateState(mutator) {
|
|
1104
|
+
const state = this.ensureInitialized();
|
|
1105
|
+
mutator(state);
|
|
1106
|
+
const normalized = normalizeState(state);
|
|
1107
|
+
this.writeState(normalized);
|
|
1108
|
+
return normalized;
|
|
1109
|
+
}
|
|
1110
|
+
ensureInitializedInternal() {
|
|
1111
|
+
this.withDatabase((db) => {
|
|
1112
|
+
const existing = this.loadRawStateRecord(db);
|
|
1113
|
+
if (existing)
|
|
1114
|
+
return;
|
|
1115
|
+
const legacy = this.readLegacyStateFile();
|
|
1116
|
+
if (legacy) {
|
|
1117
|
+
this.persistState(db, normalizeState(legacy));
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
this.persistState(db, buildInitialState());
|
|
1121
|
+
}, true);
|
|
1122
|
+
}
|
|
1123
|
+
readStateFromDatabase() {
|
|
1124
|
+
return this.withDatabase((db) => {
|
|
1125
|
+
const raw = this.loadRawStateRecord(db);
|
|
1126
|
+
if (!raw) {
|
|
1127
|
+
const initial = buildInitialState();
|
|
1128
|
+
this.persistState(db, initial);
|
|
1129
|
+
return initial;
|
|
1130
|
+
}
|
|
1131
|
+
return normalizeState(raw);
|
|
1132
|
+
}, true);
|
|
1133
|
+
}
|
|
1134
|
+
withDatabase(operation, allowRecovery) {
|
|
1135
|
+
try {
|
|
1136
|
+
const db = this.getDb();
|
|
1137
|
+
return operation(db);
|
|
1138
|
+
}
|
|
1139
|
+
catch (error) {
|
|
1140
|
+
if (allowRecovery && this.isRecoverableDatabaseError(error)) {
|
|
1141
|
+
this.recoverDatabaseFiles();
|
|
1142
|
+
return this.withDatabase(operation, false);
|
|
1143
|
+
}
|
|
1144
|
+
throw error;
|
|
1145
|
+
}
|
|
1146
|
+
finally {
|
|
1147
|
+
this.tightenStatePathPermissions();
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
getDb() {
|
|
1151
|
+
if (this.db) {
|
|
1152
|
+
return this.db;
|
|
1153
|
+
}
|
|
1154
|
+
this.ensureStateDirectory();
|
|
1155
|
+
const db = new better_sqlite3_1.default(this.paths.stateFile);
|
|
1156
|
+
try {
|
|
1157
|
+
db.pragma('journal_mode = WAL');
|
|
1158
|
+
db.pragma('synchronous = NORMAL');
|
|
1159
|
+
db.pragma('foreign_keys = ON');
|
|
1160
|
+
db.pragma('busy_timeout = 5000');
|
|
1161
|
+
db.exec(`
|
|
1162
|
+
CREATE TABLE IF NOT EXISTS connect_state_meta (
|
|
1163
|
+
key TEXT PRIMARY KEY,
|
|
1164
|
+
value TEXT NOT NULL,
|
|
1165
|
+
updated_at TEXT NOT NULL
|
|
1166
|
+
);
|
|
1167
|
+
|
|
1168
|
+
CREATE TABLE IF NOT EXISTS connect_state_sections (
|
|
1169
|
+
section_name TEXT PRIMARY KEY,
|
|
1170
|
+
payload_json TEXT NOT NULL,
|
|
1171
|
+
updated_at TEXT NOT NULL
|
|
1172
|
+
);
|
|
1173
|
+
|
|
1174
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
1175
|
+
version INTEGER PRIMARY KEY,
|
|
1176
|
+
applied_at TEXT NOT NULL
|
|
1177
|
+
);
|
|
1178
|
+
`);
|
|
1179
|
+
const appliedAt = nowIso();
|
|
1180
|
+
db.prepare(`
|
|
1181
|
+
INSERT OR IGNORE INTO schema_migrations (version, applied_at)
|
|
1182
|
+
VALUES (?, ?)
|
|
1183
|
+
`).run(STATE_SCHEMA_VERSION, appliedAt);
|
|
1184
|
+
db.prepare(`
|
|
1185
|
+
INSERT INTO connect_state_meta (key, value, updated_at)
|
|
1186
|
+
VALUES (?, ?, ?)
|
|
1187
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
1188
|
+
value = excluded.value,
|
|
1189
|
+
updated_at = excluded.updated_at
|
|
1190
|
+
`).run('schema_version', String(STATE_SCHEMA_VERSION), appliedAt);
|
|
1191
|
+
}
|
|
1192
|
+
catch (error) {
|
|
1193
|
+
db.close();
|
|
1194
|
+
throw error;
|
|
1195
|
+
}
|
|
1196
|
+
this.db = db;
|
|
1197
|
+
this.tightenStatePathPermissions();
|
|
1198
|
+
return db;
|
|
1199
|
+
}
|
|
1200
|
+
persistState(db, state) {
|
|
1201
|
+
const normalized = normalizeState(state);
|
|
1202
|
+
const updatedAt = nowIso();
|
|
1203
|
+
const upsertSection = db.prepare(`
|
|
1204
|
+
INSERT INTO connect_state_sections (section_name, payload_json, updated_at)
|
|
1205
|
+
VALUES (?, ?, ?)
|
|
1206
|
+
ON CONFLICT(section_name) DO UPDATE SET
|
|
1207
|
+
payload_json = excluded.payload_json,
|
|
1208
|
+
updated_at = excluded.updated_at
|
|
1209
|
+
`);
|
|
1210
|
+
const upsertMeta = db.prepare(`
|
|
1211
|
+
INSERT INTO connect_state_meta (key, value, updated_at)
|
|
1212
|
+
VALUES (?, ?, ?)
|
|
1213
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
1214
|
+
value = excluded.value,
|
|
1215
|
+
updated_at = excluded.updated_at
|
|
1216
|
+
`);
|
|
1217
|
+
const persist = db.transaction((nextState) => {
|
|
1218
|
+
for (const sectionName of STATE_SECTION_NAMES) {
|
|
1219
|
+
upsertSection.run(sectionName, JSON.stringify(nextState[sectionName] ?? null), updatedAt);
|
|
1220
|
+
}
|
|
1221
|
+
upsertMeta.run('last_state_write_at', updatedAt, updatedAt);
|
|
1222
|
+
});
|
|
1223
|
+
persist(normalized);
|
|
1224
|
+
}
|
|
1225
|
+
loadRawStateRecord(db) {
|
|
1226
|
+
const rows = db.prepare(`
|
|
1227
|
+
SELECT section_name, payload_json
|
|
1228
|
+
FROM connect_state_sections
|
|
1229
|
+
`).all();
|
|
1230
|
+
if (rows.length === 0) {
|
|
1231
|
+
return null;
|
|
1232
|
+
}
|
|
1233
|
+
const record = {};
|
|
1234
|
+
for (const row of rows) {
|
|
1235
|
+
try {
|
|
1236
|
+
record[row.section_name] = JSON.parse(row.payload_json);
|
|
1237
|
+
}
|
|
1238
|
+
catch (error) {
|
|
1239
|
+
throw new Error(`invalid_state_payload:${row.section_name}:${error instanceof Error ? error.message : 'parse_failed'}`);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return record;
|
|
1243
|
+
}
|
|
1244
|
+
readLegacyStateFile() {
|
|
1245
|
+
if (!node_fs_1.default.existsSync(this.paths.legacyStateFile)) {
|
|
1246
|
+
return null;
|
|
1247
|
+
}
|
|
1248
|
+
return readJsonFile(this.paths.legacyStateFile);
|
|
1249
|
+
}
|
|
1250
|
+
ensureStateDirectory() {
|
|
1251
|
+
if (!node_fs_1.default.existsSync(this.paths.stateDir)) {
|
|
1252
|
+
node_fs_1.default.mkdirSync(this.paths.stateDir, { recursive: true, mode: 0o700 });
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
tightenStatePathPermissions() {
|
|
1256
|
+
const paths = [
|
|
1257
|
+
this.paths.stateFile,
|
|
1258
|
+
`${this.paths.stateFile}-wal`,
|
|
1259
|
+
`${this.paths.stateFile}-shm`,
|
|
1260
|
+
this.paths.identityPrivateKeyFile,
|
|
1261
|
+
];
|
|
1262
|
+
for (const filePath of paths) {
|
|
1263
|
+
if (!node_fs_1.default.existsSync(filePath))
|
|
1264
|
+
continue;
|
|
1265
|
+
try {
|
|
1266
|
+
node_fs_1.default.chmodSync(filePath, 0o600);
|
|
1267
|
+
}
|
|
1268
|
+
catch {
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
recoverDatabaseFiles() {
|
|
1274
|
+
if (this.db) {
|
|
1275
|
+
try {
|
|
1276
|
+
this.db.close();
|
|
1277
|
+
}
|
|
1278
|
+
catch {
|
|
1279
|
+
// ignore close errors during recovery
|
|
1280
|
+
}
|
|
1281
|
+
this.db = null;
|
|
1282
|
+
}
|
|
1283
|
+
const suffix = `.corrupt-${Date.now()}`;
|
|
1284
|
+
const databaseSidecars = [
|
|
1285
|
+
this.paths.stateFile,
|
|
1286
|
+
`${this.paths.stateFile}-wal`,
|
|
1287
|
+
`${this.paths.stateFile}-shm`,
|
|
1288
|
+
];
|
|
1289
|
+
for (const filePath of databaseSidecars) {
|
|
1290
|
+
if (!node_fs_1.default.existsSync(filePath))
|
|
1291
|
+
continue;
|
|
1292
|
+
try {
|
|
1293
|
+
node_fs_1.default.renameSync(filePath, `${filePath}${suffix}`);
|
|
1294
|
+
}
|
|
1295
|
+
catch {
|
|
1296
|
+
try {
|
|
1297
|
+
node_fs_1.default.rmSync(filePath, { force: true });
|
|
1298
|
+
}
|
|
1299
|
+
catch {
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
isRecoverableDatabaseError(error) {
|
|
1306
|
+
if (!(error instanceof Error))
|
|
1307
|
+
return false;
|
|
1308
|
+
const message = error.message.toLowerCase();
|
|
1309
|
+
return message.includes('sqlite_corrupt')
|
|
1310
|
+
|| message.includes('database disk image is malformed')
|
|
1311
|
+
|| message.includes('file is not a database')
|
|
1312
|
+
|| message.includes('not a database')
|
|
1313
|
+
|| message.includes('malformed')
|
|
1314
|
+
|| message.includes('invalid_state_payload:');
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
exports.LocalStateStore = LocalStateStore;
|
|
1318
|
+
//# sourceMappingURL=state.js.map
|