alepha 0.15.0 → 0.15.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/README.md +43 -98
- package/dist/api/audits/index.d.ts +240 -240
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/audits/index.js +2 -2
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.d.ts +185 -185
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js +2 -2
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.d.ts +245 -245
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/notifications/index.browser.js +4 -4
- package/dist/api/notifications/index.browser.js.map +1 -1
- package/dist/api/notifications/index.d.ts +74 -74
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js +4 -4
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/parameters/index.d.ts +221 -221
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/users/index.d.ts +1632 -1631
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +26 -34
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +132 -132
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/batch/index.d.ts +122 -122
- package/dist/batch/index.d.ts.map +1 -1
- package/dist/bucket/index.d.ts +163 -163
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/cache/core/index.d.ts +46 -46
- package/dist/cache/core/index.d.ts.map +1 -1
- package/dist/cache/redis/index.d.ts.map +1 -1
- package/dist/cache/redis/index.js +2 -2
- package/dist/cache/redis/index.js.map +1 -1
- package/dist/cli/index.d.ts +5933 -201
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +609 -169
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +296 -296
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +19 -19
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +268 -79
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +768 -694
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +268 -79
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +268 -79
- package/dist/core/index.native.js.map +1 -1
- package/dist/datetime/index.d.ts +44 -44
- package/dist/datetime/index.d.ts.map +1 -1
- package/dist/email/index.d.ts +25 -25
- package/dist/email/index.d.ts.map +1 -1
- package/dist/fake/index.d.ts +5409 -5409
- package/dist/fake/index.d.ts.map +1 -1
- package/dist/fake/index.js +22 -22
- package/dist/fake/index.js.map +1 -1
- package/dist/file/index.d.ts +435 -435
- package/dist/file/index.d.ts.map +1 -1
- package/dist/lock/core/index.d.ts +208 -208
- package/dist/lock/core/index.d.ts.map +1 -1
- package/dist/lock/redis/index.d.ts.map +1 -1
- package/dist/logger/index.d.ts +24 -24
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js +1 -5
- package/dist/logger/index.js.map +1 -1
- package/dist/mcp/index.d.ts +216 -198
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +28 -4
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/index.browser.js +9 -9
- package/dist/orm/index.browser.js.map +1 -1
- package/dist/orm/index.bun.js +83 -76
- package/dist/orm/index.bun.js.map +1 -1
- package/dist/orm/index.d.ts +961 -960
- package/dist/orm/index.d.ts.map +1 -1
- package/dist/orm/index.js +88 -81
- package/dist/orm/index.js.map +1 -1
- package/dist/queue/core/index.d.ts +244 -244
- package/dist/queue/core/index.d.ts.map +1 -1
- package/dist/queue/redis/index.d.ts.map +1 -1
- package/dist/redis/index.d.ts +105 -105
- package/dist/redis/index.d.ts.map +1 -1
- package/dist/retry/index.d.ts +69 -69
- package/dist/retry/index.d.ts.map +1 -1
- package/dist/router/index.d.ts +6 -6
- package/dist/router/index.d.ts.map +1 -1
- package/dist/scheduler/index.d.ts +108 -26
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +393 -1
- package/dist/scheduler/index.js.map +1 -1
- package/dist/security/index.d.ts +532 -209
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +1422 -11
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +1296 -271
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +1249 -18
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cache/index.d.ts +56 -56
- package/dist/server/cache/index.d.ts.map +1 -1
- package/dist/server/compress/index.d.ts +3 -3
- package/dist/server/compress/index.d.ts.map +1 -1
- package/dist/server/cookies/index.d.ts +6 -6
- package/dist/server/cookies/index.d.ts.map +1 -1
- package/dist/server/core/index.d.ts +196 -186
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js +43 -27
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/cors/index.d.ts +11 -11
- package/dist/server/cors/index.d.ts.map +1 -1
- package/dist/server/health/index.d.ts.map +1 -1
- package/dist/server/helmet/index.d.ts +2 -2
- package/dist/server/helmet/index.d.ts.map +1 -1
- package/dist/server/links/index.browser.js +9 -1
- package/dist/server/links/index.browser.js.map +1 -1
- package/dist/server/links/index.d.ts +83 -83
- package/dist/server/links/index.d.ts.map +1 -1
- package/dist/server/links/index.js +13 -5
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/metrics/index.d.ts +514 -1
- package/dist/server/metrics/index.d.ts.map +1 -1
- package/dist/server/metrics/index.js +4462 -4
- package/dist/server/metrics/index.js.map +1 -1
- package/dist/server/multipart/index.d.ts +6 -6
- package/dist/server/multipart/index.d.ts.map +1 -1
- package/dist/server/proxy/index.d.ts +102 -102
- package/dist/server/proxy/index.d.ts.map +1 -1
- package/dist/server/rate-limit/index.d.ts +16 -16
- package/dist/server/rate-limit/index.d.ts.map +1 -1
- package/dist/server/static/index.d.ts +44 -44
- package/dist/server/static/index.d.ts.map +1 -1
- package/dist/server/swagger/index.d.ts +47 -47
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/sms/index.d.ts +11 -11
- package/dist/sms/index.d.ts.map +1 -1
- package/dist/sms/index.js +3 -3
- package/dist/sms/index.js.map +1 -1
- package/dist/thread/index.d.ts +71 -71
- package/dist/thread/index.d.ts.map +1 -1
- package/dist/thread/index.js +2 -2
- package/dist/thread/index.js.map +1 -1
- package/dist/topic/core/index.d.ts +318 -318
- package/dist/topic/core/index.d.ts.map +1 -1
- package/dist/topic/redis/index.d.ts +6 -6
- package/dist/topic/redis/index.d.ts.map +1 -1
- package/dist/vite/index.d.ts +2324 -1719
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +123 -475
- package/dist/vite/index.js.map +1 -1
- package/dist/websocket/index.browser.js +3 -3
- package/dist/websocket/index.browser.js.map +1 -1
- package/dist/websocket/index.d.ts +275 -275
- package/dist/websocket/index.d.ts.map +1 -1
- package/dist/websocket/index.js +3 -3
- package/dist/websocket/index.js.map +1 -1
- package/package.json +9 -9
- package/src/api/users/services/SessionService.ts +0 -10
- package/src/cli/apps/AlephaCli.ts +2 -2
- package/src/cli/apps/AlephaPackageBuilderCli.ts +9 -1
- package/src/cli/assets/apiHelloControllerTs.ts +2 -1
- package/src/cli/assets/biomeJson.ts +2 -1
- package/src/cli/assets/claudeMd.ts +9 -4
- package/src/cli/assets/dummySpecTs.ts +2 -1
- package/src/cli/assets/editorconfig.ts +2 -1
- package/src/cli/assets/mainBrowserTs.ts +2 -1
- package/src/cli/assets/mainCss.ts +24 -0
- package/src/cli/assets/tsconfigJson.ts +2 -1
- package/src/cli/assets/webAppRouterTs.ts +2 -1
- package/src/cli/assets/webHelloComponentTsx.ts +6 -2
- package/src/cli/atoms/appEntryOptions.ts +13 -0
- package/src/cli/atoms/buildOptions.ts +1 -1
- package/src/cli/atoms/changelogOptions.ts +1 -1
- package/src/cli/commands/build.ts +63 -47
- package/src/cli/commands/dev.ts +16 -33
- package/src/cli/commands/gen/env.ts +1 -1
- package/src/cli/commands/init.ts +17 -8
- package/src/cli/commands/lint.ts +1 -1
- package/src/cli/defineConfig.ts +9 -0
- package/src/cli/index.ts +2 -1
- package/src/cli/providers/AppEntryProvider.ts +131 -0
- package/src/cli/providers/ViteBuildProvider.ts +82 -0
- package/src/cli/providers/ViteDevServerProvider.ts +350 -0
- package/src/cli/providers/ViteTemplateProvider.ts +27 -0
- package/src/cli/services/AlephaCliUtils.ts +33 -2
- package/src/cli/services/PackageManagerUtils.ts +13 -6
- package/src/cli/services/ProjectScaffolder.ts +72 -49
- package/src/core/Alepha.ts +2 -8
- package/src/core/primitives/$module.ts +12 -0
- package/src/core/providers/KeylessJsonSchemaCodec.spec.ts +257 -0
- package/src/core/providers/KeylessJsonSchemaCodec.ts +396 -14
- package/src/core/providers/SchemaValidator.spec.ts +236 -0
- package/src/logger/providers/PrettyFormatterProvider.ts +0 -9
- package/src/mcp/errors/McpError.ts +30 -0
- package/src/mcp/index.ts +3 -0
- package/src/mcp/transports/SseMcpTransport.ts +16 -6
- package/src/orm/providers/DrizzleKitProvider.ts +3 -5
- package/src/orm/services/Repository.ts +11 -0
- package/src/server/core/index.ts +1 -1
- package/src/server/core/providers/BunHttpServerProvider.ts +1 -1
- package/src/server/core/providers/NodeHttpServerProvider.spec.ts +125 -0
- package/src/server/core/providers/NodeHttpServerProvider.ts +71 -22
- package/src/server/core/providers/ServerLoggerProvider.ts +2 -2
- package/src/server/core/providers/ServerProvider.ts +9 -12
- package/src/server/links/atoms/apiLinksAtom.ts +7 -0
- package/src/server/links/index.browser.ts +2 -0
- package/src/server/links/index.ts +2 -0
- package/src/vite/index.ts +3 -2
- package/src/vite/tasks/buildClient.ts +0 -1
- package/src/vite/tasks/buildServer.ts +68 -21
- package/src/vite/tasks/copyAssets.ts +5 -4
- package/src/vite/tasks/generateSitemap.ts +64 -23
- package/src/vite/tasks/index.ts +0 -2
- package/src/vite/tasks/prerenderPages.ts +49 -24
- package/src/cli/assets/indexHtml.ts +0 -15
- package/src/cli/commands/format.ts +0 -23
- package/src/vite/helpers/boot.ts +0 -117
- package/src/vite/plugins/viteAlephaDev.ts +0 -177
- package/src/vite/tasks/devServer.ts +0 -71
- package/src/vite/tasks/runAlepha.ts +0 -270
- /package/dist/orm/{chunk-DtkW-qnP.js → chunk-DH6iiROE.js} +0 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { TArray, TObject, TSchema, TUnion } from "typebox";
|
|
2
|
+
import { AlephaError } from "../errors/AlephaError.ts";
|
|
3
|
+
import { $hook } from "../primitives/$hook.ts";
|
|
2
4
|
import { SchemaCodec } from "./SchemaCodec.ts";
|
|
3
5
|
import { type Static, t } from "./TypeProvider.ts";
|
|
4
6
|
|
|
@@ -16,28 +18,99 @@ import { type Static, t } from "./TypeProvider.ts";
|
|
|
16
18
|
// Keyless: ["Alice",30,true] (17 bytes)
|
|
17
19
|
// =============================================================================
|
|
18
20
|
|
|
21
|
+
// Security: Keys that could enable prototype pollution attacks
|
|
22
|
+
const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
23
|
+
|
|
19
24
|
export interface KeylessCodec<T = any> {
|
|
20
25
|
encode: (value: T) => string;
|
|
21
26
|
decode: (str: string) => T;
|
|
22
27
|
}
|
|
23
28
|
|
|
29
|
+
export interface KeylessCodecOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Whether to use `new Function()` for code compilation.
|
|
32
|
+
* When false, uses an interpreter-based approach (safer but slower).
|
|
33
|
+
*
|
|
34
|
+
* @default Auto-detected: false in browser (CSP compatibility), true on server
|
|
35
|
+
*/
|
|
36
|
+
useFunctionCompilation?: boolean;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Maximum allowed array length during encoding/decoding.
|
|
40
|
+
* Prevents DoS attacks via large arrays.
|
|
41
|
+
*
|
|
42
|
+
* @default 10000
|
|
43
|
+
*/
|
|
44
|
+
maxArrayLength?: number;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Maximum allowed string length during encoding/decoding.
|
|
48
|
+
* Prevents DoS attacks via large strings.
|
|
49
|
+
*
|
|
50
|
+
* @default 1000000 (1MB)
|
|
51
|
+
*/
|
|
52
|
+
maxStringLength?: number;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Maximum recursion depth for nested objects.
|
|
56
|
+
* Prevents stack overflow attacks.
|
|
57
|
+
*
|
|
58
|
+
* @default 50
|
|
59
|
+
*/
|
|
60
|
+
maxDepth?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
24
63
|
/**
|
|
25
64
|
* KeylessJsonSchemaCodec provides schema-driven JSON encoding without keys.
|
|
26
65
|
*
|
|
27
66
|
* It uses the schema to determine field order, allowing the encoded output
|
|
28
67
|
* to be a simple JSON array instead of an object with keys.
|
|
29
|
-
*
|
|
30
|
-
* Performance characteristics:
|
|
31
|
-
* - Encode: 0.94-1.53x vs JSON.stringify (faster for complex objects)
|
|
32
|
-
* - Decode: 1.76-2.00x vs JSON.parse
|
|
33
|
-
* - Size: 50-56% smaller than JSON
|
|
34
68
|
*/
|
|
35
69
|
export class KeylessJsonSchemaCodec extends SchemaCodec {
|
|
36
70
|
protected readonly cache = new Map<TSchema, KeylessCodec>();
|
|
37
|
-
protected readonly
|
|
38
|
-
protected readonly
|
|
71
|
+
protected readonly textEncoder = new TextEncoder();
|
|
72
|
+
protected readonly textDecoder = new TextDecoder();
|
|
39
73
|
protected varCounter = 0;
|
|
40
74
|
|
|
75
|
+
// Options with defaults
|
|
76
|
+
protected useFunctionCompilation = true;
|
|
77
|
+
protected maxArrayLength = 10000;
|
|
78
|
+
protected maxStringLength = 1000000;
|
|
79
|
+
protected maxDepth = 50;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Configure codec options.
|
|
83
|
+
*/
|
|
84
|
+
public configure(options: KeylessCodecOptions): this {
|
|
85
|
+
if (options.useFunctionCompilation !== undefined) {
|
|
86
|
+
this.useFunctionCompilation = options.useFunctionCompilation;
|
|
87
|
+
this.cache.clear(); // Clear cache when compilation mode changes
|
|
88
|
+
}
|
|
89
|
+
if (options.maxArrayLength !== undefined) {
|
|
90
|
+
this.maxArrayLength = options.maxArrayLength;
|
|
91
|
+
}
|
|
92
|
+
if (options.maxStringLength !== undefined) {
|
|
93
|
+
this.maxStringLength = options.maxStringLength;
|
|
94
|
+
}
|
|
95
|
+
if (options.maxDepth !== undefined) {
|
|
96
|
+
this.maxDepth = options.maxDepth;
|
|
97
|
+
}
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Hook to auto-detect safe mode on configure.
|
|
103
|
+
* Disables function compilation in browser by default.
|
|
104
|
+
*/
|
|
105
|
+
protected onConfigure = $hook({
|
|
106
|
+
on: "configure",
|
|
107
|
+
handler: () => {
|
|
108
|
+
// Auto-detect: disable function compilation in browser (CSP compatibility)
|
|
109
|
+
// and test if eval/Function is available
|
|
110
|
+
this.useFunctionCompilation = this.canUseFunction();
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
41
114
|
/**
|
|
42
115
|
* Encode value to a keyless JSON string.
|
|
43
116
|
*/
|
|
@@ -55,7 +128,7 @@ export class KeylessJsonSchemaCodec extends SchemaCodec {
|
|
|
55
128
|
schema: T,
|
|
56
129
|
value: Static<T>,
|
|
57
130
|
): Uint8Array {
|
|
58
|
-
return this.
|
|
131
|
+
return this.textEncoder.encode(this.encodeToString(schema, value));
|
|
59
132
|
}
|
|
60
133
|
|
|
61
134
|
/**
|
|
@@ -63,7 +136,7 @@ export class KeylessJsonSchemaCodec extends SchemaCodec {
|
|
|
63
136
|
*/
|
|
64
137
|
public decode<T>(schema: TSchema, value: unknown): T {
|
|
65
138
|
if (value instanceof Uint8Array) {
|
|
66
|
-
const text = this.
|
|
139
|
+
const text = this.textDecoder.decode(value);
|
|
67
140
|
return this.getCodec(schema).decode(text) as T;
|
|
68
141
|
}
|
|
69
142
|
|
|
@@ -79,6 +152,88 @@ export class KeylessJsonSchemaCodec extends SchemaCodec {
|
|
|
79
152
|
return value as T;
|
|
80
153
|
}
|
|
81
154
|
|
|
155
|
+
// ===========================================================================
|
|
156
|
+
// Security Validation
|
|
157
|
+
// ===========================================================================
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Test if `new Function()` is available (not blocked by CSP).
|
|
161
|
+
*/
|
|
162
|
+
protected canUseFunction(): boolean {
|
|
163
|
+
try {
|
|
164
|
+
const fn = new Function("return true");
|
|
165
|
+
return fn() === true;
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Validate schema keys for prototype pollution.
|
|
173
|
+
* Uses a visited set to avoid infinite recursion on recursive schemas.
|
|
174
|
+
*/
|
|
175
|
+
protected validateSchemaKeys(
|
|
176
|
+
schema: TSchema,
|
|
177
|
+
depth = 0,
|
|
178
|
+
visited = new Set<TSchema>(),
|
|
179
|
+
): void {
|
|
180
|
+
// Avoid infinite recursion on recursive schemas
|
|
181
|
+
if (visited.has(schema)) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
visited.add(schema);
|
|
185
|
+
|
|
186
|
+
if (depth > this.maxDepth) {
|
|
187
|
+
throw new AlephaError(
|
|
188
|
+
`Schema depth exceeds maximum allowed (${this.maxDepth})`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (t.schema.isObject(schema)) {
|
|
193
|
+
const objSchema = schema as TObject;
|
|
194
|
+
const props = objSchema.properties as Record<string, TSchema>;
|
|
195
|
+
|
|
196
|
+
for (const key of Object.keys(props)) {
|
|
197
|
+
if (UNSAFE_KEYS.has(key)) {
|
|
198
|
+
throw new AlephaError(
|
|
199
|
+
`Unsafe schema key "${key}" detected. This key is blocked to prevent prototype pollution.`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
// Depth increases for object properties
|
|
203
|
+
this.validateSchemaKeys(props[key], depth + 1, visited);
|
|
204
|
+
}
|
|
205
|
+
} else if (t.schema.isArray(schema)) {
|
|
206
|
+
const arrSchema = schema as TArray;
|
|
207
|
+
// Depth increases for array items
|
|
208
|
+
this.validateSchemaKeys(arrSchema.items, depth + 1, visited);
|
|
209
|
+
} else if (t.schema.isUnion(schema) || t.schema.isOptional(schema)) {
|
|
210
|
+
// Optional/union wrappers don't increase depth - they're type modifiers
|
|
211
|
+
this.validateSchemaKeys(this.unwrap(schema), depth, visited);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Validate array length.
|
|
217
|
+
*/
|
|
218
|
+
protected validateArrayLength(arr: unknown[]): void {
|
|
219
|
+
if (arr.length > this.maxArrayLength) {
|
|
220
|
+
throw new AlephaError(
|
|
221
|
+
`Array length (${arr.length}) exceeds maximum allowed (${this.maxArrayLength})`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Validate string length.
|
|
228
|
+
*/
|
|
229
|
+
protected validateStringLength(str: string): void {
|
|
230
|
+
if (str.length > this.maxStringLength) {
|
|
231
|
+
throw new AlephaError(
|
|
232
|
+
`String length (${str.length}) exceeds maximum allowed (${this.maxStringLength})`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
82
237
|
// ===========================================================================
|
|
83
238
|
// Codec Compilation
|
|
84
239
|
// ===========================================================================
|
|
@@ -90,7 +245,9 @@ export class KeylessJsonSchemaCodec extends SchemaCodec {
|
|
|
90
245
|
protected getCodec<T>(schema: TSchema): KeylessCodec<T> {
|
|
91
246
|
let c = this.cache.get(schema);
|
|
92
247
|
if (!c) {
|
|
93
|
-
c = this.
|
|
248
|
+
c = this.useFunctionCompilation
|
|
249
|
+
? this.compileWithFunction(schema)
|
|
250
|
+
: this.compileInterpreted(schema);
|
|
94
251
|
this.cache.set(schema, c);
|
|
95
252
|
}
|
|
96
253
|
return c as KeylessCodec<T>;
|
|
@@ -100,7 +257,11 @@ export class KeylessJsonSchemaCodec extends SchemaCodec {
|
|
|
100
257
|
return `_${this.varCounter++}`;
|
|
101
258
|
}
|
|
102
259
|
|
|
103
|
-
|
|
260
|
+
/**
|
|
261
|
+
* Compile codec using `new Function()` for maximum performance.
|
|
262
|
+
* Only used when CSP allows and useFunctionCompilation is true.
|
|
263
|
+
*/
|
|
264
|
+
protected compileWithFunction(schema: TSchema): KeylessCodec {
|
|
104
265
|
this.varCounter = 0;
|
|
105
266
|
const encBody = this.genEnc(schema, "v");
|
|
106
267
|
|
|
@@ -119,6 +280,229 @@ export class KeylessJsonSchemaCodec extends SchemaCodec {
|
|
|
119
280
|
return { encode: encoder, decode: decoder };
|
|
120
281
|
}
|
|
121
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Compile codec using interpreter-based approach.
|
|
285
|
+
* Safer (no eval/Function) but slower. Used in browser by default.
|
|
286
|
+
*/
|
|
287
|
+
protected compileInterpreted(schema: TSchema): KeylessCodec {
|
|
288
|
+
const self = this;
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
encode(value: any): string {
|
|
292
|
+
return JSON.stringify(self.interpretEncode(schema, value));
|
|
293
|
+
},
|
|
294
|
+
decode(str: string): any {
|
|
295
|
+
const ctx = { arr: JSON.parse(str), i: 0 };
|
|
296
|
+
return self.interpretDecode(schema, ctx);
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ===========================================================================
|
|
302
|
+
// Interpreter-based Encoding (Safe Mode)
|
|
303
|
+
// ===========================================================================
|
|
304
|
+
|
|
305
|
+
protected interpretEncode(schema: TSchema, value: any): any {
|
|
306
|
+
if (
|
|
307
|
+
t.schema.isString(schema) ||
|
|
308
|
+
t.schema.isNumber(schema) ||
|
|
309
|
+
t.schema.isInteger(schema) ||
|
|
310
|
+
t.schema.isBoolean(schema) ||
|
|
311
|
+
this.isEnum(schema)
|
|
312
|
+
) {
|
|
313
|
+
return value;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (t.schema.isBigInt(schema)) {
|
|
317
|
+
return `${value}n`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (t.schema.isArray(schema)) {
|
|
321
|
+
const arrSchema = schema as TArray;
|
|
322
|
+
if (!Array.isArray(value)) return value;
|
|
323
|
+
|
|
324
|
+
if (
|
|
325
|
+
t.schema.isString(arrSchema.items) ||
|
|
326
|
+
t.schema.isNumber(arrSchema.items) ||
|
|
327
|
+
t.schema.isInteger(arrSchema.items) ||
|
|
328
|
+
t.schema.isBoolean(arrSchema.items)
|
|
329
|
+
) {
|
|
330
|
+
return value;
|
|
331
|
+
}
|
|
332
|
+
return value.map((e) => this.interpretEncode(arrSchema.items, e));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (t.schema.isObject(schema)) {
|
|
336
|
+
const objSchema = schema as TObject;
|
|
337
|
+
const props = objSchema.properties as Record<string, TSchema>;
|
|
338
|
+
const keys = Object.keys(props);
|
|
339
|
+
const req = new Set((objSchema.required as string[]) || []);
|
|
340
|
+
|
|
341
|
+
const result: any[] = [];
|
|
342
|
+
for (const k of keys) {
|
|
343
|
+
const ps = props[k];
|
|
344
|
+
const isOpt = !req.has(k) || t.schema.isOptional(ps);
|
|
345
|
+
const isNullable = this.isNullable(ps);
|
|
346
|
+
const inner = this.unwrap(ps);
|
|
347
|
+
const v = value[k];
|
|
348
|
+
|
|
349
|
+
if (isOpt) {
|
|
350
|
+
result.push(v !== undefined ? this.interpretEncode(inner, v) : null);
|
|
351
|
+
} else if (isNullable) {
|
|
352
|
+
result.push(v !== null ? this.interpretEncode(inner, v) : null);
|
|
353
|
+
} else {
|
|
354
|
+
result.push(this.interpretEncode(inner, v));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (t.schema.isOptional(schema) || t.schema.isUnion(schema)) {
|
|
361
|
+
const inner = this.unwrap(schema);
|
|
362
|
+
if (this.isNullable(schema)) {
|
|
363
|
+
return value !== null ? this.interpretEncode(inner, value) : null;
|
|
364
|
+
}
|
|
365
|
+
return value !== undefined ? this.interpretEncode(inner, value) : null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return value;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ===========================================================================
|
|
372
|
+
// Interpreter-based Decoding (Safe Mode)
|
|
373
|
+
// ===========================================================================
|
|
374
|
+
|
|
375
|
+
protected interpretDecode(
|
|
376
|
+
schema: TSchema,
|
|
377
|
+
ctx: { arr: any[]; i: number },
|
|
378
|
+
): any {
|
|
379
|
+
if (
|
|
380
|
+
t.schema.isString(schema) ||
|
|
381
|
+
t.schema.isNumber(schema) ||
|
|
382
|
+
t.schema.isInteger(schema) ||
|
|
383
|
+
t.schema.isBoolean(schema) ||
|
|
384
|
+
this.isEnum(schema)
|
|
385
|
+
) {
|
|
386
|
+
return ctx.arr[ctx.i++];
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (t.schema.isBigInt(schema)) {
|
|
390
|
+
return BigInt(ctx.arr[ctx.i++].slice(0, -1));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (t.schema.isArray(schema)) {
|
|
394
|
+
const arrSchema = schema as TArray;
|
|
395
|
+
const arr = ctx.arr[ctx.i++];
|
|
396
|
+
if (!Array.isArray(arr)) return arr;
|
|
397
|
+
|
|
398
|
+
if (t.schema.isObject(arrSchema.items)) {
|
|
399
|
+
return arr.map((e) =>
|
|
400
|
+
this.interpretDecodeFromValue(arrSchema.items, e),
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
return arr;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (t.schema.isObject(schema)) {
|
|
407
|
+
const objSchema = schema as TObject;
|
|
408
|
+
const props = objSchema.properties as Record<string, TSchema>;
|
|
409
|
+
const keys = Object.keys(props);
|
|
410
|
+
const req = new Set((objSchema.required as string[]) || []);
|
|
411
|
+
|
|
412
|
+
const result: Record<string, any> = {};
|
|
413
|
+
|
|
414
|
+
for (const k of keys) {
|
|
415
|
+
const ps = props[k];
|
|
416
|
+
const isOpt = !req.has(k) || t.schema.isOptional(ps);
|
|
417
|
+
const isNullable = this.isNullable(ps);
|
|
418
|
+
const inner = this.unwrap(ps);
|
|
419
|
+
const val = ctx.arr[ctx.i++];
|
|
420
|
+
|
|
421
|
+
if (isOpt) {
|
|
422
|
+
if (val !== null) {
|
|
423
|
+
result[k] = this.interpretDecodeFromValue(inner, val);
|
|
424
|
+
}
|
|
425
|
+
} else if (isNullable) {
|
|
426
|
+
result[k] =
|
|
427
|
+
val === null ? null : this.interpretDecodeFromValue(inner, val);
|
|
428
|
+
} else {
|
|
429
|
+
result[k] = this.interpretDecodeFromValue(inner, val);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return result;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (t.schema.isOptional(schema) || t.schema.isUnion(schema)) {
|
|
437
|
+
const inner = this.unwrap(schema);
|
|
438
|
+
const val = ctx.arr[ctx.i++];
|
|
439
|
+
|
|
440
|
+
if (val === null) {
|
|
441
|
+
return this.isNullable(schema) ? null : undefined;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (t.schema.isObject(inner) || t.schema.isArray(inner)) {
|
|
445
|
+
return this.interpretDecodeFromValue(inner, val);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return val;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return ctx.arr[ctx.i++];
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
protected interpretDecodeFromValue(schema: TSchema, value: any): any {
|
|
455
|
+
if (
|
|
456
|
+
t.schema.isString(schema) ||
|
|
457
|
+
t.schema.isNumber(schema) ||
|
|
458
|
+
t.schema.isInteger(schema) ||
|
|
459
|
+
t.schema.isBoolean(schema) ||
|
|
460
|
+
this.isEnum(schema)
|
|
461
|
+
) {
|
|
462
|
+
return value;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (t.schema.isBigInt(schema)) {
|
|
466
|
+
return BigInt(value.slice(0, -1));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (t.schema.isArray(schema)) {
|
|
470
|
+
if (!Array.isArray(value)) return value;
|
|
471
|
+
const arrSchema = schema as TArray;
|
|
472
|
+
if (t.schema.isObject(arrSchema.items)) {
|
|
473
|
+
return value.map((e) =>
|
|
474
|
+
this.interpretDecodeFromValue(arrSchema.items, e),
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
return value;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (t.schema.isObject(schema)) {
|
|
481
|
+
const objSchema = schema as TObject;
|
|
482
|
+
const props = objSchema.properties as Record<string, TSchema>;
|
|
483
|
+
const keys = Object.keys(props);
|
|
484
|
+
const result: Record<string, any> = {};
|
|
485
|
+
|
|
486
|
+
for (let idx = 0; idx < keys.length; idx++) {
|
|
487
|
+
const k = keys[idx];
|
|
488
|
+
const inner = this.unwrap(props[k]);
|
|
489
|
+
const v = value[idx];
|
|
490
|
+
|
|
491
|
+
if (t.schema.isObject(inner)) {
|
|
492
|
+
result[k] = this.interpretDecodeFromValue(inner, v);
|
|
493
|
+
} else if (t.schema.isBigInt(inner)) {
|
|
494
|
+
result[k] = BigInt(v.slice(0, -1));
|
|
495
|
+
} else {
|
|
496
|
+
result[k] = v;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return result;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return value;
|
|
504
|
+
}
|
|
505
|
+
|
|
122
506
|
// ===========================================================================
|
|
123
507
|
// Encoder - generates code that returns an array representation
|
|
124
508
|
// ===========================================================================
|
|
@@ -365,9 +749,7 @@ export class KeylessJsonSchemaCodec extends SchemaCodec {
|
|
|
365
749
|
* Reconstruct an object from a parsed array (for when input is already parsed).
|
|
366
750
|
*/
|
|
367
751
|
protected reconstructObject(schema: TSchema, arr: any[]): any {
|
|
368
|
-
if (!t.schema.isObject(schema))
|
|
369
|
-
return arr;
|
|
370
|
-
}
|
|
752
|
+
if (!t.schema.isObject(schema)) return arr;
|
|
371
753
|
|
|
372
754
|
const objSchema = schema as TObject;
|
|
373
755
|
const props = objSchema.properties as Record<string, TSchema>;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { Alepha, t } from "alepha";
|
|
2
|
+
import { describe, test } from "vitest";
|
|
3
|
+
import { SchemaValidator } from "./SchemaValidator.ts";
|
|
4
|
+
|
|
5
|
+
describe("SchemaValidator", () => {
|
|
6
|
+
describe("Basic validation", () => {
|
|
7
|
+
test("should validate simple objects", async ({ expect }) => {
|
|
8
|
+
const alepha = Alepha.create();
|
|
9
|
+
const validator = alepha.inject(SchemaValidator);
|
|
10
|
+
|
|
11
|
+
const schema = t.object({
|
|
12
|
+
name: t.text(),
|
|
13
|
+
age: t.integer(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const data = {
|
|
17
|
+
name: "Alice",
|
|
18
|
+
age: 30,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const result = validator.validate(schema, data);
|
|
22
|
+
expect(result).toEqual(data);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("should trim strings when schema has trim option", async ({
|
|
26
|
+
expect,
|
|
27
|
+
}) => {
|
|
28
|
+
const alepha = Alepha.create();
|
|
29
|
+
const validator = alepha.inject(SchemaValidator);
|
|
30
|
+
|
|
31
|
+
const schema = t.object({
|
|
32
|
+
name: t.text({ trim: true }),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const data = {
|
|
36
|
+
name: " Alice ",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const result = validator.validate(schema, data);
|
|
40
|
+
expect(result.name).toBe("Alice");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("should convert null to undefined for non-nullable fields", async ({
|
|
44
|
+
expect,
|
|
45
|
+
}) => {
|
|
46
|
+
const alepha = Alepha.create();
|
|
47
|
+
const validator = alepha.inject(SchemaValidator);
|
|
48
|
+
|
|
49
|
+
const schema = t.object({
|
|
50
|
+
name: t.text(),
|
|
51
|
+
bio: t.optional(t.text()),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const data = {
|
|
55
|
+
name: "Alice",
|
|
56
|
+
bio: null,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const result = validator.validate(schema, data);
|
|
60
|
+
expect(result.name).toBe("Alice");
|
|
61
|
+
expect(result.bio).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("Security - Prototype Pollution Protection", () => {
|
|
66
|
+
test("should filter out __proto__ key from input data", async ({
|
|
67
|
+
expect,
|
|
68
|
+
}) => {
|
|
69
|
+
const alepha = Alepha.create();
|
|
70
|
+
const validator = alepha.inject(SchemaValidator);
|
|
71
|
+
|
|
72
|
+
const schema = t.object({
|
|
73
|
+
name: t.text(),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Input data with __proto__ key via JSON.parse (bypasses literal protection)
|
|
77
|
+
const data = JSON.parse('{"name":"Alice","__proto__":{"isAdmin":true}}');
|
|
78
|
+
|
|
79
|
+
const result = validator.validate(schema, data);
|
|
80
|
+
|
|
81
|
+
// __proto__ should not be an own property in the result
|
|
82
|
+
expect(result.name).toBe("Alice");
|
|
83
|
+
// Using hasOwnProperty because 'in' operator has special behavior for __proto__
|
|
84
|
+
expect(Object.hasOwn(result, "__proto__")).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("should filter out constructor key from input data", async ({
|
|
88
|
+
expect,
|
|
89
|
+
}) => {
|
|
90
|
+
const alepha = Alepha.create();
|
|
91
|
+
const validator = alepha.inject(SchemaValidator);
|
|
92
|
+
|
|
93
|
+
const schema = t.object({
|
|
94
|
+
name: t.text(),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const data = {
|
|
98
|
+
name: "Alice",
|
|
99
|
+
constructor: { prototype: { isAdmin: true } },
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const result = validator.validate(schema, data);
|
|
103
|
+
|
|
104
|
+
expect(result.name).toBe("Alice");
|
|
105
|
+
expect(Object.hasOwn(result, "constructor")).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("should filter out prototype key from input data", async ({
|
|
109
|
+
expect,
|
|
110
|
+
}) => {
|
|
111
|
+
const alepha = Alepha.create();
|
|
112
|
+
const validator = alepha.inject(SchemaValidator);
|
|
113
|
+
|
|
114
|
+
const schema = t.object({
|
|
115
|
+
name: t.text(),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const data = {
|
|
119
|
+
name: "Alice",
|
|
120
|
+
prototype: { isAdmin: true },
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const result = validator.validate(schema, data);
|
|
124
|
+
|
|
125
|
+
expect(result.name).toBe("Alice");
|
|
126
|
+
expect(Object.hasOwn(result, "prototype")).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("should filter unsafe keys from nested objects", async ({
|
|
130
|
+
expect,
|
|
131
|
+
}) => {
|
|
132
|
+
const alepha = Alepha.create();
|
|
133
|
+
const validator = alepha.inject(SchemaValidator);
|
|
134
|
+
|
|
135
|
+
const schema = t.object({
|
|
136
|
+
user: t.object({
|
|
137
|
+
name: t.text(),
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const data = {
|
|
142
|
+
user: {
|
|
143
|
+
name: "Alice",
|
|
144
|
+
__proto__: { isAdmin: true },
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const result = validator.validate(schema, data);
|
|
149
|
+
|
|
150
|
+
expect(result.user.name).toBe("Alice");
|
|
151
|
+
expect(Object.hasOwn(result.user, "__proto__")).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("should not pollute Object.prototype", async ({ expect }) => {
|
|
155
|
+
const alepha = Alepha.create();
|
|
156
|
+
const validator = alepha.inject(SchemaValidator);
|
|
157
|
+
|
|
158
|
+
const schema = t.object({
|
|
159
|
+
name: t.text(),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Attempt to pollute Object.prototype via __proto__
|
|
163
|
+
const maliciousData = JSON.parse(
|
|
164
|
+
'{"name":"Alice","__proto__":{"polluted":"yes"}}',
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
validator.validate(schema, maliciousData);
|
|
168
|
+
|
|
169
|
+
// Object.prototype should not be polluted
|
|
170
|
+
expect(({} as any).polluted).toBeUndefined();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("should create objects without prototype chain", async ({
|
|
174
|
+
expect,
|
|
175
|
+
}) => {
|
|
176
|
+
const alepha = Alepha.create();
|
|
177
|
+
const validator = alepha.inject(SchemaValidator);
|
|
178
|
+
|
|
179
|
+
const schema = t.object({
|
|
180
|
+
name: t.text(),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const data = {
|
|
184
|
+
name: "Alice",
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const result = validator.validate(schema, data);
|
|
188
|
+
|
|
189
|
+
// The result should not have Object.prototype methods directly accessible
|
|
190
|
+
// via hasOwnProperty (it should use Object.create(null))
|
|
191
|
+
expect(result.name).toBe("Alice");
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("beforeParse", () => {
|
|
196
|
+
test("should handle arrays correctly", async ({ expect }) => {
|
|
197
|
+
const alepha = Alepha.create();
|
|
198
|
+
const validator = alepha.inject(SchemaValidator);
|
|
199
|
+
|
|
200
|
+
const schema = t.object({
|
|
201
|
+
tags: t.array(t.text({ trim: true })),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const data = {
|
|
205
|
+
tags: [" tag1 ", " tag2 "],
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const result = validator.validate(schema, data);
|
|
209
|
+
expect(result.tags).toEqual(["tag1", "tag2"]);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("should delete undefined values when option is enabled", async ({
|
|
213
|
+
expect,
|
|
214
|
+
}) => {
|
|
215
|
+
const alepha = Alepha.create();
|
|
216
|
+
const validator = alepha.inject(SchemaValidator);
|
|
217
|
+
|
|
218
|
+
const schema = t.object({
|
|
219
|
+
name: t.text(),
|
|
220
|
+
bio: t.optional(t.text()),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const data = {
|
|
224
|
+
name: "Alice",
|
|
225
|
+
bio: undefined,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const result = validator.validate(schema, data, {
|
|
229
|
+
deleteUndefined: true,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
expect(result.name).toBe("Alice");
|
|
233
|
+
expect("bio" in result).toBe(false);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|