@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/sqlite-store.d.ts +73 -0
- package/dist/@types/sqlite-store.d.ts.map +1 -1
- package/dist/index.cjs +103 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +107 -21
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
|
@@ -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":"AACA,OAAO,KAAK,EACV,YAAY,EACZ,SAAS,EACT,SAAS,EACT,KAAK,EACL,OAAO,EACP,KAAK,EACL,iBAAiB,EACjB,YAAY,EACZ,kBAAkB,EAClB,OAAO,EACP,KAAK,EACL,YAAY,EACZ,cAAc,EACd,WAAW,EACZ,MAAM,gBAAgB,CAAC;
|
|
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 = {
|