@usageflow/express 0.1.1 → 0.1.4
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 +12 -12
- package/dist/plugin.d.ts +3 -2
- package/dist/plugin.js +86 -44
- package/dist/plugin.js.map +1 -1
- package/package.json +8 -2
- package/src/plugin.ts +120 -58
package/README.md
CHANGED
|
@@ -13,50 +13,50 @@ npm install @usageflow/express
|
|
|
13
13
|
## Quick Start
|
|
14
14
|
|
|
15
15
|
```javascript
|
|
16
|
-
const express = require(
|
|
17
|
-
const { ExpressUsageFlowAPI } = require(
|
|
16
|
+
const express = require("express");
|
|
17
|
+
const { ExpressUsageFlowAPI } = require("@usageflow/express");
|
|
18
18
|
|
|
19
19
|
const app = express();
|
|
20
20
|
app.use(express.json());
|
|
21
21
|
|
|
22
22
|
// Initialize UsageFlow
|
|
23
23
|
const usageFlow = new ExpressUsageFlowAPI();
|
|
24
|
-
usageFlow.init(
|
|
24
|
+
usageFlow.init("YOUR_API_KEY");
|
|
25
25
|
|
|
26
26
|
// Create middleware
|
|
27
27
|
const middleware = usageFlow.createMiddleware(
|
|
28
28
|
[
|
|
29
|
-
{ method:
|
|
29
|
+
{ method: "*", url: "*" }, // Track all routes
|
|
30
30
|
],
|
|
31
31
|
[
|
|
32
|
-
{ method:
|
|
33
|
-
]
|
|
32
|
+
{ method: "GET", url: "/api/health" }, // Whitelist health check
|
|
33
|
+
],
|
|
34
34
|
);
|
|
35
35
|
|
|
36
36
|
// Apply middleware
|
|
37
37
|
app.use(middleware);
|
|
38
38
|
|
|
39
39
|
// Your routes
|
|
40
|
-
app.get(
|
|
41
|
-
res.json({ users: [
|
|
40
|
+
app.get("/api/users", (req, res) => {
|
|
41
|
+
res.json({ users: ["John", "Jane"] });
|
|
42
42
|
});
|
|
43
43
|
|
|
44
44
|
app.listen(3000, () => {
|
|
45
|
-
console.log(
|
|
45
|
+
console.log("Server running on port 3000");
|
|
46
46
|
});
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
## TypeScript Support
|
|
50
50
|
|
|
51
51
|
```typescript
|
|
52
|
-
import express from
|
|
53
|
-
import { ExpressUsageFlowAPI } from
|
|
52
|
+
import express from "express";
|
|
53
|
+
import { ExpressUsageFlowAPI } from "@usageflow/express";
|
|
54
54
|
|
|
55
55
|
const app = express();
|
|
56
56
|
app.use(express.json());
|
|
57
57
|
|
|
58
58
|
const usageFlow = new ExpressUsageFlowAPI();
|
|
59
|
-
usageFlow.init(
|
|
59
|
+
usageFlow.init("YOUR_API_KEY");
|
|
60
60
|
|
|
61
61
|
// Rest of the code remains the same
|
|
62
62
|
```
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Request, Response, NextFunction } from
|
|
2
|
-
import { UsageFlowAPI, Route, RequestMetadata } from
|
|
1
|
+
import { Request, Response, NextFunction } from "express";
|
|
2
|
+
import { UsageFlowAPI, Route, RequestMetadata } from "@usageflow/core";
|
|
3
3
|
declare global {
|
|
4
4
|
namespace Express {
|
|
5
5
|
interface Request {
|
|
@@ -15,4 +15,5 @@ export declare class ExpressUsageFlowAPI extends UsageFlowAPI {
|
|
|
15
15
|
private collectRequestMetadata;
|
|
16
16
|
private executeRequestWithMetadata;
|
|
17
17
|
createMiddleware(routes: Route[], whitelistRoutes?: Route[]): (request: Request, response: Response, next: NextFunction) => Promise<void>;
|
|
18
|
+
protected guessLedgerId(request: Request): string;
|
|
18
19
|
}
|
package/dist/plugin.js
CHANGED
|
@@ -7,42 +7,42 @@ class ExpressUsageFlowAPI extends core_1.UsageFlowAPI {
|
|
|
7
7
|
const headers = this.sanitizeHeaders(request.headers);
|
|
8
8
|
// Get client IP, handling forwarded headers
|
|
9
9
|
let clientIP = request.ip;
|
|
10
|
-
const forwardedFor = request.headers[
|
|
11
|
-
if (forwardedFor && typeof forwardedFor ===
|
|
12
|
-
clientIP = forwardedFor.split(
|
|
10
|
+
const forwardedFor = request.headers["x-forwarded-for"];
|
|
11
|
+
if (forwardedFor && typeof forwardedFor === "string") {
|
|
12
|
+
clientIP = forwardedFor.split(",")[0].trim();
|
|
13
13
|
}
|
|
14
14
|
const metadata = {
|
|
15
15
|
method: request.method,
|
|
16
|
-
url: request.route?.path || request.path || request.url ||
|
|
17
|
-
rawUrl: request.originalUrl ||
|
|
18
|
-
clientIP:
|
|
19
|
-
userAgent: request.headers[
|
|
16
|
+
url: request.route?.path || request.path || request.url || "/",
|
|
17
|
+
rawUrl: request.originalUrl || "/",
|
|
18
|
+
clientIP: clientIP || "unknown",
|
|
19
|
+
userAgent: request.headers["user-agent"],
|
|
20
20
|
timestamp: new Date().toISOString(),
|
|
21
21
|
headers,
|
|
22
|
-
queryParams: request.query,
|
|
23
|
-
pathParams: request.params,
|
|
24
|
-
body: request.body
|
|
22
|
+
queryParams: Object.keys(request.query).length > 0 ? request.query : null,
|
|
23
|
+
pathParams: Object.keys(request.params).length > 0 ? request.params : null,
|
|
24
|
+
body: request.body,
|
|
25
25
|
};
|
|
26
26
|
return metadata;
|
|
27
27
|
}
|
|
28
28
|
async executeRequestWithMetadata(ledgerId, metadata, request, response) {
|
|
29
29
|
if (!this.apiKey) {
|
|
30
|
-
throw new Error(
|
|
30
|
+
throw new Error("API key not initialized");
|
|
31
31
|
}
|
|
32
32
|
const headers = {
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
"x-usage-key": this.apiKey,
|
|
34
|
+
"Content-Type": "application/json",
|
|
35
35
|
};
|
|
36
36
|
const payload = {
|
|
37
37
|
alias: ledgerId,
|
|
38
38
|
amount: 1,
|
|
39
|
-
metadata
|
|
39
|
+
metadata,
|
|
40
40
|
};
|
|
41
41
|
try {
|
|
42
42
|
const response = await fetch(`${this.usageflowUrl}/api/v1/ledgers/measure/allocate`, {
|
|
43
|
-
method:
|
|
43
|
+
method: "POST",
|
|
44
44
|
headers,
|
|
45
|
-
body: JSON.stringify(payload)
|
|
45
|
+
body: JSON.stringify(payload),
|
|
46
46
|
});
|
|
47
47
|
const data = await response.json();
|
|
48
48
|
if (response.status === 400) {
|
|
@@ -53,12 +53,13 @@ class ExpressUsageFlowAPI extends core_1.UsageFlowAPI {
|
|
|
53
53
|
request.usageflow.metadata = metadata;
|
|
54
54
|
}
|
|
55
55
|
else {
|
|
56
|
-
throw new Error(data.message ||
|
|
56
|
+
throw new Error(data.message || "Unknown error occurred");
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
catch (error) {
|
|
60
|
-
if (error.message ==
|
|
61
|
-
|
|
60
|
+
if (error.message ==
|
|
61
|
+
"Failed to use resource after retries: Faile to preform operation") {
|
|
62
|
+
throw new Error("Failed to allocate resource");
|
|
62
63
|
}
|
|
63
64
|
throw error;
|
|
64
65
|
}
|
|
@@ -71,12 +72,12 @@ class ExpressUsageFlowAPI extends core_1.UsageFlowAPI {
|
|
|
71
72
|
const method = request.method;
|
|
72
73
|
const url = request.route?.path || request.path || request.url;
|
|
73
74
|
request.usageflow = {
|
|
74
|
-
startTime: Date.now()
|
|
75
|
+
startTime: Date.now(),
|
|
75
76
|
};
|
|
76
|
-
if (this.shouldSkipRoute(method, url ||
|
|
77
|
+
if (this.shouldSkipRoute(method, url || "", whitelistMap)) {
|
|
77
78
|
return next();
|
|
78
79
|
}
|
|
79
|
-
if (!this.shouldMonitorRoute(method, url ||
|
|
80
|
+
if (!this.shouldMonitorRoute(method, url || "", routesMap)) {
|
|
80
81
|
return next();
|
|
81
82
|
}
|
|
82
83
|
const metadata = await this.collectRequestMetadata(request);
|
|
@@ -86,14 +87,14 @@ class ExpressUsageFlowAPI extends core_1.UsageFlowAPI {
|
|
|
86
87
|
const originalEnd = response.end;
|
|
87
88
|
response.end = function (chunk, encoding, cb) {
|
|
88
89
|
if (!request.usageflow?.eventId) {
|
|
89
|
-
return originalEnd.call(this, chunk, encoding ||
|
|
90
|
+
return originalEnd.call(this, chunk, encoding || "utf8", cb);
|
|
90
91
|
}
|
|
91
92
|
const metadata = request.usageflow?.metadata || {};
|
|
92
93
|
metadata.responseStatusCode = response.statusCode;
|
|
93
94
|
// Add response body to metadata if it exists
|
|
94
95
|
if (chunk) {
|
|
95
96
|
try {
|
|
96
|
-
if (typeof chunk ===
|
|
97
|
+
if (typeof chunk === "string") {
|
|
97
98
|
// Try to parse as JSON if it's a string
|
|
98
99
|
try {
|
|
99
100
|
const parsed = JSON.parse(chunk);
|
|
@@ -105,7 +106,7 @@ class ExpressUsageFlowAPI extends core_1.UsageFlowAPI {
|
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
else if (Buffer.isBuffer(chunk)) {
|
|
108
|
-
const str = chunk.toString(
|
|
109
|
+
const str = chunk.toString("utf8");
|
|
109
110
|
// Try to parse as JSON if it's a buffer
|
|
110
111
|
try {
|
|
111
112
|
const parsed = JSON.parse(str);
|
|
@@ -116,53 +117,94 @@ class ExpressUsageFlowAPI extends core_1.UsageFlowAPI {
|
|
|
116
117
|
metadata.body = str;
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
|
-
else if (typeof chunk ===
|
|
120
|
+
else if (typeof chunk === "object") {
|
|
120
121
|
metadata.body = chunk;
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
124
|
catch (error) {
|
|
124
|
-
console.error(
|
|
125
|
+
console.error("Error parsing response body:", error);
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
128
|
const headers = {
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
"x-usage-key": self.apiKey,
|
|
130
|
+
"Content-Type": "application/json",
|
|
130
131
|
};
|
|
132
|
+
if (request.usageflow?.startTime) {
|
|
133
|
+
metadata.requestDuration =
|
|
134
|
+
Date.now() - request.usageflow.startTime;
|
|
135
|
+
}
|
|
131
136
|
const payload = {
|
|
132
|
-
alias: `${request.method} ${request.route?.path || request.path || request.url ||
|
|
137
|
+
alias: `${request.method} ${request.route?.path || request.path || request.url || "/"}`,
|
|
133
138
|
amount: 1,
|
|
134
139
|
allocationId: request.usageflow?.eventId,
|
|
135
|
-
metadata
|
|
140
|
+
metadata,
|
|
136
141
|
};
|
|
137
142
|
fetch(`${self.usageflowUrl}/api/v1/ledgers/measure/allocate/use`, {
|
|
138
|
-
method:
|
|
143
|
+
method: "POST",
|
|
139
144
|
headers,
|
|
140
|
-
body: JSON.stringify(payload)
|
|
141
|
-
}).catch(error => {
|
|
142
|
-
console.error(
|
|
145
|
+
body: JSON.stringify(payload),
|
|
146
|
+
}).catch((error) => {
|
|
147
|
+
console.error("Error finalizing request:", error);
|
|
143
148
|
});
|
|
144
|
-
// Handle the different
|
|
145
|
-
if (typeof chunk ===
|
|
146
|
-
return originalEnd.call(this, undefined,
|
|
149
|
+
// Handle the different overloadsx
|
|
150
|
+
if (typeof chunk === "function") {
|
|
151
|
+
return originalEnd.call(this, undefined, "utf8", chunk);
|
|
147
152
|
}
|
|
148
|
-
if (typeof encoding ===
|
|
149
|
-
return originalEnd.call(this, chunk,
|
|
153
|
+
if (typeof encoding === "function") {
|
|
154
|
+
return originalEnd.call(this, chunk, "utf8", encoding);
|
|
150
155
|
}
|
|
151
|
-
return originalEnd.call(this, chunk, encoding ||
|
|
156
|
+
return originalEnd.call(this, chunk, encoding || "utf8", cb);
|
|
152
157
|
};
|
|
153
158
|
next();
|
|
154
159
|
}
|
|
155
160
|
catch (error) {
|
|
156
|
-
console.error(
|
|
161
|
+
console.error("Error executing request with metadata:", error);
|
|
157
162
|
response.status(400).json({
|
|
158
163
|
message: error.message,
|
|
159
|
-
blocked: true
|
|
164
|
+
blocked: true,
|
|
160
165
|
});
|
|
161
166
|
return;
|
|
162
167
|
}
|
|
163
168
|
};
|
|
164
169
|
}
|
|
170
|
+
guessLedgerId(request) {
|
|
171
|
+
const method = request.method;
|
|
172
|
+
const url = request.route?.path || request.path;
|
|
173
|
+
const config = this.apiConfig;
|
|
174
|
+
if (!config?.identityFieldName || !config?.identityFieldLocation) {
|
|
175
|
+
return `${method} ${url}`;
|
|
176
|
+
}
|
|
177
|
+
const fieldName = config.identityFieldName;
|
|
178
|
+
const location = config.identityFieldLocation;
|
|
179
|
+
switch (location) {
|
|
180
|
+
case "path_params":
|
|
181
|
+
if (request.params?.[fieldName]) {
|
|
182
|
+
return `${method} ${url} ${request.params[fieldName]}`;
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
case "query_params":
|
|
186
|
+
if (request.query?.[fieldName]) {
|
|
187
|
+
return `${method} ${url} ${request.query[fieldName]}`;
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
190
|
+
case "body":
|
|
191
|
+
if (request.body?.[fieldName]) {
|
|
192
|
+
return `${method} ${url} ${request.body[fieldName]}`;
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
case "bearer_token":
|
|
196
|
+
const authHeader = request.headers.authorization;
|
|
197
|
+
const token = this.extractBearerToken(authHeader);
|
|
198
|
+
if (token) {
|
|
199
|
+
const claims = this.decodeJwtUnverified(token);
|
|
200
|
+
if (claims?.[fieldName]) {
|
|
201
|
+
return `${method} ${url} ${claims[fieldName]}`;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
return `${method} ${url}`;
|
|
207
|
+
}
|
|
165
208
|
}
|
|
166
209
|
exports.ExpressUsageFlowAPI = ExpressUsageFlowAPI;
|
|
167
|
-
;
|
|
168
210
|
//# 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,0CAAuE;AAcvE,MAAa,mBAAoB,SAAQ,mBAAY;IACzC,KAAK,CAAC,sBAAsB,
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usageflow/express",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "UsageFlow plugin for Express applications",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -10,9 +10,15 @@
|
|
|
10
10
|
"prepare": "npm run build"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@usageflow/core": "^0.1.
|
|
13
|
+
"@usageflow/core": "^0.1.3",
|
|
14
14
|
"express": "^4.18.0"
|
|
15
15
|
},
|
|
16
|
+
"homepage": "https://usageflow.io",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/usageflow/js-mongorepo.git",
|
|
20
|
+
"directory": "packages/express"
|
|
21
|
+
},
|
|
16
22
|
"devDependencies": {
|
|
17
23
|
"@types/express": "^4.17.0",
|
|
18
24
|
"@types/node": "^20.0.0",
|
package/src/plugin.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Request, Response, NextFunction } from
|
|
2
|
-
import { UsageFlowAPI, Route, RequestMetadata } from
|
|
1
|
+
import { Request, Response, NextFunction } from "express";
|
|
2
|
+
import { UsageFlowAPI, Route, RequestMetadata } from "@usageflow/core";
|
|
3
3
|
|
|
4
4
|
declare global {
|
|
5
5
|
namespace Express {
|
|
@@ -14,27 +14,31 @@ declare global {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export class ExpressUsageFlowAPI extends UsageFlowAPI {
|
|
17
|
-
private async collectRequestMetadata(
|
|
18
|
-
|
|
17
|
+
private async collectRequestMetadata(
|
|
18
|
+
request: Request,
|
|
19
|
+
): Promise<RequestMetadata> {
|
|
20
|
+
const headers = this.sanitizeHeaders(
|
|
21
|
+
request.headers as Record<string, string>,
|
|
22
|
+
);
|
|
19
23
|
|
|
20
24
|
// Get client IP, handling forwarded headers
|
|
21
25
|
let clientIP = request.ip;
|
|
22
|
-
const forwardedFor = request.headers[
|
|
23
|
-
if (forwardedFor && typeof forwardedFor ===
|
|
24
|
-
clientIP = forwardedFor.split(
|
|
26
|
+
const forwardedFor = request.headers["x-forwarded-for"];
|
|
27
|
+
if (forwardedFor && typeof forwardedFor === "string") {
|
|
28
|
+
clientIP = forwardedFor.split(",")[0].trim();
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
const metadata: RequestMetadata = {
|
|
28
32
|
method: request.method,
|
|
29
|
-
url: request.route?.path || request.path || request.url ||
|
|
30
|
-
rawUrl: request.originalUrl ||
|
|
31
|
-
clientIP:
|
|
32
|
-
userAgent: request.headers[
|
|
33
|
+
url: request.route?.path || request.path || request.url || "/",
|
|
34
|
+
rawUrl: request.originalUrl || "/",
|
|
35
|
+
clientIP: clientIP || "unknown",
|
|
36
|
+
userAgent: request.headers["user-agent"] as string,
|
|
33
37
|
timestamp: new Date().toISOString(),
|
|
34
38
|
headers,
|
|
35
|
-
queryParams: request.query
|
|
36
|
-
pathParams: request.params,
|
|
37
|
-
body: request.body
|
|
39
|
+
queryParams: Object.keys(request.query).length > 0 ? request.query : null,
|
|
40
|
+
pathParams: Object.keys(request.params).length > 0 ? request.params : null,
|
|
41
|
+
body: request.body,
|
|
38
42
|
};
|
|
39
43
|
|
|
40
44
|
return metadata;
|
|
@@ -44,29 +48,32 @@ export class ExpressUsageFlowAPI extends UsageFlowAPI {
|
|
|
44
48
|
ledgerId: string,
|
|
45
49
|
metadata: RequestMetadata,
|
|
46
50
|
request: Request,
|
|
47
|
-
response: Response
|
|
51
|
+
response: Response,
|
|
48
52
|
): Promise<void> {
|
|
49
53
|
if (!this.apiKey) {
|
|
50
|
-
throw new Error(
|
|
54
|
+
throw new Error("API key not initialized");
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
const headers = {
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
"x-usage-key": this.apiKey,
|
|
59
|
+
"Content-Type": "application/json",
|
|
56
60
|
};
|
|
57
61
|
|
|
58
62
|
const payload = {
|
|
59
63
|
alias: ledgerId,
|
|
60
64
|
amount: 1,
|
|
61
|
-
metadata
|
|
65
|
+
metadata,
|
|
62
66
|
};
|
|
63
67
|
|
|
64
68
|
try {
|
|
65
|
-
const response = await fetch(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
+
);
|
|
70
77
|
|
|
71
78
|
const data = await response.json();
|
|
72
79
|
|
|
@@ -78,11 +85,14 @@ export class ExpressUsageFlowAPI extends UsageFlowAPI {
|
|
|
78
85
|
request.usageflow!.eventId = data.eventId;
|
|
79
86
|
request.usageflow!.metadata = metadata;
|
|
80
87
|
} else {
|
|
81
|
-
throw new Error(data.message ||
|
|
88
|
+
throw new Error(data.message || "Unknown error occurred");
|
|
82
89
|
}
|
|
83
90
|
} catch (error: any) {
|
|
84
|
-
if (
|
|
85
|
-
|
|
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");
|
|
86
96
|
}
|
|
87
97
|
throw error;
|
|
88
98
|
}
|
|
@@ -98,14 +108,14 @@ export class ExpressUsageFlowAPI extends UsageFlowAPI {
|
|
|
98
108
|
const url = request.route?.path || request.path || request.url;
|
|
99
109
|
|
|
100
110
|
request.usageflow = {
|
|
101
|
-
startTime: Date.now()
|
|
111
|
+
startTime: Date.now(),
|
|
102
112
|
};
|
|
103
113
|
|
|
104
|
-
if (this.shouldSkipRoute(method, url ||
|
|
114
|
+
if (this.shouldSkipRoute(method, url || "", whitelistMap)) {
|
|
105
115
|
return next();
|
|
106
116
|
}
|
|
107
117
|
|
|
108
|
-
if (!this.shouldMonitorRoute(method, url ||
|
|
118
|
+
if (!this.shouldMonitorRoute(method, url || "", routesMap)) {
|
|
109
119
|
return next();
|
|
110
120
|
}
|
|
111
121
|
|
|
@@ -116,23 +126,29 @@ export class ExpressUsageFlowAPI extends UsageFlowAPI {
|
|
|
116
126
|
`${method} ${url}`,
|
|
117
127
|
metadata,
|
|
118
128
|
request,
|
|
119
|
-
response
|
|
129
|
+
response,
|
|
120
130
|
);
|
|
121
131
|
|
|
122
132
|
// Capture response data
|
|
123
133
|
const originalEnd = response.end;
|
|
124
|
-
response.end = function (
|
|
134
|
+
response.end = function (
|
|
135
|
+
this: Response,
|
|
136
|
+
chunk?: any,
|
|
137
|
+
encoding?: BufferEncoding,
|
|
138
|
+
cb?: () => void,
|
|
139
|
+
) {
|
|
125
140
|
if (!request.usageflow?.eventId) {
|
|
126
|
-
return originalEnd.call(this, chunk, encoding ||
|
|
141
|
+
return originalEnd.call(this, chunk, encoding || "utf8", cb);
|
|
127
142
|
}
|
|
128
143
|
|
|
129
|
-
const metadata
|
|
130
|
-
|
|
144
|
+
const metadata: Record<string, any> =
|
|
145
|
+
request.usageflow?.metadata || {};
|
|
146
|
+
metadata.responseStatusCode = response.statusCode;
|
|
131
147
|
|
|
132
148
|
// Add response body to metadata if it exists
|
|
133
149
|
if (chunk) {
|
|
134
150
|
try {
|
|
135
|
-
if (typeof chunk ===
|
|
151
|
+
if (typeof chunk === "string") {
|
|
136
152
|
// Try to parse as JSON if it's a string
|
|
137
153
|
try {
|
|
138
154
|
const parsed = JSON.parse(chunk);
|
|
@@ -142,7 +158,7 @@ export class ExpressUsageFlowAPI extends UsageFlowAPI {
|
|
|
142
158
|
(metadata as any).body = chunk;
|
|
143
159
|
}
|
|
144
160
|
} else if (Buffer.isBuffer(chunk)) {
|
|
145
|
-
const str = chunk.toString(
|
|
161
|
+
const str = chunk.toString("utf8");
|
|
146
162
|
// Try to parse as JSON if it's a buffer
|
|
147
163
|
try {
|
|
148
164
|
const parsed = JSON.parse(str);
|
|
@@ -151,55 +167,101 @@ export class ExpressUsageFlowAPI extends UsageFlowAPI {
|
|
|
151
167
|
// If not valid JSON, use the string as is
|
|
152
168
|
(metadata as any).body = str;
|
|
153
169
|
}
|
|
154
|
-
} else if (typeof chunk ===
|
|
170
|
+
} else if (typeof chunk === "object") {
|
|
155
171
|
(metadata as any).body = chunk;
|
|
156
172
|
}
|
|
157
173
|
} catch (error) {
|
|
158
|
-
console.error(
|
|
174
|
+
console.error("Error parsing response body:", error);
|
|
159
175
|
}
|
|
160
176
|
}
|
|
161
177
|
|
|
162
|
-
|
|
163
|
-
|
|
164
178
|
const headers = {
|
|
165
|
-
|
|
166
|
-
|
|
179
|
+
"x-usage-key": self.apiKey!,
|
|
180
|
+
"Content-Type": "application/json",
|
|
167
181
|
};
|
|
168
182
|
|
|
183
|
+
if (request.usageflow?.startTime) {
|
|
184
|
+
metadata.requestDuration =
|
|
185
|
+
Date.now() - request.usageflow.startTime;
|
|
186
|
+
}
|
|
187
|
+
|
|
169
188
|
const payload = {
|
|
170
|
-
alias: `${request.method} ${request.route?.path || request.path || request.url ||
|
|
189
|
+
alias: `${request.method} ${request.route?.path || request.path || request.url || "/"}`,
|
|
171
190
|
amount: 1,
|
|
172
191
|
allocationId: request.usageflow?.eventId,
|
|
173
|
-
metadata
|
|
192
|
+
metadata,
|
|
174
193
|
};
|
|
175
194
|
|
|
176
195
|
fetch(`${self.usageflowUrl}/api/v1/ledgers/measure/allocate/use`, {
|
|
177
|
-
method:
|
|
196
|
+
method: "POST",
|
|
178
197
|
headers,
|
|
179
|
-
body: JSON.stringify(payload)
|
|
180
|
-
}).catch(error => {
|
|
181
|
-
console.error(
|
|
198
|
+
body: JSON.stringify(payload),
|
|
199
|
+
}).catch((error) => {
|
|
200
|
+
console.error("Error finalizing request:", error);
|
|
182
201
|
});
|
|
183
202
|
|
|
184
|
-
// Handle the different
|
|
185
|
-
if (typeof chunk ===
|
|
186
|
-
return originalEnd.call(this, undefined,
|
|
203
|
+
// Handle the different overloadsx
|
|
204
|
+
if (typeof chunk === "function") {
|
|
205
|
+
return originalEnd.call(this, undefined, "utf8", chunk);
|
|
187
206
|
}
|
|
188
|
-
if (typeof encoding ===
|
|
189
|
-
return originalEnd.call(this, chunk,
|
|
207
|
+
if (typeof encoding === "function") {
|
|
208
|
+
return originalEnd.call(this, chunk, "utf8", encoding);
|
|
190
209
|
}
|
|
191
|
-
return originalEnd.call(this, chunk, encoding ||
|
|
210
|
+
return originalEnd.call(this, chunk, encoding || "utf8", cb);
|
|
192
211
|
} as typeof response.end;
|
|
193
212
|
|
|
194
213
|
next();
|
|
195
214
|
} catch (error: any) {
|
|
196
|
-
console.error(
|
|
215
|
+
console.error("Error executing request with metadata:", error);
|
|
197
216
|
response.status(400).json({
|
|
198
217
|
message: error.message,
|
|
199
|
-
blocked: true
|
|
218
|
+
blocked: true,
|
|
200
219
|
});
|
|
201
220
|
return;
|
|
202
221
|
}
|
|
203
222
|
};
|
|
204
223
|
}
|
|
205
|
-
|
|
224
|
+
|
|
225
|
+
protected guessLedgerId(request: Request): string {
|
|
226
|
+
const method = request.method;
|
|
227
|
+
const url = request.route?.path || request.path;
|
|
228
|
+
const config = this.apiConfig
|
|
229
|
+
|
|
230
|
+
if (!config?.identityFieldName || !config?.identityFieldLocation) {
|
|
231
|
+
return `${method} ${url}`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const fieldName = config.identityFieldName;
|
|
235
|
+
const location = config.identityFieldLocation;
|
|
236
|
+
|
|
237
|
+
switch (location) {
|
|
238
|
+
case "path_params":
|
|
239
|
+
if (request.params?.[fieldName]) {
|
|
240
|
+
return `${method} ${url} ${request.params[fieldName]}`;
|
|
241
|
+
}
|
|
242
|
+
break;
|
|
243
|
+
case "query_params":
|
|
244
|
+
if (request.query?.[fieldName]) {
|
|
245
|
+
return `${method} ${url} ${request.query[fieldName]}`;
|
|
246
|
+
}
|
|
247
|
+
break;
|
|
248
|
+
case "body":
|
|
249
|
+
if (request.body?.[fieldName]) {
|
|
250
|
+
return `${method} ${url} ${request.body[fieldName]}`;
|
|
251
|
+
}
|
|
252
|
+
break;
|
|
253
|
+
case "bearer_token":
|
|
254
|
+
const authHeader = request.headers.authorization;
|
|
255
|
+
const token = this.extractBearerToken(authHeader);
|
|
256
|
+
if (token) {
|
|
257
|
+
const claims = this.decodeJwtUnverified(token);
|
|
258
|
+
if (claims?.[fieldName]) {
|
|
259
|
+
return `${method} ${url} ${claims[fieldName]}`;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return `${method} ${url}`;
|
|
266
|
+
}
|
|
267
|
+
}
|