@gravito/ion 3.1.0 → 4.0.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.
package/README.md CHANGED
@@ -1,18 +1,19 @@
1
1
  # 🛰️ Orbit Inertia (Ion)
2
2
 
3
- > Inertia.js adapter for Gravito. Build modern monoliths with React/Vue/Svelte.
3
+ > Inertia.js v2 adapter for Gravito. Build modern monoliths with React/Vue/Svelte.
4
4
 
5
- **Orbit Inertia** (@gravito/ion) is a high-performance adapter that allows you to build single-page apps using classic server-side routing and controllers. It acts as the "glue" between Gravito (Photon) and your frontend framework, eliminating the need for a separate REST/GraphQL API.
5
+ **Orbit Inertia** (@gravito/ion) is a high-performance adapter implementing the **Inertia.js v2 protocol** for Gravito. It allows you to build single-page apps using classic server-side routing and controllers, acting as the "glue" between Gravito (Photon) and your frontend framework, eliminating the need for a separate REST/GraphQL API.
6
6
 
7
7
  ## ✨ Key Features
8
8
 
9
9
  - **🚀 Modern Monolith Architecture**: Combine the productivity of server-side routing with the interactivity of SPA frameworks.
10
10
  - **🛠️ Zero API Development**: Pass data directly from controllers to components as typed props—no more managing endpoints or manual serialization.
11
- - **⚡ High-Performance Rendering**: Built-in multi-layer caching for components and metadata, ensuring sub-millisecond overhead.
11
+ - **⚡ High-Performance Rendering**: Built-in multi-layer caching, version caching (60s TTL), and component metadata optimization.
12
12
  - **🛡️ Native Type Safety**: Full TypeScript support with generics for props, ensuring end-to-end type safety from server to client.
13
13
  - **🔗 Ecosystem Integration**: Seamlessly works with `OrbitPrism` for root templates and Gravito's session/auth modules.
14
14
  - **🔍 SEO & SSR Friendly**: Designed for modern web requirements, supporting Server-Side Rendering patterns for optimal visibility.
15
15
  - **🎨 Multi-Framework Support**: Official support for **React**, **Vue**, and **Svelte**.
16
+ - **✨ Inertia v2 Protocol**: Full support for deferred props, merge strategies, error bags, and CSRF protection.
16
17
 
17
18
  ## 📦 Installation
18
19
 
@@ -74,6 +75,71 @@ export class DashboardController {
74
75
  }
75
76
  ```
76
77
 
78
+ ## 🔧 Inertia v2 Protocol Features
79
+
80
+ ### Deferred Props (Lazy Loading)
81
+ Skip initial render, load props separately post-render:
82
+
83
+ ```typescript
84
+ inertia.render('Dashboard', {
85
+ user: { id: 1, name: 'Carl' }, // Initial
86
+ stats: InertiaService.defer(() => fetchStats(), 'heavy'), // Deferred
87
+ notifications: InertiaService.defer(() => fetchNotifications(), 'notifications')
88
+ });
89
+ ```
90
+
91
+ ### Merge Strategies (Partial Reloads)
92
+ Control how props merge during partial reloads:
93
+
94
+ ```typescript
95
+ inertia.render('Products/List', {
96
+ items: InertiaService.prepend([newProduct]), // Add to start
97
+ filters: InertiaService.deepMerge({ status: 'active' }), // Recursive merge
98
+ config: InertiaService.merge({ sortBy: 'name' }) // Shallow merge
99
+ });
100
+ ```
101
+
102
+ ### Error Bags (Form Validation)
103
+ Organize validation errors by category:
104
+
105
+ ```typescript
106
+ inertia.withErrors({
107
+ email: 'Email is required',
108
+ password: 'Must be 8+ characters'
109
+ }, 'login'); // Named bag
110
+
111
+ inertia.withErrors({
112
+ line_1: 'Invalid CSV format'
113
+ }, 'import');
114
+ ```
115
+
116
+ ### Smart Redirects
117
+ Automatic 409 response for Inertia requests, 302 for regular requests:
118
+
119
+ ```typescript
120
+ if (!user) {
121
+ return inertia.location('/login'); // Smart redirect
122
+ }
123
+ ```
124
+
125
+ ### History Control
126
+ ```typescript
127
+ inertia.encryptHistory(true); // Disable back button
128
+ inertia.clearHistory(); // Clear history after load
129
+ ```
130
+
131
+ ### CSRF Protection
132
+ Automatic XSRF-TOKEN cookie generation (Axios-compatible):
133
+
134
+ ```typescript
135
+ const ion = new OrbitIon({
136
+ csrf: {
137
+ enabled: true,
138
+ cookieName: 'XSRF-TOKEN' // Axios reads this automatically
139
+ }
140
+ });
141
+ ```
142
+
77
143
  ## 🔧 Advanced Features
78
144
 
79
145
  ### Shared Props
@@ -84,7 +150,18 @@ inertia.share('auth', { user: 'Carl' });
84
150
  ```
