+ Last updated {lastUpdatedDate} +
+ ) + } +diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..dcabaf0 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,83 @@ +name: Docs + +on: + push: + branches: [main] + paths: + - 'docs-site/**' + - '.github/workflows/docs.yml' + pull_request: + branches: [main] + paths: + - 'docs-site/**' + - '.github/workflows/docs.yml' + workflow_dispatch: + +# Cancel in-progress runs for the same ref so PR pushes don't pile up. +concurrency: + group: docs-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + pages: write + id-token: write + +jobs: + # -------------------------------------------------------------------------- + # build — runs on every push to feat/* and every PR. A broken docs build + # must NOT sneak through into main. The job uploads the produced `dist/` + # as a Pages artifact so the deploy job can publish it on main. + # -------------------------------------------------------------------------- + build: + name: Build + runs-on: ubuntu-latest + timeout-minutes: 10 + defaults: + run: + working-directory: docs-site + steps: + - uses: actions/checkout@v6 + + - uses: pnpm/action-setup@v4 + with: + version: 10 + + - uses: actions/setup-node@v5 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: docs-site/pnpm-lock.yaml + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build static site + run: pnpm run build + + - name: Verify pagefind index + run: | + test -f dist/pagefind/pagefind-entry.json \ + || (echo "pagefind index missing" >&2; exit 1) + + - name: Upload Pages artifact + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + uses: actions/upload-pages-artifact@v3 + with: + path: docs-site/dist + + # -------------------------------------------------------------------------- + # deploy — only runs on pushes to main. Publishes the artifact uploaded by + # the build job to GitHub Pages. PR builds run the build job only. + # -------------------------------------------------------------------------- + deploy: + name: Deploy to GitHub Pages + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 2b57f3a..0977ae0 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,8 @@ coverage* .DS_Store .idea/ .vscode/ + +# docs-site build artifacts +docs-site/node_modules/ +docs-site/dist/ +docs-site/.astro/ diff --git a/README.md b/README.md index c762ae0..b9405ac 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,27 @@ check is bypassed. Never enable dev mode in production. | `NOTIFY_APNS_TOPIC` | string | — | Bundle id / topic. | | `NOTIFY_APNS_SANDBOX` | bool | `false` | Use the APNs sandbox endpoint. | +## Docs + +Full documentation lives at [elloloop.github.io/notify](https://elloloop.github.io/notify/): + +- [Quick Start](https://elloloop.github.io/notify/docs/quickstart) — five-minute "hello, notify" +- [Architecture](https://elloloop.github.io/notify/docs/concepts/architecture) +- [Configuration reference](https://elloloop.github.io/notify/docs/installation/configuration) +- [gRPC / Connect API](https://elloloop.github.io/notify/docs/api-reference/grpc) +- [Send a notification](https://elloloop.github.io/notify/docs/examples/send-notification) — Go / Python / cURL +- [Subscribe over SSE](https://elloloop.github.io/notify/docs/examples/subscribe-sse) + +The source is in [`docs-site/`](./docs-site) — an Astro static site +built with the `@refraction-ui/astro` shell. Build it locally with: + +```bash +cd docs-site +pnpm install +pnpm run build # produces dist/ +pnpm run preview # serves on http://127.0.0.1:4321/notify +``` + ## License AGPL-3.0 — see [LICENSE](./LICENSE). diff --git a/docs-site/.gitignore b/docs-site/.gitignore new file mode 100644 index 0000000..dab5dd6 --- /dev/null +++ b/docs-site/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.astro/ +dist/ diff --git a/docs-site/astro.config.mjs b/docs-site/astro.config.mjs new file mode 100644 index 0000000..e388652 --- /dev/null +++ b/docs-site/astro.config.mjs @@ -0,0 +1,42 @@ +import { defineConfig } from "astro/config"; +import tailwind from "@astrojs/tailwind"; +import expressiveCode from "astro-expressive-code"; +import pagefind from "astro-pagefind"; + +export default defineConfig({ + site: "https://elloloop.github.io", + base: "/notify", + integrations: [ + expressiveCode({ + themes: ["github-dark-dimmed"], + styleOverrides: { + // Minimal, modern chrome — drop the macOS "traffic light" decoration. + frames: { + frameBoxShadowCssValue: "none", + editorTabBarBackground: "hsl(var(--muted))", + editorActiveTabIndicatorBottomColor: "hsl(var(--primary))", + editorActiveTabBorderColor: "transparent", + terminalTitlebarBackground: "hsl(var(--muted))", + terminalTitlebarBorderBottomColor: "hsl(var(--border))", + terminalBackground: "#22272e", + tooltipSuccessBackground: "hsl(var(--primary))", + }, + borderRadius: "0.5rem", + codeFontFamily: "var(--font-mono)", + codeFontSize: "13px", + codeLineHeight: "1.65", + uiFontFamily: "var(--font-sans)", + }, + defaultProps: { + // Hide the macOS-style window decoration buttons globally. + frame: "code", + }, + shiki: { + // Match the prior Shiki configuration so language tags continue to work. + }, + }), + tailwind({ applyBaseStyles: false }), + pagefind(), + ], + output: "static", +}); diff --git a/docs-site/package.json b/docs-site/package.json new file mode 100644 index 0000000..018d866 --- /dev/null +++ b/docs-site/package.json @@ -0,0 +1,22 @@ +{ + "name": "notify-docs", + "type": "module", + "version": "0.0.1", + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview" + }, + "dependencies": { + "@astrojs/tailwind": "5.1.5", + "@fontsource-variable/inter": "5.2.8", + "@fontsource-variable/jetbrains-mono": "5.2.8", + "@pagefind/default-ui": "1.5.2", + "@refraction-ui/astro": "0.4.11", + "@refraction-ui/tailwind-config": "0.1.6", + "astro": "5.18.1", + "astro-expressive-code": "0.41.7", + "astro-pagefind": "1.8.6", + "tailwindcss": "3.4.19" + } +} diff --git a/docs-site/pnpm-lock.yaml b/docs-site/pnpm-lock.yaml new file mode 100644 index 0000000..ce56430 --- /dev/null +++ b/docs-site/pnpm-lock.yaml @@ -0,0 +1,4321 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@astrojs/tailwind': + specifier: 5.1.5 + version: 5.1.5(astro@5.18.1(jiti@1.21.7)(rollup@4.60.4)(typescript@5.9.3)(yaml@2.9.0))(tailwindcss@3.4.19(yaml@2.9.0)) + '@fontsource-variable/inter': + specifier: 5.2.8 + version: 5.2.8 + '@fontsource-variable/jetbrains-mono': + specifier: 5.2.8 + version: 5.2.8 + '@pagefind/default-ui': + specifier: 1.5.2 + version: 1.5.2 + '@refraction-ui/astro': + specifier: 0.4.11 + version: 0.4.11(astro@5.18.1(jiti@1.21.7)(rollup@4.60.4)(typescript@5.9.3)(yaml@2.9.0)) + '@refraction-ui/tailwind-config': + specifier: 0.1.6 + version: 0.1.6(tailwindcss@3.4.19(yaml@2.9.0)) + astro: + specifier: 5.18.1 + version: 5.18.1(jiti@1.21.7)(rollup@4.60.4)(typescript@5.9.3)(yaml@2.9.0) + astro-expressive-code: + specifier: 0.41.7 + version: 0.41.7(astro@5.18.1(jiti@1.21.7)(rollup@4.60.4)(typescript@5.9.3)(yaml@2.9.0)) + astro-pagefind: + specifier: 1.8.6 + version: 1.8.6(astro@5.18.1(jiti@1.21.7)(rollup@4.60.4)(typescript@5.9.3)(yaml@2.9.0)) + tailwindcss: + specifier: 3.4.19 + version: 3.4.19(yaml@2.9.0) + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@astrojs/compiler@2.13.1': + resolution: {integrity: sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg==} + + '@astrojs/internal-helpers@0.7.6': + resolution: {integrity: sha512-GOle7smBWKfMSP8osUIGOlB5kaHdQLV3foCsf+5Q9Wsuu+C6Fs3Ez/ttXmhjZ1HkSgsogcM1RXSjjOVieHq16Q==} + + '@astrojs/markdown-remark@6.3.11': + resolution: {integrity: sha512-hcaxX/5aC6lQgHeGh1i+aauvSwIT6cfyFjKWvExYSxUhZZBBdvCliOtu06gbQyhbe0pGJNoNmqNlQZ5zYUuIyQ==} + + '@astrojs/prism@3.3.0': + resolution: {integrity: sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ==} + engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} + + '@astrojs/tailwind@5.1.5': + resolution: {integrity: sha512-1diguZEau7FZ9vIjzE4BwavGdhD3+JkdS8zmibl1ene+EHgIU5hI0NMgRYG3yea+Niaf7cyMwjeWeLvzq/maxg==} + peerDependencies: + astro: ^3.0.0 || ^4.0.0 || ^5.0.0 + tailwindcss: ^3.0.24 + + '@astrojs/telemetry@3.3.0': + resolution: {integrity: sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==} + engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@capsizecss/unpack@4.0.0': + resolution: {integrity: sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==} + engines: {node: '>=18'} + + '@ctrl/tinycolor@4.2.0': + resolution: {integrity: sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==} + engines: {node: '>=14'} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@expressive-code/core@0.41.7': + resolution: {integrity: sha512-ck92uZYZ9Wba2zxkiZLsZGi9N54pMSAVdrI9uW3Oo9AtLglD5RmrdTwbYPCT2S/jC36JGB2i+pnQtBm/Ib2+dg==} + + '@expressive-code/plugin-frames@0.41.7': + resolution: {integrity: sha512-diKtxjQw/979cTglRFaMCY/sR6hWF0kSMg8jsKLXaZBSfGS0I/Hoe7Qds3vVEgeoW+GHHQzMcwvgx/MOIXhrTA==} + + '@expressive-code/plugin-shiki@0.41.7': + resolution: {integrity: sha512-DL605bLrUOgqTdZ0Ot5MlTaWzppRkzzqzeGEu7ODnHF39IkEBbFdsC7pbl3LbUQ1DFtnfx6rD54k/cdofbW6KQ==} + + '@expressive-code/plugin-text-markers@0.41.7': + resolution: {integrity: sha512-Ewpwuc5t6eFdZmWlFyeuy3e1PTQC0jFvw2Q+2bpcWXbOZhPLsT7+h8lsSIJxb5mS7wZko7cKyQ2RLYDyK6Fpmw==} + + '@fontsource-variable/inter@5.2.8': + resolution: {integrity: sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==} + + '@fontsource-variable/jetbrains-mono@5.2.8': + resolution: {integrity: sha512-WBA9elru6Jdp5df2mES55wuOO0WIrn3kpXnI4+W2ek5u3ZgLS9XS4gmIlcQhiZOWEKl95meYdvK7xI+ETLCq/Q==} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@oslojs/encoding@1.1.0': + resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + + '@pagefind/darwin-arm64@1.5.2': + resolution: {integrity: sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ==} + cpu: [arm64] + os: [darwin] + + '@pagefind/darwin-x64@1.5.2': + resolution: {integrity: sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw==} + cpu: [x64] + os: [darwin] + + '@pagefind/default-ui@1.5.2': + resolution: {integrity: sha512-pm1LMnQg8N2B3n2TnjKlhaFihpz6zTiA4HiGQ6/slKO/+8K9CAU5kcjdSSPgpuk1PMuuN4hxLipUIifnrkl3Sg==} + + '@pagefind/freebsd-x64@1.5.2': + resolution: {integrity: sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA==} + cpu: [x64] + os: [freebsd] + + '@pagefind/linux-arm64@1.5.2': + resolution: {integrity: sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw==} + cpu: [arm64] + os: [linux] + + '@pagefind/linux-x64@1.5.2': + resolution: {integrity: sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA==} + cpu: [x64] + os: [linux] + + '@pagefind/windows-arm64@1.5.2': + resolution: {integrity: sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g==} + cpu: [arm64] + os: [win32] + + '@pagefind/windows-x64@1.5.2': + resolution: {integrity: sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg==} + cpu: [x64] + os: [win32] + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@refraction-ui/astro@0.4.11': + resolution: {integrity: sha512-9g/ah2LHV9HquFHD+6TGbuxIijOY0HhLpGcFGmeiVAJeeyBlA2EThV/ZwNYIaL1/1rYoXIEjBc8dX29L0Lz1zA==} + peerDependencies: + astro: '>=4.0.0' + + '@refraction-ui/tailwind-config@0.1.6': + resolution: {integrity: sha512-RLyPhAgErwBEhij/Hz74iQXDg91tOsmh/ZOlbTuElAPqwZRWKEx5skc3v3yTCq1BCK2UyByFq1ZD/6NE/ShQfQ==} + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.60.4': + resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.4': + resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.4': + resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.4': + resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.4': + resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.4': + resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.60.4': + resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.60.4': + resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.60.4': + resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.60.4': + resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.60.4': + resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.4': + resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.4': + resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.4': + resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} + cpu: [x64] + os: [win32] + + '@shikijs/core@3.23.0': + resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==} + + '@shikijs/engine-javascript@3.23.0': + resolution: {integrity: sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==} + + '@shikijs/engine-oniguruma@3.23.0': + resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==} + + '@shikijs/langs@3.23.0': + resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==} + + '@shikijs/themes@3.23.0': + resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==} + + '@shikijs/types@3.23.0': + resolution: {integrity: sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@tailwindcss/typography@0.5.19': + resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/nlcst@2.0.3': + resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@ungap/structured-clone@1.3.1': + resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-iterate@2.0.1: + resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==} + + astro-expressive-code@0.41.7: + resolution: {integrity: sha512-hUpogGc6DdAd+I7pPXsctyYPRBJDK7Q7d06s4cyP0Vz3OcbziP3FNzN0jZci1BpCvLn9675DvS7B9ctKKX64JQ==} + peerDependencies: + astro: ^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta + + astro-pagefind@1.8.6: + resolution: {integrity: sha512-pofDcWMgA3qLQX9gSJ1mc3NSJDv6o1PDs68MrtdupxMFIlAFT2yckFSiOoaab2stSDmKyX/wVaTObyj5FuBulA==} + peerDependencies: + astro: ^2.0.4 || ^3 || ^4 || ^5 || ^6 + + astro@5.18.1: + resolution: {integrity: sha512-m4VWilWZ+Xt6NPoYzC4CgGZim/zQUO7WFL0RHCH0AiEavF1153iC3+me2atDvXpf/yX4PyGUeD8wZLq1cirT3g==} + engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'} + hasBin: true + + autoprefixer@10.5.0: + resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + base-64@1.0.0: + resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} + + baseline-browser-mapping@2.10.32: + resolution: {integrity: sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==} + engines: {node: '>=6.0.0'} + hasBin: true + + bcp-47-match@2.0.3: + resolution: {integrity: sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + boxen@8.0.1: + resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} + engines: {node: '>=18'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + camelcase@8.0.0: + resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} + engines: {node: '>=16'} + + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + common-ancestor-path@1.0.1: + resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} + + cookie-es@1.2.3: + resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + crossws@0.3.5: + resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-selector-parser@3.3.0: + resolution: {integrity: sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==} + + css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + deterministic-object-hash@2.0.2: + resolution: {integrity: sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==} + engines: {node: '>=18'} + + devalue@5.8.1: + resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + + direction@2.0.1: + resolution: {integrity: sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==} + hasBin: true + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dset@3.1.4: + resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} + engines: {node: '>=4'} + + electron-to-chromium@1.5.362: + resolution: {integrity: sha512-PUY2DrLvkjkUuWqq+KPL2iWshrJsZOcIojzRQ7eXFacc9dWga7MGMJAa15VbiejSZB1PAXaRLAiKgruHP8LB1w==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + expressive-code@0.41.7: + resolution: {integrity: sha512-2wZjC8OQ3TaVEMcBtYY4Va3lo6J+Ai9jf3d4dbhURMJcU4Pbqe6EcHe424MIZI0VHUA1bR6xdpoHYi3yxokWqA==} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + flattie@1.1.1: + resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} + engines: {node: '>=8'} + + fontace@0.4.1: + resolution: {integrity: sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==} + + fontkitten@1.0.3: + resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==} + engines: {node: '>=20'} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + h3@1.15.11: + resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + hast-util-from-html@2.0.3: + resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} + + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-has-property@3.0.0: + resolution: {integrity: sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==} + + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-select@6.0.4: + resolution: {integrity: sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw==} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-to-parse5@8.0.1: + resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==} + + hast-util-to-string@3.0.1: + resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==} + + hast-util-to-text@4.0.2: + resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + + html-escaper@3.0.3: + resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + lru-cache@11.5.0: + resolution: {integrity: sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==} + engines: {node: 20 || >=22} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.5.3: + resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + mdast-util-definitions@6.0.0: + resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + neotraverse@0.6.18: + resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} + engines: {node: '>= 10'} + + nlcst-to-string@4.0.0: + resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-mock-http@1.0.4: + resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} + + node-releases@2.0.46: + resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + engines: {node: '>=18'} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + oniguruma-parser@0.12.2: + resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} + + oniguruma-to-es@4.3.6: + resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + + p-limit@6.2.0: + resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==} + engines: {node: '>=18'} + + p-queue@8.1.1: + resolution: {integrity: sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==} + engines: {node: '>=18'} + + p-timeout@6.1.4: + resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==} + engines: {node: '>=14.16'} + + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + + pagefind@1.5.2: + resolution: {integrity: sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q==} + hasBin: true + + parse-latin@7.0.0: + resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + piccolore@0.1.3: + resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@4.0.2: + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + + rehype-expressive-code@0.41.7: + resolution: {integrity: sha512-25f8ZMSF1d9CMscX7Cft0TSQIqdwjce2gDOvQ+d/w0FovsMwrSt3ODP4P3Z7wO1jsIJ4eYyaDRnIR/27bd/EMQ==} + + rehype-parse@9.0.1: + resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==} + + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + rehype-stringify@10.0.1: + resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==} + + rehype@13.0.2: + resolution: {integrity: sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-smartypants@3.0.2: + resolution: {integrity: sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==} + engines: {node: '>=16.0.0'} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + retext-latin@4.0.0: + resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==} + + retext-smartypants@6.2.0: + resolution: {integrity: sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==} + + retext-stringify@4.0.0: + resolution: {integrity: sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==} + + retext@9.0.0: + resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.60.4: + resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shiki@3.23.0: + resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + smol-toml@1.6.1: + resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} + engines: {node: '>= 18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svgo@4.0.1: + resolution: {integrity: sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==} + engines: {node: '>=16'} + hasBin: true + + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + + tinyexec@1.2.2: + resolution: {integrity: sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==} + engines: {node: '>=18'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + ultrahtml@1.6.0: + resolution: {integrity: sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==} + + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unifont@0.7.4: + resolution: {integrity: sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==} + + unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-modify-children@4.0.0: + resolution: {integrity: sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-remove-position@5.0.0: + resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-children@3.0.0: + resolution: {integrity: sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + unstorage@1.17.5: + resolution: {integrity: sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==} + peerDependencies: + '@azure/app-configuration': ^1.8.0 + '@azure/cosmos': ^4.2.0 + '@azure/data-tables': ^13.3.0 + '@azure/identity': ^4.6.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.26.0 + '@capacitor/preferences': ^6 || ^7 || ^8 + '@deno/kv': '>=0.9.0' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.34.3 + '@vercel/blob': '>=0.27.1' + '@vercel/functions': ^2.2.12 || ^3.0.0 + '@vercel/kv': ^1 || ^2 || ^3 + aws4fetch: ^1.0.20 + db0: '>=0.2.1' + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@deno/kv': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/blob': + optional: true + '@vercel/functions': + optional: true + '@vercel/kv': + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@6.4.2: + resolution: {integrity: sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + + which-pm-runs@1.1.0: + resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} + engines: {node: '>=4'} + + widest-line@5.0.0: + resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} + engines: {node: '>=18'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + xxhash-wasm@1.1.0: + resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + + yocto-spinner@0.2.3: + resolution: {integrity: sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ==} + engines: {node: '>=18.19'} + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + + zod-to-ts@1.2.0: + resolution: {integrity: sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA==} + peerDependencies: + typescript: ^4.9.4 || ^5.0.2 + zod: ^3 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@astrojs/compiler@2.13.1': {} + + '@astrojs/internal-helpers@0.7.6': {} + + '@astrojs/markdown-remark@6.3.11': + dependencies: + '@astrojs/internal-helpers': 0.7.6 + '@astrojs/prism': 3.3.0 + github-slugger: 2.0.0 + hast-util-from-html: 2.0.3 + hast-util-to-text: 4.0.2 + import-meta-resolve: 4.2.0 + js-yaml: 4.1.1 + mdast-util-definitions: 6.0.0 + rehype-raw: 7.0.0 + rehype-stringify: 10.0.1 + remark-gfm: 4.0.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + remark-smartypants: 3.0.2 + shiki: 3.23.0 + smol-toml: 1.6.1 + unified: 11.0.5 + unist-util-remove-position: 5.0.0 + unist-util-visit: 5.1.0 + unist-util-visit-parents: 6.0.2 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@astrojs/prism@3.3.0': + dependencies: + prismjs: 1.30.0 + + '@astrojs/tailwind@5.1.5(astro@5.18.1(jiti@1.21.7)(rollup@4.60.4)(typescript@5.9.3)(yaml@2.9.0))(tailwindcss@3.4.19(yaml@2.9.0))': + dependencies: + astro: 5.18.1(jiti@1.21.7)(rollup@4.60.4)(typescript@5.9.3)(yaml@2.9.0) + autoprefixer: 10.5.0(postcss@8.5.15) + postcss: 8.5.15 + postcss-load-config: 4.0.2(postcss@8.5.15) + tailwindcss: 3.4.19(yaml@2.9.0) + transitivePeerDependencies: + - ts-node + + '@astrojs/telemetry@3.3.0': + dependencies: + ci-info: 4.4.0 + debug: 4.4.3 + dlv: 1.1.3 + dset: 3.1.4 + is-docker: 3.0.0 + is-wsl: 3.1.1 + which-pm-runs: 1.1.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@capsizecss/unpack@4.0.0': + dependencies: + fontkitten: 1.0.3 + + '@ctrl/tinycolor@4.2.0': {} + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@expressive-code/core@0.41.7': + dependencies: + '@ctrl/tinycolor': 4.2.0 + hast-util-select: 6.0.4 + hast-util-to-html: 9.0.5 + hast-util-to-text: 4.0.2 + hastscript: 9.0.1 + postcss: 8.5.15 + postcss-nested: 6.2.0(postcss@8.5.15) + unist-util-visit: 5.1.0 + unist-util-visit-parents: 6.0.2 + + '@expressive-code/plugin-frames@0.41.7': + dependencies: + '@expressive-code/core': 0.41.7 + + '@expressive-code/plugin-shiki@0.41.7': + dependencies: + '@expressive-code/core': 0.41.7 + shiki: 3.23.0 + + '@expressive-code/plugin-text-markers@0.41.7': + dependencies: + '@expressive-code/core': 0.41.7 + + '@fontsource-variable/inter@5.2.8': {} + + '@fontsource-variable/jetbrains-mono@5.2.8': {} + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.10.0 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@oslojs/encoding@1.1.0': {} + + '@pagefind/darwin-arm64@1.5.2': + optional: true + + '@pagefind/darwin-x64@1.5.2': + optional: true + + '@pagefind/default-ui@1.5.2': {} + + '@pagefind/freebsd-x64@1.5.2': + optional: true + + '@pagefind/linux-arm64@1.5.2': + optional: true + + '@pagefind/linux-x64@1.5.2': + optional: true + + '@pagefind/windows-arm64@1.5.2': + optional: true + + '@pagefind/windows-x64@1.5.2': + optional: true + + '@polka/url@1.0.0-next.29': {} + + '@refraction-ui/astro@0.4.11(astro@5.18.1(jiti@1.21.7)(rollup@4.60.4)(typescript@5.9.3)(yaml@2.9.0))': + dependencies: + astro: 5.18.1(jiti@1.21.7)(rollup@4.60.4)(typescript@5.9.3)(yaml@2.9.0) + + '@refraction-ui/tailwind-config@0.1.6(tailwindcss@3.4.19(yaml@2.9.0))': + dependencies: + '@tailwindcss/typography': 0.5.19(tailwindcss@3.4.19(yaml@2.9.0)) + transitivePeerDependencies: + - tailwindcss + + '@rollup/pluginutils@5.3.0(rollup@4.60.4)': + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.4 + optionalDependencies: + rollup: 4.60.4 + + '@rollup/rollup-android-arm-eabi@4.60.4': + optional: true + + '@rollup/rollup-android-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-x64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.4': + optional: true + + '@shikijs/core@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.6 + + '@shikijs/engine-oniguruma@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + + '@shikijs/themes@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + + '@shikijs/types@3.23.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + + '@tailwindcss/typography@0.5.19(tailwindcss@3.4.19(yaml@2.9.0))': + dependencies: + postcss-selector-parser: 6.0.10 + tailwindcss: 3.4.19(yaml@2.9.0) + + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree@1.0.8': {} + + '@types/estree@1.0.9': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + + '@types/nlcst@2.0.3': + dependencies: + '@types/unist': 3.0.3 + + '@types/unist@3.0.3': {} + + '@ungap/structured-clone@1.3.1': {} + + acorn@8.16.0: {} + + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@6.2.3: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + arg@5.0.2: {} + + argparse@2.0.1: {} + + aria-query@5.3.2: {} + + array-iterate@2.0.1: {} + + astro-expressive-code@0.41.7(astro@5.18.1(jiti@1.21.7)(rollup@4.60.4)(typescript@5.9.3)(yaml@2.9.0)): + dependencies: + astro: 5.18.1(jiti@1.21.7)(rollup@4.60.4)(typescript@5.9.3)(yaml@2.9.0) + rehype-expressive-code: 0.41.7 + + astro-pagefind@1.8.6(astro@5.18.1(jiti@1.21.7)(rollup@4.60.4)(typescript@5.9.3)(yaml@2.9.0)): + dependencies: + '@pagefind/default-ui': 1.5.2 + astro: 5.18.1(jiti@1.21.7)(rollup@4.60.4)(typescript@5.9.3)(yaml@2.9.0) + pagefind: 1.5.2 + sirv: 3.0.2 + + astro@5.18.1(jiti@1.21.7)(rollup@4.60.4)(typescript@5.9.3)(yaml@2.9.0): + dependencies: + '@astrojs/compiler': 2.13.1 + '@astrojs/internal-helpers': 0.7.6 + '@astrojs/markdown-remark': 6.3.11 + '@astrojs/telemetry': 3.3.0 + '@capsizecss/unpack': 4.0.0 + '@oslojs/encoding': 1.1.0 + '@rollup/pluginutils': 5.3.0(rollup@4.60.4) + acorn: 8.16.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + boxen: 8.0.1 + ci-info: 4.4.0 + clsx: 2.1.1 + common-ancestor-path: 1.0.1 + cookie: 1.1.1 + cssesc: 3.0.0 + debug: 4.4.3 + deterministic-object-hash: 2.0.2 + devalue: 5.8.1 + diff: 8.0.4 + dlv: 1.1.3 + dset: 3.1.4 + es-module-lexer: 1.7.0 + esbuild: 0.27.7 + estree-walker: 3.0.3 + flattie: 1.1.1 + fontace: 0.4.1 + github-slugger: 2.0.0 + html-escaper: 3.0.3 + http-cache-semantics: 4.2.0 + import-meta-resolve: 4.2.0 + js-yaml: 4.1.1 + magic-string: 0.30.21 + magicast: 0.5.3 + mrmime: 2.0.1 + neotraverse: 0.6.18 + p-limit: 6.2.0 + p-queue: 8.1.1 + package-manager-detector: 1.6.0 + piccolore: 0.1.3 + picomatch: 4.0.4 + prompts: 2.4.2 + rehype: 13.0.2 + semver: 7.8.1 + shiki: 3.23.0 + smol-toml: 1.6.1 + svgo: 4.0.1 + tinyexec: 1.2.2 + tinyglobby: 0.2.16 + tsconfck: 3.1.6(typescript@5.9.3) + ultrahtml: 1.6.0 + unifont: 0.7.4 + unist-util-visit: 5.1.0 + unstorage: 1.17.5 + vfile: 6.0.3 + vite: 6.4.2(jiti@1.21.7)(yaml@2.9.0) + vitefu: 1.1.3(vite@6.4.2(jiti@1.21.7)(yaml@2.9.0)) + xxhash-wasm: 1.1.0 + yargs-parser: 21.1.1 + yocto-spinner: 0.2.3 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + zod-to-ts: 1.2.0(typescript@5.9.3)(zod@3.25.76) + optionalDependencies: + sharp: 0.34.5 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@types/node' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - idb-keyval + - ioredis + - jiti + - less + - lightningcss + - rollup + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - typescript + - uploadthing + - yaml + + autoprefixer@10.5.0(postcss@8.5.15): + dependencies: + browserslist: 4.28.2 + caniuse-lite: 1.0.30001793 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.15 + postcss-value-parser: 4.2.0 + + axobject-query@4.1.0: {} + + bail@2.0.2: {} + + base-64@1.0.0: {} + + baseline-browser-mapping@2.10.32: {} + + bcp-47-match@2.0.3: {} + + binary-extensions@2.3.0: {} + + boolbase@1.0.0: {} + + boxen@8.0.1: + dependencies: + ansi-align: 3.0.1 + camelcase: 8.0.0 + chalk: 5.6.2 + cli-boxes: 3.0.0 + string-width: 7.2.0 + type-fest: 4.41.0 + widest-line: 5.0.0 + wrap-ansi: 9.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.32 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.362 + node-releases: 2.0.46 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + camelcase-css@2.0.1: {} + + camelcase@8.0.0: {} + + caniuse-lite@1.0.30001793: {} + + ccount@2.0.1: {} + + chalk@5.6.2: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + ci-info@4.4.0: {} + + cli-boxes@3.0.0: {} + + clsx@2.1.1: {} + + comma-separated-tokens@2.0.3: {} + + commander@11.1.0: {} + + commander@4.1.1: {} + + common-ancestor-path@1.0.1: {} + + cookie-es@1.2.3: {} + + cookie@1.1.1: {} + + crossws@0.3.5: + dependencies: + uncrypto: 0.1.3 + + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-selector-parser@3.3.0: {} + + css-tree@2.2.1: + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.2.1 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css-what@6.2.2: {} + + cssesc@3.0.0: {} + + csso@5.0.5: + dependencies: + css-tree: 2.2.1 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + defu@6.1.7: {} + + dequal@2.0.3: {} + + destr@2.0.5: {} + + detect-libc@2.1.2: + optional: true + + deterministic-object-hash@2.0.2: + dependencies: + base-64: 1.0.0 + + devalue@5.8.1: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + didyoumean@1.2.2: {} + + diff@8.0.4: {} + + direction@2.0.1: {} + + dlv@1.1.3: {} + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dset@3.1.4: {} + + electron-to-chromium@1.5.362: {} + + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + + entities@4.5.0: {} + + entities@6.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + escalade@3.2.0: {} + + escape-string-regexp@5.0.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + eventemitter3@5.0.4: {} + + expressive-code@0.41.7: + dependencies: + '@expressive-code/core': 0.41.7 + '@expressive-code/plugin-frames': 0.41.7 + '@expressive-code/plugin-shiki': 0.41.7 + '@expressive-code/plugin-text-markers': 0.41.7 + + extend@3.0.2: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + flattie@1.1.1: {} + + fontace@0.4.1: + dependencies: + fontkitten: 1.0.3 + + fontkitten@1.0.3: + dependencies: + tiny-inflate: 1.0.3 + + fraction.js@5.3.4: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-east-asian-width@1.6.0: {} + + github-slugger@2.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + h3@1.15.11: + dependencies: + cookie-es: 1.2.3 + crossws: 0.3.5 + defu: 6.1.7 + destr: 2.0.5 + iron-webcrypto: 1.2.1 + node-mock-http: 1.0.4 + radix3: 1.1.2 + ufo: 1.6.4 + uncrypto: 0.1.3 + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + + hast-util-from-html@2.0.3: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.3 + parse5: 7.3.0 + vfile: 6.0.3 + vfile-message: 4.0.3 + + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.1.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-has-property@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.1 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.1 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + parse5: 7.3.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-select@6.0.4: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + bcp-47-match: 2.0.3 + comma-separated-tokens: 2.0.3 + css-selector-parser: 3.3.0 + devlop: 1.1.0 + direction: 2.0.1 + hast-util-has-property: 3.0.0 + hast-util-to-string: 3.0.1 + hast-util-whitespace: 3.0.0 + nth-check: 2.1.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-to-parse5@8.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-string@3.0.1: + dependencies: + '@types/hast': 3.0.4 + + hast-util-to-text@4.0.2: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + + html-escaper@3.0.3: {} + + html-void-elements@3.0.0: {} + + http-cache-semantics@4.2.0: {} + + import-meta-resolve@4.2.0: {} + + iron-webcrypto@1.2.1: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.3 + + is-docker@3.0.0: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-number@7.0.0: {} + + is-plain-obj@4.1.0: {} + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + jiti@1.21.7: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + kleur@3.0.3: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + longest-streak@3.1.0: {} + + lru-cache@11.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.5.3: + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + source-map-js: 1.2.1 + + markdown-table@3.0.4: {} + + mdast-util-definitions@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + unist-util-visit: 5.1.0 + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.1 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + mdn-data@2.0.28: {} + + mdn-data@2.27.1: {} + + merge2@1.4.1: {} + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.12: {} + + neotraverse@0.6.18: {} + + nlcst-to-string@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + + node-fetch-native@1.6.7: {} + + node-mock-http@1.0.4: {} + + node-releases@2.0.46: {} + + normalize-path@3.0.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + ofetch@1.5.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.4 + + ohash@2.0.11: {} + + oniguruma-parser@0.12.2: {} + + oniguruma-to-es@4.3.6: + dependencies: + oniguruma-parser: 0.12.2 + regex: 6.1.0 + regex-recursion: 6.0.2 + + p-limit@6.2.0: + dependencies: + yocto-queue: 1.2.2 + + p-queue@8.1.1: + dependencies: + eventemitter3: 5.0.4 + p-timeout: 6.1.4 + + p-timeout@6.1.4: {} + + package-manager-detector@1.6.0: {} + + pagefind@1.5.2: + optionalDependencies: + '@pagefind/darwin-arm64': 1.5.2 + '@pagefind/darwin-x64': 1.5.2 + '@pagefind/freebsd-x64': 1.5.2 + '@pagefind/linux-arm64': 1.5.2 + '@pagefind/linux-x64': 1.5.2 + '@pagefind/windows-arm64': 1.5.2 + '@pagefind/windows-x64': 1.5.2 + + parse-latin@7.0.0: + dependencies: + '@types/nlcst': 2.0.3 + '@types/unist': 3.0.3 + nlcst-to-string: 4.0.0 + unist-util-modify-children: 4.0.0 + unist-util-visit-children: 3.0.0 + vfile: 6.0.3 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + path-parse@1.0.7: {} + + piccolore@0.1.3: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + pify@2.3.0: {} + + pirates@4.0.7: {} + + postcss-import@15.1.0(postcss@8.5.15): + dependencies: + postcss: 8.5.15 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.12 + + postcss-js@4.1.0(postcss@8.5.15): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.15 + + postcss-load-config@4.0.2(postcss@8.5.15): + dependencies: + lilconfig: 3.1.3 + yaml: 2.9.0 + optionalDependencies: + postcss: 8.5.15 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.15)(yaml@2.9.0): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.15 + yaml: 2.9.0 + + postcss-nested@6.2.0(postcss@8.5.15): + dependencies: + postcss: 8.5.15 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prismjs@1.30.0: {} + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + property-information@7.1.0: {} + + queue-microtask@1.2.3: {} + + radix3@1.1.2: {} + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + + readdirp@5.0.0: {} + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + + rehype-expressive-code@0.41.7: + dependencies: + expressive-code: 0.41.7 + + rehype-parse@9.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-from-html: 2.0.3 + unified: 11.0.5 + + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + + rehype-stringify@10.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + unified: 11.0.5 + + rehype@13.0.2: + dependencies: + '@types/hast': 3.0.4 + rehype-parse: 9.0.1 + rehype-stringify: 10.0.1 + unified: 11.0.5 + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-smartypants@3.0.2: + dependencies: + retext: 9.0.0 + retext-smartypants: 6.2.0 + unified: 11.0.5 + unist-util-visit: 5.1.0 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + retext-latin@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + parse-latin: 7.0.0 + unified: 11.0.5 + + retext-smartypants@6.2.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unist-util-visit: 5.1.0 + + retext-stringify@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unified: 11.0.5 + + retext@9.0.0: + dependencies: + '@types/nlcst': 2.0.3 + retext-latin: 4.0.0 + retext-stringify: 4.0.0 + unified: 11.0.5 + + reusify@1.1.0: {} + + rollup@4.60.4: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.4 + '@rollup/rollup-android-arm64': 4.60.4 + '@rollup/rollup-darwin-arm64': 4.60.4 + '@rollup/rollup-darwin-x64': 4.60.4 + '@rollup/rollup-freebsd-arm64': 4.60.4 + '@rollup/rollup-freebsd-x64': 4.60.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 + '@rollup/rollup-linux-arm-musleabihf': 4.60.4 + '@rollup/rollup-linux-arm64-gnu': 4.60.4 + '@rollup/rollup-linux-arm64-musl': 4.60.4 + '@rollup/rollup-linux-loong64-gnu': 4.60.4 + '@rollup/rollup-linux-loong64-musl': 4.60.4 + '@rollup/rollup-linux-ppc64-gnu': 4.60.4 + '@rollup/rollup-linux-ppc64-musl': 4.60.4 + '@rollup/rollup-linux-riscv64-gnu': 4.60.4 + '@rollup/rollup-linux-riscv64-musl': 4.60.4 + '@rollup/rollup-linux-s390x-gnu': 4.60.4 + '@rollup/rollup-linux-x64-gnu': 4.60.4 + '@rollup/rollup-linux-x64-musl': 4.60.4 + '@rollup/rollup-openbsd-x64': 4.60.4 + '@rollup/rollup-openharmony-arm64': 4.60.4 + '@rollup/rollup-win32-arm64-msvc': 4.60.4 + '@rollup/rollup-win32-ia32-msvc': 4.60.4 + '@rollup/rollup-win32-x64-gnu': 4.60.4 + '@rollup/rollup-win32-x64-msvc': 4.60.4 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + sax@1.6.0: {} + + semver@7.8.1: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.1 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + optional: true + + shiki@3.23.0: + dependencies: + '@shikijs/core': 3.23.0 + '@shikijs/engine-javascript': 3.23.0 + '@shikijs/engine-oniguruma': 3.23.0 + '@shikijs/langs': 3.23.0 + '@shikijs/themes': 3.23.0 + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + sisteransi@1.0.5: {} + + smol-toml@1.6.1: {} + + source-map-js@1.2.1: {} + + space-separated-tokens@2.0.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.16 + ts-interface-checker: 0.1.13 + + supports-preserve-symlinks-flag@1.0.0: {} + + svgo@4.0.1: + dependencies: + commander: 11.1.0 + css-select: 5.2.2 + css-tree: 3.2.1 + css-what: 6.2.2 + csso: 5.0.5 + picocolors: 1.1.1 + sax: 1.6.0 + + tailwindcss@3.4.19(yaml@2.9.0): + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.15 + postcss-import: 15.1.0(postcss@8.5.15) + postcss-js: 4.1.0(postcss@8.5.15) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.15)(yaml@2.9.0) + postcss-nested: 6.2.0(postcss@8.5.15) + postcss-selector-parser: 6.1.2 + resolve: 1.22.12 + sucrase: 3.35.1 + transitivePeerDependencies: + - tsx + - yaml + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tiny-inflate@1.0.3: {} + + tinyexec@1.2.2: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + totalist@3.0.1: {} + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + ts-interface-checker@0.1.13: {} + + tsconfck@3.1.6(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + + tslib@2.8.1: + optional: true + + type-fest@4.41.0: {} + + typescript@5.9.3: {} + + ufo@1.6.4: {} + + ultrahtml@1.6.0: {} + + uncrypto@0.1.3: {} + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unifont@0.7.4: + dependencies: + css-tree: 3.2.1 + ofetch: 1.5.1 + ohash: 2.0.11 + + unist-util-find-after@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-modify-children@4.0.0: + dependencies: + '@types/unist': 3.0.3 + array-iterate: 2.0.1 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-remove-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-visit: 5.1.0 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-children@3.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + unstorage@1.17.5: + dependencies: + anymatch: 3.1.3 + chokidar: 5.0.0 + destr: 2.0.5 + h3: 1.15.11 + lru-cache: 11.5.0 + node-fetch-native: 1.6.7 + ofetch: 1.5.1 + ufo: 1.6.4 + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + util-deprecate@1.0.2: {} + + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite@6.4.2(jiti@1.21.7)(yaml@2.9.0): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 + rollup: 4.60.4 + tinyglobby: 0.2.16 + optionalDependencies: + fsevents: 2.3.3 + jiti: 1.21.7 + yaml: 2.9.0 + + vitefu@1.1.3(vite@6.4.2(jiti@1.21.7)(yaml@2.9.0)): + optionalDependencies: + vite: 6.4.2(jiti@1.21.7)(yaml@2.9.0) + + web-namespaces@2.0.1: {} + + which-pm-runs@1.1.0: {} + + widest-line@5.0.0: + dependencies: + string-width: 7.2.0 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + xxhash-wasm@1.1.0: {} + + yaml@2.9.0: {} + + yargs-parser@21.1.1: {} + + yocto-queue@1.2.2: {} + + yocto-spinner@0.2.3: + dependencies: + yoctocolors: 2.1.2 + + yoctocolors@2.1.2: {} + + zod-to-json-schema@3.25.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod-to-ts@1.2.0(typescript@5.9.3)(zod@3.25.76): + dependencies: + typescript: 5.9.3 + zod: 3.25.76 + + zod@3.25.76: {} + + zwitch@2.0.4: {} diff --git a/docs-site/src/components/Breadcrumbs.astro b/docs-site/src/components/Breadcrumbs.astro new file mode 100644 index 0000000..149d91a --- /dev/null +++ b/docs-site/src/components/Breadcrumbs.astro @@ -0,0 +1,37 @@ +--- +import { buildBreadcrumbs } from "../data/nav"; + +interface Props { + currentPath: string; +} + +const { currentPath } = Astro.props; +const crumbs = buildBreadcrumbs(currentPath); +// Render only when we have at least one segment beyond just "Docs". +const visible = crumbs.length > 1; +--- + +{ + visible && ( + + ) +} diff --git a/docs-site/src/components/EditOnGitHub.astro b/docs-site/src/components/EditOnGitHub.astro new file mode 100644 index 0000000..b06a05a --- /dev/null +++ b/docs-site/src/components/EditOnGitHub.astro @@ -0,0 +1,33 @@ +--- +import { getEditUrl } from "../data/build-info"; + +interface Props { + currentPath: string; +} + +const { currentPath } = Astro.props; +const editUrl = getEditUrl(currentPath); +--- + + + + Edit this page on GitHub + diff --git a/docs-site/src/components/Footer.astro b/docs-site/src/components/Footer.astro new file mode 100644 index 0000000..132b685 --- /dev/null +++ b/docs-site/src/components/Footer.astro @@ -0,0 +1,116 @@ +--- +const year = new Date().getFullYear(); + +const columns = [ + { + title: "Product", + links: [ + { label: "Documentation", href: "/notify/" }, + { + label: "GitHub", + href: "https://github.com/elloloop/notify", + }, + { + label: "Releases", + href: "https://github.com/elloloop/notify/releases", + }, + ], + }, + { + title: "Resources", + links: [ + { + label: "Container Image", + href: "https://github.com/elloloop/notify/pkgs/container/notify", + }, + { + label: "EntDB", + href: "https://github.com/elloloop/tenant-shard-db", + }, + { + label: "proto/notify", + href: "https://github.com/elloloop/notify/tree/main/proto/notify", + }, + ], + }, + { + title: "Community", + links: [ + { + label: "Issues", + href: "https://github.com/elloloop/notify/issues", + }, + { + label: "Discussions", + href: "https://github.com/elloloop/notify/discussions", + }, + { + label: "License (AGPL-3.0)", + href: "https://github.com/elloloop/notify/blob/main/LICENSE", + }, + ], + }, +]; +--- + + diff --git a/docs-site/src/components/GitHubBadge.astro b/docs-site/src/components/GitHubBadge.astro new file mode 100644 index 0000000..0b5ee48 --- /dev/null +++ b/docs-site/src/components/GitHubBadge.astro @@ -0,0 +1,37 @@ +--- +import { getGithubStars } from "../data/build-info"; + +const stars = await getGithubStars(); +--- + + + + { + stars ? ( + <> + Star + + {stars} + + > + ) : ( + GitHub + ) + } + diff --git a/docs-site/src/components/HeadingAnchors.astro b/docs-site/src/components/HeadingAnchors.astro new file mode 100644 index 0000000..ffa0f4e --- /dev/null +++ b/docs-site/src/components/HeadingAnchors.astro @@ -0,0 +1,46 @@ +--- +// Adds id="kebab-case" to every h2/h3 inside article.prose that doesn't already +// have one. Runs at page load so the on-page TOC and deep-linking work even +// for hand-written .astro pages with no IDs in the source. +--- + + diff --git a/docs-site/src/components/PrevNext.astro b/docs-site/src/components/PrevNext.astro new file mode 100644 index 0000000..1b2ef5b --- /dev/null +++ b/docs-site/src/components/PrevNext.astro @@ -0,0 +1,53 @@ +--- +import { findPrevNext } from "../data/nav"; + +interface Props { + currentPath: string; +} + +const { currentPath } = Astro.props; +const { prev, next } = findPrevNext(currentPath); +const hasAny = !!prev || !!next; +--- + +{ + hasAny && ( + + ) +} diff --git a/docs-site/src/components/SearchButton.astro b/docs-site/src/components/SearchButton.astro new file mode 100644 index 0000000..5067d64 --- /dev/null +++ b/docs-site/src/components/SearchButton.astro @@ -0,0 +1,55 @@ +--- +// Faux-input that opens the SearchModal. Mac shows ⌘K; Windows/Linux shows Ctrl K. +// We render both via the data attribute and let CSS pick the right one. +--- + + + + diff --git a/docs-site/src/components/SearchModal.astro b/docs-site/src/components/SearchModal.astro new file mode 100644 index 0000000..760b2da --- /dev/null +++ b/docs-site/src/components/SearchModal.astro @@ -0,0 +1,180 @@ +--- +// Cmd-K / "/" search modal powered by Pagefind. +// The button (in the header) toggles `data-search-open` on the wrapper. +import "@pagefind/default-ui/css/ui.css"; + +const bundlePath = `${import.meta.env.BASE_URL.replace(/\/$/, "")}/pagefind/`; +--- + +
+ + + + diff --git a/docs-site/src/components/TableOfContents.astro b/docs-site/src/components/TableOfContents.astro new file mode 100644 index 0000000..2b3562b --- /dev/null +++ b/docs-site/src/components/TableOfContents.astro @@ -0,0 +1,125 @@ +--- +// Right-column TOC. Walks the page DOM after load to discover h2/h3 with IDs, +// renders a sticky list, highlights the currently-visible heading via +// IntersectionObserver, and smooth-scrolls on click. +--- + + + + diff --git a/docs-site/src/components/VersionPill.astro b/docs-site/src/components/VersionPill.astro new file mode 100644 index 0000000..e5326fe --- /dev/null +++ b/docs-site/src/components/VersionPill.astro @@ -0,0 +1,84 @@ +--- +const versions = [ + { label: "v0.1 (latest)", current: true }, +]; +--- + + + diff --git a/docs-site/src/data/build-info.ts b/docs-site/src/data/build-info.ts new file mode 100644 index 0000000..01c529c --- /dev/null +++ b/docs-site/src/data/build-info.ts @@ -0,0 +1,106 @@ +// Helpers that run at build time to enrich pages with metadata. + +import { execSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +const REPO_ROOT = path.resolve( + fileURLToPath(new URL("../../../", import.meta.url)), +); + +/** + * Returns the most recent commit date (YYYY-MM-DD) for the .astro file backing + * the given page URL. Falls back to today's date if git is unavailable or the + * file is untracked. + */ +export function getLastUpdated(pageUrlPath: string): string { + const today = new Date().toISOString().slice(0, 10); + const astroPath = pageUrlToAstroPath(pageUrlPath); + if (!astroPath || !existsSync(astroPath)) return today; + try { + const out = execSync( + `git log -1 --format=%cd --date=short -- "${astroPath}"`, + { cwd: REPO_ROOT, stdio: ["ignore", "pipe", "ignore"] }, + ) + .toString() + .trim(); + return out || today; + } catch { + return today; + } +} + +/** + * Returns the GitHub-edit URL for the .astro file backing the given page URL. + */ +export function getEditUrl(pageUrlPath: string): string { + const astroPath = pageUrlToAstroPath(pageUrlPath); + if (!astroPath) { + return "https://github.com/elloloop/notify/tree/main/docs-site"; + } + const rel = path.relative(REPO_ROOT, astroPath); + return `https://github.com/elloloop/notify/edit/main/${rel}`; +} + +function pageUrlToAstroPath(pageUrlPath: string): string | null { + // pageUrlPath looks like "/notify/docs/quickstart/" or + // "/notify/" for the root. Strip the base and the trailing slash. + const base = "/notify"; + let p = pageUrlPath.startsWith(base) ? pageUrlPath.slice(base.length) : pageUrlPath; + p = p.replace(/^\//, "").replace(/\/$/, ""); + const root = path.join(REPO_ROOT, "docs-site", "src", "pages"); + if (!p) return path.join(root, "index.astro"); + // Try `+ Last updated {lastUpdatedDate} +
+ ) + } ++ Error 404 +
++ The page you're looking for doesn't exist or has been moved. Try one of + the popular destinations below, or search the docs with + ⌘K. +
+
+ Two Connect services live in
+ proto/notify/v1.
+ All RPCs accept JSON over HTTP/1 + HTTP/2 (Connect) and
+ binary gRPC — the wire format is negotiated by the
+ Content-Type header. Field numbers are frozen forever;
+ additive fields land at the end with a new number, never reuse,
+ never renumber.
+
+
+
+ The string form used in NotifyRequest.addresses.byChannel
+ keys is the lowercase suffix: in_app,
+ email, web_push, mobile_push,
+ sms, whatsapp. (In-app needs no entry —
+ destination is the user id itself.)
+
+
+
+
+
+ The platform supports at most one registered token per
+ (user, device_type). Re-registering rotates the token
+ in place; it does not create a new device row.
+
+ Backend-to-backend surface. Speaks gRPC and Connect.
+ Auth is X-Notify-Internal-Token at the transport layer.
+
addresses = 9; // userID → {byChannel: {channel: address}}
+}
+
+message ChannelAddresses {
+ map by_channel = 1;
+}
+
+message NotifyResponse {
+ int32 delivered = 1; // provider accepted at least one (user, channel) row
+ int32 pending = 2; // stored but no active provider or no destination
+ int32 failed = 3; // provider returned an error
+}`} lang="proto" />
+
+ Wire path: POST /elloloop.notify.v1.NotificationInternalService/Notify
+
+
+
+ Errors:
+
+ InvalidArgument — tenant_id, notification_id, or user_ids empty.
+ Unauthenticated — missing or mismatched X-Notify-Internal-Token.
+ Internal — store error (DB unreachable, schema migration not applied, etc.).
+
+
+ NotificationClientService
+
+ Client-facing surface for browsers and mobile apps. Speaks Connect
+ HTTP/2. Auth is Authorization: Bearer <JWT>.
+ Tenant is derived from the JWT — request bodies never carry a tenant
+ field.
+
+
+ StreamEvents
+
+
+
+ Long-lived server-stream. The first event is always a
+ HeartbeatEvent carrying the assigned
+ session_id so the client can target
+ AckDataChange back at this exact connection.
+
+
+ Errors:
+
+ Unauthenticated — missing / invalid / expired JWT.
+ Unimplemented — NOTIFY_LIVE_CONNECTIONS_ENABLED=false.
+ Canceled — client disconnect, server shutdown.
+
+
+ GetNotifications
+
+
+
+ Newest-first. Cursor is strict < on
+ created_at_ms. unread_count is the global
+ not-yet-read total, not the count on this page.
+
+
+
+
+ AckNotification
+
+
+
+ Marks one row as Read and stamps
+ read_at_ms. Idempotent — repeated calls are no-ops.
+ Cross-user / cross-tenant attempts return NotFound
+ (never PermissionDenied, which would leak the existence
+ of someone else's row).
+
+
+ AckDataChange
+
+
+
+ Cancels any pending retries for the
+ (idempotency_key, session_id) pair on the in-memory
+ RetryTracker. Sessions older than the process lifetime are silently
+ ignored (the tracker treats unknown entries as no-ops).
+
+
+ RegisterPushToken
+
+
+
+ Upserts the (user, device_type) → token
+ row. Re-calling rotates the token in place. For Web Push the
+ token is the JSON-stringified
+ PushSubscription; for FCM it is the FCM registration
+ token.
+
+
+ Error codes
+
+ Code When
+ InvalidArgumentMissing required field, malformed cursor, empty user_ids on Notify, empty id on AckNotification.
+ UnauthenticatedMissing or invalid Authorization / X-Notify-Internal-Token header. Includes expired JWT.
+ NotFoundAcking a row that doesn't belong to the calling user (or doesn't exist).
+ UnimplementedStreamEvents when NOTIFY_LIVE_CONNECTIONS_ENABLED=false.
+ CanceledStream interrupted (client close, server shutdown).
+ InternalStore error, provider error not absorbed by the orchestrator, panic recovery.
+
+
+ Generating clients
+
+ Use buf against the published proto bundle on a tagged
+ release:
+
+
+
+
+ Or import the generated stubs from the Go module:
+
+
+
+ Related
+
+
+ The email channel delivers a notification as an outbound email. v0.1
+ ships one provider — emailservice, a thin translator over
+ a caller-supplied Sender interface — with slots for
+ ses, acs, smtp, and
+ sendgrid in later waves.
+
+ Any time a user needs an out-of-band message they will see whether or + not your app is open. Password resets, comment notifications, billing + receipts, weekly summaries — anything where the email is the + delivery rail itself, not a fallback hint. +
+ +
+ emailservice is intentionally small: it maps a
+ notify.Message onto a SendEmailInput and
+ hands it to a Sender the caller supplies. The
+ Sender contract is narrow on purpose so the provider
+ package never takes a dependency on any specific email proto.
+
+
+ Wire it like this in library mode:
+
+
+
+ The Name argument is the label that surfaces in
+ observability (notify_send_total{`{provider="emailservice"}`}).
+ Pass "" to default to "emailservice". For
+ an SES-backed Sender, pass "ses"; the channel kind
+ disambiguates.
+
Set the provider block in env vars:
+
+
+
+ With NOTIFY_EMAIL_PROVIDER=none (the default) the channel
+ is disabled. Email rows in a Notify call are stored but
+ counted as Pending rather than dispatched.
+
+ Producer-side, every email send rides the standard
+ Notify RPC. The only additional thing the producer
+ supplies is the address.
+
+
+ + Response on a successful send: +
+
+
+ + The provider maps the notify fields straight through: +
+From = the provider's configured default From address.To = Message.To.Subject = Title.BodyText = Body.BodyHTML = "". (HTML rendering is the Sender's concern; wrap the Sender with a template step if you need it.)
+ On success, Provider.Send returns a
+ Receipt{`{ProviderMessageID, StatusDelivered}`} populated
+ with the Sender's MessageID. The orchestrator persists
+ StatusDelivered on the row and stamps
+ DeliveredAtMS.
+
+ On error, the receipt is StatusFailed and the error is
+ wrapped with %w so the caller can
+ errors.Is / errors.As against the
+ underlying transport error.
+
+ Empty Message.To short-circuits with
+ StatusFailed — every email transport rejects an empty
+ recipient and the platform avoids a wasted RPC.
+
+ All four slot into the same ChannelEmail registry entry
+ via different Sender implementations. None of them
+ require changes to notify.Notifier; the contract
+ stops at the provider boundary.
+
+ In-app delivery is a real-time push to any of the user's currently
+ open client connections via the
+ NotificationClientService.StreamEvents server-stream.
+ No provider configuration is needed — when the live-connections
+ subsystem is on, the in-app provider is auto-registered.
+
+ Anything you want to surface immediately in the active session: a new
+ chat message, an updated task list, a "your colleague joined the
+ document" presence event. The same rows are stored for catch-up via
+ GetNotifications, so a user who comes back online sees
+ the missed traffic.
+
+ The orchestrator's in-app provider holds a reference to the
+ realtime Registry:
+
Notify with DELIVERY_CHANNEL_IN_APP.Send.StreamEvent to every live connection for the recipient via Registry.Push.StatusDelivered. If 0 connections (user offline) → StatusPending.+ Why this design. + An in-app row to a user with no live connections gets ++ +StatusPending— not a lying +StatusDelivered. The status field is used downstream + (unread filter, read-receipt analytics, dashboards) and it would + silently mislead consumers if "pushed to nobody" looked the same as + "successfully delivered". The row remains available via +GetNotificationsfor catch-up; when the user reconnects + they see it via the regular inbox flow. +
+ StreamEvents is a server-streaming RPC that multiplexes
+ three event kinds plus heartbeats over one connection:
+
+
+ notification — the platform delivered a new notification row to this user.data_change — an opaque "something upstream that you care about may have changed" hint. The client re-fetches via its own API. Carries an idempotency_key for at-least-once retry.heartbeat — keepalive carrying the server clock. First heartbeat after handshake carries the assigned session_id so the client can target AckDataChange at this exact stream.Authorization: Bearer <JWT> and a DeviceType.Conn, registers it under the user.HeartbeatEvent carrying the assigned session_id.NOTIFY_LIVE_HEARTBEAT_INTERVAL (default 30s); the in-app provider pushes notifications + data-change events as they arrive.
+
+
+ Setting NOTIFY_LIVE_CONNECTIONS_ENABLED=false disables
+ the subsystem entirely. StreamEvents returns
+ CodeUnimplemented, the in-app provider is not
+ registered, and in-app rows stay StatusPending for
+ inbox-only consumption.
+
async (req) => {
+ req.header.set("Authorization", \`Bearer \${getAccessToken()}\`);
+ return next(req);
+ },
+ ],
+});
+const client = createClient(NotificationClientService, transport);
+
+let sessionId = "";
+
+for await (const ev of client.streamEvents({ deviceType: "DEVICE_TYPE_BROWSER" })) {
+ sessionId = ev.sessionId || sessionId;
+
+ switch (ev.event.case) {
+ case "notification": {
+ const n = ev.event.value.notification!;
+ console.log("notification:", n.title, "—", n.body);
+ showInUI(n);
+ break;
+ }
+ case "dataChange": {
+ const dc = ev.event.value;
+ console.log("data change:", dc.subjectRef);
+ await refreshUpstream(dc.subjectRef);
+ // ACK so the server stops retrying.
+ await client.ackDataChange({
+ idempotencyKey: dc.idempotencyKey,
+ sessionId,
+ });
+ break;
+ }
+ case "heartbeat":
+ console.log("heartbeat @", ev.event.value.timestampMs);
+ break;
+ }
+}`} lang="ts" title="stream.ts" />
+
+ Reconnect strategy
+
+ Streams die for legitimate reasons: laptop sleep, server rolling
+ deploy, mobile carrier handover. The client should reconnect with
+ exponential backoff and a cap:
+
+ setTimeout(r, backoffMs + Math.random() * 250));
+ backoffMs = Math.min(backoffMs * 2, maxMs);
+ }
+ }
+}`} lang="ts" />
+
+
+ Reconnecting is safe: any in-flight events the client missed are
+ still in the store and available via
+ GetNotifications. Data-change events the client did not
+ ack will be retried by the server's RetryTracker until
+ either the ack arrives or
+ NOTIFY_LIVE_RETRY_MAX_ATTEMPTS is exhausted.
+
+
+ Sending an in-app notification
+
+ No address required — the in-app channel's destination is the user
+ id itself.
+
+
+
+ Backpressure
+
+ Each Conn has a buffered EventCh (default
+ 64 deep). Registry.Push does a non-blocking send; a
+ connection whose buffer is full has the event dropped and logged
+ (event_queue_full). It stays registered for future
+ events — one slow client never blocks the producer or the rest of
+ the recipients.
+
+
+ The buffer size is currently fixed at
+ DefaultEventBuffer = 64. If a real client genuinely
+ needs deeper queueing, it's a follow-up wave; for now the right move
+ is to keep the client UI thread non-blocking on event arrival.
+
+
+ Related
+
+ - Realtime Engine — Registry / RetryTracker internals
+ - Subscribe over SSE — full client example with reconnect
+ - Ack a notification — marking the row read
+
+
+ Mobile push delivers notifications to a native Android or iOS app via
+ Firebase Cloud Messaging (HTTP v1). v0.1 ships the FCM provider;
+ APNs / Azure / AWS slot into the same ChannelMobilePush
+ registry entry in later waves.
+
+ Anything you'd want to push to a native app whose process isn't + currently running. Chat messages, calendar reminders, system + alerts, anything that needs the OS notification surface. +
+ +
+ The provider is a ~150-line hand-rolled HTTP v1 client — we
+ deliberately do not use the Firebase Admin SDK because v1 send is
+ one POST and JWT auth is a single call into
+ golang.org/x/oauth2/google; the Admin SDK's transitive
+ closure isn't worth it for the surface notify exercises.
+
+
+
+ The provider parses the service-account JSON eagerly during
+ fcm.New, so a malformed credential blob fails at
+ construction — not on first send. The OAuth2 token source is built
+ once and reused across sends.
+
+
+
+ if (!task.isSuccessful) {
+ Log.w("notify", "Fetching FCM registration token failed", task.exception)
+ return@OnCompleteListener
+ }
+ val token = task.result
+ // Send token to notify.
+ notifyApi.registerPushToken(
+ RegisterPushTokenRequest(
+ deviceType = DeviceType.DEVICE_TYPE_ANDROID,
+ token = token,
+ )
+ )
+ })`} lang="kotlin" />
+
+ iOS — registering an FCM token
+
+
+
+ Apple Push Notification service (APNs) sits under FCM here — your
+ Xcode project registers for remote notifications and Firebase
+ surfaces an FCM token. notify treats it as one address, regardless
+ of upstream pipeline.
+
+
+ Sending a mobile push
+
+
+ Wire payload
+ The provider posts this body to /v1/projects/$PROJECT/messages:send:
+
+
+
+ The data map comes from Message.Data; it is
+ delivered to your client as a flat string-to-string map alongside
+ the notification.
+
+
+ Handling ErrUnregisteredToken
+
+ FCM signals "this device token is no longer valid" two ways:
+ HTTP 404 + status: NOT_FOUND, or any
+ response with details[].errorCode == "UNREGISTERED".
+ The provider folds both into ErrUnregisteredToken:
+
+
+
+
+ The provider itself never auto-purges. See
+ Channels & Providers
+ for the rationale.
+
+
+ Testing without hitting FCM
+
+ Both the HTTP client and the OAuth2 token source are injectable via
+ constructor options. Tests use an httptest.Server and a
+ static-token oauth2.TokenSource so they never reach
+ google.com:
+
+
+
+ Receipts
+
+ On success the receipt carries the FCM message name (e.g.
+ projects/my-project/messages/100) as
+ ProviderMessageID and StatusDelivered.
+
+
+ Future providers
+
+ - APNs direct — bypass FCM for iOS, P8 JWT auth.
+ - Azure Notification Hubs.
+ - AWS SNS Mobile Push.
+
+
+ Related
+
+
+ The SMS channel delivers a text message via Twilio's
+ Messages API. The same Twilio Client backs
+ both this provider and the WhatsApp
+ provider — the channel-specific rules live on the provider, not
+ the client.
+
+ OTP codes, urgent alerts, anything that needs to reach a user when + they don't have your app installed and aren't reading email. +
+ +
+
+ + Alternatively configure a Messaging Service for sender pools / pool-of-numbers: +
+
+
+
+ When both From and MessagingServiceSID are
+ set, the provider prefers the Messaging Service.
+
+
+
+
+ Response:
+
+
+
+ The provider does a cheap structural E.164 check before calling
+ Twilio — empty, missing +, non-digit body, or length
+ outside 6..15 short-circuits to
+ StatusFailed:
+
15 {
+ return fmt.Errorf("To: %q has invalid length", s)
+ }
+ for _, r := range digits {
+ if r < '0' || r > '9' {
+ return fmt.Errorf("To: %q contains non-digit characters", s)
+ }
+ }
+ return nil
+}`} lang="go" />
+
+
+ This is intentionally structural, not a phone-number validity check
+ — Twilio remains the authority on whether a number can actually
+ receive SMS. We just avoid burning a request on obvious garbage.
+
+
+ Body composition
+
+ SMS does not carry a separate subject line. The provider collapses
+ Title and Body with Body winning when non-empty:
+
+
+ - Both supplied → only
Body is sent.
+ - Only
Title → Title is sent.
+ - Neither → empty body; Twilio rejects with code 21602.
+
+
+ Testing with httptest
+
+ Because the provider uses Go's net/http via an
+ injectable http.Client, you can swap the Twilio endpoint
+ for an httptest.Server and drive any response shape from
+ a unit test. The full test lives in
+ channels/twilio/sms_test.go;
+ abbreviated shape:
+
+
+
+ Receipts and error handling
+
+ On success the receipt carries Twilio's SID as
+ ProviderMessageID and StatusDelivered. On
+ failure the wrapped error is propagated and the row is recorded as
+ StatusFailed.
+
+
+ Related
+
+ - WhatsApp channel — same client, different prefix
+ - Channels & Providers
+ - Send a notification
+
+
+ Web Push delivers notifications to a browser even when your tab is
+ closed, via the W3C Push API (RFC 8030) with VAPID (RFC 8292) for
+ application-server identification and the aes128gcm
+ content encoding (RFC 8291) for payload encryption.
+
+ Anything you'd push to a desktop or mobile-web user who isn't + currently looking at your app. Common cases: chat messages while the + tab is in the background, new email/comment alerts, urgent system + notifications. +
+ +
+ The provider implementation lives in
+ channels/webpush and wraps
+ github.com/SherClockHolmes/webpush-go for the on-wire
+ encryption work. notify owns:
+
endpoint / keys.auth / keys.p256dh early).mailto:, https://, or bare user@host).TTL (60s) and Urgency (normal), overridable via options.200/201/202 → StatusDelivered; 410 Gone → ErrSubscriptionGone; everything else → StatusFailed with the body text.
+
+
+
+
+ The contact email is required by RFC 8292; push services may reject
+ anonymous senders. mailto: and https://
+ URLs are both accepted; a bare user@host string is
+ auto-prefixed with mailto:.
+
{
+ const data = event.data?.json() ?? {};
+ event.waitUntil(
+ self.registration.showNotification(data.title || "Notification", {
+ body: data.body,
+ data: data.data, // your deep-link key/values
+ icon: "/icon-192.png",
+ })
+ );
+});
+
+self.addEventListener("notificationclick", (event) => {
+ event.notification.close();
+ event.waitUntil(
+ clients.openWindow(event.notification.data?.url || "/")
+ );
+});`} lang="js" title="public/sw.js" />
+
+ Browser side — subscribe + register
+ c.charCodeAt(0));
+}
+
+async function subscribePush(vapidPublicKey) {
+ const reg = await navigator.serviceWorker.register("/sw.js");
+ await navigator.serviceWorker.ready;
+
+ let sub = await reg.pushManager.getSubscription();
+ if (!sub) {
+ sub = await reg.pushManager.subscribe({
+ userVisibleOnly: true,
+ applicationServerKey: urlBase64ToUint8Array(vapidPublicKey),
+ });
+ }
+
+ // Register with notify.
+ await client.registerPushToken({
+ deviceType: "DEVICE_TYPE_BROWSER",
+ token: JSON.stringify(sub), // the whole PushSubscription JSON
+ });
+}`} lang="ts" title="subscribe.ts" />
+
+ Sending a web push
+
+ The provider expects the JSON-stringified PushSubscription
+ as the address. In production this comes from the device store
+ (RegisterPushToken writes it); in a one-off call you can
+ inline it in the Addresses map.
+
+
+
+ Handling ErrSubscriptionGone
+
+ HTTP 410 from the push service means the user has unsubscribed or
+ uninstalled the browser. The provider surfaces this as a sentinel
+ you can match with errors.Is:
+
+
+
+ Why the provider does not auto-purge
+
+ Why this design.
+ Both web push (ErrSubscriptionGone) and FCM
+ (ErrUnregisteredToken) expose token-is-dead as a typed
+ error. The provider never deletes the corresponding device row
+ itself because (a) the device store is owned by the orchestrator,
+ not the provider, and (b) a future caller may want to keep the row
+ for audit or to attempt re-registration. Providers stay stateless;
+ purge logic lives at the call site.
+
+
+ Receipts
+
+ Push services don't return a message id, so successful receipts
+ have an empty ProviderMessageID. The status is
+ StatusDelivered on 200/201/202.
+
+
+ Related
+
+ - Mobile push channel — sibling channel for native apps
+ - Register a push token — full round-trip example
+ - Channels & Providers
+
+
+ The WhatsApp channel delivers messages via Twilio's WhatsApp Business
+ pipeline. It reuses the same Twilio Client as
+ SMS — only the wire-level
+ formatting differs: both addresses are prefixed with
+ whatsapp: so Twilio routes through the WhatsApp pipeline
+ instead of SMS.
+
+ Where WhatsApp is the dominant messaging app (LatAm, India, + Indonesia, much of EMEA) and your users have opted in via a + pre-approved template flow on the Twilio side. +
+ +
+
+
+ Pass the From number without the whatsapp:
+ prefix — the provider adds it for you. Same Twilio credentials can
+ drive both the SMS and WhatsApp channels; the channel kind
+ disambiguates in the registry.
+
+
+ + The producer supplies the recipient as a bare E.164 number — same as + SMS — and the channel decides the routing. +
+
+
+
+ The provider builds the Twilio request with the
+ whatsapp: prefix automatically:
+
+
+
+ ensureWhatsAppPrefix is idempotent — calling it on
+ "whatsapp:+15555550000" returns the same string.
+ Callers that have already prefixed their config (legacy code paths,
+ custom Sender wrappers) won't get a double prefix.
+
+ The provider strips the whatsapp: prefix for validation
+ and runs the same E.164 structural check used by SMS. Empty,
+ non-E.164, or length-out-of-range addresses short-circuit to
+ StatusFailed without burning a Twilio request.
+
+ Same as SMS: Body wins, falling back to Title.
+ Twilio's WhatsApp pipeline accepts plain text for in-session messages
+ and approved template references for out-of-session — at the proto
+ level notify does not model templates today; pass the rendered text
+ in Body.
+
+ Messaging Services do not take the whatsapp: prefix —
+ the prefix only applies to bare From numbers. The
+ provider handles this:
+
+
+
+ Same shape as SMS: success returns the Twilio SID as
+ ProviderMessageID, failure surfaces the error.
+
+ notify is one Go package (github.com/elloloop/notify) that
+ depends on nothing concrete, plus pluggable sub-packages for each
+ backend. The container (cmd/notifyd) is a thin wiring
+ layer; the library mode is the same logic with the wiring deferred to
+ the caller.
+
+ notify.Notifier is the orchestrator. It owns a
+ Store and a ProviderRegistry and exposes a
+ single method:
+
+
+ For each (user, channel) the orchestrator:
Notification row stamped with the request fields.Store.CreateNotification — idempotent on (TenantID, UserID, NotificationID). A repeat call returns the existing row.channel in the registry. If none, increments Pending and continues.req.Addresses (or, for in-app, the user id itself). Missing address ⇒ Pending.Provider.Send. On error, increments Failed and records StatusFailed via UpdateStatus. On success, records the receipt's status (default StatusDelivered).
+ Storage failures abort the whole call. Per-channel delivery failures
+ do not — the fan-out completes and the
+ (delivered, pending, failed) counters surface what
+ happened.
+
+
+
+ The root package has no concrete dependencies — it imports only
+ standard library and connect-go (for typed errors). That
+ is what makes it library-mode-friendly: a downstream service can
+ embed Notifier with its own Store and never
+ pull in Twilio or FCM or pgx.
+
+ cmd/notifyd is the production entry point. It does three
+ things and only three:
+
server.Config from environment variables.server.New and calls Run.
+ The internal/server package owns the Connect handlers,
+ auth middleware, lifecycle plumbing, and the in-app provider that
+ bridges the realtime engine. It depends on the root package
+ (interfaces), the generated proto stubs (wire types), and the
+ realtime + store / channel sub-packages — exactly the surface a
+ production deployment wires.
+
|
+| · StreamEvents (SSE / Connect server-stream) |
+| · GetNotifications, AckNotification, AckDataChange |
+| · RegisterPushToken |
+| │ |
+| ┌───────────┴────────────────────────────────┐ |
+| ▼ ▼ |
+| notify.Notifier ◄────── realtime.Registry / RetryTracker |
+| │ (only when LiveConnections.Enabled) |
+| ▼ |
+| notify.Store |
+| │ |
+| ▼ |
+| memory · postgres · entdb |
+| |
+| :9090 /healthz · /metrics (Prometheus exposition) |
++---------------------------------------------------------------------------+`} lang="text" />
+
+ Why two services, not one
+
+ The producer surface and the recipient surface have orthogonal auth
+ stories: producers are trusted backend code, recipients are
+ untrusted end users. Splitting them into two Connect services means:
+
+
+ - The internal token never touches a browser; the user JWT never reaches a producer.
+ - Operators can publish the recipient port behind a CDN/WAF and keep the producer port reachable only from the cluster network — at the listener level, not via middleware that could regress.
+ - The schemas evolve independently. Adding a producer-side field never expands the recipient API by accident.
+
+
+ Why notification IDs are caller-supplied
+
+ Why this design.
+ The platform is idempotent on
+ (tenant_id, user_id, notification_id) because the producer
+ knows when a logical event is "the same one" and the platform does not.
+ Examples: a webhook retry, a Kafka redelivery, an at-least-once event
+ fanout in the producer. If notify generated the id, the producer
+ would either have to send "same logical event" twice (duplicate
+ delivery) or maintain its own dedupe table that mirrors notify's. We
+ push the decision to the source of truth — the producer.
+
+
+ Related
+
+ - Channels & Providers — the channel ↔ provider taxonomy in detail
+ - Store & Conformance — the driver-agnostic spec
+ - Realtime Engine — Registry, RetryTracker, Conn
+ - Auth Model — JWT + internal token
+
++ notify validates two kinds of credentials, one per service: +
+NotificationClientService (browser / mobile) — Authorization: Bearer <JWT>, HS256 against NOTIFY_AUTH_JWT_SECRET.NotificationInternalService (backend producers) — X-Notify-Internal-Token, constant-time compare against NOTIFY_INTERNAL_TOKEN.
+ Anything calling Notify (sending a notification) is a
+ backend producer and uses the internal token. Anything streaming a
+ user's own notifications, reading their inbox, marking rows read or
+ registering a push token is the recipient SPA / mobile app and uses
+ a JWT issued for that user.
+
+ The validator is a thin wrapper around github.com/golang-jwt/jwt/v5:
+
+
+ Token requirements:
+jwt.WithValidMethods).sub claim must be a non-empty string. It becomes Claims.UserID.tenant (or tenant_id) claim must be a non-empty string. It becomes Claims.TenantID.NOTIFY_AUTH_JWT_ISSUER is set, the iss claim must match.NOTIFY_AUTH_JWT_AUDIENCE is set, the aud claim must match.exp and nbf are validated with NOTIFY_AUTH_JWT_LEEWAY (default 30s) slack.A valid token results in a typed Claims on the request context:
+
+
+ Handlers read it via ClaimsFromContext(ctx). The raw
+ jwt.MapClaims is intentionally not exposed — handlers
+ never reach for fields the platform did not promise.
+
+ Why this design. + Every client-side RPC scopes data by+ +(tenant, user). If a + client could pick the tenant via the request body, a logged-in user + could mint requests that read another tenant's data simply by typing + a different tenant_id. The platform takes +Claims.TenantIDfrom the JWT and ignores any + request-body tenant field on the client surface — the producer + surface is the only place tenant is supplied via the body, and that + surface is internal-token gated. +
+
+
+ subtle.ConstantTimeCompare avoids the timing side-channel
+ of ==. The container refuses to boot with
+ DevMode=false and no NOTIFY_INTERNAL_TOKEN
+ set — there is no "internal calls go unauthenticated" footgun.
+
+ NOTIFY_AUTH_DEV_MODE=true relaxes the boot-time validation
+ so local development doesn't need a JWT signer:
+
Bearer dev:<userid>:<tenant>[:<email>].NOTIFY_INTERNAL_TOKEN is also unset.Sample dev tokens:
+
+
+ + Never enable dev mode in production. The container + rejects boots that try to disable both real auth modes + simultaneously. +
+ ++ Authentication answers "is this user who they say they are". notify + answers it via JWT verification. +
+
+ Authorization — "may this authenticated user read this row?" — is
+ enforced at the handler boundary by scoping every Store call to
+ (claims.TenantID, claims.UserID). The standout case is
+ AckNotification: Store.UpdateStatus takes
+ only tenantID + id, not userID,
+ so the handler does a GetNotification(claims.TenantID,
+ claims.UserID, id) first. A cross-user attempt surfaces as
+ ErrNotFound, which maps to CodeNotFound on
+ the wire — never CodePermissionDenied, which would leak
+ the existence of someone else's row.
+
+ A channel is the kind of delivery rail: + in-app, email, web push, mobile push, SMS, WhatsApp. A + provider is the concrete backend that handles that + rail — Twilio for SMS, VAPID for web push, the emailservice Sender for + email, etc. notify lets you mix and match. +
+ ++ Whenever you ask "how do I send X over Y": X is the channel, Y is + the provider. The channel decides which destination shape applies + (an email address, an E.164 phone number, a device token, the user id); + the provider decides which API does the talking and which credentials + sit on disk. +
+ +
+
+
+ The proto enum (DeliveryChannel) carries the same set,
+ with frozen field numbers. Adding a new channel is an additive proto
+ change plus a new ChannelKind constant in
+ notify/model.go — never reuse, never renumber.
+
| Channel | Shipped providers | Future providers |
|---|---|---|
email | emailservice (DI Sender) | ses, acs, smtp, sendgrid |
sms | twilio | sns, acs |
whatsapp | twilio | meta |
web_push | vapid | — |
mobile_push | fcm | apns, azure, aws |
in_app | built-in realtime engine | — |
+ Every provider satisfies one tiny contract: +
+
+
+
+ On error the orchestrator records the row as StatusFailed
+ and counts it in NotifyResult.Failed. The rest of the
+ fan-out continues.
+
+ A channel is active when a provider has been registered for it.
+ Unregistered channels are still legal in a NotifyRequest —
+ the row is stored and counted as Pending, but no provider
+ call happens.
+
In library mode you register explicitly:
+
+
+
+ In container mode cmd/notifyd registers providers based on
+ the configured NOTIFY_<CHANNEL>_PROVIDER blocks. An
+ empty block leaves the channel disabled.
+
+ The orchestrator resolves an address per (user, channel)
+ in this order:
+
NotifyRequest.Addresses[userID][channel] is set, use it.in_app, use the user id itself.Pending.+ The Addresses map is opt-in. For email/SMS/WhatsApp/web-push/mobile-push + you almost always supply it (or your producer reads it from your own + user store and assembles the map before calling Notify). For in-app + you almost never need it. +
+ +
+
+ + Why this design. + Both web push (+ +ErrSubscriptionGoneon HTTP 410) and FCM + (ErrUnregisteredTokenon UNREGISTERED / NOT_FOUND) expose + "the recipient endpoint is gone" as a sentinel error. The provider + never deletes the correspondingDevicerow itself — it + just surfaces the sentinel. Why? Because the device store is owned by + the orchestrator / caller, and a future caller may want to keep the + row for audit, retry from a fresher token, or analytics. Providers + are stateless from the platform's POV. +
+ notify carries a tenant_id on every persisted row and
+ every RPC. The model is deliberately lightweight: tenants are an
+ isolation key, not a billing identity or a feature gate.
+
tenant claim and want that to flow through to the inbox.+ Two paths, depending on the service: +
+NotificationInternalService) — the body's tenant_id field. The internal-token check confirms the caller is trusted; the caller is expected to know which tenant it is acting on behalf of.NotificationClientService) — the JWT's tenant (or tenant_id) claim. The request body has no tenant field at all on these RPCs.
+ Every Store method that reads or writes carries a tenantID
+ parameter, and every implementation honors it as a hard filter:
+
CreateNotification — the (TenantID, UserID, NotificationID) triple is the uniqueness key. Re-using a notification id under a different tenant produces a new row, not an idempotent hit.GetNotification — a row that exists under a different tenant returns ErrNotFound. The same goes for cross-user reads under the same tenant.QueryUserNotifications — scoped by (TenantID, UserID). The cursor and unread count are also scoped.UpsertDevice / ListDevices — same shape.
+ The conformance suite exercises this with a dedicated
+ UserIsolation subtest that creates three rows
+ ((acme, u1, x), (acme, u2, x),
+ (beta, u1, x)) and asserts that
+ QueryUserNotifications for (acme, u1)
+ returns exactly the first.
+
+ Notify is happy to serve many tenants from one container — the + tenant id is just data. There is no per-tenant configuration knob + today, no per-tenant rate limiting, no per-tenant provider override. + Provider configuration is global, and the same providers serve every + tenant. +
+
+ If you need per-tenant behaviour (e.g. different SMS sender numbers
+ per customer), the right shape today is library mode: wire one
+ Notifier per tenant with a tenant-specific
+ ProviderRegistry sharing a single underlying
+ Store. The container will grow this as a follow-up wave;
+ until then library mode owns the customization story.
+
+ EntDB itself is tenant-sharded. The notify EntDB driver maps onto a
+ single EntDB tenant per process via NOTIFY_ENTDB_TENANT_ID:
+
+
+
+ Inside notify's logical model, you can still serve many tenants over
+ that one EntDB shard — they all live under the same EntDB tenant,
+ differentiated by the tenant_id field on each row. The
+ EntDB tenant is the storage shard; the notify tenant is the
+ application-level isolation key.
+
+ Operationally large customers may want a dedicated EntDB shard per
+ customer. The shape is to run a notify container per shard,
+ pointing each at its dedicated NOTIFY_ENTDB_TENANT_ID.
+
+ The Postgres schema carries tenant_id as a column on
+ both tables. Every WHERE clause includes it; every UNIQUE constraint
+ starts with it. There is no Postgres-level row security or schema
+ fan-out — the application-level filter is the contract.
+
+
+ + Why this design. + There is no "fanout to all tenants" RPC, no "list every tenant" + admin endpoint, no global query. The tenant boundary is a hard wall + because the auth boundary is — the JWT pins the recipient to one + tenant, and the producer surface is per-call tenant-scoped. + Cross-tenant operations would either need an admin role concept the + platform doesn't model (yet) or a producer with super-user reach + that defeats the isolation promise. Neither pays off enough at v0.1 + to be worth the footgun. ++ +
+ The realtime package is the in-memory engine that powers
+ in-app delivery: a per-user connection registry, a buffered per-conn
+ channel, and an at-least-once retry tracker. It is generic over the
+ event payload (Registry[T any]), so it has zero
+ dependency on the notify proto or any transport — the server
+ instantiates Registry[*pb.StreamEvent] at boot.
+
+ You only need to know this exists if you are: +
+NOTIFY_LIVE_CONNECTIONS_ENABLED).
+
+
+ The server constructs one Registry shared by every
+ connection, and one RetryTracker shared by every
+ in-flight retry. StreamEvents creates a fresh
+ Conn on each open, registers it, and tears it down on
+ exit. The in-app provider's Send looks up live
+ connections for the target user via Registry.Push and
+ returns StatusDelivered if any accepted the event,
+ StatusPending if none did.
+
+ Why this design. + A stalled client must never block the producer. Every+ +Push+ is a non-blocking{`select { case ch <- ev: ... default: ... }`}; + a connection whose buffer is full has the event dropped (logged via +event_queue_full) but stays registered for future + events. The alternative — backing up the orchestrator behind one slow + client — would let one misbehaving recipient stall delivery for + everyone else on the same notifier. +
+ This is a subtle invariant documented inline in
+ realtime/conn.go:
+
+ Data-change hints (DataChangeEvent) are best-effort hints
+ that something the client cares about may have changed upstream.
+ The client re-fetches its own state via its own API. For these
+ hints, the platform offers at-least-once via the
+ RetryTracker:
+
DataChangeEvent with an idempotency_key.RetryTracker.Track(ctx, key, []connID, sendCallback) spawns a goroutine per (key, connID) that re-sends on NOTIFY_LIVE_RETRY_INTERVAL ticks.{`AckDataChange{idempotency_key, session_id}`}.
+ Budget is bounded: NOTIFY_LIVE_RETRY_MAX_ATTEMPTS (default
+ 3) caps retries; a value of 0 disables retries entirely. On shutdown
+ CancelAll closes every in-flight retry so they don't
+ leak.
+
+ Set NOTIFY_LIVE_CONNECTIONS_ENABLED=false. The container
+ never constructs a Registry, the in-app provider is not registered,
+ and StreamEvents returns
+ CodeUnimplemented. In-app notifications remain
+ StatusPending; clients catch up via
+ GetNotifications.
+
+ This is a legitimate deployment shape when you only care about the + durable side of the platform — e.g. a backend running notify in + library mode purely for stored notification fanout, with the live + surface served by a separate gateway. +
+ +
+ notify treats persistence as a pluggable concern. There is one
+ Store interface, three shipped drivers
+ (memory, postgres, entdb), and a
+ shared conformance suite that every driver runs. Adding a new driver
+ is a matter of implementing the interface and passing the suite.
+
+
+ Two sentinel errors round out the contract:
+
+
+ + Why this design. + The realtime registry, the retry tracker, and per-user channel + preferences (future) all live outside the Store. The Store + holds only what is durable and shared across replicas: notification + rows and device registrations. Keeping the interface tight makes new + drivers cheap, makes the conformance suite tractable, and prevents + "the store" from becoming a leaky god-object. ++ +
+ store/conformance.RunConformance exercises every driver
+ against the same 24 leaf subtests across six categories. A failure
+ path like TestConformance/postgres/Concurrency/ConcurrentCreate_SameKey_SingleWinner
+ points straight at the driver and the exact semantic that broke.
+
| Category | Subtests | What it catches |
|---|---|---|
| Core CRUD | 8 | create / get / idempotency / status transitions / paging basics / unread filter / device upsert / user isolation |
| Pagination | 2 | full-set traversal across many pages, strict-less-than cursor semantics |
| FreshTenant | 3 | queries against an unseen tenant return empty, not error |
| RoundTrip | 3 | adversarial string values (emoji, unicode, SQL-shaped, leading/trailing whitespace, 10k chars), int64 fidelity, large payload bodies |
| Concurrency | 5 | distinct-keys-no-lost-writes, same-key-single-winner, device-upsert-same-key-single-row, update-status concurrency, read-your-writes |
| KeyEdge | 3 | long notification ids (256 chars), separator-byte collisions in composite keys, case-sensitive device types |
+ All shipped drivers pass 24/24: +
+store/<driver>/ with the implementation.store/<driver>/<driver>_test.go that runs the suite:
+ .github/workflows/conformance.yml's matrix.go test ./store/<driver>/... -race -count=1. Any failing subtest gets a one-line root-cause attribution in store/<driver>/CONFORMANCE.md — see the existing two as templates.
+ The CI check Conformance / <driver> is what branch
+ protection pins. A driver that doesn't pass cannot land.
+
+ Production-shape Docker deployment of notify. For the "what is the
+ image" overview see Installation
+ → Docker; this page covers networking, secrets, healthchecks,
+ multi-arch, and a worked docker-compose stack.
+
docker-compose.
+ The published image is built for linux/amd64 and
+ linux/arm64. Docker pulls the right variant
+ automatically. To verify:
+
+
+ + Every published tag is signed via cosign keyless OIDC. Verify before + deploying: +
+
+
+
+
+
+ Compose's .env file is fine for dev. Production:
+
+ notify speaks plaintext HTTP/2 (h2c) inside the container — terminate + TLS upstream. Typical shapes: +
+app.example.com {`{`} reverse_proxy notify:8080 {`}`}.grpc_pass for the internal port; proxy_pass for the client port (Connect over HTTP/1 works fine). /tmp/h.json; then
+ cat /tmp/h.json
+ break
+ fi
+ sleep 1
+done
+
+docker rm -f notify-smoke`} lang="bash" />
+
+
+ The same smoke is what .github/workflows/release.yml
+ runs against every published image before tagging it for promotion.
+
+
+ Resource sizing
+
+ notify's hot path is small: one Notifier.Notify is one
+ store insert plus one provider call per channel. The realtime
+ engine is in-memory and a per-conn buffered channel. Reasonable
+ starting points:
+
+
+ - CPU: 0.5 vCPU is enough for moderate traffic; the upper bound is dominated by provider call latency.
+ - Memory: 64 MiB baseline; budget ~5 KiB per active stream connection.
+ - Network: long-lived streams open a TCP connection per recipient — size your ephemeral port range accordingly if you expect 10k+ concurrent clients.
+
+
+ Related
+
+
+ notify publishes container images via
+ .github/workflows/release.yml. A push to any
+ v* tag triggers test → docker-smoke → build-and-push →
+ cosign sign → Trivy scan → proto bundle release. The exact same
+ workflow is also workflow_dispatch-triggerable for
+ one-off rebuilds.
+
+
+
+
+
+ Re-runs every gate that PRs run. Plain
+ go build ./... + go vet ./... +
+ go test -race over everything except the
+ driver-specific tree. A broken build must not publish.
+
+ Builds the server target locally
+ (docker buildx build --load), runs the container with
+ NOTIFY_AUTH_DEV_MODE=true +
+ NOTIFY_STORE_DRIVER=memory, and curls
+ /healthz until it sees
+ {`{"status":"ok"}`}. Fails the run if the container
+ crashes during boot.
+
/tmp/notify-health.json; then
+ grep -q '"status":"ok"' /tmp/notify-health.json
+ exit 0
+ fi
+ sleep 1
+done
+docker logs "$cid"
+exit 1`} lang="bash" title="docker-smoke (abridged)" />
+
+ 3. build-and-push
+
+ Cross-builds for linux/amd64 + linux/arm64
+ via docker/setup-qemu-action +
+ docker/setup-buildx-action, then publishes to
+ ghcr.io/elloloop/notify tagged from
+ docker/metadata-action — semver components, branch
+ name, sha, and latest on a tag push.
+
+
+ Permissions are scoped: contents: read,
+ packages: write (to push to ghcr),
+ id-token: write (for cosign keyless OIDC).
+
+
+
+
+ 4. scan-image
+
+ Post-publish, Trivy is pinned by SHA (defending against the
+ March 2026 advisory) and scans the published image. HIGH and
+ CRITICAL CVEs cause the workflow to fail; results are uploaded as
+ SARIF to GitHub Code Scanning.
+
+
+
+
+ 5. release-protos
+
+ On a tag push only. Packs the proto/ tree plus
+ buf.yaml + buf.gen.yaml into a tarball +
+ zip + sha256 file and attaches them to the GitHub Release. Consumers
+ who don't want to vendor a git ref can pin against the proto
+ bundle for the exact version.
+
+
+
+
+ Cutting a release
+
+ - Update
VERSION (or rely on the workflow's "Inject version from git tag" step).
+ - Open and merge a PR with the changelog entry / version bump.
+ - From
main: git tag v0.1.0 && git push origin v0.1.0.
+ - Workflow runs end-to-end: test → smoke → push → cosign → Trivy → proto bundle.
+ - The published image is reachable at
ghcr.io/elloloop/notify:0.1.0.
+
+
+ Conformance workflow (PR gate)
+
+ .github/workflows/conformance.yml is the PR-side gate.
+ It runs:
+
+
+ - A Unit job —
go build / vet / test -race over everything except the driver-specific tree.
+ - A Conformance / <driver> matrix —
memory, postgres (via the postgres:16.13-alpine3.23 service container), entdb (via ghcr.io/elloloop/tenant-shard-db:2.0.5 service container with the realentdb build tag).
+
+
+
+ Branch protection pins the Conformance / memory,
+ Conformance / postgres, Conformance / entdb,
+ and Unit checks. Nothing lands without all four green.
+
+
+ Local equivalent
+
+
+ Related
+
++ Manifests for a production-shape Kubernetes deployment of notify: + one Deployment, two Services (one internal-only, one exposed via + Ingress), a Secret for the credentials, and an HPA tuned for the + realtime workload. +
+ ++ You already run Kubernetes and want notify to ride your existing + operational story — Helm-ish chart, GitOps via Argo / Flux, mesh-of- + your-choice, etc. +
+ +"
+ NOTIFY_INTERNAL_TOKEN: "<32-byte hex>"
+ NOTIFY_POSTGRES_DSN: "postgres://notify:password@db.notify:5432/notify?sslmode=require"
+ TWILIO_AUTH_TOKEN: "..."
+ FCM_SERVICE_ACCOUNT_JSON: |
+ { "type": "service_account", ... }`} lang="yaml" title="00-namespace-secret.yaml" />
+
+
+ Project these from your secret manager via External Secrets Operator,
+ Sealed Secrets, or whatever your shop uses. The point is that they
+ live outside the manifest tree.
+
+
+ Deployment
+
+
+ Services
+
+
+ Ingress (client surface)
+
+
+
+ The long proxy-{`{read,send}`}-timeout values matter for
+ StreamEvents — without them nginx will close the
+ long-lived SSE connection after 60s.
+
+
+ NetworkPolicy (internal port lockdown)
+
+
+ Horizontal Pod Autoscaler
+
+ The realtime workload is connection-count bound rather than CPU
+ bound. The HPA below uses CPU as a coarse signal; when the
+ Prometheus metric surface is wired (follow-up wave) you can switch
+ to a custom metric (e.g. notify_live_connections).
+
+
+
+ PodDisruptionBudget
+
+
+ Rolling deploy semantics
+
+ - New pod boots,
/healthz returns 503 until Server.Run binds listeners → marks ready.
+ - Service starts routing traffic to the new pod.
+ - Old pod receives SIGTERM,
/healthz immediately flips to 503 → Service stops sending new traffic.
+ Shutdown drains in-flight requests within NOTIFY_SHUTDOWN_TIMEOUT.
+ - Open SSE streams: each handler exits on context cancel, client reconnects to whichever pod the LB picks next.
+
+
+ Related
+
+
+ Two distinct acknowledgements live on the client surface:
+ AckNotification marks a stored notification as
+ Read; AckDataChange cancels the
+ at-least-once retry of a data-change hint. Both are idempotent;
+ both surface as one Connect call.
+
AckNotification — user opened the notification, viewed the inbox item, or otherwise consumed it. Decrements unread count, stamps read_at_ms.AckDataChange — the client successfully refetched the upstream state the hint pointed at; tells the server to stop retrying.
+ The id field is the store id (the
+ Notification.id the server assigned), not the
+ producer's notification_id:
+
+
+
+
+
+
+
+
+
+ The store transitions the row to StatusRead and stamps
+ read_at_ms to the server clock. A follow-up
+ GetNotifications shows the new status and an
+ unread_count reduced by one.
+
+
+ + Why this design. ++ +Store.UpdateStatustakestenantIDand +idonly — notuserID. Without an + ownership check, any user in tenant T could mark any other user's + notification as Read by guessing the id. The handler does an + explicitGetNotification(claims.TenantID, claims.UserID, + id)first and maps a miss toCodeNotFound— + neverCodePermissionDenied, which would leak the + existence of someone else's row. +
+ Calling AckNotification on an already-read row is a
+ no-op that returns success. The store update is a status-set, not a
+ counter increment; multiple calls do not multiply the effect.
+
+
+
+
+
+
+
+ The handler calls RetryTracker.Ack(key, sessionID) on
+ the server's in-memory tracker. The matching retry goroutine
+ receives a context cancel; the entry is removed from the map. If
+ the (key, sessionID) pair is not registered (already acked,
+ process restart, etc.), the call is a no-op and still returns
+ success.
+
+ When NOTIFY_LIVE_CONNECTIONS_ENABLED=false, there is no
+ retry tracker and no live stream. AckDataChange returns
+ success without doing anything — clients calling it speculatively
+ do not need to gate on the subsystem state.
+
void }) {
+ return (
+ {
+ await client.ackNotification({ id: n.id });
+ onAcked();
+ }}
+ className={n.status === "DELIVERY_STATUS_READ" ? "muted" : "unread"}
+ >
+ {n.title}
+ {n.body}
+
+ );
+}`} lang="tsx" />
+
+ Mark-all-read
+
+ v0.1 ships no bulk-ack RPC. The recommended client-side shape is to
+ page the inbox with unreadOnly: true and ack each row.
+ A native MarkAllRead RPC is a candidate follow-up; if
+ you need it before then, file an issue on the repo.
+
+
+ Related
+
+ - API Reference
+ - Subscribe over SSE
+ - Realtime engine — RetryTracker invariants
+
+
+ One Notify call, many recipients, many channels per
+ recipient. The orchestrator stores one row per
+ (user, channel), attempts immediate delivery per row,
+ and returns aggregate counts so the producer knows what landed.
+
+ Business events that deserve every available rail: incident pages, + onboarding milestones, multi-recipient activity events ("Bob + commented on Task 42 — page Alice and Carol via in-app + email + + SMS"). +
+ +
+
+ | User | Channel | Address resolved? | Provider configured? | Result |
|---|---|---|---|---|
| alice | in_app | yes (user id) | yes (auto) | delivered if live, else pending |
| alice | yes (alice@example.com) | yes | delivered | |
| alice | sms | yes (+15555550199) | yes | delivered |
| bob | in_app | yes | yes | delivered if live, else pending |
| bob | yes (bob@example.com) | yes | delivered | |
| bob | sms | no | yes | pending (no address) |
| carol | in_app | yes | yes | delivered if live, else pending |
| carol | no | yes | pending (no address) | |
| carol | sms | yes (+15555550158) | yes | delivered |
+ The orchestrator stores all nine rows (3 users × 3 channels) and
+ reports a NotifyResponse like:
+
+
+
+ Two of those pending are "no address"; the other two
+ are "user offline for in-app". Both are recoverable: the missing
+ addresses can be supplied in a future retry; the in-app rows will
+ surface on the next StreamEvents open via
+ GetNotifications.
+
+ The single notification_id covers all nine
+ rows. A second call with the same
+ (tenant, notification_id) but a wider channel list
+ will not create new rows for the already-stored
+ (user, channel) pairs — the Store's idempotency key is
+ (tenant, user, notification_id), and that triple
+ matches per row. The second call's NotifyResponse will report
+ those rows as pending (skipped at the storage layer)
+ or re-deliver via the providers if a provider is now available
+ where one wasn't before.
+
+ The practical recommendation: choose the channel list once,
+ at the producer, when the event is generated. Retries should not
+ change the channel set; if you need to add a channel, mint a new
+ notification_id for the addition.
+
+ Omitting channels tells the orchestrator to use every
+ channel that has both an active provider AND a destination for the
+ user. For the example above with all three providers configured,
+ leaving channels off produces the same nine rows.
+
+ The difference matters when a provider is disabled: with
+ channels=[in_app, email, sms] on a deployment where
+ SMS is disabled, the SMS rows are stored as pending
+ (recovery is "wire a Twilio provider, re-run"). With empty
+ channels on the same deployment, SMS isn't part of the
+ effective set at all — no row stored, no recovery story needed.
+
+ A user who logs in after the fanout opens
+ GetNotifications and sees every row addressed to them,
+ regardless of delivery outcome. The in-app row with
+ StatusPending looks the same in the UI as a delivered
+ row — it just hadn't been pushed live yet.
+
+ For a real producer with a user-store integration, the addresses + map is usually built from a single query over your user table: +
+ 0 {
+ addrs[uid] = ¬ifyv1.ChannelAddresses{ByChannel: m}
+ }
+ }
+
+ _, err = c.Notify(ctx, ¬ifyv1.NotifyRequest{
+ TenantId: "acme",
+ NotificationId: "incident-" + incidentID,
+ UserIds: userIDs,
+ Channels: []notifyv1.DeliveryChannel{
+ notifyv1.DeliveryChannel_DELIVERY_CHANNEL_IN_APP,
+ notifyv1.DeliveryChannel_DELIVERY_CHANNEL_EMAIL,
+ notifyv1.DeliveryChannel_DELIVERY_CHANNEL_SMS,
+ },
+ SubjectRef: "incident:" + incidentID,
+ SubjectType: "incident",
+ Title: "P1: checkout service unavailable",
+ Body: "Pagerduty incident " + incidentID + " fired.",
+ Addresses: addrs,
+ })
+ return err
+}`} lang="go" />
+
+ Partial failure
+
+ A provider error on one (user, channel) row records that row as
+ StatusFailed and increments NotifyResponse.failed;
+ the rest of the fan-out continues. Storage errors abort the call
+ immediately (the platform cannot guarantee the remaining rows would
+ be stored either).
+
+
+ Producer-side strategy: log non-zero
+ failed for ops alerts; do not retry the whole call
+ automatically — the idempotency key ensures the stored rows aren't
+ duplicated, but it does not re-attempt the failed provider sends.
+ To re-send only the failed channel for the affected users, mint a
+ fresh notification_id with the narrowed audience.
+
+
+ Related
+
+
+ End-to-end flow for both Web Push (browser, VAPID) and mobile push
+ (FCM). The token is stored in notify's device table keyed by
+ (tenant, user, device_type); sending then just supplies
+ the user id and notify resolves the right token per channel.
+
+ First-launch of your client app, or whenever the browser /OS rolls
+ the underlying token (push services do this occasionally). Always
+ re-register on app start — UpsertDevice is idempotent;
+ a no-change call is a cheap no-op.
+
+
+ {
+ const data = event.data?.json() ?? {};
+ event.waitUntil(
+ self.registration.showNotification(data.title || "Notification", {
+ body: data.body,
+ data: data.data,
+ icon: "/icon-192.png",
+ })
+ );
+});
+
+self.addEventListener("notificationclick", (event) => {
+ event.notification.close();
+ event.waitUntil(
+ clients.openWindow(event.notification.data?.url || "/")
+ );
+});`} lang="js" title="public/sw.js" />
+
+ 3. Subscribe and register
+ c.charCodeAt(0));
+}
+
+export async function enableWebPush(): Promise {
+ if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
+ throw new Error("Push API not available in this browser");
+ }
+
+ // 1. Service worker.
+ const reg = await navigator.serviceWorker.register("/sw.js");
+ await navigator.serviceWorker.ready;
+
+ // 2. Permission. Must be triggered by a user gesture.
+ const perm = await Notification.requestPermission();
+ if (perm !== "granted") {
+ throw new Error("Notification permission denied");
+ }
+
+ // 3. PushManager subscription.
+ let sub = await reg.pushManager.getSubscription();
+ if (!sub) {
+ sub = await reg.pushManager.subscribe({
+ userVisibleOnly: true,
+ applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
+ });
+ }
+
+ // 4. Hand the subscription JSON to notify.
+ await client.registerPushToken({
+ deviceType: "DEVICE_TYPE_BROWSER",
+ token: JSON.stringify(sub),
+ });
+}`} lang="ts" title="features/push/web-push.ts" />
+
+ 4. Send via Web Push
+
+ From the producer, you don't need to thread the address — once
+ registered, notify will resolve the device token automatically when
+ a follow-up wave wires "store-side address lookup". Until then, in
+ v0.1 supply the subscription JSON in Addresses as
+ well:
+
+
+
+ Mobile push (FCM) — full round-trip
+
+ 1. Set up FCM on the server
+
+
+ 2. Android client — register the FCM token
+
+ if (!task.isSuccessful) {
+ Log.w("notify", "FCM token fetch failed", task.exception)
+ return@addOnCompleteListener
+ }
+ val token = task.result
+ notifyApi.registerPushToken(
+ RegisterPushTokenRequest(
+ deviceType = DeviceType.DEVICE_TYPE_ANDROID,
+ token = token,
+ )
+ )
+ }
+ }
+}
+
+// Token rotation — implement onNewToken so an OS-side token roll
+// re-registers automatically.
+class NotifyMessagingService : FirebaseMessagingService() {
+ override fun onNewToken(token: String) {
+ super.onNewToken(token)
+ notifyApi.registerPushToken(
+ RegisterPushTokenRequest(
+ deviceType = DeviceType.DEVICE_TYPE_ANDROID,
+ token = token,
+ )
+ )
+ }
+}`} lang="kotlin" />
+
+ 3. iOS client — register the FCM token
+ Bool {
+ FirebaseApp.configure()
+ Messaging.messaging().delegate = self
+ UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { _, _ in }
+ application.registerForRemoteNotifications()
+ return true
+ }
+
+ func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
+ guard let token = fcmToken else { return }
+ notifyApi.registerPushToken(
+ deviceType: .ios,
+ token: token,
+ )
+ }
+}`} lang="swift" />
+
+ 4. Producer-side send via FCM
+
+
+ Handling dead tokens
+
+ Push tokens go stale — users uninstall apps, browsers unsubscribe,
+ OSes roll tokens. Both providers surface "this token is gone" as a
+ typed error:
+
+
+ - Web Push:
webpush.ErrSubscriptionGone on HTTP 410.
+ - FCM:
fcm.ErrUnregisteredToken on UNREGISTERED / NOT_FOUND.
+
+
+
+ The provider does not purge the device row itself — that's
+ the orchestrator/caller's job. The recommended shape is to wire a
+ callback on NotifyResponse failures that calls
+ RegisterPushToken with an empty token to delete the
+ row (a follow-up wave will add an explicit
+ DeleteDevice RPC; for now the recommended path is for
+ the failing producer to remove the token from its own consumer
+ user store and stop sending to that channel).
+
+
+ Related
+
+
+ Producer-side recipes for the Notify RPC. Same wire
+ contract from every language; only the SDK and the auth-header
+ plumbing differ.
+
+ Any backend service that wants to push a notification to a user + via notify. Webhook receivers, business-logic services, scheduled + job runners — all of them call this RPC. +
+ ++ The simplest possible producer. Useful for one-off debugging, smoke + tests against a fresh deployment, or generating fixtures. +
+
+
+ Response:
+
+
+
+ delivered=1 = the email send succeeded.
+ pending=1 = the in-app row was stored, but no live
+ connection is open for user-alice; it will surface on
+ their next StreamEvents open via GetNotifications.
+
+ Production-shape Go producer using the generated Connect client. This + is what an api-gateway-style consumer looks like. +
+
+
+ + For services using Python. The same pattern works with the gRPC + stubs — Connect's JSON-over-HTTP is just easier to debug. +
+
+
+
+ Pick a notification_id that is deterministic from the
+ triggering event. Two examples:
+
"task-{`{taskID}`}-comment-{`{commentID}`}". Webhook retries of the same comment produce the same id; notify de-dupes."summary-{`{userID}`}-{`{YYYY-MM-DD}`}". A cron re-run within the same day produces the same id; notify de-dupes.+ Avoid wall-clock-derived ids for retryable events — a slow retry + crossing a millisecond boundary would create a duplicate. The + one place wall-clock is fine is event-source ids that include + their own monotonic offset (Kafka offsets, etc.). +
+ +
+ The Addresses map is per-user. Mix-and-match
+ destinations across recipients in one call:
+
+
+
+ Omit channels entirely (or pass an empty list) to fan
+ out to every channel that has both an active provider AND a
+ destination for the user. The orchestrator computes the effective
+ set at request time.
+
+ Open a long-lived stream of new notifications, data-change hints,
+ and heartbeats from a browser or mobile client. The platform speaks
+ Connect over HTTP/2; StreamEvents is a server-streaming
+ RPC, so it works equally over plain SSE-style framing.
+
+ Any client surface that wants real-time updates: a web SPA showing + chat messages without a refresh, a mobile app updating a task list + while the user has it open, a desktop tray app surfacing system + notifications. +
+ + Promise) {
+ const transport = createConnectTransport({
+ baseUrl,
+ interceptors: [
+ (next) => async (req) => {
+ const token = await getToken();
+ req.header.set("Authorization", \`Bearer \${token}\`);
+ return next(req);
+ },
+ ],
+ });
+ return createClient(NotificationClientService, transport);
+}`} lang="ts" title="shared/notify-client.ts" />
+
+ Promise;
+ onEvent: (ev: NotifyEvent) => void;
+ onError?: (err: unknown) => void;
+}): Stream {
+ const client = buildNotifyClient(opts.baseUrl, opts.getToken);
+ const abort = new AbortController();
+ let currentSession = "";
+
+ void (async () => {
+ let backoffMs = 500;
+ const maxMs = 30_000;
+
+ while (!abort.signal.aborted) {
+ try {
+ const it = client.streamEvents(
+ { deviceType: "DEVICE_TYPE_BROWSER" },
+ { signal: abort.signal },
+ );
+ for await (const ev of it) {
+ currentSession = ev.sessionId || currentSession;
+ backoffMs = 500; // reset on the first received event
+
+ switch (ev.event.case) {
+ case "notification": {
+ const n = ev.event.value.notification!;
+ opts.onEvent({
+ kind: "notification",
+ id: n.id,
+ title: n.title,
+ body: n.body,
+ subjectRef: n.subjectRef,
+ });
+ break;
+ }
+ case "dataChange": {
+ const dc = ev.event.value;
+ opts.onEvent({
+ kind: "dataChange",
+ idempotencyKey: dc.idempotencyKey,
+ subjectRef: dc.subjectRef,
+ subjectType: dc.subjectType,
+ });
+ // Ack so the server stops retrying.
+ await client.ackDataChange({
+ idempotencyKey: dc.idempotencyKey,
+ sessionId: currentSession,
+ });
+ break;
+ }
+ case "heartbeat":
+ opts.onEvent({
+ kind: "heartbeat",
+ timestampMs: ev.event.value.timestampMs,
+ });
+ break;
+ }
+ }
+ } catch (err) {
+ if (abort.signal.aborted) return;
+ opts.onError?.(err);
+ // Exponential backoff with jitter and a cap.
+ await new Promise((r) => setTimeout(r, backoffMs + Math.random() * 250));
+ backoffMs = Math.min(backoffMs * 2, maxMs);
+ }
+ }
+ })();
+
+ return {
+ cancel: () => abort.abort(),
+ sessionId: () => currentSession,
+ };
+}`} lang="ts" title="features/notifications/stream.ts" />
+
+ {
+ const stream = openStream({
+ baseUrl: "https://notify.example.com",
+ getToken: async () => localStorage.getItem("access_token") || "",
+ onEvent: (ev) => {
+ if (ev.kind === "notification") {
+ setCount((c) => c + 1);
+ }
+ },
+ onError: (err) => console.warn("stream error:", err),
+ });
+ return () => stream.cancel();
+ }, []);
+
+ return 🔔 {count};
+}`} lang="tsx" title="app/page.tsx" />
+
+ The handshake heartbeat
+
+ The server immediately sends one heartbeat after registering
+ your connection, before any traffic. That heartbeat carries the
+ server-assigned session_id, which is what you pass back
+ on AckDataChange to scope the cancellation to this
+ exact stream. The code above stores it in currentSession.
+
+
+ Reconnect strategy
+
+ Streams die for legitimate reasons: laptop sleep, server rolling
+ deploy, mobile carrier handover, network blip. The code above
+ reconnects with exponential backoff (500ms → 1s → 2s → ... → 30s)
+ with a 0–250ms jitter so a herd of clients does not synchronise
+ its reconnects.
+
+
+ Reconnecting is safe: any in-flight events the client missed are
+ still in the store and available via
+ GetNotifications. Data-change events the client did
+ not ack will be retried by the server's RetryTracker
+ until either the ack arrives or
+ NOTIFY_LIVE_RETRY_MAX_ATTEMPTS is exhausted.
+
+
+ Cold-start catch-up
+
+ A typical UI fetches the inbox once on app load to populate the
+ list, then opens the stream for live updates:
+
+
+
+ Recipe — raw cURL (for debugging only)
+
+ Useful for confirming that the stream works at all. Connect over
+ HTTP/2 uses length-prefixed JSON frames, not text/event-stream, so
+ cURL output is not human-friendly. Use --no-buffer and
+ expect binary framing.
+
+
+
+
+ For a readable debug stream, use the Connect-Go client in a small
+ Go test harness, or the connect-web client in
+ node --experimental-fetch.
+
+
+ Authorization on streams
+
+ The JWT is validated at handshake time and the resulting claims live
+ on the stream's context for its entire lifetime. The
+ platform does not re-validate mid-stream. If a token's
+ exp is reached during a long-lived stream, the existing
+ stream keeps flowing until disconnect; the client must mint a fresh
+ token before its next reconnect.
+
+
+ Related
+
+
+ Every container option is set via a NOTIFY_* environment
+ variable. cmd/notifyd calls
+ server.LoadConfigFromEnv() on boot; invalid values
+ (negative ports, missing required fields, unknown enums) fail fast
+ with a descriptive error so production never silently runs
+ misconfigured.
+
+ You're standing up a deployment, debugging a startup error, or moving + a provider configuration from one environment to another. The table + below is the full surface — there are no hidden flags. +
+ +| Variable | Type | Default | Description |
|---|---|---|---|
NOTIFY_CLIENT_PORT | int | 8080 | Public Connect/HTTP/2 listener (NotificationClientService). |
NOTIFY_INTERNAL_PORT | int | 8081 | Private gRPC listener (NotificationInternalService). |
NOTIFY_METRICS_PORT | int | 9090 | /healthz + /metrics listener. |
NOTIFY_LOG_LEVEL | enum | info | debug · info · warn · error. |
NOTIFY_SHUTDOWN_TIMEOUT | duration | 30s | Graceful-shutdown deadline (Go duration syntax: 30s, 2m, etc.). |
NOTIFY_ALLOWED_ORIGINS | csv | — | Comma-separated CORS origins for the client listener. |
| Variable | Type | Default | Description |
|---|---|---|---|
NOTIFY_STORE_DRIVER | enum | memory | memory · postgres · entdb. |
NOTIFY_POSTGRES_DSN | string | — | Required when driver=postgres. libpq-style URL. |
NOTIFY_POSTGRES_AUTOMIGRATE | bool | true | Apply pending migrations on connect. |
NOTIFY_ENTDB_ADDRESS | string | — | Required when driver=entdb. host:port. |
NOTIFY_ENTDB_TENANT_ID | string | — | Required when driver=entdb. EntDB tenant id (the storage shard). |
| Variable | Type | Default | Description |
|---|---|---|---|
NOTIFY_AUTH_JWT_SECRET | string | — | HS256 verification key. Required unless dev mode. |
NOTIFY_AUTH_JWT_ISSUER | string | — | Pinned iss claim, if set. |
NOTIFY_AUTH_JWT_AUDIENCE | string | — | Pinned aud claim, if set. |
NOTIFY_AUTH_JWT_LEEWAY | duration | 30s | Allowed clock skew when validating exp / nbf. |
NOTIFY_INTERNAL_TOKEN | string | — | Shared secret for X-Notify-Internal-Token. Required unless dev mode. |
NOTIFY_AUTH_DEV_MODE | bool | false | Accepts Bearer dev:<uid>:<tenant>. Local-dev only. |
See Auth Model for the full validation flow.
+ +| Variable | Type | Default | Description |
|---|---|---|---|
NOTIFY_LIVE_CONNECTIONS_ENABLED | bool | true | When false, StreamEvents returns Unimplemented. |
NOTIFY_LIVE_HEARTBEAT_INTERVAL | duration | 30s | Per-connection heartbeat cadence. |
NOTIFY_LIVE_RETRY_MAX_ATTEMPTS | int | 3 | At-least-once retry budget for data-change events. 0 disables. |
NOTIFY_LIVE_RETRY_INTERVAL | duration | 5s | Interval between retry attempts. |
| Variable | Type | Default | Description |
|---|---|---|---|
NOTIFY_EMAIL_PROVIDER | enum | none | none · emailservice (others land in later waves). |
NOTIFY_EMAIL_FROM | string | — | Default From address. Required when a provider is set. |
NOTIFY_EMAIL_SERVICE_ADDRESS | string | — | host:port of the elloloop EmailService. Required for emailservice. |
NOTIFY_EMAIL_SMTP_HOST | string | — | SMTP relay host (future SMTP provider). |
NOTIFY_EMAIL_SMTP_PORT | int | — | SMTP port. |
NOTIFY_EMAIL_SMTP_USERNAME | string | — | SMTP username. |
NOTIFY_EMAIL_SMTP_PASSWORD | string | — | SMTP password. |
NOTIFY_EMAIL_API_KEY | string | — | API key for SES/SendGrid/ACS providers. |
NOTIFY_EMAIL_REGION | string | — | Provider region (SES). |
NOTIFY_EMAIL_ENDPOINT | string | — | Provider endpoint override. |
| Variable | Type | Default | Description |
|---|---|---|---|
NOTIFY_SMS_PROVIDER | enum | — | twilio (others land later). |
NOTIFY_SMS_ACCOUNT_SID | string | — | Twilio Account SID. Required when provider=twilio. |
NOTIFY_SMS_AUTH_TOKEN | string | — | Twilio Auth Token. Required when provider=twilio. |
NOTIFY_SMS_MESSAGING_SERVICE_SID | string | — | Optional Twilio Messaging Service SID. |
NOTIFY_SMS_FROM | string | — | E.164 sender (e.g. +15555550000). |
NOTIFY_WHATSAPP_PROVIDER | enum | — | twilio. |
NOTIFY_WHATSAPP_ACCOUNT_SID | string | — | Required when provider=twilio. |
NOTIFY_WHATSAPP_AUTH_TOKEN | string | — | Required when provider=twilio. |
NOTIFY_WHATSAPP_FROM | string | — | E.164 sender; the whatsapp: prefix is added automatically. |
| Variable | Type | Default | Description |
|---|---|---|---|
NOTIFY_WEBPUSH_PROVIDER | enum | — | vapid. |
NOTIFY_WEBPUSH_VAPID_PUBLIC | string | — | VAPID public key (base64url). |
NOTIFY_WEBPUSH_VAPID_PRIVATE | string | — | VAPID private key (base64url). |
NOTIFY_WEBPUSH_CONTACT_EMAIL | string | — | Contact email for the push service (RFC 8292). |
| Variable | Type | Default | Description |
|---|---|---|---|
NOTIFY_MOBILEPUSH_PROVIDER | enum | — | fcm (others land later). |
NOTIFY_FCM_CREDENTIALS_JSON | string | — | Service-account JSON. Required for FCM. |
NOTIFY_FCM_PROJECT_ID | string | — | Firebase project id. Required for FCM. |
NOTIFY_APNS_KEY_P8 | string | — | APNs auth key (P8 PEM). |
NOTIFY_APNS_KEY_ID | string | — | APNs key id. |
NOTIFY_APNS_TEAM_ID | string | — | Apple developer team id. |
NOTIFY_APNS_TOPIC | string | — | Bundle id / topic. |
NOTIFY_APNS_SANDBOX | bool | false | Use the APNs sandbox endpoint. |
+NOTIFY_INTERNAL_TOKEN=<32-byte hex>
+NOTIFY_LOG_LEVEL=info
+NOTIFY_ALLOWED_ORIGINS=https://app.example.com
+
+# Email via the elloloop EmailService container.
+NOTIFY_EMAIL_PROVIDER=emailservice
+NOTIFY_EMAIL_SERVICE_ADDRESS=emailservice.internal:50053
+NOTIFY_EMAIL_FROM=noreply@example.com
+
+# SMS via Twilio.
+NOTIFY_SMS_PROVIDER=twilio
+NOTIFY_SMS_ACCOUNT_SID=AC...
+NOTIFY_SMS_AUTH_TOKEN=...
+NOTIFY_SMS_FROM=+15555550000`} lang="ini" title="notify.env" />
+
+ Use it with:
+
+
+ Boot-time validation
+
+ On startup notify validates every value. Examples of what it catches:
+
+
+ NOTIFY_STORE_DRIVER=mongo → "unknown driver \"mongo\" (want memory|postgres|entdb)".
+ NOTIFY_STORE_DRIVER=postgres without a DSN → "NOTIFY_POSTGRES_DSN is required when NOTIFY_STORE_DRIVER=postgres".
+ NOTIFY_EMAIL_PROVIDER=emailservice without NOTIFY_EMAIL_SERVICE_ADDRESS → required-field error.
+ NOTIFY_AUTH_DEV_MODE=false without NOTIFY_AUTH_JWT_SECRET → required-field error.
+ NOTIFY_CLIENT_PORT=99999 → "NOTIFY_CLIENT_PORT: must be in 1..65535".
+
+
+ Related
+
+ - JWT Keys
+ - Store Setup
+ - Observability
+
+
+ The reference deployment shape is the published multi-arch container
+ image at ghcr.io/elloloop/notify. It is a
+ FROM scratch binary plus CA certificates — no shell, no
+ package manager, ~10 MB.
+
+ This is the recommended path for every environment except library mode. + Local dev, staging, production: same image, different env vars. If you + want to embed notify in another Go binary, see + library + usage in the README. +
+ +
+
+ Pin to a specific tag in CI and production:
+
+
+ | Port | Listener | Audience |
|---|---|---|
| 8080 | NotificationClientService | Browser / mobile recipients (Connect HTTP/2) |
| 8081 | NotificationInternalService | Backend producers (gRPC / Connect) |
| 9090 | /healthz and /metrics | Orchestrators / Prometheus |
+ The Dockerfile builds a FROM --platform=$BUILDPLATFORM
+ golang:1.26.3-alpine3.23 stage that produces a static
+ binary, then COPYs the binary and CA bundle into
+ FROM scratch. The final image runs as UID
+ 65532 (non-root):
+
+
+
+ The container has no shell — you can't HEALTHCHECK CMD curl …
+ against itself. Probe from the orchestrator side instead:
+
+
+ + For Kubernetes use HTTP probes — see + Kubernetes. +
+ +
+
+ + Every published image is signed via cosign keyless OIDC and scanned by + Trivy with a HIGH/CRITICAL gate. Verify the signature on pull: +
+
+
+
+
+
+ notify validates client-facing requests with an HS256 JWT signature.
+ The verification key is a shared secret you generate, hand to the JWT
+ issuer (identity), and hand to notify via
+ NOTIFY_AUTH_JWT_SECRET.
+
+ You are setting up a production deployment, rotating a leaked secret, + or wiring an existing identity-issued JWT into notify. +
+ ++ Use a CSPRNG. Anything 32 bytes or larger is fine for HS256 (256 bits + of entropy matches the HMAC-SHA-256 output size). +
+
+
+
+
+ + In production prefer a secrets manager (Kubernetes Secret, AWS + Secrets Manager, Azure Key Vault) and project the value into the + container as an env var. +
+ ++ When the JWT issuer is identity, set the matching expected claims + so notify rejects tokens minted by anyone else: +
+
+
+
+ If your identity tenant uses different values, copy them verbatim
+ from the issuer config. A missing or mismatched claim returns
+ CodeUnauthenticated.
+
+ notify expects the standard claims plus a tenant claim: +
+
+
+
+ sub populates Claims.UserID;
+ tenant (or the alternative tenant_id)
+ populates Claims.TenantID; email is
+ optional and surfaces in observability.
+
+ Tiny one-off generator in Python — useful for manual cURLs against a + locally-deployed notify with a real JWT secret: +
+
+
+
+
+ + notify v0.1 validates against a single secret — there is no key ring + on the verifier. To rotate without a flap, the standard procedure is: +
+S2.S2, every legacy token still signed with S1 tracked by kid.NOTIFY_AUTH_JWT_SECRET=S1, one with NOTIFY_AUTH_JWT_SECRET=S2. Route traffic by inspecting the kid in the JWT header at the gateway.S1-signed tokens have expired (one access-token TTL window), retire the S1 replica and the issuer's S1 signing key.+ The simpler alternative — rolling everyone out, swapping the secret, + rolling back in — is acceptable for short outage windows but is what + rotation is supposed to avoid. A future wave will add a multi-key + verifier so notify can hold both keys natively. +
+ ++ v0.1 only validates HS256. If your identity provider signs RS256 + (e.g. you are using the + identity service's + JWKS surface), the recommended shape today is: +
+server.Dependencies.AuthValidator.+ Native RS256 + JWKS verification is on the roadmap; track + notify issues. +
+ +NOTIFY_AUTH_* var+ Pick the durable store driver that fits the deployment. Memory is the + test/dev reference; Postgres is the typical production shape; + EntDB is the right call if you already run tenant-shard-db. +
+ ++ You are sizing storage for a new environment, deciding which managed + Postgres SKU to provision, or wiring notify into an existing EntDB + cluster. +
+ +
+ No setup. NOTIFY_STORE_DRIVER=memory is the default;
+ everything lives in process memory and disappears on restart.
+
+
+ + Use this for tests, demos, and CI fixtures. It is the conformance + reference — every other driver is verified against memory's + behaviour. +
+ +
+ Use a managed Postgres 14+ (we test against
+ postgres:16.13-alpine3.23). Auto-migration is on by
+ default and applies the schema under
+ pg_advisory_lock so concurrent boots serialise.
+
+
+
+
+
+ On first boot it creates notify_notifications,
+ notify_devices and the
+ notify_schema_migrations bookkeeping table. Subsequent
+ boots see the ledger and skip already-applied versions.
+
'read';
+
+CREATE TABLE notify_devices (
+ id uuid PRIMARY KEY,
+ tenant_id text NOT NULL,
+ user_id text NOT NULL,
+ device_type text NOT NULL,
+ token text NOT NULL DEFAULT '',
+ created_at_ms bigint NOT NULL DEFAULT 0,
+ last_active_ms bigint NOT NULL DEFAULT 0,
+ UNIQUE (tenant_id, user_id, device_type)
+);`} lang="sql" title="migration 1 — init" />
+
+
+ If you want to manage migrations out of band (a CI step, golang-migrate,
+ etc.) set NOTIFY_POSTGRES_AUTOMIGRATE=false. The schema
+ is two tables plus three indexes — easy to mirror verbatim.
+
+
+ Sizing
+
+ Reasonable defaults: MaxConns = 25,
+ MaxConnLifetime = 1h, MaxConnIdleTime = 30m —
+ cooperative with pgbouncer / Azure idle reapers. notify is read-heavy
+ on the inbox surface and append-heavy on Notify; the
+ notify_notifications_user_unread partial index keeps
+ unread counts fast even with millions of read rows.
+
+
+ Conformance
+
+ The Postgres driver is verified at 24/24 against the
+ shared suite. See Postgres driver
+ or the
+ CONFORMANCE.md
+ for the per-subtest table and the design rationale.
+
+
+ entdb (tenant-shard-db)
+
+ Use this when EntDB is already a primitive in your stack
+ (e.g. you run identity against it). The notify driver requires the
+ v2 schema-aware mode (ADR-031) — v1.32.1 cannot enforce the
+ composite-uniqueness constraint that idempotent
+ CreateNotification needs.
+
+
+ Boot a local EntDB
+
+
+
+ -data-dir is required on v2 (it was implicit in v1.32.1
+ and earlier).
+
+
+ Wiring it in
+
+
+ Schema-aware mode
+
+ notify's cmd/notifyd registers the schema on the SDK
+ client at boot:
+
+
+
+
+ entdb.SchemaMessages() returns the two proto descriptors
+ (UserNotification, DeviceRegistration) declared
+ in proto/entdb_notify/notify.proto. The v2 server
+ materializes the schema from those descriptors on the first
+ ExecuteAtomic and enforces unique: true on
+ the composite_key field — which closes the
+ concurrent-create race the conformance suite probes.
+
+
+ Sharding
+
+ One notify container talks to one EntDB tenant shard
+ (NOTIFY_ENTDB_TENANT_ID). All notify-application tenants
+ live under that one storage shard, differentiated by the
+ tenant_id row field. If a large customer needs its own
+ storage shard, run a second notify container pointed at a different
+ NOTIFY_ENTDB_TENANT_ID.
+
+
+ Conformance
+
+ EntDB driver passes 24/24 on v2.0.5; the two
+ formerly-red canaries (composite uniqueness under concurrent create
+ + device upsert) flipped to green when the schema-aware enforcement
+ landed in v2.0.1. See
+ EntDB driver or
+ store/entdb/CONFORMANCE.md.
+
+
+ Related
+
++ notify is a multi-channel notification platform packaged two ways: +
+import "github.com/elloloop/notify"
+ and embed the orchestrator in-process. Inject a Store and
+ one or more Providers; call Notifier.Notify with
+ a request.
+ ghcr.io/elloloop/notify:<version>,
+ a thin server wrapper that exposes the same orchestrator over gRPC
+ (for backend producers) and Connect/HTTP-2 (for browser + mobile
+ recipients).
+ + Both packaging modes share the same internals — the dual-mode pattern + is the same one elloloop/identity + and tenant-shard-db + use. +
+ +NOTIFY_* env vars is the
+ whole deployment surface. No bespoke deploy pipelines.
+ notify.Store interface and run the same conformance
+ suite.
+ Notification carries
+ an opaque SubjectRef + SubjectType. The
+ platform never interprets them — no todo_id,
+ message_id or any other consumer-specific fields leak
+ across the API.
+ Notify
+ call is keyed by (tenant_id, user_id, notification_id).
+ Retries are safe and never duplicate stored rows or provider
+ dispatches.
+ Title + Body + Data the caller
+ hands it. Localization, MJML rendering and HTML email composition are
+ the caller's concern (or a future wave).
+ "" to keep the row but suppress dispatch.
+ StatusPending rather than
+ a lying StatusDelivered.
+ + Two services live in the proto contract + (proto/notify/v1): +
+NotificationInternalService.Notify — the backend producer
+ RPC. Fan a payload out to one-or-many users across one-or-many
+ channels.
+ NotificationClientService — the client-facing surface
+ a browser SPA or mobile app calls. RPCs: StreamEvents,
+ GetNotifications, AckNotification,
+ AckDataChange, RegisterPushToken.
+ + Both are accessible via Connect-Go / Connect-Web / Connect-Python / + gRPC. There is no separate REST surface — Connect speaks JSON natively + over HTTP/1 and HTTP/2. +
+
+ notify exposes structured JSON logs to stderr, a /healthz
+ endpoint for liveness, and a /metrics endpoint for
+ Prometheus scrapers. The metrics endpoint always returns a parseable
+ body so a scraper hitting it never sees a 404.
+
+ Setting up dashboards, debugging a slow tenant, wiring an alerting + rule, or chasing an intermittent timeout in production. +
+ +
+ Logs are emitted via Go's log/slog with a JSON handler
+ writing to stderr at the level set by NOTIFY_LOG_LEVEL
+ (default info). One event per line, no header, ingest
+ directly into Loki / Datadog / Splunk / CloudWatch.
+
server_listen — one line per listener (client / internal / metrics).notifyd_starting — boot banner with version, commit, store driver, live-connections toggle, listener ports.stream_open / stream_close — every StreamEvents session, including connection_id, user_id, tenant_id, device_type.event_queue_full — one log per dropped event when a client buffer overflows.retry_failed — per-attempt error from the at-least-once retry tracker (key, connection_id, attempt, error).server_shutdown_signal / server_shutdown_error — graceful shutdown.
+
+
+
+
+
+ {`{"status":"ok"}`} once Server.Run has bound all listeners.{`{"status":"not_ready"}`} after Shutdown begins and before the process exits.An alias /health is registered for tool-muscle-memory.
+
+
+ The readiness probe will start failing the instant
+ Shutdown begins, so an in-flight rolling update stops
+ sending new traffic to the pod immediately while
+ Shutdown drains in-flight requests.
+
+ /metrics on the metrics port returns
+ text/plain; version=0.0.4 — the standard Prometheus
+ exposition. v0.1 ships a placeholder body so scrapers don't 404;
+ real metric series land in a follow-up wave without changing the
+ route.
+
+
+
+
+ + Once the registry is wired, expect these series. Labels are + intentionally cardinality-bounded — no per-tenant labels. +
+| Metric | Type | Labels |
|---|---|---|
notify_notify_requests_total | Counter | code |
notify_send_total | Counter | channel, provider, status |
notify_send_duration_seconds | Histogram | channel, provider |
notify_store_op_total | Counter | op, code |
notify_store_op_duration_seconds | Histogram | op |
notify_live_connections | Gauge | device_type |
notify_stream_events_total | Counter | kind |
+ notify does not emit OpenTelemetry spans today. The Connect
+ interceptors are the right place to add them; once the
+ follow-up lands, set OTEL_* env vars per the standard
+ Go OTel SDK auto-configuration and traces will surface in the
+ backend of your choice.
+
+ Loki + Grafana is the cheapest path for JSON logs. Drop this in your + promtail config: +
+
+
+
+ Server.Run traps SIGINT and
+ SIGTERM, marks the health endpoint not-ready, and runs
+ a single ordered Shutdown against the three HTTP
+ servers, the retry tracker, and the store. The total time is
+ capped by NOTIFY_SHUTDOWN_TIMEOUT (default 30s).
+
+ Tuning a rolling deploy, debugging requests cut off mid-flight, or
+ sizing a Kubernetes terminationGracePeriodSeconds.
+
Server.health.markNotReady() — /healthz immediately starts returning 503 with not_ready. Kubernetes readiness probes stop steering traffic to the pod within one probe interval (default ~5s).http.Server.Shutdown is called on the client server, the internal server, and the metrics server in turn. Each waits for in-flight handlers to return; new connections are rejected.RetryTracker.CancelAll cancels every in-flight at-least-once retry so no goroutine leaks across the process exit.pgx.Pool.Close for Postgres, sdk.Client.Close for EntDB, no-op for memory). This is the last step so any in-flight handler that's still touching the store has time to return first.
+
+ + The default 30s is a reasonable starting point. Two workloads should + bump it: +
+StreamEvents connection must drain. The handler exits cleanly on context cancel, but a TCP close handshake takes a few hundred ms times N connections. For 10k open streams plan ~60s.NotificationInternalService.Notify calls may need ~10s+ each. Set NOTIFY_SHUTDOWN_TIMEOUT a comfortable margin above the longest acceptable provider latency.
+
+
+ Set it slightly longer than NOTIFY_SHUTDOWN_TIMEOUT so
+ the kubelet doesn't SIGKILL before notify finishes draining:
+
+
+ + Optional: a short preStop hook gives the load balancer / service + mesh time to stop sending new traffic before notify even sees SIGTERM: +
+
+
+
+ The container ships FROM scratch so there's no
+ /bin/sh; if you want a preStop sleep, project a
+ sidecar with a shell, or rely on the readiness-probe flip to drain
+ upstream proxies inside Server.Shutdown itself.
+
+ The at-least-once RetryTracker spawns one goroutine per
+ (key, connID) pair when a data-change event is
+ scheduled. CancelAll cancels every parent context and
+ drains the map. After shutdown:
+
<-ctx.Done().+ If you need at-least-once delivery across pod restarts for a + specific use case, push it through a durable channel (email, SMS, + web push, mobile push) — the in-app channel is real-time-or-nothing + by intent. +
+ +pgxpool.Pool.Close; waits for all checked-out connections to be returned before closing.sdk.Client.Close; closes the gRPC transport.+ The store close runs after the HTTP servers have + finished draining. This guarantees no in-flight Connect handler is + still touching the store when its connections get torn down — + avoiding spurious "connection closed" errors in the very last log + lines. +
+ +
+
+
+ If the shutdown completes within NOTIFY_SHUTDOWN_TIMEOUT,
+ the process exits with status 0. If the timeout fires first the
+ process logs server_shutdown_error with the underlying
+ cause and exits with the first non-nil error.
+
+ Five-minute "Hello, notify": run the container, send a notification + from a backend, receive it over SSE in a browser. +
+ ++ Use the memory driver and dev-mode auth — no Postgres, no EntDB, no + JWT secret. Never enable dev mode in production. +
+
+
+ Confirm it's healthy:
+
+
+
+ Backend producers call the internal service on port
+ 8081. In dev mode the
+ X-Notify-Internal-Token check is skipped. The request is
+ plain Connect-JSON over HTTP, so cURL works.
+
+
+ Response:
+
+
+
+ pending=1 is correct: no client is connected yet, so the
+ in-app provider has nowhere to deliver. The row is stored and will
+ surface via GetNotifications or the live stream as soon
+ as a client subscribes.
+
+ Client connections go to the client service on port
+ 8080. The dev-mode token shape is
+ Bearer dev:<userid>:<tenant>.
+
async (req) => {
+ req.header.set("Authorization", "Bearer dev:user-alice:acme");
+ return next(req);
+ },
+ ],
+});
+const client = createClient(NotificationClientService, transport);
+
+for await (const ev of client.streamEvents({ deviceType: "DEVICE_TYPE_BROWSER" })) {
+ switch (ev.event.case) {
+ case "notification":
+ console.log("notification:", ev.event.value.notification?.title);
+ break;
+ case "heartbeat":
+ console.log("heartbeat @", ev.event.value.timestampMs);
+ break;
+ case "dataChange":
+ console.log("data change:", ev.event.value.subjectRef);
+ break;
+ }
+}`} lang="ts" title="browser.ts" />
+
+
+ Now repeat step 2 with a different notificationId: the
+ open stream prints the title within milliseconds.
+
+
+ 4. Page the inbox history
+
+ Anything sent while no client was connected is still available via
+ GetNotifications.
+
+
+
+
+
+ 5. Mark a notification as read
+ "}'`} lang="bash" />
+
+
+ A repeat GetNotifications shows
+ status: DELIVERY_STATUS_READ,
+ readAtMs stamped, and unreadCount: 0.
+
+
+ 6. Inspect /metrics
+
+
+ The endpoint exposes a parseable Prometheus exposition; v0.1 ships
+ a placeholder so scrapers don't 404. Real metric series land in a
+ follow-up wave without changing the route.
+
+
+ Next steps
+
+ - Configuration — every environment variable, defaults, validation rules
+ - JWT Keys — production-grade HS256 secret
+ - Store Setup — Postgres or EntDB driver
+ - Send a Notification — Go + Python producer code
+ - Subscribe over SSE — full browser/mobile flow
+
+
+ store/conformance.RunConformance is the driver-agnostic
+ spec for notify.Store. Memory is the differential
+ reference; every other driver passes the same 24 leaf subtests
+ across six categories or its CI check goes red and the merge is
+ blocked.
+
TestConformance/postgres/Concurrency/ConcurrentCreate_SameKey_SingleWinner tells you exactly which semantic broke.
+
+
+ RunConformance wraps everything in
+ t.Run(d.Name, …), so the test path on failure is
+ TestConformance/<driver>/<Category>/<Subtest>.
+
| Category | Subtest | What it catches |
|---|---|---|
| Core CRUD | CreateGet | basic round-trip; id is assigned by Store, not caller |
GetNotFound | unknown id returns ErrNotFound (not a typed driver error) | |
Idempotency | second create with same (tenant, user, notification_id) returns created=false, stamps existing id, leaves stored row unchanged | |
StatusTransitions | each status stamps its matching *_at_ms field; unknown id returns ErrNotFound | |
CursorWalk_ThreePages | multi-page traversal with cursors; last page returns empty cursor | |
UnreadFilterAndCount | unreadCount is independent of the page window and UnreadOnly filter | |
DeviceUpsertRotation | re-registering the same (tenant, user, device_type) rotates token in place; doesn't create a new row | |
UserIsolation | cross-user and cross-tenant reads surface as ErrNotFound | |
| Pagination | QueryUserNotifications_AllPagesReturnEveryRow | full-set traversal across many pages — no row dropped, no row duplicated |
StrictLessThanCutoff | cursor semantics are strict <, never <= (otherwise boundary rows repeat) | |
| FreshTenant | QueryUserNotifications_Empty | query against a never-seen tenant returns empty, not error |
GetNotification_NotFound | same for a single Get | |
ListDevices_Empty | same for device listing | |
| RoundTrip | StringFields_OnCreate | adversarial values — emoji, unicode, SQL-shaped, leading/trailing whitespace, 10k chars — survive byte-for-byte |
Int64_Fidelity_Timestamps | extreme int64 values (max_int64, 2^53+1) round-trip exactly — proves the driver does not silently coerce to float64 | |
LargePayload_Body | 64 KiB and 512 KiB bodies round-trip unmodified | |
| Concurrency | ConcurrentCreate_DistinctKeys_NoLostWrites | 16 concurrent creates with distinct keys land 16 rows — the WAL/applier serializes correctly |
ConcurrentCreate_SameKey_SingleWinner | The headline test. 16 concurrent creates with the same key land exactly one row; all callers see the same canonical id | |
ConcurrentUpsertDevice_SameKey_SingleRow | concurrent device upserts on the same (tenant, user, device_type) land exactly one row; token reflects one of the writers | |
ConcurrentUpdateStatus_NoError | concurrent status transitions don't error; the final state matches one of the racers | |
ConcurrentReadYourWrites_QueryAfterCreate | a writer's very next query sees its own row (no stale-read window) | |
| KeyEdge | NotificationID_LongValue | 256-char notification id round-trips; re-create is idempotent |
NotificationID_SeparatorBytesDoNotCollide | distinct triples with separator-byte payloads do not collide on the composite key (length-prefix encoding canary) | |
DeviceType_CaseSensitive_SeparateRows | "android" and "Android" are distinct composite keys (byte-exact equality) |
+ Concurrency/ConcurrentCreate_SameKey_SingleWinner is the
+ subtest that catches the broadest class of bugs. It launches 16
+ goroutines that all call CreateNotification with the
+ same (tenant, user, notification_id):
+
+
+ + This catches: +
+
+ Each non-memory driver ships a CONFORMANCE.md with the
+ full per-subtest table, status, and a one-line root-cause note for
+ anything red. Templates to follow:
+
+ .github/workflows/conformance.yml declares one job per
+ driver in a matrix. The check name is
+ Conformance / <driver>, and that is what branch
+ protection pins. A new driver landing PR must add itself to the
+ matrix and turn the new check green.
+
+
+
+ The EntDB driver maps notify's Store contract onto
+ tenant-shard-db's
+ SDK. v0.1 targets v2 schema-aware mode (ADR-031) and
+ pins to v2.0.5 — the v2 server materializes the notify schema from
+ proto descriptors and enforces composite uniqueness, which is what
+ makes the concurrent-create conformance test pass without a
+ service-level mutex.
+
+ Pick EntDB if you already run tenant-shard-db for another service + (e.g. identity) and want notify to share the operational story — + same image management, same WAL pipeline, same scaling story. For + greenfield deployments, the Postgres driver is the more common + starting point. +
+ +
+
+
+ NOTIFY_ENTDB_TENANT_ID is the EntDB storage shard — one
+ notify container talks to one EntDB tenant. All notify-application
+ tenants live under that one EntDB tenant, differentiated by the
+ tenant_id field on each row.
+
+ Schema is declared as proto messages in
+ proto/entdb_notify/notify.proto. type_id
+ values are local to notify's tenant-shard-db instance (it runs its
+ own EntDB deployment), so the type ids do not need to coordinate
+ with identity's.
+
+
+
+ The composite_key field on each node is the unique
+ index the driver hits via sdk.GetByKey for the
+ idempotency guard. v2 server enforces the
+ unique: true annotation, closing the
+ composite-uniqueness race the v1.32.1 canary tests reproduced.
+
+ Why this design. + The composite key string is built by length-prefix encoding each + part rather than concatenating with a separator. A naive ++ +"u1|n1|n2"scheme collides for two distinct triples + that escape differently (e.g. one with a literal|in + the user id). The driver'slpEncodehelper writes +<len>:<part>for each piece so the + serialization is injective. The +KeyEdge/NotificationID_SeparatorBytesDoNotCollide+ conformance test pins this invariant — drivers that get it wrong + would silently treat two different rows as the same. +
+ The container wires the SDK with the registered schema at boot: +
+
+
+
+ entdb.SchemaMessages() returns the two proto descriptors
+ declared above. The v2 server materializes the schema from those
+ descriptors on the first ExecuteAtomic call and
+ enforces every annotation including unique: true.
+ Concurrent CreateNotification calls with the same
+ composite_key now produce exactly one row.
+
+ The Go SDK's Plan.Commit does not expose
+ wait_applied today (upstream item #U1, documented in
+ store/entdb/CONFORMANCE.md),
+ so the loser of a unique-constraint race receives a
+ success=true receipt with a pre-allocated node id that
+ never actually lands. The driver reconciles in code:
+
+
+
+ CreateNotification returns
+ (canonicalID, won) so the caller learns whether it was
+ the actual writer. UpsertDevice additionally runs a
+ no-wait UpdateFields against the canonical row in the
+ loser branch so each racer's token / last_active_ms gets a chance to
+ land (preserving memory's "last writer wins" semantics).
+
ghcr.io/elloloop/tenant-shard-db:2.0.5 (pin in docker-compose or your Helm chart).github.com/elloloop/tenant-shard-db/sdk/go/entdb/v2 v2.0.5.-addr=:50051 -data-dir=/tmp/entdb -wal-backend=memory (-data-dir is required on v2).sdk.WithSchema(entdb.SchemaMessages()...).+ 24/24 PASS on v2.0.5 — including the two + composite-uniqueness canaries that were intentionally red on v1.32.1 + (they flipped when the v2 schema-aware unique enforcement landed). + See store/entdb/CONFORMANCE.md + for the per-subtest table. +
+ +
+
+ Intentional omissions documented for transparency:
+system:notify as the actor for every read and write; cross-tenant isolation is enforced by the tenantID argument the driver passes to the SDK transport.+ The memory driver is the in-process reference implementation. All + state lives in Go maps protected by a mutex; restart loses + everything. It is the differential reference for the conformance + suite — every other driver is verified against memory's exact + behaviour. +
+ +It is the default. No env vars required.
+
+
+ Or explicitly:
+
+
+
+
+ sync.RWMutex guards two maps.created=false.
+
+ + All 24 leaf subtests across the six categories pass. memory is the + fastest of the three (no testcontainer warm-up, no EntDB SDK + handshake) and is the gate that catches a botched conformance + refactor before the slower drivers run. +
+ ++ If you are writing tests that exercise notify's behaviour from your + consumer's POV — without spinning up Postgres or EntDB — wire + memory directly: +
+
+
+
+ The Postgres driver is the typical production shape: two tables, two
+ indexes per table, pgx v5 connection pool, embedded
+ migrations applied under pg_advisory_lock. Composite
+ uniqueness is enforced by Postgres, not the application — that is
+ what makes the conformance suite's same-key concurrency tests pass
+ cleanly.
+
+ Default choice for production unless you already run EntDB. Works + with any managed Postgres 14+ (RDS, Azure Database for PostgreSQL, + Cloud SQL). +
+ +
+
+ + See Store + Setup → Postgres for the DSN options and the provisioning SQL. +
+ + 'read';
+
+CREATE TABLE notify_devices (
+ id uuid PRIMARY KEY,
+ tenant_id text NOT NULL,
+ user_id text NOT NULL,
+ device_type text NOT NULL,
+ token text NOT NULL DEFAULT '',
+ created_at_ms bigint NOT NULL DEFAULT 0,
+ last_active_ms bigint NOT NULL DEFAULT 0,
+ UNIQUE (tenant_id, user_id, device_type)
+);
+
+CREATE INDEX notify_devices_user
+ ON notify_devices (tenant_id, user_id, device_type);`} lang="sql" />
+
+ Three things matter about this schema:
+
+ - Composite uniqueness is enforced by Postgres. The
UNIQUE(tenant_id, user_id, notification_id) constraint is what makes ON CONFLICT DO NOTHING safe for the idempotent CreateNotification and what makes the same-key concurrency test pass without an application-level mutex.
+ - The paging index is the tuple the query sorts by.
(tenant_id, user_id, created_at_ms DESC, id DESC) matches ORDER BY created_at_ms DESC, id DESC, so paging is an index scan, not a sort node.
+ - The unread partial index keeps unreadCount cheap. Even with millions of read rows, the unread count is a scan over the partial index.
+
+
+ Idempotency / upsert pattern
+
+ CreateNotification:
+
+
+
+
+ If RETURNING produces a row, the writer won the
+ insert and gets created=true. If pgx.ErrNoRows
+ comes back (ON CONFLICT absorbed the dup), a follow-up SELECT
+ recovers the canonical id and returns created=false.
+ This satisfies both the basic Idempotency test and the
+ same-key concurrency test: exactly one goroutine gets the
+ RETURNING row, the rest take the recovery path.
+
+
+
+ UpsertDevice:
+
+
+
+
+ created_at_ms is intentionally not touched on conflict
+ — token rotation preserves the original create time.
+
+
+
+ UpdateStatus stamps the matching *_at_ms in
+ one statement via a CASE:
+
+
+
+
+ A RowsAffected() == 0 result maps to
+ notify.ErrNotFound.
+
+
+ Pagination
+
+ - Order:
ORDER BY created_at_ms DESC, id DESC.
+ - Cursor:
strconv.FormatInt(lastRow.CreatedAtMS, 10) — same wire format as the memory driver, so cross-driver tests interchange.
+ - Next page:
WHERE created_at_ms < $cursor (strict <, per the contract).
+ - Detect next page:
LIMIT n+1. If you get back n+1 rows, slice to n and emit nextCursor = items[n-1].CreatedAtMS. Saves a second round-trip and a count(*).
+ unreadCount is a separate SELECT count(*) FROM ... WHERE status <> 'read' — independent of the page window and any UnreadOnly filter.
+
+
+ Migrations
+
+ Embedded as a Go slice in store/postgres/migrations.go;
+ applied under pg_advisory_lock(0x6E6F74696679_00) so
+ concurrent process boots serialise on the lock instead of racing
+ the DDL. The bookkeeping table notify_schema_migrations
+ is idempotent — boots after the first see it and skip applied
+ versions.
+
+
+
+ Migrations are append-only. A new schema change becomes a new
+ migration{`{}`} entry at the end; shipped versions never get
+ edited. The advisory lock key is unlikely to collide
+ ("notify\0" in hex), but if another driver in the same
+ database picks the same key its migrations would block on ours.
+
+
+ Connection pool
+
+ MaxConns = 25 by default — matches identity's DefaultMaxConns.
+ MaxConnLifetime = 1h, MaxConnIdleTime = 30m — cooperate with pgbouncer / Azure idle reapers.
+
+
+ Conformance results
+
+ 24/24 pass under -race. Total wall time after the
+ testcontainers Postgres warm-up is ~3.3s on Apple Silicon. See
+ store/postgres/CONFORMANCE.md
+ for the per-subtest table and design notes.
+
+
+ Running the suite locally
+
+
+
+ The first run pulls postgres:16.13-alpine3.23; subsequent
+ runs reuse the layer cache.
+
+
+ Related
+
++ Multi-channel notification platform as a single, deployable container. + Pull a pinned image, point it at a durable store (memory · Postgres · + EntDB), + configure one or more channel providers, and you have in-app real-time + SSE, email, web push, mobile push, SMS and WhatsApp delivery — all over + Connect-RPC (HTTP/JSON) and gRPC. +
+ +Sender interface. Ships with the elloloop EmailService backend; slots for SES, ACS, SMTP, SendGrid.whatsapp: prefix.ErrSubscriptionGone sentinel on 410 responses for token-purge logic.ErrUnregisteredToken sentinel for the standard UNREGISTERED / NOT_FOUND cases.Store contract; all drivers run the same 24-subtest conformance suite.Notify call is keyed by (tenant, user, notification_id). Retries never duplicate deliveries.pending → delivered → acked → read (or failed) tracked per row./healthz, /metrics on a separate port.cmd/notifyd wires it all from environment variables; library mode is the same code, minus the wiring.+ One container, three listeners. Treat it like + tenant-shard-db + or identity: pin a + tag, deploy once per environment. Auth is JWT (HS256) against a shared + secret — no callback into any consumer. +
+ +
+
+ The container exposes three ports:
+8080 — NotificationClientService (browser / mobile, Connect HTTP/2)8081 — NotificationInternalService (backend producers, gRPC)9090 — /healthz and /metrics (Prometheus exposition)+ The proto contract carries two services so the auth + transport story + cleanly splits the two callers: +
+| Service | Caller | Auth |
|---|---|---|
NotificationInternalService | Backend producers (your services that send notifications) | X-Notify-Internal-Token shared secret |
NotificationClientService | Browser + mobile clients (recipients reading + streaming) | Authorization: Bearer <JWT> |
+ All durable state — notification rows and device registrations —
+ lives in the configured store. Memory is the in-process reference
+ used for tests and dev; Postgres uses pgx; EntDB
+ targets the v2 schema-aware mode (ADR-031) and rides server-enforced
+ composite-key uniqueness.
+
| Type | Where (memory/postgres) | Where (entdb) |
|---|---|---|
| Notification | notify_notifications table | UserNotification node, type_id = 1 |
| Device | notify_devices table | DeviceRegistration node, type_id = 2 |
+ notify runs its own EntDB instance; type_id values are
+ scoped to that deployment and do not collide with the identity
+ namespace.
+
NOTIFY_* variable component.
+ Expressive-code now provides chrome (border/radius/copy buttons). We just
+ guarantee horizontal scroll inside the container so blocks never widen
+ the page on mobile. */
+ .prose pre.astro-code,
+ .prose pre {
+ max-width: 100%;
+ overflow-x: auto;
+ }
+ /* Fallback background for any non-Shiki (legacy raw blocks if any). */
+ .prose pre:not(.astro-code):not([class*="ec-"]):not(.expressive-code pre) {
+ @apply relative my-6 overflow-x-auto rounded-lg border border-zinc-800/80 px-5 py-4 text-[13px] leading-[1.65] shadow-md;
+ background-color: hsl(var(--code-bg));
+ color: hsl(var(--code-fg));
+ font-family: var(--font-mono);
+ }
+ .prose pre code {
+ @apply block border-0 bg-transparent p-0 font-normal;
+ font-family: var(--font-mono);
+ font-feature-settings: "ss02", "cv01", "cv02";
+ }
+ /* Custom thin scrollbars inside legacy code blocks */
+ .prose pre::-webkit-scrollbar {
+ height: 0.5rem;
+ }
+ .prose pre::-webkit-scrollbar-track {
+ background: transparent;
+ }
+ .prose pre::-webkit-scrollbar-thumb {
+ background: rgb(63 63 70);
+ border-radius: 0.25rem;
+ }
+ .prose pre::-webkit-scrollbar-thumb:hover {
+ background: rgb(82 82 91);
+ }
+
+ /* Expressive-code wrapper — never let the frame widen the page. */
+ .prose .expressive-code {
+ @apply my-6 max-w-full;
+ }
+ .prose .expressive-code .frame {
+ box-shadow: none;
+ }
+ .prose .expressive-code pre {
+ border-radius: 0.5rem;
+ }
+
+ /* Tables — wrapped in .table-scroll for horizontal overflow on mobile. */
+ .prose .table-scroll {
+ @apply -mx-2 my-6 overflow-x-auto rounded-lg border border-border;
+ position: relative;
+ }
+ /* Subtle gradient-fade on the right edge to hint scrollability. */
+ .prose .table-scroll::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ width: 24px;
+ pointer-events: none;
+ background: linear-gradient(
+ to left,
+ hsl(var(--background)),
+ hsl(var(--background) / 0)
+ );
+ opacity: 0;
+ transition: opacity 200ms;
+ }
+ @media (max-width: 768px) {
+ .prose .table-scroll::after {
+ opacity: 0.85;
+ }
+ }
+ .prose table {
+ @apply w-full border-collapse text-[14px];
+ margin: 0;
+ border: 0;
+ }
+ .prose thead {
+ @apply bg-muted/60;
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ }
+ .prose th {
+ @apply border-b border-border px-4 py-2.5 text-left text-xs font-semibold uppercase tracking-wider text-muted-foreground;
+ }
+ .prose td {
+ @apply border-b border-border/70 px-4 py-2.5 align-top text-foreground;
+ overflow-wrap: break-word;
+ }
+ .prose tbody tr {
+ transition: background-color 150ms;
+ }
+ .prose tbody tr:hover {
+ @apply bg-muted/30;
+ }
+ .prose tbody tr:last-child td {
+ @apply border-b-0;
+ }
+ .prose tbody tr:nth-child(even) {
+ @apply bg-muted/15;
+ }
+ .prose tbody tr:nth-child(even):hover {
+ @apply bg-muted/40;
+ }
+ .prose table code {
+ @apply whitespace-nowrap;
+ }
+
+ /* Bare table (one without a wrapper) — make it scroll on its own. */
+ .prose > table {
+ display: block;
+ overflow-x: auto;
+ white-space: nowrap;
+ }
+
+ /* Blockquotes / callouts fallback */
+ .prose blockquote {
+ @apply my-6 rounded-r-lg border-l-4 border-primary bg-primary/5 px-5 py-4 text-foreground;
+ }
+ .prose blockquote p {
+ @apply mb-0;
+ }
+}
+
+/* Dark-mode token overrides. Kept outside @layer base because Tailwind's
+ purge step can drop the `.dark` selector if it doesn't appear in a
+ content file — and the theme toggle adds it dynamically. */
+.dark {
+ --background: 222 47% 5%;
+ --foreground: 210 40% 98%;
+ --card: 222 47% 7%;
+ --card-foreground: 210 40% 98%;
+ --popover: 222 47% 7%;
+ --popover-foreground: 210 40% 98%;
+ --primary: 234 89% 74%;
+ --primary-foreground: 222 47% 11%;
+ --secondary: 217 33% 17%;
+ --secondary-foreground: 210 40% 98%;
+ --muted: 217 33% 17%;
+ --muted-foreground: 215 20% 65%;
+ --accent: 217 33% 17%;
+ --accent-foreground: 210 40% 98%;
+ --destructive: 0 63% 31%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 217 33% 17%;
+ --input: 217 33% 17%;
+ --ring: 234 89% 74%;
+
+ --code-bg: 222 47% 3%;
+ --code-fg: 210 40% 98%;
+}
+
+/* Pagefind UI — recolor to match the theme. */
+:where(.pagefind-ui) {
+ --pagefind-ui-primary: hsl(var(--primary));
+ --pagefind-ui-text: hsl(var(--foreground));
+ --pagefind-ui-background: hsl(var(--background));
+ --pagefind-ui-border: hsl(var(--border));
+ --pagefind-ui-tag: hsl(var(--muted));
+ --pagefind-ui-border-width: 1px;
+ --pagefind-ui-border-radius: 0.5rem;
+ --pagefind-ui-image-border-radius: 0.5rem;
+ --pagefind-ui-image-box-ratio: 3 / 2;
+ --pagefind-ui-font: var(--font-sans);
+}
diff --git a/docs-site/tailwind.config.mjs b/docs-site/tailwind.config.mjs
new file mode 100644
index 0000000..ed1623b
--- /dev/null
+++ b/docs-site/tailwind.config.mjs
@@ -0,0 +1,28 @@
+import { refractionPreset } from "@refraction-ui/tailwind-config";
+
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: [
+ "./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}",
+ // refraction-ui's components ship Tailwind utility classes inside
+ // node_modules. Without scanning them, classes like the AppShellOverlay's
+ // `bg-black/50` and `z-30` get purged and the mobile sidebar backdrop
+ // becomes invisible — making the hamburger appear broken.
+ "./node_modules/@refraction-ui/astro/dist/**/*.{astro,js,mjs,ts}",
+ "./node_modules/.pnpm/@refraction-ui+astro@*/node_modules/@refraction-ui/astro/dist/**/*.{astro,js,mjs,ts}",
+ ],
+ presets: [refractionPreset],
+ darkMode: "class",
+ // The .dark class is toggled by the theme script at runtime, and Tailwind's
+ // JIT only emits a base-layer rule if its selector appears in `content`.
+ // Safelist `dark` so the CSS-variable overrides survive the purge step.
+ safelist: ["dark"],
+ theme: {
+ extend: {
+ fontFamily: {
+ sans: ["var(--font-sans)"],
+ mono: ["var(--font-mono)"],
+ },
+ },
+ },
+};
diff --git a/docs-site/tsconfig.json b/docs-site/tsconfig.json
new file mode 100644
index 0000000..b7243b9
--- /dev/null
+++ b/docs-site/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "react"
+ }
+}