@daloyjs/core 1.0.0-beta.0 → 1.0.0-beta.2

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 CHANGED
@@ -278,13 +278,21 @@ new App({
278
278
  customCss: ":root { --scalar-color-accent: #2563eb; }",
279
279
  hideTestRequestButton: true,
280
280
  },
281
+ swagger: {
282
+ docExpansion: "none",
283
+ displayRequestDuration: true,
284
+ },
281
285
  tags: ["Docs"],
282
286
  },
283
287
  });
284
288
  ```
285
289
 
286
- Switch UIs with one word. `ui: "redoc"` renders Redoc instead, and its
287
- options are forwarded to `Redoc.init` via `docs.redoc`:
290
+ Switch UIs with one word. Scalar and Swagger UI include developer request
291
+ consoles for authenticated endpoints: Scalar selects the first configured
292
+ OpenAPI security scheme by default, and Swagger UI keeps values from the
293
+ Authorize dialog across reloads. `ui: "redoc"` renders Redoc instead, and its
294
+ options are forwarded to `Redoc.init` via `docs.redoc`; Redoc displays security
295
+ requirements but is a read-only reference UI, not a Try It console:
288
296
 
289
297
  ```ts
290
298
  new App({
@@ -299,7 +307,8 @@ new App({
299
307
  The `scalar` option is forwarded to Scalar's HTML API as JSON configuration,
300
308
  with Daloy keeping the live `openapiPath` as the source. Use it for themes,
301
309
  custom CSS, layout, auth defaults, and client visibility without copying the
302
- HTML helper. Redoc spins up a `blob:` Web Worker for search, so the
310
+ HTML helper. The `swagger` option is forwarded to `SwaggerUIBundle` with Daloy
311
+ owning `url` / `dom_id`. Redoc spins up a `blob:` Web Worker for search, so the
303
312
  auto-mounted `/docs` page widens its CSP with `worker-src 'self' blob:` for
304
313
  `ui: "redoc"` only — Scalar and Swagger UI keep the tighter default.
305
314
 
@@ -413,7 +422,7 @@ miss 4,763,878 ops/sec
413
422
 
414
423
  ### Cold-start tip (serverless / edge)
415
424
 
416
- For deployments where every millisecond of startup matters (Lambda, Vercel Edge, Cloudflare Workers, Fastly Compute), import `App` from the deep entry point instead of the barrel:
425
+ For deployments where every millisecond of startup matters (Lambda, Vercel, Cloudflare Workers, Fastly Compute), import `App` from the deep entry point instead of the barrel:
417
426
 
418
427
  ```ts
419
428
  import { App } from "@daloyjs/core/app"; // ~13 ms faster cold start than "@daloyjs/core"
@@ -498,14 +507,14 @@ The core only ever sees `Request → Response`. Adapters live at the edge.
498
507
 
499
508
  ## Status
500
509
 
501
- DaloyJS is now in the **`1.0.0` beta** (`1.0.0-beta.0`). The public API is feature-complete and stable for the 1.0 line; from `1.0.0` onward, breaking changes follow SemVer and deprecations get at least one minor cycle. Small adjustments are still possible before the `1.0.0` GA if beta feedback surfaces something. The framework is already in use for production trials.
510
+ DaloyJS is now in the **`1.0.0` beta** (`1.0.0-beta.2`). The public API is feature-complete and stable for the 1.0 line; from `1.0.0` onward, breaking changes follow SemVer and deprecations get at least one minor cycle. Small adjustments are still possible before the `1.0.0` GA if beta feedback surfaces something. The framework is already in use for production trials.
502
511
 
503
512
  **Release quality bar.** Every release ships with **≥90% line + function coverage and ≥90% branch coverage**, strict TypeScript, OpenSSF Scorecard, CodeQL + Opengrep dual SAST, zizmor workflow linting, and npm provenance. Coverage was relaxed from a former 100% gate so complex security work isn't blocked chasing throwaway tests for unreachable defensive branches or tsx source-map phantoms; see [AGENTS.md](AGENTS.md) for the policy.
504
513
 
505
514
  ### Routing, validation, and docs
506
515
 
507
516
  - Contract-first routing with Standard Schema validation (Zod 4, Valibot, ArkType, TypeBox) and OpenAPI 3.1 generated from a single source of truth.
508
- - Live OpenAPI 3.1 spec served as both JSON (`GET /openapi.json`) and YAML (`GET /openapi.yaml`) when `docs: true`, with a choice of Scalar (default), Swagger UI, or Redoc via `docs.ui`, plus Scalar theming/custom CSS via `docs.scalar` and Redoc options via `docs.redoc`.
517
+ - Live OpenAPI 3.1 spec served as both JSON (`GET /openapi.json`) and YAML (`GET /openapi.yaml`) when `docs: true`, with a choice of Scalar (default), Swagger UI, or Redoc via `docs.ui`, plus Scalar theming/custom CSS/auth defaults via `docs.scalar`, Swagger UI options via `docs.swagger` (including persisted Authorize credentials), and Redoc options via `docs.redoc`.
509
518
  - Zero-config OpenAPI `info` autofill from `package.json` (Node / Bun) or `deno.json` / `deno.jsonc` (Deno); explicit `openapi.info` values always win.
510
519
  - RFC 7231 + RFC 5789 HTTP-method allowlist enforced inside `app.route()` (WebDAV, `TRACE`, `CONNECT` rejected at the framework boundary).
511
520
  - AI-friendly route metadata via optional `meta: { examples, extensions, summary, description, tags }`; examples are validated against your schemas at build time, surfaced as OpenAPI `examples` + `x-daloy-*` extensions, and dumped as `routes.json` / `routes.yaml` via `daloy inspect --ai`.
@@ -516,7 +525,7 @@ DaloyJS is now in the **`1.0.0` beta** (`1.0.0-beta.0`). The public API is featu
516
525
 
517
526
  - Adapters for Node (Heroku, Railway, Render, Fly.io), Bun, Deno, Cloudflare Workers, Vercel Node / Edge / Next.js / Netlify Edge, Fastly Compute, and AWS Lambda / Netlify Functions / Lambda Function URLs.
518
527
  - `daloy dev` watch loop delegates to the host runtime's native watcher (`node --import tsx --watch`, `bun --hot`, or `deno run --watch`) with a `--runtime` override for cross-runtime `package.json` scripts.
519
- - `pnpm create daloy` scaffolder with Node, Bun, Deno, Cloudflare Worker, and Vercel Edge templates, plus optional `--with-ci` GitHub Actions / Dependabot / CODEOWNERS / SECURITY.md hardening. The completion summary surfaces official install links (nodejs.org, pnpm.io, bun.sh) for any runtime or package manager your selections need but that is missing from `PATH`, and skips a doomed dependency install when the chosen package manager is absent.
528
+ - `pnpm create daloy` scaffolder with Node, Bun, Deno, Cloudflare Worker, and Vercel templates, plus optional `--with-ci` GitHub Actions / Dependabot / CODEOWNERS / SECURITY.md hardening. The completion summary surfaces official install links (nodejs.org, pnpm.io, bun.sh) for any runtime or package manager your selections need but that is missing from `PATH`, and skips a doomed dependency install when the chosen package manager is absent.
520
529
  - Container-first templates: `HEALTHCHECK` to `/readyz`, `STOPSIGNAL SIGTERM`, non-root user, `tini` as PID 1.
521
530
  - Generated `deploy.yml` for container templates signs every pushed GHCR image with **Sigstore Cosign** (keyless OIDC) and attaches an **SPDX SBOM attestation** so consumers can `cosign verify` and `cosign verify-attestation --type spdxjson` instead of trusting the registry alone.
522
531
  - Pretty `printStartupBanner()` / `formatStartupBanner()` helpers at `@daloyjs/core/banner`, used by every starter template (TTY + `NO_COLOR` / `FORCE_COLOR` aware, ASCII fallback for dumb terminals).
@@ -37,6 +37,15 @@ export function serve(app, opts = {}) {
37
37
  dispatchToApp(app, req, res, trustProxy, undefined);
38
38
  return;
39
39
  }
40
+ // Refuse Fetch-forbidden methods (CONNECT/TRACE/TRACK) before building a
41
+ // `Request` — `new Request` throws a `TypeError` for them, which would
42
+ // otherwise be caught and reported as a generic 500 instead of a clean,
43
+ // intentional method refusal. Placed after the GET/HEAD fast path so the
44
+ // hot path never pays for this check.
45
+ if (FETCH_FORBIDDEN_METHODS.has(method.toUpperCase())) {
46
+ writeMethodRefused(res);
47
+ return;
48
+ }
40
49
  // POST/PUT/PATCH/DELETE with a small known content-length: pre-buffer
41
50
  // bytes from the Node socket directly so the Request constructor gets a
42
51
  // Uint8Array body instead of `Readable.toWeb(req)`. This skips the
@@ -223,6 +232,45 @@ function attachClientCertificate(req, request) {
223
232
  return normalizePeerCertificate(raw, sock.authorized === true);
224
233
  });
225
234
  }
235
+ /**
236
+ * Methods the WHATWG Fetch standard forbids on a `Request` (`CONNECT`,
237
+ * `TRACE`, `TRACK`). `new Request(url, { method })` throws a `TypeError` for
238
+ * these, so the Node adapter refuses them *before* constructing a `Request` —
239
+ * otherwise that `TypeError` surfaces as a generic `500` instead of a
240
+ * deliberate method refusal. Refusing `TRACE`/`TRACK` also closes Cross-Site
241
+ * Tracing, and `CONNECT` has no meaning for an origin server, so a categorical
242
+ * refusal is the correct secure default. (`CONNECT` is normally routed to
243
+ * Node's `connect` event rather than the request listener; it is included here
244
+ * defensively for runtimes/proxies that surface it as a normal request.)
245
+ */
246
+ const FETCH_FORBIDDEN_METHODS = new Set([
247
+ "CONNECT",
248
+ "TRACE",
249
+ "TRACK",
250
+ ]);
251
+ /**
252
+ * Refuse a Fetch-forbidden HTTP method with a spec-correct `501 Not
253
+ * Implemented`. `501` is more accurate than `405` here because the method is
254
+ * unsupported for *every* resource (not just the matched route), and unlike
255
+ * `405` it does not require an `Allow` header the adapter cannot compute before
256
+ * routing. Mirrors {@link writeAdapterError}'s RFC 9457 problem+json shape;
257
+ * `Connection: close` avoids reusing a socket whose (illegal) request body was
258
+ * never drained.
259
+ *
260
+ * @param res - The Node {@link ServerResponse} to write the refusal to.
261
+ */
262
+ function writeMethodRefused(res) {
263
+ if (res.headersSent)
264
+ return;
265
+ res.statusCode = 501;
266
+ res.setHeader("content-type", "application/problem+json");
267
+ res.setHeader("connection", "close");
268
+ res.end(JSON.stringify({
269
+ type: "https://daloyjs.dev/errors/not-implemented",
270
+ title: "Not Implemented",
271
+ status: 501,
272
+ }));
273
+ }
226
274
  function writeAdapterError(res, e) {
227
275
  if (!res.headersSent) {
228
276
  res.statusCode = 500;
@@ -19,7 +19,7 @@
19
19
  * `toEdgeHandler` is kept as a backward-compatible alias of `toWebHandler`.
20
20
  */
21
21
  import type { App } from "../app.js";
22
- /** Web-standard handler shape used by Vercel Edge Functions, Next.js route handlers, and middleware. */
22
+ /** Web-standard handler shape used by Vercel Functions, Next.js route handlers, and middleware. */
23
23
  export type WebHandler = (req: Request) => Promise<Response>;
24
24
  /** Default export shape for Vercel's web-standard `{ fetch }` runtime. */
25
25
  export interface FetchHandler {
package/dist/app.d.ts CHANGED
@@ -2,7 +2,7 @@ import { WebSocketRegistry, type WebSocketHandler } from "./websocket.js";
2
2
  import { type Logger } from "./logger.js";
3
3
  import type { HttpMethod, Hooks, PathString, RequestSchemas, ResponsesMap, RouteDefinition } from "./types.js";
4
4
  import { type OpenAPIInfo, type OpenAPIOptions } from "./openapi.js";
5
- import { type DocsAssetOptions, type DocsContentSecurityPolicyOptions, type RedocConfiguration, type ScalarJsonValue, type ScalarReferenceConfiguration } from "./docs.js";
5
+ import { type DocsAssetOptions, type DocsContentSecurityPolicyOptions, type RedocConfiguration, type ScalarJsonValue, type ScalarReferenceConfiguration, type SwaggerUiConfiguration } from "./docs.js";
6
6
  import { type AsyncAPIServer } from "./asyncapi.js";
7
7
  import { type SecureHeadersOptions } from "./middleware.js";
8
8
  import { type LoadSheddingOptions } from "./load-shedding.js";
@@ -13,6 +13,8 @@ import { type BehindProxyConfig } from "./conn-info.js";
13
13
  export declare function _resetCrashHandlersForTests(): void;
14
14
  /** @internal Test-only helper to reset the latch between tests. */
15
15
  export declare function _resetInsecureDefaultsLogForTests(): void;
16
+ /** @internal Test-only helper to reset the indeterminate-env warning latch. */
17
+ export declare function _resetIndeterminateEnvWarningForTests(): void;
16
18
  /**
17
19
  * Named security posture preset. Currently only one value is supported:
18
20
  *
@@ -244,7 +246,7 @@ export interface AppOptions {
244
246
  * {@link AppOptions.secureDefaults} is not `false`. Pass `false` to opt
245
247
  * out (long-running CLI processes / test harnesses that intentionally
246
248
  * swallow rejections may want this). No-op on runtimes without
247
- * `process.on` (Cloudflare Workers / Vercel Edge / Fastly).
249
+ * `process.on` (Cloudflare Workers / Vercel / Fastly).
248
250
  *
249
251
  * @since 0.18.0
250
252
  */
@@ -378,8 +380,21 @@ export interface DocsRouteOptions {
378
380
  * renders Redoc.
379
381
  */
380
382
  ui?: "scalar" | "swagger" | "redoc";
381
- /** Scalar API reference UI configuration. Ignored unless `ui` is `"scalar"`. */
383
+ /**
384
+ * Scalar API reference UI configuration. Ignored unless `ui` is `"scalar"`.
385
+ * When OpenAPI security schemes are configured and this object does not set
386
+ * `authentication.preferredSecurityScheme`, DaloyJS selects the first scheme
387
+ * so Scalar's developer console opens with an auth type ready to fill.
388
+ */
382
389
  scalar?: ScalarReferenceConfiguration;
390
+ /**
391
+ * Swagger UI configuration, forwarded to `SwaggerUIBundle`. Ignored unless
392
+ * `ui: "swagger"`. DaloyJS owns `url` and `dom_id`; credentials entered in
393
+ * the Authorize dialog persist across reloads by default.
394
+ *
395
+ * @since 0.42.0
396
+ */
397
+ swagger?: SwaggerUiConfiguration;
383
398
  /**
384
399
  * Redoc UI configuration, forwarded to `Redoc.init`. Ignored unless
385
400
  * `ui: "redoc"`.
@@ -728,7 +743,7 @@ export declare const DALOY_RAW_STREAM: unique symbol;
728
743
  * - graceful shutdown and lifecycle observability
729
744
  *
730
745
  * `App` is **runtime-agnostic**: the same instance runs on Node, Bun, Deno,
731
- * Cloudflare Workers, Vercel Edge, AWS Lambda, and Fastly Compute via the
746
+ * Cloudflare Workers, Vercel, AWS Lambda, and Fastly Compute via the
732
747
  * dedicated adapters.
733
748
  *
734
749
  * @example
@@ -992,6 +1007,32 @@ export declare class App<Routes extends readonly RouteDefinition<any, any, any,
992
1007
  * a misconfigured surface.
993
1008
  */
994
1009
  private assertSecureHookConfig;
1010
+ /**
1011
+ * Whether the resolved runtime environment is indeterminate: no explicit
1012
+ * {@link AppOptions.env} / {@link AppOptions.production} was provided AND
1013
+ * `process.env.NODE_ENV` is unset or empty. This is the common default on
1014
+ * edge runtimes (Cloudflare Workers, Deno Deploy), which have no
1015
+ * `NODE_ENV`. Deliberately does not sniff the runtime — it only reports
1016
+ * whether the environment signal is absent — so it stays runtime-portable.
1017
+ *
1018
+ * @returns `true` when neither an explicit option nor `NODE_ENV` resolves the
1019
+ * environment; `false` otherwise (including when `NODE_ENV` is `development` /
1020
+ * `test`, which is a known, non-production answer).
1021
+ */
1022
+ private isEnvIndeterminate;
1023
+ /**
1024
+ * Emit a single once-per-process warning when a production-only secure-default
1025
+ * refusal (wildcard CORS origin, weak session secret) would not fire purely
1026
+ * because the environment is indeterminate (see {@link isEnvIndeterminate}).
1027
+ * Routed through the logger, so `logger: false` silences it. Only warns when a
1028
+ * risky config is actually present, so a clean app stays quiet. Changes no
1029
+ * enforcement; pure observability for the documented "set `env` on edge"
1030
+ * requirement.
1031
+ *
1032
+ * @param record - The hook object, carrying the wildcard-CORS / session
1033
+ * markers used by {@link assertSecureHookConfig}.
1034
+ */
1035
+ private warnIndeterminateEnvSecurity;
995
1036
  private assertRouteAuthPayloadConfig;
996
1037
  private resetBootGuardCache;
997
1038
  /**
package/dist/app.js CHANGED
@@ -43,6 +43,18 @@ let insecureDefaultsLoggedThisProcess = false;
43
43
  export function _resetInsecureDefaultsLogForTests() {
44
44
  insecureDefaultsLoggedThisProcess = false;
45
45
  }
46
+ /**
47
+ * Once-per-process latch for the "production-only secure-default guard skipped
48
+ * because the runtime environment is indeterminate" warning (see
49
+ * {@link App.warnIndeterminateEnvSecurity}). Mirrors
50
+ * {@link insecureDefaultsLoggedThisProcess}: one heads-up per process, not one
51
+ * per `App` / per `.use()` call.
52
+ */
53
+ let indeterminateEnvSecurityWarnedThisProcess = false;
54
+ /** @internal Test-only helper to reset the indeterminate-env warning latch. */
55
+ export function _resetIndeterminateEnvWarningForTests() {
56
+ indeterminateEnvSecurityWarnedThisProcess = false;
57
+ }
46
58
  /**
47
59
  * The exact set of fields the `"internal-service"` preset flips off when
48
60
  * the caller has not set them explicitly. Surfaced through the boot
@@ -613,9 +625,22 @@ export class App {
613
625
  assertSecureHookConfig(hooks) {
614
626
  if (this.options.secureDefaults === false)
615
627
  return;
616
- if (!this.isProduction())
617
- return;
618
628
  const record = hooks;
629
+ if (!this.isProduction()) {
630
+ // The refusals below are production-only. When the environment is
631
+ // *indeterminate* (no explicit `env` / `production` option and no
632
+ // `NODE_ENV` — the common default on edge runtimes such as Workers /
633
+ // Deno Deploy / Vercel), those refusals never fire, so a risky
634
+ // config could ship unguarded. Surface that once as a warning when such
635
+ // a config is actually present. Enforcement is unchanged — this only
636
+ // makes the silent skip observable. The runtime itself is deliberately
637
+ // NOT sniffed (that would couple the core to specific platforms and
638
+ // break runtime portability); we key only on "is the env signal absent?".
639
+ if (this.isEnvIndeterminate()) {
640
+ this.warnIndeterminateEnvSecurity(record);
641
+ }
642
+ return;
643
+ }
619
644
  if (record[CORS_WILDCARD_ORIGIN_MARKER] === true) {
620
645
  throw new Error('cors({ origin: "*" }) refused in production: a wildcard CORS origin exposes every state-changing route cross-origin. ' +
621
646
  "Replace the wildcard with an explicit allowlist (string[] or predicate), or pass " +
@@ -630,6 +655,70 @@ export class App {
630
655
  }
631
656
  }
632
657
  }
658
+ /**
659
+ * Whether the resolved runtime environment is indeterminate: no explicit
660
+ * {@link AppOptions.env} / {@link AppOptions.production} was provided AND
661
+ * `process.env.NODE_ENV` is unset or empty. This is the common default on
662
+ * edge runtimes (Cloudflare Workers, Deno Deploy), which have no
663
+ * `NODE_ENV`. Deliberately does not sniff the runtime — it only reports
664
+ * whether the environment signal is absent — so it stays runtime-portable.
665
+ *
666
+ * @returns `true` when neither an explicit option nor `NODE_ENV` resolves the
667
+ * environment; `false` otherwise (including when `NODE_ENV` is `development` /
668
+ * `test`, which is a known, non-production answer).
669
+ */
670
+ isEnvIndeterminate() {
671
+ if (this.options.env !== undefined ||
672
+ this.options.production !== undefined) {
673
+ return false;
674
+ }
675
+ const nodeEnv = typeof process !== "undefined" && typeof process.env !== "undefined"
676
+ ? process.env.NODE_ENV
677
+ : undefined;
678
+ return nodeEnv === undefined || nodeEnv === "";
679
+ }
680
+ /**
681
+ * Emit a single once-per-process warning when a production-only secure-default
682
+ * refusal (wildcard CORS origin, weak session secret) would not fire purely
683
+ * because the environment is indeterminate (see {@link isEnvIndeterminate}).
684
+ * Routed through the logger, so `logger: false` silences it. Only warns when a
685
+ * risky config is actually present, so a clean app stays quiet. Changes no
686
+ * enforcement; pure observability for the documented "set `env` on edge"
687
+ * requirement.
688
+ *
689
+ * @param record - The hook object, carrying the wildcard-CORS / session
690
+ * markers used by {@link assertSecureHookConfig}.
691
+ */
692
+ warnIndeterminateEnvSecurity(record) {
693
+ if (indeterminateEnvSecurityWarnedThisProcess)
694
+ return;
695
+ const risky = [];
696
+ if (record[CORS_WILDCARD_ORIGIN_MARKER] === true) {
697
+ risky.push('cors({ origin: "*" })');
698
+ }
699
+ if (record[SESSION_HOOK_MARKER] === true) {
700
+ const secrets = record[SESSION_SECRETS_MARKER];
701
+ if (Array.isArray(secrets)) {
702
+ const hasWeak = secrets.some((s) => {
703
+ try {
704
+ assertStrongSecret(s, "session");
705
+ return false;
706
+ }
707
+ catch {
708
+ return true;
709
+ }
710
+ });
711
+ if (hasWeak)
712
+ risky.push("a weak session secret");
713
+ }
714
+ }
715
+ if (risky.length === 0)
716
+ return;
717
+ indeterminateEnvSecurityWarnedThisProcess = true;
718
+ this.log.warn({ event: "secure_defaults.env_indeterminate", risky }, `DaloyJS: ${risky.join(" and ")} present, but the runtime environment is indeterminate ` +
719
+ `(no env option and no NODE_ENV). The production-only refuse-to-boot guard is therefore ` +
720
+ `inactive. If this is production (e.g. an edge runtime), set app({ env: "production" }).`);
721
+ }
633
722
  assertRouteAuthPayloadConfig(route) {
634
723
  const auth = route.auth;
635
724
  if (!auth || auth.payload !== false)
@@ -845,9 +934,16 @@ export class App {
845
934
  // (unless the caller explicitly overrode it). Scalar / Swagger UI keep the
846
935
  // tighter default. The policy is deterministic from `ui` + `opts.csp`, so
847
936
  // compute it once at mount time instead of per request.
848
- const docsCsp = docsContentSecurityPolicy(ui === "redoc"
849
- ? { ...opts.csp, allowBlobWorkers: opts.csp?.allowBlobWorkers ?? true }
850
- : opts.csp);
937
+ const docsConnectOrigins = [
938
+ ...(opts.csp?.connectOrigins ?? []),
939
+ ...serverConnectOrigins(this.options.openapi?.servers),
940
+ ];
941
+ const docsCsp = docsContentSecurityPolicy({
942
+ ...(ui === "redoc"
943
+ ? { ...opts.csp, allowBlobWorkers: opts.csp?.allowBlobWorkers ?? true }
944
+ : opts.csp),
945
+ ...(docsConnectOrigins.length ? { connectOrigins: docsConnectOrigins } : {}),
946
+ });
851
947
  this.route({
852
948
  method: "GET",
853
949
  path: docsPath,
@@ -863,6 +959,7 @@ export class App {
863
959
  ? swaggerUiHtml({
864
960
  specUrl: openapiPath,
865
961
  title,
962
+ configuration: opts.swagger,
866
963
  assets: opts.assets,
867
964
  })
868
965
  : ui === "redoc"
@@ -875,7 +972,7 @@ export class App {
875
972
  : scalarHtml({
876
973
  specUrl: openapiPath,
877
974
  title,
878
- configuration: opts.scalar,
975
+ configuration: scalarConfigurationWithPreferredAuth(opts.scalar, this.options.openapi?.securitySchemes),
879
976
  assets: opts.assets,
880
977
  });
881
978
  return {
@@ -2684,6 +2781,47 @@ function responsePipeline(fns) {
2684
2781
  return current;
2685
2782
  };
2686
2783
  }
2784
+ function serverConnectOrigins(servers) {
2785
+ if (!servers)
2786
+ return [];
2787
+ const origins = [];
2788
+ for (const server of servers) {
2789
+ try {
2790
+ const url = new URL(server.url);
2791
+ if ((url.protocol === "http:" || url.protocol === "https:") &&
2792
+ !origins.includes(url.origin)) {
2793
+ origins.push(url.origin);
2794
+ }
2795
+ }
2796
+ catch {
2797
+ // Relative server URLs are already covered by connect-src 'self'.
2798
+ }
2799
+ }
2800
+ return origins;
2801
+ }
2802
+ function scalarConfigurationWithPreferredAuth(configuration, schemes) {
2803
+ const preferredSecurityScheme = schemes ? Object.keys(schemes)[0] : undefined;
2804
+ if (!preferredSecurityScheme)
2805
+ return configuration;
2806
+ const authentication = configuration?.authentication;
2807
+ if (authentication &&
2808
+ typeof authentication === "object" &&
2809
+ !Array.isArray(authentication) &&
2810
+ "preferredSecurityScheme" in authentication) {
2811
+ return configuration;
2812
+ }
2813
+ return {
2814
+ ...(configuration ?? {}),
2815
+ authentication: {
2816
+ ...(authentication &&
2817
+ typeof authentication === "object" &&
2818
+ !Array.isArray(authentication)
2819
+ ? authentication
2820
+ : {}),
2821
+ preferredSecurityScheme,
2822
+ },
2823
+ };
2824
+ }
2687
2825
  function finalizeResponse(res, ctx, hooks, stripFingerprint = true) {
2688
2826
  let final = res;
2689
2827
  const finish = (f) => {
@@ -3034,12 +3172,27 @@ async function readBody(req, ct, limit, multipart) {
3034
3172
  return out;
3035
3173
  }
3036
3174
  if (ct.includes("multipart/form-data")) {
3037
- // Multipart: rely on platform parser, but enforce content-length first.
3175
+ // Fast-fail on an honestly-declared oversize body.
3038
3176
  const cl = req.headers.get("content-length");
3039
3177
  if (cl && Number(cl) > limit) {
3040
3178
  throw new PayloadTooLargeError(limit);
3041
3179
  }
3042
- const fd = await req.formData();
3180
+ // Then cap the ACTUAL bytes before handing them to the platform
3181
+ // formData() parser. Content-Length is not a sufficient gate on its own: a
3182
+ // chunked upload sends none, and a lying small value slips past the check —
3183
+ // in both cases the platform parser would otherwise buffer the whole body
3184
+ // in memory on runtimes whose adapter does not cap at the socket layer
3185
+ // (Workers / Deno / Vercel). `readBodyLimited` streams the body and
3186
+ // throws `PayloadTooLargeError` the instant it exceeds `limit`; we then
3187
+ // re-parse the bounded bytes with the standard `formData()` parser,
3188
+ // preserving the multipart boundary via the original Content-Type. This is
3189
+ // Web-standard only (`Request` + `formData`), so it stays runtime-portable.
3190
+ const bytes = await readBodyLimited(req, limit);
3191
+ const fd = await new Request(req.url, {
3192
+ method: "POST",
3193
+ headers: { "content-type": ct },
3194
+ body: bytes,
3195
+ }).formData();
3043
3196
  const out = {};
3044
3197
  let fields = 0;
3045
3198
  let files = 0;
@@ -3256,7 +3409,7 @@ const PACKAGE_JSON_CACHE = {};
3256
3409
  * none is found while walking up from `process.cwd()`, falls back to
3257
3410
  * `deno.json` / `deno.jsonc` so Deno projects get the same DX without a
3258
3411
  * `package.json`. Returns an empty object on edge runtimes (Cloudflare
3259
- * Workers, Vercel Edge) where `node:fs` is absent, on any I/O or parse
3412
+ * Workers) where `node:fs` is absent, on any I/O or parse
3260
3413
  * error, and when nothing is found.
3261
3414
  *
3262
3415
  * The result is memoized at module scope: manifests do not change
@@ -3,7 +3,7 @@
3
3
  * BREACH-aware safe defaults.
4
4
  *
5
5
  * Built on the web-standard `CompressionStream` API so the same line works
6
- * on Node, Bun, Deno, Cloudflare Workers, Vercel Edge, and Fastly Compute.
6
+ * on Node, Bun, Deno, Cloudflare Workers, Vercel, and Fastly Compute.
7
7
  * The middleware deliberately refuses to expose a configurable compression
8
8
  * level — `CompressionStream` uses the runtime's documented default (gzip
9
9
  * level 6 on Node / V8); a `level: 9` opt-in is rejected at construction
@@ -68,10 +68,10 @@ export interface CompressionOptions {
68
68
  */
69
69
  authCookieNames?: readonly string[];
70
70
  /**
71
- * Deliberately unsupported. Provided as a `never`-typed trap so the type
72
- * system flags accidental opt-in. The actual `CompressionStream` never
73
- * exposes a level knob — passing any value triggers a construction-time
74
- * refusal.
71
+ * Deliberately unsupported. Provided as a `never`-typed trap so the type
72
+ * system flags accidental opt-in. The actual `CompressionStream` never
73
+ * exposes a level knob — passing any value triggers a construction-time
74
+ * refusal.
75
75
  *
76
76
  * @internal
77
77
  */
@@ -3,7 +3,7 @@
3
3
  * BREACH-aware safe defaults.
4
4
  *
5
5
  * Built on the web-standard `CompressionStream` API so the same line works
6
- * on Node, Bun, Deno, Cloudflare Workers, Vercel Edge, and Fastly Compute.
6
+ * on Node, Bun, Deno, Cloudflare Workers, Vercel, and Fastly Compute.
7
7
  * The middleware deliberately refuses to expose a configurable compression
8
8
  * level — `CompressionStream` uses the runtime's documented default (gzip
9
9
  * level 6 on Node / V8); a `level: 9` opt-in is rejected at construction
package/dist/docs.d.ts CHANGED
@@ -254,7 +254,7 @@ export interface DocsAssetOptions {
254
254
  */
255
255
  crossOrigin?: "anonymous" | "use-credentials";
256
256
  }
257
- /** Shared options for {@link scalarHtml} and {@link swaggerUiHtml}. */
257
+ /** Shared options for {@link scalarHtml}, {@link swaggerUiHtml}, and {@link redocHtml}. */
258
258
  export interface DocsOptions {
259
259
  /** Absolute or relative URL of the OpenAPI document to render. */
260
260
  specUrl: string;
@@ -273,6 +273,53 @@ export interface ScalarHtmlOptions extends DocsOptions {
273
273
  /** Forwarded to the Scalar `<script id="api-reference">` tag. */
274
274
  configuration?: ScalarReferenceConfiguration;
275
275
  }
276
+ /** JSON-only subset of Swagger UI's `SwaggerUIBundle(...)` configuration. */
277
+ export interface SwaggerUiConfiguration {
278
+ [key: string]: ScalarJsonValue | undefined;
279
+ /**
280
+ * Preserve developer-entered API credentials across reloads. Defaults to
281
+ * `true` in {@link swaggerUiHtml} so the built-in docs behave like a local
282
+ * developer console instead of forgetting the bearer token on every refresh.
283
+ */
284
+ persistAuthorization?: boolean;
285
+ /** Expand operations by default: `"list"`, `"full"`, or `"none"`. */
286
+ docExpansion?: "list" | "full" | "none";
287
+ /** Enable filtering operations by tag/path text. */
288
+ filter?: boolean | string;
289
+ /** Show operation extension fields such as `x-*`. */
290
+ showExtensions?: boolean;
291
+ /** Show common extension fields. */
292
+ showCommonExtensions?: boolean;
293
+ /** Render request duration after "Try it out" calls. */
294
+ displayRequestDuration?: boolean;
295
+ /** Sort operations by HTTP method, alphabetically, or with no sorter. */
296
+ operationsSorter?: "alpha" | "method";
297
+ /** Sort tags alphabetically or with no sorter. */
298
+ tagsSorter?: "alpha";
299
+ /**
300
+ * Internal URL wiring owned by DaloyJS. Use {@link DocsOptions.specUrl}
301
+ * instead of passing Swagger UI's `url` directly.
302
+ */
303
+ url?: never;
304
+ /**
305
+ * Internal DOM mount point owned by DaloyJS. The generated HTML always mounts
306
+ * Swagger UI into `#swagger`.
307
+ */
308
+ dom_id?: never;
309
+ /** Function-valued Swagger UI plugins cannot cross the server-to-HTML boundary. */
310
+ plugins?: never;
311
+ /** Function-valued presets cannot cross the server-to-HTML boundary. */
312
+ presets?: never;
313
+ /** Function-valued request interceptors cannot cross the server-to-HTML boundary. */
314
+ requestInterceptor?: never;
315
+ /** Function-valued response interceptors cannot cross the server-to-HTML boundary. */
316
+ responseInterceptor?: never;
317
+ }
318
+ /** Options for {@link swaggerUiHtml}; adds Swagger UI-specific configuration. */
319
+ export interface SwaggerUiHtmlOptions extends DocsOptions {
320
+ /** Forwarded into `SwaggerUIBundle(...)` after `url` and `dom_id`. */
321
+ configuration?: SwaggerUiConfiguration;
322
+ }
276
323
  /**
277
324
  * Options for {@link redocHtml}; adds Redoc-specific UI configuration.
278
325
  *
@@ -300,6 +347,14 @@ export interface AsyncApiHtmlOptions extends DocsOptions {
300
347
  export interface DocsContentSecurityPolicyOptions {
301
348
  /** Extra origins to allow for `script-src` / `style-src` (defaults to jsDelivr). */
302
349
  assetOrigins?: string[];
350
+ /**
351
+ * Extra origins to allow for `connect-src`. Add API origins here when the
352
+ * docs UI should send "Try it" requests somewhere other than the same origin
353
+ * serving the docs page.
354
+ *
355
+ * @since 0.42.0
356
+ */
357
+ connectOrigins?: string[];
303
358
  /** When set, allows nonce-protected inline scripts instead of `'unsafe-inline'`. */
304
359
  scriptNonce?: string;
305
360
  /** When `false`, omits `'unsafe-inline'` from `style-src`. Defaults to `true`. */
@@ -331,8 +386,12 @@ export declare function scalarHtml(opts: ScalarHtmlOptions): string;
331
386
  /**
332
387
  * Render a Swagger UI HTML page that loads `opts.specUrl`. Same usage as
333
388
  * {@link scalarHtml} but emits the classic Swagger UI bundle.
389
+ *
390
+ * Developer-entered credentials are persisted by default
391
+ * (`persistAuthorization: true`) so routes with OpenAPI security requirements
392
+ * can be exercised after using Swagger UI's Authorize dialog.
334
393
  */
335
- export declare function swaggerUiHtml(opts: DocsOptions): string;
394
+ export declare function swaggerUiHtml(opts: SwaggerUiHtmlOptions): string;
336
395
  /**
337
396
  * Render a Redoc HTML page that loads `opts.specUrl`. Same usage as
338
397
  * {@link scalarHtml} / {@link swaggerUiHtml} but emits the Redoc standalone
package/dist/docs.js CHANGED
@@ -67,10 +67,13 @@ export function scalarHtml(opts) {
67
67
  /**
68
68
  * Render a Swagger UI HTML page that loads `opts.specUrl`. Same usage as
69
69
  * {@link scalarHtml} but emits the classic Swagger UI bundle.
70
+ *
71
+ * Developer-entered credentials are persisted by default
72
+ * (`persistAuthorization: true`) so routes with OpenAPI security requirements
73
+ * can be exercised after using Swagger UI's Authorize dialog.
70
74
  */
71
75
  export function swaggerUiHtml(opts) {
72
76
  const title = escapeHtml(opts.title ?? "API Docs");
73
- const url = escapeHtml(opts.specUrl);
74
77
  const cssUrl = escapeHtml(opts.assets?.swaggerUiCssUrl ??
75
78
  `${JSDELIVR_ORIGIN}/npm/swagger-ui-dist/swagger-ui.css`);
76
79
  const bundleUrl = escapeHtml(opts.assets?.swaggerUiBundleUrl ??
@@ -78,6 +81,12 @@ export function swaggerUiHtml(opts) {
78
81
  const cssSri = integrityAttr(opts.assets?.swaggerUiCssIntegrity, opts.assets?.crossOrigin);
79
82
  const bundleSri = integrityAttr(opts.assets?.swaggerUiBundleIntegrity, opts.assets?.crossOrigin);
80
83
  const nonce = nonceAttr(opts.scriptNonce);
84
+ const configuration = jsonForScript({
85
+ persistAuthorization: true,
86
+ ...(opts.configuration ?? {}),
87
+ url: opts.specUrl,
88
+ dom_id: "#swagger",
89
+ });
81
90
  return `<!doctype html>
82
91
  <html><head>
83
92
  <meta charset="utf-8" />
@@ -87,7 +96,7 @@ export function swaggerUiHtml(opts) {
87
96
  </head><body>
88
97
  <div id="swagger"></div>
89
98
  <script src="${bundleUrl}"${bundleSri}${nonce}></script>
90
- <script${nonce}>window.onload=()=>SwaggerUIBundle({url:"${url}",dom_id:"#swagger"});</script>
99
+ <script${nonce}>window.onload=()=>SwaggerUIBundle(${configuration});</script>
91
100
  </body></html>`;
92
101
  }
93
102
  /**
@@ -179,12 +188,13 @@ export function docsContentSecurityPolicy(opts = {}) {
179
188
  const styleSrc = ["'self'", ...assetOrigins];
180
189
  if (opts.allowInlineStyles !== false)
181
190
  styleSrc.push("'unsafe-inline'");
191
+ const connectSrc = ["'self'", ...(opts.connectOrigins ?? [])];
182
192
  const directives = [
183
193
  "default-src 'self'",
184
194
  `script-src ${scriptSrc.join(" ")}`,
185
195
  `style-src ${styleSrc.join(" ")}`,
186
196
  "img-src 'self' data: https:",
187
- "connect-src 'self'",
197
+ `connect-src ${connectSrc.join(" ")}`,
188
198
  ];
189
199
  // Redoc spawns a Web Worker from a `blob:` URL; without an explicit
190
200
  // worker-src the browser falls back to script-src, which forbids `blob:`
@@ -205,6 +215,7 @@ export function htmlResponse(html, opts = {}) {
205
215
  "content-security-policy": opts.contentSecurityPolicy ??
206
216
  docsContentSecurityPolicy({
207
217
  assetOrigins: opts.assetOrigins,
218
+ connectOrigins: opts.connectOrigins,
208
219
  scriptNonce: opts.scriptNonce,
209
220
  allowInlineStyles: opts.allowInlineStyles,
210
221
  }),
@@ -26,7 +26,7 @@
26
26
  *
27
27
  * This module is dependency-free and uses only Web Crypto + Web Standard
28
28
  * `Request`/`Response`, so it runs unchanged on Node, Bun, Deno, Cloudflare
29
- * Workers, and Vercel Edge.
29
+ * Workers, and Vercel.
30
30
  *
31
31
  * @module
32
32
  * @since 0.37.0
@@ -26,7 +26,7 @@
26
26
  *
27
27
  * This module is dependency-free and uses only Web Crypto + Web Standard
28
28
  * `Request`/`Response`, so it runs unchanged on Node, Bun, Deno, Cloudflare
29
- * Workers, and Vercel Edge.
29
+ * Workers, and Vercel.
30
30
  *
31
31
  * @module
32
32
  * @since 0.37.0
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export { findRoutesMissingResponseBodySchema } from "./app.js";
4
4
  export { _resetPackageJsonCacheForTests } from "./app.js";
5
5
  export { _resetCrashHandlersForTests } from "./app.js";
6
6
  export { _resetInsecureDefaultsLogForTests } from "./app.js";
7
+ export { _resetIndeterminateEnvWarningForTests } from "./app.js";
7
8
  export type { AppOptions, AppOpenAPIOptions, DocsRouteOptions, AsyncAPIRouteOptions, HealthRouteOptions, CspReportRouteOptions, MetricsRouteOptions, IntrospectedRoute, PluginInstalledEvent, PluginExtension, ShutdownEvent, SecurityPreset, } from "./app.js";
8
9
  export { getConnInfo, setConnInfo, assertBehindProxy, resolveClientIp, readRemoteAddress, readRemotePort, pickForwardedForByHops, } from "./conn-info.js";
9
10
  export type { BehindProxyConfig, ConnInfo } from "./conn-info.js";
@@ -73,7 +74,7 @@ export type { RequestIdOptions, SecureHeadersOptions, CspDirectivesOptions, Cors
73
74
  export type { BearerAuthOptions, BearerAuthVerifyHook } from "./middleware.js";
74
75
  export { createLogger, noopLogger, DEFAULT_REDACT_KEYS } from "./logger.js";
75
76
  export type { Logger, LogLevel, ConsoleLoggerOptions, LoggerRedactionOptions, } from "./logger.js";
76
- export type { ScalarJsonPrimitive, ScalarJsonValue, ScalarReferenceConfiguration, ScalarTheme, RedocConfiguration, RedocHtmlOptions, AsyncApiHtmlOptions, DocsAssetOptions, } from "./docs.js";
77
+ export type { ScalarJsonPrimitive, ScalarJsonValue, ScalarReferenceConfiguration, ScalarTheme, RedocConfiguration, RedocHtmlOptions, SwaggerUiConfiguration, SwaggerUiHtmlOptions, AsyncApiHtmlOptions, DocsAssetOptions, } from "./docs.js";
77
78
  export { formatStartupBanner, printStartupBanner } from "./banner.js";
78
79
  export type { StartupBannerLink, StartupBannerOptions } from "./banner.js";
79
80
  export { sseStream, sseResponse, ndjsonStream, ndjsonResponse, } from "./streaming.js";
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ export { findRoutesMissingResponseBodySchema } from "./app.js";
4
4
  export { _resetPackageJsonCacheForTests } from "./app.js";
5
5
  export { _resetCrashHandlersForTests } from "./app.js";
6
6
  export { _resetInsecureDefaultsLogForTests } from "./app.js";
7
+ export { _resetIndeterminateEnvWarningForTests } from "./app.js";
7
8
  export { getConnInfo, setConnInfo, assertBehindProxy, resolveClientIp, readRemoteAddress, readRemotePort, pickForwardedForByHops, } from "./conn-info.js";
8
9
  export { subdomains, PSL_SNAPSHOT_DATE, PSL_PUBLIC_SUFFIXES, MAX_SNAPSHOT_AGE_DAYS, } from "./subdomains.js";
9
10
  export { defineDependency, DEPENDENCY_MARKER } from "./dependency.js";
package/dist/metrics.d.ts CHANGED
@@ -18,7 +18,7 @@
18
18
  *
19
19
  * Everything is built on Web-standard primitives (plus optional `process.*`
20
20
  * gauges guarded for non-Node runtimes), so it runs unchanged on Node, Bun,
21
- * Deno, Cloudflare Workers, and Vercel Edge.
21
+ * Deno, Cloudflare Workers, and Vercel.
22
22
  *
23
23
  * @module
24
24
  * @since 0.37.0
package/dist/metrics.js CHANGED
@@ -18,7 +18,7 @@
18
18
  *
19
19
  * Everything is built on Web-standard primitives (plus optional `process.*`
20
20
  * gauges guarded for non-Node runtimes), so it runs unchanged on Node, Bun,
21
- * Deno, Cloudflare Workers, and Vercel Edge.
21
+ * Deno, Cloudflare Workers, and Vercel.
22
22
  *
23
23
  * @module
24
24
  * @since 0.37.0
@@ -231,10 +231,7 @@ export class Histogram extends Metric {
231
231
  }
232
232
  /** @internal */
233
233
  render() {
234
- const lines = [
235
- `# HELP ${this.name} ${escapeHelp(this.help)}`,
236
- `# TYPE ${this.name} histogram`,
237
- ];
234
+ const lines = [`# HELP ${this.name} ${escapeHelp(this.help)}`, `# TYPE ${this.name} histogram`];
238
235
  for (const s of this.series.values()) {
239
236
  for (let i = 0; i < this.bounds.length; i++) {
240
237
  lines.push(`${this.name}_bucket${renderLabelBlock(s.labels, ["le", String(this.bounds[i])])} ${s.counts[i]}`);
@@ -72,7 +72,7 @@ export interface CspDirectivesOptions {
72
72
  * Pair with {@link SecureHeadersOptions.reportingEndpoints} (or
73
73
  * {@link App.cspReportRoute}) to register the receiver.
74
74
  *
75
- * @since 0.20.0
75
+ * @since 0.20.0
76
76
  */
77
77
  reportTo?: string;
78
78
  }
@@ -112,7 +112,7 @@ export interface SecureHeadersOptions {
112
112
  * `reportTo: "csp-endpoint"` here as a shortcut) to direct CSP
113
113
  * violation reports there.
114
114
  *
115
- * @since 0.20.0
115
+ * @since 0.20.0
116
116
  */
117
117
  reportingEndpoints?: Record<string, string>;
118
118
  /**
@@ -121,7 +121,7 @@ export interface SecureHeadersOptions {
121
121
  * header so violation reports land at the matching
122
122
  * `reportingEndpoints` URL.
123
123
  *
124
- * @since 0.20.0
124
+ * @since 0.20.0
125
125
  */
126
126
  reportTo?: string;
127
127
  }
@@ -500,9 +500,9 @@ export interface BearerAuthOptions {
500
500
  realm?: string;
501
501
  /**
502
502
  * Optional per-request revalidation hook. Called after `validate`
503
- * accepts the token. Returning `false` rejects the request with `403`;
504
- * returning `true` or `undefined` accepts. Use for revocation lists,
505
- * token-version counters, etc.
503
+ * accepts the token. Returning `false` rejects the request with `403`;
504
+ * returning `true` or `undefined` accepts. Use for revocation lists,
505
+ * token-version counters, etc.
506
506
  *
507
507
  * @since 0.22.0
508
508
  */
@@ -44,7 +44,7 @@ function generateCspNonce() {
44
44
  const cryptoApi = globalThis.crypto;
45
45
  if (!cryptoApi?.getRandomValues) {
46
46
  throw new Error("secureHeaders(): WebCrypto is required to generate a CSP nonce. " +
47
- "Run on Node 20+, Bun, Deno, Cloudflare Workers, or Vercel Edge.");
47
+ "Run on Node 20+, Bun, Deno, Cloudflare Workers, or Vercel.");
48
48
  }
49
49
  const nonceBytes = new Uint8Array(16);
50
50
  cryptoApi.getRandomValues(nonceBytes);
@@ -193,10 +193,7 @@ export function secureHeaders(opts = {}) {
193
193
  }
194
194
  cspOpt = { directives, reportTo: opts.reportTo };
195
195
  }
196
- else if (opts.reportTo &&
197
- cspOpt !== false &&
198
- typeof cspOpt === "object" &&
199
- !cspOpt.reportTo) {
196
+ else if (opts.reportTo && cspOpt !== false && typeof cspOpt === "object" && !cspOpt.reportTo) {
200
197
  cspOpt = { ...cspOpt, reportTo: opts.reportTo };
201
198
  }
202
199
  const cspIsDynamic = cspOpt !== false && typeof cspOpt === "object";
@@ -479,10 +476,9 @@ export function cors(opts) {
479
476
  // produces broken-but-not-obviously-broken behavior in production. Fail
480
477
  // closed at construction time instead.
481
478
  if (opts.credentials) {
482
- const includesWildcard = opts.origin === "*" ||
483
- (Array.isArray(opts.origin) && opts.origin.includes("*"));
479
+ const includesWildcard = opts.origin === "*" || (Array.isArray(opts.origin) && opts.origin.includes("*"));
484
480
  if (includesWildcard) {
485
- throw new Error("cors(): origin: \"*\" cannot be combined with credentials: true. " +
481
+ throw new Error('cors(): origin: "*" cannot be combined with credentials: true. ' +
486
482
  "Pass an explicit origin string, an array of allowed origins, or a predicate function instead.");
487
483
  }
488
484
  }
@@ -558,8 +554,7 @@ export function cors(opts) {
558
554
  };
559
555
  hooks[CORS_HOOK_MARKER] = true;
560
556
  hooks[CORS_ORIGIN_ALLOW_MARKER] = (origin) => allow(origin) !== null;
561
- const hasWildcard = opts.origin === "*" ||
562
- (Array.isArray(opts.origin) && opts.origin.includes("*"));
557
+ const hasWildcard = opts.origin === "*" || (Array.isArray(opts.origin) && opts.origin.includes("*"));
563
558
  if (hasWildcard) {
564
559
  hooks[CORS_WILDCARD_ORIGIN_MARKER] = true;
565
560
  }
@@ -732,9 +727,7 @@ export function loginThrottle(opts = {}) {
732
727
  groupId,
733
728
  keyGenerator,
734
729
  ...(opts.store ? { store: opts.store } : {}),
735
- ...(opts.trustProxyHeaders !== undefined
736
- ? { trustProxyHeaders: opts.trustProxyHeaders }
737
- : {}),
730
+ ...(opts.trustProxyHeaders !== undefined ? { trustProxyHeaders: opts.trustProxyHeaders } : {}),
738
731
  ...(opts.retryAfter !== undefined ? { retryAfter: opts.retryAfter } : {}),
739
732
  });
740
733
  let slowdownBuckets = SHARED_LOGIN_THROTTLE_BUCKETS.get(groupId);
@@ -1229,9 +1222,7 @@ export function requireScopes(scopes) {
1229
1222
  });
1230
1223
  }
1231
1224
  const owned = readUserScopes(user);
1232
- const missing = owned === null
1233
- ? aggregate.slice()
1234
- : aggregate.filter((s) => !owned.has(s));
1225
+ const missing = owned === null ? aggregate.slice() : aggregate.filter((s) => !owned.has(s));
1235
1226
  if (missing.length > 0) {
1236
1227
  throw new ForbiddenError(`Missing required scope(s): ${missing.join(", ")}.`);
1237
1228
  }
@@ -22,7 +22,7 @@
22
22
  *
23
23
  * Everything here is built on Web-standard `URL` / `Request` and `btoa` /
24
24
  * `atob`, so it runs unchanged on Node, Bun, Deno, Cloudflare Workers, and
25
- * Vercel Edge.
25
+ * Vercel.
26
26
  *
27
27
  * @module
28
28
  * @since 0.37.0
@@ -22,7 +22,7 @@
22
22
  *
23
23
  * Everything here is built on Web-standard `URL` / `Request` and `btoa` /
24
24
  * `atob`, so it runs unchanged on Node, Bun, Deno, Cloudflare Workers, and
25
- * Vercel Edge.
25
+ * Vercel.
26
26
  *
27
27
  * @module
28
28
  * @since 0.37.0
@@ -24,7 +24,7 @@
24
24
  *
25
25
  * The compressed input itself is bounded by `maxCompressedBytes` before a single
26
26
  * byte is inflated. Built on the web-standard `DecompressionStream`, so the same
27
- * line works on Node, Bun, Deno, Cloudflare Workers, and Vercel Edge. Zero
27
+ * line works on Node, Bun, Deno, Cloudflare Workers, and Vercel. Zero
28
28
  * runtime dependencies.
29
29
  *
30
30
  * The middleware runs in the {@link "./types.js".Hooks.onRequest} phase — before
@@ -24,7 +24,7 @@
24
24
  *
25
25
  * The compressed input itself is bounded by `maxCompressedBytes` before a single
26
26
  * byte is inflated. Built on the web-standard `DecompressionStream`, so the same
27
- * line works on Node, Bun, Deno, Cloudflare Workers, and Vercel Edge. Zero
27
+ * line works on Node, Bun, Deno, Cloudflare Workers, and Vercel. Zero
28
28
  * runtime dependencies.
29
29
  *
30
30
  * The middleware runs in the {@link "./types.js".Hooks.onRequest} phase — before
@@ -34,7 +34,7 @@
34
34
  *
35
35
  * This module is dependency-free and uses only Web Standard
36
36
  * `Request`/`Response` + `Headers`, so it runs unchanged on Node, Bun, Deno,
37
- * Cloudflare Workers, and Vercel Edge.
37
+ * Cloudflare Workers, and Vercel.
38
38
  *
39
39
  * @module
40
40
  * @since 0.37.0
@@ -34,7 +34,7 @@
34
34
  *
35
35
  * This module is dependency-free and uses only Web Standard
36
36
  * `Request`/`Response` + `Headers`, so it runs unchanged on Node, Bun, Deno,
37
- * Cloudflare Workers, and Vercel Edge.
37
+ * Cloudflare Workers, and Vercel.
38
38
  *
39
39
  * @module
40
40
  * @since 0.37.0
@@ -304,9 +304,7 @@ export function responseCache(opts = {}) {
304
304
  const reqCc = parseCacheControl(ctx.request.headers.get("cache-control"));
305
305
  if (reqCc.has("no-store"))
306
306
  return undefined;
307
- const rawKey = opts.keyGenerator
308
- ? opts.keyGenerator(ctx)
309
- : defaultKey(ctx, varyHeaders);
307
+ const rawKey = opts.keyGenerator ? opts.keyGenerator(ctx) : defaultKey(ctx, varyHeaders);
310
308
  if (rawKey === null)
311
309
  return undefined;
312
310
  const key = `${keyPrefix}${rawKey}`;
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.5",
4
- "serialNumber": "urn:uuid:24c97016-8019-52fa-bf46-65a4325559ec",
4
+ "serialNumber": "urn:uuid:086b2383-b351-57fb-bd98-0fbf04f2342b",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-06-21T08:21:23.570Z",
7
+ "timestamp": "2026-06-22T21:07:28.471Z",
8
8
  "tools": [
9
9
  {
10
10
  "vendor": "DaloyJS",
11
11
  "name": "daloy-generate-sbom",
12
- "version": "1.0.0-beta.0"
12
+ "version": "1.0.0-beta.2"
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-beta.0",
22
+ "bom-ref": "pkg:npm/@daloyjs/core@1.0.0-beta.2",
23
23
  "name": "@daloyjs/core",
24
- "version": "1.0.0-beta.0",
24
+ "version": "1.0.0-beta.2",
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-beta.0",
26
+ "purl": "pkg:npm/@daloyjs/core@1.0.0-beta.2",
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-beta.0",
49
+ "tagId": "swidtag--daloyjs-core-1.0.0-beta.2",
50
50
  "name": "@daloyjs/core",
51
- "version": "1.0.0-beta.0",
51
+ "version": "1.0.0-beta.2",
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-beta.0",
60
+ "ref": "pkg:npm/@daloyjs/core@1.0.0-beta.2",
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-beta.0",
6
- "documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-1.0.0-beta.0-24c97016-8019-52fa-bf46-65a4325559ec",
5
+ "name": "@daloyjs/core-1.0.0-beta.2",
6
+ "documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-1.0.0-beta.2-086b2383-b351-57fb-bd98-0fbf04f2342b",
7
7
  "creationInfo": {
8
- "created": "2026-06-21T08:21:23.570Z",
8
+ "created": "2026-06-22T21:07:28.471Z",
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-beta.0",
19
+ "versionInfo": "1.0.0-beta.2",
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-beta.0"
30
+ "referenceLocator": "pkg:npm/@daloyjs/core@1.0.0-beta.2"
31
31
  }
32
32
  ]
33
33
  }
@@ -22,7 +22,7 @@
22
22
  *
23
23
  * Everything is built on Web-standard primitives (`AbortController`,
24
24
  * `Intl.DateTimeFormat` for timezone wall-clock math, `setTimeout`), so it
25
- * runs unchanged on Node, Bun, Deno, Cloudflare Workers, and Vercel Edge, with
25
+ * runs unchanged on Node, Bun, Deno, Cloudflare Workers, and Vercel, with
26
26
  * zero runtime dependencies. Pair it with {@link App.cron} for an app-managed
27
27
  * scheduler whose lifecycle is tied to graceful shutdown, or drive a
28
28
  * {@link Scheduler} directly.
package/dist/scheduler.js CHANGED
@@ -22,7 +22,7 @@
22
22
  *
23
23
  * Everything is built on Web-standard primitives (`AbortController`,
24
24
  * `Intl.DateTimeFormat` for timezone wall-clock math, `setTimeout`), so it
25
- * runs unchanged on Node, Bun, Deno, Cloudflare Workers, and Vercel Edge, with
25
+ * runs unchanged on Node, Bun, Deno, Cloudflare Workers, and Vercel, with
26
26
  * zero runtime dependencies. Pair it with {@link App.cron} for an app-managed
27
27
  * scheduler whose lifecycle is tied to graceful shutdown, or drive a
28
28
  * {@link Scheduler} directly.
@@ -63,11 +63,27 @@ const CRON_ALIASES = {
63
63
  "@hourly": "0 * * * *",
64
64
  };
65
65
  const MONTH_NAMES = {
66
- jan: 1, feb: 2, mar: 3, apr: 4, may: 5, jun: 6,
67
- jul: 7, aug: 8, sep: 9, oct: 10, nov: 11, dec: 12,
66
+ jan: 1,
67
+ feb: 2,
68
+ mar: 3,
69
+ apr: 4,
70
+ may: 5,
71
+ jun: 6,
72
+ jul: 7,
73
+ aug: 8,
74
+ sep: 9,
75
+ oct: 10,
76
+ nov: 11,
77
+ dec: 12,
68
78
  };
69
79
  const DAY_NAMES = {
70
- sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6,
80
+ sun: 0,
81
+ mon: 1,
82
+ tue: 2,
83
+ wed: 3,
84
+ thu: 4,
85
+ fri: 5,
86
+ sat: 6,
71
87
  };
72
88
  function resolveNamed(token, names) {
73
89
  const lower = token.toLowerCase();
@@ -163,7 +179,13 @@ export function parseCron(expression) {
163
179
  };
164
180
  }
165
181
  const WEEKDAY_INDEX = {
166
- Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6,
182
+ Sun: 0,
183
+ Mon: 1,
184
+ Tue: 2,
185
+ Wed: 3,
186
+ Thu: 4,
187
+ Fri: 5,
188
+ Sat: 6,
167
189
  };
168
190
  function wallClockOf(date, timeZone) {
169
191
  if (timeZone === undefined || timeZone === "UTC") {
@@ -323,7 +345,12 @@ export class Scheduler {
323
345
  skipped: 0,
324
346
  };
325
347
  this.#tasks.set(def.name, task);
326
- this.#logger?.debug({ event: "scheduler.task.defined", task: def.name, cron: def.cron, intervalMs: def.intervalMs }, `Scheduled task "${def.name}" defined`);
348
+ this.#logger?.debug({
349
+ event: "scheduler.task.defined",
350
+ task: def.name,
351
+ cron: def.cron,
352
+ intervalMs: def.intervalMs,
353
+ }, `Scheduled task "${def.name}" defined`);
327
354
  if (this.#started && !this.#stopped)
328
355
  this.#arm(task, def.runOnStart === true);
329
356
  return this;
@@ -518,7 +545,13 @@ export class Scheduler {
518
545
  catch (error) {
519
546
  task.failures++;
520
547
  task.lastError = error;
521
- this.#logger?.error({ event: "scheduler.task.failed", task: task.def.name, runCount, timedOut, err: serializeError(error) }, `Scheduled task "${task.def.name}" failed`);
548
+ this.#logger?.error({
549
+ event: "scheduler.task.failed",
550
+ task: task.def.name,
551
+ runCount,
552
+ timedOut,
553
+ err: serializeError(error),
554
+ }, `Scheduled task "${task.def.name}" failed`);
522
555
  try {
523
556
  task.def.onError?.(error, { name: task.def.name, runCount, timedOut });
524
557
  }
package/dist/session.d.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  * backed by Redis, Cloudflare KV, Vercel KV, or any other shared store.
14
14
  *
15
15
  * The module is dependency-free and uses Web Crypto (`crypto.subtle`), so it
16
- * works on Node, Bun, Deno, Cloudflare Workers, and Vercel Edge.
16
+ * works on Node, Bun, Deno, Cloudflare Workers, and Vercel.
17
17
  */
18
18
  import type { BaseContext, Hooks } from "./types.js";
19
19
  /**
package/dist/session.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * backed by Redis, Cloudflare KV, Vercel KV, or any other shared store.
14
14
  *
15
15
  * The module is dependency-free and uses Web Crypto (`crypto.subtle`), so it
16
- * works on Node, Bun, Deno, Cloudflare Workers, and Vercel Edge.
16
+ * works on Node, Bun, Deno, Cloudflare Workers, and Vercel.
17
17
  */
18
18
  import { assertCookieAttributes, readRequestCookie, serializeClearCookie, serializeCookie, } from "./cookie.js";
19
19
  import { timingSafeEqual } from "./security.js";
@@ -385,7 +385,9 @@ const DEFAULT_ROTATE_SESSION_KEYS = [
385
385
  ];
386
386
  function sessionFromContext(ctx) {
387
387
  const value = ctx.state[STATE_KEY];
388
- if (!value || typeof value !== "object" || typeof value.regenerate !== "function") {
388
+ if (!value ||
389
+ typeof value !== "object" ||
390
+ typeof value.regenerate !== "function") {
389
391
  throw new Error("rotateSession(): session() must run before rotateSession().");
390
392
  }
391
393
  return value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daloyjs/core",
3
- "version": "1.0.0-beta.0",
3
+ "version": "1.0.0-beta.2",
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": {