@replayio-app-building/netlify-recorder 0.19.0 → 0.21.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 +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +45 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -464,13 +464,14 @@ Creates the `audit_log` table, its indexes, and a reusable PL/pgSQL trigger func
|
|
|
464
464
|
**Parameters:**
|
|
465
465
|
- `sql` — A Neon SQL tagged-template function
|
|
466
466
|
|
|
467
|
-
### `databaseAuditMonitorTable(sql, tableName): Promise<void>`
|
|
467
|
+
### `databaseAuditMonitorTable(sql, tableName, primaryKeyColumn?): Promise<void>`
|
|
468
468
|
|
|
469
469
|
Attaches a trigger to the specified table that logs INSERT, UPDATE, and DELETE operations to the `audit_log` table. Throws if `tableName` is `'audit_log'`.
|
|
470
470
|
|
|
471
471
|
**Parameters:**
|
|
472
472
|
- `sql` — A Neon SQL tagged-template function
|
|
473
473
|
- `tableName` — Name of the table to monitor (must match `^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
|
474
|
+
- `primaryKeyColumn` — Name of the primary key column (default: `"id"`, must match `^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
|
474
475
|
|
|
475
476
|
### `databaseAuditDumpLogTable(sql): Promise<Record<string, unknown>[]>`
|
|
476
477
|
|
package/dist/index.d.ts
CHANGED
|
@@ -332,7 +332,7 @@ declare function databaseAuditEnsureLogTable(sql: SqlFunction): Promise<void>;
|
|
|
332
332
|
*
|
|
333
333
|
* Throws if `tableName` is `'audit_log'` (cannot monitor itself).
|
|
334
334
|
*/
|
|
335
|
-
declare function databaseAuditMonitorTable(sql: SqlFunction, tableName: string): Promise<void>;
|
|
335
|
+
declare function databaseAuditMonitorTable(sql: SqlFunction, tableName: string, primaryKeyColumn?: string): Promise<void>;
|
|
336
336
|
/**
|
|
337
337
|
* Returns all rows from the `audit_log` table, ordered by `performed_at` DESC.
|
|
338
338
|
*/
|
package/dist/index.js
CHANGED
|
@@ -423,13 +423,21 @@ function replaceAll(text, searchValue, mask) {
|
|
|
423
423
|
function buildMask(value) {
|
|
424
424
|
return "*".repeat(value.length);
|
|
425
425
|
}
|
|
426
|
+
function buildPlaceholder(key) {
|
|
427
|
+
if (key === "DATABASE_URL" || key.endsWith("_DATABASE_URL")) {
|
|
428
|
+
return "postgresql://replay:replay@localhost:5432/replay";
|
|
429
|
+
}
|
|
430
|
+
return `placeholder_${key.toLowerCase()}`;
|
|
431
|
+
}
|
|
426
432
|
function redactBlobData(blobData) {
|
|
427
433
|
const redactions = /* @__PURE__ */ new Map();
|
|
434
|
+
const placeholders = /* @__PURE__ */ new Map();
|
|
428
435
|
const redactedEnvReads = blobData.capturedData.envReads.map(
|
|
429
436
|
(read) => {
|
|
430
437
|
if (shouldRedact(read.key, read.value) && read.value !== void 0) {
|
|
431
438
|
const mask = buildMask(read.value);
|
|
432
439
|
redactions.set(read.value, mask);
|
|
440
|
+
placeholders.set(read.value, buildPlaceholder(read.key));
|
|
433
441
|
return { ...read, value: mask };
|
|
434
442
|
}
|
|
435
443
|
return { ...read };
|
|
@@ -462,11 +470,29 @@ function redactBlobData(blobData) {
|
|
|
462
470
|
}
|
|
463
471
|
return out;
|
|
464
472
|
}
|
|
473
|
+
const sortedPlaceholders = [...placeholders.entries()].sort(
|
|
474
|
+
(a, b) => b[0].length - a[0].length
|
|
475
|
+
);
|
|
476
|
+
function scrubForRequest(text) {
|
|
477
|
+
if (text === void 0) return void 0;
|
|
478
|
+
let result = text;
|
|
479
|
+
for (const [original, placeholder] of sortedPlaceholders) {
|
|
480
|
+
result = replaceAll(result, original, placeholder);
|
|
481
|
+
}
|
|
482
|
+
return result;
|
|
483
|
+
}
|
|
484
|
+
function scrubHeadersForRequest(headers) {
|
|
485
|
+
const out = {};
|
|
486
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
487
|
+
out[k] = scrubForRequest(v) ?? v;
|
|
488
|
+
}
|
|
489
|
+
return out;
|
|
490
|
+
}
|
|
465
491
|
const redactedRequestInfo = {
|
|
466
492
|
...blobData.requestInfo,
|
|
467
|
-
url:
|
|
468
|
-
headers:
|
|
469
|
-
body:
|
|
493
|
+
url: scrubForRequest(blobData.requestInfo.url) ?? blobData.requestInfo.url,
|
|
494
|
+
headers: scrubHeadersForRequest(blobData.requestInfo.headers),
|
|
495
|
+
body: scrubForRequest(blobData.requestInfo.body)
|
|
470
496
|
};
|
|
471
497
|
const redactedNetworkCalls = blobData.capturedData.networkCalls.map(
|
|
472
498
|
(call) => ({
|
|
@@ -1095,7 +1121,12 @@ async function databaseAuditEnsureLogTable(sql) {
|
|
|
1095
1121
|
changed_cols TEXT[];
|
|
1096
1122
|
req_id TEXT;
|
|
1097
1123
|
call_idx INTEGER;
|
|
1124
|
+
pk_col TEXT;
|
|
1125
|
+
pk_val TEXT;
|
|
1098
1126
|
BEGIN
|
|
1127
|
+
-- Primary key column name passed as trigger argument; default to 'id'
|
|
1128
|
+
pk_col := COALESCE(TG_ARGV[0], 'id');
|
|
1129
|
+
|
|
1099
1130
|
-- Read application context injected by the network interceptor
|
|
1100
1131
|
req_id := COALESCE(current_setting('app.replay_request_id', true), '');
|
|
1101
1132
|
IF req_id = '' THEN req_id := NULL; END IF;
|
|
@@ -1107,21 +1138,24 @@ async function databaseAuditEnsureLogTable(sql) {
|
|
|
1107
1138
|
END;
|
|
1108
1139
|
|
|
1109
1140
|
IF TG_OP = 'INSERT' THEN
|
|
1141
|
+
EXECUTE format('SELECT ($1).%I::TEXT', pk_col) USING NEW INTO pk_val;
|
|
1110
1142
|
INSERT INTO audit_log (table_name, record_id, action, new_data, replay_request_id, replay_request_call_index)
|
|
1111
|
-
VALUES (TG_TABLE_NAME,
|
|
1143
|
+
VALUES (TG_TABLE_NAME, pk_val, 'INSERT', to_jsonb(NEW), req_id, call_idx);
|
|
1112
1144
|
RETURN NEW;
|
|
1113
1145
|
ELSIF TG_OP = 'UPDATE' THEN
|
|
1146
|
+
EXECUTE format('SELECT ($1).%I::TEXT', pk_col) USING NEW INTO pk_val;
|
|
1114
1147
|
SELECT ARRAY_AGG(n.key) INTO changed_cols
|
|
1115
1148
|
FROM jsonb_each_text(to_jsonb(NEW)) n
|
|
1116
1149
|
LEFT JOIN jsonb_each_text(to_jsonb(OLD)) o ON n.key = o.key
|
|
1117
1150
|
WHERE o.value IS DISTINCT FROM n.value;
|
|
1118
1151
|
|
|
1119
1152
|
INSERT INTO audit_log (table_name, record_id, action, old_data, new_data, changed_fields, replay_request_id, replay_request_call_index)
|
|
1120
|
-
VALUES (TG_TABLE_NAME,
|
|
1153
|
+
VALUES (TG_TABLE_NAME, pk_val, 'UPDATE', to_jsonb(OLD), to_jsonb(NEW), COALESCE(changed_cols, ARRAY[]::TEXT[]), req_id, call_idx);
|
|
1121
1154
|
RETURN NEW;
|
|
1122
1155
|
ELSIF TG_OP = 'DELETE' THEN
|
|
1156
|
+
EXECUTE format('SELECT ($1).%I::TEXT', pk_col) USING OLD INTO pk_val;
|
|
1123
1157
|
INSERT INTO audit_log (table_name, record_id, action, old_data, replay_request_id, replay_request_call_index)
|
|
1124
|
-
VALUES (TG_TABLE_NAME,
|
|
1158
|
+
VALUES (TG_TABLE_NAME, pk_val, 'DELETE', to_jsonb(OLD), req_id, call_idx);
|
|
1125
1159
|
RETURN OLD;
|
|
1126
1160
|
END IF;
|
|
1127
1161
|
RETURN NULL;
|
|
@@ -1129,19 +1163,22 @@ async function databaseAuditEnsureLogTable(sql) {
|
|
|
1129
1163
|
$$ LANGUAGE plpgsql
|
|
1130
1164
|
`;
|
|
1131
1165
|
}
|
|
1132
|
-
async function databaseAuditMonitorTable(sql, tableName) {
|
|
1166
|
+
async function databaseAuditMonitorTable(sql, tableName, primaryKeyColumn = "id") {
|
|
1133
1167
|
if (tableName === "audit_log") {
|
|
1134
1168
|
throw new Error("Cannot monitor the audit_log table itself");
|
|
1135
1169
|
}
|
|
1136
1170
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
|
|
1137
1171
|
throw new Error(`Invalid table name: ${tableName}`);
|
|
1138
1172
|
}
|
|
1173
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(primaryKeyColumn)) {
|
|
1174
|
+
throw new Error(`Invalid primary key column name: ${primaryKeyColumn}`);
|
|
1175
|
+
}
|
|
1139
1176
|
const triggerName = `audit_trigger_${tableName}`;
|
|
1140
1177
|
await sql(
|
|
1141
1178
|
`DROP TRIGGER IF EXISTS ${triggerName} ON "${tableName}"`
|
|
1142
1179
|
);
|
|
1143
1180
|
await sql(
|
|
1144
|
-
`CREATE TRIGGER ${triggerName} AFTER INSERT OR UPDATE OR DELETE ON "${tableName}" FOR EACH ROW EXECUTE FUNCTION audit_trigger_function()`
|
|
1181
|
+
`CREATE TRIGGER ${triggerName} AFTER INSERT OR UPDATE OR DELETE ON "${tableName}" FOR EACH ROW EXECUTE FUNCTION audit_trigger_function('${primaryKeyColumn}')`
|
|
1145
1182
|
);
|
|
1146
1183
|
}
|
|
1147
1184
|
async function databaseAuditDumpLogTable(sql) {
|