@vouchington/request-contract-validation 0.0.1
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/LICENSE +21 -0
- package/README.md +22 -0
- package/dist/index.d.mts +18 -0
- package/dist/index.mjs +80 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jonathan Ong
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# @vouchington/request-contract-validation
|
|
2
|
+
|
|
3
|
+
Compile a caller supplied JSON Schema request bundle once, then validate body, header, path, and
|
|
4
|
+
query carriers with Ajv and standard formats. Unknown operations throw; a known operation without
|
|
5
|
+
a schema for a carrier accepts that carrier unchanged. Only supplied carriers are checked by
|
|
6
|
+
`validateOperation`.
|
|
7
|
+
The API is synchronous; asynchronous Ajv validators are rejected during construction.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { RequestContractValidatorRegistry } from '@vouchington/request-contract-validation'
|
|
11
|
+
|
|
12
|
+
const registry = new RequestContractValidatorRegistry({
|
|
13
|
+
components: {},
|
|
14
|
+
operations: { 'POST:/items': { body: { type: 'object', required: ['name'] } } },
|
|
15
|
+
})
|
|
16
|
+
const error = registry.validateOperation('POST:/items', { body: { name: 'example' } })
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Applications supply generated or hand-written contracts and decide authentication, authorization,
|
|
20
|
+
rate limits, request lifecycle, HTTP status, and error mapping. Header names in object inputs are
|
|
21
|
+
lowercased before validation; case-insensitive duplicates are rejected when a header contract is
|
|
22
|
+
present. This package has no generated bundle or shared registry.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type RequestCarrier = 'body' | 'header' | 'path' | 'query';
|
|
2
|
+
export type RequestContract = Partial<Record<RequestCarrier, unknown>>;
|
|
3
|
+
export type RequestContractsBundle = {
|
|
4
|
+
components: Record<string, unknown>;
|
|
5
|
+
operations: Record<string, RequestContract>;
|
|
6
|
+
};
|
|
7
|
+
export type RequestValidationError = {
|
|
8
|
+
message: string;
|
|
9
|
+
};
|
|
10
|
+
/** Compiles caller-owned request contracts once; callers choose the request boundary and error policy. */
|
|
11
|
+
export declare class RequestContractValidatorRegistry {
|
|
12
|
+
private readonly validators;
|
|
13
|
+
constructor(bundle: RequestContractsBundle);
|
|
14
|
+
hasOperation(operation: string): boolean;
|
|
15
|
+
validateBody(operation: string, value: unknown): RequestValidationError | null;
|
|
16
|
+
validate(operation: string, carrier: RequestCarrier, value: unknown): RequestValidationError | null;
|
|
17
|
+
validateOperation(operation: string, input: Partial<Record<RequestCarrier, unknown>>): RequestValidationError | null;
|
|
18
|
+
}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import Ajv from 'ajv';
|
|
2
|
+
import addFormats from 'ajv-formats';
|
|
3
|
+
const AjvCtor = Ajv;
|
|
4
|
+
const installFormats = addFormats;
|
|
5
|
+
const requestCarriers = ['body', 'header', 'path', 'query'];
|
|
6
|
+
const duplicateHeaderNames = Symbol('duplicate header names');
|
|
7
|
+
/** Compiles caller-owned request contracts once; callers choose the request boundary and error policy. */
|
|
8
|
+
export class RequestContractValidatorRegistry {
|
|
9
|
+
validators = new Map();
|
|
10
|
+
constructor(bundle) {
|
|
11
|
+
if (!bundle || !isRecord(bundle.components) || !isRecord(bundle.operations)) {
|
|
12
|
+
throw new TypeError('Request contract bundle needs components and operations maps');
|
|
13
|
+
}
|
|
14
|
+
const ajv = new AjvCtor({ allErrors: false, strict: false, strictSchema: true });
|
|
15
|
+
installFormats(ajv);
|
|
16
|
+
for (const [name, schema] of Object.entries(bundle.components)) {
|
|
17
|
+
ajv.addSchema(schema, `#/components/schemas/${name}`);
|
|
18
|
+
}
|
|
19
|
+
for (const [operation, contract] of Object.entries(bundle.operations)) {
|
|
20
|
+
if (!isRecord(contract))
|
|
21
|
+
throw new TypeError(`Invalid request contract for ${operation}`);
|
|
22
|
+
const carriers = {};
|
|
23
|
+
for (const carrier of requestCarriers) {
|
|
24
|
+
if (Object.hasOwn(contract, carrier)) {
|
|
25
|
+
const validator = ajv.compile(contract[carrier]);
|
|
26
|
+
if ('$async' in validator && validator.$async) {
|
|
27
|
+
throw new TypeError('Asynchronous request contracts are not supported');
|
|
28
|
+
}
|
|
29
|
+
carriers[carrier] = validator;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
this.validators.set(operation, carriers);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
hasOperation(operation) {
|
|
36
|
+
return this.validators.has(operation);
|
|
37
|
+
}
|
|
38
|
+
validateBody(operation, value) {
|
|
39
|
+
return this.validate(operation, 'body', value);
|
|
40
|
+
}
|
|
41
|
+
validate(operation, carrier, value) {
|
|
42
|
+
const validators = this.validators.get(operation);
|
|
43
|
+
if (!validators)
|
|
44
|
+
throw new Error(`No request contract for ${operation}`);
|
|
45
|
+
const validator = validators[carrier];
|
|
46
|
+
if (!validator)
|
|
47
|
+
return null;
|
|
48
|
+
const normalized = normalizeCarrierValue(carrier, value);
|
|
49
|
+
if (normalized !== duplicateHeaderNames && validator(normalized))
|
|
50
|
+
return null;
|
|
51
|
+
return { message: `Invalid request ${carrier}` };
|
|
52
|
+
}
|
|
53
|
+
validateOperation(operation, input) {
|
|
54
|
+
if (!this.hasOperation(operation))
|
|
55
|
+
throw new Error(`No request contract for ${operation}`);
|
|
56
|
+
for (const carrier of requestCarriers) {
|
|
57
|
+
if (!Object.hasOwn(input, carrier))
|
|
58
|
+
continue;
|
|
59
|
+
const error = this.validate(operation, carrier, input[carrier]);
|
|
60
|
+
if (error)
|
|
61
|
+
return error;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function isRecord(value) {
|
|
67
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
68
|
+
}
|
|
69
|
+
function normalizeCarrierValue(carrier, value) {
|
|
70
|
+
if (carrier !== 'header' || !isRecord(value))
|
|
71
|
+
return value;
|
|
72
|
+
const normalized = new Map();
|
|
73
|
+
for (const [name, headerValue] of Object.entries(value)) {
|
|
74
|
+
const key = name.toLowerCase();
|
|
75
|
+
if (normalized.has(key))
|
|
76
|
+
return duplicateHeaderNames;
|
|
77
|
+
normalized.set(key, headerValue);
|
|
78
|
+
}
|
|
79
|
+
return Object.fromEntries(normalized);
|
|
80
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vouchington/request-contract-validation",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Bundle-injected runtime validation of request contract carriers.",
|
|
5
|
+
"homepage": "https://github.com/vouchington/vouchington-platform/tree/main/packages/request-contract-validation#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/vouchington/vouchington-platform/issues"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "Jonathan Ong",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/vouchington/vouchington-platform.git",
|
|
14
|
+
"directory": "packages/request-contract-validation"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "./dist/index.mjs",
|
|
23
|
+
"types": "./dist/index.d.mts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.mts",
|
|
27
|
+
"import": "./dist/index.mjs",
|
|
28
|
+
"default": "./dist/index.mjs"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc --project tsconfig.build.json",
|
|
36
|
+
"prepack": "pnpm run build"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"ajv": "^8.20.0",
|
|
40
|
+
"ajv-formats": "^3.0.1"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=24.0.0"
|
|
44
|
+
}
|
|
45
|
+
}
|