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.
Files changed (51) hide show
  1. package/QUICKSTART.md +55 -0
  2. package/README.md +96 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.js +4724 -0
  5. package/dist/index.d.ts +7 -0
  6. package/dist/index.js +21 -0
  7. package/dist/launcher.d.ts +33 -0
  8. package/dist/launcher.js +9344 -0
  9. package/dist/ps-list-loader.d.ts +5 -0
  10. package/dist/ps-list-loader.js +20 -0
  11. package/dist/v1/agent-observation.d.ts +42 -0
  12. package/dist/v1/agent-observation.js +499 -0
  13. package/dist/v1/api.d.ts +276 -0
  14. package/dist/v1/api.js +390 -0
  15. package/dist/v1/credential-store.d.ts +92 -0
  16. package/dist/v1/credential-store.js +797 -0
  17. package/dist/v1/currency.d.ts +41 -0
  18. package/dist/v1/currency.js +127 -0
  19. package/dist/v1/daemon.d.ts +50 -0
  20. package/dist/v1/daemon.js +265 -0
  21. package/dist/v1/discovery.d.ts +61 -0
  22. package/dist/v1/discovery.js +168 -0
  23. package/dist/v1/filesystem-models.d.ts +11 -0
  24. package/dist/v1/filesystem-models.js +261 -0
  25. package/dist/v1/heartbeat.d.ts +45 -0
  26. package/dist/v1/heartbeat.js +463 -0
  27. package/dist/v1/lifecycle-monitor.d.ts +78 -0
  28. package/dist/v1/lifecycle-monitor.js +512 -0
  29. package/dist/v1/lmstudio.d.ts +11 -0
  30. package/dist/v1/lmstudio.js +148 -0
  31. package/dist/v1/ollama.d.ts +19 -0
  32. package/dist/v1/ollama.js +164 -0
  33. package/dist/v1/openai-compatible.d.ts +12 -0
  34. package/dist/v1/openai-compatible.js +124 -0
  35. package/dist/v1/process-scout.d.ts +50 -0
  36. package/dist/v1/process-scout.js +715 -0
  37. package/dist/v1/providers.d.ts +50 -0
  38. package/dist/v1/providers.js +106 -0
  39. package/dist/v1/service.d.ts +680 -0
  40. package/dist/v1/service.js +8286 -0
  41. package/dist/v1/state.d.ts +87 -0
  42. package/dist/v1/state.js +1318 -0
  43. package/dist/v1/test-credential-backend.d.ts +19 -0
  44. package/dist/v1/test-credential-backend.js +49 -0
  45. package/dist/v1/types.d.ts +873 -0
  46. package/dist/v1/types.js +3 -0
  47. package/dist/v1/update.d.ts +38 -0
  48. package/dist/v1/update.js +184 -0
  49. package/dist/v1/vitality-pulse.d.ts +36 -0
  50. package/dist/v1/vitality-pulse.js +512 -0
  51. package/package.json +53 -0
@@ -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