@origintrail-official/dkg-node-ui 10.0.13 → 10.0.14

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/dist/db.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import Database from 'better-sqlite3';
2
2
  import { join } from 'node:path';
3
- import { RESPONSE_CACHE_BYTES, parseContextGraphJoinPolicyRecord, } from '@origintrail-official/dkg-core';
3
+ import { RESPONSE_CACHE_BYTES, parseContextGraphJoinPolicyRecord, DEFAULT_SYNC_CHECKPOINT_TTL_MS, isValidSyncCheckpointEntry, transitionSyncCheckpointManifestOffset, transitionSyncCheckpointOffset, transitionSyncCheckpointResponderSession, withoutSyncCheckpointResponderSession, } from '@origintrail-official/dkg-core';
4
4
  export { SqliteChainEventCursorStore, SqliteContextGraphRegistryScanCursorStore, } from './chain-cursor-stores.js';
5
- const SCHEMA_VERSION = 31;
5
+ export const SCHEMA_VERSION = 34;
6
6
  // Default operator retention. Lowered from 90 → 14 days on V15 (2026-05) after
7
7
  // a production incident in which the `logs` table + its FTS5 shadow tables
8
8
  // grew to ~9 GB on a 12-day-old node and corrupted the SQLite page (header
@@ -172,7 +172,7 @@ export class DashboardDB {
172
172
  );
173
173
  `);
174
174
  };
175
- const ensureSyncCheckpointResponderSessionColumns = () => {
175
+ const ensureSyncCheckpointResumeColumns = () => {
176
176
  const table = this.db.prepare(`
177
177
  SELECT 1 AS found FROM sqlite_master
178
178
  WHERE type = 'table' AND name = 'sync_checkpoints'
@@ -187,6 +187,18 @@ export class DashboardDB {
187
187
  if (!columns.has('responder_session_expires_at')) {
188
188
  this.db.exec(`ALTER TABLE sync_checkpoints ADD COLUMN responder_session_expires_at INTEGER;`);
189
189
  }
190
+ if (!columns.has('responder_session_offset')) {
191
+ this.db.exec(`ALTER TABLE sync_checkpoints ADD COLUMN responder_session_offset INTEGER;`);
192
+ }
193
+ if (!columns.has('manifest_digest')) {
194
+ this.db.exec(`ALTER TABLE sync_checkpoints ADD COLUMN manifest_digest TEXT;`);
195
+ }
196
+ if (!columns.has('manifest_prefix_digest')) {
197
+ this.db.exec(`ALTER TABLE sync_checkpoints ADD COLUMN manifest_prefix_digest TEXT;`);
198
+ }
199
+ if (!columns.has('terminal')) {
200
+ this.db.exec(`ALTER TABLE sync_checkpoints ADD COLUMN terminal INTEGER NOT NULL DEFAULT 0 CHECK (terminal IN (0, 1));`);
201
+ }
190
202
  };
191
203
  if (version > SCHEMA_VERSION)
192
204
  return;
@@ -194,7 +206,7 @@ export class DashboardDB {
194
206
  // Repair restored/development databases that carry the current version
195
207
  // but lost an idempotent schema adjunct.
196
208
  ensureJoinApprovalRepairMarker();
197
- ensureSyncCheckpointResponderSessionColumns();
209
+ ensureSyncCheckpointResumeColumns();
198
210
  ensureJoinPolicyAuditCapTrigger();
199
211
  return;
200
212
  }
@@ -789,7 +801,13 @@ export class DashboardDB {
789
801
  key TEXT PRIMARY KEY,
790
802
  offset INTEGER NOT NULL CHECK (offset >= 0),
791
803
  updated_at INTEGER NOT NULL,
792
- expires_at INTEGER NOT NULL
804
+ expires_at INTEGER NOT NULL,
805
+ responder_session_id TEXT,
806
+ responder_session_expires_at INTEGER,
807
+ responder_session_offset INTEGER CHECK (responder_session_offset >= 0),
808
+ manifest_digest TEXT,
809
+ manifest_prefix_digest TEXT,
810
+ terminal INTEGER NOT NULL DEFAULT 0 CHECK (terminal IN (0, 1))
793
811
  );
794
812
  CREATE INDEX IF NOT EXISTS idx_sync_checkpoints_expires_at
795
813
  ON sync_checkpoints(expires_at);
@@ -1023,7 +1041,7 @@ export class DashboardDB {
1023
1041
  // OFFSET pagination is scoped to an immutable responder row list. Keep
1024
1042
  // the opaque responder token beside the verified offset so a requester
1025
1043
  // restart can resume that same list instead of discarding durable work.
1026
- ensureSyncCheckpointResponderSessionColumns();
1044
+ ensureSyncCheckpointResumeColumns();
1027
1045
  }
