@momsfriendlydevco/cowboy 1.6.1 → 1.7.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/lib/cowboy.js +4 -2
- package/lib/request.js +18 -0
- package/lib/response.js +5 -2
- package/middleware/apiTally.js +166 -0
- package/middleware/etagCaching.js +8 -2
- package/middleware/index.js +2 -0
- package/package.json +1 -1
package/lib/cowboy.js
CHANGED
|
@@ -178,10 +178,12 @@ export class Cowboy {
|
|
|
178
178
|
router: this,
|
|
179
179
|
pathTidy: this.settings.pathTidy,
|
|
180
180
|
});
|
|
181
|
+
req.cowboy = this;
|
|
181
182
|
|
|
182
183
|
await req.parseBody();
|
|
183
184
|
|
|
184
185
|
let res = new CowboyResponse();
|
|
186
|
+
res.cowboy = this;
|
|
185
187
|
debug('Incoming request:', req.toString());
|
|
186
188
|
|
|
187
189
|
// Exec all earlyMiddleware - every time
|
|
@@ -203,7 +205,7 @@ export class Cowboy {
|
|
|
203
205
|
)
|
|
204
206
|
);
|
|
205
207
|
}
|
|
206
|
-
return await res.sendStatus(404).toCloudflareResponse(); // No matching route
|
|
208
|
+
return await res.sendStatus(404).toCloudflareResponse(req); // No matching route
|
|
207
209
|
}
|
|
208
210
|
|
|
209
211
|
// Populate params
|
|
@@ -218,7 +220,7 @@ export class Cowboy {
|
|
|
218
220
|
|
|
219
221
|
if (!response) throw new Error('Middleware chain ended without returning a response!');
|
|
220
222
|
if (!response.toCloudflareResponse) throw new Error('Eventual middleware chain output should have a .toCloudflareResponse() method');
|
|
221
|
-
return await response.toCloudflareResponse();
|
|
223
|
+
return await response.toCloudflareResponse(req);
|
|
222
224
|
}
|
|
223
225
|
|
|
224
226
|
|
package/lib/request.js
CHANGED
|
@@ -5,6 +5,22 @@ import debug from '#lib/debug';
|
|
|
5
5
|
* @augments {CloudflareRequest}
|
|
6
6
|
*/
|
|
7
7
|
export default class CowboyRequest {
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Handle to the parent `cowboy` instance
|
|
11
|
+
*
|
|
12
|
+
* @type {Cowboy}
|
|
13
|
+
*/
|
|
14
|
+
cowboy;
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The original Cloudflare Request object
|
|
19
|
+
* @type {CloudflareRequest}
|
|
20
|
+
*/
|
|
21
|
+
cfReq;
|
|
22
|
+
|
|
23
|
+
|
|
8
24
|
/**
|
|
9
25
|
* Extracted request path with leading slash
|
|
10
26
|
* @type {String}
|
|
@@ -42,6 +58,8 @@ export default class CowboyRequest {
|
|
|
42
58
|
|
|
43
59
|
|
|
44
60
|
constructor(cfReq, props) {
|
|
61
|
+
this.cfReq = cfReq;
|
|
62
|
+
|
|
45
63
|
// Copy all cfReq keys locally as a shallow copy
|
|
46
64
|
Object.assign(
|
|
47
65
|
this,
|
package/lib/response.js
CHANGED
|
@@ -4,6 +4,7 @@ import debug from '#lib/debug';
|
|
|
4
4
|
* Generic all-in-one response wrapper to mangle responses without having to memorize all the weird syntax that Wrangler / Cloudflare workers need
|
|
5
5
|
*/
|
|
6
6
|
export default class CowboyResponse {
|
|
7
|
+
cowboy;
|
|
7
8
|
body = '';
|
|
8
9
|
code = null;
|
|
9
10
|
headers = {};
|
|
@@ -138,11 +139,13 @@ export default class CowboyResponse {
|
|
|
138
139
|
|
|
139
140
|
/**
|
|
140
141
|
* Convert the current CoyboyResponse into a CloudflareResponse object
|
|
142
|
+
*
|
|
143
|
+
* @param {CowboyRequest} [req] Optional CowboyRequest object to use when computing the response
|
|
141
144
|
* @returns {CloudflareResponse} The cloudflare output object
|
|
142
145
|
*/
|
|
143
|
-
async toCloudflareResponse() {
|
|
146
|
+
async toCloudflareResponse(req) {
|
|
144
147
|
// Await all beforeServeCallbacks
|
|
145
|
-
await Array.fromAsync(this.beforeServeCallbacks, cb => cb.call(this, this));
|
|
148
|
+
await Array.fromAsync(this.beforeServeCallbacks, cb => cb.call(this, req, this));
|
|
146
149
|
|
|
147
150
|
debug('CF-Response', JSON.stringify({
|
|
148
151
|
status: this.code,
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* APITally LogPush output support
|
|
3
|
+
* This middleware outputs a Base64 + Gziped trace for digest by the remote APITally.io service
|
|
4
|
+
*
|
|
5
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
6
|
+
* @param {Boolean|Function} [options.enabled] Whether to perform an output action, defaults to being enabled only if we are likely running within a Cloudflare Co-location
|
|
7
|
+
* @param {String} [options.pathPrefix=''] Prefix to prepend to all endpoint paths
|
|
8
|
+
* @param {String} [options.client='js-serverless:hono'] The client string to use when reporting to APITally
|
|
9
|
+
* @param {String} [options.clientVersion] The client version string to use when reporting to APITally
|
|
10
|
+
* @param {Set<String>} [options.logMethods] Methods whitelist to use when computing the endpoints to APITally
|
|
11
|
+
*
|
|
12
|
+
* @returns {CowboyMiddleware}
|
|
13
|
+
*/
|
|
14
|
+
export default function CowboyMiddlewareApiTally(options) {
|
|
15
|
+
let settings = {
|
|
16
|
+
enabled(req, res) {
|
|
17
|
+
return !! req.cfReq.cf?.colo; // Only run if we have a Cloudflare data center allocated - otherwise assume local execution
|
|
18
|
+
},
|
|
19
|
+
pathPrefix: '',
|
|
20
|
+
client: 'js-serverless:hono',
|
|
21
|
+
clientVersion: '1.0.0',
|
|
22
|
+
logMethods: new Set([
|
|
23
|
+
'DELETE',
|
|
24
|
+
'GET',
|
|
25
|
+
'PATCH',
|
|
26
|
+
'POST',
|
|
27
|
+
'PUT',
|
|
28
|
+
]),
|
|
29
|
+
...options,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
let isFirstRequest = true;
|
|
33
|
+
|
|
34
|
+
return (req, res) => {
|
|
35
|
+
req.startTime = Date.now();
|
|
36
|
+
|
|
37
|
+
// Queue up callback function to call after handling the request
|
|
38
|
+
res.beforeServe(async (req, res) => {
|
|
39
|
+
if ( // Skip adding APITally output if we're not enabled
|
|
40
|
+
!settings.enabled
|
|
41
|
+
|| (
|
|
42
|
+
typeof settings.enabled == 'function'
|
|
43
|
+
&& !(await settings.enabled(req, res))
|
|
44
|
+
)
|
|
45
|
+
) return;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Actual output data structure to encode + output
|
|
49
|
+
* This is copied from https://github.com/apitally/apitally-js-serverless/blob/main/src/hono/middleware.ts
|
|
50
|
+
* @type {Object}
|
|
51
|
+
*/
|
|
52
|
+
let outputData = {
|
|
53
|
+
instanceUuid: crypto.randomUUID(), // NOTE: Docs say required string but composer returns undefined outside of isFirstRequest
|
|
54
|
+
requestUuid: crypto.randomUUID(),
|
|
55
|
+
consumer: undefined, // FIXME: No idea what this is but its optional anyway
|
|
56
|
+
startup: isFirstRequest ? {
|
|
57
|
+
paths: res.cowboy.routes
|
|
58
|
+
.flatMap(route =>
|
|
59
|
+
route.methods
|
|
60
|
+
.filter(method => settings.logMethods.has(method))
|
|
61
|
+
.map(method =>
|
|
62
|
+
route.matcher.pathStrings
|
|
63
|
+
.filter(path => path.startsWith('/')) // Exclude complex RegEx matches
|
|
64
|
+
.map(path => ({
|
|
65
|
+
method,
|
|
66
|
+
path: settings.pathPrefix + path,
|
|
67
|
+
}))
|
|
68
|
+
)
|
|
69
|
+
),
|
|
70
|
+
client: settings.client,
|
|
71
|
+
versions: {
|
|
72
|
+
'@apitally/serverless': settings.clientVersion,
|
|
73
|
+
},
|
|
74
|
+
} : undefined,
|
|
75
|
+
request: {
|
|
76
|
+
path: req.cfReq.routePath,
|
|
77
|
+
headers: Object.entries(req.headers),
|
|
78
|
+
size: req.cfReq.headers.has('content-length') ? Number.parseInt(req.cfReq.headers.get('content-length')) : undefined,
|
|
79
|
+
consumer: undefined, // FIXME: Where does this come from type is optional string
|
|
80
|
+
body: bytesToBase64(pojoToUint8Array(req.body)),
|
|
81
|
+
},
|
|
82
|
+
response: {
|
|
83
|
+
responseTime: Math.floor((Date.now() - req.startTime) / 1000),
|
|
84
|
+
headers: Object.entries(res.headers),
|
|
85
|
+
size:
|
|
86
|
+
res.headers['Content-Type'] == 'application/json' ? JSON.stringify(res.body).length
|
|
87
|
+
: res.headers['Content-Type'].startsWith('text/') ? res.body.length
|
|
88
|
+
: undefined,
|
|
89
|
+
body:
|
|
90
|
+
res.headers['Content-Type'] == 'application/json' ? bytesToBase64(pojoToUint8Array(res.body))
|
|
91
|
+
: res.headers['Content-Type'].startsWith('text/') ? new TextEncoder().encode(res.body)
|
|
92
|
+
: undefined,
|
|
93
|
+
},
|
|
94
|
+
validationErrors: undefined, // FIXME: Populate somehow, type is ValidationError[]
|
|
95
|
+
exception: undefined, // FIXME: Populate somehow when erroring out, type struct below
|
|
96
|
+
/*
|
|
97
|
+
res.code === 500 && res.error
|
|
98
|
+
? {
|
|
99
|
+
type: res.error.name, // FIXME: string
|
|
100
|
+
msg: truncateExceptionMessage(res.error.message), // FIXME: string
|
|
101
|
+
stackTrace: truncateExceptionStackTrace(res.error.stack ?? ""), // FIXME: string
|
|
102
|
+
}
|
|
103
|
+
: undefined,
|
|
104
|
+
*/
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// console.log('APITally data', JSON.stringify(outputData, null, '\t'));
|
|
108
|
+
|
|
109
|
+
console.log('apitally:' + await gzipBase64(JSON.stringify(outputData)));
|
|
110
|
+
if (isFirstRequest) isFirstRequest = false; // Disable need for more endpoint reporting after the first report
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
// Utilify functions taken from source {{{
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Convert a standard JS POJO into a Uint8Array type
|
|
120
|
+
*
|
|
121
|
+
* @param {Object|Array} obj Input object to work with
|
|
122
|
+
* @returns {Uint8Array} Output Uint8Array type
|
|
123
|
+
*/
|
|
124
|
+
function pojoToUint8Array(obj) {
|
|
125
|
+
return new TextEncoder().encode(
|
|
126
|
+
JSON.stringify(obj)
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Convert an incoming Uint8Array into Base64 encoding
|
|
132
|
+
*
|
|
133
|
+
* @see https://github.com/apitally/apitally-js-serverless/blob/main/src/common/bytes.ts
|
|
134
|
+
*
|
|
135
|
+
* @param {Uint8Array} bytes The incoming byte stream to convert
|
|
136
|
+
* @returns {String} Base64 Encoded content
|
|
137
|
+
*/
|
|
138
|
+
function bytesToBase64(bytes) {
|
|
139
|
+
/* eslint-disable unicorn/numeric-separators-style, unicorn/prefer-code-point */
|
|
140
|
+
const chunks = [];
|
|
141
|
+
const chunkSize = 0x1000; // 4096 bytes
|
|
142
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
143
|
+
chunks.push(String.fromCharCode(...bytes.subarray(i, i + chunkSize)));
|
|
144
|
+
}
|
|
145
|
+
return btoa(chunks.join(""));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Encode a given input string via Gzip
|
|
151
|
+
*
|
|
152
|
+
* @param {String} json The input string to encode
|
|
153
|
+
* @returns {Uint8Array} The encoded input
|
|
154
|
+
*/
|
|
155
|
+
async function gzipBase64(json) {
|
|
156
|
+
const encoder = new TextEncoder();
|
|
157
|
+
const gzipStream = new CompressionStream("gzip");
|
|
158
|
+
const writer = gzipStream.writable.getWriter();
|
|
159
|
+
writer.write(encoder.encode(json));
|
|
160
|
+
writer.close();
|
|
161
|
+
|
|
162
|
+
const compressed = await new Response(gzipStream.readable).arrayBuffer();
|
|
163
|
+
return bytesToBase64(new Uint8Array(compressed));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// }}}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
// Utility: sortKeys(o) - Re-
|
|
1
|
+
// Utility: sortKeys(o) - Re-compose a JSON object so that keys are in a predictable order {{{
|
|
2
|
+
/**
|
|
3
|
+
* Re-compose a JSON object so that keys are in a predictable order
|
|
4
|
+
*
|
|
5
|
+
* @param {Object} o Incoming object/value to resort
|
|
6
|
+
* @returns {Object} The incoming object, shallow copied into another with sorted keys
|
|
7
|
+
*/
|
|
2
8
|
function sortKeys(o) {
|
|
3
9
|
if (typeof o !== 'object' || o === null) return o;
|
|
4
10
|
if (Array.isArray(o)) return o.map(sortKeys);
|
|
@@ -25,7 +31,7 @@ function sortKeys(o) {
|
|
|
25
31
|
*
|
|
26
32
|
* @returns {CowboyMiddleware} A CowboyMiddleware compatible function - this can be used on individual requests or globally
|
|
27
33
|
*/
|
|
28
|
-
export default function
|
|
34
|
+
export default function CowboyMiddlewareEtagCaching(options) {
|
|
29
35
|
let settings = {
|
|
30
36
|
enabled(req, settings) { // eslint-disable-line no-unused-vars
|
|
31
37
|
return (
|
package/middleware/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import apiTally from '#middleware/apiTally';
|
|
1
2
|
import cors from '#middleware/cors';
|
|
2
3
|
import devOnly from '#middleware/devOnly';
|
|
3
4
|
import etagCaching from '#middleware/etagCaching';
|
|
@@ -9,6 +10,7 @@ import validateParams from '#middleware/validateParams';
|
|
|
9
10
|
import validateQuery from '#middleware/validateQuery';
|
|
10
11
|
|
|
11
12
|
export default {
|
|
13
|
+
apiTally,
|
|
12
14
|
cors,
|
|
13
15
|
devOnly,
|
|
14
16
|
etagCaching,
|