void 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/dist/{account-cmd-ChVKPZ-w.mjs → account-cmd-DRhmCWcm.mjs} +3 -3
  2. package/dist/{application-analysis-2vGZrKzg.mjs → application-analysis-HRFsZOv3.mjs} +3 -3
  3. package/dist/{application-routing-DnUQoOJD.mjs → application-routing-P4TggvWp.mjs} +1 -1
  4. package/dist/{auth-link-D4uMdOsw.mjs → auth-link-CVXP1G6G.mjs} +3 -3
  5. package/dist/{auth-router-62dna450.mjs → auth-router-D1pGdbvB.mjs} +2 -2
  6. package/dist/{better-auth-shared-CiclgPde.mjs → better-auth-shared-C958aM7W.mjs} +28 -3
  7. package/dist/{build-cmd-D93K_4Sa.mjs → build-cmd-DmjbuT6d.mjs} +1 -1
  8. package/dist/{cache-CiIzEED4.mjs → cache-2khBO1MR.mjs} +1 -1
  9. package/dist/{cancel-deploy-BNoGfn2e.mjs → cancel-deploy-_itbbvY3.mjs} +1 -1
  10. package/dist/cli/cli.mjs +27 -27
  11. package/dist/client-CEtr5UOR.mjs +1 -0
  12. package/dist/{client-Qayy9xEh.mjs → client-CoxPEhJq.mjs} +6 -2
  13. package/dist/{cloudflare-cmd-DiFGaLow.mjs → cloudflare-cmd-fZ6AMSWk.mjs} +1 -1
  14. package/dist/{cloudflare-config-Chet188_.mjs → cloudflare-config-eQunqX6A.mjs} +1 -1
  15. package/dist/{cloudflare-connect-BdgAQBc7.mjs → cloudflare-connect-9OZe4XVQ.mjs} +1 -1
  16. package/dist/{cloudflare-operations-CWA24vCh.mjs → cloudflare-operations-CrskD4_O.mjs} +4 -4
  17. package/dist/{cloudflare-operations-D8MoezuN.mjs → cloudflare-operations-oLvAd9Nd.mjs} +1 -1
  18. package/dist/{config-Bt4rw93x.mjs → config-BBlVXZCU.mjs} +1 -1
  19. package/dist/{config-DkA9jYF2.mjs → config-BITKQ3O6.mjs} +2 -1
  20. package/dist/{config-BszFVhpk.mjs → config-BgysUhnm.mjs} +1 -1
  21. package/dist/{config-CzsNzGVH.d.mts → config-CPxJmt4M.d.mts} +2 -0
  22. package/dist/config-entry.d.mts +1 -1
  23. package/dist/{connect-b9mJbb6X.mjs → connect-CKvmkl2R.mjs} +4 -4
  24. package/dist/{create-project-rnjum4_V.mjs → create-project-BiMmR5wA.mjs} +1 -1
  25. package/dist/{create-project-eomwm0wa.mjs → create-project-Bq0M8MtO.mjs} +1 -1
  26. package/dist/{db-UPWmiSk1.mjs → db-cq5RW4Vy.mjs} +17 -17
  27. package/dist/{delete-BeeYi1WO.mjs → delete-CY4L8fi2.mjs} +1 -1
  28. package/dist/deploy-B029oHlE.mjs +2 -0
  29. package/dist/{deploy-CwmCcTlq.mjs → deploy-D_gEojD2.mjs} +79 -42
  30. package/dist/{deploy-managed-static-gPDxyp3v.mjs → deploy-managed-static-BHVbB_-A.mjs} +11 -11
  31. package/dist/{deploy-transport-L6zzPE9T.mjs → deploy-transport-DEyOUfac.mjs} +2 -2
  32. package/dist/deploy.mjs +5 -4
  33. package/dist/{dev-inbox-CwH5VuZj.mjs → dev-inbox-CUUS6iUg.mjs} +1 -1
  34. package/dist/{dispatch-CiJ1Xrba.mjs → dispatch-BNLB5_Gm.mjs} +1 -1
  35. package/dist/{domain-3xdH9p2L.mjs → domain-BlH90Ods.mjs} +2 -2
  36. package/dist/{email-Du3YRnM5.mjs → email-Bkq8ZL1p.mjs} +6 -6
  37. package/dist/{email-cloudflare-CtD8qE13.mjs → email-cloudflare-MvJIxrwx.mjs} +5 -107
  38. package/dist/{env-Cnvp0pGy.mjs → env-AvuCSeCH.mjs} +2 -2
  39. package/dist/{gen-BvVmpZ5f.mjs → gen-BD-dfjxY.mjs} +3 -3
  40. package/dist/gen-CIHXh9I8.mjs +1 -0
  41. package/dist/{github-cmd-X2GKiLi-.mjs → github-cmd-CA-lBdkn.mjs} +1 -1
  42. package/dist/{handler-Drm_lyrV.d.mts → handler-BR6Tvjkp.d.mts} +8 -3
  43. package/dist/{handler-xmzz0U38.mjs → handler-CKeTtYID.mjs} +2 -12
  44. package/dist/headers-CuTEdP9C.mjs +445 -0
  45. package/dist/{help-EWDNxST-.mjs → help-1OCFDsRr.mjs} +9 -1
  46. package/dist/help-Ba8Dq5-r.mjs +1 -0
  47. package/dist/{inbound-DSKSHDUj.d.mts → inbound-BHt4dIlL.d.mts} +16 -5
  48. package/dist/{inbound-DQfekMtz.mjs → inbound-CdO-qY0w.mjs} +2 -76
  49. package/dist/index.d.mts +1 -1
  50. package/dist/index.mjs +65 -37
  51. package/dist/{info-jW8_d3VI.mjs → info-CJxua_db.mjs} +4 -4
  52. package/dist/{init-jDQ5G9QC.mjs → init-B-4CluYx.mjs} +12 -11
  53. package/dist/{isr-config-DhBkzCdY.mjs → isr-config-Czzb4mIq.mjs} +1 -47
  54. package/dist/{link-DOyxX5Xs.mjs → link-DAXOT-G_.mjs} +2 -2
  55. package/dist/{list-C9pTYDtl.mjs → list-BYSOo9fm.mjs} +2 -2
  56. package/dist/{live-CDBHyYiR.d.mts → live-0xrcVpyq.d.mts} +1 -1
  57. package/dist/{local-d1-CKfek6Z4.mjs → local-d1-BLgsNUj9.mjs} +1 -1
  58. package/dist/login-BpbYeu-n.mjs +1 -0
  59. package/dist/{login-koQbjzPd.mjs → login-CJopTWX4.mjs} +2 -2
  60. package/dist/{logs-cTU5TsRW.mjs → logs-DE0weBsR.mjs} +13 -11
  61. package/dist/{migrate-C62pGTfr.mjs → migrate-ChHHZBTi.mjs} +3 -3
  62. package/dist/migrate-HdA6Kh2p.mjs +1 -0
  63. package/dist/{mime-DKkO7ZtA.mjs → mime-CtP6-qAF.mjs} +143 -15
  64. package/dist/nitro.mjs +3 -3
  65. package/dist/{node-BfAt2rz8.mjs → node-CZ6lGNLn.mjs} +1 -1
  66. package/dist/{operator-cmd-CnAvSqJ5.mjs → operator-cmd-BUqsdk2K.mjs} +28 -10
  67. package/dist/operator-cmd-wBYyp8iQ.mjs +1 -0
  68. package/dist/pages/client.d.mts +143 -11
  69. package/dist/pages/client.mjs +173 -12
  70. package/dist/pages/index.d.mts +22 -3
  71. package/dist/pages/index.mjs +18 -4
  72. package/dist/pages/islands-plugin.d.mts +46 -2
  73. package/dist/pages/islands-plugin.mjs +91 -5
  74. package/dist/pages/prefetch.d.mts +2 -2
  75. package/dist/pages/prefetch.mjs +1 -1
  76. package/dist/pages/protocol.d.mts +2 -2
  77. package/dist/pages/protocol.mjs +2 -2
  78. package/dist/platform-args-C7XUw9mw.mjs +1 -0
  79. package/dist/{platform-args-Bv9hoLEL.mjs → platform-args-Cx1M-7bq.mjs} +18 -4
  80. package/dist/{platform-auth-config-CLj0EvnB.mjs → platform-auth-config-K4m_LE8b.mjs} +4 -4
  81. package/dist/{platform-auth-protection-DQDSxVhr.mjs → platform-auth-protection-KF5Vo-1I.mjs} +2 -2
  82. package/dist/{platform-auth-recovery-DD3oLerq.mjs → platform-auth-recovery-BOdFrl7R.mjs} +2 -2
  83. package/dist/{platform-cmd-WnOmdLzR.mjs → platform-cmd-DpCHawGF.mjs} +1 -1
  84. package/dist/{platform-cmd-PLNLg-2N.mjs → platform-cmd-Ds3EAV5t.mjs} +5 -4
  85. package/dist/{platform-domain-Bnq0vg0q.mjs → platform-domain-rWJshybd.mjs} +2 -2
  86. package/dist/{platform-lifecycle-BQiN9HN5.mjs → platform-lifecycle-BH34I0rh.mjs} +1 -1
  87. package/dist/{platform-lifecycle-BCsFg1Tl.mjs → platform-lifecycle-D9RdGub1.mjs} +66 -31
  88. package/dist/{platform-management-nzCDwtiN.mjs → platform-management-BRefdHGD.mjs} +21 -3
  89. package/dist/{platform-management-CMJR8Zi-.mjs → platform-management-BfLW0Zlo.mjs} +1 -1
  90. package/dist/{platform-recovery-DBNoME3I.mjs → platform-recovery-DZuZGJtt.mjs} +2 -2
  91. package/dist/platform-runtime-logs-CdJGnk3f.mjs +68 -0
  92. package/dist/{platform-setup-VEcHu0Vq.mjs → platform-setup-Vw1AaUN8.mjs} +15 -2
  93. package/dist/{plugin-inference-DmwjC3y2.mjs → plugin-inference-CdLbxZL_.mjs} +1 -1
  94. package/dist/{prefetch-L93_1Jk7.mjs → prefetch-Dx2jIbtg.mjs} +19 -1
  95. package/dist/{prefetch-CG0K6JPd.d.mts → prefetch-NdNeubbH.d.mts} +7 -1
  96. package/dist/{prepare-pfl2LIMy.mjs → prepare-BJW-f1f1.mjs} +4 -4
  97. package/dist/{prepare-BPsfLWaW.mjs → prepare-BMGeKL16.mjs} +1 -1
  98. package/dist/prepare-BdxJJCMv.mjs +1 -0
  99. package/dist/{project-cmd-5Izu4Pg3.mjs → project-cmd-B_bUyZ3J.mjs} +15 -15
  100. package/dist/{project-team-DSUyjZFh.mjs → project-team-MjT5K4rQ.mjs} +1 -1
  101. package/dist/{project-token-wj09YQSB.mjs → project-token-BKHGwY3k.mjs} +1 -1
  102. package/dist/{project-zero-trust-BMHf2YF0.mjs → project-zero-trust-Bzdd05-h.mjs} +1 -1
  103. package/dist/{protocol-0-lp9ZYx.mjs → protocol-COyySMki.mjs} +130 -107
  104. package/dist/{protocol-xdrl-In0.d.mts → protocol-CkwjASep.d.mts} +2 -2
  105. package/dist/{provision-CRiNnlfE.mjs → provision-BbCzpbXs.mjs} +1 -1
  106. package/dist/{provision-CxHIGBIZ.mjs → provision-DQF6Kw-6.mjs} +1 -1
  107. package/dist/{requests-DicgB5jU.mjs → requests-daJmVpjk.mjs} +1 -1
  108. package/dist/{rollback-DrDZOKNE.mjs → rollback-BgL0wY1J.mjs} +2 -2
  109. package/dist/{route-types-irZc1txT.mjs → route-types-CZc1YXjF.mjs} +40 -4
  110. package/dist/route-url-C_YIH_zO.d.mts +37 -0
  111. package/dist/routes-stub.d.mts +6 -2
  112. package/dist/runtime/better-auth-mysql.mjs +1 -1
  113. package/dist/runtime/better-auth-pg.mjs +1 -1
  114. package/dist/runtime/better-auth.mjs +1 -1
  115. package/dist/runtime/email/testing.d.mts +1 -1
  116. package/dist/runtime/email/testing.mjs +3 -3
  117. package/dist/runtime/email.d.mts +1 -1
  118. package/dist/runtime/email.mjs +3 -3
  119. package/dist/runtime/handler.d.mts +1 -1
  120. package/dist/runtime/handler.mjs +2 -2
  121. package/dist/runtime/live-client.d.mts +1 -1
  122. package/dist/runtime/live.d.mts +1 -1
  123. package/dist/runtime/route-url.d.mts +2 -2
  124. package/dist/runtime/routing.d.mts +3 -3
  125. package/dist/runtime/routing.mjs +37 -16
  126. package/dist/runtime/validator.d.mts +1 -1
  127. package/dist/runtime/ws-server.d.mts +1 -1
  128. package/dist/runtime/ws.d.mts +1 -1
  129. package/dist/{scan-DSQtH3oS.mjs → scan-X2rDDRNa.mjs} +18 -10
  130. package/dist/{secret-C_QtTzJG.mjs → secret-CPq-B_vp.mjs} +2 -2
  131. package/dist/{static-asset-marker-NR1XS2z5.mjs → static-asset-marker-DsQKz6lt.mjs} +8 -1
  132. package/dist/validation-error-uAAjlsiW.mjs +82 -0
  133. package/dist/{version-metadata-Blf5H9BQ.mjs → version-metadata-C8hLzQkv.mjs} +1 -1
  134. package/dist/{wrangler-p-kLCJqF.mjs → wrangler-DESSbN8L.mjs} +2 -2
  135. package/dist/wrangler-DUB3MGHV.mjs +1 -0
  136. package/dist/{wrapper-iejSJDwL.mjs → wrapper-C9SUxoj7.mjs} +2 -2
  137. package/dist/{ws-DzBD-NUj.d.mts → ws-Dmjg5NH2.d.mts} +1 -1
  138. package/docs/guide/edge/headers.md +2 -0
  139. package/docs/guide/edge/rewrites.md +1 -1
  140. package/docs/guide/email/receiving.md +1 -1
  141. package/docs/guide/email/sending.md +42 -12
  142. package/docs/guide/pages-routing/actions-and-forms.md +21 -1
  143. package/docs/guide/pages-routing/islands.md +3 -3
  144. package/docs/guide/pages-routing/layouts.md +16 -2
  145. package/docs/guide/pages-routing/loaders.md +2 -2
  146. package/docs/guide/pages-routing/markdown.md +1 -1
  147. package/docs/guide/pages-routing/overview.md +103 -0
  148. package/docs/guide/platform/administration/connected-apps.md +4 -2
  149. package/docs/guide/platform/installation/credentials.md +6 -4
  150. package/docs/guide/platform/installation/maintenance.md +13 -0
  151. package/docs/guide/platform/installation/setup.md +22 -1
  152. package/docs/guide/ssg.md +3 -3
  153. package/docs/guide/type-safety.md +42 -1
  154. package/docs/integrations/frameworks/astro.md +17 -12
  155. package/docs/integrations/frameworks/overview.md +2 -2
  156. package/docs/reference/api/pages.md +101 -50
  157. package/docs/reference/api/rewrites.md +4 -1
  158. package/docs/reference/api/types.md +14 -10
  159. package/docs/reference/api.md +1 -1
  160. package/docs/reference/cli/platform-installation.md +4 -1
  161. package/docs/reference/cli/setup.md +1 -1
  162. package/docs/reference/config.md +10 -0
  163. package/package.json +8 -7
  164. package/schema.json +4 -0
  165. package/skills/void/SKILL.md +5 -0
  166. package/dist/client-DVtAKeUm.mjs +0 -1
  167. package/dist/deploy-CfU7CR-b.mjs +0 -2
  168. package/dist/gen-i61rdQgg.mjs +0 -1
  169. package/dist/headers-DEUBAM6W.mjs +0 -225
  170. package/dist/help-BcZDUcTw.mjs +0 -1
  171. package/dist/login-C0MiQrYs.mjs +0 -1
  172. package/dist/migrate-BG09-KOv.mjs +0 -1
  173. package/dist/operator-cmd-BhHXIL2G.mjs +0 -1
  174. package/dist/platform-args-C1kqYUen.mjs +0 -1
  175. package/dist/prepare-BJMmfq3W.mjs +0 -1
  176. package/dist/route-url-BGOiyKNz.d.mts +0 -24
  177. package/dist/validation-error-rDVbUJet.mjs +0 -47
  178. package/dist/wrangler-CPRX2d9A.mjs +0 -1
