openredaction 1.0.9 → 1.1.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 +9 -56
- package/dist/index.cli.cjs +79 -152
- package/dist/index.d.mts +29 -320
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +29 -320
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +161 -1148
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +156 -1137
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +3 -14
- package/dist/react.d.mts.map +1 -1
- package/dist/react.d.ts +3 -14
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +79 -152
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +79 -152
- package/dist/react.mjs.map +1 -1
- package/dist/server.d.mts +1809 -0
- package/dist/server.d.mts.map +1 -0
- package/dist/server.d.ts +1809 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +18124 -0
- package/dist/server.js.map +1 -0
- package/dist/server.mjs +18109 -0
- package/dist/server.mjs.map +1 -0
- package/dist/workers/worker.cjs +17240 -0
- package/package.json +24 -4
package/dist/index.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import * as path from "path";
|
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
import { Worker } from "worker_threads";
|
|
7
7
|
import { cpus } from "os";
|
|
8
|
+
import { Readable } from "stream";
|
|
8
9
|
|
|
9
10
|
//#region rolldown:runtime
|
|
10
11
|
var __defProp = Object.defineProperty;
|
|
@@ -1001,332 +1002,6 @@ var InMemoryMetricsCollector = class {
|
|
|
1001
1002
|
}
|
|
1002
1003
|
};
|
|
1003
1004
|
|
|
1004
|
-
//#endregion
|
|
1005
|
-
//#region src/metrics/PrometheusServer.ts
|
|
1006
|
-
/**
|
|
1007
|
-
* Prometheus metrics HTTP server
|
|
1008
|
-
* Provides a lightweight HTTP server for exposing metrics to Prometheus
|
|
1009
|
-
*/
|
|
1010
|
-
var PrometheusServer = class {
|
|
1011
|
-
constructor(metricsCollector, options) {
|
|
1012
|
-
this.isRunning = false;
|
|
1013
|
-
this.requestCount = 0;
|
|
1014
|
-
this.metricsCollector = metricsCollector;
|
|
1015
|
-
this.options = {
|
|
1016
|
-
port: options?.port ?? 9090,
|
|
1017
|
-
host: options?.host ?? "0.0.0.0",
|
|
1018
|
-
metricsPath: options?.metricsPath ?? "/metrics",
|
|
1019
|
-
prefix: options?.prefix ?? "openredaction",
|
|
1020
|
-
healthPath: options?.healthPath ?? "/health",
|
|
1021
|
-
enableCors: options?.enableCors ?? false,
|
|
1022
|
-
username: options?.username,
|
|
1023
|
-
password: options?.password
|
|
1024
|
-
};
|
|
1025
|
-
}
|
|
1026
|
-
/**
|
|
1027
|
-
* Start the Prometheus metrics server
|
|
1028
|
-
*/
|
|
1029
|
-
async start() {
|
|
1030
|
-
if (this.isRunning) throw new Error("[PrometheusServer] Server is already running");
|
|
1031
|
-
try {
|
|
1032
|
-
this.server = __require("http").createServer(this.handleRequest.bind(this));
|
|
1033
|
-
return new Promise((resolve, reject) => {
|
|
1034
|
-
this.server.listen(this.options.port, this.options.host, () => {
|
|
1035
|
-
this.isRunning = true;
|
|
1036
|
-
console.log(`[PrometheusServer] Metrics server started on http://${this.options.host}:${this.options.port}${this.options.metricsPath}`);
|
|
1037
|
-
resolve();
|
|
1038
|
-
});
|
|
1039
|
-
this.server.on("error", (error) => {
|
|
1040
|
-
reject(/* @__PURE__ */ new Error(`[PrometheusServer] Failed to start server: ${error.message}`));
|
|
1041
|
-
});
|
|
1042
|
-
});
|
|
1043
|
-
} catch (error) {
|
|
1044
|
-
throw new Error(`[PrometheusServer] Failed to initialize HTTP server: ${error.message}`);
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
/**
|
|
1048
|
-
* Stop the server
|
|
1049
|
-
*/
|
|
1050
|
-
async stop() {
|
|
1051
|
-
if (!this.isRunning || !this.server) return;
|
|
1052
|
-
return new Promise((resolve, reject) => {
|
|
1053
|
-
this.server.close((error) => {
|
|
1054
|
-
if (error) reject(/* @__PURE__ */ new Error(`[PrometheusServer] Failed to stop server: ${error.message}`));
|
|
1055
|
-
else {
|
|
1056
|
-
this.isRunning = false;
|
|
1057
|
-
console.log("[PrometheusServer] Server stopped");
|
|
1058
|
-
resolve();
|
|
1059
|
-
}
|
|
1060
|
-
});
|
|
1061
|
-
});
|
|
1062
|
-
}
|
|
1063
|
-
/**
|
|
1064
|
-
* Handle incoming HTTP requests
|
|
1065
|
-
*/
|
|
1066
|
-
handleRequest(req, res) {
|
|
1067
|
-
this.requestCount++;
|
|
1068
|
-
if (this.options.enableCors) {
|
|
1069
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1070
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
1071
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
1072
|
-
if (req.method === "OPTIONS") {
|
|
1073
|
-
res.writeHead(204);
|
|
1074
|
-
res.end();
|
|
1075
|
-
return;
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
if (req.method !== "GET") {
|
|
1079
|
-
res.writeHead(405, { "Content-Type": "text/plain" });
|
|
1080
|
-
res.end("Method Not Allowed");
|
|
1081
|
-
return;
|
|
1082
|
-
}
|
|
1083
|
-
if (this.options.username && this.options.password) {
|
|
1084
|
-
const authHeader = req.headers.authorization;
|
|
1085
|
-
if (!authHeader || !this.validateAuth(authHeader)) {
|
|
1086
|
-
res.writeHead(401, {
|
|
1087
|
-
"Content-Type": "text/plain",
|
|
1088
|
-
"WWW-Authenticate": "Basic realm=\"Prometheus Metrics\""
|
|
1089
|
-
});
|
|
1090
|
-
res.end("Unauthorized");
|
|
1091
|
-
return;
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
const url = req.url;
|
|
1095
|
-
if (url === this.options.metricsPath) this.handleMetrics(req, res);
|
|
1096
|
-
else if (url === this.options.healthPath) this.handleHealth(req, res);
|
|
1097
|
-
else if (url === "/") this.handleRoot(req, res);
|
|
1098
|
-
else {
|
|
1099
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
1100
|
-
res.end("Not Found");
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
/**
|
|
1104
|
-
* Handle /metrics endpoint
|
|
1105
|
-
*/
|
|
1106
|
-
handleMetrics(_req, res) {
|
|
1107
|
-
try {
|
|
1108
|
-
this.lastScrapeTime = /* @__PURE__ */ new Date();
|
|
1109
|
-
const exporter = this.metricsCollector.getExporter();
|
|
1110
|
-
const metrics = exporter.getMetrics();
|
|
1111
|
-
const prometheusFormat = exporter.exportPrometheus(metrics, this.options.prefix);
|
|
1112
|
-
const serverMetrics = this.getServerMetrics();
|
|
1113
|
-
const fullMetrics = prometheusFormat + "\n" + serverMetrics;
|
|
1114
|
-
res.writeHead(200, { "Content-Type": "text/plain; version=0.0.4" });
|
|
1115
|
-
res.end(fullMetrics);
|
|
1116
|
-
} catch (error) {
|
|
1117
|
-
console.error("[PrometheusServer] Error exporting metrics:", error);
|
|
1118
|
-
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
1119
|
-
res.end("Internal Server Error");
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
/**
|
|
1123
|
-
* Handle /health endpoint
|
|
1124
|
-
*/
|
|
1125
|
-
handleHealth(_req, res) {
|
|
1126
|
-
const health = {
|
|
1127
|
-
status: "healthy",
|
|
1128
|
-
uptime: process.uptime(),
|
|
1129
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1130
|
-
metrics: {
|
|
1131
|
-
requestCount: this.requestCount,
|
|
1132
|
-
lastScrapeTime: this.lastScrapeTime?.toISOString()
|
|
1133
|
-
}
|
|
1134
|
-
};
|
|
1135
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1136
|
-
res.end(JSON.stringify(health, null, 2));
|
|
1137
|
-
}
|
|
1138
|
-
/**
|
|
1139
|
-
* Handle / root endpoint
|
|
1140
|
-
*/
|
|
1141
|
-
handleRoot(_req, res) {
|
|
1142
|
-
const html = `
|
|
1143
|
-
<!DOCTYPE html>
|
|
1144
|
-
<html>
|
|
1145
|
-
<head>
|
|
1146
|
-
<title>OpenRedaction Prometheus Exporter</title>
|
|
1147
|
-
<style>
|
|
1148
|
-
body { font-family: sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; }
|
|
1149
|
-
h1 { color: #333; }
|
|
1150
|
-
a { color: #0066cc; }
|
|
1151
|
-
.endpoint { background: #f5f5f5; padding: 10px; margin: 10px 0; border-radius: 4px; }
|
|
1152
|
-
</style>
|
|
1153
|
-
</head>
|
|
1154
|
-
<body>
|
|
1155
|
-
<h1>OpenRedaction Prometheus Exporter</h1>
|
|
1156
|
-
<p>This server exposes metrics for Prometheus monitoring.</p>
|
|
1157
|
-
|
|
1158
|
-
<h2>Endpoints</h2>
|
|
1159
|
-
<div class="endpoint">
|
|
1160
|
-
<strong>GET <a href="${this.options.metricsPath}">${this.options.metricsPath}</a></strong><br>
|
|
1161
|
-
Prometheus metrics in text format
|
|
1162
|
-
</div>
|
|
1163
|
-
<div class="endpoint">
|
|
1164
|
-
<strong>GET <a href="${this.options.healthPath}">${this.options.healthPath}</a></strong><br>
|
|
1165
|
-
Health check endpoint (JSON)
|
|
1166
|
-
</div>
|
|
1167
|
-
|
|
1168
|
-
<h2>Configuration</h2>
|
|
1169
|
-
<ul>
|
|
1170
|
-
<li>Host: ${this.options.host}</li>
|
|
1171
|
-
<li>Port: ${this.options.port}</li>
|
|
1172
|
-
<li>Metrics Prefix: ${this.options.prefix}</li>
|
|
1173
|
-
<li>CORS Enabled: ${this.options.enableCors}</li>
|
|
1174
|
-
<li>Authentication: ${this.options.username ? "Enabled" : "Disabled"}</li>
|
|
1175
|
-
</ul>
|
|
1176
|
-
|
|
1177
|
-
<h2>Prometheus Configuration</h2>
|
|
1178
|
-
<p>Add this to your <code>prometheus.yml</code>:</p>
|
|
1179
|
-
<pre>
|
|
1180
|
-
scrape_configs:
|
|
1181
|
-
- job_name: 'openredaction'
|
|
1182
|
-
static_configs:
|
|
1183
|
-
- targets: ['${this.options.host}:${this.options.port}']
|
|
1184
|
-
metrics_path: '${this.options.metricsPath}'
|
|
1185
|
-
${this.options.username ? ` basic_auth:
|
|
1186
|
-
username: '${this.options.username}'
|
|
1187
|
-
password: '${this.options.password}'` : ""}
|
|
1188
|
-
</pre>
|
|
1189
|
-
</body>
|
|
1190
|
-
</html>
|
|
1191
|
-
`.trim();
|
|
1192
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1193
|
-
res.end(html);
|
|
1194
|
-
}
|
|
1195
|
-
/**
|
|
1196
|
-
* Validate basic authentication
|
|
1197
|
-
*/
|
|
1198
|
-
validateAuth(authHeader) {
|
|
1199
|
-
try {
|
|
1200
|
-
const base64Credentials = authHeader.split(" ")[1];
|
|
1201
|
-
const [username, password] = Buffer.from(base64Credentials, "base64").toString("utf-8").split(":");
|
|
1202
|
-
return username === this.options.username && password === this.options.password;
|
|
1203
|
-
} catch {
|
|
1204
|
-
return false;
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
/**
|
|
1208
|
-
* Get server-specific metrics in Prometheus format
|
|
1209
|
-
*/
|
|
1210
|
-
getServerMetrics() {
|
|
1211
|
-
const prefix = this.options.prefix;
|
|
1212
|
-
const timestamp = Date.now();
|
|
1213
|
-
const lines = [];
|
|
1214
|
-
lines.push(`# HELP ${prefix}_server_uptime_seconds Server uptime in seconds`);
|
|
1215
|
-
lines.push(`# TYPE ${prefix}_server_uptime_seconds counter`);
|
|
1216
|
-
lines.push(`${prefix}_server_uptime_seconds ${process.uptime().toFixed(2)} ${timestamp}`);
|
|
1217
|
-
lines.push("");
|
|
1218
|
-
lines.push(`# HELP ${prefix}_server_requests_total Total number of requests to metrics server`);
|
|
1219
|
-
lines.push(`# TYPE ${prefix}_server_requests_total counter`);
|
|
1220
|
-
lines.push(`${prefix}_server_requests_total ${this.requestCount} ${timestamp}`);
|
|
1221
|
-
lines.push("");
|
|
1222
|
-
if (this.lastScrapeTime) {
|
|
1223
|
-
const lastScrapeSeconds = Math.floor((Date.now() - this.lastScrapeTime.getTime()) / 1e3);
|
|
1224
|
-
lines.push(`# HELP ${prefix}_server_last_scrape_seconds Seconds since last metrics scrape`);
|
|
1225
|
-
lines.push(`# TYPE ${prefix}_server_last_scrape_seconds gauge`);
|
|
1226
|
-
lines.push(`${prefix}_server_last_scrape_seconds ${lastScrapeSeconds} ${timestamp}`);
|
|
1227
|
-
lines.push("");
|
|
1228
|
-
}
|
|
1229
|
-
const mem = process.memoryUsage();
|
|
1230
|
-
lines.push(`# HELP ${prefix}_server_memory_bytes Server memory usage in bytes`);
|
|
1231
|
-
lines.push(`# TYPE ${prefix}_server_memory_bytes gauge`);
|
|
1232
|
-
lines.push(`${prefix}_server_memory_bytes{type="rss"} ${mem.rss} ${timestamp}`);
|
|
1233
|
-
lines.push(`${prefix}_server_memory_bytes{type="heapTotal"} ${mem.heapTotal} ${timestamp}`);
|
|
1234
|
-
lines.push(`${prefix}_server_memory_bytes{type="heapUsed"} ${mem.heapUsed} ${timestamp}`);
|
|
1235
|
-
lines.push(`${prefix}_server_memory_bytes{type="external"} ${mem.external} ${timestamp}`);
|
|
1236
|
-
lines.push("");
|
|
1237
|
-
return lines.join("\n");
|
|
1238
|
-
}
|
|
1239
|
-
/**
|
|
1240
|
-
* Get server statistics
|
|
1241
|
-
*/
|
|
1242
|
-
getStats() {
|
|
1243
|
-
return {
|
|
1244
|
-
isRunning: this.isRunning,
|
|
1245
|
-
requestCount: this.requestCount,
|
|
1246
|
-
lastScrapeTime: this.lastScrapeTime,
|
|
1247
|
-
uptime: process.uptime(),
|
|
1248
|
-
host: this.options.host,
|
|
1249
|
-
port: this.options.port,
|
|
1250
|
-
metricsPath: this.options.metricsPath
|
|
1251
|
-
};
|
|
1252
|
-
}
|
|
1253
|
-
};
|
|
1254
|
-
/**
|
|
1255
|
-
* Create a Prometheus server instance
|
|
1256
|
-
*/
|
|
1257
|
-
function createPrometheusServer(metricsCollector, options) {
|
|
1258
|
-
return new PrometheusServer(metricsCollector, options);
|
|
1259
|
-
}
|
|
1260
|
-
/**
|
|
1261
|
-
* Example Grafana dashboard JSON for OpenRedaction metrics
|
|
1262
|
-
* Can be imported directly into Grafana
|
|
1263
|
-
*/
|
|
1264
|
-
const GRAFANA_DASHBOARD_TEMPLATE = { dashboard: {
|
|
1265
|
-
title: "OpenRedaction Metrics",
|
|
1266
|
-
tags: [
|
|
1267
|
-
"pii",
|
|
1268
|
-
"redaction",
|
|
1269
|
-
"security"
|
|
1270
|
-
],
|
|
1271
|
-
timezone: "browser",
|
|
1272
|
-
panels: [
|
|
1273
|
-
{
|
|
1274
|
-
id: 1,
|
|
1275
|
-
title: "Total Redactions",
|
|
1276
|
-
type: "graph",
|
|
1277
|
-
targets: [{
|
|
1278
|
-
expr: "rate(openredaction_total_redactions[5m])",
|
|
1279
|
-
legendFormat: "Redactions per second"
|
|
1280
|
-
}]
|
|
1281
|
-
},
|
|
1282
|
-
{
|
|
1283
|
-
id: 2,
|
|
1284
|
-
title: "PII Detected by Type",
|
|
1285
|
-
type: "graph",
|
|
1286
|
-
targets: [{
|
|
1287
|
-
expr: "rate(openredaction_pii_by_type[5m])",
|
|
1288
|
-
legendFormat: "{{type}}"
|
|
1289
|
-
}]
|
|
1290
|
-
},
|
|
1291
|
-
{
|
|
1292
|
-
id: 3,
|
|
1293
|
-
title: "Average Processing Time",
|
|
1294
|
-
type: "graph",
|
|
1295
|
-
targets: [{
|
|
1296
|
-
expr: "openredaction_avg_processing_time_ms",
|
|
1297
|
-
legendFormat: "Processing time (ms)"
|
|
1298
|
-
}]
|
|
1299
|
-
},
|
|
1300
|
-
{
|
|
1301
|
-
id: 4,
|
|
1302
|
-
title: "Error Rate",
|
|
1303
|
-
type: "graph",
|
|
1304
|
-
targets: [{
|
|
1305
|
-
expr: "rate(openredaction_total_errors[5m])",
|
|
1306
|
-
legendFormat: "Errors per second"
|
|
1307
|
-
}]
|
|
1308
|
-
},
|
|
1309
|
-
{
|
|
1310
|
-
id: 5,
|
|
1311
|
-
title: "Operations by Redaction Mode",
|
|
1312
|
-
type: "piechart",
|
|
1313
|
-
targets: [{
|
|
1314
|
-
expr: "openredaction_by_redaction_mode",
|
|
1315
|
-
legendFormat: "{{mode}}"
|
|
1316
|
-
}]
|
|
1317
|
-
},
|
|
1318
|
-
{
|
|
1319
|
-
id: 6,
|
|
1320
|
-
title: "Server Memory Usage",
|
|
1321
|
-
type: "graph",
|
|
1322
|
-
targets: [{
|
|
1323
|
-
expr: "openredaction_server_memory_bytes",
|
|
1324
|
-
legendFormat: "{{type}}"
|
|
1325
|
-
}]
|
|
1326
|
-
}
|
|
1327
|
-
]
|
|
1328
|
-
} };
|
|
1329
|
-
|
|
1330
1005
|
//#endregion
|
|
1331
1006
|
//#region src/rbac/roles.ts
|
|
1332
1007
|
/**
|
|
@@ -1678,6 +1353,70 @@ function validateRoutingNumber(routingNumber, _context) {
|
|
|
1678
1353
|
return (3 * (digits[0] + digits[3] + digits[6]) + 7 * (digits[1] + digits[4] + digits[7]) + (digits[2] + digits[5] + digits[8])) % 10 === 0;
|
|
1679
1354
|
}
|
|
1680
1355
|
/**
|
|
1356
|
+
* Luhn check for arbitrary digit string (e.g. Canadian SIN)
|
|
1357
|
+
*/
|
|
1358
|
+
function validateLuhnDigits(digits) {
|
|
1359
|
+
let sum = 0;
|
|
1360
|
+
let isEven = false;
|
|
1361
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
1362
|
+
let d = parseInt(digits[i], 10);
|
|
1363
|
+
if (Number.isNaN(d)) return false;
|
|
1364
|
+
if (isEven) {
|
|
1365
|
+
d *= 2;
|
|
1366
|
+
if (d > 9) d -= 9;
|
|
1367
|
+
}
|
|
1368
|
+
sum += d;
|
|
1369
|
+
isEven = !isEven;
|
|
1370
|
+
}
|
|
1371
|
+
return sum % 10 === 0;
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* SWIFT/BIC format validation (ISO 9362: 8 or 11 characters)
|
|
1375
|
+
*/
|
|
1376
|
+
function validateSWIFTBIC(bic, _context) {
|
|
1377
|
+
const cleaned = bic.replace(/\s/g, "").toUpperCase();
|
|
1378
|
+
return /^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$/.test(cleaned);
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Canadian Social Insurance Number — Luhn checksum on 9 digits
|
|
1382
|
+
*/
|
|
1383
|
+
function validateCanadianSIN(sin, _context) {
|
|
1384
|
+
const cleaned = sin.replace(/[\s-]/g, "");
|
|
1385
|
+
if (!/^\d{9}$/.test(cleaned)) return false;
|
|
1386
|
+
if (cleaned === "000000000") return false;
|
|
1387
|
+
return validateLuhnDigits(cleaned);
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Australian Tax File Number — weighted checksum (8 or 9 digits)
|
|
1391
|
+
*/
|
|
1392
|
+
function validateAustralianTFN(tfn, _context) {
|
|
1393
|
+
const cleaned = tfn.replace(/\s/g, "");
|
|
1394
|
+
if (!/^\d{8}$/.test(cleaned) && !/^\d{9}$/.test(cleaned)) return false;
|
|
1395
|
+
const weights = cleaned.length === 8 ? [
|
|
1396
|
+
1,
|
|
1397
|
+
4,
|
|
1398
|
+
3,
|
|
1399
|
+
7,
|
|
1400
|
+
5,
|
|
1401
|
+
8,
|
|
1402
|
+
6,
|
|
1403
|
+
9
|
|
1404
|
+
] : [
|
|
1405
|
+
1,
|
|
1406
|
+
4,
|
|
1407
|
+
3,
|
|
1408
|
+
7,
|
|
1409
|
+
5,
|
|
1410
|
+
8,
|
|
1411
|
+
6,
|
|
1412
|
+
9,
|
|
1413
|
+
10
|
|
1414
|
+
];
|
|
1415
|
+
let sum = 0;
|
|
1416
|
+
for (let i = 0; i < cleaned.length; i++) sum += parseInt(cleaned[i], 10) * weights[i];
|
|
1417
|
+
return sum % 11 === 0;
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1681
1420
|
* Context-aware name validator to reduce false positives
|
|
1682
1421
|
*/
|
|
1683
1422
|
function validateName(name, context) {
|
|
@@ -12100,6 +11839,38 @@ const transportLogisticsPreset = {
|
|
|
12100
11839
|
]
|
|
12101
11840
|
};
|
|
12102
11841
|
/**
|
|
11842
|
+
* PCI-DSS oriented preset — cardholder data and common payment identifiers
|
|
11843
|
+
*/
|
|
11844
|
+
const pciDssPreset = {
|
|
11845
|
+
includeNames: true,
|
|
11846
|
+
includeEmails: true,
|
|
11847
|
+
includePhones: true,
|
|
11848
|
+
includeAddresses: true,
|
|
11849
|
+
categories: [
|
|
11850
|
+
"personal",
|
|
11851
|
+
"contact",
|
|
11852
|
+
"financial",
|
|
11853
|
+
"network"
|
|
11854
|
+
]
|
|
11855
|
+
};
|
|
11856
|
+
/**
|
|
11857
|
+
* SOC 2 oriented preset — broad PII and credentials for trust services contexts
|
|
11858
|
+
*/
|
|
11859
|
+
const soc2Preset = {
|
|
11860
|
+
includeNames: true,
|
|
11861
|
+
includeEmails: true,
|
|
11862
|
+
includePhones: true,
|
|
11863
|
+
includeAddresses: true,
|
|
11864
|
+
categories: [
|
|
11865
|
+
"personal",
|
|
11866
|
+
"contact",
|
|
11867
|
+
"financial",
|
|
11868
|
+
"government",
|
|
11869
|
+
"network",
|
|
11870
|
+
"digital-identity"
|
|
11871
|
+
]
|
|
11872
|
+
};
|
|
11873
|
+
/**
|
|
12103
11874
|
* Get preset configuration by name
|
|
12104
11875
|
*/
|
|
12105
11876
|
function getPreset(name) {
|
|
@@ -12116,6 +11887,10 @@ function getPreset(name) {
|
|
|
12116
11887
|
case "transport-logistics":
|
|
12117
11888
|
case "transportation":
|
|
12118
11889
|
case "logistics": return transportLogisticsPreset;
|
|
11890
|
+
case "pci-dss":
|
|
11891
|
+
case "pci_dss": return pciDssPreset;
|
|
11892
|
+
case "soc2":
|
|
11893
|
+
case "soc-2": return soc2Preset;
|
|
12119
11894
|
default: return {};
|
|
12120
11895
|
}
|
|
12121
11896
|
}
|
|
@@ -12634,7 +12409,7 @@ var ConfigLoader = class {
|
|
|
12634
12409
|
static createDefaultConfig(outputPath = ".openredaction.config.js") {
|
|
12635
12410
|
fs.writeFileSync(outputPath, `/**
|
|
12636
12411
|
* OpenRedaction Configuration
|
|
12637
|
-
* @see https://github.com/
|
|
12412
|
+
* @see https://github.com/sam247/openredaction
|
|
12638
12413
|
*/
|
|
12639
12414
|
export default {
|
|
12640
12415
|
// Extend built-in presets
|
|
@@ -15294,26 +15069,6 @@ function createValidationError(value, patternType) {
|
|
|
15294
15069
|
patternType
|
|
15295
15070
|
});
|
|
15296
15071
|
}
|
|
15297
|
-
function createHighMemoryError(textSize) {
|
|
15298
|
-
const sizeMB = (textSize / 1024 / 1024).toFixed(2);
|
|
15299
|
-
return new OpenRedactionError(`Text size is ${sizeMB}MB. Consider using streaming API for large documents.`, "HIGH_MEMORY_USAGE", {
|
|
15300
|
-
message: "Use StreamingDetector for memory-efficient processing of large documents",
|
|
15301
|
-
code: `import { createStreamingDetector } from 'openredaction';
|
|
15302
|
-
|
|
15303
|
-
const streaming = createStreamingDetector(redactor, {
|
|
15304
|
-
chunkSize: 2048,
|
|
15305
|
-
overlap: 100
|
|
15306
|
-
});
|
|
15307
|
-
|
|
15308
|
-
for await (const chunk of streaming.processStream(largeText)) {
|
|
15309
|
-
console.log(chunk.detections);
|
|
15310
|
-
}`,
|
|
15311
|
-
docs: "https://github.com/sam247/openredaction#streaming-api"
|
|
15312
|
-
}, {
|
|
15313
|
-
textSize,
|
|
15314
|
-
sizeMB
|
|
15315
|
-
});
|
|
15316
|
-
}
|
|
15317
15072
|
function createConfigLoadError(path, reason) {
|
|
15318
15073
|
return new OpenRedactionError(`Failed to load config from '${path}': ${reason}`, "CONFIG_LOAD_ERROR", {
|
|
15319
15074
|
message: "Check that your config file exists and is valid JSON",
|
|
@@ -15471,133 +15226,6 @@ function compileSafeRegex(pattern, flags) {
|
|
|
15471
15226
|
return new RegExp(patternStr, finalFlags);
|
|
15472
15227
|
}
|
|
15473
15228
|
|
|
15474
|
-
//#endregion
|
|
15475
|
-
//#region src/utils/ai-assist.ts
|
|
15476
|
-
/**
|
|
15477
|
-
* Get the AI endpoint URL from options or environment
|
|
15478
|
-
*/
|
|
15479
|
-
function getAIEndpoint(aiOptions) {
|
|
15480
|
-
if (!aiOptions?.enabled) return null;
|
|
15481
|
-
if (aiOptions.endpoint) return aiOptions.endpoint;
|
|
15482
|
-
if (typeof process !== "undefined" && process.env) {
|
|
15483
|
-
const envEndpoint = process.env.OPENREDACTION_AI_ENDPOINT;
|
|
15484
|
-
if (envEndpoint) return envEndpoint;
|
|
15485
|
-
}
|
|
15486
|
-
return null;
|
|
15487
|
-
}
|
|
15488
|
-
/**
|
|
15489
|
-
* Check if fetch is available in the current environment
|
|
15490
|
-
*/
|
|
15491
|
-
function isFetchAvailable() {
|
|
15492
|
-
return typeof fetch !== "undefined";
|
|
15493
|
-
}
|
|
15494
|
-
/**
|
|
15495
|
-
* Call the AI endpoint to get additional PII entities
|
|
15496
|
-
* Returns null if AI is disabled, endpoint unavailable, or on error
|
|
15497
|
-
*/
|
|
15498
|
-
async function callAIDetect(text, endpoint, debug) {
|
|
15499
|
-
if (!isFetchAvailable()) {
|
|
15500
|
-
if (debug) console.warn("[OpenRedaction] AI assist requires fetch API. Not available in this environment.");
|
|
15501
|
-
return null;
|
|
15502
|
-
}
|
|
15503
|
-
try {
|
|
15504
|
-
const url = endpoint.endsWith("/ai-detect") ? endpoint : `${endpoint}/ai-detect`;
|
|
15505
|
-
if (debug) console.log(`[OpenRedaction] Calling AI endpoint: ${url}`);
|
|
15506
|
-
const response = await fetch(url, {
|
|
15507
|
-
method: "POST",
|
|
15508
|
-
headers: { "Content-Type": "application/json" },
|
|
15509
|
-
body: JSON.stringify({ text })
|
|
15510
|
-
});
|
|
15511
|
-
if (!response.ok) {
|
|
15512
|
-
if (debug) {
|
|
15513
|
-
const statusText = response.status === 429 ? "Rate limit exceeded (429)" : `${response.status}: ${response.statusText}`;
|
|
15514
|
-
console.warn(`[OpenRedaction] AI endpoint returned ${statusText}`);
|
|
15515
|
-
}
|
|
15516
|
-
return null;
|
|
15517
|
-
}
|
|
15518
|
-
const data = await response.json();
|
|
15519
|
-
if (!data.entities || !Array.isArray(data.entities)) {
|
|
15520
|
-
if (debug) console.warn("[OpenRedaction] Invalid AI response format: missing entities array");
|
|
15521
|
-
return null;
|
|
15522
|
-
}
|
|
15523
|
-
return data.entities;
|
|
15524
|
-
} catch (error) {
|
|
15525
|
-
if (debug) console.warn(`[OpenRedaction] AI endpoint error: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
15526
|
-
return null;
|
|
15527
|
-
}
|
|
15528
|
-
}
|
|
15529
|
-
/**
|
|
15530
|
-
* Validate an AI entity
|
|
15531
|
-
*/
|
|
15532
|
-
function validateAIEntity(entity, textLength) {
|
|
15533
|
-
if (!entity.type || !entity.value || typeof entity.start !== "number" || typeof entity.end !== "number") return false;
|
|
15534
|
-
if (entity.start < 0 || entity.end < 0 || entity.start >= entity.end) return false;
|
|
15535
|
-
if (entity.start >= textLength || entity.end > textLength) return false;
|
|
15536
|
-
if (entity.value.length !== entity.end - entity.start) return false;
|
|
15537
|
-
return true;
|
|
15538
|
-
}
|
|
15539
|
-
/**
|
|
15540
|
-
* Check if two detections overlap significantly
|
|
15541
|
-
* Returns true if they overlap by more than 50% of the shorter detection
|
|
15542
|
-
*/
|
|
15543
|
-
function detectionsOverlap(det1, det2) {
|
|
15544
|
-
const [start1, end1] = det1.position;
|
|
15545
|
-
const [start2, end2] = det2.position;
|
|
15546
|
-
const overlapStart = Math.max(start1, start2);
|
|
15547
|
-
const overlapEnd = Math.min(end1, end2);
|
|
15548
|
-
if (overlapStart >= overlapEnd) return false;
|
|
15549
|
-
const overlapLength = overlapEnd - overlapStart;
|
|
15550
|
-
const length1 = end1 - start1;
|
|
15551
|
-
const length2 = end2 - start2;
|
|
15552
|
-
return overlapLength > Math.min(length1, length2) * .5;
|
|
15553
|
-
}
|
|
15554
|
-
/**
|
|
15555
|
-
* Convert AI entity to PIIDetection format
|
|
15556
|
-
*/
|
|
15557
|
-
function convertAIEntityToDetection(entity, text) {
|
|
15558
|
-
if (!validateAIEntity(entity, text.length)) return null;
|
|
15559
|
-
const actualValue = text.substring(entity.start, entity.end);
|
|
15560
|
-
let type = entity.type.toUpperCase();
|
|
15561
|
-
if (type.includes("EMAIL") || type === "EMAIL_ADDRESS") type = "EMAIL";
|
|
15562
|
-
else if (type.includes("PHONE") || type === "PHONE_NUMBER") type = "PHONE_US";
|
|
15563
|
-
else if (type.includes("NAME") || type === "PERSON") type = "NAME";
|
|
15564
|
-
else if (type.includes("SSN") || type === "SOCIAL_SECURITY_NUMBER") type = "SSN";
|
|
15565
|
-
else if (type.includes("ADDRESS")) type = "ADDRESS_STREET";
|
|
15566
|
-
let severity = "medium";
|
|
15567
|
-
if (type === "SSN" || type === "CREDIT_CARD") severity = "critical";
|
|
15568
|
-
else if (type === "EMAIL" || type === "PHONE_US" || type === "NAME") severity = "high";
|
|
15569
|
-
return {
|
|
15570
|
-
type,
|
|
15571
|
-
value: actualValue,
|
|
15572
|
-
placeholder: `[${type}_${Math.random().toString(36).substring(2, 9)}]`,
|
|
15573
|
-
position: [entity.start, entity.end],
|
|
15574
|
-
severity,
|
|
15575
|
-
confidence: entity.confidence ?? .7
|
|
15576
|
-
};
|
|
15577
|
-
}
|
|
15578
|
-
/**
|
|
15579
|
-
* Merge AI entities with regex detections
|
|
15580
|
-
* Prefers regex detections on conflicts
|
|
15581
|
-
*/
|
|
15582
|
-
function mergeAIEntities(regexDetections, aiEntities, text) {
|
|
15583
|
-
const merged = [...regexDetections];
|
|
15584
|
-
const processedRanges = regexDetections.map((d) => d.position);
|
|
15585
|
-
for (const aiEntity of aiEntities) {
|
|
15586
|
-
const detection = convertAIEntityToDetection(aiEntity, text);
|
|
15587
|
-
if (!detection) continue;
|
|
15588
|
-
let hasOverlap = false;
|
|
15589
|
-
for (const regexDet of regexDetections) if (detectionsOverlap(regexDet, detection)) {
|
|
15590
|
-
hasOverlap = true;
|
|
15591
|
-
break;
|
|
15592
|
-
}
|
|
15593
|
-
if (!hasOverlap) {
|
|
15594
|
-
merged.push(detection);
|
|
15595
|
-
processedRanges.push(detection.position);
|
|
15596
|
-
}
|
|
15597
|
-
}
|
|
15598
|
-
return merged;
|
|
15599
|
-
}
|
|
15600
|
-
|
|
15601
15229
|
//#endregion
|
|
15602
15230
|
//#region src/config/ConfigExporter.ts
|
|
15603
15231
|
var ConfigExporter_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -17742,7 +17370,7 @@ var OpenRedaction = class OpenRedaction {
|
|
|
17742
17370
|
redactionMode: "placeholder",
|
|
17743
17371
|
enableContextAnalysis: true,
|
|
17744
17372
|
confidenceThreshold: .5,
|
|
17745
|
-
enableFalsePositiveFilter:
|
|
17373
|
+
enableFalsePositiveFilter: true,
|
|
17746
17374
|
falsePositiveThreshold: .7,
|
|
17747
17375
|
enableMultiPass: false,
|
|
17748
17376
|
multiPassCount: 3,
|
|
@@ -17971,8 +17599,9 @@ var OpenRedaction = class OpenRedaction {
|
|
|
17971
17599
|
throw error;
|
|
17972
17600
|
}
|
|
17973
17601
|
}
|
|
17974
|
-
if (this.nerDetector &&
|
|
17975
|
-
const
|
|
17602
|
+
if (this.nerDetector && this.nerDetector.isAvailable()) {
|
|
17603
|
+
const nerMatches = this.nerDetector.detect(text);
|
|
17604
|
+
let piiMatches = detections.map((det) => ({
|
|
17976
17605
|
type: det.type,
|
|
17977
17606
|
value: det.value,
|
|
17978
17607
|
start: det.position[0],
|
|
@@ -17983,11 +17612,43 @@ var OpenRedaction = class OpenRedaction {
|
|
|
17983
17612
|
after: text.substring(det.position[1], Math.min(text.length, det.position[1] + 50))
|
|
17984
17613
|
}
|
|
17985
17614
|
}));
|
|
17986
|
-
|
|
17987
|
-
|
|
17988
|
-
|
|
17989
|
-
|
|
17990
|
-
|
|
17615
|
+
if (detections.length > 0) {
|
|
17616
|
+
const hybridMatches = this.nerDetector.hybridDetection(piiMatches, text);
|
|
17617
|
+
detections = detections.map((det, index) => ({
|
|
17618
|
+
...det,
|
|
17619
|
+
confidence: hybridMatches[index].confidence
|
|
17620
|
+
}));
|
|
17621
|
+
piiMatches = detections.map((det) => ({
|
|
17622
|
+
type: det.type,
|
|
17623
|
+
value: det.value,
|
|
17624
|
+
start: det.position[0],
|
|
17625
|
+
end: det.position[1],
|
|
17626
|
+
confidence: det.confidence || 1,
|
|
17627
|
+
context: {
|
|
17628
|
+
before: text.substring(Math.max(0, det.position[0] - 50), det.position[0]),
|
|
17629
|
+
after: text.substring(det.position[1], Math.min(text.length, det.position[1] + 50))
|
|
17630
|
+
}
|
|
17631
|
+
}));
|
|
17632
|
+
}
|
|
17633
|
+
const nerOnly = this.nerDetector.extractNEROnly(nerMatches, piiMatches);
|
|
17634
|
+
for (const ner of nerOnly) {
|
|
17635
|
+
const syntheticPattern = {
|
|
17636
|
+
type: `NER_${ner.type}`,
|
|
17637
|
+
regex: /.^/,
|
|
17638
|
+
priority: 1,
|
|
17639
|
+
placeholder: `[NER_${ner.type}_{n}]`,
|
|
17640
|
+
severity: "medium"
|
|
17641
|
+
};
|
|
17642
|
+
const placeholder = this.generatePlaceholder(ner.text, syntheticPattern);
|
|
17643
|
+
detections.push({
|
|
17644
|
+
type: syntheticPattern.type,
|
|
17645
|
+
value: ner.text,
|
|
17646
|
+
placeholder,
|
|
17647
|
+
position: [ner.start, ner.end],
|
|
17648
|
+
severity: "medium",
|
|
17649
|
+
confidence: ner.confidence
|
|
17650
|
+
});
|
|
17651
|
+
}
|
|
17991
17652
|
}
|
|
17992
17653
|
if (this.contextRulesEngine && detections.length > 0) {
|
|
17993
17654
|
const piiMatches = detections.map((det) => ({
|
|
@@ -18011,7 +17672,7 @@ var OpenRedaction = class OpenRedaction {
|
|
|
18011
17672
|
}
|
|
18012
17673
|
/**
|
|
18013
17674
|
* Detect PII in text
|
|
18014
|
-
*
|
|
17675
|
+
* Async API for detection pipeline (NER, multi-pass, etc.)
|
|
18015
17676
|
*/
|
|
18016
17677
|
async detect(text) {
|
|
18017
17678
|
if (this.rbacManager && !this.rbacManager.hasPermission("detection:detect")) throw new Error("[OpenRedaction] Permission denied: detection:detect required");
|
|
@@ -18051,21 +17712,6 @@ var OpenRedaction = class OpenRedaction {
|
|
|
18051
17712
|
}
|
|
18052
17713
|
detections = mergePassDetections(passDetections, this.multiPassConfig);
|
|
18053
17714
|
} else detections = this.processPatterns(text, this.patterns, processedRanges);
|
|
18054
|
-
if (this.options.ai?.enabled) {
|
|
18055
|
-
const aiEndpoint = getAIEndpoint(this.options.ai);
|
|
18056
|
-
if (aiEndpoint) try {
|
|
18057
|
-
if (this.options.debug) console.log("[OpenRedaction] AI assist enabled, calling AI endpoint...");
|
|
18058
|
-
const aiEntities = await callAIDetect(text, aiEndpoint, this.options.debug);
|
|
18059
|
-
if (aiEntities && aiEntities.length > 0) {
|
|
18060
|
-
if (this.options.debug) console.log(`[OpenRedaction] AI returned ${aiEntities.length} additional entities`);
|
|
18061
|
-
detections = mergeAIEntities(detections, aiEntities, text);
|
|
18062
|
-
if (this.options.debug) console.log(`[OpenRedaction] After AI merge: ${detections.length} total detections`);
|
|
18063
|
-
} else if (this.options.debug) console.log("[OpenRedaction] AI endpoint returned no additional entities");
|
|
18064
|
-
} catch (error) {
|
|
18065
|
-
if (this.options.debug) console.warn(`[OpenRedaction] AI assist failed, using regex-only: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
18066
|
-
}
|
|
18067
|
-
else if (this.options.debug) console.warn("[OpenRedaction] AI assist enabled but no endpoint configured. Set ai.endpoint or OPENREDACTION_AI_ENDPOINT env var.");
|
|
18068
|
-
}
|
|
18069
17715
|
detections.sort((a, b) => b.position[0] - a.position[0]);
|
|
18070
17716
|
let redacted = text;
|
|
18071
17717
|
const redactionMap = {};
|
|
@@ -18459,6 +18105,10 @@ var OpenRedaction = class OpenRedaction {
|
|
|
18459
18105
|
//#region src/streaming/StreamingDetector.ts
|
|
18460
18106
|
init_document();
|
|
18461
18107
|
/**
|
|
18108
|
+
* Streaming API for processing large documents
|
|
18109
|
+
* Allows efficient processing of documents in chunks
|
|
18110
|
+
*/
|
|
18111
|
+
/**
|
|
18462
18112
|
* Streaming detector for large documents
|
|
18463
18113
|
*/
|
|
18464
18114
|
var StreamingDetector = class {
|
|
@@ -18569,8 +18219,13 @@ var StreamingDetector = class {
|
|
|
18569
18219
|
if (buffer.length > 0) for await (const result of this.processStream(buffer)) yield result;
|
|
18570
18220
|
} else {
|
|
18571
18221
|
const nodeStream = readableStream;
|
|
18572
|
-
|
|
18573
|
-
|
|
18222
|
+
const iterable = typeof nodeStream[Symbol.asyncIterator] === "function" ? nodeStream : Readable.from(nodeStream);
|
|
18223
|
+
for await (const chunk of iterable) {
|
|
18224
|
+
if (typeof chunk === "string") buffer += chunk;
|
|
18225
|
+
else {
|
|
18226
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
18227
|
+
buffer += decoder.decode(buf, { stream: true });
|
|
18228
|
+
}
|
|
18574
18229
|
while (buffer.length >= this.options.chunkSize) {
|
|
18575
18230
|
const textChunk = buffer.substring(0, this.options.chunkSize);
|
|
18576
18231
|
buffer = buffer.substring(this.options.chunkSize);
|
|
@@ -19612,647 +19267,11 @@ function verifyWebhookSignature(payload, signature, secret, algorithm = "sha256"
|
|
|
19612
19267
|
}
|
|
19613
19268
|
}
|
|
19614
19269
|
|
|
19615
|
-
//#endregion
|
|
19616
|
-
//#region src/api/APIServer.ts
|
|
19617
|
-
/**
|
|
19618
|
-
* REST API Server
|
|
19619
|
-
* Lightweight HTTP server for OpenRedaction with Express-like interface
|
|
19620
|
-
*/
|
|
19621
|
-
var APIServer = class {
|
|
19622
|
-
constructor(config) {
|
|
19623
|
-
this.isRunning = false;
|
|
19624
|
-
this.rateLimitTracking = /* @__PURE__ */ new Map();
|
|
19625
|
-
this.config = {
|
|
19626
|
-
port: config?.port ?? 3e3,
|
|
19627
|
-
host: config?.host ?? "0.0.0.0",
|
|
19628
|
-
enableCors: config?.enableCors ?? true,
|
|
19629
|
-
corsOrigin: config?.corsOrigin ?? "*",
|
|
19630
|
-
apiKey: config?.apiKey,
|
|
19631
|
-
enableRateLimit: config?.enableRateLimit ?? true,
|
|
19632
|
-
rateLimit: config?.rateLimit ?? 60,
|
|
19633
|
-
bodyLimit: config?.bodyLimit ?? "10mb",
|
|
19634
|
-
enableLogging: config?.enableLogging ?? true,
|
|
19635
|
-
tenantManager: config?.tenantManager,
|
|
19636
|
-
webhookManager: config?.webhookManager,
|
|
19637
|
-
auditLogger: config?.auditLogger,
|
|
19638
|
-
prometheusServer: config?.prometheusServer,
|
|
19639
|
-
defaultOptions: config?.defaultOptions
|
|
19640
|
-
};
|
|
19641
|
-
if (!this.config.tenantManager) this.detector = new OpenRedaction(this.config.defaultOptions);
|
|
19642
|
-
}
|
|
19643
|
-
/**
|
|
19644
|
-
* Start the API server
|
|
19645
|
-
*/
|
|
19646
|
-
async start() {
|
|
19647
|
-
if (this.isRunning) throw new Error("[APIServer] Server is already running");
|
|
19648
|
-
try {
|
|
19649
|
-
this.server = __require("http").createServer(this.handleRequest.bind(this));
|
|
19650
|
-
return new Promise((resolve, reject) => {
|
|
19651
|
-
this.server.listen(this.config.port, this.config.host, () => {
|
|
19652
|
-
this.isRunning = true;
|
|
19653
|
-
console.log(`[APIServer] Server started on http://${this.config.host}:${this.config.port}`);
|
|
19654
|
-
console.log(`[APIServer] API Documentation: http://${this.config.host}:${this.config.port}/api/docs`);
|
|
19655
|
-
resolve();
|
|
19656
|
-
});
|
|
19657
|
-
this.server.on("error", (error) => {
|
|
19658
|
-
reject(/* @__PURE__ */ new Error(`[APIServer] Failed to start server: ${error.message}`));
|
|
19659
|
-
});
|
|
19660
|
-
});
|
|
19661
|
-
} catch (error) {
|
|
19662
|
-
throw new Error(`[APIServer] Failed to initialize HTTP server: ${error.message}`);
|
|
19663
|
-
}
|
|
19664
|
-
}
|
|
19665
|
-
/**
|
|
19666
|
-
* Stop the server
|
|
19667
|
-
*/
|
|
19668
|
-
async stop() {
|
|
19669
|
-
if (!this.isRunning || !this.server) return;
|
|
19670
|
-
return new Promise((resolve, reject) => {
|
|
19671
|
-
this.server.close((error) => {
|
|
19672
|
-
if (error) reject(/* @__PURE__ */ new Error(`[APIServer] Failed to stop server: ${error.message}`));
|
|
19673
|
-
else {
|
|
19674
|
-
this.isRunning = false;
|
|
19675
|
-
console.log("[APIServer] Server stopped");
|
|
19676
|
-
resolve();
|
|
19677
|
-
}
|
|
19678
|
-
});
|
|
19679
|
-
});
|
|
19680
|
-
}
|
|
19681
|
-
/**
|
|
19682
|
-
* Handle incoming HTTP requests
|
|
19683
|
-
*/
|
|
19684
|
-
async handleRequest(req, res) {
|
|
19685
|
-
const startTime = Date.now();
|
|
19686
|
-
try {
|
|
19687
|
-
const apiReq = await this.parseRequest(req);
|
|
19688
|
-
if (this.config.enableCors) {
|
|
19689
|
-
res.setHeader("Access-Control-Allow-Origin", Array.isArray(this.config.corsOrigin) ? this.config.corsOrigin.join(", ") : this.config.corsOrigin);
|
|
19690
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
19691
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key, X-Tenant-ID");
|
|
19692
|
-
if (req.method === "OPTIONS") {
|
|
19693
|
-
res.writeHead(204);
|
|
19694
|
-
res.end();
|
|
19695
|
-
return;
|
|
19696
|
-
}
|
|
19697
|
-
}
|
|
19698
|
-
if (this.config.apiKey) {
|
|
19699
|
-
if ((apiReq.headers["x-api-key"] || apiReq.headers["authorization"]?.toString().replace("Bearer ", "")) !== this.config.apiKey) {
|
|
19700
|
-
this.sendResponse(res, {
|
|
19701
|
-
status: 401,
|
|
19702
|
-
body: {
|
|
19703
|
-
error: "Unauthorized",
|
|
19704
|
-
message: "Invalid API key"
|
|
19705
|
-
}
|
|
19706
|
-
});
|
|
19707
|
-
return;
|
|
19708
|
-
}
|
|
19709
|
-
}
|
|
19710
|
-
if (this.config.tenantManager) {
|
|
19711
|
-
const tenantApiKey = apiReq.headers["x-api-key"];
|
|
19712
|
-
if (tenantApiKey) {
|
|
19713
|
-
const tenant = this.config.tenantManager.authenticateByApiKey(tenantApiKey);
|
|
19714
|
-
if (tenant) apiReq.tenantId = tenant.tenantId;
|
|
19715
|
-
else {
|
|
19716
|
-
this.sendResponse(res, {
|
|
19717
|
-
status: 401,
|
|
19718
|
-
body: {
|
|
19719
|
-
error: "Unauthorized",
|
|
19720
|
-
message: "Invalid tenant API key"
|
|
19721
|
-
}
|
|
19722
|
-
});
|
|
19723
|
-
return;
|
|
19724
|
-
}
|
|
19725
|
-
} else {
|
|
19726
|
-
const tenantId = apiReq.headers["x-tenant-id"];
|
|
19727
|
-
if (!tenantId) {
|
|
19728
|
-
this.sendResponse(res, {
|
|
19729
|
-
status: 400,
|
|
19730
|
-
body: {
|
|
19731
|
-
error: "Bad Request",
|
|
19732
|
-
message: "X-Tenant-ID header required for multi-tenant mode"
|
|
19733
|
-
}
|
|
19734
|
-
});
|
|
19735
|
-
return;
|
|
19736
|
-
}
|
|
19737
|
-
apiReq.tenantId = tenantId;
|
|
19738
|
-
}
|
|
19739
|
-
}
|
|
19740
|
-
if (this.config.enableRateLimit) {
|
|
19741
|
-
const clientKey = apiReq.tenantId || apiReq.ip || "unknown";
|
|
19742
|
-
if (!this.checkRateLimit(clientKey)) {
|
|
19743
|
-
this.sendResponse(res, {
|
|
19744
|
-
status: 429,
|
|
19745
|
-
body: {
|
|
19746
|
-
error: "Too Many Requests",
|
|
19747
|
-
message: `Rate limit exceeded: ${this.config.rateLimit} requests per minute`
|
|
19748
|
-
}
|
|
19749
|
-
});
|
|
19750
|
-
return;
|
|
19751
|
-
}
|
|
19752
|
-
}
|
|
19753
|
-
const response = await this.routeRequest(apiReq);
|
|
19754
|
-
if (this.config.enableLogging) {
|
|
19755
|
-
const durationMs = Date.now() - startTime;
|
|
19756
|
-
console.log(`[APIServer] ${req.method} ${req.url} ${response.status} ${durationMs}ms`);
|
|
19757
|
-
}
|
|
19758
|
-
this.sendResponse(res, response);
|
|
19759
|
-
} catch (error) {
|
|
19760
|
-
console.error("[APIServer] Request handler error:", error);
|
|
19761
|
-
this.sendResponse(res, {
|
|
19762
|
-
status: 500,
|
|
19763
|
-
body: {
|
|
19764
|
-
error: "Internal Server Error",
|
|
19765
|
-
message: error.message
|
|
19766
|
-
}
|
|
19767
|
-
});
|
|
19768
|
-
}
|
|
19769
|
-
}
|
|
19770
|
-
/**
|
|
19771
|
-
* Parse HTTP request
|
|
19772
|
-
*/
|
|
19773
|
-
async parseRequest(req) {
|
|
19774
|
-
const url = new URL(req.url, `http://${req.headers.host || "localhost"}`);
|
|
19775
|
-
let body = {};
|
|
19776
|
-
if (req.method !== "GET" && req.method !== "HEAD") body = await this.parseBody(req);
|
|
19777
|
-
return {
|
|
19778
|
-
body,
|
|
19779
|
-
headers: req.headers,
|
|
19780
|
-
query: Object.fromEntries(url.searchParams),
|
|
19781
|
-
params: {},
|
|
19782
|
-
ip: req.socket.remoteAddress
|
|
19783
|
-
};
|
|
19784
|
-
}
|
|
19785
|
-
/**
|
|
19786
|
-
* Parse request body
|
|
19787
|
-
*/
|
|
19788
|
-
async parseBody(req) {
|
|
19789
|
-
return new Promise((resolve, reject) => {
|
|
19790
|
-
let body = "";
|
|
19791
|
-
req.on("data", (chunk) => {
|
|
19792
|
-
body += chunk.toString();
|
|
19793
|
-
});
|
|
19794
|
-
req.on("end", () => {
|
|
19795
|
-
try {
|
|
19796
|
-
resolve(JSON.parse(body || "{}"));
|
|
19797
|
-
} catch {
|
|
19798
|
-
resolve({});
|
|
19799
|
-
}
|
|
19800
|
-
});
|
|
19801
|
-
req.on("error", reject);
|
|
19802
|
-
});
|
|
19803
|
-
}
|
|
19804
|
-
/**
|
|
19805
|
-
* Route request to appropriate handler
|
|
19806
|
-
*/
|
|
19807
|
-
async routeRequest(req) {
|
|
19808
|
-
const path = new URL(req.headers.host ? `http://${req.headers.host}` : "http://localhost").pathname;
|
|
19809
|
-
const method = req.headers[":method"] || "GET";
|
|
19810
|
-
if (path === "/api/detect" && method === "POST") return this.handleDetect(req);
|
|
19811
|
-
if (path === "/api/redact" && method === "POST") return this.handleRedact(req);
|
|
19812
|
-
if (path === "/api/restore" && method === "POST") return this.handleRestore(req);
|
|
19813
|
-
if (path === "/api/audit/logs" && method === "GET") return this.handleAuditLogs(req);
|
|
19814
|
-
if (path === "/api/audit/stats" && method === "GET") return this.handleAuditStats(req);
|
|
19815
|
-
if (path === "/api/metrics" && method === "GET") return this.handleMetrics(req);
|
|
19816
|
-
if (path === "/api/patterns" && method === "GET") return this.handleGetPatterns(req);
|
|
19817
|
-
if (path === "/api/health" && method === "GET") return this.handleHealth(req);
|
|
19818
|
-
if (path === "/api/docs" && method === "GET") return this.handleDocs(req);
|
|
19819
|
-
if (path === "/" && method === "GET") return this.handleRoot(req);
|
|
19820
|
-
return {
|
|
19821
|
-
status: 404,
|
|
19822
|
-
body: {
|
|
19823
|
-
error: "Not Found",
|
|
19824
|
-
message: `Route not found: ${method} ${path}`
|
|
19825
|
-
}
|
|
19826
|
-
};
|
|
19827
|
-
}
|
|
19828
|
-
/**
|
|
19829
|
-
* Handle POST /api/detect
|
|
19830
|
-
*/
|
|
19831
|
-
async handleDetect(req) {
|
|
19832
|
-
const { text } = req.body;
|
|
19833
|
-
if (!text || typeof text !== "string") return {
|
|
19834
|
-
status: 400,
|
|
19835
|
-
body: {
|
|
19836
|
-
error: "Bad Request",
|
|
19837
|
-
message: "Missing or invalid \"text\" field"
|
|
19838
|
-
}
|
|
19839
|
-
};
|
|
19840
|
-
try {
|
|
19841
|
-
let result;
|
|
19842
|
-
if (req.tenantId && this.config.tenantManager) result = await this.config.tenantManager.detect(req.tenantId, text);
|
|
19843
|
-
else if (this.detector) result = await this.detector.detect(text);
|
|
19844
|
-
else throw new Error("No detector available");
|
|
19845
|
-
if (this.config.webhookManager) {
|
|
19846
|
-
await this.config.webhookManager.emitHighRiskPII(result, req.tenantId);
|
|
19847
|
-
await this.config.webhookManager.emitBulkPII(result, 10, req.tenantId);
|
|
19848
|
-
}
|
|
19849
|
-
return {
|
|
19850
|
-
status: 200,
|
|
19851
|
-
body: {
|
|
19852
|
-
success: true,
|
|
19853
|
-
result: {
|
|
19854
|
-
detections: result.detections,
|
|
19855
|
-
stats: result.stats
|
|
19856
|
-
}
|
|
19857
|
-
}
|
|
19858
|
-
};
|
|
19859
|
-
} catch (error) {
|
|
19860
|
-
return {
|
|
19861
|
-
status: 500,
|
|
19862
|
-
body: {
|
|
19863
|
-
error: "Detection Failed",
|
|
19864
|
-
message: error.message
|
|
19865
|
-
}
|
|
19866
|
-
};
|
|
19867
|
-
}
|
|
19868
|
-
}
|
|
19869
|
-
/**
|
|
19870
|
-
* Handle POST /api/redact
|
|
19871
|
-
*/
|
|
19872
|
-
async handleRedact(req) {
|
|
19873
|
-
const { text } = req.body;
|
|
19874
|
-
if (!text || typeof text !== "string") return {
|
|
19875
|
-
status: 400,
|
|
19876
|
-
body: {
|
|
19877
|
-
error: "Bad Request",
|
|
19878
|
-
message: "Missing or invalid \"text\" field"
|
|
19879
|
-
}
|
|
19880
|
-
};
|
|
19881
|
-
try {
|
|
19882
|
-
let result;
|
|
19883
|
-
if (req.tenantId && this.config.tenantManager) result = await this.config.tenantManager.detect(req.tenantId, text);
|
|
19884
|
-
else if (this.detector) result = await this.detector.detect(text);
|
|
19885
|
-
else throw new Error("No detector available");
|
|
19886
|
-
return {
|
|
19887
|
-
status: 200,
|
|
19888
|
-
body: {
|
|
19889
|
-
success: true,
|
|
19890
|
-
result: {
|
|
19891
|
-
original: result.original,
|
|
19892
|
-
redacted: result.redacted,
|
|
19893
|
-
detections: result.detections,
|
|
19894
|
-
stats: result.stats
|
|
19895
|
-
}
|
|
19896
|
-
}
|
|
19897
|
-
};
|
|
19898
|
-
} catch (error) {
|
|
19899
|
-
return {
|
|
19900
|
-
status: 500,
|
|
19901
|
-
body: {
|
|
19902
|
-
error: "Redaction Failed",
|
|
19903
|
-
message: error.message
|
|
19904
|
-
}
|
|
19905
|
-
};
|
|
19906
|
-
}
|
|
19907
|
-
}
|
|
19908
|
-
/**
|
|
19909
|
-
* Handle POST /api/restore
|
|
19910
|
-
*/
|
|
19911
|
-
async handleRestore(req) {
|
|
19912
|
-
const { redacted, redactionMap } = req.body;
|
|
19913
|
-
if (!redacted || !redactionMap) return {
|
|
19914
|
-
status: 400,
|
|
19915
|
-
body: {
|
|
19916
|
-
error: "Bad Request",
|
|
19917
|
-
message: "Missing \"redacted\" or \"redactionMap\" fields"
|
|
19918
|
-
}
|
|
19919
|
-
};
|
|
19920
|
-
try {
|
|
19921
|
-
let detector;
|
|
19922
|
-
if (req.tenantId && this.config.tenantManager) detector = this.config.tenantManager.getDetector(req.tenantId);
|
|
19923
|
-
else if (this.detector) detector = this.detector;
|
|
19924
|
-
else throw new Error("No detector available");
|
|
19925
|
-
return {
|
|
19926
|
-
status: 200,
|
|
19927
|
-
body: {
|
|
19928
|
-
success: true,
|
|
19929
|
-
result: { restored: detector.restore(redacted, redactionMap) }
|
|
19930
|
-
}
|
|
19931
|
-
};
|
|
19932
|
-
} catch (error) {
|
|
19933
|
-
return {
|
|
19934
|
-
status: 500,
|
|
19935
|
-
body: {
|
|
19936
|
-
error: "Restore Failed",
|
|
19937
|
-
message: error.message
|
|
19938
|
-
}
|
|
19939
|
-
};
|
|
19940
|
-
}
|
|
19941
|
-
}
|
|
19942
|
-
/**
|
|
19943
|
-
* Handle GET /api/audit/logs
|
|
19944
|
-
*/
|
|
19945
|
-
async handleAuditLogs(req) {
|
|
19946
|
-
if (!this.config.auditLogger) return {
|
|
19947
|
-
status: 501,
|
|
19948
|
-
body: {
|
|
19949
|
-
error: "Not Implemented",
|
|
19950
|
-
message: "Audit logging not configured"
|
|
19951
|
-
}
|
|
19952
|
-
};
|
|
19953
|
-
try {
|
|
19954
|
-
const limit = parseInt(req.query.limit || "100");
|
|
19955
|
-
return {
|
|
19956
|
-
status: 200,
|
|
19957
|
-
body: {
|
|
19958
|
-
success: true,
|
|
19959
|
-
logs: await this.config.auditLogger.queryLogs({ limit })
|
|
19960
|
-
}
|
|
19961
|
-
};
|
|
19962
|
-
} catch (error) {
|
|
19963
|
-
return {
|
|
19964
|
-
status: 500,
|
|
19965
|
-
body: {
|
|
19966
|
-
error: "Query Failed",
|
|
19967
|
-
message: error.message
|
|
19968
|
-
}
|
|
19969
|
-
};
|
|
19970
|
-
}
|
|
19971
|
-
}
|
|
19972
|
-
/**
|
|
19973
|
-
* Handle GET /api/audit/stats
|
|
19974
|
-
*/
|
|
19975
|
-
async handleAuditStats(_req) {
|
|
19976
|
-
if (!this.config.auditLogger) return {
|
|
19977
|
-
status: 501,
|
|
19978
|
-
body: {
|
|
19979
|
-
error: "Not Implemented",
|
|
19980
|
-
message: "Audit logging not configured"
|
|
19981
|
-
}
|
|
19982
|
-
};
|
|
19983
|
-
try {
|
|
19984
|
-
return {
|
|
19985
|
-
status: 200,
|
|
19986
|
-
body: {
|
|
19987
|
-
success: true,
|
|
19988
|
-
stats: await this.config.auditLogger.getStatsAsync()
|
|
19989
|
-
}
|
|
19990
|
-
};
|
|
19991
|
-
} catch (error) {
|
|
19992
|
-
return {
|
|
19993
|
-
status: 500,
|
|
19994
|
-
body: {
|
|
19995
|
-
error: "Query Failed",
|
|
19996
|
-
message: error.message
|
|
19997
|
-
}
|
|
19998
|
-
};
|
|
19999
|
-
}
|
|
20000
|
-
}
|
|
20001
|
-
/**
|
|
20002
|
-
* Handle GET /api/metrics
|
|
20003
|
-
*/
|
|
20004
|
-
async handleMetrics(req) {
|
|
20005
|
-
if (this.detector) return {
|
|
20006
|
-
status: 200,
|
|
20007
|
-
body: {
|
|
20008
|
-
success: true,
|
|
20009
|
-
metrics: {}
|
|
20010
|
-
}
|
|
20011
|
-
};
|
|
20012
|
-
if (this.config.tenantManager && req.tenantId) return {
|
|
20013
|
-
status: 200,
|
|
20014
|
-
body: {
|
|
20015
|
-
success: true,
|
|
20016
|
-
metrics: this.config.tenantManager.getTenantUsage(req.tenantId)
|
|
20017
|
-
}
|
|
20018
|
-
};
|
|
20019
|
-
return {
|
|
20020
|
-
status: 501,
|
|
20021
|
-
body: {
|
|
20022
|
-
error: "Not Implemented",
|
|
20023
|
-
message: "Metrics not configured"
|
|
20024
|
-
}
|
|
20025
|
-
};
|
|
20026
|
-
}
|
|
20027
|
-
/**
|
|
20028
|
-
* Handle GET /api/patterns
|
|
20029
|
-
*/
|
|
20030
|
-
async handleGetPatterns(req) {
|
|
20031
|
-
try {
|
|
20032
|
-
let detector;
|
|
20033
|
-
if (req.tenantId && this.config.tenantManager) detector = this.config.tenantManager.getDetector(req.tenantId);
|
|
20034
|
-
else if (this.detector) detector = this.detector;
|
|
20035
|
-
else throw new Error("No detector available");
|
|
20036
|
-
return {
|
|
20037
|
-
status: 200,
|
|
20038
|
-
body: {
|
|
20039
|
-
success: true,
|
|
20040
|
-
patterns: detector.getPatterns().map((p) => ({
|
|
20041
|
-
type: p.type,
|
|
20042
|
-
priority: p.priority,
|
|
20043
|
-
description: p.description,
|
|
20044
|
-
severity: p.severity
|
|
20045
|
-
}))
|
|
20046
|
-
}
|
|
20047
|
-
};
|
|
20048
|
-
} catch (error) {
|
|
20049
|
-
return {
|
|
20050
|
-
status: 500,
|
|
20051
|
-
body: {
|
|
20052
|
-
error: "Query Failed",
|
|
20053
|
-
message: error.message
|
|
20054
|
-
}
|
|
20055
|
-
};
|
|
20056
|
-
}
|
|
20057
|
-
}
|
|
20058
|
-
/**
|
|
20059
|
-
* Handle GET /api/health
|
|
20060
|
-
*/
|
|
20061
|
-
async handleHealth(_req) {
|
|
20062
|
-
return {
|
|
20063
|
-
status: 200,
|
|
20064
|
-
body: {
|
|
20065
|
-
status: "healthy",
|
|
20066
|
-
uptime: process.uptime(),
|
|
20067
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
20068
|
-
multiTenant: !!this.config.tenantManager,
|
|
20069
|
-
features: {
|
|
20070
|
-
audit: !!this.config.auditLogger,
|
|
20071
|
-
webhooks: !!this.config.webhookManager,
|
|
20072
|
-
prometheus: !!this.config.prometheusServer
|
|
20073
|
-
}
|
|
20074
|
-
}
|
|
20075
|
-
};
|
|
20076
|
-
}
|
|
20077
|
-
/**
|
|
20078
|
-
* Handle GET /api/docs
|
|
20079
|
-
*/
|
|
20080
|
-
async handleDocs(_req) {
|
|
20081
|
-
return {
|
|
20082
|
-
status: 200,
|
|
20083
|
-
body: `
|
|
20084
|
-
<!DOCTYPE html>
|
|
20085
|
-
<html>
|
|
20086
|
-
<head>
|
|
20087
|
-
<title>OpenRedaction API Documentation</title>
|
|
20088
|
-
<style>
|
|
20089
|
-
body { font-family: sans-serif; max-width: 1200px; margin: 50px auto; padding: 20px; }
|
|
20090
|
-
h1 { color: #333; }
|
|
20091
|
-
h2 { color: #666; margin-top: 30px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
|
|
20092
|
-
.endpoint { background: #f5f5f5; padding: 15px; margin: 15px 0; border-radius: 4px; border-left: 4px solid #0066cc; }
|
|
20093
|
-
.method { display: inline-block; padding: 4px 8px; border-radius: 3px; font-weight: bold; font-size: 12px; margin-right: 10px; }
|
|
20094
|
-
.method.post { background: #49cc90; color: white; }
|
|
20095
|
-
.method.get { background: #61affe; color: white; }
|
|
20096
|
-
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-family: monospace; }
|
|
20097
|
-
pre { background: #f5f5f5; padding: 15px; border-radius: 4px; overflow-x: auto; }
|
|
20098
|
-
</style>
|
|
20099
|
-
</head>
|
|
20100
|
-
<body>
|
|
20101
|
-
<h1>OpenRedaction REST API</h1>
|
|
20102
|
-
<p>Production-ready PII detection and redaction API</p>
|
|
20103
|
-
|
|
20104
|
-
<h2>Authentication</h2>
|
|
20105
|
-
<p>Include your API key in the <code>X-API-Key</code> header or <code>Authorization: Bearer YOUR_KEY</code> header.</p>
|
|
20106
|
-
${this.config.tenantManager ? "<p>For multi-tenant mode, also include <code>X-Tenant-ID</code> header.</p>" : ""}
|
|
20107
|
-
|
|
20108
|
-
<h2>Endpoints</h2>
|
|
20109
|
-
|
|
20110
|
-
<div class="endpoint">
|
|
20111
|
-
<span class="method post">POST</span>
|
|
20112
|
-
<strong>/api/detect</strong>
|
|
20113
|
-
<p>Detect PII in text without redaction</p>
|
|
20114
|
-
<pre>{
|
|
20115
|
-
"text": "My email is john@example.com and SSN is 123-45-6789",
|
|
20116
|
-
"options": {
|
|
20117
|
-
"includeNames": true,
|
|
20118
|
-
"includeEmails": true
|
|
20119
|
-
}
|
|
20120
|
-
}</pre>
|
|
20121
|
-
</div>
|
|
20122
|
-
|
|
20123
|
-
<div class="endpoint">
|
|
20124
|
-
<span class="method post">POST</span>
|
|
20125
|
-
<strong>/api/redact</strong>
|
|
20126
|
-
<p>Detect and redact PII in text</p>
|
|
20127
|
-
<pre>{
|
|
20128
|
-
"text": "My email is john@example.com",
|
|
20129
|
-
"options": {
|
|
20130
|
-
"redactionMode": "placeholder"
|
|
20131
|
-
}
|
|
20132
|
-
}</pre>
|
|
20133
|
-
</div>
|
|
20134
|
-
|
|
20135
|
-
<div class="endpoint">
|
|
20136
|
-
<span class="method post">POST</span>
|
|
20137
|
-
<strong>/api/restore</strong>
|
|
20138
|
-
<p>Restore original text from redacted text</p>
|
|
20139
|
-
<pre>{
|
|
20140
|
-
"redacted": "My email is [EMAIL_1]",
|
|
20141
|
-
"redactionMap": {
|
|
20142
|
-
"[EMAIL_1]": "john@example.com"
|
|
20143
|
-
}
|
|
20144
|
-
}</pre>
|
|
20145
|
-
</div>
|
|
20146
|
-
|
|
20147
|
-
<div class="endpoint">
|
|
20148
|
-
<span class="method get">GET</span>
|
|
20149
|
-
<strong>/api/patterns</strong>
|
|
20150
|
-
<p>Get available PII patterns</p>
|
|
20151
|
-
</div>
|
|
20152
|
-
|
|
20153
|
-
<div class="endpoint">
|
|
20154
|
-
<span class="method get">GET</span>
|
|
20155
|
-
<strong>/api/audit/logs</strong>
|
|
20156
|
-
<p>Get audit logs (requires audit logger)</p>
|
|
20157
|
-
<p>Query params: <code>limit</code> (default: 100)</p>
|
|
20158
|
-
</div>
|
|
20159
|
-
|
|
20160
|
-
<div class="endpoint">
|
|
20161
|
-
<span class="method get">GET</span>
|
|
20162
|
-
<strong>/api/audit/stats</strong>
|
|
20163
|
-
<p>Get audit statistics (requires audit logger)</p>
|
|
20164
|
-
</div>
|
|
20165
|
-
|
|
20166
|
-
<div class="endpoint">
|
|
20167
|
-
<span class="method get">GET</span>
|
|
20168
|
-
<strong>/api/metrics</strong>
|
|
20169
|
-
<p>Get usage metrics</p>
|
|
20170
|
-
</div>
|
|
20171
|
-
|
|
20172
|
-
<div class="endpoint">
|
|
20173
|
-
<span class="method get">GET</span>
|
|
20174
|
-
<strong>/api/health</strong>
|
|
20175
|
-
<p>Health check endpoint</p>
|
|
20176
|
-
</div>
|
|
20177
|
-
|
|
20178
|
-
<h2>Configuration</h2>
|
|
20179
|
-
<ul>
|
|
20180
|
-
<li>Port: ${this.config.port}</li>
|
|
20181
|
-
<li>Host: ${this.config.host}</li>
|
|
20182
|
-
<li>CORS: ${this.config.enableCors ? "Enabled" : "Disabled"}</li>
|
|
20183
|
-
<li>Rate Limiting: ${this.config.enableRateLimit ? `${this.config.rateLimit} req/min` : "Disabled"}</li>
|
|
20184
|
-
<li>Multi-Tenant: ${this.config.tenantManager ? "Enabled" : "Disabled"}</li>
|
|
20185
|
-
<li>Audit Logging: ${this.config.auditLogger ? "Enabled" : "Disabled"}</li>
|
|
20186
|
-
<li>Webhooks: ${this.config.webhookManager ? "Enabled" : "Disabled"}</li>
|
|
20187
|
-
</ul>
|
|
20188
|
-
</body>
|
|
20189
|
-
</html>
|
|
20190
|
-
`.trim(),
|
|
20191
|
-
headers: { "Content-Type": "text/html" }
|
|
20192
|
-
};
|
|
20193
|
-
}
|
|
20194
|
-
/**
|
|
20195
|
-
* Handle GET /
|
|
20196
|
-
*/
|
|
20197
|
-
async handleRoot(_req) {
|
|
20198
|
-
return {
|
|
20199
|
-
status: 200,
|
|
20200
|
-
body: {
|
|
20201
|
-
name: "OpenRedaction API",
|
|
20202
|
-
version: "1.0.0",
|
|
20203
|
-
documentation: `/api/docs`,
|
|
20204
|
-
health: `/api/health`,
|
|
20205
|
-
endpoints: [
|
|
20206
|
-
"POST /api/detect",
|
|
20207
|
-
"POST /api/redact",
|
|
20208
|
-
"POST /api/restore",
|
|
20209
|
-
"GET /api/patterns",
|
|
20210
|
-
"GET /api/audit/logs",
|
|
20211
|
-
"GET /api/audit/stats",
|
|
20212
|
-
"GET /api/metrics",
|
|
20213
|
-
"GET /api/health"
|
|
20214
|
-
]
|
|
20215
|
-
}
|
|
20216
|
-
};
|
|
20217
|
-
}
|
|
20218
|
-
/**
|
|
20219
|
-
* Send HTTP response
|
|
20220
|
-
*/
|
|
20221
|
-
sendResponse(res, response) {
|
|
20222
|
-
const headers = {
|
|
20223
|
-
"Content-Type": "application/json",
|
|
20224
|
-
...response.headers
|
|
20225
|
-
};
|
|
20226
|
-
res.writeHead(response.status, headers);
|
|
20227
|
-
if (typeof response.body === "string") res.end(response.body);
|
|
20228
|
-
else res.end(JSON.stringify(response.body));
|
|
20229
|
-
}
|
|
20230
|
-
/**
|
|
20231
|
-
* Check rate limit
|
|
20232
|
-
*/
|
|
20233
|
-
checkRateLimit(clientKey) {
|
|
20234
|
-
const now = Date.now();
|
|
20235
|
-
const timestamps = this.rateLimitTracking.get(clientKey) || [];
|
|
20236
|
-
const oneMinuteAgo = now - 60 * 1e3;
|
|
20237
|
-
const recentTimestamps = timestamps.filter((ts) => ts > oneMinuteAgo);
|
|
20238
|
-
if (recentTimestamps.length >= this.config.rateLimit) return false;
|
|
20239
|
-
recentTimestamps.push(now);
|
|
20240
|
-
this.rateLimitTracking.set(clientKey, recentTimestamps);
|
|
20241
|
-
return true;
|
|
20242
|
-
}
|
|
20243
|
-
};
|
|
20244
|
-
/**
|
|
20245
|
-
* Create an API server instance
|
|
20246
|
-
*/
|
|
20247
|
-
function createAPIServer(config) {
|
|
20248
|
-
return new APIServer(config);
|
|
20249
|
-
}
|
|
20250
|
-
|
|
20251
19270
|
//#endregion
|
|
20252
19271
|
//#region src/index.ts
|
|
20253
19272
|
init_ConfigExporter();
|
|
20254
19273
|
init_HealthCheck();
|
|
20255
19274
|
|
|
20256
19275
|
//#endregion
|
|
20257
|
-
export { ADMIN_ROLE, ALL_PERMISSIONS, ANALYST_ROLE,
|
|
19276
|
+
export { ADMIN_ROLE, ALL_PERMISSIONS, ANALYST_ROLE, BatchProcessor, ConfigExporter, ConfigLoader, ConsoleAuditLogger, ContextRulesEngine, CsvProcessor, DEFAULT_DOMAIN_VOCABULARIES, DEFAULT_PROXIMITY_RULES, DEFAULT_SEVERITY_MAP, DEFAULT_TIER_QUOTAS, DocumentProcessor, ExplainAPI, HealthChecker, InMemoryAuditLogger, InMemoryMetricsCollector, JsonProcessor, LocalLearningStore, NERDetector, OCRProcessor, OPERATOR_ROLE, OpenRedaction, OpenRedactionError, PersistentAuditLogger, PriorityOptimizer, RBACManager, RegexMaxMatchesError, RegexTimeoutError, ReportGenerator, SEVERITY_SCORES, SeverityClassifier, StreamingDetector, TenantManager, TenantNotFoundError, TenantQuotaExceededError, TenantSuspendedError, VIEWER_ROLE, WebhookManager, WorkerPool, XlsxProcessor, allPatterns, analyzeContextFeatures, analyzeFullContext, calculateContextConfidence, calculateRisk, ccpaPreset, commonFalsePositives, compileSafeRegex, contactPatterns, createBatchProcessor, createCacheDisabledError, createConfigLoadError, createConfigPreset, createContextRulesEngine, createCsvProcessor, createCustomRole, createDocumentProcessor, createExplainAPI, createHealthChecker, createInvalidPatternError, createJsonProcessor, createLearningDisabledError, createMultiPassDisabledError, createNERDetector, createOCRProcessor, createOptimizationDisabledError, createPersistentAuditLogger, createPriorityOptimizer, createRBACManager, createReportGenerator, createSeverityClassifier, createSimpleMultiPass, createStreamingDetector, createTenantManager, createValidationError, createWebhookManager, createWorkerPool, createXlsxProcessor, defaultPasses, detectPII, educationPreset, exportForVersionControl, extractContext, filterFalsePositives, financePreset, financialPatterns, gdprPreset, generateReport, getPatternsByCategory, getPredefinedRole, getPreset, getSeverity, governmentPatterns, groupPatternsByPass, healthCheckMiddleware, healthcarePreset, healthcareResearchPreset, hipaaPreset, inferDocumentType, isFalsePositive, isUnsafePattern, mergePassDetections, networkPatterns, openredactionMiddleware, pciDssPreset, personalPatterns, safeExec, safeExecAll, soc2Preset, transportLogisticsPreset, validateAustralianTFN, validateCanadianSIN, validateEmail, validateIBAN, validateLuhn, validateNHS, validateNINO, validateName, validatePattern, validateRoutingNumber, validateSSN, validateSWIFTBIC, validateSortCode, validateUKPassport, verifyWebhookSignature };
|
|
20258
19277
|
//# sourceMappingURL=index.mjs.map
|