@rotorsoft/act-sqlite 1.4.1 → 1.5.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.
@@ -1,4 +1,5 @@
1
1
  import type { BlockedLease, Committed, EventMeta, Lease, Message, Query, QueryStatsOptions, QueryStreams, QueryStreamsResult, Schemas, Store, StreamFilter, StreamPosition, StreamStats } from "@rotorsoft/act";
2
+ import { type Encryption } from "@rotorsoft/act-crypto";
2
3
  /**
3
4
  * SQLite store configuration
4
5
  */
@@ -7,6 +8,33 @@ export interface SqliteConfig {
7
8
  url: string;
8
9
  /** Auth token for libSQL server connections (optional) */
9
10
  authToken?: string;
11
+ /**
12
+ * Adapter-layer envelope encryption for the `events.pii` column.
13
+ * Optional — when present, every non-null PII payload is encrypted
14
+ * before INSERT and decrypted on every read; when absent, the
15
+ * column is stored and read as plaintext (the framework's default
16
+ * behavior).
17
+ *
18
+ * Cipher and wire format come from `@rotorsoft/act-crypto`:
19
+ * AES-256-GCM with a versioned base64-framed envelope. The TEXT
20
+ * column stores `JSON.stringify(...)`-ed values either way, so
21
+ * encrypted writes land as a JSON-stringified base64 string and
22
+ * plaintext writes as a JSON-stringified object. The read path
23
+ * discriminates by `typeof` after `JSON.parse` — strings get
24
+ * decrypted, objects pass through — which makes mixed-data
25
+ * rollouts transparent.
26
+ *
27
+ * `forget_pii` semantics are unchanged: the column is set to
28
+ * `NULL` regardless of whether the prior value was plaintext or
29
+ * ciphertext.
30
+ *
31
+ * Encryption at rest at the **storage** layer (SQLite SEE, an
32
+ * encrypted volume, OS-level FDE) composes orthogonally with
33
+ * adapter-layer encryption. See
34
+ * `docs/docs/guides/pii-encryption-at-rest.md` for the decision
35
+ * matrix.
36
+ */
37
+ pii_encryption?: Encryption;
10
38
  }
11
39
  /** Translate a stream filter (regex-shaped or plain substring) into a
12
40
  * SQL LIKE pattern. Honors `^` / `$` anchors and converts `.*` → `%`,
@@ -49,7 +77,34 @@ export declare function streamPatternToLike(input: string): string;
49
77
  */
