diff --git a/.env.example b/.env.example index 2443b39..83ecb45 100644 --- a/.env.example +++ b/.env.example @@ -120,8 +120,9 @@ FEDI_DOMAIN=example.com # --- Optional: Web Push (PWA phone notifications) --- # Lets you install the site to your phone's home screen and receive push # notifications for likes/boosts/replies/follows/DMs/comments — even when closed. -# Dormant until these are set. Generate a keypair with: -# npx web-push generate-vapid-keys +# EASIEST: generate keys in Admin → Site settings → Phone notifications (one +# click, stored encrypted at rest — no keys in this file). These env vars are a +# fallback for automated deploys; a key generated in the admin panel wins. # VAPID_PUBLIC_KEY= # VAPID_PRIVATE_KEY= # VAPID_SUBJECT=mailto:you@example.com diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c6e003..2aa6753 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## 1.19.0 (2026-07-23) + +### Added +- **Set your avatar and banner during first-run setup** (#59) — the setup wizard now takes a profile picture and banner (optional), so a fresh install lands looking like *you* without a later trip to the admin panel. Uploads are gated by your setup token (the same one that protects setup) and go through the same optimise/EXIF-strip pipeline as everywhere else. A corrupt or mislabelled image is now a clear error instead of a 500. +- **Set session + token lifetimes from the admin** (#59) — a new **Security** section in **Admin → Site settings** sets how long an admin session stays signed in (default 30 days) and how long a generated app token lasts (0 = never expires), with no `ADMIN_SESSION_TTL_DAYS` / `APP_TOKEN_TTL_DAYS` env editing. Changes apply to newly-created sessions and tokens; the env vars still work as defaults. +- **A third theme — "Classic Blog"** (#250) — a left sidebar of your bio, recent posts and links beside a serif reading column, on a warm charcoal ground with a pine-green accent: a traditional personal-blog counterpoint to the default's glass and Editorial's sepia. Pick it in **Admin → Site settings → Appearance → Theme** (no restart) or set `THEME=classic`. Default instances render identically — nothing changes unless you choose it. A theme can now also **preset the sidebar's side and blocks** (Classic Blog ships a left sidebar with the nav kept out of it), which you can still override per-site. +- **Turn on phone notifications without touching the server** (#59) — **Admin → Site settings → Phone notifications** generates your Web Push (VAPID) keypair in one click, so PWA push works with no `npx web-push generate-vapid-keys` or `.env` editing. The private signing key is **stored AES-256-GCM-encrypted at rest** and never sent to the browser; the `VAPID_*` env vars still work as a fallback. Regenerating keys correctly **unsubscribes every device** (a subscription is bound to the old key and can't receive new sends) — and this also fixes a latent bug where, after any key change, a device would show notifications as "on" while silently receiving nothing; it now detects the mismatch and prompts you to re-enable. +- **Put the sidebar where you want it, with the blocks you want** (#307) — the sidebar layout can now sit on the **left or right**, and its blocks are an **ordered list you control**: `about, recent, sections, connect`. The order you write is the order they render, and leaving one out hides it — so dropping `sections` stops your nav appearing in both the header and the sidebar. Set both in **Admin → Site settings → Appearance** (they appear once you pick the Sidebar page width), or during first-run setup. On mobile your content always comes first regardless of which side you choose. + +- **Themeable text colour** (#250) — text now resolves through a `content-*` ramp (primary → strong → muted → subtle → faint → dim → ghost) instead of fixed Tailwind greys, so a theme can move body copy and not just its background. This is the groundwork a **light theme** and the upcoming in-admin **custom palette** both need: `@theme` extends Tailwind's palette rather than replacing it, so hard-coded `text-white` / `text-gray-*` could never follow a theme — which is why every theme so far has had to be dark. **Nothing changes visually:** each token defaults to the exact neutral it replaces, and every built-in theme shares the default ramp, so the emitted CSS is unchanged. The site's header, footer, sidebar, mobile menu and notification menu are converted; the rest of the app follows in later passes, and until then themes stay dark (the contrast invariant still enforces it). + +### Changed +- **Web Push is documented** (#59) — `docs/configuration.md` gains a **Phone Notifications** section. Push had no documentation anywhere: how to switch it on, the iOS home-screen requirement, that regenerating keys deliberately unsubscribes every device, and that the `VAPID_*` env vars are now only a fallback. +- **`docs/theming.md` rewritten for the current theme system** (#250) — the old guide predated themes entirely (it told you to hand-edit `globals.css` and swap avatar files on disk). It now covers what's web-editable in the admin panel, the `Theme` token contract + region×variant model, and a **developer scaffold** for adding a built-in theme (one data file + one registry line, with the dark/contrast and self-hosted-font constraints spelled out). +- Dependency refresh: `next` 16.2.11, `react`/`react-dom` 19.2.8, `postcss` 8.5.22, `marked` 18.0.7, `@atproto/api` 0.20.31, `eslint-config-next` 16.2.11. (`typescript` remains blocked upstream — #234.) + +### Fixed +- **Phone notifications couldn't be switched on at all on a fresh install** (#59) — the 🔔 menu offered **Enable phone notifications** even when the server had no Web Push keypair yet, then failed on the click with a terse *"push not configured on server"* — and nothing pointed at the panel that would fix it, which sat in a different screen entirely. The menu now checks first and, when there's no keypair, offers **Set up phone notifications**: one click creates the keys *and* enables the device. If that can't work (for instance `ADMIN_SECRET` isn't set, so the private key can't be encrypted at rest) it now tells you the actual reason. iPhone users still need **Share → Add to Home Screen** first — iOS only gives push to installed apps — and the menu keeps saying so. + +### Security +- **Patched a high-severity image-processing vulnerability** (#329) — `sharp` moves to 0.35.3, clearing [GHSA-f88m-g3jw-g9cj](https://github.com/advisories/GHSA-f88m-g3jw-g9cj) (four libvips CVEs, CVSS 7.0). This one was genuinely reachable rather than theoretical: FediHome decodes images fetched from **arbitrary remote instances** when it caches fediverse media, and `/_next/image` is a public endpoint that fetches and decodes thumbnails from the allow-listed PeerTube hosts — so bytes an attacker controls reached the vulnerable decoders. Upgrading alone would **not** have fixed it: Next.js pins its own copy of `sharp`, which would have left a second, still-vulnerable one (and a duplicate native image library) in the tree, invisible to type-checking, tests and builds alike. `sharp` is therefore pinned tree-wide via `overrides`, so exactly one patched copy is installed. Giving `postcss` the same treatment clears the last remaining transitive advisory (#12), and `npm audit` now reports **0 vulnerabilities**. + ## 1.18.0 (2026-07-21) ### Fixed diff --git a/docs/configuration.md b/docs/configuration.md index 3de26a7..cf89521 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -112,6 +112,40 @@ Some settings are stored in the database (`SiteSettings` table) and managed thro Changes made in the admin panel are saved to the database and take effect immediately without restarting the server. +## Phone Notifications (Web Push) + +FediHome can push likes, boosts, replies, follows, DMs and comments to your phone +or desktop — even when the site isn't open. **There is nothing to configure on the +server:** the keys it needs are generated in-app the first time you turn it on. + +**Turning it on:** + +1. Open the **🔔 menu** (visible only when you're signed in as the owner) and + choose **Set up phone notifications**. That generates your Web Push **VAPID + keypair**, stores the private half AES-256-GCM-encrypted, and subscribes the + device you're on — one click, no `.env` editing and no restart. +2. Hit **Test** to confirm a notification actually arrives. + +**On iPhone/iPad you must install the site first.** iOS only exposes push to +home-screen apps, so in Safari tap **Share → Add to Home Screen**, then open +FediHome *from the Home Screen* and enable notifications there. Until you do, the +menu shows install guidance instead of an enable button — that's expected, not an +error. + +Repeat step 1 on each device you want notifications on; subscriptions are +per-device. + +**Rotating keys.** **Admin → Site settings → Phone notifications** shows the key +status and can **regenerate** or **clear** the keypair. Regenerating deliberately +**unsubscribes every device**: a subscription is bound to the key it was created +with and can never receive a send signed by a new one. Each device simply detects +the mismatch and offers to re-enable. + +**Env fallback.** `VAPID_PUBLIC_KEY`, `VAPID_PRIVATE_KEY` and `VAPID_SUBJECT` still +work for automated deploys, but a key generated in the admin panel takes +precedence. Generating in-app requires `ADMIN_SECRET` to be set, since that's what +encrypts the private key at rest. + ## Navigation Configuration The `nav` object in `site.config.ts` controls which top-level pages appear in the navbar: diff --git a/docs/theming.md b/docs/theming.md index d312963..b124856 100644 --- a/docs/theming.md +++ b/docs/theming.md @@ -1,235 +1,197 @@ # Theming -FediHome uses Tailwind CSS with custom design tokens defined in CSS variables. You can customize colors, fonts, and layout without modifying component code. - -## Theme Architecture - -All theme variables are defined in `src/app/globals.css` inside the `@theme` block. Tailwind picks these up as custom utilities, so they're available throughout the app as classes like `bg-surface-900` or `text-accent-400`. - -The default theme is a dark palette with blue accents: - -```css -@theme { - /* Neutral slate dark palette */ - --color-surface-950: #0a0a0f; - --color-surface-900: #111118; - --color-surface-800: #1a1a24; - --color-surface-700: #252530; - --color-surface-600: #3a3a4a; - - /* Blue accent */ - --color-accent-50: #eff6ff; - --color-accent-100: #dbeafe; - --color-accent-200: #bfdbfe; - --color-accent-300: #93c5fd; - --color-accent-400: #60a5fa; - --color-accent-500: #3b82f6; - --color-accent-600: #2563eb; - --color-accent-700: #1d4ed8; - --color-accent-800: #1e40af; - --color-accent-900: #1e3a8a; - - /* Success green */ - --color-moss-400: #34d399; - --color-moss-500: #10b981; - --color-moss-600: #059669; - - /* Typography */ - --font-display: "Source Serif 4", "Georgia", serif; - --font-body: "Inter", system-ui, -apple-system, sans-serif; - --font-mono: "JetBrains Mono", "Fira Code", monospace; +FediHome's look is a **theme**: a bundle of design tokens (colour, type, feel) +plus a chosen **layout variant for each region** of the page. There are two ways +to change it — pick one based on who you are: + +- **Site owner?** Everything below in [From the admin panel](#from-the-admin-panel) + is web-editable, no file editing or restart. +- **Developer adding a built-in theme?** Jump to [Authoring a theme](#authoring-a-theme). + +--- + +## From the admin panel + +**Admin → Site settings → Appearance** controls all of this live: + +| Control | What it does | +|---|---| +| **Theme** | Pick a built-in theme — Cards (default), Editorial, Classic Blog. Sets colours, fonts, feel and every region's layout at once. | +| **Accent colour** | Any `#RRGGBB`, **per theme** (each theme remembers its own). "Use theme's accent" reverts to the theme's built-in colour. | +| **Header / Feed / Footer / Page width** | Override any single region's variant (bar/centered/minimal, cards/list, row/minimal/columns, normal/narrow/sidebar). "Inherit from theme" uses the theme's own default. | +| **Sidebar side / blocks** | When the shell is a sidebar: left or right, and an ordered list of blocks (`about, recent, sections, connect`) — omit one to hide it. | + +**Profile picture & banner** live in **Admin → Site settings → Your profile** +(or during first-run setup) — upload directly, no file swapping. + +Under the hood these write to the `SiteSetting` / `SiteSettings` overlay, read +back within ~60s. Env vars (`THEME`, `LAYOUT_*`, `SIDEBAR_*`, `ACCENT_COLOR`, …) +still work as defaults for automated deploys; a saved admin value wins. + +--- + +## How themes work + +A **theme is pure data** (`src/lib/themes/.ts`): the design tokens the UI +resolves to at runtime, plus its layout presets. Everything visual references a +CSS variable (`var(--color-*)`, `var(--font-*)`, `var(--radius-*)`, +`var(--glass-filter)`), defined in `src/app/globals.css`'s `@theme` block. At +runtime `buildThemeStyle` (`src/lib/themes/index.ts`) emits **only the tokens +that differ from the default** as a `:root:root{…}` block and injects it in the +root layout — so the default theme injects nothing and is byte-identical, and a +theme only ships its deltas. + +The `Theme` contract (`src/lib/themes/types.ts`): + +```ts +interface Theme { + id: string; + name: string; + description?: string; + tokens: { + colors: Record; // surface-950…600, accent-50…900, moss-400…600, content-* + fonts: { display; body; mono }; + feel: { radiusCard; radiusButton; glassFilter }; // e.g. "8px", "blur(12px)", "none" + }; + layout: { feed; header; footer; shell }; // a variant per region + sidebar?: { side?; blocks? }; // preset, if shell is "sidebar" } ``` -## Changing the Accent Color - -The accent color is used for links, buttons, borders, badges, and interactive elements throughout the UI. - -### Method 1: Admin Panel (per theme) - -The simplest way is **Admin → Site settings → Appearance**: pick a colour under -"Accent colour" and Save — it applies live, no restart. The accent is **per -theme**: each theme (Cards, Editorial, …) remembers its own, so switching themes -restores the accent you chose for it. Use **"Use theme's accent"** to fall back -to the theme's built-in accent. (Stored in the `SiteSettings` profile row — -`accentColor` for the default theme, `themeAccents` for the rest.) - -### Method 2: Edit globals.css - -For full control over the entire accent scale, edit `src/app/globals.css`: - -```css -@theme { - /* Example: change to purple */ - --color-accent-50: #faf5ff; - --color-accent-100: #f3e8ff; - --color-accent-200: #e9d5ff; - --color-accent-300: #d8b4fe; - --color-accent-400: #c084fc; - --color-accent-500: #a855f7; - --color-accent-600: #9333ea; - --color-accent-700: #7e22ce; - --color-accent-800: #6b21a8; - --color-accent-900: #581c87; -} -``` - -Each shade (50 through 900) is used in different contexts: -- **accent-400/500** — Primary interactive elements (links, buttons) -- **accent-50/100** — Very light tints (text on dark backgrounds) -- **accent-600/700** — Hover states, button gradients -- **accent-800/900** — Darkest accents (rarely used in dark theme) - -You can generate a consistent color scale from any base color using tools like [uicolors.app](https://uicolors.app/create) or [tailwindshades.com](https://www.tailwindshades.com/). - -The button gradient in `.btn-primary` and border colors in `.glass-card` also reference the accent color directly. If you change the accent palette, update these too: - -```css -.btn-primary { - background: linear-gradient(135deg, #a855f7 0%, #9333ea 100%); -} - -.btn-primary:hover { - box-shadow: 0 4px 16px rgba(168, 85, 247, 0.25); -} - -.glass-card { - border: 1px solid rgba(168, 85, 247, 0.1); -} - -.glass-card:hover { - border-color: rgba(168, 85, 247, 0.2); -} -``` - -## Changing Surface Colors - -The surface palette controls the background colors of the page, cards, and panels: - -```css -@theme { - /* Example: warmer dark palette */ - --color-surface-950: #0f0d0a; - --color-surface-900: #1a1714; - --color-surface-800: #252118; - --color-surface-700: #332d22; - --color-surface-600: #4a4232; -} -``` - -The body background is `surface-950`, cards use `surface-900`, and borders/dividers use `surface-700/800`. - -## Changing Fonts - -FediHome ships with three font families: - -- **Display** (`font-display`) — Used for headings. Default: Source Serif 4 (serif) -- **Body** (`font-body`) — Used for body text, navigation, UI elements. Default: Inter (sans-serif) -- **Mono** (`font-mono`) — Used for code blocks and inline code. Default: JetBrains Mono - -### Replacing a Font - -1. Download the font file(s) in `.woff2` format -2. Place them in `public/fonts/` -3. Update the `@font-face` rule in `globals.css`: - -```css -@font-face { - font-family: "Your Font Name"; - src: url("/fonts/YourFont-Variable.woff2") format("woff2"); - font-weight: 100 900; - font-style: normal; - font-display: swap; -} -``` - -4. Update the `@theme` block: - -```css -@theme { - --font-body: "Your Font Name", system-ui, sans-serif; -} -``` - -### Using System Fonts Only - -To skip custom font loading entirely and use system fonts: - -```css -@theme { - --font-display: Georgia, "Times New Roman", serif; - --font-body: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; - --font-mono: "SF Mono", "Cascadia Code", "Consolas", monospace; -} -``` - -Then remove or comment out the `@font-face` rules. - -## Custom CSS Overrides - -You can add custom CSS at the bottom of `globals.css`. Styles defined there take precedence over Tailwind utilities when specificity is equal. - -Examples: - -```css -/* Make all headings use the accent color */ -h1, h2, h3, h4, h5, h6 { - color: var(--color-accent-400); -} - -/* Rounded avatar instead of default */ -.avatar-img { - border-radius: 50%; -} - -/* Custom link underline style */ -a:hover { - text-decoration: underline; - text-underline-offset: 3px; -} - -/* Hide the footer entirely */ -footer { - display: none; -} -``` - -## Component Classes - -FediHome uses several reusable CSS classes defined in `globals.css`: - -| Class | Purpose | -|-------|---------| -| `.glass-card` | Card container with translucent background, blur, and subtle border. Used for post cards, timeline entries, and panels. | -| `.btn-primary` | Solid gradient button (accent color). Used for primary actions. | -| `.btn-outlined` | Outlined button with accent border. Used for secondary actions. | -| `.divider` | A subtle horizontal line with gradient fade on edges. | -| `.fedi-badge` | Small pill-shaped badge for Fediverse interaction counts. | -| `.lightbox-overlay` | Full-screen overlay for the photo lightbox. | - -You can override any of these to change the look of core UI elements. - -## Uploading Custom Avatar and Banner - -### Avatar - -Replace the file at `public/images/avatar.png` with your own image. The recommended size is 400x400 pixels. PNG or WebP format. - -The avatar is used in: -- The site navbar -- Your ActivityPub profile (what Mastodon users see) -- RSS feed metadata - -### Banner - -Replace the file at `public/images/banner.webp` with your own image. The recommended size is at least 1500x500 pixels. WebP format is preferred for file size. - -The banner is used in: -- The homepage header area -- Your ActivityPub profile header image - -### Open Graph Image - -Replace `public/images/og-image.webp` with a custom image (1200x630 pixels recommended). This is the default image shown when your site URL is shared on social media without a specific post cover image. - -After replacing any of these files, the changes take effect immediately (or after clearing any CDN cache if you use one). +### The text ramp + +Colour tokens cover the *ground* (`surface-*`) and the *brand* (`accent-*`), but +body text was long hard-coded to Tailwind neutrals — which `@theme` can't move, +because it extends the palette rather than replacing it. That's the single reason +every theme has had to be dark. + +`content-*` (`src/lib/themes/content.ts`) is the text ramp that fixes it, +brightest → dimmest: + +| Token | Role | Was | +|---|---|---| +| `content` | headings, body copy | `text-white` | +| `content-strong` | just under primary | `text-gray-200` | +| `content-muted` | secondary copy | `text-gray-300` | +| `content-subtle` | labels, captions | `text-gray-400` | +| `content-faint` | timestamps, counts, bylines | `text-gray-500` | +| `content-dim` | decorative, de-emphasised | `text-gray-600` | +| `content-ghost` | hairline text | `text-gray-700` | + +Use `text-content`, `text-content-faint`, … in components. Each token **defaults +to the exact Tailwind neutral it replaces** (`globals.css` sets +`--color-content-faint: var(--color-gray-500)`), so migrating a utility is a pure +rename with no visual change, and a theme that doesn't set them renders +identically to before. + +The migration is **partial** — the layout chrome is on tokens, the rest of the app +isn't yet. Until it finishes, light themes stay blocked and the dark-only contrast +invariant stands. If you're touching a component's text colours, moving them onto +these tokens is the way to help (#250). + +`themes.test.ts` asserts the readable tier (`content` … `content-subtle`) stays AA +on each theme's own `surface-950`, and that the ramp is monotonic. `content-faint` +and below are metadata and decoration and sit below AA by design. + +### Regions × variants + +Layout is a curated catalogue, not arbitrary markup (so a theme can't break +ActivityPub actor pages, feeds, SEO or a11y). Each region offers a fixed set of +variants (`src/lib/themes/layout.ts`, `LAYOUT_REGIONS`): + +| Region | Variants | +|---|---| +| `feed` | `cards` · `list` | +| `header` | `bar` · `centered` · `minimal` | +| `footer` | `row` · `minimal` · `columns` | +| `shell` | `normal` · `narrow` · `sidebar` | + +A theme sets a default variant per region; the owner can override any one from +the admin panel (`resolveLayout` / `resolveSidebar` layer owner → theme → +built-in default). + +--- + +## Authoring a theme + +Adding a built-in theme is **one data file + one registry line** — no other +wiring. It auto-appears in the admin theme picker and the first-run wizard +(both iterate the `THEMES` registry). + +1. **Create `src/lib/themes/.ts`.** Copy an existing one as a template — + `editorial.ts` (warm, list feed) or `classic.ts` (sidebar shell) are the + closest to a "second look". + + ```ts + import type { Theme } from "./types"; + + export const OCEAN_THEME: Theme = { + id: "ocean", + name: "Ocean", + description: "Cool teal on deep navy — a calm reading theme.", + tokens: { + colors: { + "surface-950": "#0a1420", /* … 900/800/700/600, monotonic in luminance */ + // Pin the accent ramp from a single base — see the constraint below: + "accent-500": "#2f8f83", /* …the full 50–900 scale, pinned */ + "moss-400": "#…", "moss-500": "#…", "moss-600": "#…", + }, + fonts: { display: '…', body: '…', mono: '"JetBrains Mono", monospace' }, + feel: { radiusCard: "12px", radiusButton: "8px", glassFilter: "blur(12px)" }, + }, + layout: { feed: "list", header: "bar", footer: "row", shell: "normal" }, + // sidebar: { side: "left", blocks: ["about", "recent", "connect"] }, // if shell: "sidebar" + }; + ``` + +2. **Register it** in `src/lib/themes/registry.ts`: + + ```ts + import { OCEAN_THEME } from "./ocean"; + export const THEMES = { …, [OCEAN_THEME.id]: OCEAN_THEME }; + ``` + +3. **Run the tests** — `themes.test.ts` iterates every registered theme and + enforces the constraints below, so a bad theme fails CI rather than shipping. + +### Constraints (enforced by `themes.test.ts`) + +- **Themes must (still) be dark.** Most components hard-code Tailwind neutrals + (`text-white`, `text-gray-*`) for body/secondary text, and `@theme` *extends* + the palette so those neutrals stay fixed while `surface-*` moves. On a light + ground `text-white` lands at ~1:1 — an invisible site. A contrast invariant + requires `white` / `gray-200` / `gray-400` to stay ≥ 4.5:1 (AA) on your + `surface-950`. The migration that lifts this is underway — see + [The text ramp](#the-text-ramp) — and is tracked in #250. +- **Fonts swap families only.** `buildThemeStyle` can't register `@font-face`, + and only **Inter** and **Source Serif 4** are self-hosted, so pick among those + (+ system fallbacks). Adding a new face means an `@font-face` in `globals.css`. +- **Pin the accent ramp.** Generate the 50–900 scale from your base with + `deriveAccentScale("#…")` (`src/lib/themes/color.ts`) and paste the literal + values — so the shipped theme's identity can't drift if that ramp is ever + retuned. (This is the same ramp a per-theme custom accent goes through.) +- **`mono` and any token equal to the default diff out** and are never emitted — + only your real deltas ship. + +--- + +## Deeper changes in `globals.css` + +For anything below the token layer, edit `src/app/globals.css`: + +- The `@theme` block defines the **default** token values (what other themes diff + against) and the Tailwind utilities (`bg-surface-900`, `text-accent-400`, …). +- The `@font-face` rules self-host the fonts (`public/fonts/`). +- Component classes — `.glass-card`, `.btn-primary`, `.btn-outlined`, `.divider`, + `.fedi-badge`, `.lightbox-overlay` — style the reusable UI pieces and read the + feel tokens (`var(--radius-card)`, `var(--glass-filter)`). Override any of them + for a site-wide tweak. + +Changes here are compiled into the build (not runtime-swappable per theme), so +they affect every theme's baseline. + +## Open Graph image + +Avatar and banner are web-editable (above). The social-share fallback image is +still a file: replace `public/images/og-image.webp` (1200×630 recommended). It's +used when a URL is shared without a post-specific cover. diff --git a/package-lock.json b/package-lock.json index 4a9f643..b6f5467 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "fedihome", - "version": "1.18.0", + "version": "1.19.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fedihome", - "version": "1.18.0", + "version": "1.19.0", "hasInstallScript": true, "license": "ISC", "dependencies": { - "@atproto/api": "^0.20.30", + "@atproto/api": "^0.20.31", "@fedify/fedify": "^2.3.3", "@fedify/next": "^2.3.3", "@prisma/adapter-pg": "^7.9.0", @@ -22,20 +22,20 @@ "@types/web-push": "^3.6.4", "dotenv": "^17.4.2", "fast-xml-parser": "^5.10.1", - "marked": "^18.0.6", + "marked": "^18.0.7", "music-metadata": "^11.14.0", - "next": "^16.2.10", + "next": "^16.2.11", "next-mdx-remote": "^6.0.0", "nodemailer": "^9.0.1", "pg": "^8.21.0", - "postcss": "^8.5.20", + "postcss": "^8.5.22", "prisma": "^7.9.0", - "react": "^19.2.6", - "react-dom": "^19.2.6", + "react": "^19.2.8", + "react-dom": "^19.2.8", "react-masonry-css": "^1.0.16", "remark-gfm": "^4.0.1", "sanitize-html": "^2.17.6", - "sharp": "^0.34.5", + "sharp": "^0.35.3", "tailwindcss": "^4.3.3", "typescript": "^6.0.3", "web-push": "^3.6.7" @@ -47,7 +47,7 @@ "@types/sanitize-html": "^2.16.1", "@vitest/coverage-v8": "^4.1.10", "eslint": "^10.7.0", - "eslint-config-next": "^16.2.10", + "eslint-config-next": "^16.2.11", "tsx": "^4.23.1", "vitest": "^4.1.10" } @@ -65,9 +65,9 @@ } }, "node_modules/@atproto/api": { - "version": "0.20.30", - "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.20.30.tgz", - "integrity": "sha512-t/9pPW+60AFeefiT186z4Z16yz4UmNGtb0EYVyCIHkwYk6nat57OvJA3oEbp4NK9yd/REsVCTBk6gOk8PNWyQQ==", + "version": "0.20.31", + "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.20.31.tgz", + "integrity": "sha512-TovCQLQv5ti1jqh8UH6jJ0EFuWRjGdUtFyFR5xYC/IkIulwHDuyrVaXdCv7VLWiHftp92DevtZnFRm+BZsZZdw==", "license": "MIT", "dependencies": { "@atproto/common-web": "^0.5.6", @@ -1353,9 +1353,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", + "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", "cpu": [ "arm64" ], @@ -1365,19 +1365,19 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "@img/sharp-libvips-darwin-arm64": "1.3.2" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", + "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", "cpu": [ "x64" ], @@ -1387,19 +1387,38 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "@img/sharp-libvips-darwin-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", + "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", + "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", "cpu": [ "arm64" ], @@ -1413,9 +1432,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", + "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", "cpu": [ "x64" ], @@ -1429,9 +1448,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", + "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", "cpu": [ "arm" ], @@ -1445,9 +1464,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", + "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", "cpu": [ "arm64" ], @@ -1461,9 +1480,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", + "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", "cpu": [ "ppc64" ], @@ -1477,9 +1496,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", + "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", "cpu": [ "riscv64" ], @@ -1493,9 +1512,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", + "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", "cpu": [ "s390x" ], @@ -1509,9 +1528,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", + "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", "cpu": [ "x64" ], @@ -1525,9 +1544,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", + "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", "cpu": [ "arm64" ], @@ -1541,9 +1560,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", + "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", "cpu": [ "x64" ], @@ -1557,9 +1576,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", + "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", "cpu": [ "arm" ], @@ -1569,19 +1588,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "@img/sharp-libvips-linux-arm": "1.3.2" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", + "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", "cpu": [ "arm64" ], @@ -1591,19 +1610,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "@img/sharp-libvips-linux-arm64": "1.3.2" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", + "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", "cpu": [ "ppc64" ], @@ -1613,19 +1632,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "@img/sharp-libvips-linux-ppc64": "1.3.2" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", + "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", "cpu": [ "riscv64" ], @@ -1635,19 +1654,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "@img/sharp-libvips-linux-riscv64": "1.3.2" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", + "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", "cpu": [ "s390x" ], @@ -1657,19 +1676,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "@img/sharp-libvips-linux-s390x": "1.3.2" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", + "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", "cpu": [ "x64" ], @@ -1679,19 +1698,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "@img/sharp-libvips-linux-x64": "1.3.2" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", + "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", "cpu": [ "arm64" ], @@ -1701,19 +1720,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", + "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", "cpu": [ "x64" ], @@ -1723,38 +1742,54 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "@img/sharp-libvips-linuxmusl-x64": "1.3.2" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", + "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", + "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", "cpu": [ "wasm32" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.7.0" + "@img/sharp-wasm32": "0.35.3" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", + "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", "cpu": [ "arm64" ], @@ -1764,16 +1799,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", + "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", "cpu": [ "ia32" ], @@ -1783,16 +1818,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", + "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", "cpu": [ "x64" ], @@ -1802,7 +1837,7 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -1953,15 +1988,15 @@ } }, "node_modules/@next/env": { - "version": "16.2.10", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.10.tgz", - "integrity": "sha512-zLPxg9M0MEHmygpj5OuxjQ+vHMiy/K7cSp74G8ecYolmgUWw0RwN02tF56npup/+qaI8JB97hQgS/r2Hb6QwVA==", + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.11.tgz", + "integrity": "sha512-0do5A3BJ2gxWr0ZCMcD6BhW+e595jyxdTl3rXTS6lOtD8ektMiW6CO+EPwt1Eca1DBnm90r/7GdiKWBKxH++DA==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "16.2.10", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.10.tgz", - "integrity": "sha512-Gs8D2m21VnJeFo9qvYIIqJH94frWerWYu41BprU1pLtRVF7PCQNLiFZZ3fG+iPuj3K83Cwv/rt+msLOy8Qgu3Q==", + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.11.tgz", + "integrity": "sha512-vMEf/aXOpzFFdtIvFYOnIDPKb0xBbrXONsz83CcKdRrekfxNdL8PNkq5qHqAHSXVlIifnX68LOMaxr3z5PkeLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1969,9 +2004,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.10", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.10.tgz", - "integrity": "sha512-v9IdJCa0H0mbo+8z5zwUpOk1Vj7RjkcI5uNYf5Ws1y6szf/p3Mzl9hLaST8SCt6L9h8NGnruZcd2+o0NTNwDhA==", + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.11.tgz", + "integrity": "sha512-wryL4pjKmDwGv2ox6+GZDFxvmtSRLqApBR8kL1j4+vhB7Z5vJC/zAnXpiR9Xkfzl0AS8WLMnsuGV/UKI67/rrw==", "cpu": [ "arm64" ], @@ -1985,9 +2020,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.2.10", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.10.tgz", - "integrity": "sha512-17IS0jJRViROGmA9uGdNR8VPJpfbnaVG7E9qhso5jDLkmyd0lSDORWxbcKINzcFqzZqGwGtMSnrFRxBpuUYjLQ==", + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.11.tgz", + "integrity": "sha512-aZl2j4f/fLyjQvOhv0Oe9UaMAQHolYpKhctsoYzplSumKJKPUmgjcf6545aBtysLTcu994TREd0+pSgNE4ohmg==", "cpu": [ "x64" ], @@ -2001,9 +2036,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.10", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.10.tgz", - "integrity": "sha512-GRQRsRtuciNJvB54AvvuQTiq0oZtFwa1owQqtZD8wwnGpM2L39MV22kpI72YSXLKIyY40LC66EiLFv4PiicXxg==", + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.11.tgz", + "integrity": "sha512-5jEriyEnH/LWFy27L2ZG0XaLlyEJIjhsImEsiS9P563PKEVp2BVups/xfOucIrsvVntp11oNcZwjHvaDPYVB5g==", "cpu": [ "arm64" ], @@ -2017,9 +2052,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.10", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.10.tgz", - "integrity": "sha512-zkN9MQYS7UQBro+FnISUq1itaQjXI9xqISzuQ+2bc921NcJ1x4yPCqrn77tVN6/dOOXaaWVX3k6/bR07pPwK+A==", + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.11.tgz", + "integrity": "sha512-eIjcpx2fnnFSSkZDbTxy74KnokUXDjfoLClpWelfgHLf621aTqswhwXQ7GkD5K5rplrS6LZ/Bj+mVuvzluBOEg==", "cpu": [ "arm64" ], @@ -2033,9 +2068,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.10", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.10.tgz", - "integrity": "sha512-iCVJnwvrPYECvA6WM/7+oo+OiTvedIKLxtCLAZP4xZR3nXa1zmzZyLPbYCmWvpd4CvMYF1EMTafd0ii3DygLvA==", + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.11.tgz", + "integrity": "sha512-8WgzpaWMs46qJT9kiV47cje86L0x/Mu9t8/Gwj+pnbgW3rETVfCnaScPjlYUwNScpOozdcIMHWmAvuZJUonR2w==", "cpu": [ "x64" ], @@ -2049,9 +2084,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.10", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.10.tgz", - "integrity": "sha512-ov2g4H0dHY9bPoOU83m91hWT7Iq5qy13bUnyyshLU3HGR1Ownn0X9QpmDPc5iIUaahTp7f7LeGAhV4DSFtackw==", + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.11.tgz", + "integrity": "sha512-I3UgPds7G4ZYnTb/H+5GBGuUT2DhAk6j0mL6A4s63RjFs74wB2hOWP0vaxsK+3NJraExt3eYEPQ/UtT0x/64Nw==", "cpu": [ "x64" ], @@ -2065,9 +2100,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.10", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.10.tgz", - "integrity": "sha512-DwAnhLX76HQiFFQNgWlcK+JzlnD1rZ+UK/WY0ZMI/deXpvgnesjNYrqcfo1JzBuz4Kf7o3brIBL0glI1junatA==", + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.11.tgz", + "integrity": "sha512-n89CjtcThnjrwgJMAiI5xbqwLY51zvwC9tSlArmVndAJLYVl9T9UAdlkXTmZvE++idoXe8KdglQlhNRdUp1c6g==", "cpu": [ "arm64" ], @@ -2081,9 +2116,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.10", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.10.tgz", - "integrity": "sha512-0JXq3b85Jk9Jg4ntLUbXSPvoDw3gpZou7twuKdoFG2jOw635v7+IiXfTaa0TxVMyx78pUjnrVYwLgjKfX4e6/A==", + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.11.tgz", + "integrity": "sha512-md8CLNggS1Dx9pUgApzps5uAf+N8GN9xywzmNx9vHAWo94HtBwCCqkSnhIrdfQe83Dhz8Lfo/20Nb1Zxal092w==", "cpu": [ "x64" ], @@ -6085,13 +6120,13 @@ } }, "node_modules/eslint-config-next": { - "version": "16.2.10", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.10.tgz", - "integrity": "sha512-HSybLOY0QKf39i4FWUqPN0xWiNDi6A6UqJmZtgDkS3zMqjXTqULvj/sueXx3cdCG0mVG+qH6k5/qdegklH1d1w==", + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.11.tgz", + "integrity": "sha512-FIpbK/dUyxUExchDB7eBg3k+VU8R2iR/Cx9/kqTBUTFv2bOIR9aRrpno4rvAQ9VhiPQAyFKNA2NlZwouGWtclA==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "16.2.10", + "@next/eslint-plugin-next": "16.2.11", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", @@ -8722,9 +8757,9 @@ } }, "node_modules/marked": { - "version": "18.0.6", - "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.6.tgz", - "integrity": "sha512-MrV5puXBfuiy6wl6DLaq3BtIJQAJToAd5zt/ZKhRfGRAuFPALE7/4Y7jnxRQoEgK/pBgurGqLyAuRgZ2xOjr6w==", + "version": "18.0.7", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.7.tgz", + "integrity": "sha512-iDVQ5ldaiKXn6b2JroX5kgRfmwgqolW7NpaEzTl1k/2Zh1njIEN9yniyLV/mOvWwtsE8OGgkjsCYvijuPk1dtA==", "license": "MIT", "bin": { "marked": "bin/marked.js" @@ -9941,12 +9976,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "16.2.10", - "resolved": "https://registry.npmjs.org/next/-/next-16.2.10.tgz", - "integrity": "sha512-2som5AVXb3kE6Yjine3/mNbBayYF58eguBWIVVUdr1y/L426xyVEgYxgBG+1QC34P2x5E+tcDup6XkuOAX3dCA==", + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.11.tgz", + "integrity": "sha512-B339zaqbyK8cmxhoAvLrcwoabwCP1wz21zSzfqxqXAemTu2BXnH7tQnfcglKv1vnMUIDBc+Hth7XODQriTZiRQ==", "license": "MIT", "dependencies": { - "@next/env": "16.2.10", + "@next/env": "16.2.11", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -9960,14 +9995,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.10", - "@next/swc-darwin-x64": "16.2.10", - "@next/swc-linux-arm64-gnu": "16.2.10", - "@next/swc-linux-arm64-musl": "16.2.10", - "@next/swc-linux-x64-gnu": "16.2.10", - "@next/swc-linux-x64-musl": "16.2.10", - "@next/swc-win32-arm64-msvc": "16.2.10", - "@next/swc-win32-x64-msvc": "16.2.10", + "@next/swc-darwin-arm64": "16.2.11", + "@next/swc-darwin-x64": "16.2.11", + "@next/swc-linux-arm64-gnu": "16.2.11", + "@next/swc-linux-arm64-musl": "16.2.11", + "@next/swc-linux-x64-gnu": "16.2.11", + "@next/swc-linux-x64-musl": "16.2.11", + "@next/swc-win32-arm64-msvc": "16.2.11", + "@next/swc-win32-x64-msvc": "16.2.11", "sharp": "^0.34.5" }, "peerDependencies": { @@ -10015,34 +10050,6 @@ "react": ">=16" } }, - "node_modules/next/node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/node-exports-info": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", @@ -10545,9 +10552,9 @@ } }, "node_modules/postcss": { - "version": "8.5.20", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz", - "integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==", + "version": "8.5.22", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", + "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", "funding": [ { "type": "opencollective", @@ -10794,24 +10801,24 @@ } }, "node_modules/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", - "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", - "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.7" + "react": "^19.2.8" } }, "node_modules/react-is": { @@ -11503,47 +11510,52 @@ "license": "MIT" }, "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", + "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", "license": "Apache-2.0", "dependencies": { - "@img/colour": "^1.0.0", + "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", - "semver": "^7.7.3" + "semver": "^7.8.5" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "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" + "@img/sharp-darwin-arm64": "0.35.3", + "@img/sharp-darwin-x64": "0.35.3", + "@img/sharp-freebsd-wasm32": "0.35.3", + "@img/sharp-libvips-darwin-arm64": "1.3.2", + "@img/sharp-libvips-darwin-x64": "1.3.2", + "@img/sharp-libvips-linux-arm": "1.3.2", + "@img/sharp-libvips-linux-arm64": "1.3.2", + "@img/sharp-libvips-linux-ppc64": "1.3.2", + "@img/sharp-libvips-linux-riscv64": "1.3.2", + "@img/sharp-libvips-linux-s390x": "1.3.2", + "@img/sharp-libvips-linux-x64": "1.3.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", + "@img/sharp-libvips-linuxmusl-x64": "1.3.2", + "@img/sharp-linux-arm": "0.35.3", + "@img/sharp-linux-arm64": "0.35.3", + "@img/sharp-linux-ppc64": "0.35.3", + "@img/sharp-linux-riscv64": "0.35.3", + "@img/sharp-linux-s390x": "0.35.3", + "@img/sharp-linux-x64": "0.35.3", + "@img/sharp-linuxmusl-arm64": "0.35.3", + "@img/sharp-linuxmusl-x64": "0.35.3", + "@img/sharp-webcontainers-wasm32": "0.35.3", + "@img/sharp-win32-arm64": "0.35.3", + "@img/sharp-win32-ia32": "0.35.3", + "@img/sharp-win32-x64": "0.35.3" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/sharp/node_modules/semver": { diff --git a/package.json b/package.json index c57d5a6..193faeb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fedihome", - "version": "1.18.0", + "version": "1.19.0", "description": "Your home on the Fediverse — self-hosted publishing for blog, photos, and federated social.", "main": "index.js", "scripts": { @@ -20,7 +20,7 @@ "author": "", "license": "ISC", "dependencies": { - "@atproto/api": "^0.20.30", + "@atproto/api": "^0.20.31", "@fedify/fedify": "^2.3.3", "@fedify/next": "^2.3.3", "@prisma/adapter-pg": "^7.9.0", @@ -32,20 +32,20 @@ "@types/web-push": "^3.6.4", "dotenv": "^17.4.2", "fast-xml-parser": "^5.10.1", - "marked": "^18.0.6", + "marked": "^18.0.7", "music-metadata": "^11.14.0", - "next": "^16.2.10", + "next": "^16.2.11", "next-mdx-remote": "^6.0.0", "nodemailer": "^9.0.1", "pg": "^8.21.0", - "postcss": "^8.5.20", + "postcss": "^8.5.22", "prisma": "^7.9.0", - "react": "^19.2.6", - "react-dom": "^19.2.6", + "react": "^19.2.8", + "react-dom": "^19.2.8", "react-masonry-css": "^1.0.16", "remark-gfm": "^4.0.1", "sanitize-html": "^2.17.6", - "sharp": "^0.34.5", + "sharp": "^0.35.3", "tailwindcss": "^4.3.3", "typescript": "^6.0.3", "web-push": "^3.6.7" @@ -57,13 +57,15 @@ "@types/sanitize-html": "^2.16.1", "@vitest/coverage-v8": "^4.1.10", "eslint": "^10.7.0", - "eslint-config-next": "^16.2.10", + "eslint-config-next": "^16.2.11", "tsx": "^4.23.1", "vitest": "^4.1.10" }, "overrides": { "yaml": "^2.8.3", "js-yaml": "^4.2.0", + "sharp": "$sharp", + "postcss": "$postcss", "@opentelemetry/core": "^2.8.0", "@hono/node-server": "^1.19.13" } diff --git a/site.config.ts b/site.config.ts index c51c548..6b60ecc 100644 --- a/site.config.ts +++ b/site.config.ts @@ -112,6 +112,30 @@ export const siteConfig = { shell: process.env.LAYOUT_SHELL || "", }, + // Sidebar options (#307), used when layout.shell is "sidebar". `side` is + // "right" (default) or "left"; `blocks` is a comma-separated, ORDERED list of + // about/recent/sections/connect — the order is the render order, and omitting + // one hides it (drop `sections` to stop duplicating your header nav). Empty = + // built-in defaults. + sidebar: { + side: process.env.SIDEBAR_SIDE || "", + blocks: process.env.SIDEBAR_BLOCKS || "", + }, + + // Security policy (#59), web-editable in Admin → Site settings → Security. + // Days. adminSession default 30; appToken 0 = never expires (long-lived + + // revocable). Changes affect only newly-minted sessions/tokens. + security: { + adminSessionTtlDays: (() => { + const n = Number(process.env.ADMIN_SESSION_TTL_DAYS); + return Number.isInteger(n) && n > 0 ? n : 30; + })(), + appTokenTtlDays: (() => { + const n = Number(process.env.APP_TOKEN_TTL_DAYS); + return Number.isInteger(n) && n > 0 ? n : 0; + })(), + }, + // /audio podcast RSS feed overrides (#59). Empty = derive from your profile // (title " — Audio", author = your name, etc.). All web-editable in // Admin → Site settings; env vars remain as defaults. diff --git a/src/app/(public)/layout.tsx b/src/app/(public)/layout.tsx index ee1e8ac..306b990 100644 --- a/src/app/(public)/layout.tsx +++ b/src/app/(public)/layout.tsx @@ -31,10 +31,18 @@ export default async function PublicLayout({ children }: { children: React.React // Same two-column grid + breakpoint already used by the photo detail page, // so it collapses to a single column on mobile consistently with the rest // of the site. + // + // Side (#307) is done with CSS `order`, not by reordering the DOM: content + // stays FIRST in source order, so on mobile (single column) the reader gets + // the post before the sidebar either way. Only the desktop columns swap. + const onLeft = site.sidebar.side === "left"; + const cols = onLeft ? "lg:grid-cols-[300px_1fr]" : "lg:grid-cols-[1fr_300px]"; return ( -
-
{children}
- +
+
{children}
+
+ +
); } diff --git a/src/app/admin/site/SiteSettingsClient.tsx b/src/app/admin/site/SiteSettingsClient.tsx index 424db4c..bc4a657 100644 --- a/src/app/admin/site/SiteSettingsClient.tsx +++ b/src/app/admin/site/SiteSettingsClient.tsx @@ -27,6 +27,7 @@ export default function SiteSettingsClient({ accent, analyticsStatus, analyticsKey, + pushKey, encryptionAvailable, profile, profileDefaults, @@ -37,6 +38,7 @@ export default function SiteSettingsClient({ accent: { accentColor: string; themeAccents: Record }; analyticsStatus: { embedCode: string | null; unresolved: boolean }; analyticsKey: { configured: boolean; source: "db" | "env" | null }; + pushKey: { configured: boolean; source: "db" | "env" | null; subject: string }; encryptionAvailable: boolean; profile: { authorName: string; authorTagline: string; authorBio: string; @@ -55,6 +57,37 @@ export default function SiteSettingsClient({ const [keyStatus, setKeyStatus] = useState(analyticsKey); const [keyInput, setKeyInput] = useState(""); const [keyBusy, setKeyBusy] = useState(false); + // Web-push (VAPID) keys (#59) — generate/clear via a dedicated route; the + // private key is encrypted at rest and never sent to the browser. + const [pushStatus, setPushStatus] = useState(pushKey); + const [pushBusy, setPushBusy] = useState(false); + + async function postPushKeys(action: "generate" | "clear"): Promise { + if (action === "generate" && pushStatus.configured && + !confirm("Generate new push keys? Every device currently enrolled for push will stop receiving notifications until it re-enables them.")) { + return; + } + setPushBusy(true); + setResult(null); + try { + const res = await fetch("/api/admin/push-keys", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action }), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + setResult({ ok: false, msg: data.error || "Couldn't update the push keys." }); + return; + } + setPushStatus(data.status); + setResult({ ok: true, msg: action === "clear" ? "Push keys cleared." : "Push keys generated — re-enable notifications on each device." }); + } catch { + setResult({ ok: false, msg: "Couldn't update the push keys." }); + } finally { + setPushBusy(false); + } + } /* ---- Profile overlay (#59) — name/tagline/bio/summary + avatar/banner ---- */ // Held separately from `cfg` because the profile is a DIFFERENT store (the @@ -274,6 +307,10 @@ export default function SiteSettingsClient({ "layout.header": cfg.layout.header, "layout.footer": cfg.layout.footer, "layout.shell": cfg.layout.shell, + "sidebar.side": cfg.sidebar.side, + "sidebar.blocks": sidebarText, + "security.adminSessionTtlDays": String(cfg.security.adminSessionTtlDays), + "security.appTokenTtlDays": String(cfg.security.appTokenTtlDays), "contact.email": cfg.contact.email, "podcast.title": cfg.podcast.title, "podcast.author": cfg.podcast.author, @@ -306,7 +343,8 @@ export default function SiteSettingsClient({ "footer.webringUrl", "footer.webringLabel", "footer.badgeSrc", "footer.badgeHref", "footer.badgeAlt", "footer.fundingUrl", "footer.fundingLabel", "download.macos.enabled", "download.macos.releaseUrl", "download.macos.appStoreUrl", - "theme.id", "layout.feed", "layout.header", "layout.footer", "layout.shell", "contact.email", + "theme.id", "layout.feed", "layout.header", "layout.footer", "layout.shell", + "sidebar.side", "sidebar.blocks", "security.adminSessionTtlDays", "security.appTokenTtlDays", "contact.email", "podcast.title", "podcast.author", "podcast.description", "podcast.email", "podcast.image", "categories.photos", "categories.videos", "categories.audio", "analytics.siteId", "analytics.embedId", @@ -324,6 +362,8 @@ export default function SiteSettingsClient({ const setFooter = (patch: Partial) => setCfg((c) => ({ ...c, footer: { ...c.footer, ...patch } })); const setDownload = (patch: Partial) => setCfg((c) => ({ ...c, download: { ...c.download, ...patch } })); const setLayout = (patch: Partial) => setCfg((c) => ({ ...c, layout: { ...c.layout, ...patch } })); + const setSidebar = (patch: Partial) => setCfg((c) => ({ ...c, sidebar: { ...c.sidebar, ...patch } })); + const setSecurity = (patch: Partial) => setCfg((c) => ({ ...c, security: { ...c.security, ...patch } })); const setContact = (patch: Partial) => setCfg((c) => ({ ...c, contact: { ...c.contact, ...patch } })); const setPodcast = (patch: Partial) => setCfg((c) => ({ ...c, podcast: { ...c.podcast, ...patch } })); const setAnalytics = (patch: Partial) => setCfg((c) => ({ ...c, analytics: { ...c.analytics, ...patch } })); @@ -340,6 +380,11 @@ export default function SiteSettingsClient({ const [catText, setCatText] = useState(catCsv(effective)); useEffect(() => { setCatText(catCsv(cfg)); }, [cfg.categories]); // eslint-disable-line react-hooks/exhaustive-deps + // Sidebar block order (#307) — same raw-text treatment as categories above, so + // typing a comma doesn't get eaten by a `join()`-bound controlled input. + const [sidebarText, setSidebarText] = useState(effective.sidebar.blocks.join(", ")); + useEffect(() => { setSidebarText(cfg.sidebar.blocks.join(", ")); }, [cfg.sidebar.blocks]); + const text = (label: string, value: string, onChange: (v: string) => void, placeholder = "") => (
)} + {section("Phone notifications (Web Push)", <> +

+ Push notifications to your installed app (PWA) need a VAPID keypair. Generate one here — no + npx web-push or .env editing. The private key is stored encrypted; + after generating, enable notifications on each device from the 🔔 menu. +

+

+ {pushStatus.configured ? ( + ✓ Push keys configured{pushStatus.source === "env" ? " (from env)" : ""}. + ) : ( + No push keys yet — notifications are off until you generate them. + )} +

+ {pushStatus.configured && ( +

+ ⚠️ Regenerating replaces your keys and unsubscribes every device — each one has to re-enable push. +

+ )} + {!encryptionAvailable && ( +

Set ADMIN_SECRET to store the private key encrypted at rest.

+ )} +
+ + {pushStatus.source === "db" && ( + + )} +
+ )} + + {section("Security", <> +

+ Session and token lifetimes, in days. Changes apply to newly-created sessions and + tokens only — existing ones keep their original expiry. +

+ + + )} +
@@ -612,6 +657,45 @@ export default function SetupWizard() {

+ {/* Optional avatar/banner (#59) — needs the token above, so it lives here. */} +
+ +
+ {(["avatar", "banner"] as const).map((kind) => { + const p = kind === "avatar" ? avatarPath : bannerPath; + return ( +
+ {p && ( + // eslint-disable-next-line @next/next/no-img-element + + )} + +
+ ); + })} +
+

You can also set these later in the admin panel.

+
+ + {error && ( +
+

{error}

+
+ )} +
-

- {item.actor}{" "} +

+ {item.actor}{" "} {item.summary}

- {timeAgo(item.createdAt)} - + {timeAgo(item.createdAt)} + {item.source} {typeEmojis[item.type]}
{item.type === "follow" && ( - + view profile )} @@ -373,7 +373,7 @@ export default function NotificationBell() { type="button" title="Dismiss" onClick={(e) => handleMaintenanceAction(e, item.maintenanceId!, "dismissed")} - className="w-6 h-6 flex items-center justify-center rounded text-gray-500 hover:bg-red-500/20 hover:text-red-400 transition-colors" + className="w-6 h-6 flex items-center justify-center rounded text-content-faint hover:bg-red-500/20 hover:text-red-400 transition-colors" > @@ -398,13 +398,13 @@ export default function NotificationBell() { className={`relative w-9 h-9 flex items-center justify-center rounded-lg transition-colors ${ isActive ? "bg-accent-500/20 text-accent-400" - : "text-gray-500 hover:text-gray-300 hover:bg-surface-800/50" + : "text-content-faint hover:text-content-muted hover:bg-surface-800/50" }`} title={cat.label} > {unread > 0 && ( - + {unread > 99 ? "99" : unread} )} diff --git a/src/components/layout/PushSetup.tsx b/src/components/layout/PushSetup.tsx index 0319dba..c1cedf5 100644 --- a/src/components/layout/PushSetup.tsx +++ b/src/components/layout/PushSetup.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useState } from "react"; +import { resolvePushStatus, subKeyOf } from "@/lib/push-client"; /** * Web Push enrollment UI, shown in the NotificationBell dropdown (admin-only). @@ -10,7 +11,11 @@ import { useEffect, useState } from "react"; * server-side. On iOS, push only works from a home-screen install, so when not * running standalone we show Add-to-Home-Screen guidance instead. * - * Dormant until the server has VAPID keys set (the enable button reports it). + * If the server has no VAPID keypair yet, this generates one first — the bell is + * owner-only and /api/admin/push-keys re-checks that, so the whole thing is one + * click with no .env editing (#59). It used to show the enable button regardless + * and fail with an opaque "push not configured on server", which left push + * unreachable on a fresh install. */ function urlBase64ToUint8Array(base64String: string): Uint8Array { @@ -23,7 +28,25 @@ function urlBase64ToUint8Array(base64String: string): Uint8Array { return output; } -type Status = "loading" | "unsupported" | "ios-needs-install" | "ready" | "on" | "denied"; +type PushInfo = { configured: boolean; publicKey: string; count: number }; + +/** `GET /api/push` — the server's key + how many devices it currently knows about. */ +async function fetchPushInfo(): Promise { + try { + const res = await fetch("/api/push"); + if (!res.ok) return null; + const d = await res.json(); + return { + configured: !!d?.configured, + publicKey: typeof d?.publicKey === "string" ? d.publicKey : "", + count: typeof d?.count === "number" ? d.count : 0, + }; + } catch { + return null; + } +} + +type Status = "loading" | "unsupported" | "ios-needs-install" | "needs-keys" | "ready" | "on" | "denied"; export default function PushSetup() { const [status, setStatus] = useState("loading"); @@ -65,7 +88,24 @@ export default function PushSetup() { (await navigator.serviceWorker.getRegistration()) || (await navigator.serviceWorker.register("/sw.js")); const existing = await reg.pushManager.getSubscription(); - if (!cancelled) setStatus(existing ? "on" : "ready"); + // Always ask the server, even with no local subscription: that's the only + // way to learn it has no keypair yet, and showing "Enable" in that state + // is a dead end (#59). Don't trust a bare subscription either — after a + // key rotation it's bound to a dead key and silently receives nothing. + const info = await fetchPushInfo(); + if (cancelled) return; + setStatus( + info + ? resolvePushStatus({ + configured: info.configured, + serverKey: info.publicKey, + serverCount: info.count, + subKey: existing ? subKeyOf(existing.options?.applicationServerKey) : null, + }) + : // Couldn't reach the server — offer the button and let the click + // report the real reason rather than guessing a state. + "ready", + ); } catch { if (!cancelled) setStatus("unsupported"); } @@ -80,20 +120,49 @@ export default function PushSetup() { setBusy(true); setMsg(null); try { + // Must stay first and un-awaited-before: the permission prompt has to run + // inside the user gesture (Safari enforces this strictly). const perm = await Notification.requestPermission(); if (perm !== "granted") { - setStatus(perm === "denied" ? "denied" : "ready"); + setStatus(perm === "denied" ? "denied" : status); setBusy(false); return; } - const keyRes = await fetch("/api/push"); - if (!keyRes.ok) throw new Error("could not load push config"); - const { publicKey, configured } = await keyRes.json(); - if (!configured || !publicKey) throw new Error("push not configured on server"); + let info = await fetchPushInfo(); + if (!info) throw new Error("Couldn't load the push settings."); + + // No keypair on the server yet — make one, rather than dead-ending on + // "push not configured". Safe to do from here: this menu is owner-only, + // /api/admin/push-keys re-checks admin + origin, and with no keys there + // are no existing subscriptions for the generate to purge. + if (!info.configured || !info.publicKey) { + setMsg("Setting up push on the server…"); + const gen = await fetch("/api/admin/push-keys", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "generate" }), + }); + const genBody = await gen.json().catch(() => null); + // Surface the server's own reason (e.g. ADMIN_SECRET missing, so the + // private key can't be encrypted at rest) instead of a generic failure. + if (!gen.ok) throw new Error(genBody?.error || "Couldn't set up push on the server."); + info = await fetchPushInfo(); + if (!info?.configured || !info.publicKey) { + throw new Error("Push keys were created but couldn't be read back."); + } + } + const publicKey = info.publicKey; const reg = await navigator.serviceWorker.ready; let sub = await reg.pushManager.getSubscription(); + // If an existing subscription is bound to a DIFFERENT (rotated) key, drop it + // and re-subscribe — the browser throws InvalidStateError if you subscribe + // with a new applicationServerKey while an old subscription exists. + if (sub && subKeyOf(sub.options?.applicationServerKey) !== publicKey) { + await sub.unsubscribe().catch(() => {}); + sub = null; + } if (!sub) { sub = await reg.pushManager.subscribe({ userVisibleOnly: true, @@ -156,12 +225,12 @@ export default function PushSetup() { if (status === "loading") return null; return ( -
+
{status === "ios-needs-install" && (

📲 To get push on your iPhone: tap the{" "} - Share icon →{" "} - Add to Home Screen, then open the app + Share icon →{" "} + Add to Home Screen, then open the app from your Home Screen and enable notifications here.

)} @@ -177,6 +246,22 @@ export default function PushSetup() {

)} + {status === "needs-keys" && ( +
+ +

+ One click — this creates your push keys on the server, then enables + notifications on this device. +

+
+ )} + {status === "ready" && ( @@ -209,7 +294,7 @@ export default function PushSetup() {
)} - {msg &&

{msg}

} + {msg &&

{msg}

}
); } diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index a02e449..5496a03 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,28 +1,33 @@ import Link from "next/link"; import Image from "next/image"; import { getSidebarData } from "./sidebarData"; +import { SIDEBAR_BLOCKS, type SidebarBlock } from "@/lib/sidebar"; /** * The sidebar column (#250) — rendered beside the page content when the owner - * picks the `sidebar` shell variant. Four blocks: about/bio, recent posts, - * sections, and connect (handle + links). Styled with the existing - * `glass-card` idiom so it inherits the active theme's surface + feel tokens. + * picks the `sidebar` shell variant. Styled with the existing `glass-card` idiom + * so it inherits the active theme's surface + feel tokens. + * + * Which blocks appear, and in what order, is owner-configurable (#307): the + * `blocks` list IS the render order, and omitting a block hides it. That's also + * how you stop the header and sidebar both showing your nav — drop `sections`. * * No tags block yet: there is no public tag route, so those links would go * nowhere. It lands with a public `/tags/[tag]` page in a later slice. */ -export default async function Sidebar() { +export default async function Sidebar({ blocks = SIDEBAR_BLOCKS }: { blocks?: SidebarBlock[] }) { const { authorName, authorBio, authorTagline, avatarPath, navLinks, recentPosts, fediAddress, contactEmail, footer, } = await getSidebarData(); const block = "glass-card p-5 h-fit"; - const heading = "text-xs font-semibold text-white uppercase tracking-wider mb-3"; + const heading = "text-xs font-semibold text-content uppercase tracking-wider mb-3"; - return ( -