85
151
 
86
152
  ### Partial Reloads
87
- Ion supports Inertia's partial reloads, allowing the client to request only specific data to save bandwidth.
153
+ Ion supports Inertia's partial reloads with smart merge strategies, allowing the client to request only specific data to save bandwidth.
154
+
155
+ ### Method Chaining
156
+ All methods support fluent interface:
157
+
158
+ ```typescript
159
+ return await inertia
160
+ .encryptHistory()
161
+ .clearHistory()
162
+ .withErrors({ email: 'Invalid' })
163
+ .render('SecurePage', props);
164
+ ```
88
165
 
89
166
  ### Manual Serialization Control
90
167
  Customize how your data is converted to JSON for the client:
package/README.zh-TW.md CHANGED
@@ -1,18 +1,19 @@
1
1
  # 🛰️ Orbit Inertia (Ion)
2
2
 
3
- > Gravito 的 Inertia.js 轉接器,用於建立現代化單體應用 (Modern Monolith)。支援 React、Vue 與 Svelte。
3
+ > Gravito 的 Inertia.js v2 轉接器,用於建立現代化單體應用 (Modern Monolith)。支援 React、Vue 與 Svelte。
4
4
 
5
- **Orbit Inertia** (@gravito/ion) 是一個高效能的轉接器,讓您能夠使用傳統的伺服器端路由與控制器來建立單頁應用程式 (SPA)。它扮演了 Gravito (Photon) 與前端框架之間的「膠水」,消除了開發獨立 REST 或 GraphQL API 的需求。
5
+ **Orbit Inertia** (@gravito/ion) 是一個高效能的轉接器,實現 **Inertia.js v2 協議**,讓您能夠使用傳統的伺服器端路由與控制器來建立單頁應用程式 (SPA)。它扮演了 Gravito (Photon) 與前端框架之間的「膠水」,消除了開發獨立 REST 或 GraphQL API 的需求。
6
6
 
7
7
  ## ✨ 核心特性
8
8
 
9
9
  - **🚀 現代化單體架構**:結合伺服器端開發的高效率與 SPA 框架的高互動性。
10
10
  - **🛠️ 零 API 開發**:直接將資料從控制器傳遞到組件作為具備型別的 Props,不再需要管理 API 端點或手動處理序列化。
11
- - **⚡ 高效能渲染**:內建針對組件與元資料 (Metadata) 的多層快取機制,確保毫秒級的極低開銷。
11
+ - **⚡ 高效能渲染**:內建多層快取、版本快取 (60 秒 TTL) 與組件元資料最佳化,確保毫秒級的極低開銷。
12
12
  - **🛡️ 原生型別安全**:完整的 TypeScript 支援,透過 Generics 確保從伺服器到前端的端到端型別安全。
13
13
  - **🔗 生態系整合**:與 `OrbitPrism` (模板引擎) 及 Gravito 的 Session/Auth 模組完美協作。
14
14
  - **🔍 SEO 與 SSR 友善**:專為現代 Web 需求設計,支援伺服器端渲染 (SSR) 模式以優化搜尋引擎可見度。
15
15
  - **🎨 多框架支援**:官方支援 **React**、**Vue** 與 **Svelte**。
16
+ - **✨ Inertia v2 協議**:完整支援延遲 Props、合併策略、錯誤包與 CSRF 防護。
16
17
 
17
18
  ## 📦 安裝
18
19
 
@@ -74,6 +75,71 @@ export class DashboardController {
74
75
  }