@@ -1,5 +1,5 @@
1
- import { A as filterInternalBindings, M as createClientAssetTags, N as createManifestLoader, O as createAssetFallback, j as ClientAssetTags, k as createWorkerFetch } from "../handler-Drm_lyrV.mjs";
2
- import { a as handlePageMutation, i as handlePageGet } from "../protocol-xdrl-In0.mjs";
1
+ import { A as filterInternalBindings, M as createClientAssetTags, N as createManifestLoader, O as createAssetFallback, j as ClientAssetTags, k as createWorkerFetch } from "../handler-BR6Tvjkp.mjs";
2
+ import { a as handlePageMutation, i as handlePageGet } from "../protocol-CkwjASep.mjs";
3
3
  import { AsyncLocalStorage } from "node:async_hooks";
4
4
  import { Context, Hono, MiddlewareHandler, TypedResponse } from "hono";
5
5
  import { RouteDestination, RouteUrlInput } from "void/routes";
@@ -100,7 +100,7 @@ export type RoutingRuntime = {
100
100
  contextMiddleware: MiddlewareHandler;
101
101
  requestMiddleware: MiddlewareHandler;
102
102
  routingMiddleware: (rules: Array<RoutingRule>) => MiddlewareHandler;
103
- headerMiddleware: (rules: Array<RoutingHeaderRule>) => MiddlewareHandler;
103
+ headerMiddleware: (rules: Array<RoutingHeaderRule>, phase?: "auth-error") => MiddlewareHandler;
104
104
  applyFallback: (context: Context, rules: Array<RoutingRule>) => Promise<Response | undefined>;
105
105
  markNoMatch: (request: Request, response: Response) => Response;
106
106
  };
