@odatnurd/cf-requests 0.1.6 → 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 +107 -78
- 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,126 +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
|
-
// we build the context, so that both the header and the body can be derived
|
|
73
|
-
// from the same source, ensuring the multipart boundary matches.
|
|
74
|
-
let tempRequest = null;
|
|
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' };
|
|
75
108
|
if (dataType === 'form') {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
});
|
|
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;
|
|
84
121
|
}
|
|
85
122
|
|
|
123
|
+
// Create the response now.
|
|
124
|
+
const rawRequest = new Request('http://localhost/', options)
|
|
86
125
|
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
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.
|
|
90
131
|
const ctx = {
|
|
91
132
|
req: {
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
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.
|
|
95
139
|
param: () => data,
|
|
96
140
|
json: async () => data,
|
|
97
|
-
query: (key) => data[key],
|
|
98
|
-
queries: (key) => {
|
|
99
|
-
const result = {};
|
|
100
|
-
for(const [k, v] of Object.entries(data)) {
|
|
101
|
-
result[k] = Array.isArray(v) ? v : [v];
|
|
102
|
-
}
|
|
103
|
-
return key ? result[key] : result;
|
|
104
|
-
},
|
|
105
|
-
cookie: () => data,
|
|
106
|
-
formData: async () => {
|
|
107
|
-
if (dataType === 'form') {
|
|
108
|
-
return tempRequest.formData();
|
|
109
|
-
}
|
|
110
|
-
// Fallback for other types, though not strictly needed by the validator
|
|
111
|
-
const formData = new FormData();
|
|
112
|
-
for (const key in data) {
|
|
113
|
-
formData.append(key, data[key]);
|
|
114
|
-
}
|
|
115
|
-
return formData;
|
|
116
|
-
},
|
|
117
|
-
// For form data, the validator expects to be able to get the raw body
|
|
118
|
-
// as an ArrayBuffer. We can simulate this by URL-encoding the data.
|
|
119
|
-
arrayBuffer: async () => tempRequest ? tempRequest.arrayBuffer() : new ArrayBuffer(0),
|
|
120
|
-
// The validator also uses a bodyCache property to store parsed bodies.
|
|
121
|
-
bodyCache: {},
|
|
122
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: {},
|
|
123
158
|
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
headers: new Headers(dataType === 'cookie'
|
|
128
|
-
? { 'Cookie': Object.entries(data).map(([k,v]) => `${k}=${v}`).join('; ') }
|
|
129
|
-
: {}),
|
|
130
|
-
|
|
131
|
-
// The validator invokes this to get headers out of the request when the
|
|
132
|
-
// data type is JSON.
|
|
133
|
-
header: (name) => {
|
|
134
|
-
// If there is no name, return the data back directly; this call pattern
|
|
135
|
-
// happens when the data type is header.
|
|
159
|
+
// The context supports gathering either a single header by name, or all
|
|
160
|
+
// headers (by passing undefined as a name.
|
|
161
|
+
header: name => {
|
|
136
162
|
if (name === undefined) {
|
|
137
163
|
return data;
|
|
138
164
|
}
|
|
139
165
|
|
|
140
166
|
return name.toLowerCase() !== 'content-type' ? undefined : {
|
|
141
167
|
json: 'application/json',
|
|
142
|
-
form:
|
|
168
|
+
form: rawRequest.headers.get('Content-Type'),
|
|
143
169
|
}[dataType];
|
|
144
170
|
},
|
|
145
171
|
|
|
146
|
-
//
|
|
147
|
-
// 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.
|
|
148
175
|
addValidatedData: (target, data) => validData = data
|
|
149
176
|
},
|
|
150
177
|
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
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 => {
|
|
156
184
|
errorResponse = new Response(
|
|
157
185
|
JSON.stringify(payload), {
|
|
158
186
|
status: responseStatus,
|
|
159
|
-
statusText:
|
|
187
|
+
statusText: STATUS_TEXT[responseStatus] ?? 'Unknown Error',
|
|
160
188
|
headers: { "Content-Type": "application/json" }
|
|
161
189
|
}
|
|
162
190
|
);
|
|
163
191
|
},
|
|
164
192
|
};
|
|
165
193
|
|
|
194
|
+
// Execute the middleware with an empty next().
|
|
166
195
|
// Run the middleware; we either capture a result in the error payload or the
|
|
167
196
|
// validation result.
|
|
168
|
-
await middleware(ctx,
|
|
197
|
+
await middleware(ctx, () => {});
|
|
169
198
|
|
|
170
199
|
// Return the error payload if validation failed, otherwise return the
|
|
171
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
|
}
|