@momsfriendlydevco/cowboy 1.0.18 → 1.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/README.md CHANGED
@@ -136,6 +136,11 @@ This function will, in order:
136
136
  6. Return the final response - if it the function did not already explicitly do so
137
137
 
138
138
 
139
+ Cowboy.proxy(path, request, env)
140
+ --------------------------------
141
+ Forward from one route to another as if the second route was called first.
142
+
143
+
139
144
  CowboyRequest
140
145
  -------------
141
146
  ```javascript
@@ -376,3 +381,8 @@ cowboy()
376
381
  (req, res, env) => /* ... */
377
382
  )
378
383
  ```
384
+
385
+
386
+ parseJwt()
387
+ ----------
388
+ Parse the incoming request as a JWT string and decode its contents into `req.body`.
package/lib/cowboy.js CHANGED
@@ -166,6 +166,41 @@ export class Cowboy {
166
166
  }
167
167
 
168
168
 
169
+ /**
170
+ * Call a router function as if it were invoked directly
171
+ * This function exists as an easier way to remap body contents without
172
+ *
173
+ * @param {String} url The URL path to call
174
+ * @param {Object} request Additional request options
175
+ * @param {Object} env The environment object to use
176
+ *
177
+ * @returns {CloudflareResponse} The returned CloudflareResponse object
178
+ */
179
+ async proxy(url, options, env) {
180
+ if (!url || !options || !env) throw new Error('Url + options + env must be specified to proxy()');
181
+
182
+ return this.fetch(new Request(new URL(url, 'http://localhost').toString(), {
183
+ ...options,
184
+ body: (()=> {
185
+ if ( // Being handed a native Cloudflare request body?
186
+ !options.body
187
+ || options.body instanceof FormData
188
+ || options.body instanceof ReadableStream
189
+ || options.body instanceof URLSearchParams
190
+ || typeof options.body == 'string'
191
+ ) {
192
+ return options.body;
193
+ } else if (typeof options.body == 'object') { // Convert POJO into Formdata
194
+ let body = new FormData();
195
+ Object.entries(options.body)
196
+ .forEach(([key, val]) => body.append(key, val))
197
+ return body;
198
+ }
199
+ })(),
200
+ }), env);
201
+ }
202
+
203
+
169
204
  async execMiddleware({middleware, req, res, env}) {
170
205
  let middlewareStack = middleware
171
206
  .map(m => {
@@ -183,7 +218,7 @@ export class Cowboy {
183
218
  while (middlewareStack.length > 0) {
184
219
  let middleware = middlewareStack.shift();
185
220
  try {
186
- response = await middleware(req, res, env);
221
+ response = await middleware.call(this, req, res, env);
187
222
  if (response?.hasSent) { // Stop middleware chain as some intermediate has signalled the chain should end
188
223
  response = res;
189
224
  break;
@@ -193,7 +228,7 @@ export class Cowboy {
193
228
  } catch (e) {
194
229
  let errorText = typeof e == 'string' ? e : e.toString();
195
230
 
196
- debug('Error thrown', errorText);
231
+ debug('Error thrown', e);
197
232
 
198
233
  // Form: '404: Not found'
199
234
  if (/^(\d{3}):/.test(errorText)) {
@@ -226,8 +261,8 @@ export class Cowboy {
226
261
  * @returns {Cowboy} This chainable Cowboy router instance
227
262
  */
228
263
  init() {
229
- debug('INIT!');
230
264
  if (this.doneInit) return this; // Already completed init
265
+ debug('INIT!');
231
266
 
232
267
  if (this.settings.patchAxios) {
233
268
  // TODO: Patch Axios somehow
package/lib/request.js CHANGED
@@ -28,6 +28,14 @@ export default class CowboyRequest {
28
28
  /**
29
29
  * Raw body payload provided by cfReq
30
30
  * This gets translated into a usable object after a call to `parseBody()`
31
+ * @type {Buffer}
32
+ */
33
+ text = null;
34
+
35
+
36
+ /**
37
+ * Body payload provided by cfReq (
38
+ * Defaults to the same value as `text` until its parsed by `parseBody()`
31
39
  * @type {*}
32
40
  */
33
41
  body = null;
@@ -60,6 +68,9 @@ export default class CowboyRequest {
60
68
  // Slurp the headers
61
69
  this.headers = Object.fromEntries(cfReq.headers.entries());
62
70
 
71
+ // Hold the raw data
72
+ this.text = cfReq.text.bind(cfReq),
73
+
63
74
  // Hold the body element - this wont be decoded until parseBody() is called
64
75
  this.body = {
65
76
  json: cfReq.json.bind(cfReq),
@@ -110,7 +121,8 @@ export default class CowboyRequest {
110
121
  throw new Error('Invalid text body');
111
122
  }
112
123
  default:
113
- debug('Empty Body Payload');
124
+ debug('Empty Body Payload - assuming raw payload');
125
+ this.text = await this.body.text();
114
126
  this.body = {};
115
127
  }
116
128
  }
package/lib/response.js CHANGED
@@ -25,6 +25,24 @@ export default class CowboyResponse {
25
25
  }
26
26
 
27
27
 
28
+ /**
29
+ * ExpressJS-like type setter and shortcut function
30
+ * Recognises various shorthand types or defaults to setting a MIME type
31
+ * @param {String} type The type string to set, can be a shorthand string or a mime type
32
+ * @returns {CowboyResponse} This chainable instance
33
+ */
34
+ type(type) {
35
+ switch (type) {
36
+ case 'html': return this.set('Content-Type', 'text/html');
37
+ case 'json': return this.set('Content-Type', 'application/json');
38
+ case 'text': return this.set('Content-Type', 'text/plain');
39
+ default:
40
+ if (!/\//.test(type)) throw new Error(`Shorthand type "${type}" is not recognised and does not look like a valid mime type`);
41
+ return this.set('Content-Type', type);
42
+ }
43
+ }
44
+
45
+
28
46
  /**
29
47
  * Send data and (optionally) mark the response as complete
30
48
  * @param {*} data The data to transmit
@@ -34,7 +52,12 @@ export default class CowboyResponse {
34
52
  send(data, end = true) {
35
53
  if (this.code === null) this.code = 200; // Assume OK if not told otherwise
36
54
 
37
- if (typeof data == 'string') {
55
+ if (
56
+ typeof data == 'string'
57
+ || data instanceof FormData
58
+ || data instanceof ReadableStream
59
+ || data instanceof URLSearchParams
60
+ ) {
38
61
  this.body = data;
39
62
  } else {
40
63
  this.body = JSON.stringify(data);
@@ -1,4 +1,5 @@
1
1
  import cors from '#middleware/cors';
2
+ import parseJwt from '#middleware/parseJwt';
2
3
  import validate from '#middleware/validate';
3
4
  import validateBody from '#middleware/validateBody';
4
5
  import validateHeaders from '#middleware/validateHeaders';
@@ -7,6 +8,7 @@ import validateQuery from '#middleware/validateQuery';
7
8
 
8
9
  export default {
9
10
  cors,
11
+ parseJwt,
10
12
  validate,
11
13
  validateBody,
12
14
  validateHeaders,
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Return a parsing middleware layer which accepts a JWT body and decodes the object into req.body
3
+ *
4
+ * @param {Object} [options] Additional options to mutate behaviour
5
+ * @param {Function} [options.isJwt] Async function, called as `(req, res)` to determine if the input is a JWT payload, defaults to checking the content-type header
6
+ */
7
+ export default function CowboyMiddlewareParseJwt(options) {
8
+ let settings = {
9
+ async isJwt(req, res) { // eslint-disable-line no-unused-vars
10
+ return req.headers['content-type'] == 'application/jwt';
11
+ },
12
+ ...options,
13
+ };
14
+
15
+ return async (req, res) => {
16
+ if (await !settings.isJwt(req, res)) return;
17
+
18
+ const base64 = req.text.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
19
+ req.body = JSON.parse(atob(base64));
20
+ }
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momsfriendlydevco/cowboy",
3
- "version": "1.0.18",
3
+ "version": "1.1.0",
4
4
  "description": "Wrapper around Cloudflare Wrangler to provide a more Express-like experience",
5
5
  "scripts": {
6
6
  "lint": "eslint ."
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@momsfriendlydevco/joyful": "^1.0.1",
39
- "@momsfriendlydevco/path-match": "^1.0.0",
39
+ "@momsfriendlydevco/path-match": "^1.1.0",
40
40
  "toml": "^3.0.0"
41
41
  },
42
42
  "devDependencies": {