@@ -2,10 +2,10 @@ import { n as createClientAssetTags, r as createManifestLoader, t as ASSET_EXTEN
2
2
  import { t as renderDefaultErrorPage } from "../error-pages-COVBnzK7.mjs";
3
3
  import { t as convertReturnValue } from "../response-HmNjB30w.mjs";
4
4
  import { i as searchWithoutActionKey, n as hasActionKey, r as markActionKey, t as actionKeySegment } from "../action-key-XtKKbbsi.mjs";
5
- import { n as handlePageGet, r as handlePageMutation } from "../protocol-0-lp9ZYx.mjs";
6
- import { t as VoidAssetRewriteError } from "../handler-xmzz0U38.mjs";
5
+ import { n as handlePageGet, r as handlePageMutation } from "../protocol-COyySMki.mjs";
6
+ import { t as VoidAssetRewriteError } from "../handler-CKeTtYID.mjs";
7
+ import { i as authDependentRequests, n as markStaticAsset, r as SHARED_CACHE_HEADERS, t as isUnchangedStaticAsset } from "../static-asset-marker-DsQKz6lt.mjs";
7
8
  import { r as toUrlString } from "../route-url-CRDF-I0I.mjs";
8
- import { n as markStaticAsset } from "../static-asset-marker-NR1XS2z5.mjs";
9
9
  import { AsyncLocalStorage } from "node:async_hooks";
10
10
  import { compilePattern, matchPattern, matchPatternCaptures, mergeQueryIntoLocation, redirectToHttps, substituteParams } from "@void/edge";
11
11
  // src/runtime/routing-worker.ts
@@ -747,6 +747,8 @@ function createErrorRenderer(options) {
747
747
  function createRoutingRuntime(app, options = {}) {
748
748
  const originalUrls = /* @__PURE__ */ new WeakMap();
749
749
  const fallbackHandled = /* @__PURE__ */ new WeakSet();
750
+ const fallbackServed = /* @__PURE__ */ new WeakSet();
751
+ const headerApplied = /* @__PURE__ */ new WeakSet();
750
752
  const warnedPatterns = /* @__PURE__ */ new Set();
751
753
  const requestContext = new AsyncLocalStorage();
752
754
  const assetExtensions = options.blockedAssetExtensions ?? ASSET_EXTENSIONS;
@@ -761,7 +763,11 @@ function createRoutingRuntime(app, options = {}) {
761
763
  headers
762
764
  });
763
765
  };
764
- const dispatch = (request, c) => Promise.resolve(options.nodeTarget ? app.fetch(request, c.env) : app.fetch(request, c.env, c.executionCtx));
766
+ const dispatch = async (request, c, trace) => {
767
+ const response = annotate(await (options.nodeTarget ? app.fetch(request, c.env) : app.fetch(request, c.env, c.executionCtx)), trace);
768
+ if (authDependentRequests.has(request)) authDependentRequests.add(c.req.raw);
769
+ return response;
770
+ };
765
771
  const rememberOriginal = (request, c) => {
766
772
  originalUrls.set(request, originalUrls.get(c.req.raw) ?? new URL(c.req.url));
767
773
  };
@@ -793,10 +799,12 @@ function createRoutingRuntime(app, options = {}) {
793
799
  const request = new Request(rewriteUrl.toString(), c.req.raw);
794
800
  rememberOriginal(request, c);
795
801
  keepActionKey(c.req.raw, request, search);
796
- if (fallback) fallbackHandled.add(request);
797
- const response = await dispatch(request, c);
802
+ if (fallback) {
803
+ fallbackHandled.add(request);
804
+ fallbackServed.add(c.req.raw);
805
+ }
798
806
  const kind = rule.force === true ? "rewrite" : "fallback";
799
- return annotate(response, `${kind}[${rule.source}] -> ${destination} (${rule.origin || "unknown"})`);
807
+ return dispatch(request, c, `${kind}[${rule.source}] -> ${destination} (${rule.origin || "unknown"})`);
800
808
  }
801
809
  const location = mergeQueryIntoLocation(destination, url.search);
802
810
  return annotate(c.redirect(location, rule.status), `redirect[${rule.source}] -> ${destination} ${rule.status} (${rule.origin || "unknown"})`);
@@ -825,7 +833,7 @@ function createRoutingRuntime(app, options = {}) {
825
833
  const request = new Request(url.toString(), c.req.raw);
826
834
  rememberOriginal(request, c);
827
835
  keepActionKey(c.req.raw, request, search);
828
- return dispatch(request, c).then((response) => annotate(response, `c.rewrite -> ${path} (middleware)`));
836
+ return dispatch(request, c, `c.rewrite -> ${path} (middleware)`);
829
837
  };
830
838
  const redirect = context.redirect.bind(context);
831
839
  c.redirect = (destination, status) => {
@@ -855,8 +863,19 @@ function createRoutingRuntime(app, options = {}) {
855
863
  c.res.headers.set("X-Void-Routing", "pass-through");
856
864
  } catch {}
857
865
  },
858
- headerMiddleware: (rules) => async (c, next) => {
866
+ headerMiddleware: (rules, phase) => async (c, next) => {
867
+ const authErrorsOnly = phase === "auth-error";
859
868
  await next();
869
+ if (authErrorsOnly && (!c.error || headerApplied.has(c))) return;
870
+ if (!authErrorsOnly) headerApplied.add(c);
871
+ const protectCache = authErrorsOnly || authDependentRequests.has(c.req.raw);
872
+ if (protectCache) {
873
+ authDependentRequests.add(c.req.raw);
874
+ c.header("Cache-Control", "no-store");
875
+ c.header("Pragma", "no-cache");
876
+ for (const name of SHARED_CACHE_HEADERS) if (c.res.headers.has(name)) c.header(name, void 0);
877
+ }
878
+ if (fallbackServed.has(c.req.raw)) return;
860
879
  const pathname = new URL(c.req.url).pathname;
861
880
  const matched = [];
862
881
  for (const rule of rules) {
@@ -869,13 +888,15 @@ function createRoutingRuntime(app, options = {}) {
869
888
  if (matchPattern(rule.pattern, pathname)) matched.push(...rule.headers);
870
889
  }
871
890
  if (!matched.length) return;
872
- const headers = new Headers(c.res.headers);
873
- for (const [name, value] of matched) headers.set(name, value);
874
- c.res = new Response(c.res.body, {
875
- headers,
876
- status: c.res.status,
877
- statusText: c.res.statusText
878
- });
891
+ const last = /* @__PURE__ */ new Map();
892
+ for (const [name, value] of matched) last.set(name.toLowerCase(), [name, value]);
893
+ const asset = isUnchangedStaticAsset(c);
894
+ for (const [key, [name, value]] of last) {
895
+ if (protectCache && (key === "cache-control" || key === "pragma" || SHARED_CACHE_HEADERS.includes(key))) continue;
896
+ if (key === "set-cookie" && c.res.headers.getSetCookie().at(-1) === value) continue;
897
+ c.header(name, value, { append: key === "set-cookie" });
898
+ }
899
+ if (asset) markStaticAsset(c, c.res);
879
900
  },
880
901
  applyFallback: (c, rules) => fallbackHandled.has(c.req.raw) ? Promise.resolve(void 0) : applyRules(c, rules, true),
881
902
  markNoMatch
@@ -1,2 +1,2 @@
1
- import { D as runValidation, E as ValidatorSlots, T as HandlerInput } from "../handler-Drm_lyrV.mjs";
1
+ import { D as runValidation, E as ValidatorSlots, T as HandlerInput } from "../handler-BR6Tvjkp.mjs";
2
2
  export { HandlerInput, ValidatorSlots, runValidation };
@@ -1,6 +1,6 @@
1
1
  import { n as AuthState } from "../auth-CH4PLAX1.mjs";
2
2
  import { r as VoidAuthRuntimeOptions, t as VoidAuthEnv } from "../better-auth-shared-BJyG2mH0.mjs";
3
- import { f as WebSocketInstanceKey, t as AnyWebSocketDefinition } from "../ws-DzBD-NUj.mjs";
3
+ import { f as WebSocketInstanceKey, t as AnyWebSocketDefinition } from "../ws-Dmjg5NH2.mjs";
4
4
  export type WebSocketRouteMeta = {
5
5
  name?: string;
6
6
  key?: WebSocketInstanceKey;
@@ -1,2 +1,2 @@
1
- import { a as RoomDefinition, c as VoidSocket, d as WebSocketConnection, f as WebSocketInstanceKey, h as defineWebSocket, i as RoomController, l as VoidSocketCloseEvent, m as defineRoom, n as ConnectCommonOptions, o as SocketContext, p as connect, r as RoomContext, s as SocketDefinition, t as AnyWebSocketDefinition, u as WebSocketBaseContext } from "../ws-DzBD-NUj.mjs";
1
+ import { a as RoomDefinition, c as VoidSocket, d as WebSocketConnection, f as WebSocketInstanceKey, h as defineWebSocket, i as RoomController, l as VoidSocketCloseEvent, m as defineRoom, n as ConnectCommonOptions, o as SocketContext, p as connect, r as RoomContext, s as SocketDefinition, t as AnyWebSocketDefinition, u as WebSocketBaseContext } from "../ws-Dmjg5NH2.mjs";
2
2
  export { AnyWebSocketDefinition, ConnectCommonOptions, RoomContext, RoomController, RoomDefinition, SocketContext, SocketDefinition, VoidSocket, VoidSocketCloseEvent, WebSocketBaseContext, WebSocketConnection, WebSocketInstanceKey, connect, defineRoom, defineWebSocket };
@@ -575,11 +575,13 @@ async function scanPages(root, options) {
575
575
  if (rendering.shouldPrerender && revalidate === void 0) revalidate = MAX_REVALIDATE;
576
576
  let metadata;
577
577
  let layout;
578
- if (ext === ".md") ({metadata, layout} = deriveFromSource(join(pagesDir, file), "page-markdown", (source) => ({
579
- metadata: extractFrontmatterMeta(source),
580
- layout: extractFrontmatterLayout(source)
581
- })));
582
- else layout = deriveFromSource(join(pagesDir, file), "page-layout", (source) => componentLayout(source, file));
578
+ if (ext === ".md") {
579
+ const { default: matter } = await import("gray-matter");
580
+ ({metadata, layout} = deriveFromSource(join(pagesDir, file), "page-markdown", (source) => ({
581
+ metadata: extractFrontmatterMeta(source),
582
+ layout: extractFrontmatterLayout(source, matter)
583
+ })));
584
+ } else layout = deriveFromSource(join(pagesDir, file), "page-layout", (source) => componentLayout(source, file));
583
585
  pages.push({
584
586
  pattern: route.pattern,
585
587
  params: route.params,
@@ -701,12 +703,18 @@ function extractFrontmatterMeta(source) {
701
703
  };
702
704
  }
703
705
  /** Extract layout field from YAML frontmatter in markdown source. */
704
- function extractFrontmatterLayout(source) {
706
+ function extractFrontmatterLayout(source, matter) {
705
707
  const yaml = markdownFrontmatter(source);
706
708
  if (yaml === void 0) return;
707
- const value = frontmatterField(yaml, "layout");
708
- if (!value) return;
709
- if (value === "false") return false;
710
- return value;
709
+ let frontmatter;
710
+ try {
711
+ frontmatter = matter(`---\n${yaml}\n---`, {}).data;
712
+ } catch {
713
+ return;
714
+ }
715
+ const value = frontmatter.layout;
716
+ if (value === void 0 || value === null || value === "") return;
717
+ if (value === false || value === "false") return false;
718
+ return typeof value === "string" ? value : JSON.stringify(value);
711
719
  }
712
720
  export { resolveLayoutChain as a, editIslandSource as c, deriveFromSource as d, invalidateSourceAnalysis as f, ROUTE_EXTENSIONS as g, parseWebSocketFilename as h, resolveEffectiveLayoutChain as i, findIslandImports as l, parseRouteFilename as m, scanPages as n, resolveLayoutIds as o, isRouteEnabledForEnvironment as p, generateComponentManifest as r, resolveNamedLayout as s, pageFileEnvironment as t, findStaticCssImports as u };
@@ -1,7 +1,7 @@
1
1
  import { n as readDeploymentPlatform, r as readProjectConfig } from "./project-CLwxcEdq.mjs";
2
2
  import { c as getToken } from "./auth-Dy8svcqQ.mjs";
3
3
  import { A as confirm, H as outro, L as log, O as cancel, U as password, tt as isCancel } from "./dist-D23AAMxT.mjs";
4
- import { r as PlatformClient } from "./client-Qayy9xEh.mjs";
4
+ import { r as PlatformClient } from "./client-CoxPEhJq.mjs";
5
5
  import { m as printSuccess, o as formatCount, p as note } from "./output-BCdLioKP.mjs";
6
6
  import { a as resolveProjectBySlug, t as getRequestedProjectSlug } from "./resolve-project-D0YNnJfm.mjs";
7
7
  import { _ as validateSecretValues } from "./env-validation-GV1oD4EV.mjs";
@@ -97,7 +97,7 @@ async function resolveSecretBackend(root, args) {
97
97
  }
98
98
  async function resolveCloudflareSecretBackend(root, args) {
99
99
  if (args.projectName) throw new Error("secret: --project selects a Void platform project and is not used for direct Cloudflare apps.");
100
- const { deleteCloudflareSecret, listCloudflareSecrets, putCloudflareSecret, resolveCloudflareOperationContext, syncCloudflareSecrets } = await import("./cloudflare-operations-D8MoezuN.mjs");
100
+ const { deleteCloudflareSecret, listCloudflareSecrets, putCloudflareSecret, resolveCloudflareOperationContext, syncCloudflareSecrets } = await import("./cloudflare-operations-oLvAd9Nd.mjs");
101
101
  const context = resolveCloudflareOperationContext(root);
102
102
  return {
103
103
  target: "Cloudflare Worker",
@@ -1,3 +1,10 @@
1
+ // src/runtime/auth-response.ts
2
+ const authDependentRequests = /* @__PURE__ */ new WeakSet();
3
+ const SHARED_CACHE_HEADERS = [
4
+ "cdn-cache-control",
5
+ "cloudflare-cdn-cache-control",
6
+ "surrogate-control"
7
+ ];
1
8
  // src/runtime/static-asset-marker.ts
2
9
  const STATIC_ASSET_KEY = "__voidStaticAsset";
3
10
  /**
@@ -28,4 +35,4 @@ function isUnchangedStaticAsset(c) {
28
35
  304
29
36
  ].includes(c.res.status) && c.res.body === mark.body && (c.res.headers.get("cache-control") ?? "") === mark.policy && !c.res.headers.has("set-cookie");
30
37
  }
31
- export { markStaticAsset as n, isUnchangedStaticAsset as t };
38
+ export { authDependentRequests as i, markStaticAsset as n, SHARED_CACHE_HEADERS as r, isUnchangedStaticAsset as t };
@@ -0,0 +1,82 @@
1
+ // src/runtime/handler-composition.ts
2
+ const compositionKey = "__voidHandlerComposition";
3
+ function getHandlerComposition(handler) {
4
+ return handler?.[compositionKey];
5
+ }
6
+ /** Record the composition; a nested composed leaf is flattened into one chain. */
7
+ function setHandlerComposition(handler, middlewares, leaf) {
8
+ const nested = getHandlerComposition(leaf);
9
+ const composition = nested ? {
10
+ middlewares: [...middlewares, ...nested.middlewares],
11
+ leaf: nested.leaf
12
+ } : {
13
+ middlewares,
14
+ leaf
15
+ };
16
+ Object.defineProperty(handler, compositionKey, { value: composition });
17
+ return handler;
18
+ }
19
+ /**
20
+ * Run middleware on the current context around `endpoint`, the way Hono's
21
+ * compose does: a Response returned by a middleware replaces `c.res`, the
22
+ * endpoint's Response becomes `c.res`, and middleware can edit `c.res` after
23
+ * `next()`. Returns the final `c.res`.
24
+ */
25
+ async function executeComposedHandler(c, middlewares, endpoint) {
26
+ let index = 0;
27
+ const dispatch = async () => {
28
+ if (index < middlewares.length) {
29
+ const response = await middlewares[index++](c, dispatch);
30
+ if (response instanceof Response) c.res = response;
31
+ } else c.res = await endpoint();
32
+ };
33
+ await dispatch();
34
+ return c.res;
35
+ }
36
+ // src/pages/defer.ts
37
+ const DEFERRED = Symbol("deferred");
38
+ function defer(groupOrResolver, maybeResolver) {
39
+ const [group, resolver] = typeof groupOrResolver === "string" ? [groupOrResolver, maybeResolver] : [null, groupOrResolver];
40
+ return {
41
+ [DEFERRED]: true,
42
+ group,
43
+ resolver
44
+ };
45
+ }
46
+ /**
47
+ * Separate a loader's return value into immediate props and deferred sentinels.
48
+ * For single-prop deferred (group=null), the object key becomes the group name.
49
+ * For named groups, all keys sharing the same group name are collected.
50
+ */
51
+ function separateDeferred(props) {
52
+ const immediateProps = {};
53
+ const deferredMap = {};
54
+ for (const [key, value] of Object.entries(props)) if (value && typeof value === "object" && DEFERRED in value) {
55
+ const sentinel = value;
56
+ const groupName = sentinel.group ?? key;
57
+ if (!deferredMap[groupName]) deferredMap[groupName] = {
58
+ resolver: sentinel.resolver,
59
+ keys: []
60
+ };
61
+ deferredMap[groupName].keys.push(key);
62
+ immediateProps[key] = {
63
+ value: null,
64
+ loading: true,
65
+ error: null
66
+ };
67
+ } else immediateProps[key] = value;
68
+ return {
69
+ immediateProps,
70
+ deferredMap
71
+ };
72
+ }
73
+ // src/pages/validation-error.ts
74
+ var ValidationError = class extends Error {
75
+ errors;
76
+ constructor(errors) {
77
+ super("Validation failed");
78
+ this.name = "ValidationError";
79
+ this.errors = errors;
80
+ }
81
+ };
82
+ export { executeComposedHandler as a, separateDeferred as i, DEFERRED as n, getHandlerComposition as o, defer as r, setHandlerComposition as s, ValidationError as t };
@@ -1,5 +1,5 @@
1
1
  import { a as join } from "./dist-DMYe8CZJ.mjs";
2
- import { l as bindingNamesForField, p as findBindingNameOwner } from "./wrangler-p-kLCJqF.mjs";
2
+ import { l as bindingNamesForField, p as findBindingNameOwner } from "./wrangler-DESSbN8L.mjs";
3
3
  // src/shared/untrusted-env.ts
4
4
  /** Authentication inputs that Wrangler may consume from the environment. */
5
5
  const WRANGLER_AUTH_CREDENTIAL_ENV_VARS = [
@@ -1,7 +1,7 @@
1
1
  import { a as join, c as resolve, n as dirname, o as normalize, t as basename } from "./dist-DMYe8CZJ.mjs";
2
2
  import { o as TYPED_CONFIG_FILE, s as loadTypedVoidConfigFile } from "./project-paths-CdGv9Tmi.mjs";
3
- import { h as validateConfig, p as readConfig } from "./config-DkA9jYF2.mjs";
4
- import { a as persistCloudflareConfig, i as materializeCloudflareConfig, r as ignoreMaterializedCloudflareConfig } from "./cloudflare-config-Chet188_.mjs";
3
+ import { h as validateConfig, p as readConfig } from "./config-BITKQ3O6.mjs";
4
+ import { a as persistCloudflareConfig, i as materializeCloudflareConfig, r as ignoreMaterializedCloudflareConfig } from "./cloudflare-config-eQunqX6A.mjs";
5
5
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
6
6
  import { parseSync } from "vite";
7
7
  import { appendSqliteClassMigration, isRecord } from "@void/deploy-core";
@@ -0,0 +1 @@
1
+ export { b as readRootWranglerConfig } from "./wrangler-DESSbN8L.mjs";
@@ -1,9 +1,9 @@
1
1
  import { a as join, l as toSlash, n as dirname, r as extname } from "./dist-DMYe8CZJ.mjs";
2
2
  import { n as readProjectPaths } from "./project-paths-CdGv9Tmi.mjs";
3
- import { l as getDatabaseDialect, p as readConfig } from "./config-DkA9jYF2.mjs";
3
+ import { l as getDatabaseDialect, p as readConfig } from "./config-BITKQ3O6.mjs";
4
4
  import { n as isIdentifier, r as isLiteral } from "./ast-Cv0JwXqb.mjs";
5
5
  import { t as MagicString } from "./dist-B6eN80gc.mjs";
6
- import { c as voidDbBundlerPlugin, r as cloudflareQueueName, t as generateQueueResolverSource } from "./dispatch-CiJ1Xrba.mjs";
6
+ import { c as voidDbBundlerPlugin, r as cloudflareQueueName, t as generateQueueResolverSource } from "./dispatch-BNLB5_Gm.mjs";
7
7
  import { t as WORKER_CONDITIONS } from "./worker-conditions-CBZ0RYsZ.mjs";
8
8
  import { createRequire } from "node:module";
9
9
  import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
@@ -1,6 +1,6 @@
1
1
  import { t as StandardSchemaV1 } from "./standard-schema-CuDawHOC.mjs";
2
2
  import { r as AuthUser } from "./auth-CH4PLAX1.mjs";
3
- import { a as CloudEnv } from "./handler-Drm_lyrV.mjs";
3
+ import { a as CloudEnv } from "./handler-BR6Tvjkp.mjs";
4
4
  import { WebSocketRouteMap } from "void/routes";
5
5
  type MaybePromise<T> = T | Promise<T>;
6
6
  type ParamsRecord = Record<string, string>;
@@ -24,6 +24,8 @@ Define custom response headers in [`void.config.ts`](../../reference/config) usi
24
24
  - **Header values** use `Name: value` format. The value may contain colons.
25
25
  - All matching rules are merged. When multiple rules set the same header name, the **last match wins**.
26
26
  - User-defined `Cache-Control` overrides the built-in default. The default still applies when no rule matches.
27
+ - With [authentication](../auth.md), responses to requests that send cookies or credentials stay private, and auth endpoints stay uncached, during local development, with `void deploy --platform cloudflare`, and on [other runtimes](../deployment.md#other-runtimes). On a [Void platform](../deployment.md#deploy-to-a-team-platform), a `Cache-Control` rule replaces the app's value, so do not match signed-in pages or `/api/auth/*` with a public cache rule.
28
+ - Rules apply to matching error responses too, including 404 responses. An immutable cache rule can therefore cache a missing file. Use immutable caching only for URLs that change when their content changes.
27
29
 
28
30
  ## Scope
29
31
 
@@ -56,7 +56,7 @@ Keep access checks on any destination that needs them.
56
56
 
57
57
  A destination without `?` preserves the incoming query string. `c.rewrite('/search')` keeps `?q=...`, while `c.rewrite('/search?q=all')` replaces it.
58
58
 
59
- Your editor suggests known routes through the [`RewriteDestination`](../../reference/api/rewrites.md#rewritedestination) type. Dynamic strings are also accepted.
59
+ Your editor suggests known routes through the [`RewriteDestination`](../../reference/api/rewrites.md#rewritedestination) type. Dynamic strings are also accepted. With [`routing.typedRoutes`](../../reference/config.md#routing-typedroutes), a string must match one of your routes; add `as RouteUrl` to a path built at runtime, such as ``c.rewrite(`/en${c.req.path}` as RouteUrl)``.
60
60
 
61
61
  ### Runtime rewrites cannot reach static assets
62
62
 
@@ -78,7 +78,7 @@ export default defineEmail(async (message, env, ctx, info) => {
78
78
  });
79
79
  ```
80
80
 
81
- `replyEmail()` replies to the original sender with a threaded subject. You can override `subject`.
81
+ `replyEmail()` replies to the original sender with a threaded subject. You can override `subject`. To reply after the handler returns, use [`sendEmail` with `threading`](./sending.md#reply-later).
82
82
 
83
83
  On a Void platform, the default sender is your project's shared address, or the address that received mail on your registered domain. Use that default for shared mail; `message.to` has the project prefix removed. Sender overrides must belong to your project.
84
84
 
@@ -67,18 +67,19 @@ For mail to arbitrary users, [register your own domain](./domains.md#your-own-do
67
67
 
68
68
  ## Options {#options}
69
69
 
70
- | Option | Type | Notes |
71
- | ---------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------- |
72
- | `from` | `string \| { email, name? }` | Optional. Pinned to the project sender — see below. |
73
- | `to` | `Address \| Address[]` | Required. One or more recipients. |
74
- | `subject` | `string` | Required. UTF-8 supported (encoded as RFC 2047). |
75
- | `text` | `string` | At least one of `text` / `html` is required. |
76
- | `html` | `string` | Sent as `multipart/alternative` if both are provided. |
77
- | `replyTo` | `Address` | Optional `Reply-To` header. |
78
- | `cc`, `bcc` | `Address \| Address[]` | Optional. Each recipient is sent its own message envelope. |
79
- | `headers` | `Record<string, string>` | Custom headers; reserved headers (From, Date, etc.) are ignored. |
80
- | `attachments` | `Attachment[]` | See [Attachments](#attachments). |
81
- | `idempotencyKey` | `string` | Optional on the Void Platform: 1–128 printable, non-space ASCII characters. Reuse for retries of the same send. |
70
+ | Option | Type | Notes |
71
+ | ---------------- | ----------------------------- | --------------------------------------------------------------------------------------------------------------- |
72
+ | `from` | `string \| { email, name? }` | Optional. Pinned to the project sender — see below. |
73
+ | `to` | `Address \| Address[]` | Required. One or more recipients. |
74
+ | `subject` | `string` | Required. UTF-8 supported (encoded as RFC 2047). |
75
+ | `text` | `string` | At least one of `text` / `html` is required. |
76
+ | `html` | `string` | Sent as `multipart/alternative` if both are provided. |
77
+ | `replyTo` | `Address` | Optional `Reply-To` header. |
78
+ | `cc`, `bcc` | `Address \| Address[]` | Optional. Each recipient is sent its own message envelope. |
79
+ | `headers` | `Record<string, string>` | Custom headers; reserved headers (From, Date, etc.) are ignored. |
80
+ | `threading` | `{ inReplyTo?, references? }` | Optional. Threads the email under an earlier message. See [Reply later](#reply-later). |
81
+ | `attachments` | `Attachment[]` | See [Attachments](#attachments). |
82
+ | `idempotencyKey` | `string` | Optional on the Void Platform: 1–128 printable, non-space ASCII characters. Reuse for retries of the same send. |
82
83
 
83
84
  You can send to at most 50 recipients across `to`, `cc`, and `bcc`. Use standard email addresses with ASCII local parts, and punycode for internationalized domains.
84
85
 
@@ -88,6 +89,35 @@ On the platform the sender is pinned to your project. `from` must be your projec
88
89
 
89
90
  On your own Cloudflare account, `from` defaults to `email.from` from `void.config.ts` and must be on a domain your account can send from; Cloudflare rejects any other sender and `sendEmail` reports it as `INVALID_FROM`.
90
91
 
92
+ ## Reply later {#reply-later}
93
+
94
+ `replyEmail()` works only while the inbound handler runs. To reply later, for example from a queue job, save the thread ids in the handler and pass them to `sendEmail` as `threading`:
95
+
96
+ ```ts
97
+ // email/support.ts
98
+ export default defineEmail(async (message) => {
99
+ await tickets.save({
100
+ from: message.from,
101
+ subject: message.headers.get('Subject') ?? '',
102
+ messageId: message.headers.get('Message-ID') ?? '',
103
+ references: message.headers.get('References') ?? '',
104
+ });
105
+ });
106
+
107
+ // Later, in a queue job
108
+ await sendEmail({
109
+ to: ticket.from,
110
+ subject: `Re: ${ticket.subject}`,
111
+ text: 'We fixed it.',
112
+ threading: {
113
+ inReplyTo: ticket.messageId,
114
+ references: `${ticket.references} ${ticket.messageId}`,
115
+ },
116
+ });
117
+ ```
118
+
119
+ `inReplyTo` is the parent message's `Message-ID`. `references` lists the thread's ids, oldest first, ending with the parent; it defaults to `inReplyTo`. Mail clients use them to show the reply in the same conversation. An id that cannot be sent, such as one with a control character, is left out. If `references` cannot be sent, it falls back to `inReplyTo`. The email still sends. `headers` cannot set `In-Reply-To` or `References`.
120
+
91
121
  ## Attachments {#attachments}
92
122
 
93
123
  ```ts
@@ -29,6 +29,8 @@ export const action = defineHandler.withValidator({
29
29
  | Nothing (void) | Re-runs the loader, page re-renders with fresh props. |
30
30
  | `c.redirect('/path')` | Navigates to another page. |
31
31
 
32
+ Middleware does not run again after the action. To change [shared data](./layouts.md#shared-data), update it in the action or redirect.
33
+
32
34
  ## Form
33
35
 
34
36
  Pair `Form` with `useForm` to submit values and display validation and action errors. Inputs stay editable when a submission fails.
@@ -196,8 +198,26 @@ const form = useForm(
196
198
 
197
199
  `onError` receives a field-error map or a `VoidActionError`. Callbacks can be async; `form.pending` stays true until they finish, and thrown errors reach the nearest React error boundary. Callbacks run before React commits the updated UI, so use an effect or ref callback to focus an element that appears after submission.
198
200
 
201
+ If a callback awaits `router.visit()` or `router.refresh()`, pass `{ resolveOn: 'data' }` so it can finish before React commits the navigation. See [React navigation completion](../../reference/api/pages.md#react-navigation-completion).
202
+
199
203
  Vue, Svelte, and Solid submissions return a promise instead; use `await form.post()` to run work after a submission.
200
204
 
205
+ ### Scroll after submission
206
+
207
+ Actions and forms keep the current scroll position when updating the page in place. Redirects and submissions that open another page scroll to the top, or to the destination's hash target.
208
+
209
+ Pass `preserveScroll: false` to reset scroll after an in-place update, or `true` to keep it even after a redirect:
210
+
211
+ ```ts
212
+ await action('/users/:id?update', {
213
+ params: { id: '42' },
214
+ data: { name: 'Alice' },
215
+ preserveScroll: false,
216
+ });
217
+ ```
218
+
219
+ In React, set `preserveScroll` in the third argument to `useForm()`. In Vue, Svelte, and Solid, pass it to the submit method, such as `form.post({ preserveScroll: false })`.
220
+
201
221
  ## Named Actions
202
222
 
203
223
  When a page needs multiple mutations, such as updating and deleting a user on the same page, export `actions` (plural) instead of `action`:
@@ -270,7 +290,7 @@ if (!result.ok) {
270
290
 
271
291
  `action()` defaults to `POST`. Pass `data`, `params`, and `method` to set the body, route parameters, or HTTP method.
272
292
 
273
- A successful call returns `{ ok: true, pageData }`. Expected errors, such as validation failures or conflicts, return `{ ok: false, error }`.
293
+ A successful call returns `{ ok: true, pageData }`. Expected errors, such as validation failures or conflicts, return `{ ok: false, error }`. `error.message` is the message of an [`HTTPException`](../../integrations/hono.md#utilities) with a `4xx` status, or the body of another plain-text `4xx` response. For other errors, it is a status summary such as `Action failed with 404 Not Found`.
274
294
 
275
295
  ## Validation Errors
276
296
 
@@ -96,11 +96,11 @@ export default function BlogIndex({ posts }) {
96
96
 
97
97
  ```vue [Vue]
98
98
  <!-- pages/blog/index.island.vue -->
99
- <script setup>
99
+ <script setup lang="ts">
100
100
  import Counter from './_Counter.vue' with { island: 'load' };
101
101
  import PostForm from './_PostForm.vue' with { island: 'visible' };
102
102
 
103
- defineProps({ posts: Array });
103
+ const { posts } = defineProps<{ posts: { slug: string; title: string }[] }>();
104
104
  </script>
105
105
 
106
106
  <template>
@@ -249,7 +249,7 @@ export default function PostForm() {
249
249
  ```
250
250
 
251
251
  ```vue [Vue]
252
- <script setup>
252
+ <script setup lang="ts">
253
253
  import { useIslandForm } from '@void/vue';
254
254
 
255
255
  const form = useIslandForm({ title: '', body: '' });
@@ -167,7 +167,7 @@ export default function Page() {
167
167
  ```
168
168
 
169
169
  ```vue [Vue]
170
- <script>
170
+ <script lang="ts">
171
171
  export const layout = 'landing';
172
172
  </script>
173
173
 
@@ -238,4 +238,18 @@ export default defineMiddleware(async (c, next) => {
238
238
  });
239
239
  ```
240
240
 
241
- Call `useShared()` from your framework adapter, as in the layout examples above. Its return type is inferred from `CloudContextVariables`. Shared data comes from middleware; page props come from the loader. See [Context variables](../type-safety.md#context-variables).
241
+ Call `useShared()` from your framework adapter, as in the layout examples above. Its return type is inferred from `CloudContextVariables`. See [Context variables](../type-safety.md#context-variables).
242
+
243
+ Middleware sets the starting value. A loader or action can update it before the page is sent:
244
+
245
+ ```ts
246
+ // pages/profile.server.ts
247
+ import { defineHandler } from 'void';
248
+
249
+ export const action = defineHandler(async (c) => {
250
+ const user = await updateProfile(c);
251
+ c.set('shared', { ...c.get('shared'), auth: { user } });
252
+ });
253
+ ```
254
+
255
+ Middleware runs before the action. If an action changes data that shared data is built from, update shared data in the action, or redirect with `c.redirect()` so middleware runs again on the next page load.
@@ -51,7 +51,7 @@ export default function UsersPage({ users }: Props) {
51
51
  <!-- pages/users/index.vue -->
52
52
  <script setup lang="ts">
53
53
  import type { Props } from './index.server';
54
- defineProps<Props>();
54
+ const { users } = defineProps<Props>();
55
55
  </script>
56
56
 
57
57
  <template>
@@ -177,7 +177,7 @@ Check `loading` first, then `error`. TypeScript narrows `value` to `T` in the re
177
177
  ```vue [Vue]
178
178
  <script setup lang="ts">
179
179
  import type { Props } from './dashboard.server';
180
- defineProps<Props>();
180
+ const { projects, usage } = defineProps<Props>();
181
181
  </script>
182
182
 
183
183
  <template>
@@ -190,7 +190,7 @@ export default function DocsLayout({ children }) {
190
190
  ```
191
191
 
192
192
  ```vue [Vue]
193
- <script setup>
193
+ <script setup lang="ts">
194
194
  import { useFrontmatter } from '@void/md';
195
195
 
196
196
  const fm = useFrontmatter();