75
76
  ```
76
77
 
78
+ ## 🔧 Inertia v2 協議功能
79
+
80
+ ### 延遲 Props (Deferred Props)
81
+ 跳過初始渲染,在後續操作中單獨載入 Props:
82
+
83
+ ```typescript
84
+ inertia.render('Dashboard', {
85
+ user: { id: 1, name: 'Carl' }, // 初始載入
86
+ stats: InertiaService.defer(() => fetchStats(), 'heavy'), // 延遲載入
87
+ notifications: InertiaService.defer(() => fetchNotifications(), 'notifications')
88
+ });
89
+ ```
90
+
91
+ ### 合併策略 (Merge Strategies)
92
+ 控制局部重新載入時 Props 如何合併:
93
+
94
+ ```typescript
95
+ inertia.render('Products/List', {
96
+ items: InertiaService.prepend([newProduct]), // 加到開頭
97
+ filters: InertiaService.deepMerge({ status: 'active' }), // 遞迴合併
98
+ config: InertiaService.merge({ sortBy: 'name' }) // 淺合併
99
+ });
100
+ ```
101
+
102
+ ### 錯誤包 (Error Bags)
103
+ 按類別組織表單驗證錯誤:
104
+
105
+ ```typescript
106
+ inertia.withErrors({
107
+ email: '電郵為必填',
108
+ password: '必須為 8 個字元以上'
109
+ }, 'login'); // 命名包
110
+
111
+ inertia.withErrors({
112
+ line_1: '無效的 CSV 格式'
113
+ }, 'import');
114
+ ```
115
+
116
+ ### 智慧重定向 (Smart Redirects)
117
+ 自動為 Inertia 請求回傳 409,普通請求回傳 302:
118
+
119
+ ```typescript
120
+ if (!user) {
121
+ return inertia.location('/login'); // 智慧重定向
122
+ }
123
+ ```
124
+
125
+ ### 瀏覽歷史控制 (History Control)
126
+ ```typescript
127
+ inertia.encryptHistory(true); // 停用返回按鈕
128
+ inertia.clearHistory(); // 載入後清除歷史
129
+ ```
130
+
131
+ ### CSRF 防護
132
+ 自動產生 XSRF-TOKEN Cookie (與 Axios 相容):
133
+
134
+ ```typescript
135
+ const ion = new OrbitIon({
136
+ csrf: {
137
+ enabled: true,
138
+ cookieName: 'XSRF-TOKEN' // Axios 會自動讀取
139
+ }
140
+ });
141
+ ```
142
+
77
143
  ## 🔧 進階功能
78
144
 
79
145
  ### 共享 Props (Shared Props)
@@ -84,7 +150,18 @@ inertia.share('auth', { user: 'Carl' });
84
150
  ```
85
151
 
86
152
  ### 局部重新載入 (Partial Reloads)
87
- Ion 支援 Inertia 的局部重載機制,允許客戶端僅請求特定資料以節省頻寬。
153
+ Ion 支援 Inertia 的局部重載機制,搭配智慧合併策略,允許客戶端僅請求特定資料以節省頻寬。
154
+
155
+ ### 方法鏈式調用 (Method Chaining)
156
+ 所有方法都支援流暢介面:
157
+
158
+ ```typescript
159
+ return await inertia
160
+ .encryptHistory()
161
+ .clearHistory()
162
+ .withErrors({ email: '無效' })
163
+ .render('SecurePage', props);
164
+ ```
88
165
 
89
166
  ### 手動序列化控制
90
167
  自定義資料如何轉換為 JSON 傳遞給客戶端:
package/dist/index.cjs CHANGED
@@ -97,8 +97,71 @@ var InertiaService = class {
97
97
  this.onRenderCallback = config.onRender;
98
98
  }
99
99
  sharedProps = {};
100
+ errorBags = {};
101
+ encryptHistoryFlag = false;
102
+ clearHistoryFlag = false;
100
103
  logLevel;
101
104
  onRenderCallback;
