@daloyjs/core 1.0.0-rc.2 → 1.0.0-rc.4

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.
@@ -28,6 +28,7 @@
28
28
  * @since 0.37.0
29
29
  */
30
30
  import { BadRequestError } from "./errors.js";
31
+ import { safeJsonParseLimited } from "./security.js";
31
32
  import { isForbiddenObjectKey } from "./security.js";
32
33
  /**
33
34
  * Hard cap on the length of an encoded cursor string accepted by
@@ -91,7 +92,9 @@ export function decodeCursor(cursor) {
91
92
  }
92
93
  let parsed;
93
94
  try {
94
- parsed = JSON.parse(json);
95
+ // Use limited parse for structural safety (wide/deep cursors).
96
+ // Cursors are length-capped at 4k so even default limits are very generous here.
97
+ parsed = safeJsonParseLimited(json, 1000, 20);
95
98
  }
96
99
  catch {
97
100
  throw new BadRequestError("Malformed pagination cursor.");
@@ -39,6 +39,7 @@
39
39
  * @module
40
40
  * @since 0.37.0
41
41
  */
42
+ import { markSchemaValidatedResponse } from "./internal-response.js";
42
43
  /** Internal `ctx.state` key carrying the pending cache key between hooks. */
43
44
  const PENDING_STATE_KEY = "__responseCachePending";
