mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
Add short URL sharing via paste service to fix URL truncation (#188)
* Add short URL sharing via paste service to fix URL truncation on Slack/messaging apps Share URLs for large plans can be 10-40KB+ because the entire plan + annotations are compressed into the URL hash. Services like Slack, WhatsApp, and Twitter truncate these URLs, making shared plans unviewable (related: #142). This adds an optional paste-service backend that stores compressed payloads and returns short ~60-char URLs (e.g. share.plannotator.ai/p/aBcDeFgH). Changes: - Add Cloudflare Worker paste service (apps/paste-worker/) with KV storage and 90-day TTL for stored plans - Add createShortShareUrl() and loadFromPasteId() to sharing utils - Update useSharing hook to auto-generate short URLs with 1s debounce - Update ExportModal to show short URL as primary copy target with full hash URL as backup - Portal automatically supports /p/<id> paths via useSharing hook - Fully backward compatible: hash-based URLs continue to work unchanged - Graceful degradation: falls back to hash URLs if paste service is unavailable * Fix critical bugs found during code review of short URL feature - Worker: return only { id } so client constructs URL with its own shareBaseUrl (fixes self-hosted deployments) - importFromShareUrl: handle /p/<id> short URLs in addition to hash URLs (fixes teammate import via short links) - Anchor /p/<id> regex with ^ to prevent false matches on nested paths - Pass shareBaseUrl to loadFromPasteId (was always defaulting) - replaceState preserves base path instead of hardcoding / - Clear stale shortShareUrl immediately on debounce to prevent showing outdated link during the 1s delay - Add shareBaseUrl to dependency arrays for loadFromHash and importFromShareUrl callbacks - Remove unused url field fallback from paste API response parsing * Fix pasteApiUrl/shareBaseUrl confusion and add remote session share URLs Bug fix (addresses @backnotprop's code review): - useSharing hook now accepts separate `pasteApiUrl` parameter instead of incorrectly passing `shareBaseUrl` to `loadFromPasteId`. These are different domains (share portal vs paste backend). When omitted, the default `https://paste.plannotator.ai` is used correctly. - Fixed in all three call sites: loadFromHash, importFromShareUrl, and generateShortUrl (via createShortShareUrl options). Remote session share URLs (implements #192): - When running on a remote instance (SSH, devcontainer), Plannotator now generates a share.plannotator.ai URL and prints it to stderr so the user can open the plan review in their local browser. - Works for all three modes: plan review, code review, and annotate. - Uses the same deflate-raw + base64url encoding as the client. - Fails silently if URL generation fails — port forwarding still works. - New server utility: packages/server/share-url.ts * Address code review findings: fix spread limit, CORS, deps array - share-url.ts: Replace btoa(String.fromCharCode(...compressed)) with a loop to avoid RangeError on plans >65K compressed bytes - share-url.ts: Add cross-reference comment noting the server-side SharePayload is an intentional subset of the canonical UI type - paste-worker: Only return CORS headers for allowed origins; disallowed origins get no CORS headers instead of misleading partial headers - useSharing.ts: Remove unused shareBaseUrl from loadFromHash dependency array (loadFromHash only uses pasteApiUrl, not shareBaseUrl) * Restructure paste service with pluggable storage, fix consent flow and security issues Replaces monolithic paste-worker with multi-target paste-service architecture: - Core logic (handler, storage interface, CORS) separated from deployment targets - Filesystem store (self-hosted), KV store (Cloudflare), S3 stub (future) - Bun binary target + Cloudflare Worker target - Fix auto-upload removed: short URLs only created on explicit user click - Clear stale short URL state when content changes - Fix require('fs') in ESM module, add path-traversal guard in FsPasteStore - Return 400 (not 500) for malformed JSON in both targets - Fix compress() spread limit for large plans - Updated docs: self-hosting guide, sharing guide, env vars, API endpoints Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Simplify paste service: extract shared router, remove TOCTOU patterns - Extract duplicated HTTP routing from bun.ts and cloudflare.ts into handleRequest() in core/handler.ts — targets are now thin wrappers - Move ID_PATTERN regex to core (was duplicated in both targets) - Remove existsSync guard before mkdirSync({recursive:true}) in fs.ts - Remove file.exists() check before file.json() in fs.ts get() — the try/catch already handles missing files - Extract DEFAULT_SHARE_BASE constant in sharing.ts (was hardcoded twice) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Deduplicate compression, image parsing, and remote share URL logic - Create packages/shared/ with compress/decompress (single source of truth) — both @plannotator/server and @plannotator/ui import from here - Export parseShareableImages from sharing.ts, remove duplicate parseGlobalAttachments from useSharing.ts - Extract writeRemoteShareLink() helper in share-url.ts, replace 3 near-identical blocks in apps/hook/server/index.ts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix modulo bias in paste ID generation and remove nonexistent doc artifacts - Use rejection sampling in generateId() to eliminate modulo bias (bytes >= 248 discarded, uniform distribution over 62 chars) - Remove references to install-paste.sh and Docker image that don't exist yet; point self-hosting docs to GitHub Releases binaries Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Michael Ramos <mdramos8@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -74,6 +79,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.
|
||||
|
||||
@@ -173,6 +179,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).
|
||||
@@ -265,7 +280,7 @@ Text highlighting uses `web-highlighter` library. Code blocks use manual `<mark>
|
||||
|
||||
**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:**
|
||||
|
||||
|
||||
@@ -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(() => {});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Set the `PLANNOTATOR_SHARE_URL` environment variable to your portal's URL:
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
When `PLANNOTATOR_SHARE_URL` is not set, the default `https://share.plannotator.ai` is used.
|
||||
### Configuration
|
||||
|
||||
## Verify
|
||||
| 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
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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": "<compressed base64 string>"
|
||||
}
|
||||
```
|
||||
|
||||
Returns: `{ "id": "aBcDeFgH" }` (201 Created)
|
||||
|
||||
Limits: 512KB max payload. Auto-deleted after configured TTL (default: 7 days).
|
||||
|
||||
### GET `/api/paste/:id`
|
||||
|
||||
Returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": "<compressed base64 string>"
|
||||
}
|
||||
```
|
||||
|
||||
Or: `{ "error": "Paste not found or expired" }` (404)
|
||||
|
||||
Cached for 1 hour (`Cache-Control: public, max-age=3600`).
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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<string, string> {
|
||||
if (allowedOrigins.includes(requestOrigin) || allowedOrigins.includes("*")) {
|
||||
return {
|
||||
...BASE_CORS_HEADERS,
|
||||
"Access-Control-Allow-Origin": requestOrigin,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -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<PasteOptions> = {}
|
||||
): 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<string | null> {
|
||||
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<string, string>,
|
||||
options?: Partial<PasteOptions>
|
||||
): Promise<Response> {
|
||||
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 }
|
||||
);
|
||||
}
|
||||
@@ -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<void>;
|
||||
get(id: string): Promise<string | null>;
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
const entry: PasteFile = {
|
||||
data,
|
||||
expiresAt: Date.now() + ttlSeconds * 1000,
|
||||
};
|
||||
await Bun.write(this.safePath(id), JSON.stringify(entry));
|
||||
}
|
||||
|
||||
async get(id: string): Promise<string | null> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
await this.kv.put(`paste:${id}`, data, { expirationTtl: ttlSeconds });
|
||||
}
|
||||
|
||||
async get(id: string): Promise<string | null> {
|
||||
return this.kv.get(`paste:${id}`);
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
throw new Error("S3PasteStore not yet implemented");
|
||||
}
|
||||
|
||||
async get(_id: string): Promise<string | null> {
|
||||
throw new Error("S3PasteStore not yet implemented");
|
||||
}
|
||||
}
|
||||
@@ -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`);
|
||||
@@ -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<Response> {
|
||||
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);
|
||||
},
|
||||
};
|
||||
@@ -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"
|
||||
@@ -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=="],
|
||||
|
||||
+15
-2
@@ -372,6 +372,7 @@ const App: React.FC = () => {
|
||||
const [permissionMode, setPermissionMode] = useState<PermissionMode>('bypassPermissions');
|
||||
const [sharingEnabled, setSharingEnabled] = useState(true);
|
||||
const [shareBaseUrl, setShareBaseUrl] = useState<string | undefined>(undefined);
|
||||
const [pasteApiUrl, setPasteApiUrl] = useState<string | undefined>(undefined);
|
||||
const [repoInfo, setRepoInfo] = useState<{ display: string; branch?: string } | null>(null);
|
||||
const [showExportDropdown, setShowExportDropdown] = useState(false);
|
||||
const [initialExportTab, setInitialExportTab] = useState<'share' | 'annotations' | 'notes'>();
|
||||
@@ -425,9 +426,13 @@ const App: React.FC = () => {
|
||||
isLoadingShared,
|
||||
shareUrl,
|
||||
shareUrlSize,
|
||||
shortShareUrl,
|
||||
isGeneratingShortUrl,
|
||||
shortUrlError,
|
||||
pendingSharedAnnotations,
|
||||
sharedGlobalAttachments,
|
||||
clearPendingSharedAnnotations,
|
||||
generateShortUrl,
|
||||
importFromShareUrl,
|
||||
} = useSharing(
|
||||
markdown,
|
||||
@@ -440,7 +445,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)
|
||||
@@ -481,7 +487,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') {
|
||||
@@ -493,6 +499,9 @@ const App: React.FC = () => {
|
||||
if (data.shareBaseUrl) {
|
||||
setShareBaseUrl(data.shareBaseUrl);
|
||||
}
|
||||
if (data.pasteApiUrl) {
|
||||
setPasteApiUrl(data.pasteApiUrl);
|
||||
}
|
||||
if (data.repoInfo) {
|
||||
setRepoInfo(data.repoInfo);
|
||||
}
|
||||
@@ -1274,6 +1283,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 ? <TaterSpritePullup /> : undefined}
|
||||
|
||||
@@ -59,6 +59,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) */
|
||||
@@ -105,7 +107,7 @@ const RETRY_DELAY_MS = 500;
|
||||
export async function startPlannotatorServer(
|
||||
options: ServerOptions
|
||||
): Promise<ServerResult> {
|
||||
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();
|
||||
@@ -196,7 +198,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 a linked markdown document
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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<string> {
|
||||
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<void> {
|
||||
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`
|
||||
);
|
||||
}
|
||||
@@ -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<string> {
|
||||
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<unknown> {
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "@plannotator/shared",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"exports": {
|
||||
"./compress": "./compress.ts"
|
||||
}
|
||||
}
|
||||
@@ -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<ExportModalProps> = ({
|
||||
onClose,
|
||||
shareUrl,
|
||||
shareUrlSize,
|
||||
shortShareUrl = '',
|
||||
isGeneratingShortUrl = false,
|
||||
shortUrlError = '',
|
||||
onGenerateShortUrl,
|
||||
annotationsOutput,
|
||||
annotationCount,
|
||||
taterSprite,
|
||||
@@ -44,7 +56,7 @@ export const ExportModal: React.FC<ExportModalProps> = ({
|
||||
}) => {
|
||||
const defaultTab = initialTab || (sharingEnabled ? 'share' : 'annotations');
|
||||
const [activeTab, setActiveTab] = useState<Tab>(defaultTab);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [copied, setCopied] = useState<'short' | 'full' | 'annotations' | false>(false);
|
||||
const [saveStatus, setSaveStatus] = useState<Record<SaveTarget, SaveStatus>>({ obsidian: 'idle', bear: 'idle' });
|
||||
const [saveErrors, setSaveErrors] = useState<Record<string, string>>({});
|
||||
|
||||
@@ -72,10 +84,10 @@ export const ExportModal: React.FC<ExportModalProps> = ({
|
||||
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<ExportModalProps> = ({
|
||||
};
|
||||
|
||||
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<ExportModalProps> = ({
|
||||
{/* Tab content */}
|
||||
{activeTab === 'share' && sharingEnabled ? (
|
||||
<div className="space-y-4">
|
||||
{/* Short URL — primary copy target when available */}
|
||||
{shortShareUrl ? (
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-muted-foreground mb-2">
|
||||
Share Link
|
||||
</label>
|
||||
<div className="relative group">
|
||||
<input
|
||||
readOnly
|
||||
value={shortShareUrl}
|
||||
className="w-full bg-muted rounded-lg p-3 pr-20 text-xs font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
|
||||
onClick={e => (e.target as HTMLInputElement).select()}
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleCopy(shortShareUrl, 'short')}
|
||||
className="absolute top-1.5 right-2 px-2 py-1 rounded text-xs font-medium bg-background/80 hover:bg-background border border-border/50 transition-colors flex items-center gap-1"
|
||||
>
|
||||
{copied === 'short' ? (
|
||||
<>
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Copied
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Copy
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground mt-1">
|
||||
Short link — safe for Slack, email, and messaging apps.
|
||||
</p>
|
||||
</div>
|
||||
) : isGeneratingShortUrl ? (
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground p-3 bg-muted rounded-lg">
|
||||
<svg className="w-3 h-3 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path d="M12 2v4m0 12v4m-7.07-3.93l2.83-2.83m8.48-8.48l2.83-2.83M2 12h4m12 0h4m-3.93 7.07l-2.83-2.83M7.76 7.76L4.93 4.93" />
|
||||
</svg>
|
||||
Generating short link...
|
||||
</div>
|
||||
) : urlIsLarge && onGenerateShortUrl ? (
|
||||
<div className="p-3 bg-amber-500/10 border border-amber-500/20 rounded-lg">
|
||||
<p className="text-xs text-amber-600 dark:text-amber-400 mb-2">
|
||||
This URL may be too long for some messaging apps.
|
||||
</p>
|
||||
<button
|
||||
onClick={onGenerateShortUrl}
|
||||
className="px-3 py-1.5 rounded-md text-xs font-medium bg-primary text-primary-foreground hover:opacity-90 transition-opacity"
|
||||
>
|
||||
Create short link
|
||||
</button>
|
||||
{shortUrlError && (
|
||||
<p className="text-[10px] text-amber-500 mt-1">({shortUrlError})</p>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Full hash URL — always available */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-muted-foreground mb-2">
|
||||
Shareable URL
|
||||
{shortShareUrl ? 'Full URL (backup)' : 'Shareable URL'}
|
||||
</label>
|
||||
<div className="relative group">
|
||||
<textarea
|
||||
readOnly
|
||||
value={shareUrl}
|
||||
className="w-full h-32 bg-muted rounded-lg p-3 pr-20 text-xs font-mono resize-none focus:outline-none focus:ring-2 focus:ring-accent/50"
|
||||
className="w-full h-24 bg-muted rounded-lg p-3 pr-20 text-xs font-mono resize-none focus:outline-none focus:ring-2 focus:ring-accent/50"
|
||||
onClick={e => (e.target as HTMLTextAreaElement).select()}
|
||||
/>
|
||||
<button
|
||||
onClick={handleCopyUrl}
|
||||
onClick={() => handleCopy(shareUrl, 'full')}
|
||||
className="absolute top-2 right-2 px-2 py-1 rounded text-xs font-medium bg-background/80 hover:bg-background border border-border/50 transition-colors flex items-center gap-1"
|
||||
>
|
||||
{copied ? (
|
||||
{copied === 'full' ? (
|
||||
<>
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
@@ -260,10 +332,15 @@ export const ExportModal: React.FC<ExportModalProps> = ({
|
||||
{shareUrlSize}
|
||||
</div>
|
||||
</div>
|
||||
{!shortShareUrl && !isGeneratingShortUrl && !urlIsLarge && (
|
||||
<p className="text-[10px] text-muted-foreground mt-1">
|
||||
This URL contains the full plan and annotations.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
This URL contains the full plan and all annotations. Anyone with this link can view and add to your annotations.
|
||||
Anyone with this link can view and add to your annotations.
|
||||
</p>
|
||||
</div>
|
||||
) : activeTab === 'notes' && showNotesTab ? (
|
||||
@@ -384,7 +461,7 @@ export const ExportModal: React.FC<ExportModalProps> = ({
|
||||
onClick={handleCopyAnnotations}
|
||||
className="px-3 py-1.5 rounded-md text-xs font-medium bg-muted hover:bg-muted/80 transition-colors"
|
||||
>
|
||||
{copied ? 'Copied!' : 'Copy'}
|
||||
{copied === 'annotations' ? 'Copied!' : 'Copy'}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDownloadAnnotations}
|
||||
|
||||
+128
-35
@@ -3,18 +3,23 @@
|
||||
*
|
||||
* Handles:
|
||||
* - Loading shared state from URL hash on mount
|
||||
* - Generating shareable URLs
|
||||
* - Loading from paste-service short URLs (/p/<id>)
|
||||
* - Generating shareable URLs (hash-based and short)
|
||||
* - Tracking whether current session is from a shared link
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Annotation, type ImageAttachment } from '../types';
|
||||
import {
|
||||
type SharePayload,
|
||||
parseShareHash,
|
||||
generateShareUrl,
|
||||
decompress,
|
||||
fromShareable,
|
||||
parseShareableImages,
|
||||
formatUrlSize,
|
||||
createShortShareUrl,
|
||||
loadFromPasteId,
|
||||
} from '../utils/sharing';
|
||||
|
||||
export interface ImportResult {
|
||||
@@ -37,6 +42,15 @@ interface UseSharingResult {
|
||||
/** Human-readable size of the share URL */
|
||||
shareUrlSize: string;
|
||||
|
||||
/** Short share URL backed by the paste service (empty string when unavailable) */
|
||||
shortShareUrl: string;
|
||||
|
||||
/** Whether the short URL is currently being generated */
|
||||
isGeneratingShortUrl: boolean;
|
||||
|
||||
/** Error message from the last short URL generation attempt, or empty string */
|
||||
shortUrlError: string;
|
||||
|
||||
/** Annotations loaded from share that need to be applied to DOM */
|
||||
pendingSharedAnnotations: Annotation[] | null;
|
||||
|
||||
@@ -49,26 +63,13 @@ interface UseSharingResult {
|
||||
/** Manually trigger share URL generation */
|
||||
refreshShareUrl: () => Promise<void>;
|
||||
|
||||
/** Generate a short URL via the paste service (user must explicitly trigger this) */
|
||||
generateShortUrl: () => Promise<void>;
|
||||
|
||||
/** Import annotations from a teammate's share URL */
|
||||
importFromShareUrl: (url: string) => Promise<ImportResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse SharePayload.g (which can be old string[] or new [path,name][] format) into ImageAttachment[]
|
||||
*/
|
||||
function parseGlobalAttachments(g: unknown[] | undefined): ImageAttachment[] {
|
||||
if (!g?.length) return [];
|
||||
return g.map((item, idx) => {
|
||||
if (typeof item === 'string') {
|
||||
const name = item.split('/').pop()?.replace(/\.[^.]+$/, '') || `image-${idx + 1}`;
|
||||
return { path: item, name };
|
||||
}
|
||||
if (Array.isArray(item) && item.length >= 2) {
|
||||
return { path: item[0] as string, name: item[1] as string };
|
||||
}
|
||||
return { path: String(item), name: `image-${idx + 1}` };
|
||||
});
|
||||
}
|
||||
|
||||
export function useSharing(
|
||||
markdown: string,
|
||||
@@ -78,12 +79,16 @@ export function useSharing(
|
||||
setAnnotations: (a: Annotation[]) => void,
|
||||
setGlobalAttachments: (g: ImageAttachment[]) => void,
|
||||
onSharedLoad?: () => void,
|
||||
shareBaseUrl?: string
|
||||
shareBaseUrl?: string,
|
||||
pasteApiUrl?: string
|
||||
): UseSharingResult {
|
||||
const [isSharedSession, setIsSharedSession] = useState(false);
|
||||
const [isLoadingShared, setIsLoadingShared] = useState(true);
|
||||
const [shareUrl, setShareUrl] = useState('');
|
||||
const [shareUrlSize, setShareUrlSize] = useState('');
|
||||
const [shortShareUrl, setShortShareUrl] = useState('');
|
||||
const [isGeneratingShortUrl, setIsGeneratingShortUrl] = useState(false);
|
||||
const [shortUrlError, setShortUrlError] = useState('');
|
||||
const [pendingSharedAnnotations, setPendingSharedAnnotations] = useState<Annotation[] | null>(null);
|
||||
const [sharedGlobalAttachments, setSharedGlobalAttachments] = useState<ImageAttachment[] | null>(null);
|
||||
|
||||
@@ -92,9 +97,40 @@ export function useSharing(
|
||||
setSharedGlobalAttachments(null);
|
||||
}, []);
|
||||
|
||||
// Load shared state from URL hash
|
||||
// Load shared state from URL hash (or paste-service short URL)
|
||||
const loadFromHash = useCallback(async () => {
|
||||
try {
|
||||
// Check for short URL path pattern: /p/<id>
|
||||
const pathMatch = window.location.pathname.match(/^\/p\/([A-Za-z0-9]{6,16})$/);
|
||||
if (pathMatch) {
|
||||
const pasteId = pathMatch[1];
|
||||
const payload = await loadFromPasteId(pasteId, pasteApiUrl);
|
||||
if (payload) {
|
||||
setMarkdown(payload.p);
|
||||
|
||||
const restoredAnnotations = fromShareable(payload.a);
|
||||
setAnnotations(restoredAnnotations);
|
||||
|
||||
if (payload.g?.length) {
|
||||
const parsed = parseShareableImages(payload.g) ?? [];
|
||||
setGlobalAttachments(parsed);
|
||||
setSharedGlobalAttachments(parsed);
|
||||
}
|
||||
|
||||
setPendingSharedAnnotations(restoredAnnotations);
|
||||
setIsSharedSession(true);
|
||||
onSharedLoad?.();
|
||||
|
||||
// Remove the /p/<id> path from browser history so a refresh doesn't
|
||||
// attempt a network fetch. The plan is now held in memory.
|
||||
const basePath = window.location.pathname.replace(/\/p\/[A-Za-z0-9]+$/, '') || '/';
|
||||
window.history.replaceState({}, '', basePath);
|
||||
|
||||
return true;
|
||||
}
|
||||
// Paste fetch failed — fall through to try the hash fragment instead.
|
||||
}
|
||||
|
||||
const payload = await parseShareHash();
|
||||
|
||||
if (payload) {
|
||||
@@ -107,7 +143,7 @@ export function useSharing(
|
||||
|
||||
// Restore global attachments if present
|
||||
if (payload.g?.length) {
|
||||
const parsed = parseGlobalAttachments(payload.g);
|
||||
const parsed = parseShareableImages(payload.g) ?? [];
|
||||
setGlobalAttachments(parsed);
|
||||
setSharedGlobalAttachments(parsed);
|
||||
}
|
||||
@@ -135,7 +171,7 @@ export function useSharing(
|
||||
console.error('Failed to load from share hash:', e);
|
||||
return false;
|
||||
}
|
||||
}, [setMarkdown, setAnnotations, setGlobalAttachments, onSharedLoad]);
|
||||
}, [setMarkdown, setAnnotations, setGlobalAttachments, onSharedLoad, pasteApiUrl]);
|
||||
|
||||
// Load from hash on mount
|
||||
useEffect(() => {
|
||||
@@ -172,21 +208,74 @@ export function useSharing(
|
||||
refreshShareUrl();
|
||||
}, [refreshShareUrl]);
|
||||
|
||||
// Import annotations from a teammate's share URL
|
||||
// Clear stale short URL when content changes (does NOT auto-regenerate —
|
||||
// the user must explicitly click "Create short link" again)
|
||||
useEffect(() => {
|
||||
setShortShareUrl('');
|
||||
setShortUrlError('');
|
||||
}, [markdown, annotations]);
|
||||
|
||||
/**
|
||||
* Generate a short URL via the paste service.
|
||||
* Only called when the user explicitly clicks "Create short link".
|
||||
* Clears the short URL if the service is unavailable — the full
|
||||
* hash-based URL remains usable as a fallback.
|
||||
*/
|
||||
const generateShortUrl = useCallback(async () => {
|
||||
if (!markdown) return;
|
||||
|
||||
setIsGeneratingShortUrl(true);
|
||||
setShortUrlError('');
|
||||
|
||||
try {
|
||||
const result = await createShortShareUrl(
|
||||
markdown,
|
||||
annotations,
|
||||
globalAttachments,
|
||||
{ pasteApiUrl, shareBaseUrl }
|
||||
);
|
||||
|
||||
if (result) {
|
||||
setShortShareUrl(result.shortUrl);
|
||||
} else {
|
||||
setShortShareUrl('');
|
||||
setShortUrlError('Short URL service unavailable');
|
||||
}
|
||||
} catch {
|
||||
setShortShareUrl('');
|
||||
setShortUrlError('Failed to generate short URL');
|
||||
} finally {
|
||||
setIsGeneratingShortUrl(false);
|
||||
}
|
||||
}, [markdown, annotations, globalAttachments, shareBaseUrl, pasteApiUrl]);
|
||||
|
||||
// Import annotations from a teammate's share URL (supports both hash-based and short /p/<id> URLs)
|
||||
const importFromShareUrl = useCallback(async (url: string): Promise<ImportResult> => {
|
||||
try {
|
||||
// Extract hash from URL
|
||||
const hashIndex = url.indexOf('#');
|
||||
if (hashIndex === -1) {
|
||||
return { success: false, count: 0, planTitle: '', error: 'Invalid share URL: no hash fragment found' };
|
||||
}
|
||||
const hash = url.slice(hashIndex + 1);
|
||||
if (!hash) {
|
||||
return { success: false, count: 0, planTitle: '', error: 'Invalid share URL: empty hash' };
|
||||
}
|
||||
let payload: SharePayload | undefined;
|
||||
|
||||
// Decompress payload
|
||||
const payload = await decompress(hash);
|
||||
// Check for short URL pattern: /p/<id>
|
||||
const shortMatch = url.match(/\/p\/([A-Za-z0-9]{6,16})(?:\?|$)/);
|
||||
if (shortMatch) {
|
||||
const pasteId = shortMatch[1];
|
||||
const loaded = await loadFromPasteId(pasteId, pasteApiUrl);
|
||||
if (!loaded) {
|
||||
return { success: false, count: 0, planTitle: '', error: 'Failed to load from short URL — paste may have expired' };
|
||||
}
|
||||
payload = loaded;
|
||||
} else {
|
||||
// Fall back to hash-based URL
|
||||
const hashIndex = url.indexOf('#');
|
||||
if (hashIndex === -1) {
|
||||
return { success: false, count: 0, planTitle: '', error: 'Invalid share URL: no hash fragment or short link found' };
|
||||
}
|
||||
const hash = url.slice(hashIndex + 1);
|
||||
if (!hash) {
|
||||
return { success: false, count: 0, planTitle: '', error: 'Invalid share URL: empty hash' };
|
||||
}
|
||||
|
||||
payload = await decompress(hash);
|
||||
}
|
||||
|
||||
// Extract plan title from embedded plan text
|
||||
const lines = (payload.p || '').trim().split('\n');
|
||||
@@ -218,7 +307,7 @@ export function useSharing(
|
||||
|
||||
// Handle global attachments (deduplicate by path)
|
||||
if (payload.g?.length) {
|
||||
const parsed = parseGlobalAttachments(payload.g);
|
||||
const parsed = parseShareableImages(payload.g) ?? [];
|
||||
const existingPaths = new Set(globalAttachments.map(g => g.path));
|
||||
const newAttachments = parsed.filter(p => !existingPaths.has(p.path));
|
||||
if (newAttachments.length > 0) {
|
||||
@@ -233,17 +322,21 @@ export function useSharing(
|
||||
const errorMessage = e instanceof Error ? e.message : 'Failed to decompress share URL';
|
||||
return { success: false, count: 0, planTitle: '', error: errorMessage };
|
||||
}
|
||||
}, [annotations, globalAttachments, setAnnotations, setGlobalAttachments]);
|
||||
}, [annotations, globalAttachments, setAnnotations, setGlobalAttachments, pasteApiUrl]);
|
||||
|
||||
return {
|
||||
isSharedSession,
|
||||
isLoadingShared,
|
||||
shareUrl,
|
||||
shareUrlSize,
|
||||
shortShareUrl,
|
||||
isGeneratingShortUrl,
|
||||
shortUrlError,
|
||||
pendingSharedAnnotations,
|
||||
sharedGlobalAttachments,
|
||||
clearPendingSharedAnnotations,
|
||||
refreshShareUrl,
|
||||
generateShortUrl,
|
||||
importFromShareUrl,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"./types": "./types.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@plannotator/shared": "workspace:*",
|
||||
"@plannotator/web-highlighter": "^0.8.1",
|
||||
"diff": "^8.0.3",
|
||||
"highlight.js": "^11.11.1",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
|
||||
import { Annotation, AnnotationType, type ImageAttachment } from '../types';
|
||||
import { compress, decompress } from '@plannotator/shared/compress';
|
||||
|
||||
// Image in shareable format: plain string (old) or [path, name] tuple (new)
|
||||
type ShareableImage = string | [string, string];
|
||||
@@ -30,7 +31,7 @@ export interface SharePayload {
|
||||
/**
|
||||
* Convert ShareableImage[] to ImageAttachment[] (handles old plain-string format)
|
||||
*/
|
||||
function parseShareableImages(raw: ShareableImage[] | undefined): ImageAttachment[] | undefined {
|
||||
export function parseShareableImages(raw: ShareableImage[] | undefined): ImageAttachment[] | undefined {
|
||||
if (!raw?.length) return undefined;
|
||||
return raw.map(img => {
|
||||
if (typeof img === 'string') {
|
||||
@@ -45,56 +46,13 @@ function parseShareableImages(raw: ShareableImage[] | undefined): ImageAttachmen
|
||||
/**
|
||||
* Convert ImageAttachment[] to ShareableImage[] for compact serialization
|
||||
*/
|
||||
function toShareableImages(images: ImageAttachment[] | undefined): ShareableImage[] | undefined {
|
||||
export function toShareableImages(images: ImageAttachment[] | undefined): ShareableImage[] | undefined {
|
||||
if (!images?.length) return undefined;
|
||||
return images.map(img => [img.path, img.name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compress a SharePayload to a base64url string
|
||||
*/
|
||||
export async function compress(payload: SharePayload): Promise<string> {
|
||||
const json = JSON.stringify(payload);
|
||||
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);
|
||||
|
||||
// Convert to base64url (URL-safe base64)
|
||||
const base64 = btoa(String.fromCharCode(...compressed));
|
||||
return base64
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Decompress a base64url string back to SharePayload
|
||||
*/
|
||||
export async function decompress(b64: string): Promise<SharePayload> {
|
||||
// Restore standard base64
|
||||
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) as SharePayload;
|
||||
}
|
||||
// Re-export compress/decompress from shared package (single source of truth)
|
||||
export { compress, decompress };
|
||||
|
||||
/**
|
||||
* Convert full Annotation objects to minimal shareable format
|
||||
@@ -187,7 +145,7 @@ export async function generateShareUrl(
|
||||
markdown: string,
|
||||
annotations: Annotation[],
|
||||
globalAttachments?: ImageAttachment[],
|
||||
baseUrl: string = 'https://share.plannotator.ai'
|
||||
baseUrl: string = DEFAULT_SHARE_BASE
|
||||
): Promise<string> {
|
||||
const payload: SharePayload = {
|
||||
p: markdown,
|
||||
@@ -228,3 +186,94 @@ export function formatUrlSize(url: string): string {
|
||||
}
|
||||
return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Short URL support (paste-service backed)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const DEFAULT_PASTE_API = 'https://paste.plannotator.ai';
|
||||
const DEFAULT_SHARE_BASE = 'https://share.plannotator.ai';
|
||||
|
||||
/**
|
||||
* Create a short share URL by posting compressed plan data to the paste service.
|
||||
*
|
||||
* Returns `{ shortUrl, id }` on success, or `null` when the paste service is
|
||||
* unavailable (e.g. self-hosted environments without a paste backend). Callers
|
||||
* should fall back to the hash-based URL in that case.
|
||||
*
|
||||
* The request has a 5-second timeout so UI responsiveness is not affected.
|
||||
*/
|
||||
export async function createShortShareUrl(
|
||||
markdown: string,
|
||||
annotations: Annotation[],
|
||||
globalAttachments?: ImageAttachment[],
|
||||
options?: {
|
||||
/** Override the paste API base URL (default: https://paste.plannotator.ai) */
|
||||
pasteApiUrl?: string;
|
||||
/** Override the share site base URL used in the returned short link */
|
||||
shareBaseUrl?: string;
|
||||
}
|
||||
): Promise<{ shortUrl: string; id: string } | null> {
|
||||
const pasteApi = options?.pasteApiUrl ?? DEFAULT_PASTE_API;
|
||||
const shareBase = options?.shareBaseUrl ?? DEFAULT_SHARE_BASE;
|
||||
|
||||
try {
|
||||
const payload: SharePayload = {
|
||||
p: markdown,
|
||||
a: toShareable(annotations),
|
||||
g: globalAttachments?.length ? toShareableImages(globalAttachments) : undefined,
|
||||
};
|
||||
|
||||
const compressed = await compress(payload);
|
||||
|
||||
const response = await fetch(`${pasteApi}/api/paste`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ data: compressed }),
|
||||
signal: AbortSignal.timeout(5_000),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(`[sharing] Paste service returned ${response.status}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = (await response.json()) as { id: string };
|
||||
const shortUrl = `${shareBase}/p/${result.id}`;
|
||||
|
||||
return { shortUrl, id: result.id };
|
||||
} catch (e) {
|
||||
// Service unavailable — expected for self-hosted setups without a paste backend.
|
||||
// The caller is responsible for falling back to hash-based sharing silently.
|
||||
console.debug('[sharing] Short URL service unavailable, using hash-based sharing:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load plan data from a paste service using the paste ID embedded in a short URL.
|
||||
*
|
||||
* Fetches the compressed payload from `<pasteApiUrl>/api/paste/<pasteId>` and
|
||||
* decompresses it into a `SharePayload`. Returns `null` on any failure.
|
||||
*/
|
||||
export async function loadFromPasteId(
|
||||
pasteId: string,
|
||||
pasteApiUrl: string = DEFAULT_PASTE_API
|
||||
): Promise<SharePayload | null> {
|
||||
try {
|
||||
const response = await fetch(`${pasteApiUrl}/api/paste/${pasteId}`, {
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(`[sharing] Paste fetch returned ${response.status} for id ${pasteId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = (await response.json()) as { data: string };
|
||||
return await decompress(result.data);
|
||||
} catch (e) {
|
||||
console.warn('[sharing] Failed to load from paste ID:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user