@dokebi/template-inertia 0.4.6 → 0.4.7

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/AI.md CHANGED
@@ -59,6 +59,7 @@ route.post('/orders', async (request, reply, page) => {
59
59
  - The Vue side is type-checked too: `dokebi app add --inertia` makes `npm run typecheck` run `vue-tsc` after `tsc` (`node typecheck-vue.mjs -p tsconfig.vue.json`), which checks the `<script>` and the `<template>` of every `.vue` file. Write pages as `<script setup lang="ts">` with `defineProps<{ … }>()`; the shared props (`$page.props`, `usePage().props`) are typed in `src/infra/inertia/page.d.ts` (add a key there for what `src/infra/inertia/shared.ts` returns), and a prop only some pages get with `usePage<{ … }>()`. A Vite build is not a Vue type check.
60
60
  - `redirect(request, reply, localUrl)`: 302 for GET/HEAD, 303 for mutations; an Inertia `#` navigation gets 409 + `X-Inertia-Redirect`.
61
61
  - `location(request, reply, url)`: a local or external http(s) navigation. Inertia requests get 409 + `X-Inertia-Location`.
62
+ - Both write the URL into a response header: characters outside printable ASCII (a Korean path or query value) are percent-encoded as UTF-8 for you, and what is already encoded (`%EA%B0%80`) stays as it is; text that is not well formed (a lone surrogate) is `INVALID_URL`.
62
63
 
63
64
  ### 3.1 deferred, merge and once props (D-0052)
64
65
 
package/dist/i18n.js CHANGED
@@ -267,7 +267,10 @@ export class I18n {
267
267
  const formats = this.#formatsOf(locale);
268
268
  let formatter = formats.numbers.get(format);
269
269
  if (!formatter) {
270
- const preset = format === 'default' ? {} : this.#settings.numberFormats[format];
270
+ // Own names only: the settings are parsed JSON, where `constructor` or `toString` would
271
+ // name an Object.prototype member and format with the default (audit R9-g13-7).
272
+ const presets = this.#settings.numberFormats;
273
+ const preset = format === 'default' ? {} : Object.hasOwn(presets, format) ? presets[format] : undefined;
271
274
  if (!preset)
272
275
  throw new PageI18nError('INVALID_FORMAT', 'Unknown number format');
273
276
  try {
@@ -294,7 +297,8 @@ export class I18n {
294
297
  const formats = this.#formatsOf(locale);
295
298
  let formatter = formats.dates.get(format);
296
299
  if (!formatter) {
297
- const preset = format === 'default' ? {} : this.#settings.dateFormats[format];
300
+ const presets = this.#settings.dateFormats;
301
+ const preset = format === 'default' ? {} : Object.hasOwn(presets, format) ? presets[format] : undefined;
298
302
  if (!preset)
299
303
  throw new PageI18nError('INVALID_FORMAT', 'Unknown date format');
300
304
  try {
package/dist/index.js CHANGED
@@ -1036,7 +1036,7 @@ class InertiaRuntime {
1036
1036
  }
1037
1037
  export const createPlugin = definePlugin({
1038
1038
  name: 'dokebi-template-inertia',
1039
- version: '0.4.6',
1039
+ version: '0.4.7',
1040
1040
  core: '^0.4.2',
1041
1041
  options,
1042
1042
  entry: parseEntry,
package/dist/responder.js CHANGED
@@ -41,6 +41,36 @@ function localUrl(value) {
41
41
  throw new InertiaUsageError('INVALID_URL', 'Expected a local absolute URL');
42
42
  return value;
43
43
  }
44
+ /**
45
+ * A checked redirect or location URL as a response header can carry it (audit R9-g13-3). Node
46
+ * refuses a header value with a character above U+00FF, and Fastify then destroys the socket:
47
+ * a redirect to a path with Korean text was a dropped connection, not a redirect.
48
+ * Printable ASCII (`%XX` included) is written as it is; anything else is percent-encoded as
49
+ * UTF-8, as a browser does with the same text in a link (an external URL through the URL
50
+ * parser, so its host becomes punycode). Text that is not well formed (a lone surrogate) is
51
+ * INVALID_URL.
52
+ */
53
+ const LONE_SURROGATE = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
54
+ function headerUrl(url) {
55
+ if (/^[\x21-\x7e]*$/.test(url))
56
+ return url;
57
+ if (LONE_SURROGATE.test(url))
58
+ throw new InertiaUsageError('INVALID_URL', 'Expected a URL of well-formed text');
59
+ return url.startsWith('/')
60
+ ? url.replace(/[^\x21-\x7e]+/g, (text) => encodeURIComponent(text))
61
+ : new URL(url).href;
62
+ }
63
+ /**
64
+ * The location of a version mismatch (409): the request's own path and query. A request target
65
+ * is client input: `//host/x` or `/\host/x` as a location would send the browser to that host
66
+ * (and was refused with INVALID_URL, a 500 any client could ask for; audit R9-g13-4), so the
67
+ * leading slashes become one and a backslash is written `%5C`. A target that is no path (`*`,
68
+ * an absolute form) goes to `/`.
69
+ */
70
+ function ownLocation(url) {
71
+ const path = url.replace(/^\/+/, '/').replace(/\\/g, '%5C');
72
+ return path.startsWith('/') ? path : '/';
73
+ }
44
74
  export function vary(reply) {
45
75
  const values = String(reply.getHeader('vary') ?? '')
46
76
  .split(',')
@@ -301,7 +331,7 @@ export class InertiaResponder {
301
331
  request.headers['x-inertia-version'] !== this.options.version) {
302
332
  return reply
303
333
  .code(409)
304
- .header('X-Inertia-Location', localUrl(request.url))
334
+ .header('X-Inertia-Location', ownLocation(request.url))
305
335
  .header('X-Inertia-Version', this.options.version)
306
336
  .send();
307
337
  }
@@ -553,11 +583,11 @@ export class InertiaResponder {
553
583
  }
554
584
  redirect(request, reply, url) {
555
585
  this.#check();
556
- localUrl(url);
586
+ const target = headerUrl(localUrl(url));
557
587
  vary(reply);
558
- if (request.headers['x-inertia'] === 'true' && url.includes('#'))
559
- return reply.code(409).header('X-Inertia-Redirect', url).send();
560
- return reply.redirect(url, ['GET', 'HEAD'].includes(request.method) ? 302 : 303);
588
+ if (request.headers['x-inertia'] === 'true' && target.includes('#'))
589
+ return reply.code(409).header('X-Inertia-Redirect', target).send();
590
+ return reply.redirect(target, ['GET', 'HEAD'].includes(request.method) ? 302 : 303);
561
591
  }
562
592
  location(request, reply, url) {
563
593
  this.#check();
@@ -568,10 +598,11 @@ export class InertiaResponder {
568
598
  !/^https?:$/.test(new URL(url).protocol) ||
569
599
  /[\r\n]/.test(url))
570
600
  throw new InertiaUsageError('INVALID_URL', 'Invalid location');
601
+ const target = headerUrl(url);
571
602
  vary(reply);
572
603
  return request.headers['x-inertia'] === 'true'
573
- ? reply.code(409).header('X-Inertia-Location', url).send()
574
- : reply.redirect(url, 302);
604
+ ? reply.code(409).header('X-Inertia-Location', target).send()
605
+ : reply.redirect(target, 302);
575
606
  }
576
607
  }
577
608
  const RESCUED = Symbol('rescued');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dokebi/template-inertia",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "Inertia v3 server adapter for Dokebi: page JSON, Vite manifest assets and optional server-side rendering.",
5
5
  "keywords": [
6
6
  "dokebi",
@@ -135,11 +135,11 @@
135
135
  ]
136
136
  },
137
137
  "devDependencies": {
138
- "@dokebi/i18n": "0.2.2",
139
- "@dokebi/session": "0.4.6",
140
- "@dokebi/web": "0.4.6",
141
- "@dokebi/websocket": "0.4.2",
142
- "@dokebi/websocket-hub": "0.4.2",
138
+ "@dokebi/i18n": "0.2.4",
139
+ "@dokebi/session": "0.4.7",
140
+ "@dokebi/web": "0.4.7",
141
+ "@dokebi/websocket": "0.4.3",
142
+ "@dokebi/websocket-hub": "0.4.3",
143
143
  "@inertiajs/vue3": "3.9.1",
144
144
  "vue": "3.5.44"
145
145
  },