@usageflow/express 0.1.5 → 0.1.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/dist/plugin.d.ts +6 -13
- package/dist/plugin.js +29 -77
- package/dist/plugin.js.map +1 -1
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +139 -0
- package/dist/utils.js.map +1 -0
- package/package.json +10 -4
- package/src/plugin.ts +38 -102
package/dist/plugin.d.ts
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from "express";
|
|
2
|
-
import { UsageFlowAPI, Route
|
|
3
|
-
declare global {
|
|
4
|
-
namespace Express {
|
|
5
|
-
interface Request {
|
|
6
|
-
usageflow?: {
|
|
7
|
-
startTime: number;
|
|
8
|
-
eventId?: string;
|
|
9
|
-
metadata?: RequestMetadata;
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
}
|
|
2
|
+
import { UsageFlowAPI, Route } from "@usageflow/core";
|
|
14
3
|
export declare class ExpressUsageFlowAPI extends UsageFlowAPI {
|
|
4
|
+
/**
|
|
5
|
+
* Get the route pattern (e.g., /express/users/:id) from the request
|
|
6
|
+
* Tries multiple methods to get the route pattern since request.route
|
|
7
|
+
* may not be available in middleware that runs before route matching
|
|
8
|
+
*/
|
|
15
9
|
private collectRequestMetadata;
|
|
16
10
|
private executeRequestWithMetadata;
|
|
17
11
|
createMiddleware(routes: Route[], whitelistRoutes?: Route[]): (request: Request, response: Response, next: NextFunction) => Promise<void>;
|
|
18
|
-
protected guessLedgerId(request: Request): string;
|
|
19
12
|
}
|
package/dist/plugin.js
CHANGED
|
@@ -3,6 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ExpressUsageFlowAPI = void 0;
|
|
4
4
|
const core_1 = require("@usageflow/core");
|
|
5
5
|
class ExpressUsageFlowAPI extends core_1.UsageFlowAPI {
|
|
6
|
+
/**
|
|
7
|
+
* Get the route pattern (e.g., /express/users/:id) from the request
|
|
8
|
+
* Tries multiple methods to get the route pattern since request.route
|
|
9
|
+
* may not be available in middleware that runs before route matching
|
|
10
|
+
*/
|
|
6
11
|
async collectRequestMetadata(request) {
|
|
7
12
|
const headers = this.sanitizeHeaders(request.headers);
|
|
8
13
|
// Get client IP, handling forwarded headers
|
|
@@ -11,9 +16,20 @@ class ExpressUsageFlowAPI extends core_1.UsageFlowAPI {
|
|
|
11
16
|
if (forwardedFor && typeof forwardedFor === "string") {
|
|
12
17
|
clientIP = forwardedFor.split(",")[0].trim();
|
|
13
18
|
}
|
|
19
|
+
const routePattern = request.route?.path || request.app._router.stack.find((route) => {
|
|
20
|
+
// a => a.path == request.url
|
|
21
|
+
if (!route.route)
|
|
22
|
+
return false;
|
|
23
|
+
if (route.path) {
|
|
24
|
+
return route.path == request.url;
|
|
25
|
+
}
|
|
26
|
+
if (route.regexp) {
|
|
27
|
+
return route.regexp.test(request.url);
|
|
28
|
+
}
|
|
29
|
+
})?.route?.path || request.url;
|
|
14
30
|
const metadata = {
|
|
15
31
|
method: request.method,
|
|
16
|
-
url:
|
|
32
|
+
url: routePattern,
|
|
17
33
|
rawUrl: request.originalUrl || "/",
|
|
18
34
|
clientIP: clientIP || "unknown",
|
|
19
35
|
userAgent: request.headers["user-agent"],
|
|
@@ -29,40 +45,13 @@ class ExpressUsageFlowAPI extends core_1.UsageFlowAPI {
|
|
|
29
45
|
if (!this.apiKey) {
|
|
30
46
|
throw new Error("API key not initialized");
|
|
31
47
|
}
|
|
32
|
-
const headers = {
|
|
33
|
-
"x-usage-key": this.apiKey,
|
|
34
|
-
"Content-Type": "application/json",
|
|
35
|
-
};
|
|
36
48
|
const payload = {
|
|
37
49
|
alias: ledgerId,
|
|
38
50
|
amount: 1,
|
|
39
51
|
metadata,
|
|
52
|
+
duration: 1000
|
|
40
53
|
};
|
|
41
|
-
|
|
42
|
-
const response = await fetch(`${this.usageflowUrl}/api/v1/ledgers/measure/allocate`, {
|
|
43
|
-
method: "POST",
|
|
44
|
-
headers,
|
|
45
|
-
body: JSON.stringify(payload),
|
|
46
|
-
});
|
|
47
|
-
const data = await response.json();
|
|
48
|
-
if (response.status === 400) {
|
|
49
|
-
throw new Error(data.message);
|
|
50
|
-
}
|
|
51
|
-
if (response.ok) {
|
|
52
|
-
request.usageflow.eventId = data.eventId;
|
|
53
|
-
request.usageflow.metadata = metadata;
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
throw new Error(data.message || "Unknown error occurred");
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
catch (error) {
|
|
60
|
-
if (error.message ==
|
|
61
|
-
"Failed to use resource after retries: Faile to preform operation") {
|
|
62
|
-
throw new Error("Failed to allocate resource");
|
|
63
|
-
}
|
|
64
|
-
throw error;
|
|
65
|
-
}
|
|
54
|
+
await this.allocationRequest(request, payload, metadata);
|
|
66
55
|
}
|
|
67
56
|
createMiddleware(routes, whitelistRoutes = []) {
|
|
68
57
|
const routesMap = this.createRoutesMap(routes);
|
|
@@ -70,7 +59,8 @@ class ExpressUsageFlowAPI extends core_1.UsageFlowAPI {
|
|
|
70
59
|
const self = this;
|
|
71
60
|
return async (request, response, next) => {
|
|
72
61
|
const method = request.method;
|
|
73
|
-
const
|
|
62
|
+
const routePattern = self.getRoutePattern(request);
|
|
63
|
+
const url = routePattern;
|
|
74
64
|
request.usageflow = {
|
|
75
65
|
startTime: Date.now(),
|
|
76
66
|
};
|
|
@@ -81,6 +71,7 @@ class ExpressUsageFlowAPI extends core_1.UsageFlowAPI {
|
|
|
81
71
|
return next();
|
|
82
72
|
}
|
|
83
73
|
const metadata = await this.collectRequestMetadata(request);
|
|
74
|
+
metadata.url = url;
|
|
84
75
|
let ledgerId = this.guessLedgerId(request);
|
|
85
76
|
try {
|
|
86
77
|
await this.executeRequestWithMetadata(ledgerId, metadata, request, response);
|
|
@@ -138,15 +129,13 @@ class ExpressUsageFlowAPI extends core_1.UsageFlowAPI {
|
|
|
138
129
|
alias: ledgerId,
|
|
139
130
|
amount: 1,
|
|
140
131
|
allocationId: request.usageflow?.eventId,
|
|
141
|
-
metadata,
|
|
132
|
+
metadata: metadata,
|
|
142
133
|
};
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
body: JSON.stringify(payload),
|
|
147
|
-
}).catch((error) => {
|
|
148
|
-
console.error("Error finalizing request:", error);
|
|
134
|
+
self.useAllocationRequest(payload).catch((err) => {
|
|
135
|
+
console.error("[UsageFlow] Error finalizing allocation request:", err);
|
|
136
|
+
throw err;
|
|
149
137
|
});
|
|
138
|
+
// Send via WebSocket if connected
|
|
150
139
|
// Handle the different overloadsx
|
|
151
140
|
if (typeof chunk === "function") {
|
|
152
141
|
return originalEnd.call(this, undefined, "utf8", chunk);
|
|
@@ -159,7 +148,8 @@ class ExpressUsageFlowAPI extends core_1.UsageFlowAPI {
|
|
|
159
148
|
next();
|
|
160
149
|
}
|
|
161
150
|
catch (error) {
|
|
162
|
-
console.error("Error executing request with metadata:", error);
|
|
151
|
+
console.error("[UsageFlow] Error executing request with metadata:", error);
|
|
152
|
+
console.error("[UsageFlow] Error stack:", error?.stack);
|
|
163
153
|
response.status(400).json({
|
|
164
154
|
message: error.message,
|
|
165
155
|
blocked: true,
|
|
@@ -168,44 +158,6 @@ class ExpressUsageFlowAPI extends core_1.UsageFlowAPI {
|
|
|
168
158
|
}
|
|
169
159
|
};
|
|
170
160
|
}
|
|
171
|
-
guessLedgerId(request) {
|
|
172
|
-
const method = request.method;
|
|
173
|
-
const url = request.route?.path || request.path;
|
|
174
|
-
const config = this.apiConfig;
|
|
175
|
-
if (!config?.identityFieldName || !config?.identityFieldLocation) {
|
|
176
|
-
return `${method} ${url}`;
|
|
177
|
-
}
|
|
178
|
-
const fieldName = config.identityFieldName;
|
|
179
|
-
const location = config.identityFieldLocation;
|
|
180
|
-
switch (location) {
|
|
181
|
-
case "path_params":
|
|
182
|
-
if (request.params?.[fieldName]) {
|
|
183
|
-
return `${method} ${url} ${request.params[fieldName]}`;
|
|
184
|
-
}
|
|
185
|
-
break;
|
|
186
|
-
case "query_params":
|
|
187
|
-
if (request.query?.[fieldName]) {
|
|
188
|
-
return `${method} ${url} ${request.query[fieldName]}`;
|
|
189
|
-
}
|
|
190
|
-
break;
|
|
191
|
-
case "body":
|
|
192
|
-
if (request.body?.[fieldName]) {
|
|
193
|
-
return `${method} ${url} ${request.body[fieldName]}`;
|
|
194
|
-
}
|
|
195
|
-
break;
|
|
196
|
-
case "bearer_token":
|
|
197
|
-
const authHeader = request.headers.authorization;
|
|
198
|
-
const token = this.extractBearerToken(authHeader);
|
|
199
|
-
if (token) {
|
|
200
|
-
const claims = this.decodeJwtUnverified(token);
|
|
201
|
-
if (claims?.[fieldName]) {
|
|
202
|
-
return `${method} ${url} ${claims[fieldName]}`;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
break;
|
|
206
|
-
}
|
|
207
|
-
return `${method} ${url}`;
|
|
208
|
-
}
|
|
209
161
|
}
|
|
210
162
|
exports.ExpressUsageFlowAPI = ExpressUsageFlowAPI;
|
|
211
163
|
//# sourceMappingURL=plugin.js.map
|
package/dist/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":";;;AACA,
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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"}
|
package/dist/utils.d.ts
ADDED
package/dist/utils.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sendAsync = sendAsync;
|
|
4
|
+
function sendAsync(ws, payload) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
const id = Math.random().toString(36).slice(2);
|
|
7
|
+
const message = { id, ...payload };
|
|
8
|
+
function handleMessage(data) {
|
|
9
|
+
const response = JSON.parse(data.toString());
|
|
10
|
+
if (response.replyTo === id) {
|
|
11
|
+
ws.off('message', handleMessage);
|
|
12
|
+
resolve(response);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
ws.on('message', handleMessage);
|
|
16
|
+
ws.send(JSON.stringify(message), (err) => {
|
|
17
|
+
if (err)
|
|
18
|
+
reject(err);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
// export class UsageFlowSocketManger {
|
|
23
|
+
// private ws: WebSocket | null = null;
|
|
24
|
+
// private wsUrl: string = "ws://127.0.0.1:9000/ws";
|
|
25
|
+
// private wsConnected: boolean = false;
|
|
26
|
+
// constructor(private apiKey: string) {
|
|
27
|
+
// this.apiKey = apiKey;
|
|
28
|
+
// this.connect().catch((error) => {
|
|
29
|
+
// console.error("[UsageFlow] Failed to establish WebSocket connection:", error);
|
|
30
|
+
// });
|
|
31
|
+
// }
|
|
32
|
+
// async establishWebSocketConnection(): Promise<void> {
|
|
33
|
+
// if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
34
|
+
// this.wsConnected = true;
|
|
35
|
+
// return;
|
|
36
|
+
// }
|
|
37
|
+
// if (!this.apiKey) {
|
|
38
|
+
// console.warn("[UsageFlow] Cannot establish WebSocket connection: API key not initialized");
|
|
39
|
+
// return;
|
|
40
|
+
// }
|
|
41
|
+
// return new Promise((resolve, reject) => {
|
|
42
|
+
// try {
|
|
43
|
+
// console.log(`[UsageFlow] Establishing WebSocket connection to ${this.wsUrl}`);
|
|
44
|
+
// if (!this.apiKey) {
|
|
45
|
+
// reject(new Error("API key not available"));
|
|
46
|
+
// return;
|
|
47
|
+
// }
|
|
48
|
+
// const headers: Record<string, string> = {
|
|
49
|
+
// "x-usage-key": this.apiKey,
|
|
50
|
+
// };
|
|
51
|
+
// this.ws = new WebSocket(this.wsUrl, {
|
|
52
|
+
// headers,
|
|
53
|
+
// family: 4 // This forces IPv4
|
|
54
|
+
// });
|
|
55
|
+
// this.ws.on("open", () => {
|
|
56
|
+
// console.log("[UsageFlow] WebSocket connection established");
|
|
57
|
+
// this.wsConnected = true;
|
|
58
|
+
// resolve();
|
|
59
|
+
// });
|
|
60
|
+
// this.ws.on("error", (error: Error) => {
|
|
61
|
+
// console.error("[UsageFlow] WebSocket error:", error);
|
|
62
|
+
// this.wsConnected = false;
|
|
63
|
+
// reject(error);
|
|
64
|
+
// });
|
|
65
|
+
// this.ws.on("close", () => {
|
|
66
|
+
// console.log("[UsageFlow] WebSocket connection closed");
|
|
67
|
+
// this.wsConnected = false;
|
|
68
|
+
// // Attempt to reconnect after a delay
|
|
69
|
+
// setTimeout(() => {
|
|
70
|
+
// if (this.apiKey) {
|
|
71
|
+
// this.establishWebSocketConnection().catch(console.error);
|
|
72
|
+
// }
|
|
73
|
+
// }, 5000);
|
|
74
|
+
// });
|
|
75
|
+
// this.ws.on("message", (data: WebSocket.Data) => {
|
|
76
|
+
// console.log("[UsageFlow] WebSocket message received:", data.toString());
|
|
77
|
+
// });
|
|
78
|
+
// } catch (error) {
|
|
79
|
+
// console.error("[UsageFlow] Error creating WebSocket connection:", error);
|
|
80
|
+
// this.wsConnected = false;
|
|
81
|
+
// reject(error);
|
|
82
|
+
// }
|
|
83
|
+
// });
|
|
84
|
+
// }
|
|
85
|
+
// public async connect(): Promise<void> {
|
|
86
|
+
// this.ws = new WebSocket(this.wsUrl);
|
|
87
|
+
// this.ws.on("open", () => {
|
|
88
|
+
// this.wsConnected = true;
|
|
89
|
+
// });
|
|
90
|
+
// this.ws.on("error", (error: Error) => {
|
|
91
|
+
// this.wsConnected = false;
|
|
92
|
+
// throw error;
|
|
93
|
+
// });
|
|
94
|
+
// this.ws.on("close", () => {
|
|
95
|
+
// this.wsConnected = false;
|
|
96
|
+
// });
|
|
97
|
+
// }
|
|
98
|
+
// public async sendAsync<T>(payload: UsageFlowSocketMessage): Promise<T> {
|
|
99
|
+
// if (!this.ws || !this.wsConnected) {
|
|
100
|
+
// throw new Error("WebSocket not connected");
|
|
101
|
+
// }
|
|
102
|
+
// const socketResponse = await sendAsync(this.ws, payload);
|
|
103
|
+
// return socketResponse as T;
|
|
104
|
+
// }
|
|
105
|
+
// public send(payload: UsageFlowSocketMessage): void {
|
|
106
|
+
// if (!this.ws || !this.wsConnected) {
|
|
107
|
+
// throw new Error("WebSocket not connected");
|
|
108
|
+
// }
|
|
109
|
+
// this.ws.send(JSON.stringify(payload));
|
|
110
|
+
// }
|
|
111
|
+
// public close(): void {
|
|
112
|
+
// if (this.ws) {
|
|
113
|
+
// this.ws.close();
|
|
114
|
+
// }
|
|
115
|
+
// this.ws = null;
|
|
116
|
+
// this.wsConnected = false;
|
|
117
|
+
// }
|
|
118
|
+
// public isConnected(): boolean {
|
|
119
|
+
// return this.wsConnected;
|
|
120
|
+
// }
|
|
121
|
+
// public getWs(): WebSocket | null {
|
|
122
|
+
// return this.ws;
|
|
123
|
+
// }
|
|
124
|
+
// /**
|
|
125
|
+
// * Clean up resources including WebSocket connection
|
|
126
|
+
// */
|
|
127
|
+
// public destroy(): void {
|
|
128
|
+
// if (this.ws) {
|
|
129
|
+
// console.log("[UsageFlow] Closing WebSocket connection");
|
|
130
|
+
// this.ws.removeAllListeners();
|
|
131
|
+
// if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
|
|
132
|
+
// this.ws.close();
|
|
133
|
+
// }
|
|
134
|
+
// this.ws = null;
|
|
135
|
+
// this.wsConnected = false;
|
|
136
|
+
// }
|
|
137
|
+
// }
|
|
138
|
+
// }
|
|
139
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usageflow/express",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "UsageFlow plugin for Express applications",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc",
|
|
9
9
|
"test": "jest",
|
|
10
|
-
"prepare": "npm run build"
|
|
10
|
+
"prepare": "npm run build",
|
|
11
|
+
"prepublishOnly": "npm run build"
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
11
15
|
},
|
|
12
16
|
"dependencies": {
|
|
13
|
-
"@usageflow/core": "^0.1.
|
|
14
|
-
"express": "^4.18.0"
|
|
17
|
+
"@usageflow/core": "^0.1.5",
|
|
18
|
+
"express": "^4.18.0",
|
|
19
|
+
"ws": "^8.16.0"
|
|
15
20
|
},
|
|
16
21
|
"homepage": "https://usageflow.io",
|
|
17
22
|
"repository": {
|
|
@@ -22,6 +27,7 @@
|
|
|
22
27
|
"devDependencies": {
|
|
23
28
|
"@types/express": "^4.17.0",
|
|
24
29
|
"@types/node": "^20.0.0",
|
|
30
|
+
"@types/ws": "^8.5.10",
|
|
25
31
|
"typescript": "^5.0.0",
|
|
26
32
|
"jest": "^29.0.0",
|
|
27
33
|
"@types/jest": "^29.0.0",
|
package/src/plugin.ts
CHANGED
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from "express";
|
|
2
|
-
import { UsageFlowAPI, Route, RequestMetadata } from "@usageflow/core";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
namespace Express {
|
|
6
|
-
interface Request {
|
|
7
|
-
usageflow?: {
|
|
8
|
-
startTime: number;
|
|
9
|
-
eventId?: string;
|
|
10
|
-
metadata?: RequestMetadata;
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
}
|
|
2
|
+
import { UsageFlowAPI, Route, RequestMetadata, RequestForAllocation, UsageFlowRequest } from "@usageflow/core";
|
|
3
|
+
|
|
4
|
+
|
|
15
5
|
|
|
16
6
|
export class ExpressUsageFlowAPI extends UsageFlowAPI {
|
|
7
|
+
/**
|
|
8
|
+
* Get the route pattern (e.g., /express/users/:id) from the request
|
|
9
|
+
* Tries multiple methods to get the route pattern since request.route
|
|
10
|
+
* may not be available in middleware that runs before route matching
|
|
11
|
+
*/
|
|
12
|
+
|
|
17
13
|
private async collectRequestMetadata(
|
|
18
14
|
request: Request,
|
|
19
15
|
): Promise<RequestMetadata> {
|
|
@@ -28,9 +24,22 @@ export class ExpressUsageFlowAPI extends UsageFlowAPI {
|
|
|
28
24
|
clientIP = forwardedFor.split(",")[0].trim();
|
|
29
25
|
}
|
|
30
26
|
|
|
27
|
+
const routePattern = request.route?.path || request.app._router.stack.find((route: any) => {
|
|
28
|
+
// a => a.path == request.url
|
|
29
|
+
if (!route.route) return false;
|
|
30
|
+
if (route.path) {
|
|
31
|
+
return route.path == request.url;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (route.regexp) {
|
|
35
|
+
return route.regexp.test(request.url);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
})?.route?.path || request.url;
|
|
39
|
+
|
|
31
40
|
const metadata: RequestMetadata = {
|
|
32
41
|
method: request.method,
|
|
33
|
-
url:
|
|
42
|
+
url: routePattern,
|
|
34
43
|
rawUrl: request.originalUrl || "/",
|
|
35
44
|
clientIP: clientIP || "unknown",
|
|
36
45
|
userAgent: request.headers["user-agent"] as string,
|
|
@@ -54,48 +63,14 @@ export class ExpressUsageFlowAPI extends UsageFlowAPI {
|
|
|
54
63
|
throw new Error("API key not initialized");
|
|
55
64
|
}
|
|
56
65
|
|
|
57
|
-
const headers = {
|
|
58
|
-
"x-usage-key": this.apiKey,
|
|
59
|
-
"Content-Type": "application/json",
|
|
60
|
-
};
|
|
61
|
-
|
|
62
66
|
const payload = {
|
|
63
67
|
alias: ledgerId,
|
|
64
68
|
amount: 1,
|
|
65
69
|
metadata,
|
|
70
|
+
duration: 1000
|
|
66
71
|
};
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
const response = await fetch(
|
|
70
|
-
`${this.usageflowUrl}/api/v1/ledgers/measure/allocate`,
|
|
71
|
-
{
|
|
72
|
-
method: "POST",
|
|
73
|
-
headers,
|
|
74
|
-
body: JSON.stringify(payload),
|
|
75
|
-
},
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
const data = await response.json();
|
|
79
|
-
|
|
80
|
-
if (response.status === 400) {
|
|
81
|
-
throw new Error(data.message);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (response.ok) {
|
|
85
|
-
request.usageflow!.eventId = data.eventId;
|
|
86
|
-
request.usageflow!.metadata = metadata;
|
|
87
|
-
} else {
|
|
88
|
-
throw new Error(data.message || "Unknown error occurred");
|
|
89
|
-
}
|
|
90
|
-
} catch (error: any) {
|
|
91
|
-
if (
|
|
92
|
-
error.message ==
|
|
93
|
-
"Failed to use resource after retries: Faile to preform operation"
|
|
94
|
-
) {
|
|
95
|
-
throw new Error("Failed to allocate resource");
|
|
96
|
-
}
|
|
97
|
-
throw error;
|
|
98
|
-
}
|
|
73
|
+
await this.allocationRequest(request as unknown as UsageFlowRequest, payload, metadata);
|
|
99
74
|
}
|
|
100
75
|
|
|
101
76
|
public createMiddleware(routes: Route[], whitelistRoutes: Route[] = []) {
|
|
@@ -103,9 +78,11 @@ export class ExpressUsageFlowAPI extends UsageFlowAPI {
|
|
|
103
78
|
const whitelistMap = this.createRoutesMap(whitelistRoutes);
|
|
104
79
|
const self = this;
|
|
105
80
|
|
|
81
|
+
|
|
106
82
|
return async (request: Request, response: Response, next: NextFunction) => {
|
|
107
83
|
const method = request.method;
|
|
108
|
-
const
|
|
84
|
+
const routePattern = self.getRoutePattern(request);
|
|
85
|
+
const url = routePattern;
|
|
109
86
|
|
|
110
87
|
request.usageflow = {
|
|
111
88
|
startTime: Date.now(),
|
|
@@ -120,6 +97,7 @@ export class ExpressUsageFlowAPI extends UsageFlowAPI {
|
|
|
120
97
|
}
|
|
121
98
|
|
|
122
99
|
const metadata = await this.collectRequestMetadata(request);
|
|
100
|
+
metadata.url = url;
|
|
123
101
|
let ledgerId = this.guessLedgerId(request);
|
|
124
102
|
|
|
125
103
|
try {
|
|
@@ -186,20 +164,19 @@ export class ExpressUsageFlowAPI extends UsageFlowAPI {
|
|
|
186
164
|
Date.now() - request.usageflow.startTime;
|
|
187
165
|
}
|
|
188
166
|
|
|
189
|
-
const payload = {
|
|
167
|
+
const payload: RequestForAllocation = {
|
|
190
168
|
alias: ledgerId,
|
|
191
169
|
amount: 1,
|
|
192
170
|
allocationId: request.usageflow?.eventId,
|
|
193
|
-
metadata,
|
|
171
|
+
metadata: metadata as RequestMetadata,
|
|
194
172
|
};
|
|
195
173
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
body: JSON.stringify(payload),
|
|
200
|
-
}).catch((error) => {
|
|
201
|
-
console.error("Error finalizing request:", error);
|
|
174
|
+
self.useAllocationRequest(payload).catch((err: any) => {
|
|
175
|
+
console.error("[UsageFlow] Error finalizing allocation request:", err);
|
|
176
|
+
throw err;
|
|
202
177
|
});
|
|
178
|
+
// Send via WebSocket if connected
|
|
179
|
+
|
|
203
180
|
|
|
204
181
|
// Handle the different overloadsx
|
|
205
182
|
if (typeof chunk === "function") {
|
|
@@ -213,7 +190,8 @@ export class ExpressUsageFlowAPI extends UsageFlowAPI {
|
|
|
213
190
|
|
|
214
191
|
next();
|
|
215
192
|
} catch (error: any) {
|
|
216
|
-
console.error("Error executing request with metadata:", error);
|
|
193
|
+
console.error("[UsageFlow] Error executing request with metadata:", error);
|
|
194
|
+
console.error("[UsageFlow] Error stack:", error?.stack);
|
|
217
195
|
response.status(400).json({
|
|
218
196
|
message: error.message,
|
|
219
197
|
blocked: true,
|
|
@@ -223,46 +201,4 @@ export class ExpressUsageFlowAPI extends UsageFlowAPI {
|
|
|
223
201
|
};
|
|
224
202
|
}
|
|
225
203
|
|
|
226
|
-
protected guessLedgerId(request: Request): string {
|
|
227
|
-
const method = request.method;
|
|
228
|
-
const url = request.route?.path || request.path;
|
|
229
|
-
const config = this.apiConfig
|
|
230
|
-
|
|
231
|
-
if (!config?.identityFieldName || !config?.identityFieldLocation) {
|
|
232
|
-
return `${method} ${url}`;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const fieldName = config.identityFieldName;
|
|
236
|
-
const location = config.identityFieldLocation;
|
|
237
|
-
|
|
238
|
-
switch (location) {
|
|
239
|
-
case "path_params":
|
|
240
|
-
if (request.params?.[fieldName]) {
|
|
241
|
-
return `${method} ${url} ${request.params[fieldName]}`;
|
|
242
|
-
}
|
|
243
|
-
break;
|
|
244
|
-
case "query_params":
|
|
245
|
-
if (request.query?.[fieldName]) {
|
|
246
|
-
return `${method} ${url} ${request.query[fieldName]}`;
|
|
247
|
-
}
|
|
248
|
-
break;
|
|
249
|
-
case "body":
|
|
250
|
-
if (request.body?.[fieldName]) {
|
|
251
|
-
return `${method} ${url} ${request.body[fieldName]}`;
|
|
252
|
-
}
|
|
253
|
-
break;
|
|
254
|
-
case "bearer_token":
|
|
255
|
-
const authHeader = request.headers.authorization;
|
|
256
|
-
const token = this.extractBearerToken(authHeader);
|
|
257
|
-
if (token) {
|
|
258
|
-
const claims = this.decodeJwtUnverified(token);
|
|
259
|
-
if (claims?.[fieldName]) {
|
|
260
|
-
return `${method} ${url} ${claims[fieldName]}`;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
break;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return `${method} ${url}`;
|
|
267
|
-
}
|
|
268
204
|
}
|