@reckona/mreact-router 0.0.203 → 0.0.205
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 +53 -9
- package/dist/actions.d.ts +6 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +70 -10
- package/dist/actions.js.map +1 -1
- package/dist/adapters/aws-lambda.d.ts +15 -4
- package/dist/adapters/aws-lambda.d.ts.map +1 -1
- package/dist/adapters/aws-lambda.js +301 -63
- package/dist/adapters/aws-lambda.js.map +1 -1
- package/dist/adapters/cloudflare.d.ts +2 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +18 -10
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/edge.d.ts +1 -1
- package/dist/adapters/edge.d.ts.map +1 -1
- package/dist/adapters/edge.js.map +1 -1
- package/dist/adapters/node.d.ts +5 -3
- package/dist/adapters/node.d.ts.map +1 -1
- package/dist/adapters/node.js +2 -1
- package/dist/adapters/node.js.map +1 -1
- package/dist/adapters/static.d.ts.map +1 -1
- package/dist/adapters/static.js +24 -2
- package/dist/adapters/static.js.map +1 -1
- package/dist/build.d.ts +1 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +91 -5
- package/dist/build.js.map +1 -1
- package/dist/built-runtime.d.ts +2 -0
- package/dist/built-runtime.d.ts.map +1 -1
- package/dist/built-runtime.js +16 -0
- package/dist/built-runtime.js.map +1 -1
- package/dist/cache.d.ts +3 -1
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +211 -13
- package/dist/cache.js.map +1 -1
- package/dist/cli.js +12 -1
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +32 -1
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +21 -2
- package/dist/config.js.map +1 -1
- package/dist/csp.d.ts +1 -0
- package/dist/csp.d.ts.map +1 -1
- package/dist/csp.js +11 -6
- package/dist/csp.js.map +1 -1
- package/dist/dehydrate-policy.d.ts +3 -0
- package/dist/dehydrate-policy.d.ts.map +1 -0
- package/dist/dehydrate-policy.js +16 -0
- package/dist/dehydrate-policy.js.map +1 -0
- package/dist/dev-server.d.ts +7 -2
- package/dist/dev-server.d.ts.map +1 -1
- package/dist/dev-server.js +120 -11
- package/dist/dev-server.js.map +1 -1
- package/dist/http.d.ts +1 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +147 -7
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +22 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +22 -8
- package/dist/logger.js.map +1 -1
- package/dist/metadata.d.ts +8 -0
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +60 -22
- package/dist/metadata.js.map +1 -1
- package/dist/navigation-marker.d.ts.map +1 -1
- package/dist/navigation-marker.js +72 -9
- package/dist/navigation-marker.js.map +1 -1
- package/dist/navigation.d.ts +1 -0
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +1 -1
- package/dist/navigation.js.map +1 -1
- package/dist/node-server.d.ts +5 -2
- package/dist/node-server.d.ts.map +1 -1
- package/dist/node-server.js +75 -6
- package/dist/node-server.js.map +1 -1
- package/dist/prerender-entry.d.ts +14 -0
- package/dist/prerender-entry.d.ts.map +1 -1
- package/dist/prerender-entry.js +40 -3
- package/dist/prerender-entry.js.map +1 -1
- package/dist/render.d.ts +9 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +102 -123
- package/dist/render.js.map +1 -1
- package/dist/serve.d.ts +8 -2
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +22 -20
- package/dist/serve.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +52 -19
- package/dist/session.js.map +1 -1
- package/dist/upgrade.d.ts +59 -2
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +282 -1
- package/dist/upgrade.js.map +1 -1
- package/dist/vite-config.d.ts +2 -0
- package/dist/vite-config.d.ts.map +1 -1
- package/dist/vite-config.js +2 -0
- package/dist/vite-config.js.map +1 -1
- package/dist/vite.d.ts +4 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +117 -6
- package/dist/vite.js.map +1 -1
- package/package.json +11 -11
- package/src/actions.ts +81 -11
- package/src/adapters/aws-lambda.ts +426 -115
- package/src/adapters/cloudflare.ts +29 -13
- package/src/adapters/edge.ts +2 -0
- package/src/adapters/node.ts +8 -10
- package/src/adapters/static.ts +31 -2
- package/src/build.ts +117 -10
- package/src/built-runtime.ts +28 -0
- package/src/cache.ts +262 -15
- package/src/cli.ts +14 -1
- package/src/client.ts +32 -1
- package/src/config.ts +43 -6
- package/src/csp.ts +14 -11
- package/src/dehydrate-policy.ts +23 -0
- package/src/dev-server.ts +188 -15
- package/src/http.ts +172 -6
- package/src/index.ts +14 -1
- package/src/logger.ts +47 -10
- package/src/metadata.ts +125 -60
- package/src/navigation-marker.ts +79 -9
- package/src/navigation.ts +1 -1
- package/src/node-server.ts +128 -22
- package/src/prerender-entry.ts +52 -3
- package/src/render.ts +807 -753
- package/src/serve.ts +49 -56
- package/src/session.ts +56 -25
- package/src/upgrade.ts +402 -3
- package/src/vite-config.ts +6 -0
- package/src/vite.ts +157 -5
package/src/actions.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "@reckona/mreact-compiler";
|
|
9
9
|
import {
|
|
10
10
|
createServerActionHandler,
|
|
11
|
+
ensureServerActionReplayStoreContract,
|
|
11
12
|
type ServerActionHandlerOptions,
|
|
12
13
|
type ServerActionReplayClaim,
|
|
13
14
|
type ServerActionRegistry,
|
|
@@ -19,7 +20,7 @@ import { bundleRouterModule, type RouterCompatBuildApi } from "./bundle-pipeline
|
|
|
19
20
|
import { type AppRouterCache, withRouteCacheContext } from "./cache.js";
|
|
20
21
|
import { fileImportMetaUrlPlugin, importAppRouterSourceModule } from "./module-runner.js";
|
|
21
22
|
import { createAppRouterImportPolicyPlugin, type AppRouterImportPolicy } from "./import-policy.js";
|
|
22
|
-
import { cookies, type RequestCookies } from "./navigation.js";
|
|
23
|
+
import { cookies, isSafeInternalRedirect, type RequestCookies } from "./navigation.js";
|
|
23
24
|
export {
|
|
24
25
|
createFormCsrfToken,
|
|
25
26
|
formCsrfCookie,
|
|
@@ -72,6 +73,7 @@ const actionTokenSecret = configuredActionTokenSecret(process.env.MREACT_SERVER_
|
|
|
72
73
|
// traffic does not trip false-positive 409s.
|
|
73
74
|
const DEFAULT_REPLAY_TTL_MS = 10 * 60 * 1000;
|
|
74
75
|
const DEFAULT_REPLAY_MAX_ENTRIES = 50_000;
|
|
76
|
+
const DEFAULT_REPLAY_MINIMUM_RETENTION_MS = 60_000;
|
|
75
77
|
const DEFAULT_ACTION_BODY_MAX_BYTES = 10 * 1024 * 1024;
|
|
76
78
|
const DEFAULT_ACTION_FORM_MAX_FIELDS = 1_000;
|
|
77
79
|
let warnedUnrestrictedServerActions = false;
|
|
@@ -115,30 +117,59 @@ function configuredActionTokenSecret(value: string | undefined): string {
|
|
|
115
117
|
class BoundedReplayStore {
|
|
116
118
|
private readonly entries = new Map<
|
|
117
119
|
string,
|
|
118
|
-
{ state: "in-flight"
|
|
120
|
+
| { state: "in-flight"; leaseExpiresAt: number }
|
|
121
|
+
| { state: "completed"; expiresAt: number; protectedUntil: number }
|
|
119
122
|
>();
|
|
123
|
+
private capacityWarningActive = false;
|
|
124
|
+
private warnedLeaseReclamation = false;
|
|
125
|
+
private readonly minimumRetentionMs: number;
|
|
120
126
|
|
|
121
127
|
constructor(
|
|
122
128
|
private readonly ttlMs: number,
|
|
123
129
|
private readonly maxEntries: number,
|
|
124
|
-
) {
|
|
130
|
+
) {
|
|
131
|
+
this.minimumRetentionMs = Math.min(ttlMs, DEFAULT_REPLAY_MINIMUM_RETENTION_MS);
|
|
132
|
+
}
|
|
125
133
|
|
|
126
134
|
claim(value: string): ServerActionReplayClaim {
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
if (this.entries.size < this.maxEntries) {
|
|
137
|
+
this.capacityWarningActive = false;
|
|
138
|
+
}
|
|
139
|
+
|
|
127
140
|
const existing = this.entries.get(value);
|
|
128
|
-
if (existing?.state === "completed" && existing.expiresAt
|
|
141
|
+
if (existing?.state === "completed" && existing.expiresAt <= now) {
|
|
142
|
+
this.entries.delete(value);
|
|
143
|
+
} else if (existing?.state === "in-flight" && existing.leaseExpiresAt <= now) {
|
|
129
144
|
this.entries.delete(value);
|
|
145
|
+
this.warnLeaseReclamation();
|
|
130
146
|
}
|
|
131
147
|
|
|
132
148
|
if (this.entries.has(value)) return { status: "replay" };
|
|
133
149
|
if (this.entries.size >= this.maxEntries) {
|
|
134
|
-
|
|
150
|
+
this.warnCapacity();
|
|
151
|
+
let reclaimableCompletedKey: string | undefined;
|
|
135
152
|
for (const [key, entry] of this.entries) {
|
|
136
|
-
if (
|
|
153
|
+
if (
|
|
154
|
+
(entry.state === "completed" && entry.expiresAt <= now) ||
|
|
155
|
+
(entry.state === "in-flight" && entry.leaseExpiresAt <= now)
|
|
156
|
+
) {
|
|
157
|
+
this.entries.delete(key);
|
|
158
|
+
} else if (
|
|
159
|
+
reclaimableCompletedKey === undefined &&
|
|
160
|
+
entry.state === "completed" &&
|
|
161
|
+
entry.protectedUntil <= now
|
|
162
|
+
) {
|
|
163
|
+
reclaimableCompletedKey = key;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (this.entries.size >= this.maxEntries && reclaimableCompletedKey !== undefined) {
|
|
167
|
+
this.entries.delete(reclaimableCompletedKey);
|
|
137
168
|
}
|
|
138
169
|
if (this.entries.size >= this.maxEntries) return { status: "capacity-exceeded" };
|
|
139
170
|
}
|
|
140
171
|
|
|
141
|
-
const entry = { state: "in-flight" as const };
|
|
172
|
+
const entry = { state: "in-flight" as const, leaseExpiresAt: now + this.ttlMs };
|
|
142
173
|
this.entries.set(value, entry);
|
|
143
174
|
let finalized = false;
|
|
144
175
|
return {
|
|
@@ -147,16 +178,51 @@ class BoundedReplayStore {
|
|
|
147
178
|
if (finalized) return;
|
|
148
179
|
finalized = true;
|
|
149
180
|
if (this.entries.get(value) === entry) {
|
|
150
|
-
|
|
181
|
+
const completedAt = Date.now();
|
|
182
|
+
this.entries.delete(value);
|
|
183
|
+
this.entries.set(value, {
|
|
184
|
+
state: "completed",
|
|
185
|
+
expiresAt: completedAt + this.ttlMs,
|
|
186
|
+
protectedUntil: completedAt + this.minimumRetentionMs,
|
|
187
|
+
});
|
|
151
188
|
}
|
|
152
189
|
},
|
|
153
190
|
};
|
|
154
191
|
}
|
|
155
192
|
|
|
193
|
+
// Exposed for tests; not part of the ServerActionReplayStore interface.
|
|
194
|
+
clear(): void {
|
|
195
|
+
this.entries.clear();
|
|
196
|
+
this.capacityWarningActive = false;
|
|
197
|
+
this.warnedLeaseReclamation = false;
|
|
198
|
+
}
|
|
199
|
+
|
|
156
200
|
// Exposed for tests; not part of the ServerActionReplayStore interface.
|
|
157
201
|
size(): number {
|
|
158
202
|
return this.entries.size;
|
|
159
203
|
}
|
|
204
|
+
|
|
205
|
+
private warnCapacity(): void {
|
|
206
|
+
if (this.capacityWarningActive) return;
|
|
207
|
+
this.capacityWarningActive = true;
|
|
208
|
+
let completed = 0;
|
|
209
|
+
let inFlight = 0;
|
|
210
|
+
for (const entry of this.entries.values()) {
|
|
211
|
+
if (entry.state === "completed") completed += 1;
|
|
212
|
+
else inFlight += 1;
|
|
213
|
+
}
|
|
214
|
+
console.error(
|
|
215
|
+
`mreact-router: default server action replay store reached capacity (size=${this.entries.size}, maxEntries=${this.maxEntries}, completed=${completed}, inFlight=${inFlight}, minimumRetentionMs=${this.minimumRetentionMs}). Completed claims remain exact through minimumRetentionMs and may be reclaimed under pressure afterward; expired in-flight leases may also be reclaimed. Applications requiring exact protection for the full action lifetime and retention window must use a shared replay store.`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private warnLeaseReclamation(): void {
|
|
220
|
+
if (this.warnedLeaseReclamation) return;
|
|
221
|
+
this.warnedLeaseReclamation = true;
|
|
222
|
+
console.error(
|
|
223
|
+
`mreact-router: default server action replay store reclaimed an expired in-flight claim after leaseMs=${this.ttlMs}; exact duplicate-execution protection no longer applies to that abandoned claim.`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
160
226
|
}
|
|
161
227
|
|
|
162
228
|
const usedFormActionNonces = new BoundedReplayStore(
|
|
@@ -168,7 +234,7 @@ const usedFormActionNonces = new BoundedReplayStore(
|
|
|
168
234
|
// so tests can drive its eviction semantics directly. Not part of the
|
|
169
235
|
// public surface (prefixed with `__`).
|
|
170
236
|
export function __clearDefaultReplayStore(): void {
|
|
171
|
-
|
|
237
|
+
usedFormActionNonces.clear();
|
|
172
238
|
}
|
|
173
239
|
|
|
174
240
|
export function __readDefaultReplayStore(): BoundedReplayStore {
|
|
@@ -795,8 +861,9 @@ function sameOriginRefererPath(request: Request): string | undefined {
|
|
|
795
861
|
const requestUrl = new URL(request.url);
|
|
796
862
|
const refererUrl = new URL(referer, requestUrl);
|
|
797
863
|
|
|
798
|
-
|
|
799
|
-
|
|
864
|
+
const location = `${refererUrl.pathname}${refererUrl.search}`;
|
|
865
|
+
return refererUrl.origin === requestUrl.origin && isSafeInternalRedirect(location)
|
|
866
|
+
? location
|
|
800
867
|
: undefined;
|
|
801
868
|
} catch {
|
|
802
869
|
return undefined;
|
|
@@ -1497,6 +1564,9 @@ async function claimFormNonce(
|
|
|
1497
1564
|
replayStore: ServerActionReplayStore,
|
|
1498
1565
|
): Promise<Extract<Awaited<ReturnType<ServerActionReplayStore["claim"]>>, { status: "claimed" }> | Response> {
|
|
1499
1566
|
let claim: Awaited<ReturnType<ServerActionReplayStore["claim"]>>;
|
|
1567
|
+
if (!ensureServerActionReplayStoreContract(replayStore)) {
|
|
1568
|
+
return replayStoreUnavailableResponse();
|
|
1569
|
+
}
|
|
1500
1570
|
try {
|
|
1501
1571
|
claim = await replayStore.claim(nonce);
|
|
1502
1572
|
} catch {
|