@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.cjs CHANGED
@@ -20,167 +20,682 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ InertiaConfigError: () => InertiaConfigError,
24
+ InertiaDataError: () => InertiaDataError,
25
+ InertiaError: () => InertiaError,
23
26
  InertiaService: () => InertiaService,
27
+ InertiaTemplateError: () => InertiaTemplateError,
24
28
  OrbitIon: () => OrbitIon,
25
29
  default: () => index_default
26
30
  });
27
31
  module.exports = __toCommonJS(index_exports);
28
32
 
33
+ // src/errors.ts
34
+ var InertiaError = class extends Error {
35
+ constructor(code, httpStatus = 500, details) {
36
+ super(code);
37
+ this.code = code;
38
+ this.httpStatus = httpStatus;
39
+ this.details = details;
40
+ this.name = "InertiaError";
41
+ Object.setPrototypeOf(this, new.target.prototype);
42
+ Error.captureStackTrace?.(this, this.constructor);
43
+ }
44
+ toJSON() {
45
+ return {
46
+ name: this.name,
47
+ code: this.code,
48
+ httpStatus: this.httpStatus,
49
+ message: this.message,
50
+ details: this.details
51
+ };
52
+ }
53
+ };
54
+ var InertiaConfigError = class extends InertiaError {
55
+ constructor(message, details) {
56
+ super("CONFIG_ERROR", 500, {
57
+ ...details,
58
+ message,
59
+ hint: details?.hint ?? "Check your OrbitIon configuration and ensure dependencies are loaded."
60
+ });
61
+ this.name = "InertiaConfigError";
62
+ }
63
+ };
64
+ var InertiaDataError = class extends InertiaError {
65
+ constructor(message, details) {
66
+ super("DATA_ERROR", 500, {
67
+ ...details,
68
+ message,
69
+ hint: details?.hint ?? "Ensure all props are JSON-serializable (no circular refs, BigInt, or functions)."
70
+ });
71
+ this.name = "InertiaDataError";
72
+ }
73
+ };
74
+ var InertiaTemplateError = class extends InertiaError {
75
+ constructor(message, details) {
76
+ super("TEMPLATE_ERROR", 500, {
77
+ ...details,
78
+ message,
79
+ hint: details?.hint ?? "Check if the root view template exists and is properly configured."
80
+ });
81
+ this.name = "InertiaTemplateError";
82
+ }
83
+ };
84
+
29
85
  // src/InertiaService.ts
30
86
  var InertiaService = class {
31
87
  /**
32
- * Create a new InertiaService instance
88
+ * Initializes a new instance of the Inertia service.
33
89
  *
34
- * @param context - The Gravito request context
35
- * @param config - Optional configuration
90
+ * @param context - The current Gravito request context.
91
+ * @param config - Instance configuration options.
36
92
  */
37
93
  constructor(context, config = {}) {
38
94
  this.context = context;
39
95
  this.config = config;
96
+ this.logLevel = config.logLevel ?? "info";
97
+ this.onRenderCallback = config.onRender;
40
98
  }
41
99
  sharedProps = {};
100
+ errorBags = {};
101
+ encryptHistoryFlag = false;
102
+ clearHistoryFlag = false;
103
+ logLevel;
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
+ }
42
127
  /**
43
- * Escape a string for safe use in HTML attributes
128
+ * Marks a prop to be merged (shallow) with existing data during partial reloads.
44
129
  *
45
- * Strategy: JSON.stringify already escapes special characters including
46
- * quotes as \". We need to escape these for HTML attributes, but we must
47
- * be careful not to break JSON escape sequences.
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.
48
143
  *
49
- * The solution: Escape backslash-quote sequences (\" from JSON.stringify)
50
- * as \\" so they become \\" in HTML, which the browser decodes
51
- * to \\" (valid JSON), not \" (invalid JSON).
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.
52
155
  *
53
- * @param value - The string to escape.
54
- * @returns The escaped string.
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
+ }
165
+ /**
166
+ * Internal logging helper that respects the configured log level.
167
+ *
168
+ * Routes logs to the Gravito context logger if available.
169
+ *
170
+ * @param level - Log severity level.
171
+ * @param message - Descriptive message.
172
+ * @param data - Optional metadata for the log.
173
+ */
174
+ log(level, message, data) {
175
+ const levels = ["debug", "info", "warn", "error", "silent"];
176
+ const currentLevelIndex = levels.indexOf(this.logLevel);
177
+ const messageLevelIndex = levels.indexOf(level);
178
+ if (this.logLevel === "silent" || messageLevelIndex < currentLevelIndex) {
179
+ return;
180
+ }
181
+ const logger = typeof this.context.get === "function" ? this.context.get("logger") : void 0;
182
+ if (logger && typeof logger[level] === "function") {
183
+ logger[level](message, data);
184
+ }
185
+ }
186
+ /**
187
+ * Escapes a string for safe embedding into a single-quoted HTML attribute.
188
+ *
189
+ * This ensures that JSON strings can be safely passed to the frontend
190
+ * via the `data-page` attribute without breaking the HTML structure or
191
+ * introducing XSS vulnerabilities.
192
+ *
193
+ * @param value - Raw JSON or text string.
194
+ * @returns Safely escaped HTML attribute value.
55
195
  */
