@webjsdev/server 0.9.0 → 0.10.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 (102) hide show
  1. package/README.md +4 -1
  2. package/index.d.ts +793 -125
  3. package/index.js +8 -4
  4. package/package.json +3 -3
  5. package/src/action-flash.js +185 -0
  6. package/src/action-params.js +156 -0
  7. package/src/action-route.js +8 -9
  8. package/src/action-seed.js +44 -5
  9. package/src/actions.js +97 -15
  10. package/src/api.js +67 -10
  11. package/src/app-layout.js +133 -0
  12. package/src/asset-hash.js +82 -49
  13. package/src/auth.js +612 -108
  14. package/src/browser-entries.js +2 -17
  15. package/src/build-epoch.js +1 -1
  16. package/src/cache-tags.js +5 -1
  17. package/src/check/rules-actions.js +37 -1
  18. package/src/check/rules-components.js +114 -40
  19. package/src/check/rules-imports.js +175 -4
  20. package/src/check/rules-route-modules.js +200 -0
  21. package/src/check/rules-routes.js +55 -0
  22. package/src/check/rules-routing.js +143 -95
  23. package/src/check/rules-typescript.js +24 -2
  24. package/src/check/rules.js +104 -9
  25. package/src/check/runner-support.js +53 -171
  26. package/src/check/runner.js +22 -6
  27. package/src/client-signals.js +171 -0
  28. package/src/component-scanner.js +22 -16
  29. package/src/context.js +152 -16
  30. package/src/cookie-store.js +269 -0
  31. package/src/core-exports.js +190 -0
  32. package/src/csrf.js +21 -3
  33. package/src/dev/bun-app-source.js +95 -5
  34. package/src/dev/config.js +7 -51
  35. package/src/dev/dev-service-worker.js +48 -0
  36. package/src/dev/esm-hot-hooks.js +373 -0
  37. package/src/dev/handler.js +502 -155
  38. package/src/dev/helpers.js +127 -96
  39. package/src/dev/hot-host.js +33 -6
  40. package/src/dev/poke-state.js +110 -0
  41. package/src/dev/serve.js +177 -129
  42. package/src/dev/server.js +92 -33
  43. package/src/dev/specifier-versions.js +125 -0
  44. package/src/dev/watch-ignore.js +49 -4
  45. package/src/dev/watch-poll.js +78 -0
  46. package/src/dev/watch-tree.js +59 -22
  47. package/src/dev-classify.js +59 -26
  48. package/src/dev-error.js +104 -3
  49. package/src/dev-hot-classify.js +291 -0
  50. package/src/dev-hot-client.js +245 -0
  51. package/src/dev-import.js +22 -5
  52. package/src/dev-overlay.js +26 -0
  53. package/src/dev-ready-probe.js +69 -0
  54. package/src/dev-regenerate.js +2 -2
  55. package/src/dev-reload-hold.js +49 -20
  56. package/src/dev-reload-reason.js +61 -0
  57. package/src/dev-reload-worker.js +62 -13
  58. package/src/dev-secret.js +73 -0
  59. package/src/dev-source-locations-hook.js +4 -1
  60. package/src/dev-source-locations.js +33 -2
  61. package/src/dev.js +0 -1
  62. package/src/env-files.js +124 -0
  63. package/src/file-storage.js +2 -2
  64. package/src/foreign-imports.js +228 -0
  65. package/src/form-action-identity.js +52 -25
  66. package/src/form-dispatch.js +142 -66
  67. package/src/html-cache.js +35 -4
  68. package/src/importmap.js +2 -2
  69. package/src/instrumentation.js +59 -3
  70. package/src/js-scan.js +5 -4
  71. package/src/jwe.js +155 -0
  72. package/src/lazy-components.js +166 -0
  73. package/src/listener-core.js +4 -2
  74. package/src/lit-hints.js +172 -0
  75. package/src/middleware-load.js +248 -0
  76. package/src/module-graph.js +73 -47
  77. package/src/module-side-effect.js +284 -0
  78. package/src/public-files.js +73 -0
  79. package/src/redirect-location.js +19 -0
  80. package/src/redirects.js +9 -1
  81. package/src/route-frontier.js +151 -0
  82. package/src/route-report.js +130 -0
  83. package/src/router.js +120 -13
  84. package/src/session.js +32 -10
  85. package/src/ssr/client-router-flag.js +1 -1
  86. package/src/ssr/document.js +37 -13
  87. package/src/ssr/head.js +78 -23
  88. package/src/ssr/preloads.js +31 -45
  89. package/src/ssr/render.js +241 -55
  90. package/src/ssr/responses.js +51 -4
  91. package/src/ssr/shipped-modules.js +22 -16
  92. package/src/testing.d.ts +71 -2
  93. package/src/testing.js +103 -21
  94. package/src/thenable-params.js +26 -0
  95. package/src/vendor/jspm.js +12 -1
  96. package/src/vendor/pins.js +9 -8
  97. package/src/vendor/scanner.js +14 -9
  98. package/src/vendor.js +5 -3
  99. package/webjs-config.schema.json +1 -6
  100. package/src/component-elision.js +0 -1828
  101. package/src/elision-differential.js +0 -101
  102. package/src/elision-report.js +0 -193
package/index.d.ts CHANGED
@@ -34,6 +34,13 @@ export * from './src/testing.js';
34
34
 
35
35
  /** A webjs middleware: receives the request + a `next()` continuation. */
36
36
  export type Middleware = (req: Request, next: () => Promise<Response>) => Promise<Response> | Response;
37
+ /**
38
+ * The request an app's `middleware.ts` / `proxy.ts` (and a segment middleware)
39
+ * receives: the platform `Request` plus Next's `request.cookies`, the same
40
+ * store `cookies()` returns. Annotate with it where Next.js code writes
41
+ * `NextRequest`.
42
+ */
43
+ export type MiddlewareRequest = Request & { readonly cookies: MiddlewareRequestCookies };
37
44
 
38
45
  // `Handle` is re-exported from ./src/testing.js (the `export *` above), so it
39
46
  // is not re-declared here. It is IMPORTED as well, because `export *` re-exports
@@ -45,16 +52,49 @@ export type Middleware = (req: Request, next: () => Promise<Response>) => Promis
45
52
  * Mirrors AGENTS.md's documented shape; `@webjsdev/core` does not export it,
46
53
  * so it is defined here (the one server-owned shared type).
47
54
  */
48
- export type ActionResult<T = unknown> =
49
- | { success: true; data?: T; redirect?: string }
55
+ export type ActionResult<T = any> =
56
+ | {
57
+ success: true;
58
+ data?: T;
59
+ redirect?: string;
60
+ message?: string;
61
+ // The failure fields, absent here, so a read before narrowing on
62
+ // `success` (`result.error ?? result.fieldErrors?.body`) type-checks.
63
+ error?: undefined;
64
+ errors?: undefined;
65
+ fieldErrors?: undefined;
66
+ values?: undefined;
67
+ status?: undefined;
68
+ }
50
69
  | {
51
70
  success: false;
52
71
  error?: string;
72
+ /** Next.js's `{ errors }` spelling, read as a failure like `fieldErrors`. */
73
+ errors?: Record<string, string | string[]>;
74
+ message?: string;
53
75
  fieldErrors?: Record<string, string>;
54
76
  values?: Record<string, string>;
55
77
  status?: number;
56
78
  };
57
79
 
80
+ /**
81
+ * What a form-bound action receives: a real `FormData` (so `get`, `getAll` and
82
+ * `instanceof FormData` work, as in Next) that also carries each field as a
83
+ * property, so `input.title` reads it too. A `name[]` field arrives as an array
84
+ * under `name`; a file input arrives as a `File`. Pass the field shape for
85
+ * typed property reads.
86
+ *
87
+ * @example
88
+ * 'use server';
89
+ * import type { FormInput } from '@webjsdev/server';
90
+ * export async function createTask(input: FormInput<{ title: string; tags: string[] }>) {
91
+ * const title = input.title.trim(); // or input.get('title')
92
+ * ...
93
+ * }
94
+ */
95
+ export type FormInput<Fields extends Record<string, unknown> = Record<string, string | string[] | File>> =
96
+ FormData & Fields;
97
+
58
98
  /** The pluggable cache store interface (`memoryStore` / `redisStore` / custom). */
