diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0f8acee8..b22b068d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,6 +62,22 @@ jobs: bun build apps/hook/server/index.ts --compile --target=bun-windows-x64 --outfile plannotator-win32-x64.exe sha256sum plannotator-win32-x64.exe > plannotator-win32-x64.exe.sha256 + # Paste service binaries + bun build apps/paste-service/targets/bun.ts --compile --target=bun-darwin-arm64 --outfile plannotator-paste-darwin-arm64 + sha256sum plannotator-paste-darwin-arm64 > plannotator-paste-darwin-arm64.sha256 + + bun build apps/paste-service/targets/bun.ts --compile --target=bun-darwin-x64 --outfile plannotator-paste-darwin-x64 + sha256sum plannotator-paste-darwin-x64 > plannotator-paste-darwin-x64.sha256 + + bun build apps/paste-service/targets/bun.ts --compile --target=bun-linux-x64 --outfile plannotator-paste-linux-x64 + sha256sum plannotator-paste-linux-x64 > plannotator-paste-linux-x64.sha256 + + bun build apps/paste-service/targets/bun.ts --compile --target=bun-linux-arm64 --outfile plannotator-paste-linux-arm64 + sha256sum plannotator-paste-linux-arm64 > plannotator-paste-linux-arm64.sha256 + + bun build apps/paste-service/targets/bun.ts --compile --target=bun-windows-x64 --outfile plannotator-paste-win32-x64.exe + sha256sum plannotator-paste-win32-x64.exe > plannotator-paste-win32-x64.exe.sha256 + - name: Upload artifacts uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: diff --git a/CLAUDE.md b/CLAUDE.md index 2d3fe481..e535096b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,6 +20,10 @@ plannotator/ │ │ └── review-editor.html # Built code review app │ ├── marketing/ # Marketing site, docs, and blog (plannotator.ai) │ │ └── astro.config.mjs # Astro 5 static site with content collections +│ ├── paste-service/ # Paste service for short URL sharing +│ │ ├── core/ # Platform-agnostic logic (handler, storage interface, cors) +│ │ ├── stores/ # Storage backends (fs, kv, s3) +│ │ └── targets/ # Deployment entries (bun.ts, cloudflare.ts) │ └── review/ # Standalone review server (for development) │ ├── index.html │ ├── index.tsx @@ -30,6 +34,7 @@ plannotator/ │ │ ├── review.ts # startReviewServer(), handleReviewServerReady() │ │ ├── annotate.ts # startAnnotateServer(), handleAnnotateServerReady() │ │ ├── storage.ts # Plan saving to disk (getPlanDir, savePlan, etc.) +│ │ ├── share-url.ts # Server-side share URL generation for remote sessions │ │ ├── remote.ts # isRemoteSession(), getServerPort() │ │ ├── browser.ts # openBrowser() │ │ ├── integrations.ts # Obsidian, Bear integrations @@ -73,6 +78,7 @@ claude --plugin-dir ./apps/hook | `PLANNOTATOR_PORT` | Fixed port to use. Default: random locally, `19432` for remote sessions. | | `PLANNOTATOR_BROWSER` | Custom browser to open plans in. macOS: app name or path. Linux/Windows: executable path. | | `PLANNOTATOR_SHARE_URL` | Custom base URL for share links (self-hosted portal). Default: `https://share.plannotator.ai`. | +| `PLANNOTATOR_PASTE_URL` | Base URL of the paste service API for short URL sharing. Default: `https://paste.plannotator.ai`. | **Legacy:** `SSH_TTY` and `SSH_CONNECTION` are still detected. Prefer `PLANNOTATOR_REMOTE=1` for explicit control. @@ -170,6 +176,15 @@ Send Annotations → feedback sent to agent session All servers use random ports locally or fixed port (`19432`) in remote mode. +### Paste Service (`apps/paste-service/`) + +| Endpoint | Method | Purpose | +| --------------------- | ------ | ------------------------------------------ | +| `/api/paste` | POST | Store compressed plan data, returns `{ id }` | +| `/api/paste/:id` | GET | Retrieve stored compressed data | + +Runs as a separate service on port `19433` (self-hosted) or as a Cloudflare Worker (hosted). + ## Plan Version History Every plan is automatically saved to `~/.plannotator/history/{project}/{slug}/` on arrival, before the user sees the UI. Versions are numbered sequentially (`001.md`, `002.md`, etc.). The slug is derived from the plan's first `# Heading` + today's date via `generateSlug()`, scoped by project name (git repo or cwd). Same heading on the same day = same slug = same plan being iterated on. Identical resubmissions are deduplicated (no new file if content matches the latest version). @@ -262,7 +277,7 @@ Text highlighting uses `web-highlighter` library. Code blocks use manual ` **Location:** `packages/ui/utils/sharing.ts`, `packages/ui/hooks/useSharing.ts` -Shares full plan + annotations via URL hash using deflate compression. +Shares full plan + annotations via URL hash using deflate compression. For large plans, short URLs are created via the paste service (user must explicitly confirm). **Payload format:** diff --git a/apps/hook/server/index.ts b/apps/hook/server/index.ts index 1b8c553e..a0508cde 100644 --- a/apps/hook/server/index.ts +++ b/apps/hook/server/index.ts @@ -36,6 +36,7 @@ import { handleAnnotateServerReady, } from "@plannotator/server/annotate"; import { getGitContext, runGitDiff } from "@plannotator/server/git"; +import { writeRemoteShareLink } from "@plannotator/server/share-url"; // Embed the built HTML at compile time // @ts-ignore - Bun import attribute for text @@ -55,6 +56,9 @@ const sharingEnabled = process.env.PLANNOTATOR_SHARE !== "disabled"; // Custom share portal URL for self-hosting const shareBaseUrl = process.env.PLANNOTATOR_SHARE_URL || undefined; +// Paste service URL for short URL sharing +const pasteApiUrl = process.env.PLANNOTATOR_PASTE_URL || undefined; + if (args[0] === "review") { // ============================================ // CODE REVIEW MODE @@ -79,7 +83,13 @@ if (args[0] === "review") { sharingEnabled, shareBaseUrl, htmlContent: reviewHtmlContent, - onReady: handleReviewServerReady, + onReady: async (url, isRemote, port) => { + handleReviewServerReady(url, isRemote, port); + + if (isRemote && sharingEnabled && rawPatch) { + await writeRemoteShareLink(rawPatch, shareBaseUrl, "review changes", "diff only").catch(() => {}); + } + }, }); // Wait for user feedback @@ -126,7 +136,13 @@ if (args[0] === "review") { sharingEnabled, shareBaseUrl, htmlContent: planHtmlContent, - onReady: handleAnnotateServerReady, + onReady: async (url, isRemote, port) => { + handleAnnotateServerReady(url, isRemote, port); + + if (isRemote && sharingEnabled) { + await writeRemoteShareLink(markdown, shareBaseUrl, "annotate", "document only").catch(() => {}); + } + }, }); // Wait for user feedback @@ -173,9 +189,14 @@ if (args[0] === "review") { permissionMode, sharingEnabled, shareBaseUrl, + pasteApiUrl, htmlContent: planHtmlContent, - onReady: (url, isRemote, port) => { + onReady: async (url, isRemote, port) => { handleServerReady(url, isRemote, port); + + if (isRemote && sharingEnabled) { + await writeRemoteShareLink(planContent, shareBaseUrl, "review the plan", "plan only").catch(() => {}); + } }, }); diff --git a/apps/marketing/src/content/docs/guides/self-hosting.md b/apps/marketing/src/content/docs/guides/self-hosting.md index 06e079f2..d6a1dc27 100644 --- a/apps/marketing/src/content/docs/guides/self-hosting.md +++ b/apps/marketing/src/content/docs/guides/self-hosting.md @@ -1,14 +1,40 @@ --- title: "Self-Hosting" -description: "Self-host the Plannotator share portal for private plan sharing." +description: "Deploy and self-host the full Plannotator system — hook, share portal, and paste service." sidebar: order: 23 section: "Guides" --- -The share portal is a static single-page application. It has no backend, no database, and makes no network requests. All plan data is encoded in the URL hash. +Plannotator has three components. Only the hook is required. -## Build +## Components + +| Component | Required | What it does | +|-----------|----------|--------------| +| Hook | Yes | Local binary that intercepts `ExitPlanMode`, runs the review UI | +| Share Portal | Optional | Static site that renders shared plans. When you open a share link, this is what loads in your browser. | +| Paste Service | Optional | Storage backend for the share portal. When a plan is too large for a URL, the paste service holds the compressed data and the portal fetches it on load. | + +### How sharing works + +Small plans are encoded entirely in the URL hash — the share portal reads the hash and renders the plan. No backend involved. The data remains private — it never leaves the URL. + +Large plans don't fit in a URL. **When a user explicitly confirms** short link creation, the compressed plan is sent to the paste service, which stores it and returns a short ID. A compressed plan goes in, a link to retrieve it comes out. The share URL becomes `share.plannotator.ai/p/aBcDeFgH` (or `your-portal.example.com/p/aBcDeFgH` if self-hosting). When someone opens that link, the portal fetches the compressed data from the paste service, decompresses it, and renders the plan. + +**Without paste service:** Sharing still works for plans that fit in a URL. Those plans stay completely private — the data lives only in the URL hash and never touches a server. Large plans show a warning that the URL may be truncated by messaging apps. + +**With paste service:** Large plans get short, reliable URLs that work everywhere. Plannotator temporarily stores the compressed plan data — it auto-deletes after the configured TTL. + +## 1. Install the Hook + +See [Installation](/docs/getting-started/installation/) for hook setup instructions. + +## 2. Deploy the Share Portal + +The share portal is a static single-page application. It has no backend, no database, and makes no network requests beyond fetching paste data for short URLs. + +### Build ```bash bun install @@ -17,11 +43,11 @@ bun run build:portal Output: `apps/portal/dist/` -## Deploy +### Deploy Upload the `dist/` folder to any static hosting provider. -### Nginx +#### Nginx ```nginx server { @@ -32,7 +58,7 @@ server { } ``` -### AWS S3 + CloudFront +#### AWS S3 + CloudFront ```bash aws s3 sync apps/portal/dist/ s3://your-bucket/ --delete @@ -40,27 +66,50 @@ aws s3 sync apps/portal/dist/ s3://your-bucket/ --delete Configure the CloudFront distribution to return `/index.html` for 404s (SPA routing). -### Vercel / Netlify / Cloudflare Pages +#### Vercel / Netlify / Cloudflare Pages Point to the repository root: - **Build command**: `bun run build:portal` - **Output directory**: `apps/portal/dist` -## Configure Plannotator +## 3. Deploy the Paste Service + +The paste service accepts compressed plan data and returns a short ID. A compressed plan goes in, a link to retrieve it comes out. Pastes auto-delete after the configured TTL. No database required. + +The paste service is fully open source — the same codebase you're looking at. -Set the `PLANNOTATOR_SHARE_URL` environment variable to your portal's URL: +### Run the binary + +Download the paste service binary for your platform from [GitHub Releases](https://github.com/backnotprop/plannotator/releases). Binaries are available for macOS (ARM64, x64), Linux (x64, ARM64), and Windows (x64). ```bash -export PLANNOTATOR_SHARE_URL=https://plannotator.internal.example.com +chmod +x plannotator-paste-* +./plannotator-paste-darwin-arm64 # or whichever matches your platform ``` -All share links generated by Plannotator will now point to your self-hosted portal. The import dialog placeholder updates automatically. +Pastes stored to `~/.plannotator/pastes/` by default. + +### Configuration -When `PLANNOTATOR_SHARE_URL` is not set, the default `https://share.plannotator.ai` is used. +| Variable | Default | Description | +|----------|---------|-------------| +| `PASTE_PORT` | `19433` | Server port | +| `PASTE_DATA_DIR` | `~/.plannotator/pastes` | Storage directory | +| `PASTE_TTL_DAYS` | `7` | Auto-delete after N days | +| `PASTE_MAX_SIZE` | `524288` | Max payload size (512KB) | +| `PASTE_ALLOWED_ORIGINS` | (see defaults) | CORS allowed origins | + +## 4. Connect the Components + +```bash +export PLANNOTATOR_SHARE_URL=https://your-portal.example.com +export PLANNOTATOR_PASTE_URL=https://your-paste.example.com +``` -## Verify +## 5. Verify 1. Start a plan review in Claude Code or OpenCode -2. Add an annotation, click Export → Share +2. Add annotations, click **Export** → **Share** 3. Confirm the share URL starts with your configured domain -4. Open the link — the plan and annotations should render correctly +4. If the plan is large, click **Create short link** when prompted +5. Open the short URL — the plan should render correctly diff --git a/apps/marketing/src/content/docs/guides/sharing-and-collaboration.md b/apps/marketing/src/content/docs/guides/sharing-and-collaboration.md index fb178180..418145c4 100644 --- a/apps/marketing/src/content/docs/guides/sharing-and-collaboration.md +++ b/apps/marketing/src/content/docs/guides/sharing-and-collaboration.md @@ -1,12 +1,12 @@ --- title: "Sharing & Collaboration" -description: "Share plans and annotations via URL — no backend required." +description: "Share plans and annotations via URL with optional short links for large plans." sidebar: order: 21 section: "Guides" --- -Plannotator lets you share plans and annotations with teammates via URL. All data is encoded in the URL hash — no backend, no accounts, no server stores anything. +Plannotator lets you share plans and annotations with teammates via URL. Small plans are encoded entirely in the URL hash — no backend, no accounts, no server stores anything. Large plans can optionally use short URLs via the [paste service](/docs/guides/self-hosting/#3-deploy-the-paste-service). ## How sharing works @@ -57,13 +57,34 @@ When sharing is disabled: - The "Copy Share Link" quick action is removed - The Import Review option is hidden +## Short URLs for large plans + +When a plan is too large for a URL (~2KB+ compressed), messaging apps like Slack and WhatsApp may truncate it. Plannotator can create a short link by temporarily storing the compressed plan in a paste service. + +### How it works + +1. Click **Export** → **Share** +2. If the URL is large, you'll see a notice: "This plan is too large for a URL" +3. Click **Create short link** to confirm +4. The compressed plan is temporarily stored, then automatically deleted after the configured TTL +5. A short URL like `share.plannotator.ai/p/aBcDeFgH` is generated +6. Both the short URL and the full hash URL are shown — the short URL is safe for messaging apps + +### Privacy + +- Plans are only uploaded when you explicitly click "Create short link" — no data leaves your machine until you confirm +- Pastes auto-expire and are permanently deleted (hosted: a few days, self-hosted: configurable via `PASTE_TTL_DAYS`) +- The paste service is fully open source — you can audit exactly what it does +- Self-hosters can run their own paste service for complete control — see the [self-hosting guide](/docs/guides/self-hosting/) +- If the paste service is unavailable, the full hash URL is always available as fallback + ## Self-hosting the share portal By default, share URLs point to `https://share.plannotator.ai`. You can self-host the portal and point Plannotator at your instance. See the [self-hosting guide](/docs/guides/self-hosting/) for details. ## Privacy model -- Plans and annotations are never sent to any server -- The share portal is a static page — it only reads the URL hash client-side +- Plans and annotations are never sent to any server — the data lives entirely in the URL hash +- The share portal is a static page — it only reads the hash and renders client-side - No analytics, no tracking, no cookies on the share portal -- If you self-host, you have complete control over the infrastructure +- Short URLs are opt-in — data is only uploaded when you explicitly click "Create short link" (see [Short URLs for large plans](#short-urls-for-large-plans) for details) diff --git a/apps/marketing/src/content/docs/reference/api-endpoints.md b/apps/marketing/src/content/docs/reference/api-endpoints.md index 6f692a19..ce34a9ad 100644 --- a/apps/marketing/src/content/docs/reference/api-endpoints.md +++ b/apps/marketing/src/content/docs/reference/api-endpoints.md @@ -133,3 +133,42 @@ Body: "annotations": [] } ``` + +## Paste service + +Stores compressed plan data for short URL sharing. Runs as a separate service from the plan/review/annotate servers. + +Default: `https://paste.plannotator.ai` (or self-hosted) + +| Endpoint | Method | Purpose | +|----------|--------|---------| +| `/api/paste` | POST | Store compressed plan data, returns `{ id }` | +| `/api/paste/:id` | GET | Retrieve stored compressed data | + +### POST `/api/paste` + +Body: + +```json +{ + "data": "" +} +``` + +Returns: `{ "id": "aBcDeFgH" }` (201 Created) + +Limits: 512KB max payload. Auto-deleted after configured TTL (default: 7 days). + +### GET `/api/paste/:id` + +Returns: + +```json +{ + "data": "" +} +``` + +Or: `{ "error": "Paste not found or expired" }` (404) + +Cached for 1 hour (`Cache-Control: public, max-age=3600`). diff --git a/apps/marketing/src/content/docs/reference/environment-variables.md b/apps/marketing/src/content/docs/reference/environment-variables.md index 79a85686..a6976d9e 100644 --- a/apps/marketing/src/content/docs/reference/environment-variables.md +++ b/apps/marketing/src/content/docs/reference/environment-variables.md @@ -19,6 +19,24 @@ All Plannotator environment variables and their defaults. | `PLANNOTATOR_SHARE` | enabled | Set to `disabled` to turn off sharing. Hides share UI and import options. | | `PLANNOTATOR_SHARE_URL` | `https://share.plannotator.ai` | Base URL for share links. Set this when self-hosting the share portal. | +## Paste service variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `PLANNOTATOR_PASTE_URL` | `https://paste.plannotator.ai` | Base URL of the paste service API. Set this when self-hosting the paste service. | + +### Self-hosted paste service + +When running your own paste service binary, these variables configure it: + +| Variable | Default | Description | +|----------|---------|-------------| +| `PASTE_PORT` | `19433` | Server port | +| `PASTE_DATA_DIR` | `~/.plannotator/pastes` | Filesystem storage directory | +| `PASTE_TTL_DAYS` | `7` | Paste expiration in days | +| `PASTE_MAX_SIZE` | `524288` | Max payload size in bytes (512KB) | +| `PASTE_ALLOWED_ORIGINS` | `https://share.plannotator.ai,http://localhost:3001` | CORS allowed origins (comma-separated) | + ## Install script variables | Variable | Default | Description | diff --git a/apps/paste-service/core/cors.ts b/apps/paste-service/core/cors.ts new file mode 100644 index 00000000..9fb39ec9 --- /dev/null +++ b/apps/paste-service/core/cors.ts @@ -0,0 +1,25 @@ +const BASE_CORS_HEADERS = { + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + "Access-Control-Max-Age": "86400", +}; + +export function getAllowedOrigins(envValue?: string): string[] { + if (envValue) { + return envValue.split(",").map((o) => o.trim()); + } + return ["https://share.plannotator.ai", "http://localhost:3001"]; +} + +export function corsHeaders( + requestOrigin: string, + allowedOrigins: string[] +): Record { + if (allowedOrigins.includes(requestOrigin) || allowedOrigins.includes("*")) { + return { + ...BASE_CORS_HEADERS, + "Access-Control-Allow-Origin": requestOrigin, + }; + } + return {}; +} diff --git a/apps/paste-service/core/handler.ts b/apps/paste-service/core/handler.ts new file mode 100644 index 00000000..d88d9732 --- /dev/null +++ b/apps/paste-service/core/handler.ts @@ -0,0 +1,144 @@ +import type { PasteStore } from "./storage"; +import { corsHeaders } from "./cors"; + +export interface PasteOptions { + maxSize: number; + ttlSeconds: number; +} + +const DEFAULT_OPTIONS: PasteOptions = { + maxSize: 524_288, // 512 KB + ttlSeconds: 7 * 24 * 60 * 60, // 7 days +}; + +const ID_PATTERN = /^\/api\/paste\/([A-Za-z0-9]{6,16})$/; + +/** + * Generate a short URL-safe ID (8 chars, ~47.6 bits of entropy). + * Uses Web Crypto with rejection sampling to avoid modulo bias. + */ +function generateId(): string { + const chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const limit = 256 - (256 % chars.length); // 248 — largest multiple of 62 that fits in a byte + const id: string[] = []; + while (id.length < 8) { + const bytes = new Uint8Array(16); // oversample to minimize rounds + crypto.getRandomValues(bytes); + for (const b of bytes) { + if (b < limit) { + id.push(chars[b % chars.length]); + if (id.length === 8) break; + } + } + } + return id.join(""); +} + +export async function createPaste( + data: string, + store: PasteStore, + options: Partial = {} +): Promise<{ id: string }> { + const opts = { ...DEFAULT_OPTIONS, ...options }; + + if (!data || typeof data !== "string") { + throw new PasteError('Missing or invalid "data" field', 400); + } + + if (data.length > opts.maxSize) { + throw new PasteError( + `Payload too large (max ${Math.round(opts.maxSize / 1024)} KB compressed)`, + 413 + ); + } + + const id = generateId(); + await store.put(id, data, opts.ttlSeconds); + return { id }; +} + +export async function getPaste( + id: string, + store: PasteStore +): Promise { + return store.get(id); +} + +export class PasteError extends Error { + constructor( + message: string, + public status: number + ) { + super(message); + } +} + +/** + * Shared HTTP request handler for the paste service. + * Both Bun and Cloudflare targets delegate to this after wiring up their store. + */ +export async function handleRequest( + request: Request, + store: PasteStore, + cors: Record, + options?: Partial +): Promise { + const url = new URL(request.url); + + if (request.method === "OPTIONS") { + return new Response(null, { status: 204, headers: cors }); + } + + if (url.pathname === "/api/paste" && request.method === "POST") { + let body: { data?: unknown }; + try { + body = (await request.json()) as { data?: unknown }; + } catch { + return Response.json( + { error: "Invalid JSON body" }, + { status: 400, headers: cors } + ); + } + try { + const result = await createPaste(body.data as string, store, options); + return Response.json(result, { status: 201, headers: cors }); + } catch (e) { + if (e instanceof PasteError) { + return Response.json( + { error: e.message }, + { status: e.status, headers: cors } + ); + } + return Response.json( + { error: "Failed to store paste" }, + { status: 500, headers: cors } + ); + } + } + + const match = url.pathname.match(ID_PATTERN); + if (match && request.method === "GET") { + const data = await getPaste(match[1], store); + if (!data) { + return Response.json( + { error: "Paste not found or expired" }, + { status: 404, headers: cors } + ); + } + return Response.json( + { data }, + { + headers: { + ...cors, + "Cache-Control": "public, max-age=3600", + }, + } + ); + } + + return Response.json( + { error: "Not found. Valid paths: POST /api/paste, GET /api/paste/:id" }, + { status: 404, headers: cors } + ); +} diff --git a/apps/paste-service/core/storage.ts b/apps/paste-service/core/storage.ts new file mode 100644 index 00000000..cc210560 --- /dev/null +++ b/apps/paste-service/core/storage.ts @@ -0,0 +1,9 @@ +/** + * PasteStore interface — pluggable storage backend for paste data. + * + * Implementations: FsPasteStore (filesystem), KvPasteStore (CF KV) + */ +export interface PasteStore { + put(id: string, data: string, ttlSeconds: number): Promise; + get(id: string): Promise; +} diff --git a/apps/paste-service/package.json b/apps/paste-service/package.json new file mode 100644 index 00000000..db099668 --- /dev/null +++ b/apps/paste-service/package.json @@ -0,0 +1,14 @@ +{ + "name": "@plannotator/paste-service", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "bun run targets/bun.ts", + "dev:cf": "wrangler dev", + "deploy:cf": "wrangler deploy" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20241218.0", + "wrangler": "^3.99.0" + } +} diff --git a/apps/paste-service/stores/fs.ts b/apps/paste-service/stores/fs.ts new file mode 100644 index 00000000..9e4a72a5 --- /dev/null +++ b/apps/paste-service/stores/fs.ts @@ -0,0 +1,70 @@ +import { mkdirSync, readdirSync, readFileSync, unlinkSync } from "fs"; +import { join, resolve } from "path"; +import type { PasteStore } from "../core/storage"; + +interface PasteFile { + data: string; + expiresAt: number; +} + +export class FsPasteStore implements PasteStore { + private resolvedDir: string; + + constructor(private dataDir: string) { + mkdirSync(dataDir, { recursive: true }); + this.resolvedDir = resolve(dataDir); + this.sweep(); + } + + private safePath(id: string): string { + const filePath = resolve(join(this.dataDir, `${id}.json`)); + if (!filePath.startsWith(this.resolvedDir)) { + throw new Error("Invalid paste ID"); + } + return filePath; + } + + async put(id: string, data: string, ttlSeconds: number): Promise { + const entry: PasteFile = { + data, + expiresAt: Date.now() + ttlSeconds * 1000, + }; + await Bun.write(this.safePath(id), JSON.stringify(entry)); + } + + async get(id: string): Promise { + const path = this.safePath(id); + try { + const entry: PasteFile = await Bun.file(path).json(); + if (Date.now() > entry.expiresAt) { + unlinkSync(path); + return null; + } + return entry.data; + } catch { + return null; + } + } + + /** Delete expired pastes on startup */ + private sweep(): void { + try { + const files = readdirSync(this.dataDir).filter((f) => f.endsWith(".json")); + const now = Date.now(); + for (const file of files) { + const path = join(this.dataDir, file); + try { + const raw = readFileSync(path, "utf-8"); + const entry: PasteFile = JSON.parse(raw); + if (now > entry.expiresAt) { + unlinkSync(path); + } + } catch { + // skip malformed files + } + } + } catch { + // dataDir might not exist yet + } + } +} diff --git a/apps/paste-service/stores/kv.ts b/apps/paste-service/stores/kv.ts new file mode 100644 index 00000000..74ce189a --- /dev/null +++ b/apps/paste-service/stores/kv.ts @@ -0,0 +1,17 @@ +import type { PasteStore } from "../core/storage"; + +/** + * Cloudflare KV-backed paste store. + * Uses KV's native expirationTtl for automatic cleanup. + */ +export class KvPasteStore implements PasteStore { + constructor(private kv: KVNamespace) {} + + async put(id: string, data: string, ttlSeconds: number): Promise { + await this.kv.put(`paste:${id}`, data, { expirationTtl: ttlSeconds }); + } + + async get(id: string): Promise { + return this.kv.get(`paste:${id}`); + } +} diff --git a/apps/paste-service/stores/s3.ts b/apps/paste-service/stores/s3.ts new file mode 100644 index 00000000..faee4aab --- /dev/null +++ b/apps/paste-service/stores/s3.ts @@ -0,0 +1,17 @@ +import type { PasteStore } from "../core/storage"; + +/** + * S3-compatible paste store (future implementation). + * + * TTL handled via S3 lifecycle rules configured on the bucket. + * Implement when needed for AWS Lambda or other cloud deployments. + */ +export class S3PasteStore implements PasteStore { + async put(_id: string, _data: string, _ttlSeconds: number): Promise { + throw new Error("S3PasteStore not yet implemented"); + } + + async get(_id: string): Promise { + throw new Error("S3PasteStore not yet implemented"); + } +} diff --git a/apps/paste-service/targets/bun.ts b/apps/paste-service/targets/bun.ts new file mode 100644 index 00000000..a670e924 --- /dev/null +++ b/apps/paste-service/targets/bun.ts @@ -0,0 +1,28 @@ +import { homedir } from "os"; +import { join } from "path"; +import { handleRequest } from "../core/handler"; +import { corsHeaders, getAllowedOrigins } from "../core/cors"; +import { FsPasteStore } from "../stores/fs"; + +const port = parseInt(process.env.PASTE_PORT || "19433", 10); +const dataDir = + process.env.PASTE_DATA_DIR || join(homedir(), ".plannotator", "pastes"); +const ttlDays = parseInt(process.env.PASTE_TTL_DAYS || "7", 10); +const ttlSeconds = ttlDays * 24 * 60 * 60; +const maxSize = parseInt(process.env.PASTE_MAX_SIZE || "524288", 10); +const allowedOrigins = getAllowedOrigins(process.env.PASTE_ALLOWED_ORIGINS); + +const store = new FsPasteStore(dataDir); + +Bun.serve({ + port, + async fetch(request) { + const origin = request.headers.get("Origin") ?? ""; + const cors = corsHeaders(origin, allowedOrigins); + return handleRequest(request, store, cors, { maxSize, ttlSeconds }); + }, +}); + +console.log(`Plannotator paste service running on http://localhost:${port}`); +console.log(`Storage: ${dataDir}`); +console.log(`TTL: ${ttlDays} days`); diff --git a/apps/paste-service/targets/cloudflare.ts b/apps/paste-service/targets/cloudflare.ts new file mode 100644 index 00000000..4b31f171 --- /dev/null +++ b/apps/paste-service/targets/cloudflare.ts @@ -0,0 +1,18 @@ +import { handleRequest } from "../core/handler"; +import { corsHeaders, getAllowedOrigins } from "../core/cors"; +import { KvPasteStore } from "../stores/kv"; + +interface Env { + PASTE_KV: KVNamespace; + ALLOWED_ORIGINS?: string; +} + +export default { + async fetch(request: Request, env: Env): Promise { + const origin = request.headers.get("Origin") ?? ""; + const allowed = getAllowedOrigins(env.ALLOWED_ORIGINS); + const cors = corsHeaders(origin, allowed); + const store = new KvPasteStore(env.PASTE_KV); + return handleRequest(request, store, cors); + }, +}; diff --git a/apps/paste-service/wrangler.toml b/apps/paste-service/wrangler.toml new file mode 100644 index 00000000..32bc52a3 --- /dev/null +++ b/apps/paste-service/wrangler.toml @@ -0,0 +1,12 @@ +name = "plannotator-paste" +main = "targets/cloudflare.ts" +compatibility_date = "2024-12-01" + +[[kv_namespaces]] +binding = "PASTE_KV" +# Run `wrangler kv:namespace create PASTE_KV` to get your IDs and fill them in. +id = "REPLACE_WITH_KV_NAMESPACE_ID" +preview_id = "REPLACE_WITH_PREVIEW_KV_NAMESPACE_ID" + +[vars] +ALLOWED_ORIGINS = "https://share.plannotator.ai,http://localhost:3001" diff --git a/bun.lock b/bun.lock index b0842c0e..b3aa583d 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "plannotator", @@ -65,6 +64,14 @@ "bun": ">=1.0.0", }, }, + "apps/paste-service": { + "name": "@plannotator/paste-service", + "version": "0.1.0", + "devDependencies": { + "@cloudflare/workers-types": "^4.20241218.0", + "wrangler": "^3.99.0", + }, + }, "apps/pi-extension": { "name": "@plannotator/pi-extension", "version": "0.9.2", @@ -135,14 +142,22 @@ "packages/server": { "name": "@plannotator/server", "version": "0.9.2", + "dependencies": { + "@plannotator/shared": "workspace:*", + }, "peerDependencies": { "bun": ">=1.0.0", }, }, + "packages/shared": { + "name": "@plannotator/shared", + "version": "0.0.1", + }, "packages/ui": { "name": "@plannotator/ui", "version": "0.0.1", "dependencies": { + "@plannotator/shared": "workspace:*", "@plannotator/web-highlighter": "^0.8.1", "diff": "^8.0.3", "highlight.js": "^11.11.1", @@ -301,8 +316,30 @@ "@chevrotain/utils": ["@chevrotain/utils@11.0.3", "", {}, "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="], + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.3.4", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q=="], + + "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.0.2", "", { "peerDependencies": { "unenv": "2.0.0-rc.14", "workerd": "^1.20250124.0" }, "optionalPeers": ["workerd"] }, "sha512-nyzYnlZjjV5xT3LizahG1Iu6mnrCaxglJ04rZLpDwlDVDZ7v46lNsfxhV3A/xtfgQuSHmLnc6SVI+KwBpc3Lwg=="], + + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250718.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-FHf4t7zbVN8yyXgQ/r/GqLPaYZSGUVzeR7RnL28Mwj2djyw2ZergvytVc7fdGcczl6PQh+VKGfZCfUqpJlbi9g=="], + + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250718.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fUiyUJYyqqp4NqJ0YgGtp4WJh/II/YZsUnEb6vVy5Oeas8lUOxnN+ZOJ8N/6/5LQCVAtYCChRiIrBbfhTn5Z8Q=="], + + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250718.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5+eb3rtJMiEwp08Kryqzzu8d1rUcK+gdE442auo5eniMpT170Dz0QxBrqkg2Z48SFUPYbj+6uknuA5tzdRSUSg=="], + + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250718.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Aa2M/DVBEBQDdATMbn217zCSFKE+ud/teS+fFS+OQqKABLn0azO2qq6ANAHYOIE6Q3Sq4CxDIQr8lGdaJHwUog=="], + + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250718.0", "", { "os": "win32", "cpu": "x64" }, "sha512-dY16RXKffmugnc67LTbyjdDHZn5NoTF1yHEf2fN4+OaOnoGSp3N1x77QubTDwqZ9zECWxgQfDLjddcH8dWeFhg=="], + + "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260301.1", "", {}, "sha512-klKnECMb5A4GtVF0P5NH6rCjtyjqIEKJaz6kEtx9YPHhfFO2HUEarO+MI4F8WPchgeZqpGlEpDhRapzrOTw51Q=="], + + "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], + "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], + "@esbuild-plugins/node-globals-polyfill": ["@esbuild-plugins/node-globals-polyfill@0.2.3", "", { "peerDependencies": { "esbuild": "*" } }, "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw=="], + + "@esbuild-plugins/node-modules-polyfill": ["@esbuild-plugins/node-modules-polyfill@0.2.2", "", { "dependencies": { "escape-string-regexp": "^4.0.0", "rollup-plugin-node-polyfills": "^0.2.1" }, "peerDependencies": { "esbuild": "*" } }, "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], @@ -355,6 +392,8 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], + "@google/genai": ["@google/genai@1.42.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-+3nlMTcrQufbQ8IumGkOphxD5Pd5kKyJOzLcnY0/1IuE8upJk5aLmoexZ2BJhBp1zAjRJMEB4a2CJwKI9e2EYw=="], "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], @@ -501,6 +540,8 @@ "@plannotator/opencode": ["@plannotator/opencode@workspace:apps/opencode-plugin"], + "@plannotator/paste-service": ["@plannotator/paste-service@workspace:apps/paste-service"], + "@plannotator/pi-extension": ["@plannotator/pi-extension@workspace:apps/pi-extension"], "@plannotator/portal": ["@plannotator/portal@workspace:apps/portal"], @@ -511,6 +552,8 @@ "@plannotator/server": ["@plannotator/server@workspace:packages/server"], + "@plannotator/shared": ["@plannotator/shared@workspace:packages/shared"], + "@plannotator/ui": ["@plannotator/ui@workspace:packages/ui"], "@plannotator/web-highlighter": ["@plannotator/web-highlighter@0.8.1", "", {}, "sha512-FlteNOwRj9iNSY/AhFMtqOnVS4FvsACvTw6IiOM1y8iDyhiU/WeZOgjURENvIY+wuUaiS9DDFmg0PrHMyuMR1Q=="], @@ -845,6 +888,8 @@ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + "acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="], + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], @@ -869,6 +914,8 @@ "array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="], + "as-table": ["as-table@1.0.55", "", { "dependencies": { "printable-characters": "^1.0.42" } }, "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ=="], + "ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="], "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], @@ -891,6 +938,8 @@ "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], @@ -941,10 +990,14 @@ "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], + "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], @@ -1049,7 +1102,7 @@ "dagre-d3-es": ["dagre-d3-es@7.0.13", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q=="], - "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], + "data-uri-to-buffer": ["data-uri-to-buffer@2.0.2", "", {}, "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA=="], "dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="], @@ -1113,7 +1166,7 @@ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], @@ -1139,6 +1192,10 @@ "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + "exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="], + + "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -1177,12 +1234,16 @@ "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + "get-source": ["get-source@2.0.12", "", { "dependencies": { "data-uri-to-buffer": "^2.0.0", "source-map": "^0.6.1" } }, "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w=="], + "get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="], "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], + "google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="], "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], @@ -1257,6 +1318,8 @@ "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], + "is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], + "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], @@ -1463,10 +1526,14 @@ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "miniflare": ["miniflare@3.20250718.3", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "stoppable": "1.1.0", "undici": "^5.28.5", "workerd": "1.20250718.0", "ws": "8.18.0", "youch": "3.3.4", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-JuPrDJhwLrNLEJiNLWO7ZzJrv/Vv9kZuwMYCfv0LskQDM6Eonw4OvywO3CH/wCGjgHzha/qyjUh8JQ068TjDgQ=="], + "minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="], "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], @@ -1477,6 +1544,8 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], @@ -1545,6 +1614,8 @@ "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "perfect-freehand": ["perfect-freehand@1.2.2", "", {}, "sha512-eh31l019WICQ03pkF3FSzHxB8n07ItqIQ++G5UV8JX0zVOXzgTGCqnRR0jJ2h9U8/2uW4W4mtGJELt9kEV0CFQ=="], @@ -1563,6 +1634,8 @@ "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "printable-characters": ["printable-characters@1.0.42", "", {}, "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ=="], + "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], @@ -1643,6 +1716,12 @@ "rollup": ["rollup@4.54.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.54.0", "@rollup/rollup-android-arm64": "4.54.0", "@rollup/rollup-darwin-arm64": "4.54.0", "@rollup/rollup-darwin-x64": "4.54.0", "@rollup/rollup-freebsd-arm64": "4.54.0", "@rollup/rollup-freebsd-x64": "4.54.0", "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", "@rollup/rollup-linux-arm-musleabihf": "4.54.0", "@rollup/rollup-linux-arm64-gnu": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-loong64-gnu": "4.54.0", "@rollup/rollup-linux-ppc64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-musl": "4.54.0", "@rollup/rollup-linux-s390x-gnu": "4.54.0", "@rollup/rollup-linux-x64-gnu": "4.54.0", "@rollup/rollup-linux-x64-musl": "4.54.0", "@rollup/rollup-openharmony-arm64": "4.54.0", "@rollup/rollup-win32-arm64-msvc": "4.54.0", "@rollup/rollup-win32-ia32-msvc": "4.54.0", "@rollup/rollup-win32-x64-gnu": "4.54.0", "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw=="], + "rollup-plugin-inject": ["rollup-plugin-inject@3.0.2", "", { "dependencies": { "estree-walker": "^0.6.1", "magic-string": "^0.25.3", "rollup-pluginutils": "^2.8.1" } }, "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w=="], + + "rollup-plugin-node-polyfills": ["rollup-plugin-node-polyfills@0.2.1", "", { "dependencies": { "rollup-plugin-inject": "^3.0.0" } }, "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA=="], + + "rollup-pluginutils": ["rollup-pluginutils@2.8.2", "", { "dependencies": { "estree-walker": "^0.6.1" } }, "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ=="], + "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], @@ -1667,6 +1746,8 @@ "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], "sitemap": ["sitemap@8.0.2", "", { "dependencies": { "@types/node": "^17.0.5", "@types/sax": "^1.2.1", "arg": "^5.0.0", "sax": "^1.4.1" }, "bin": { "sitemap": "dist/cli.js" } }, "sha512-LwktpJcyZDoa0IL6KT++lQ53pbSrx2c9ge41/SeLTyqy2XUNA6uR4+P9u5IVo5lPeL2arAcOKn1aZAxoYbCKlQ=="], @@ -1683,10 +1764,16 @@ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "sourcemap-codec": ["sourcemap-codec@1.4.8", "", {}, "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="], + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + "stacktracey": ["stacktracey@2.1.8", "", { "dependencies": { "as-table": "^1.0.36", "get-source": "^2.0.12" } }, "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw=="], + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + "stoppable": ["stoppable@1.1.0", "", {}, "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw=="], + "stream-replace-string": ["stream-replace-string@2.0.0", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="], "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], @@ -1755,10 +1842,12 @@ "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], - "undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="], + "undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "unenv": ["unenv@2.0.0-rc.14", "", { "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.1", "ohash": "^2.0.10", "pathe": "^2.0.3", "ufo": "^1.5.4" } }, "sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q=="], + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], "unifont": ["unifont@0.7.3", "", { "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "sha512-b0GtQzKCyuSHGsfj5vyN8st7muZ6VCI4XD4vFlr7Uy1rlWVYxC3npnfk8MyreHxJYrz1ooLDqDzFe9XqQTlAhA=="], @@ -1827,6 +1916,10 @@ "widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="], + "workerd": ["workerd@1.20250718.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250718.0", "@cloudflare/workerd-darwin-arm64": "1.20250718.0", "@cloudflare/workerd-linux-64": "1.20250718.0", "@cloudflare/workerd-linux-arm64": "1.20250718.0", "@cloudflare/workerd-windows-64": "1.20250718.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-kqkIJP/eOfDlUyBzU7joBg+tl8aB25gEAGqDap+nFWb+WHhnooxjGHgxPBy3ipw2hnShPFNOQt5lFRxbwALirg=="], + + "wrangler": ["wrangler@3.114.17", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.3.4", "@cloudflare/unenv-preset": "2.0.2", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@esbuild-plugins/node-modules-polyfill": "0.2.2", "blake3-wasm": "2.1.5", "esbuild": "0.17.19", "miniflare": "3.20250718.3", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.14", "workerd": "1.20250718.0" }, "optionalDependencies": { "fsevents": "~2.3.2", "sharp": "^0.33.5" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250408.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-tAvf7ly+tB+zwwrmjsCyJ2pJnnc7SZhbnNwXbH+OIdVas3zTSmjcZOjmLKcGGptssAA3RyTKhcF9BvKZzMUycA=="], + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], @@ -1851,6 +1944,8 @@ "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + "youch": ["youch@3.3.4", "", { "dependencies": { "cookie": "^0.7.1", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg=="], + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], @@ -1889,10 +1984,14 @@ "@chevrotain/gast/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], + "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "@mariozechner/pi-ai/undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="], + "@opencode-ai/plugin/zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], "@pierre/diffs/diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], @@ -1943,6 +2042,10 @@ "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "get-source/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "get-uri/data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], + "hast-util-from-html/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], "hast-util-raw/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], @@ -1953,10 +2056,18 @@ "magicast/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "miniflare/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], + + "miniflare/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], + + "miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="], + "node-fetch/data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], "p-retry/retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], @@ -1969,6 +2080,12 @@ "rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + "rollup-plugin-inject/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="], + + "rollup-plugin-inject/magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="], + + "rollup-pluginutils/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="], + "sitemap/@types/node": ["@types/node@17.0.45", "", {}, "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="], "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -1977,6 +2094,10 @@ "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "wrangler/esbuild": ["esbuild@0.17.19", "", { "optionalDependencies": { "@esbuild/android-arm": "0.17.19", "@esbuild/android-arm64": "0.17.19", "@esbuild/android-x64": "0.17.19", "@esbuild/darwin-arm64": "0.17.19", "@esbuild/darwin-x64": "0.17.19", "@esbuild/freebsd-arm64": "0.17.19", "@esbuild/freebsd-x64": "0.17.19", "@esbuild/linux-arm": "0.17.19", "@esbuild/linux-arm64": "0.17.19", "@esbuild/linux-ia32": "0.17.19", "@esbuild/linux-loong64": "0.17.19", "@esbuild/linux-mips64el": "0.17.19", "@esbuild/linux-ppc64": "0.17.19", "@esbuild/linux-riscv64": "0.17.19", "@esbuild/linux-s390x": "0.17.19", "@esbuild/linux-x64": "0.17.19", "@esbuild/netbsd-x64": "0.17.19", "@esbuild/openbsd-x64": "0.17.19", "@esbuild/sunos-x64": "0.17.19", "@esbuild/win32-arm64": "0.17.19", "@esbuild/win32-ia32": "0.17.19", "@esbuild/win32-x64": "0.17.19" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw=="], + + "wrangler/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], + "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -1987,6 +2108,8 @@ "yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], + "youch/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + "@astrojs/react/@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], "@astrojs/react/@vitejs/plugin-react/react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], @@ -2087,6 +2210,88 @@ "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.17.19", "", { "os": "android", "cpu": "arm" }, "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A=="], + + "wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.17.19", "", { "os": "android", "cpu": "arm64" }, "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA=="], + + "wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.17.19", "", { "os": "android", "cpu": "x64" }, "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww=="], + + "wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.17.19", "", { "os": "darwin", "cpu": "arm64" }, "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg=="], + + "wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.17.19", "", { "os": "darwin", "cpu": "x64" }, "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw=="], + + "wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.17.19", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ=="], + + "wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.17.19", "", { "os": "freebsd", "cpu": "x64" }, "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ=="], + + "wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.17.19", "", { "os": "linux", "cpu": "arm" }, "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA=="], + + "wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.17.19", "", { "os": "linux", "cpu": "arm64" }, "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg=="], + + "wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.17.19", "", { "os": "linux", "cpu": "ia32" }, "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ=="], + + "wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ=="], + + "wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A=="], + + "wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.17.19", "", { "os": "linux", "cpu": "ppc64" }, "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg=="], + + "wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA=="], + + "wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.17.19", "", { "os": "linux", "cpu": "s390x" }, "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q=="], + + "wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.17.19", "", { "os": "linux", "cpu": "x64" }, "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw=="], + + "wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.17.19", "", { "os": "none", "cpu": "x64" }, "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q=="], + + "wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.17.19", "", { "os": "openbsd", "cpu": "x64" }, "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g=="], + + "wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.17.19", "", { "os": "sunos", "cpu": "x64" }, "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg=="], + + "wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.17.19", "", { "os": "win32", "cpu": "arm64" }, "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag=="], + + "wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.17.19", "", { "os": "win32", "cpu": "ia32" }, "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw=="], + + "wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.17.19", "", { "os": "win32", "cpu": "x64" }, "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA=="], + + "wrangler/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], + + "wrangler/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], + + "wrangler/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="], + + "wrangler/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="], + + "wrangler/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="], + + "wrangler/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="], + + "wrangler/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="], + + "wrangler/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="], + + "wrangler/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="], + + "wrangler/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="], + + "wrangler/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="], + + "wrangler/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="], + + "wrangler/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.0.4" }, "os": "linux", "cpu": "s390x" }, "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q=="], + + "wrangler/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="], + + "wrangler/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="], + + "wrangler/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="], + + "wrangler/sharp/@img/sharp-wasm32": ["@img/sharp-wasm32@0.33.5", "", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="], + + "wrangler/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.33.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="], + + "wrangler/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], + "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], diff --git a/packages/editor/App.tsx b/packages/editor/App.tsx index 4db80acd..5c57a92c 100644 --- a/packages/editor/App.tsx +++ b/packages/editor/App.tsx @@ -371,6 +371,7 @@ const App: React.FC = () => { const [permissionMode, setPermissionMode] = useState('bypassPermissions'); const [sharingEnabled, setSharingEnabled] = useState(true); const [shareBaseUrl, setShareBaseUrl] = useState(undefined); + const [pasteApiUrl, setPasteApiUrl] = useState(undefined); const [repoInfo, setRepoInfo] = useState<{ display: string; branch?: string } | null>(null); const [showExportDropdown, setShowExportDropdown] = useState(false); const [initialExportTab, setInitialExportTab] = useState<'share' | 'annotations' | 'notes'>(); @@ -417,9 +418,13 @@ const App: React.FC = () => { isLoadingShared, shareUrl, shareUrlSize, + shortShareUrl, + isGeneratingShortUrl, + shortUrlError, pendingSharedAnnotations, sharedGlobalAttachments, clearPendingSharedAnnotations, + generateShortUrl, importFromShareUrl, } = useSharing( markdown, @@ -432,7 +437,8 @@ const App: React.FC = () => { // When loaded from share, mark as loaded setIsLoading(false); }, - shareBaseUrl + shareBaseUrl, + pasteApiUrl ); // Fetch available agents for OpenCode (for validation on approve) @@ -473,7 +479,7 @@ const App: React.FC = () => { if (!res.ok) throw new Error('Not in API mode'); return res.json(); }) - .then((data: { plan: string; origin?: 'claude-code' | 'opencode' | 'pi'; mode?: 'annotate'; sharingEnabled?: boolean; shareBaseUrl?: string; repoInfo?: { display: string; branch?: string }; previousPlan?: string | null; versionInfo?: { version: number; totalVersions: number; project: string } }) => { + .then((data: { plan: string; origin?: 'claude-code' | 'opencode' | 'pi'; mode?: 'annotate'; sharingEnabled?: boolean; shareBaseUrl?: string; pasteApiUrl?: string; repoInfo?: { display: string; branch?: string }; previousPlan?: string | null; versionInfo?: { version: number; totalVersions: number; project: string } }) => { setMarkdown(data.plan); setIsApiMode(true); if (data.mode === 'annotate') { @@ -485,6 +491,9 @@ const App: React.FC = () => { if (data.shareBaseUrl) { setShareBaseUrl(data.shareBaseUrl); } + if (data.pasteApiUrl) { + setPasteApiUrl(data.pasteApiUrl); + } if (data.repoInfo) { setRepoInfo(data.repoInfo); } @@ -1221,6 +1230,10 @@ const App: React.FC = () => { onClose={() => { setShowExport(false); setInitialExportTab(undefined); }} shareUrl={shareUrl} shareUrlSize={shareUrlSize} + shortShareUrl={shortShareUrl} + isGeneratingShortUrl={isGeneratingShortUrl} + shortUrlError={shortUrlError} + onGenerateShortUrl={generateShortUrl} annotationsOutput={annotationsOutput} annotationCount={annotations.length} taterSprite={taterMode ? : undefined} diff --git a/packages/server/index.ts b/packages/server/index.ts index ea3b84d0..c2ebbf26 100644 --- a/packages/server/index.ts +++ b/packages/server/index.ts @@ -56,6 +56,8 @@ export interface ServerOptions { sharingEnabled?: boolean; /** Custom base URL for share links (default: https://share.plannotator.ai) */ shareBaseUrl?: string; + /** Base URL of the paste service API for short URL sharing */ + pasteApiUrl?: string; /** Called when server starts with the URL, remote status, and port */ onReady?: (url: string, isRemote: boolean, port: number) => void; /** OpenCode client for querying available agents (OpenCode only) */ @@ -102,7 +104,7 @@ const RETRY_DELAY_MS = 500; export async function startPlannotatorServer( options: ServerOptions ): Promise { - const { plan, origin, htmlContent, permissionMode, sharingEnabled = true, shareBaseUrl, onReady } = options; + const { plan, origin, htmlContent, permissionMode, sharingEnabled = true, shareBaseUrl, pasteApiUrl, onReady } = options; const isRemote = isRemoteSession(); const configuredPort = getServerPort(); @@ -192,7 +194,7 @@ export async function startPlannotatorServer( // API: Get plan content if (url.pathname === "/api/plan") { - return Response.json({ plan, origin, permissionMode, sharingEnabled, shareBaseUrl, repoInfo, previousPlan, versionInfo }); + return Response.json({ plan, origin, permissionMode, sharingEnabled, shareBaseUrl, pasteApiUrl, repoInfo, previousPlan, versionInfo }); } // API: Serve images (local paths or temp uploads) diff --git a/packages/server/package.json b/packages/server/package.json index 4a3d3fcd..4d016f19 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -13,11 +13,15 @@ "./browser": "./browser.ts", "./storage": "./storage.ts", "./git": "./git.ts", - "./repo": "./repo.ts" + "./repo": "./repo.ts", + "./share-url": "./share-url.ts" }, "files": [ "*.ts" ], + "dependencies": { + "@plannotator/shared": "workspace:*" + }, "peerDependencies": { "bun": ">=1.0.0" } diff --git a/packages/server/share-url.ts b/packages/server/share-url.ts new file mode 100644 index 00000000..9b7528fc --- /dev/null +++ b/packages/server/share-url.ts @@ -0,0 +1,53 @@ +/** + * Server-side share URL generation for remote sessions + * + * Generates a share.plannotator.ai URL from plan content so remote users + * can open the review in their local browser without port forwarding. + */ + +import { compress } from "@plannotator/shared/compress"; + +const DEFAULT_SHARE_BASE = "https://share.plannotator.ai"; + +/** + * Generate a share URL from plan markdown content. + * + * Returns the full hash-based URL. For remote sessions, this lets the + * user open the plan in their local browser without any backend needed. + */ +export async function generateRemoteShareUrl( + plan: string, + shareBaseUrl?: string +): Promise { + const base = shareBaseUrl || DEFAULT_SHARE_BASE; + const hash = await compress({ p: plan, a: [] }); + return `${base}/#${hash}`; +} + +/** + * Format byte size as human-readable string + */ +export function formatSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + const kb = bytes / 1024; + return kb < 100 ? `${kb.toFixed(1)} KB` : `${Math.round(kb)} KB`; +} + +/** + * Generate a remote share URL and write it to stderr for the user. + * Silently does nothing on failure. + */ +export async function writeRemoteShareLink( + content: string, + shareBaseUrl: string | undefined, + verb: string, + noun: string +): Promise { + const shareUrl = await generateRemoteShareUrl(content, shareBaseUrl); + const size = formatSize(new TextEncoder().encode(shareUrl).length); + process.stderr.write( + `\n Open this link on your local machine to ${verb}:\n` + + ` ${shareUrl}\n\n` + + ` (${size} — ${noun}, annotations added in browser)\n\n` + ); +} diff --git a/packages/shared/compress.ts b/packages/shared/compress.ts new file mode 100644 index 00000000..70c5099a --- /dev/null +++ b/packages/shared/compress.ts @@ -0,0 +1,51 @@ +/** + * Portable deflate-raw + base64url compression. + * + * Uses only Web APIs (CompressionStream, TextEncoder, btoa) so it works + * in browsers, Bun, and edge runtimes. Both @plannotator/server and + * @plannotator/ui import from here — single source of truth. + */ + +export async function compress(data: unknown): Promise { + const json = JSON.stringify(data); + const byteArray = new TextEncoder().encode(json); + + const stream = new CompressionStream('deflate-raw'); + const writer = stream.writable.getWriter(); + writer.write(byteArray); + writer.close(); + + const buffer = await new Response(stream.readable).arrayBuffer(); + const compressed = new Uint8Array(buffer); + + // Loop instead of spread to avoid RangeError on large payloads + // (String.fromCharCode(...arr) has a ~65K argument limit) + let binary = ''; + for (let i = 0; i < compressed.length; i++) { + binary += String.fromCharCode(compressed[i]); + } + const base64 = btoa(binary); + return base64 + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); +} + +export async function decompress(b64: string): Promise { + const base64 = b64 + .replace(/-/g, '+') + .replace(/_/g, '/'); + + const binary = atob(base64); + const byteArray = Uint8Array.from(binary, c => c.charCodeAt(0)); + + const stream = new DecompressionStream('deflate-raw'); + const writer = stream.writable.getWriter(); + writer.write(byteArray); + writer.close(); + + const buffer = await new Response(stream.readable).arrayBuffer(); + const json = new TextDecoder().decode(buffer); + + return JSON.parse(json); +} diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 00000000..4a21378b --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,8 @@ +{ + "name": "@plannotator/shared", + "version": "0.0.1", + "private": true, + "exports": { + "./compress": "./compress.ts" + } +} diff --git a/packages/ui/components/ExportModal.tsx b/packages/ui/components/ExportModal.tsx index 4ae98d21..7d1ade41 100644 --- a/packages/ui/components/ExportModal.tsx +++ b/packages/ui/components/ExportModal.tsx @@ -15,6 +15,14 @@ interface ExportModalProps { onClose: () => void; shareUrl: string; shareUrlSize: string; + /** Short share URL from the paste service (empty string when unavailable) */ + shortShareUrl?: string; + /** Whether the short URL is currently being generated */ + isGeneratingShortUrl?: boolean; + /** Error from the last short URL generation attempt (empty string = no error) */ + shortUrlError?: string; + /** Generate a short URL on demand (user clicks "Create short link") */ + onGenerateShortUrl?: () => void; annotationsOutput: string; annotationCount: number; taterSprite?: React.ReactNode; @@ -34,6 +42,10 @@ export const ExportModal: React.FC = ({ onClose, shareUrl, shareUrlSize, + shortShareUrl = '', + isGeneratingShortUrl = false, + shortUrlError = '', + onGenerateShortUrl, annotationsOutput, annotationCount, taterSprite, @@ -44,7 +56,7 @@ export const ExportModal: React.FC = ({ }) => { const defaultTab = initialTab || (sharingEnabled ? 'share' : 'annotations'); const [activeTab, setActiveTab] = useState(defaultTab); - const [copied, setCopied] = useState(false); + const [copied, setCopied] = useState<'short' | 'full' | 'annotations' | false>(false); const [saveStatus, setSaveStatus] = useState>({ obsidian: 'idle', bear: 'idle' }); const [saveErrors, setSaveErrors] = useState>({}); @@ -72,10 +84,10 @@ export const ExportModal: React.FC = ({ const isObsidianReady = obsidianSettings.enabled && effectiveVaultPath.trim().length > 0; const isBearReady = bearSettings.enabled; - const handleCopyUrl = async () => { + const handleCopy = async (text: string, which: 'short' | 'full' | 'annotations') => { try { - await navigator.clipboard.writeText(shareUrl); - setCopied(true); + await navigator.clipboard.writeText(text); + setCopied(which); setTimeout(() => setCopied(false), 2000); } catch (e) { console.error('Failed to copy:', e); @@ -83,15 +95,12 @@ export const ExportModal: React.FC = ({ }; const handleCopyAnnotations = async () => { - try { - await navigator.clipboard.writeText(annotationsOutput); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch (e) { - console.error('Failed to copy:', e); - } + await handleCopy(annotationsOutput, 'annotations'); }; + // Whether the hash URL is large enough to warrant a short URL option + const urlIsLarge = shareUrl.length > 2048; + const handleDownloadAnnotations = () => { const blob = new Blob([annotationsOutput], { type: 'text/plain' }); const url = URL.createObjectURL(blob); @@ -225,22 +234,85 @@ export const ExportModal: React.FC = ({ {/* Tab content */} {activeTab === 'share' && sharingEnabled ? (
+ {/* Short URL — primary copy target when available */} + {shortShareUrl ? ( +
+ +
+ (e.target as HTMLInputElement).select()} + /> + +
+

+ Short link — safe for Slack, email, and messaging apps. +

+
+ ) : isGeneratingShortUrl ? ( +
+ + + + Generating short link... +
+ ) : urlIsLarge && onGenerateShortUrl ? ( +
+

+ This URL may be too long for some messaging apps. +

+ + {shortUrlError && ( +

({shortUrlError})

+ )} +
+ ) : null} + + {/* Full hash URL — always available */}