@reproapp/node-sdk 0.0.46 → 0.0.48

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,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
+ });
@@ -0,0 +1,39 @@
1
+ process.env.REPRO_SDK_BACKGROUND_WORK_TIMEOUT_MS = '20';
2
+ process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '0';
3
+ process.env.REPRO_SDK_BACKGROUND_BUSY_IDLE_DELAY_MS = '0';
4
+
5
+ const assert = require('node:assert/strict');
6
+ const { __reproTestHooks } = require('../dist');
7
+
8
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
9
+
10
+ async function main() {
11
+ const hooks = __reproTestHooks;
12
+ hooks.resetSdkBackgroundQueuesForTest();
13
+ const order = [];
14
+
15
+ hooks.scheduleSdkBackgroundWorkForTest(async () => {
16
+ order.push('first-start');
17
+ await sleep(80);
18
+ order.push('first-end');
19
+ }, { priority: 'high' });
20
+ hooks.scheduleSdkBackgroundWorkForTest(() => {
21
+ order.push('second');
22
+ }, { priority: 'high' });
23
+
24
+ await hooks.drainSdkBackgroundQueueForTest();
25
+
26
+ assert.deepEqual(
27
+ order,
28
+ ['first-start', 'first-end', 'second'],
29
+ 'a slow background capture must finish before the queue advances',
30
+ );
31
+
32
+ hooks.resetSdkBackgroundQueuesForTest();
33
+ console.log('sdk background timeout isolation OK');
34
+ }
35
+
36
+ main().catch((error) => {
37
+ console.error(error);
38
+ process.exit(1);
39
+ });
@@ -0,0 +1,152 @@
1
+ const assert = require('node:assert/strict');
2
+ const http = require('node:http');
3
+
4
+ process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '25';
5
+ process.env.REPRO_SDK_TRACE_INLINE_VALUE_MAX_SERIALIZED_CHARS = '2048';
6
+ process.env.TRACE_IDLE_FLUSH_MS = '10';
7
+ process.env.TRACE_LINGER_AFTER_FINISH_MS = '0';
8
+
9
+ const originalFetch = global.fetch;
10
+ const capturedBodies = [];
11
+
12
+ global.fetch = async (url, init = {}) => {
13
+ assert.match(String(url), /\/v1\/ingest\/events$/);
14
+ capturedBodies.push(JSON.parse(String(init.body || '{}')));
15
+ return {
16
+ ok: true,
17
+ status: 202,
18
+ json: async () => ({ ok: true }),
19
+ text: async () => '{"ok":true}',
20
+ };
21
+ };
22
+
23
+ const { initRepro } = require('../dist');
24
+ const { flushIngestQueue } = require('../dist/ingest/client');
25
+
26
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
27
+
28
+ async function waitFor(predicate, timeoutMs = 4000) {
29
+ const deadline = Date.now() + timeoutMs;
30
+ while (Date.now() < deadline) {
31
+ if (predicate()) return;
32
+ await sleep(20);
33
+ }
34
+ assert(predicate(), 'timed out waiting for semantic trace capture');
35
+ }
36
+
37
+ function eventsOf(type) {
38
+ return capturedBodies
39
+ .flatMap((body) => Array.isArray(body?.events) ? body.events : [])
40
+ .filter((event) => event?.event_type === type);
41
+ }
42
+
43
+ function get(url) {
44
+ return new Promise((resolve, reject) => {
45
+ const request = http.get(url, (response) => {
46
+ let body = '';
47
+ response.setEncoding('utf8');
48
+ response.on('data', (chunk) => { body += chunk; });
49
+ response.on('end', () => resolve({ statusCode: response.statusCode, body }));
50
+ });
51
+ request.on('error', reject);
52
+ });
53
+ }
54
+
55
+ async function main() {
56
+ const config = {
57
+ tenantId: 'TENANT_semantic_trace',
58
+ appId: 'APP_semantic_trace',
59
+ appSecret: 'secret',
60
+ appName: 'semantic-trace',
61
+ serviceName: 'semantic-trace',
62
+ ingestBase: 'http://127.0.0.1:65535',
63
+ tracing: {
64
+ disableFunctionTypes: ['constructor'],
65
+ fullValueCapture: true,
66
+ logFunctionCalls: false,
67
+ },
68
+ };
69
+
70
+ await initRepro(config);
71
+ const { startServer } = require('./fixtures/semantic-serialization-trace-server');
72
+ const server = await startServer(config);
73
+
74
+ try {
75
+ const port = server.address().port;
76
+ const response = await get(
77
+ `http://127.0.0.1:${port}/semantic-trace` +
78
+ `?__repro_sid=S_semantic_trace&__repro_aid=A_semantic_trace&__repro_start=${Date.now()}`,
79
+ );
80
+ assert.equal(response.statusCode, 200);
81
+ assert.deepEqual(JSON.parse(response.body), {
82
+ accepted: true,
83
+ originalGeneratorObserved: true,
84
+ originalShipmentObserved: true,
85
+ runtimeConfigStillReadable: true,
86
+ shipmentNumber: 'SHIP-TRACE-1001',
87
+ lastKit: 'KIT-0159',
88
+ });
89
+
90
+ await waitFor(() => eventsOf('backend_request').length === 1);
91
+ await waitFor(() => eventsOf('trace_batch').some((event) =>
92
+ event.payload?.trace?.some((traceEvent) => traceEvent?.fn === 'recordShipment')
93
+ ));
94
+ await flushIngestQueue();
95
+
96
+ const traceBatches = eventsOf('trace_batch');
97
+ const traceEvents = traceBatches.flatMap((event) => event.payload?.trace || []);
98
+ const enter = traceEvents.find((event) => event?.type === 'enter' && event?.fn === 'recordShipment');
99
+ assert(enter, JSON.stringify(traceEvents));
100
+
101
+ const traceValues = traceBatches.flatMap((event) => event.payload?.traceValues || []);
102
+ const argsValue = traceValues.find((entry) =>
103
+ entry?.id === enter.argsValueCapture?.valueId && entry?.target === 'trace.args'
104
+ );
105
+ const capturedArgs = argsValue?.value || enter.args;
106
+ assert(Array.isArray(capturedArgs), JSON.stringify({ enter, traceValues }));
107
+
108
+ assert.equal(capturedArgs[0].__class, 'ShipmentGenerator');
109
+ assert.equal(capturedArgs[0].triggerAction, 'ShipmentCreated');
110
+ assert.deepEqual(capturedArgs[0].logger, {
111
+ __kind: 'repro-semantic',
112
+ category: 'logger',
113
+ className: 'Logger',
114
+ context: 'ShipmentGenerator',
115
+ });
116
+ assert.deepEqual(capturedArgs[0].apiConfigService.configService, {
117
+ __kind: 'repro-semantic',
118
+ category: 'configuration-service',
119
+ className: 'ConfigService',
120
+ });
121
+
122
+ assert.equal(capturedArgs[1].shipmentNumber, 'SHIP-TRACE-1001');
123
+ assert.equal(capturedArgs[1].site.address.country.regulatory.region, 'FDA');
124
+ assert.equal(capturedArgs[1].kits.length, 160);
125
+ assert.equal(capturedArgs[1].kits[159].kitNumber, 'KIT-0159');
126
+
127
+ const allCapturedJson = JSON.stringify(capturedBodies);
128
+ assert(!allCapturedJson.includes('sendgrid-runtime-secret'));
129
+ assert(!allCapturedJson.includes('runtime-password'));
130
+ assert(!allCapturedJson.includes('config-secret-799'));
131
+ assert(!allCapturedJson.includes('logger-runtime-state'));
132
+
133
+ const argsBytes = Buffer.byteLength(JSON.stringify(capturedArgs));
134
+ assert(argsBytes < 30000, `semantic trace args unexpectedly large: ${argsBytes} bytes`);
135
+
136
+ console.log('semantic serialization real trace pipeline OK');
137
+ } finally {
138
+ await new Promise((resolve, reject) => {
139
+ server.close((error) => error ? reject(error) : resolve());
140
+ });
141
+ }
142
+ }
143
+
144
+ main()
145
+ .catch((error) => {
146
+ console.error(error);
147
+ process.exitCode = 1;
148
+ })
149
+ .finally(async () => {
150
+ await flushIngestQueue().catch(() => {});
151
+ global.fetch = originalFetch;
152
+ });
@@ -0,0 +1,229 @@
1
+ const assert = require('node:assert/strict');
2
+ const { __reproTestHooks } = require('../dist');
3
+
4
+ const {
5
+ projectSemanticInfrastructureValueForTest: project,
6
+ sanitizeTraceValueForPrivacyForTest: sanitize,
7
+ } = __reproTestHooks;
8
+
9
+ class Logger {
10
+ constructor(context) {
11
+ this.context = context;
12
+ this.internalBuffer = Array.from({ length: 100 }, () => 'log-internal-state');
13
+ }
14
+
15
+ log() {}
16
+ }
17
+
18
+ class ConfigService {
19
+ constructor() {
20
+ this.cache = {
21
+ SENDGRID_API_KEY: 'secret-sendgrid-value-that-must-not-be-captured',
22
+ DATABASE_URL: 'mongodb://user:password@database/repro',
23
+ };
24
+ this.internalConfig = {
25
+ infrastructure: Array.from({ length: 500 }, (_, index) => ({
26
+ index,
27
+ secret: `internal-secret-${index}`,
28
+ nested: { socket: { host: 'database.internal', port: 27017 } },
29
+ })),
30
+ };
31
+ }
32
+
33
+ get(key) {
34
+ return this.cache[key];
35
+ }
36
+ }
37
+
38
+ class ApiConfigService {
39
+ constructor(configService) {
40
+ this.configService = configService;
41
+ }
42
+
43
+ getDatabaseConfig() {
44
+ return this.configService.get('DATABASE_URL');
45
+ }
46
+ }
47
+
48
+ class GlobalUserRepository {
49
+ constructor(apiConfigService) {
50
+ this.apiConfigService = apiConfigService;
51
+ }
52
+ }
53
+
54
+ class UserService {
55
+ constructor(globalUserRepository) {
56
+ this.globalUserRepository = globalUserRepository;
57
+ this.logger = new Logger('UserService');
58
+ }
59
+ }
60
+
61
+ class StudyConfigService {
62
+ constructor(userService) {
63
+ this.userService = userService;
64
+ }
65
+ }
66
+
67
+ class BlindingMailerService {
68
+ constructor() {
69
+ this.logger = new Logger('BlindingMailerService');
70
+ this.fromEmail = 'automated@example.test';
71
+ }
72
+ }
73
+
74
+ class SiteStatusChangedGenerator {
75
+ constructor(configService) {
76
+ this.blindingMailerService = new BlindingMailerService();
77
+ this.logger = new Logger('SiteStatusChangedGenerator');
78
+ this.studyConfigService = new StudyConfigService(
79
+ new UserService(new GlobalUserRepository(new ApiConfigService(configService))),
80
+ );
81
+ this.triggerAction = 'SiteStatusChanged';
82
+ }
83
+ }
84
+
85
+ class ClinicalShipmentDto {
86
+ constructor() {
87
+ this.shipmentNumber = 'SHIP-10023';
88
+ this.site = {
89
+ number: 'SITE-0042',
90
+ address: {
91
+ country: {
92
+ code: 'US',
93
+ regulatory: { region: 'FDA', enabled: true },
94
+ },
95
+ },
96
+ };
97
+ this.kits = Array.from({ length: 40 }, (_, index) => ({
98
+ kitNumber: `KIT-${String(index).padStart(4, '0')}`,
99
+ status: index % 2 ? 'released' : 'available',
100
+ temperature: { minC: 2, maxC: 8 },
101
+ }));
102
+ }
103
+ }
104
+
105
+ class PricingService {
106
+ constructor() {
107
+ this.price = 125;
108
+ this.currency = 'USD';
109
+ this.breakdown = { base: 100, tax: 25 };
110
+ }
111
+ }
112
+
113
+ class DeliveryClient {
114
+ send() {}
115
+ connect() {}
116
+ disconnect() {}
117
+ }
118
+
119
+ class RuntimeConfigService {
120
+ constructor() {
121
+ Object.defineProperty(this, 'dangerousGetter', {
122
+ enumerable: true,
123
+ get() {
124
+ throw new Error('semantic projection invoked an application getter');
125
+ },
126
+ });
127
+ }
128
+
129
+ get() {
130
+ return 'value';
131
+ }
132
+ }
133
+
134
+ class RedisClient {
135
+ get() {}
136
+ set() {}
137
+ pipeline() {}
138
+ }
139
+
140
+ class KafkaProducer {
141
+ connect() {}
142
+ disconnect() {}
143
+ send() {}
144
+ }
145
+
146
+ class ChannelModel {
147
+ publish() {}
148
+ sendToQueue() {}
149
+ ack() {}
150
+ }
151
+
152
+ class Pool {
153
+ query() {}
154
+ connect() {}
155
+ end() {}
156
+ }
157
+
158
+ class DataSource {
159
+ createQueryRunner() {}
160
+ }
161
+
162
+ function main() {
163
+ const configService = new ConfigService();
164
+ const generator = new SiteStatusChangedGenerator(configService);
165
+ const businessDto = new ClinicalShipmentDto();
166
+
167
+ const rawJson = JSON.stringify([generator, businessDto]);
168
+ const captured = sanitize([generator, businessDto]);
169
+ const capturedJson = JSON.stringify(captured);
170
+
171
+ assert.equal(captured[0].__class, 'SiteStatusChangedGenerator');
172
+ assert.equal(captured[0].triggerAction, 'SiteStatusChanged');
173
+ assert.equal(captured[0].blindingMailerService.fromEmail, 'automated@example.test');
174
+ assert.deepEqual(captured[0].logger, {
175
+ __kind: 'repro-semantic',
176
+ category: 'logger',
177
+ className: 'Logger',
178
+ context: 'SiteStatusChangedGenerator',
179
+ });
180
+ assert.deepEqual(
181
+ captured[0]
182
+ .studyConfigService
183
+ .userService
184
+ .globalUserRepository
185
+ .apiConfigService
186
+ .configService,
187
+ {
188
+ __kind: 'repro-semantic',
189
+ category: 'configuration-service',
190
+ className: 'ConfigService',
191
+ },
192
+ );
193
+ assert(!capturedJson.includes('secret-sendgrid-value-that-must-not-be-captured'));
194
+ assert(!capturedJson.includes('internal-secret-499'));
195
+
196
+ assert.deepEqual(
197
+ { ...captured[1], __class: undefined },
198
+ { ...JSON.parse(JSON.stringify(businessDto)), __class: undefined },
199
+ 'business DTOs must remain byte-for-byte equivalent after JSON serialization',
200
+ );
201
+
202
+ assert(
203
+ Buffer.byteLength(capturedJson) < Buffer.byteLength(rawJson) * 0.15,
204
+ `semantic capture did not reduce infrastructure graph enough: raw=${Buffer.byteLength(rawJson)} captured=${Buffer.byteLength(capturedJson)}`,
205
+ );
206
+
207
+ assert.equal(project(configService).value.category, 'configuration-service');
208
+ assert.equal(project(new Logger('test')).value.category, 'logger');
209
+ assert.equal(project(new RedisClient()).value.category, 'redis-client');
210
+ assert.equal(project(new KafkaProducer()).value.category, 'kafka-producer');
211
+ assert.equal(project(new ChannelModel()).value.category, 'amqp-channel');
212
+ assert.equal(project(new Pool()).value.category, 'postgres-pool');
213
+ assert.equal(project(new DataSource()).value.category, 'typeorm-infrastructure');
214
+ assert.equal(project(new DeliveryClient()), null, 'generic clients must not be mistaken for Kafka');
215
+
216
+ assert.equal(project(new PricingService()), null, 'data-only application classes must remain complete');
217
+ assert.deepEqual(
218
+ sanitize(new PricingService()),
219
+ { ...JSON.parse(JSON.stringify(new PricingService())), __class: 'PricingService' },
220
+ 'data-only application classes must not be semantically projected',
221
+ );
222
+
223
+ assert.doesNotThrow(() => project(new RuntimeConfigService()));
224
+ assert.equal(project(new RuntimeConfigService()).value.category, 'configuration-service');
225
+
226
+ console.log('semantic serialization adversarial coverage OK');
227
+ }
228
+
229
+ main();