1028
1046
  if (version < 31) {
1029
1047
  this.db.exec(`
@@ -1042,6 +1060,41 @@ export class DashboardDB {
1042
1060
  CHECK (length(checksum) > 0)
1043
1061
  );
1044
1062
  `);
1063
+ }
1064
+ if (version < 32) {
1065
+ this.db.exec(`
1066
+ CREATE TABLE IF NOT EXISTS selected_vm_reconcile_cursors (
1067
+ deployment_id TEXT NOT NULL CHECK (length(trim(deployment_id)) > 0),
1068
+ context_graph_id TEXT NOT NULL,
1069
+ on_chain_context_graph_id TEXT NOT NULL,
1070
+ name_hash TEXT NOT NULL,
1071
+ watermark INTEGER NOT NULL CHECK (watermark >= 0),
1072
+ updated_at INTEGER NOT NULL,
1073
+ PRIMARY KEY (deployment_id, context_graph_id, on_chain_context_graph_id)
1074
+ );
1075
+ CREATE INDEX IF NOT EXISTS idx_selected_vm_reconcile_cursor_cg
1076
+ ON selected_vm_reconcile_cursors(deployment_id, context_graph_id);
1077
+ `);
1078
+ }
1079
+ if (version < 33) {
1080
+ // A durable DATA offset is reusable only with cryptographic proof of the
1081
+ // canonical META generation and verified descriptor prefix that gave it
1082
+ // meaning. Persist both digests beside the offset/session so production
1083
+ // SQLite checkpoints have the same safety contract as the in-memory
1084
+ // requester store and survive daemon restarts.
1085
+ ensureSyncCheckpointResumeColumns();
1086
+ }
1087
+ if (version < 34) {
1088
+ // The V33 manifest-bound continuation must not share a key with V32,
1089
+ // which interpreted OFFSET as an unverified raw responder coordinate.
1090
+ // Drop every pre-versioned durable DATA row before new code starts using
1091
+ // the `|checkpoint:v2` namespace, so a later rollback also fails closed.
1092
+ ensureSyncCheckpointResumeColumns();
1093
+ this.db.prepare(`
1094
+ DELETE FROM sync_checkpoints
1095
+ WHERE key LIKE '%|durable|data%'
1096
+ AND key NOT LIKE '%|durable|data|checkpoint:v2%'
1097
+ `).run();
1045
1098
  }
1046
1099
  this.db.pragma(`user_version = ${SCHEMA_VERSION}`);
