@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.
- package/dist/index.d.ts +10 -2
- package/dist/index.js +235 -123
- package/dist/ingest/client.js +11 -0
- package/dist/privacy.d.ts +1 -0
- package/dist/privacy.js +306 -7
- package/package.json +3 -3
- package/src/index.ts +355 -108
- package/src/ingest/client.ts +13 -0
- package/src/privacy.ts +409 -7
- package/test/ingest-high-priority-fairness.test.js +76 -0
- package/test/policy-capture-attestation.test.js +234 -0
- package/test/policy-session-completion.testcontainers.test.js +271 -0
- package/test/privacy-policy-index.test.js +194 -0
- package/test/sdk-background-timeout-isolation.test.js +39 -0
- package/test/trace-batch-size.test.js +4 -0
package/src/ingest/client.ts
CHANGED
|
@@ -140,6 +140,9 @@ const selectSendBatch = (
|
|
|
140
140
|
if (first.priority === 'normal') {
|
|
141
141
|
return { batch: first, queueItems: 1 };
|
|
142
142
|
}
|
|
143
|
+
if (first.attempts > 0) {
|
|
144
|
+
return { batch: first, queueItems: 1 };
|
|
145
|
+
}
|
|
143
146
|
|
|
144
147
|
const entries = first.entries.slice();
|
|
145
148
|
let bytes = serializedBytes(entries);
|
|
@@ -376,6 +379,16 @@ const drainIngestQueue = async (): Promise<void> => {
|
|
|
376
379
|
backoffMs = 0;
|
|
377
380
|
continue;
|
|
378
381
|
}
|
|
382
|
+
if (
|
|
383
|
+
batch.priority === 'high' &&
|
|
384
|
+
selected.queueItems === 1 &&
|
|
385
|
+
ingestQueue.length > 1
|
|
386
|
+
) {
|
|
387
|
+
const [failedItem] = ingestQueue.splice(0, 1);
|
|
388
|
+
if (failedItem) {
|
|
389
|
+
ingestQueue.push(failedItem);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
379
392
|
|
|
380
393
|
backoffMs = Math.min(
|
|
381
394
|
DEFAULT_QUEUE_BACKOFF_MAX_MS,
|
package/src/privacy.ts
CHANGED
|
@@ -79,6 +79,25 @@ type NormalizedStatement = {
|
|
|
79
79
|
descendantRawTextMode?: RuntimePrivacyRawTextMode;
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
+
type FunctionStatementTrieNode = {
|
|
83
|
+
children: Map<string, FunctionStatementTrieNode>;
|
|
84
|
+
terminal: NormalizedStatement[];
|
|
85
|
+
subtree: NormalizedStatement[];
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
type RuntimePrivacySurfaceIndex = {
|
|
89
|
+
contextualStatements: NormalizedStatement[];
|
|
90
|
+
fieldStatementsBySuffix: Map<string, NormalizedStatement[]>;
|
|
91
|
+
fieldWildcardTargetStatements: NormalizedStatement[];
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
type RuntimePrivacyStatementIndex = {
|
|
95
|
+
bySurface: Map<RuntimePrivacySurface, RuntimePrivacySurfaceIndex>;
|
|
96
|
+
functionStatements: FunctionStatementTrieNode;
|
|
97
|
+
candidateCache: Map<string, NormalizedStatement[]>;
|
|
98
|
+
hasNumericTargetSegments: boolean;
|
|
99
|
+
};
|
|
100
|
+
|
|
82
101
|
type TransformFallback = {
|
|
83
102
|
action: RuntimePrivacyAction;
|
|
84
103
|
rawTextMode?: RuntimePrivacyRawTextMode;
|
|
@@ -120,13 +139,17 @@ export type NormalizedRuntimePrivacyPolicy = {
|
|
|
120
139
|
strength?: 'weak' | 'medium' | 'strict';
|
|
121
140
|
statements: NormalizedStatement[];
|
|
122
141
|
rawTextHints: NormalizedRawTextHint[];
|
|
142
|
+
fingerprint: string;
|
|
123
143
|
};
|
|
124
144
|
|
|
125
145
|
const EMPTY_RUNTIME_PRIVACY_POLICY: NormalizedRuntimePrivacyPolicy = {
|
|
126
146
|
statements: [],
|
|
127
147
|
rawTextHints: [],
|
|
148
|
+
fingerprint: 'empty',
|
|
128
149
|
};
|
|
129
150
|
|
|
151
|
+
const runtimePrivacyStatementIndexes = new WeakMap<object, RuntimePrivacyStatementIndex>();
|
|
152
|
+
const RUNTIME_PRIVACY_CANDIDATE_CACHE_MAX_ENTRIES = 2048;
|
|
130
153
|
const RUNTIME_PRIVACY_PROVENANCE_MAX_ENTRIES = 1000;
|
|
131
154
|
const RUNTIME_PRIVACY_PROVENANCE_MAX_SERIALIZED_LENGTH = 200_000;
|
|
132
155
|
let objectRuntimePrivacyProvenance = new WeakMap<object, RuntimePrivacyProvenance>();
|
|
@@ -303,16 +326,16 @@ export function normalizeRuntimePrivacyPolicy(raw: unknown): NormalizedRuntimePr
|
|
|
303
326
|
const policy = raw as Record<string, any>;
|
|
304
327
|
const flattened = flattenStatements(policy);
|
|
305
328
|
const rawTextHints = normalizeRawTextHints(policy.rawTextHints);
|
|
306
|
-
const statements
|
|
329
|
+
const statements = deduplicateNormalizedStatements(flattened
|
|
307
330
|
.map((entry, index) => normalizeStatement(entry, index))
|
|
308
331
|
.filter((entry): entry is NormalizedStatement => entry !== null)
|
|
309
|
-
.sort((left, right) => left.priority - right.priority);
|
|
332
|
+
.sort((left, right) => left.priority - right.priority));
|
|
310
333
|
|
|
311
334
|
if (!statements.length) {
|
|
312
335
|
return null;
|
|
313
336
|
}
|
|
314
337
|
|
|
315
|
-
|
|
338
|
+
const normalizedPolicy: NormalizedRuntimePrivacyPolicy = {
|
|
316
339
|
environment: typeof policy.environment === 'string' ? policy.environment : undefined,
|
|
317
340
|
strength:
|
|
318
341
|
policy.strength === 'weak' || policy.strength === 'medium' || policy.strength === 'strict'
|
|
@@ -320,7 +343,15 @@ export function normalizeRuntimePrivacyPolicy(raw: unknown): NormalizedRuntimePr
|
|
|
320
343
|
: undefined,
|
|
321
344
|
statements,
|
|
322
345
|
rawTextHints,
|
|
346
|
+
fingerprint: runtimePrivacyPolicyFingerprint(
|
|
347
|
+
typeof policy.environment === 'string' ? policy.environment : undefined,
|
|
348
|
+
typeof policy.strength === 'string' ? policy.strength : undefined,
|
|
349
|
+
statements,
|
|
350
|
+
rawTextHints,
|
|
351
|
+
),
|
|
323
352
|
};
|
|
353
|
+
runtimePrivacyStatementIndexes.set(normalizedPolicy, buildRuntimePrivacyStatementIndex(statements));
|
|
354
|
+
return normalizedPolicy;
|
|
324
355
|
}
|
|
325
356
|
|
|
326
357
|
export function applyRuntimePrivacyPolicy(
|
|
@@ -425,6 +456,365 @@ function normalizeStatement(input: any, index: number): NormalizedStatement | nu
|
|
|
425
456
|
};
|
|
426
457
|
}
|
|
427
458
|
|
|
459
|
+
function deduplicateNormalizedStatements(statements: NormalizedStatement[]): NormalizedStatement[] {
|
|
460
|
+
const seen = new Set<string>();
|
|
461
|
+
const deduplicated: NormalizedStatement[] = [];
|
|
462
|
+
for (const statement of statements) {
|
|
463
|
+
const key = normalizedStatementSemanticKey(statement);
|
|
464
|
+
if (seen.has(key)) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
seen.add(key);
|
|
468
|
+
deduplicated.push(statement);
|
|
469
|
+
}
|
|
470
|
+
return deduplicated;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function normalizedStatementSemanticKey(statement: NormalizedStatement): string {
|
|
474
|
+
return JSON.stringify({
|
|
475
|
+
scope: statement.scope,
|
|
476
|
+
selectorTokens: statement.selectorTokens,
|
|
477
|
+
selectorLeaves: statement.selectorLeaves,
|
|
478
|
+
action: statement.action,
|
|
479
|
+
pattern: statement.pattern
|
|
480
|
+
? {
|
|
481
|
+
source: statement.pattern.source,
|
|
482
|
+
flags: statement.pattern.flags,
|
|
483
|
+
}
|
|
484
|
+
: null,
|
|
485
|
+
targetMatchers: statement.targetMatchers.map((matcher) => ({
|
|
486
|
+
surfaces: matcher.surfaces,
|
|
487
|
+
path: matcher.path,
|
|
488
|
+
})),
|
|
489
|
+
textPolicy: statement.textPolicy ?? null,
|
|
490
|
+
rawTextMode: statement.rawTextMode ?? null,
|
|
491
|
+
rawTextHintNames: statement.rawTextHintNames ?? null,
|
|
492
|
+
descendantRawTextMode: statement.descendantRawTextMode ?? null,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function runtimePrivacyPolicyFingerprint(
|
|
497
|
+
environment: string | undefined,
|
|
498
|
+
strength: string | undefined,
|
|
499
|
+
statements: NormalizedStatement[],
|
|
500
|
+
rawTextHints: NormalizedRawTextHint[],
|
|
501
|
+
): string {
|
|
502
|
+
const serialized = JSON.stringify({
|
|
503
|
+
environment: environment ?? null,
|
|
504
|
+
strength: strength ?? null,
|
|
505
|
+
statements: statements.map(normalizedStatementSemanticKey),
|
|
506
|
+
rawTextHints: rawTextHints.map((hint) => ({
|
|
507
|
+
name: hint.name,
|
|
508
|
+
action: hint.action,
|
|
509
|
+
pattern: hint.pattern.source,
|
|
510
|
+
flags: hint.pattern.flags,
|
|
511
|
+
tokenLabel: hint.tokenLabel,
|
|
512
|
+
})),
|
|
513
|
+
});
|
|
514
|
+
return createHash('sha256').update(serialized).digest('hex');
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function createFunctionStatementTrieNode(): FunctionStatementTrieNode {
|
|
518
|
+
return {
|
|
519
|
+
children: new Map<string, FunctionStatementTrieNode>(),
|
|
520
|
+
terminal: [],
|
|
521
|
+
subtree: [],
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function appendUniqueStatement(target: NormalizedStatement[], statement: NormalizedStatement): void {
|
|
526
|
+
if (!target.includes(statement)) {
|
|
527
|
+
target.push(statement);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function functionSelectorSegments(value: string): string[] {
|
|
532
|
+
return normalizeFunctionSelector(value)
|
|
533
|
+
.split('.')
|
|
534
|
+
.map((segment) => segment.trim())
|
|
535
|
+
.filter(Boolean)
|
|
536
|
+
.reverse();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function addFunctionStatementToTrie(
|
|
540
|
+
root: FunctionStatementTrieNode,
|
|
541
|
+
selector: string,
|
|
542
|
+
statement: NormalizedStatement,
|
|
543
|
+
): void {
|
|
544
|
+
const segments = functionSelectorSegments(selector);
|
|
545
|
+
if (!segments.length) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
let node = root;
|
|
550
|
+
for (const segment of segments) {
|
|
551
|
+
let child = node.children.get(segment);
|
|
552
|
+
if (!child) {
|
|
553
|
+
child = createFunctionStatementTrieNode();
|
|
554
|
+
node.children.set(segment, child);
|
|
555
|
+
}
|
|
556
|
+
node = child;
|
|
557
|
+
appendUniqueStatement(node.subtree, statement);
|
|
558
|
+
}
|
|
559
|
+
appendUniqueStatement(node.terminal, statement);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function createRuntimePrivacySurfaceIndex(): RuntimePrivacySurfaceIndex {
|
|
563
|
+
return {
|
|
564
|
+
contextualStatements: [],
|
|
565
|
+
fieldStatementsBySuffix: new Map<string, NormalizedStatement[]>(),
|
|
566
|
+
fieldWildcardTargetStatements: [],
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function appendStatementToMap(
|
|
571
|
+
target: Map<string, NormalizedStatement[]>,
|
|
572
|
+
key: string,
|
|
573
|
+
statement: NormalizedStatement,
|
|
574
|
+
): void {
|
|
575
|
+
if (!key) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const current = target.get(key);
|
|
579
|
+
if (current) {
|
|
580
|
+
appendUniqueStatement(current, statement);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
target.set(key, [statement]);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function explicitTargetSurfaces(statement: NormalizedStatement): Set<RuntimePrivacySurface> | null {
|
|
587
|
+
if (!statement.targetMatchers.length) {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
return new Set(statement.targetMatchers.flatMap((matcher) => matcher.surfaces));
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function scopeCanMatchSurface(
|
|
594
|
+
statement: NormalizedStatement,
|
|
595
|
+
surface: RuntimePrivacySurface,
|
|
596
|
+
): boolean {
|
|
597
|
+
if (statement.scope === 'FUNCTION') {
|
|
598
|
+
return surface.startsWith('trace.');
|
|
599
|
+
}
|
|
600
|
+
if (statement.scope === 'ROUTE') {
|
|
601
|
+
return surface.startsWith('request.') || surface === 'response.body';
|
|
602
|
+
}
|
|
603
|
+
if (statement.scope === 'SCHEMA' || statement.scope === 'QUERY') {
|
|
604
|
+
return surface.startsWith('db.');
|
|
605
|
+
}
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function addFieldStatementToSurfaceIndex(
|
|
610
|
+
index: RuntimePrivacySurfaceIndex,
|
|
611
|
+
statement: NormalizedStatement,
|
|
612
|
+
): void {
|
|
613
|
+
for (const selector of statement.selectorLeaves) {
|
|
614
|
+
appendStatementToMap(
|
|
615
|
+
index.fieldStatementsBySuffix,
|
|
616
|
+
normalizeComparableToken(selector),
|
|
617
|
+
statement,
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
for (const matcher of statement.targetMatchers) {
|
|
621
|
+
const terminal = matcher.path?.[matcher.path.length - 1];
|
|
622
|
+
if (!terminal) {
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
if (terminal === '*') {
|
|
626
|
+
appendUniqueStatement(index.fieldWildcardTargetStatements, statement);
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
appendStatementToMap(
|
|
630
|
+
index.fieldStatementsBySuffix,
|
|
631
|
+
normalizeComparableToken(terminal),
|
|
632
|
+
statement,
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function buildRuntimePrivacyStatementIndex(
|
|
638
|
+
statements: NormalizedStatement[],
|
|
639
|
+
): RuntimePrivacyStatementIndex {
|
|
640
|
+
const bySurface = new Map<RuntimePrivacySurface, RuntimePrivacySurfaceIndex>(
|
|
641
|
+
ALL_SURFACES.map((surface) => [surface, createRuntimePrivacySurfaceIndex()]),
|
|
642
|
+
);
|
|
643
|
+
const functionStatements = createFunctionStatementTrieNode();
|
|
644
|
+
let hasNumericTargetSegments = false;
|
|
645
|
+
|
|
646
|
+
for (const statement of statements) {
|
|
647
|
+
for (const matcher of statement.targetMatchers) {
|
|
648
|
+
if (matcher.path?.some((segment) => /^\d+$/.test(segment))) {
|
|
649
|
+
hasNumericTargetSegments = true;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (statement.scope === 'FUNCTION') {
|
|
654
|
+
for (const selector of statement.selectorTokens) {
|
|
655
|
+
addFunctionStatementToTrie(functionStatements, selector, statement);
|
|
656
|
+
}
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const targetSurfaces = explicitTargetSurfaces(statement);
|
|
661
|
+
for (const surface of ALL_SURFACES) {
|
|
662
|
+
if (!scopeCanMatchSurface(statement, surface)) {
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
// FIELD protections deliberately have cross-surface fallback semantics.
|
|
666
|
+
if (
|
|
667
|
+
statement.scope !== 'FIELD' &&
|
|
668
|
+
targetSurfaces &&
|
|
669
|
+
!targetSurfaces.has(surface)
|
|
670
|
+
) {
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
const surfaceIndex = bySurface.get(surface)!;
|
|
674
|
+
if (statement.scope === 'FIELD') {
|
|
675
|
+
addFieldStatementToSurfaceIndex(surfaceIndex, statement);
|
|
676
|
+
} else {
|
|
677
|
+
appendUniqueStatement(surfaceIndex.contextualStatements, statement);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return {
|
|
683
|
+
bySurface,
|
|
684
|
+
functionStatements,
|
|
685
|
+
candidateCache: new Map<string, NormalizedStatement[]>(),
|
|
686
|
+
hasNumericTargetSegments,
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function getRuntimePrivacyStatementIndex(
|
|
691
|
+
policy: NormalizedRuntimePrivacyPolicy,
|
|
692
|
+
): RuntimePrivacyStatementIndex {
|
|
693
|
+
const existing = runtimePrivacyStatementIndexes.get(policy);
|
|
694
|
+
if (existing) {
|
|
695
|
+
return existing;
|
|
696
|
+
}
|
|
697
|
+
const created = buildRuntimePrivacyStatementIndex(policy.statements);
|
|
698
|
+
runtimePrivacyStatementIndexes.set(policy, created);
|
|
699
|
+
return created;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function matchingFunctionStatements(
|
|
703
|
+
root: FunctionStatementTrieNode,
|
|
704
|
+
functionName: string,
|
|
705
|
+
): NormalizedStatement[] {
|
|
706
|
+
const segments = functionSelectorSegments(functionName);
|
|
707
|
+
if (!segments.length) {
|
|
708
|
+
return [];
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const matches = new Set<NormalizedStatement>();
|
|
712
|
+
let node: FunctionStatementTrieNode | undefined = root;
|
|
713
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
714
|
+
node = node.children.get(segments[index]);
|
|
715
|
+
if (!node) {
|
|
716
|
+
break;
|
|
717
|
+
}
|
|
718
|
+
for (const statement of node.terminal) {
|
|
719
|
+
matches.add(statement);
|
|
720
|
+
}
|
|
721
|
+
if (index === segments.length - 1) {
|
|
722
|
+
for (const statement of node.subtree) {
|
|
723
|
+
matches.add(statement);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return Array.from(matches);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function normalizedPathSuffixes(path: string[], preserveNumericSegments: boolean): string[] {
|
|
731
|
+
const joined = path
|
|
732
|
+
.filter((segment) =>
|
|
733
|
+
segment !== '*' &&
|
|
734
|
+
(preserveNumericSegments || !/^\d+$/.test(segment)),
|
|
735
|
+
)
|
|
736
|
+
.map(normalizeComparableToken)
|
|
737
|
+
.filter(Boolean)
|
|
738
|
+
.join('.');
|
|
739
|
+
if (!joined) {
|
|
740
|
+
return [];
|
|
741
|
+
}
|
|
742
|
+
const segments = joined.split('.').filter(Boolean);
|
|
743
|
+
const suffixes: string[] = [];
|
|
744
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
745
|
+
suffixes.push(segments.slice(index).join('.'));
|
|
746
|
+
}
|
|
747
|
+
return suffixes;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function runtimePrivacyCandidateCacheKey(
|
|
751
|
+
index: RuntimePrivacyStatementIndex,
|
|
752
|
+
surface: RuntimePrivacySurface,
|
|
753
|
+
context: RuntimePrivacyApplyContext,
|
|
754
|
+
path: string[],
|
|
755
|
+
): string {
|
|
756
|
+
const pathKey = path
|
|
757
|
+
.map((segment) =>
|
|
758
|
+
!index.hasNumericTargetSegments && /^\d+$/.test(segment)
|
|
759
|
+
? '*'
|
|
760
|
+
: segment,
|
|
761
|
+
)
|
|
762
|
+
.join('.');
|
|
763
|
+
return [
|
|
764
|
+
surface,
|
|
765
|
+
normalizeFunctionSelector(context.trace?.fn?.trim() ?? ''),
|
|
766
|
+
context.routeKey?.trim() ?? '',
|
|
767
|
+
String(context.db?.type ?? ''),
|
|
768
|
+
String(context.db?.collection ?? ''),
|
|
769
|
+
String(context.db?.op ?? ''),
|
|
770
|
+
pathKey,
|
|
771
|
+
].join('\u0000');
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function candidateStatementsForMatch(
|
|
775
|
+
policy: NormalizedRuntimePrivacyPolicy,
|
|
776
|
+
surface: RuntimePrivacySurface,
|
|
777
|
+
context: RuntimePrivacyApplyContext,
|
|
778
|
+
path: string[],
|
|
779
|
+
): NormalizedStatement[] {
|
|
780
|
+
const index = getRuntimePrivacyStatementIndex(policy);
|
|
781
|
+
const cacheKey = runtimePrivacyCandidateCacheKey(index, surface, context, path);
|
|
782
|
+
const cached = index.candidateCache.get(cacheKey);
|
|
783
|
+
if (cached) {
|
|
784
|
+
return cached;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const surfaceIndex = index.bySurface.get(surface);
|
|
788
|
+
const candidateSet = new Set<NormalizedStatement>(
|
|
789
|
+
surfaceIndex?.contextualStatements ?? [],
|
|
790
|
+
);
|
|
791
|
+
if (surfaceIndex && path.length) {
|
|
792
|
+
for (const suffix of normalizedPathSuffixes(path, index.hasNumericTargetSegments)) {
|
|
793
|
+
for (const statement of surfaceIndex.fieldStatementsBySuffix.get(suffix) ?? []) {
|
|
794
|
+
candidateSet.add(statement);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
for (const statement of surfaceIndex.fieldWildcardTargetStatements) {
|
|
798
|
+
candidateSet.add(statement);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (surface.startsWith('trace.')) {
|
|
802
|
+
const functionName = context.trace?.fn?.trim() ?? '';
|
|
803
|
+
if (functionName) {
|
|
804
|
+
for (const statement of matchingFunctionStatements(index.functionStatements, functionName)) {
|
|
805
|
+
candidateSet.add(statement);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
const candidates = Array.from(candidateSet)
|
|
810
|
+
.sort((left, right) => left.priority - right.priority);
|
|
811
|
+
if (index.candidateCache.size >= RUNTIME_PRIVACY_CANDIDATE_CACHE_MAX_ENTRIES) {
|
|
812
|
+
index.candidateCache.clear();
|
|
813
|
+
}
|
|
814
|
+
index.candidateCache.set(cacheKey, candidates);
|
|
815
|
+
return candidates;
|
|
816
|
+
}
|
|
817
|
+
|
|
428
818
|
function resolveEffectiveAction(
|
|
429
819
|
baseAction: RuntimePrivacyAction | null | undefined,
|
|
430
820
|
value: any,
|
|
@@ -1299,7 +1689,7 @@ function matchStatement(
|
|
|
1299
1689
|
path: string[],
|
|
1300
1690
|
): NormalizedStatement | null {
|
|
1301
1691
|
let candidates: Array<{ statement: NormalizedStatement; targetScore: number; score: number }> = [];
|
|
1302
|
-
for (const statement of policy
|
|
1692
|
+
for (const statement of candidateStatementsForMatch(policy, surface, context, path)) {
|
|
1303
1693
|
if (shouldIgnoreBroadContainerRuleOnDescendant(statement, surface, path)) {
|
|
1304
1694
|
continue;
|
|
1305
1695
|
}
|
|
@@ -1705,12 +2095,24 @@ function querySelectorMatches(
|
|
|
1705
2095
|
|
|
1706
2096
|
function patternMatches(pattern: RegExp | undefined, leaf: string, joinedPath: string, value: any): boolean {
|
|
1707
2097
|
if (!pattern) return false;
|
|
1708
|
-
if (leaf && pattern
|
|
1709
|
-
if (joinedPath && pattern
|
|
1710
|
-
if (typeof value === 'string' && pattern
|
|
2098
|
+
if (leaf && testRuntimePrivacyPattern(pattern, leaf)) return true;
|
|
2099
|
+
if (joinedPath && testRuntimePrivacyPattern(pattern, joinedPath)) return true;
|
|
2100
|
+
if (typeof value === 'string' && testRuntimePrivacyPattern(pattern, value)) return true;
|
|
1711
2101
|
return false;
|
|
1712
2102
|
}
|
|
1713
2103
|
|
|
2104
|
+
function testRuntimePrivacyPattern(pattern: RegExp, value: string): boolean {
|
|
2105
|
+
// Generated policies may contain global/sticky expressions. RegExp.test mutates
|
|
2106
|
+
// lastIndex for those flags, which otherwise makes identical captures alternate
|
|
2107
|
+
// between matched and unmatched.
|
|
2108
|
+
pattern.lastIndex = 0;
|
|
2109
|
+
try {
|
|
2110
|
+
return pattern.test(value);
|
|
2111
|
+
} finally {
|
|
2112
|
+
pattern.lastIndex = 0;
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
|
|
1714
2116
|
function functionSelectorMatches(selector: string, fn: string): boolean {
|
|
1715
2117
|
const normalizedSelector = normalizeFunctionSelector(selector);
|
|
1716
2118
|
const normalizedFn = normalizeFunctionSelector(fn);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
|
|
3
|
+
const originalFetch = global.fetch;
|
|
4
|
+
const attempts = [];
|
|
5
|
+
|
|
6
|
+
global.fetch = async (_url, init = {}) => {
|
|
7
|
+
const body = JSON.parse(String(init.body || '{}'));
|
|
8
|
+
const requestIds = (body.events || []).map((event) => event?.payload?.requestRid);
|
|
9
|
+
attempts.push(requestIds);
|
|
10
|
+
const hasPoisonedItem = requestIds.includes('R_poisoned');
|
|
11
|
+
return {
|
|
12
|
+
ok: !hasPoisonedItem,
|
|
13
|
+
status: hasPoisonedItem ? 503 : 202,
|
|
14
|
+
json: async () => ({ ok: !hasPoisonedItem }),
|
|
15
|
+
text: async () => JSON.stringify({ ok: !hasPoisonedItem }),
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const { enqueueIngestEntries, flushIngestQueue } = require('../dist/ingest/client');
|
|
20
|
+
|
|
21
|
+
function manifest(requestRid) {
|
|
22
|
+
return {
|
|
23
|
+
actionId: 'A_fairness',
|
|
24
|
+
requestRid,
|
|
25
|
+
requestCaptureManifest: {
|
|
26
|
+
scopeId: `req:${requestRid}`,
|
|
27
|
+
requestRid,
|
|
28
|
+
transport: 'http',
|
|
29
|
+
expected: {
|
|
30
|
+
backendRequest: 1,
|
|
31
|
+
traceBatch: 0,
|
|
32
|
+
dbChange: 0,
|
|
33
|
+
emailEvent: 0,
|
|
34
|
+
},
|
|
35
|
+
closedAt: Date.now(),
|
|
36
|
+
},
|
|
37
|
+
t: Date.now(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function main() {
|
|
42
|
+
const cfg = {
|
|
43
|
+
tenantId: 'TENANT_test',
|
|
44
|
+
appId: 'APP_test',
|
|
45
|
+
appSecret: 'secret',
|
|
46
|
+
serviceName: 'ingest-fairness-test',
|
|
47
|
+
ingestBase: 'http://127.0.0.1:65535',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
enqueueIngestEntries(cfg, 'S_fairness', [manifest('R_poisoned')], { priority: 'high' });
|
|
51
|
+
enqueueIngestEntries(cfg, 'S_fairness', [manifest('R_valid')], { priority: 'high' });
|
|
52
|
+
|
|
53
|
+
await flushIngestQueue();
|
|
54
|
+
await flushIngestQueue();
|
|
55
|
+
await flushIngestQueue();
|
|
56
|
+
|
|
57
|
+
assert(
|
|
58
|
+
attempts.some((requestIds) => requestIds.length === 1 && requestIds[0] === 'R_valid'),
|
|
59
|
+
`valid completion evidence remained blocked: ${JSON.stringify(attempts)}`,
|
|
60
|
+
);
|
|
61
|
+
assert(
|
|
62
|
+
attempts.filter((requestIds) => requestIds.includes('R_poisoned')).length >= 2,
|
|
63
|
+
`failed high-priority evidence was dropped instead of retained: ${JSON.stringify(attempts)}`,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
console.log('ingest high priority fairness OK');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
main()
|
|
70
|
+
.catch((error) => {
|
|
71
|
+
console.error(error);
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
})
|
|
74
|
+
.finally(() => {
|
|
75
|
+
global.fetch = originalFetch;
|
|
76
|
+
});
|