@usageflow/core 0.4.1 → 0.4.3
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/base.d.ts +34 -1
- package/dist/base.js +301 -7
- package/dist/base.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/base.ts +325 -7
- package/src/types.ts +8 -0
- package/test/base.test.ts +741 -24
- package/test/src/types.d.ts +3 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/base.ts
CHANGED
|
@@ -318,7 +318,7 @@ export abstract class UsageFlowAPI {
|
|
|
318
318
|
public guessLedgerId(
|
|
319
319
|
request: UsageFlowRequest,
|
|
320
320
|
overrideUrl?: string,
|
|
321
|
-
): { ledgerId: string, hasLimit: boolean } {
|
|
321
|
+
): { ledgerId: string, hasLimit: boolean, responseTrackingField?: string } {
|
|
322
322
|
const method = request.method;
|
|
323
323
|
const url = overrideUrl || this.getRoutePattern(request);
|
|
324
324
|
const configs = this.apiConfigs;
|
|
@@ -331,23 +331,25 @@ export abstract class UsageFlowAPI {
|
|
|
331
331
|
const fieldName = config.identityFieldName!;
|
|
332
332
|
const location = config.identityFieldLocation;
|
|
333
333
|
const hasLimit = config.hasRateLimit || false;
|
|
334
|
+
const isResponseTrackingEnabled = config.isResponseTrackingEnabled;
|
|
335
|
+
const responseTrackingField = isResponseTrackingEnabled ? config.responseTrackingField : undefined;
|
|
334
336
|
|
|
335
337
|
if (method === config.method && url === config.url) {
|
|
336
338
|
|
|
337
339
|
switch (location) {
|
|
338
340
|
case "path_params":
|
|
339
341
|
if (request.params?.[fieldName]) {
|
|
340
|
-
return { ledgerId: `${method} ${url} ${request.params[fieldName]}`, hasLimit };
|
|
342
|
+
return { ledgerId: `${method} ${url} ${request.params[fieldName]}`, hasLimit, responseTrackingField };
|
|
341
343
|
}
|
|
342
344
|
break;
|
|
343
345
|
case "query_params":
|
|
344
346
|
if (request.query?.[fieldName]) {
|
|
345
|
-
return { ledgerId: `${method} ${url} ${request.query[fieldName]}`, hasLimit };
|
|
347
|
+
return { ledgerId: `${method} ${url} ${request.query[fieldName]}`, hasLimit, responseTrackingField };
|
|
346
348
|
}
|
|
347
349
|
break;
|
|
348
350
|
case "body":
|
|
349
351
|
if (request.body?.[fieldName]) {
|
|
350
|
-
return { ledgerId: `${method} ${url} ${request.body[fieldName]}`, hasLimit };
|
|
352
|
+
return { ledgerId: `${method} ${url} ${request.body[fieldName]}`, hasLimit, responseTrackingField };
|
|
351
353
|
}
|
|
352
354
|
break;
|
|
353
355
|
case "bearer_token":
|
|
@@ -359,7 +361,7 @@ export abstract class UsageFlowAPI {
|
|
|
359
361
|
if (token) {
|
|
360
362
|
const claims = this.decodeJwtUnverified(token);
|
|
361
363
|
if (claims?.[fieldName]) {
|
|
362
|
-
return { ledgerId: `${method} ${url} ${claims[fieldName]}`, hasLimit };
|
|
364
|
+
return { ledgerId: `${method} ${url} ${claims[fieldName]}`, hasLimit, responseTrackingField };
|
|
363
365
|
}
|
|
364
366
|
}
|
|
365
367
|
break;
|
|
@@ -367,7 +369,38 @@ export abstract class UsageFlowAPI {
|
|
|
367
369
|
case "headers": {
|
|
368
370
|
const headerValue = this.getHeaderValue(request.headers, fieldName);
|
|
369
371
|
if (headerValue) {
|
|
370
|
-
return { ledgerId: `${method} ${url} ${headerValue}`, hasLimit };
|
|
372
|
+
return { ledgerId: `${method} ${url} ${headerValue}`, hasLimit, responseTrackingField };
|
|
373
|
+
}
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
case "cookie": {
|
|
378
|
+
// Handle JWT cookie format: '[technique=jwt]cookieName[pick=claim]'
|
|
379
|
+
const jwtCookieMatch = this.parseJwtCookieField(fieldName);
|
|
380
|
+
if (jwtCookieMatch) {
|
|
381
|
+
const { cookieName, claim } = jwtCookieMatch;
|
|
382
|
+
const cookieValue = this.getCookieValue(request.headers, cookieName);
|
|
383
|
+
if (cookieValue) {
|
|
384
|
+
const claims = this.decodeJwtUnverified(cookieValue);
|
|
385
|
+
if (claims?.[claim]) {
|
|
386
|
+
return { ledgerId: `${method} ${url} ${claims[claim]}`, hasLimit, responseTrackingField };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Handle standard cookie access (e.g., "cookie.session" or "session")
|
|
393
|
+
let cookieValue: string | null = null;
|
|
394
|
+
if (fieldName.toLowerCase().startsWith('cookie.')) {
|
|
395
|
+
const cookieName = fieldName.substring(7); // Remove "cookie." prefix
|
|
396
|
+
cookieValue = this.getCookieValue(request.headers, cookieName);
|
|
397
|
+
} else {
|
|
398
|
+
// Use dot notation for regular headers
|
|
399
|
+
cookieValue = this.getByDotNotation(request.headers, fieldName);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (cookieValue) {
|
|
403
|
+
return { ledgerId: `${method} ${url} ${cookieValue}`, hasLimit, responseTrackingField };
|
|
371
404
|
}
|
|
372
405
|
break;
|
|
373
406
|
}
|
|
@@ -375,7 +408,7 @@ export abstract class UsageFlowAPI {
|
|
|
375
408
|
}
|
|
376
409
|
}
|
|
377
410
|
|
|
378
|
-
return { ledgerId: `${method} ${url}`, hasLimit: false };
|
|
411
|
+
return { ledgerId: `${method} ${url}`, hasLimit: false, responseTrackingField: undefined };
|
|
379
412
|
}
|
|
380
413
|
|
|
381
414
|
private async fetchApiPolicies(): Promise<void> {
|
|
@@ -445,6 +478,7 @@ export abstract class UsageFlowAPI {
|
|
|
445
478
|
payload: RequestForAllocation,
|
|
446
479
|
metadata: RequestMetadata,
|
|
447
480
|
hasLimit: boolean,
|
|
481
|
+
responseTrackingField?: string,
|
|
448
482
|
): Promise<void> {
|
|
449
483
|
if (this.socketManager && this.socketManager.isConnected()) {
|
|
450
484
|
try {
|
|
@@ -477,6 +511,7 @@ export abstract class UsageFlowAPI {
|
|
|
477
511
|
});
|
|
478
512
|
request.usageflow!.eventId = payload.allocationId;
|
|
479
513
|
request.usageflow!.metadata = metadata;
|
|
514
|
+
request.usageflow!.responseTrackingField = responseTrackingField;
|
|
480
515
|
}
|
|
481
516
|
} catch (error: any) {
|
|
482
517
|
console.error(
|
|
@@ -648,4 +683,287 @@ export abstract class UsageFlowAPI {
|
|
|
648
683
|
public getBlockedEndpoints(): Map<string, boolean> {
|
|
649
684
|
return this.blockedEndpoints;
|
|
650
685
|
}
|
|
686
|
+
|
|
687
|
+
private getByDotNotation(object: any, path: string): any {
|
|
688
|
+
return path.split('.').reduce((acc, part) => acc?.[part], object);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Extract a value from an object using a dot-notation path
|
|
693
|
+
* Supports nested paths like "data.amount" or "result.items[0].count"
|
|
694
|
+
* Supports wildcard array notation like "items[*].id" to iterate over arrays
|
|
695
|
+
* @param obj - The object to extract the value from
|
|
696
|
+
* @param path - The dot-notation path to the value (e.g., "data.amount" or "items[*].id")
|
|
697
|
+
* @returns The value at the path, or undefined if not found. For wildcard arrays, returns an array of values.
|
|
698
|
+
*/
|
|
699
|
+
public getValueByPath(obj: any, path: string): any {
|
|
700
|
+
if (!obj || !path) {
|
|
701
|
+
return undefined;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Parse path into segments, preserving [*] as a special marker
|
|
705
|
+
const segments: Array<string | '*'> = [];
|
|
706
|
+
let currentSegment = '';
|
|
707
|
+
let i = 0;
|
|
708
|
+
|
|
709
|
+
while (i < path.length) {
|
|
710
|
+
if (path[i] === '[' && i + 1 < path.length && path[i + 1] === '*') {
|
|
711
|
+
// Found [*]
|
|
712
|
+
if (currentSegment) {
|
|
713
|
+
segments.push(currentSegment);
|
|
714
|
+
currentSegment = '';
|
|
715
|
+
}
|
|
716
|
+
segments.push('*');
|
|
717
|
+
i += 3; // Skip '[', '*', ']'
|
|
718
|
+
} else if (path[i] === '[') {
|
|
719
|
+
// Found indexed array access like [0]
|
|
720
|
+
if (currentSegment) {
|
|
721
|
+
segments.push(currentSegment);
|
|
722
|
+
currentSegment = '';
|
|
723
|
+
}
|
|
724
|
+
// Extract the index
|
|
725
|
+
let index = '';
|
|
726
|
+
i++; // Skip '['
|
|
727
|
+
while (i < path.length && path[i] !== ']') {
|
|
728
|
+
index += path[i];
|
|
729
|
+
i++;
|
|
730
|
+
}
|
|
731
|
+
if (index) {
|
|
732
|
+
segments.push(index);
|
|
733
|
+
}
|
|
734
|
+
i++; // Skip ']'
|
|
735
|
+
} else if (path[i] === '.') {
|
|
736
|
+
if (currentSegment) {
|
|
737
|
+
segments.push(currentSegment);
|
|
738
|
+
currentSegment = '';
|
|
739
|
+
}
|
|
740
|
+
i++;
|
|
741
|
+
} else {
|
|
742
|
+
currentSegment += path[i];
|
|
743
|
+
i++;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (currentSegment) {
|
|
748
|
+
segments.push(currentSegment);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Recursive helper function to process segments
|
|
752
|
+
const processSegments = (currentObj: any, segs: Array<string | '*'>, segIndex: number): any => {
|
|
753
|
+
if (currentObj === null || currentObj === undefined) {
|
|
754
|
+
return undefined;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// If we've processed all segments, return the current value
|
|
758
|
+
if (segIndex >= segs.length) {
|
|
759
|
+
return currentObj;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const segment = segs[segIndex];
|
|
763
|
+
|
|
764
|
+
// Handle wildcard array iteration
|
|
765
|
+
if (segment === '*') {
|
|
766
|
+
if (!Array.isArray(currentObj)) {
|
|
767
|
+
return undefined;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// If this is the last segment, return the first element of the array
|
|
771
|
+
if (segIndex === segs.length - 1) {
|
|
772
|
+
return currentObj.length > 0 ? currentObj[0] : undefined;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Otherwise, iterate over the array and return the first valid result
|
|
776
|
+
for (const item of currentObj) {
|
|
777
|
+
const result = processSegments(item, segs, segIndex + 1);
|
|
778
|
+
if (result !== undefined) {
|
|
779
|
+
return result;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return undefined;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Handle regular property access
|
|
787
|
+
const nextObj = currentObj[segment];
|
|
788
|
+
return processSegments(nextObj, segs, segIndex + 1);
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
return processSegments(obj, segments, 0);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Parse and extract a specific cookie value from the Cookie header
|
|
796
|
+
* @param headers The request headers object
|
|
797
|
+
* @param cookieName The name of the cookie to extract (e.g., "session")
|
|
798
|
+
* @returns The cookie value or null if not found
|
|
799
|
+
*/
|
|
800
|
+
private getCookieValue(
|
|
801
|
+
headers: Record<string, string | string[] | undefined> | Headers,
|
|
802
|
+
cookieName: string,
|
|
803
|
+
): string | null {
|
|
804
|
+
let cookieHeader = this.getHeaderValue(headers, 'cookie');
|
|
805
|
+
if (!cookieHeader) {
|
|
806
|
+
cookieHeader = this.getHeaderValue(headers, 'Cookie');
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (!cookieHeader) {
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Parse cookies from the Cookie header string
|
|
814
|
+
// Format: "name1=value1; name2=value2; name3=value3"
|
|
815
|
+
const cookies = cookieHeader.split(';').map(cookie => {
|
|
816
|
+
const [name, ...valueParts] = cookie.trim().split('=');
|
|
817
|
+
return {
|
|
818
|
+
name: name.trim(),
|
|
819
|
+
value: valueParts.join('=').trim(), // Handle values that might contain '='
|
|
820
|
+
};
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
// Find the cookie with the matching name (case-insensitive)
|
|
824
|
+
const cookie = cookies.find(c => c.name.toLowerCase() === cookieName.toLowerCase());
|
|
825
|
+
return cookie ? cookie.value : null;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Parse JWT cookie field format: '[technique=jwt]cookieName[pick=claim]'
|
|
830
|
+
* @param fieldName The field name to parse
|
|
831
|
+
* @returns Object with cookieName and claim, or null if not a JWT cookie format
|
|
832
|
+
*/
|
|
833
|
+
private parseJwtCookieField(
|
|
834
|
+
fieldName: string,
|
|
835
|
+
): { cookieName: string; claim: string } | null {
|
|
836
|
+
const techniqueMatch = fieldName.match(/^\[technique=([^\]]+)\]/);
|
|
837
|
+
if (!techniqueMatch || techniqueMatch[1] !== 'jwt') {
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const pickMatch = fieldName.match(/\[pick=([^\]]+)\]/);
|
|
842
|
+
if (!pickMatch) {
|
|
843
|
+
return null;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Extract cookie name: everything between [technique=jwt] and [pick=...]
|
|
847
|
+
const techniqueEnd = techniqueMatch[0].length;
|
|
848
|
+
const pickStart = fieldName.indexOf('[pick=');
|
|
849
|
+
if (pickStart === -1 || pickStart <= techniqueEnd) {
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const cookieName = fieldName.substring(techniqueEnd, pickStart);
|
|
854
|
+
if (!cookieName) {
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
return {
|
|
859
|
+
cookieName,
|
|
860
|
+
claim: pickMatch[1],
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
public getResponseBody(response: Response): any {
|
|
865
|
+
if (response.body) {
|
|
866
|
+
return response.body;
|
|
867
|
+
}
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Extract schema from a JavaScript object/array
|
|
873
|
+
* Recursively processes objects, arrays, and primitives to build a schema structure
|
|
874
|
+
* @param obj - The object to extract schema from
|
|
875
|
+
* @param path - The current path in the object hierarchy (for nested objects)
|
|
876
|
+
* @returns Schema object with type information and paths
|
|
877
|
+
*/
|
|
878
|
+
public extractSchema(obj: any, path: string = ''): any {
|
|
879
|
+
// Handle null
|
|
880
|
+
if (obj === null || obj === undefined) {
|
|
881
|
+
return 'null';
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Handle arrays
|
|
885
|
+
if (Array.isArray(obj)) {
|
|
886
|
+
if (obj.length === 0) {
|
|
887
|
+
return 'array';
|
|
888
|
+
}
|
|
889
|
+
// Get schema of first element as representative
|
|
890
|
+
return {
|
|
891
|
+
type: 'array',
|
|
892
|
+
items: this.extractSchema(obj[0], path),
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Handle objects
|
|
897
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
898
|
+
const schema: Record<string, any> = {};
|
|
899
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
900
|
+
const fieldPath = path ? `${path}.${key}` : key;
|
|
901
|
+
|
|
902
|
+
if (value === null || value === undefined) {
|
|
903
|
+
schema[key] = {
|
|
904
|
+
type: 'null',
|
|
905
|
+
path: fieldPath,
|
|
906
|
+
};
|
|
907
|
+
} else if (typeof value === 'boolean') {
|
|
908
|
+
// Check bool before number (important for type detection)
|
|
909
|
+
schema[key] = {
|
|
910
|
+
type: 'boolean',
|
|
911
|
+
path: fieldPath,
|
|
912
|
+
};
|
|
913
|
+
} else if (typeof value === 'number') {
|
|
914
|
+
// Check if it's an integer or float
|
|
915
|
+
if (Number.isInteger(value)) {
|
|
916
|
+
schema[key] = {
|
|
917
|
+
type: 'integer',
|
|
918
|
+
path: fieldPath,
|
|
919
|
+
};
|
|
920
|
+
} else {
|
|
921
|
+
schema[key] = {
|
|
922
|
+
type: 'float',
|
|
923
|
+
path: fieldPath,
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
} else if (typeof value === 'string') {
|
|
927
|
+
schema[key] = {
|
|
928
|
+
type: 'string',
|
|
929
|
+
path: fieldPath,
|
|
930
|
+
};
|
|
931
|
+
} else if (Array.isArray(value)) {
|
|
932
|
+
schema[key] = {
|
|
933
|
+
type: 'array',
|
|
934
|
+
path: fieldPath,
|
|
935
|
+
items: value.length > 0 ? this.extractSchema(value[0], fieldPath) : 'unknown',
|
|
936
|
+
};
|
|
937
|
+
} else if (typeof value === 'object') {
|
|
938
|
+
schema[key] = {
|
|
939
|
+
type: 'object',
|
|
940
|
+
path: fieldPath,
|
|
941
|
+
properties: this.extractSchema(value, fieldPath),
|
|
942
|
+
};
|
|
943
|
+
} else {
|
|
944
|
+
schema[key] = {
|
|
945
|
+
type: typeof value,
|
|
946
|
+
path: fieldPath,
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
return schema;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Handle primitive types
|
|
955
|
+
if (typeof obj === 'boolean') {
|
|
956
|
+
return 'boolean';
|
|
957
|
+
} else if (typeof obj === 'number') {
|
|
958
|
+
if (Number.isInteger(obj)) {
|
|
959
|
+
return 'integer';
|
|
960
|
+
} else {
|
|
961
|
+
return 'float';
|
|
962
|
+
}
|
|
963
|
+
} else if (typeof obj === 'string') {
|
|
964
|
+
return 'string';
|
|
965
|
+
} else {
|
|
966
|
+
return typeof obj;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
651
969
|
}
|
package/src/types.ts
CHANGED
|
@@ -5,6 +5,7 @@ declare global {
|
|
|
5
5
|
startTime: number;
|
|
6
6
|
eventId?: string;
|
|
7
7
|
metadata?: RequestMetadata;
|
|
8
|
+
responseTrackingField?: string;
|
|
8
9
|
};
|
|
9
10
|
baseUrl: string;
|
|
10
11
|
}
|
|
@@ -16,6 +17,8 @@ declare global {
|
|
|
16
17
|
startTime: number;
|
|
17
18
|
eventId?: string;
|
|
18
19
|
metadata?: RequestMetadata;
|
|
20
|
+
responseTrackingField?: string;
|
|
21
|
+
|
|
19
22
|
};
|
|
20
23
|
}
|
|
21
24
|
}
|
|
@@ -26,6 +29,7 @@ declare global {
|
|
|
26
29
|
startTime: number;
|
|
27
30
|
eventId?: string;
|
|
28
31
|
metadata?: RequestMetadata;
|
|
32
|
+
responseTrackingField?: string;
|
|
29
33
|
};
|
|
30
34
|
}
|
|
31
35
|
}
|
|
@@ -63,6 +67,9 @@ export interface UsageFlowConfig {
|
|
|
63
67
|
identityFieldName?: string;
|
|
64
68
|
identityFieldLocation?: string;
|
|
65
69
|
hasRateLimit?: boolean;
|
|
70
|
+
responseTrackingField?: string;
|
|
71
|
+
isResponseTrackingEnabled?: boolean;
|
|
72
|
+
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
export interface BlockedEndpoint {
|
|
@@ -140,6 +147,7 @@ export interface UsageFlowRequest {
|
|
|
140
147
|
eventId?: string;
|
|
141
148
|
metadata?: RequestMetadata;
|
|
142
149
|
startTime?: number;
|
|
150
|
+
responseTrackingField?: string;
|
|
143
151
|
};
|
|
144
152
|
routeOptions?: {
|
|
145
153
|
url: string;
|