59
99
  export interface CacheStore {
60
100
  get(key: string): Promise<string | null>;
@@ -165,6 +205,21 @@ export interface ServerHandle {
165
205
  * Create an embeddable request handler. Returns `{ handle, rebuild, routeFor,
166
206
  * warmup, getRouteTable, appDir, dev, logger }`. Throws at boot on an
167
207
  * unsupported Node version or a failed `env.{js,ts}` validation.
208
+ *
209
+ * @example
210
+ * ```ts
211
+ * // test/home.test.ts, run with `webjs test`: the whole pipeline, no server, no network
212
+ * import { test } from 'node:test';
213
+ * import assert from 'node:assert/strict';
214
+ * import { createRequestHandler } from '@webjsdev/server';
215
+ *
216
+ * test('the home page renders', async () => {
217
+ * const app = await createRequestHandler({ appDir: process.cwd() });
218
+ * const res = await app.handle(new Request('http://localhost/'));
219
+ * assert.equal(res.status, 200);
220
+ * assert.match(await res.text(), /<h1/);
221
+ * });
222
+ * ```
168
223
  */
169
224
  export declare function createRequestHandler(opts: RequestHandlerOptions): Promise<RequestHandler>;
170
225
 
@@ -197,7 +252,25 @@ export declare function assertNodeVersion(opts?: {
197
252
  // env-schema.js (#236)
198
253
  // ---------------------------------------------------------------------------
199
254
 
200
- /** Pure env validator: check an env object against a schema or validator function. */
255
+ /**
256
+ * Pure env validator: check an env object against a schema or validator function.
257
+ *
258
+ * @example
259
+ * ```ts
260
+ * // env.ts at the app root: WebJs runs validateEnv(schema, process.env) at boot
261
+ * // and refuses to start, listing every missing or invalid variable at once.
262
+ * export default {
263
+ * DATABASE_URL: 'url',
264
+ * AUTH_SECRET: { type: 'string', minLength: 32 },
265
+ * PORT: { type: 'number', default: 3000 }, // a default makes it optional
266
+ * NODE_ENV: { type: 'enum', values: ['development', 'production', 'test'] },
267
+ * };
268
+ *
269
+ * // Calling it yourself (a test, a script) returns the verdict instead of throwing:
270
+ * import { validateEnv } from '@webjsdev/server';
271
+ * const { ok, errors } = validateEnv({ API_KEY: 'string' }, process.env);
272
+ * ```
273
+ */
201
274
  export declare function validateEnv(
202
275
  schema: object | ((env: Record<string, string | undefined>) => void),
203
276
  env: Record<string, string | undefined>,
@@ -248,6 +321,14 @@ export declare function matchApi(
248
321
  table: unknown,
249
322
  pathname: string,
250
323
  ): { route: { file: string }; params: Record<string, string> } | null;
324
+ /**
325
+ * The URL paths of every STATIC page route in a route table, sorted and
326
+ * deduped. Dynamic routes are excluded (listing one would mean inventing
327
+ * param values).
328
+ */
329
+ export declare function staticPageRoutes(table: {
330
+ pages?: Array<{ routeDir?: string; paramNames?: string[] }>;
331
+ } | undefined): string[];
251
332
 
252
333
  // ---------------------------------------------------------------------------
253
334
  // route-types.js (#258)
@@ -315,6 +396,22 @@ export declare function invokeAction(
315
396
  * optional boundary validator, and dispatches through the request abort signal
316
397
  * + the optional middleware chain. The always-works baseline is a hand-written
317
398
  * `route.ts` that imports and calls the action; this is the convenience.
399
+ *
400
+ * @example
401
+ * ```ts
402
+ * // app/api/posts/[id]/route.ts: GET /api/posts/7 calls getPost({ id: '7' })
403
+ * import { route } from '@webjsdev/server';
404
+ * import { getPost } from '#modules/posts/actions/get-post.server.ts'; // 'use server' actions
405
+ * import { updatePost } from '#modules/posts/actions/update-post.server.ts';
406
+ *
407
+ * export const GET = route(getPost); // the result is sent as JSON, a returned Response as is
408
+ * export const PUT = route(updatePost, { // the JSON body merges into the same input object
409
+ * validate: (input) => {
410
+ * if (typeof input.title !== 'string') throw new Error('title is required'); // a throw answers 400
411
+ * return { id: String(input.id), title: input.title };
412
+ * },
413
+ * });
414
+ * ```
318
415
  */
319
416
  export declare function route<A, R>(
320
417
  action: (input: A, ctx: { req: Request; params: Record<string, string> }) => R | Promise<R>,
@@ -342,8 +439,7 @@ export declare function setVendorEntries(
342
439
  /**
343
440
  * Bare-specifier npm imports an app could load in the browser, found by walking
344
441
  * the module graph from every browser-bound entry rather than by scanning the
345
- * directory. Applies no elision pruning, so the result is a superset of what the
346
- * running server serves.
442
+ * directory. The result is a superset of what the running server serves.
347
443
  */
348
444
  export declare function scanBareImports(dir: string): Promise<Set<string>>;
349
445
  /** Extract the package name from an import specifier (`dayjs/plugin/utc` -> `dayjs`). */
@@ -492,8 +588,8 @@ export declare function extractComponents(src: string): Array<{ className: strin
492
588
  * because there is no registration call anywhere or because the tag is
493
589
  * computed. The registration cross-reference is app-wide, so a class a sibling
494
590
  * module registers is not an orphan. Neither is visible to the
495
- * scanner, so both always lose the elision verdict, the tag-to-module registry
496
- * entry, and the preload hint; assume the element does not upgrade either.
591
+ * scanner, so both always lose the tag-to-module registry entry, the boot import
592
+ * and the preload hint; assume the element does not upgrade either.
497
593
  * This is about REGISTRATION, never about import reachability. The full
498
594
  * semantics, including the one case where a computed tag still upgrades, live
499
595
  * on `findOrphanComponents` in `src/component-scanner.js`.
@@ -503,75 +599,53 @@ export declare function findOrphanComponents(
503
599
  ): Promise<Array<{ className: string; file: string }>>;
504
600
 
505
601
  // ---------------------------------------------------------------------------
506
- // elision-report.js (#646, #1308)
602
+ // route-report.js (#1686)
507
603
  // ---------------------------------------------------------------------------
508
604
 
509
- /** Which rule in the analyser forced a component to ship. First match wins. */
510
- export type ElisionEvidence = 'own' | 'observed' | 'closure' | 'render' | 'import' | 'unreadable';
511
-
512
- /** One component module's verdict. `evidence` / `reason` / `by` are null when elided. */
513
- export interface ElisionComponentRow {
514
- /** App-relative path of the component module. */
515
- file: string;
516
- /** Every tag this file registers, sorted. */
517
- tags: string[];
518
- verdict: 'elided' | 'shipped';
519
- evidence: ElisionEvidence | null;
520
- reason: string | null;
521
- /** App-relative path of the module that forced the ship, where one did. */
522
- by: string | null;
523
- }
524
-
525
- /** One page/layout route module's verdict. */
526
- export interface ElisionRouteModuleRow {
605
+ /**
606
+ * One route module (page, layout or boundary). A route module never ships to
607
+ * the browser; the boot imports `emits`, its component frontier, in its place.
608
+ */
609
+ export interface RouteModuleRow {
527
610
  file: string;
528
- verdict: 'inert' | 'import-only' | 'shipped';
529
- /** For `import-only`: the component modules the boot emits in its place. */
611
+ kind: 'page' | 'layout' | 'error' | 'loading' | 'not-found' | 'forbidden' | 'unauthorized' | 'global-not-found';
612
+ /** The component modules the boot imports in this module's place (empty: zero JS). */
530
613
  emits: string[];
531
- /** For `shipped`: the first client-effecting blocker, or null when its own code is the cause. */
532
- blocker: string | null;
533
- reason: string | null;
614
+ /** Advisory notes on code that looks meant for the browser but runs only on the server. */
615
+ hints: string[];
534
616
  }
535
617
 
536
618
  /** A `class X extends WebComponent` with no literal-tag registration. */
537
- export interface ElisionOrphanRow {
619
+ export interface OrphanRow {
538
620
  file: string;
539
621
  className: string;
540
622
  }
541
623
 
542
- export interface ElisionSummary {
543
- components: number;
544
- elided: number;
545
- shipped: number;
624
+ export interface RouteReportSummary {
546
625
  routeModules: number;
547
- inert: number;
548
- importOnly: number;
549
- shippedWhole: number;
626
+ /** Route modules whose frontier is empty: the browser loads nothing for them. */
627
+ zeroJs: number;
628
+ /** Route modules whose frontier names at least one component. */
629
+ carriers: number;
550
630
  orphans: number;
551
631
  }
552
632
 
553
- /** The whole app-level elision verdict. Every path is app-relative; every array is sorted by `file`. */
554
- export interface ElisionReport {
633
+ /** What each route costs the browser. Every path is app-relative; every array is sorted by `file`. */
634
+ export interface RouteReport {
555
635
  analysed: boolean;
556
636
  /** Why nothing was analysed, when `analysed` is false. */
557
- skipped: 'no-app' | 'elide-off' | 'unanalysable' | null;
558
- components: ElisionComponentRow[];
559
- routeModules: ElisionRouteModuleRow[];
560
- orphans: ElisionOrphanRow[];
561
- summary: ElisionSummary;
637
+ skipped: 'no-app' | 'unanalysable' | null;
638
+ routeModules: RouteModuleRow[];
639
+ orphans: OrphanRow[];
640
+ summary: RouteReportSummary;
562
641
  }
563
642
 
564
643
  /**
565
- * App-level elision report, BOTH directions (#646, #1308): every component
566
- * module as elided or shipped (a shipped one naming the evidence and the module
567
- * that forced it), every page/layout as inert / import-only / shipped (a
568
- * shipped one naming the first client-effecting blocker), and every orphan
569
- * component class that gets no verdict at all. A reporting layer over
570
- * `analyzeElision`, never a second analysis and never a build artifact.
571
- * Consumed by `webjs elision`, the MCP `list_elision` tool, and both `webjs
572
- * doctor` elision checks.
644
+ * Every route module with the components the boot loads in its place and any
645
+ * hints about browser-looking code in it, plus every orphan component class.
646
+ * Consumed by both `webjs doctor` route checks.
573
647
  */
574
- export declare function analyzeAppElision(appDir: string): Promise<ElisionReport>;
648
+ export declare function analyzeRouteModules(appDir: string): Promise<RouteReport>;
575
649
 
576
650
  // ---------------------------------------------------------------------------
577
651
  // dev-regenerate.js (#967, #1672)
@@ -615,39 +689,82 @@ export declare function regenerateStatus(
615
689
  ): Promise<Array<{ output: string; command: string; state: RegenerateState }>>;
616
690
 
617
691
  // ---------------------------------------------------------------------------
618
- // elision-differential.js (#1308)
692
+ // context.js (per-request context helpers)
619
693
  // ---------------------------------------------------------------------------
620
694
 
621
695
  /**
622
- * Mask the JS-loaded set (importmap, boot script, modulepreload hints, vendor
623
- * connection hints, content stamps) out of an SSR response so a diff sees only
624
- * observable output. The ONE definition shared by the framework's own
625
- * differential guard and `webjs elision --verify`, so an app can diff its own
626
- * two captures with exactly the framework's comparator.
696
+ * The in-flight request's headers, read-only as Next.js's `ReadonlyHeaders`:
697
+ * `set`, `append` and `delete` throw at runtime and are absent from the type.
627
698
  */
628
- export declare function maskJsSet(html: string): string;
629
-
699
+ export type ReadonlyHeaders = Omit<Headers, 'set' | 'append' | 'delete'>;
700
+ /** Read-only headers for the in-flight request. Throws outside a request scope. */
701
+ export declare function headers(): ReadonlyHeaders;
702
+ /** A cookie as read from the request. */
703
+ export interface RequestCookie { name: string; value: string }
704
+ /** A cookie to write (Next's `ResponseCookie`; `maxAge` in seconds, `path` defaults to `/`). */
705
+ export interface ResponseCookie {
706
+ name: string;
707
+ value: string;
708
+ expires?: Date | number;
709
+ maxAge?: number;
710
+ domain?: string;
711
+ path?: string;
712
+ secure?: boolean;
713
+ httpOnly?: boolean;
714
+ sameSite?: 'lax' | 'strict' | 'none' | 'Lax' | 'Strict' | 'None' | boolean;
715
+ partitioned?: boolean;
716
+ priority?: 'low' | 'medium' | 'high';
717
+ }
718
+ /** The request's cookie store (Next.js's `cookies()` shape). */
630
719
  /**
631
- * The URL paths of every STATIC page route in a `buildRouteTable` result,
632
- * sorted and deduped. Dynamic routes are excluded (rendering one would mean
633
- * inventing param values).
720
+ * Next.js's `RequestCookies`, `request.cookies` in middleware and a route
721
+ * handler. Reads are the request's cookies; `set`, `delete` and `clear` change
722
+ * what the rest of this request reads (the page's `cookies()` included) and
723
+ * send nothing to the browser. Set a response cookie with `cookies().set()`.
634
724
  */
635
- export declare function staticPageRoutes(table: {
636
- pages?: Array<{ routeDir?: string; paramNames?: string[] }>;
637
- }): string[];
638
-
639
- // ---------------------------------------------------------------------------
640
- // context.js (per-request context helpers)
641
- // ---------------------------------------------------------------------------
642
-
643
- /** Read-only headers for the in-flight request. Throws outside a request scope. */
644
- export declare function headers(): Headers;
645
- /** Read-only cookie jar for the in-flight request. Throws outside a request scope. */
646
- export declare function cookies(): {
647
- get(name: string): string | undefined;
725
+ export interface MiddlewareRequestCookies {
726
+ get(name: string | { name: string }): RequestCookie | undefined;
727
+ getAll(name?: string | { name: string }): RequestCookie[];
648
728
  has(name: string): boolean;
649
- entries(): [string, string][];
650
- };
729
+ readonly size: number;
730
+ set(name: string, value: string): MiddlewareRequestCookies;
731
+ set(cookie: RequestCookie): MiddlewareRequestCookies;
732
+ delete(names: string): boolean;
733
+ delete(names: string[]): boolean[];
734
+ clear(): MiddlewareRequestCookies;
735
+ toString(): string;
736
+ [Symbol.iterator](): IterableIterator<[string, RequestCookie]>;
737
+ }
738
+ export interface RequestCookies {
739
+ get(name: string | { name: string }): RequestCookie | undefined;
740
+ getAll(name?: string | { name: string }): RequestCookie[];
741
+ has(name: string): boolean;
742
+ readonly size: number;
743
+ set(name: string, value: string, options?: Omit<ResponseCookie, 'name' | 'value'>): RequestCookies;
744
+ set(cookie: ResponseCookie): RequestCookies;
745
+ /** Expire a cookie. Pass the `path` / `domain` it was set with, or the browser keeps it. */
746
+ delete(name: string, options?: Omit<ResponseCookie, 'name' | 'value' | 'expires' | 'maxAge'>): RequestCookies;
747
+ delete(cookie: { name: string; path?: string; domain?: string }): RequestCookies;
748
+ toString(): string;
749
+ [Symbol.iterator](): IterableIterator<[string, RequestCookie]>;
750
+ }
751
+ /**
752
+ * The in-flight request's cookies, Next.js's `cookies()`: `cookies().get('x')?.value`
753
+ * reads, `set` / `delete` write a `Set-Cookie` onto this request's response. Also
754
+ * awaitable (`(await cookies()).get('x')`). Throws outside a request scope.
755
+ *
756
+ * `get` returns a {@link RequestCookie} (`{ name, value }`) or `undefined`, as
757
+ * in Next.js, never the bare string.
758
+ *
759
+ * @example
760
+ * ```ts
761
+ * import { cookies } from '@webjsdev/server';
762
+ * const theme: string = cookies().get('theme')?.value ?? 'light';
763
+ * cookies().set('theme', 'dark', { path: '/', maxAge: 60 * 60 * 24 * 365, httpOnly: true });
764
+ * cookies().delete('theme');
765
+ * ```
766
+ */
767
+ export declare function cookies(): RequestCookies & Promise<RequestCookies>;
651
768
  /** The raw `Request` for the in-flight scope, or null at module top-level. */
652
769
  export declare function getRequest(): Request | null;
653
770
  /** Run `fn` within a request scope. */
@@ -668,7 +785,18 @@ export declare function defaultLogger(opts?: { dev?: boolean }): Logger;
668
785
  // rate-limit.js
669
786
  // ---------------------------------------------------------------------------
670
787
 
671
- /** Build a rate-limit middleware `(req, next) => Response`. */
788
+ /**
789
+ * Build a rate-limit middleware `(req, next) => Response`.
790
+ *
791
+ * @example
792
+ * ```ts
793
+ * // app/api/auth/middleware.ts: 10 requests a minute per client on every /api/auth route
794
+ * import { rateLimit } from '@webjsdev/server';
795
+ * export default rateLimit({ window: '1m', max: 10 }); // over it answers 429 + retry-after
796
+ * // Behind Cloudflare, key on the visitor rather than the edge:
797
+ * // rateLimit({ window: '1m', max: 10, trustProxy: true, clientIpHeader: 'cf-connecting-ip' })
798
+ * ```
799
+ */
672
800
  export declare function rateLimit(opts?: {
673
801
  window?: number | string;
674
802
  max?: number;
@@ -715,6 +843,19 @@ export declare function parseWindow(w: number | string): number;
715
843
  * whenever a CDN sits in front: the default chain's leftmost
716
844
  * `X-Forwarded-For` entry is then the CDN's egress address rather than the
717
845
  * visitor.
846
+ *
847
+ * @example
848
+ * ```ts
849
+ * // app/api/votes/route.ts: one vote per client address
850
+ * import { clientIp } from '@webjsdev/server';
851
+ * import { recordVote } from '#modules/votes/queries/record-vote.server.ts';
852
+ *
853
+ * export async function POST(req: Request) {
854
+ * const ip = clientIp(req); // the socket peer, never a client-sent header
855
+ * // Behind Cloudflare: clientIp(req, { trustProxy: true, header: 'cf-connecting-ip' })
856
+ * return Response.json({ counted: await recordVote(ip) });
857
+ * }
858
+ * ```
718
859
  */
719
860
  export declare function clientIp(
720
861
  req: Request,
@@ -740,7 +881,22 @@ export interface CorsOptions {
740
881
  maxAge?: number;
741
882
  }
742
883
 
743
- /** Build a CORS middleware `(req, next) => Response`. */
884
+ /**
885
+ * Build a CORS middleware `(req, next) => Response`.
886
+ *
887
+ * @example
888
+ * ```ts
889
+ * // app/api/public/middleware.ts: CORS for every route.ts under /api/public,
890
+ * // the OPTIONS preflight answered for you
891
+ * import { cors } from '@webjsdev/server';
892
+ *
893
+ * export default cors({
894
+ * origin: ['https://app.example.com', /\.example\.com$/], // or '*' for a public, cookie-free API
895
+ * credentials: true, // needs a listed origin, never '*'
896
+ * maxAge: 600, // preflight cache, in seconds
897
+ * });
898
+ * ```
899
+ */
744
900
  export declare function cors(options?: CorsOptions): Middleware;
745
901
  /** Resolve the allowed origin for a request given a policy (shared CORS core). */
746
902
  export declare function resolveOrigin(
@@ -772,6 +928,21 @@ export declare function setStore(store: CacheStore): void;
772
928
  * Wrap an async function with TTL caching. Preserves the wrapped signature and
773
929
  * adds an `invalidate()` member. `tags` (static `string[]` or a per-arg
774
930
  * `(...args) => string[]`) enable `revalidateTag` cross-module eviction.
931
+ *
932
+ * @example
933
+ * ```ts
934
+ * // modules/posts/queries/get-post.server.ts
935
+ * import { cache } from '@webjsdev/server';
936
+ * import { eq } from 'drizzle-orm';
937
+ * import { db } from '#db/connection.server.ts';
938
+ * import { posts } from '#db/schema.server.ts';
939
+ *
940
+ * export const getPost = cache(
941
+ * async (id: number) => (await db.select().from(posts).where(eq(posts.id, id)).limit(1))[0],
942
+ * { key: 'post', ttl: 300, tags: (id) => ['post:' + id] }, // ttl in seconds, default 60
943
+ * );
944
+ * // A write elsewhere evicts just that id: await revalidateTag('post:' + id)
945
+ * ```
775
946
  */
776
947
  export declare function cache<T extends (...args: any[]) => Promise<any>>(
777
948
  fn: T,
@@ -786,8 +957,27 @@ export declare function cache<T extends (...args: any[]) => Promise<any>>(
786
957
  // cache-tags.js (#242) + html-cache.js (#241): server cache invalidation
787
958
  // ---------------------------------------------------------------------------
788
959
 
789
- /** Evict every `cache()` entry tagged with `tag`. */
790
- export declare function revalidateTag(tag: string): Promise<void>;
960
+ /**
961
+ * Evict every `cache()` entry tagged with `tag`. Takes Next.js 16's second
962
+ * argument, `revalidateTag(tag, 'max')` (a cache-life profile, or
963
+ * `{ expire }`), for source compatibility: WebJs evicts at once whatever the
964
+ * profile, since `cache()` has no stale-while-revalidate window to honour.
965
+ *
966
+ * @example
967
+ * ```ts
968
+ * // modules/posts/actions/rename-post.server.ts
969
+ * 'use server';
970
+ * import { revalidateTag } from '@webjsdev/server';
971
+ * import { updatePostTitle } from '../queries/update-post-title.server.ts';
972
+ *
973
+ * export async function renamePost(input: { id: number; title: string }) {
974
+ * await updatePostTitle(input.id, input.title);
975
+ * await revalidateTag('post:' + input.id); // every cache() read tagged 'post:<id>' recomputes
976
+ * return { success: true };
977
+ * }
978
+ * ```
979
+ */
980
+ export declare function revalidateTag(tag: string, profile?: string | { expire?: number }): Promise<void>;
791
981
  /** Evict every `cache()` entry tagged with any of `tags`. */
792
982
  export declare function revalidateTags(tags: string[]): Promise<void>;
793
983
  /**
@@ -800,7 +990,7 @@ export declare function revalidateTags(tags: string[]): Promise<void>;
800
990
  * `webjs.basePath` the absolute url is the PUBLIC one and its mount prefix is
801
991
  * stripped, so it evicts the entry the write actually stored.
802
992
  */
803
- export declare function revalidatePath(path: string): Promise<void>;
993
+ export declare function revalidatePath(path: string, type?: 'page' | 'layout'): Promise<void>;
804
994
  /** Evict all cached HTML (per-process generation bump). */
805
995
  export declare function revalidateAll(): Promise<void>;
806
996
 
@@ -826,12 +1016,34 @@ export declare class Session {
826
1016
  set(key: string, value: unknown): void;
827
1017
  has(key: string): boolean;
828
1018
  unset(key: string): void;
829
- /** Set a value readable once on the next request, then cleared. */
1019
+ /** Set a value that lasts until a `get()` reads it (Remix's flash); `has()` does not consume it. */
830
1020
  flash(key: string, value: unknown): void;
831
1021
  destroy(): void;
1022
+ /**
1023
+ * Give the session a new id, keeping its data. Call it after a login to
1024
+ * prevent session fixation; `deleteOld` also deletes the old id's stored data.
1025
+ */
1026
+ regenerateId(deleteOld?: boolean): void;
832
1027
  }
833
1028
 
834
- /** Session middleware factory. */
1029
+ /**
1030
+ * Session middleware factory: a signed session cookie for apps that do not use
1031
+ * `createAuth`. It throws at import without `secret` or `SESSION_SECRET`.
1032
+ *
1033
+ * @example
1034
+ * ```ts
1035
+ * // middleware.ts
1036
+ * import { session } from '@webjsdev/server';
1037
+ * export default session({ secret: process.env.SESSION_SECRET });
1038
+ *
1039
+ * // In a 'use server' action or a page:
1040
+ * import { getRequest, getSession } from '@webjsdev/server';
1041
+ * const s = getSession(getRequest()!);
1042
+ * s.regenerateId(true); // after a login, against session fixation
1043
+ * s.set('userId', user.id);
1044
+ * const userId = s.get<string>('userId');
1045
+ * ```
1046
+ */
835
1047
  export declare function session(opts?: {
836
1048
  storage?: SessionStorage;
837
1049
  cookieName?: string;
@@ -850,14 +1062,50 @@ export declare function storeSessionStorage(opts?: Record<string, unknown>): Ses
850
1062
  export declare const cookieSession: typeof cookieSessionStorage;
851
1063
  /** Alias of `storeSessionStorage`. */
852
1064
  export declare const storeSession: typeof storeSessionStorage;
853
- /** Read the session for a request (after the session middleware ran). */
854
- export declare function getSession(req: Request): Session;
1065
+ /**
1066
+ * The session for the current request. With no argument it reads the request
1067
+ * in scope (as `cookies()` and `headers()` do); passing `getRequest()` also works.
1068
+ * Needs `session()` as root middleware; throws outside it.
1069
+ *
1070
+ * @example
1071
+ * ```ts
1072
+ * // middleware.ts
1073
+ * import { session } from '@webjsdev/server';
1074
+ * export default session(); // SESSION_SECRET from .env
1075
+ *
1076
+ * // in a page, action or route handler
1077
+ * import { getSession } from '@webjsdev/server';
1078
+ * const s = getSession();
1079
+ * s.set('cart', [1, 2]); // persists across requests
1080
+ * s.flash('notice', 'Saved'); // read once by the next request
1081
+ * const notice = s.get('notice'); // unknown; narrow it before use
1082
+ * ```
1083
+ */
1084
+ export declare function getSession(req?: Request | null): Session;
855
1085
 
856
1086
  // ---------------------------------------------------------------------------
857
1087
  // broadcast.js
858
1088
  // ---------------------------------------------------------------------------
859
1089
 
860
- /** Broadcast data to all WebSocket clients connected to a route path. */
1090
+ /**
1091
+ * Broadcast data to all WebSocket clients connected to a route path.
1092
+ *
1093
+ * @example
1094
+ * ```ts
1095
+ * // app/api/chat/route.ts: each socket that connects here is registered under '/api/chat'
1096
+ * import { broadcast } from '@webjsdev/server';
1097
+ *
1098
+ * type Socket = { on(event: 'message', cb: (data: Buffer) => void): void }; // or ws's own WebSocket type
1099
+ * export function WS(ws: Socket) {
1100
+ * ws.on('message', (data) => broadcast('/api/chat', data.toString(), { except: ws }));
1101
+ * }
1102
+ * // A 'use server' action can push to the same sockets after a write:
1103
+ * // broadcast('/api/chat', JSON.stringify({ kind: 'joined', name }))
1104
+ * ```
1105
+ * The path is the connected URL's pathname, so a dynamic route broadcasts to
1106
+ * its concrete path (`'/api/rooms/' + params.roomId`). One process only: fan out
1107
+ * across instances yourself (Redis pub/sub).
1108
+ */
861
1109
  export declare function broadcast(
862
1110
  path: string,
863
1111
  data: string | Uint8Array,
@@ -868,9 +1116,14 @@ export declare function broadcast(
868
1116
  // file-storage.js (#247)
869
1117
  // ---------------------------------------------------------------------------
870
1118
 
871
- /** A streaming handle returned by `FileStore.get` (body is a stream, never buffered). */
1119
+ /**
1120
+ * A streaming handle returned by `FileStore.get` (body is a stream, never
1121
+ * buffered). The body is a web `ReadableStream`, so `new Response(file.body)`
1122
+ * serves it as is; an adapter whose SDK returns a Node stream converts it with
1123
+ * `Readable.toWeb`.
1124
+ */
872
1125
  export interface StoredObjectHandle {
873
- body: ReadableStream | import('node:stream').Readable;
1126
+ body: ReadableStream<Uint8Array>;
874
1127
  size: number;
875
1128
  contentType: string;
876
1129
  }
@@ -900,22 +1153,117 @@ export interface FileStore {
900
1153
 
901
1154
  /** The default uploads directory (relative to cwd), gitignore-friendly. */
902
1155
  export declare const DEFAULT_UPLOAD_DIR: string;
903
- /** Local-disk file store (the default adapter). Streams writes. */
1156
+ /**
1157
+ * Local-disk file store (the default adapter). Streams writes.
1158
+ *
1159
+ * @example
1160
+ * ```ts
1161
+ * // instrumentation.ts at the app root: register() runs once at boot
1162
+ * import { setFileStore, diskStore } from '@webjsdev/server';
1163
+ *
1164
+ * export function register() {
1165
+ * // Defaults: dir '<cwd>/.webjs/uploads', baseUrl '/uploads'. url(key) is baseUrl + '/' + key,
1166
+ * // and nothing serves it for you: add the route.ts at that path (see verifySignedUrl).
1167
+ * setFileStore(diskStore({ dir: '/var/data/uploads', baseUrl: '/files' }));
1168
+ * }
1169
+ * ```
1170
+ */
904
1171
  export declare function diskStore(opts?: { dir?: string; baseUrl?: string }): FileStore;
905
- /** Set the default file store (call at startup to use a custom dir or S3). */
1172
+ /**
1173
+ * Set the default file store (call at startup to use a custom dir or S3).
1174
+ *
1175
+ * @example
1176
+ * ```ts
1177
+ * // instrumentation.ts at the app root: every getFileStore() call follows the swap
1178
+ * import { setFileStore, diskStore } from '@webjsdev/server';
1179
+ * import { r2Store } from '#lib/r2-store.server.ts'; // your adapter: put, get, delete, url
1180
+ *
1181
+ * export function register() {
1182
+ * setFileStore(process.env.R2_BUCKET ? r2Store(process.env.R2_BUCKET) : diskStore());
1183
+ * }
1184
+ * ```
1185
+ */
906
1186
  export declare function setFileStore(store: FileStore): void;
907
- /** Get the default file store (`diskStore` under `<cwd>/.webjs/uploads` unless set). */
1187
+ /**
1188
+ * Get the default file store (`diskStore` under `<cwd>/.webjs/uploads` unless set).
1189
+ *
1190
+ * @example
1191
+ * ```ts
1192
+ * // In a 'use server' action bound to <form action=${save} enctype="multipart/form-data">
1193
+ * // with <input type="file" name="photo">: the field arrives as a File.
1194
+ * import { getFileStore, generateKey } from '@webjsdev/server';
1195
+ * const key = generateKey(input.photo.name); // opaque, traversal-safe
1196
+ * await getFileStore().put(key, input.photo); // streams to disk
1197
+ *
1198
+ * // app/uploads/[key]/route.ts serves it back:
1199
+ * export async function GET(_req: Request, { params }: { params: { key: string } }) {
1200
+ * let file;
1201
+ * try { file = await getFileStore().get(params.key); } catch { file = null; } // an unsafe key throws
1202
+ * if (!file) return new Response('Not found', { status: 404 });
1203
+ * return new Response(file.body, { headers: { 'content-type': file.contentType } });
1204
+ * }
1205
+ * ```
1206
+ */
908
1207
  export declare function getFileStore(): FileStore;
909
- /** Generate a random, opaque, traversal-safe key, preserving a whitelisted extension. */
1208
+ /**
1209
+ * Generate a random, opaque, traversal-safe key, preserving a whitelisted extension.
1210
+ *
1211
+ * @example
1212
+ * ```ts
1213
+ * // modules/photos/actions/upload-photo.server.ts, bound as <form action=${uploadPhoto}>
1214
+ * // holding <input type="file" name="photo">
1215
+ * 'use server';
1216
+ * import { getFileStore, generateKey, type FormInput } from '@webjsdev/server';
1217
+ *
1218
+ * export async function uploadPhoto(input: FormInput<{ photo: File }>) {
1219
+ * const key = generateKey(input.photo.name); // '<uuid>.jpg': never the user's own name or path
1220
+ * await getFileStore().put(key, input.photo);
1221
+ * return { success: true, data: { key } }; // store the key on the row
1222
+ * }
1223
+ * ```
1224
+ */
910
1225
  export declare function generateKey(filename?: string): string;
911
1226
  /** Validate a key and return the absolute path it resolves to (throws on traversal). */
912
1227
  export declare function assertSafeKey(dir: string, key: string): string;
913
- /** Mint a signed, expiring URL for a stored object (HMAC-SHA256). */
1228
+ /**
1229
+ * Mint a signed, expiring URL for a stored object (HMAC-SHA256).
1230
+ *
1231
+ * @example
1232
+ * ```ts
1233
+ * // modules/photos/utils/photo-url.server.ts: a link that works for an hour, no session needed
1234
+ * import { signedUrl } from '@webjsdev/server';
1235
+ *
1236
+ * export function photoUrl(key: string) {
1237
+ * return signedUrl(key, {
1238
+ * secret: process.env.FILE_URL_SECRET!,
1239
+ * base: '/files/' + encodeURIComponent(key), // the route.ts serving it (default getFileStore().url(key))
1240
+ * expiresIn: 3600, // seconds, the default (0 or less is already expired)
1241
+ * });
1242
+ * }
1243
+ * // returns '/files/<key>?key=<key>&exp=<unix seconds>&sig=<hmac>', checked by verifySignedUrl
1244
+ * ```
1245
+ */
914
1246
  export declare function signedUrl(
915
1247
  key: string,
916
1248
  opts: { secret: string; expiresIn?: number; base?: string },
917
1249
  ): string;
918
- /** Verify a signed URL (or parsed params); constant-time, checks expiry + tamper. */
1250
+ /**
1251
+ * Verify a signed URL (or parsed params); constant-time, checks expiry + tamper.
1252
+ *
1253
+ * @example
1254
+ * ```ts
1255
+ * // app/files/[key]/route.ts: serve a stored file only to a valid, unexpired signed link
1256
+ * import { getFileStore, verifySignedUrl } from '@webjsdev/server';
1257
+ *
1258
+ * export async function GET(req: Request, { params }: { params: { key: string } }) {
1259
+ * const check = verifySignedUrl(req.url, process.env.FILE_URL_SECRET!); // { valid, key, reason? }
1260
+ * if (!check.valid || check.key !== params.key) return new Response('Forbidden', { status: 403 });
1261
+ * const file = await getFileStore().get(params.key);
1262
+ * if (!file) return new Response('Not found', { status: 404 });
1263
+ * return new Response(file.body, { headers: { 'content-type': file.contentType, 'x-content-type-options': 'nosniff' } });
1264
+ * }
1265
+ * ```
1266
+ */
919
1267
  export declare function verifySignedUrl(
920
1268
  input: string | URL | URLSearchParams | { key?: string; exp?: string | number; sig?: string },
921
1269
  secret: string,
@@ -925,9 +1273,39 @@ export declare function verifySignedUrl(
925
1273
  // json.js
926
1274
  // ---------------------------------------------------------------------------
927
1275
 
928
- /** Content-negotiated JSON response (rich wire format when the client asks for it). */
1276
+ /**
1277
+ * Content-negotiated JSON response (rich wire format when the client asks for it).
1278
+ *
1279
+ * @example
1280
+ * ```ts
1281
+ * // app/api/posts/route.ts
1282
+ * import { json } from '@webjsdev/server';
1283
+ * import { listPosts } from '#modules/posts/queries/list-posts.server.ts';
1284
+ *
1285
+ * export async function GET() {
1286
+ * // Plain JSON for curl and fetch; richFetch from @webjsdev/core gets Date, Map and BigInt back intact.
1287
+ * return json(await listPosts(), { headers: { 'cache-control': 'max-age=60' } });
1288
+ * }
1289
+ * ```
1290
+ */
929
1291
  export declare function json<T>(data: T, init?: ResponseInit): Promise<Response>;
930
- /** Parse a request body (webjs rich format or plain JSON); enforces the body-size limit. */
1292
+ /**
1293
+ * Parse a request body (WebJs rich format or plain JSON); enforces the body-size limit.
1294
+ *
1295
+ * @example
1296
+ * ```ts
1297
+ * // app/api/posts/route.ts
1298
+ * import { json, readBody } from '@webjsdev/server';
1299
+ * import { createPost } from '#modules/posts/queries/create-post.server.ts';
1300
+ *
1301
+ * export async function POST(req: Request) {
1302
+ * const body = (await readBody(req)) as { title?: unknown } | null; // null for an empty body
1303
+ * if (typeof body?.title !== 'string' || !body.title) return json({ error: 'title is required' }, { status: 422 });
1304
+ * return json(await createPost({ title: body.title }), { status: 201 });
1305
+ * }
1306
+ * // A body over the limit (1 MiB by default) is answered 413 before it is parsed.
1307
+ * ```
1308
+ */
931
1309
  export declare function readBody(req: Request): Promise<unknown>;
932
1310
 
933
1311
  // ---------------------------------------------------------------------------
@@ -946,7 +1324,25 @@ export interface SitemapIndexEntry {
946
1324
  url: string;
947
1325
  lastModified?: string | Date;
948
1326
  }
949
- /** Serialize entries into a spec-valid `<urlset>` XML document. */
1327
+ /**
1328
+ * Serialize entries into a spec-valid `<urlset>` XML document.
1329
+ *
1330
+ * @example
1331
+ * ```ts
1332
+ * // app/sitemap.ts, served at /sitemap.xml. Returning the entries array also works,
1333
+ * // sitemap() is for when you want the XML string yourself.
1334
+ * import { sitemap, type MetadataRouteContext } from '@webjsdev/server';
1335
+ * import { listPosts } from '#modules/posts/queries/list-posts.server.ts';
1336
+ *
1337
+ * export default async function ({ siteUrl, pages }: MetadataRouteContext) {
1338
+ * const posts = await listPosts();
1339
+ * return sitemap([
1340
+ * ...(await pages()), // every static page, already absolute
1341
+ * ...posts.map((p) => ({ url: siteUrl + '/posts/' + p.id, lastModified: p.updatedAt, priority: 0.7 })),
1342
+ * ]);
1343
+ * }
1344
+ * ```
1345
+ */
950
1346
  export declare function sitemap(entries: SitemapEntry[]): string;
951
1347
  /** Serialize child sitemaps into a `<sitemapindex>` XML document. */
952
1348
  export declare function sitemapIndex(sitemaps: SitemapIndexEntry[]): string;
@@ -965,7 +1361,23 @@ export interface RobotsConfig {
965
1361
  sitemap?: string | string[];
966
1362
  host?: string;
967
1363
  }
968
- /** Serialize a robots config into robots.txt text. */
1364
+ /**
1365
+ * Serialize a robots config into robots.txt text.
1366
+ *
1367
+ * @example
1368
+ * ```ts
1369
+ * // app/robots.ts, served at /robots.txt. Return the config and WebJs runs robots() on it.
1370
+ * import type { MetadataRouteContext } from '@webjsdev/server';
1371
+ *
1372
+ * export default function ({ siteUrl }: MetadataRouteContext) {
1373
+ * return { rules: { userAgent: '*', disallow: ['/api/', '/account'] }, sitemap: siteUrl + '/sitemap.xml' };
1374
+ * }
1375
+ *
1376
+ * // Calling it yourself returns the text, for a handler building its own Response:
1377
+ * import { robots } from '@webjsdev/server';
1378
+ * robots({ rules: [{ userAgent: '*', allow: '/' }, { userAgent: 'GPTBot', disallow: '/' }] });
1379
+ * ```
1380
+ */
969
1381
  export declare function robots(config?: RobotsConfig): string;
970
1382
 
971
1383
  /**
@@ -999,9 +1411,42 @@ export type RobotsRouteResult = RobotsConfig | string | Response;
999
1411
 
1000
1412
  /** The content type that negotiates and carries a stream-action response. */
1001
1413
  export declare const STREAM_MIME: string;
1002
- /** Report whether a request opted into a stream-action response (its `Accept` carries the stream MIME). */
1414
+ /**
1415
+ * Report whether a request opted into a stream-action response (its `Accept` carries the stream MIME).
1416
+ *
1417
+ * @example
1418
+ * ```ts
1419
+ * // app/todos/[id]/done/route.ts, posted by <form method="post" action="/todos/7/done">
1420
+ * import { acceptsStream, stream, streamResponse } from '@webjsdev/server';
1421
+ * import { completeTodo } from '#modules/todos/queries/complete-todo.server.ts';
1422
+ *
1423
+ * export async function POST(req: Request, { params }: { params: { id: string } }) {
1424
+ * await completeTodo(Number(params.id));
1425
+ * // The client router asks for a stream on a form submit, a browser with JS off does not.
1426
+ * if (acceptsStream(req)) return streamResponse(stream.remove('todo-' + params.id));
1427
+ * return Response.redirect(new URL('/todos', req.url), 303);
1428
+ * }
1429
+ * ```
1430
+ */
1003
1431
  export declare function acceptsStream(req: Request | { headers?: Headers }): boolean;
1004
- /** The stream-action builder; each method returns one `<webjs-stream>` HTML string. */
1432
+ /**
1433
+ * The stream-action builder; each method returns one `<webjs-stream>` HTML string.
1434
+ *
1435
+ * @example
1436
+ * ```ts
1437
+ * // modules/comments/utils/comment-streams.server.ts
1438
+ * import { stream } from '@webjsdev/server';
1439
+ * import { escapeText } from '@webjsdev/core';
1440
+ *
1441
+ * // The target is an element id. It is escaped, the content is not: escape user text yourself.
1442
+ * export const commentAdded = (c: { id: number; body: string }) =>
1443
+ * stream.append('comments', '<li id="comment-' + c.id + '">' + escapeText(c.body) + '</li>');
1444
+ * export const commentDeleted = (id: number) => stream.remove('comment-' + id);
1445
+ * // Also prepend, before, after, replace (the element itself) and update (its children).
1446
+ * // Send them with streamResponse(...) to a form submit, or broadcast(path, html) to sockets,
1447
+ * // where connectWS(path, { onMessage: (m) => renderStream(m) }) applies them.
1448
+ * ```
1449
+ */
1005
1450
  export declare const stream: {
1006
1451
  append(target: string, content: string): string;
1007
1452
  prepend(target: string, content: string): string;
@@ -1011,7 +1456,25 @@ export declare const stream: {
1011
1456
  update(target: string, content: string): string;
1012
1457
  remove(target: string): string;
1013
1458
  };
1014
- /** Wrap one or more stream-action strings in a `Response` carrying the stream content type. */
1459
+ /**
1460
+ * Wrap one or more stream-action strings in a `Response` carrying the stream content type.
1461
+ *
1462
+ * @example
1463
+ * ```ts
1464
+ * // modules/todos/actions/add-todo.server.ts, bound as <form action=${addTodo}> beside <ul id="todos">
1465
+ * 'use server';
1466
+ * import { acceptsStream, getRequest, stream, streamResponse, type FormInput } from '@webjsdev/server';
1467
+ * import { escapeText } from '@webjsdev/core';
1468
+ * import { insertTodo } from '../queries/insert-todo.server.ts';
1469
+ *
1470
+ * export async function addTodo(input: FormInput<{ title: string }>) {
1471
+ * const todo = await insertTodo(input.title);
1472
+ * const req = getRequest();
1473
+ * if (req && acceptsStream(req)) return streamResponse(stream.append('todos', '<li>' + escapeText(todo.title) + '</li>'));
1474
+ * return { success: true, redirect: '/todos' }; // JS off: the usual redirect and full render
1475
+ * }
1476
+ * ```
1477
+ */
1015
1478
  export declare function streamResponse(...parts: string[]): Response;
1016
1479
 
1017
1480
  // ---------------------------------------------------------------------------
@@ -1080,13 +1543,102 @@ export type ResolvedAuthUser = keyof AuthUser extends never
1080
1543
  : AuthUser;
1081
1544
 
1082
1545
  /** Auth configuration for `createAuth`. */
1546
+ /**
1547
+ * The Auth.js adapter methods `createAuth` calls, the shape `@auth/*-adapter`
1548
+ * packages implement (`@auth/core/adapters`).
1549
+ */
1550
+ export interface AuthAdapter {
1551
+ createUser(user: Record<string, any>): Promise<any> | any;
1552
+ getUser?(id: string): Promise<any> | any;
1553
+ getUserByEmail(email: string): Promise<any> | any;
1554
+ getUserByAccount(account: { provider: string; providerAccountId: string }): Promise<any> | any;
1555
+ updateUser?(user: Record<string, any>): Promise<any> | any;
1556
+ linkAccount(account: Record<string, any>): Promise<any> | any;
1557
+ createSession(session: { sessionToken: string; userId: string; expires: Date }): Promise<any> | any;
1558
+ getSessionAndUser(sessionToken: string): Promise<{ session: { sessionToken: string; userId: string; expires: Date }; user: any } | null> | { session: any; user: any } | null;
1559
+ updateSession?(session: { sessionToken: string; expires?: Date }): Promise<any> | any;
1560
+ deleteSession(sessionToken: string): Promise<any> | any;
1561
+ }
1562
+ /**
1563
+ * The token the `jwt` and `session` callbacks see, Auth.js's `JWT`: the
1564
+ * standard claims plus whatever the `jwt` callback put on it.
1565
+ */
1566
+ export interface AuthJWT {
1567
+ sub?: string;
1568
+ name?: string | null;
1569
+ email?: string | null;
1570
+ image?: string | null;
1571
+ iat?: number;
1572
+ exp?: number;
1573
+ [key: string]: unknown;
1574
+ }
1575
+
1576
+ /** The user the callbacks see, Auth.js's `User`: what `authorize` or the provider returned. */
1577
+ export interface AuthCallbackUser {
1578
+ id?: string;
1579
+ name?: string | null;
1580
+ email?: string | null;
1581
+ image?: string | null;
1582
+ [key: string]: unknown;
1583
+ }
1584
+
1585
+ /** The session the `session` callback receives and returns, Auth.js's `Session`. */
1586
+ export interface AuthCallbackSession {
1587
+ user: ResolvedAuthUser & { name?: string | null; email?: string | null; image?: string | null };
1588
+ expires: string;
1589
+ [key: string]: unknown;
1590
+ }
1591
+
1592
+ type AuthAwaitable<T> = T | Promise<T>;
1593
+
1594
+ /**
1595
+ * Auth.js's callbacks, with the parameters Auth.js passes, so a callback
1596
+ * written as Auth.js documents it (`jwt({ token, user })`,
1597
+ * `session({ session, token })`) is typed with no annotation. Declared as
1598
+ * methods so a callback annotated with its own narrower types still compiles.
1599
+ */
1600
+ export interface AuthCallbacks {
1601
+ /** At sign-in (`trigger: 'signIn'`, with `user`, `account` and `profile`) and on every session read. */
1602
+ jwt?(params: { token: AuthJWT; user: AuthCallbackUser; account?: Record<string, any> | null; profile?: Record<string, any>; trigger?: 'signIn' | 'signUp' | 'update'; session?: any }): AuthAwaitable<AuthJWT | null>;
1603
+ /** On every session read. `token` under the jwt strategy, `user` under the database strategy. */
1604
+ session?(params: { session: AuthCallbackSession; token: AuthJWT; user: AuthCallbackUser }): AuthAwaitable<any>;
1605
+ /** Return true to allow, a url string to redirect there instead, anything else to deny (Auth.js's rule). */
1606
+ signIn?(params: { user: AuthCallbackUser; account?: Record<string, any> | null; profile?: Record<string, any>; credentials?: Record<string, any> }): AuthAwaitable<boolean | string>;
1607
+ redirect?(params: { url: string; baseUrl: string }): AuthAwaitable<string>;
1608
+ /** Run by `auth` as middleware: true continues, false redirects to sign in, a Response is sent as is. */
1609
+ authorized?: (params: { auth: { user: Record<string, unknown> } | null; request: Request }) => boolean | Response | Promise<boolean | Response>;
1610
+ }
1611
+
1083
1612
  export interface AuthConfig {
1084
- providers: ProviderConfig[];
1085
- session?: { strategy?: 'jwt' | 'database'; maxAge?: number };
1086
- secret: string;
1087
- callbacks?: { session?: Function; jwt?: Function; signIn?: Function; redirect?: Function };
1088
- adapter?: { load?: Function; save?: Function; destroy?: Function };
1613
+ /** A provider config, or the provider function uncalled (`providers: [GitHub]`), as Auth.js accepts. */
1614
+ providers: Array<ProviderConfig | (() => ProviderConfig)>;
1615
+ /**
1616
+ * `strategy` defaults to `'database'` when an Auth.js adapter is given, else
1617
+ * `'jwt'`. `updateAge` (seconds, default a day) is how often a database
1618
+ * session's expiry is pushed forward, as in Auth.js.
1619
+ */
1620
+ session?: { strategy?: 'jwt' | 'database'; maxAge?: number; updateAge?: number };
1621
+ /** Signs the session. Defaults to `process.env.AUTH_SECRET`, as in Auth.js. */
1622
+ secret?: string | undefined;
1623
+ callbacks?: AuthCallbacks;
1624
+ /**
1625
+ * An Auth.js database adapter (`DrizzleAdapter(db)` from `@auth/drizzle-adapter`,
1626
+ * and the rest), or WebJs's own session store for the database strategy.
1627
+ */
1628
+ adapter?: AuthAdapter | { load?: Function; save?: Function; destroy?: Function };
1089
1629
  pages?: { signIn?: string; signOut?: string; error?: string };
1630
+ /**
1631
+ * Auth.js's `useSecureCookies`: force the Secure flag and the `__Host-`
1632
+ * cookie names on or off. Unset, they follow the auth URL's protocol
1633
+ * (`AUTH_URL`'s when set, else the request's), never `NODE_ENV`.
1634
+ */
1635
+ useSecureCookies?: boolean;
1636
+ /**
1637
+ * Where the auth routes are mounted, as Auth.js's `basePath`. Defaults to
1638
+ * the path of `AUTH_URL` when it has one, else `/api/auth`. `AUTH_URL` (or
1639
+ * `NEXTAUTH_URL`) also sets the origin OAuth callback URLs use.
1640
+ */
1641
+ basePath?: string;
1090
1642
  }
1091
1643
 
1092
1644
  /**
@@ -1097,26 +1649,142 @@ export interface AuthConfig {
1097
1649
  * via `createAuth<MyUser>(...)`.
1098
1650
  */
1099
1651
  export interface AuthInstance<TUser = ResolvedAuthUser> {
1100
- auth: (req?: Request) => Promise<{ user: TUser } | null>;
1101
- signIn: (
1102
- provider: string,
1103
- data?: Record<string, unknown>,
1104
- opts?: { redirectTo?: string; req?: Request },
1105
- ) => Promise<Response>;
1106
- signOut: (opts?: { redirectTo?: string; req?: Request }) => Promise<Response>;
1652
+ /**
1653
+ * Auth.js's `auth`, in its three shapes: `auth()` / `auth(req)` reads the
1654
+ * session; `export { auth as middleware }` runs as middleware (it sets
1655
+ * `req.auth` and applies `callbacks.authorized`); `auth((req) => ...)` wraps
1656
+ * a middleware whose `req.auth` holds the session.
1657
+ */
1658
+ auth: {
1659
+ (req?: Request | null): Promise<{ user: TUser } | null>;
1660
+ (req: Request, next: unknown): Promise<Response | undefined>;
1661
+ <R>(handler: (req: Request & { auth: { user: TUser } | null }, ...rest: any[]) => R): (req: Request, ...rest: any[]) => Promise<Response | Awaited<R>>;
1662
+ };
1663
+ /**
1664
+ * Auth.js v5's `signIn`. Inside a request (a server action, a route handler,
1665
+ * middleware) it sets the session cookie on the response and throws a
1666
+ * redirect, so `await signIn('credentials', formData)` in a form action signs
1667
+ * in and navigates; a credentials failure throws {@link CredentialsSignin}.
1668
+ * `{ redirect: false }` (in the options or, as Auth.js accepts it, in the
1669
+ * data) returns the target url instead of throwing. With no request in scope
1670
+ * it returns the Response the `/api/auth/*` routes send. The redirecting
1671
+ * form is typed `Promise<Response>`, so `return signIn(...)` fits an action
1672
+ * that returns `ActionResult | Response`.
1673
+ */
1674
+ signIn: {
1675
+ (
1676
+ provider: string,
1677
+ data: (FormData | Record<string, unknown>) | undefined,
1678
+ opts: { redirectTo?: string; redirect: false; req?: Request },
1679
+ ): Promise<string>;
1680
+ (
1681
+ provider: string,
1682
+ data: Record<string, unknown> & { redirect: false },
1683
+ opts?: { redirectTo?: string; req?: Request },
1684
+ ): Promise<string>;
1685
+ (
1686
+ provider: string,
1687
+ data?: FormData | Record<string, unknown>,
1688
+ opts?: { redirectTo?: string; redirect?: true; req?: Request },
1689
+ ): Promise<Response>;
1690
+ };
1691
+ /** Auth.js v5's `signOut`: clears the session and throws a redirect inside a request. */
1692
+ signOut: {
1693
+ (opts: { redirectTo?: string; redirect: false; req?: Request }): Promise<string>;
1694
+ (opts?: { redirectTo?: string; redirect?: true; req?: Request }): Promise<Response>;
1695
+ };
1107
1696
  handlers: { GET: (req: Request) => Promise<Response>; POST: (req: Request) => Promise<Response> };
1108
1697
  }
1109
1698
 
1110
1699
  /**
1111
- * Create the auth system. Parameterise with the session-user shape
1112
- * (`createAuth<MyUser>(...)`) for per-instance typing, or augment the
1113
- * {@link AuthUser} interface for global typing; both make `auth().user` typed
1114
- * with no cast. Defaults to the open {@link AuthUser}, so existing untyped code
1115
- * keeps compiling unchanged.
1700
+ * Create the auth system, Auth.js's API. `createAuth<MyUser>(...)` (or an
1701
+ * augmented {@link AuthUser}) types `auth().user` with no cast.
1702
+ *
1703
+ * @example
1704
+ * ```ts
1705
+ * // modules/auth/auth.server.ts
1706
+ * import { createAuth, Credentials, AuthError } from '@webjsdev/server';
1707
+ * export const { auth, signIn, signOut, handlers } = createAuth({
1708
+ * providers: [Credentials({
1709
+ * async authorize(c: { email: string; password: string }) {
1710
+ * const user = await findUserByEmail(c.email); // your own query
1711
+ * if (!user || !(await verifyPassword(c.password, user.passwordHash))) return null;
1712
+ * return { id: String(user.id), email: user.email };
1713
+ * },
1714
+ * })],
1715
+ * pages: { error: '/login' }, // an uncaught failure lands on /login?error=CredentialsSignin
1716
+ * });
1717
+ * // app/api/auth/[...path]/route.ts: export const { GET, POST } = handlers;
1718
+ * // Read the user anywhere on the server: const session = await auth();
1719
+ *
1720
+ * // A 'use server' action bound as <form action=${logIn}>, as with Auth.js in Next.
1721
+ * // AuthError comes from '@webjsdev/server' (as above), never from auth.server.ts:
1722
+ * export async function logIn(formData: FormData) {
1723
+ * try {
1724
+ * await signIn('credentials', formData, { redirectTo: '/dashboard' });
1725
+ * } catch (error) {
1726
+ * if (error instanceof AuthError) return { success: false, error: 'Wrong email or password.' };
1727
+ * throw error; // the success redirect
1728
+ * }
1729
+ * }
1730
+ * // Sign out the same way: await signOut({ redirectTo: '/' }).
1731
+ * ```
1116
1732
  */
1117
1733
  export declare function createAuth<TUser = ResolvedAuthUser>(config: AuthConfig): AuthInstance<TUser>;
1118
- /** The credentials (email/password) provider. */
1119
- export declare function Credentials(opts: Record<string, unknown>): ProviderConfig;
1734
+ /** Next.js's navigation helpers (`next/navigation`), re-exported from `@webjsdev/core` so server code can import them from either package. */
1735
+ export declare const notFound: typeof import('@webjsdev/core').notFound;
1736
+ export declare const forbidden: typeof import('@webjsdev/core').forbidden;
1737
+ export declare const unauthorized: typeof import('@webjsdev/core').unauthorized;
1738
+ export declare const redirect: typeof import('@webjsdev/core').redirect;
1739
+ export declare const permanentRedirect: typeof import('@webjsdev/core').permanentRedirect;
1740
+
1741
+ /** Auth.js's base error: `error instanceof AuthError` and `error.type` say why a sign-in failed. */
1742
+ export declare class AuthError extends Error {
1743
+ type: string;
1744
+ }
1745
+ /** Thrown by `signIn('credentials', ...)` when `authorize` returns null. */
1746
+ export declare class CredentialsSignin extends AuthError {
1747
+ type: 'CredentialsSignin';
1748
+ code: string;
1749
+ }
1750
+ /**
1751
+ * Options for the credentials provider, Auth.js's `Credentials`. `C` is the
1752
+ * shape of the submitted fields, inferred from `authorize`'s annotation
1753
+ * (`authorize(credentials: { email: string; password: string })`).
1754
+ */
1755
+ export interface CredentialsOptions<C extends Record<string, unknown> = Record<string, unknown>> {
1756
+ /**
1757
+ * Check the submitted fields (the sign-in form's fields, or the object passed
1758
+ * to `signIn('credentials', data)`). Return the session user, or `null` to
1759
+ * refuse the sign-in.
1760
+ */
1761
+ authorize: (credentials: C) => Promise<Record<string, unknown> | null> | Record<string, unknown> | null;
1762
+ [key: string]: unknown;
1763
+ }
1764
+ /**
1765
+ * The credentials (email/password) provider, Auth.js's `Credentials`.
1766
+ *
1767
+ * @example
1768
+ * ```ts
1769
+ * // modules/auth/auth.server.ts
1770
+ * import { createAuth, Credentials } from '@webjsdev/server';
1771
+ * import { findUserByEmail } from '#modules/users/queries/find-user-by-email.server.ts';
1772
+ * import { verifyPassword } from './password.server.ts';
1773
+ *
1774
+ * export const { auth, signIn, signOut, handlers } = createAuth({
1775
+ * providers: [Credentials({
1776
+ * async authorize(credentials: { email: string; password: string }) {
1777
+ * const user = await findUserByEmail(credentials.email);
1778
+ * if (!user || !(await verifyPassword(credentials.password, user.passwordHash))) return null;
1779
+ * return { id: String(user.id), email: user.email }; // the session user, null refuses
1780
+ * },
1781
+ * })],
1782
+ * });
1783
+ * ```
1784
+ */
1785
+ export declare function Credentials<C extends Record<string, unknown> = Record<string, unknown>>(
1786
+ opts: CredentialsOptions<C>,
1787
+ ): ProviderConfig;
1120
1788
  /** The Google OAuth provider. */
1121
1789
  export declare function Google(opts?: Record<string, unknown>): ProviderConfig;
1122
1790
  /** The GitHub OAuth provider. */