Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c605384356 | ||
|
|
a7602c9659 | ||
|
|
8b5da6762c |
@@ -3,4 +3,8 @@
|
|||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
extends: ['..'],
|
extends: ['..'],
|
||||||
compatibilityDate: '2025-07-15',
|
compatibilityDate: '2025-07-15',
|
||||||
|
// An app without pages: vue-seo-layer's HTML sitemap pages would be its
|
||||||
|
// only routes and turn the router on, so every other path would answer
|
||||||
|
// 404. A site with pages keeps them.
|
||||||
|
seo: { htmlSitemap: false },
|
||||||
})
|
})
|
||||||
|
|||||||
33
README.md
33
README.md
@@ -3,13 +3,15 @@
|
|||||||
A Nuxt 4 layer that renders a menu of the SummerCMS menu plugin as an accessible site navigation.
|
A Nuxt 4 layer that renders a menu of the SummerCMS menu plugin as an accessible site navigation.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm add git+https://git.golem15.com/golem15/vue-menu-layer.git#v0.2.0
|
pnpm add git+https://git.golem15.com/golem15/vue-menu-layer.git#v0.5.0
|
||||||
```
|
```
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
The layer is consumed with `extends: ['vue-menu-layer']`. It reads one route of the menu plugin's anonymous API, `GET /_menu/api/v1/menus/{code}`, during SSR, so the first paint already has the menu, and renders it with `<MenuNav code="…">`. Editors manage menus in the SummerCMS admin; the site needs no menu data of its own.
|
The layer is consumed with `extends: ['vue-menu-layer']`. It reads one route of the menu plugin's anonymous API, `GET /_menu/api/v1/menus/{code}`, during SSR, so the first paint already has the menu, and renders it with `<MenuNav code="…">`. Editors manage menus in the SummerCMS admin; the site needs no menu data of its own.
|
||||||
|
|
||||||
|
The layer extends [vue-seo-layer](https://git.golem15.com/golem15/vue-seo-layer), which sets every page's head from the SummerCMS SEO plugin and serves the shared site cache purge route; an app that extends vue-menu-layer gets it with no extra entry in `extends`.
|
||||||
|
|
||||||
The layer is neutral: it ships no site colours, no site copy and no Nuxt modules. Its look comes from `--menu-*` CSS custom properties, which an app overrides, and every component can be replaced by a file of the same name in the app.
|
The layer is neutral: it ships no site colours, no site copy and no Nuxt modules. Its look comes from `--menu-*` CSS custom properties, which an app overrides, and every component can be replaced by a file of the same name in the app.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@@ -20,7 +22,7 @@ The layer is neutral: it ships no site colours, no site copy and no Nuxt modules
|
|||||||
- **Active state**: the entry whose URL path equals the current route path (decoded, query, hash and trailing slash ignored; an absolute URL only on the request host) gets `aria-current="page"` and `menu-nav__link--active`; its ancestors get `menu-nav__item--trail`.
|
- **Active state**: the entry whose URL path equals the current route path (decoded, query, hash and trailing slash ignored; an absolute URL only on the request host) gets `aria-current="page"` and `menu-nav__link--active`; its ancestors get `menu-nav__item--trail`.
|
||||||
- **Dividers**: `aria-hidden` list items, a horizontal line in panels and a short vertical line at the top level on the desktop.
|
- **Dividers**: `aria-hidden` list items, a horizontal line in panels and a short vertical line at the top level on the desktop.
|
||||||
- **Empty states**: a missing menu (404), a failed request or a menu without entries renders nothing, or the `#fallback` slot. There is never an empty `<nav>`.
|
- **Empty states**: a missing menu (404), a failed request or a menu without entries renders nothing, or the `#fallback` slot. There is never an empty `<nav>`.
|
||||||
- **Site cache purge**: the shared route `POST /api/__site__/cache/purge` drops the site's route cache when the backend saves, deletes or reorders a menu or menu entry (or a FAQ or post, through the other core layers). See [Site cache purge](#site-cache-purge).
|
- **Site cache purge**: vue-seo-layer's shared route `POST /api/__site__/cache/purge` drops the site's route cache when the backend saves, deletes or reorders a menu or menu entry (or a FAQ or post, through the other core layers). See [Site cache purge](#site-cache-purge).
|
||||||
- **Rate limits**: server-side fetches forward the visitor's `x-forwarded-for` (also on route-cached renders), so the API's per-client throttle sees visitors, not the Nuxt server.
|
- **Rate limits**: server-side fetches forward the visitor's `x-forwarded-for` (also on route-cached renders), so the API's per-client throttle sees visitors, not the Nuxt server.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@@ -66,31 +68,20 @@ Slots:
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `runtimeConfig.menu.apiBase` | `http://127.0.0.1:8096/_menu/api/v1` | `NUXT_MENU_API_BASE` (server only, used for SSR) |
|
| `runtimeConfig.menu.apiBase` | `http://127.0.0.1:8096/_menu/api/v1` | `NUXT_MENU_API_BASE` (server only, used for SSR) |
|
||||||
| `runtimeConfig.public.menu.apiBase` | `/_menu/api/v1` | `NUXT_PUBLIC_MENU_API_BASE` (browser, same origin) |
|
| `runtimeConfig.public.menu.apiBase` | `/_menu/api/v1` | `NUXT_PUBLIC_MENU_API_BASE` (browser, same origin) |
|
||||||
| `runtimeConfig.cachePurge.token` | `''` (route off) | `NUXT_CACHE_PURGE_TOKEN` (server only; at least 32 characters) |
|
|
||||||
| `runtimeConfig.cachePurge.allowProxied` | `false` | `NUXT_CACHE_PURGE_ALLOW_PROXIED` (server only) |
|
The purge settings (`runtimeConfig.cachePurge.token`, `NUXT_CACHE_PURGE_TOKEN`; `cachePurge.allowProxied`, `NUXT_CACHE_PURGE_ALLOW_PROXIED`; the deprecated `siteCache.*` names) and the SEO options (`seo.*`, `NUXT_SEO_API_BASE` and the rest) are vue-seo-layer's; see its README.
|
||||||
| `runtimeConfig.siteCache.purgeToken` | `''` | Deprecated (one release): `NUXT_SITE_CACHE_PURGE_TOKEN`, the earlier name of `cachePurge.token`, read when that is empty |
|
|
||||||
| `runtimeConfig.siteCache.allowProxied` | `false` | Deprecated (one release): `NUXT_SITE_CACHE_ALLOW_PROXIED`, the earlier name of `cachePurge.allowProxied` |
|
|
||||||
|
|
||||||
The base URLs always come from runtime config, never from request input. In production the web server routes `/_menu/` to the SummerCMS binary.
|
The base URLs always come from runtime config, never from request input. In production the web server routes `/_menu/` to the SummerCMS binary.
|
||||||
|
|
||||||
## Site cache purge
|
## Site cache purge
|
||||||
|
|
||||||
A site that route-caches its pages (for example `routeRules: { '/**': { swr: 300 } }`) shows an admin edit only when the cache window ends. The layer ships the shared purge route that the SummerCMS backend calls after an admin save commits:
|
The purge route `POST /api/__site__/cache/purge` that the SummerCMS backend calls after an admin save commits lives in vue-seo-layer: one copy for every layer, which also empties the SEO layer's resolve cache. Its contract is unchanged (`Authorization: Bearer <NUXT_CACHE_PURGE_TOKEN>`, body `{"kind":"<kind>","slug":"<slug>"}`, answer `{"purged": <n>}`, 404 without a token of at least 32 characters, 401 for a wrong token, proxied requests refused unless `NUXT_CACHE_PURGE_ALLOW_PROXIED=true`). Set the token and the backend's `site_cache.purge_url` as vue-seo-layer's README describes.
|
||||||
|
|
||||||
```
|
### Upgrading from 0.4
|
||||||
POST /api/__site__/cache/purge
|
|
||||||
Authorization: Bearer <NUXT_CACHE_PURGE_TOKEN>
|
|
||||||
{"kind":"menu_entry","slug":"partition:3"}
|
|
||||||
→ {"purged": <n>}
|
|
||||||
```
|
|
||||||
|
|
||||||
- It removes every entry of Nitro's route cache (pages, their `_payload.json` and feeds) and logs `[site-cache] purge: …` with the kind and slug.
|
- The layer extends vue-seo-layer. Nothing is needed in the app's `extends`, but every page now resolves through the SEO plugin (`GET /_seo/api/v1/resolve`) and takes its head from the answer; set `NUXT_SEO_API_BASE` when the backend is not on `http://127.0.0.1:8096`, or `seo: { cms: false }` for a site without the SEO plugin.
|
||||||
- Set `NUXT_CACHE_PURGE_TOKEN` to the backend's `site_cache.purge_token` (at least 32 characters) and point the backend's `site_cache.purge_url` at `http://127.0.0.1:<port>/api/__site__/cache/purge`. Without a token of that length the route answers 404.
|
- `server/api/__site__/cache/purge.post.ts`, `server/lib/siteCachePurge.ts` and `server/plugins/site-cache-purge.ts` are removed from this layer: the same route, library and plugin come from vue-seo-layer, with the same environment variables. An app that imported `server/lib/siteCachePurge.ts` from this layer imports it from vue-seo-layer.
|
||||||
- It answers 404 to any request that came through a reverse proxy (`X-Forwarded-For`, `X-Real-IP` or `Forwarded`), so only the backend on the same machine reaches it, and 401 to a missing or wrong bearer token (compared in constant time).
|
- **HTML sitemap pages.** The layer brings vue-seo-layer's HTML sitemap pages (pl `/mapa-strony`, en `/sitemap` by default, set in `seo.htmlSitemap.paths`). An app that renders its pages through a root catch-all (`pages/[...slug].vue`, as with the pages plugin) gets none, so a CMS page at such a path is not shadowed, unless it sets `seo.htmlSitemap.overCatchAll: true`; an app with its own page at such a path keeps its page; `seo: { htmlSitemap: false }` turns them off. An app without pages of its own (only `app.vue`) gets none and needs no setting.
|
||||||
- Behind a TLS-terminating proxy, for a backend that reaches the site at an https `site_cache.purge_url` on another host, the proxy adds those headers. Set `NUXT_CACHE_PURGE_ALLOW_PROXIED=true` (`runtimeConfig.cachePurge.allowProxied`, default `false`) and the route accepts proxied requests; the bearer token is then the only gate, so keep it long and secret. Unset, proxied requests stay refused.
|
|
||||||
- The layer adds the route rule `'/api/__site__/**': { cache: false }`; without it, a site-wide `swr` rule would answer the purge from the cache.
|
|
||||||
- vue-journal-layer, vue-menu-layer and vue-faq-layer ship the same `server/lib/siteCachePurge.ts`, `server/api/__site__/cache/purge.post.ts` and `server/plugins/site-cache-purge.ts` byte for byte (a test pins their sha256), so a site that extends several of them gets one route, whichever copy Nitro keeps.
|
|
||||||
- Never put a secret in a `NUXT_SITE_*` or `NUXT_PUBLIC_SITE_*` variable: nuxt-site-config (in a site that also uses `@nuxtjs/seo`, for example through vue-journal-layer) copies every such variable into the site config, which is published in every page's payload. The Nitro plugin `server/plugins/site-cache-purge.ts` removes `cachePurgeToken` and `cacheAllowProxied` from that site config on its `site-config:init` hook, whatever names the site uses, and logs one `[site-cache]` deprecation warning per process when a setting comes from an old name (variable names only, never a value). Without nuxt-site-config the plugin does nothing.
|
|
||||||
|
|
||||||
### Upgrading from 0.3
|
### Upgrading from 0.3
|
||||||
|
|
||||||
@@ -151,7 +142,7 @@ Commit, tag `vX.Y.Z` (annotated) and push the tag; apps pin `#vX.Y.Z` in their d
|
|||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm test # node --test: link rules, client, rendered NavItem markup, site cache purge
|
pnpm test # node --test: link rules, client, rendered NavItem markup
|
||||||
pnpm build:playground && pnpm smoke:ssr # SSR of the playground against testing/fixture-api.mjs
|
pnpm build:playground && pnpm smoke:ssr # SSR of the playground against testing/fixture-api.mjs
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
//
|
//
|
||||||
// Every path below is absolute and built from this file's directory: a
|
// Every path below is absolute and built from this file's directory: a
|
||||||
// relative or ~ path in a layer config resolves against the consuming
|
// relative or ~ path in a layer config resolves against the consuming
|
||||||
// project, not the layer. The layer declares no Nuxt modules.
|
// project, not the layer. The layer declares no Nuxt modules; it extends
|
||||||
|
// vue-seo-layer, which owns the page head and the shared site cache purge
|
||||||
|
// route POST /api/__site__/cache/purge.
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
const currentDir = fileURLToPath(new URL('.', import.meta.url))
|
const currentDir = fileURLToPath(new URL('.', import.meta.url))
|
||||||
@@ -13,6 +15,9 @@ export default defineNuxtConfig({
|
|||||||
compatibilityDate: '2025-07-15',
|
compatibilityDate: '2025-07-15',
|
||||||
devtools: { enabled: false },
|
devtools: { enabled: false },
|
||||||
|
|
||||||
|
// Resolved from this layer's own node_modules.
|
||||||
|
extends: ['vue-seo-layer'],
|
||||||
|
|
||||||
// Tokens first, then the neutral styles that use them. The app's own css
|
// Tokens first, then the neutral styles that use them. The app's own css
|
||||||
// array loads after these, so its token overrides win.
|
// array loads after these, so its token overrides win.
|
||||||
css: [`${currentDir}app/assets/css/menu-tokens.css`, `${currentDir}app/assets/css/menu-base.css`],
|
css: [`${currentDir}app/assets/css/menu-tokens.css`, `${currentDir}app/assets/css/menu-base.css`],
|
||||||
@@ -23,31 +28,6 @@ export default defineNuxtConfig({
|
|||||||
// comes from request input.
|
// comes from request input.
|
||||||
apiBase: 'http://127.0.0.1:8096/_menu/api/v1',
|
apiBase: 'http://127.0.0.1:8096/_menu/api/v1',
|
||||||
},
|
},
|
||||||
cachePurge: {
|
|
||||||
// Server-only bearer token of the shared site cache purge route
|
|
||||||
// POST /api/__site__/cache/purge (NUXT_CACHE_PURGE_TOKEN), the
|
|
||||||
// backend's site_cache.purge_token. Shorter than 32 characters, the
|
|
||||||
// route answers 404. Not under NUXT_SITE_: nuxt-site-config copies
|
|
||||||
// every NUXT_SITE_* variable into the site config, which is published
|
|
||||||
// in every page's payload.
|
|
||||||
token: '',
|
|
||||||
// NUXT_CACHE_PURGE_ALLOW_PROXIED: set it to true when the backend
|
|
||||||
// reaches the site through a TLS-terminating proxy (an https
|
|
||||||
// site_cache.purge_url on another host), which adds X-Forwarded-For,
|
|
||||||
// X-Real-IP or Forwarded. The route then accepts proxied requests and
|
|
||||||
// the bearer token is the only gate; unset, it answers 404 to any
|
|
||||||
// proxied request.
|
|
||||||
allowProxied: false,
|
|
||||||
},
|
|
||||||
// Deprecated (one release): the earlier names of cachePurge.token
|
|
||||||
// (NUXT_SITE_CACHE_PURGE_TOKEN) and cachePurge.allowProxied
|
|
||||||
// (NUXT_SITE_CACHE_ALLOW_PROXIED), read when the new ones are unset. The
|
|
||||||
// layer's server/plugins/site-cache-purge.ts strips them from the site
|
|
||||||
// config and logs a deprecation warning.
|
|
||||||
siteCache: {
|
|
||||||
purgeToken: '',
|
|
||||||
allowProxied: false,
|
|
||||||
},
|
|
||||||
public: {
|
public: {
|
||||||
menu: {
|
menu: {
|
||||||
// Same-origin base for browser fetches (NUXT_PUBLIC_MENU_API_BASE).
|
// Same-origin base for browser fetches (NUXT_PUBLIC_MENU_API_BASE).
|
||||||
@@ -55,12 +35,4 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// The shared site routes are never route-cached. Nitro wraps every
|
|
||||||
// handler matching an app rule such as '/**': { swr } in its cached
|
|
||||||
// handler, which keys by path, does not skip POST and hides request
|
|
||||||
// headers, so the purge route would answer from the cache without running.
|
|
||||||
routeRules: {
|
|
||||||
'/api/__site__/**': { cache: false },
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vue-menu-layer",
|
"name": "vue-menu-layer",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./nuxt.config.ts",
|
"main": "./nuxt.config.ts",
|
||||||
@@ -14,6 +14,9 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.18"
|
"node": ">=22.18"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vue-seo-layer": "git+https://git.golem15.com/golem15/vue-seo-layer.git#v0.2.0"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"nuxt": "4.4.8"
|
"nuxt": "4.4.8"
|
||||||
},
|
},
|
||||||
|
|||||||
206
pnpm-lock.yaml
generated
206
pnpm-lock.yaml
generated
@@ -7,10 +7,14 @@ settings:
|
|||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
|
dependencies:
|
||||||
|
vue-seo-layer:
|
||||||
|
specifier: git+https://git.golem15.com/golem15/vue-seo-layer.git#v0.2.0
|
||||||
|
version: git+https://git.golem15.com/golem15/vue-seo-layer.git#3868f3d74dcebb7a35af43f1defcd68cfe2933b7(nuxt@4.4.8(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.133.0)(@vue/compiler-sfc@3.5.43)(cac@6.7.14)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.2.2))(magicast@0.5.5)(rollup-plugin-visualizer@7.1.1(rollup@4.64.2))(rollup@4.64.2)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.51.2)(typescript@5.9.3)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2)))
|
||||||
devDependencies:
|
devDependencies:
|
||||||
nuxt:
|
nuxt:
|
||||||
specifier: 4.4.8
|
specifier: 4.4.8
|
||||||
version: 4.4.8(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.133.0)(@vue/compiler-sfc@3.5.43)(cac@6.7.14)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(magicast@0.5.5)(rollup-plugin-visualizer@7.1.1(rollup@4.64.2))(rollup@4.64.2)(srvx@0.11.22)(terser@5.51.2)(typescript@5.9.3)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))
|
version: 4.4.8(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.133.0)(@vue/compiler-sfc@3.5.43)(cac@6.7.14)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.2.2))(magicast@0.5.5)(rollup-plugin-visualizer@7.1.1(rollup@4.64.2))(rollup@4.64.2)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.51.2)(typescript@5.9.3)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))
|
||||||
typescript:
|
typescript:
|
||||||
specifier: 5.9.3
|
specifier: 5.9.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
@@ -3256,6 +3260,13 @@ packages:
|
|||||||
vite:
|
vite:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
vue-seo-layer@git+https://git.golem15.com/golem15/vue-seo-layer.git#3868f3d74dcebb7a35af43f1defcd68cfe2933b7:
|
||||||
|
resolution: {commit: 3868f3d74dcebb7a35af43f1defcd68cfe2933b7, repo: https://git.golem15.com/golem15/vue-seo-layer.git, type: git}
|
||||||
|
version: 0.2.0
|
||||||
|
engines: {node: '>=22.18'}
|
||||||
|
peerDependencies:
|
||||||
|
nuxt: 4.4.8
|
||||||
|
|
||||||
vue@3.5.35:
|
vue@3.5.35:
|
||||||
resolution: {integrity: sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==}
|
resolution: {integrity: sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3350,20 +3361,20 @@ snapshots:
|
|||||||
|
|
||||||
'@babel/compat-data@7.29.7': {}
|
'@babel/compat-data@7.29.7': {}
|
||||||
|
|
||||||
'@babel/core@7.29.7':
|
'@babel/core@7.29.7(supports-color@10.2.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.29.7
|
'@babel/code-frame': 7.29.7
|
||||||
'@babel/generator': 7.29.8
|
'@babel/generator': 7.29.8
|
||||||
'@babel/helper-compilation-targets': 7.29.7
|
'@babel/helper-compilation-targets': 7.29.7
|
||||||
'@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
|
'@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
|
||||||
'@babel/helpers': 7.29.10
|
'@babel/helpers': 7.29.10
|
||||||
'@babel/parser': 7.29.9
|
'@babel/parser': 7.29.9
|
||||||
'@babel/template': 7.29.7
|
'@babel/template': 7.29.7
|
||||||
'@babel/traverse': 7.29.10
|
'@babel/traverse': 7.29.10(supports-color@10.2.2)
|
||||||
'@babel/types': 7.29.8
|
'@babel/types': 7.29.8
|
||||||
'@jridgewell/remapping': 2.3.5
|
'@jridgewell/remapping': 2.3.5
|
||||||
convert-source-map: 2.0.0
|
convert-source-map: 2.0.0
|
||||||
debug: 4.4.3
|
debug: 4.4.3(supports-color@10.2.2)
|
||||||
gensync: 1.0.0-beta.2
|
gensync: 1.0.0-beta.2
|
||||||
json5: 2.2.3
|
json5: 2.2.3
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
@@ -3390,41 +3401,41 @@ snapshots:
|
|||||||
lru-cache: 5.1.1
|
lru-cache: 5.1.1
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
|
|
||||||
'@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)':
|
'@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7
|
'@babel/core': 7.29.7(supports-color@10.2.2)
|
||||||
'@babel/helper-annotate-as-pure': 7.29.7
|
'@babel/helper-annotate-as-pure': 7.29.7
|
||||||
'@babel/helper-member-expression-to-functions': 7.29.7
|
'@babel/helper-member-expression-to-functions': 7.29.7(supports-color@10.2.2)
|
||||||
'@babel/helper-optimise-call-expression': 7.29.7
|
'@babel/helper-optimise-call-expression': 7.29.7
|
||||||
'@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7)
|
'@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
|
||||||
'@babel/helper-skip-transparent-expression-wrappers': 7.29.7
|
'@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@10.2.2)
|
||||||
'@babel/traverse': 7.29.10
|
'@babel/traverse': 7.29.10(supports-color@10.2.2)
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@babel/helper-globals@7.29.7': {}
|
'@babel/helper-globals@7.29.7': {}
|
||||||
|
|
||||||
'@babel/helper-member-expression-to-functions@7.29.7':
|
'@babel/helper-member-expression-to-functions@7.29.7(supports-color@10.2.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/traverse': 7.29.10
|
'@babel/traverse': 7.29.10(supports-color@10.2.2)
|
||||||
'@babel/types': 7.29.8
|
'@babel/types': 7.29.8
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@babel/helper-module-imports@7.29.7':
|
'@babel/helper-module-imports@7.29.7(supports-color@10.2.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/traverse': 7.29.10
|
'@babel/traverse': 7.29.10(supports-color@10.2.2)
|
||||||
'@babel/types': 7.29.8
|
'@babel/types': 7.29.8
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
|
'@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7
|
'@babel/core': 7.29.7(supports-color@10.2.2)
|
||||||
'@babel/helper-module-imports': 7.29.7
|
'@babel/helper-module-imports': 7.29.7(supports-color@10.2.2)
|
||||||
'@babel/helper-validator-identifier': 7.29.7
|
'@babel/helper-validator-identifier': 7.29.7
|
||||||
'@babel/traverse': 7.29.10
|
'@babel/traverse': 7.29.10(supports-color@10.2.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -3434,18 +3445,18 @@ snapshots:
|
|||||||
|
|
||||||
'@babel/helper-plugin-utils@7.29.7': {}
|
'@babel/helper-plugin-utils@7.29.7': {}
|
||||||
|
|
||||||
'@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)':
|
'@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7
|
'@babel/core': 7.29.7(supports-color@10.2.2)
|
||||||
'@babel/helper-member-expression-to-functions': 7.29.7
|
'@babel/helper-member-expression-to-functions': 7.29.7(supports-color@10.2.2)
|
||||||
'@babel/helper-optimise-call-expression': 7.29.7
|
'@babel/helper-optimise-call-expression': 7.29.7
|
||||||
'@babel/traverse': 7.29.10
|
'@babel/traverse': 7.29.10(supports-color@10.2.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@babel/helper-skip-transparent-expression-wrappers@7.29.7':
|
'@babel/helper-skip-transparent-expression-wrappers@7.29.7(supports-color@10.2.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/traverse': 7.29.10
|
'@babel/traverse': 7.29.10(supports-color@10.2.2)
|
||||||
'@babel/types': 7.29.8
|
'@babel/types': 7.29.8
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -3473,24 +3484,24 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 8.0.6
|
'@babel/types': 8.0.6
|
||||||
|
|
||||||
'@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)':
|
'@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7
|
'@babel/core': 7.29.7(supports-color@10.2.2)
|
||||||
'@babel/helper-plugin-utils': 7.29.7
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
'@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)':
|
'@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7
|
'@babel/core': 7.29.7(supports-color@10.2.2)
|
||||||
'@babel/helper-plugin-utils': 7.29.7
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
|
|
||||||
'@babel/plugin-transform-typescript@7.29.9(@babel/core@7.29.7)':
|
'@babel/plugin-transform-typescript@7.29.9(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7
|
'@babel/core': 7.29.7(supports-color@10.2.2)
|
||||||
'@babel/helper-annotate-as-pure': 7.29.7
|
'@babel/helper-annotate-as-pure': 7.29.7
|
||||||
'@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7)
|
'@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
|
||||||
'@babel/helper-plugin-utils': 7.29.7
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
'@babel/helper-skip-transparent-expression-wrappers': 7.29.7
|
'@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@10.2.2)
|
||||||
'@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7)
|
'@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -3500,7 +3511,7 @@ snapshots:
|
|||||||
'@babel/parser': 7.29.9
|
'@babel/parser': 7.29.9
|
||||||
'@babel/types': 7.29.8
|
'@babel/types': 7.29.8
|
||||||
|
|
||||||
'@babel/traverse@7.29.10':
|
'@babel/traverse@7.29.10(supports-color@10.2.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.29.7
|
'@babel/code-frame': 7.29.7
|
||||||
'@babel/generator': 7.29.8
|
'@babel/generator': 7.29.8
|
||||||
@@ -3508,7 +3519,7 @@ snapshots:
|
|||||||
'@babel/parser': 7.29.9
|
'@babel/parser': 7.29.9
|
||||||
'@babel/template': 7.29.7
|
'@babel/template': 7.29.7
|
||||||
'@babel/types': 7.29.8
|
'@babel/types': 7.29.8
|
||||||
debug: 4.4.3
|
debug: 4.4.3(supports-color@10.2.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -3690,19 +3701,19 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.6.0
|
'@jridgewell/sourcemap-codec': 1.6.0
|
||||||
|
|
||||||
'@kwsites/file-exists@1.1.1':
|
'@kwsites/file-exists@1.1.1(supports-color@10.2.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3
|
debug: 4.4.3(supports-color@10.2.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@kwsites/promise-deferred@1.1.1': {}
|
'@kwsites/promise-deferred@1.1.1': {}
|
||||||
|
|
||||||
'@mapbox/node-pre-gyp@2.0.3':
|
'@mapbox/node-pre-gyp@2.0.3(supports-color@10.2.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
consola: 3.4.2
|
consola: 3.4.2
|
||||||
detect-libc: 2.1.2
|
detect-libc: 2.1.2
|
||||||
https-proxy-agent: 7.0.6
|
https-proxy-agent: 7.0.6(supports-color@10.2.2)
|
||||||
node-fetch: 2.7.0
|
node-fetch: 2.7.0
|
||||||
nopt: 8.1.0
|
nopt: 8.1.0
|
||||||
semver: 7.8.5
|
semver: 7.8.5
|
||||||
@@ -3733,7 +3744,7 @@ snapshots:
|
|||||||
'@nodelib/fs.scandir': 2.1.5
|
'@nodelib/fs.scandir': 2.1.5
|
||||||
fastq: 1.20.3
|
fastq: 1.20.3
|
||||||
|
|
||||||
'@nuxt/cli@3.37.0(@nuxt/schema@4.4.8)(cac@6.7.14)(magicast@0.5.5)':
|
'@nuxt/cli@3.37.0(@nuxt/schema@4.4.8)(cac@6.7.14)(magicast@0.5.5)(supports-color@10.2.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@bomb.sh/tab': 0.0.19(cac@6.7.14)(citty@0.2.2)
|
'@bomb.sh/tab': 0.0.19(cac@6.7.14)(citty@0.2.2)
|
||||||
'@clack/prompts': 1.8.1
|
'@clack/prompts': 1.8.1
|
||||||
@@ -3741,7 +3752,7 @@ snapshots:
|
|||||||
citty: 0.2.2
|
citty: 0.2.2
|
||||||
confbox: 0.2.4
|
confbox: 0.2.4
|
||||||
consola: 3.4.2
|
consola: 3.4.2
|
||||||
debug: 4.4.3
|
debug: 4.4.3(supports-color@10.2.2)
|
||||||
defu: 6.1.7
|
defu: 6.1.7
|
||||||
exsolve: 1.1.3
|
exsolve: 1.1.3
|
||||||
fuse.js: 7.5.0
|
fuse.js: 7.5.0
|
||||||
@@ -3802,7 +3813,7 @@ snapshots:
|
|||||||
pkg-types: 2.3.3
|
pkg-types: 2.3.3
|
||||||
semver: 7.8.5
|
semver: 7.8.5
|
||||||
|
|
||||||
'@nuxt/devtools@3.4.2(@nuxt/schema@4.4.8)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.133.0)(unplugin@3.4.0(esbuild@0.28.2)(rollup@4.64.2)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2)))(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))(vue@3.5.35(typescript@5.9.3))':
|
'@nuxt/devtools@3.4.2(@nuxt/schema@4.4.8)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.2.2))(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.133.0)(supports-color@10.2.2)(unplugin@3.4.0(esbuild@0.28.2)(rollup@4.64.2)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2)))(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))(vue@3.5.35(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nuxt/devtools-kit': 3.4.2(@nuxt/schema@4.4.8)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.133.0)(unplugin@3.4.0(esbuild@0.28.2)(rollup@4.64.2)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2)))(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))
|
'@nuxt/devtools-kit': 3.4.2(@nuxt/schema@4.4.8)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.133.0)(unplugin@3.4.0(esbuild@0.28.2)(rollup@4.64.2)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2)))(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))
|
||||||
'@nuxt/devtools-wizard': 3.4.2
|
'@nuxt/devtools-wizard': 3.4.2
|
||||||
@@ -3828,11 +3839,11 @@ snapshots:
|
|||||||
perfect-debounce: 2.1.0
|
perfect-debounce: 2.1.0
|
||||||
pkg-types: 2.3.3
|
pkg-types: 2.3.3
|
||||||
semver: 7.8.5
|
semver: 7.8.5
|
||||||
simple-git: 3.36.0
|
simple-git: 3.36.0(supports-color@10.2.2)
|
||||||
sirv: 3.0.2
|
sirv: 3.0.2
|
||||||
structured-clone-es: 2.0.1
|
structured-clone-es: 2.0.1
|
||||||
tinyglobby: 0.2.17
|
tinyglobby: 0.2.17
|
||||||
unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1)
|
unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))
|
||||||
vite: 7.3.7(jiti@2.7.0)(terser@5.51.2)
|
vite: 7.3.7(jiti@2.7.0)(terser@5.51.2)
|
||||||
vite-plugin-inspect: 11.4.1(@nuxt/kit@4.6.0(@nuxt/schema@4.4.8)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.133.0)(unplugin@3.4.0(esbuild@0.28.2)(rollup@4.64.2)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))))(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))
|
vite-plugin-inspect: 11.4.1(@nuxt/kit@4.6.0(@nuxt/schema@4.4.8)(confbox@0.3.1)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.133.0)(unplugin@3.4.0(esbuild@0.28.2)(rollup@4.64.2)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))))(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))
|
||||||
vite-plugin-vue-tracer: 1.5.0(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))(vue@3.5.35(typescript@5.9.3))
|
vite-plugin-vue-tracer: 1.5.0(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))(vue@3.5.35(typescript@5.9.3))
|
||||||
@@ -3930,7 +3941,7 @@ snapshots:
|
|||||||
- magic-string
|
- magic-string
|
||||||
- unplugin
|
- unplugin
|
||||||
|
|
||||||
'@nuxt/nitro-server@4.4.8(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(db0@0.3.4)(esbuild@0.28.2)(ioredis@5.11.1)(magicast@0.5.5)(nuxt@4.4.8(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.133.0)(@vue/compiler-sfc@3.5.43)(cac@6.7.14)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(magicast@0.5.5)(rollup-plugin-visualizer@7.1.1(rollup@4.64.2))(rollup@4.64.2)(srvx@0.11.22)(terser@5.51.2)(typescript@5.9.3)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2)))(oxc-parser@0.133.0)(rollup@4.64.2)(srvx@0.11.22)(typescript@5.9.3)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))':
|
'@nuxt/nitro-server@4.4.8(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(db0@0.3.4)(esbuild@0.28.2)(ioredis@5.11.1(supports-color@10.2.2))(magicast@0.5.5)(nuxt@4.4.8(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.133.0)(@vue/compiler-sfc@3.5.43)(cac@6.7.14)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.2.2))(magicast@0.5.5)(rollup-plugin-visualizer@7.1.1(rollup@4.64.2))(rollup@4.64.2)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.51.2)(typescript@5.9.3)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2)))(oxc-parser@0.133.0)(rollup@4.64.2)(srvx@0.11.22)(supports-color@10.2.2)(typescript@5.9.3)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nuxt/devalue': 2.0.2
|
'@nuxt/devalue': 2.0.2
|
||||||
'@nuxt/kit': 4.4.8(magicast@0.5.5)
|
'@nuxt/kit': 4.4.8(magicast@0.5.5)
|
||||||
@@ -3947,8 +3958,8 @@ snapshots:
|
|||||||
impound: 1.2.0(esbuild@0.28.2)(rollup@4.64.2)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))
|
impound: 1.2.0(esbuild@0.28.2)(rollup@4.64.2)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))
|
||||||
klona: 2.0.6
|
klona: 2.0.6
|
||||||
mocked-exports: 0.1.1
|
mocked-exports: 0.1.1
|
||||||
nitropack: 2.13.4(oxc-parser@0.133.0)(srvx@0.11.22)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))
|
nitropack: 2.13.4(oxc-parser@0.133.0)(srvx@0.11.22)(supports-color@10.2.2)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))
|
||||||
nuxt: 4.4.8(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.133.0)(@vue/compiler-sfc@3.5.43)(cac@6.7.14)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(magicast@0.5.5)(rollup-plugin-visualizer@7.1.1(rollup@4.64.2))(rollup@4.64.2)(srvx@0.11.22)(terser@5.51.2)(typescript@5.9.3)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))
|
nuxt: 4.4.8(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.133.0)(@vue/compiler-sfc@3.5.43)(cac@6.7.14)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.2.2))(magicast@0.5.5)(rollup-plugin-visualizer@7.1.1(rollup@4.64.2))(rollup@4.64.2)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.51.2)(typescript@5.9.3)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))
|
||||||
nypm: 0.6.10
|
nypm: 0.6.10
|
||||||
ohash: 2.0.12
|
ohash: 2.0.12
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
@@ -3956,12 +3967,12 @@ snapshots:
|
|||||||
std-env: 4.3.0
|
std-env: 4.3.0
|
||||||
ufo: 1.6.4
|
ufo: 1.6.4
|
||||||
unctx: 2.5.0
|
unctx: 2.5.0
|
||||||
unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1)
|
unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))
|
||||||
vue: 3.5.35(typescript@5.9.3)
|
vue: 3.5.35(typescript@5.9.3)
|
||||||
vue-bundle-renderer: 2.4.0
|
vue-bundle-renderer: 2.4.0
|
||||||
vue-devtools-stub: 0.1.0
|
vue-devtools-stub: 0.1.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7)
|
'@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@azure/app-configuration'
|
- '@azure/app-configuration'
|
||||||
- '@azure/cosmos'
|
- '@azure/cosmos'
|
||||||
@@ -4026,12 +4037,12 @@ snapshots:
|
|||||||
rc9: 3.1.0
|
rc9: 3.1.0
|
||||||
std-env: 4.3.0
|
std-env: 4.3.0
|
||||||
|
|
||||||
'@nuxt/vite-builder@4.4.8(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(magicast@0.5.5)(nuxt@4.4.8(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.133.0)(@vue/compiler-sfc@3.5.43)(cac@6.7.14)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(magicast@0.5.5)(rollup-plugin-visualizer@7.1.1(rollup@4.64.2))(rollup@4.64.2)(srvx@0.11.22)(terser@5.51.2)(typescript@5.9.3)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2)))(rollup-plugin-visualizer@7.1.1(rollup@4.64.2))(rollup@4.64.2)(terser@5.51.2)(typescript@5.9.3)(vue@3.5.35(typescript@5.9.3))':
|
'@nuxt/vite-builder@4.4.8(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(magicast@0.5.5)(nuxt@4.4.8(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.133.0)(@vue/compiler-sfc@3.5.43)(cac@6.7.14)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.2.2))(magicast@0.5.5)(rollup-plugin-visualizer@7.1.1(rollup@4.64.2))(rollup@4.64.2)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.51.2)(typescript@5.9.3)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2)))(rollup-plugin-visualizer@7.1.1(rollup@4.64.2))(rollup@4.64.2)(supports-color@10.2.2)(terser@5.51.2)(typescript@5.9.3)(vue@3.5.35(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nuxt/kit': 4.4.8(magicast@0.5.5)
|
'@nuxt/kit': 4.4.8(magicast@0.5.5)
|
||||||
'@rollup/plugin-replace': 6.0.3(rollup@4.64.2)
|
'@rollup/plugin-replace': 6.0.3(rollup@4.64.2)
|
||||||
'@vitejs/plugin-vue': 6.0.9(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))(vue@3.5.35(typescript@5.9.3))
|
'@vitejs/plugin-vue': 6.0.9(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))(vue@3.5.35(typescript@5.9.3))
|
||||||
'@vitejs/plugin-vue-jsx': 5.1.6(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))(vue@3.5.35(typescript@5.9.3))
|
'@vitejs/plugin-vue-jsx': 5.1.6(supports-color@10.2.2)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))(vue@3.5.35(typescript@5.9.3))
|
||||||
autoprefixer: 10.6.1(postcss@8.5.29)
|
autoprefixer: 10.6.1(postcss@8.5.29)
|
||||||
consola: 3.4.2
|
consola: 3.4.2
|
||||||
cssnano: 8.0.10(postcss@8.5.29)
|
cssnano: 8.0.10(postcss@8.5.29)
|
||||||
@@ -4044,7 +4055,7 @@ snapshots:
|
|||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
mlly: 1.8.2
|
mlly: 1.8.2
|
||||||
mocked-exports: 0.1.1
|
mocked-exports: 0.1.1
|
||||||
nuxt: 4.4.8(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.133.0)(@vue/compiler-sfc@3.5.43)(cac@6.7.14)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(magicast@0.5.5)(rollup-plugin-visualizer@7.1.1(rollup@4.64.2))(rollup@4.64.2)(srvx@0.11.22)(terser@5.51.2)(typescript@5.9.3)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))
|
nuxt: 4.4.8(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.133.0)(@vue/compiler-sfc@3.5.43)(cac@6.7.14)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.2.2))(magicast@0.5.5)(rollup-plugin-visualizer@7.1.1(rollup@4.64.2))(rollup@4.64.2)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.51.2)(typescript@5.9.3)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))
|
||||||
nypm: 0.6.10
|
nypm: 0.6.10
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
pkg-types: 2.3.3
|
pkg-types: 2.3.3
|
||||||
@@ -4053,13 +4064,14 @@ snapshots:
|
|||||||
std-env: 4.3.0
|
std-env: 4.3.0
|
||||||
ufo: 1.6.4
|
ufo: 1.6.4
|
||||||
unenv: 2.0.0-rc.24
|
unenv: 2.0.0-rc.24
|
||||||
|
unplugin: 2.3.11
|
||||||
vite: 7.3.7(jiti@2.7.0)(terser@5.51.2)
|
vite: 7.3.7(jiti@2.7.0)(terser@5.51.2)
|
||||||
vite-node: 5.3.0(jiti@2.7.0)(terser@5.51.2)
|
vite-node: 5.3.0(jiti@2.7.0)(terser@5.51.2)
|
||||||
vite-plugin-checker: 0.14.5(typescript@5.9.3)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))
|
vite-plugin-checker: 0.14.5(typescript@5.9.3)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))
|
||||||
vue: 3.5.35(typescript@5.9.3)
|
vue: 3.5.35(typescript@5.9.3)
|
||||||
vue-bundle-renderer: 2.4.0
|
vue-bundle-renderer: 2.4.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7)
|
'@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))
|
||||||
rollup-plugin-visualizer: 7.1.1(rollup@4.64.2)
|
rollup-plugin-visualizer: 7.1.1(rollup@4.64.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@biomejs/biome'
|
- '@biomejs/biome'
|
||||||
@@ -4467,9 +4479,9 @@ snapshots:
|
|||||||
unhead: 2.1.17
|
unhead: 2.1.17
|
||||||
vue: 3.5.35(typescript@5.9.3)
|
vue: 3.5.35(typescript@5.9.3)
|
||||||
|
|
||||||
'@vercel/nft@1.11.0(rollup@4.64.2)':
|
'@vercel/nft@1.11.0(rollup@4.64.2)(supports-color@10.2.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mapbox/node-pre-gyp': 2.0.3
|
'@mapbox/node-pre-gyp': 2.0.3(supports-color@10.2.2)
|
||||||
'@rollup/pluginutils': 5.4.0(rollup@4.64.2)
|
'@rollup/pluginutils': 5.4.0(rollup@4.64.2)
|
||||||
acorn: 8.19.0
|
acorn: 8.19.0
|
||||||
acorn-import-attributes: 1.9.5(acorn@8.19.0)
|
acorn-import-attributes: 1.9.5(acorn@8.19.0)
|
||||||
@@ -4486,13 +4498,13 @@ snapshots:
|
|||||||
- rollup
|
- rollup
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@vitejs/plugin-vue-jsx@5.1.6(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))(vue@3.5.35(typescript@5.9.3))':
|
'@vitejs/plugin-vue-jsx@5.1.6(supports-color@10.2.2)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))(vue@3.5.35(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7
|
'@babel/core': 7.29.7(supports-color@10.2.2)
|
||||||
'@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7)
|
'@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))
|
||||||
'@babel/plugin-transform-typescript': 7.29.9(@babel/core@7.29.7)
|
'@babel/plugin-transform-typescript': 7.29.9(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
|
||||||
'@rolldown/pluginutils': 1.0.1
|
'@rolldown/pluginutils': 1.0.1
|
||||||
'@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.7)
|
'@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
|
||||||
vite: 7.3.7(jiti@2.7.0)(terser@5.51.2)
|
vite: 7.3.7(jiti@2.7.0)(terser@5.51.2)
|
||||||
vue: 3.5.35(typescript@5.9.3)
|
vue: 3.5.35(typescript@5.9.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -4516,27 +4528,27 @@ snapshots:
|
|||||||
|
|
||||||
'@vue/babel-helper-vue-transform-on@2.0.1': {}
|
'@vue/babel-helper-vue-transform-on@2.0.1': {}
|
||||||
|
|
||||||
'@vue/babel-plugin-jsx@2.0.1(@babel/core@7.29.7)':
|
'@vue/babel-plugin-jsx@2.0.1(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-module-imports': 7.29.7
|
'@babel/helper-module-imports': 7.29.7(supports-color@10.2.2)
|
||||||
'@babel/helper-plugin-utils': 7.29.7
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
'@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7)
|
'@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))
|
||||||
'@babel/template': 7.29.7
|
'@babel/template': 7.29.7
|
||||||
'@babel/traverse': 7.29.10
|
'@babel/traverse': 7.29.10(supports-color@10.2.2)
|
||||||
'@babel/types': 7.29.8
|
'@babel/types': 7.29.8
|
||||||
'@vue/babel-helper-vue-transform-on': 2.0.1
|
'@vue/babel-helper-vue-transform-on': 2.0.1
|
||||||
'@vue/babel-plugin-resolve-type': 2.0.1(@babel/core@7.29.7)
|
'@vue/babel-plugin-resolve-type': 2.0.1(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)
|
||||||
'@vue/shared': 3.5.43
|
'@vue/shared': 3.5.43
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@babel/core': 7.29.7
|
'@babel/core': 7.29.7(supports-color@10.2.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@vue/babel-plugin-resolve-type@2.0.1(@babel/core@7.29.7)':
|
'@vue/babel-plugin-resolve-type@2.0.1(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.29.7
|
'@babel/code-frame': 7.29.7
|
||||||
'@babel/core': 7.29.7
|
'@babel/core': 7.29.7(supports-color@10.2.2)
|
||||||
'@babel/helper-module-imports': 7.29.7
|
'@babel/helper-module-imports': 7.29.7(supports-color@10.2.2)
|
||||||
'@babel/helper-plugin-utils': 7.29.7
|
'@babel/helper-plugin-utils': 7.29.7
|
||||||
'@babel/parser': 7.29.9
|
'@babel/parser': 7.29.9
|
||||||
'@vue/compiler-sfc': 3.5.43
|
'@vue/compiler-sfc': 3.5.43
|
||||||
@@ -4999,9 +5011,11 @@ snapshots:
|
|||||||
|
|
||||||
db0@0.3.4: {}
|
db0@0.3.4: {}
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3(supports-color@10.2.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
optionalDependencies:
|
||||||
|
supports-color: 10.2.2
|
||||||
|
|
||||||
deepmerge@4.3.1: {}
|
deepmerge@4.3.1: {}
|
||||||
|
|
||||||
@@ -5288,10 +5302,10 @@ snapshots:
|
|||||||
|
|
||||||
http-shutdown@1.2.2: {}
|
http-shutdown@1.2.2: {}
|
||||||
|
|
||||||
https-proxy-agent@7.0.6:
|
https-proxy-agent@7.0.6(supports-color@10.2.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 7.1.4
|
agent-base: 7.1.4
|
||||||
debug: 4.4.3
|
debug: 4.4.3(supports-color@10.2.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -5328,11 +5342,11 @@ snapshots:
|
|||||||
|
|
||||||
ini@4.1.1: {}
|
ini@4.1.1: {}
|
||||||
|
|
||||||
ioredis@5.11.1:
|
ioredis@5.11.1(supports-color@10.2.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ioredis/commands': 1.10.0
|
'@ioredis/commands': 1.10.0
|
||||||
cluster-key-slot: 1.1.1
|
cluster-key-slot: 1.1.1
|
||||||
debug: 4.4.3
|
debug: 4.4.3(supports-color@10.2.2)
|
||||||
denque: 2.1.0
|
denque: 2.1.0
|
||||||
redis-errors: 1.2.0
|
redis-errors: 1.2.0
|
||||||
redis-parser: 3.0.0
|
redis-parser: 3.0.0
|
||||||
@@ -5541,7 +5555,7 @@ snapshots:
|
|||||||
|
|
||||||
nanotar@0.3.0: {}
|
nanotar@0.3.0: {}
|
||||||
|
|
||||||
nitropack@2.13.4(oxc-parser@0.133.0)(srvx@0.11.22)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2)):
|
nitropack@2.13.4(oxc-parser@0.133.0)(srvx@0.11.22)(supports-color@10.2.2)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@cloudflare/kv-asset-handler': 0.4.2
|
'@cloudflare/kv-asset-handler': 0.4.2
|
||||||
'@rollup/plugin-alias': 6.0.0(rollup@4.64.2)
|
'@rollup/plugin-alias': 6.0.0(rollup@4.64.2)
|
||||||
@@ -5551,7 +5565,7 @@ snapshots:
|
|||||||
'@rollup/plugin-node-resolve': 16.0.3(rollup@4.64.2)
|
'@rollup/plugin-node-resolve': 16.0.3(rollup@4.64.2)
|
||||||
'@rollup/plugin-replace': 6.0.3(rollup@4.64.2)
|
'@rollup/plugin-replace': 6.0.3(rollup@4.64.2)
|
||||||
'@rollup/plugin-terser': 1.0.0(rollup@4.64.2)
|
'@rollup/plugin-terser': 1.0.0(rollup@4.64.2)
|
||||||
'@vercel/nft': 1.11.0(rollup@4.64.2)
|
'@vercel/nft': 1.11.0(rollup@4.64.2)(supports-color@10.2.2)
|
||||||
archiver: 7.0.1
|
archiver: 7.0.1
|
||||||
c12: 3.3.4(magicast@0.5.5)
|
c12: 3.3.4(magicast@0.5.5)
|
||||||
chokidar: 5.0.0
|
chokidar: 5.0.0
|
||||||
@@ -5575,7 +5589,7 @@ snapshots:
|
|||||||
h3: 1.15.11
|
h3: 1.15.11
|
||||||
hookable: 5.5.3
|
hookable: 5.5.3
|
||||||
httpxy: 0.5.5
|
httpxy: 0.5.5
|
||||||
ioredis: 5.11.1
|
ioredis: 5.11.1(supports-color@10.2.2)
|
||||||
jiti: 2.7.0
|
jiti: 2.7.0
|
||||||
klona: 2.0.6
|
klona: 2.0.6
|
||||||
knitwork: 1.3.0
|
knitwork: 1.3.0
|
||||||
@@ -5598,7 +5612,7 @@ snapshots:
|
|||||||
scule: 1.3.0
|
scule: 1.3.0
|
||||||
semver: 7.8.5
|
semver: 7.8.5
|
||||||
serve-placeholder: 2.0.2
|
serve-placeholder: 2.0.2
|
||||||
serve-static: 2.2.1
|
serve-static: 2.2.1(supports-color@10.2.2)
|
||||||
source-map: 0.7.6
|
source-map: 0.7.6
|
||||||
std-env: 4.3.0
|
std-env: 4.3.0
|
||||||
ufo: 1.6.4
|
ufo: 1.6.4
|
||||||
@@ -5608,7 +5622,7 @@ snapshots:
|
|||||||
unenv: 2.0.0-rc.24
|
unenv: 2.0.0-rc.24
|
||||||
unimport: 6.5.0(esbuild@0.28.2)(oxc-parser@0.133.0)(rollup@4.64.2)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))
|
unimport: 6.5.0(esbuild@0.28.2)(oxc-parser@0.133.0)(rollup@4.64.2)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))
|
||||||
unplugin-utils: 0.3.2
|
unplugin-utils: 0.3.2
|
||||||
unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1)
|
unstorage: 1.17.5(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2))
|
||||||
untyped: 2.0.0
|
untyped: 2.0.0
|
||||||
unwasm: 0.5.3
|
unwasm: 0.5.3
|
||||||
youch: 4.1.1
|
youch: 4.1.1
|
||||||
@@ -5689,16 +5703,16 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
boolbase: 1.0.0
|
boolbase: 1.0.0
|
||||||
|
|
||||||
nuxt@4.4.8(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.133.0)(@vue/compiler-sfc@3.5.43)(cac@6.7.14)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(magicast@0.5.5)(rollup-plugin-visualizer@7.1.1(rollup@4.64.2))(rollup@4.64.2)(srvx@0.11.22)(terser@5.51.2)(typescript@5.9.3)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2)):
|
nuxt@4.4.8(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.133.0)(@vue/compiler-sfc@3.5.43)(cac@6.7.14)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.2.2))(magicast@0.5.5)(rollup-plugin-visualizer@7.1.1(rollup@4.64.2))(rollup@4.64.2)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.51.2)(typescript@5.9.3)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@dxup/nuxt': 0.4.1(magicast@0.5.5)(typescript@5.9.3)
|
'@dxup/nuxt': 0.4.1(magicast@0.5.5)(typescript@5.9.3)
|
||||||
'@nuxt/cli': 3.37.0(@nuxt/schema@4.4.8)(cac@6.7.14)(magicast@0.5.5)
|
'@nuxt/cli': 3.37.0(@nuxt/schema@4.4.8)(cac@6.7.14)(magicast@0.5.5)(supports-color@10.2.2)
|
||||||
'@nuxt/devtools': 3.4.2(@nuxt/schema@4.4.8)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.133.0)(unplugin@3.4.0(esbuild@0.28.2)(rollup@4.64.2)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2)))(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))(vue@3.5.35(typescript@5.9.3))
|
'@nuxt/devtools': 3.4.2(@nuxt/schema@4.4.8)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.2.2))(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.133.0)(supports-color@10.2.2)(unplugin@3.4.0(esbuild@0.28.2)(rollup@4.64.2)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2)))(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))(vue@3.5.35(typescript@5.9.3))
|
||||||
'@nuxt/kit': 4.4.8(magicast@0.5.5)
|
'@nuxt/kit': 4.4.8(magicast@0.5.5)
|
||||||
'@nuxt/nitro-server': 4.4.8(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(db0@0.3.4)(esbuild@0.28.2)(ioredis@5.11.1)(magicast@0.5.5)(nuxt@4.4.8(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.133.0)(@vue/compiler-sfc@3.5.43)(cac@6.7.14)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(magicast@0.5.5)(rollup-plugin-visualizer@7.1.1(rollup@4.64.2))(rollup@4.64.2)(srvx@0.11.22)(terser@5.51.2)(typescript@5.9.3)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2)))(oxc-parser@0.133.0)(rollup@4.64.2)(srvx@0.11.22)(typescript@5.9.3)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))
|
'@nuxt/nitro-server': 4.4.8(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(db0@0.3.4)(esbuild@0.28.2)(ioredis@5.11.1(supports-color@10.2.2))(magicast@0.5.5)(nuxt@4.4.8(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.133.0)(@vue/compiler-sfc@3.5.43)(cac@6.7.14)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.2.2))(magicast@0.5.5)(rollup-plugin-visualizer@7.1.1(rollup@4.64.2))(rollup@4.64.2)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.51.2)(typescript@5.9.3)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2)))(oxc-parser@0.133.0)(rollup@4.64.2)(srvx@0.11.22)(supports-color@10.2.2)(typescript@5.9.3)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))
|
||||||
'@nuxt/schema': 4.4.8
|
'@nuxt/schema': 4.4.8
|
||||||
'@nuxt/telemetry': 2.9.1(@nuxt/kit@4.4.8(magicast@0.5.5))
|
'@nuxt/telemetry': 2.9.1(@nuxt/kit@4.4.8(magicast@0.5.5))
|
||||||
'@nuxt/vite-builder': 4.4.8(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(magicast@0.5.5)(nuxt@4.4.8(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@oxc-project/types@0.133.0)(@vue/compiler-sfc@3.5.43)(cac@6.7.14)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1)(magicast@0.5.5)(rollup-plugin-visualizer@7.1.1(rollup@4.64.2))(rollup@4.64.2)(srvx@0.11.22)(terser@5.51.2)(typescript@5.9.3)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2)))(rollup-plugin-visualizer@7.1.1(rollup@4.64.2))(rollup@4.64.2)(terser@5.51.2)(typescript@5.9.3)(vue@3.5.35(typescript@5.9.3))
|
'@nuxt/vite-builder': 4.4.8(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(magicast@0.5.5)(nuxt@4.4.8(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.133.0)(@vue/compiler-sfc@3.5.43)(cac@6.7.14)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.2.2))(magicast@0.5.5)(rollup-plugin-visualizer@7.1.1(rollup@4.64.2))(rollup@4.64.2)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.51.2)(typescript@5.9.3)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2)))(rollup-plugin-visualizer@7.1.1(rollup@4.64.2))(rollup@4.64.2)(supports-color@10.2.2)(terser@5.51.2)(typescript@5.9.3)(vue@3.5.35(typescript@5.9.3))
|
||||||
'@unhead/vue': 2.1.17(vue@3.5.35(typescript@5.9.3))
|
'@unhead/vue': 2.1.17(vue@3.5.35(typescript@5.9.3))
|
||||||
'@vue/shared': 3.5.43
|
'@vue/shared': 3.5.43
|
||||||
chokidar: 5.0.0
|
chokidar: 5.0.0
|
||||||
@@ -6276,9 +6290,9 @@ snapshots:
|
|||||||
|
|
||||||
semver@7.8.5: {}
|
semver@7.8.5: {}
|
||||||
|
|
||||||
send@1.2.1:
|
send@1.2.1(supports-color@10.2.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3
|
debug: 4.4.3(supports-color@10.2.2)
|
||||||
encodeurl: 2.0.0
|
encodeurl: 2.0.0
|
||||||
escape-html: 1.0.3
|
escape-html: 1.0.3
|
||||||
etag: 1.8.1
|
etag: 1.8.1
|
||||||
@@ -6300,12 +6314,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
defu: 6.1.7
|
defu: 6.1.7
|
||||||
|
|
||||||
serve-static@2.2.1:
|
serve-static@2.2.1(supports-color@10.2.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
encodeurl: 2.0.0
|
encodeurl: 2.0.0
|
||||||
escape-html: 1.0.3
|
escape-html: 1.0.3
|
||||||
parseurl: 1.3.3
|
parseurl: 1.3.3
|
||||||
send: 1.2.1
|
send: 1.2.1(supports-color@10.2.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -6323,13 +6337,13 @@ snapshots:
|
|||||||
|
|
||||||
signal-exit@4.1.0: {}
|
signal-exit@4.1.0: {}
|
||||||
|
|
||||||
simple-git@3.36.0:
|
simple-git@3.36.0(supports-color@10.2.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@kwsites/file-exists': 1.1.1
|
'@kwsites/file-exists': 1.1.1(supports-color@10.2.2)
|
||||||
'@kwsites/promise-deferred': 1.1.1
|
'@kwsites/promise-deferred': 1.1.1
|
||||||
'@simple-git/args-pathspec': 1.0.4
|
'@simple-git/args-pathspec': 1.0.4
|
||||||
'@simple-git/argv-parser': 1.1.1
|
'@simple-git/argv-parser': 1.1.1
|
||||||
debug: 4.4.3
|
debug: 4.4.3(supports-color@10.2.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -6600,7 +6614,7 @@ snapshots:
|
|||||||
escape-string-regexp: 5.0.0
|
escape-string-regexp: 5.0.0
|
||||||
ufo: 1.6.4
|
ufo: 1.6.4
|
||||||
|
|
||||||
unstorage@1.17.5(db0@0.3.4)(ioredis@5.11.1):
|
unstorage@1.17.5(db0@0.3.4)(ioredis@5.11.1(supports-color@10.2.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
anymatch: 3.1.3
|
anymatch: 3.1.3
|
||||||
chokidar: 5.0.0
|
chokidar: 5.0.0
|
||||||
@@ -6612,7 +6626,7 @@ snapshots:
|
|||||||
ufo: 1.6.4
|
ufo: 1.6.4
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
db0: 0.3.4
|
db0: 0.3.4
|
||||||
ioredis: 5.11.1
|
ioredis: 5.11.1(supports-color@10.2.2)
|
||||||
|
|
||||||
untun@0.2.2: {}
|
untun@0.2.2: {}
|
||||||
|
|
||||||
@@ -6765,6 +6779,10 @@ snapshots:
|
|||||||
- unloader
|
- unloader
|
||||||
- webpack
|
- webpack
|
||||||
|
|
||||||
|
vue-seo-layer@git+https://git.golem15.com/golem15/vue-seo-layer.git#3868f3d74dcebb7a35af43f1defcd68cfe2933b7(nuxt@4.4.8(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.133.0)(@vue/compiler-sfc@3.5.43)(cac@6.7.14)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.2.2))(magicast@0.5.5)(rollup-plugin-visualizer@7.1.1(rollup@4.64.2))(rollup@4.64.2)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.51.2)(typescript@5.9.3)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))):
|
||||||
|
dependencies:
|
||||||
|
nuxt: 4.4.8(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2)))(@oxc-project/types@0.133.0)(@vue/compiler-sfc@3.5.43)(cac@6.7.14)(confbox@0.3.1)(db0@0.3.4)(dotenv@17.4.2)(esbuild@0.28.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.2.2))(magicast@0.5.5)(rollup-plugin-visualizer@7.1.1(rollup@4.64.2))(rollup@4.64.2)(srvx@0.11.22)(supports-color@10.2.2)(terser@5.51.2)(typescript@5.9.3)(vite@7.3.7(jiti@2.7.0)(terser@5.51.2))
|
||||||
|
|
||||||
vue@3.5.35(typescript@5.9.3):
|
vue@3.5.35(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/compiler-dom': 3.5.35
|
'@vue/compiler-dom': 3.5.35
|
||||||
|
|||||||
48
server/api/__site__/cache/purge.post.ts
vendored
48
server/api/__site__/cache/purge.post.ts
vendored
@@ -1,48 +0,0 @@
|
|||||||
// POST /api/__site__/cache/purge: removes every Nitro route-cache entry
|
|
||||||
// (pages, _payload.json and feeds), so an admin save shows on the next page
|
|
||||||
// view. The backend calls it on loopback after the save commits, with
|
|
||||||
// `Authorization: Bearer <runtimeConfig.cachePurge.token>`
|
|
||||||
// (NUXT_CACHE_PURGE_TOKEN) and the body {"kind":"<kind>","slug":"<slug>"};
|
|
||||||
// the answer is {"purged":<n>}. The kind and slug only reach the log line.
|
|
||||||
//
|
|
||||||
// 404 without a token of at least 32 characters, and for any request that
|
|
||||||
// came through a reverse proxy (X-Forwarded-For, X-Real-IP or Forwarded)
|
|
||||||
// unless runtimeConfig.cachePurge.allowProxied
|
|
||||||
// (NUXT_CACHE_PURGE_ALLOW_PROXIED) is true or "true": a backend that reaches
|
|
||||||
// the site through a TLS-terminating proxy opts in, and the bearer token is
|
|
||||||
// then the only gate. 401 for a missing or wrong bearer token. The
|
|
||||||
// '/api/__site__/**' route rule keeps this route out of the route cache.
|
|
||||||
//
|
|
||||||
// Deprecated for one release: siteCache.purgeToken
|
|
||||||
// (NUXT_SITE_CACHE_PURGE_TOKEN) and siteCache.allowProxied
|
|
||||||
// (NUXT_SITE_CACHE_ALLOW_PROXIED), read when the new names are unset (see
|
|
||||||
// resolveSitePurgeConfig).
|
|
||||||
//
|
|
||||||
// Shared: several layers ship this file byte for byte, and Nitro keeps one
|
|
||||||
// handler per route and method.
|
|
||||||
|
|
||||||
import { authorizeSitePurge, parseSitePurgeEvent, purgeSiteRouteCache, resolveSitePurgeConfig } from '../../../lib/siteCachePurge.ts'
|
|
||||||
|
|
||||||
const PROXY_HEADERS = ['x-forwarded-for', 'x-real-ip', 'forwarded']
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
|
||||||
setResponseHeader(event, 'cache-control', 'no-store')
|
|
||||||
const { token, allowProxied } = resolveSitePurgeConfig(useRuntimeConfig(event))
|
|
||||||
const verdict = authorizeSitePurge({
|
|
||||||
token,
|
|
||||||
authorization: getRequestHeader(event, 'authorization'),
|
|
||||||
proxied: PROXY_HEADERS.some((name) => getRequestHeader(event, name) !== undefined),
|
|
||||||
allowProxied,
|
|
||||||
})
|
|
||||||
if (verdict === 'disabled' || verdict === 'proxied') {
|
|
||||||
throw createError({ statusCode: 404, statusMessage: 'Not Found' })
|
|
||||||
}
|
|
||||||
if (verdict === 'unauthorized') {
|
|
||||||
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { kind, slug } = parseSitePurgeEvent(await readBody(event).catch(() => null))
|
|
||||||
const purged = await purgeSiteRouteCache(useStorage('cache'))
|
|
||||||
console.info(`[site-cache] purge: removed ${purged} route cache entries (${kind} ${slug})`)
|
|
||||||
return { purged }
|
|
||||||
})
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
// Shared site cache purge: the authorization rules of the purge route
|
|
||||||
// POST /api/__site__/cache/purge, the purge of Nitro's route cache, the
|
|
||||||
// parsing of the purge request body, the purge settings read from runtime
|
|
||||||
// config (new and deprecated names) and the removal of the purge keys from
|
|
||||||
// nuxt-site-config's site config. Pure TypeScript outside Nitro's and Nuxt's
|
|
||||||
// auto-import directories: vue-journal-layer, vue-menu-layer and
|
|
||||||
// vue-faq-layer ship this file byte for byte, and `node --test`, the route
|
|
||||||
// and the Nitro plugin import it directly.
|
|
||||||
|
|
||||||
import { createHash, timingSafeEqual } from 'node:crypto'
|
|
||||||
|
|
||||||
/** The shortest purge token the route accepts; with a shorter one it is off. */
|
|
||||||
export const MIN_SITE_PURGE_TOKEN_LENGTH = 32
|
|
||||||
|
|
||||||
/** The cache storage base of Nitro's route cache (routeRules cache and swr). */
|
|
||||||
export const SITE_ROUTE_CACHE_BASE = 'nitro:routes'
|
|
||||||
|
|
||||||
/** Longest slug the purge log line carries. */
|
|
||||||
const MAX_SLUG_LENGTH = 200
|
|
||||||
|
|
||||||
/** A purge kind: lower-case letters and underscores, up to 40 characters. */
|
|
||||||
const KIND_PATTERN = /^[a-z_]{1,40}$/
|
|
||||||
|
|
||||||
export type SitePurgeVerdict = 'ok' | 'disabled' | 'proxied' | 'unauthorized'
|
|
||||||
|
|
||||||
export interface SitePurgeAuthInput {
|
|
||||||
/** The configured token (see resolveSitePurgeConfig). */
|
|
||||||
token: unknown
|
|
||||||
/** The request's Authorization header, if any. */
|
|
||||||
authorization?: string | null
|
|
||||||
/** True when the request carried X-Forwarded-For, X-Real-IP or Forwarded. */
|
|
||||||
proxied: boolean
|
|
||||||
/**
|
|
||||||
* The allowProxied setting (see resolveSitePurgeConfig): when true a
|
|
||||||
* proxied request is not refused, and the bearer token is the only gate.
|
|
||||||
* For a backend that reaches the site through a TLS-terminating proxy,
|
|
||||||
* which adds forwarding headers.
|
|
||||||
*/
|
|
||||||
allowProxied?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
function digest(value: string): Buffer {
|
|
||||||
return createHash('sha256').update(value, 'utf8').digest()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decides a purge request: `disabled` when no token of at least
|
|
||||||
* MIN_SITE_PURGE_TOKEN_LENGTH characters is configured, `proxied` when the
|
|
||||||
* request came through a reverse proxy (checked before the token) unless
|
|
||||||
* allowProxied is true, `ok` when the Bearer value equals the token (compared
|
|
||||||
* in constant time over sha256 digests) and `unauthorized` otherwise. With
|
|
||||||
* allowProxied the token is the only gate, so it must stay long and secret.
|
|
||||||
*/
|
|
||||||
export function authorizeSitePurge({ token, authorization, proxied, allowProxied }: SitePurgeAuthInput): SitePurgeVerdict {
|
|
||||||
if (typeof token !== 'string' || token.length < MIN_SITE_PURGE_TOKEN_LENGTH) return 'disabled'
|
|
||||||
if (proxied && allowProxied !== true) return 'proxied'
|
|
||||||
const m = /^Bearer[ \t]+(\S+)[ \t]*$/i.exec(authorization ?? '')
|
|
||||||
if (!m) return 'unauthorized'
|
|
||||||
return timingSafeEqual(digest(m[1]), digest(token)) ? 'ok' : 'unauthorized'
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The part of a Nitro/unstorage storage the purge uses. */
|
|
||||||
export interface SitePurgeStorage {
|
|
||||||
getKeys(base?: string): Promise<string[]> | string[]
|
|
||||||
removeItem(key: string): Promise<unknown> | unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes every entry of Nitro's route cache (pages, their _payload.json and
|
|
||||||
* feeds) from storage, which is `useStorage('cache')`, and returns how many
|
|
||||||
* it removed. Other cache groups (cached functions, handlers) stay.
|
|
||||||
*/
|
|
||||||
export async function purgeSiteRouteCache(storage: SitePurgeStorage): Promise<number> {
|
|
||||||
const keys = await storage.getKeys(SITE_ROUTE_CACHE_BASE)
|
|
||||||
let removed = 0
|
|
||||||
for (const key of keys) {
|
|
||||||
if (!key.startsWith(SITE_ROUTE_CACHE_BASE + ':')) continue
|
|
||||||
await storage.removeItem(key)
|
|
||||||
removed++
|
|
||||||
}
|
|
||||||
return removed
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SitePurgeEvent {
|
|
||||||
kind: string
|
|
||||||
slug: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the kind and slug of a purge request body for the log line: kind is
|
|
||||||
* kept when it is lower-case letters and underscores up to 40 characters
|
|
||||||
* (faq, menu, menu_entry, post, category, ...), else it is unknown; the slug
|
|
||||||
* loses control characters and is cut to 200 characters.
|
|
||||||
*/
|
|
||||||
export function parseSitePurgeEvent(body: unknown): SitePurgeEvent {
|
|
||||||
if (body === null || typeof body !== 'object' || Array.isArray(body)) return { kind: 'unknown', slug: '' }
|
|
||||||
const raw = body as Record<string, unknown>
|
|
||||||
const kind = typeof raw.kind === 'string' && KIND_PATTERN.test(raw.kind) ? raw.kind : 'unknown'
|
|
||||||
// eslint-disable-next-line no-control-regex
|
|
||||||
const slug = typeof raw.slug === 'string' ? raw.slug.replace(/[\u0000-\u001f\u007f-\u009f]/g, '').slice(0, MAX_SLUG_LENGTH) : ''
|
|
||||||
return { kind, slug }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The site config keys nuxt-site-config derives from the deprecated
|
|
||||||
* variables NUXT_SITE_CACHE_PURGE_TOKEN and NUXT_SITE_CACHE_ALLOW_PROXIED: it
|
|
||||||
* copies every NUXT_SITE_* variable into the site config, camel-cased, and
|
|
||||||
* Nuxt serializes the site config into every page's payload.
|
|
||||||
*/
|
|
||||||
export const SITE_CONFIG_PURGE_KEYS: readonly string[] = Object.freeze(['cachePurgeToken', 'cacheAllowProxied'])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The globalThis flag that keeps the deprecation warning to one per process,
|
|
||||||
* however many layers ship the Nitro plugin.
|
|
||||||
*/
|
|
||||||
export const SITE_PURGE_WARNED = Symbol.for('summercms.siteCachePurge.deprecationWarned')
|
|
||||||
|
|
||||||
export type SitePurgeDeprecated = 'token' | 'allowProxied'
|
|
||||||
|
|
||||||
export interface SitePurgeConfig {
|
|
||||||
/** The token the route checks; authorizeSitePurge decides whether it is usable. */
|
|
||||||
token: unknown
|
|
||||||
/** True when a proxied request may reach the token check. */
|
|
||||||
allowProxied: boolean
|
|
||||||
/** The settings that came from a deprecated name. */
|
|
||||||
deprecated: SitePurgeDeprecated[]
|
|
||||||
}
|
|
||||||
|
|
||||||
function section(config: unknown, name: string): Record<string, unknown> {
|
|
||||||
if (config === null || typeof config !== 'object') return {}
|
|
||||||
const value = (config as Record<string, unknown>)[name]
|
|
||||||
return value !== null && typeof value === 'object' ? (value as Record<string, unknown>) : {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isTrue = (value: unknown) => value === true || value === 'true'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the purge settings from runtime config. The current names are
|
|
||||||
* `cachePurge.token` (NUXT_CACHE_PURGE_TOKEN) and `cachePurge.allowProxied`
|
|
||||||
* (NUXT_CACHE_PURGE_ALLOW_PROXIED). Deprecated for one release:
|
|
||||||
* `siteCache.purgeToken` (NUXT_SITE_CACHE_PURGE_TOKEN) and
|
|
||||||
* `siteCache.allowProxied` (NUXT_SITE_CACHE_ALLOW_PROXIED). A non-empty new
|
|
||||||
* token wins over the old one; allowProxied is on when either flag is true or
|
|
||||||
* "true". `deprecated` lists the settings taken from an old name.
|
|
||||||
*/
|
|
||||||
export function resolveSitePurgeConfig(config: unknown): SitePurgeConfig {
|
|
||||||
const current = section(config, 'cachePurge')
|
|
||||||
const old = section(config, 'siteCache')
|
|
||||||
const deprecated: SitePurgeDeprecated[] = []
|
|
||||||
|
|
||||||
let token: unknown = current.token
|
|
||||||
if (typeof token !== 'string' || token === '') {
|
|
||||||
if (typeof old.purgeToken === 'string' && old.purgeToken !== '') {
|
|
||||||
token = old.purgeToken
|
|
||||||
deprecated.push('token')
|
|
||||||
} else if (token === undefined) {
|
|
||||||
token = old.purgeToken
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let allowProxied = isTrue(current.allowProxied)
|
|
||||||
if (!allowProxied && isTrue(old.allowProxied)) {
|
|
||||||
allowProxied = true
|
|
||||||
deprecated.push('allowProxied')
|
|
||||||
}
|
|
||||||
return { token, allowProxied, deprecated }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes SITE_CONFIG_PURGE_KEYS from every entry of a nuxt-site-config stack
|
|
||||||
* (the array of partial site configs it merges per request) and returns how
|
|
||||||
* many keys it removed. Anything that is not an array of objects is left
|
|
||||||
* alone and counts 0.
|
|
||||||
*/
|
|
||||||
export function redactSiteConfigStack(stack: unknown): number {
|
|
||||||
if (!Array.isArray(stack)) return 0
|
|
||||||
let removed = 0
|
|
||||||
for (const entry of stack) {
|
|
||||||
if (entry === null || typeof entry !== 'object') continue
|
|
||||||
for (const key of SITE_CONFIG_PURGE_KEYS) {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(entry, key)) {
|
|
||||||
delete (entry as Record<string, unknown>)[key]
|
|
||||||
removed++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return removed
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEPRECATED_NAMES: Record<SitePurgeDeprecated, [string, string]> = {
|
|
||||||
token: ['NUXT_SITE_CACHE_PURGE_TOKEN', 'NUXT_CACHE_PURGE_TOKEN'],
|
|
||||||
allowProxied: ['NUXT_SITE_CACHE_ALLOW_PROXIED', 'NUXT_CACHE_PURGE_ALLOW_PROXIED'],
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The one-line deprecation warning for the settings resolveSitePurgeConfig
|
|
||||||
* took from an old name, or '' when there are none. It names variables
|
|
||||||
* only, never a value.
|
|
||||||
*/
|
|
||||||
export function sitePurgeDeprecationMessage(deprecated: readonly SitePurgeDeprecated[]): string {
|
|
||||||
const names = [...new Set(deprecated)].filter((d) => d in DEPRECATED_NAMES).map((d) => `${DEPRECATED_NAMES[d][0]} -> ${DEPRECATED_NAMES[d][1]}`)
|
|
||||||
if (names.length === 0) return ''
|
|
||||||
return (
|
|
||||||
`[site-cache] deprecated purge settings in use, rename them: ${names.join(', ')}. ` +
|
|
||||||
'The old names work until the next minor release. Earlier layer versions published every NUXT_SITE_* value ' +
|
|
||||||
'in the page payload through nuxt-site-config, so rotate a token that was ever set under NUXT_SITE_CACHE_PURGE_TOKEN.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
// Keeps the site cache purge settings out of the public site config.
|
|
||||||
//
|
|
||||||
// nuxt-site-config (pulled in by @nuxtjs/seo) copies every NUXT_SITE_* and
|
|
||||||
// NUXT_PUBLIC_SITE_* environment variable into the site config, camel-cased,
|
|
||||||
// and Nuxt serializes the site config into every page's payload (and into
|
|
||||||
// window.__NUXT_SITE_CONFIG__ on client-only pages). The purge token's
|
|
||||||
// earlier name NUXT_SITE_CACHE_PURGE_TOKEN therefore reached every page as
|
|
||||||
// cachePurgeToken, and NUXT_SITE_CACHE_ALLOW_PROXIED as cacheAllowProxied.
|
|
||||||
// The settings now live under NUXT_CACHE_PURGE_TOKEN and
|
|
||||||
// NUXT_CACHE_PURGE_ALLOW_PROXIED, outside that prefix. The old names keep
|
|
||||||
// working for one release, so this plugin also removes the two keys from
|
|
||||||
// every entry of the site config stack on nuxt-site-config's
|
|
||||||
// `site-config:init` hook, before anything reads it. The stripping stays
|
|
||||||
// with the new names too: a site that still sets an old variable, or sets
|
|
||||||
// both, must not publish it.
|
|
||||||
//
|
|
||||||
// When a setting comes from an old name, the plugin logs one deprecation
|
|
||||||
// warning per process (globalThis[SITE_PURGE_WARNED], so a site extending
|
|
||||||
// several layers that ship this file warns once). The warning names
|
|
||||||
// variables, never a value.
|
|
||||||
//
|
|
||||||
// Without nuxt-site-config the hook is never called and the plugin does
|
|
||||||
// nothing else. Shared: several layers ship this file byte for byte;
|
|
||||||
// running it more than once is harmless.
|
|
||||||
|
|
||||||
import { SITE_PURGE_WARNED, redactSiteConfigStack, resolveSitePurgeConfig, sitePurgeDeprecationMessage } from '../lib/siteCachePurge.ts'
|
|
||||||
|
|
||||||
interface SiteConfigInitContext {
|
|
||||||
siteConfig?: { stack?: unknown }
|
|
||||||
}
|
|
||||||
|
|
||||||
type LooseHooks = { hook: (name: string, fn: (ctx: SiteConfigInitContext | undefined) => void) => unknown }
|
|
||||||
|
|
||||||
export default defineNitroPlugin((nitroApp) => {
|
|
||||||
const g = globalThis as Record<symbol, unknown>
|
|
||||||
if (!g[SITE_PURGE_WARNED]) {
|
|
||||||
const message = sitePurgeDeprecationMessage(resolveSitePurgeConfig(useRuntimeConfig()).deprecated)
|
|
||||||
if (message) {
|
|
||||||
g[SITE_PURGE_WARNED] = true
|
|
||||||
console.warn(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The hook name is untyped in an app without nuxt-site-config.
|
|
||||||
;(nitroApp.hooks as unknown as LooseHooks).hook('site-config:init', (ctx) => {
|
|
||||||
redactSiteConfigStack(ctx?.siteConfig?.stack)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -144,7 +144,13 @@ async function main() {
|
|||||||
let nuxt = null
|
let nuxt = null
|
||||||
const failures = /** @type {string[]} */ ([])
|
const failures = /** @type {string[]} */ ([])
|
||||||
try {
|
try {
|
||||||
nuxt = await startNuxtServer({ outputDir: args.output, env: { [API_ENV]: `${api.url}${API_PREFIX}`, ...args.env } })
|
// vue-seo-layer resolves every page through the SEO API; the fixture API
|
||||||
|
// has none and answers 404, so the layer renders without a CMS answer
|
||||||
|
// (fail open) and never reaches a backend on the default port.
|
||||||
|
nuxt = await startNuxtServer({
|
||||||
|
outputDir: args.output,
|
||||||
|
env: { [API_ENV]: `${api.url}${API_PREFIX}`, NUXT_SEO_API_BASE: `${api.url}/_seo/api/v1`, ...args.env },
|
||||||
|
})
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
let check
|
let check
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,438 +0,0 @@
|
|||||||
// Shared site cache purge: route authorization, the route-cache purge, the
|
|
||||||
// purge body parsing, the purge settings under their new and deprecated
|
|
||||||
// names, the site config stripping plugin and the byte-identity of the files
|
|
||||||
// every layer ships.
|
|
||||||
import { test } from 'node:test'
|
|
||||||
import assert from 'node:assert/strict'
|
|
||||||
import { createHash } from 'node:crypto'
|
|
||||||
import { readFileSync } from 'node:fs'
|
|
||||||
import {
|
|
||||||
MIN_SITE_PURGE_TOKEN_LENGTH,
|
|
||||||
SITE_CONFIG_PURGE_KEYS,
|
|
||||||
SITE_PURGE_WARNED,
|
|
||||||
SITE_ROUTE_CACHE_BASE,
|
|
||||||
authorizeSitePurge,
|
|
||||||
parseSitePurgeEvent,
|
|
||||||
purgeSiteRouteCache,
|
|
||||||
redactSiteConfigStack,
|
|
||||||
resolveSitePurgeConfig,
|
|
||||||
sitePurgeDeprecationMessage,
|
|
||||||
} from '../server/lib/siteCachePurge.ts'
|
|
||||||
|
|
||||||
// The sha256 of the three shared files. vue-journal-layer, vue-menu-layer and
|
|
||||||
// vue-faq-layer ship them byte for byte; a change lands in all three with
|
|
||||||
// new values here.
|
|
||||||
const SHARED_LIB_SHA256 = 'da5e588b94de6a07f99f203d51cc953f497af886d0fad03709d67064a7b09b2a'
|
|
||||||
const SHARED_ROUTE_SHA256 = 'b52189cd8b48586ebeade602d2e23951a8b8156fcc319974a5b3b09f1b409d93'
|
|
||||||
const SHARED_PLUGIN_SHA256 = 'bd4e00888d3121ae9b07cf4f8e130dd54a4fc575ca0b42771d744c36949633a6'
|
|
||||||
|
|
||||||
const TOKEN = 'a'.repeat(MIN_SITE_PURGE_TOKEN_LENGTH)
|
|
||||||
|
|
||||||
function sha256(relative: string): string {
|
|
||||||
return createHash('sha256').update(readFileSync(new URL(relative, import.meta.url))).digest('hex')
|
|
||||||
}
|
|
||||||
|
|
||||||
test('sitePurge: the shared files match the pinned hashes', () => {
|
|
||||||
assert.equal(sha256('../server/lib/siteCachePurge.ts'), SHARED_LIB_SHA256, 'server/lib/siteCachePurge.ts')
|
|
||||||
assert.equal(sha256('../server/api/__site__/cache/purge.post.ts'), SHARED_ROUTE_SHA256, 'server/api/__site__/cache/purge.post.ts')
|
|
||||||
assert.equal(sha256('../server/plugins/site-cache-purge.ts'), SHARED_PLUGIN_SHA256, 'server/plugins/site-cache-purge.ts')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('sitePurge: a missing or short token disables the route', () => {
|
|
||||||
for (const token of [undefined, null, '', 'short', 'b'.repeat(MIN_SITE_PURGE_TOKEN_LENGTH - 1), 12345678901234567890, {}]) {
|
|
||||||
assert.equal(authorizeSitePurge({ token, authorization: `Bearer ${String(token)}`, proxied: false }), 'disabled', `token ${JSON.stringify(token)}`)
|
|
||||||
}
|
|
||||||
assert.equal(authorizeSitePurge({ token: '', authorization: 'Bearer x', proxied: true }), 'disabled')
|
|
||||||
assert.equal(MIN_SITE_PURGE_TOKEN_LENGTH, 32)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('sitePurge: a proxied request is refused before the token is checked', () => {
|
|
||||||
assert.equal(authorizeSitePurge({ token: TOKEN, authorization: `Bearer ${TOKEN}`, proxied: true }), 'proxied')
|
|
||||||
assert.equal(authorizeSitePurge({ token: TOKEN, authorization: null, proxied: true }), 'proxied')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('sitePurge: authorize ok with the token and unauthorized otherwise', () => {
|
|
||||||
assert.equal(authorizeSitePurge({ token: TOKEN, authorization: `Bearer ${TOKEN}`, proxied: false }), 'ok')
|
|
||||||
assert.equal(authorizeSitePurge({ token: TOKEN, authorization: `bearer ${TOKEN}`, proxied: false }), 'ok')
|
|
||||||
for (const authorization of [undefined, null, '', 'Bearer', `Basic ${TOKEN}`, `${TOKEN}`, `Bearer ${TOKEN}x`, `Bearer ${TOKEN.slice(1)}`, `Bearer ${TOKEN} extra`, 'Bearer ' + 'b'.repeat(32)]) {
|
|
||||||
assert.equal(authorizeSitePurge({ token: TOKEN, authorization, proxied: false }), 'unauthorized', `authorization ${JSON.stringify(authorization)}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/** A fake unstorage prefix storage over a plain key list. */
|
|
||||||
function fakeStorage(keys: string[]) {
|
|
||||||
const live = new Set(keys)
|
|
||||||
const removed: string[] = []
|
|
||||||
return {
|
|
||||||
removed,
|
|
||||||
live,
|
|
||||||
async getKeys(base?: string) {
|
|
||||||
return [...live].filter((k) => !base || k === base || k.startsWith(base + ':'))
|
|
||||||
},
|
|
||||||
async removeItem(key: string) {
|
|
||||||
live.delete(key)
|
|
||||||
removed.push(key)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test('sitePurge: purge removes only nitro:routes: keys', async () => {
|
|
||||||
const routes = [
|
|
||||||
`${SITE_ROUTE_CACHE_BASE}:_:faq.abc123.json`,
|
|
||||||
`${SITE_ROUTE_CACHE_BASE}:_:faq_payloadjson.def456.json`,
|
|
||||||
`${SITE_ROUTE_CACHE_BASE}:_:index.0a1b2c.json`,
|
|
||||||
]
|
|
||||||
const others = ['nitro:functions:_:faq.1.json', 'nitro:handlers:_:api.2.json', 'nitro:routesx:_:near.3.json', 'other:routes:_:x.json']
|
|
||||||
const storage = fakeStorage([...routes, ...others])
|
|
||||||
assert.equal(SITE_ROUTE_CACHE_BASE, 'nitro:routes')
|
|
||||||
assert.equal(await purgeSiteRouteCache(storage), routes.length)
|
|
||||||
assert.deepEqual([...storage.removed].sort(), [...routes].sort())
|
|
||||||
assert.deepEqual([...storage.live].sort(), [...others].sort())
|
|
||||||
assert.equal(await purgeSiteRouteCache(storage), 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('sitePurge: purge event kinds and slugs', () => {
|
|
||||||
for (const kind of ['faq', 'menu', 'menu_entry', 'post', 'category']) {
|
|
||||||
assert.deepEqual(parseSitePurgeEvent({ kind, slug: 'x' }), { kind, slug: 'x' })
|
|
||||||
}
|
|
||||||
for (const kind of ['FAQ', 'menu-entry', 'a'.repeat(41), '', 7, null]) {
|
|
||||||
assert.deepEqual(parseSitePurgeEvent({ kind, slug: 'x' }), { kind: 'unknown', slug: 'x' }, JSON.stringify(kind))
|
|
||||||
}
|
|
||||||
assert.deepEqual(parseSitePurgeEvent({ kind: 'faq', slug: 'a\nb\r\tc\u0000d\u007fe\u0085f' }), { kind: 'faq', slug: 'abcdef' })
|
|
||||||
assert.equal(parseSitePurgeEvent({ kind: 'faq', slug: 'x'.repeat(500) }).slug.length, 200)
|
|
||||||
for (const body of [null, undefined, 'faq', 42, ['faq', 'x']]) {
|
|
||||||
assert.deepEqual(parseSitePurgeEvent(body), { kind: 'unknown', slug: '' }, JSON.stringify(body))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test('sitePurge: ok in constant time over sha256 digests, whatever the bearer length', () => {
|
|
||||||
// timingSafeEqual on raw strings throws on a length mismatch; over digests
|
|
||||||
// every length compares in constant time and answers unauthorized.
|
|
||||||
for (const length of [1, 31, 33, 64, 4096]) {
|
|
||||||
assert.equal(authorizeSitePurge({ token: TOKEN, authorization: `Bearer ${'a'.repeat(length)}`, proxied: false }), 'unauthorized', `length ${length}`)
|
|
||||||
}
|
|
||||||
const long = 'k'.repeat(200)
|
|
||||||
assert.equal(authorizeSitePurge({ token: long, authorization: `Bearer ${long}`, proxied: false }), 'ok')
|
|
||||||
assert.equal(authorizeSitePurge({ token: TOKEN, authorization: `Bearer\t${TOKEN}\t`, proxied: false }), 'ok')
|
|
||||||
assert.equal(authorizeSitePurge({ token: TOKEN, authorization: `BEARER ${TOKEN} `, proxied: false }), 'ok')
|
|
||||||
assert.equal(authorizeSitePurge({ token: TOKEN, authorization: ` Bearer ${TOKEN}`, proxied: false }), 'unauthorized')
|
|
||||||
assert.equal(authorizeSitePurge({ token: TOKEN, authorization: `Bearer ${TOKEN}\n`, proxied: false }), 'unauthorized')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('sitePurge: purge works with a synchronous storage and skips the bare base key', async () => {
|
|
||||||
const live = new Set([`${SITE_ROUTE_CACHE_BASE}:_:a.json`, SITE_ROUTE_CACHE_BASE, 'nitro:other:_:b.json'])
|
|
||||||
const removed: string[] = []
|
|
||||||
const storage = {
|
|
||||||
getKeys: (base?: string) => [...live].filter((k) => !base || k === base || k.startsWith(base + ':')),
|
|
||||||
removeItem: (key: string) => {
|
|
||||||
live.delete(key)
|
|
||||||
removed.push(key)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
assert.equal(await purgeSiteRouteCache(storage), 1)
|
|
||||||
assert.deepEqual(removed, [`${SITE_ROUTE_CACHE_BASE}:_:a.json`])
|
|
||||||
assert.ok(live.has(SITE_ROUTE_CACHE_BASE))
|
|
||||||
assert.equal(await purgeSiteRouteCache({ getKeys: () => [], removeItem: () => assert.fail('nothing to remove') }), 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('sitePurge: purge event edge cases', () => {
|
|
||||||
assert.deepEqual(parseSitePurgeEvent({ kind: 'a'.repeat(40), slug: '' }), { kind: 'a'.repeat(40), slug: '' })
|
|
||||||
assert.deepEqual(parseSitePurgeEvent({ kind: 'faq' }), { kind: 'faq', slug: '' })
|
|
||||||
assert.deepEqual(parseSitePurgeEvent({ kind: 'faq', slug: 42 }), { kind: 'faq', slug: '' })
|
|
||||||
assert.deepEqual(parseSitePurgeEvent({ kind: 'post', slug: 'x', extra: 'ignored' }), { kind: 'post', slug: 'x' })
|
|
||||||
assert.equal(parseSitePurgeEvent({ kind: 'faq', slug: 'y'.repeat(200) }).slug.length, 200)
|
|
||||||
// Control characters are stripped before the cut.
|
|
||||||
const slug = '\u0001'.repeat(50) + 'z'.repeat(200)
|
|
||||||
assert.equal(parseSitePurgeEvent({ kind: 'faq', slug }).slug, 'z'.repeat(200))
|
|
||||||
assert.deepEqual(parseSitePurgeEvent({}), { kind: 'unknown', slug: '' })
|
|
||||||
})
|
|
||||||
|
|
||||||
/** One call of the shared route with Nitro's auto-imports stubbed. */
|
|
||||||
async function callRoute(opts: {
|
|
||||||
token?: unknown
|
|
||||||
allowProxied?: unknown
|
|
||||||
cachePurge?: { token?: unknown; allowProxied?: unknown }
|
|
||||||
headers?: Record<string, string>
|
|
||||||
body?: unknown
|
|
||||||
bodyError?: boolean
|
|
||||||
keys?: string[]
|
|
||||||
}) {
|
|
||||||
const responseHeaders: Record<string, string> = {}
|
|
||||||
const logs: string[] = []
|
|
||||||
const live = new Set(opts.keys ?? [])
|
|
||||||
const g = globalThis as Record<string, unknown>
|
|
||||||
g.defineEventHandler = (fn: unknown) => fn
|
|
||||||
g.setResponseHeader = (_e: unknown, name: string, value: string) => {
|
|
||||||
responseHeaders[name] = value
|
|
||||||
}
|
|
||||||
g.useRuntimeConfig = () => ({
|
|
||||||
siteCache: opts.token === undefined ? undefined : { purgeToken: opts.token, allowProxied: opts.allowProxied },
|
|
||||||
cachePurge: opts.cachePurge,
|
|
||||||
})
|
|
||||||
g.getRequestHeader = (_e: unknown, name: string) => opts.headers?.[name]
|
|
||||||
g.createError = (input: { statusCode: number; statusMessage: string }) => Object.assign(new Error(input.statusMessage), input)
|
|
||||||
g.readBody = async () => {
|
|
||||||
if (opts.bodyError) throw new Error('invalid JSON')
|
|
||||||
return opts.body
|
|
||||||
}
|
|
||||||
g.useStorage = (base: string) => {
|
|
||||||
assert.equal(base, 'cache')
|
|
||||||
return {
|
|
||||||
getKeys: async (prefix?: string) => [...live].filter((k) => !prefix || k.startsWith(prefix + ':')),
|
|
||||||
removeItem: async (key: string) => {
|
|
||||||
live.delete(key)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const info = console.info
|
|
||||||
console.info = (line: string) => {
|
|
||||||
logs.push(line)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const route = (await import('../server/api/__site__/cache/purge.post.ts')).default as (e: unknown) => Promise<unknown>
|
|
||||||
try {
|
|
||||||
return { result: await route({}), status: 200, responseHeaders, logs, live }
|
|
||||||
} catch (err) {
|
|
||||||
return { result: undefined, status: (err as { statusCode?: number }).statusCode, responseHeaders, logs, live }
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
console.info = info
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test('sitePurge: route answers 404 when disabled or proxied and 401 for a wrong bearer', async () => {
|
|
||||||
const keys = [`${SITE_ROUTE_CACHE_BASE}:_:index.json`]
|
|
||||||
for (const token of [undefined, '', 'short']) {
|
|
||||||
const r = await callRoute({ token, headers: { authorization: `Bearer ${String(token)}` }, keys })
|
|
||||||
assert.equal(r.status, 404, `token ${JSON.stringify(token)}`)
|
|
||||||
assert.equal(r.live.size, 1)
|
|
||||||
}
|
|
||||||
for (const name of ['x-forwarded-for', 'x-real-ip', 'forwarded']) {
|
|
||||||
const r = await callRoute({ token: TOKEN, headers: { authorization: `Bearer ${TOKEN}`, [name]: '203.0.113.9' }, keys })
|
|
||||||
assert.equal(r.status, 404, name)
|
|
||||||
assert.equal(r.live.size, 1)
|
|
||||||
}
|
|
||||||
for (const authorization of [undefined, 'Bearer wrong', `Basic ${TOKEN}`]) {
|
|
||||||
const r = await callRoute({ token: TOKEN, headers: authorization ? { authorization } : {}, keys })
|
|
||||||
assert.equal(r.status, 401, String(authorization))
|
|
||||||
assert.equal(r.live.size, 1)
|
|
||||||
assert.equal(r.responseHeaders['cache-control'], 'no-store')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test('sitePurge: route purges, answers the count and logs kind and slug only', async () => {
|
|
||||||
const keys = [`${SITE_ROUTE_CACHE_BASE}:_:index.json`, `${SITE_ROUTE_CACHE_BASE}:_:faq.json`, 'nitro:functions:_:x.json']
|
|
||||||
const r = await callRoute({ token: TOKEN, headers: { authorization: `Bearer ${TOKEN}` }, body: { kind: 'faq', slug: 'ship\nping' }, keys })
|
|
||||||
assert.equal(r.status, 200)
|
|
||||||
assert.deepEqual(r.result, { purged: 2 })
|
|
||||||
assert.deepEqual([...r.live], ['nitro:functions:_:x.json'])
|
|
||||||
assert.equal(r.responseHeaders['cache-control'], 'no-store')
|
|
||||||
assert.deepEqual(r.logs, ['[site-cache] purge: removed 2 route cache entries (faq shipping)'])
|
|
||||||
assert.ok(!r.logs.join('').includes(TOKEN))
|
|
||||||
|
|
||||||
const bad = await callRoute({ token: TOKEN, headers: { authorization: `Bearer ${TOKEN}` }, bodyError: true, keys: [] })
|
|
||||||
assert.equal(bad.status, 200)
|
|
||||||
assert.deepEqual(bad.result, { purged: 0 })
|
|
||||||
assert.deepEqual(bad.logs, ['[site-cache] purge: removed 0 route cache entries (unknown )'])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('sitePurge: allowProxied lets a proxied request reach the token check', () => {
|
|
||||||
assert.equal(authorizeSitePurge({ token: TOKEN, authorization: `Bearer ${TOKEN}`, proxied: true, allowProxied: true }), 'ok')
|
|
||||||
assert.equal(authorizeSitePurge({ token: TOKEN, authorization: 'Bearer ' + 'b'.repeat(32), proxied: true, allowProxied: true }), 'unauthorized')
|
|
||||||
assert.equal(authorizeSitePurge({ token: TOKEN, authorization: null, proxied: true, allowProxied: true }), 'unauthorized')
|
|
||||||
assert.equal(authorizeSitePurge({ token: 'short', authorization: 'Bearer short', proxied: true, allowProxied: true }), 'disabled')
|
|
||||||
assert.equal(authorizeSitePurge({ token: '', authorization: 'Bearer x', proxied: true, allowProxied: true }), 'disabled')
|
|
||||||
// Without the flag, or with anything but true, a proxied request stays refused.
|
|
||||||
assert.equal(authorizeSitePurge({ token: TOKEN, authorization: `Bearer ${TOKEN}`, proxied: true, allowProxied: false }), 'proxied')
|
|
||||||
assert.equal(authorizeSitePurge({ token: TOKEN, authorization: `Bearer ${TOKEN}`, proxied: true }), 'proxied')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('sitePurge: route accepts a proxied request with allowProxied and the right bearer', async () => {
|
|
||||||
const keys = [`${SITE_ROUTE_CACHE_BASE}:_:index.json`, `${SITE_ROUTE_CACHE_BASE}:_:faq.json`]
|
|
||||||
const headers = { authorization: `Bearer ${TOKEN}`, 'x-forwarded-for': '203.0.113.9', 'x-real-ip': '203.0.113.9', forwarded: 'for=203.0.113.9;proto=https' }
|
|
||||||
for (const allowProxied of [true, 'true']) {
|
|
||||||
const r = await callRoute({ token: TOKEN, allowProxied, headers, body: { kind: 'faq', slug: 'shipping' }, keys })
|
|
||||||
assert.equal(r.status, 200, `allowProxied ${JSON.stringify(allowProxied)}`)
|
|
||||||
assert.deepEqual(r.result, { purged: 2 })
|
|
||||||
assert.equal(r.live.size, 0)
|
|
||||||
}
|
|
||||||
const wrong = await callRoute({ token: TOKEN, allowProxied: true, headers: { ...headers, authorization: 'Bearer wrong' }, keys })
|
|
||||||
assert.equal(wrong.status, 401)
|
|
||||||
assert.equal(wrong.live.size, 2)
|
|
||||||
for (const allowProxied of [false, undefined, 'false', 1]) {
|
|
||||||
const r = await callRoute({ token: TOKEN, allowProxied, headers, keys })
|
|
||||||
assert.equal(r.status, 404, `allowProxied ${JSON.stringify(allowProxied)}`)
|
|
||||||
assert.equal(r.live.size, 2)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test('sitePurge: resolve config prefers a non-empty new token over the deprecated one', () => {
|
|
||||||
const A = 'n'.repeat(MIN_SITE_PURGE_TOKEN_LENGTH)
|
|
||||||
const B = 'o'.repeat(MIN_SITE_PURGE_TOKEN_LENGTH)
|
|
||||||
assert.deepEqual(resolveSitePurgeConfig({ cachePurge: { token: A }, siteCache: { purgeToken: B } }), { token: A, allowProxied: false, deprecated: [] })
|
|
||||||
assert.deepEqual(resolveSitePurgeConfig({ cachePurge: { token: A, allowProxied: false }, siteCache: { purgeToken: '' } }), { token: A, allowProxied: false, deprecated: [] })
|
|
||||||
assert.deepEqual(resolveSitePurgeConfig({ cachePurge: { token: '' }, siteCache: { purgeToken: B } }), { token: B, allowProxied: false, deprecated: ['token'] })
|
|
||||||
assert.deepEqual(resolveSitePurgeConfig({ siteCache: { purgeToken: B } }), { token: B, allowProxied: false, deprecated: ['token'] })
|
|
||||||
assert.deepEqual(resolveSitePurgeConfig({ cachePurge: null, siteCache: { purgeToken: B } }), { token: B, allowProxied: false, deprecated: ['token'] })
|
|
||||||
assert.deepEqual(resolveSitePurgeConfig({ cachePurge: { token: '' }, siteCache: { purgeToken: '' } }), { token: '', allowProxied: false, deprecated: [] })
|
|
||||||
})
|
|
||||||
|
|
||||||
test('sitePurge: resolve config turns allowProxied on for either flag, true or "true" only', () => {
|
|
||||||
for (const value of [true, 'true']) {
|
|
||||||
assert.deepEqual(resolveSitePurgeConfig({ cachePurge: { allowProxied: value } }).allowProxied, true, `new ${JSON.stringify(value)}`)
|
|
||||||
assert.deepEqual(resolveSitePurgeConfig({ cachePurge: { allowProxied: value } }).deprecated, [])
|
|
||||||
const old = resolveSitePurgeConfig({ siteCache: { allowProxied: value } })
|
|
||||||
assert.equal(old.allowProxied, true, `old ${JSON.stringify(value)}`)
|
|
||||||
assert.deepEqual(old.deprecated, ['allowProxied'])
|
|
||||||
// The new flag on: the old one is not counted as in use.
|
|
||||||
assert.deepEqual(resolveSitePurgeConfig({ cachePurge: { allowProxied: value }, siteCache: { allowProxied: value } }).deprecated, [])
|
|
||||||
}
|
|
||||||
for (const value of [false, undefined, 'false', 1, null, 'TRUE']) {
|
|
||||||
const r = resolveSitePurgeConfig({ cachePurge: { allowProxied: value }, siteCache: { allowProxied: value } })
|
|
||||||
assert.equal(r.allowProxied, false, JSON.stringify(value))
|
|
||||||
assert.deepEqual(r.deprecated, [])
|
|
||||||
}
|
|
||||||
const B = 'o'.repeat(MIN_SITE_PURGE_TOKEN_LENGTH)
|
|
||||||
assert.deepEqual(resolveSitePurgeConfig({ siteCache: { purgeToken: B, allowProxied: 'true' } }).deprecated, ['token', 'allowProxied'])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('sitePurge: resolve config tolerates a missing or malformed config', () => {
|
|
||||||
for (const config of [undefined, null, 'x', 42, {}, { cachePurge: 'x', siteCache: 7 }]) {
|
|
||||||
assert.deepEqual(resolveSitePurgeConfig(config), { token: undefined, allowProxied: false, deprecated: [] }, JSON.stringify(config))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test('sitePurge: route reads the new token, falls back to the deprecated one, and the new one wins', async () => {
|
|
||||||
const keys = [`${SITE_ROUTE_CACHE_BASE}:_:index.json`]
|
|
||||||
const NEW = 'n'.repeat(MIN_SITE_PURGE_TOKEN_LENGTH)
|
|
||||||
const r = await callRoute({ cachePurge: { token: TOKEN }, headers: { authorization: `Bearer ${TOKEN}` }, keys })
|
|
||||||
assert.equal(r.status, 200)
|
|
||||||
assert.deepEqual(r.result, { purged: 1 })
|
|
||||||
const old = await callRoute({ token: TOKEN, cachePurge: { token: '' }, headers: { authorization: `Bearer ${TOKEN}` }, keys })
|
|
||||||
assert.equal(old.status, 200)
|
|
||||||
const both = { token: TOKEN, cachePurge: { token: NEW } }
|
|
||||||
assert.equal((await callRoute({ ...both, headers: { authorization: `Bearer ${NEW}` }, keys })).status, 200)
|
|
||||||
const refused = await callRoute({ ...both, headers: { authorization: `Bearer ${TOKEN}` }, keys })
|
|
||||||
assert.equal(refused.status, 401)
|
|
||||||
assert.equal(refused.live.size, 1)
|
|
||||||
const short = await callRoute({ cachePurge: { token: 'short' }, headers: { authorization: 'Bearer short' }, keys })
|
|
||||||
assert.equal(short.status, 404)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('sitePurge: route lets a proxied request reach the token check with cachePurge.allowProxied', async () => {
|
|
||||||
const keys = [`${SITE_ROUTE_CACHE_BASE}:_:index.json`]
|
|
||||||
const headers = { authorization: `Bearer ${TOKEN}`, 'x-forwarded-for': '203.0.113.9' }
|
|
||||||
for (const allowProxied of [true, 'true']) {
|
|
||||||
const r = await callRoute({ cachePurge: { token: TOKEN, allowProxied }, headers, keys })
|
|
||||||
assert.equal(r.status, 200, `allowProxied ${JSON.stringify(allowProxied)}`)
|
|
||||||
const wrong = await callRoute({ cachePurge: { token: TOKEN, allowProxied }, headers: { ...headers, authorization: 'Bearer wrong' }, keys })
|
|
||||||
assert.equal(wrong.status, 401)
|
|
||||||
}
|
|
||||||
// The deprecated flag with the new token still opens it.
|
|
||||||
assert.equal((await callRoute({ token: '', allowProxied: true, cachePurge: { token: TOKEN }, headers, keys })).status, 200)
|
|
||||||
for (const allowProxied of [false, undefined, 'false', 1]) {
|
|
||||||
const r = await callRoute({ cachePurge: { token: TOKEN, allowProxied }, headers, keys })
|
|
||||||
assert.equal(r.status, 404, `allowProxied ${JSON.stringify(allowProxied)}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test('sitePurge: redactSiteConfigStack removes the purge keys from every stack entry', () => {
|
|
||||||
assert.deepEqual([...SITE_CONFIG_PURGE_KEYS], ['cachePurgeToken', 'cacheAllowProxied'])
|
|
||||||
const stack: Record<string, unknown>[] = [
|
|
||||||
{ url: 'https://acme.test', name: 'Acme', cachePurgeToken: TOKEN, cacheAllowProxied: 'true' },
|
|
||||||
{ _context: 'x' },
|
|
||||||
]
|
|
||||||
assert.equal(redactSiteConfigStack(stack), 2)
|
|
||||||
assert.deepEqual(stack, [{ url: 'https://acme.test', name: 'Acme' }, { _context: 'x' }])
|
|
||||||
assert.equal(redactSiteConfigStack(stack), 0)
|
|
||||||
const spread = [{ cachePurgeToken: TOKEN }, { cachePurgeToken: TOKEN, indexable: true }]
|
|
||||||
assert.equal(redactSiteConfigStack(spread), 2)
|
|
||||||
assert.deepEqual(spread, [{}, { indexable: true }])
|
|
||||||
for (const value of [undefined, null, 'stack', 7, {}, { cachePurgeToken: TOKEN }]) {
|
|
||||||
assert.equal(redactSiteConfigStack(value), 0, JSON.stringify(value))
|
|
||||||
}
|
|
||||||
assert.equal(redactSiteConfigStack([null, undefined, 'x', 3]), 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('sitePurge: the deprecation message names variables, never a value', () => {
|
|
||||||
assert.equal(sitePurgeDeprecationMessage([]), '')
|
|
||||||
const token = sitePurgeDeprecationMessage(['token'])
|
|
||||||
assert.match(token, /^\[site-cache\] /)
|
|
||||||
assert.ok(token.includes('NUXT_SITE_CACHE_PURGE_TOKEN -> NUXT_CACHE_PURGE_TOKEN'))
|
|
||||||
assert.ok(!token.includes('NUXT_SITE_CACHE_ALLOW_PROXIED'))
|
|
||||||
assert.match(token, /rotate/)
|
|
||||||
assert.ok(!token.includes('\n'))
|
|
||||||
const both = sitePurgeDeprecationMessage(['token', 'allowProxied'])
|
|
||||||
assert.ok(both.includes('NUXT_SITE_CACHE_PURGE_TOKEN -> NUXT_CACHE_PURGE_TOKEN'))
|
|
||||||
assert.ok(both.includes('NUXT_SITE_CACHE_ALLOW_PROXIED -> NUXT_CACHE_PURGE_ALLOW_PROXIED'))
|
|
||||||
assert.ok(sitePurgeDeprecationMessage(['allowProxied']).includes('NUXT_CACHE_PURGE_ALLOW_PROXIED'))
|
|
||||||
})
|
|
||||||
|
|
||||||
type SiteConfigHook = (ctx: unknown) => void
|
|
||||||
|
|
||||||
/** Runs the shared Nitro plugin on a fake nitroApp with the given runtime config. */
|
|
||||||
async function runPlugin(config: unknown) {
|
|
||||||
const g = globalThis as Record<string, unknown>
|
|
||||||
g.defineNitroPlugin = (fn: unknown) => fn
|
|
||||||
g.useRuntimeConfig = () => config
|
|
||||||
const hooks: Record<string, SiteConfigHook[]> = {}
|
|
||||||
const nitroApp = {
|
|
||||||
hooks: {
|
|
||||||
hook(name: string, fn: SiteConfigHook) {
|
|
||||||
;(hooks[name] ??= []).push(fn)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const warnings: string[] = []
|
|
||||||
const warn = console.warn
|
|
||||||
console.warn = (line: string) => {
|
|
||||||
warnings.push(line)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const plugin = (await import('../server/plugins/site-cache-purge.ts')).default as (app: unknown) => void
|
|
||||||
plugin(nitroApp)
|
|
||||||
} finally {
|
|
||||||
console.warn = warn
|
|
||||||
}
|
|
||||||
return { hooks, warnings }
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetWarned() {
|
|
||||||
delete (globalThis as Record<symbol, unknown>)[SITE_PURGE_WARNED]
|
|
||||||
}
|
|
||||||
|
|
||||||
test('sitePurge: plugin strips the purge keys on site-config:init', async () => {
|
|
||||||
resetWarned()
|
|
||||||
const { hooks, warnings } = await runPlugin({ cachePurge: { token: TOKEN } })
|
|
||||||
assert.equal(hooks['site-config:init']?.length, 1)
|
|
||||||
assert.deepEqual(warnings, [])
|
|
||||||
const stack = [{ url: 'https://acme.test', cachePurgeToken: TOKEN, cacheAllowProxied: 'true' }, { _context: 'x' }]
|
|
||||||
hooks['site-config:init'][0]({ siteConfig: { stack }, event: {} })
|
|
||||||
assert.deepEqual(stack, [{ url: 'https://acme.test' }, { _context: 'x' }])
|
|
||||||
for (const ctx of [undefined, null, {}, { siteConfig: {} }, { siteConfig: { stack: 'x' } }]) {
|
|
||||||
hooks['site-config:init'][0](ctx)
|
|
||||||
}
|
|
||||||
resetWarned()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('sitePurge: plugin warns once per process about a deprecated name, without the value', async () => {
|
|
||||||
resetWarned()
|
|
||||||
const config = { siteCache: { purgeToken: TOKEN, allowProxied: 'true' } }
|
|
||||||
const first = await runPlugin(config)
|
|
||||||
assert.equal(first.warnings.length, 1)
|
|
||||||
assert.ok(first.warnings[0].includes('NUXT_CACHE_PURGE_TOKEN'))
|
|
||||||
assert.ok(first.warnings[0].includes('NUXT_SITE_CACHE_PURGE_TOKEN'))
|
|
||||||
assert.ok(first.warnings[0].includes('NUXT_CACHE_PURGE_ALLOW_PROXIED'))
|
|
||||||
assert.ok(!first.warnings[0].includes(TOKEN))
|
|
||||||
// Another layer's copy of the plugin in the same process stays quiet.
|
|
||||||
const second = await runPlugin(config)
|
|
||||||
assert.deepEqual(second.warnings, [])
|
|
||||||
assert.equal(second.hooks['site-config:init']?.length, 1)
|
|
||||||
resetWarned()
|
|
||||||
|
|
||||||
const fresh = await runPlugin({ cachePurge: { token: TOKEN }, siteCache: { purgeToken: 'o'.repeat(MIN_SITE_PURGE_TOKEN_LENGTH) } })
|
|
||||||
assert.deepEqual(fresh.warnings, [], 'the new name wins: no warning')
|
|
||||||
const none = await runPlugin({})
|
|
||||||
assert.deepEqual(none.warnings, [])
|
|
||||||
resetWarned()
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user