44
45
  /**
@@ -183,7 +184,7 @@ function buildResponseFromCache(entry, outcome, statusHeaderName, isHead) {
183
184
  if (statusHeaderName)
184
185
  headers.set(statusHeaderName, outcome);
185
186
  const body = isHead || entry.body === "" ? null : base64ToBytes(entry.body);
186
- return new Response(body, { status: entry.status, headers });
187
+ return markSchemaValidatedResponse(new Response(body, { status: entry.status, headers }));
187
188
  }
188
189
  function isPromiseLike(value) {
189
190
  return (value !== null &&
@@ -31,7 +31,10 @@
31
31
  * ```ts
32
32
  * import { safeRedirect } from "@daloyjs/core";
33
33
  *
34
- * app.get("/login/callback", (ctx) => {
34
+ * app.get("/login/callback", {
35
+ * acknowledgeNoResponseBodySchema: true,
36
+ * responses: { 303: {} },
37
+ * }, (ctx) => {
35
38
  * const next = new URL(ctx.request.url).searchParams.get("next") ?? "/";
36
39
  * return safeRedirect(next, {
37
40
  * allowedPaths: ["/", "/dashboard", "/account"],
@@ -31,7 +31,10 @@
31
31
  * ```ts
32
32
  * import { safeRedirect } from "@daloyjs/core";
33
33
  *
34
- * app.get("/login/callback", (ctx) => {
34
+ * app.get("/login/callback", {
35
+ * acknowledgeNoResponseBodySchema: true,
36
+ * responses: { 303: {} },
37
+ * }, (ctx) => {
35
38
  * const next = new URL(ctx.request.url).searchParams.get("next") ?? "/";
36
39
  * return safeRedirect(next, {
37
40
  * allowedPaths: ["/", "/dashboard", "/account"],
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.5",
4
- "serialNumber": "urn:uuid:8448de36-3a72-553d-882f-b5b36bbd1426",
4
+ "serialNumber": "urn:uuid:6aa5f3fd-e017-5e4a-b206-49dc6d141768",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-07-07T09:24:36.816Z",
7
+ "timestamp": "2026-07-12T20:39:15.454Z",
8
8
  "tools": [
9
9
  {
10
10
  "vendor": "DaloyJS",
11
11
  "name": "daloy-generate-sbom",
12
- "version": "1.0.0-rc.2"
12
+ "version": "1.0.0-rc.4"
13
13
  }
14
14
  ],
15
15
  "authors": [
@@ -19,11 +19,11 @@
19
19
  ],
20
20
  "component": {
21
21
  "type": "library",
22
- "bom-ref": "pkg:npm/@daloyjs/core@1.0.0-rc.2",
22
+ "bom-ref": "pkg:npm/@daloyjs/core@1.0.0-rc.4",
23
23
  "name": "@daloyjs/core",
24
- "version": "1.0.0-rc.2",
24
+ "version": "1.0.0-rc.4",
25
25
  "description": "DaloyJS is a runtime-portable, contract-first TypeScript web framework with built-in OpenAPI (Hey API), typed client generation, large-scale maintainability, and security-first defaults. Hono-grade portability, Elysia-grade DX, FastAPI-grade docs, Fastify-grade ops — distributed via pnpm.",
26
- "purl": "pkg:npm/@daloyjs/core@1.0.0-rc.2",
26
+ "purl": "pkg:npm/@daloyjs/core@1.0.0-rc.4",
27
27
  "licenses": [
28
28
  {
29
29
  "license": {
@@ -46,9 +46,9 @@
46
46
  }
47
47
  ],
48
48
  "swid": {
49
- "tagId": "swidtag--daloyjs-core-1.0.0-rc.2",
49
+ "tagId": "swidtag--daloyjs-core-1.0.0-rc.4",
50
50
  "name": "@daloyjs/core",
51
- "version": "1.0.0-rc.2",
51
+ "version": "1.0.0-rc.4",
52
52
  "tagVersion": 0,
53
53
  "patch": false
54
54
  }
@@ -57,7 +57,7 @@
57
57
  "components": [],
58
58
  "dependencies": [
59
59
  {
60
- "ref": "pkg:npm/@daloyjs/core@1.0.0-rc.2",
60
+ "ref": "pkg:npm/@daloyjs/core@1.0.0-rc.4",
61
61
  "dependsOn": []
62
62
  }
63
63
  ]
@@ -2,10 +2,10 @@
2
2
  "spdxVersion": "SPDX-2.3",
3
3
  "dataLicense": "CC0-1.0",
4
4
  "SPDXID": "SPDXRef-DOCUMENT",
5
- "name": "@daloyjs/core-1.0.0-rc.2",
6
- "documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-1.0.0-rc.2-8448de36-3a72-553d-882f-b5b36bbd1426",
5
+ "name": "@daloyjs/core-1.0.0-rc.4",
6
+ "documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-1.0.0-rc.4-6aa5f3fd-e017-5e4a-b206-49dc6d141768",
7
7
  "creationInfo": {
8
- "created": "2026-07-07T09:24:36.816Z",
8
+ "created": "2026-07-12T20:39:15.454Z",
9
9
  "creators": [
10
10
  "Tool: daloy-generate-sbom",
11
11
  "Organization: DaloyJS"
@@ -16,7 +16,7 @@
16
16
  {
17
17
  "SPDXID": "SPDXRef-Package--daloyjs-core",
18
18
  "name": "@daloyjs/core",
19
- "versionInfo": "1.0.0-rc.2",
19
+ "versionInfo": "1.0.0-rc.4",
20
20
  "downloadLocation": "https://github.com/daloyjs/daloy",
21
21
  "filesAnalyzed": false,
22
22
  "licenseConcluded": "MIT",
@@ -27,7 +27,7 @@
27
27
  {
28
28
  "referenceCategory": "PACKAGE-MANAGER",
29
29
  "referenceType": "purl",
30
- "referenceLocator": "pkg:npm/@daloyjs/core@1.0.0-rc.2"
30
+ "referenceLocator": "pkg:npm/@daloyjs/core@1.0.0-rc.4"
31
31
  }
32
32
  ]
33
33
  }
@@ -460,3 +460,24 @@ export declare function hasMongoOperatorKeys(value: unknown): boolean;
460
460
  * @since 0.35.0
461
461
  */
462
462
  export declare function assertNoMongoOperators(value: unknown): void;
