@voltro/runtime 0.86.0 → 0.87.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/CHANGELOG.md +5321 -0
- package/README.md +43 -0
- package/THIRD-PARTY-NOTICES.md +860 -221
- package/dist/analyticsSink.js +8 -14
- package/dist/apiKeys.js +82 -78
- package/dist/apiRoleBucketSampler.js +64 -0
- package/dist/apiRoleWorkloadHistory.js +56 -0
- package/dist/apiRoleWorkloadMetrics.js +78 -0
- package/dist/apiRoleWorkloadStorage.js +95 -0
- package/dist/approvals.js +67 -74
- package/dist/authMiddlewareLayer.js +7 -6
- package/dist/bindEvent.js +48 -28
- package/dist/bootListener.js +106 -39
- package/dist/bootStoreCodec.js +38 -37
- package/dist/circuitBreaker.js +1 -72
- package/dist/composeAnalytics.js +31 -10
- package/dist/computedQueryCache.js +231 -0
- package/dist/connectionsFacade.js +32 -30
- package/dist/coordinatedSchedule.js +84 -78
- package/dist/dataStore.js +154 -121
- package/dist/declaredTablesGuard.js +2 -1
- package/dist/dispatcher.js +403 -328
- package/dist/effectStore.js +2 -2
- package/dist/engineClients.js +24 -0
- package/dist/externalizedTasks.js +19 -6
- package/dist/fieldCipher.js +109 -40
- package/dist/fieldKeyRegistry.js +295 -0
- package/dist/fieldKeyRekey.js +286 -0
- package/dist/fieldKeyScan.js +136 -0
- package/dist/fieldKeyring.js +77 -0
- package/dist/hot.d.ts +56 -0
- package/dist/hot.js +3 -0
- package/dist/hotDataStore.js +148 -0
- package/dist/hotDataStoreSnapshot.js +98 -0
- package/dist/hotDataStoreTables.js +18 -0
- package/dist/http-range.d.ts +53 -0
- package/dist/http-range.js +52 -0
- package/dist/httpClientSsrf.js +12 -37
- package/dist/httpClientTracing.js +12 -9
- package/dist/httpCompression.js +17 -17
- package/dist/httpInputStream.js +82 -0
- package/dist/httpRouteMounts.js +1 -0
- package/dist/idempotencyStore.js +92 -52
- package/dist/index.d.ts +1164 -451
- package/dist/index.js +143 -132
- package/dist/kvFacade.js +23 -17
- package/dist/memoryQueryJoins.js +16 -16
- package/dist/memorySetOperations.js +43 -0
- package/dist/metrics.js +38 -24
- package/dist/outbox.js +1 -1
- package/dist/readRecorder.js +35 -34
- package/dist/readScopePredicate.js +12 -0
- package/dist/replicatedDataStore.js +22 -4
- package/dist/rowFilter.js +56 -39
- package/dist/rpcConnections.js +8 -8
- package/dist/rpcServer.js +522 -449
- package/dist/rpcWorkloadAdmission.js +50 -0
- package/dist/rywPositionStore.js +82 -47
- package/dist/scheduler.js +6 -1
- package/dist/servePipeline.js +215 -146
- package/dist/setReadScopes.js +28 -0
- package/dist/snapshotValueCodec.js +46 -0
- package/dist/sourceResolution.js +34 -24
- package/dist/storeMiddleware.js +211 -209
- package/dist/subscriber.js +1 -0
- package/dist/tenantScope.js +8 -20
- package/dist/traceparent.js +8 -19
- package/dist/webSocketGatewayRuntime.js +111 -71
- package/dist/workflowRuntime.js +9 -9
- package/package.json +19 -7
package/dist/effectStore.js
CHANGED
|
@@ -35,7 +35,7 @@ var l = c({ scope: "store" }), u = class extends e.Service()("@voltro/EffectStor
|
|
|
35
35
|
}, v = (e) => ({
|
|
36
36
|
query: ((t) => m("query", t.table, () => e.query(t))),
|
|
37
37
|
insert: (t, n) => m("insert", t, () => e.insert(t, n)),
|
|
38
|
-
insertMany: (t, n) => m("insertMany", t, () => e.insertMany(t, n)),
|
|
38
|
+
insertMany: (t, n, r) => m("insertMany", t, () => e.insertMany(t, n, r)),
|
|
39
39
|
patchJson: (t, n, r, i, a) => m("patchJson", t, () => e.patchJson(t, n, r, i, a)),
|
|
40
40
|
update: (t, n, r) => m("update", t, () => e.update(t, n, r)),
|
|
41
41
|
updateMany: (t, n, r) => m("updateMany", t, () => e.updateMany(t, n, r)),
|
|
@@ -66,6 +66,6 @@ var l = c({ scope: "store" }), u = class extends e.Service()("@voltro/EffectStor
|
|
|
66
66
|
conflictColumns: e.conflictColumns,
|
|
67
67
|
...e.update === void 0 ? {} : { update: e.update },
|
|
68
68
|
...e.updateValues === void 0 ? {} : { updateValues: e.updateValues }
|
|
69
|
-
}), x = (e, t, n) => e.insert(t.tableName, n), S = (e, t, n) => e.insertMany(t.tableName, n), C = (e, t, n, r) => e.upsert(t.tableName, n, b(r)), w = (e, t, n, r) => e.upsertWithOutcome(t.tableName, n, b(r)), T = (e, t, n, r) => e.insertIgnoreWithOutcome(t.tableName, n, { conflictColumns: r.conflictColumns });
|
|
69
|
+
}), x = (e, t, n) => e.insert(t.tableName, n), S = (e, t, n, r) => e.insertMany(t.tableName, n, r), C = (e, t, n, r) => e.upsert(t.tableName, n, b(r)), w = (e, t, n, r) => e.upsertWithOutcome(t.tableName, n, b(r)), T = (e, t, n, r) => e.insertIgnoreWithOutcome(t.tableName, n, { conflictColumns: r.conflictColumns });
|
|
70
70
|
//#endregion
|
|
71
71
|
export { u as EffectStore, T as insertIgnoreRowOutcomeEffect, S as insertManyRowsEffect, x as insertRowEffect, y as makeEffectStoreLayer, C as upsertRowEffect, w as upsertRowOutcomeEffect };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/engineClients.ts
|
|
2
|
+
var e = /* @__PURE__ */ new Map(), t = 0, n = (e) => {
|
|
3
|
+
let t = /^([a-z]{1,16})\/([0-9A-Za-z.+-]{1,100})(?:; surface=([0-9a-f]{64}))?$/.exec(e.trim());
|
|
4
|
+
return t ? {
|
|
5
|
+
target: t[1],
|
|
6
|
+
version: t[2],
|
|
7
|
+
surface: t[3] ?? null
|
|
8
|
+
} : null;
|
|
9
|
+
}, r = (r, i) => {
|
|
10
|
+
let a = r.headers["x-voltro-sdk"];
|
|
11
|
+
if (typeof a != "string" || e.size >= 1024) return;
|
|
12
|
+
let o = n(a);
|
|
13
|
+
if (!o) return;
|
|
14
|
+
let s = "client-" + String(++t);
|
|
15
|
+
e.set(s, Object.freeze({
|
|
16
|
+
id: s,
|
|
17
|
+
...o,
|
|
18
|
+
connectedAt: Date.now()
|
|
19
|
+
})), i.once("close", () => {
|
|
20
|
+
e.delete(s);
|
|
21
|
+
});
|
|
22
|
+
}, i = () => [...e.values()];
|
|
23
|
+
//#endregion
|
|
24
|
+
export { i as engineClientsSnapshot, n as parseEngineSdkHeader, r as trackEngineClient };
|
|
@@ -16,10 +16,23 @@ var e = 6e4, t = (e) => {
|
|
|
16
16
|
if (t !== void 0) return t;
|
|
17
17
|
let n = /* @__PURE__ */ new Map();
|
|
18
18
|
return e[r] = n, n;
|
|
19
|
-
}, a = (e) => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
}, a = (e) => Object.freeze({
|
|
20
|
+
register: (t) => {
|
|
21
|
+
let n = Object.freeze({
|
|
22
|
+
name: t.name,
|
|
23
|
+
intervalMs: t.intervalMs,
|
|
24
|
+
cron: t.cron,
|
|
25
|
+
run: t.run
|
|
26
|
+
});
|
|
27
|
+
if (e.has(n.name)) throw Error("Externalized task name is already registered: " + n.name);
|
|
28
|
+
e.set(n.name, n);
|
|
29
|
+
let r = !1;
|
|
30
|
+
return () => {
|
|
31
|
+
r || (r = !0, e.get(n.name) === n && e.delete(n.name));
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
get: (t) => e.get(t),
|
|
35
|
+
names: () => [...e.keys()].sort()
|
|
36
|
+
}), o = () => a(/* @__PURE__ */ new Map()), s = (e) => a(i()).register(e), c = (e) => i().get(e), l = () => [...i().keys()].sort();
|
|
24
37
|
//#endregion
|
|
25
|
-
export { e as MIN_EXTERNALIZABLE_INTERVAL_MS, t as cronForInterval, n as externalizationRefusal,
|
|
38
|
+
export { e as MIN_EXTERNALIZABLE_INTERVAL_MS, t as cronForInterval, n as externalizationRefusal, c as externalizedTask, l as externalizedTaskNames, o as makeExternalizedTaskRegistry, s as registerExternalizedTask };
|
package/dist/fieldCipher.js
CHANGED
|
@@ -1,61 +1,130 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { recordFieldDecrypt as e } from "./metrics.js";
|
|
2
|
+
import { BOUND_ENCRYPTED_PREFIX as t, ENCRYPTED_PREFIX as n, FIELD_KEY_ID_PATTERN as r, ciphertextKeyId as i, decodeFieldValue as a, encodeFieldValue as o, isEncrypted as s } from "@voltro/database";
|
|
3
|
+
import { createCipheriv as c, createDecipheriv as l, createHmac as u, randomBytes as d, scryptSync as f } from "node:crypto";
|
|
3
4
|
//#region src/fieldCipher.ts
|
|
4
|
-
var
|
|
5
|
+
var p = (e) => /^[0-9a-f]{64}$/i.test(e) ? Buffer.from(e, "hex") : f(e, "voltro:field-encryption:v1", 32), m = (e, t) => {
|
|
5
6
|
let n = e === "r" ? t?.row : e === "c" ? t?.column : void 0;
|
|
6
7
|
if (n === void 0) throw Error(e === "r" || e === "c" ? `field cipher: ciphertext is bound to its ${e === "r" ? "row" : "column"} and was read without that binding` : "field cipher: malformed ciphertext");
|
|
7
8
|
return Buffer.from(n, "utf8");
|
|
8
|
-
},
|
|
9
|
-
if (
|
|
9
|
+
}, h = (e, i) => {
|
|
10
|
+
if (e.length !== 32) throw Error(`field cipher key must be 32 bytes, got ${e.length}`);
|
|
11
|
+
if (i !== void 0 && !r.test(i)) throw Error(`field cipher: "${i}" is not a key id (a short lowercase slug)`);
|
|
12
|
+
let a = i === void 0 ? "" : `:${i}`;
|
|
10
13
|
return {
|
|
11
14
|
encrypt: (r, i) => {
|
|
12
|
-
let o =
|
|
13
|
-
l !== void 0 &&
|
|
14
|
-
let
|
|
15
|
-
return l === void 0 ? `${
|
|
15
|
+
let o = d(12), s = c("aes-256-gcm", e, o), l = i === void 0 ? void 0 : i.row === void 0 ? "c" : "r";
|
|
16
|
+
l !== void 0 && s.setAAD(m(l, i));
|
|
17
|
+
let u = Buffer.concat([s.update(r, "utf8"), s.final()]), f = s.getAuthTag(), p = `${o.toString("base64")}:${f.toString("base64")}:${u.toString("base64")}`;
|
|
18
|
+
return l === void 0 ? `${n}${p}${a}` : `${t}${l}:${p}${a}`;
|
|
16
19
|
},
|
|
17
|
-
decrypt: (
|
|
18
|
-
let
|
|
19
|
-
if (!
|
|
20
|
-
let
|
|
21
|
-
return c !== void 0 &&
|
|
20
|
+
decrypt: (i, a) => {
|
|
21
|
+
let o = i.startsWith(t), s = i.slice(o ? t.length : n.length).split(":"), c = o ? s.shift() ?? "" : void 0, [u, d, f, ...p] = s;
|
|
22
|
+
if (!u || !d || f === void 0 || p.length > 1 || p.length === 1 && !r.test(p[0])) throw Error("field cipher: malformed ciphertext");
|
|
23
|
+
let h = l("aes-256-gcm", e, Buffer.from(u, "base64"));
|
|
24
|
+
return c !== void 0 && h.setAAD(m(c, a)), h.setAuthTag(Buffer.from(d, "base64")), Buffer.concat([h.update(Buffer.from(f, "base64")), h.final()]).toString("utf8");
|
|
22
25
|
}
|
|
23
26
|
};
|
|
24
|
-
},
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
}, g = (e) => `k${u("sha256", p(e)).update("voltro:field-key-id:v1").digest("hex").slice(0, 10)}`, _ = class extends Error {
|
|
28
|
+
_tag = "FieldKeyringConfigError";
|
|
29
|
+
constructor(e) {
|
|
30
|
+
super(e), this.name = "FieldKeyringConfigError";
|
|
31
|
+
}
|
|
32
|
+
}, v = (e) => e !== void 0 && typeof e.sealWith == "function" && typeof e.activate == "function", y = (t, n = {}) => {
|
|
33
|
+
let a = /* @__PURE__ */ new Map(), o = /* @__PURE__ */ new Map();
|
|
34
|
+
for (let e of t.keys) {
|
|
35
|
+
if (!r.test(e.id)) throw new _(`field encryption: "${e.id}" is not a key id — use a short lowercase slug (letters, digits, "-", at most 16)`);
|
|
36
|
+
if (e.secret.trim() === "") throw new _(`field encryption: key "${e.id}" is empty`);
|
|
37
|
+
if (a.has(e.id)) throw new _(`field encryption: key id "${e.id}" is configured twice`);
|
|
38
|
+
let t = p(e.secret).toString("hex"), n = o.get(t);
|
|
39
|
+
if (n !== void 0) throw new _(`field encryption: keys "${n}" and "${e.id}" are the same key — a rotation needs a NEW key, and two ids for one key make every count per key wrong`);
|
|
40
|
+
a.set(e.id, e.secret), o.set(t, e.id);
|
|
41
|
+
}
|
|
42
|
+
if (!a.has(t.active)) throw new _(`field encryption: the active key "${t.active}" is not one of the configured keys (${[...a.keys()].join(", ") || "none"})`);
|
|
43
|
+
let s = new Map([...a].map(([e, t]) => [e, h(p(t), e)])), c = t.active, l = n.sealOnlyOnceActivated !== !0, u = [], d = () => {
|
|
44
|
+
u = [[c, s.get(c)], ...[...s].filter(([e]) => e !== c)];
|
|
45
|
+
};
|
|
46
|
+
d();
|
|
47
|
+
let f = (e, t, n) => {
|
|
28
48
|
try {
|
|
29
49
|
return e.decrypt(t, n), !0;
|
|
30
50
|
} catch {
|
|
31
51
|
return !1;
|
|
32
52
|
}
|
|
53
|
+
}, m = (e, t) => {
|
|
54
|
+
let n = i(e), r = n === void 0 ? void 0 : s.get(n);
|
|
55
|
+
return r !== void 0 && f(r, e, t) ? n : u.find(([, n]) => n !== r && f(n, e, t))?.[0];
|
|
33
56
|
};
|
|
34
57
|
return {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
58
|
+
get activeKeyId() {
|
|
59
|
+
return c;
|
|
60
|
+
},
|
|
61
|
+
requestedKeyId: t.active,
|
|
62
|
+
keyIds: u.map(([e]) => e),
|
|
63
|
+
fingerprints: Object.freeze(Object.fromEntries([...a].map(([e, t]) => [e, g(t)]))),
|
|
64
|
+
encrypt: (e, t) => {
|
|
65
|
+
if (!l) throw Error("field cipher: nothing may be sealed before the boot has checked the keys against the database (verifyFieldKeyring decides which key is active) — a write reached an .encrypted() column during boot, before that check");
|
|
66
|
+
return s.get(c).encrypt(e, t);
|
|
67
|
+
},
|
|
68
|
+
sealWith: (e, t, n) => {
|
|
69
|
+
let r = s.get(e);
|
|
70
|
+
if (r === void 0) throw Error(`field cipher: this keyring does not hold key "${e}"`);
|
|
71
|
+
return r.encrypt(t, n);
|
|
72
|
+
},
|
|
73
|
+
opensWith: (e, t, n) => {
|
|
74
|
+
let r = s.get(e);
|
|
75
|
+
return r !== void 0 && f(r, t, n);
|
|
76
|
+
},
|
|
77
|
+
activate: (e) => {
|
|
78
|
+
if (!s.has(e)) throw Error(`field cipher: cannot activate key "${e}", which this keyring does not hold`);
|
|
79
|
+
c = e, l = !0, d();
|
|
80
|
+
},
|
|
81
|
+
decrypt: (t, n) => {
|
|
82
|
+
let r = i(t), a = r === void 0 ? void 0 : s.get(r), o = n?.column ?? "unbound";
|
|
83
|
+
if (r !== void 0 && a === void 0 && u.every(([, e]) => !f(e, t, n))) throw e(r, o, "failed"), Error(`field cipher: the value is sealed under key "${r}", which this keyring does not hold (it holds ${u.map(([e]) => e).join(", ")})`);
|
|
84
|
+
let c;
|
|
85
|
+
if (a !== void 0) try {
|
|
86
|
+
let i = a.decrypt(t, n);
|
|
87
|
+
return e(r, o, "ok"), i;
|
|
88
|
+
} catch (e) {
|
|
89
|
+
c = e;
|
|
44
90
|
}
|
|
91
|
+
for (let [r, i] of u) if (i !== a) try {
|
|
92
|
+
let a = i.decrypt(t, n);
|
|
93
|
+
return e(r, o, "ok"), a;
|
|
94
|
+
} catch (e) {
|
|
95
|
+
c ??= e;
|
|
96
|
+
}
|
|
97
|
+
throw e(r ?? "unnamed", o, "failed"), c ?? /* @__PURE__ */ Error("field cipher: malformed ciphertext");
|
|
45
98
|
},
|
|
46
|
-
|
|
99
|
+
keyIdOf: m,
|
|
100
|
+
sealedWithPreviousKey: (e, t) => {
|
|
101
|
+
let n = m(e, t);
|
|
102
|
+
return n !== void 0 && n !== c;
|
|
103
|
+
}
|
|
47
104
|
};
|
|
48
|
-
},
|
|
49
|
-
p = e;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
105
|
+
}, b = (e, t = []) => {
|
|
106
|
+
let n = p(e).toString("hex"), r = [...new Map(t.filter((e) => e.trim() !== "" && p(e).toString("hex") !== n).map((e) => [p(e).toString("hex"), e])).values()];
|
|
107
|
+
return y({
|
|
108
|
+
active: g(e),
|
|
109
|
+
keys: [{
|
|
110
|
+
id: g(e),
|
|
111
|
+
secret: e
|
|
112
|
+
}, ...r.map((e) => ({
|
|
113
|
+
id: g(e),
|
|
114
|
+
secret: e
|
|
115
|
+
}))]
|
|
116
|
+
});
|
|
117
|
+
}, x, S = (e) => {
|
|
118
|
+
x = e;
|
|
119
|
+
}, C = () => x, w = "no field cipher registered — set VOLTRO_FIELD_ENCRYPTION_KEY (`voltro secret generate field-encryption`; `voltro dev` mints one), or call setFieldCipher.", T = (e, t) => {
|
|
120
|
+
let n = C();
|
|
121
|
+
if (!n) throw Error(`encryptField: ${w}`);
|
|
122
|
+
return o(n, e, t);
|
|
123
|
+
}, E = (e, t) => {
|
|
124
|
+
if (!s(e)) return e;
|
|
125
|
+
let n = C();
|
|
126
|
+
if (!n) throw Error(`decryptField: ${w}`);
|
|
127
|
+
return a(n, e, void 0, t);
|
|
59
128
|
};
|
|
60
129
|
//#endregion
|
|
61
|
-
export {
|
|
130
|
+
export { _ as FieldKeyringConfigError, E as decryptField, p as deriveKey, T as encryptField, g as fieldKeyIdOf, C as getFieldCipher, v as isFieldKeyring, h as makeAesCipher, b as makeFieldCipher, y as makeFieldKeyring, S as setFieldCipher };
|
|
@@ -0,0 +1,295 @@
|
|
|
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
|
+
//#region 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
|
+
//#endregion
|
|
295
|
+
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 };
|