@volidator/node 1.0.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/README.md +518 -0
- package/dist/chunk-KEXIKZGN.js +56 -0
- package/dist/chunk-KEXIKZGN.js.map +1 -0
- package/dist/chunk-YI4DCCHT.js +663 -0
- package/dist/chunk-YI4DCCHT.js.map +1 -0
- package/dist/index.cjs +689 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +250 -0
- package/dist/index.d.ts +250 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/next.cjs +735 -0
- package/dist/middleware/next.cjs.map +1 -0
- package/dist/middleware/next.d.cts +12 -0
- package/dist/middleware/next.d.ts +12 -0
- package/dist/middleware/next.js +8 -0
- package/dist/middleware/next.js.map +1 -0
- package/dist/plugins/clerk.cjs +762 -0
- package/dist/plugins/clerk.cjs.map +1 -0
- package/dist/plugins/clerk.d.cts +15 -0
- package/dist/plugins/clerk.d.ts +15 -0
- package/dist/plugins/clerk.js +35 -0
- package/dist/plugins/clerk.js.map +1 -0
- package/dist/plugins/universal.cjs +774 -0
- package/dist/plugins/universal.cjs.map +1 -0
- package/dist/plugins/universal.d.cts +22 -0
- package/dist/plugins/universal.d.ts +22 -0
- package/dist/plugins/universal.js +47 -0
- package/dist/plugins/universal.js.map +1 -0
- package/package.json +74 -0
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/plugins/universal.ts
|
|
21
|
+
var universal_exports = {};
|
|
22
|
+
__export(universal_exports, {
|
|
23
|
+
createUniversalAudit: () => createUniversalAudit
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(universal_exports);
|
|
26
|
+
|
|
27
|
+
// src/index.ts
|
|
28
|
+
var VolidatorClient = class _VolidatorClient {
|
|
29
|
+
constructor(config) {
|
|
30
|
+
this.apiKey = config.apiKey;
|
|
31
|
+
this.endpoint = config.endpoint || "https://ingestion.volidator.com";
|
|
32
|
+
this.projectId = config.projectId;
|
|
33
|
+
this.clientSecret = config.clientSecret;
|
|
34
|
+
this.redactKeys = config.redactKeys || [];
|
|
35
|
+
this.referenceKeys = config.referenceKeys || [];
|
|
36
|
+
this.maxMetadataSize = config.maxMetadataSize || 10240;
|
|
37
|
+
if (config.keyring && config.activeEncryptionKeyId) {
|
|
38
|
+
this.keyring = config.keyring;
|
|
39
|
+
this.activeKeyId = config.activeEncryptionKeyId;
|
|
40
|
+
} else if (config.encryptionKey) {
|
|
41
|
+
this.activeKeyId = "v1";
|
|
42
|
+
this.keyring = { "v1": config.encryptionKey };
|
|
43
|
+
} else {
|
|
44
|
+
throw new Error("Either encryptionKey OR (keyring AND activeEncryptionKeyId) must be provided in VolidatorClient constructor.");
|
|
45
|
+
}
|
|
46
|
+
if (Object.keys(this.keyring).length > 5) {
|
|
47
|
+
throw new Error("Keyring size cannot exceed 5 keys.");
|
|
48
|
+
}
|
|
49
|
+
if (!this.keyring[this.activeKeyId]) {
|
|
50
|
+
throw new Error(`Active key ID '${this.activeKeyId}' must exist in the keyring.`);
|
|
51
|
+
}
|
|
52
|
+
this.hashedKeyring = {};
|
|
53
|
+
this.telemetryConfig = _VolidatorClient.resolveTelemetryConfig(config.telemetry || { preset: "standard" });
|
|
54
|
+
this.compliance = new VolidatorCompliance(this);
|
|
55
|
+
this.agent = new VolidatorAgent(this);
|
|
56
|
+
}
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Telemetry Context Parser (Zero-latency header parsing)
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
static extractContext(req) {
|
|
61
|
+
const getHeader = (name) => {
|
|
62
|
+
if (!req) return "";
|
|
63
|
+
if (typeof req.headers?.get === "function") {
|
|
64
|
+
return req.headers.get(name) || "";
|
|
65
|
+
}
|
|
66
|
+
if (req.headers && typeof req.headers === "object") {
|
|
67
|
+
return req.headers[name] || req.headers[name.toLowerCase()] || "";
|
|
68
|
+
}
|
|
69
|
+
return "";
|
|
70
|
+
};
|
|
71
|
+
const rawIp = getHeader("cf-connecting-ip") || getHeader("x-real-ip") || getHeader("x-forwarded-for");
|
|
72
|
+
const ip = rawIp ? rawIp.split(",")[0].trim() : req?.socket?.remoteAddress || "";
|
|
73
|
+
const userAgent = getHeader("user-agent");
|
|
74
|
+
return {
|
|
75
|
+
ip,
|
|
76
|
+
userAgent,
|
|
77
|
+
location: {
|
|
78
|
+
country: getHeader("cf-ipcountry") || getHeader("x-vercel-ip-country") || "",
|
|
79
|
+
region: getHeader("cf-region-code") || getHeader("x-vercel-ip-country-region") || "",
|
|
80
|
+
city: getHeader("cf-ipcity") || getHeader("x-vercel-ip-city") || ""
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// OpenTelemetry W3C traceparent context parser
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
static extractTraceContext(req) {
|
|
88
|
+
if (!req) return {};
|
|
89
|
+
const getHeader = (name) => {
|
|
90
|
+
if (typeof req.headers?.get === "function") {
|
|
91
|
+
return req.headers.get(name) || "";
|
|
92
|
+
}
|
|
93
|
+
if (req.headers && typeof req.headers === "object") {
|
|
94
|
+
return req.headers[name] || req.headers[name.toLowerCase()] || "";
|
|
95
|
+
}
|
|
96
|
+
return "";
|
|
97
|
+
};
|
|
98
|
+
const traceparent = getHeader("traceparent");
|
|
99
|
+
if (!traceparent) return {};
|
|
100
|
+
const parts = traceparent.split("-");
|
|
101
|
+
if (parts.length !== 4) return {};
|
|
102
|
+
return { traceId: parts[1], spanId: parts[2] };
|
|
103
|
+
}
|
|
104
|
+
static resolveTelemetryConfig(config) {
|
|
105
|
+
const preset = config.preset || "standard";
|
|
106
|
+
let ip = "anonymize";
|
|
107
|
+
let userAgent = "parse";
|
|
108
|
+
let location = true;
|
|
109
|
+
if (preset === "strict") {
|
|
110
|
+
ip = "skip";
|
|
111
|
+
userAgent = "skip";
|
|
112
|
+
location = false;
|
|
113
|
+
} else if (preset === "full") {
|
|
114
|
+
ip = "track";
|
|
115
|
+
userAgent = "track";
|
|
116
|
+
location = true;
|
|
117
|
+
}
|
|
118
|
+
if (config.ip !== void 0) ip = config.ip;
|
|
119
|
+
if (config.userAgent !== void 0) userAgent = config.userAgent;
|
|
120
|
+
if (config.location !== void 0) location = config.location;
|
|
121
|
+
return { ip, userAgent, location };
|
|
122
|
+
}
|
|
123
|
+
parseUserAgent(ua) {
|
|
124
|
+
let browser = "Unknown Browser";
|
|
125
|
+
let os = "Unknown OS";
|
|
126
|
+
let type = "Desktop";
|
|
127
|
+
if (/Volidator-Ingest-Worker/i.test(ua)) {
|
|
128
|
+
return {
|
|
129
|
+
browser: "Volidator Ingest Worker",
|
|
130
|
+
os: "Linux Server",
|
|
131
|
+
type: "Server"
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (/mobile/i.test(ua)) {
|
|
135
|
+
type = "Mobile";
|
|
136
|
+
} else if (/tablet|ipad/i.test(ua)) {
|
|
137
|
+
type = "Tablet";
|
|
138
|
+
} else if (/server|bot/i.test(ua)) {
|
|
139
|
+
type = "Server";
|
|
140
|
+
}
|
|
141
|
+
if (/chrome|crios/i.test(ua) && !/edge|edg/i.test(ua) && !/opr/i.test(ua)) {
|
|
142
|
+
const match = ua.match(/(?:chrome|crios)\/([0-9\.]+)/i);
|
|
143
|
+
browser = `Chrome ${match ? match[1].split(".")[0] : ""}`.trim();
|
|
144
|
+
} else if (/safari/i.test(ua) && !/chrome|crios/i.test(ua) && !/android/i.test(ua)) {
|
|
145
|
+
browser = /mobile/i.test(ua) ? "Safari Mobile" : "Safari";
|
|
146
|
+
} else if (/firefox|fxios/i.test(ua)) {
|
|
147
|
+
browser = "Firefox";
|
|
148
|
+
} else if (/edge|edg/i.test(ua)) {
|
|
149
|
+
browser = "Edge";
|
|
150
|
+
}
|
|
151
|
+
if (/windows/i.test(ua)) {
|
|
152
|
+
os = "Windows";
|
|
153
|
+
} else if (/macintosh|mac os x/i.test(ua)) {
|
|
154
|
+
os = "macOS";
|
|
155
|
+
} else if (/iphone|ipad|ipod/i.test(ua)) {
|
|
156
|
+
os = "iOS";
|
|
157
|
+
} else if (/android/i.test(ua)) {
|
|
158
|
+
os = "Android";
|
|
159
|
+
} else if (/linux/i.test(ua)) {
|
|
160
|
+
os = "Linux";
|
|
161
|
+
}
|
|
162
|
+
return { browser, os, type };
|
|
163
|
+
}
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Blind Index & Key Hashing (WebCrypto)
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
async _getHashedKey(id) {
|
|
168
|
+
if (this.hashedKeyring[id]) return this.hashedKeyring[id];
|
|
169
|
+
const rawKey = this.keyring[id];
|
|
170
|
+
const buf = new TextEncoder().encode(rawKey);
|
|
171
|
+
const hash = await globalThis.crypto.subtle.digest("SHA-256", buf);
|
|
172
|
+
this.hashedKeyring[id] = new Uint8Array(hash);
|
|
173
|
+
return this.hashedKeyring[id];
|
|
174
|
+
}
|
|
175
|
+
async generateBlindIndex(value, keyBuffer) {
|
|
176
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
177
|
+
"raw",
|
|
178
|
+
keyBuffer,
|
|
179
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
180
|
+
false,
|
|
181
|
+
["sign"]
|
|
182
|
+
);
|
|
183
|
+
const signature = await globalThis.crypto.subtle.sign(
|
|
184
|
+
"HMAC",
|
|
185
|
+
key,
|
|
186
|
+
new TextEncoder().encode(value)
|
|
187
|
+
);
|
|
188
|
+
return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
189
|
+
}
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Payload Encryption — AES-256-GCM with prepended 12-byte IV
|
|
192
|
+
// Layout: [IV (12B)] [Ciphertext] [Auth Tag (16B)]
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
async encryptPayload(payload) {
|
|
195
|
+
const text = JSON.stringify(payload);
|
|
196
|
+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
197
|
+
const activeHashedKey = await this._getHashedKey(this.activeKeyId);
|
|
198
|
+
const cryptoKey = await globalThis.crypto.subtle.importKey(
|
|
199
|
+
"raw",
|
|
200
|
+
activeHashedKey,
|
|
201
|
+
{ name: "AES-GCM" },
|
|
202
|
+
false,
|
|
203
|
+
["encrypt"]
|
|
204
|
+
);
|
|
205
|
+
const encryptedBuffer = await globalThis.crypto.subtle.encrypt(
|
|
206
|
+
{ name: "AES-GCM", iv },
|
|
207
|
+
cryptoKey,
|
|
208
|
+
new TextEncoder().encode(text)
|
|
209
|
+
);
|
|
210
|
+
const finalBuffer = new Uint8Array(iv.length + encryptedBuffer.byteLength);
|
|
211
|
+
finalBuffer.set(iv, 0);
|
|
212
|
+
finalBuffer.set(new Uint8Array(encryptedBuffer), iv.length);
|
|
213
|
+
let binary = "";
|
|
214
|
+
const len = finalBuffer.byteLength;
|
|
215
|
+
for (let i = 0; i < len; i++) {
|
|
216
|
+
binary += String.fromCharCode(finalBuffer[i]);
|
|
217
|
+
}
|
|
218
|
+
const base64 = btoa(binary);
|
|
219
|
+
return `${this.activeKeyId}:${base64}`;
|
|
220
|
+
}
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// prepareLogEntry() — Prepares, redacts, and encrypts a single log entry payload
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
async prepareLogEntry(payload, maxMetaOverride) {
|
|
225
|
+
const actorRaw = payload.actor || payload.actorId || "unknown";
|
|
226
|
+
const targetRaw = payload.target || payload.targetId || "unknown";
|
|
227
|
+
const tenantRaw = payload.tenant || payload.tenantId || "";
|
|
228
|
+
const truncate = (str, maxLen) => {
|
|
229
|
+
if (str.length > maxLen) {
|
|
230
|
+
return str.slice(0, maxLen) + "...";
|
|
231
|
+
}
|
|
232
|
+
return str;
|
|
233
|
+
};
|
|
234
|
+
const extractPii = (v) => typeof v === "object" ? v.pii : v;
|
|
235
|
+
const extractId = (v) => typeof v === "object" ? v.id : v;
|
|
236
|
+
const actor = truncate(extractPii(actorRaw), 255);
|
|
237
|
+
const target = truncate(extractPii(targetRaw), 255);
|
|
238
|
+
const tenant = tenantRaw ? truncate(extractPii(tenantRaw), 255) : "";
|
|
239
|
+
const action = truncate(payload.action, 255);
|
|
240
|
+
const logTelemetry = payload.telemetry ? _VolidatorClient.resolveTelemetryConfig({ ...this.telemetryConfig, ...payload.telemetry }) : this.telemetryConfig;
|
|
241
|
+
let payloadCtx = payload.context || {};
|
|
242
|
+
if (payload.req) {
|
|
243
|
+
const extracted = _VolidatorClient.extractContext(payload.req);
|
|
244
|
+
payloadCtx = {
|
|
245
|
+
...extracted,
|
|
246
|
+
...payloadCtx,
|
|
247
|
+
location: {
|
|
248
|
+
...extracted.location,
|
|
249
|
+
...payloadCtx.location
|
|
250
|
+
},
|
|
251
|
+
device: {
|
|
252
|
+
...extracted.device,
|
|
253
|
+
...payloadCtx.device
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
const context = {};
|
|
258
|
+
const rawIp = payloadCtx.ip || "";
|
|
259
|
+
const rawUa = payloadCtx.userAgent || "";
|
|
260
|
+
if (logTelemetry.location) {
|
|
261
|
+
context.location = {};
|
|
262
|
+
if (payloadCtx.location) {
|
|
263
|
+
context.location.country = payloadCtx.location.country || "";
|
|
264
|
+
context.location.region = payloadCtx.location.region || "";
|
|
265
|
+
const isFull = payload.telemetry?.preset === "full" || !payload.telemetry && this.telemetryConfig.ip === "track";
|
|
266
|
+
if (logTelemetry.ip === "track" || isFull) {
|
|
267
|
+
context.location.city = payloadCtx.location.city || "";
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (logTelemetry.ip === "anonymize" && rawIp) {
|
|
272
|
+
context.ip = await this.generateBlindIndex(rawIp, await this._getHashedKey(this.activeKeyId));
|
|
273
|
+
} else if (logTelemetry.ip === "track" && rawIp) {
|
|
274
|
+
context.ip = rawIp;
|
|
275
|
+
}
|
|
276
|
+
if (logTelemetry.userAgent !== "skip") {
|
|
277
|
+
if (rawUa) {
|
|
278
|
+
context.device = this.parseUserAgent(rawUa);
|
|
279
|
+
if (logTelemetry.userAgent === "track") {
|
|
280
|
+
context.userAgent = truncate(rawUa, 1e3);
|
|
281
|
+
}
|
|
282
|
+
} else if (payloadCtx.device) {
|
|
283
|
+
context.device = payloadCtx.device;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const scrub = (value, key) => this.redactKeys.includes(key) ? `[REDACTED:${key}]` : value;
|
|
287
|
+
const applyRef = (rawValue, key) => {
|
|
288
|
+
if (this.referenceKeys.includes(key)) {
|
|
289
|
+
const id = extractId(rawValue);
|
|
290
|
+
return `[REF:${id}]`;
|
|
291
|
+
}
|
|
292
|
+
return scrub(extractPii(rawValue), key);
|
|
293
|
+
};
|
|
294
|
+
const rawMetadata = payload.metadata || {};
|
|
295
|
+
const safeMetadata = {};
|
|
296
|
+
for (const [k, v] of Object.entries(rawMetadata)) {
|
|
297
|
+
const metaKey = `metadata.${k}`;
|
|
298
|
+
if (this.referenceKeys.includes(metaKey) && typeof v === "object" && v !== null && "id" in v) {
|
|
299
|
+
safeMetadata[k] = `[REF:${v.id}]`;
|
|
300
|
+
} else if (this.redactKeys.includes(metaKey) && typeof v === "string") {
|
|
301
|
+
safeMetadata[k] = `[REDACTED:${k}]`;
|
|
302
|
+
} else {
|
|
303
|
+
safeMetadata[k] = v;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const limitDepth = (obj, currentDepth = 1) => {
|
|
307
|
+
if (currentDepth > 5) {
|
|
308
|
+
return "[Truncated - Depth Exceeded]";
|
|
309
|
+
}
|
|
310
|
+
if (typeof obj !== "object" || obj === null) {
|
|
311
|
+
if (typeof obj === "string") {
|
|
312
|
+
return truncate(obj, 1e3);
|
|
313
|
+
}
|
|
314
|
+
return obj;
|
|
315
|
+
}
|
|
316
|
+
if (Array.isArray(obj)) {
|
|
317
|
+
return obj.map((item) => limitDepth(item, currentDepth + 1));
|
|
318
|
+
}
|
|
319
|
+
const result = {};
|
|
320
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
321
|
+
result[key] = limitDepth(val, currentDepth + 1);
|
|
322
|
+
}
|
|
323
|
+
return result;
|
|
324
|
+
};
|
|
325
|
+
const processedMetadata = limitDepth(safeMetadata);
|
|
326
|
+
const serializedMeta = JSON.stringify(processedMetadata);
|
|
327
|
+
const maxMetaLimit = maxMetaOverride || this.maxMetadataSize;
|
|
328
|
+
if (serializedMeta.length > maxMetaLimit) {
|
|
329
|
+
throw new Error(`Metadata size exceeds maximum allowed limit of ${maxMetaLimit / 1024}KB.`);
|
|
330
|
+
}
|
|
331
|
+
const safeActor = applyRef(actorRaw, "actor");
|
|
332
|
+
const safeTarget = applyRef(targetRaw, "target");
|
|
333
|
+
const safeTenant = tenantRaw ? applyRef(tenantRaw, "tenant") : void 0;
|
|
334
|
+
const enrichedPayload = {
|
|
335
|
+
actor: safeActor,
|
|
336
|
+
action,
|
|
337
|
+
target: safeTarget,
|
|
338
|
+
metadata: processedMetadata
|
|
339
|
+
};
|
|
340
|
+
if (safeTenant) {
|
|
341
|
+
enrichedPayload.tenant = safeTenant;
|
|
342
|
+
}
|
|
343
|
+
if (Object.keys(context).length > 0) {
|
|
344
|
+
enrichedPayload.context = context;
|
|
345
|
+
}
|
|
346
|
+
let traceId = payload.traceId;
|
|
347
|
+
let spanId = payload.spanId;
|
|
348
|
+
let parentSpanId = payload.parentSpanId;
|
|
349
|
+
if (payload.req) {
|
|
350
|
+
const extractedTrace = _VolidatorClient.extractTraceContext(payload.req);
|
|
351
|
+
if (!traceId && extractedTrace.traceId) {
|
|
352
|
+
traceId = extractedTrace.traceId;
|
|
353
|
+
}
|
|
354
|
+
if (!spanId && extractedTrace.spanId) {
|
|
355
|
+
spanId = extractedTrace.spanId;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (spanId) {
|
|
359
|
+
enrichedPayload.spanId = spanId;
|
|
360
|
+
}
|
|
361
|
+
if (parentSpanId) {
|
|
362
|
+
enrichedPayload.parentSpanId = parentSpanId;
|
|
363
|
+
}
|
|
364
|
+
const activeKeyBuffer = await this._getHashedKey(this.activeKeyId);
|
|
365
|
+
const actorBlindIndex = await this.generateBlindIndex(actor, activeKeyBuffer);
|
|
366
|
+
const actionBlindIndex = await this.generateBlindIndex(action, activeKeyBuffer);
|
|
367
|
+
const targetBlindIndex = await this.generateBlindIndex(target, activeKeyBuffer);
|
|
368
|
+
const tenantBlindIndex = tenant ? await this.generateBlindIndex(tenant, activeKeyBuffer) : void 0;
|
|
369
|
+
const traceBlindIndex = traceId ? await this.generateBlindIndex(traceId, activeKeyBuffer) : void 0;
|
|
370
|
+
const encryptedPayload = await this.encryptPayload(enrichedPayload);
|
|
371
|
+
return {
|
|
372
|
+
actorBlindIndex,
|
|
373
|
+
actionBlindIndex,
|
|
374
|
+
targetBlindIndex,
|
|
375
|
+
tenantBlindIndex,
|
|
376
|
+
traceBlindIndex,
|
|
377
|
+
encryptedPayload
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
// log() — Encrypt and ingest a single audit event
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
async log(payload, maxMetaOverride) {
|
|
384
|
+
const entry = await this.prepareLogEntry(payload, maxMetaOverride);
|
|
385
|
+
try {
|
|
386
|
+
const res = await fetch(`${this.endpoint}/v1/log`, {
|
|
387
|
+
method: "POST",
|
|
388
|
+
headers: {
|
|
389
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
390
|
+
"Content-Type": "application/json"
|
|
391
|
+
},
|
|
392
|
+
body: JSON.stringify(entry)
|
|
393
|
+
});
|
|
394
|
+
return res.ok;
|
|
395
|
+
} catch (err) {
|
|
396
|
+
console.error(`[Volidator] Failed to send log: ${err.message}`);
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
// ---------------------------------------------------------------------------
|
|
401
|
+
// logBatch() — Bulk encrypt and ingest a batch of up to 100 audit events
|
|
402
|
+
// ---------------------------------------------------------------------------
|
|
403
|
+
async logBatch(payloads, maxMetaOverride) {
|
|
404
|
+
if (!Array.isArray(payloads) || payloads.length === 0) {
|
|
405
|
+
return { accepted: 0, rejected: 0 };
|
|
406
|
+
}
|
|
407
|
+
const batch = payloads.slice(0, 100);
|
|
408
|
+
const preparedEntries = [];
|
|
409
|
+
let rejected = payloads.length - batch.length;
|
|
410
|
+
const results = await Promise.allSettled(
|
|
411
|
+
batch.map((p) => this.prepareLogEntry(p, maxMetaOverride))
|
|
412
|
+
);
|
|
413
|
+
for (const res of results) {
|
|
414
|
+
if (res.status === "fulfilled") {
|
|
415
|
+
preparedEntries.push(res.value);
|
|
416
|
+
} else {
|
|
417
|
+
rejected++;
|
|
418
|
+
console.error(`[Volidator] Failed to prepare batch entry: ${res.reason?.message}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (preparedEntries.length === 0) {
|
|
422
|
+
return { accepted: 0, rejected };
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
const res = await fetch(`${this.endpoint}/v1/logs/batch`, {
|
|
426
|
+
method: "POST",
|
|
427
|
+
headers: {
|
|
428
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
429
|
+
"Content-Type": "application/json"
|
|
430
|
+
},
|
|
431
|
+
body: JSON.stringify({ logs: preparedEntries })
|
|
432
|
+
});
|
|
433
|
+
if (res.ok) {
|
|
434
|
+
return { accepted: preparedEntries.length, rejected };
|
|
435
|
+
} else {
|
|
436
|
+
console.error(`[Volidator] Batch ingestion endpoint returned status: ${res.status}`);
|
|
437
|
+
return { accepted: 0, rejected: rejected + preparedEntries.length };
|
|
438
|
+
}
|
|
439
|
+
} catch (err) {
|
|
440
|
+
console.error(`[Volidator] Failed to send log batch: ${err.message}`);
|
|
441
|
+
return { accepted: 0, rejected: rejected + preparedEntries.length };
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// ---------------------------------------------------------------------------
|
|
445
|
+
// generateEmbedToken() — Sign a HS256 JWT for the embeddable dashboard widget
|
|
446
|
+
// ---------------------------------------------------------------------------
|
|
447
|
+
async generateEmbedToken(config) {
|
|
448
|
+
if (!this.projectId || !this.clientSecret) {
|
|
449
|
+
throw new Error(
|
|
450
|
+
"generateEmbedToken() requires projectId and clientSecret in the VolidatorClient constructor."
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
const {
|
|
454
|
+
actorId,
|
|
455
|
+
targetId,
|
|
456
|
+
tenantId,
|
|
457
|
+
scope,
|
|
458
|
+
expiresIn = "2h",
|
|
459
|
+
dashboardUrl = "https://dash.volidator.com",
|
|
460
|
+
hostOrigin,
|
|
461
|
+
view
|
|
462
|
+
} = config;
|
|
463
|
+
const defaultScope = scope || (tenantId ? "tenant" : "actor");
|
|
464
|
+
if (defaultScope !== "auditor" && !actorId && !targetId && !tenantId) {
|
|
465
|
+
throw new Error(
|
|
466
|
+
"At least one of actorId, targetId, or tenantId must be provided to generateEmbedToken()."
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
const actorBlindIndexes = actorId ? await Promise.all(Object.keys(this.keyring).map(async (id) => this.generateBlindIndex(actorId, await this._getHashedKey(id)))) : void 0;
|
|
470
|
+
const targetBlindIndexes = targetId ? await Promise.all(Object.keys(this.keyring).map(async (id) => this.generateBlindIndex(targetId, await this._getHashedKey(id)))) : void 0;
|
|
471
|
+
const tenantBlindIndexes = tenantId ? await Promise.all(Object.keys(this.keyring).map(async (id) => this.generateBlindIndex(tenantId, await this._getHashedKey(id)))) : void 0;
|
|
472
|
+
const parsedExpiry = this.parseExpiry(expiresIn);
|
|
473
|
+
const maxExpiry = defaultScope === "auditor" ? 604800 : 3600;
|
|
474
|
+
const expiresInSeconds = Math.min(parsedExpiry, maxExpiry);
|
|
475
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
476
|
+
const payload = {
|
|
477
|
+
pid: this.projectId,
|
|
478
|
+
scope: defaultScope,
|
|
479
|
+
iat: now,
|
|
480
|
+
exp: now + expiresInSeconds
|
|
481
|
+
};
|
|
482
|
+
if (actorBlindIndexes) payload.abi = actorBlindIndexes;
|
|
483
|
+
if (targetBlindIndexes) payload.tgb = targetBlindIndexes;
|
|
484
|
+
if (tenantBlindIndexes) payload.tbi = tenantBlindIndexes;
|
|
485
|
+
if (view) {
|
|
486
|
+
const compressed = {};
|
|
487
|
+
if (Array.isArray(view.columns)) {
|
|
488
|
+
compressed.cols = view.columns.map((col) => {
|
|
489
|
+
if (col === "createdAt") return "cat";
|
|
490
|
+
if (col === "actor") return "act";
|
|
491
|
+
if (col === "action") return "acn";
|
|
492
|
+
if (col === "target") return "tgt";
|
|
493
|
+
if (col.startsWith("metadata.")) {
|
|
494
|
+
return `m.${col.slice(9)}`;
|
|
495
|
+
}
|
|
496
|
+
return col;
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
if (view.defaultFilter) {
|
|
500
|
+
compressed.flt = {};
|
|
501
|
+
if (view.defaultFilter.search !== void 0) {
|
|
502
|
+
compressed.flt.q = view.defaultFilter.search;
|
|
503
|
+
}
|
|
504
|
+
if (view.defaultFilter.action !== void 0) {
|
|
505
|
+
compressed.flt.act = view.defaultFilter.action;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
payload.view = compressed;
|
|
509
|
+
}
|
|
510
|
+
const token = await this.signHS256JWT(payload, this.clientSecret);
|
|
511
|
+
const keyringString = Object.entries(this.keyring).map(([id, key]) => `${id}:${key}`).join(",");
|
|
512
|
+
const hostParam = hostOrigin ? `?host=${encodeURIComponent(hostOrigin)}` : "";
|
|
513
|
+
const embedUrl = `${dashboardUrl}/embed/${token}${hostParam}#${keyringString}`;
|
|
514
|
+
return { token, embedUrl };
|
|
515
|
+
}
|
|
516
|
+
async signHS256JWT(payload, secret) {
|
|
517
|
+
const header = { alg: "HS256", typ: "JWT" };
|
|
518
|
+
const encode = (obj) => btoa(JSON.stringify(obj)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
519
|
+
const unsigned = `${encode(header)}.${encode(payload)}`;
|
|
520
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
521
|
+
"raw",
|
|
522
|
+
new TextEncoder().encode(secret),
|
|
523
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
524
|
+
false,
|
|
525
|
+
["sign"]
|
|
526
|
+
);
|
|
527
|
+
const signatureBuffer = await globalThis.crypto.subtle.sign(
|
|
528
|
+
"HMAC",
|
|
529
|
+
key,
|
|
530
|
+
new TextEncoder().encode(unsigned)
|
|
531
|
+
);
|
|
532
|
+
let binary = "";
|
|
533
|
+
const arr = new Uint8Array(signatureBuffer);
|
|
534
|
+
for (let i = 0; i < arr.byteLength; i++) {
|
|
535
|
+
binary += String.fromCharCode(arr[i]);
|
|
536
|
+
}
|
|
537
|
+
const signature = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
538
|
+
return `${unsigned}.${signature}`;
|
|
539
|
+
}
|
|
540
|
+
parseExpiry(expiry) {
|
|
541
|
+
const match = expiry.match(/^(\d+)(s|m|h|d)$/);
|
|
542
|
+
if (!match) return 7200;
|
|
543
|
+
const [, num, unit] = match;
|
|
544
|
+
const n = parseInt(num, 10);
|
|
545
|
+
switch (unit) {
|
|
546
|
+
case "s":
|
|
547
|
+
return n;
|
|
548
|
+
case "m":
|
|
549
|
+
return n * 60;
|
|
550
|
+
case "h":
|
|
551
|
+
return n * 3600;
|
|
552
|
+
case "d":
|
|
553
|
+
return n * 86400;
|
|
554
|
+
default:
|
|
555
|
+
return 7200;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
var VolidatorCompliance = class {
|
|
560
|
+
constructor(client) {
|
|
561
|
+
this.client = client;
|
|
562
|
+
}
|
|
563
|
+
async logWithControl(action, soc2Control, isoControl, payload) {
|
|
564
|
+
const metadata = {
|
|
565
|
+
...payload.metadata,
|
|
566
|
+
soc2_control: soc2Control,
|
|
567
|
+
iso27001: isoControl
|
|
568
|
+
};
|
|
569
|
+
return this.client.log({
|
|
570
|
+
...payload,
|
|
571
|
+
action,
|
|
572
|
+
metadata
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
async accessRevoked(payload) {
|
|
576
|
+
return this.logWithControl("access.revoked", "CC6.1", "A.9.2.6", payload);
|
|
577
|
+
}
|
|
578
|
+
async accessGranted(payload) {
|
|
579
|
+
return this.logWithControl("access.granted", "CC6.1", "A.9.2.1", payload);
|
|
580
|
+
}
|
|
581
|
+
async dataExported(payload) {
|
|
582
|
+
return this.logWithControl("data.exported", "CC6.6", "A.12.4.1", payload);
|
|
583
|
+
}
|
|
584
|
+
async systemConfigChanged(payload) {
|
|
585
|
+
return this.logWithControl("system.config_changed", "CC6.2", "A.12.1.2", payload);
|
|
586
|
+
}
|
|
587
|
+
async mfaEnabled(payload) {
|
|
588
|
+
return this.logWithControl("mfa.enabled", "CC6.3", "A.9.4.2", payload);
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
var VolidatorAgent = class {
|
|
592
|
+
constructor(client) {
|
|
593
|
+
this.client = client;
|
|
594
|
+
}
|
|
595
|
+
async logAgent(action, euAiAct, nistAiRmf, soc2Control, isoControl, payload) {
|
|
596
|
+
const {
|
|
597
|
+
actor,
|
|
598
|
+
actorId,
|
|
599
|
+
target,
|
|
600
|
+
targetId,
|
|
601
|
+
tenant,
|
|
602
|
+
tenantId,
|
|
603
|
+
metadata,
|
|
604
|
+
context,
|
|
605
|
+
telemetry,
|
|
606
|
+
req,
|
|
607
|
+
traceId,
|
|
608
|
+
spanId,
|
|
609
|
+
parentSpanId,
|
|
610
|
+
...agentData
|
|
611
|
+
} = payload;
|
|
612
|
+
const enrichedMetadata = {
|
|
613
|
+
...metadata,
|
|
614
|
+
...agentData,
|
|
615
|
+
eu_ai_act: euAiAct,
|
|
616
|
+
nist_ai_rmf: nistAiRmf,
|
|
617
|
+
soc2_control: soc2Control,
|
|
618
|
+
iso27001: isoControl
|
|
619
|
+
};
|
|
620
|
+
return this.client.log(
|
|
621
|
+
{
|
|
622
|
+
actor,
|
|
623
|
+
actorId,
|
|
624
|
+
target,
|
|
625
|
+
targetId,
|
|
626
|
+
tenant,
|
|
627
|
+
tenantId,
|
|
628
|
+
action,
|
|
629
|
+
metadata: enrichedMetadata,
|
|
630
|
+
context,
|
|
631
|
+
telemetry,
|
|
632
|
+
req,
|
|
633
|
+
traceId,
|
|
634
|
+
spanId,
|
|
635
|
+
parentSpanId
|
|
636
|
+
},
|
|
637
|
+
65536
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Logs a tool call or external API invocation by an agent.
|
|
642
|
+
* Maps to EU AI Act Article 12, NIST AI RMF MANAGE 2.2, SOC2 CC6.6, ISO 27001 A.12.4.1.
|
|
643
|
+
*/
|
|
644
|
+
async toolCall(payload) {
|
|
645
|
+
return this.logAgent("agent.tool_call", "Article 12", "MANAGE 2.2", "CC6.6", "A.12.4.1", payload);
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Logs a key decision made autonomously by an AI agent model.
|
|
649
|
+
* Maps to EU AI Act Article 12 & 13, NIST AI RMF GOVERN 1.7, SOC2 CC6.2, ISO 27001 A.18.1.3.
|
|
650
|
+
*/
|
|
651
|
+
async decision(payload) {
|
|
652
|
+
return this.logAgent("agent.decision", "Article 12 & 13", "GOVERN 1.7", "CC6.2", "A.18.1.3", payload);
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Logs a request for human review or permission escalation.
|
|
656
|
+
* Maps to EU AI Act Article 14, NIST AI RMF GOVERN 5.1, SOC2 CC6.3, ISO 27001 A.6.1.2.
|
|
657
|
+
*/
|
|
658
|
+
async escalation(payload) {
|
|
659
|
+
return this.logAgent("agent.escalation", "Article 14", "GOVERN 5.1", "CC6.3", "A.6.1.2", payload);
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Logs anomalous environment inputs, potential prompt injections, or policy violations.
|
|
663
|
+
* Maps to EU AI Act Article 9, NIST AI RMF MANAGE 2.4, SOC2 CC7.2, ISO 27001 A.16.1.2.
|
|
664
|
+
*/
|
|
665
|
+
async anomaly(payload) {
|
|
666
|
+
return this.logAgent("agent.anomaly", "Article 9", "MANAGE 2.4", "CC7.2", "A.16.1.2", payload);
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Logs a model refusal to execute a user prompt or command due to alignment safety.
|
|
670
|
+
* Maps to EU AI Act Article 5, NIST AI RMF GOVERN 1.1, SOC2 CC6.8, ISO 27001 A.18.1.3.
|
|
671
|
+
*/
|
|
672
|
+
async refusal(payload) {
|
|
673
|
+
return this.logAgent("agent.refusal", "Article 5", "GOVERN 1.1", "CC6.8", "A.18.1.3", payload);
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Logs an execution context handoff from one agent to another.
|
|
677
|
+
* Maps to EU AI Act Article 12, NIST AI RMF MAP 1.6, SOC2 CC6.6, ISO 27001 A.12.4.1.
|
|
678
|
+
*/
|
|
679
|
+
async handoff(payload) {
|
|
680
|
+
return this.logAgent("agent.handoff", "Article 12", "MAP 1.6", "CC6.6", "A.12.4.1", payload);
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
// src/middleware/next.ts
|
|
685
|
+
function withVolidator(client, handler) {
|
|
686
|
+
return async (req, ctx = {}, ...args) => {
|
|
687
|
+
const extractedContext = VolidatorClient.extractContext(req);
|
|
688
|
+
const scopedLog = async (payload) => {
|
|
689
|
+
return client.log({
|
|
690
|
+
...payload,
|
|
691
|
+
context: {
|
|
692
|
+
...extractedContext,
|
|
693
|
+
...payload.context,
|
|
694
|
+
location: {
|
|
695
|
+
...extractedContext.location,
|
|
696
|
+
...payload.context?.location || {}
|
|
697
|
+
},
|
|
698
|
+
device: {
|
|
699
|
+
...extractedContext.device,
|
|
700
|
+
...payload.context?.device || {}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
};
|
|
705
|
+
const withControl = (action, soc2Control, isoControl, payload) => scopedLog({
|
|
706
|
+
...payload,
|
|
707
|
+
action,
|
|
708
|
+
metadata: {
|
|
709
|
+
...payload.metadata,
|
|
710
|
+
soc2_control: soc2Control,
|
|
711
|
+
iso27001: isoControl
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
const scopedCompliance = {
|
|
715
|
+
accessRevoked: (p) => withControl("access.revoked", "CC6.1", "A.9.2.6", p),
|
|
716
|
+
accessGranted: (p) => withControl("access.granted", "CC6.1", "A.9.2.1", p),
|
|
717
|
+
dataExported: (p) => withControl("data.exported", "CC6.6", "A.12.4.1", p),
|
|
718
|
+
systemConfigChanged: (p) => withControl("system.config_changed", "CC6.2", "A.12.1.2", p),
|
|
719
|
+
mfaEnabled: (p) => withControl("mfa.enabled", "CC6.3", "A.9.4.2", p)
|
|
720
|
+
};
|
|
721
|
+
const newCtx = {
|
|
722
|
+
...ctx,
|
|
723
|
+
volidator: {
|
|
724
|
+
log: scopedLog,
|
|
725
|
+
compliance: scopedCompliance
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
return handler(req, newCtx, ...args);
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// src/plugins/universal.ts
|
|
733
|
+
function createUniversalAudit({ client, getUserId, getSession, getMetadata }) {
|
|
734
|
+
return function withAuthAudit(handler) {
|
|
735
|
+
const baseHandler = withVolidator(client, async (req, ctx) => {
|
|
736
|
+
let session = null;
|
|
737
|
+
let userId;
|
|
738
|
+
let injectedMetadata = {};
|
|
739
|
+
try {
|
|
740
|
+
if (getSession) {
|
|
741
|
+
session = await getSession(req, ctx);
|
|
742
|
+
}
|
|
743
|
+
userId = await getUserId(req, session ?? ctx);
|
|
744
|
+
if (getMetadata) {
|
|
745
|
+
injectedMetadata = await getMetadata(req, session ?? ctx) || {};
|
|
746
|
+
}
|
|
747
|
+
} catch (e) {
|
|
748
|
+
console.warn("[Volidator] Failed to execute IAM callbacks in Universal plugin", e);
|
|
749
|
+
}
|
|
750
|
+
const originalLog = ctx.volidator.log;
|
|
751
|
+
ctx.volidator.log = async (payload) => {
|
|
752
|
+
const enrichedPayload = { ...payload };
|
|
753
|
+
if (!enrichedPayload.actor && userId) {
|
|
754
|
+
enrichedPayload.actor = userId;
|
|
755
|
+
}
|
|
756
|
+
if (Object.keys(injectedMetadata).length > 0) {
|
|
757
|
+
enrichedPayload.metadata = {
|
|
758
|
+
...injectedMetadata,
|
|
759
|
+
...enrichedPayload.metadata || {}
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
return originalLog(enrichedPayload);
|
|
763
|
+
};
|
|
764
|
+
ctx.session = session;
|
|
765
|
+
return handler(req, ctx);
|
|
766
|
+
});
|
|
767
|
+
return baseHandler;
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
771
|
+
0 && (module.exports = {
|
|
772
|
+
createUniversalAudit
|
|
773
|
+
});
|
|
774
|
+
//# sourceMappingURL=universal.cjs.map
|