463
+ /**
464
+ * Like {@link safeJsonParse}, but additionally enforces structural limits
465
+ * (total object keys across the tree and maximum nesting depth) to defend
466
+ * against wide-object / hash-flood and deep-nesting DoS payloads that stay
467
+ * under the byte cap.
468
+ *
469
+ * The limits are checked with a single pre-parse text scan
470
+ * ({@link assertJsonTextStructure}), so an oversized structure is rejected
471
+ * *before* it is parsed or allocated. On success the text is handed to
472
+ * {@link safeJsonParse}, which applies the same prototype-pollution stripping
473
+ * as every other body parser — so the security posture is identical, only the
474
+ * structural bounds are added.
475
+ *
476
+ * @param text - JSON text. Empty string returns `undefined`.
477
+ * @param maxKeys - Maximum total object keys (0 or negative = unlimited).
478
+ * @param maxDepth - Maximum nesting depth (0 or negative = unlimited).
479
+ * @returns Parsed value (with dangerous keys stripped).
480
+ * @throws {BadRequestError} on invalid JSON or when limits are exceeded.
481
+ * @since 1.0.0
482
+ */
483
+ export declare function safeJsonParseLimited(text: string, maxKeys?: number, maxDepth?: number): unknown;
package/dist/security.js CHANGED
@@ -876,3 +876,92 @@ export function assertNoMongoOperators(value) {
876
876
  throw new BadRequestError("Operator-prefixed key rejected");
877
877
  }
878
878
  }
879
+ /**
880
+ * Enforces structural limits by scanning raw JSON *text* in a single
881
+ * allocation-free pass, before the value is parsed — so a wide-object or
882
+ * deep-nesting bomb is rejected without ever being materialized into memory.
883
+ *
884
+ * Object keys are counted via `:` delimiters that sit outside string literals:
885
+ * every JSON object member is `"key": value`, and array elements carry no
886
+ * colon, so the number of structural colons equals the total object-key count
887
+ * across the whole tree. Nesting depth tracks the running `{`/`[` … `}`/`]`
888
+ * balance, again ignoring characters inside strings. Both checks short-circuit
889
+ * the instant a limit is exceeded, giving bounded-time rejection.
890
+ *
891
+ * This mirrors the accounting of the older recursive object walk but costs one
892
+ * tight character loop instead of a second full traversal of the parsed graph,
893
+ * keeping the common (small-body) path close to a bare `JSON.parse`.
894
+ *
895
+ * @param text - The raw JSON text (already known to be non-empty).
896
+ * @param maxKeys - Maximum total object keys (`<= 0` disables the key check).
897
+ * @param maxDepth - Maximum nesting depth (`<= 0` disables the depth check).
898
+ * @throws {BadRequestError} When the key or depth limit is exceeded.
899
+ * @internal
900
+ */
901
+ function assertJsonTextStructure(text, maxKeys, maxDepth) {
902
+ let depth = 0;
903
+ let keyCount = 0;
904
+ let inString = false;
905
+ let escaped = false;
906
+ for (let i = 0; i < text.length; i++) {
907
+ const c = text.charCodeAt(i);
908
+ if (inString) {
909
+ if (escaped)
910
+ escaped = false;
911
+ else if (c === 0x5c /* \ */)
912
+ escaped = true;
913
+ else if (c === 0x22 /* " */)
914
+ inString = false;
915
+ continue;
916
+ }
917
+ switch (c) {
918
+ case 0x22 /* " */:
919
+ inString = true;
920
+ break;
921
+ case 0x7b /* { */:
922
+ case 0x5b /* [ */:
923
+ depth++;
924
+ if (maxDepth > 0 && depth > maxDepth) {
925
+ throw new BadRequestError("JSON exceeds maximum nesting depth");
926
+ }
927
+ break;
928
+ case 0x7d /* } */:
929
+ case 0x5d /* ] */:
930
+ depth--;
931
+ break;
932
+ case 0x3a /* : */:
933
+ if (maxKeys > 0 && ++keyCount > maxKeys) {
934
+ throw new BadRequestError("JSON exceeds maximum key count");
935
+ }
936
+ break;
937
+ }
938
+ }
939
+ }
940
+ /**
941
+ * Like {@link safeJsonParse}, but additionally enforces structural limits
942
+ * (total object keys across the tree and maximum nesting depth) to defend
943
+ * against wide-object / hash-flood and deep-nesting DoS payloads that stay
944
+ * under the byte cap.
945
+ *
946
+ * The limits are checked with a single pre-parse text scan
947
+ * ({@link assertJsonTextStructure}), so an oversized structure is rejected
948
+ * *before* it is parsed or allocated. On success the text is handed to
949
+ * {@link safeJsonParse}, which applies the same prototype-pollution stripping
950
+ * as every other body parser — so the security posture is identical, only the
951
+ * structural bounds are added.
952
+ *
953
+ * @param text - JSON text. Empty string returns `undefined`.
954
+ * @param maxKeys - Maximum total object keys (0 or negative = unlimited).
955
+ * @param maxDepth - Maximum nesting depth (0 or negative = unlimited).
956
+ * @returns Parsed value (with dangerous keys stripped).
957
+ * @throws {BadRequestError} on invalid JSON or when limits are exceeded.
958
+ * @since 1.0.0
959
+ */
960
+ export function safeJsonParseLimited(text, maxKeys = 10_000, maxDepth = 50) {
961
+ if (text.length === 0)
962
+ return undefined;
963
+ if (maxKeys > 0 || maxDepth > 0) {
964
+ assertJsonTextStructure(text, maxKeys, maxDepth);
965
+ }
966
+ return safeJsonParse(text);
967
+ }
package/dist/tenancy.d.ts CHANGED
@@ -48,7 +48,7 @@
48
48
  *
