@voltro/runtime 0.86.0 → 0.88.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 (184) hide show
  1. package/CHANGELOG.md +5484 -0
  2. package/README.md +43 -0
  3. package/THIRD-PARTY-NOTICES.md +1511 -276
  4. package/dist/aggregate.js +1 -2
  5. package/dist/aggregateExtrema.js +1 -2
  6. package/dist/aggregateIvm.js +1 -2
  7. package/dist/analyticsMirror.js +1 -2
  8. package/dist/analyticsSink.js +9 -16
  9. package/dist/apiKeyUsage.js +1 -2
  10. package/dist/apiKeys.js +83 -80
  11. package/dist/apiKeysWiring.js +1 -2
  12. package/dist/apiRoleBucketSampler.js +63 -0
  13. package/dist/apiRoleWorkloadHistory.js +55 -0
  14. package/dist/apiRoleWorkloadMetrics.js +77 -0
  15. package/dist/apiRoleWorkloadStorage.js +94 -0
  16. package/dist/approvalBuiltins.js +1 -2
  17. package/dist/approvals.js +68 -76
  18. package/dist/authMiddlewareLayer.js +8 -8
  19. package/dist/bindEvent.js +49 -30
  20. package/dist/boot-refusal.d.ts +10 -0
  21. package/dist/{bootRefusal.js → boot-refusal.js} +1 -2
  22. package/dist/bootListener.js +203 -47
  23. package/dist/bootStoreCodec.js +39 -39
  24. package/dist/businessRules.js +1 -2
  25. package/dist/changeClaimsTable.js +1 -2
  26. package/dist/circuitBreaker.js +1 -72
  27. package/dist/columnOnUpdate.js +1 -2
  28. package/dist/composeAnalytics.js +32 -12
  29. package/dist/computedQuery.js +1 -2
  30. package/dist/computedQueryCache.js +289 -0
  31. package/dist/connectionBuiltins.js +1 -2
  32. package/dist/connectionCredentials.js +1 -2
  33. package/dist/connectionState.js +1 -2
  34. package/dist/connectionVault.js +1 -2
  35. package/dist/connectionsFacade.js +33 -32
  36. package/dist/coordinatedSchedule.js +85 -80
  37. package/dist/crdtHistory.js +1 -2
  38. package/dist/crdtMerge.js +1 -2
  39. package/dist/crdtVersionsTable.js +1 -2
  40. package/dist/credentialBound.js +1 -2
  41. package/dist/crud.js +1 -2
  42. package/dist/dataStore.js +155 -123
  43. package/dist/dataloader.js +1 -2
  44. package/dist/declaredTablesGuard.js +3 -3
  45. package/dist/defineExecutor.js +1 -2
  46. package/dist/dependencyGraph.js +1 -2
  47. package/dist/devices.js +1 -2
  48. package/dist/devicesWiring.js +1 -2
  49. package/dist/dialectReplicationAdapter.js +1 -2
  50. package/dist/dispatcher.js +404 -330
  51. package/dist/effectStore.js +3 -4
  52. package/dist/engineClients.js +23 -0
  53. package/dist/eventBus.js +1 -2
  54. package/dist/eventPublish.js +1 -2
  55. package/dist/events.js +1 -2
  56. package/dist/expectation.js +1 -2
  57. package/dist/expectationEvaluator.js +2 -3
  58. package/dist/experiment.js +1 -2
  59. package/dist/experimentEvaluator.js +1 -2
  60. package/dist/externalizedTasks.js +20 -8
  61. package/dist/fieldCipher.js +110 -42
  62. package/dist/fieldKeyRegistry.js +294 -0
  63. package/dist/fieldKeyRekey.js +285 -0
  64. package/dist/fieldKeyScan.js +135 -0
  65. package/dist/fieldKeyring.js +76 -0
  66. package/dist/fingerprint.js +1 -2
  67. package/dist/finops.js +1 -2
  68. package/dist/finopsAccountant.js +1 -2
  69. package/dist/handlerBody.js +1 -2
  70. package/dist/healthProbes.js +1 -2
  71. package/dist/hot.d.ts +56 -0
  72. package/dist/hot.js +3 -0
  73. package/dist/hotDataStore.js +147 -0
  74. package/dist/hotDataStoreSnapshot.js +97 -0
  75. package/dist/hotDataStoreTables.js +17 -0
  76. package/dist/http-range.d.ts +53 -0
  77. package/dist/http-range.js +51 -0
  78. package/dist/httpClientSsrf.js +13 -39
  79. package/dist/httpClientTracing.js +13 -11
  80. package/dist/httpCompression.js +18 -19
  81. package/dist/httpInputStream.js +81 -0
  82. package/dist/httpKeepAlive.js +1 -2
  83. package/dist/httpRouteMounts.js +2 -2
  84. package/dist/idempotencyStore.js +93 -54
  85. package/dist/idleDetector.js +1 -2
  86. package/dist/index.d.ts +1185 -452
  87. package/dist/index.js +144 -134
  88. package/dist/insertRequiredColumns.js +1 -2
  89. package/dist/instanceMembership.js +1 -2
  90. package/dist/ivm.js +1 -2
  91. package/dist/kvDataStore.js +1 -2
  92. package/dist/kvFacade.js +24 -19
  93. package/dist/matcher.js +1 -2
  94. package/dist/memoryColumnDefaults.js +1 -2
  95. package/dist/memoryQueryJoins.js +17 -18
  96. package/dist/memorySetOperations.js +42 -0
  97. package/dist/metrics.js +39 -26
  98. package/dist/observability.js +2 -3
  99. package/dist/observedGraph.js +1 -2
  100. package/dist/originGuard.js +1 -2
  101. package/dist/outbox.js +2 -3
  102. package/dist/pluginRefRules.js +1 -2
  103. package/dist/policyGuardResolver.js +1 -2
  104. package/dist/preview.js +1 -2
  105. package/dist/projectedRows.js +1 -2
  106. package/dist/provenance.js +1 -2
  107. package/dist/rangeIndex.js +1 -2
  108. package/dist/rawReadScope.js +2 -3
  109. package/dist/reaction.js +1 -2
  110. package/dist/reactiveConfig.js +1 -2
  111. package/dist/readClassification.js +1 -2
  112. package/dist/readRecorder.js +36 -36
  113. package/dist/readScopePredicate.js +11 -0
  114. package/dist/rebac.js +1 -2
  115. package/dist/rebacInterceptor.js +1 -2
  116. package/dist/registry.js +1 -2
  117. package/dist/relevantFields.js +1 -2
  118. package/dist/replicaHealth.js +1 -2
  119. package/dist/replicaLocality.js +1 -2
  120. package/dist/replicaObservations.js +1 -2
  121. package/dist/replicaSelection.js +1 -2
  122. package/dist/replicatedDataStore.js +23 -6
  123. package/dist/requestLoader.js +1 -2
  124. package/dist/resumeCensus.js +1 -2
  125. package/dist/revalidation.js +1 -2
  126. package/dist/rowFilter.js +57 -41
  127. package/dist/rowStateBytes.js +1 -2
  128. package/dist/rpcBodyCap.js +1 -2
  129. package/dist/rpcConnections.js +9 -10
  130. package/dist/rpcDefectDiagnostics.js +1 -2
  131. package/dist/rpcServer.js +523 -451
  132. package/dist/rpcWorkloadAdmission.js +49 -0
  133. package/dist/runAsSystem.js +1 -2
  134. package/dist/rywPositionStore.js +83 -49
  135. package/dist/schedule.js +1 -2
  136. package/dist/scheduleBackfillRequests.js +1 -2
  137. package/dist/scheduleClaimsTable.js +1 -2
  138. package/dist/scheduleRunsTable.js +1 -2
  139. package/dist/scheduler.js +7 -3
  140. package/dist/schemaRegistry.js +1 -2
  141. package/dist/secretsResolver.js +1 -2
  142. package/dist/securityHeaders.js +1 -2
  143. package/dist/servePipeline.js +238 -154
  144. package/dist/serverOnlyAudit.js +1 -2
  145. package/dist/serverTracer.js +1 -2
  146. package/dist/serviceCredentials.js +1 -2
  147. package/dist/setReadScopes.js +27 -0
  148. package/dist/singleNodeOrchestrator.js +1 -2
  149. package/dist/snapshotValueCodec.js +45 -0
  150. package/dist/sourceResolution.js +35 -26
  151. package/dist/storeMiddleware.js +213 -212
  152. package/dist/storeQuery.js +1 -2
  153. package/dist/storeRead.js +1 -2
  154. package/dist/subscriber.js +2 -2
  155. package/dist/subscriptionOutbox.js +1 -2
  156. package/dist/subscriptionRegistry.js +1 -2
  157. package/dist/subscriptionResume.js +1 -2
  158. package/dist/subscriptionSocketMetrics.js +1 -2
  159. package/dist/systemSubject.js +1 -2
  160. package/dist/tenantScope.js +9 -22
  161. package/dist/tenantSelection.js +1 -2
  162. package/dist/timeline.js +1 -2
  163. package/dist/traceContext.js +1 -2
  164. package/dist/traceSink.js +1 -2
  165. package/dist/traceSpansTable.js +1 -2
  166. package/dist/traceparent.js +9 -21
  167. package/dist/transactionSubject.js +1 -2
  168. package/dist/trustedProxy.js +2 -3
  169. package/dist/undo.js +1 -2
  170. package/dist/undoBuiltins.js +1 -2
  171. package/dist/undoCapture.js +1 -2
  172. package/dist/undoLogTable.js +1 -2
  173. package/dist/upsertBinding.js +1 -2
  174. package/dist/wakeOrchestrator.js +1 -2
  175. package/dist/wakeRegistry.js +1 -2
  176. package/dist/wakeRouter.js +1 -2
  177. package/dist/wakeSupervisor.js +1 -2
  178. package/dist/wakeupsTable.js +1 -2
  179. package/dist/webSocketGatewayRuntime.js +112 -73
  180. package/dist/webhookVerification.js +1 -2
  181. package/dist/wireColumnStrip.js +1 -2
  182. package/dist/workflowReadBuiltins.js +1 -2
  183. package/dist/workflowRuntime.js +10 -11
  184. package/package.json +36 -17
