@usageflow/core 0.2.5 → 0.3.0

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/src/base.ts CHANGED
@@ -9,19 +9,31 @@ import {
9
9
  RequestMetadata,
10
10
  UsageFlowRequest,
11
11
  UsageFlowAPIConfig,
12
+ BlockedEndpoint,
12
13
  } from "./types";
13
14
  import { UsageFlowSocketManger } from "./socket";
15
+ import { UsageFlowLogger, usLogger } from "@usageflow/logger";
16
+ import { randomUUID } from "crypto";
14
17
 
15
18
  export abstract class UsageFlowAPI {
16
19
  protected apiKey: string | null = null;
17
20
  protected usageflowUrl: string = "https://api.usageflow.io";
18
- protected webServer: 'express' | 'fastify' | 'nestjs' = 'express';
21
+ protected webServer: "express" | "fastify" | "nestjs" = "express";
19
22
  protected apiConfigs: UsageFlowConfig[] = [];
23
+ protected blockedEndpoints: string[] = [];
20
24
  private configUpdateInterval: NodeJS.Timeout | null = null;
21
- socketManager: UsageFlowSocketManger | null = null;
25
+ private socketManager: UsageFlowSocketManger | null = null;
22
26
  private applicationId: boolean = false;
23
-
24
- constructor(config: UsageFlowAPIConfig = { apiKey: '', poolSize: 5 }) {
27
+ private logger: UsageFlowLogger;
28
+
29
+ constructor(config: UsageFlowAPIConfig = { apiKey: "", poolSize: 5 }) {
30
+ // Initialize logger with service name
31
+ this.logger = usLogger({ service: 'USAGEFLOW_BASE', pretty: true, silent: process.env.UF_LOGS_ENABLED !== 'true' });
32
+ // Set default context for all logs
33
+ this.logger.setContext({
34
+ component: 'UsageFlowAPI',
35
+ webServer: this.webServer,
36
+ });
25
37
  this.init(config.apiKey, this.usageflowUrl, config.poolSize);
26
38
  }
27
39
 
@@ -41,18 +53,30 @@ export abstract class UsageFlowAPI {
41
53
  // this.startConfigUpdater();
42
54
  this.socketManager = new UsageFlowSocketManger(apiKey, poolSize);
43
55
  // Connect the socket manager
44
- this.socketManager.connect().catch((error) => {
45
- console.error("[UsageFlow] Failed to establish WebSocket connection:", error);
46
- }).then(() => {
47
- this.startConfigUpdater();
48
- });
49
-
56
+ this.socketManager
57
+ .connect()
58
+ .catch((error) => {
59
+ this.logger.error(
60
+ "[UsageFlow] Failed to establish WebSocket connection:",
61
+ error,
62
+ );
63
+ })
64
+ .then(() => {
65
+ this.logger.info("WebSocket connection established");
66
+ if (this.socketManager?.isConnected()) {
67
+ this.startConfigUpdater();
68
+ } else {
69
+ this.logger.error("WebSocket connection failed");
70
+ }
71
+ });
50
72
 
51
73
  return this;
52
74
  }
53
75
 
54
- public setWebServer(webServer: 'express' | 'fastify' | 'nestjs'): void {
76
+ public setWebServer(webServer: "express" | "fastify" | "nestjs"): void {
55
77
  this.webServer = webServer;
78
+ // Update logger context with new web server
79
+ this.logger.setContext({ webServer });
56
80
  }
57
81
 
58
82
  public getApiKey(): string | null {
@@ -67,26 +91,27 @@ export abstract class UsageFlowAPI {
67
91
  * Start background config update process
68
92
  */
69
93
  private startConfigUpdater(): void {
94
+ this.logger.info("Starting background config update process");
70
95
  if (this.configUpdateInterval) {
71
96
  clearInterval(this.configUpdateInterval);
72
97
  }
73
98
 
74
- this.fetchApiPolicies().catch(console.error);
99
+ this.fetchApiPolicies().catch(this.logger.error);
75
100
  this.configUpdateInterval = setInterval(async () => {
76
- await this.fetchApiPolicies();
77
- }, 60000);
78
-
79
- // this.fetchApiConfig().catch(console.error);
101
+ await Promise.all([
102
+ this.fetchApiPolicies(),
103
+ this.fetchBlockedEndpoints(),
104
+ ]);
105
+ }, 10000);
80
106
  }
81
107
 
82
108
  public getRoutePattern(request: UsageFlowRequest): string {
83
-
84
- if (this.webServer === 'fastify') {
85
- return request?.routeOptions?.url || request.url || '';
109
+ if (this.webServer === "fastify") {
110
+ return request?.routeOptions?.url || request.url || "";
86
111
  }
87
112
 
88
113
  // For NestJS, prioritize route.path with baseUrl
89
- if (this.webServer === 'nestjs') {
114
+ if (this.webServer === "nestjs") {
90
115
  // Method 1: Use request.route.path (available after route matching in NestJS)
91
116
  if (request.route?.path) {
92
117
  const baseUrl = request.baseUrl || "";
@@ -102,7 +127,9 @@ export abstract class UsageFlowAPI {
102
127
  let path = request.path || request.url?.split("?")[0] || "/";
103
128
 
104
129
  // Split path into segments and replace matching segments with param names
105
- const pathSegments = path.split("/").filter(segment => segment !== "");
130
+ const pathSegments = path
131
+ .split("/")
132
+ .filter((segment) => segment !== "");
106
133
  const paramEntries = Object.entries(request.params);
107
134
 
108
135
  // Replace segments that match param values with :paramName
@@ -121,21 +148,22 @@ export abstract class UsageFlowAPI {
121
148
  }
122
149
  }
123
150
 
124
- const routePattern = request.route?.path || request.app._router.stack.find((route: any) => {
125
- // a => a.path == request.url
126
- if (!route.route) return false;
127
- if (route.path) {
128
- return route.path == request.url;
129
- }
130
-
131
- if (route.regexp) {
132
- const patterned = route.regexp.test(request.url);
133
- if (patterned) {
134
- return patterned;
151
+ const routePattern =
152
+ request.route?.path ||
153
+ request.app._router.stack.find((route: any) => {
154
+ // a => a.path == request.url
155
+ if (!route.route) return false;
156
+ if (route.path) {
157
+ return route.path == request.url;
135
158
  }
136
- }
137
159
 
138
- })?.route?.path
160
+ if (route.regexp) {
161
+ const patterned = route.regexp.test(request.url);
162
+ if (patterned) {
163
+ return patterned;
164
+ }
165
+ }
166
+ })?.route?.path;
139
167
 
140
168
  if (routePattern) {
141
169
  return routePattern;
@@ -163,7 +191,9 @@ export abstract class UsageFlowAPI {
163
191
  if (layer.route) {
164
192
  const route = layer.route;
165
193
  const routePath = route.path;
166
- const routeMethods = Object.keys(route.methods).map(m => m.toLowerCase());
194
+ const routeMethods = Object.keys(route.methods).map((m) =>
195
+ m.toLowerCase(),
196
+ );
167
197
 
168
198
  // Check if method matches and path pattern matches
169
199
  if (routeMethods.includes(method) || routeMethods.includes("*")) {
@@ -173,7 +203,9 @@ export abstract class UsageFlowAPI {
173
203
  if (request.params && Object.keys(request.params).length > 0) {
174
204
  // Check if route path contains the param names
175
205
  const paramNames = Object.keys(request.params);
176
- const routeHasParams = paramNames.some(param => routePath.includes(`:${param}`));
206
+ const routeHasParams = paramNames.some((param) =>
207
+ routePath.includes(`:${param}`),
208
+ );
177
209
  if (routeHasParams) {
178
210
  return pattern;
179
211
  }
@@ -184,7 +216,11 @@ export abstract class UsageFlowAPI {
184
216
  }
185
217
  }
186
218
  }
187
- } else if (layer.name === "router" && layer.handle && layer.handle.stack) {
219
+ } else if (
220
+ layer.name === "router" &&
221
+ layer.handle &&
222
+ layer.handle.stack
223
+ ) {
188
224
  // Handle router middleware (nested routers)
189
225
  const mountPath = layer.regexp.source
190
226
  .replace("\\/?", "")
@@ -198,19 +234,30 @@ export abstract class UsageFlowAPI {
198
234
  if (subLayer.route) {
199
235
  const route = subLayer.route;
200
236
  const routePath = route.path;
201
- const routeMethods = Object.keys(route.methods).map(m => m.toLowerCase());
202
-
203
- if (routeMethods.includes(method) || routeMethods.includes("*")) {
237
+ const routeMethods = Object.keys(route.methods).map((m) =>
238
+ m.toLowerCase(),
239
+ );
240
+
241
+ if (
242
+ routeMethods.includes(method) ||
243
+ routeMethods.includes("*")
244
+ ) {
204
245
  const fullPath = mountPath + routePath;
205
246
  // Check if this route matches by examining params
206
- if (request.params && Object.keys(request.params).length > 0) {
247
+ if (
248
+ request.params &&
249
+ Object.keys(request.params).length > 0
250
+ ) {
207
251
  const paramNames = Object.keys(request.params);
208
- const routeHasParams = paramNames.some(param => routePath.includes(`:${param}`));
252
+ const routeHasParams = paramNames.some((param) =>
253
+ routePath.includes(`:${param}`),
254
+ );
209
255
  if (routeHasParams) {
210
256
  return fullPath;
211
257
  }
212
258
  } else if (!routePath.includes(":")) {
213
- const currentPath = request.path || request.url?.split("?")[0] || "";
259
+ const currentPath =
260
+ request.path || request.url?.split("?")[0] || "";
214
261
  if (currentPath === fullPath || currentPath === routePath) {
215
262
  return fullPath;
216
263
  }
@@ -223,7 +270,10 @@ export abstract class UsageFlowAPI {
223
270
  }
224
271
  } catch (error) {
225
272
  // Silently fail and try next method
226
- console.debug("[UsageFlow] Could not extract route from router stack:", error);
273
+ console.debug(
274
+ "[UsageFlow] Could not extract route from router stack:",
275
+ error,
276
+ );
227
277
  }
228
278
 
229
279
  // Method 3: Reconstruct pattern from params and path
@@ -255,111 +305,160 @@ export abstract class UsageFlowAPI {
255
305
  return baseUrl + path || "/";
256
306
  }
257
307
 
258
- public guessLedgerId(request: UsageFlowRequest, overrideUrl?: string): string {
308
+ public guessLedgerId(
309
+ request: UsageFlowRequest,
310
+ overrideUrl?: string,
311
+ ): { ledgerId: string, hasLimit: boolean } {
259
312
  const method = request.method;
260
313
  const url = overrideUrl || this.getRoutePattern(request);
261
- const configs = this.apiConfigs
314
+ const configs = this.apiConfigs;
262
315
 
263
316
  if (!configs.length) {
264
- return `${method} ${url}`;
317
+ return { ledgerId: `${method} ${url}`, hasLimit: false };
265
318
  }
266
319
 
267
-
268
320
  for (const config of configs) {
269
321
  const fieldName = config.identityFieldName!;
270
322
  const location = config.identityFieldLocation;
323
+ const hasLimit = config.hasRateLimit || false;
271
324
 
272
- switch (location) {
273
- case "path_params":
274
- if (request.params?.[fieldName]) {
275
- return `${method} ${url} ${request.params[fieldName]}`;
276
- }
277
- break;
278
- case "query_params":
279
- if (request.query?.[fieldName]) {
280
- return `${method} ${url} ${request.query[fieldName]}`;
281
- }
282
- break;
283
- case "body":
284
- if (request.body?.[fieldName]) {
285
- return `${method} ${url} ${request.body[fieldName]}`;
286
- }
287
- break;
288
- case "bearer_token":
289
- const authHeader = this.getHeaderValue(request.headers, 'authorization');
290
- const token = this.extractBearerToken(authHeader || undefined);
291
- if (token) {
292
- const claims = this.decodeJwtUnverified(token);
293
- if (claims?.[fieldName]) {
294
- return `${method} ${url} ${claims[fieldName]}`;
325
+ if (method === config.method && url === config.url) {
326
+
327
+ switch (location) {
328
+ case "path_params":
329
+ if (request.params?.[fieldName]) {
330
+ return { ledgerId: `${method} ${url} ${request.params[fieldName]}`, hasLimit };
295
331
  }
296
- }
297
- break;
332
+ break;
333
+ case "query_params":
334
+ if (request.query?.[fieldName]) {
335
+ return { ledgerId: `${method} ${url} ${request.query[fieldName]}`, hasLimit };
336
+ }
337
+ break;
338
+ case "body":
339
+ if (request.body?.[fieldName]) {
340
+ return { ledgerId: `${method} ${url} ${request.body[fieldName]}`, hasLimit };
341
+ }
342
+ break;
343
+ case "bearer_token":
344
+ const authHeader = this.getHeaderValue(
345
+ request.headers,
346
+ "authorization",
347
+ );
348
+ const token = this.extractBearerToken(authHeader || undefined);
349
+ if (token) {
350
+ const claims = this.decodeJwtUnverified(token);
351
+ if (claims?.[fieldName]) {
352
+ return { ledgerId: `${method} ${url} ${claims[fieldName]}`, hasLimit };
353
+ }
354
+ }
355
+ break;
298
356
 
299
- case "headers": {
300
- const headerValue = this.getHeaderValue(request.headers, fieldName);
301
- if (headerValue) {
302
- return `${method} ${url} ${headerValue}`;
357
+ case "headers": {
358
+ const headerValue = this.getHeaderValue(request.headers, fieldName);
359
+ if (headerValue) {
360
+ return { ledgerId: `${method} ${url} ${headerValue}`, hasLimit };
361
+ }
362
+ break;
303
363
  }
304
- break;
305
364
  }
306
365
  }
307
366
  }
308
367
 
309
- return `${method} ${url}`;
368
+ return { ledgerId: `${method} ${url}`, hasLimit: false };
310
369
  }
311
370
 
312
371
  private async fetchApiPolicies(): Promise<void> {
313
372
  if (this.socketManager && this.socketManager.isConnected()) {
314
- const response = await this.socketManager.sendAsync<{ policies: UsageFlowConfig[], total: number }>({
373
+ const response = await this.socketManager.sendAsync<{
374
+ policies: UsageFlowConfig[];
375
+ total: number;
376
+ }>({
315
377
  type: "get_application_policies",
316
378
  payload: null,
317
379
  });
318
- if (response.type === 'success') {
380
+ if (response.type === "success") {
319
381
  this.apiConfigs = response.payload?.policies || [];
320
382
  }
321
383
  }
322
384
  }
323
385
 
324
- async useAllocationRequest(
325
- payload: RequestForAllocation,
326
- ): Promise<void> {
386
+
387
+ private async fetchBlockedEndpoints(): Promise<void> {
327
388
  if (this.socketManager && this.socketManager.isConnected()) {
328
- this.socketManager.sendAsync<any>({
329
- type: "use_allocation",
330
- payload
331
- }).catch((error) => {
332
- console.error("[UsageFlow] Error sending finalization via WebSocket:", error);
333
- throw error;
389
+ const response = await this.socketManager.sendAsync<{
390
+ endpoints: BlockedEndpoint[];
391
+ total: number;
392
+ }>({
393
+ type: "get_blocked_endpoints",
394
+ payload: null,
334
395
  });
396
+ if (response.type === "success") {
397
+ response.payload?.endpoints || [];
398
+ this.blockedEndpoints = response.payload?.endpoints.map(({ method, url, identity }) => `${method} ${url} ${identity}`) || [];
399
+ }
335
400
  }
336
401
  }
337
402
 
403
+ async useAllocationRequest(payload: RequestForAllocation): Promise<void> {
404
+ if (this.socketManager && this.socketManager.isConnected()) {
405
+ this.socketManager
406
+ .sendAsync<any>({
407
+ type: "use_allocation",
408
+ payload,
409
+ })
410
+ .catch((error) => {
411
+ console.error(
412
+ "[UsageFlow] Error sending finalization via WebSocket:",
413
+ error,
414
+ );
415
+ throw error;
416
+ });
417
+ }
418
+ }
338
419
 
339
420
  async allocationRequest(
340
421
  request: UsageFlowRequest,
341
422
  payload: RequestForAllocation,
342
423
  metadata: RequestMetadata,
424
+ hasLimit: boolean,
343
425
  ): Promise<void> {
344
426
  if (this.socketManager && this.socketManager.isConnected()) {
345
427
  try {
346
- const allocationResponse = await this.socketManager.sendAsync<any>({
347
- type: "request_for_allocation",
348
- payload
349
- });
350
-
351
- if (allocationResponse.type === 'error') {
352
- throw new Error(allocationResponse.message || allocationResponse.error);
353
- }
354
- if (allocationResponse.type === 'success') {
355
- request.usageflow!.eventId = allocationResponse.payload.allocationId;
356
- request.usageflow!.metadata = metadata;
357
- return;
428
+ if (hasLimit) {
429
+ const allocationResponse = await this.socketManager.sendAsync<any>({
430
+ type: "request_for_allocation",
431
+ payload,
432
+ });
433
+
434
+ if (allocationResponse.type === "error") {
435
+ throw new Error(
436
+ allocationResponse.message || allocationResponse.error,
437
+ );
438
+ }
439
+ if (allocationResponse.type === "success") {
440
+ request.usageflow!.eventId = allocationResponse.payload.allocationId;
441
+ request.usageflow!.metadata = metadata;
442
+ return;
443
+ } else {
444
+ throw new Error(
445
+ allocationResponse.message || "Unknown error occurred",
446
+ );
447
+ }
358
448
  } else {
359
- throw new Error(allocationResponse.message || "Unknown error occurred");
449
+ payload.allocationId = randomUUID();
450
+ await this.socketManager.send({
451
+ type: "request_for_allocation",
452
+ payload,
453
+ });
454
+ request.usageflow!.eventId = payload.allocationId;
455
+ request.usageflow!.metadata = metadata;
360
456
  }
361
457
  } catch (error: any) {
362
- console.error("[UsageFlow] WebSocket allocation failed, falling back to HTTP:", error);
458
+ console.error(
459
+ "[UsageFlow] WebSocket allocation failed, falling back to HTTP:",
460
+ error,
461
+ );
363
462
  throw error;
364
463
  // Fall through to HTTP request
365
464
  }
@@ -468,8 +567,8 @@ export abstract class UsageFlowAPI {
468
567
  }
469
568
 
470
569
  // Check if it's a bearer token
471
- const parts = authHeader.split(' ');
472
- if (parts.length !== 2 || parts[0].toLowerCase() !== 'bearer') {
570
+ const parts = authHeader.split(" ");
571
+ if (parts.length !== 2 || parts[0].toLowerCase() !== "bearer") {
473
572
  return null;
474
573
  }
475
574
 
@@ -479,14 +578,19 @@ export abstract class UsageFlowAPI {
479
578
  /**
480
579
  * Get header value from headers object (handles both Record and Headers types)
481
580
  */
482
- private getHeaderValue(headers: Record<string, string | string[] | undefined> | Headers, key: string): string | null {
581
+ private getHeaderValue(
582
+ headers: Record<string, string | string[] | undefined> | Headers,
583
+ key: string,
584
+ ): string | null {
483
585
  // Check if it's a Headers object (from Fetch API) by checking for the 'get' method
484
- if (headers && typeof (headers as any).get === 'function') {
586
+ if (headers && typeof (headers as any).get === "function") {
485
587
  return (headers as any).get(key) || null;
486
588
  }
487
589
  // Otherwise, treat it as a Record
488
- const value = (headers as Record<string, string | string[] | undefined>)[key];
489
- if (typeof value === 'string') {
590
+ const value = (headers as Record<string, string | string[] | undefined>)[
591
+ key
592
+ ];
593
+ if (typeof value === "string") {
490
594
  return value;
491
595
  }
492
596
  if (Array.isArray(value) && value.length > 0) {
@@ -503,14 +607,14 @@ export abstract class UsageFlowAPI {
503
607
  public decodeJwtUnverified(token: string): Record<string, any> | null {
504
608
  try {
505
609
  // Split the token into parts
506
- const parts = token.split('.');
610
+ const parts = token.split(".");
507
611
  if (parts.length !== 3) {
508
612
  return null;
509
613
  }
510
614
 
511
615
  // Decode the payload (claims)
512
616
  const payload = parts[1];
513
- const decoded = Buffer.from(payload, 'base64').toString('utf-8');
617
+ const decoded = Buffer.from(payload, "base64").toString("utf-8");
514
618
  return JSON.parse(decoded);
515
619
  } catch (error) {
516
620
  return null;
package/src/types.ts CHANGED
@@ -62,6 +62,13 @@ export interface UsageFlowConfig {
62
62
  method: string;
63
63
  identityFieldName?: string;
64
64
  identityFieldLocation?: string;
65
+ hasRateLimit?: boolean;
66
+ }
67
+
68
+ export interface BlockedEndpoint {
69
+ method: string;
70
+ url: string;
71
+ identity: string
65
72
  }
66
73
 
67
74
  export interface UsageFlowResponse {
@@ -90,7 +97,7 @@ export interface RequestForAllocation {
90
97
  duration?: number;
91
98
  }
92
99
 
93
- export type MessageTypes = 'request_for_allocation' | 'use_allocation' | 'get_application_policies';
100
+ export type MessageTypes = 'request_for_allocation' | 'use_allocation' | 'get_application_policies' | 'get_blocked_endpoints';
94
101
 
95
102
  export interface UsageFlowSocketMessage {
96
103
  type: MessageTypes;