@webjsdev/server 0.9.0 → 0.10.0

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 (101) hide show
  1. package/README.md +4 -1
  2. package/index.d.ts +427 -109
  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 +96 -14
  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 +262 -0
  31. package/src/core-exports.js +190 -0
  32. package/src/dev/bun-app-source.js +95 -5
  33. package/src/dev/config.js +7 -51
  34. package/src/dev/dev-service-worker.js +48 -0
  35. package/src/dev/esm-hot-hooks.js +373 -0
  36. package/src/dev/handler.js +502 -155
  37. package/src/dev/helpers.js +127 -96
  38. package/src/dev/hot-host.js +33 -6
  39. package/src/dev/poke-state.js +110 -0
  40. package/src/dev/serve.js +177 -129
  41. package/src/dev/server.js +92 -33
  42. package/src/dev/specifier-versions.js +125 -0
  43. package/src/dev/watch-ignore.js +49 -4
  44. package/src/dev/watch-poll.js +78 -0
  45. package/src/dev/watch-tree.js +59 -22
  46. package/src/dev-classify.js +59 -26
  47. package/src/dev-error.js +104 -3
  48. package/src/dev-hot-classify.js +291 -0
  49. package/src/dev-hot-client.js +245 -0
  50. package/src/dev-import.js +22 -5
  51. package/src/dev-overlay.js +26 -0
  52. package/src/dev-ready-probe.js +69 -0
  53. package/src/dev-regenerate.js +2 -2
  54. package/src/dev-reload-hold.js +49 -20
  55. package/src/dev-reload-reason.js +61 -0
  56. package/src/dev-reload-worker.js +62 -13
  57. package/src/dev-secret.js +73 -0
  58. package/src/dev-source-locations-hook.js +4 -1
  59. package/src/dev-source-locations.js +33 -2
  60. package/src/dev.js +0 -1
  61. package/src/env-files.js +124 -0
  62. package/src/file-storage.js +2 -2
  63. package/src/foreign-imports.js +228 -0
  64. package/src/form-action-identity.js +52 -25
  65. package/src/form-dispatch.js +140 -65
  66. package/src/html-cache.js +35 -4
  67. package/src/importmap.js +2 -2
  68. package/src/instrumentation.js +59 -3
  69. package/src/js-scan.js +5 -4
  70. package/src/jwe.js +155 -0
  71. package/src/lazy-components.js +166 -0
  72. package/src/listener-core.js +4 -2
  73. package/src/lit-hints.js +172 -0
  74. package/src/middleware-load.js +248 -0
  75. package/src/module-graph.js +73 -47
  76. package/src/module-side-effect.js +284 -0
  77. package/src/public-files.js +73 -0
  78. package/src/redirect-location.js +19 -0
  79. package/src/redirects.js +9 -1
  80. package/src/route-frontier.js +151 -0
  81. package/src/route-report.js +130 -0
  82. package/src/router.js +120 -13
  83. package/src/session.js +32 -10
  84. package/src/ssr/client-router-flag.js +1 -1
  85. package/src/ssr/document.js +37 -13
  86. package/src/ssr/head.js +78 -23
  87. package/src/ssr/preloads.js +31 -45
  88. package/src/ssr/render.js +241 -55
  89. package/src/ssr/responses.js +51 -4
  90. package/src/ssr/shipped-modules.js +22 -16
  91. package/src/testing.d.ts +31 -2
  92. package/src/testing.js +80 -17
  93. package/src/thenable-params.js +26 -0
  94. package/src/vendor/jspm.js +12 -1
  95. package/src/vendor/pins.js +9 -8
  96. package/src/vendor/scanner.js +14 -9
  97. package/src/vendor.js +5 -3
  98. package/webjs-config.schema.json +1 -6
  99. package/src/component-elision.js +0 -1828
  100. package/src/elision-differential.js +0 -101
  101. 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>;
@@ -248,6 +288,14 @@ export declare function matchApi(
248
288
  table: unknown,
249
289
  pathname: string,
250
290
  ): { route: { file: string }; params: Record<string, string> } | null;
