@usageflow/core 0.2.5 → 0.2.6
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 +207 -26
- package/dist/base.d.ts +4 -4
- package/dist/base.js +75 -45
- package/dist/base.js.map +1 -1
- package/jest.config.js +14 -0
- package/package.json +6 -7
- package/src/base.ts +139 -71
- package/test/base.test.ts +290 -0
- package/test/socket.test.ts +72 -0
- package/test/src/base.d.ts +62 -0
- package/test/src/base.js +440 -0
- package/test/src/base.js.map +1 -0
- package/test/src/index.d.ts +3 -0
- package/test/src/index.js +20 -0
- package/test/src/index.js.map +1 -0
- package/test/src/socket.d.ts +26 -0
- package/test/src/socket.js +266 -0
- package/test/src/socket.js.map +1 -0
- package/test/src/types.d.ts +117 -0
- package/test/src/types.js +11 -0
- package/test/src/types.js.map +1 -0
- package/test/tsconfig.test.tsbuildinfo +1 -0
- package/test/types.test.ts +56 -0
- package/tsconfig.json +2 -1
- package/tsconfig.test.json +11 -0
- package/tsconfig.test.tsbuildinfo +1 -0
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usageflow/core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Core functionality for UsageFlow integrations",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc",
|
|
9
|
-
"test": "
|
|
9
|
+
"test": "tsx --test test/base.test.ts test/socket.test.ts test/types.test.ts",
|
|
10
10
|
"prepare": "npm run build",
|
|
11
11
|
"prepublishOnly": "npm run build"
|
|
12
12
|
},
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"axios": "^1.6.0",
|
|
18
|
-
"ws": "^8.16.0"
|
|
18
|
+
"ws": "^8.16.0",
|
|
19
|
+
"@usageflow/logger": "^0.1.1"
|
|
19
20
|
},
|
|
20
21
|
"homepage": "https://usageflow.io",
|
|
21
22
|
"repository": {
|
|
@@ -26,9 +27,7 @@
|
|
|
26
27
|
"devDependencies": {
|
|
27
28
|
"@types/node": "^20.0.0",
|
|
28
29
|
"@types/ws": "^8.5.10",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"@types/jest": "^29.0.0",
|
|
32
|
-
"ts-jest": "^29.0.0"
|
|
30
|
+
"tsx": "^4.20.6",
|
|
31
|
+
"typescript": "^5.0.0"
|
|
33
32
|
}
|
|
34
33
|
}
|
package/src/base.ts
CHANGED
|
@@ -11,17 +11,26 @@ import {
|
|
|
11
11
|
UsageFlowAPIConfig,
|
|
12
12
|
} from "./types";
|
|
13
13
|
import { UsageFlowSocketManger } from "./socket";
|
|
14
|
+
import { UsageFlowLogger, usLogger } from "@usageflow/logger";
|
|
14
15
|
|
|
15
16
|
export abstract class UsageFlowAPI {
|
|
16
17
|
protected apiKey: string | null = null;
|
|
17
18
|
protected usageflowUrl: string = "https://api.usageflow.io";
|
|
18
|
-
protected webServer:
|
|
19
|
+
protected webServer: "express" | "fastify" | "nestjs" = "express";
|
|
19
20
|
protected apiConfigs: UsageFlowConfig[] = [];
|
|
20
21
|
private configUpdateInterval: NodeJS.Timeout | null = null;
|
|
21
|
-
socketManager: UsageFlowSocketManger | null = null;
|
|
22
|
+
private socketManager: UsageFlowSocketManger | null = null;
|
|
22
23
|
private applicationId: boolean = false;
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
private logger: UsageFlowLogger;
|
|
25
|
+
|
|
26
|
+
constructor(config: UsageFlowAPIConfig = { apiKey: "", poolSize: 5 }) {
|
|
27
|
+
// Initialize logger with service name
|
|
28
|
+
this.logger = usLogger({ service: 'USAGEFLOW_BASE', pretty: true, silent: process.env.UF_LOGS_ENABLED !== 'true' });
|
|
29
|
+
// Set default context for all logs
|
|
30
|
+
this.logger.setContext({
|
|
31
|
+
component: 'UsageFlowAPI',
|
|
32
|
+
webServer: this.webServer,
|
|
33
|
+
});
|
|
25
34
|
this.init(config.apiKey, this.usageflowUrl, config.poolSize);
|
|
26
35
|
}
|
|
27
36
|
|
|
@@ -41,18 +50,30 @@ export abstract class UsageFlowAPI {
|
|
|
41
50
|
// this.startConfigUpdater();
|
|
42
51
|
this.socketManager = new UsageFlowSocketManger(apiKey, poolSize);
|
|
43
52
|
// Connect the socket manager
|
|
44
|
-
this.socketManager
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
this.socketManager
|
|
54
|
+
.connect()
|
|
55
|
+
.catch((error) => {
|
|
56
|
+
this.logger.error(
|
|
57
|
+
"[UsageFlow] Failed to establish WebSocket connection:",
|
|
58
|
+
error,
|
|
59
|
+
);
|
|
60
|
+
})
|
|
61
|
+
.then(() => {
|
|
62
|
+
this.logger.info("WebSocket connection established");
|
|
63
|
+
if (this.socketManager?.isConnected()) {
|
|
64
|
+
this.startConfigUpdater();
|
|
65
|
+
} else {
|
|
66
|
+
this.logger.error("WebSocket connection failed");
|
|
67
|
+
}
|
|
68
|
+
});
|
|
50
69
|
|
|
51
70
|
return this;
|
|
52
71
|
}
|
|
53
72
|
|
|
54
|
-
public setWebServer(webServer:
|
|
73
|
+
public setWebServer(webServer: "express" | "fastify" | "nestjs"): void {
|
|
55
74
|
this.webServer = webServer;
|
|
75
|
+
// Update logger context with new web server
|
|
76
|
+
this.logger.setContext({ webServer });
|
|
56
77
|
}
|
|
57
78
|
|
|
58
79
|
public getApiKey(): string | null {
|
|
@@ -67,11 +88,12 @@ export abstract class UsageFlowAPI {
|
|
|
67
88
|
* Start background config update process
|
|
68
89
|
*/
|
|
69
90
|
private startConfigUpdater(): void {
|
|
91
|
+
this.logger.info("Starting background config update process");
|
|
70
92
|
if (this.configUpdateInterval) {
|
|
71
93
|
clearInterval(this.configUpdateInterval);
|
|
72
94
|
}
|
|
73
95
|
|
|
74
|
-
this.fetchApiPolicies().catch(
|
|
96
|
+
this.fetchApiPolicies().catch(this.logger.error);
|
|
75
97
|
this.configUpdateInterval = setInterval(async () => {
|
|
76
98
|
await this.fetchApiPolicies();
|
|
77
99
|
}, 60000);
|
|
@@ -80,13 +102,12 @@ export abstract class UsageFlowAPI {
|
|
|
80
102
|
}
|
|
81
103
|
|
|
82
104
|
public getRoutePattern(request: UsageFlowRequest): string {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return request?.routeOptions?.url || request.url || '';
|
|
105
|
+
if (this.webServer === "fastify") {
|
|
106
|
+
return request?.routeOptions?.url || request.url || "";
|
|
86
107
|
}
|
|
87
108
|
|
|
88
109
|
// For NestJS, prioritize route.path with baseUrl
|
|
89
|
-
if (this.webServer ===
|
|
110
|
+
if (this.webServer === "nestjs") {
|
|
90
111
|
// Method 1: Use request.route.path (available after route matching in NestJS)
|
|
91
112
|
if (request.route?.path) {
|
|
92
113
|
const baseUrl = request.baseUrl || "";
|
|
@@ -102,7 +123,9 @@ export abstract class UsageFlowAPI {
|
|
|
102
123
|
let path = request.path || request.url?.split("?")[0] || "/";
|
|
103
124
|
|
|
104
125
|
// Split path into segments and replace matching segments with param names
|
|
105
|
-
const pathSegments = path
|
|
126
|
+
const pathSegments = path
|
|
127
|
+
.split("/")
|
|
128
|
+
.filter((segment) => segment !== "");
|
|
106
129
|
const paramEntries = Object.entries(request.params);
|
|
107
130
|
|
|
108
131
|
// Replace segments that match param values with :paramName
|
|
@@ -121,21 +144,22 @@ export abstract class UsageFlowAPI {
|
|
|
121
144
|
}
|
|
122
145
|
}
|
|
123
146
|
|
|
124
|
-
const routePattern =
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (route.regexp) {
|
|
132
|
-
const patterned = route.regexp.test(request.url);
|
|
133
|
-
if (patterned) {
|
|
134
|
-
return patterned;
|
|
147
|
+
const routePattern =
|
|
148
|
+
request.route?.path ||
|
|
149
|
+
request.app._router.stack.find((route: any) => {
|
|
150
|
+
// a => a.path == request.url
|
|
151
|
+
if (!route.route) return false;
|
|
152
|
+
if (route.path) {
|
|
153
|
+
return route.path == request.url;
|
|
135
154
|
}
|
|
136
|
-
}
|
|
137
155
|
|
|
138
|
-
|
|
156
|
+
if (route.regexp) {
|
|
157
|
+
const patterned = route.regexp.test(request.url);
|
|
158
|
+
if (patterned) {
|
|
159
|
+
return patterned;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
})?.route?.path;
|
|
139
163
|
|
|
140
164
|
if (routePattern) {
|
|
141
165
|
return routePattern;
|
|
@@ -163,7 +187,9 @@ export abstract class UsageFlowAPI {
|
|
|
163
187
|
if (layer.route) {
|
|
164
188
|
const route = layer.route;
|
|
165
189
|
const routePath = route.path;
|
|
166
|
-
const routeMethods = Object.keys(route.methods).map(m =>
|
|
190
|
+
const routeMethods = Object.keys(route.methods).map((m) =>
|
|
191
|
+
m.toLowerCase(),
|
|
192
|
+
);
|
|
167
193
|
|
|
168
194
|
// Check if method matches and path pattern matches
|
|
169
195
|
if (routeMethods.includes(method) || routeMethods.includes("*")) {
|
|
@@ -173,7 +199,9 @@ export abstract class UsageFlowAPI {
|
|
|
173
199
|
if (request.params && Object.keys(request.params).length > 0) {
|
|
174
200
|
// Check if route path contains the param names
|
|
175
201
|
const paramNames = Object.keys(request.params);
|
|
176
|
-
const routeHasParams = paramNames.some(param =>
|
|
202
|
+
const routeHasParams = paramNames.some((param) =>
|
|
203
|
+
routePath.includes(`:${param}`),
|
|
204
|
+
);
|
|
177
205
|
if (routeHasParams) {
|
|
178
206
|
return pattern;
|
|
179
207
|
}
|
|
@@ -184,7 +212,11 @@ export abstract class UsageFlowAPI {
|
|
|
184
212
|
}
|
|
185
213
|
}
|
|
186
214
|
}
|
|
187
|
-
} else if (
|
|
215
|
+
} else if (
|
|
216
|
+
layer.name === "router" &&
|
|
217
|
+
layer.handle &&
|
|
218
|
+
layer.handle.stack
|
|
219
|
+
) {
|
|
188
220
|
// Handle router middleware (nested routers)
|
|
189
221
|
const mountPath = layer.regexp.source
|
|
190
222
|
.replace("\\/?", "")
|
|
@@ -198,19 +230,30 @@ export abstract class UsageFlowAPI {
|
|
|
198
230
|
if (subLayer.route) {
|
|
199
231
|
const route = subLayer.route;
|
|
200
232
|
const routePath = route.path;
|
|
201
|
-
const routeMethods = Object.keys(route.methods).map(m =>
|
|
202
|
-
|
|
203
|
-
|
|
233
|
+
const routeMethods = Object.keys(route.methods).map((m) =>
|
|
234
|
+
m.toLowerCase(),
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
if (
|
|
238
|
+
routeMethods.includes(method) ||
|
|
239
|
+
routeMethods.includes("*")
|
|
240
|
+
) {
|
|
204
241
|
const fullPath = mountPath + routePath;
|
|
205
242
|
// Check if this route matches by examining params
|
|
206
|
-
if (
|
|
243
|
+
if (
|
|
244
|
+
request.params &&
|
|
245
|
+
Object.keys(request.params).length > 0
|
|
246
|
+
) {
|
|
207
247
|
const paramNames = Object.keys(request.params);
|
|
208
|
-
const routeHasParams = paramNames.some(param =>
|
|
248
|
+
const routeHasParams = paramNames.some((param) =>
|
|
249
|
+
routePath.includes(`:${param}`),
|
|
250
|
+
);
|
|
209
251
|
if (routeHasParams) {
|
|
210
252
|
return fullPath;
|
|
211
253
|
}
|
|
212
254
|
} else if (!routePath.includes(":")) {
|
|
213
|
-
const currentPath =
|
|
255
|
+
const currentPath =
|
|
256
|
+
request.path || request.url?.split("?")[0] || "";
|
|
214
257
|
if (currentPath === fullPath || currentPath === routePath) {
|
|
215
258
|
return fullPath;
|
|
216
259
|
}
|
|
@@ -223,7 +266,10 @@ export abstract class UsageFlowAPI {
|
|
|
223
266
|
}
|
|
224
267
|
} catch (error) {
|
|
225
268
|
// Silently fail and try next method
|
|
226
|
-
console.debug(
|
|
269
|
+
console.debug(
|
|
270
|
+
"[UsageFlow] Could not extract route from router stack:",
|
|
271
|
+
error,
|
|
272
|
+
);
|
|
227
273
|
}
|
|
228
274
|
|
|
229
275
|
// Method 3: Reconstruct pattern from params and path
|
|
@@ -255,16 +301,18 @@ export abstract class UsageFlowAPI {
|
|
|
255
301
|
return baseUrl + path || "/";
|
|
256
302
|
}
|
|
257
303
|
|
|
258
|
-
public guessLedgerId(
|
|
304
|
+
public guessLedgerId(
|
|
305
|
+
request: UsageFlowRequest,
|
|
306
|
+
overrideUrl?: string,
|
|
307
|
+
): string {
|
|
259
308
|
const method = request.method;
|
|
260
309
|
const url = overrideUrl || this.getRoutePattern(request);
|
|
261
|
-
const configs = this.apiConfigs
|
|
310
|
+
const configs = this.apiConfigs;
|
|
262
311
|
|
|
263
312
|
if (!configs.length) {
|
|
264
313
|
return `${method} ${url}`;
|
|
265
314
|
}
|
|
266
315
|
|
|
267
|
-
|
|
268
316
|
for (const config of configs) {
|
|
269
317
|
const fieldName = config.identityFieldName!;
|
|
270
318
|
const location = config.identityFieldLocation;
|
|
@@ -286,7 +334,10 @@ export abstract class UsageFlowAPI {
|
|
|
286
334
|
}
|
|
287
335
|
break;
|
|
288
336
|
case "bearer_token":
|
|
289
|
-
const authHeader = this.getHeaderValue(
|
|
337
|
+
const authHeader = this.getHeaderValue(
|
|
338
|
+
request.headers,
|
|
339
|
+
"authorization",
|
|
340
|
+
);
|
|
290
341
|
const token = this.extractBearerToken(authHeader || undefined);
|
|
291
342
|
if (token) {
|
|
292
343
|
const claims = this.decodeJwtUnverified(token);
|
|
@@ -311,31 +362,36 @@ export abstract class UsageFlowAPI {
|
|
|
311
362
|
|
|
312
363
|
private async fetchApiPolicies(): Promise<void> {
|
|
313
364
|
if (this.socketManager && this.socketManager.isConnected()) {
|
|
314
|
-
const response = await this.socketManager.sendAsync<{
|
|
365
|
+
const response = await this.socketManager.sendAsync<{
|
|
366
|
+
policies: UsageFlowConfig[];
|
|
367
|
+
total: number;
|
|
368
|
+
}>({
|
|
315
369
|
type: "get_application_policies",
|
|
316
370
|
payload: null,
|
|
317
371
|
});
|
|
318
|
-
if (response.type ===
|
|
372
|
+
if (response.type === "success") {
|
|
319
373
|
this.apiConfigs = response.payload?.policies || [];
|
|
320
374
|
}
|
|
321
375
|
}
|
|
322
376
|
}
|
|
323
377
|
|
|
324
|
-
async useAllocationRequest(
|
|
325
|
-
payload: RequestForAllocation,
|
|
326
|
-
): Promise<void> {
|
|
378
|
+
async useAllocationRequest(payload: RequestForAllocation): Promise<void> {
|
|
327
379
|
if (this.socketManager && this.socketManager.isConnected()) {
|
|
328
|
-
this.socketManager
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
380
|
+
this.socketManager
|
|
381
|
+
.sendAsync<any>({
|
|
382
|
+
type: "use_allocation",
|
|
383
|
+
payload,
|
|
384
|
+
})
|
|
385
|
+
.catch((error) => {
|
|
386
|
+
console.error(
|
|
387
|
+
"[UsageFlow] Error sending finalization via WebSocket:",
|
|
388
|
+
error,
|
|
389
|
+
);
|
|
390
|
+
throw error;
|
|
391
|
+
});
|
|
335
392
|
}
|
|
336
393
|
}
|
|
337
394
|
|
|
338
|
-
|
|
339
395
|
async allocationRequest(
|
|
340
396
|
request: UsageFlowRequest,
|
|
341
397
|
payload: RequestForAllocation,
|
|
@@ -345,21 +401,28 @@ export abstract class UsageFlowAPI {
|
|
|
345
401
|
try {
|
|
346
402
|
const allocationResponse = await this.socketManager.sendAsync<any>({
|
|
347
403
|
type: "request_for_allocation",
|
|
348
|
-
payload
|
|
404
|
+
payload,
|
|
349
405
|
});
|
|
350
406
|
|
|
351
|
-
if (allocationResponse.type ===
|
|
352
|
-
throw new Error(
|
|
407
|
+
if (allocationResponse.type === "error") {
|
|
408
|
+
throw new Error(
|
|
409
|
+
allocationResponse.message || allocationResponse.error,
|
|
410
|
+
);
|
|
353
411
|
}
|
|
354
|
-
if (allocationResponse.type ===
|
|
412
|
+
if (allocationResponse.type === "success") {
|
|
355
413
|
request.usageflow!.eventId = allocationResponse.payload.allocationId;
|
|
356
414
|
request.usageflow!.metadata = metadata;
|
|
357
415
|
return;
|
|
358
416
|
} else {
|
|
359
|
-
throw new Error(
|
|
417
|
+
throw new Error(
|
|
418
|
+
allocationResponse.message || "Unknown error occurred",
|
|
419
|
+
);
|
|
360
420
|
}
|
|
361
421
|
} catch (error: any) {
|
|
362
|
-
console.error(
|
|
422
|
+
console.error(
|
|
423
|
+
"[UsageFlow] WebSocket allocation failed, falling back to HTTP:",
|
|
424
|
+
error,
|
|
425
|
+
);
|
|
363
426
|
throw error;
|
|
364
427
|
// Fall through to HTTP request
|
|
365
428
|
}
|
|
@@ -468,8 +531,8 @@ export abstract class UsageFlowAPI {
|
|
|
468
531
|
}
|
|
469
532
|
|
|
470
533
|
// Check if it's a bearer token
|
|
471
|
-
const parts = authHeader.split(
|
|
472
|
-
if (parts.length !== 2 || parts[0].toLowerCase() !==
|
|
534
|
+
const parts = authHeader.split(" ");
|
|
535
|
+
if (parts.length !== 2 || parts[0].toLowerCase() !== "bearer") {
|
|
473
536
|
return null;
|
|
474
537
|
}
|
|
475
538
|
|
|
@@ -479,14 +542,19 @@ export abstract class UsageFlowAPI {
|
|
|
479
542
|
/**
|
|
480
543
|
* Get header value from headers object (handles both Record and Headers types)
|
|
481
544
|
*/
|
|
482
|
-
private getHeaderValue(
|
|
545
|
+
private getHeaderValue(
|
|
546
|
+
headers: Record<string, string | string[] | undefined> | Headers,
|
|
547
|
+
key: string,
|
|
548
|
+
): string | null {
|
|
483
549
|
// Check if it's a Headers object (from Fetch API) by checking for the 'get' method
|
|
484
|
-
if (headers && typeof (headers as any).get ===
|
|
550
|
+
if (headers && typeof (headers as any).get === "function") {
|
|
485
551
|
return (headers as any).get(key) || null;
|
|
486
552
|
}
|
|
487
553
|
// Otherwise, treat it as a Record
|
|
488
|
-
const value = (headers as Record<string, string | string[] | undefined>)[
|
|
489
|
-
|
|
554
|
+
const value = (headers as Record<string, string | string[] | undefined>)[
|
|
555
|
+
key
|
|
556
|
+
];
|
|
557
|
+
if (typeof value === "string") {
|
|
490
558
|
return value;
|
|
491
559
|
}
|
|
492
560
|
if (Array.isArray(value) && value.length > 0) {
|
|
@@ -503,14 +571,14 @@ export abstract class UsageFlowAPI {
|
|
|
503
571
|
public decodeJwtUnverified(token: string): Record<string, any> | null {
|
|
504
572
|
try {
|
|
505
573
|
// Split the token into parts
|
|
506
|
-
const parts = token.split(
|
|
574
|
+
const parts = token.split(".");
|
|
507
575
|
if (parts.length !== 3) {
|
|
508
576
|
return null;
|
|
509
577
|
}
|
|
510
578
|
|
|
511
579
|
// Decode the payload (claims)
|
|
512
580
|
const payload = parts[1];
|
|
513
|
-
const decoded = Buffer.from(payload,
|
|
581
|
+
const decoded = Buffer.from(payload, "base64").toString("utf-8");
|
|
514
582
|
return JSON.parse(decoded);
|
|
515
583
|
} catch (error) {
|
|
516
584
|
return null;
|