105
+ /**
106
+ * Creates a deferred prop that will be loaded after the initial render.
107
+ *
108
+ * @param factory - Function that resolves to the prop value.
109
+ * @param group - Optional group name for organizing deferred props.
110
+ * @returns A deferred prop definition.
111
+ *
112
+ * @example
113
+ * ```typescript
114
+ * props: {
115
+ * user: { id: 1 },
116
+ * stats: InertiaService.defer(() => fetchStats())
117
+ * }
118
+ * ```
119
+ */
120
+ static defer(factory, group = "default") {
121
+ return {
122
+ _type: "deferred",
123
+ factory,
124
+ group
125
+ };
126
+ }
127
+ /**
128
+ * Marks a prop to be merged (shallow) with existing data during partial reloads.
129
+ *
130
+ * @param value - The prop value to merge.
131
+ * @param matchOn - Optional key(s) to match on when merging arrays.
132
+ * @returns A merge prop definition.
133
+ */
134
+ static merge(value, matchOn) {
135
+ return {
136
+ _type: "merge",
137
+ value,
138
+ matchOn
139
+ };
140
+ }
141
+ /**
142
+ * Marks a prop to prepend to an array during partial reloads.
143
+ *
144
+ * @param value - The prop value to prepend.
145
+ * @returns A prepend prop definition.
146
+ */
147
+ static prepend(value) {
148
+ return {
149
+ _type: "prepend",
150
+ value
151
+ };
152
+ }
153
+ /**
154
+ * Marks a prop to be deep-merged with existing data during partial reloads.
155
+ *
156
+ * @param value - The prop value to deep merge.
157
+ * @returns A deep merge prop definition.
158
+ */
159
+ static deepMerge(value) {
160
+ return {
161
+ _type: "deepMerge",
162
+ value
163
+ };
164
+ }
102
165
  /**
103
166
  * Internal logging helper that respects the configured log level.
104
167
  *
@@ -186,10 +249,14 @@ var InertiaService = class {
186
249
  const partialData = this.context.req.header("X-Inertia-Partial-Data");
187
250
  const partialExcept = this.context.req.header("X-Inertia-Partial-Except");
188
251
  const partialComponent = this.context.req.header("X-Inertia-Partial-Component");
252
+ const resetKeys = this.context.req.header("X-Inertia-Reset");
189
253
  const isPartial = partialComponent === component;
190
254
  const only = partialData && isPartial ? partialData.split(",") : [];
191
255
  const except = partialExcept && isPartial ? partialExcept.split(",") : [];
256
+ const resetKeyList = resetKeys ? resetKeys.split(",") : [];
192
257
  const resolved = {};
258
+ const deferredGroups2 = {};
259
+ const mergedKeys2 = [];
193
260
  for (const [key, value] of Object.entries(p)) {
194
261
  if (only.length > 0 && !only.includes(key)) {
195
262
  continue;
@@ -197,6 +264,33 @@ var InertiaService = class {
197
264
  if (except.length > 0 && except.includes(key)) {
198
265
  continue;
199
266
  }
267
+ if (value && typeof value === "object" && "_type" in value && value._type === "deferred") {
268
+ const deferred = value;
269
+ const group = deferred.group ?? "default";
270
+ if (!deferredGroups2[group]) {
271
+ deferredGroups2[group] = [];
272
+ }
273
+ deferredGroups2[group].push(key);
274
+ continue;
275
+ }
276
+ if (value && typeof value === "object" && "_type" in value && ["merge", "prepend", "deepMerge"].includes(value._type)) {
277
+ const merged = value;
278
+ const existing = mergedKeys2.find((m) => m.mode === merged._type);
279
+ if (existing) {
280
+ existing.keys.push(key);
281
+ } else {
282
+ mergedKeys2.push({
283
+ keys: [key],
284
+ mode: merged._type
285
+ });
286
+ }
287
+ resolved[key] = merged.value;
288
+ continue;
289
+ }
290
+ if (resetKeyList.includes(key)) {
291
+ resolved[key] = void 0;
292
+ continue;
293
+ }
200
294
  if (typeof value === "function") {
201
295
  const propStart = performance.now();
202
296
  try {
@@ -212,7 +306,7 @@ var InertiaService = class {
212
306
  resolved[key] = value;
213
307
  }
214
308
  }
215
- return resolved;
309
+ return { resolved, deferredGroups: deferredGroups2, mergedKeys: mergedKeys2 };
216
310
  };
217
311
  const resolveVersion = async () => {
218
312
  if (typeof this.config.version === "function") {
@@ -228,12 +322,32 @@ var InertiaService = class {
228
322
  }
229
323
  return { ...this.sharedProps, ...propsToMerge };
230
324
  };
325
+ const {
326
+ resolved: resolvedPropsData,
327
+ deferredGroups,
328
+ mergedKeys
329
+ } = await resolveProps(getMergedProps());
231
330
  const page = {
232
331
  component,
233
- props: await resolveProps(getMergedProps()),
332
+ props: resolvedPropsData,
234
333
  url: pageUrl,
235
334
  version
236
335
  };
336
+ if (Object.keys(deferredGroups).length > 0) {
337
+ page.deferredProps = deferredGroups;
338
+ }
339
+ if (mergedKeys.length > 0) {
340
+ page.mergeProps = mergedKeys;
341
+ }
342
+ if (this.encryptHistoryFlag) {
343
+ page.encryptHistory = true;
344
+ }
345
+ if (this.clearHistoryFlag) {
346
+ page.clearHistory = true;
347
+ }
348
+ if (Object.keys(this.errorBags).length > 0) {
349
+ page.errorBags = this.errorBags;
350
+ }
237
351
  let pageJson;
238
352
  try {
239
353
  pageJson = JSON.stringify(page);
@@ -322,6 +436,42 @@ var InertiaService = class {
322
436
  duration: `${duration.toFixed(2)}ms`,
323
437
  error
324
438
  });
439
+ const isDev = process.env.NODE_ENV !== "production";
440
+ if (isDev && error instanceof Error) {
441
+ const errorHtml = `
442
+ <!DOCTYPE html>
443
+ <html>
444
+ <head>
445
+ <meta charset="utf-8">
446
+ <meta name="viewport" content="width=device-width, initial-scale=1">
447
+ <title>Inertia Render Error</title>
448
+ <style>
449
+ * { margin: 0; padding: 0; box-sizing: border-box; }
450
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto; background: #f5f5f5; padding: 20px; }
451
+ .container { max-width: 800px; margin: 40px auto; background: white; border-radius: 8px; padding: 30px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
452
+ h1 { color: #d32f2f; margin-bottom: 10px; font-size: 24px; }
453
+ .meta { color: #666; margin-bottom: 20px; font-size: 14px; }
454
+ .component { background: #f5f5f5; padding: 10px 15px; border-left: 3px solid #1976d2; margin-bottom: 20px; font-family: monospace; }
455
+ .error-message { background: #ffebee; border: 1px solid #ef5350; border-radius: 4px; padding: 15px; margin-bottom: 20px; color: #c62828; }
456
+ .stack { background: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; padding: 15px; overflow-x: auto; font-family: monospace; font-size: 12px; color: #333; line-height: 1.5; }
457
+ .hint { background: #e8f5e9; border-left: 3px solid #4caf50; padding: 15px; margin-top: 20px; color: #2e7d32; }
458
+ </style>
459
+ </head>
460
+ <body>
461
+ <div class="container">
462
+ <h1>\u{1F6A8} Inertia Render Error</h1>
463
+ <div class="meta">Component: <span class="component">${component}</span></div>
464
+ <div class="error-message">${error.message || "Unknown error"}</div>
465
+ ${error.stack ? `<div><strong>Stack Trace:</strong><div class="stack">${error.stack.split("\n").map((line) => line.replace(/</g, "&lt;").replace(/>/g, "&gt;")).join("\n")}</div></div>` : ""}
466
+ <div class="hint">
467
+ <strong>\u{1F4A1} Dev Mode Tip:</strong> This enhanced error page is only visible in development mode. In production, a generic error message is shown.
468
+ </div>
469
+ </div>
470
+ </body>
471
+ </html>
472
+ `;
473
+ return this.context.html(errorHtml, 500);
474
+ }
325
475
  return new Response("Inertia Render Error", { status: 500 });
326
476
  }
327
477
  }
@@ -362,7 +512,7 @@ var InertiaService = class {
362
512
  * ```
363
513
  */
