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