@odatnurd/cf-requests 0.1.5 → 0.1.7
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 +7 -8
- package/aegis/index.js +113 -45
- package/package.json +27 -3
package/README.md
CHANGED
|
@@ -16,6 +16,9 @@ and allowing file based in Cloudflare Workers, where that is not directly
|
|
|
16
16
|
possible. This again is not strictly required, but examples below assume that
|
|
17
17
|
this is the case.
|
|
18
18
|
|
|
19
|
+
> ℹ️ Technically, the validation wrapper will accept any validator (e.g. zod) as
|
|
20
|
+
> long as the object passed in conforms to the validation contract; see below.
|
|
21
|
+
|
|
19
22
|
|
|
20
23
|
## Installation
|
|
21
24
|
|
|
@@ -37,7 +40,7 @@ forward.
|
|
|
37
40
|
### Example
|
|
38
41
|
|
|
39
42
|
Assuming a file named `test.joker.json` that contains the following Joker
|
|
40
|
-
schema:
|
|
43
|
+
schema, and and you are using the Joker Rollup plugin:
|
|
41
44
|
|
|
42
45
|
```json
|
|
43
46
|
{
|
|
@@ -58,6 +61,7 @@ fields declared by the schema are present.
|
|
|
58
61
|
```js
|
|
59
62
|
import { validate, success, routeHandler } from '#lib/common';
|
|
60
63
|
|
|
64
|
+
// Use the Joker rollup plugin to obtain the object we require
|
|
61
65
|
import * as testSchema from '#schemas/test';
|
|
62
66
|
|
|
63
67
|
|
|
@@ -89,15 +93,10 @@ To use these utilities, you must install the required peer dependencies into
|
|
|
89
93
|
your own project's `devDependencies` if you have not already done so.
|
|
90
94
|
|
|
91
95
|
```sh
|
|
92
|
-
pnpm add -D @
|
|
96
|
+
pnpm add -D @axel669/aegis @axel669/joker @odatnurd/cf-aegis miniflare
|
|
93
97
|
```
|
|
94
98
|
|
|
95
|
-
|
|
96
|
-
> [@odatnurd/d1-query](https://www.npmjs.com/package/@odatnurd/d1-query) in your
|
|
97
|
-
> project, that library should be installed as a regular `dependency` and not a
|
|
98
|
-
> `devDependency`
|
|
99
|
-
|
|
100
|
-
The `@odatnurd/cf-requests` module exports the following functions:
|
|
99
|
+
The `@odatnurd/cf-requests/aegis` module exports the following functions:
|
|
101
100
|
|
|
102
101
|
|
|
103
102
|
### Helper Functions
|
package/aegis/index.js
CHANGED
|
@@ -8,6 +8,45 @@ import { validate } from '../lib/handlers.js';
|
|
|
8
8
|
/******************************************************************************/
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
/* A mapping of all of the common status errors that might be returned by the
|
|
12
|
+
* validator. */
|
|
13
|
+
const STATUS_TEXT = {
|
|
14
|
+
400: 'Bad Request',
|
|
15
|
+
401: 'Unauthorized',
|
|
16
|
+
402: 'Payment Required',
|
|
17
|
+
403: 'Forbidden',
|
|
18
|
+
404: 'Not Found',
|
|
19
|
+
405: 'Method Not Allowed',
|
|
20
|
+
406: 'Not Acceptable',
|
|
21
|
+
407: 'Proxy Authentication Required',
|
|
22
|
+
408: 'Request Timeout',
|
|
23
|
+
409: 'Conflict',
|
|
24
|
+
410: 'Gone',
|
|
25
|
+
411: 'Length Required',
|
|
26
|
+
412: 'Precondition Failed',
|
|
27
|
+
413: 'Payload Too Large',
|
|
28
|
+
414: 'URI Too Long',
|
|
29
|
+
415: 'Unsupported Media Type',
|
|
30
|
+
416: 'Range Not Satisfiable',
|
|
31
|
+
417: 'Expectation Failed',
|
|
32
|
+
418: "I'm a teapot",
|
|
33
|
+
421: 'Misdirected Request',
|
|
34
|
+
422: 'Unprocessable Entity',
|
|
35
|
+
423: 'Locked',
|
|
36
|
+
424: 'Failed Dependency',
|
|
37
|
+
425: 'Too Early',
|
|
38
|
+
426: 'Upgrade Required',
|
|
39
|
+
428: 'Precondition Required',
|
|
40
|
+
429: 'Too Many Requests',
|
|
41
|
+
431: 'Request Header Fields Too Large',
|
|
42
|
+
451: 'Unavailable For Legal Reasons',
|
|
43
|
+
500: 'Internal Server Error',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
/******************************************************************************/
|
|
48
|
+
|
|
49
|
+
|
|
11
50
|
/*
|
|
12
51
|
* Initializes some custom Aegis checks that make testing of schema and data
|
|
13
52
|
* requests easier.
|
|
@@ -46,87 +85,116 @@ export function initializeRequestChecks() {
|
|
|
46
85
|
* within it. */
|
|
47
86
|
export async function schemaTest(dataType, schema, data, validator) {
|
|
48
87
|
// If a validator is provided, use it; otherwise use ours. This requires that
|
|
49
|
-
// you provide a call-compatible validator. This is here
|
|
50
|
-
// migrations of old code that is using a different validator than the
|
|
51
|
-
// this library currently uses.
|
|
88
|
+
// you provide a call-compatible validator. This is here primarily to support
|
|
89
|
+
// some migrations of old code that is using a different validator than the
|
|
90
|
+
// one this library currently uses.
|
|
52
91
|
validator = validator ??= validate;
|
|
53
92
|
|
|
54
93
|
// Use the Hono factory to create our middleware, just as a caller would.
|
|
55
|
-
// Create a middleware using the Hono factory method for this, using the
|
|
56
|
-
// schema object and data type provided.
|
|
57
94
|
const middleware = validator(dataType, schema);
|
|
58
95
|
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
// generates a response, so that we can put it into the response object.
|
|
96
|
+
// A successful test captures the validated and masked JSON output, while a
|
|
97
|
+
// failed test generates a failure JSON response and has a specific status
|
|
98
|
+
// as a result of the validator's call to fail().
|
|
63
99
|
let validData = null;
|
|
64
100
|
let errorResponse = null;
|
|
65
101
|
let responseStatus = 200;
|
|
66
102
|
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
103
|
+
// In order to handle formdata, cookie, and header validation we need a
|
|
104
|
+
// request object to put into the context. These portions are parsed out of
|
|
105
|
+
// the response by the validator and thus can't be backfilled. This also
|
|
106
|
+
// ensures that for formData we get a proper form encoded body.
|
|
107
|
+
const options = { method: 'POST' };
|
|
108
|
+
if (dataType === 'form') {
|
|
109
|
+
// For form data, turn the passed in object into FormData and add it to
|
|
110
|
+
// the body.
|
|
111
|
+
options.body = new FormData();
|
|
112
|
+
Object.entries(data).forEach(([k, v]) => options.body.append(k, v));
|
|
113
|
+
|
|
114
|
+
} else if (dataType === 'cookie') {
|
|
115
|
+
// If we are testing cookies, we need a cookie header
|
|
116
|
+
options.headers = { 'Cookie': Object.entries(data).map(([k,v]) => `${k}=${v}`).join('; ') }
|
|
117
|
+
|
|
118
|
+
} else if (dataType === 'header') {
|
|
119
|
+
// If we are testing a header, we need actual headers.
|
|
120
|
+
options.headers = data;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Create the response now.
|
|
124
|
+
const rawRequest = new Request('http://localhost/', options)
|
|
125
|
+
|
|
126
|
+
// Construct a mock Hono context object to pass to the middleware. We have
|
|
127
|
+
// here a mix of functions that the validator will call to get data that Hono
|
|
128
|
+
// has already processed or should process, such as the JSON body or the
|
|
129
|
+
// mapped request URI paramters, as well as a raw Request object for things
|
|
130
|
+
// that Hono does not tend to parse, such as form data and headers.
|
|
74
131
|
const ctx = {
|
|
75
132
|
req: {
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
133
|
+
// The raw request; used by form data, headers, and cookies.
|
|
134
|
+
raw: rawRequest,
|
|
135
|
+
|
|
136
|
+
// These methods in the context convey information that Hono parses as a
|
|
137
|
+
// part of its request handling; as such we can return the data back
|
|
138
|
+
// directly.
|
|
79
139
|
param: () => data,
|
|
80
140
|
json: async () => data,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
141
|
+
|
|
142
|
+
// Query paramters must always return the value of a key as an array
|
|
143
|
+
// since they can appear more than once; also, if you provide no key, you
|
|
144
|
+
// get them all. We're precomputing here for no good reason.
|
|
145
|
+
queries: (() => {
|
|
146
|
+
const result = Object.entries(data).reduce((acc, [key, value]) => {
|
|
147
|
+
acc[key] = Array.isArray(value) ? value : [value];
|
|
148
|
+
return acc;
|
|
149
|
+
}, {});
|
|
150
|
+
|
|
151
|
+
return key => key ? result[key] : result;
|
|
152
|
+
})(),
|
|
153
|
+
|
|
154
|
+
// For form data, the validator expects to be able to get at the raw body
|
|
155
|
+
// and a place to cache the parsed body data.
|
|
156
|
+
arrayBuffer: async () => rawRequest.arrayBuffer(),
|
|
157
|
+
bodyCache: {},
|
|
158
|
+
|
|
159
|
+
// The context supports gathering either a single header by name, or all
|
|
160
|
+
// headers (by passing undefined as a name.
|
|
161
|
+
header: name => {
|
|
97
162
|
if (name === undefined) {
|
|
98
163
|
return data;
|
|
99
164
|
}
|
|
100
165
|
|
|
101
166
|
return name.toLowerCase() !== 'content-type' ? undefined : {
|
|
102
167
|
json: 'application/json',
|
|
103
|
-
form: '
|
|
168
|
+
form: rawRequest.headers.get('Content-Type'),
|
|
104
169
|
}[dataType];
|
|
105
170
|
},
|
|
106
171
|
|
|
107
|
-
//
|
|
108
|
-
// the
|
|
172
|
+
// The validator invokes this to store the validated data back to the
|
|
173
|
+
// context; here we just capture it as the validated data for later
|
|
174
|
+
// return.
|
|
109
175
|
addValidatedData: (target, data) => validData = data
|
|
110
176
|
},
|
|
111
177
|
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
178
|
+
// If a failure occurs, the validator should call fail(), which invokes
|
|
179
|
+
// thee two endpoints to place an error status and JSON payload into the
|
|
180
|
+
// response. Here we just create an actual response object, since that is
|
|
181
|
+
// what the middleware would return.
|
|
182
|
+
status: status => responseStatus = status,
|
|
183
|
+
json: payload => {
|
|
117
184
|
errorResponse = new Response(
|
|
118
185
|
JSON.stringify(payload), {
|
|
119
186
|
status: responseStatus,
|
|
120
|
-
statusText:
|
|
187
|
+
statusText: STATUS_TEXT[responseStatus] ?? 'Unknown Error',
|
|
121
188
|
headers: { "Content-Type": "application/json" }
|
|
122
189
|
}
|
|
123
190
|
);
|
|
124
191
|
},
|
|
125
192
|
};
|
|
126
193
|
|
|
194
|
+
// Execute the middleware with an empty next().
|
|
127
195
|
// Run the middleware; we either capture a result in the error payload or the
|
|
128
196
|
// validation result.
|
|
129
|
-
await middleware(ctx,
|
|
197
|
+
await middleware(ctx, () => {});
|
|
130
198
|
|
|
131
199
|
// Return the error payload if validation failed, otherwise return the
|
|
132
200
|
// validated data from the success path.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@odatnurd/cf-requests",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Simple Cloudflare Hono request wrapper",
|
|
5
5
|
"author": "OdatNurd (https://odatnurd.net)",
|
|
6
6
|
"homepage": "https://github.com/OdatNurd/cf-requests",
|
|
@@ -26,10 +26,34 @@
|
|
|
26
26
|
"routing",
|
|
27
27
|
"aegis"
|
|
28
28
|
],
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@axel669/aegis": "^0.3.1",
|
|
31
|
+
"@axel669/joker": "^0.3.5",
|
|
32
|
+
"@odatnurd/cf-aegis": "^0.1.2",
|
|
33
|
+
"miniflare": "^4.20250813.0"
|
|
34
|
+
},
|
|
29
35
|
"peerDependencies": {
|
|
30
|
-
"
|
|
36
|
+
"@axel669/aegis": "^0.3.1",
|
|
37
|
+
"@axel669/joker": "^0.3.5",
|
|
38
|
+
"@odatnurd/cf-aegis": "^0.1.2",
|
|
39
|
+
"hono": "^4.7.0",
|
|
40
|
+
"miniflare": "^4.20250813.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"@axel669/aegis": {
|
|
44
|
+
"optional": true
|
|
45
|
+
},
|
|
46
|
+
"@axel669/joker": {
|
|
47
|
+
"optional": true
|
|
48
|
+
},
|
|
49
|
+
"@odatnurd/cf-aegis": {
|
|
50
|
+
"optional": true
|
|
51
|
+
},
|
|
52
|
+
"miniflare": {
|
|
53
|
+
"optional": true
|
|
54
|
+
}
|
|
31
55
|
},
|
|
32
56
|
"scripts": {
|
|
33
|
-
"test": "
|
|
57
|
+
"test": "aegis test/aegis.config.js"
|
|
34
58
|
}
|
|
35
59
|
}
|