@gravito/ion 3.0.1 → 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/dist/index.js CHANGED
@@ -1,158 +1,669 @@
1
+ // src/errors.ts
2
+ var InertiaError = class extends Error {
3
+ constructor(code, httpStatus = 500, details) {
4
+ super(code);
5
+ this.code = code;
6
+ this.httpStatus = httpStatus;
7
+ this.details = details;
8
+ this.name = "InertiaError";
9
+ Object.setPrototypeOf(this, new.target.prototype);
10
+ Error.captureStackTrace?.(this, this.constructor);
11
+ }
12
+ toJSON() {
13
+ return {
14
+ name: this.name,
15
+ code: this.code,
16
+ httpStatus: this.httpStatus,
17
+ message: this.message,
18
+ details: this.details
19
+ };
20
+ }
21
+ };
22
+ var InertiaConfigError = class extends InertiaError {
23
+ constructor(message, details) {
24
+ super("CONFIG_ERROR", 500, {
25
+ ...details,
26
+ message,
27
+ hint: details?.hint ?? "Check your OrbitIon configuration and ensure dependencies are loaded."
28
+ });
29
+ this.name = "InertiaConfigError";
30
+ }
31
+ };
32
+ var InertiaDataError = class extends InertiaError {
33
+ constructor(message, details) {
34
+ super("DATA_ERROR", 500, {
35
+ ...details,
36
+ message,
37
+ hint: details?.hint ?? "Ensure all props are JSON-serializable (no circular refs, BigInt, or functions)."
38
+ });
39
+ this.name = "InertiaDataError";
40
+ }
41
+ };
42
+ var InertiaTemplateError = class extends InertiaError {
43
+ constructor(message, details) {
44
+ super("TEMPLATE_ERROR", 500, {
45
+ ...details,
46
+ message,
47
+ hint: details?.hint ?? "Check if the root view template exists and is properly configured."
48
+ });
49
+ this.name = "InertiaTemplateError";
50
+ }
51
+ };
52
+
1
53
  // src/InertiaService.ts
