@usageflow/express 0.1.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/dist/index.d.ts +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +18 -0
- package/dist/plugin.js +168 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +27 -0
- package/src/index.ts +1 -0
- package/src/plugin.ts +205 -0
- package/tsconfig.json +9 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ExpressUsageFlowAPI } from './plugin';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ExpressUsageFlowAPI = void 0;
|
|
4
|
+
var plugin_1 = require("./plugin");
|
|
5
|
+
Object.defineProperty(exports, "ExpressUsageFlowAPI", { enumerable: true, get: function () { return plugin_1.ExpressUsageFlowAPI; } });
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,mCAA+C;AAAtC,6GAAA,mBAAmB,OAAA"}
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { UsageFlowAPI, Route, RequestMetadata } from '@usageflow/core';
|
|
3
|
+
declare global {
|
|
4
|
+
namespace Express {
|
|
5
|
+
interface Request {
|
|
6
|
+
usageflow?: {
|
|
7
|
+
startTime: number;
|
|
8
|
+
eventId?: string;
|
|
9
|
+
metadata?: RequestMetadata;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export declare class ExpressUsageFlowAPI extends UsageFlowAPI {
|
|
15
|
+
private collectRequestMetadata;
|
|
16
|
+
private executeRequestWithMetadata;
|
|
17
|
+
createMiddleware(routes: Route[], whitelistRoutes?: Route[]): (request: Request, response: Response, next: NextFunction) => Promise<void>;
|
|
18
|
+
}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ExpressUsageFlowAPI = void 0;
|
|
4
|
+
const core_1 = require("@usageflow/core");
|
|
5
|
+
class ExpressUsageFlowAPI extends core_1.UsageFlowAPI {
|
|
6
|
+
async collectRequestMetadata(request) {
|
|
7
|
+
const headers = this.sanitizeHeaders(request.headers);
|
|
8
|
+
// Get client IP, handling forwarded headers
|
|
9
|
+
let clientIP = request.ip;
|
|
10
|
+
const forwardedFor = request.headers['x-forwarded-for'];
|
|
11
|
+
if (forwardedFor && typeof forwardedFor === 'string') {
|
|
12
|
+
clientIP = forwardedFor.split(',')[0].trim();
|
|
13
|
+
}
|
|
14
|
+
const metadata = {
|
|
15
|
+
method: request.method,
|
|
16
|
+
url: request.route?.path || request.path || request.url || '/',
|
|
17
|
+
rawUrl: request.originalUrl || '/',
|
|
18
|
+
clientIP: request.ip || 'unknown',
|
|
19
|
+
userAgent: request.headers['user-agent'],
|
|
20
|
+
timestamp: new Date().toISOString(),
|
|
21
|
+
headers,
|
|
22
|
+
queryParams: request.query,
|
|
23
|
+
pathParams: request.params,
|
|
24
|
+
body: request.body
|
|
25
|
+
};
|
|
26
|
+
return metadata;
|
|
27
|
+
}
|
|
28
|
+
async executeRequestWithMetadata(ledgerId, metadata, request, response) {
|
|
29
|
+
if (!this.apiKey) {
|
|
30
|
+
throw new Error('API key not initialized');
|
|
31
|
+
}
|
|
32
|
+
const headers = {
|
|
33
|
+
'x-usage-key': this.apiKey,
|
|
34
|
+
'Content-Type': 'application/json'
|
|
35
|
+
};
|
|
36
|
+
const payload = {
|
|
37
|
+
alias: ledgerId,
|
|
38
|
+
amount: 1,
|
|
39
|
+
metadata
|
|
40
|
+
};
|
|
41
|
+
try {
|
|
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 == 'Failed to use resource after retries: Faile to preform operation') {
|
|
61
|
+
throw new Error('Failed to allocate resource');
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
createMiddleware(routes, whitelistRoutes = []) {
|
|
67
|
+
const routesMap = this.createRoutesMap(routes);
|
|
68
|
+
const whitelistMap = this.createRoutesMap(whitelistRoutes);
|
|
69
|
+
const self = this;
|
|
70
|
+
return async (request, response, next) => {
|
|
71
|
+
const method = request.method;
|
|
72
|
+
const url = request.route?.path || request.path || request.url;
|
|
73
|
+
request.usageflow = {
|
|
74
|
+
startTime: Date.now()
|
|
75
|
+
};
|
|
76
|
+
if (this.shouldSkipRoute(method, url || '', whitelistMap)) {
|
|
77
|
+
return next();
|
|
78
|
+
}
|
|
79
|
+
if (!this.shouldMonitorRoute(method, url || '', routesMap)) {
|
|
80
|
+
return next();
|
|
81
|
+
}
|
|
82
|
+
const metadata = await this.collectRequestMetadata(request);
|
|
83
|
+
try {
|
|
84
|
+
await this.executeRequestWithMetadata(`${method} ${url}`, metadata, request, response);
|
|
85
|
+
// Capture response data
|
|
86
|
+
const originalEnd = response.end;
|
|
87
|
+
response.end = function (chunk, encoding, cb) {
|
|
88
|
+
if (!request.usageflow?.eventId) {
|
|
89
|
+
return originalEnd.call(this, chunk, encoding || 'utf8', cb);
|
|
90
|
+
}
|
|
91
|
+
const metadata = request.usageflow?.metadata || {};
|
|
92
|
+
metadata.responseStatusCode = response.statusCode;
|
|
93
|
+
// Add response body to metadata if it exists
|
|
94
|
+
if (chunk) {
|
|
95
|
+
try {
|
|
96
|
+
if (typeof chunk === 'string') {
|
|
97
|
+
// Try to parse as JSON if it's a string
|
|
98
|
+
try {
|
|
99
|
+
const parsed = JSON.parse(chunk);
|
|
100
|
+
metadata.body = parsed;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// If not valid JSON, use the string as is
|
|
104
|
+
metadata.body = chunk;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else if (Buffer.isBuffer(chunk)) {
|
|
108
|
+
const str = chunk.toString('utf8');
|
|
109
|
+
// Try to parse as JSON if it's a buffer
|
|
110
|
+
try {
|
|
111
|
+
const parsed = JSON.parse(str);
|
|
112
|
+
metadata.body = parsed;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// If not valid JSON, use the string as is
|
|
116
|
+
metadata.body = str;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else if (typeof chunk === 'object') {
|
|
120
|
+
metadata.body = chunk;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
console.error('Error parsing response body:', error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const headers = {
|
|
128
|
+
'x-usage-key': self.apiKey,
|
|
129
|
+
'Content-Type': 'application/json'
|
|
130
|
+
};
|
|
131
|
+
const payload = {
|
|
132
|
+
alias: `${request.method} ${request.route?.path || request.path || request.url || '/'}`,
|
|
133
|
+
amount: 1,
|
|
134
|
+
allocationId: request.usageflow?.eventId,
|
|
135
|
+
metadata
|
|
136
|
+
};
|
|
137
|
+
fetch(`${self.usageflowUrl}/api/v1/ledgers/measure/allocate/use`, {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
headers,
|
|
140
|
+
body: JSON.stringify(payload)
|
|
141
|
+
}).catch(error => {
|
|
142
|
+
console.error('Error finalizing request:', error);
|
|
143
|
+
});
|
|
144
|
+
// Handle the different overloads
|
|
145
|
+
if (typeof chunk === 'function') {
|
|
146
|
+
return originalEnd.call(this, undefined, 'utf8', chunk);
|
|
147
|
+
}
|
|
148
|
+
if (typeof encoding === 'function') {
|
|
149
|
+
return originalEnd.call(this, chunk, 'utf8', encoding);
|
|
150
|
+
}
|
|
151
|
+
return originalEnd.call(this, chunk, encoding || 'utf8', cb);
|
|
152
|
+
};
|
|
153
|
+
next();
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
console.error('Error executing request with metadata:', error);
|
|
157
|
+
response.status(400).json({
|
|
158
|
+
message: error.message,
|
|
159
|
+
blocked: true
|
|
160
|
+
});
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
exports.ExpressUsageFlowAPI = ExpressUsageFlowAPI;
|
|
167
|
+
;
|
|
168
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usageflow/express",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "UsageFlow plugin for Express applications",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"test": "jest",
|
|
10
|
+
"prepare": "npm run build"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@usageflow/core": "^0.1.0",
|
|
14
|
+
"express": "^4.18.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/express": "^4.17.0",
|
|
18
|
+
"@types/node": "^20.0.0",
|
|
19
|
+
"typescript": "^5.0.0",
|
|
20
|
+
"jest": "^29.0.0",
|
|
21
|
+
"@types/jest": "^29.0.0",
|
|
22
|
+
"ts-jest": "^29.0.0"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"express": ">=4.17.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ExpressUsageFlowAPI } from './plugin';
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { UsageFlowAPI, Route, RequestMetadata } from '@usageflow/core';
|
|
3
|
+
|
|
4
|
+
declare global {
|
|
5
|
+
namespace Express {
|
|
6
|
+
interface Request {
|
|
7
|
+
usageflow?: {
|
|
8
|
+
startTime: number;
|
|
9
|
+
eventId?: string;
|
|
10
|
+
metadata?: RequestMetadata;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class ExpressUsageFlowAPI extends UsageFlowAPI {
|
|
17
|
+
private async collectRequestMetadata(request: Request): Promise<RequestMetadata> {
|
|
18
|
+
const headers = this.sanitizeHeaders(request.headers as Record<string, string>);
|
|
19
|
+
|
|
20
|
+
// Get client IP, handling forwarded headers
|
|
21
|
+
let clientIP = request.ip;
|
|
22
|
+
const forwardedFor = request.headers['x-forwarded-for'];
|
|
23
|
+
if (forwardedFor && typeof forwardedFor === 'string') {
|
|
24
|
+
clientIP = forwardedFor.split(',')[0].trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const metadata: RequestMetadata = {
|
|
28
|
+
method: request.method,
|
|
29
|
+
url: request.route?.path || request.path || request.url || '/',
|
|
30
|
+
rawUrl: request.originalUrl || '/',
|
|
31
|
+
clientIP: request.ip || 'unknown',
|
|
32
|
+
userAgent: request.headers['user-agent'] as string,
|
|
33
|
+
timestamp: new Date().toISOString(),
|
|
34
|
+
headers,
|
|
35
|
+
queryParams: request.query as Record<string, any>,
|
|
36
|
+
pathParams: request.params,
|
|
37
|
+
body: request.body
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return metadata;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private async executeRequestWithMetadata(
|
|
44
|
+
ledgerId: string,
|
|
45
|
+
metadata: RequestMetadata,
|
|
46
|
+
request: Request,
|
|
47
|
+
response: Response
|
|
48
|
+
): Promise<void> {
|
|
49
|
+
if (!this.apiKey) {
|
|
50
|
+
throw new Error('API key not initialized');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const headers = {
|
|
54
|
+
'x-usage-key': this.apiKey,
|
|
55
|
+
'Content-Type': 'application/json'
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const payload = {
|
|
59
|
+
alias: ledgerId,
|
|
60
|
+
amount: 1,
|
|
61
|
+
metadata
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetch(`${this.usageflowUrl}/api/v1/ledgers/measure/allocate`, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers,
|
|
68
|
+
body: JSON.stringify(payload)
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const data = await response.json();
|
|
72
|
+
|
|
73
|
+
if (response.status === 400) {
|
|
74
|
+
throw new Error(data.message);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (response.ok) {
|
|
78
|
+
request.usageflow!.eventId = data.eventId;
|
|
79
|
+
request.usageflow!.metadata = metadata;
|
|
80
|
+
} else {
|
|
81
|
+
throw new Error(data.message || 'Unknown error occurred');
|
|
82
|
+
}
|
|
83
|
+
} catch (error: any) {
|
|
84
|
+
if (error.message == 'Failed to use resource after retries: Faile to preform operation') {
|
|
85
|
+
throw new Error('Failed to allocate resource');
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public createMiddleware(routes: Route[], whitelistRoutes: Route[] = []) {
|
|
92
|
+
const routesMap = this.createRoutesMap(routes);
|
|
93
|
+
const whitelistMap = this.createRoutesMap(whitelistRoutes);
|
|
94
|
+
const self = this;
|
|
95
|
+
|
|
96
|
+
return async (request: Request, response: Response, next: NextFunction) => {
|
|
97
|
+
const method = request.method;
|
|
98
|
+
const url = request.route?.path || request.path || request.url;
|
|
99
|
+
|
|
100
|
+
request.usageflow = {
|
|
101
|
+
startTime: Date.now()
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (this.shouldSkipRoute(method, url || '', whitelistMap)) {
|
|
105
|
+
return next();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!this.shouldMonitorRoute(method, url || '', routesMap)) {
|
|
109
|
+
return next();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const metadata = await this.collectRequestMetadata(request);
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await this.executeRequestWithMetadata(
|
|
116
|
+
`${method} ${url}`,
|
|
117
|
+
metadata,
|
|
118
|
+
request,
|
|
119
|
+
response
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Capture response data
|
|
123
|
+
const originalEnd = response.end;
|
|
124
|
+
response.end = function (this: Response, chunk?: any, encoding?: BufferEncoding, cb?: () => void) {
|
|
125
|
+
if (!request.usageflow?.eventId) {
|
|
126
|
+
return originalEnd.call(this, chunk, encoding || 'utf8', cb);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const metadata = request.usageflow?.metadata || {};
|
|
130
|
+
(metadata as any).responseStatusCode = response.statusCode;
|
|
131
|
+
|
|
132
|
+
// Add response body to metadata if it exists
|
|
133
|
+
if (chunk) {
|
|
134
|
+
try {
|
|
135
|
+
if (typeof chunk === 'string') {
|
|
136
|
+
// Try to parse as JSON if it's a string
|
|
137
|
+
try {
|
|
138
|
+
const parsed = JSON.parse(chunk);
|
|
139
|
+
(metadata as any).body = parsed;
|
|
140
|
+
} catch {
|
|
141
|
+
// If not valid JSON, use the string as is
|
|
142
|
+
(metadata as any).body = chunk;
|
|
143
|
+
}
|
|
144
|
+
} else if (Buffer.isBuffer(chunk)) {
|
|
145
|
+
const str = chunk.toString('utf8');
|
|
146
|
+
// Try to parse as JSON if it's a buffer
|
|
147
|
+
try {
|
|
148
|
+
const parsed = JSON.parse(str);
|
|
149
|
+
(metadata as any).body = parsed;
|
|
150
|
+
} catch {
|
|
151
|
+
// If not valid JSON, use the string as is
|
|
152
|
+
(metadata as any).body = str;
|
|
153
|
+
}
|
|
154
|
+
} else if (typeof chunk === 'object') {
|
|
155
|
+
(metadata as any).body = chunk;
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error('Error parsing response body:', error);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
const headers = {
|
|
165
|
+
'x-usage-key': self.apiKey!,
|
|
166
|
+
'Content-Type': 'application/json'
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const payload = {
|
|
170
|
+
alias: `${request.method} ${request.route?.path || request.path || request.url || '/'}`,
|
|
171
|
+
amount: 1,
|
|
172
|
+
allocationId: request.usageflow?.eventId,
|
|
173
|
+
metadata
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
fetch(`${self.usageflowUrl}/api/v1/ledgers/measure/allocate/use`, {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
headers,
|
|
179
|
+
body: JSON.stringify(payload)
|
|
180
|
+
}).catch(error => {
|
|
181
|
+
console.error('Error finalizing request:', error);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Handle the different overloads
|
|
185
|
+
if (typeof chunk === 'function') {
|
|
186
|
+
return originalEnd.call(this, undefined, 'utf8', chunk);
|
|
187
|
+
}
|
|
188
|
+
if (typeof encoding === 'function') {
|
|
189
|
+
return originalEnd.call(this, chunk, 'utf8', encoding);
|
|
190
|
+
}
|
|
191
|
+
return originalEnd.call(this, chunk, encoding || 'utf8', cb);
|
|
192
|
+
} as typeof response.end;
|
|
193
|
+
|
|
194
|
+
next();
|
|
195
|
+
} catch (error: any) {
|
|
196
|
+
console.error('Error executing request with metadata:', error);
|
|
197
|
+
response.status(400).json({
|
|
198
|
+
message: error.message,
|
|
199
|
+
blocked: true
|
|
200
|
+
});
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
};
|