291
+ /**
292
+ * The URL paths of every STATIC page route in a route table, sorted and
293
+ * deduped. Dynamic routes are excluded (listing one would mean inventing
294
+ * param values).
295
+ */
296
+ export declare function staticPageRoutes(table: {
297
+ pages?: Array<{ routeDir?: string; paramNames?: string[] }>;
298
+ } | undefined): string[];
251
299
 
252
300
  // ---------------------------------------------------------------------------
253
301
  // route-types.js (#258)
@@ -342,8 +390,7 @@ export declare function setVendorEntries(
342
390
  /**
343
391
  * Bare-specifier npm imports an app could load in the browser, found by walking
344
392
  * 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.
393
+ * directory. The result is a superset of what the running server serves.
347
394
  */
348
395
  export declare function scanBareImports(dir: string): Promise<Set<string>>;
349
396
  /** Extract the package name from an import specifier (`dayjs/plugin/utc` -> `dayjs`). */
@@ -492,8 +539,8 @@ export declare function extractComponents(src: string): Array<{ className: strin
492
539
  * because there is no registration call anywhere or because the tag is
493
540
  * computed. The registration cross-reference is app-wide, so a class a sibling
494
541
  * 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.
542
+ * scanner, so both always lose the tag-to-module registry entry, the boot import
543
+ * and the preload hint; assume the element does not upgrade either.
497
544
  * This is about REGISTRATION, never about import reachability. The full
498
545
  * semantics, including the one case where a computed tag still upgrades, live
499
546
  * on `findOrphanComponents` in `src/component-scanner.js`.
@@ -503,75 +550,53 @@ export declare function findOrphanComponents(
503
550
  ): Promise<Array<{ className: string; file: string }>>;
504
551
 
505
552
  // ---------------------------------------------------------------------------
506
- // elision-report.js (#646, #1308)
553
+ // route-report.js (#1686)
507
554
  // ---------------------------------------------------------------------------
508
555
 
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 {
556
+ /**
557
+ * One route module (page, layout or boundary). A route module never ships to
558
+ * the browser; the boot imports `emits`, its component frontier, in its place.
559
+ */
560
+ export interface RouteModuleRow {
527
561
  file: string;
528
- verdict: 'inert' | 'import-only' | 'shipped';
529
- /** For `import-only`: the component modules the boot emits in its place. */
562
+ kind: 'page' | 'layout' | 'error' | 'loading' | 'not-found' | 'forbidden' | 'unauthorized' | 'global-not-found';
563
+ /** The component modules the boot imports in this module's place (empty: zero JS). */
530
564
  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;
565
+ /** Advisory notes on code that looks meant for the browser but runs only on the server. */
566
+ hints: string[];
534
567
  }
535
568
 
536
569
  /** A `class X extends WebComponent` with no literal-tag registration. */
537
- export interface ElisionOrphanRow {
570
+ export interface OrphanRow {
538
571
  file: string;
539
572
  className: string;
540
573
  }
541
574
 
542
- export interface ElisionSummary {
543
- components: number;
544
- elided: number;
545
- shipped: number;
575
+ export interface RouteReportSummary {
546
576
  routeModules: number;
547
- inert: number;
548
- importOnly: number;
549
- shippedWhole: number;
577
+ /** Route modules whose frontier is empty: the browser loads nothing for them. */
578
+ zeroJs: number;
579
+ /** Route modules whose frontier names at least one component. */
580
+ carriers: number;
550
581
  orphans: number;
551
582
  }
552
583
 
553
- /** The whole app-level elision verdict. Every path is app-relative; every array is sorted by `file`. */
554
- export interface ElisionReport {
584
+ /** What each route costs the browser. Every path is app-relative; every array is sorted by `file`. */
585
+ export interface RouteReport {
555
586
  analysed: boolean;
556
587
  /** 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;
588
+ skipped: 'no-app' | 'unanalysable' | null;
589
+ routeModules: RouteModuleRow[];
590
+ orphans: OrphanRow[];
591
+ summary: RouteReportSummary;
562
592
  }
563
593
 
564
594
  /**
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.
595
+ * Every route module with the components the boot loads in its place and any
596
+ * hints about browser-looking code in it, plus every orphan component class.
597
+ * Consumed by both `webjs doctor` route checks.
573
598
  */
574
- export declare function analyzeAppElision(appDir: string): Promise<ElisionReport>;
599
+ export declare function analyzeRouteModules(appDir: string): Promise<RouteReport>;
575
600
 
576
601
  // ---------------------------------------------------------------------------
577
602
  // dev-regenerate.js (#967, #1672)
@@ -615,39 +640,80 @@ export declare function regenerateStatus(
615
640
  ): Promise<Array<{ output: string; command: string; state: RegenerateState }>>;
616
641
 
617
642
  // ---------------------------------------------------------------------------
618
- // elision-differential.js (#1308)
643
+ // context.js (per-request context helpers)
619
644
  // ---------------------------------------------------------------------------
620
645
 
621
646
  /**
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.
647
+ * The in-flight request's headers, read-only as Next.js's `ReadonlyHeaders`:
648
+ * `set`, `append` and `delete` throw at runtime and are absent from the type.
627
649
  */
628
- export declare function maskJsSet(html: string): string;
629
-
650
+ export type ReadonlyHeaders = Omit<Headers, 'set' | 'append' | 'delete'>;
651
+ /** Read-only headers for the in-flight request. Throws outside a request scope. */
652
+ export declare function headers(): ReadonlyHeaders;
653
+ /** A cookie as read from the request. */
654
+ export interface RequestCookie { name: string; value: string }
655
+ /** A cookie to write (Next's `ResponseCookie`; `maxAge` in seconds, `path` defaults to `/`). */
656
+ export interface ResponseCookie {
657
+ name: string;
658
+ value: string;
659
+ expires?: Date | number;
660
+ maxAge?: number;
661
+ domain?: string;
662
+ path?: string;
663
+ secure?: boolean;
664
+ httpOnly?: boolean;
665
+ sameSite?: 'lax' | 'strict' | 'none' | 'Lax' | 'Strict' | 'None' | boolean;
666
+ partitioned?: boolean;
667
+ priority?: 'low' | 'medium' | 'high';
668
+ }
669
+ /** The request's cookie store (Next.js's `cookies()` shape). */
630
670
  /**
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).
671
+ * Next.js's `RequestCookies`, `request.cookies` in middleware and a route
672
+ * handler. Reads are the request's cookies; `set`, `delete` and `clear` change
673
+ * what the rest of this request reads (the page's `cookies()` included) and
674
+ * send nothing to the browser. Set a response cookie with `cookies().set()`.
634
675
  */
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;
676
+ export interface MiddlewareRequestCookies {
677
+ get(name: string | { name: string }): RequestCookie | undefined;
678
+ getAll(name?: string | { name: string }): RequestCookie[];
648
679
  has(name: string): boolean;
649
- entries(): [string, string][];
650
- };
680
+ readonly size: number;
681
+ set(name: string, value: string): MiddlewareRequestCookies;
682
+ set(cookie: RequestCookie): MiddlewareRequestCookies;
683
+ delete(names: string): boolean;
684
+ delete(names: string[]): boolean[];
685
+ clear(): MiddlewareRequestCookies;
686
+ toString(): string;
687
+ [Symbol.iterator](): IterableIterator<[string, RequestCookie]>;
688
+ }
689
+ export interface RequestCookies {
690
+ get(name: string | { name: string }): RequestCookie | undefined;
691
+ getAll(name?: string | { name: string }): RequestCookie[];
692
+ has(name: string): boolean;
693
+ readonly size: number;
694
+ set(name: string, value: string, options?: Omit<ResponseCookie, 'name' | 'value'>): RequestCookies;
695
+ set(cookie: ResponseCookie): RequestCookies;
696
+ delete(name: string | { name: string; path?: string; domain?: string }): RequestCookies;
697
+ toString(): string;
698
+ [Symbol.iterator](): IterableIterator<[string, RequestCookie]>;
699
+ }
700
+ /**
701
+ * The in-flight request's cookies, Next.js's `cookies()`: `cookies().get('x')?.value`
702
+ * reads, `set` / `delete` write a `Set-Cookie` onto this request's response. Also
703
+ * awaitable (`(await cookies()).get('x')`). Throws outside a request scope.
704
+ *
705
+ * `get` returns a {@link RequestCookie} (`{ name, value }`) or `undefined`, as
706
+ * in Next.js, never the bare string.
707
+ *
708
+ * @example
709
+ * ```ts
710
+ * import { cookies } from '@webjsdev/server';
711
+ * const theme: string = cookies().get('theme')?.value ?? 'light';
712
+ * cookies().set('theme', 'dark', { path: '/', maxAge: 60 * 60 * 24 * 365, httpOnly: true });
713
+ * cookies().delete('theme');
714
+ * ```
715
+ */
716
+ export declare function cookies(): RequestCookies & Promise<RequestCookies>;
651
717
  /** The raw `Request` for the in-flight scope, or null at module top-level. */
652
718
  export declare function getRequest(): Request | null;
653
719
  /** Run `fn` within a request scope. */
@@ -786,8 +852,13 @@ export declare function cache<T extends (...args: any[]) => Promise<any>>(
786
852
  // cache-tags.js (#242) + html-cache.js (#241): server cache invalidation
787
853
  // ---------------------------------------------------------------------------
788
854
 
789
- /** Evict every `cache()` entry tagged with `tag`. */
790
- export declare function revalidateTag(tag: string): Promise<void>;
855
+ /**
856
+ * Evict every `cache()` entry tagged with `tag`. Takes Next.js 16's second
857
+ * argument, `revalidateTag(tag, 'max')` (a cache-life profile, or
858
+ * `{ expire }`), for source compatibility: WebJs evicts at once whatever the
859
+ * profile, since `cache()` has no stale-while-revalidate window to honour.
860
+ */
861
+ export declare function revalidateTag(tag: string, profile?: string | { expire?: number }): Promise<void>;
791
862
  /** Evict every `cache()` entry tagged with any of `tags`. */
792
863
  export declare function revalidateTags(tags: string[]): Promise<void>;
793
864
  /**
@@ -800,7 +871,7 @@ export declare function revalidateTags(tags: string[]): Promise<void>;
800
871
  * `webjs.basePath` the absolute url is the PUBLIC one and its mount prefix is
801
872
  * stripped, so it evicts the entry the write actually stored.
802
873
  */
803
- export declare function revalidatePath(path: string): Promise<void>;
874
+ export declare function revalidatePath(path: string, type?: 'page' | 'layout'): Promise<void>;
804
875
  /** Evict all cached HTML (per-process generation bump). */
805
876
  export declare function revalidateAll(): Promise<void>;
806
877
 
@@ -826,12 +897,34 @@ export declare class Session {
826
897
  set(key: string, value: unknown): void;
827
898
  has(key: string): boolean;
828
899
  unset(key: string): void;
829
- /** Set a value readable once on the next request, then cleared. */
900
+ /** Set a value that lasts until a `get()` reads it (Remix's flash); `has()` does not consume it. */
830
901
  flash(key: string, value: unknown): void;
831
902
  destroy(): void;
903
+ /**
904
+ * Give the session a new id, keeping its data. Call it after a login to
905
+ * prevent session fixation; `deleteOld` also deletes the old id's stored data.
906
+ */
907
+ regenerateId(deleteOld?: boolean): void;
832
908
  }
833
909
 
834
- /** Session middleware factory. */
910
+ /**
911
+ * Session middleware factory: a signed session cookie for apps that do not use
912
+ * `createAuth`. It throws at import without `secret` or `SESSION_SECRET`.
913
+ *
914
+ * @example
915
+ * ```ts
916
+ * // middleware.ts
917
+ * import { session } from '@webjsdev/server';
918
+ * export default session({ secret: process.env.SESSION_SECRET });
919
+ *
920
+ * // In a 'use server' action or a page:
921
+ * import { getRequest, getSession } from '@webjsdev/server';
922
+ * const s = getSession(getRequest()!);
923
+ * s.regenerateId(true); // after a login, against session fixation
924
+ * s.set('userId', user.id);
925
+ * const userId = s.get<string>('userId');
926
+ * ```
927
+ */
835
928
  export declare function session(opts?: {
836
929
  storage?: SessionStorage;
837
930
  cookieName?: string;
@@ -850,8 +943,26 @@ export declare function storeSessionStorage(opts?: Record<string, unknown>): Ses
850
943
  export declare const cookieSession: typeof cookieSessionStorage;
851
944
  /** Alias of `storeSessionStorage`. */
852
945
  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;
946
+ /**
947
+ * The session for the current request. With no argument it reads the request
948
+ * in scope (as `cookies()` and `headers()` do); passing `getRequest()` also works.
949
+ * Needs `session()` as root middleware; throws outside it.
950
+ *
951
+ * @example
952
+ * ```ts
953
+ * // middleware.ts
954
+ * import { session } from '@webjsdev/server';
955
+ * export default session(); // SESSION_SECRET from .env
956
+ *
957
+ * // in a page, action or route handler
958
+ * import { getSession } from '@webjsdev/server';
959
+ * const s = getSession();
960
+ * s.set('cart', [1, 2]); // persists across requests
961
+ * s.flash('notice', 'Saved'); // read once by the next request
962
+ * const notice = s.get('notice'); // unknown; narrow it before use
963
+ * ```
964
+ */
965
+ export declare function getSession(req?: Request | null): Session;
855
966
 
856
967
  // ---------------------------------------------------------------------------
857
968
  // broadcast.js
@@ -868,9 +979,14 @@ export declare function broadcast(
868
979
  // file-storage.js (#247)
869
980
  // ---------------------------------------------------------------------------
870
981
 
871
- /** A streaming handle returned by `FileStore.get` (body is a stream, never buffered). */
982
+ /**
983
+ * A streaming handle returned by `FileStore.get` (body is a stream, never
984
+ * buffered). The body is a web `ReadableStream`, so `new Response(file.body)`
985
+ * serves it as is; an adapter whose SDK returns a Node stream converts it with
986
+ * `Readable.toWeb`.
987
+ */
872
988
  export interface StoredObjectHandle {
873
- body: ReadableStream | import('node:stream').Readable;
989
+ body: ReadableStream<Uint8Array>;
874
990
  size: number;
875
991
  contentType: string;
876
992
  }
@@ -904,7 +1020,26 @@ export declare const DEFAULT_UPLOAD_DIR: string;
904
1020
  export declare function diskStore(opts?: { dir?: string; baseUrl?: string }): FileStore;
905
1021
  /** Set the default file store (call at startup to use a custom dir or S3). */
906
1022
  export declare function setFileStore(store: FileStore): void;
907
- /** Get the default file store (`diskStore` under `<cwd>/.webjs/uploads` unless set). */
1023
+ /**
1024
+ * Get the default file store (`diskStore` under `<cwd>/.webjs/uploads` unless set).
1025
+ *
1026
+ * @example
1027
+ * ```ts
1028
+ * // In a 'use server' action bound to <form action=${save} enctype="multipart/form-data">
1029
+ * // with <input type="file" name="photo">: the field arrives as a File.
1030
+ * import { getFileStore, generateKey } from '@webjsdev/server';
1031
+ * const key = generateKey(input.photo.name); // opaque, traversal-safe
1032
+ * await getFileStore().put(key, input.photo); // streams to disk
1033
+ *
1034
+ * // app/uploads/[key]/route.ts serves it back:
1035
+ * export async function GET(_req: Request, { params }: { params: { key: string } }) {
1036
+ * let file;
1037
+ * try { file = await getFileStore().get(params.key); } catch { file = null; } // an unsafe key throws
1038
+ * if (!file) return new Response('Not found', { status: 404 });
1039
+ * return new Response(file.body, { headers: { 'content-type': file.contentType } });
1040
+ * }
1041
+ * ```
1042
+ */
908
1043
  export declare function getFileStore(): FileStore;
909
1044
  /** Generate a random, opaque, traversal-safe key, preserving a whitelisted extension. */
910
1045
  export declare function generateKey(filename?: string): string;
@@ -1080,13 +1215,102 @@ export type ResolvedAuthUser = keyof AuthUser extends never
1080
1215
  : AuthUser;
1081
1216
 
1082
1217
  /** Auth configuration for `createAuth`. */
1218
+ /**
1219
+ * The Auth.js adapter methods `createAuth` calls, the shape `@auth/*-adapter`
1220
+ * packages implement (`@auth/core/adapters`).
1221
+ */
1222
+ export interface AuthAdapter {
1223
+ createUser(user: Record<string, any>): Promise<any> | any;
1224
+ getUser?(id: string): Promise<any> | any;
1225
+ getUserByEmail(email: string): Promise<any> | any;
1226
+ getUserByAccount(account: { provider: string; providerAccountId: string }): Promise<any> | any;
1227
+ updateUser?(user: Record<string, any>): Promise<any> | any;
1228
+ linkAccount(account: Record<string, any>): Promise<any> | any;
1229
+ createSession(session: { sessionToken: string; userId: string; expires: Date }): Promise<any> | any;
1230
+ getSessionAndUser(sessionToken: string): Promise<{ session: { sessionToken: string; userId: string; expires: Date }; user: any } | null> | { session: any; user: any } | null;
1231
+ updateSession?(session: { sessionToken: string; expires?: Date }): Promise<any> | any;
1232
+ deleteSession(sessionToken: string): Promise<any> | any;
1233
+ }
1234
+ /**
1235
+ * The token the `jwt` and `session` callbacks see, Auth.js's `JWT`: the
1236
+ * standard claims plus whatever the `jwt` callback put on it.
1237
+ */
1238
+ export interface AuthJWT {
1239
+ sub?: string;
1240
+ name?: string | null;
1241
+ email?: string | null;
1242
+ image?: string | null;
1243
+ iat?: number;
1244
+ exp?: number;
1245
+ [key: string]: unknown;
1246
+ }
1247
+
1248
+ /** The user the callbacks see, Auth.js's `User`: what `authorize` or the provider returned. */
1249
+ export interface AuthCallbackUser {
1250
+ id?: string;
1251
+ name?: string | null;
1252
+ email?: string | null;
1253
+ image?: string | null;
1254
+ [key: string]: unknown;
1255
+ }
1256
+
1257
+ /** The session the `session` callback receives and returns, Auth.js's `Session`. */
1258
+ export interface AuthCallbackSession {
1259
+ user: ResolvedAuthUser & { name?: string | null; email?: string | null; image?: string | null };
1260
+ expires: string;
1261
+ [key: string]: unknown;
1262
+ }
1263
+
1264
+ type AuthAwaitable<T> = T | Promise<T>;
1265
+
1266
+ /**
1267
+ * Auth.js's callbacks, with the parameters Auth.js passes, so a callback
1268
+ * written as Auth.js documents it (`jwt({ token, user })`,
1269
+ * `session({ session, token })`) is typed with no annotation. Declared as
1270
+ * methods so a callback annotated with its own narrower types still compiles.
1271
+ */
1272
+ export interface AuthCallbacks {
1273
+ /** At sign-in (`trigger: 'signIn'`, with `user`, `account` and `profile`) and on every session read. */
1274
+ jwt?(params: { token: AuthJWT; user: AuthCallbackUser; account?: Record<string, any> | null; profile?: Record<string, any>; trigger?: 'signIn' | 'signUp' | 'update'; session?: any }): AuthAwaitable<AuthJWT | null>;
1275
+ /** On every session read. `token` under the jwt strategy, `user` under the database strategy. */
1276
+ session?(params: { session: AuthCallbackSession; token: AuthJWT; user: AuthCallbackUser }): AuthAwaitable<any>;
1277
+ /** Return true to allow, a url string to redirect there instead, anything else to deny (Auth.js's rule). */
1278
+ signIn?(params: { user: AuthCallbackUser; account?: Record<string, any> | null; profile?: Record<string, any>; credentials?: Record<string, any> }): AuthAwaitable<boolean | string>;
1279
+ redirect?(params: { url: string; baseUrl: string }): AuthAwaitable<string>;
1280
+ /** Run by `auth` as middleware: true continues, false redirects to sign in, a Response is sent as is. */
1281
+ authorized?: (params: { auth: { user: Record<string, unknown> } | null; request: Request }) => boolean | Response | Promise<boolean | Response>;
1282
+ }
1283
+
1083
1284
  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 };
1285
+ /** A provider config, or the provider function uncalled (`providers: [GitHub]`), as Auth.js accepts. */
1286
+ providers: Array<ProviderConfig | (() => ProviderConfig)>;
1287
+ /**
1288
+ * `strategy` defaults to `'database'` when an Auth.js adapter is given, else
1289
+ * `'jwt'`. `updateAge` (seconds, default a day) is how often a database
1290
+ * session's expiry is pushed forward, as in Auth.js.
1291
+ */
1292
+ session?: { strategy?: 'jwt' | 'database'; maxAge?: number; updateAge?: number };
1293
+ /** Signs the session. Defaults to `process.env.AUTH_SECRET`, as in Auth.js. */
1294
+ secret?: string | undefined;
1295
+ callbacks?: AuthCallbacks;
1296
+ /**
1297
+ * An Auth.js database adapter (`DrizzleAdapter(db)` from `@auth/drizzle-adapter`,
1298
+ * and the rest), or WebJs's own session store for the database strategy.
1299
+ */
1300
+ adapter?: AuthAdapter | { load?: Function; save?: Function; destroy?: Function };
1089
1301
  pages?: { signIn?: string; signOut?: string; error?: string };
1302
+ /**
1303
+ * Auth.js's `useSecureCookies`: force the Secure flag and the `__Host-`
1304
+ * cookie names on or off. Unset, they follow the auth URL's protocol
1305
+ * (`AUTH_URL`'s when set, else the request's), never `NODE_ENV`.
1306
+ */
1307
+ useSecureCookies?: boolean;
1308
+ /**
1309
+ * Where the auth routes are mounted, as Auth.js's `basePath`. Defaults to
1310
+ * the path of `AUTH_URL` when it has one, else `/api/auth`. `AUTH_URL` (or
1311
+ * `NEXTAUTH_URL`) also sets the origin OAuth callback URLs use.
1312
+ */
1313
+ basePath?: string;
1090
1314
  }
1091
1315
 
1092
1316
  /**
@@ -1097,26 +1321,120 @@ export interface AuthConfig {
1097
1321
  * via `createAuth<MyUser>(...)`.
1098
1322
  */
1099
1323
  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>;
1324
+ /**
1325
+ * Auth.js's `auth`, in its three shapes: `auth()` / `auth(req)` reads the
1326
+ * session; `export { auth as middleware }` runs as middleware (it sets
1327
+ * `req.auth` and applies `callbacks.authorized`); `auth((req) => ...)` wraps
1328
+ * a middleware whose `req.auth` holds the session.
1329
+ */
1330
+ auth: {
1331
+ (req?: Request | null): Promise<{ user: TUser } | null>;
1332
+ (req: Request, next: unknown): Promise<Response | undefined>;
1333
+ <R>(handler: (req: Request & { auth: { user: TUser } | null }, ...rest: any[]) => R): (req: Request, ...rest: any[]) => Promise<Response | Awaited<R>>;
1334
+ };
1335
+ /**
1336
+ * Auth.js v5's `signIn`. Inside a request (a server action, a route handler,
1337
+ * middleware) it sets the session cookie on the response and throws a
1338
+ * redirect, so `await signIn('credentials', formData)` in a form action signs
1339
+ * in and navigates; a credentials failure throws {@link CredentialsSignin}.
1340
+ * `{ redirect: false }` (in the options or, as Auth.js accepts it, in the
1341
+ * data) returns the target url instead of throwing. With no request in scope
1342
+ * it returns the Response the `/api/auth/*` routes send. The redirecting
1343
+ * form is typed `Promise<Response>`, so `return signIn(...)` fits an action
1344
+ * that returns `ActionResult | Response`.
1345
+ */
1346
+ signIn: {
1347
+ (
1348
+ provider: string,
1349
+ data: (FormData | Record<string, unknown>) | undefined,
1350
+ opts: { redirectTo?: string; redirect: false; req?: Request },
1351
+ ): Promise<string>;
1352
+ (
1353
+ provider: string,
1354
+ data: Record<string, unknown> & { redirect: false },
1355
+ opts?: { redirectTo?: string; req?: Request },
1356
+ ): Promise<string>;
1357
+ (
1358
+ provider: string,
1359
+ data?: FormData | Record<string, unknown>,
1360
+ opts?: { redirectTo?: string; redirect?: true; req?: Request },
1361
+ ): Promise<Response>;
1362
+ };
1363
+ /** Auth.js v5's `signOut`: clears the session and throws a redirect inside a request. */
1364
+ signOut: {
1365
+ (opts: { redirectTo?: string; redirect: false; req?: Request }): Promise<string>;
1366
+ (opts?: { redirectTo?: string; redirect?: true; req?: Request }): Promise<Response>;
1367
+ };
1107
1368
  handlers: { GET: (req: Request) => Promise<Response>; POST: (req: Request) => Promise<Response> };
1108
1369
  }
1109
1370
 
1110
1371
  /**
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.
1372
+ * Create the auth system, Auth.js's API. `createAuth<MyUser>(...)` (or an
1373
+ * augmented {@link AuthUser}) types `auth().user` with no cast.
1374
+ *
1375
+ * @example
1376
+ * ```ts
1377
+ * // modules/auth/auth.server.ts
1378
+ * export const { auth, signIn, signOut, handlers } = createAuth({
1379
+ * providers: [Credentials({
1380
+ * async authorize(c: { email: string; password: string }) {
1381
+ * const user = await findUserByEmail(c.email); // your own query
1382
+ * if (!user || !(await verifyPassword(c.password, user.passwordHash))) return null;
1383
+ * return { id: String(user.id), email: user.email };
1384
+ * },
1385
+ * })],
1386
+ * pages: { error: '/login' }, // an uncaught failure lands on /login?error=CredentialsSignin
1387
+ * });
1388
+ * // app/api/auth/[...path]/route.ts: export const { GET, POST } = handlers;
1389
+ * // Read the user anywhere on the server: const session = await auth();
1390
+ *
1391
+ * // A 'use server' action bound as <form action=${logIn}>, as with Auth.js in Next:
1392
+ * export async function logIn(formData: FormData) {
1393
+ * try {
1394
+ * await signIn('credentials', formData, { redirectTo: '/dashboard' });
1395
+ * } catch (error) {
1396
+ * if (error instanceof AuthError) return { success: false, error: 'Wrong email or password.' };
1397
+ * throw error; // the success redirect
1398
+ * }
1399
+ * }
1400
+ * // Sign out the same way: await signOut({ redirectTo: '/' }).
1401
+ * ```
1116
1402
  */
1117
1403
  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;
1404
+ /** Next.js's navigation helpers (`next/navigation`), re-exported from `@webjsdev/core` so server code can import them from either package. */
1405
+ export declare const notFound: typeof import('@webjsdev/core').notFound;
1406
+ export declare const forbidden: typeof import('@webjsdev/core').forbidden;
1407
+ export declare const unauthorized: typeof import('@webjsdev/core').unauthorized;
1408
+ export declare const redirect: typeof import('@webjsdev/core').redirect;
1409
+ export declare const permanentRedirect: typeof import('@webjsdev/core').permanentRedirect;
1410
+
1411
+ /** Auth.js's base error: `error instanceof AuthError` and `error.type` say why a sign-in failed. */
1412
+ export declare class AuthError extends Error {
1413
+ type: string;
1414
+ }
1415
+ /** Thrown by `signIn('credentials', ...)` when `authorize` returns null. */
1416
+ export declare class CredentialsSignin extends AuthError {
1417
+ type: 'CredentialsSignin';
1418
+ code: string;
1419
+ }
1420
+ /**
1421
+ * Options for the credentials provider, Auth.js's `Credentials`. `C` is the
1422
+ * shape of the submitted fields, inferred from `authorize`'s annotation
1423
+ * (`authorize(credentials: { email: string; password: string })`).
1424
+ */
1425
+ export interface CredentialsOptions<C extends Record<string, unknown> = Record<string, unknown>> {
1426
+ /**
1427
+ * Check the submitted fields (the sign-in form's fields, or the object passed
1428
+ * to `signIn('credentials', data)`). Return the session user, or `null` to
1429
+ * refuse the sign-in.
1430
+ */
1431
+ authorize: (credentials: C) => Promise<Record<string, unknown> | null> | Record<string, unknown> | null;
1432
+ [key: string]: unknown;
1433
+ }
1434
+ /** The credentials (email/password) provider, Auth.js's `Credentials`. */
1435
+ export declare function Credentials<C extends Record<string, unknown> = Record<string, unknown>>(
1436
+ opts: CredentialsOptions<C>,
1437
+ ): ProviderConfig;
1120
1438
  /** The Google OAuth provider. */
1121
1439
  export declare function Google(opts?: Record<string, unknown>): ProviderConfig;
1122
1440
  /** The GitHub OAuth provider. */