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.
Files changed (222) hide show
  1. package/README.md +43 -98
  2. package/dist/api/audits/index.d.ts +240 -240
  3. package/dist/api/audits/index.d.ts.map +1 -1
  4. package/dist/api/audits/index.js +2 -2
  5. package/dist/api/audits/index.js.map +1 -1
  6. package/dist/api/files/index.d.ts +185 -185
  7. package/dist/api/files/index.d.ts.map +1 -1
  8. package/dist/api/files/index.js +2 -2
  9. package/dist/api/files/index.js.map +1 -1
  10. package/dist/api/jobs/index.d.ts +245 -245
  11. package/dist/api/jobs/index.d.ts.map +1 -1
  12. package/dist/api/notifications/index.browser.js +4 -4
  13. package/dist/api/notifications/index.browser.js.map +1 -1
  14. package/dist/api/notifications/index.d.ts +74 -74
  15. package/dist/api/notifications/index.d.ts.map +1 -1
  16. package/dist/api/notifications/index.js +4 -4
  17. package/dist/api/notifications/index.js.map +1 -1
  18. package/dist/api/parameters/index.d.ts +221 -221
  19. package/dist/api/parameters/index.d.ts.map +1 -1
  20. package/dist/api/users/index.d.ts +1632 -1631
  21. package/dist/api/users/index.d.ts.map +1 -1
  22. package/dist/api/users/index.js +26 -34
  23. package/dist/api/users/index.js.map +1 -1
  24. package/dist/api/verifications/index.d.ts +132 -132
  25. package/dist/api/verifications/index.d.ts.map +1 -1
  26. package/dist/batch/index.d.ts +122 -122
  27. package/dist/batch/index.d.ts.map +1 -1
  28. package/dist/bucket/index.d.ts +163 -163
  29. package/dist/bucket/index.d.ts.map +1 -1
  30. package/dist/cache/core/index.d.ts +46 -46
  31. package/dist/cache/core/index.d.ts.map +1 -1
  32. package/dist/cache/redis/index.d.ts.map +1 -1
  33. package/dist/cache/redis/index.js +2 -2
  34. package/dist/cache/redis/index.js.map +1 -1
  35. package/dist/cli/index.d.ts +5933 -201
  36. package/dist/cli/index.d.ts.map +1 -1
  37. package/dist/cli/index.js +609 -169
  38. package/dist/cli/index.js.map +1 -1
  39. package/dist/command/index.d.ts +296 -296
  40. package/dist/command/index.d.ts.map +1 -1
  41. package/dist/command/index.js +19 -19
  42. package/dist/command/index.js.map +1 -1
  43. package/dist/core/index.browser.js +268 -79
  44. package/dist/core/index.browser.js.map +1 -1
  45. package/dist/core/index.d.ts +768 -694
  46. package/dist/core/index.d.ts.map +1 -1
  47. package/dist/core/index.js +268 -79
  48. package/dist/core/index.js.map +1 -1
  49. package/dist/core/index.native.js +268 -79
  50. package/dist/core/index.native.js.map +1 -1
  51. package/dist/datetime/index.d.ts +44 -44
  52. package/dist/datetime/index.d.ts.map +1 -1
  53. package/dist/email/index.d.ts +25 -25
  54. package/dist/email/index.d.ts.map +1 -1
  55. package/dist/fake/index.d.ts +5409 -5409
  56. package/dist/fake/index.d.ts.map +1 -1
  57. package/dist/fake/index.js +22 -22
  58. package/dist/fake/index.js.map +1 -1
  59. package/dist/file/index.d.ts +435 -435
  60. package/dist/file/index.d.ts.map +1 -1
  61. package/dist/lock/core/index.d.ts +208 -208
  62. package/dist/lock/core/index.d.ts.map +1 -1
  63. package/dist/lock/redis/index.d.ts.map +1 -1
  64. package/dist/logger/index.d.ts +24 -24
  65. package/dist/logger/index.d.ts.map +1 -1
  66. package/dist/logger/index.js +1 -5
  67. package/dist/logger/index.js.map +1 -1
  68. package/dist/mcp/index.d.ts +216 -198
  69. package/dist/mcp/index.d.ts.map +1 -1
  70. package/dist/mcp/index.js +28 -4
  71. package/dist/mcp/index.js.map +1 -1
  72. package/dist/orm/index.browser.js +9 -9
  73. package/dist/orm/index.browser.js.map +1 -1
  74. package/dist/orm/index.bun.js +83 -76
  75. package/dist/orm/index.bun.js.map +1 -1
  76. package/dist/orm/index.d.ts +961 -960
  77. package/dist/orm/index.d.ts.map +1 -1
  78. package/dist/orm/index.js +88 -81
  79. package/dist/orm/index.js.map +1 -1
  80. package/dist/queue/core/index.d.ts +244 -244
  81. package/dist/queue/core/index.d.ts.map +1 -1
  82. package/dist/queue/redis/index.d.ts.map +1 -1
  83. package/dist/redis/index.d.ts +105 -105
  84. package/dist/redis/index.d.ts.map +1 -1
  85. package/dist/retry/index.d.ts +69 -69
  86. package/dist/retry/index.d.ts.map +1 -1
  87. package/dist/router/index.d.ts +6 -6
  88. package/dist/router/index.d.ts.map +1 -1
  89. package/dist/scheduler/index.d.ts +108 -26
  90. package/dist/scheduler/index.d.ts.map +1 -1
  91. package/dist/scheduler/index.js +393 -1
  92. package/dist/scheduler/index.js.map +1 -1
  93. package/dist/security/index.d.ts +532 -209
  94. package/dist/security/index.d.ts.map +1 -1
  95. package/dist/security/index.js +1422 -11
  96. package/dist/security/index.js.map +1 -1
  97. package/dist/server/auth/index.d.ts +1296 -271
  98. package/dist/server/auth/index.d.ts.map +1 -1
  99. package/dist/server/auth/index.js +1249 -18
  100. package/dist/server/auth/index.js.map +1 -1
  101. package/dist/server/cache/index.d.ts +56 -56
  102. package/dist/server/cache/index.d.ts.map +1 -1
  103. package/dist/server/compress/index.d.ts +3 -3
  104. package/dist/server/compress/index.d.ts.map +1 -1
  105. package/dist/server/cookies/index.d.ts +6 -6
  106. package/dist/server/cookies/index.d.ts.map +1 -1
  107. package/dist/server/core/index.d.ts +196 -186
  108. package/dist/server/core/index.d.ts.map +1 -1
  109. package/dist/server/core/index.js +43 -27
  110. package/dist/server/core/index.js.map +1 -1
  111. package/dist/server/cors/index.d.ts +11 -11
  112. package/dist/server/cors/index.d.ts.map +1 -1
  113. package/dist/server/health/index.d.ts.map +1 -1
  114. package/dist/server/helmet/index.d.ts +2 -2
  115. package/dist/server/helmet/index.d.ts.map +1 -1
  116. package/dist/server/links/index.browser.js +9 -1
  117. package/dist/server/links/index.browser.js.map +1 -1
  118. package/dist/server/links/index.d.ts +83 -83
  119. package/dist/server/links/index.d.ts.map +1 -1
  120. package/dist/server/links/index.js +13 -5
  121. package/dist/server/links/index.js.map +1 -1
  122. package/dist/server/metrics/index.d.ts +514 -1
  123. package/dist/server/metrics/index.d.ts.map +1 -1
  124. package/dist/server/metrics/index.js +4462 -4
  125. package/dist/server/metrics/index.js.map +1 -1
  126. package/dist/server/multipart/index.d.ts +6 -6
  127. package/dist/server/multipart/index.d.ts.map +1 -1
  128. package/dist/server/proxy/index.d.ts +102 -102
  129. package/dist/server/proxy/index.d.ts.map +1 -1
  130. package/dist/server/rate-limit/index.d.ts +16 -16
  131. package/dist/server/rate-limit/index.d.ts.map +1 -1
  132. package/dist/server/static/index.d.ts +44 -44
  133. package/dist/server/static/index.d.ts.map +1 -1
  134. package/dist/server/swagger/index.d.ts +47 -47
  135. package/dist/server/swagger/index.d.ts.map +1 -1
  136. package/dist/sms/index.d.ts +11 -11
  137. package/dist/sms/index.d.ts.map +1 -1
  138. package/dist/sms/index.js +3 -3
  139. package/dist/sms/index.js.map +1 -1
  140. package/dist/thread/index.d.ts +71 -71
  141. package/dist/thread/index.d.ts.map +1 -1
  142. package/dist/thread/index.js +2 -2
  143. package/dist/thread/index.js.map +1 -1
  144. package/dist/topic/core/index.d.ts +318 -318
  145. package/dist/topic/core/index.d.ts.map +1 -1
  146. package/dist/topic/redis/index.d.ts +6 -6
  147. package/dist/topic/redis/index.d.ts.map +1 -1
  148. package/dist/vite/index.d.ts +2324 -1719
  149. package/dist/vite/index.d.ts.map +1 -1
  150. package/dist/vite/index.js +123 -475
  151. package/dist/vite/index.js.map +1 -1
  152. package/dist/websocket/index.browser.js +3 -3
  153. package/dist/websocket/index.browser.js.map +1 -1
  154. package/dist/websocket/index.d.ts +275 -275
  155. package/dist/websocket/index.d.ts.map +1 -1
  156. package/dist/websocket/index.js +3 -3
  157. package/dist/websocket/index.js.map +1 -1
  158. package/package.json +9 -9
  159. package/src/api/users/services/SessionService.ts +0 -10
  160. package/src/cli/apps/AlephaCli.ts +2 -2
  161. package/src/cli/apps/AlephaPackageBuilderCli.ts +9 -1
  162. package/src/cli/assets/apiHelloControllerTs.ts +2 -1
  163. package/src/cli/assets/biomeJson.ts +2 -1
  164. package/src/cli/assets/claudeMd.ts +9 -4
  165. package/src/cli/assets/dummySpecTs.ts +2 -1
  166. package/src/cli/assets/editorconfig.ts +2 -1
  167. package/src/cli/assets/mainBrowserTs.ts +2 -1
  168. package/src/cli/assets/mainCss.ts +24 -0
  169. package/src/cli/assets/tsconfigJson.ts +2 -1
  170. package/src/cli/assets/webAppRouterTs.ts +2 -1
  171. package/src/cli/assets/webHelloComponentTsx.ts +6 -2
  172. package/src/cli/atoms/appEntryOptions.ts +13 -0
  173. package/src/cli/atoms/buildOptions.ts +1 -1
  174. package/src/cli/atoms/changelogOptions.ts +1 -1
  175. package/src/cli/commands/build.ts +63 -47
  176. package/src/cli/commands/dev.ts +16 -33
  177. package/src/cli/commands/gen/env.ts +1 -1
  178. package/src/cli/commands/init.ts +17 -8
  179. package/src/cli/commands/lint.ts +1 -1
  180. package/src/cli/defineConfig.ts +9 -0
  181. package/src/cli/index.ts +2 -1
  182. package/src/cli/providers/AppEntryProvider.ts +131 -0
  183. package/src/cli/providers/ViteBuildProvider.ts +82 -0
  184. package/src/cli/providers/ViteDevServerProvider.ts +350 -0
  185. package/src/cli/providers/ViteTemplateProvider.ts +27 -0
  186. package/src/cli/services/AlephaCliUtils.ts +33 -2
  187. package/src/cli/services/PackageManagerUtils.ts +13 -6
  188. package/src/cli/services/ProjectScaffolder.ts +72 -49
  189. package/src/core/Alepha.ts +2 -8
  190. package/src/core/primitives/$module.ts +12 -0
  191. package/src/core/providers/KeylessJsonSchemaCodec.spec.ts +257 -0
  192. package/src/core/providers/KeylessJsonSchemaCodec.ts +396 -14
  193. package/src/core/providers/SchemaValidator.spec.ts +236 -0
  194. package/src/logger/providers/PrettyFormatterProvider.ts +0 -9
  195. package/src/mcp/errors/McpError.ts +30 -0
  196. package/src/mcp/index.ts +3 -0
  197. package/src/mcp/transports/SseMcpTransport.ts +16 -6
  198. package/src/orm/providers/DrizzleKitProvider.ts +3 -5
  199. package/src/orm/services/Repository.ts +11 -0
  200. package/src/server/core/index.ts +1 -1
  201. package/src/server/core/providers/BunHttpServerProvider.ts +1 -1
  202. package/src/server/core/providers/NodeHttpServerProvider.spec.ts +125 -0
  203. package/src/server/core/providers/NodeHttpServerProvider.ts +71 -22
  204. package/src/server/core/providers/ServerLoggerProvider.ts +2 -2
  205. package/src/server/core/providers/ServerProvider.ts +9 -12
  206. package/src/server/links/atoms/apiLinksAtom.ts +7 -0
  207. package/src/server/links/index.browser.ts +2 -0
  208. package/src/server/links/index.ts +2 -0
  209. package/src/vite/index.ts +3 -2
  210. package/src/vite/tasks/buildClient.ts +0 -1
  211. package/src/vite/tasks/buildServer.ts +68 -21
  212. package/src/vite/tasks/copyAssets.ts +5 -4
  213. package/src/vite/tasks/generateSitemap.ts +64 -23
  214. package/src/vite/tasks/index.ts +0 -2
  215. package/src/vite/tasks/prerenderPages.ts +49 -24
  216. package/src/cli/assets/indexHtml.ts +0 -15
  217. package/src/cli/commands/format.ts +0 -23
  218. package/src/vite/helpers/boot.ts +0 -117
  219. package/src/vite/plugins/viteAlephaDev.ts +0 -177
  220. package/src/vite/tasks/devServer.ts +0 -71
  221. package/src/vite/tasks/runAlepha.ts +0 -270
  222. /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 encoder = new TextEncoder();
38
- protected readonly decoder = new TextDecoder();
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.encoder.encode(this.encodeToString(schema, value));
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.decoder.decode(value);
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.compile(schema);
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
- protected compile(schema: TSchema): KeylessCodec {
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
+ });