56
196
  escapeForSingleQuotedHtmlAttribute(value) {
57
197
  return value.replace(/&/g, "&amp;").replace(/\\"/g, "\\&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/'/g, "&#039;");
58
198
  }
59
199
  /**
60
- * Render an Inertia component
200
+ * Renders an Inertia component and returns the appropriate HTTP response.
201
+ *
202
+ * This method handles the entire Inertia lifecycle:
203
+ * - Detects if the request is an Inertia AJAX request.
204
+ * - Validates asset versions.
205
+ * - Resolves props (executes functions, handles partial reloads).
206
+ * - Performs SSR if configured.
207
+ * - Wraps the result in a JSON response or the root HTML template.
61
208
  *
62
- * @param component - The component name to render
63
- * @param props - Props to pass to the component
64
- * @param rootVars - Additional variables for the root template
65
- * @returns HTTP Response
209
+ * @param component - Frontend component name (e.g., 'Users/Profile').
210
+ * @param props - Data passed to the component. Can include functions for lazy evaluation.
211
+ * @param rootVars - Variables passed to the root HTML template (only used on initial load).
212
+ * @param status - Optional HTTP status code.
213
+ * @returns A promise that resolves to a Gravito HTTP Response.
214
+ *
215
+ * @throws {@link InertiaError}
216
+ * Thrown if:
217
+ * - The `ViewService` is missing from the context (required for initial load).
218
+ * - Prop serialization fails due to circular references or non-serializable data.
66
219
  *
67
220
  * @example
68
221
  * ```typescript
69
- * return inertia.render('Users/Index', {
70
- * users: await User.all(),
71
- * filters: { search: ctx.req.query('search') }
72
- * })
222
+ * // Standard render
223
+ * return await inertia.render('Welcome', { name: 'Guest' });
224
+ *
225
+ * // Partial reload support (only 'stats' will be resolved if requested)
226
+ * return await inertia.render('Dashboard', {
227
+ * user: auth.user,
228
+ * stats: () => db.getStats()
229
+ * });
73
230
  * ```
74
231
  */
75
- render(component, props = {}, rootVars = {}) {
76
- let pageUrl;
232
+ async render(component, props, rootVars = {}, status) {
233
+ const startTime = performance.now();
234
+ const isInertiaRequest = Boolean(this.context.req.header("X-Inertia"));
77
235
  try {
78
- const reqUrl = new URL(this.context.req.url, "http://localhost");
79
- pageUrl = reqUrl.pathname + reqUrl.search;
80
- } catch {
81
- pageUrl = this.context.req.url;
82
- }
83
- const resolveProps = (p) => {
84
- const resolved = {};
85
- for (const [key, value] of Object.entries(p)) {
86
- resolved[key] = typeof value === "function" ? value() : value;
236
+ this.log("debug", "[InertiaService] Starting render", {
237
+ component,
238
+ isInertiaRequest,
239
+ propsCount: props ? Object.keys(props).length : 0
240
+ });
241
+ let pageUrl;
242
+ try {
243
+ const reqUrl = new URL(this.context.req.url, "http://localhost");
244
+ pageUrl = reqUrl.pathname + reqUrl.search;
245
+ } catch {
246
+ pageUrl = this.context.req.url;
87
247
  }
88
- return resolved;
89
- };
90
- const page = {
91
- component,
92
- props: resolveProps({ ...this.sharedProps, ...props }),
93
- url: pageUrl,
94
- version: this.config.version
95
- };
96
- if (this.context.req.header("X-Inertia")) {
97
- this.context.header("X-Inertia", "true");
98
- this.context.header("Vary", "Accept");
99
- return this.context.json(page);
100
- }
101
- const view = this.context.get("view");
102
- const rootView = this.config.rootView ?? "app";
103
- if (!view) {
104
- throw new Error("OrbitPrism is required for the initial page load in OrbitIon");
248
+ const resolveProps = async (p) => {
249
+ const partialData = this.context.req.header("X-Inertia-Partial-Data");
250
+ const partialExcept = this.context.req.header("X-Inertia-Partial-Except");
251
+ const partialComponent = this.context.req.header("X-Inertia-Partial-Component");
252
+ const resetKeys = this.context.req.header("X-Inertia-Reset");
253
+ const isPartial = partialComponent === component;
254
+ const only = partialData && isPartial ? partialData.split(",") : [];
255
+ const except = partialExcept && isPartial ? partialExcept.split(",") : [];
256
+ const resetKeyList = resetKeys ? resetKeys.split(",") : [];
257
+ const resolved = {};
258
+ const deferredGroups2 = {};
259
+ const mergedKeys2 = [];
260
+ for (const [key, value] of Object.entries(p)) {
261
+ if (only.length > 0 && !only.includes(key)) {
262
+ continue;
263
+ }
264
+ if (except.length > 0 && except.includes(key)) {
265
+ continue;
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
+ }
294
+ if (typeof value === "function") {
295
+ const propStart = performance.now();
296
+ try {
297
+ resolved[key] = await value();
298
+ this.log("debug", `[InertiaService] Resolved lazy prop: ${key}`, {
299
+ duration: `${(performance.now() - propStart).toFixed(2)}ms`
300
+ });
301
+ } catch (err) {
302
+ this.log("error", `[InertiaService] Failed to resolve lazy prop: ${key}`, { err });
303
+ throw new InertiaDataError(`Failed to resolve lazy prop: ${key}`, { cause: err });
304
+ }
305
+ } else {
306
+ resolved[key] = value;
307
+ }
308
+ }
309
+ return { resolved, deferredGroups: deferredGroups2, mergedKeys: mergedKeys2 };
310
+ };
311
+ const resolveVersion = async () => {
312
+ if (typeof this.config.version === "function") {
313
+ return await this.config.version();
314
+ }
315
+ return this.config.version;
316
+ };
317
+ const version = await resolveVersion();
318
+ const getMergedProps = () => {
319
+ const propsToMerge = props ?? {};
320
+ if (Object.keys(propsToMerge).length === 0) {
321
+ return this.sharedProps;
322
+ }
323
+ return { ...this.sharedProps, ...propsToMerge };
324
+ };
325
+ const {
326
+ resolved: resolvedPropsData,
327
+ deferredGroups,
328
+ mergedKeys
329
+ } = await resolveProps(getMergedProps());
330
+ const page = {
331
+ component,
332
+ props: resolvedPropsData,
333
+ url: pageUrl,
334
+ version
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
+ }
351
+ let pageJson;
352
+ try {
353
+ pageJson = JSON.stringify(page);
354
+ } catch (error) {
355
+ throw new InertiaDataError(
356
+ `Inertia page serialization failed for component: ${component}`,
357
+ {
358
+ cause: error
359
+ }
360
+ );
361
+ }
362
+ let response;
363
+ if (isInertiaRequest) {
364
+ const clientVersion = this.context.req.header("X-Inertia-Version");
365
+ if (version && clientVersion && clientVersion !== version) {
366
+ this.context.header("X-Inertia-Location", pageUrl);
367
+ return new Response("", { status: 409 });
368
+ }
369
+ this.context.header("X-Inertia", "true");
370
+ this.context.header("Vary", "Accept");
371
+ response = this.context.json(page, status);
372
+ } else {
373
+ const view = this.context.get("view");
374
+ const rootView = this.config.rootView ?? "app";
375
+ if (!view) {
376
+ throw new InertiaConfigError(
377
+ "ViewService (OrbitPrism) is required for initial page load but was not found in context.",
378
+ { hint: "Ensure OrbitPrism is loaded before OrbitIon in your orbit configuration" }
379
+ );
380
+ }
381
+ const isDev = process.env.NODE_ENV !== "production";
382
+ let ssrData = { head: [], body: "" };
383
+ if (this.config.ssr?.enabled && this.config.ssr.render) {
384
+ try {
385
+ ssrData = await this.config.ssr.render(page);
386
+ } catch (error) {
387
+ this.log("error", "[InertiaService] SSR rendering failed", { error });
388
+ }
389
+ }
390
+ response = this.context.html(
391
+ view.render(
392
+ rootView,
393
+ {
394
+ ...rootVars,
395
+ page: this.escapeForSingleQuotedHtmlAttribute(pageJson),
396
+ isDev,
397
+ ssrHead: ssrData.head.join("\n"),
398
+ ssrBody: ssrData.body
399
+ },
400
+ { layout: "" }
401
+ ),
402
+ status
403
+ );
404
+ }
405
+ const duration = performance.now() - startTime;
406
+ this.log("info", "[InertiaService] Render complete", {
407
+ component,
408
+ duration: `${duration.toFixed(2)}ms`,
409
+ isInertiaRequest,
410
+ status: status ?? 200
411
+ });
412
+ if (this.onRenderCallback) {
413
+ this.onRenderCallback({
414
+ component,
415
+ duration,
416
+ isInertiaRequest,
417
+ propsCount: props ? Object.keys(props).length : 0,
418
+ timestamp: Date.now(),
419
+ status
420
+ });
421
+ }
422
+ return response;
423
+ } catch (error) {
424
+ const duration = performance.now() - startTime;
425
+ if (error instanceof InertiaError) {
426
+ this.log("error", "[InertiaService] Render failed", {
427
+ component,
428
+ duration: `${duration.toFixed(2)}ms`,
429
+ errorCode: error.code,
430
+ errorDetails: error.details
431
+ });
432
+ throw error;
433
+ }
434
+ this.log("error", "[InertiaService] Unexpected render error", {
435
+ component,
436
+ duration: `${duration.toFixed(2)}ms`,
437
+ error
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
+ }
475
+ return new Response("Inertia Render Error", { status: 500 });
105
476
  }
106
- const isDev = process.env.NODE_ENV !== "production";
107
- return this.context.html(
108
- view.render(
109
- rootView,
110
- {
111
- ...rootVars,
112
- page: this.escapeForSingleQuotedHtmlAttribute(JSON.stringify(page)),
113
- isDev
114
- },
115
- { layout: "" }
116
- )
117
- );
118
477
  }
119
- // ...
120
478
  /**
121
- * Share data with all Inertia responses
479
+ * Registers a piece of data to be shared with all Inertia responses for the current request.
122
480
  *
123
- * Shared props are merged with component-specific props on every render.
481
+ * Shared props are automatically merged with component-specific props during the render phase.
482
+ * This is the primary mechanism for providing global state like authentication details,
483
+ * flash messages, or configuration to the frontend.
124
484
  *
125
- * @param key - The prop key
126
- * @param value - The prop value
485
+ * @param key - Identifier for the shared prop.
486
+ * @param value - Value to share. Must be JSON serializable.
127
487
  *
128
488
  * @example
129
489
  * ```typescript
130
- * // In middleware
131
- * inertia.share('auth', { user: ctx.get('auth')?.user() })
132
- * inertia.share('flash', ctx.get('session')?.getFlash('message'))
490
+ * inertia.share('appName', 'Gravito Store');
491
+ * inertia.share('auth', { user: ctx.get('user') });
133
492
  * ```
134
493
  */
135
494
  share(key, value) {
136
495
  this.sharedProps[key] = value;
137
496
  }
138
497
  /**
139
- * Share multiple props at once
498
+ * Shares multiple props in a single operation.
499
+ *
500
+ * Merges the provided object into the existing shared props state. Existing keys
501
+ * with the same name will be overwritten.
140
502
  *
141
- * @param props - Object of props to share
503
+ * @param props - Object containing key-value pairs to merge into the shared state.
504
+ *
505
+ * @example
506
+ * ```typescript
507
+ * inertia.shareAll({
508
+ * version: '1.2.0',
509
+ * environment: 'production',
510
+ * features: ['chat', 'search']
511
+ * });
512
+ * ```
142
513
  */
143
514
  shareAll(props) {
144
- Object.assign(this.sharedProps, props);
515
+ this.sharedProps = { ...this.sharedProps, ...props };
145
516
  }
146
517
  /**
147
- * Get all shared props
518
+ * Returns a shallow copy of the current shared props.
519
+ *
520
+ * Useful for inspecting the shared state or for manual prop merging in advanced scenarios.
521
+ *
522
+ * @returns A dictionary containing all currently registered shared props.
148
523
  *
149
- * @returns A shallow copy of the shared props object.
524
+ * @example
525
+ * ```typescript
526
+ * const shared = inertia.getSharedProps();
527
+ * if (!shared.auth) {
528
+ * console.warn('Authentication data is missing from shared props');
529
+ * }
530
+ * ```
150
531
  */
151
532
  getSharedProps() {
152
533
  return { ...this.sharedProps };
153
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
+ }
154
623
  };
155
624
 
156
625
  // src/index.ts
157
626
  var OrbitIon = class {
627
+ /**
628
+ * Initializes the Orbit with custom configuration.
629
+ *
630
+ * @param options - Configuration overrides for the Inertia service.
631
+ */
158
632
  constructor(options = {}) {
159
633
  this.options = options;
160
634
  }
161
635
  /**
162
- * Install the inertia orbit into PlanetCore
636
+ * Registers the Inertia middleware and service factory into PlanetCore.
637
+ *
638
+ * This method is called automatically by Gravito during the boot process.
639
+ * It sets up the `InertiaService` for each request and attaches
640
+ * the `InertiaHelper` proxy to the context.
641
+ *
642
+ * @param core - The Gravito micro-kernel instance.
643
+ *
644
+ * @example
645
+ * ```typescript
646
+ * const ion = new OrbitIon({ version: '1.0' });
647
+ * ion.install(core);
648
+ * ```
163
649
  */
164
650
  install(core) {
165
651
  core.logger.info("\u{1F6F0}\uFE0F Orbit Inertia installed (Callable Interface)");
166
652
  const appVersion = this.options.version ?? core.config.get("APP_VERSION", "1.0.0");
167
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
+ }
168
680
  core.adapter.use("*", async (c, next) => {
169
681
  const service = new InertiaService(c, {
170
- version: String(appVersion),
171
- rootView
682
+ version: resolveAppVersion,
683
+ rootView,
684
+ ssr: this.options.ssr
172
685
  });
173
- const inertiaProxy = (component, props = {}, rootVars = {}) => {
174
- return service.render(component, props, rootVars);
686
+ const inertiaProxy = async (component, props = {}, rootVars = {}, status) => {
687
+ return await service.render(component, props, rootVars, status);
175
688
  };
176
689
  Object.assign(inertiaProxy, {
177
690
  share: service.share.bind(service),
178
691
  shareAll: service.shareAll.bind(service),
179
692
  getSharedProps: service.getSharedProps.bind(service),
180
693
  render: service.render.bind(service),
181
- // Also allow .render()
694
+ location: service.location.bind(service),
695
+ encryptHistory: service.encryptHistory.bind(service),
696
+ clearHistory: service.clearHistory.bind(service),
697
+ withErrors: service.withErrors.bind(service),
182
698
  service
183
- // Access to the raw service instance
184
699
  });
185
700
  c.set("inertia", inertiaProxy);
186
701
  return await next();
@@ -190,6 +705,10 @@ var OrbitIon = class {
190
705
  var index_default = OrbitIon;
191
706
  // Annotate the CommonJS export names for ESM import in node:
192
707
  0 && (module.exports = {
708
+ InertiaConfigError,
709
+ InertiaDataError,
710
+ InertiaError,
193
711
  InertiaService,
712
+ InertiaTemplateError,
194
713
  OrbitIon
195
714
  });