@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.
Files changed (140) hide show
  1. package/README.md +53 -9
  2. package/dist/actions.d.ts +6 -0
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +70 -10
  5. package/dist/actions.js.map +1 -1
  6. package/dist/adapters/aws-lambda.d.ts +15 -4
  7. package/dist/adapters/aws-lambda.d.ts.map +1 -1
  8. package/dist/adapters/aws-lambda.js +301 -63
  9. package/dist/adapters/aws-lambda.js.map +1 -1
  10. package/dist/adapters/cloudflare.d.ts +2 -1
  11. package/dist/adapters/cloudflare.d.ts.map +1 -1
  12. package/dist/adapters/cloudflare.js +18 -10
  13. package/dist/adapters/cloudflare.js.map +1 -1
  14. package/dist/adapters/edge.d.ts +1 -1
  15. package/dist/adapters/edge.d.ts.map +1 -1
  16. package/dist/adapters/edge.js.map +1 -1
  17. package/dist/adapters/node.d.ts +5 -3
  18. package/dist/adapters/node.d.ts.map +1 -1
  19. package/dist/adapters/node.js +2 -1
  20. package/dist/adapters/node.js.map +1 -1
  21. package/dist/adapters/static.d.ts.map +1 -1
  22. package/dist/adapters/static.js +24 -2
  23. package/dist/adapters/static.js.map +1 -1
  24. package/dist/build.d.ts +1 -0
  25. package/dist/build.d.ts.map +1 -1
  26. package/dist/build.js +91 -5
  27. package/dist/build.js.map +1 -1
  28. package/dist/built-runtime.d.ts +2 -0
  29. package/dist/built-runtime.d.ts.map +1 -1
  30. package/dist/built-runtime.js +16 -0
  31. package/dist/built-runtime.js.map +1 -1
  32. package/dist/cache.d.ts +3 -1
  33. package/dist/cache.d.ts.map +1 -1
  34. package/dist/cache.js +211 -13
  35. package/dist/cache.js.map +1 -1
  36. package/dist/cli.js +12 -1
  37. package/dist/cli.js.map +1 -1
  38. package/dist/client.d.ts.map +1 -1
  39. package/dist/client.js +32 -1
  40. package/dist/client.js.map +1 -1
  41. package/dist/config.d.ts +3 -0
  42. package/dist/config.d.ts.map +1 -1
  43. package/dist/config.js +21 -2
  44. package/dist/config.js.map +1 -1
  45. package/dist/csp.d.ts +1 -0
  46. package/dist/csp.d.ts.map +1 -1
  47. package/dist/csp.js +11 -6
  48. package/dist/csp.js.map +1 -1
  49. package/dist/dehydrate-policy.d.ts +3 -0
  50. package/dist/dehydrate-policy.d.ts.map +1 -0
  51. package/dist/dehydrate-policy.js +16 -0
  52. package/dist/dehydrate-policy.js.map +1 -0
  53. package/dist/dev-server.d.ts +7 -2
  54. package/dist/dev-server.d.ts.map +1 -1
  55. package/dist/dev-server.js +120 -11
  56. package/dist/dev-server.js.map +1 -1
  57. package/dist/http.d.ts +1 -1
  58. package/dist/http.d.ts.map +1 -1
  59. package/dist/http.js +147 -7
  60. package/dist/http.js.map +1 -1
  61. package/dist/index.d.ts +4 -3
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +1 -0
  64. package/dist/index.js.map +1 -1
  65. package/dist/logger.d.ts +22 -1
  66. package/dist/logger.d.ts.map +1 -1
  67. package/dist/logger.js +22 -8
  68. package/dist/logger.js.map +1 -1
  69. package/dist/metadata.d.ts +8 -0
  70. package/dist/metadata.d.ts.map +1 -1
  71. package/dist/metadata.js +60 -22
  72. package/dist/metadata.js.map +1 -1
  73. package/dist/navigation-marker.d.ts.map +1 -1
  74. package/dist/navigation-marker.js +72 -9
  75. package/dist/navigation-marker.js.map +1 -1
  76. package/dist/navigation.d.ts +1 -0
  77. package/dist/navigation.d.ts.map +1 -1
  78. package/dist/navigation.js +1 -1
  79. package/dist/navigation.js.map +1 -1
  80. package/dist/node-server.d.ts +5 -2
  81. package/dist/node-server.d.ts.map +1 -1
  82. package/dist/node-server.js +75 -6
  83. package/dist/node-server.js.map +1 -1
  84. package/dist/prerender-entry.d.ts +14 -0
  85. package/dist/prerender-entry.d.ts.map +1 -1
  86. package/dist/prerender-entry.js +40 -3
  87. package/dist/prerender-entry.js.map +1 -1
  88. package/dist/render.d.ts +9 -1
  89. package/dist/render.d.ts.map +1 -1
  90. package/dist/render.js +102 -123
  91. package/dist/render.js.map +1 -1
  92. package/dist/serve.d.ts +8 -2
  93. package/dist/serve.d.ts.map +1 -1
  94. package/dist/serve.js +22 -20
  95. package/dist/serve.js.map +1 -1
  96. package/dist/session.d.ts.map +1 -1
  97. package/dist/session.js +52 -19
  98. package/dist/session.js.map +1 -1
  99. package/dist/upgrade.d.ts +59 -2
  100. package/dist/upgrade.d.ts.map +1 -1
  101. package/dist/upgrade.js +282 -1
  102. package/dist/upgrade.js.map +1 -1
  103. package/dist/vite-config.d.ts +2 -0
  104. package/dist/vite-config.d.ts.map +1 -1
  105. package/dist/vite-config.js +2 -0
  106. package/dist/vite-config.js.map +1 -1
  107. package/dist/vite.d.ts +4 -1
  108. package/dist/vite.d.ts.map +1 -1
  109. package/dist/vite.js +117 -6
  110. package/dist/vite.js.map +1 -1
  111. package/package.json +11 -11
  112. package/src/actions.ts +81 -11
  113. package/src/adapters/aws-lambda.ts +426 -115
  114. package/src/adapters/cloudflare.ts +29 -13
  115. package/src/adapters/edge.ts +2 -0
  116. package/src/adapters/node.ts +8 -10
  117. package/src/adapters/static.ts +31 -2
  118. package/src/build.ts +117 -10
  119. package/src/built-runtime.ts +28 -0
  120. package/src/cache.ts +262 -15
  121. package/src/cli.ts +14 -1
  122. package/src/client.ts +32 -1
  123. package/src/config.ts +43 -6
  124. package/src/csp.ts +14 -11
  125. package/src/dehydrate-policy.ts +23 -0
  126. package/src/dev-server.ts +188 -15
  127. package/src/http.ts +172 -6
  128. package/src/index.ts +14 -1
  129. package/src/logger.ts +47 -10
  130. package/src/metadata.ts +125 -60
  131. package/src/navigation-marker.ts +79 -9
  132. package/src/navigation.ts +1 -1
  133. package/src/node-server.ts +128 -22
  134. package/src/prerender-entry.ts +52 -3
  135. package/src/render.ts +807 -753
  136. package/src/serve.ts +49 -56
  137. package/src/session.ts +56 -25
  138. package/src/upgrade.ts +402 -3
  139. package/src/vite-config.ts +6 -0
  140. 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" } | { state: "completed"; expiresAt: number }
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 < Date.now()) {
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
- const now = Date.now();
150
+ this.warnCapacity();
151
+ let reclaimableCompletedKey: string | undefined;
135
152
  for (const [key, entry] of this.entries) {
136
- if (entry.state === "completed" && entry.expiresAt < now) this.entries.delete(key);
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
- this.entries.set(value, { state: "completed", expiresAt: Date.now() + this.ttlMs });
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
- (usedFormActionNonces as unknown as { entries: Map<string, number> }).entries.clear();
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
- return refererUrl.origin === requestUrl.origin
799
- ? `${refererUrl.pathname}${refererUrl.search}`
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 {