@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.
- package/README.md +4 -1
- package/index.d.ts +793 -125
- package/index.js +8 -4
- package/package.json +3 -3
- package/src/action-flash.js +185 -0
- package/src/action-params.js +156 -0
- package/src/action-route.js +8 -9
- package/src/action-seed.js +44 -5
- package/src/actions.js +97 -15
- package/src/api.js +67 -10
- package/src/app-layout.js +133 -0
- package/src/asset-hash.js +82 -49
- package/src/auth.js +612 -108
- package/src/browser-entries.js +2 -17
- package/src/build-epoch.js +1 -1
- package/src/cache-tags.js +5 -1
- package/src/check/rules-actions.js +37 -1
- package/src/check/rules-components.js +114 -40
- package/src/check/rules-imports.js +175 -4
- package/src/check/rules-route-modules.js +200 -0
- package/src/check/rules-routes.js +55 -0
- package/src/check/rules-routing.js +143 -95
- package/src/check/rules-typescript.js +24 -2
- package/src/check/rules.js +104 -9
- package/src/check/runner-support.js +53 -171
- package/src/check/runner.js +22 -6
- package/src/client-signals.js +171 -0
- package/src/component-scanner.js +22 -16
- package/src/context.js +152 -16
- package/src/cookie-store.js +269 -0
- package/src/core-exports.js +190 -0
- package/src/csrf.js +21 -3
- package/src/dev/bun-app-source.js +95 -5
- package/src/dev/config.js +7 -51
- package/src/dev/dev-service-worker.js +48 -0
- package/src/dev/esm-hot-hooks.js +373 -0
- package/src/dev/handler.js +502 -155
- package/src/dev/helpers.js +127 -96
- package/src/dev/hot-host.js +33 -6
- package/src/dev/poke-state.js +110 -0
- package/src/dev/serve.js +177 -129
- package/src/dev/server.js +92 -33
- package/src/dev/specifier-versions.js +125 -0
- package/src/dev/watch-ignore.js +49 -4
- package/src/dev/watch-poll.js +78 -0
- package/src/dev/watch-tree.js +59 -22
- package/src/dev-classify.js +59 -26
- package/src/dev-error.js +104 -3
- package/src/dev-hot-classify.js +291 -0
- package/src/dev-hot-client.js +245 -0
- package/src/dev-import.js +22 -5
- package/src/dev-overlay.js +26 -0
- package/src/dev-ready-probe.js +69 -0
- package/src/dev-regenerate.js +2 -2
- package/src/dev-reload-hold.js +49 -20
- package/src/dev-reload-reason.js +61 -0
- package/src/dev-reload-worker.js +62 -13
- package/src/dev-secret.js +73 -0
- package/src/dev-source-locations-hook.js +4 -1
- package/src/dev-source-locations.js +33 -2
- package/src/dev.js +0 -1
- package/src/env-files.js +124 -0
- package/src/file-storage.js +2 -2
- package/src/foreign-imports.js +228 -0
- package/src/form-action-identity.js +52 -25
- package/src/form-dispatch.js +142 -66
- package/src/html-cache.js +35 -4
- package/src/importmap.js +2 -2
- package/src/instrumentation.js +59 -3
- package/src/js-scan.js +5 -4
- package/src/jwe.js +155 -0
- package/src/lazy-components.js +166 -0
- package/src/listener-core.js +4 -2
- package/src/lit-hints.js +172 -0
- package/src/middleware-load.js +248 -0
- package/src/module-graph.js +73 -47
- package/src/module-side-effect.js +284 -0
- package/src/public-files.js +73 -0
- package/src/redirect-location.js +19 -0
- package/src/redirects.js +9 -1
- package/src/route-frontier.js +151 -0
- package/src/route-report.js +130 -0
- package/src/router.js +120 -13
- package/src/session.js +32 -10
- package/src/ssr/client-router-flag.js +1 -1
- package/src/ssr/document.js +37 -13
- package/src/ssr/head.js +78 -23
- package/src/ssr/preloads.js +31 -45
- package/src/ssr/render.js +241 -55
- package/src/ssr/responses.js +51 -4
- package/src/ssr/shipped-modules.js +22 -16
- package/src/testing.d.ts +71 -2
- package/src/testing.js +103 -21
- package/src/thenable-params.js +26 -0
- package/src/vendor/jspm.js +12 -1
- package/src/vendor/pins.js +9 -8
- package/src/vendor/scanner.js +14 -9
- package/src/vendor.js +5 -3
- package/webjs-config.schema.json +1 -6
- package/src/component-elision.js +0 -1828
- package/src/elision-differential.js +0 -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 =
|
|
49
|
-
| {
|
|
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
|
-
/**
|
|
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.
|
|
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
|
|
496
|
-
*
|
|
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
|
-
//
|
|
602
|
+
// route-report.js (#1686)
|
|
507
603
|
// ---------------------------------------------------------------------------
|
|
508
604
|
|
|
509
|
-
/**
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
export interface
|
|
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
|
-
|
|
529
|
-
/**
|
|
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
|
-
/**
|
|
532
|
-
|
|
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
|
|
619
|
+
export interface OrphanRow {
|
|
538
620
|
file: string;
|
|
539
621
|
className: string;
|
|
540
622
|
}
|
|
541
623
|
|
|
542
|
-
export interface
|
|
543
|
-
components: number;
|
|
544
|
-
elided: number;
|
|
545
|
-
shipped: number;
|
|
624
|
+
export interface RouteReportSummary {
|
|
546
625
|
routeModules: number;
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
/**
|
|
554
|
-
export interface
|
|
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' | '
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
*
|
|
566
|
-
*
|
|
567
|
-
*
|
|
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
|
|
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
|
-
//
|
|
692
|
+
// context.js (per-request context helpers)
|
|
619
693
|
// ---------------------------------------------------------------------------
|
|
620
694
|
|
|
621
695
|
/**
|
|
622
|
-
*
|
|
623
|
-
*
|
|
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
|
|
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
|
-
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
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
|
|
636
|
-
|
|
637
|
-
}):
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
790
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
854
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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:
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
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
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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.
|
|
1112
|
-
*
|
|
1113
|
-
*
|
|
1114
|
-
*
|
|
1115
|
-
*
|
|
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
|
-
/**
|
|
1119
|
-
export declare
|
|
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. */
|