@@ -0,0 +1,294 @@
1
+ import { derivedRowId as e, encryptedColumnsOf as t, eq as n, fieldBinding as r, fieldBindingsOf as i, id as a, isEncrypted as o, isNotNull as s, json as c, lt as l, table as u, text as d, timestamp as f } from "@voltro/database";
2
+ import { randomUUID as p } from "node:crypto";
3
+ import { hostname as m } from "node:os";
4
+ // src/fieldKeyRegistry.ts
5
+ var h = u("_voltro_field_keys", {
6
+ id: a({ prefix: "fkey" }),
7
+ keyId: d().maxLength(16).unique(),
8
+ state: d().maxLength(16),
9
+ canary: d().maxLength(160),
10
+ registeredAt: f().default("now"),
11
+ activatedAt: f().nullable(),
12
+ replacedAt: f().nullable(),
13
+ retiredAt: f().nullable(),
14
+ requestedAt: f().nullable()
15
+ }), g = u("_voltro_field_key_holders", {
16
+ id: a({ prefix: "fkh" }),
17
+ holderId: d().maxLength(128).unique(),
18
+ keyIds: d().maxLength(512),
19
+ activeKeyId: d().maxLength(16),
20
+ heartbeatAt: f(),
21
+ expiresAt: f()
22
+ }).nonReactive(), _ = u("_voltro_field_key_reports", {
23
+ id: a({ prefix: "fkrep" }),
24
+ kind: d().maxLength(16).unique(),
25
+ reportedAt: f(),
26
+ report: c()
27
+ }).nonReactive(), v = "_voltro_field_keys", y = "_voltro_field_key_reports", b = async (t, n, r, i = /* @__PURE__ */ new Date()) => {
28
+ await t.upsert(y, {
29
+ id: e("fkrep", n),
30
+ kind: n,
31
+ reportedAt: i,
32
+ report: JSON.stringify(r)
33
+ }, { conflictColumns: ["kind"] });
34
+ }, x = async (e) => {
35
+ let t = await e.query({
36
+ table: y,
37
+ predicate: void 0,
38
+ order: [],
39
+ take: void 0,
40
+ skip: void 0,
41
+ projection: void 0
42
+ }), n = {};
43
+ for (let e of t) {
44
+ let t = String(e.kind), r = e.report, i = typeof r == "string" ? JSON.parse(r) : r, a = e.reportedAt instanceof Date ? e.reportedAt : new Date(String(e.reportedAt));
45
+ t === "status" && (n.status = {
46
+ reportedAt: a,
47
+ report: i
48
+ }), t === "rekey" && (n.rekey = {
49
+ reportedAt: a,
50
+ report: i
51
+ });
52
+ }
53
+ return n;
54
+ }, S = "_voltro_field_key_holders", C = 3e4, w = 1e4, T = 10, E = () => `${m().slice(0, 80)}:${process.pid}:${p().replaceAll("-", "").slice(0, 12)}`, D = "voltro:field-key-canary:v1", O = (e) => r(v, "canary", e), k = 3, A = class extends Error {
55
+ _tag = "FieldKeyringRefusal";
56
+ constructor(e) {
57
+ super(e), this.name = "FieldKeyringRefusal";
58
+ }
59
+ }, j = (e) => ({
60
+ table: v,
61
+ predicate: void 0,
62
+ order: [],
63
+ take: void 0,
64
+ skip: void 0,
65
+ projection: void 0,
66
+ ...e === void 0 ? {} : { lock: e }
67
+ }), M = (e) => ({
68
+ keyId: String(e.keyId),
69
+ state: String(e.state),
70
+ canary: String(e.canary),
71
+ registeredAt: N(e.registeredAt),
72
+ activatedAt: N(e.activatedAt),
73
+ replacedAt: N(e.replacedAt),
74
+ retiredAt: N(e.retiredAt),
75
+ requestedAt: N(e.requestedAt)
76
+ }), N = (e) => e instanceof Date ? e : e == null ? null : new Date(typeof e == "number" ? e : String(e)), P = async (e) => (await e.query(j())).map(M), F = async (t, n, r, i = { holderId: E() }) => {
77
+ let a = n.requestedKeyId, o = new Set(n.keyIds), s = [], c = await P(t);
78
+ B(c, n, o, s), await H(t, n, r, s);
79
+ let l = new Set(c.map((e) => e.keyId)), u = n.keyIds.filter((e) => !l.has(e)), d = c.length === 0;
80
+ for (let r of u) {
81
+ let i = d && r === a;
82
+ await t.insertIgnore(v, {
83
+ id: e("fkey", r),
84
+ keyId: r,
85
+ state: i ? "active" : "pending",
86
+ canary: n.sealWith(r, D, O(r)),
87
+ ...i ? { activatedAt: /* @__PURE__ */ new Date() } : {}
88
+ }, { conflictColumns: ["keyId"] });
89
+ }
90
+ let f = await G(t, n, i, !0);
91
+ return f.note !== void 0 && s.push(f.note), {
92
+ active: f.active,
93
+ registered: u,
94
+ ...f.activated === void 0 ? {} : { activated: f.activated },
95
+ ...f.waiting === void 0 ? {} : { waiting: f.waiting },
96
+ notes: s
97
+ };
98
+ }, I = async (e, t, n) => {
99
+ let r = [], i = await P(e);
100
+ B(i, t, new Set(t.keyIds), r), await H(e, t, n, r);
101
+ let a = i.find((e) => e.state === "active")?.keyId, o = i.find((e) => e.requestedAt !== null && e.state === "pending")?.keyId;
102
+ return {
103
+ active: a,
104
+ ...o === void 0 ? {} : { requested: o },
105
+ notes: r
106
+ };
107
+ }, L = async (e, t, n) => {
108
+ let r = await G(e, t, n, !1);
109
+ return {
110
+ active: r.active,
111
+ ...r.activated === void 0 ? {} : { activated: r.activated },
112
+ ...r.waiting === void 0 ? {} : { waiting: r.waiting },
113
+ unheld: !t.keyIds.includes(r.active)
114
+ };
115
+ }, R = async (e, t) => {
116
+ await e.deleteMany(S, { where: n("holderId", t) });
117
+ }, z = (e, t, n) => {
118
+ let r, i = !1, a = "", o = (e, t) => {
119
+ a !== e && (a = e, t());
120
+ }, s = async () => {
121
+ try {
122
+ let r = await L(e, t, n);
123
+ r.activated !== void 0 && n.log.info(`field encryption: key "${r.activated.to}" is active now${r.activated.from === void 0 ? "" : ` (replacing "${r.activated.from}")`} — every live process holds it`), r.unheld ? o(`unheld:${r.active}`, () => n.log.error(`field encryption: the database's active key "${r.active}" is not held by this process (it holds ${t.keyIds.join(", ")}) — it missed its heartbeats while the fleet rotated. Values sealed under "${r.active}" cannot be read here; restart it with that key.`)) : r.waiting === void 0 ? a = "" : o(`waiting:${r.waiting.keyId}:${r.waiting.holders.join(",")}`, () => n.log.info(`field encryption: key "${r.waiting.keyId}" waits to become active — ${r.waiting.holders.length} live process(es) do not hold it yet: ${r.waiting.holders.join(", ")}`));
124
+ } catch (e) {
125
+ o("failed", () => n.log.warn(`field encryption: key heartbeat failed — ${e instanceof Error ? e.message : String(e)}`));
126
+ }
127
+ }, c = setInterval(() => {
128
+ i || r !== void 0 || (r = s().finally(() => {
129
+ r = void 0;
130
+ }));
131
+ }, n.intervalMs ?? 1e4);
132
+ return c.unref(), { stop: async () => {
133
+ i = !0, clearInterval(c), await r, await R(e, n.holderId);
134
+ } };
135
+ }, B = (e, t, n, r) => {
136
+ let i = new Map(e.map((e) => [e.keyId, e])), a = (n) => e.find((e) => e.keyId !== n && e.state !== "retired" && t.opensWith(n, e.canary, O(e.keyId)))?.keyId;
137
+ for (let e of t.keyIds) {
138
+ let n = i.get(e), o = a(e);
139
+ if (n !== void 0 && n.state !== "retired" && !t.opensWith(e, n.canary, O(e))) throw new A(`field encryption: the key supplied as "${e}" is not the key this database registered as "${e}"` + (o === void 0 ? "" : ` — it is the key registered as "${o}"`) + `.
140
+ Values sealed under "${e}" cannot be read with it. Supply the original key under that id;\n a NEW key needs a new id (\`voltro secret generate field-encryption\` makes a key).`);
141
+ if (o !== void 0) throw new A(`field encryption: the key supplied as "${e}" is the key this database registered as "${o}".\n One key under two ids makes every count per key wrong, and a rotation to it is no rotation.
142
+ Supply it as "${o}" — or, for a rotation, generate a new key (\`voltro secret generate field-encryption\`).`);
143
+ n?.state === "retired" && r.push(`field encryption: key "${e}" is retired — nothing is sealed under it; it can be removed from the environment`);
144
+ }
145
+ V(e, n);
146
+ }, V = (e, t) => {
147
+ let n = e.filter((e) => (e.state === "active" || e.state === "decrypt-only") && !t.has(e.keyId));
148
+ if (n.length > 0) {
149
+ let e = n.map((e) => `"${e.keyId}" (${e.state})`).join(", "), r = n.filter((e) => e.state === "decrypt-only").map((e) => e.keyId);
150
+ throw new A(`field encryption: this database has values sealed under ${e}, which this process does not hold (it holds ${[...t].join(", ")}).\n This is what a rollback past a key rotation, a database restored from another environment, or a key
151
+ removed from VOLTRO_FIELD_ENCRYPTION_KEYS too early look like. Supply the key(s) under their ids.` + (r.length === 0 ? "" : `
152
+ Keeping a replaced key (${r.join(", ")}) is always fine — it only decrypts. To drop it instead, supply it\n once more, run \`voltro secret rekey\` (every value moves under the active key), then
153
+ \`voltro secret retire field-encryption ${r[0]}\`${r.length > 1 ? " (per key)" : ""} — a retired key can leave the environment.`));
154
+ }
155
+ }, H = async (e, n, r, a) => {
156
+ for (let c of r) {
157
+ let r = c;
158
+ if (typeof r != "object" || !r || typeof r.fields != "object") continue;
159
+ let l = r.tableName;
160
+ if (l !== void 0 && l !== "_voltro_field_keys") for (let c of t(r)) {
161
+ let t = (await e.query({
162
+ table: l,
163
+ predicate: s(c),
164
+ order: [],
165
+ take: k,
166
+ skip: void 0,
167
+ projection: ["id", c]
168
+ })).filter((e) => o(e[c])), u = t.filter((e) => i(r, c, e.id).every((t) => n.keyIdOf?.(String(e[c]), t) === void 0));
169
+ if (u.length !== 0) {
170
+ if (u.length === t.length) throw new A(`field encryption: none of the configured keys (${n.keyIds.join(", ")}) opens the values in ${l}.${c} — ${u.length} of ${u.length} sampled.\n The configured key is not the one this data was sealed with: a key from another environment, a
171
+ regenerated key, or data restored from elsewhere. Supply the original key; this refuses at boot
172
+ rather than failing every read of the column.`);
173
+ a.push(`field encryption: ${u.length} of ${t.length} sampled values in ${l}.${c} open with no configured key`);
174
+ }
175
+ }
176
+ }
177
+ }, U = () => ({
178
+ table: S,
179
+ predicate: void 0,
180
+ order: [],
181
+ take: void 0,
182
+ skip: void 0,
183
+ projection: void 0
184
+ }), W = async (e) => (await e.query(U())).map((e) => ({
185
+ holderId: String(e.holderId),
186
+ keyIds: String(e.keyIds).split(",").filter((e) => e !== ""),
187
+ activeKeyId: String(e.activeKeyId),
188
+ heartbeatAt: new Date(q(e.heartbeatAt)),
189
+ expiresAt: new Date(q(e.expiresAt))
190
+ })), G = async (t, r, i, a) => {
191
+ let o = i.now?.() ?? Date.now(), s = i.leaseMs ?? 3e4, c = r.requestedKeyId, u = await t.transactional(async (t) => {
192
+ let u = (await t.query(j("update"))).map(M);
193
+ a && V(u, new Set(r.keyIds));
194
+ let d = new Date(o), f = u.filter((e) => e.state === "active"), p = f.find((e) => e.keyId === c) ?? [...f].sort((e, t) => (e.activatedAt?.getTime() ?? 0) - (t.activatedAt?.getTime() ?? 0) || e.keyId.localeCompare(t.keyId))[0];
195
+ for (let e of f) e.keyId !== p?.keyId && await t.updateMany(v, {
196
+ state: "decrypt-only",
197
+ replacedAt: d
198
+ }, { where: n("keyId", e.keyId) });
199
+ let m, h = u.find((e) => e.requestedAt !== null && e.state === "pending" && e.keyId !== p?.keyId)?.keyId;
200
+ if (a && p?.keyId !== c) {
201
+ let e = u.find((e) => e.keyId === c);
202
+ if (e === void 0) throw new A(`field encryption: key "${c}" was not recorded`);
203
+ if (e.state === "retired") throw new A(`field encryption: the active key "${c}" is retired — nothing may be sealed under it again.\n Name another key in VOLTRO_FIELD_ENCRYPTION_ACTIVE${p === void 0 ? "" : ` (the database's active key is "${p.keyId}")`}.`);
204
+ e.state === "decrypt-only" && p !== void 0 ? m = `field encryption: VOLTRO_FIELD_ENCRYPTION_ACTIVE names "${c}", which was replaced by "${p.keyId}" — new values stay under "${p.keyId}"` : (await K(t, u, c), await t.updateMany(v, { requestedAt: d }, { where: n("keyId", c) }), h = c);
205
+ }
206
+ await t.deleteMany(S, { where: l("expiresAt", /* @__PURE__ */ new Date(o - T * s)) });
207
+ let g = (await t.query(U())).filter((e) => String(e.holderId) !== i.holderId && q(e.expiresAt) > o).map((e) => ({
208
+ holderId: String(e.holderId),
209
+ keyIds: String(e.keyIds).split(",")
210
+ })), _, y;
211
+ if (h !== void 0) {
212
+ let e = [...r.keyIds.includes(h) ? [] : [i.holderId], ...g.filter((e) => !e.keyIds.includes(h)).map((e) => e.holderId)].sort();
213
+ e.length === 0 ? (p !== void 0 && await t.updateMany(v, {
214
+ state: "decrypt-only",
215
+ replacedAt: d
216
+ }, { where: n("keyId", p.keyId) }), await t.updateMany(v, {
217
+ state: "active",
218
+ activatedAt: d,
219
+ requestedAt: null
220
+ }, { where: n("keyId", h) }), _ = {
221
+ from: p?.keyId,
222
+ to: h
223
+ }, p = u.find((e) => e.keyId === h)) : y = {
224
+ keyId: h,
225
+ holders: e
226
+ };
227
+ }
228
+ if (p === void 0) throw new A("field encryption: no key is recorded as active");
229
+ let b = p.keyId;
230
+ return await t.upsert(S, {
231
+ id: e("fkh", i.holderId),
232
+ holderId: i.holderId,
233
+ keyIds: r.keyIds.join(","),
234
+ activeKeyId: r.keyIds.includes(b) ? b : r.activeKeyId,
235
+ heartbeatAt: d,
236
+ expiresAt: new Date(o + s)
237
+ }, { conflictColumns: ["holderId"] }), {
238
+ active: b,
239
+ ..._ === void 0 ? {} : { activated: _ },
240
+ ...y === void 0 ? {} : { waiting: y },
241
+ ...m === void 0 ? {} : { note: m }
242
+ };
243
+ });
244
+ return r.keyIds.includes(u.active) && r.activate(u.active), u;
245
+ }, K = async (e, t, r) => {
246
+ for (let i of t) i.requestedAt !== null && i.keyId !== r && await e.updateMany(v, { requestedAt: null }, { where: n("keyId", i.keyId) });
247
+ }, q = (e) => e instanceof Date ? e.getTime() : typeof e == "number" ? e : Date.parse(String(e)), J = async (t, n, r) => {
248
+ await t.insertIgnore(v, {
249
+ id: e("fkey", r),
250
+ keyId: r,
251
+ state: "pending",
252
+ canary: n.sealWith(r, D, O(r))
253
+ }, { conflictColumns: ["keyId"] });
254
+ }, Y = async (e, t) => (await e.query(U())).filter((e) => q(e.expiresAt) > t).map((e) => ({
255
+ holderId: String(e.holderId),
256
+ keyIds: String(e.keyIds).split(","),
257
+ activeKeyId: String(e.activeKeyId)
258
+ })), X = (e, t, r = {}) => e.transactional(async (e) => {
259
+ let i = r.now?.() ?? Date.now(), a = (await e.query(j("update"))).map(M), o = a.find((e) => e.keyId === t);
260
+ if (o === void 0) throw new A(`field encryption: "${t}" is not a recorded key (recorded: ${a.map((e) => e.keyId).join(", ") || "none"})`);
261
+ if (o.state === "retired") return;
262
+ if (o.state === "active") throw new A(`field encryption: "${t}" is the active key — rotate to another key first (\`voltro secret rotate field-encryption\`)`);
263
+ let s = (await Y(e, i)).filter((e) => e.activeKeyId === t).map((e) => e.holderId);
264
+ if (s.length > 0) throw new A(`field encryption: a live process still seals new values under "${t}": ${s.join(", ")} — it adopts the active key on its next heartbeat; try again then`);
265
+ await e.updateMany(v, {
266
+ state: "retired",
267
+ retiredAt: new Date(i),
268
+ requestedAt: null
269
+ }, { where: n("keyId", t) });
270
+ }), Z = (e, t, r = {}) => e.transactional(async (e) => {
271
+ let i = r.now?.() ?? Date.now(), a = new Date(i), o = (await e.query(j("update"))).map(M), s = o.find((e) => e.keyId === t);
272
+ if (s === void 0) throw new A(`field encryption: "${t}" is not a recorded key (recorded: ${o.map((e) => e.keyId).join(", ") || "none"})`);
273
+ if (s.state === "retired") throw new A(`field encryption: "${t}" is retired — nothing may be sealed under it again`);
274
+ let c = o.find((e) => e.state === "active"), l = (await Y(e, i)).filter((e) => !e.keyIds.includes(t)).map((e) => e.holderId).sort();
275
+ if (c?.keyId === t) return {
276
+ from: t,
277
+ to: t,
278
+ lacking: l
279
+ };
280
+ for (let t of o) t.state === "active" && await e.updateMany(v, {
281
+ state: "decrypt-only",
282
+ replacedAt: a
283
+ }, { where: n("keyId", t.keyId) });
284
+ return await e.updateMany(v, {
285
+ state: "active",
286
+ activatedAt: a,
287
+ requestedAt: null
288
+ }, { where: n("keyId", t) }), {
289
+ from: c?.keyId,
290
+ to: t,
291
+ lacking: l
292
+ };
293
+ });
294
+ export { v as FIELD_KEYS_TABLE, w as FIELD_KEY_HEARTBEAT_MS, S as FIELD_KEY_HOLDERS_TABLE, C as FIELD_KEY_LEASE_MS, y as FIELD_KEY_REPORTS_TABLE, A as FieldKeyringRefusal, g as _voltroFieldKeyHoldersTable, _ as _voltroFieldKeyReportsTable, h as _voltroFieldKeysTable, Z as activateFieldKeyNow, I as checkFieldKeyring, E as fieldKeyHolderId, L as heartbeatFieldKeys, W as readFieldKeyHolders, x as readFieldKeyReports, P as readFieldKeys, b as recordFieldKeyReport, J as registerFieldKey, R as releaseFieldKeyHolder, X as retireFieldKey, z as startFieldKeyHolder, F as verifyFieldKeyring };
@@ -0,0 +1,285 @@
1
+ import { ENVELOPE_PATTERN as e, cellText as t, declaredEncryptedColumns as n, declaredRenames as r, discoverSealedColumns as i, reasonOf as a } from "./fieldKeyScan.js";
2
+ import { Effect as o } from "effect";
3
+ import { ciphertextKeyId as s, fieldBinding as c, quoteIdent as l } from "@voltro/database";
4
+ import { SqlClient as u } from "effect/sql";
5
+ import { STEP_JOURNAL_BINDING as d, WORKFLOW_JOURNAL_BINDING as f } from "@voltro/workflow";
6
+ // src/fieldKeyRekey.ts
7
+ var p = (e) => {
8
+ let t = e.renames?.get(e.table), n = t?.columns.get(e.column);
9
+ return {
10
+ tables: t?.previousTable === void 0 ? [e.table] : [e.table, t.previousTable],
11
+ columns: n === void 0 ? [e.column] : [e.column, n]
12
+ };
13
+ }, m = (e) => {
14
+ let t = p(e);
15
+ return t.tables.length > 1 || t.columns.length > 1;
16
+ }, h = 32, g = (e, t) => {
17
+ typeof e == "string" && e !== "" && e.length <= 191 && !e.startsWith("enc:") ? t.add(e) : (typeof e == "number" || typeof e == "bigint") && t.add(String(e));
18
+ }, _ = (e) => {
19
+ let t = /* @__PURE__ */ new Map(), n;
20
+ try {
21
+ n = JSON.parse(e);
22
+ } catch {
23
+ return t;
24
+ }
25
+ let r = (e, n, i) => {
26
+ if (typeof e == "string") {
27
+ if (!e.startsWith("enc:")) return;
28
+ let r = /* @__PURE__ */ new Set();
29
+ for (let e of i) for (let t of Object.values(e)) g(t, r);
30
+ t.set(e, {
31
+ key: n,
32
+ ids: r
33
+ });
34
+ } else if (Array.isArray(e)) for (let t of e) r(t, n, i);
35
+ else if (typeof e == "object" && e) {
36
+ let t = e;
37
+ for (let [e, n] of Object.entries(t)) r(n, e, [t, ...i]);
38
+ }
39
+ };
40
+ return r(n, void 0, []), t;
41
+ }, v = (e, t, n) => {
42
+ if (!e.startsWith("enc:v2:")) return [void 0];
43
+ let r = n.rowId === void 0 || n.rowId === null ? void 0 : String(n.rowId);
44
+ if (e.startsWith("enc:v2:c:")) {
45
+ let e = p(n), t = [
46
+ ...e.tables.flatMap((t) => e.columns.map((e) => c(t, e))),
47
+ f,
48
+ d,
49
+ ...[...n.declared].flatMap(([e, t]) => [...t].map((t) => c(e, t)))
50
+ ];
51
+ return [...new Map(t.map((e) => [e.column, e])).values()];
52
+ }
53
+ let i = [], a = /* @__PURE__ */ new Set(), o = (e, t, n) => {
54
+ if (n === void 0) return;
55
+ let r = c(e, t, n);
56
+ r.row === void 0 || a.has(r.row) || (a.add(r.row), i.push(r));
57
+ }, s = p(n);
58
+ for (let e of s.tables) for (let t of s.columns) o(e, t, r);
59
+ let l = /* @__PURE__ */ new Set();
60
+ for (let e of t?.ids ?? []) l.add(e);
61
+ for (let e of Object.values(n.row)) g(e, l);
62
+ let u = [...l].slice(0, h), m = t?.key ?? n.column;
63
+ for (let [e, t] of n.declared) if (t.has(m)) for (let t of u) o(e, m, t);
64
+ return i;
65
+ }, y = (t, n, r) => {
66
+ let i = n.activeKeyId, a = [...new Set(t.match(e) ?? [])];
67
+ if (a.length === 0) return {
68
+ text: t,
69
+ rekeyed: 0,
70
+ current: 0,
71
+ unresolved: 0
72
+ };
73
+ let o = _(t), l = t, u = 0, d = 0, f = 0, h = m(r), g = (e) => {
74
+ if (e === void 0 || !h) return e;
75
+ let t = p(r);
76
+ return !t.tables.some((n) => t.columns.some((t) => e.column === `${n}:${t}`)) || e.column === `${r.table}:${r.column}` ? e : e.row === void 0 ? c(r.table, r.column) : c(r.table, r.column, r.rowId);
77
+ };
78
+ for (let e of a) {
79
+ let a = t.split(e).length - 1;
80
+ if (s(e) === i && !h) {
81
+ d += a;
82
+ continue;
83
+ }
84
+ let c, p = !1;
85
+ for (let t of v(e, o.get(e), r)) {
86
+ let r;
87
+ try {
88
+ r = n.decrypt(e, t);
89
+ } catch {
90
+ continue;
91
+ }
92
+ let a = g(t);
93
+ if (s(e) === i && a === t) {
94
+ p = !0;
95
+ break;
96
+ }
97
+ c = n.sealWith(i, r, a);
98
+ break;
99
+ }
100
+ p ? d += a : c === void 0 ? f += a : (l = l.split(e).join(c), u += a);
101
+ }
102
+ return {
103
+ text: l,
104
+ rekeyed: u,
105
+ current: d,
106
+ unresolved: f
107
+ };
108
+ }, b = (t, n, r) => {
109
+ let i = _(t), a = 0, o = 0;
110
+ for (let s of t.match(e) ?? []) v(s, i.get(s), r).some((e) => {
111
+ try {
112
+ return n.decrypt(s, e), !0;
113
+ } catch {
114
+ return !1;
115
+ }
116
+ }) ? a += 1 : o += 1;
117
+ return {
118
+ opened: a,
119
+ unopened: o
120
+ };
121
+ }, x = 200, S = (e, t, n, r, i) => e === "postgres" ? r`CAST(${r(t)} AS TEXT) = ${i}` : e === "mysql" || e === "mariadb" ? n === "json" ? r`CAST(${r(t)} AS CHAR) = ${i}` : r`CAST(${r(t)} AS BINARY) = CAST(${i} AS BINARY)` : e === "mssql" ? r`CAST(${r(t)} AS VARBINARY(MAX)) = CAST(${i} AS VARBINARY(MAX))` : r`${r(t)} = ${i}`, C = (e, t, s, c = {}) => o.gen(function* () {
122
+ let l = yield* u.SqlClient, d = n(s), f = r(s), { columns: p, unreadable: m } = yield* i(e, d), h = [...m], g = [];
123
+ for (let n of p) {
124
+ if (n.found === 0) continue;
125
+ let r = yield* w(l, e, t, d, f, n, c.dryRun === !0, c.beforeWrite).pipe(o.catch((e) => o.sync(() => {
126
+ h.push({
127
+ table: n.table,
128
+ column: n.column,
129
+ reason: a(e)
130
+ });
131
+ })));
132
+ r !== void 0 && (g.push(r), c.onProgress?.(r));
133
+ }
134
+ return {
135
+ active: t.activeKeyId,
136
+ locations: g,
137
+ unreadable: h
138
+ };
139
+ }), w = (e, n, r, i, a, s, c, u) => o.gen(function* () {
140
+ let d = {
141
+ cells: 0,
142
+ rekeyed: 0,
143
+ current: 0,
144
+ unresolved: 0,
145
+ changedMeanwhile: 0,
146
+ unaddressable: 0
147
+ }, f = s.key, p = n === "postgres" && s.type === "json" ? (yield* e.unsafe(`SELECT data_type AS t FROM information_schema.columns WHERE table_name = '${s.table.replaceAll("'", "''")}' AND column_name = '${s.column.replaceAll("'", "''")}' AND table_schema = current_schema()`).unprepared)[0]?.t : void 0, m = (e) => typeof e == "number" || typeof e == "bigint" ? String(e) : `'${String(e).replaceAll("'", "''")}'`, h = (r, i, a) => o.gen(function* () {
148
+ u !== void 0 && (yield* u({
149
+ table: s.table,
150
+ column: s.column,
151
+ rowId: r
152
+ }));
153
+ let o = p === "jsonb" ? e`CAST(${a} AS JSONB)` : p === "json" ? e`CAST(${a} AS JSON)` : e`${a}`;
154
+ yield* e`UPDATE ${e(s.table)} SET ${e(s.column)} = ${o} WHERE ${e(f)} = ${r} AND ${S(n, s.column, s.type, e, i)}`;
155
+ let c = (yield* e.unsafe(`SELECT ${s.asText} AS v FROM ${l(s.table, n)} WHERE ${l(f, n)} = ${m(r)}`).unprepared)[0]?.v;
156
+ return t(c) === a;
157
+ }), g = (r) => o.gen(function* () {
158
+ if (u !== void 0) for (let e of r) yield* u({
159
+ table: s.table,
160
+ column: s.column,
161
+ rowId: e.rowId
162
+ });
163
+ let i = (t) => p === "jsonb" ? e`CAST(${t} AS JSONB)` : p === "json" ? e`CAST(${t} AS JSON)` : e`${t}`, a = e.join(" ", !1)(r.map((t) => e`WHEN ${t.rowId} THEN ${i(t.next)}`)), o = e.join(" ", !1)(r.map((t) => e`WHEN ${e(f)} = ${t.rowId} AND ${S(n, s.column, s.type, e, t.text)} THEN 1`));
164
+ yield* e`UPDATE ${e(s.table)} SET ${e(s.column)} = CASE ${e(f)} ${a} END WHERE ${e(f)} IN ${e.in(r.map((e) => e.rowId))} AND (CASE ${o} ELSE 0 END) = 1`;
165
+ let c = yield* e.unsafe(`SELECT ${s.asText} AS v, ${l(f, n)} AS k FROM ${l(s.table, n)} WHERE ${l(f, n)} IN (${r.map((e) => m(e.rowId)).join(", ")})`).unprepared, d = new Map(c.map((e) => [String(e.k), t(e.v)]));
166
+ return r.map((e) => d.get(String(e.rowId)) === e.next);
167
+ }), _;
168
+ for (;;) {
169
+ let n = yield* e.unsafe(s.page(_, x)).unprepared, l = [];
170
+ for (let e of n) {
171
+ let n = t(e.__v), { __v: o, __k: u, ...p } = e, m = y(n, r, {
172
+ table: s.table,
173
+ column: s.column,
174
+ rowId: u,
175
+ row: p,
176
+ declared: i,
177
+ renames: a
178
+ });
179
+ d.cells += 1, d.current += m.current, d.unresolved += m.unresolved, m.rekeyed !== 0 && (f === void 0 ? d.unaddressable += m.rekeyed : c ? d.rekeyed += m.rekeyed : l.push({
180
+ rowId: u,
181
+ text: n,
182
+ next: m.text,
183
+ rekeyed: m.rekeyed
184
+ }));
185
+ }
186
+ if (l.length > 0) {
187
+ let e = yield* g(l).pipe(o.catch(() => o.forEach(l, (e) => h(e.rowId, e.text, e.next))));
188
+ l.forEach((t, n) => {
189
+ e[n] === !0 ? d.rekeyed += t.rekeyed : d.changedMeanwhile += 1;
190
+ });
191
+ }
192
+ if (f === void 0 || n.length < x) break;
193
+ _ = n[n.length - 1].__k;
194
+ }
195
+ return {
196
+ table: s.table,
197
+ column: s.column,
198
+ ...d
199
+ };
200
+ }), T = (e, a, s) => o.gen(function* () {
201
+ let o = yield* u.SqlClient, c = n(s), l = r(s), { columns: d, unreadable: f } = yield* i(e, c), p = [], m = 0;
202
+ for (let e of d) {
203
+ if (e.found === 0) continue;
204
+ let n = 0, r;
205
+ for (;;) {
206
+ let i = yield* o.unsafe(e.page(r, x)).unprepared;
207
+ for (let r of i) {
208
+ let { __v: i, __k: o, ...s } = r, u = b(t(i), a, {
209
+ table: e.table,
210
+ column: e.column,
211
+ rowId: o,
212
+ row: s,
213
+ declared: c,
214
+ renames: l
215
+ });
216
+ m += u.opened, n += u.unopened;
217
+ }
218
+ if (e.key === void 0 || i.length < x) break;
219
+ r = i[i.length - 1].__k;
220
+ }
221
+ n > 0 && p.push({
222
+ table: e.table,
223
+ column: e.column,
224
+ count: n
225
+ });
226
+ }
227
+ return {
228
+ opened: m,
229
+ unopened: p,
230
+ unreadable: f
231
+ };
232
+ }), E = (t) => {
233
+ let n = t.match(e);
234
+ return n !== null && n.length === 1 && n[0] === t;
235
+ }, D = (a, s, c) => o.gen(function* () {
236
+ let o = yield* u.SqlClient, d = n(c), f = r(c), { columns: p, unreadable: m, tables: h } = yield* i(a, d), g = [], _ = [], v = (e, t) => s !== void 0 && b(e, s, t).unopened === 0;
237
+ for (let n of p) {
238
+ if (n.found === 0) continue;
239
+ let r = 0, i = 0, c;
240
+ for (;;) {
241
+ let l = yield* o.unsafe(n.page(c, x)).unprepared;
242
+ for (let c of l) {
243
+ let l = t(c.__v), { __v: u, __k: p, ...m } = c, h = {
244
+ table: n.table,
245
+ column: n.column,
246
+ rowId: p,
247
+ row: m,
248
+ declared: d,
249
+ renames: f
250
+ };
251
+ if (E(l)) {
252
+ if (v(l, h)) continue;
253
+ if (n.key === void 0) {
254
+ i += 1;
255
+ continue;
256
+ }
257
+ yield* o`UPDATE ${o(n.table)} SET ${o(n.column)} = NULL WHERE ${o(n.key)} = ${p} AND ${S(a, n.column, n.type, o, l)}`, r += 1;
258
+ } else i += s === void 0 ? (l.match(e) ?? []).length : b(l, s, h).unopened;
259
+ }
260
+ if (n.key === void 0 || l.length < x) break;
261
+ c = l[l.length - 1].__k;
262
+ }
263
+ r > 0 && g.push({
264
+ table: n.table,
265
+ column: n.column,
266
+ count: r
267
+ }), i > 0 && _.push({
268
+ table: n.table,
269
+ column: n.column,
270
+ count: i
271
+ });
272
+ }
273
+ let y = new Set(s?.keyIds ?? []), C = (e) => l(e, a), w = [];
274
+ if (h.has("_voltro_field_keys")) {
275
+ w = (yield* o.unsafe(`SELECT ${C("keyId")} AS k FROM ${C("_voltro_field_keys")}`).unprepared).map((e) => String(e.k)).filter((e) => !y.has(e));
276
+ for (let e of w) yield* o`DELETE FROM ${o("_voltro_field_keys")} WHERE ${o("keyId")} = ${e}`;
277
+ }
278
+ return h.has("_voltro_field_key_holders") && (yield* o.unsafe(`DELETE FROM ${C("_voltro_field_key_holders")}`).unprepared), {
279
+ nulled: g,
280
+ unreadable: _,
281
+ forgottenKeys: w,
282
+ unreadableColumns: m
283
+ };
284
+ });
285
+ export { D as nullUnreadableSealedValues, b as openCell, y as rekeyCell, C as rekeySealedValues, T as verifySealedValues };