49
49
  * @since 0.42.0
50
50
  */
51
- import type { BaseContext, Hooks } from "./types.js";
51
+ import type { BaseContext, Hooks, PreBodyContext } from "./types.js";
52
52
  /**
53
53
  * Resolves a raw (un-normalized) tenant id from a request, or a nullish value
54
54
  * when this strategy cannot determine one. Resolvers are tried in order and
@@ -249,4 +249,4 @@ export interface TenantScopeOptions {
249
249
  * @returns A key function suitable for `keyGenerator` / `scope`.
250
250
  * @since 0.42.0
251
251
  */
252
- export declare function tenantScope(opts?: TenantScopeOptions): (ctx: BaseContext<any, any>) => string;
252
+ export declare function tenantScope(opts?: TenantScopeOptions): (ctx: BaseContext<any, any> | PreBodyContext<any>) => string;
package/dist/types.d.ts CHANGED
@@ -103,7 +103,8 @@ export type InferRequest<R extends RequestSchemas | undefined, P extends string>
103
103
  /**
104
104
  * Describes a single HTTP response variant declared by a route.
105
105
  *
106
- * - `description` — surfaces in OpenAPI documentation. Required.
106
+ * - `description` — optional OpenAPI documentation; omitted values receive a
107
+ * stable `HTTP <status> response` fallback.
107
108
  * - `body` — Standard-Schema validator for the response body; when
108
109
  * present, DaloyJS validates the handler's return value against it
109
110
  * (controlled by `AppOptions.validateResponses`).
@@ -114,8 +115,8 @@ export type InferRequest<R extends RequestSchemas | undefined, P extends string>
114
115
  * @since 0.1.0
115
116
  */
116
117
  export interface ResponseSpec {
117
- /** Human-readable description emitted into the OpenAPI response object. Required. */
118
- description: string;
118
+ /** Human-readable OpenAPI description. Defaults to `HTTP <status> response`. */
119
+ description?: string;
119
120
  /** Response-body validator; handler return values are checked against it when `AppOptions.validateResponses` is on. */
120
121
  body?: StandardSchemaV1;
121
122
  /** Documented response headers keyed by header name; surfaced in the OpenAPI document. */
@@ -273,20 +274,53 @@ export interface BaseContext<P extends string, R extends RequestSchemas | undefi
273
274
  headers: Headers;
274
275
  };
