@rsc-kit/core 0.30.38 → 0.30.42
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/dist/cache.d.ts +79 -2
- package/dist/cache.js +151 -5
- package/dist/cache.js.map +1 -1
- package/dist/changed.js +2 -1
- package/dist/changed.js.map +1 -1
- package/dist/conformance.d.ts +1 -0
- package/dist/conformance.js +27 -0
- package/dist/conformance.js.map +1 -1
- package/dist/flash.d.ts +18 -0
- package/dist/flash.js +29 -0
- package/dist/flash.js.map +1 -0
- package/dist/flashCookie.d.ts +17 -0
- package/dist/flashCookie.js +36 -0
- package/dist/flashCookie.js.map +1 -0
- package/dist/host.d.ts +28 -17
- package/dist/host.js +109 -31
- package/dist/host.js.map +1 -1
- package/dist/hostCalls.js +27 -11
- package/dist/hostCalls.js.map +1 -1
- package/dist/js/ActivityRouter.js +3 -3
- package/dist/js/ActivityRouter.js.map +1 -1
- package/dist/js/Changes.js +1 -1
- package/dist/js/Changes.js.map +1 -1
- package/dist/js/DefaultRouteError.d.ts +1 -1
- package/dist/js/DefaultRouteError.js.map +1 -1
- package/dist/js/Form.d.ts +2 -2
- package/dist/js/Form.js +5 -5
- package/dist/js/Form.js.map +1 -1
- package/dist/js/Link.js +2 -2
- package/dist/js/Link.js.map +1 -1
- package/dist/js/RedirectBoundary.js +2 -2
- package/dist/js/RedirectBoundary.js.map +1 -1
- package/dist/js/SegmentBoundary.js +5 -5
- package/dist/js/SegmentBoundary.js.map +1 -1
- package/dist/js/SlotBoundary.js +3 -3
- package/dist/js/SlotBoundary.js.map +1 -1
- package/dist/js/changesStore.js +1 -1
- package/dist/js/changesStore.js.map +1 -1
- package/dist/js/createViteRscApp.js +16 -17
- package/dist/js/createViteRscApp.js.map +1 -1
- package/dist/js/errors.d.ts +3 -1
- package/dist/js/errors.js +5 -2
- package/dist/js/errors.js.map +1 -1
- package/dist/js/navigate.d.ts +17 -2
- package/dist/js/navigate.js +50 -19
- package/dist/js/navigate.js.map +1 -1
- package/dist/js/nuqs.js +2 -2
- package/dist/js/nuqs.js.map +1 -1
- package/dist/js/pagePayload.js +2 -2
- package/dist/js/pagePayload.js.map +1 -1
- package/dist/js/payloadKey.d.ts +32 -0
- package/dist/js/payloadKey.js +61 -0
- package/dist/js/payloadKey.js.map +1 -0
- package/dist/js/refreshOn.js +3 -3
- package/dist/js/refreshOn.js.map +1 -1
- package/dist/js/section.d.ts +4 -4
- package/dist/js/section.js +2 -2
- package/dist/js/section.js.map +1 -1
- package/dist/js/updateStore.js +6 -2
- package/dist/js/updateStore.js.map +1 -1
- package/dist/js/useAppUpdate.js +1 -1
- package/dist/js/useAppUpdate.js.map +1 -1
- package/dist/js/useFlash.d.ts +16 -0
- package/dist/js/useFlash.js +56 -0
- package/dist/js/useFlash.js.map +1 -0
- package/dist/js/useOnline.js +1 -1
- package/dist/js/useOnline.js.map +1 -1
- package/dist/js/usePathname.js +1 -1
- package/dist/js/usePathname.js.map +1 -1
- package/dist/manifest.d.ts +7 -0
- package/dist/manifest.js.map +1 -1
- package/dist/pageCache.d.ts +47 -0
- package/dist/pageCache.js +177 -0
- package/dist/pageCache.js.map +1 -0
- package/dist/prerender.js +4 -0
- package/dist/prerender.js.map +1 -1
- package/dist/request.d.ts +36 -0
- package/dist/request.js +80 -0
- package/dist/request.js.map +1 -1
- package/dist/testing.d.ts +9 -0
- package/dist/testing.js +14 -2
- package/dist/testing.js.map +1 -1
- package/dist/vite.d.ts +27 -0
- package/dist/vite.js +120 -14
- package/dist/vite.js.map +1 -1
- package/package.json +11 -3
package/dist/host.js
CHANGED
|
@@ -18,6 +18,8 @@ import { PARAM_PLACEHOLDER, pathKey, patternKey } from "./prerender.js";
|
|
|
18
18
|
import { apiKey } from "./apiPrerender.js";
|
|
19
19
|
import { hostPath, hostSegment, routableHost } from "./hostRouting.js";
|
|
20
20
|
import { withRevalidation } from "./revalidate.js";
|
|
21
|
+
import { withoutPayloadKey } from "./js/payloadKey.js";
|
|
22
|
+
import { cachingFor, finishCacheable, noteReadsSoFar } from "./pageCache.js";
|
|
21
23
|
/**
|
|
22
24
|
* @internal For a host adapter that embeds the engine. An app imports this
|
|
23
25
|
* from `@rsc-kit/core/revalidate`.
|
|
@@ -56,19 +58,19 @@ function saidNotFound(request, route) {
|
|
|
56
58
|
* nearest not-found.tsx above it.
|
|
57
59
|
*/
|
|
58
60
|
export function notFoundRouteOf(request) {
|
|
59
|
-
return notFoundFrom.get(request);
|
|
61
|
+
return notFoundFrom.get(withoutPayloadKey(request));
|
|
60
62
|
}
|
|
61
63
|
/** Whether a page or guard of this app answered the request with notFound(). */
|
|
62
64
|
export function pageSaidNotFound(request) {
|
|
63
|
-
return notFoundRequests.has(request);
|
|
65
|
+
return notFoundRequests.has(withoutPayloadKey(request));
|
|
64
66
|
}
|
|
65
67
|
export { revalidate } from "./revalidate.js";
|
|
66
68
|
import { isCrawler } from "./crawler.js";
|
|
67
69
|
import { inlineFlight, inlineFlightStart } from "./inlineFlight.js";
|
|
68
70
|
import { currentNotFound, withRedirect } from "./redirect.js";
|
|
69
71
|
import { compressed } from "./compress.js";
|
|
70
|
-
import { withCache } from "./cache.js";
|
|
71
|
-
import { takeAfterWork, withRequest, withResponseDraft, withUrl } from "./request.js";
|
|
72
|
+
import { setCacheBuild, withCache } from "./cache.js";
|
|
73
|
+
import { notFoundRowSent, renderAsNobody, takeAfterWork, withRequest, withResponseDraft, withUrl } from "./request.js";
|
|
72
74
|
import { criticalAssetsOf, linkHeader, mergeAssets } from "./earlyHints.js";
|
|
73
75
|
/**
|
|
74
76
|
* @internal For a host adapter that embeds the engine. An app imports this
|
|
@@ -141,11 +143,18 @@ function isNavigation(request) {
|
|
|
141
143
|
function afterFormPost(to) {
|
|
142
144
|
return { ...to, status: 303 };
|
|
143
145
|
}
|
|
144
|
-
function redirectResponse(to, isPayloadRequest) {
|
|
146
|
+
function redirectResponse(to, isPayloadRequest, revalidated = []) {
|
|
145
147
|
if (isPayloadRequest) {
|
|
146
148
|
return new Response(null, {
|
|
147
149
|
status: 204,
|
|
148
|
-
headers: {
|
|
150
|
+
headers: {
|
|
151
|
+
[HEADER.redirect]: to.location,
|
|
152
|
+
Vary: VARY_ON_RSC,
|
|
153
|
+
// What an action that redirected also revalidated - see
|
|
154
|
+
// followActionRedirect. Without it the client cannot tell a redirect
|
|
155
|
+
// that changed what a layout reads from one that did not.
|
|
156
|
+
...(revalidated.length ? { [HEADER.revalidate]: revalidated.join(",") } : {}),
|
|
157
|
+
},
|
|
149
158
|
});
|
|
150
159
|
}
|
|
151
160
|
return new Response(null, {
|
|
@@ -262,28 +271,36 @@ function refusalMessage(error) {
|
|
|
262
271
|
return typeof message === "string" && message !== "" ? message : "Refused.";
|
|
263
272
|
}
|
|
264
273
|
/**
|
|
265
|
-
* Whether a
|
|
274
|
+
* Whether a request that changes something came from this site.
|
|
266
275
|
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
* and it is what Next does for server actions.
|
|
276
|
+
* Asked of an action, a query, a form posted to a page, and every POST, PUT,
|
|
277
|
+
* PATCH or DELETE a route.ts answers - the last having had no check at all, so
|
|
278
|
+
* a handler reading cookies() could be posted to from any site with the
|
|
279
|
+
* visitor's. The rule is Go's http.CrossOriginProtection, and what Laravel 13
|
|
280
|
+
* asks before it looks at a CSRF token:
|
|
273
281
|
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
282
|
+
* 1. Sec-Fetch-Site: same-origin (or none - typed in the address bar) passes.
|
|
283
|
+
* The browser sets it and a page cannot, and every browser has since 2023.
|
|
284
|
+
* 2. An Origin listed in trustedOrigins passes.
|
|
285
|
+
* 3. Any other Sec-Fetch-Site - same-site, cross-site - is refused.
|
|
286
|
+
* 4. Without one, Origin is compared with the host, X-Forwarded-Host first.
|
|
287
|
+
* 5. With neither, the caller is not a browser - a webhook, a server - and
|
|
288
|
+
* passes: there is no visitor's cookie to have been tricked into sending.
|
|
279
289
|
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
290
|
+
* Compared against X-Forwarded-Host before Host, because a proxied deployment
|
|
291
|
+
* is the normal one: the backend forwards the name the visitor typed, while
|
|
292
|
+
* Host is this process on loopback. Comparing against Host alone would refuse
|
|
293
|
+
* every legitimate post that arrived through a proxy.
|
|
284
294
|
*/
|
|
285
|
-
export function
|
|
295
|
+
export function originAllowed(request, url, trusted = []) {
|
|
296
|
+
const site = request.headers.get("sec-fetch-site");
|
|
297
|
+
if (site === "same-origin" || site === "none")
|
|
298
|
+
return true;
|
|
286
299
|
const origin = request.headers.get("origin");
|
|
300
|
+
if (origin && trusted.includes(origin.toLowerCase()))
|
|
301
|
+
return true;
|
|
302
|
+
if (site)
|
|
303
|
+
return false;
|
|
287
304
|
if (!origin)
|
|
288
305
|
return true;
|
|
289
306
|
const expected = request.headers.get("x-forwarded-host") ??
|
|
@@ -293,10 +310,27 @@ export function actionOriginAllowed(request, url) {
|
|
|
293
310
|
return new URL(origin).host === expected;
|
|
294
311
|
}
|
|
295
312
|
catch {
|
|
296
|
-
//
|
|
313
|
+
// "null" - a sandboxed iframe, a redirected cross-site post - and anything
|
|
314
|
+
// else that is not a url is not one this can vouch for.
|
|
297
315
|
return false;
|
|
298
316
|
}
|
|
299
317
|
}
|
|
318
|
+
/** trustedOrigins as Origin headers spell them, or a thrown error naming the bad one. */
|
|
319
|
+
function trustedOriginsOf(origins) {
|
|
320
|
+
return (origins ?? []).map((origin) => {
|
|
321
|
+
let parsed = null;
|
|
322
|
+
try {
|
|
323
|
+
parsed = new URL(origin);
|
|
324
|
+
}
|
|
325
|
+
catch { }
|
|
326
|
+
if (!parsed || parsed.origin === "null" || parsed.origin !== origin.replace(/\/$/, "").toLowerCase()) {
|
|
327
|
+
throw new Error(`[rsc-kit] trustedOrigins: ${JSON.stringify(origin)} is not an origin. ` +
|
|
328
|
+
"Write the scheme and host as a browser sends them, with no path: 'https://app.example.com'.");
|
|
329
|
+
}
|
|
330
|
+
return parsed.origin;
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
|
300
334
|
const DEFAULT_MAX_ACTION_BODY = 8 * 1024 * 1024;
|
|
301
335
|
/** A form post whose body passed the ceiling: answered 413, never rendered. */
|
|
302
336
|
const TOO_LARGE = Symbol("too large");
|
|
@@ -446,9 +480,12 @@ export function createRscHandler(options) {
|
|
|
446
480
|
// own. Resolved on the first request: the engine reads it from a build
|
|
447
481
|
// product it only has at runtime.
|
|
448
482
|
let version = engine.buildId ? undefined : options.version;
|
|
483
|
+
// Told to the cache once, so answers kept across requests are this build's.
|
|
484
|
+
let cacheBuildSet = false;
|
|
449
485
|
// The edge contract's marker, as the build left it beside the pages - see edgeMarker.
|
|
450
486
|
let edgeMarkerRead = null;
|
|
451
487
|
const maxActionBody = options.maxActionBody ?? DEFAULT_MAX_ACTION_BODY;
|
|
488
|
+
const trustedOrigins = trustedOriginsOf(options.trustedOrigins);
|
|
452
489
|
const compress = options.compress ?? true;
|
|
453
490
|
// Annotated rather than inferred: the narrowing below is lost inside the
|
|
454
491
|
// closures that use it, and every one of them runs after the throw.
|
|
@@ -594,7 +631,7 @@ export function createRscHandler(options) {
|
|
|
594
631
|
return null;
|
|
595
632
|
if (!matchPage(routes, url))
|
|
596
633
|
return null;
|
|
597
|
-
if (!
|
|
634
|
+
if (!originAllowed(request, url, trustedOrigins))
|
|
598
635
|
return null;
|
|
599
636
|
// Through the same ceiling as an action. This path is reached with no
|
|
600
637
|
// guard and no session - any page url - and formData() reads whatever
|
|
@@ -639,19 +676,39 @@ export function createRscHandler(options) {
|
|
|
639
676
|
return async function handle(request) {
|
|
640
677
|
if (version === undefined && engine.buildId)
|
|
641
678
|
version = await engine.buildId();
|
|
679
|
+
if (!cacheBuildSet) {
|
|
680
|
+
setCacheBuild(version);
|
|
681
|
+
cacheBuildSet = true;
|
|
682
|
+
}
|
|
642
683
|
if (!signingChecked) {
|
|
643
684
|
assertSigningSecret();
|
|
644
685
|
signingChecked = true;
|
|
645
686
|
}
|
|
687
|
+
// A page that exports revalidate is judged as it arrived - its ?_rsc=
|
|
688
|
+
// key is part of check 1 - and answered for as the app sees it.
|
|
689
|
+
const arrived = request;
|
|
690
|
+
request = withoutPayloadKey(request);
|
|
691
|
+
const page = matchPage(routes, new URL(request.url));
|
|
692
|
+
const caching = cachingFor(arrived, page?.route.revalidate);
|
|
693
|
+
if (caching && (page?.route.hostMiddleware?.length ?? 0) > 0)
|
|
694
|
+
caching.refused ??= "host-middleware";
|
|
646
695
|
return await withRequest(request, () => withCache(() =>
|
|
647
696
|
// Open for the whole request and sealed the moment an answer exists,
|
|
648
697
|
// so middleware — which runs before any rendering — can put headers on
|
|
649
698
|
// it, and a component, which runs after, is told why it cannot.
|
|
650
699
|
withResponseDraft(async ({ taken, seal }) => {
|
|
651
|
-
|
|
700
|
+
if (caching)
|
|
701
|
+
renderAsNobody();
|
|
702
|
+
const rendered = await route(request);
|
|
652
703
|
seal();
|
|
653
|
-
if (!
|
|
704
|
+
if (!rendered) {
|
|
705
|
+
// The page said notFound(): the entry in front renders the 404
|
|
706
|
+
// in a scope of its own, and needs what this one read.
|
|
707
|
+
if (caching)
|
|
708
|
+
noteReadsSoFar(caching);
|
|
654
709
|
return null;
|
|
710
|
+
}
|
|
711
|
+
let response = rendered;
|
|
655
712
|
const collected = taken();
|
|
656
713
|
// Under whatever the answer already has, and before the middleware's,
|
|
657
714
|
// so a page or a middleware that asked for its own value keeps it.
|
|
@@ -673,6 +730,14 @@ export function createRscHandler(options) {
|
|
|
673
730
|
for (const cookie of collected.getSetCookie()) {
|
|
674
731
|
response.headers.append("Set-Cookie", cookie);
|
|
675
732
|
}
|
|
733
|
+
// A page a shared cache may keep, rendered whole and judged. Not a
|
|
734
|
+
// stored answer: that one was frozen for nobody, and says so.
|
|
735
|
+
if (caching && !servedFrom.has(response)) {
|
|
736
|
+
const hints = hinted.get(response);
|
|
737
|
+
response = await finishCacheable(request, response, caching, () => pageSaidNotFound(request) || notFoundRowSent());
|
|
738
|
+
if (hints)
|
|
739
|
+
hinted.set(response, hints);
|
|
740
|
+
}
|
|
676
741
|
// How this answer was served - the header a developer reads in the
|
|
677
742
|
// Network tab, the way X-Nextjs-Cache is. Always: it names no
|
|
678
743
|
// product, and a CDN rule or a check can key on it.
|
|
@@ -760,7 +825,7 @@ export function createRscHandler(options) {
|
|
|
760
825
|
return asset;
|
|
761
826
|
}
|
|
762
827
|
if (request.method === "POST" && url.pathname === HEADER.actionPath) {
|
|
763
|
-
if (!
|
|
828
|
+
if (!originAllowed(request, url, trustedOrigins)) {
|
|
764
829
|
return new Response("Cross-origin action", { status: 403 });
|
|
765
830
|
}
|
|
766
831
|
return await handleAction(request, url);
|
|
@@ -777,6 +842,12 @@ export function createRscHandler(options) {
|
|
|
777
842
|
return stored;
|
|
778
843
|
}
|
|
779
844
|
if (api && engine.handleApiRoute) {
|
|
845
|
+
// A route.ts that changes something is a form any site could post to
|
|
846
|
+
// with the visitor's cookies, and nothing else here stands in the way:
|
|
847
|
+
// no custom header, so no preflight. Refused before its guards run.
|
|
848
|
+
if (!SAFE_METHODS.has(request.method) && !originAllowed(request, url, trustedOrigins)) {
|
|
849
|
+
return new Response("Cross-origin request", { status: 403 });
|
|
850
|
+
}
|
|
780
851
|
// The guards above it run first, exactly as they would for a page in the
|
|
781
852
|
// same directory. A route.ts is colocated with the pages it belongs
|
|
782
853
|
// with, so adding one under a guarded path must not open a way around
|
|
@@ -810,7 +881,7 @@ export function createRscHandler(options) {
|
|
|
810
881
|
// cannot read this answer — CORS sees to that — but it can still cause
|
|
811
882
|
// the read to run with the visitor's cookies. A query is side-effect
|
|
812
883
|
// free by contract, so this is depth rather than the only defence.
|
|
813
|
-
if (!
|
|
884
|
+
if (!originAllowed(request, url, trustedOrigins)) {
|
|
814
885
|
return new Response("Cross-origin query", { status: 403 });
|
|
815
886
|
}
|
|
816
887
|
return await handleQuery(request, url);
|
|
@@ -1961,13 +2032,20 @@ export function createRscHandler(options) {
|
|
|
1961
2032
|
// already knows how to read.
|
|
1962
2033
|
return await withRedirect(async (redirected) => {
|
|
1963
2034
|
let stream;
|
|
2035
|
+
let marked = () => [];
|
|
1964
2036
|
try {
|
|
1965
|
-
({ stream } = await withRevalidation((taken) =>
|
|
2037
|
+
({ stream } = await withRevalidation((taken) => {
|
|
2038
|
+
marked = taken;
|
|
2039
|
+
return engine.handleAction(actionId, body, contentType, page, taken);
|
|
2040
|
+
}));
|
|
1966
2041
|
}
|
|
1967
2042
|
catch (error) {
|
|
1968
2043
|
const to = redirected();
|
|
2044
|
+
// What it revalidated before redirecting goes with the redirect:
|
|
2045
|
+
// dropped, revalidate('all') then redirect() left every layout as it
|
|
2046
|
+
// was, and the one way to ask for them did nothing.
|
|
1969
2047
|
if (to)
|
|
1970
|
-
return redirectResponse(to, true);
|
|
2048
|
+
return redirectResponse(to, true, marked());
|
|
1971
2049
|
throw error;
|
|
1972
2050
|
}
|
|
1973
2051
|
return new Response(stream, {
|