2
54
  var InertiaService = class {
3
55
  /**
4
- * Create a new InertiaService instance
56
+ * Initializes a new instance of the Inertia service.
5
57
  *
6
- * @param context - The Gravito request context
7
- * @param config - Optional configuration
58
+ * @param context - The current Gravito request context.
59
+ * @param config - Instance configuration options.
8
60
  */
9
61
  constructor(context, config = {}) {
10
62
  this.context = context;
11
63
  this.config = config;
64
+ this.logLevel = config.logLevel ?? "info";
65
+ this.onRenderCallback = config.onRender;
12
66
  }
13
67
  sharedProps = {};
68
+ errorBags = {};
69
+ encryptHistoryFlag = false;
70
+ clearHistoryFlag = false;
71
+ logLevel;
72
+ onRenderCallback;
73
+ /**
74
+ * Creates a deferred prop that will be loaded after the initial render.
75
+ *
76
+ * @param factory - Function that resolves to the prop value.
77
+ * @param group - Optional group name for organizing deferred props.
78
+ * @returns A deferred prop definition.
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * props: {
83
+ * user: { id: 1 },
84
+ * stats: InertiaService.defer(() => fetchStats())
85
+ * }
86
+ * ```
87
+ */
88
+ static defer(factory, group = "default") {
89
+ return {
90
+ _type: "deferred",
91
+ factory,
92
+ group
93
+ };
94
+ }
14
95
  /**
15
- * Escape a string for safe use in HTML attributes
96
+ * Marks a prop to be merged (shallow) with existing data during partial reloads.
16
97
  *
17
- * Strategy: JSON.stringify already escapes special characters including
18
- * quotes as \". We need to escape these for HTML attributes, but we must
19
- * be careful not to break JSON escape sequences.
98
+ * @param value - The prop value to merge.
99
+ * @param matchOn - Optional key(s) to match on when merging arrays.
100
+ * @returns A merge prop definition.
101
+ */
102
+ static merge(value, matchOn) {
103
+ return {
104
+ _type: "merge",
105
+ value,
106
+ matchOn
107
+ };
108
+ }
109
+ /**
110
+ * Marks a prop to prepend to an array during partial reloads.
20
111
  *
21
- * The solution: Escape backslash-quote sequences (\" from JSON.stringify)
22
- * as \\" so they become \\" in HTML, which the browser decodes
23
- * to \\" (valid JSON), not \" (invalid JSON).
112
+ * @param value - The prop value to prepend.
113
+ * @returns A prepend prop definition.
114
+ */
115
+ static prepend(value) {
116
+ return {
117
+ _type: "prepend",
118
+ value
119
+ };
120
+ }
121
+ /**
122
+ * Marks a prop to be deep-merged with existing data during partial reloads.
24
123
  *
25
- * @param value - The string to escape.
26
- * @returns The escaped string.
124
+ * @param value - The prop value to deep merge.
125
+ * @returns A deep merge prop definition.
126
+ */
127
+ static deepMerge(value) {
128
+ return {
129
+ _type: "deepMerge",
130
+ value
131
+ };
132
+ }
133
+ /**
134
+ * Internal logging helper that respects the configured log level.
135
+ *
136
+ * Routes logs to the Gravito context logger if available.
137
+ *
138
+ * @param level - Log severity level.
139
+ * @param message - Descriptive message.
140
+ * @param data - Optional metadata for the log.
141
+ */
142
+ log(level, message, data) {
143
+ const levels = ["debug", "info", "warn", "error", "silent"];
144
+ const currentLevelIndex = levels.indexOf(this.logLevel);
145
+ const messageLevelIndex = levels.indexOf(level);
146
+ if (this.logLevel === "silent" || messageLevelIndex < currentLevelIndex) {
147
+ return;
148
+ }
149
+ const logger = typeof this.context.get === "function" ? this.context.get("logger") : void 0;
150
+ if (logger && typeof logger[level] === "function") {
151
+ logger[level](message, data);
152
+ }
153
+ }
154
+ /**
155
+ * Escapes a string for safe embedding into a single-quoted HTML attribute.
156
+ *
157
+ * This ensures that JSON strings can be safely passed to the frontend
158
+ * via the `data-page` attribute without breaking the HTML structure or
159
+ * introducing XSS vulnerabilities.
160
+ *
161
+ * @param value - Raw JSON or text string.
162
+ * @returns Safely escaped HTML attribute value.
27
163
  */
28
164
  escapeForSingleQuotedHtmlAttribute(value) {
29
165
  return value.replace(/&/g, "&amp;").replace(/\\"/g, "\\&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/'/g, "&#039;");
30
166
  }
31
167
  /**
32
- * Render an Inertia component
168
+ * Renders an Inertia component and returns the appropriate HTTP response.
169
+ *
170
+ * This method handles the entire Inertia lifecycle:
171
+ * - Detects if the request is an Inertia AJAX request.
172
+ * - Validates asset versions.
173
+ * - Resolves props (executes functions, handles partial reloads).
174
+ * - Performs SSR if configured.
175
+ * - Wraps the result in a JSON response or the root HTML template.
33
176
  *
34
- * @param component - The component name to render
35
- * @param props - Props to pass to the component
36
- * @param rootVars - Additional variables for the root template
37
- * @returns HTTP Response
177
+ * @param component - Frontend component name (e.g., 'Users/Profile').
178
+ * @param props - Data passed to the component. Can include functions for lazy evaluation.
179
+ * @param rootVars - Variables passed to the root HTML template (only used on initial load).
180
+ * @param status - Optional HTTP status code.
181
+ * @returns A promise that resolves to a Gravito HTTP Response.
182
+ *
183
+ * @throws {@link InertiaError}
184
+ * Thrown if:
185
+ * - The `ViewService` is missing from the context (required for initial load).
186
+ * - Prop serialization fails due to circular references or non-serializable data.
38
187
  *
39
188
  * @example
40
189
  * ```typescript
41
- * return inertia.render('Users/Index', {
42
- * users: await User.all(),
43
- * filters: { search: ctx.req.query('search') }
44
- * })
190
+ * // Standard render
191
+ * return await inertia.render('Welcome', { name: 'Guest' });
192
+ *
193
+ * // Partial reload support (only 'stats' will be resolved if requested)
194
+ * return await inertia.render('Dashboard', {
195
+ * user: auth.user,
196
+ * stats: () => db.getStats()
197
+ * });
45
198
  * ```
46
199
  */
47
- render(component, props = {}, rootVars = {}) {
48
- let pageUrl;
200
+ async render(component, props, rootVars = {}, status) {
201
+ const startTime = performance.now();
202
+ const isInertiaRequest = Boolean(this.context.req.header("X-Inertia"));
49
203
  try {
50
- const reqUrl = new URL(this.context.req.url, "http://localhost");
51
- pageUrl = reqUrl.pathname + reqUrl.search;
52
- } catch {
53
- pageUrl = this.context.req.url;
54
- }
55
- const resolveProps = (p) => {
56
- const resolved = {};
57
- for (const [key, value] of Object.entries(p)) {
58
- resolved[key] = typeof value === "function" ? value() : value;
204
+ this.log("debug", "[InertiaService] Starting render", {
205
+ component,
206
+ isInertiaRequest,
207
+ propsCount: props ? Object.keys(props).length : 0
208
+ });
209
+ let pageUrl;
210
+ try {
211
+ const reqUrl = new URL(this.context.req.url, "http://localhost");
212
+ pageUrl = reqUrl.pathname + reqUrl.search;
213
+ } catch {
214
+ pageUrl = this.context.req.url;
59
215
  }
60
- return resolved;
61
- };
62
- const page = {
63
- component,
64
- props: resolveProps({ ...this.sharedProps, ...props }),
65
- url: pageUrl,
66
- version: this.config.version
67
- };
68
- if (this.context.req.header("X-Inertia")) {
69
- this.context.header("X-Inertia", "true");
70
- this.context.header("Vary", "Accept");
71
- return this.context.json(page);
72
- }
73
- const view = this.context.get("view");
74
- const rootView = this.config.rootView ?? "app";
75
- if (!view) {
76
- throw new Error("OrbitPrism is required for the initial page load in OrbitIon");
216
+ const resolveProps = async (p) => {
217
+ const partialData = this.context.req.header("X-Inertia-Partial-Data");
218
+ const partialExcept = this.context.req.header("X-Inertia-Partial-Except");
219
+ const partialComponent = this.context.req.header("X-Inertia-Partial-Component");
220
+ const resetKeys = this.context.req.header("X-Inertia-Reset");
221
+ const isPartial = partialComponent === component;
222
+ const only = partialData && isPartial ? partialData.split(",") : [];
223
+ const except = partialExcept && isPartial ? partialExcept.split(",") : [];
224
+ const resetKeyList = resetKeys ? resetKeys.split(",") : [];
225
+ const resolved = {};
226
+ const deferredGroups2 = {};
227
+ const mergedKeys2 = [];
228
+ for (const [key, value] of Object.entries(p)) {
229
+ if (only.length > 0 && !only.includes(key)) {
230
+ continue;
231
+ }
232
+ if (except.length > 0 && except.includes(key)) {
233
+ continue;
234
+ }
235
+ if (value && typeof value === "object" && "_type" in value && value._type === "deferred") {
236
+ const deferred = value;
237
+ const group = deferred.group ?? "default";
238
+ if (!deferredGroups2[group]) {
239
+ deferredGroups2[group] = [];
240
+ }
241
+ deferredGroups2[group].push(key);
242
+ continue;
243
+ }
244
+ if (value && typeof value === "object" && "_type" in value && ["merge", "prepend", "deepMerge"].includes(value._type)) {
245
+ const merged = value;
246
+ const existing = mergedKeys2.find((m) => m.mode === merged._type);
247
+ if (existing) {
248
+ existing.keys.push(key);
249
+ } else {
250
+ mergedKeys2.push({
251
+ keys: [key],
252
+ mode: merged._type
253
+ });
254
+ }
255
+ resolved[key] = merged.value;
256
+ continue;
257
+ }
258
+ if (resetKeyList.includes(key)) {
259
+ resolved[key] = void 0;
260
+ continue;
261
+ }
262
+ if (typeof value === "function") {
263
+ const propStart = performance.now();
264
+ try {
265
+ resolved[key] = await value();
266
+ this.log("debug", `[InertiaService] Resolved lazy prop: ${key}`, {
267
+ duration: `${(performance.now() - propStart).toFixed(2)}ms`
268
+ });
269
+ } catch (err) {
270
+ this.log("error", `[InertiaService] Failed to resolve lazy prop: ${key}`, { err });
271
+ throw new InertiaDataError(`Failed to resolve lazy prop: ${key}`, { cause: err });
272
+ }
273
+ } else {
274
+ resolved[key] = value;
275
+ }
276
+ }
277
+ return { resolved, deferredGroups: deferredGroups2, mergedKeys: mergedKeys2 };
278
+ };
279
+ const resolveVersion = async () => {
280
+ if (typeof this.config.version === "function") {
281
+ return await this.config.version();
282
+ }
283
+ return this.config.version;
284
+ };
285
+ const version = await resolveVersion();
286
+ const getMergedProps = () => {
287
+ const propsToMerge = props ?? {};
288
+ if (Object.keys(propsToMerge).length === 0) {
289
+ return this.sharedProps;
290
+ }
291
+ return { ...this.sharedProps, ...propsToMerge };
292
+ };
293
+ const {
294
+ resolved: resolvedPropsData,
295
+ deferredGroups,
296
+ mergedKeys
297
+ } = await resolveProps(getMergedProps());
298
+ const page = {
299
+ component,
300
+ props: resolvedPropsData,
301
+ url: pageUrl,
302
+ version
303
+ };
304
+ if (Object.keys(deferredGroups).length > 0) {
305
+ page.deferredProps = deferredGroups;
306
+ }
307
+ if (mergedKeys.length > 0) {
308
+ page.mergeProps = mergedKeys;
309
+ }
310
+ if (this.encryptHistoryFlag) {
311
+ page.encryptHistory = true;
312
+ }
313
+ if (this.clearHistoryFlag) {
314
+ page.clearHistory = true;
315
+ }
316
+ if (Object.keys(this.errorBags).length > 0) {
317
+ page.errorBags = this.errorBags;
318
+ }
319
+ let pageJson;
320
+ try {
321
+ pageJson = JSON.stringify(page);
322
+ } catch (error) {
323
+ throw new InertiaDataError(
324
+ `Inertia page serialization failed for component: ${component}`,
325
+ {
326
+ cause: error
327
+ }
328
+ );
329
+ }
330
+ let response;
331
+ if (isInertiaRequest) {
332
+ const clientVersion = this.context.req.header("X-Inertia-Version");
333
+ if (version && clientVersion && clientVersion !== version) {
334
+ this.context.header("X-Inertia-Location", pageUrl);
335
+ return new Response("", { status: 409 });
336
+ }
337
+ this.context.header("X-Inertia", "true");
338
+ this.context.header("Vary", "Accept");
339
+ response = this.context.json(page, status);
340
+ } else {
341
+ const view = this.context.get("view");
342
+ const rootView = this.config.rootView ?? "app";
343
+ if (!view) {
344
+ throw new InertiaConfigError(
345
+ "ViewService (OrbitPrism) is required for initial page load but was not found in context.",
346
+ { hint: "Ensure OrbitPrism is loaded before OrbitIon in your orbit configuration" }
347
+ );
348
+ }
349
+ const isDev = process.env.NODE_ENV !== "production";
350
+ let ssrData = { head: [], body: "" };
351
+ if (this.config.ssr?.enabled && this.config.ssr.render) {
352
+ try {
353
+ ssrData = await this.config.ssr.render(page);
354
+ } catch (error) {
355
+ this.log("error", "[InertiaService] SSR rendering failed", { error });
356
+ }
357
+ }
358
+ response = this.context.html(
359
+ view.render(
360
+ rootView,
361
+ {
362
+ ...rootVars,
363
+ page: this.escapeForSingleQuotedHtmlAttribute(pageJson),
364
+ isDev,
365
+ ssrHead: ssrData.head.join("\n"),
366
+ ssrBody: ssrData.body
367
+ },
368
+ { layout: "" }
369
+ ),
370
+ status
371
+ );
372
+ }
373
+ const duration = performance.now() - startTime;
374
+ this.log("info", "[InertiaService] Render complete", {
375
+ component,
376
+ duration: `${duration.toFixed(2)}ms`,
377
+ isInertiaRequest,
378
+ status: status ?? 200
379
+ });
380
+ if (this.onRenderCallback) {
381
+ this.onRenderCallback({
382
+ component,
383
+ duration,
384
+ isInertiaRequest,
385
+ propsCount: props ? Object.keys(props).length : 0,
386
+ timestamp: Date.now(),
387
+ status
388
+ });
389
+ }
390
+ return response;
391
+ } catch (error) {
392
+ const duration = performance.now() - startTime;
393
+ if (error instanceof InertiaError) {
394
+ this.log("error", "[InertiaService] Render failed", {
395
+ component,
396
+ duration: `${duration.toFixed(2)}ms`,
397
+ errorCode: error.code,
398
+ errorDetails: error.details
399
+ });
400
+ throw error;
401
+ }
402
+ this.log("error", "[InertiaService] Unexpected render error", {
403
+ component,
404
+ duration: `${duration.toFixed(2)}ms`,
405
+ error
406
+ });
407
+ const isDev = process.env.NODE_ENV !== "production";
408
+ if (isDev && error instanceof Error) {
409
+ const errorHtml = `
410
+ <!DOCTYPE html>
411
+ <html>
412
+ <head>
413
+ <meta charset="utf-8">
414
+ <meta name="viewport" content="width=device-width, initial-scale=1">
415
+ <title>Inertia Render Error</title>
416
+ <style>
417
+ * { margin: 0; padding: 0; box-sizing: border-box; }
418
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto; background: #f5f5f5; padding: 20px; }
419
+ .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); }
420
+ h1 { color: #d32f2f; margin-bottom: 10px; font-size: 24px; }
421
+ .meta { color: #666; margin-bottom: 20px; font-size: 14px; }
422
+ .component { background: #f5f5f5; padding: 10px 15px; border-left: 3px solid #1976d2; margin-bottom: 20px; font-family: monospace; }
423
+ .error-message { background: #ffebee; border: 1px solid #ef5350; border-radius: 4px; padding: 15px; margin-bottom: 20px; color: #c62828; }
424
+ .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; }
425
+ .hint { background: #e8f5e9; border-left: 3px solid #4caf50; padding: 15px; margin-top: 20px; color: #2e7d32; }
426
+ </style>
427
+ </head>
428
+ <body>
429
+ <div class="container">
430
+ <h1>\u{1F6A8} Inertia Render Error</h1>
431
+ <div class="meta">Component: <span class="component">${component}</span></div>
432
+ <div class="error-message">${error.message || "Unknown error"}</div>
433
+ ${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>` : ""}
434
+ <div class="hint">
435
+ <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.
436
+ </div>
437
+ </div>
438
+ </body>
439
+ </html>
440
+ `;
441
+ return this.context.html(errorHtml, 500);
442
+ }
443
+ return new Response("Inertia Render Error", { status: 500 });
77
444
  }
78
- const isDev = process.env.NODE_ENV !== "production";
79
- return this.context.html(
80
- view.render(
81
- rootView,
82
- {
83
- ...rootVars,
84
- page: this.escapeForSingleQuotedHtmlAttribute(JSON.stringify(page)),
85
- isDev
86
- },
87
- { layout: "" }
88
- )
89
- );
90
445
  }
91
- // ...
92
446
  /**
93
- * Share data with all Inertia responses
447
+ * Registers a piece of data to be shared with all Inertia responses for the current request.
94
448
  *
95
- * Shared props are merged with component-specific props on every render.
449
+ * Shared props are automatically merged with component-specific props during the render phase.
450
+ * This is the primary mechanism for providing global state like authentication details,
451
+ * flash messages, or configuration to the frontend.
96
452
  *
97
- * @param key - The prop key
98
- * @param value - The prop value
453
+ * @param key - Identifier for the shared prop.
454
+ * @param value - Value to share. Must be JSON serializable.
99
455
  *
100
456
  * @example
101
457
  * ```typescript
102
- * // In middleware
103
- * inertia.share('auth', { user: ctx.get('auth')?.user() })
104
- * inertia.share('flash', ctx.get('session')?.getFlash('message'))
458
+ * inertia.share('appName', 'Gravito Store');
459
+ * inertia.share('auth', { user: ctx.get('user') });
105
460
  * ```
106
461
  */
107
462
  share(key, value) {
108
463
  this.sharedProps[key] = value;
109
464
  }
110
465
  /**
111
- * Share multiple props at once
466
+ * Shares multiple props in a single operation.
467
+ *
468
+ * Merges the provided object into the existing shared props state. Existing keys
469
+ * with the same name will be overwritten.
112
470
  *
113
- * @param props - Object of props to share
471
+ * @param props - Object containing key-value pairs to merge into the shared state.
472
+ *
473
+ * @example
474
+ * ```typescript
475
+ * inertia.shareAll({
476
+ * version: '1.2.0',
477
+ * environment: 'production',
478
+ * features: ['chat', 'search']
479
+ * });
480
+ * ```
114
481
  */
115
482
  shareAll(props) {
116
- Object.assign(this.sharedProps, props);
483
+ this.sharedProps = { ...this.sharedProps, ...props };
117
484
  }
118
485
  /**
119
- * Get all shared props
486
+ * Returns a shallow copy of the current shared props.
487
+ *
488
+ * Useful for inspecting the shared state or for manual prop merging in advanced scenarios.
489
+ *
490
+ * @returns A dictionary containing all currently registered shared props.
120
491
  *
121
- * @returns A shallow copy of the shared props object.
492
+ * @example
493
+ * ```typescript
494
+ * const shared = inertia.getSharedProps();
495
+ * if (!shared.auth) {
496
+ * console.warn('Authentication data is missing from shared props');
497
+ * }
498
+ * ```
122
499
  */
123
500
  getSharedProps() {
124
501
  return { ...this.sharedProps };
125
502
  }
503
+ /**
504
+ * Instructs the Inertia client to navigate to a different URL.
505
+ *
506
+ * For Inertia AJAX requests, this returns a 409 Conflict with the `X-Inertia-Location` header.
507
+ * For regular page loads, this returns a 302 Found redirect.
508
+ *
509
+ * This is useful for server-side redirects triggered by authentication or authorization checks.
510
+ *
511
+ * @param url - The URL to redirect to.
512
+ * @returns A redirect response.
513
+ *
514
+ * @example
515
+ * ```typescript
516
+ * if (!ctx.get('user')) {
517
+ * return inertia.location('/login');
518
+ * }
519
+ * ```
520
+ */
521
+ location(url) {
522
+ const isInertiaRequest = Boolean(this.context.req.header("X-Inertia"));
523
+ if (isInertiaRequest) {
524
+ this.context.header("X-Inertia-Location", url);
525
+ return new Response("", { status: 409 });
526
+ }
527
+ this.context.header("Location", url);
528
+ return new Response("", { status: 302 });
529
+ }
530
+ /**
531
+ * Controls whether the Inertia client should encrypt browser history.
532
+ *
533
+ * When enabled, history is not written to the browser's History API,
534
+ * preventing users from using the browser back button to return to previous pages.
535
+ *
536
+ * @param encrypt - Whether to encrypt history (defaults to true).
537
+ * @returns The service instance for method chaining.
538
+ *
539
+ * @example
540
+ * ```typescript
541
+ * return await inertia.encryptHistory(true).render('SecurePage');
542
+ * ```
543
+ */
544
+ encryptHistory(encrypt = true) {
545
+ this.encryptHistoryFlag = encrypt;
546
+ return this;
547
+ }
548
+ /**
549
+ * Clears the browser history after the page load.
550
+ *
551
+ * Useful for sensitive operations or multi-step wizards where you don't want
552
+ * users navigating back to previous states.
553
+ *
554
+ * @returns The service instance for method chaining.
555
+ *
556
+ * @example
557
+ * ```typescript
558
+ * return await inertia.clearHistory().render('SuccessPage');
559
+ * ```
560
+ */
561
+ clearHistory() {
562
+ this.clearHistoryFlag = true;
563
+ return this;
564
+ }
565
+ /**
566
+ * Registers form validation errors organized into named bags.
567
+ *
568
+ * Error bags allow multiple validation failure scenarios to coexist.
569
+ * For example, you might have 'default' errors and 'import' errors from different forms.
570
+ *
571
+ * @param errors - Validation errors, with field names as keys and error messages as values.
572
+ * @param bag - The error bag name (defaults to 'default').
573
+ * @returns The service instance for method chaining.
574
+ *
575
+ * @example
576
+ * ```typescript
577
+ * inertia.withErrors({
578
+ * email: 'Email is required',
579
+ * password: 'Must be 8+ characters'
580
+ * }, 'login');
581
+ *
582
+ * inertia.withErrors({
583
+ * line_1: 'Invalid CSV format'
584
+ * }, 'import');
585
+ * ```
586
+ */
587
+ withErrors(errors, bag = "default") {
588
+ this.errorBags[bag] = errors;
589
+ return this;
590
+ }
126
591
  };
127
592
 
128
593
  // src/index.ts
129
594
  var OrbitIon = class {
595
+ /**
596
+ * Initializes the Orbit with custom configuration.
597
+ *
598
+ * @param options - Configuration overrides for the Inertia service.
599
+ */
130
600
  constructor(options = {}) {
131
601
  this.options = options;
132
602
  }
133
603
  /**
134
- * Install the inertia orbit into PlanetCore
604
+ * Registers the Inertia middleware and service factory into PlanetCore.
605
+ *
606
+ * This method is called automatically by Gravito during the boot process.
607
+ * It sets up the `InertiaService` for each request and attaches
608
+ * the `InertiaHelper` proxy to the context.
609
+ *
610
+ * @param core - The Gravito micro-kernel instance.
611
+ *
612
+ * @example
613
+ * ```typescript
614
+ * const ion = new OrbitIon({ version: '1.0' });
615
+ * ion.install(core);
616
+ * ```
135
617
  */
136
618
  install(core) {
137
619
  core.logger.info("\u{1F6F0}\uFE0F Orbit Inertia installed (Callable Interface)");
138
620
  const appVersion = this.options.version ?? core.config.get("APP_VERSION", "1.0.0");
139
621
  const rootView = this.options.rootView ?? "app";
622
+ const csrfEnabled = this.options.csrf?.enabled !== false;
623
+ const csrfCookieName = this.options.csrf?.cookieName ?? "XSRF-TOKEN";
624
+ const VERSION_CACHE_TTL = 6e4;
625
+ let cachedVersion;
626
+ let versionCacheTime = 0;
627
+ const resolveAppVersion = async () => {
628
+ if (typeof appVersion !== "function") {
629
+ return appVersion;
630
+ }
631
+ const now = Date.now();
632
+ if (cachedVersion && now - versionCacheTime < VERSION_CACHE_TTL) {
633
+ return cachedVersion;
634
+ }
635
+ cachedVersion = await appVersion();
636
+ versionCacheTime = now;
637
+ return cachedVersion;
638
+ };
639
+ if (csrfEnabled) {
640
+ core.adapter.use("*", async (c, next) => {
641
+ const token = crypto.randomUUID();
642
+ const secure = process.env.NODE_ENV === "production";
643
+ const cookieValue = `${csrfCookieName}=${token}; Path=/; ${secure ? "Secure; " : ""}SameSite=Lax`;
644
+ c.header("Set-Cookie", cookieValue);
645
+ return await next();
646
+ });
647
+ }
140
648
  core.adapter.use("*", async (c, next) => {
141
649
  const service = new InertiaService(c, {
142
- version: String(appVersion),
143
- rootView
650
+ version: resolveAppVersion,
651
+ rootView,
652
+ ssr: this.options.ssr
144
653
  });
145
- const inertiaProxy = (component, props = {}, rootVars = {}) => {
146
- return service.render(component, props, rootVars);
654
+ const inertiaProxy = async (component, props = {}, rootVars = {}, status) => {
655
+ return await service.render(component, props, rootVars, status);
147
656
  };
148
657
  Object.assign(inertiaProxy, {
149
658
  share: service.share.bind(service),
150
659
  shareAll: service.shareAll.bind(service),
151
660
  getSharedProps: service.getSharedProps.bind(service),
152
661
  render: service.render.bind(service),
153
- // Also allow .render()
662
+ location: service.location.bind(service),
663
+ encryptHistory: service.encryptHistory.bind(service),
664
+ clearHistory: service.clearHistory.bind(service),
665
+ withErrors: service.withErrors.bind(service),
154
666
  service
155
- // Access to the raw service instance
156
667
  });
157
668
  c.set("inertia", inertiaProxy);
158
669
  return await next();
@@ -161,7 +672,11 @@ var OrbitIon = class {
161
672
  };
162
673
  var index_default = OrbitIon;
163
674
  export {
675
+ InertiaConfigError,
676
+ InertiaDataError,
677
+ InertiaError,
164
678
  InertiaService,
679
+ InertiaTemplateError,
165
680
  OrbitIon,
166
681
  index_default as default
167
682
  };