275
276
  }
277
+ /**
278
+ * Minimal request context exposed before request-body I/O or schema
279
+ * validation. It is intended for cheap perimeter decisions such as bearer,
280
+ * API-key, mTLS, and header-only JWT authentication.
281
+ *
282
+ * Path, query, and header values are raw at this phase, and `body` is always
283
+ * `undefined`. Middleware that requires validated input or body bytes belongs
284
+ * in {@link Hooks.beforeHandle}.
285
+ *
286
+ * @typeParam P - Literal route path used to infer raw path-parameter names.
287
+ * @since 1.0.0
288
+ */
289
+ export interface PreBodyContext<P extends string = string> {
290
+ /** Original web-standard Request. Its body stream has not been consumed by DaloyJS. */
291
+ request: Request;
292
+ /** Raw router path parameters. */
293
+ params: PathParams<P>;
294
+ /** Raw query-string values, materialized lazily. */
295
+ query: Record<string, string | string[] | undefined>;
296
+ /** Raw request headers, materialized lazily. */
297
+ headers: Record<string, string | undefined>;
298
+ /** Always `undefined`; request-body I/O has not started. */
299
+ body: undefined;
300
+ /** Mutable per-request state shared with later hooks and the handler. */
301
+ state: AppState & Record<string, unknown>;
302
+ /** Response headers/status available to a short-circuiting perimeter hook. */
303
+ set: {
304
+ status?: number;
305
+ headers: Headers;
306
+ };
307
+ }
276
308
  /**
277
309
  * Lifecycle hooks fired around request handling. Hooks compose pipeline-style
278
310
  * — the global hooks (`AppOptions.hooks`) run first, then group hooks added
279
311
  * with `app.use()`, then per-route hooks. Returning a `Response` from
280
- * `beforeHandle` or `onSend` short-circuits/replaces the response.
312
+ * `preBody`, `beforeHandle`, or `onSend` short-circuits/replaces the response.
281
313
  *
282
314
  * Ordering for a successful request:
283
315
  * 1. `onRequest` — before any context is built (raw `Request`).
284
- * 2. `beforeHandle` with the built context; may short-circuit.
285
- * 3. *handler runs*
286
- * 4. `afterHandle` may transform the handler return value.
287
- * 5. *response is serialized + validated*
288
- * 6. `onSend` — may mutate or replace the outgoing `Response`.
289
- * 7. `onResponse` — fire-and-forget observer (cannot change anything).
316
+ * 2. `preBody` after routing, before body I/O or validation.
317
+ * 3. *request schemas are validated and the body is read when declared*
318
+ * 4. `beforeHandle` with the validated context; may short-circuit.
319
+ * 5. *handler runs*
320
+ * 6. `afterHandle` — may transform the handler return value.
321
+ * 7. *response is serialized + validated*
322
+ * 8. `onSend` — may mutate or replace the outgoing `Response`.
323
+ * 9. `onResponse` — fire-and-forget observer (cannot change anything).
290
324
  *
291
325
  * `onError` runs on the error path before serialization.
292
326
  *
@@ -295,7 +329,9 @@ export interface BaseContext<P extends string, R extends RequestSchemas | undefi
295
329
  export interface Hooks {
296
330
  /** Runs first, before validation or context building. Receives the raw web-standard `Request`. */
297
331
  onRequest?: (req: Request) => void | Promise<void>;