50
78
  export declare class SqliteStore implements Store {
51
79
  private client;
80
+ /**
81
+ * Memoized key resolver for the optional `pii_encryption` envelope.
82
+ * Initialized in the constructor when encryption is configured;
83
+ * `undefined` otherwise. The resolver caches the operator's key on
84
+ * first use — rotation means restarting the store with a fresh
85
+ * provider.
86
+ */
87
+ private readonly _resolve_pii_key;
52
88
  constructor(config?: Partial<SqliteConfig>);
89
+ /**
90
+ * Write-side pii encoder: encrypts when `pii_encryption` is configured,
91
+ * stringifies for the TEXT column either way. `null` passes through so
92
+ * `forget_pii` semantics survive intact (the row format skips the
93
+ * column entirely on a `NULL` write).
94
+ *
95
+ * @internal
96
+ */
97
+ private _stringify_pii_for_write;
98
+ /**
99
+ * Read-side pii decoder: parses the TEXT column, and when
100
+ * `pii_encryption` is configured, transparently decrypts any value
101
+ * that came back as a JSON string. Plaintext writes deserialize to
102
+ * objects and pass through — which is what makes mixed-data
103
+ * rollouts (some pre-encryption rows, some post-) read cleanly.
104
+ *
105
+ * @internal
106
+ */
107
+ private _parse_pii_from_read;
53
108
  seed(): Promise<void>;
54
109
  drop(): Promise<void>;
55
110
  dispose(): Promise<void>;
@@ -133,5 +188,23 @@ export declare class SqliteStore implements Store {
133
188
  * from the source.
134
189
  */
135
190
  restore(driver: (callback: (event: Committed<Schemas, keyof Schemas>) => Promise<number>) => Promise<void>): Promise<void>;
191
+ /**
192
+ * Wipe the sensitive-data payload for every event on the stream — the
193
+ * physical-erasure side of the sensitive-data epic (#566). Sets
194
+ * `events.pii` to `NULL` for the stream's events; `events.data` and
195
+ * the rest of the row are never touched.
196
+ *
197
+ * Single `UPDATE` under SQLite's writer lock, bounded by events-per-
198
+ * stream. Idempotent — the `pii IS NOT NULL` predicate filters out
199
+ * already-wiped rows so a second call returns `0`.
200
+ *
201
+ * SQLite doesn't auto-reclaim space; freed pages stay in the file
202
+ * until an operator-scheduled `PRAGMA incremental_vacuum` or a full
203
+ * `VACUUM`. The production checklist documents the cadence.
204
+ *
205
+ * @param stream Target stream
206
+ * @returns Count of events whose `pii` was set to `NULL`
207
+ */
208
+ forget_pii(stream: string): Promise<number>;
136
209
  }
137
210
  //# sourceMappingURL=sqlite-store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite-store.d.ts","sourceRoot":"","sources":["../../src/sqlite-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sqlite-store.d.ts","sourceRoot":"","sources":["../../src/sqlite-store.ts"],"names":[],"mappings":"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"}
package/dist/index.cjs CHANGED
@@ -37,6 +37,7 @@ module.exports = __toCommonJS(index_exports);
37
37
 
38
38
  // src/sqlite-store.ts
39
39
  var import_client = require("@libsql/client");
40
+ var import_act_crypto = require("@rotorsoft/act-crypto");
40
41
  var DEFAULT_CONFIG = {
41
42
  url: "file::memory:"
42
43
  };
@@ -52,12 +53,53 @@ function streamPatternToLike(input) {
52
53
  }
53
54
  var SqliteStore = class {
54
55
  client;
56
+ /**
57
+ * Memoized key resolver for the optional `pii_encryption` envelope.
58
+ * Initialized in the constructor when encryption is configured;
59
+ * `undefined` otherwise. The resolver caches the operator's key on
60
+ * first use — rotation means restarting the store with a fresh
61
+ * provider.
62
+ */
63
+ _resolve_pii_key;
55
64
  constructor(config = {}) {
56
65
  const cfg = { ...DEFAULT_CONFIG, ...config };
57
66
  this.client = (0, import_client.createClient)({
58
67
  url: cfg.url,
59
68
  authToken: cfg.authToken
60
69
  });
70
+ this._resolve_pii_key = cfg.pii_encryption ? (0, import_act_crypto.makeKeyResolver)(cfg.pii_encryption) : void 0;
71
+ }
72
+ /**
73
+ * Write-side pii encoder: encrypts when `pii_encryption` is configured,
74
+ * stringifies for the TEXT column either way. `null` passes through so
75
+ * `forget_pii` semantics survive intact (the row format skips the
76
+ * column entirely on a `NULL` write).
77
+ *
78
+ * @internal
79
+ */
80
+ async _stringify_pii_for_write(pii) {
81
+ if (pii == null) return null;
82
+ if (this._resolve_pii_key) {
83
+ return JSON.stringify(await (0, import_act_crypto.encrypt)(pii, this._resolve_pii_key));
84
+ }
85
+ return JSON.stringify(pii);
86
+ }
87
+ /**
88
+ * Read-side pii decoder: parses the TEXT column, and when
89
+ * `pii_encryption` is configured, transparently decrypts any value
90
+ * that came back as a JSON string. Plaintext writes deserialize to
91
+ * objects and pass through — which is what makes mixed-data
92
+ * rollouts (some pre-encryption rows, some post-) read cleanly.
93
+ *
94
+ * @internal
95
+ */
96
+ async _parse_pii_from_read(raw) {
97
+ if (raw == null) return null;
98
+ const parsed = JSON.parse(raw);
99
+ if (this._resolve_pii_key && typeof parsed === "string") {
100
+ return await (0, import_act_crypto.decrypt)(parsed, this._resolve_pii_key);
101
+ }
102
+ return parsed;
61
103
  }
62
104
  async seed() {
63
105
  await this.client.execute("PRAGMA journal_mode=WAL");
@@ -70,9 +112,14 @@ var SqliteStore = class {
70
112
  data TEXT NOT NULL,
71
113
  meta TEXT NOT NULL,
72
114
  created TEXT NOT NULL,
115
+ pii TEXT,
73
116
  UNIQUE(stream, version)
74
117
  )
75
118
  `);
119
+ try {
120
+ await this.client.execute("ALTER TABLE events ADD COLUMN pii TEXT");
121
+ } catch {
122
+ }
76
123
  await this.client.execute(
77
124
  "CREATE INDEX IF NOT EXISTS idx_events_stream ON events(stream)"
78
125
  );
@@ -141,16 +188,18 @@ var SqliteStore = class {
141
188
  const now = (/* @__PURE__ */ new Date()).toISOString();
142
189
  const committed = [];
143
190
  let version = current_version + 1;
144
- for (const { name, data } of msgs) {
191
+ for (const { name, data, pii } of msgs) {
192
+ const pii_for_write = await this._stringify_pii_for_write(pii);
145
193
  const result = await tx.execute({
146
- sql: "INSERT INTO events (stream, version, name, data, meta, created) VALUES (?, ?, ?, ?, ?, ?)",
194
+ sql: "INSERT INTO events (stream, version, name, data, meta, created, pii) VALUES (?, ?, ?, ?, ?, ?, ?)",
147
195
  args: [
148
196
  stream,
149
197
  version,
150
198
  name,
151
199
  JSON.stringify(data),
152
200
  JSON.stringify(meta),
153
- now
201
+ now,
202
+ pii_for_write
154
203
  ]
155
204
  });
156
205
  committed.push({
@@ -160,7 +209,8 @@ var SqliteStore = class {
160
209
  created: new Date(now),
161
210
  name,
162
211
  data,
163
- meta
212
+ meta,
213
+ ...pii == null ? {} : { pii }
164
214
  });
165
215
  version++;
166
216
  }
@@ -219,6 +269,7 @@ var SqliteStore = class {
219
269
  const result = await this.client.execute({ sql, args });
220
270
  let count = 0;
221
271
  for (const row of result.rows) {
272
+ const pii_value = await this._parse_pii_from_read(row.pii);
222
273
  await Promise.resolve(
223
274
  callback({
224
275
  id: Number(row.id),
@@ -227,7 +278,8 @@ var SqliteStore = class {
227
278
  created: new Date(row.created),
228
279
  name: row.name,
229
280
  data: JSON.parse(row.data),
230
- meta: JSON.parse(row.meta)
281
+ meta: JSON.parse(row.meta),
282
+ pii: pii_value
231
283
  })
232
284
  );
233
285
  count++;
@@ -634,7 +686,7 @@ var SqliteStore = class {
634
686
  * `(stream, version)` unique index. Parallel queries when tail set.
635
687
  */
636
688
  async _query_stats_heads_only(from_clause, where_clause, args, want_tail) {
637
- const cols = `e.id, e.stream, e.version, e.name, e.data, e.created, e.meta`;
689
+ const cols = `e.id, e.stream, e.version, e.name, e.data, e.created, e.meta, e.pii`;
638
690
  const head_sql = `SELECT * FROM (
639
691
  SELECT ${cols}, ROW_NUMBER() OVER (PARTITION BY e.stream ORDER BY e.version DESC) AS rn
640
692
  FROM ${from_clause}
@@ -649,24 +701,25 @@ var SqliteStore = class {
649
701
  this.client.execute({ sql: head_sql, args }),
650
702
  tail_sql ? this.client.execute({ sql: tail_sql, args }) : null
651
703
  ]);
652
- const to_committed = (row) => ({
704
+ const to_committed = async (row) => ({
653
705
  id: Number(row.id),
654
706
  stream: row.stream,
655
707
  version: Number(row.version),
656
708
  name: row.name,
657
709
  data: JSON.parse(row.data),
658
710
  meta: JSON.parse(row.meta),
659
- created: new Date(row.created)
711
+ created: new Date(row.created),
712
+ pii: await this._parse_pii_from_read(row.pii)
660
713
  });
661
714
  const out = /* @__PURE__ */ new Map();
662
715
  for (const row of headRes.rows) {
663
716
  out.set(row.stream, {
664
- head: to_committed(row)
717
+ head: await to_committed(row)
665
718
  });
666
719
  }
667
720
  if (tailRes) {
668
721
  for (const row of tailRes.rows) {
669
- out.get(row.stream).tail = to_committed(row);
722
+ out.get(row.stream).tail = await to_committed(row);
670
723
  }
671
724
  }
672
725
  return out;
@@ -684,10 +737,10 @@ var SqliteStore = class {
684
737
  )` : "";
685
738
  const tail_join = want_tail ? `LEFT JOIN tails t ON t.stream = h.stream` : "";
686
739
  const tail_cols = want_tail ? `, t.id AS t_id, t.stream AS t_stream, t.version AS t_version,
687
- t.name AS t_name, t.data AS t_data, t.created AS t_created, t.meta AS t_meta` : "";
740
+ t.name AS t_name, t.data AS t_data, t.created AS t_created, t.meta AS t_meta, t.pii AS t_pii` : "";
688
741
  const sql = `
689
742
  WITH ef AS (
690
- SELECT e.id, e.stream, e.version, e.name, e.data, e.created, e.meta
743
+ SELECT e.id, e.stream, e.version, e.name, e.data, e.created, e.meta, e.pii
691
744
  FROM ${from_clause}
692
745
  ${where_clause}
693
746
  ),
@@ -709,7 +762,7 @@ var SqliteStore = class {
709
762
  )
710
763
  ${tail_cte}
711
764
  SELECT
712
- h.id, h.stream, h.version, h.name, h.data, h.created, h.meta,
765
+ h.id, h.stream, h.version, h.name, h.data, h.created, h.meta, h.pii,
713
766
  a.cnt AS agg_count,
714
767
  a.names AS agg_names
715
768
  ${tail_cols}
@@ -718,38 +771,41 @@ var SqliteStore = class {
718
771
  ${tail_join}
719
772
  `;
720
773
  const res = await this.client.execute({ sql, args });
721
- const to_committed = (id, stream, version, name, data, meta, created) => ({
774
+ const to_committed = async (id, stream, version, name, data, meta, created, pii) => ({
722
775
  id: Number(id),
723
776
  stream,
724
777
  version: Number(version),
725
778
  name,
726
779
  data: JSON.parse(data),
727
780
  meta: JSON.parse(meta),
728
- created: new Date(created)
781
+ created: new Date(created),
782
+ pii: await this._parse_pii_from_read(pii)
729
783
  });
730
784
  const out = /* @__PURE__ */ new Map();
731
785
  for (const row of res.rows) {
732
786
  const r = row;
733
787
  const stats = {
734
- head: to_committed(
788
+ head: await to_committed(
735
789
  r.id,
736
790
  r.stream,
737
791
  r.version,
738
792
  r.name,
739
793
  r.data,
740
794
  r.meta,
741
- r.created
795
+ r.created,
796
+ r.pii
742
797
  )
743
798
  };
744
799
  if (want_tail && r.t_id !== null && r.t_id !== void 0) {
745
- stats.tail = to_committed(
800
+ stats.tail = await to_committed(
746
801
  r.t_id,
747
802
  r.t_stream,
748
803
  r.t_version,
749
804
  r.t_name,
750
805
  r.t_data,
751
806
  r.t_meta,
752
- r.t_created
807
+ r.t_created,
808
+ r.t_pii
753
809
  );
754
810
  }
755
811
  if (want_count) stats.count = Number(r.agg_count);
@@ -839,15 +895,17 @@ var SqliteStore = class {
839
895
  await tx.execute("DELETE FROM streams");
840
896
  await tx.execute("DELETE FROM sqlite_sequence WHERE name = 'events'");
841
897
  await driver(async (event) => {
898
+ const pii_for_write = await this._stringify_pii_for_write(event.pii);
842
899
  const ins = await tx.execute({
843
- sql: "INSERT INTO events (stream, version, name, data, meta, created) VALUES (?, ?, ?, ?, ?, ?)",
900
+ sql: "INSERT INTO events (stream, version, name, data, meta, created, pii) VALUES (?, ?, ?, ?, ?, ?, ?)",
844
901
  args: [
845
902
  event.stream,
846
903
  event.version,
847
904
  event.name,
848
905
  JSON.stringify(event.data),
849
906
  JSON.stringify(event.meta),
850
- event.created.toISOString()
907
+ event.created.toISOString(),
908
+ pii_for_write
851
909
  ]
852
910
  });
853
911
  return Number(ins.lastInsertRowid);
@@ -858,6 +916,30 @@ var SqliteStore = class {
858
916
  throw error;
859
917
  }
860
918
  }
919
+ /**
920
+ * Wipe the sensitive-data payload for every event on the stream — the
921
+ * physical-erasure side of the sensitive-data epic (#566). Sets
922
+ * `events.pii` to `NULL` for the stream's events; `events.data` and
923
+ * the rest of the row are never touched.
924
+ *
925
+ * Single `UPDATE` under SQLite's writer lock, bounded by events-per-
926
+ * stream. Idempotent — the `pii IS NOT NULL` predicate filters out
927
+ * already-wiped rows so a second call returns `0`.
928
+ *
929
+ * SQLite doesn't auto-reclaim space; freed pages stay in the file
930
+ * until an operator-scheduled `PRAGMA incremental_vacuum` or a full
931
+ * `VACUUM`. The production checklist documents the cadence.
932
+ *
933
+ * @param stream Target stream
934
+ * @returns Count of events whose `pii` was set to `NULL`
935
+ */
936
+ async forget_pii(stream) {
937
+ const r = await this.client.execute({
938
+ sql: "UPDATE events SET pii = NULL WHERE stream = ? AND pii IS NOT NULL",
939
+ args: [stream]
940
+ });
941
+ return r.rowsAffected ?? 0;
942
+ }
861
943
  };
862
944
  // Annotate the CommonJS export names for ESM import in node:
863
945
  0 && (module.exports = {