1047
1100
  if (upgradedExistingDb && !this.explicitRetentionDays) {
@@ -1640,6 +1693,29 @@ export class DashboardDB {
1640
1693
  deleteVmReconcileNegativesForContextGraph(contextGraphId) {
1641
1694
  this.stmt('deleteVmReconcileNegativesForContextGraph', 'DELETE FROM vm_reconcile_negative_cache WHERE context_graph_id = ?').run(contextGraphId);
1642
1695
  }
1696
+ upsertSelectedVmReconcileCursor(record) {
1697
+ this.stmt('upsertSelectedVmReconcileCursor', `
1698
+ INSERT INTO selected_vm_reconcile_cursors (
1699
+ deployment_id, context_graph_id, on_chain_context_graph_id,
1700
+ name_hash, watermark, updated_at
1701
+ ) VALUES (
1702
+ @deployment_id, @context_graph_id, @on_chain_context_graph_id,
1703
+ @name_hash, @watermark, @updated_at
1704
+ )
1705
+ ON CONFLICT(deployment_id, context_graph_id, on_chain_context_graph_id) DO UPDATE SET
1706
+ name_hash = excluded.name_hash,
1707
+ watermark = excluded.watermark,
1708
+ updated_at = excluded.updated_at
1709
+ `).run(record);
1710
+ }
1711
+ getSelectedVmReconcileCursor(deploymentId, contextGraphId, onChainContextGraphId) {
1712
+ return this.stmt('getSelectedVmReconcileCursor', `
1713
+ SELECT * FROM selected_vm_reconcile_cursors
1714
+ WHERE deployment_id = ?
1715
+ AND context_graph_id = ?
1716
+ AND on_chain_context_graph_id = ?
1717
+ `).get(deploymentId, contextGraphId, onChainContextGraphId);
1718
+ }
1643
1719
  // --- Phase F: chain-driven VM reconciliation telemetry ---
1644
1720
  /**
1645
1721
  * Persist one {@link ReplicationEvent} (best-effort sink target). `ordinal`,
@@ -2691,7 +2767,6 @@ export class DashboardDB {
2691
2767
  }
2692
2768
  }
2693
2769
  // --- Sync requester checkpoints (issue #1138 A3) ---
2694
- const DEFAULT_SYNC_CHECKPOINT_TTL_MS = 24 * 60 * 60 * 1000;
2695
2770
  export class SqliteSyncCheckpointStore {
2696
2771
  db;
2697
2772
  clock;
@@ -2701,73 +2776,128 @@ export class SqliteSyncCheckpointStore {
2701
2776
  this.clock = options.clock ?? (() => Date.now());
2702
2777
  this.ttlMs = options.ttlMs ?? DEFAULT_SYNC_CHECKPOINT_TTL_MS;
2703
2778
  }
2704
- get(key) {
2705
- const now = this.clock();
2779
+ readRow(key) {
2706
2780
  const row = this.db.prepare(`SELECT offset, updated_at, expires_at,
2707
- responder_session_id, responder_session_expires_at
2781
+ responder_session_id, responder_session_expires_at, responder_session_offset,
2782
+ manifest_digest, manifest_prefix_digest, terminal
2708
2783
  FROM sync_checkpoints WHERE key = ?`).get(key);
2709
2784
  if (!row)
2710
2785
  return undefined;
2711
- if (row.expires_at < now) {
2712
- this.delete(key);
2713
- return undefined;
2714
- }
2715
- const hasFreshResponderSession = Boolean(row.responder_session_id)
2716
- && Number.isSafeInteger(row.responder_session_expires_at)
2717
- && (row.responder_session_expires_at ?? 0) > now;
2718
- if (row.responder_session_id && !hasFreshResponderSession) {
2719
- this.clearResponderSession(key);
2720
- }
2721
2786
  return {
2722
2787
  offset: row.offset,
2723
2788
  updatedAtMs: row.updated_at,
2724
2789
  expiresAtMs: row.expires_at,
2725
- ...(hasFreshResponderSession
2726
- ? {
2727
- responderSessionId: row.responder_session_id,
2728
- responderSessionExpiresAtMs: row.responder_session_expires_at,
2729
- }
2790
+ ...(row.terminal === 1 ? { terminal: true } : {}),
2791
+ ...(row.manifest_digest ? { manifestDigest: row.manifest_digest } : {}),
2792
+ ...(row.manifest_prefix_digest
2793
+ ? { manifestPrefixDigest: row.manifest_prefix_digest }
2794
+ : {}),
2795
+ // Preserve each nullable session column independently so the shared
2796
+ // validator can distinguish an absent session from a torn/malformed
2797
+ // persisted session. Collapsing a partial row to no session fields would
2798
+ // turn corrupt durable state into an apparently valid ordinary offset.
2799
+ ...(row.responder_session_id !== null
2800
+ ? { responderSessionId: row.responder_session_id }
2801
+ : {}),
2802
+ ...(row.responder_session_expires_at !== null
2803
+ ? { responderSessionExpiresAtMs: row.responder_session_expires_at }
2804
+ : {}),
2805
+ ...(row.responder_session_offset !== null
2806
+ ? { responderSessionOffset: row.responder_session_offset }
2730
2807
  : {}),
2731
2808
  };
2732
2809
  }
2733
- set(key, value, nowMs = this.clock()) {
2734
- if (!Number.isSafeInteger(value) || value < 0) {
2735
- throw new Error(`Invalid sync checkpoint offset for ${key}: ${value}`);
2810
+ get(key, now = this.clock()) {
2811
+ const entry = this.readRow(key);
2812
+ if (!entry)
2813
+ return undefined;
2814
+ if (!isValidSyncCheckpointEntry(entry) || entry.expiresAtMs < now) {
2815
+ this.delete(key);
2816
+ return undefined;
2817
+ }
2818
+ if (entry.responderSessionId
2819
+ && (entry.responderSessionExpiresAtMs ?? 0) <= now) {
2820
+ const withoutExpiredSession = withoutSyncCheckpointResponderSession(entry);
2821
+ this.writeEntry(key, withoutExpiredSession);
2822
+ return withoutExpiredSession;
2736
2823
  }
2824
+ return entry;
2825
+ }
2826
+ writeEntry(key, entry) {
2737
2827
  this.db.prepare(`
2738
- INSERT INTO sync_checkpoints (key, offset, updated_at, expires_at)
2739
- VALUES (?, ?, ?, ?)
2828
+ INSERT INTO sync_checkpoints (
2829
+ key, offset, updated_at, expires_at,
2830
+ responder_session_id, responder_session_expires_at, responder_session_offset,
2831
+ manifest_digest, manifest_prefix_digest, terminal
2832
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2740
2833
  ON CONFLICT(key) DO UPDATE SET
2741
2834
  offset = excluded.offset,
2742
2835
  updated_at = excluded.updated_at,
2743
- expires_at = excluded.expires_at
2744
- `).run(key, value, nowMs, nowMs + this.ttlMs);
2836
+ expires_at = excluded.expires_at,
2837
+ responder_session_id = excluded.responder_session_id,
2838
+ responder_session_expires_at = excluded.responder_session_expires_at,
2839
+ responder_session_offset = excluded.responder_session_offset,
2840
+ manifest_digest = excluded.manifest_digest,
2841
+ manifest_prefix_digest = excluded.manifest_prefix_digest,
2842
+ terminal = excluded.terminal
2843
+ `).run(key, entry.offset, entry.updatedAtMs, entry.expiresAtMs, entry.responderSessionId ?? null, entry.responderSessionExpiresAtMs ?? null, entry.responderSessionOffset ?? null, entry.manifestDigest ?? null, entry.manifestPrefixDigest ?? null, entry.terminal ? 1 : 0);
2844
+ }
2845
+ set(key, value, nowMs = this.clock(), responderSessionOffset) {
2846
+ const transition = this.db.transaction(() => {
2847
+ this.writeEntry(key, transitionSyncCheckpointOffset({
2848
+ key,
2849
+ existing: this.get(key, nowMs),
2850
+ value,
2851
+ nowMs,
2852
+ ttlMs: this.ttlMs,
2853
+ responderSessionOffset,
2854
+ }));
2855
+ });
2856
+ transition();
2745
2857
  }
2746
- setResponderSession(key, sessionId, expiresAtMs, nowMs = this.clock()) {
2747
- if (!sessionId || !Number.isSafeInteger(expiresAtMs)) {
2748
- throw new Error(`Invalid sync responder session for ${key}`);
2749
- }
2858
+ setManifestBoundOffset(key, value, manifestDigest, nowMs = this.clock(), manifestPrefixDigest, terminal = false, responderSessionOffset) {
2859
+ const transition = this.db.transaction(() => {
2860
+ this.writeEntry(key, transitionSyncCheckpointManifestOffset({
2861
+ key,
2862
+ existing: this.get(key, nowMs),
2863
+ value,
2864
+ manifestDigest,
2865
+ nowMs,
2866
+ ttlMs: this.ttlMs,
2867
+ manifestPrefixDigest,
2868
+ terminal,
2869
+ responderSessionOffset,
2870
+ }));
2871
+ });
2872
+ transition();
2873
+ }
2874
+ setResponderSession(key, sessionId, expiresAtMs, nowMs = this.clock(), manifestDigest, manifestPrefixDigest, responderSessionOffset) {
2750
2875
  if (expiresAtMs <= nowMs) {
2751
2876
  this.clearResponderSession(key);
2752
2877
  return;
2753
2878
  }
2754
- this.db.prepare(`
2755
- INSERT INTO sync_checkpoints (
2756
- key, offset, updated_at, expires_at,
2757
- responder_session_id, responder_session_expires_at
2758
- ) VALUES (?, 0, ?, ?, ?, ?)
2759
- ON CONFLICT(key) DO UPDATE SET
2760
- responder_session_id = excluded.responder_session_id,
2761
- responder_session_expires_at = excluded.responder_session_expires_at
2762
- `).run(key, nowMs, nowMs + this.ttlMs, sessionId, expiresAtMs);
2879
+ const transition = this.db.transaction(() => {
2880
+ this.writeEntry(key, transitionSyncCheckpointResponderSession({
2881
+ key,
2882
+ existing: this.get(key, nowMs),
2883
+ sessionId,
2884
+ expiresAtMs,
2885
+ nowMs,
2886
+ ttlMs: this.ttlMs,
2887
+ manifestDigest,
2888
+ manifestPrefixDigest,
2889
+ responderSessionOffset,
2890
+ }));
2891
+ });
2892
+ transition();
2763
2893
  }
2764
2894
  clearResponderSession(key) {
2765
- this.db.prepare(`
2766
- UPDATE sync_checkpoints
2767
- SET responder_session_id = NULL,
2768
- responder_session_expires_at = NULL
2769
- WHERE key = ?
2770
- `).run(key);
2895
+ const transition = this.db.transaction(() => {
2896
+ const existing = this.readRow(key);
2897
+ if (existing)
2898
+ this.writeEntry(key, withoutSyncCheckpointResponderSession(existing));
2899
+ });
2900
+ transition();
2771
2901
  }
2772
2902
  delete(key) {
2773
2903
  this.db.prepare(`DELETE FROM sync_checkpoints WHERE key = ?`).run(key);