298
- /** Runs with the validated {@link BaseContext} before the handler. Returning a `Response` short-circuits the handler entirely (useful for auth guards). */
332
+ /** Runs after route matching but before request-body I/O or schema validation. Use for cheap header/certificate auth; `ctx.body` is always undefined. A successful raw `Response` requires the route's `acknowledgeNoResponseBodySchema` flag; `4xx`/`5xx` denials do not. */
333
+ preBody?: (ctx: PreBodyContext<any>) => void | Response | Promise<void | Response>;
334
+ /** Runs with the validated {@link BaseContext} before the handler. Returning a `Response` short-circuits the handler entirely. Successful raw responses require the route's `acknowledgeNoResponseBodySchema` flag; `4xx`/`5xx` auth denials do not. */
299
335
  beforeHandle?: (ctx: BaseContext<any, any>) => void | Response | Promise<void | Response>;
300
336
  /** Runs after the handler with its raw return value. Return a non-`undefined` value to replace the result before serialization and response-schema validation. */
301
337
  afterHandle?: (ctx: BaseContext<any, any>, result: unknown) => void | unknown | Promise<void | unknown>;
@@ -358,16 +394,22 @@ export interface RouteDefinition<P extends PathString = PathString, M extends Ht
358
394
  /** Optional per-route API version label. Informational metadata only; not emitted into the OpenAPI document. */
359
395
  version?: string;
360
396
  /**
361
- * Acknowledge that this route's `2xx` responses intentionally carry no
362
- * response body schema — an opaque, framework-controlled, or non-JSON body
363
- * (a raw `Response`, an HTML page, a spec document, a proxied payload).
397
+ * Acknowledge that this route's output intentionally is not protected by a
398
+ * response body schema — either because its `2xx` response declares no body
399
+ * schema or because it returns an opaque, framework-controlled raw
400
+ * `Response` (an HTML page, stream, spec document, or proxied payload).
364
401
  *
365
402
  * Setting this suppresses the `security.response.bodySchemaMissing` boot
366
403
  * warning and the `audit.response.bodySchema` `daloy doctor` finding for
367
- * this route only. It documents intent; it does not add protection —
368
- * response field-level stripping (OWASP API3) still does not run for a
369
- * `2xx` response without a body schema, so never set this on a route whose
370
- * handler builds JSON from domain objects.
404
+ * this route only. It also explicitly authorizes a handler or `afterHandle`
405
+ * hook to return a raw `Response`, or a `preBody` / `beforeHandle` hook to
406
+ * short-circuit with a successful (`2xx`/`3xx`) raw `Response`. DaloyJS fails
407
+ * closed with a `500` when those cases lack this acknowledgement. Ordinary
408
+ * `4xx`/`5xx` hook denials and errors remain available without opting out of
409
+ * response validation. This flag documents intent; it does not add
410
+ * protection — response field-level stripping (OWASP API3) does not run for
411
+ * an opaque body, so never set this on a route whose handler builds JSON from
412
+ * domain objects.
371
413
  */
372
414
  acknowledgeNoResponseBodySchema?: boolean;
373
415
  /**
@@ -489,8 +531,11 @@ export interface RouteDefinition<P extends PathString = PathString, M extends Ht
489
531
  * forwarded verbatim).
490
532
  *
491
533
  * A returned `Response` **bypasses response-schema validation and the
492
- * typed-client body type by design** there is no schema that can describe
493
- * an opaque stream. It is still finalized exactly like every other response,
534
+ * typed-client body type by design** and therefore requires
535
+ * `acknowledgeNoResponseBodySchema: true` on the route. Without that explicit
536
+ * acknowledgement, DaloyJS refuses the response with a `500` instead of
537
+ * silently weakening the contract. An acknowledged raw response is still
538
+ * finalized exactly like every other response,
494
539
  * so no security control is skipped: headers set via `ctx.set` (including
495
540
  * `secureHeaders()` and CORS) are copied onto it, `x-request-id` is added
496
541
  * when absent, any `onSend` / `onResponse` hooks run, server-fingerprint
@@ -503,6 +548,26 @@ export interface RouteDefinition<P extends PathString = PathString, M extends Ht
503
548
  */
504
549
  handler: (ctx: BaseContext<P, Req>) => HandlerReturn<Res> | Response | Promise<HandlerReturn<Res> | Response>;
505
550
  }