364
514
  shareAll(props) {
365
- Object.assign(this.sharedProps, props);
515
+ this.sharedProps = { ...this.sharedProps, ...props };
366
516
  }
367
517
  /**
368
518
  * Returns a shallow copy of the current shared props.
@@ -382,6 +532,94 @@ var InertiaService = class {
382
532
  getSharedProps() {
383
533
  return { ...this.sharedProps };
384
534
  }
535
+ /**
536
+ * Instructs the Inertia client to navigate to a different URL.
537
+ *
538
+ * For Inertia AJAX requests, this returns a 409 Conflict with the `X-Inertia-Location` header.
539
+ * For regular page loads, this returns a 302 Found redirect.
540
+ *
541
+ * This is useful for server-side redirects triggered by authentication or authorization checks.
542
+ *
543
+ * @param url - The URL to redirect to.
544
+ * @returns A redirect response.
545
+ *
546
+ * @example
547
+ * ```typescript
548
+ * if (!ctx.get('user')) {
549
+ * return inertia.location('/login');
550
+ * }
551
+ * ```
552
+ */
553
+ location(url) {
554
+ const isInertiaRequest = Boolean(this.context.req.header("X-Inertia"));
555
+ if (isInertiaRequest) {
556
+ this.context.header("X-Inertia-Location", url);
557
+ return new Response("", { status: 409 });
558
+ }
559
+ this.context.header("Location", url);
560
+ return new Response("", { status: 302 });
561
+ }
562
+ /**
563
+ * Controls whether the Inertia client should encrypt browser history.
564
+ *
565
+ * When enabled, history is not written to the browser's History API,
566
+ * preventing users from using the browser back button to return to previous pages.
567
+ *
568
+ * @param encrypt - Whether to encrypt history (defaults to true).
569
+ * @returns The service instance for method chaining.
570
+ *
571
+ * @example
572
+ * ```typescript
573
+ * return await inertia.encryptHistory(true).render('SecurePage');
574
+ * ```
575
+ */
576
+ encryptHistory(encrypt = true) {
577
+ this.encryptHistoryFlag = encrypt;
578
+ return this;
579
+ }
580
+ /**
581
+ * Clears the browser history after the page load.
582
+ *
583
+ * Useful for sensitive operations or multi-step wizards where you don't want
584
+ * users navigating back to previous states.
585
+ *
586
+ * @returns The service instance for method chaining.
587
+ *
588
+ * @example
589
+ * ```typescript
590
+ * return await inertia.clearHistory().render('SuccessPage');
591
+ * ```
592
+ */
593
+ clearHistory() {
594
+ this.clearHistoryFlag = true;
595
+ return this;
596
+ }
597
+ /**
598
+ * Registers form validation errors organized into named bags.
599
+ *
600
+ * Error bags allow multiple validation failure scenarios to coexist.
601
+ * For example, you might have 'default' errors and 'import' errors from different forms.
602
+ *
603
+ * @param errors - Validation errors, with field names as keys and error messages as values.
604
+ * @param bag - The error bag name (defaults to 'default').
605
+ * @returns The service instance for method chaining.
606
+ *
607
+ * @example
608
+ * ```typescript
609
+ * inertia.withErrors({
610
+ * email: 'Email is required',
611
+ * password: 'Must be 8+ characters'
612
+ * }, 'login');
613
+ *
614
+ * inertia.withErrors({
615
+ * line_1: 'Invalid CSV format'
616
+ * }, 'import');
617
+ * ```
618
+ */
619
+ withErrors(errors, bag = "default") {
620
+ this.errorBags[bag] = errors;
621
+ return this;
622
+ }
385
623
  };
