@reproapp/node-sdk 0.0.46 → 0.0.47

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.
@@ -0,0 +1,234 @@
1
+ process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '0';
2
+ process.env.REPRO_SDK_BACKGROUND_BUSY_IDLE_DELAY_MS = '0';
3
+ process.env.REPRO_SDK_BACKGROUND_MAX_DEFER_MS = '0';
4
+ process.env.TRACE_IDLE_FLUSH_MS = '10';
5
+ process.env.TRACE_LINGER_AFTER_FINISH_MS = '0';
6
+
7
+ const assert = require('node:assert/strict');
8
+ const http = require('node:http');
9
+ const express = require('express');
10
+
11
+ const {
12
+ __reproTestHooks,
13
+ initReproTracing,
14
+ reproMiddleware,
15
+ } = require('../dist');
16
+ const {
17
+ normalizeRuntimePrivacyPolicy,
18
+ } = require('../dist/privacy');
19
+ const { flushIngestQueue } = require('../dist/ingest/client');
20
+
21
+ function flattenEvents(posts) {
22
+ return posts.flatMap((body) => Array.isArray(body?.events) ? body.events : []);
23
+ }
24
+
25
+ async function waitForCapturedEvent(capturedBodies, sessionId, eventType, timeoutMs = 2000) {
26
+ const deadline = Date.now() + timeoutMs;
27
+ while (Date.now() < deadline) {
28
+ const found = flattenEvents(capturedBodies).some(
29
+ (event) => event.session_id === sessionId && event.event_type === eventType,
30
+ );
31
+ if (found) return;
32
+ await new Promise((resolve) => setTimeout(resolve, 10));
33
+ }
34
+ assert.fail(`timed out waiting for ${eventType} in ${sessionId}`);
35
+ }
36
+
37
+ function requestJson(port, path) {
38
+ return new Promise((resolve, reject) => {
39
+ const request = http.get({
40
+ host: '127.0.0.1',
41
+ port,
42
+ path,
43
+ headers: {
44
+ 'x-bug-session-id': `S_attestation_${path.slice(1)}`,
45
+ 'x-bug-action-id': `A_attestation_${path.slice(1)}`,
46
+ 'x-bug-request-start': String(Date.now()),
47
+ },
48
+ }, (response) => {
49
+ const chunks = [];
50
+ response.on('data', (chunk) => chunks.push(chunk));
51
+ response.on('end', () => {
52
+ try {
53
+ resolve({
54
+ status: response.statusCode,
55
+ body: JSON.parse(Buffer.concat(chunks).toString('utf8')),
56
+ });
57
+ } catch (error) {
58
+ reject(error);
59
+ }
60
+ });
61
+ });
62
+ request.on('error', reject);
63
+ });
64
+ }
65
+
66
+ async function drainCapture() {
67
+ await __reproTestHooks.drainSdkBackgroundQueueForTest();
68
+ await flushIngestQueue();
69
+ }
70
+
71
+ function policyWithProtectedSecret() {
72
+ return normalizeRuntimePrivacyPolicy({
73
+ environment: 'dev',
74
+ statements: [{
75
+ sid: 'drop-internal-secret',
76
+ when: {
77
+ scope: 'FIELD',
78
+ selector: 'field:internalSecret',
79
+ target: 'response.body.internalSecret|trace.args.internalSecret',
80
+ },
81
+ apply: { action: 'DROP' },
82
+ meta: { priority: 1 },
83
+ }],
84
+ });
85
+ }
86
+
87
+ async function main() {
88
+ const capturedBodies = [];
89
+ const originalFetch = global.fetch;
90
+ global.fetch = async (_url, init = {}) => {
91
+ capturedBodies.push(JSON.parse(String(init.body || '{}')));
92
+ return {
93
+ ok: true,
94
+ status: 202,
95
+ json: async () => ({ accepted: 1 }),
96
+ text: async () => '{"accepted":1}',
97
+ };
98
+ };
99
+
100
+ let server;
101
+ try {
102
+ initReproTracing({ instrument: false, logFunctionCalls: false });
103
+
104
+ const failingCfg = {
105
+ tenantId: 'TENANT_test',
106
+ appId: 'APP_policy_failure',
107
+ appSecret: 'secret',
108
+ serviceName: 'policy-attestation',
109
+ privacy: { environment: 'dev' },
110
+ ingestBase: 'http://127.0.0.1:65535',
111
+ };
112
+ const failingPolicy = policyWithProtectedSecret();
113
+ assert(failingPolicy);
114
+ // Fault injection after normalization exercises the real SDK failure path
115
+ // without adding a production-only bypass to the policy engine.
116
+ failingPolicy.statements[0].selectorLeaves = null;
117
+ __reproTestHooks.setRuntimePrivacyPolicyForTest(failingCfg, failingPolicy);
118
+
119
+ const rawCfg = {
120
+ ...failingCfg,
121
+ appId: 'APP_policy_raw',
122
+ privacy: {
123
+ environment: 'dev',
124
+ captureRaw: true,
125
+ },
126
+ };
127
+ __reproTestHooks.setRuntimePrivacyPolicyForTest(rawCfg, failingPolicy);
128
+
129
+ const app = express();
130
+ app.use('/failed', reproMiddleware(failingCfg));
131
+ app.get('/failed', (_request, response) => {
132
+ const value = global.__repro_call(
133
+ (input) => input,
134
+ null,
135
+ [{ internalSecret: 'super-secret-policy-value' }],
136
+ __filename,
137
+ 1,
138
+ 'PolicyService.capture',
139
+ true,
140
+ );
141
+ response.json({
142
+ ok: true,
143
+ internalSecret: value.internalSecret,
144
+ });
145
+ });
146
+ app.use('/raw', reproMiddleware(rawCfg));
147
+ app.get('/raw', (_request, response) => {
148
+ response.json({
149
+ ok: true,
150
+ internalSecret: 'raw-mode-secret',
151
+ });
152
+ });
153
+
154
+ server = await new Promise((resolve) => {
155
+ const listening = app.listen(0, '127.0.0.1', () => resolve(listening));
156
+ });
157
+ const port = server.address().port;
158
+
159
+ const failedResponse = await requestJson(port, '/failed');
160
+ assert.equal(failedResponse.status, 200);
161
+ assert.equal(failedResponse.body.internalSecret, 'super-secret-policy-value');
162
+ await waitForCapturedEvent(
163
+ capturedBodies,
164
+ 'S_attestation_failed',
165
+ 'request_capture_manifest',
166
+ );
167
+ await drainCapture();
168
+
169
+ const failedEvents = flattenEvents(capturedBodies)
170
+ .filter((event) => event.session_id === 'S_attestation_failed');
171
+ const failedRequest = failedEvents.find((event) => event.event_type === 'backend_request');
172
+ const failedManifest = failedEvents.find((event) => event.event_type === 'request_capture_manifest');
173
+ const failedTrace = failedEvents.find((event) => event.event_type === 'trace_batch');
174
+ assert(failedRequest, 'policy failure must retain backend request metadata');
175
+ assert(failedManifest, 'policy failure must emit a terminal request manifest');
176
+ assert(failedTrace, 'policy failure must retain a metadata-only trace');
177
+ assert.equal(
178
+ failedManifest.payload.requestCaptureManifest.expected.backendRequest,
179
+ 1,
180
+ 'observed request count must not become zero after policy failure',
181
+ );
182
+ assert.equal(failedManifest.payload.requestCaptureManifest.privacy.mode, 'policy');
183
+ assert.equal(failedManifest.payload.requestCaptureManifest.privacy.status, 'failed');
184
+ assert.match(
185
+ failedManifest.payload.requestCaptureManifest.privacy.policyFingerprint,
186
+ /^[a-f0-9]{64}$/,
187
+ );
188
+ assert(
189
+ failedManifest.payload.requestCaptureManifest.captureFailures.length > 0,
190
+ 'policy failure must be declared for hydration',
191
+ );
192
+ assert.equal(
193
+ JSON.stringify(failedEvents).includes('super-secret-policy-value'),
194
+ false,
195
+ 'failed policy materialization must fail closed without raw-value leakage',
196
+ );
197
+
198
+ capturedBodies.length = 0;
199
+ const rawResponse = await requestJson(port, '/raw');
200
+ assert.equal(rawResponse.status, 200);
201
+ await waitForCapturedEvent(
202
+ capturedBodies,
203
+ 'S_attestation_raw',
204
+ 'request_capture_manifest',
205
+ );
206
+ await drainCapture();
207
+
208
+ const rawEvents = flattenEvents(capturedBodies)
209
+ .filter((event) => event.session_id === 'S_attestation_raw');
210
+ const rawManifest = rawEvents.find((event) => event.event_type === 'request_capture_manifest');
211
+ assert(rawManifest);
212
+ assert.equal(rawManifest.payload.requestCaptureManifest.privacy.mode, 'raw');
213
+ assert.equal(rawManifest.payload.requestCaptureManifest.privacy.status, 'bypassed');
214
+ assert.equal(rawManifest.payload.requestCaptureManifest.captureFailures, undefined);
215
+ assert(
216
+ JSON.stringify(rawEvents).includes('raw-mode-secret'),
217
+ 'captureRaw must continue to retain raw values explicitly',
218
+ );
219
+
220
+ console.log('policy capture attestation OK');
221
+ } finally {
222
+ await __reproTestHooks.drainSdkBackgroundQueueForTest().catch(() => {});
223
+ await flushIngestQueue().catch(() => {});
224
+ global.fetch = originalFetch;
225
+ if (server) {
226
+ await new Promise((resolve) => server.close(resolve));
227
+ }
228
+ }
229
+ }
230
+
231
+ main().catch((error) => {
232
+ console.error(error);
233
+ process.exit(1);
234
+ });
@@ -0,0 +1,271 @@
1
+ process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '0';
2
+ process.env.REPRO_SDK_BACKGROUND_BUSY_IDLE_DELAY_MS = '0';
3
+ process.env.REPRO_SDK_BACKGROUND_MAX_DEFER_MS = '0';
4
+ process.env.TRACE_IDLE_FLUSH_MS = '10';
5
+ process.env.TRACE_LINGER_AFTER_FINISH_MS = '0';
6
+
7
+ const assert = require('node:assert/strict');
8
+ const http = require('node:http');
9
+ const express = require('express');
10
+ const mongoose = require('mongoose');
11
+ const { GenericContainer, Wait } = require('testcontainers');
12
+
13
+ const {
14
+ __reproTestHooks,
15
+ initReproTracing,
16
+ reproMiddleware,
17
+ reproMongoosePlugin,
18
+ } = require('../dist');
19
+ const { flushIngestQueue } = require('../dist/ingest/client');
20
+
21
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
22
+
23
+ function flattenEvents(posts) {
24
+ return posts.flatMap((body) => Array.isArray(body?.events) ? body.events : []);
25
+ }
26
+
27
+ function eventsOf(posts, eventType) {
28
+ return flattenEvents(posts).filter((event) => event?.event_type === eventType);
29
+ }
30
+
31
+ function largePolicy() {
32
+ return {
33
+ environment: 'dev',
34
+ statements: [
35
+ ...Array.from({ length: 5000 }, (_, index) => ({
36
+ sid: `function-${index}`,
37
+ when: {
38
+ scope: 'FUNCTION',
39
+ selector: `GeneratedService${index}.method${index}`,
40
+ target: 'trace.args|trace.returnValue',
41
+ },
42
+ apply: { action: 'KEEP_EXACT' },
43
+ meta: { priority: index + 10 },
44
+ })),
45
+ {
46
+ sid: 'drop-internal-secret',
47
+ when: {
48
+ scope: 'FIELD',
49
+ selector: 'field:internalSecret',
50
+ target: 'response.body.data[].internalSecret|db.resultMeta[].internalSecret',
51
+ },
52
+ apply: { action: 'DROP' },
53
+ meta: { priority: 1 },
54
+ },
55
+ ],
56
+ };
57
+ }
58
+
59
+ async function startMongo() {
60
+ const container = await new GenericContainer('mongo:7')
61
+ .withExposedPorts(27017)
62
+ .withTmpFs({ '/data/db': 'rw,noexec,nosuid,size=1g' })
63
+ .withCommand(['mongod', '--bind_ip_all', '--setParameter', 'diagnosticDataCollectionEnabled=false'])
64
+ .withWaitStrategy(Wait.forListeningPorts())
65
+ .start();
66
+ return {
67
+ container,
68
+ uri: `mongodb://${container.getHost()}:${container.getMappedPort(27017)}/repro_policy_completion`,
69
+ };
70
+ }
71
+
72
+ function requestNames(port, index) {
73
+ return new Promise((resolve, reject) => {
74
+ const request = http.get({
75
+ host: '127.0.0.1',
76
+ port,
77
+ path: '/names',
78
+ headers: {
79
+ 'x-bug-session-id': 'S_policy_completion',
80
+ 'x-bug-action-id': `A_policy_completion_${index}`,
81
+ 'x-bug-request-start': String(Date.now()),
82
+ },
83
+ }, (response) => {
84
+ const chunks = [];
85
+ response.on('data', (chunk) => chunks.push(chunk));
86
+ response.on('end', () => {
87
+ try {
88
+ assert.equal(response.statusCode, 200);
89
+ const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'));
90
+ assert.equal(parsed.data.length, 8);
91
+ resolve();
92
+ } catch (error) {
93
+ reject(error);
94
+ }
95
+ });
96
+ });
97
+ request.on('error', reject);
98
+ });
99
+ }
100
+
101
+ async function waitForCompleteCapture(capturedBodies, expectedRequests, timeoutMs = 30000) {
102
+ const deadline = Date.now() + timeoutMs;
103
+ while (Date.now() < deadline) {
104
+ await __reproTestHooks.drainSdkBackgroundQueueForTest();
105
+ await flushIngestQueue();
106
+ const backendRequests = eventsOf(capturedBodies, 'backend_request');
107
+ const manifests = eventsOf(capturedBodies, 'request_capture_manifest');
108
+ const traceBatches = eventsOf(capturedBodies, 'trace_batch');
109
+ if (
110
+ backendRequests.length === expectedRequests &&
111
+ manifests.length === expectedRequests &&
112
+ traceBatches.length >= expectedRequests
113
+ ) {
114
+ return;
115
+ }
116
+ await sleep(25);
117
+ }
118
+ assert.fail(JSON.stringify({
119
+ backendRequests: eventsOf(capturedBodies, 'backend_request').length,
120
+ manifests: eventsOf(capturedBodies, 'request_capture_manifest').length,
121
+ traceBatches: eventsOf(capturedBodies, 'trace_batch').length,
122
+ dbChanges: eventsOf(capturedBodies, 'db_change').length,
123
+ }));
124
+ }
125
+
126
+ async function main() {
127
+ const capturedBodies = [];
128
+ const originalFetch = global.fetch;
129
+ global.fetch = async (_url, init = {}) => {
130
+ capturedBodies.push(JSON.parse(String(init.body || '{}')));
131
+ return {
132
+ ok: true,
133
+ status: 202,
134
+ json: async () => ({ accepted: 1 }),
135
+ text: async () => '{"accepted":1}',
136
+ };
137
+ };
138
+
139
+ let mongo;
140
+ let connection;
141
+ let server;
142
+ try {
143
+ mongo = await startMongo();
144
+ connection = await mongoose.createConnection(mongo.uri).asPromise();
145
+ initReproTracing({ instrument: false, logFunctionCalls: false });
146
+
147
+ const cfg = {
148
+ tenantId: 'TENANT_test',
149
+ appId: 'APP_test',
150
+ appSecret: 'secret',
151
+ serviceName: 'policy-session-completion',
152
+ captureHeaders: false,
153
+ privacy: {
154
+ environment: 'dev',
155
+ policy: largePolicy(),
156
+ },
157
+ ingestBase: 'http://127.0.0.1:65535',
158
+ };
159
+
160
+ const depotSchema = new mongoose.Schema({
161
+ location: String,
162
+ description: String,
163
+ internalSecret: String,
164
+ });
165
+ reproMongoosePlugin(cfg)(depotSchema);
166
+ const Depot = connection.model('Depot', depotSchema, 'depots');
167
+ await Depot.insertMany(Array.from({ length: 8 }, (_, index) => ({
168
+ location: `Depot ${index}`,
169
+ description: `Large depot description ${index} `.repeat(500),
170
+ internalSecret: `secret-${index}`,
171
+ })));
172
+
173
+ async function loadNames() {
174
+ return Depot.find({}).lean();
175
+ }
176
+
177
+ const app = express();
178
+ app.use(reproMiddleware(cfg));
179
+ app.get('/names', async (_request, response, next) => {
180
+ try {
181
+ const data = await global.__repro_call(
182
+ loadNames,
183
+ null,
184
+ [],
185
+ __filename,
186
+ 1,
187
+ 'loadNames',
188
+ true,
189
+ );
190
+ response.json({ data, message: 'depot names', statusCode: 200 });
191
+ } catch (error) {
192
+ next(error);
193
+ }
194
+ });
195
+
196
+ server = await new Promise((resolve) => {
197
+ const listening = app.listen(0, '127.0.0.1', () => resolve(listening));
198
+ });
199
+ const port = server.address().port;
200
+
201
+ const startedAt = Date.now();
202
+ await Promise.all(Array.from({ length: 14 }, (_, index) => requestNames(port, index)));
203
+ await waitForCompleteCapture(capturedBodies, 14);
204
+ const elapsedMs = Date.now() - startedAt;
205
+
206
+ const backendRequests = eventsOf(capturedBodies, 'backend_request');
207
+ const manifests = eventsOf(capturedBodies, 'request_capture_manifest');
208
+ const dbChanges = eventsOf(capturedBodies, 'db_change');
209
+ assert.equal(backendRequests.length, 14);
210
+ assert.equal(manifests.length, 14);
211
+ assert(dbChanges.length >= 14, `expected Mongo evidence for every request, got ${dbChanges.length}`);
212
+ assert(
213
+ manifests.every((event) => event.payload?.requestCaptureManifest?.expected?.backendRequest === 1),
214
+ 'every request manifest must account for its backend request',
215
+ );
216
+ assert(
217
+ manifests.every((event) => event.payload?.requestCaptureManifest?.expected?.traceBatch >= 1),
218
+ 'every request manifest must account for its trace batches',
219
+ );
220
+ assert(
221
+ manifests.every((event) => event.payload?.requestCaptureManifest?.expected?.dbChange >= 1),
222
+ 'every request manifest must declare each observed Mongo operation before materialization',
223
+ );
224
+ assert(
225
+ manifests.every((event) => event.payload?.requestCaptureManifest?.privacy?.status === 'applied'),
226
+ 'every request manifest must attest successful policy application',
227
+ );
228
+ assert(
229
+ manifests.every((event) =>
230
+ /^[a-f0-9]{64}$/.test(
231
+ event.payload?.requestCaptureManifest?.privacy?.policyFingerprint ?? '',
232
+ ),
233
+ ),
234
+ 'every policy attestation must identify the exact normalized policy',
235
+ );
236
+ assert(
237
+ manifests.every((event) => !event.payload?.requestCaptureManifest?.captureFailures),
238
+ 'successful policy materialization must not report capture failures',
239
+ );
240
+
241
+ const responseValues = backendRequests.flatMap((event) => event.payload?.requestValues || [])
242
+ .filter((entry) => entry.target === 'response.body');
243
+ assert.equal(responseValues.length, 14);
244
+ assert.equal(responseValues[0].value.data.length, 8);
245
+ assert.equal(responseValues[0].value.data[0].internalSecret, '[dropped]');
246
+ assert(
247
+ elapsedMs < 30000,
248
+ `large-policy session completion exceeded 30 seconds: ${elapsedMs}ms`,
249
+ );
250
+
251
+ console.log(`policy session completion testcontainers OK (${elapsedMs}ms)`);
252
+ } finally {
253
+ await __reproTestHooks.drainSdkBackgroundQueueForTest().catch(() => {});
254
+ await flushIngestQueue().catch(() => {});
255
+ global.fetch = originalFetch;
256
+ if (server) {
257
+ await new Promise((resolve) => server.close(resolve));
258
+ }
259
+ if (connection) {
260
+ await connection.close().catch(() => {});
261
+ }
262
+ if (mongo?.container) {
263
+ await mongo.container.stop().catch(() => {});
264
+ }
265
+ }
266
+ }
267
+
268
+ main().catch((error) => {
269
+ console.error(error);
270
+ process.exit(1);
271
+ });
@@ -0,0 +1,194 @@
1
+ const assert = require('node:assert/strict');
2
+ const {
3
+ applyRuntimePrivacyPolicyAsync,
4
+ normalizeRuntimePrivacyPolicy,
5
+ } = require('../dist/privacy');
6
+
7
+ function functionStatement(index) {
8
+ return {
9
+ sid: `function-${index}`,
10
+ when: {
11
+ scope: 'FUNCTION',
12
+ selector: `Service${index}.method${index}`,
13
+ target: 'trace.args.payload',
14
+ },
15
+ apply: { action: 'KEEP_EXACT' },
16
+ meta: { priority: index + 10 },
17
+ };
18
+ }
19
+
20
+ async function main() {
21
+ const uniqueFunctionStatements = Array.from(
22
+ { length: 3000 },
23
+ (_, index) => functionStatement(index),
24
+ );
25
+ const duplicateFunctionStatements = Array.from(
26
+ { length: 2000 },
27
+ (_, index) => ({
28
+ ...functionStatement(index % 100),
29
+ sid: `duplicate-${index}`,
30
+ meta: { priority: 10_000 + index },
31
+ }),
32
+ );
33
+ const matchingFunctionStatement = {
34
+ sid: 'matching-function',
35
+ when: {
36
+ scope: 'FUNCTION',
37
+ selector: 'CapturedService.loadNames',
38
+ target: 'trace.args.payload.privateNote',
39
+ },
40
+ apply: { action: 'DROP' },
41
+ meta: { priority: 1 },
42
+ };
43
+ const responseFieldStatement = {
44
+ sid: 'drop-private-note',
45
+ when: {
46
+ scope: 'FIELD',
47
+ selector: 'field:privateNote',
48
+ target: 'response.body.data[].privateNote',
49
+ },
50
+ apply: { action: 'DROP' },
51
+ meta: { priority: 2 },
52
+ };
53
+ const exactTargetFieldStatement = {
54
+ sid: 'drop-targeted-secret',
55
+ when: {
56
+ scope: 'FIELD',
57
+ selector: 'field:generatedAlias',
58
+ target: 'response.body.data[].targetedSecret',
59
+ },
60
+ apply: { action: 'DROP' },
61
+ meta: { priority: 3 },
62
+ };
63
+ const positionalTargetFieldStatement = {
64
+ sid: 'tokenize-positional-owner',
65
+ when: {
66
+ scope: 'FIELD',
67
+ selector: 'field:incidentOwnerName',
68
+ target: 'trace.args[4]',
69
+ },
70
+ apply: { action: 'TOKENIZE' },
71
+ meta: { priority: 3 },
72
+ };
73
+ const globalPatternStatement = {
74
+ sid: 'drop-secret-text',
75
+ when: {
76
+ scope: 'PATTERN',
77
+ selector: 'secret-text',
78
+ pattern: 'secret-value',
79
+ target: 'response.body.data[].notes',
80
+ },
81
+ apply: { action: 'DROP' },
82
+ meta: { priority: 4 },
83
+ };
84
+
85
+ const policy = normalizeRuntimePrivacyPolicy({
86
+ environment: 'dev',
87
+ statements: [
88
+ ...uniqueFunctionStatements,
89
+ ...duplicateFunctionStatements,
90
+ matchingFunctionStatement,
91
+ responseFieldStatement,
92
+ exactTargetFieldStatement,
93
+ positionalTargetFieldStatement,
94
+ globalPatternStatement,
95
+ ],
96
+ });
97
+
98
+ assert(policy);
99
+ assert.equal(
100
+ policy.statements.length,
101
+ uniqueFunctionStatements.length + 5,
102
+ 'semantically duplicate generated statements must be removed',
103
+ );
104
+ assert.match(policy.fingerprint, /^[a-f0-9]{64}$/);
105
+ const normalizedPatternStatement = policy.statements.find(
106
+ (statement) => statement.sid === globalPatternStatement.sid,
107
+ );
108
+ assert(normalizedPatternStatement);
109
+ normalizedPatternStatement.pattern = /secret-value/gi;
110
+
111
+ const response = {
112
+ data: Array.from({ length: 1000 }, (_, index) => ({
113
+ id: `depot-${index}`,
114
+ location: `Location ${index}`,
115
+ country: `Country ${index % 20}`,
116
+ status: 'active',
117
+ privateNote: `private-${index}`,
118
+ targetedSecret: `targeted-${index}`,
119
+ notes: `secret-value-${index}`,
120
+ })),
121
+ message: 'depot names',
122
+ statusCode: 200,
123
+ };
124
+
125
+ const startedAt = Date.now();
126
+ const transformedResponse = await applyRuntimePrivacyPolicyAsync(
127
+ policy,
128
+ 'response.body',
129
+ response,
130
+ {
131
+ appId: 'APP_test',
132
+ appSecret: 'secret',
133
+ routeKey: 'GET /api/v1/depots/:id/names',
134
+ },
135
+ );
136
+ const elapsedMs = Date.now() - startedAt;
137
+
138
+ assert.equal(transformedResponse.data.length, 1000);
139
+ assert.equal(transformedResponse.data[0].id, 'depot-0');
140
+ assert.equal(transformedResponse.data[999].privateNote, '[dropped]');
141
+ assert.equal(transformedResponse.data[0].targetedSecret, '[dropped]');
142
+ assert.equal(transformedResponse.data[999].targetedSecret, '[dropped]');
143
+ assert(
144
+ transformedResponse.data.every((entry) => entry.notes === '[dropped]'),
145
+ 'global/sticky regex state must not make identical pattern matches alternate',
146
+ );
147
+ assert(
148
+ elapsedMs < 1500,
149
+ `large response policy evaluation took ${elapsedMs}ms; function rules were likely scanned per node`,
150
+ );
151
+
152
+ const transformedTrace = await applyRuntimePrivacyPolicyAsync(
153
+ policy,
154
+ 'trace.args',
155
+ {
156
+ payload: {
157
+ safeValue: 'visible',
158
+ privateNote: 'must-not-be-visible',
159
+ },
160
+ },
161
+ {
162
+ appId: 'APP_test',
163
+ appSecret: 'secret',
164
+ trace: {
165
+ fn: 'CapturedService.loadNames',
166
+ file: '/app/src/captured.service.ts',
167
+ },
168
+ },
169
+ );
170
+ assert.equal(transformedTrace.payload.safeValue, 'visible');
171
+ assert.equal(transformedTrace.payload.privateNote, '[dropped]');
172
+
173
+ const transformedPositionalTrace = await applyRuntimePrivacyPolicyAsync(
174
+ policy,
175
+ 'trace.args',
176
+ ['safe', 'safe', 'safe', 'safe', 'Jane Security'],
177
+ {
178
+ appId: 'APP_test',
179
+ appSecret: 'secret',
180
+ trace: {
181
+ fn: 'CapturedService.loadNames',
182
+ file: '/app/src/captured.service.ts',
183
+ },
184
+ },
185
+ );
186
+ assert.match(transformedPositionalTrace[4], /^4_tok_[a-f0-9]{12}$/);
187
+
188
+ console.log(`privacy policy index OK (${elapsedMs}ms)`);
189
+ }
190
+
191
+ main().catch((error) => {
192
+ console.error(error);
193
+ process.exit(1);
194
+ });