551
+ /**
552
+ * Define a route contract outside an {@link "./app.js".App} while preserving
553
+ * its literal path, method, operation id, schemas, and contextually typed
554
+ * handler.
555
+ *
556
+ * Export contracts from route modules, collect them in a literal tuple, then
557
+ * register the tuple with {@link "./app.js".App.registerRoutes}. The returned
558
+ * App retains the complete tuple for the no-codegen typed client.
559
+ *
560
+ * @param definition - Complete route contract and handler.
561
+ * @returns The same definition object, unchanged at runtime.
562
+ * @since 1.0.0
563
+ */
564
+ export declare function defineRoute<const P extends PathString, const M extends HttpMethod, Req extends RequestSchemas | undefined, Res extends ResponsesMap, const Op extends string | undefined = undefined>(definition: RouteDefinition<P, M, Req, Res> & {
565
+ operationId?: Op;
566
+ }): RouteDefinition<P, M, Req, Res> & (Op extends string ? {
567
+ operationId: Op;
568
+ } : {
569
+ operationId?: undefined;
570
+ });
506
571
  /**
507
572
  * One operation inside an OpenAPI Callback Object. Mirrors a route minus
508
573
  * `path` (the URL is supplied at runtime via the expression key) and
package/dist/types.js CHANGED
@@ -1 +1,16 @@
1
- export {};
1
+ /**
2
+ * Define a route contract outside an {@link "./app.js".App} while preserving
3
+ * its literal path, method, operation id, schemas, and contextually typed
4
+ * handler.
5
+ *
6
+ * Export contracts from route modules, collect them in a literal tuple, then
7
+ * register the tuple with {@link "./app.js".App.registerRoutes}. The returned
8
+ * App retains the complete tuple for the no-codegen typed client.
9
+ *
10
+ * @param definition - Complete route contract and handler.
11
+ * @returns The same definition object, unchanged at runtime.
12
+ * @since 1.0.0
13
+ */
14
+ export function defineRoute(definition) {
15
+ return definition;
16
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daloyjs/core",
3
- "version": "1.0.0-rc.2",
3
+ "version": "1.0.0-rc.4",
4
4
  "description": "DaloyJS is a runtime-portable, contract-first TypeScript web framework with built-in OpenAPI (Hey API), typed client generation, large-scale maintainability, and security-first defaults. Hono-grade portability, Elysia-grade DX, FastAPI-grade docs, Fastify-grade ops — distributed via pnpm.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -46,6 +46,10 @@
46
46
  "types": "./dist/index.d.ts",
47
47
  "import": "./dist/index.js"
48
48
  },
49
+ "./app": {
50
+ "types": "./dist/app.d.ts",
51
+ "import": "./dist/app.js"
52
+ },
49
53
  "./node": {
50
54
  "types": "./dist/adapters/node.d.ts",
51
55
  "import": "./dist/adapters/node.js"
@@ -246,6 +250,8 @@
246
250
  "example": "node --import tsx examples/basic.ts",
247
251
  "bench": "node --import tsx bench/router.bench.ts",
248
252
  "bench:serverless": "node --import tsx bench/serverless-cold-path.bench.ts",
253
+ "bench:json": "node --import tsx bench/json-body.bench.ts",
254
+ "bench:json-e2e": "node --import tsx bench/json-body-e2e.bench.ts",
249
255
  "test": "node --import tsx --test tests/**/*.test.ts",
250
256
  "test:red-team": "node --import tsx --test tests/red-team-attacks.test.ts tests/red-team-attacks-2.test.ts tests/red-team-attacks-3.test.ts tests/red-team-attacks-4.test.ts tests/red-team-attacks-5.test.ts tests/red-team-attacks-6.test.ts tests/red-team-attacks-7.test.ts tests/red-team-attacks-8.test.ts tests/red-team-attacks-9.test.ts tests/red-team-attacks-10.test.ts",
251
257
  "red-team:live": "node --import tsx red-team-live/run.ts",