386
624
 
387
625
  // src/index.ts
@@ -413,9 +651,35 @@ var OrbitIon = class {
413
651
  core.logger.info("\u{1F6F0}\uFE0F Orbit Inertia installed (Callable Interface)");
414
652
  const appVersion = this.options.version ?? core.config.get("APP_VERSION", "1.0.0");
415
653
  const rootView = this.options.rootView ?? "app";
654
+ const csrfEnabled = this.options.csrf?.enabled !== false;
655
+ const csrfCookieName = this.options.csrf?.cookieName ?? "XSRF-TOKEN";
656
+ const VERSION_CACHE_TTL = 6e4;
657
+ let cachedVersion;
658
+ let versionCacheTime = 0;
659
+ const resolveAppVersion = async () => {
660
+ if (typeof appVersion !== "function") {
661
+ return appVersion;
662
+ }
663
+ const now = Date.now();
664
+ if (cachedVersion && now - versionCacheTime < VERSION_CACHE_TTL) {
665
+ return cachedVersion;
666
+ }
667
+ cachedVersion = await appVersion();
668
+ versionCacheTime = now;
669
+ return cachedVersion;
670
+ };
671
+ if (csrfEnabled) {
672
+ core.adapter.use("*", async (c, next) => {
673
+ const token = crypto.randomUUID();
674
+ const secure = process.env.NODE_ENV === "production";
675
+ const cookieValue = `${csrfCookieName}=${token}; Path=/; ${secure ? "Secure; " : ""}SameSite=Lax`;
676
+ c.header("Set-Cookie", cookieValue);
677
+ return await next();
678
+ });
679
+ }
416
680
  core.adapter.use("*", async (c, next) => {
417
681
  const service = new InertiaService(c, {
418
- version: appVersion,
682
+ version: resolveAppVersion,
419
683
  rootView,
420
684
  ssr: this.options.ssr
421
685
  });
@@ -427,6 +691,10 @@ var OrbitIon = class {
427
691
  shareAll: service.shareAll.bind(service),
428
692
  getSharedProps: service.getSharedProps.bind(service),
429
693
  render: service.render.bind(service),
694
+ location: service.location.bind(service),
695
+ encryptHistory: service.encryptHistory.bind(service),
696
+ clearHistory: service.clearHistory.bind(service),
697
+ withErrors: service.withErrors.bind(service),
430
698
  service
431
699
  });
432
700
  c.set("inertia", inertiaProxy);