Compare commits

..

3 Commits

Author SHA1 Message Date
yannickschuchmann
a7b81d5791 Add README documentation and refactor test suite
Replace minimal README with full project docs (setup, usage, architecture).
Remove trivial tests and add meaningful edge case coverage.
2026-03-08 20:39:15 +00:00
yannickschuchmann
b936095286 Add implementation 2026-03-08 18:01:28 +00:00
yannickschuchmann
cc5394c3db Add CLAUDE.md, plan and tasklist 2026-03-08 13:49:28 +00:00
72 changed files with 68477 additions and 11226 deletions

23
.gitignore vendored
View File

@@ -1,24 +1 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

55
CLAUDE.md Normal file
View File

@@ -0,0 +1,55 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project
P2P Poll App — a peer-to-peer polling application for the Evocracy democratic coding research experiment (https://demcode.evocracy.org/).
### Goal
Build a simple P2P polling app where users can add poll options and vote. Data exchange works peer-to-peer between clients (no central server for data), though a signaling/sync server for connection establishment is acceptable.
### Research Context
- **Phase 1 (2 weeks):** Submit individual code proposal
- **Phase 2:** Groups of 3 merge solutions into a working prototype
- **Phase 3:** Representatives iteratively merge until one final solution remains
- Code must be clean, explainable, and easy to merge with others' work
- Final code published open-source under MIT license
- Language: JavaScript/TypeScript
## Tech Stack
- **Runtime:** Bun
- **Framework:** Waku (minimal React framework with RSC support)
- **Styling:** Tailwind CSS v4
- **P2P/Data sync:** Automerge with automerge-repo (CRDT-based)
- **Networking:** `automerge-repo-network-websocket` (WebSocketClientAdapter) + `automerge-repo-network-broadcastchannel` (cross-tab sync)
- **Storage:** `automerge-repo-storage-indexeddb` (client-side persistence)
**Waku documentation:** https://waku.gg/llms.txt
## Commands
```bash
bun install # Install dependencies
bun run dev # Start dev server
bun run build # Production build
bun test # Run tests (Bun's built-in test runner)
```
## Architecture
- Waku pages router: pages live in `src/pages/`, layouts in `_layout.tsx`
- Client components use `'use client'` directive
- Automerge Repo is initialized in a client-side provider component wrapping the app
- The shared CRDT document holds the poll state (title, options, votes)
- Peers sync via a lightweight WebSocket sync server (can use `automerge-repo` sync server or the public `wss://sync.automerge.org`)
- `BroadcastChannelNetworkAdapter` enables cross-tab sync
- `useDocument` hook from `@automerge/automerge-repo-react-hooks` for reactive document access
- Every Waku page/layout must export `getConfig` specifying render mode (`'static'` or `'dynamic'`)
## Key Design Principles
- Keep it simple and merge-friendly for Phase 2 group work
- Minimal file count, clear separation of concerns
- No over-engineering — this is a research experiment, not production software

174
README.md
View File

@@ -1,129 +1,91 @@
# 🗳️ P2P Verified Polling App
# P2P Poll App
A decentralized, real-time polling application built with **Nuxt 3**, **Yjs**, and **WebRTC**. This app allows users to create and participate in polls where every vote is cryptographically signed and verified peer-to-peer, ensuring data integrity without a central authority "owning" the results.
A peer-to-peer polling application where users can create polls, add options, and vote — all without a central server. Built for the [Evocracy democratic coding research experiment](https://demcode.evocracy.org/).
---
## How It Works
## 🌟 Key Features
Data is synchronized directly between peers using [Automerge](https://automerge.org/) CRDTs (Conflict-free Replicated Data Types). There is no backend database — every client holds a full copy of the poll document and changes merge automatically, even when made offline or concurrently.
* **Serverless Real-time Sync:** Uses **Yjs** (CRDTs) and **WebRTC** to sync poll data directly between browsers. No database is required for live updates.
* **Persistence with Nitro:** While the logic is P2P, the **Nuxt/Nitro** backend provides a "Snapshot" service to ensure polls persist even after all peers go offline.
* **Cryptographic Integrity:** Every vote is signed using **RSA-PSS (Web Crypto API)**. Each user has a unique private key (stored locally via `.pem` files) to ensure votes cannot be forged or tampered with.
* **Chained Verification:** Implements a "History-Signing" logic where each new vote signs the entire preceding state of the poll, creating a verifiable chain of trust.
* **Privacy First:** Users identify via UUIDs and Public/Private key pairs rather than traditional accounts.
**Sync layers:**
---
- **WebSocket** (`wss://sync.automerge.org`) — cross-device sync via a public relay
- **BroadcastChannel** — instant cross-tab sync within the same browser
- **IndexedDB** — local persistence across page reloads and offline use
## ⚙️ How It Works
## Requirements
### 1. Identity Creation
When a new user is created, the system generates a unique **UUID (User ID)** and an **RSA Key Pair**. The user is prompted to save their **Private Key** as a `.pem` file, named after their User ID (e.g., `550e8400-e29b.pem`). This file acts as their "Passport"—it is never uploaded to the server and must be kept secure by the user.
- [Bun](https://bun.sh/) (standalone runtime, no Node.js needed)
### 2. Authentication
Upon returning to the app, users load their local `.pem` file. The application extracts the Private Key for signing and the UUID for identification. No passwords or central servers are involved in this local-first login process.
### 3. Joining a Poll
When a user joins a poll, the app fetches the latest binary snapshot from the server to populate a local **Y.Doc**. This ensures the user sees the current state immediately, even before connecting to other peers.
### 4. The P2P Mesh
The app establishes connections to other active voters via a WebRTC signaling server. Any changes made to the poll (adding options or voting) are broadcasted instantly to all peers using Conflict-free Replicated Data Types (CRDTs) to prevent sync conflicts.
### 5. Casting a Signed Vote
To ensure security, the voting process follows a strict cryptographic chain:
* The app captures the current list of votes.
* It appends the new vote data (User ID + Timestamp).
* It signs the **entire array** (the previous history + the new vote) using the user's RSA private key.
* The signed update is merged into the shared Yjs Map and broadcasted.
### 6. Distributed Verification
Whenever a peer receives a new update, they fetch the voter's **Public Key** from the API. They then verify that the signature matches the current state of the poll history. If a signature is invalid or the history has been tampered with, the vote is rejected by the peer's local state.
---
## 🛠️ Tech Stack
* **Framework:** [Nuxt 3](https://nuxt.com/) (Vue 3 + TypeScript)
* **Conflict-Free Replicated Data Types (CRDT):** [Yjs](https://yjs.dev/)
* **P2P Transport:** `y-webrtc`
* **Security:** Web Crypto API (SubtleCrypto)
* **Backend/Storage:** Nitro (Nuxt's server engine) with filesystem storage drivers
# AI Disclaimer
This App was developed with the assistance of AI.
# Nuxt Minimal Starter
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
## Getting Started
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
bun install # Install dependencies
bun run dev # Start dev server (http://localhost:3000)
```
## Development Server
## Usage
Start the development server on `http://localhost:3000`:
1. **Create a poll** — Enter a title on the home page and click "Create"
2. **Share it** — Copy the shareable link and send it to others
3. **Vote** — Click an option to vote; click again to unvote
4. **Add options** — Anyone with the link can add new poll options
Each browser gets a stable peer ID (stored in localStorage) so votes are tracked per-device and double-voting is prevented.
## Tech Stack
| Layer | Technology |
|---|---|
| Runtime | [Bun](https://bun.sh/) |
| Framework | [Waku](https://waku.gg/) (React Server Components) |
| Styling | Tailwind CSS v4 |
| P2P sync | Automerge + automerge-repo |
| Storage | IndexedDB (client-side) |
## Project Structure
```
src/
├── pages/ # Waku page router
│ ├── _root.tsx # HTML document shell
│ ├── _layout.tsx # Root layout + Providers
│ ├── index.tsx # Home page (create/join polls)
│ └── poll/[id].tsx # Poll view page
├── components/
│ ├── Providers.tsx # Automerge Repo initialization
│ ├── HomeClient.tsx # Create/join poll UI
│ ├── PollPageClient.tsx # Poll ID validation
│ ├── PollView.tsx # Poll display, voting, options
│ └── ConnectionStatus.tsx # P2P connection indicator
├── lib/
│ ├── types.ts # Poll & PollOption interfaces
│ ├── repo.ts # Automerge Repo singleton
│ ├── poll.ts # Pure poll mutation functions
│ ├── peer.ts # Peer ID management
│ └── __tests__/ # Unit tests
└── styles/
└── global.css # Tailwind CSS
```
## Testing
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
bun test
```
## Production
Covers poll creation, voting/unvoting, double-vote prevention, option management, and peer ID persistence.
Build the application for production:
## Architecture Notes
```bash
# npm
npm run build
- **Pure business logic** — Poll mutations in `src/lib/poll.ts` are pure functions, used inside Automerge's `changeDoc()` for CRDT-safe updates
- **No server state** — The WebSocket relay only forwards sync messages; it never stores or processes poll data
- **Offline-first** — The app works fully offline; changes sync when connectivity resumes
- **Conflict-free** — Concurrent edits (e.g., two users voting at the same time) merge automatically without conflicts
# pnpm
pnpm build
## Built With
# yarn
yarn build
This project was built in collaboration with [Claude Code](https://claude.ai/code), Anthropic's agentic coding tool.
# bun
bun run build
```
## License
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
MIT

View File

@@ -1,175 +0,0 @@
<style>
/* Basic styling to make it look clean */
body {
font-family: system-ui, -apple-system, sans-serif;
background-color: #f4f4f9;
color: #333;
margin: 0;
display: flex;
justify-content: center;
padding: 2rem;
}
header {
margin-bottom: 2rem;
text-align: center;
}
h1 { margin: 0 0 0.5rem 0; }
input {
flex-grow: 1;
padding: 0.75rem;
border: 1px solid #ccc;
border-radius: 6px;
font-size: 1rem;
}
button,
.button {
background: #3b82f6;
color: white;
border: none;
padding: 0.75rem 1rem;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
transition: background 0.2s;
}
button:hover,
.button:hover { background: #2563eb; }
.status {
font-size: 0.85rem;
color: #666;
}
.status .connected { color: #10b981; font-weight: bold; }
.connectionFailed { color: #FF2525; font-weight: bold; }
.poll-container {
background: white;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
width: 100%;
max-width: 500px;
}
.back-btn {
margin-left: 1rem;
padding: 0.2rem 0.5rem;
font-size: 0.7rem;
background: #64748b;
}
/* Hide the actual file input */
input[type="file"] {
display: none;
}
</style>
<template>
<div class="poll-container">
<header>
<h1 @click="activePollId = null" style="cursor:pointer">P2P Polling App 🗳</h1>
<div class="status">
<button v-if="activePollId" @click="activePollId = null" class="back-btn"> Back To List</button>
<span :class="{ 'connected': isConnected }">
{{ isConnected ? 'Synced' : 'Waiting for other Peers...' }}
</span>
<span> | Peers online: {{ connectedPeers }}</span>
<h2 v-if="connectionAttempFailed" class="connectionFailed"> Connection to Signaling Server Failed!</h2>
<div v-if="user===null" style="margin-top: 10px;">
<button @click="createUser">Create New User</button>
Or
<label title="Select Key File">
<span class="button">Login</span>
<input
type="file"
accept=".pem"
@change="loadUser"
/>
</label>
</div>
</div>
</header>
<main>
<PollList v-if="!activePollId" :userid="user?.userid" @select-poll="selectPoll" />
<Poll v-else :activePollId="activePollId" :userid="user?.userid" :poll-data="pollData" :addOption="addOption" :vote="vote"/>
</main>
</div>
</template>
<script setup lang="ts">
import { v4 as uuidv4 } from 'uuid';
const activePollId = ref<string | null>(null);
const user = shallowRef<UserData | null>(null);
const { pollData, isConnected, connectionAttempFailed, connectedPeers, addOption, vote } = usePoll(activePollId,user);
const selectPoll = (id: string) => {
activePollId.value = id;
};
const createUser = async () => {
try {
const keypair : CryptoKeyPair = await generateUserKeyPair();
console.log('keypair:', keypair);
const uuid = uuidv4();
user.value = {
userid: uuid,
private_key: keypair.privateKey,
public_key: keypair.publicKey,
};
const prvKeyString = await exportPrivateKey(keypair.privateKey);
await savePrivateKeyToFile(prvKeyString,uuid+".pem")
const pubKeyString = await exportPublicKey(keypair.publicKey);
await $fetch(`/api/users/${uuid}`, {
method: 'POST',
body: { public_key: pubKeyString }
});
} catch (err) {
user.value = null
console.error("Failed to create new User!", err);
}
};
const loadUser = async (event: Event) => {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (file) {
try {
const content = await file.text();
console.log("File loaded: ");
if (file.name && content) {
try {
const uuid = file.name.replace(".pem", "");
// Standardize the string for the importer
const pkBase64 = content.replace(/-----BEGIN PRIVATE KEY-----|-----END PRIVATE KEY-----/g, "").replace(/\s+/g, "");
const key = await stringToCryptoKey(pkBase64, "private");
user.value = {
userid: uuid,
private_key: key,
public_key: undefined, // Note: You might need to import a pub key too!
};
console.log("Login successful for:", uuid);
} catch (err) {
console.error("Crypto Import Error:", err);
alert("The file content is not a valid Private Key.");
}
}
} catch (e) {
console.error("Failed to read file", e);
}
}
};
</script>

View File

@@ -1,83 +0,0 @@
<style scoped>
.poll-list {
list-style: none;
padding: 0;
margin: 0;
}
.poll-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
margin-bottom: 0.5rem;
}
.poll-title {
font-size: 1.1rem;
color: #3b82f6;
text-transform: uppercase;
letter-spacing: 1px;
}
.add-option-form {
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
}
.option-name { font-weight: 500; }
.vote-section { display: flex; align-items: center; gap: 1rem; }
.vote-count { font-size: 0.9rem; color: #475569; }
.vote-btn { padding: 0.4rem 0.8rem; background: #10b981; }
.vote-btn:hover { background: #059669; }
.vote-btn:disabled,
.vote-btn[disabled] { background: #888888; }
.vote-btn:disabled:hover,
.vote-btn[disabled]:hover { background: #AAAAAA; }
</style>
<template>
<div>
<h2 class="poll-title">Poll: {{ activePollId }}</h2>
<p v-if="Object.keys(pollData).length==0">Note: Add at least one Option to save the Poll.</p>
<form @submit.prevent="handleAddNewOption" class="add-option-form" v-if="userid">
<input v-model="newOption" placeholder="Enter a new poll option..." required />
<button type="submit">Add Option</button>
</form>
<ul class="poll-list">
<li v-for="(votes, optionName) in pollData" :key="optionName" class="poll-item">
<span class="option-name">{{ optionName }}</span>
<div class="vote-section">
<span class="vote-count">{{ votes.length }} {{ votes.length === 1 ? 'vote' : 'votes' }}</span>
<button @click="vote(String(optionName))" class="vote-btn" :disabled="userid==undefined || voted(votes)">+1</button>
</div>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import type { PollProps, SignedData, VoteData } from '@/utils/types'
const props = defineProps<PollProps>()
const newOption = ref('');
const handleAddNewOption = () => {
props.addOption(newOption.value);
newOption.value = '';
};
const voted = (votes: SignedData<VoteData>[]) => {
for(let vote of votes){
if(vote.data.userid == props.userid){
return true;
}
}
return false;
}
</script>

View File

@@ -1,64 +0,0 @@
<style scoped>
.poll-list { margin-top: 1rem; }
.empty-state { text-align: center; color: #94a3b8; font-style: italic; }
.create-poll { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; }
.poll-links { list-style: none; padding: 0; }
.poll-link-btn {
width: 100%;
text-align: left;
background: #f1f5f9;
color: #1e293b;
margin-bottom: 0.5rem;
display: flex;
justify-content: space-between;
}
.poll-link-btn:hover { background: #e2e8f0; }
</style>
<template>
<div class="poll-list">
<h3>Available Polls</h3>
<ul v-if="polls && polls.length > 0" class="poll-links">
<li v-for="id in polls" :key="id">
<button class="poll-link-btn" @click="$emit('select-poll', id)">
{{ id }} <span></span>
</button>
</li>
</ul>
<p v-else class="empty-state">No polls found. Create the first one!</p>
<div class="create-poll" v-if="userid !== undefined">
<input
v-model="newPollId"
placeholder="Enter new poll name..."
@keyup.enter="createPoll"
/>
<button @click="createPoll">Create & Join</button>
</div>
</div>
</template>
<script setup lang="ts">
import type { PollListProps } from '@/utils/types'
const props = defineProps<PollListProps>()
const newPollId = ref('');
const polls = ref<string[]>([]);
// Fetch existing polls on mount
const fetchPolls = async () => {
const data = await $fetch<{ polls: string[] }>('/api/polls');
polls.value = data.polls;
};
const createPoll = () => {
const id = newPollId.value.trim().toLowerCase().replace(/\s+/g, '-');
if (id) {
// In a real app, you might want to POST to create it first,
// but here we just navigate to it and let usePoll handle the save.
emit('select-poll', id);
}
};
const emit = defineEmits(['select-poll']);
onMounted(fetchPolls);
</script>

View File

@@ -1,131 +0,0 @@
// composables/usePoll.ts
import { ref, watch, onUnmounted } from 'vue';
import * as Y from 'yjs';
export const usePoll = (pollId: Ref<string | null>, user: Ref<UserData | null>) => {
const pollData = ref<PollData>({});
const isConnected = ref(false);
const connectionAttempFailed = ref(false);
const connectedPeers = ref(1);
let ydoc: Y.Doc | null = null;
let provider: any = null;
let yMap: Y.Map<SignedData<VoteData>[]> | null = null;
const cleanup = () => {
if (provider) provider.disconnect();
if (ydoc) ydoc.destroy();
isConnected.value = false;
pollData.value = {};
};
const initPoll = async (id: string) => {
cleanup(); // Clear previous session
ydoc = new Y.Doc();
// 1. Fetch Snapshot from Nuxt API
try {
const response = await $fetch<{ update: number[] | null }>(`/api/polls/${id}`).catch((e) => {
console.error("Failed to get poll: " + id,e)
});
//trust the server without verification.
if (response?.update) {
Y.applyUpdate(ydoc, new Uint8Array(response.update));
}
} catch (err) {
console.error('Persistence fetch failed', err);
}
yMap = ydoc.getMap<SignedData<VoteData>[]>('shared-poll');
// 2. Local State Sync
yMap.observe(async () => {
await performUpdateAndVerify();
saveStateToServer(id);
});
await performUpdateAndVerify();
// 3. P2P Connection
const { WebrtcProvider } = await import('y-webrtc');
provider = new WebrtcProvider(`nuxt-p2p-${id}`, ydoc, {
signaling: ["ws://localhost:4444", "ws://lynxpi.ddns.net:4444"]
});
provider.on('synced', (arg: {synced: boolean}) => {
isConnected.value = arg.synced;
console.log('Connection synced:', arg.synced) // "connected" or "disconnected"
});
provider.on('status', (event: { connected: boolean }) => {
console.log('Connection status:', event.connected) // "connected" or "disconnected"
})
provider.on('peers', (data: any) => {
connectedPeers.value = data.webrtcPeers.length + 1
});
};
const saveStateToServer = async (id: string) => {
if (!ydoc) return;
const stateUpdate = Y.encodeStateAsUpdate(ydoc);
await $fetch(`/api/polls/${id}`, {
method: 'POST',
body: { update: Array.from(stateUpdate) }
}).catch((e) => {
console.error("Failed to update poll",e)
});
};
// Watch for ID changes (e.g., user clicks a link or goes back)
watch(pollId, (newId) => {
if (newId && import.meta.client) {
initPoll(newId);
} else {
cleanup();
}
}, { immediate: true });
onUnmounted(cleanup);
const addOption = (optionName: string) => {
if (yMap && !yMap.has(optionName)) yMap.set(optionName, []);
};
const performUpdateAndVerify = async () => {
const pollDataUpdate = yMap!.toJSON();
console.log("Poll Data Update: ", pollDataUpdate)
for(var option in pollDataUpdate){
console.log("verifying votes for option: " + option);
const votes = pollDataUpdate[option] || [];
const verified = await verifyAllVotesForOption(votes);
if(!verified){
console.error("Failed to verify option: "+option)
return;
}
}
console.log("All options verified! :)")
pollData.value = pollDataUpdate
}
const vote = async (optionName: string) => {
const currentUser = user.value;
if (currentUser != undefined && yMap?.has(optionName)) {
const voteData = [...(yMap.get(optionName) || [])];
if(voteData != undefined && currentUser.private_key){
var unsignedVoteData : VoteData = {
userid: currentUser.userid,
timestamp: new Date().toISOString()
}
var newVote : SignedData<VoteData> = {
data: unsignedVoteData,
signature: "",
}
voteData?.push(newVote)
const signature = await signVote(voteData,currentUser.private_key);
newVote.signature=signature
yMap?.set(optionName, voteData);
}
}
};
return { pollData, isConnected, connectionAttempFailed, connectedPeers, addOption, vote };
};

View File

@@ -1,2 +0,0 @@
export const user = (user: Ref<UserData | null>) => {
}

View File

@@ -1,187 +0,0 @@
// utils/crypto.ts
export const generateUserKeyPair = async () => {
return await window.crypto.subtle.generateKey(
{
name: "RSASSA-PKCS1-v1_5",
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]), // 65537
hash: "SHA-256",
},
true, // extractable
["sign", "verify"]
);
};
export const signVote = async (data: any, privateKey: CryptoKey) => {
const encoder = new TextEncoder();
const encodedData = encoder.encode(JSON.stringify(data));
const signature = await window.crypto.subtle.sign(
"RSASSA-PKCS1-v1_5",
privateKey,
encodedData
);
// Convert to Base64 or Hex to store in Yjs easily
return btoa(String.fromCharCode(...new Uint8Array(signature)));
};
export const verifyVote = async (data: any, signatureStr: string, publicKey: CryptoKey) => {
const encoder = new TextEncoder();
const encodedData = encoder.encode(JSON.stringify(data));
// Convert Base64 back to Uint8Array
const signature = Uint8Array.from(atob(signatureStr), c => c.charCodeAt(0));
return await window.crypto.subtle.verify(
"RSASSA-PKCS1-v1_5",
publicKey,
signature,
encodedData
);
};
/**
* Verifies a specific vote within an array of votes by
* reconstructing the "signed state" at that point in time.
*/
export const verifyChainedVote = async (
voteData: SignedData<VoteData>[],
index: number
) => {
const voteToVerify = voteData[index];
console.log("Verifying vote: " + voteToVerify)
if(voteToVerify) {
// 1. Reconstruct the exact data state the user signed
// We need the array exactly as it was when they pushed their vote
const historicalState = voteData.slice(0, index + 1).map((v, i) => {
if (i === index) {
// For the current vote, the signature must be empty string
// because it wasn't signed yet when passed to signVote
return { ...v, signature: "" };
}
return v;
});
try {
// 2. Fetch public key
const response = await $fetch<{ public_key: string }>(`/api/users/${voteToVerify.data.userid}`);
console.log("Got key: ",response)
const pubKey = await stringToCryptoKey(response.public_key, 'public');
console.log("Using pubKey to verify Vote.")
// 3. Verify: Does this historicalState match the signature?
return await verifyVote(historicalState, voteToVerify.signature, pubKey);
} catch (err) {
console.error("Verification failed")
console.error(err);
return false;
}
}
console.error("Vote is undefined or null");
return false;
};
export const verifyAllVotesForOption = async (votes: SignedData<VoteData>[]) => {
console.log("verifying votes for option ",votes);
for (let i = votes.length-1; i >= 0 ; i--) {
const isValid = await verifyChainedVote(votes, i);
if(!isValid){
console.error("Error! Invalid Vote at: " + i,votes)
return false;
}
}
return true;
};
// Helper to convert ArrayBuffer to Base64 string
const bufferToBase64 = (buf: ArrayBuffer) =>
window.btoa(String.fromCharCode(...new Uint8Array(buf)));
export const exportPublicKey = async (key: CryptoKey) => {
// Export Public Key
const exportedPublic = await window.crypto.subtle.exportKey("spki", key);
const publicKeyString = bufferToBase64(exportedPublic);
return publicKeyString;
};
export const exportPrivateKey = async (key: CryptoKey) => {
// Export Private Key
const exportedPrivate = await window.crypto.subtle.exportKey("pkcs8", key);
const privateKeyString = bufferToBase64(exportedPrivate);
return privateKeyString;
};
/**
* Converts a Base64 string back into a usable CryptoKey object
* @param keyStr The Base64 string (without PEM headers)
* @param type 'public' or 'private'
*/
export const stringToCryptoKey = async (keyStr: string, type: 'public' | 'private'): Promise<CryptoKey> => {
// 1. Convert Base64 string to a Uint8Array (binary)
const binaryString = window.atob(keyStr);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
// 2. Identify the format based on the key type
// Public keys usually use 'spki', Private keys use 'pkcs8'
const format = type === 'public' ? 'spki' : 'pkcs8';
const usages: KeyUsage[] = type === 'public' ? ['verify'] : ['sign'];
// 3. Import the key
return await window.crypto.subtle.importKey(
format,
bytes.buffer,
{
name: "RSASSA-PKCS1-v1_5",
hash: "SHA-256",
},
true, // extractable (set to false if you want to lock it in memory)
usages
);
};
export const savePrivateKeyToFile = (privateKeyStr: string, filename: string) => {
// Optional: Wrap in PEM headers for standard formatting
const pemHeader = "-----BEGIN PRIVATE KEY-----\n";
const pemFooter = "\n-----END PRIVATE KEY-----";
const fileContent = pemHeader + privateKeyStr + pemFooter;
const blob = new Blob([fileContent], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
// Cleanup
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
export const loadPrivateKeyFromFile = async (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target?.result as string;
// Clean up the string by removing PEM headers and newlines
const cleanKey = content
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replace(/\s+/g, ""); // Removes all whitespace/newlines
resolve(cleanKey);
};
reader.onerror = () => reject("Error reading file");
reader.readAsText(file);
});
};

View File

@@ -1,36 +0,0 @@
export interface PollProps {
userid: string | undefined,
activePollId: string,
pollData: PollData,
addOption: (name: string) => void,
vote: (optionName: string) => void
}
export interface PollListProps {
userid: string | undefined,
}
export interface PollData extends Record<string, SignedData<VoteData>[]> {
}
export interface SignedData<T> {
data: T,
signature: string
}
export interface VoteData {
userid: string,
timestamp: string
}
export interface OptionData {
userid: string,
timestamp: string,
optionName: string
}
export interface UserData {
userid: string,
private_key: CryptoKey | undefined,
public_key: CryptoKey | undefined
}

588
bun.lock Normal file
View File

@@ -0,0 +1,588 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "p2p-poll-app",
"dependencies": {
"@automerge/automerge": "^3.2.4",
"@automerge/automerge-repo": "^2.5.3",
"@automerge/automerge-repo-network-broadcastchannel": "^2.5.3",
"@automerge/automerge-repo-network-websocket": "^2.5.3",
"@automerge/automerge-repo-react-hooks": "^2.5.3",
"@automerge/automerge-repo-storage-indexeddb": "^2.5.3",
"@tailwindcss/vite": "^4.2.1",
"react": "~19.2.4",
"react-dom": "~19.2.4",
"react-server-dom-webpack": "~19.2.4",
"tailwindcss": "^4.2.1",
"vite-plugin-top-level-await": "^1.6.0",
"vite-plugin-wasm": "^3.5.0",
"waku": "1.0.0-alpha.5",
},
"devDependencies": {
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"typescript": "^5.9.3",
},
},
},
"packages": {
"@automerge/automerge": ["@automerge/automerge@3.2.4", "", {}, "sha512-/IAShHSxme5d4ZK0Vs4A0P+tGaR/bSz6KtIJSCIPfwilCxsIqfRHAoNjmsXip6TGouadmbuw3WfLop6cal8pPQ=="],
"@automerge/automerge-repo": ["@automerge/automerge-repo@2.5.3", "", { "dependencies": { "@automerge/automerge": "2.2.8 - 3", "bs58check": "^3.0.1", "cbor-x": "^1.3.0", "debug": "^4.3.4", "eventemitter3": "^5.0.1", "fast-sha256": "^1.3.0", "uuid": "^9.0.0", "xstate": "^5.9.1" } }, "sha512-5ppzsUlzCs6PY+GeFlquaYLnVnYPz/hWzedep37zjb15tqZju1ukPPkBzT7KGEhEAnA99l4vfhfqSVC+1GktJg=="],
"@automerge/automerge-repo-network-broadcastchannel": ["@automerge/automerge-repo-network-broadcastchannel@2.5.3", "", { "dependencies": { "@automerge/automerge-repo": "2.5.3" } }, "sha512-V/JpOsOkPPqe+hJs7Zs8oSKO3dp2/4Qd6D+x3D1rda7OhyOmlJ1HnMLghycnhHDQG8lCP9ex3AH6MYe1ZqgCKw=="],
"@automerge/automerge-repo-network-websocket": ["@automerge/automerge-repo-network-websocket@2.5.3", "", { "dependencies": { "@automerge/automerge-repo": "2.5.3", "cbor-x": "^1.3.0", "debug": "^4.3.4", "eventemitter3": "^5.0.1", "isomorphic-ws": "^5.0.0", "ws": "^8.7.0" } }, "sha512-p6K1YLo34cyGxJ6oMWqeBbWX9rGvEPGyPdX2eq8S/iWNDjYf8lbjKSOhOD8DX87uAMZN/y1XOY/RckwS8lPJbQ=="],
"@automerge/automerge-repo-react-hooks": ["@automerge/automerge-repo-react-hooks@2.5.3", "", { "dependencies": { "@automerge/automerge": "2.2.8 - 3", "@automerge/automerge-repo": "2.5.3", "eventemitter3": "^5.0.1", "react-usestateref": "^1.0.8" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-573jhVX1LMiBf8zV0NaYT2PgsEuqHNz0RnKSzIp49+g+swgb+KtRhpdiF38tbQoDnkFsKpYGR6P+qoAVW8veaw=="],
"@automerge/automerge-repo-storage-indexeddb": ["@automerge/automerge-repo-storage-indexeddb@2.5.3", "", { "dependencies": { "@automerge/automerge-repo": "2.5.3" } }, "sha512-i4tSzY7sC8ZqurHm4Sd9H1lFL2Don9v9yaSoq0FXi4T+4Wg8eus/mtMlUCmS5XwYQL8GX+epbplrlBaBe+fYiw=="],
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
"@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
"@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
"@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
"@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=="],
"@cbor-extract/cbor-extract-darwin-arm64": ["@cbor-extract/cbor-extract-darwin-arm64@2.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w=="],
"@cbor-extract/cbor-extract-darwin-x64": ["@cbor-extract/cbor-extract-darwin-x64@2.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w=="],
"@cbor-extract/cbor-extract-linux-arm": ["@cbor-extract/cbor-extract-linux-arm@2.2.0", "", { "os": "linux", "cpu": "arm" }, "sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q=="],
"@cbor-extract/cbor-extract-linux-arm64": ["@cbor-extract/cbor-extract-linux-arm64@2.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ=="],
"@cbor-extract/cbor-extract-linux-x64": ["@cbor-extract/cbor-extract-linux-x64@2.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw=="],
"@cbor-extract/cbor-extract-win32-x64": ["@cbor-extract/cbor-extract-win32-x64@2.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
"@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
"@rollup/plugin-virtual": ["@rollup/plugin-virtual@3.0.2", "", { "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
"@swc/core": ["@swc/core@1.15.18", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.15.18", "@swc/core-darwin-x64": "1.15.18", "@swc/core-linux-arm-gnueabihf": "1.15.18", "@swc/core-linux-arm64-gnu": "1.15.18", "@swc/core-linux-arm64-musl": "1.15.18", "@swc/core-linux-x64-gnu": "1.15.18", "@swc/core-linux-x64-musl": "1.15.18", "@swc/core-win32-arm64-msvc": "1.15.18", "@swc/core-win32-ia32-msvc": "1.15.18", "@swc/core-win32-x64-msvc": "1.15.18" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-z87aF9GphWp//fnkRsqvtY+inMVPgYW3zSlXH1kJFvRT5H/wiAn+G32qW5l3oEk63KSF1x3Ov0BfHCObAmT8RA=="],
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.15.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+mIv7uBuSaywN3C9LNuWaX1jJJ3SKfiJuE6Lr3bd+/1Iv8oMU7oLBjYMluX1UrEPzwN2qCdY6Io0yVicABoCwQ=="],
"@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.15.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-wZle0eaQhnzxWX5V/2kEOI6Z9vl/lTFEC6V4EWcn+5pDjhemCpQv9e/TDJ0GIoiClX8EDWRvuZwh+Z3dhL1NAg=="],
"@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.15.18", "", { "os": "linux", "cpu": "arm" }, "sha512-ao61HGXVqrJFHAcPtF4/DegmwEkVCo4HApnotLU8ognfmU8x589z7+tcf3hU+qBiU1WOXV5fQX6W9Nzs6hjxDw=="],
"@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.15.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-3xnctOBLIq3kj8PxOCgPrGjBLP/kNOddr6f5gukYt/1IZxsITQaU9TDyjeX6jG+FiCIHjCuWuffsyQDL5Ew1bg=="],
"@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.15.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-0a+Lix+FSSHBSBOA0XznCcHo5/1nA6oLLjcnocvzXeqtdjnPb+SvchItHI+lfeiuj1sClYPDvPMLSLyXFaiIKw=="],
"@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.15.18", "", { "os": "linux", "cpu": "x64" }, "sha512-wG9J8vReUlpaHz4KOD/5UE1AUgirimU4UFT9oZmupUDEofxJKYb1mTA/DrMj0s78bkBiNI+7Fo2EgPuvOJfuAA=="],
"@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.15.18", "", { "os": "linux", "cpu": "x64" }, "sha512-4nwbVvCphKzicwNWRmvD5iBaZj8JYsRGa4xOxJmOyHlMDpsvvJ2OR2cODlvWyGFH6BYL1MfIAK3qph3hp0Az6g=="],
"@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.15.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-zk0RYO+LjiBCat2RTMHzAWaMky0cra9loH4oRrLKLLNuL+jarxKLFDA8xTZWEkCPLjUTwlRN7d28eDLLMgtUcQ=="],
"@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.15.18", "", { "os": "win32", "cpu": "ia32" }, "sha512-yVuTrZ0RccD5+PEkpcLOBAuPbYBXS6rslENvIXfvJGXSdX5QGi1ehC4BjAMl5FkKLiam4kJECUI0l7Hq7T1vwg=="],
"@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.15.18", "", { "os": "win32", "cpu": "x64" }, "sha512-7NRmE4hmUQNCbYU3Hn9Tz57mK9Qq4c97ZS+YlamlK6qG9Fb5g/BB3gPDe0iLlJkns/sYv2VWSkm8c3NmbEGjbg=="],
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
"@swc/types": ["@swc/types@0.1.25", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g=="],
"@swc/wasm": ["@swc/wasm@1.15.18", "", {}, "sha512-zeSORFArxqUwfVMTRHu8AN9k9LlfSn0CKDSzLhJDITpgLoS0xpnocxsgMjQjUcVYDgO47r9zLP49HEjH/iGsFg=="],
"@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
"@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="],
"@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/node": ["@types/node@25.3.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA=="],
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="],
"@vitejs/plugin-rsc": ["@vitejs/plugin-rsc@0.5.21", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.5", "es-module-lexer": "^2.0.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.21", "periscopic": "^4.0.2", "srvx": "^0.11.7", "strip-literal": "^3.1.0", "turbo-stream": "^3.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*", "react-server-dom-webpack": "*", "vite": "*" }, "optionalPeers": ["react-server-dom-webpack"] }, "sha512-uNayLT8IKvWoznvQyfwKuGiEFV28o7lxUDnw/Av36VCuGpDFZnMmvVCwR37gTvnSmnpul9V0tdJqY3tBKEaDqw=="],
"@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="],
"@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="],
"@webassemblyjs/helper-api-error": ["@webassemblyjs/helper-api-error@1.13.2", "", {}, "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ=="],
"@webassemblyjs/helper-buffer": ["@webassemblyjs/helper-buffer@1.14.1", "", {}, "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA=="],
"@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.13.2", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA=="],
"@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.13.2", "", {}, "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA=="],
"@webassemblyjs/helper-wasm-section": ["@webassemblyjs/helper-wasm-section@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/wasm-gen": "1.14.1" } }, "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw=="],
"@webassemblyjs/ieee754": ["@webassemblyjs/ieee754@1.13.2", "", { "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw=="],
"@webassemblyjs/leb128": ["@webassemblyjs/leb128@1.13.2", "", { "dependencies": { "@xtuc/long": "4.2.2" } }, "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw=="],
"@webassemblyjs/utf8": ["@webassemblyjs/utf8@1.13.2", "", {}, "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ=="],
"@webassemblyjs/wasm-edit": ["@webassemblyjs/wasm-edit@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/helper-wasm-section": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-opt": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1", "@webassemblyjs/wast-printer": "1.14.1" } }, "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ=="],
"@webassemblyjs/wasm-gen": ["@webassemblyjs/wasm-gen@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg=="],
"@webassemblyjs/wasm-opt": ["@webassemblyjs/wasm-opt@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1" } }, "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw=="],
"@webassemblyjs/wasm-parser": ["@webassemblyjs/wasm-parser@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ=="],
"@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="],
"@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="],
"@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"acorn-import-phases": ["acorn-import-phases@1.0.4", "", { "peerDependencies": { "acorn": "^8.14.0" } }, "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ=="],
"acorn-loose": ["acorn-loose@8.5.2", "", { "dependencies": { "acorn": "^8.15.0" } }, "sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A=="],
"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=="],
"ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
"base-x": ["base-x@4.0.1", "", {}, "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="],
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
"bs58": ["bs58@5.0.0", "", { "dependencies": { "base-x": "^4.0.0" } }, "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ=="],
"bs58check": ["bs58check@3.0.1", "", { "dependencies": { "@noble/hashes": "^1.2.0", "bs58": "^5.0.0" } }, "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ=="],
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="],
"cbor-extract": ["cbor-extract@2.2.0", "", { "dependencies": { "node-gyp-build-optional-packages": "5.1.1" }, "optionalDependencies": { "@cbor-extract/cbor-extract-darwin-arm64": "2.2.0", "@cbor-extract/cbor-extract-darwin-x64": "2.2.0", "@cbor-extract/cbor-extract-linux-arm": "2.2.0", "@cbor-extract/cbor-extract-linux-arm64": "2.2.0", "@cbor-extract/cbor-extract-linux-x64": "2.2.0", "@cbor-extract/cbor-extract-win32-x64": "2.2.0" }, "bin": { "download-cbor-prebuilds": "bin/download-prebuilds.js" } }, "sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA=="],
"cbor-x": ["cbor-x@1.6.0", "", { "optionalDependencies": { "cbor-extract": "^2.2.0" } }, "sha512-0kareyRwHSkL6ws5VXHEf8uY1liitysCVJjlmhaLG+IXLqhSaOO+t63coaso7yjwEzWZzLy8fJo06gZDVQM9Qg=="],
"chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="],
"commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
"electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="],
"enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="],
"es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="],
"esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
"estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="],
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="],
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"hono": ["hono@4.12.5", "", {}, "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg=="],
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
"isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="],
"jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
"loader-runner": ["loader-runner@4.3.1", "", {}, "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
"node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.1.1", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-test": "build-test.js", "node-gyp-build-optional-packages-optional": "optional.js" } }, "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw=="],
"node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
"periscopic": ["periscopic@4.0.2", "", { "dependencies": { "@types/estree": "*", "is-reference": "^3.0.2", "zimmerframe": "^1.0.0" } }, "sha512-sqpQDUy8vgB7ycLkendSKS6HnVz1Rneoc3Rc+ZBUCe2pbqlVuCC5vF52l0NJ1aiMg/r1qfYF9/myz8CZeI2rjA=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
"react-server-dom-webpack": ["react-server-dom-webpack@19.2.4", "", { "dependencies": { "acorn-loose": "^8.3.0", "neo-async": "^2.6.1", "webpack-sources": "^3.2.0" }, "peerDependencies": { "react": "^19.2.4", "react-dom": "^19.2.4", "webpack": "^5.59.0" } }, "sha512-zEhkWv6RhXDctC2N7yEUHg3751nvFg81ydHj8LTTZuukF/IF1gcOKqqAL6Ds+kS5HtDVACYPik0IvzkgYXPhlQ=="],
"react-usestateref": ["react-usestateref@1.0.9", "", { "peerDependencies": { "react": ">16.0.0" } }, "sha512-t8KLsI7oje0HzfzGhxFXzuwbf1z9vhBM1ptHLUIHhYqZDKFuI5tzdhEVxSNzUkYxwF8XdpOErzHlKxvP7sTERw=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
"rsc-html-stream": ["rsc-html-stream@0.0.7", "", {}, "sha512-v9+fuY7usTgvXdNl8JmfXCvSsQbq2YMd60kOeeMIqCJFZ69fViuIxztHei7v5mlMMa2h3SqS+v44Gu9i9xANZA=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="],
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
"srvx": ["srvx@0.11.8", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-2n9t0YnAXPJjinytvxccNgs7rOA5gmE7Wowt/8Dy2dx2fDC6sBhfBpbrCvjYKALlVukPS/Uq3QwkolKNa7P/2Q=="],
"strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="],
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
"terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="],
"terser-webpack-plugin": ["terser-webpack-plugin@5.3.17", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"turbo-stream": ["turbo-stream@3.2.0", "", {}, "sha512-EK+bZ9UVrVh7JLslVFOV0GEMsociOqVOvEMTAd4ixMyffN5YNIEdLZWXUx5PJqDbTxSIBWw04HS9gCY4frYQDQ=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
"uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
"vite-plugin-top-level-await": ["vite-plugin-top-level-await@1.6.0", "", { "dependencies": { "@rollup/plugin-virtual": "^3.0.2", "@swc/core": "^1.12.14", "@swc/wasm": "^1.12.14", "uuid": "10.0.0" }, "peerDependencies": { "vite": ">=2.8" } }, "sha512-bNhUreLamTIkoulCR9aDXbTbhLk6n1YE8NJUTTxl5RYskNRtzOR0ASzSjBVRtNdjIfngDXo11qOsybGLNsrdww=="],
"vite-plugin-wasm": ["vite-plugin-wasm@3.5.0", "", { "peerDependencies": { "vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7" } }, "sha512-X5VWgCnqiQEGb+omhlBVsvTfxikKtoOgAzQ95+BZ8gQ+VfMHIjSHr0wyvXFQCa0eKQ0fKyaL0kWcEnYqBac4lQ=="],
"vitefu": ["vitefu@1.1.2", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw=="],
"waku": ["waku@1.0.0-alpha.5", "", { "dependencies": { "@hono/node-server": "^1.19.0", "@vitejs/plugin-react": "^5.1.0", "@vitejs/plugin-rsc": "^0.5.19", "dotenv": "^17.3.1", "hono": "^4.10.0", "magic-string": "^0.30.21", "picocolors": "^1.1.1", "rsc-html-stream": "^0.0.7", "vite": "^7.2.0" }, "peerDependencies": { "react": "~19.2.4", "react-dom": "~19.2.4", "react-server-dom-webpack": "~19.2.4" }, "bin": { "waku": "cli.js" } }, "sha512-DCKZD3grtGvuQdAQyIfqz9PkZoppto7Ysjf5yubD0oXcSQFJSlzLTz7APGuLiGf0NH67PBw1DFGxnV3Tf3J3BA=="],
"watchpack": ["watchpack@2.5.1", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg=="],
"webpack": ["webpack@5.105.4", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.16.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.20.0", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.17", "watchpack": "^2.5.1", "webpack-sources": "^3.3.4" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw=="],
"webpack-sources": ["webpack-sources@3.3.4", "", {}, "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q=="],
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
"xstate": ["xstate@5.28.0", "", {}, "sha512-Iaqq6ZrUzqeUtA3hC5LQKZfR8ZLzEFTImMHJM3jWEdVvXWdKvvVLXZEiNQWm3SCA9ZbEou/n5rcsna1wb9t28A=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
"@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@vitejs/plugin-rsc/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.5", "", {}, "sha512-RxlLX/DPoarZ9PtxVrQgZhPoor987YtKQqCo5zkjX+0S0yLJ7Vv515Wk6+xtTL67VONKJKxETWZwuZjss2idYw=="],
"esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"vite-plugin-top-level-await/uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
}
}

210
dist/public/assets/_id_-cxsEKuSj.js vendored Normal file
View File

@@ -0,0 +1,210 @@
import { r as a, j as e, __tla as __tla_0 } from "./index-I8cR0Dsm.js";
import { i as j, __tla as __tla_1 } from "./fullfat_bundler-C8o4MXnP.js";
import { u as v, a as N } from "./index-ChbWTil4.js";
import { h as p, a as w, u as C, v as k } from "./poll-BI_0HvZY.js";
let $;
let __tla = Promise.all([
(() => {
try {
return __tla_0;
} catch {
}
})(),
(() => {
try {
return __tla_1;
} catch {
}
})()
]).then(async () => {
const h = "p2p-poll-peer-id";
function g() {
return crypto.randomUUID();
}
function S() {
if (typeof globalThis.localStorage > "u") return g();
let s = localStorage.getItem(h);
return s || (s = g(), localStorage.setItem(h, s)), s;
}
function D() {
const s = v(), [r, d] = a.useState(false), l = a.useCallback(() => {
d(s.peers.length > 0);
}, [
s
]);
return a.useEffect(() => {
l();
const o = () => l();
return s.networkSubsystem.on("peer", o), s.networkSubsystem.on("peer-disconnected", o), () => {
s.networkSubsystem.off("peer", o), s.networkSubsystem.off("peer-disconnected", o);
};
}, [
s,
l
]), e.jsxs("div", {
className: "flex items-center gap-2 text-xs text-gray-500",
children: [
e.jsx("span", {
className: `inline-block h-2 w-2 rounded-full ${r ? "bg-green-500" : "bg-yellow-500"}`
}),
r ? "Connected" : "Connecting..."
]
});
}
function I({ docUrl: s }) {
const [r, d] = N(s), [l, o] = a.useState(""), [b, u] = a.useState(false), i = a.useMemo(() => S(), []);
if (!r) return e.jsx("div", {
className: "flex items-center justify-center py-12",
children: e.jsx("div", {
className: "text-gray-500",
children: "Loading poll..."
})
});
const c = r.options.reduce((t, n) => t + n.votes.length, 0), x = () => {
const t = l.trim();
t && (d((n) => w(n, t)), o(""));
}, f = (t) => {
p(r, t, i) ? d((n) => C(n, t, i)) : d((n) => k(n, t, i));
}, y = () => {
const t = window.location.href;
navigator.clipboard.writeText(t).then(() => {
u(true), setTimeout(() => u(false), 2e3);
});
};
return e.jsxs("div", {
className: "space-y-6",
children: [
e.jsxs("div", {
className: "flex items-start justify-between",
children: [
e.jsxs("div", {
children: [
e.jsx("h2", {
className: "text-2xl font-bold",
children: r.title
}),
e.jsxs("p", {
className: "mt-1 text-sm text-gray-500",
children: [
c,
" vote",
c !== 1 ? "s" : "",
" total"
]
})
]
}),
e.jsx(D, {})
]
}),
e.jsxs("div", {
className: "space-y-3",
children: [
r.options.map((t) => {
const n = p(r, t.id, i), m = c > 0 ? t.votes.length / c * 100 : 0;
return e.jsxs("div", {
className: "relative overflow-hidden rounded-lg border border-gray-200 bg-white",
children: [
e.jsx("div", {
className: "absolute inset-y-0 left-0 bg-blue-50 transition-all duration-300",
style: {
width: `${m}%`
}
}),
e.jsxs("div", {
className: "relative flex items-center justify-between px-4 py-3",
children: [
e.jsxs("button", {
onClick: () => f(t.id),
className: `flex items-center gap-2 text-left text-sm font-medium ${n ? "text-blue-600" : "text-gray-700 hover:text-blue-600"}`,
children: [
e.jsx("span", {
className: `flex h-5 w-5 items-center justify-center rounded border text-xs ${n ? "border-blue-600 bg-blue-600 text-white" : "border-gray-300"}`,
children: n ? "\u2713" : ""
}),
t.text
]
}),
e.jsxs("span", {
className: "text-sm text-gray-500",
children: [
t.votes.length,
" (",
m.toFixed(0),
"%)"
]
})
]
})
]
}, t.id);
}),
r.options.length === 0 && e.jsx("p", {
className: "py-4 text-center text-sm text-gray-400",
children: "No options yet. Add one below!"
})
]
}),
e.jsxs("div", {
className: "flex gap-2",
children: [
e.jsx("input", {
type: "text",
value: l,
onChange: (t) => o(t.target.value),
onKeyDown: (t) => t.key === "Enter" && x(),
placeholder: "Add an option...",
className: "flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
}),
e.jsx("button", {
onClick: x,
className: "rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700",
children: "Add"
})
]
}),
e.jsx("div", {
className: "border-t border-gray-200 pt-4",
children: e.jsx("button", {
onClick: y,
className: "rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-50",
children: b ? "Copied!" : "Copy shareable link"
})
})
]
});
}
function U({ id: s }) {
const r = `automerge:${s}`;
return j(r) ? e.jsx("div", {
className: "rounded-lg border border-gray-200 bg-white p-6 shadow-sm",
children: e.jsx(I, {
docUrl: r
})
}) : e.jsxs("div", {
className: "rounded-lg border border-red-200 bg-red-50 p-6 text-center",
children: [
e.jsx("h2", {
className: "text-lg font-semibold text-red-800",
children: "Invalid Poll ID"
}),
e.jsx("p", {
className: "mt-2 text-sm text-red-600",
children: "The poll ID in the URL is not valid."
}),
e.jsx("a", {
href: "/",
className: "mt-4 inline-block text-sm text-blue-600 hover:underline",
children: "Go back home"
})
]
});
}
$ = {
default: U
};
});
export {
__tla,
$ as export_4af94835fa0f
};

49
dist/public/assets/_layout-C9jEQBWP.js vendored Normal file
View File

@@ -0,0 +1,49 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/repo-CjvObF6Z.js","assets/fullfat_bundler-C8o4MXnP.js"])))=>i.map(i=>d[i]);
import { r as a, _ as l, j as i, __tla as __tla_0 } from "./index-I8cR0Dsm.js";
import { R as p } from "./index-ChbWTil4.js";
let v;
let __tla = Promise.all([
(() => {
try {
return __tla_0;
} catch {
}
})()
]).then(async () => {
function f({ children: c }) {
const [o, d] = a.useState(null);
return a.useEffect(() => {
let n, e;
return l(async () => {
const { getRepo: t, cleanupRepo: r } = await import("./repo-CjvObF6Z.js").then(async (m) => {
await m.__tla;
return m;
});
return {
getRepo: t,
cleanupRepo: r
};
}, __vite__mapDeps([0,1])).then(({ getRepo: t, cleanupRepo: r }) => {
const s = t();
d(s), n = r, e = () => {
s.networkSubsystem.adapters.forEach((u) => u.disconnect());
}, window.addEventListener("beforeunload", e);
}), () => {
e && window.removeEventListener("beforeunload", e), n == null ? void 0 : n();
};
}, []), o ? i.jsx(p.Provider, {
value: o,
children: c
}) : i.jsx("div", {
className: "flex min-h-screen items-center justify-center text-gray-400",
children: "Loading..."
});
}
v = {
default: f
};
});
export {
__tla,
v as export_125820ecd802
};

File diff suppressed because one or more lines are too long

Binary file not shown.

6
dist/public/assets/client-CsOmnPdF.js vendored Normal file
View File

@@ -0,0 +1,6 @@
import { I as r, E as o, S as e, C as t } from "./index-I8cR0Dsm.js";
const n = { Children: t, Slot: e }, s = { ErrorBoundary: o, INTERNAL_ServerRouter: r };
export {
s as export_6d786e16fc6b,
n as export_847a2b1045ef
};

File diff suppressed because it is too large Load Diff

21
dist/public/assets/index-BQZM9uZj.js vendored Normal file
View File

@@ -0,0 +1,21 @@
import { r as i, j as t } from "./index-I8cR0Dsm.js";
import { u as m } from "./index-ChbWTil4.js";
import { c as x } from "./poll-BI_0HvZY.js";
function p() {
const [o, d] = i.useState(""), [r, c] = i.useState(""), u = m(), s = () => {
if (!o.trim()) return;
const e = u.create();
e.change((l) => {
const a = x(o.trim());
l.title = a.title, l.options = a.options;
}), window.location.href = `/poll/${e.documentId}`;
}, n = () => {
const e = r.trim();
e && (window.location.href = `/poll/${e}`);
};
return t.jsxs("div", { className: "space-y-8", children: [t.jsx("title", { children: "P2P Poll" }), t.jsxs("section", { className: "rounded-lg border border-gray-200 bg-white p-6 shadow-sm", children: [t.jsx("h2", { className: "mb-4 text-lg font-semibold", children: "Create a New Poll" }), t.jsxs("div", { className: "flex gap-2", children: [t.jsx("input", { type: "text", value: o, onChange: (e) => d(e.target.value), onKeyDown: (e) => e.key === "Enter" && s(), placeholder: "Enter poll title...", className: "flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" }), t.jsx("button", { onClick: s, className: "rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700", children: "Create" })] })] }), t.jsxs("section", { className: "rounded-lg border border-gray-200 bg-white p-6 shadow-sm", children: [t.jsx("h2", { className: "mb-4 text-lg font-semibold", children: "Join an Existing Poll" }), t.jsxs("div", { className: "flex gap-2", children: [t.jsx("input", { type: "text", value: r, onChange: (e) => c(e.target.value), onKeyDown: (e) => e.key === "Enter" && n(), placeholder: "Paste poll ID or link...", className: "flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" }), t.jsx("button", { onClick: n, className: "rounded-md bg-gray-600 px-4 py-2 text-sm font-medium text-white hover:bg-gray-700", children: "Join" })] })] })] });
}
const f = { default: p };
export {
f as export_5ce7e027532e
};

2543
dist/public/assets/index-ChbWTil4.js vendored Normal file

File diff suppressed because it is too large Load Diff

10668
dist/public/assets/index-I8cR0Dsm.js vendored Normal file

File diff suppressed because it is too large Load Diff

27
dist/public/assets/poll-BI_0HvZY.js vendored Normal file
View File

@@ -0,0 +1,27 @@
function u(n) {
return { title: n, options: [] };
}
function c(n, t) {
n.options.push({ id: crypto.randomUUID(), text: t, votes: [] });
}
function f(n, t, i) {
const o = n.options.find((s) => s.id === t);
return o ? o.votes.includes(i) : false;
}
function r(n, t, i) {
const o = n.options.find((s) => s.id === t);
o && (o.votes.includes(i) || o.votes.push(i));
}
function d(n, t, i) {
const o = n.options.find((e) => e.id === t);
if (!o) return;
const s = o.votes.indexOf(i);
s !== -1 && o.votes.splice(s, 1);
}
export {
c as a,
u as c,
f as h,
d as u,
r as v
};

1254
dist/public/assets/repo-CjvObF6Z.js vendored Normal file

File diff suppressed because it is too large Load Diff

13
dist/serve-node.js vendored Normal file
View File

@@ -0,0 +1,13 @@
import { INTERNAL_runFetch, unstable_serverEntry } from './server/index.js';
const { serve } = unstable_serverEntry;
const host = process.env.HOST;
const port = process.env.PORT;
serve({
fetch: (req, ...args) => INTERNAL_runFetch(process.env, req, ...args),
...(host ? { hostname: host } : {}),
...(port ? { port: parseInt(port, 10) } : {}),
});

View File

@@ -0,0 +1,54 @@
export default {
"bootstrapScriptContent": "import(\"/assets/index-I8cR0Dsm.js\")",
"clientReferenceDeps": {
"847a2b1045ef": {
"js": [
"/assets/client-CsOmnPdF.js",
"/assets/index-I8cR0Dsm.js"
],
"css": []
},
"6d786e16fc6b": {
"js": [
"/assets/client-CsOmnPdF.js",
"/assets/index-I8cR0Dsm.js"
],
"css": []
},
"5ce7e027532e": {
"js": [
"/assets/index-BQZM9uZj.js",
"/assets/index-I8cR0Dsm.js",
"/assets/index-ChbWTil4.js",
"/assets/poll-BI_0HvZY.js"
],
"css": []
},
"4af94835fa0f": {
"js": [
"/assets/_id_-cxsEKuSj.js",
"/assets/index-I8cR0Dsm.js",
"/assets/fullfat_bundler-C8o4MXnP.js",
"/assets/index-ChbWTil4.js",
"/assets/poll-BI_0HvZY.js"
],
"css": []
},
"125820ecd802": {
"js": [
"/assets/_layout-C9jEQBWP.js",
"/assets/index-I8cR0Dsm.js",
"/assets/index-ChbWTil4.js"
],
"css": []
}
},
"serverResources": {
"src/pages/_layout.tsx": {
"js": [],
"css": [
"/assets/_layout-ChjUcnq2.css"
]
}
}
}

1
dist/server/__waku_build_metadata.js vendored Normal file
View File

@@ -0,0 +1 @@
export const buildMetadata = new Map([["defineRouter:cachedElements","{}"],["defineRouter:path2moduleIds","{}"]]);

25
dist/server/assets/_id_-D3sxE6t8.js vendored Normal file
View File

@@ -0,0 +1,25 @@
import { r as registerClientReference, a as jsxRuntime_reactServerExports } from "./server-entry-inner-DFzcLk7e.js";
import "node:async_hooks";
import "node:path";
import "http";
import "http2";
import "stream";
import "crypto";
import "fs";
import "path";
import "process";
import "../__vite_rsc_assets_manifest.js";
import "../__waku_build_metadata.js";
const PollPageClient = /* @__PURE__ */ registerClientReference(() => {
throw new Error("Unexpectedly client reference export 'default' is called on server");
}, "4af94835fa0f", "default");
async function PollPage({ id }) {
return /* @__PURE__ */ jsxRuntime_reactServerExports.jsx(PollPageClient, { id });
}
const getConfig = async () => {
return { render: "dynamic" };
};
export {
PollPage as default,
getConfig
};

File diff suppressed because one or more lines are too long

58
dist/server/assets/_layout-Cnq791cx.js vendored Normal file
View File

@@ -0,0 +1,58 @@
import assetsManifest from "../__vite_rsc_assets_manifest.js";
import { _ as __vite_rsc_react__, r as registerClientReference, a as jsxRuntime_reactServerExports } from "./server-entry-inner-DFzcLk7e.js";
import "node:async_hooks";
import "node:path";
import "http";
import "http2";
import "stream";
import "crypto";
import "fs";
import "path";
import "process";
import "../__waku_build_metadata.js";
const RemoveDuplicateServerCss = void 0;
const Resources = /* @__PURE__ */ ((React, deps, RemoveDuplicateServerCss2, precedence) => {
return function Resources2() {
return React.createElement(React.Fragment, null, [...deps.css.map((href) => React.createElement("link", {
key: "css:" + href,
rel: "stylesheet",
...{ precedence },
href,
"data-rsc-css-href": href
})), RemoveDuplicateServerCss2]);
};
})(
__vite_rsc_react__,
assetsManifest.serverResources["src/pages/_layout.tsx"],
RemoveDuplicateServerCss,
"vite-rsc/importer-resources"
);
const Providers = /* @__PURE__ */ registerClientReference(() => {
throw new Error("Unexpectedly client reference export 'default' is called on server");
}, "125820ecd802", "default");
const Layout = ({ children }) => /* @__PURE__ */ jsxRuntime_reactServerExports.jsx(Providers, { children: /* @__PURE__ */ jsxRuntime_reactServerExports.jsxs("div", { className: "min-h-screen bg-gray-50 text-gray-900", children: [
/* @__PURE__ */ jsxRuntime_reactServerExports.jsx("header", { className: "border-b border-gray-200 bg-white px-4 py-3", children: /* @__PURE__ */ jsxRuntime_reactServerExports.jsx("h1", { className: "text-xl font-bold", children: /* @__PURE__ */ jsxRuntime_reactServerExports.jsx("a", { href: "/", className: "hover:text-blue-600", children: "P2P Poll" }) }) }),
/* @__PURE__ */ jsxRuntime_reactServerExports.jsx("main", { className: "mx-auto max-w-xl px-4 py-8", children })
] }) });
const $$default = Layout;
let getConfig = async () => {
return { render: "static" };
};
const $$wrap_$$default = /* @__PURE__ */ __vite_rsc_wrap_css__($$default, "Layout");
function __vite_rsc_wrap_css__(value, name) {
if (typeof value !== "function") return value;
function __wrapper(props) {
return __vite_rsc_react__.createElement(
__vite_rsc_react__.Fragment,
null,
__vite_rsc_react__.createElement(Resources),
__vite_rsc_react__.createElement(value, props)
);
}
Object.defineProperty(__wrapper, "name", { value: name });
return __wrapper;
}
export {
$$wrap_$$default as default,
getConfig
};

28
dist/server/assets/_root-_BZAZd6T.js vendored Normal file
View File

@@ -0,0 +1,28 @@
import { a as jsxRuntime_reactServerExports } from "./server-entry-inner-DFzcLk7e.js";
import "node:async_hooks";
import "node:path";
import "http";
import "http2";
import "stream";
import "crypto";
import "fs";
import "path";
import "process";
import "../__vite_rsc_assets_manifest.js";
import "../__waku_build_metadata.js";
function Root({ children }) {
return /* @__PURE__ */ jsxRuntime_reactServerExports.jsxs("html", { lang: "en", children: [
/* @__PURE__ */ jsxRuntime_reactServerExports.jsxs("head", { children: [
/* @__PURE__ */ jsxRuntime_reactServerExports.jsx("meta", { charSet: "utf-8" }),
/* @__PURE__ */ jsxRuntime_reactServerExports.jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" })
] }),
/* @__PURE__ */ jsxRuntime_reactServerExports.jsx("body", { children })
] });
}
const getConfig = () => {
return { render: "dynamic" };
};
export {
Root as default,
getConfig
};

25
dist/server/assets/index-BU_VRdfq.js vendored Normal file
View File

@@ -0,0 +1,25 @@
import { r as registerClientReference, a as jsxRuntime_reactServerExports } from "./server-entry-inner-DFzcLk7e.js";
import "node:async_hooks";
import "node:path";
import "http";
import "http2";
import "stream";
import "crypto";
import "fs";
import "path";
import "process";
import "../__vite_rsc_assets_manifest.js";
import "../__waku_build_metadata.js";
const HomeClient = /* @__PURE__ */ registerClientReference(() => {
throw new Error("Unexpectedly client reference export 'default' is called on server");
}, "5ce7e027532e", "default");
async function HomePage() {
return /* @__PURE__ */ jsxRuntime_reactServerExports.jsx(HomeClient, {});
}
const getConfig = async () => {
return { render: "dynamic" };
};
export {
HomePage as default,
getConfig
};

File diff suppressed because it is too large Load Diff

54
dist/server/build.js vendored Normal file
View File

@@ -0,0 +1,54 @@
import { createRequire } from "node:module";
import { pathToFileURL } from "node:url";
import { I as INTERNAL_setAllEnv, s as serverEntry, j as joinPath, __tla as __tla_0 } from "./assets/server-entry-inner-DFzcLk7e.js";
import "node:async_hooks";
import "node:path";
import "http";
import "http2";
import "stream";
import "crypto";
import "fs";
import "path";
import "process";
import "./__vite_rsc_assets_manifest.js";
import "./__waku_build_metadata.js";
let INTERNAL_runBuild;
let __tla = Promise.all([
(() => {
try {
return __tla_0;
} catch {
}
})()
]).then(async () => {
function resolveModuleId(moduleId, rootDir) {
if (moduleId.startsWith("file://")) {
return moduleId;
}
if (moduleId.startsWith("/")) {
return pathToFileURL(joinPath(rootDir, moduleId.slice(1))).href;
}
const require2 = createRequire(joinPath(rootDir, "DUMMY.js"));
const resolved = require2.resolve(moduleId);
return pathToFileURL(resolved).href;
}
INTERNAL_runBuild = async function({ rootDir, emitFile }) {
INTERNAL_setAllEnv(process.env);
let build = serverEntry.build;
for (const enhancer of serverEntry.buildEnhancers || []) {
const moduleId = resolveModuleId(enhancer, rootDir);
const mod = await import(moduleId).then(async (m) => {
await m.__tla;
return m;
});
build = await mod.default(build);
}
await build({
emitFile
}, serverEntry.buildOptions || {});
};
});
export {
INTERNAL_runBuild,
__tla
};

22
dist/server/index.js vendored Normal file
View File

@@ -0,0 +1,22 @@
import { s as serverEntry, I as INTERNAL_setAllEnv } from "./assets/server-entry-inner-DFzcLk7e.js";
import "node:async_hooks";
import "node:path";
import "http";
import "http2";
import "stream";
import "crypto";
import "fs";
import "path";
import "process";
import "./__vite_rsc_assets_manifest.js";
import "./__waku_build_metadata.js";
async function INTERNAL_runFetch(env, req, ...args) {
INTERNAL_setAllEnv(env);
return serverEntry.fetch(req, ...args);
}
const entry_server = serverEntry.defaultExport;
export {
INTERNAL_runFetch,
entry_server as default,
serverEntry as unstable_serverEntry
};

View File

@@ -0,0 +1,54 @@
export default {
"bootstrapScriptContent": "import(\"/assets/index-I8cR0Dsm.js\")",
"clientReferenceDeps": {
"847a2b1045ef": {
"js": [
"/assets/client-CsOmnPdF.js",
"/assets/index-I8cR0Dsm.js"
],
"css": []
},
"6d786e16fc6b": {
"js": [
"/assets/client-CsOmnPdF.js",
"/assets/index-I8cR0Dsm.js"
],
"css": []
},
"5ce7e027532e": {
"js": [
"/assets/index-BQZM9uZj.js",
"/assets/index-I8cR0Dsm.js",
"/assets/index-ChbWTil4.js",
"/assets/poll-BI_0HvZY.js"
],
"css": []
},
"4af94835fa0f": {
"js": [
"/assets/_id_-cxsEKuSj.js",
"/assets/index-I8cR0Dsm.js",
"/assets/fullfat_bundler-C8o4MXnP.js",
"/assets/index-ChbWTil4.js",
"/assets/poll-BI_0HvZY.js"
],
"css": []
},
"125820ecd802": {
"js": [
"/assets/_layout-C9jEQBWP.js",
"/assets/index-I8cR0Dsm.js",
"/assets/index-ChbWTil4.js"
],
"css": []
}
},
"serverResources": {
"src/pages/_layout.tsx": {
"js": [],
"css": [
"/assets/_layout-ChjUcnq2.css"
]
}
}
}

192
dist/server/ssr/assets/_id_-BAEItp57.js vendored Normal file
View File

@@ -0,0 +1,192 @@
import { r as reactExports, j as jsxRuntimeExports } from "../index.js";
import { i as isValidAutomergeUrl } from "./fullfat_node-75TjwUrn.js";
import { u as useRepo, a as useDocument } from "./index-BSpyO9eA.js";
import { h as hasVoted, a as addOption, u as unvote, v as vote } from "./poll-R5-eIJ_b.js";
import "../__vite_rsc_assets_manifest.js";
import "node:async_hooks";
import "tty";
import "util";
import "os";
import "node:crypto";
import "crypto";
import "module";
import "fs";
const PEER_ID_KEY = "p2p-poll-peer-id";
function generateUUID() {
return crypto.randomUUID();
}
function getPeerId() {
if (typeof globalThis.localStorage === "undefined") {
return generateUUID();
}
let id = localStorage.getItem(PEER_ID_KEY);
if (!id) {
id = generateUUID();
localStorage.setItem(PEER_ID_KEY, id);
}
return id;
}
function ConnectionStatus() {
const repo = useRepo();
const [connected, setConnected] = reactExports.useState(false);
const updateStatus = reactExports.useCallback(() => {
setConnected(repo.peers.length > 0);
}, [repo]);
reactExports.useEffect(() => {
updateStatus();
const onChange = () => updateStatus();
repo.networkSubsystem.on("peer", onChange);
repo.networkSubsystem.on("peer-disconnected", onChange);
return () => {
repo.networkSubsystem.off("peer", onChange);
repo.networkSubsystem.off("peer-disconnected", onChange);
};
}, [repo, updateStatus]);
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-xs text-gray-500", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(
"span",
{
className: `inline-block h-2 w-2 rounded-full ${connected ? "bg-green-500" : "bg-yellow-500"}`
}
),
connected ? "Connected" : "Connecting..."
] });
}
function PollView({ docUrl }) {
const [doc, changeDoc] = useDocument(docUrl);
const [newOption, setNewOption] = reactExports.useState("");
const [copied, setCopied] = reactExports.useState(false);
const peerId = reactExports.useMemo(() => getPeerId(), []);
if (!doc) {
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-center py-12", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-gray-500", children: "Loading poll..." }) });
}
const totalVotes = doc.options.reduce((sum, o) => sum + o.votes.length, 0);
const handleAddOption = () => {
const text = newOption.trim();
if (!text) return;
changeDoc((d) => addOption(d, text));
setNewOption("");
};
const handleVote = (optionId) => {
if (hasVoted(doc, optionId, peerId)) {
changeDoc((d) => unvote(d, optionId, peerId));
} else {
changeDoc((d) => vote(d, optionId, peerId));
}
};
const handleCopy = () => {
const url = window.location.href;
navigator.clipboard.writeText(url).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2e3);
});
};
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-start justify-between", children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-2xl font-bold", children: doc.title }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "mt-1 text-sm text-gray-500", children: [
totalVotes,
" vote",
totalVotes !== 1 ? "s" : "",
" total"
] })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsx(ConnectionStatus, {})
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-3", children: [
doc.options.map((option) => {
const voted = hasVoted(doc, option.id, peerId);
const pct = totalVotes > 0 ? option.votes.length / totalVotes * 100 : 0;
return /* @__PURE__ */ jsxRuntimeExports.jsxs(
"div",
{
className: "relative overflow-hidden rounded-lg border border-gray-200 bg-white",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(
"div",
{
className: "absolute inset-y-0 left-0 bg-blue-50 transition-all duration-300",
style: { width: `${pct}%` }
}
),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative flex items-center justify-between px-4 py-3", children: [
/* @__PURE__ */ jsxRuntimeExports.jsxs(
"button",
{
onClick: () => handleVote(option.id),
className: `flex items-center gap-2 text-left text-sm font-medium ${voted ? "text-blue-600" : "text-gray-700 hover:text-blue-600"}`,
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(
"span",
{
className: `flex h-5 w-5 items-center justify-center rounded border text-xs ${voted ? "border-blue-600 bg-blue-600 text-white" : "border-gray-300"}`,
children: voted ? "\u2713" : ""
}
),
option.text
]
}
),
/* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-sm text-gray-500", children: [
option.votes.length,
" (",
pct.toFixed(0),
"%)"
] })
] })
]
},
option.id
);
}),
doc.options.length === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "py-4 text-center text-sm text-gray-400", children: "No options yet. Add one below!" })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(
"input",
{
type: "text",
value: newOption,
onChange: (e) => setNewOption(e.target.value),
onKeyDown: (e) => e.key === "Enter" && handleAddOption(),
placeholder: "Add an option...",
className: "flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
}
),
/* @__PURE__ */ jsxRuntimeExports.jsx(
"button",
{
onClick: handleAddOption,
className: "rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700",
children: "Add"
}
)
] }),
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border-t border-gray-200 pt-4", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
"button",
{
onClick: handleCopy,
className: "rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-50",
children: copied ? "Copied!" : "Copy shareable link"
}
) })
] });
}
function PollPageClient({ id }) {
const automergeUrl = `automerge:${id}`;
if (!isValidAutomergeUrl(automergeUrl)) {
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "rounded-lg border border-red-200 bg-red-50 p-6 text-center", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-lg font-semibold text-red-800", children: "Invalid Poll ID" }),
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "mt-2 text-sm text-red-600", children: "The poll ID in the URL is not valid." }),
/* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: "/", className: "mt-4 inline-block text-sm text-blue-600 hover:underline", children: "Go back home" })
] });
}
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "rounded-lg border border-gray-200 bg-white p-6 shadow-sm", children: /* @__PURE__ */ jsxRuntimeExports.jsx(PollView, { docUrl: automergeUrl }) });
}
const export_4af94835fa0f = {
default: PollPageClient
};
export {
export_4af94835fa0f
};

View File

@@ -0,0 +1,53 @@
import { r as reactExports, j as jsxRuntimeExports, __tla as __tla_0 } from "../index.js";
import { R as RepoContext } from "./index-BSpyO9eA.js";
import "../__vite_rsc_assets_manifest.js";
import "node:async_hooks";
let export_125820ecd802;
let __tla = Promise.all([
(() => {
try {
return __tla_0;
} catch {
}
})()
]).then(async () => {
function Providers({ children }) {
const [repo, setRepo] = reactExports.useState(null);
reactExports.useEffect(() => {
let cleanup;
let handleBeforeUnload;
import("./repo-zy9lifAg.js").then(({ getRepo, cleanupRepo }) => {
const r = getRepo();
setRepo(r);
cleanup = cleanupRepo;
handleBeforeUnload = () => {
r.networkSubsystem.adapters.forEach((adapter) => adapter.disconnect());
};
window.addEventListener("beforeunload", handleBeforeUnload);
});
return () => {
if (handleBeforeUnload) {
window.removeEventListener("beforeunload", handleBeforeUnload);
}
cleanup == null ? void 0 : cleanup();
};
}, []);
if (!repo) {
return jsxRuntimeExports.jsx("div", {
className: "flex min-h-screen items-center justify-center text-gray-400",
children: "Loading..."
});
}
return jsxRuntimeExports.jsx(RepoContext.Provider, {
value: repo,
children
});
}
export_125820ecd802 = {
default: Providers
};
});
export {
__tla,
export_125820ecd802
};

View File

@@ -0,0 +1,104 @@
import { j as jsxRuntimeExports, S as Slot, r as reactExports, C as Children } from "../index.js";
import "../__vite_rsc_assets_manifest.js";
import "node:async_hooks";
const RouterContext = /* @__PURE__ */ reactExports.createContext(null);
const notAvailableInServer = (name) => () => {
throw new Error(`${name} is not in the server`);
};
function renderError(message) {
return /* @__PURE__ */ jsxRuntimeExports.jsxs("html", {
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("head", {
children: /* @__PURE__ */ jsxRuntimeExports.jsx("title", {
children: "Unhandled Error"
})
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs("body", {
style: {
height: "100vh",
display: "flex",
flexDirection: "column",
placeContent: "center",
placeItems: "center",
fontSize: "16px",
margin: 0
},
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("h1", {
children: "Caught an unexpected error"
}),
/* @__PURE__ */ jsxRuntimeExports.jsxs("p", {
children: [
"Error: ",
message
]
})
]
})
]
});
}
class ErrorBoundary extends reactExports.Component {
constructor(props) {
super(props);
this.state = {};
}
static getDerivedStateFromError(error) {
return {
error
};
}
render() {
if ("error" in this.state) {
if (this.state.error instanceof Error) {
return renderError(this.state.error.message);
}
return renderError(String(this.state.error));
}
return this.props.children;
}
}
const getRouteSlotId = (path) => "route:" + path;
const MOCK_ROUTE_CHANGE_LISTENER = {
on: () => notAvailableInServer("routeChange:on"),
off: () => notAvailableInServer("routeChange:off")
};
function INTERNAL_ServerRouter({ route, httpstatus }) {
const routeElement = /* @__PURE__ */ jsxRuntimeExports.jsx(Slot, {
id: getRouteSlotId(route.path)
});
const rootElement = /* @__PURE__ */ jsxRuntimeExports.jsxs(Slot, {
id: "root",
children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("meta", {
name: "httpstatus",
content: `${httpstatus}`
}),
routeElement
]
});
return /* @__PURE__ */ jsxRuntimeExports.jsx(jsxRuntimeExports.Fragment, {
children: /* @__PURE__ */ jsxRuntimeExports.jsx(RouterContext, {
value: {
route,
changeRoute: notAvailableInServer("changeRoute"),
prefetchRoute: notAvailableInServer("prefetchRoute"),
routeChangeEvents: MOCK_ROUTE_CHANGE_LISTENER,
fetchingSlices: /* @__PURE__ */ new Set()
},
children: rootElement
})
});
}
const export_847a2b1045ef = {
Children,
Slot
};
const export_6d786e16fc6b = {
ErrorBoundary,
INTERNAL_ServerRouter
};
export {
export_6d786e16fc6b,
export_847a2b1045ef
};

File diff suppressed because it is too large Load Diff

5592
dist/server/ssr/assets/index-BSpyO9eA.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,82 @@
import { r as reactExports, j as jsxRuntimeExports } from "../index.js";
import { u as useRepo } from "./index-BSpyO9eA.js";
import { c as createPoll } from "./poll-R5-eIJ_b.js";
import "../__vite_rsc_assets_manifest.js";
import "node:async_hooks";
function HomeClient() {
const [title, setTitle] = reactExports.useState("");
const [joinId, setJoinId] = reactExports.useState("");
const repo = useRepo();
const handleCreate = () => {
if (!title.trim()) return;
const handle = repo.create();
handle.change((doc) => {
const poll = createPoll(title.trim());
doc.title = poll.title;
doc.options = poll.options;
});
window.location.href = `/poll/${handle.documentId}`;
};
const handleJoin = () => {
const id = joinId.trim();
if (!id) return;
window.location.href = `/poll/${id}`;
};
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-8", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("title", { children: "P2P Poll" }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("section", { className: "rounded-lg border border-gray-200 bg-white p-6 shadow-sm", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "mb-4 text-lg font-semibold", children: "Create a New Poll" }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(
"input",
{
type: "text",
value: title,
onChange: (e) => setTitle(e.target.value),
onKeyDown: (e) => e.key === "Enter" && handleCreate(),
placeholder: "Enter poll title...",
className: "flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
}
),
/* @__PURE__ */ jsxRuntimeExports.jsx(
"button",
{
onClick: handleCreate,
className: "rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700",
children: "Create"
}
)
] })
] }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("section", { className: "rounded-lg border border-gray-200 bg-white p-6 shadow-sm", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "mb-4 text-lg font-semibold", children: "Join an Existing Poll" }),
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(
"input",
{
type: "text",
value: joinId,
onChange: (e) => setJoinId(e.target.value),
onKeyDown: (e) => e.key === "Enter" && handleJoin(),
placeholder: "Paste poll ID or link...",
className: "flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
}
),
/* @__PURE__ */ jsxRuntimeExports.jsx(
"button",
{
onClick: handleJoin,
className: "rounded-md bg-gray-600 px-4 py-2 text-sm font-medium text-white hover:bg-gray-700",
children: "Join"
}
)
] })
] })
] });
}
const export_5ce7e027532e = {
default: HomeClient
};
export {
export_5ce7e027532e
};

39
dist/server/ssr/assets/poll-R5-eIJ_b.js vendored Normal file
View File

@@ -0,0 +1,39 @@
function createPoll(title) {
return {
title,
options: []
};
}
function addOption(poll, text) {
poll.options.push({
id: crypto.randomUUID(),
text,
votes: []
});
}
function hasVoted(poll, optionId, peerId) {
const option = poll.options.find((o) => o.id === optionId);
if (!option) return false;
return option.votes.includes(peerId);
}
function vote(poll, optionId, peerId) {
const option = poll.options.find((o) => o.id === optionId);
if (!option) return;
if (option.votes.includes(peerId)) return;
option.votes.push(peerId);
}
function unvote(poll, optionId, peerId) {
const option = poll.options.find((o) => o.id === optionId);
if (!option) return;
const idx = option.votes.indexOf(peerId);
if (idx !== -1) {
option.votes.splice(idx, 1);
}
}
export {
addOption as a,
createPoll as c,
hasVoted as h,
unvote as u,
vote as v
};

5494
dist/server/ssr/assets/repo-zy9lifAg.js vendored Normal file

File diff suppressed because it is too large Load Diff

11296
dist/server/ssr/index.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +0,0 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
devtools: { enabled: true },
vite: {
optimizeDeps: {
include: ['yjs', 'y-webrtc']
}
},
// ... existing config
nitro: {
storage: {
polls: {
driver: 'fs',
base: './.data/polls'
},
users: {
driver: 'fs',
base: './.data/users'
}
}
}
})

10118
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +1,32 @@
{
"name": "p2p-poll",
"name": "p2p-poll-app",
"version": "0.0.0",
"type": "module",
"private": true,
"scripts": {
"build": "nuxt build",
"dev": "PORT=4444 npx y-webrtc & nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
"dev": "waku dev",
"build": "waku build",
"start": "waku start"
},
"dependencies": {
"nuxt": "^4.1.3",
"uuid": "^13.0.0",
"vue": "^3.5.30",
"vue-router": "^5.0.3",
"y-webrtc": "^10.3.0",
"yjs": "^13.6.30"
"@automerge/automerge": "^3.2.4",
"@automerge/automerge-repo": "^2.5.3",
"@automerge/automerge-repo-network-broadcastchannel": "^2.5.3",
"@automerge/automerge-repo-network-websocket": "^2.5.3",
"@automerge/automerge-repo-react-hooks": "^2.5.3",
"@automerge/automerge-repo-storage-indexeddb": "^2.5.3",
"@tailwindcss/vite": "^4.2.1",
"react": "~19.2.4",
"react-dom": "~19.2.4",
"react-server-dom-webpack": "~19.2.4",
"tailwindcss": "^4.2.1",
"vite-plugin-top-level-await": "^1.6.0",
"vite-plugin-wasm": "^3.5.0",
"waku": "1.0.0-alpha.5"
},
"devDependencies": {
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"typescript": "^5.9.3"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,2 +0,0 @@
User-Agent: *
Disallow:

View File

@@ -1,68 +0,0 @@
import * as Y from 'yjs';
// server/api/polls/[id].ts
export default defineEventHandler(async (event) => {
const method = event.node.req.method;
const pollId = getRouterParam(event, 'id');
// We use Nitro's built-in storage.
// 'polls' is the storage namespace.
const storage = useStorage('polls');
if (!pollId) {
throw createError({ statusCode: 400, statusMessage: 'Poll ID required' });
}
// GET: Fetch the saved Yjs document state
if (method === 'GET') {
const data = await storage.getItem(`poll:${pollId}`);
// Return the array of numbers (or null if it doesn't exist yet)
return { update: data || null };
}
// POST: Save a new Yjs document state
if (method === 'POST') {
const body = await readBody(event);
if (body.update && Array.isArray(body.update)) {
// create a temp Y.Doc to encode the Data
const tempDoc = new Y.Doc();
Y.applyUpdate(tempDoc, new Uint8Array(body.update));
const yMap = tempDoc.getMap('shared-poll');
const pollData = yMap.toJSON();
// verify pollData
for(var option in pollData){
const votes = pollData[option] || [];
var pubKeys: CryptoKey[] = [];
const verifyAllVotesForOption = async (votes: SignedData<VoteData>[]) => {
console.log("verifying votes for option " + option,votes);
// check last votes first. if there is something wrong, its likely in the last vote.
for (let i = votes.length-1; i >= 0 ; i--) {
const userStorage = useStorage('users');
const votePubKeyString = await userStorage.getItem(`user:${votes[i]?.data.userid}`);
//console.log("Using public key: "+votePubKeyString)
const votePubKey = await stringToCryptoKey(String(votePubKeyString),'public')
const isValid = await verifyChainedVote(votes, i,votePubKey);
if(!isValid){
console.error("Error! Invalid Vote at: " + i,votes)
return false;
}
}
return true;
};
const verified = await verifyAllVotesForOption(votes);
if(!verified){
console.error("Failed to verify option: "+option)
throw createError({ statusCode: 400, statusMessage: 'PollData contains unverifyable content!' });
}
}
// Save the binary update (sent as an array of numbers) to storage
await storage.setItem(`poll:${pollId}`, body.update);
return { success: true };
}
throw createError({ statusCode: 400, statusMessage: 'Invalid update payload' });
}
});

View File

@@ -1,15 +0,0 @@
// server/api/polls/index.get.ts
export default defineEventHandler(async () => {
const storage = useStorage('polls');
// Get all keys in the 'polls' namespace
const allKeys = await storage.getKeys();
// Filter for our specific poll prefix and strip it for the UI
// poll:my-id -> my-id
const polls = allKeys
.filter(key => key.startsWith('poll:'))
.map(key => key.replace('poll:', ''));
return { polls };
});

View File

@@ -1,41 +0,0 @@
// server/api/users/[id].ts
export default defineEventHandler(async (event) => {
const method = event.node.req.method;
const userId = getRouterParam(event, 'id');
// We use Nitro's built-in storage.
// 'polls' is the storage namespace.
const storage = useStorage('users');
if (!userId) {
throw createError({ statusCode: 400, statusMessage: 'User ID required' });
}
// GET: Fetch the saved Yjs document state
if (method === 'GET') {
const data = await storage.getItem(`user:${userId}`);
// Return the array of numbers (or null if it doesn't exist yet)
return { public_key: data };
}
// POST: Save a new Yjs document state
if (method === 'POST') {
const body = await readBody(event);
if (body.public_key) {
const data = await storage.getItem(`user:${userId}`);
if (data == undefined || data == null) {
// Save the binary update (sent as an array of numbers) to storage
await storage.setItem(`user:${userId}`, body.public_key);
console.log("New User created: " + userId)
console.log("Public Key: " + body.public_key);
return { success: true };
}
throw createError({ statusCode: 400, statusMessage: 'User already exists.' });
}
throw createError({ statusCode: 400, statusMessage: 'Invalid update payload' });
}
});

View File

@@ -1,86 +0,0 @@
import { SignedData, VoteData } from "./types";
/**
* Gets the WebCrypto API regardless of environment (Node vs Browser)
*/
const getCrypto = () => {
return (globalThis as any).crypto;
};
export const verifyVote = async (data: any, signatureStr: string, publicKey: CryptoKey) => {
const encoder = new TextEncoder();
const encodedData = encoder.encode(JSON.stringify(data));
// Convert Base64 back to Uint8Array
const signature = Uint8Array.from(atob(signatureStr), c => c.charCodeAt(0));
return await getCrypto().subtle.verify(
"RSASSA-PKCS1-v1_5",
publicKey,
signature,
encodedData
);
};
/**
* Verifies a specific vote within an array of votes by
* reconstructing the "signed state" at that point in time.
*/
export const verifyChainedVote = async (
voteData: SignedData<VoteData>[],
index: number,
pubKey: CryptoKey
) => {
const voteToVerify = voteData[index];
console.log("Verifying vote: " + voteToVerify)
if(voteToVerify) {
// 1. Reconstruct the exact data state the user signed
// We need the array exactly as it was when they pushed their vote
const historicalState = voteData.slice(0, index + 1).map((v, i) => {
if (i === index) {
// For the current vote, the signature must be empty string
// because it wasn't signed yet when passed to signVote
return { ...v, signature: "" };
}
return v;
});
try {
// 3. Verify: Does this historicalState match the signature?
return await verifyVote(historicalState, voteToVerify.signature, pubKey);
} catch (err) {
console.error("Verification failed")
console.error(err);
return false;
}
}
console.error("Vote is undefined or null");
return false;
};
/**
* Converts a Base64 string back into a usable CryptoKey object
* @param keyStr The Base64 string (without PEM headers)
* @param type 'public' or 'private'
*/
export const stringToCryptoKey = async (keyStr: string, type: 'public' | 'private'): Promise<CryptoKey> => {
// 1. Convert Base64 string to a Uint8Array (binary)
const bytes = Buffer.from(keyStr, 'base64');
// 2. Identify the format based on the key type
// Public keys usually use 'spki', Private keys use 'pkcs8'
const format = type === 'public' ? 'spki' : 'pkcs8';
const usages: KeyUsage[] = type === 'public' ? ['verify'] : ['sign'];
// 3. Import the key
return await getCrypto().subtle.importKey(
format,
bytes,
{
name: "RSASSA-PKCS1-v1_5",
hash: "SHA-256",
},
true, // extractable (set to false if you want to lock it in memory)
usages
);
};

View File

@@ -1,36 +0,0 @@
export interface PollProps {
userid: string | undefined,
activePollId: string,
pollData: PollData,
addOption: (name: string) => void,
vote: (optionName: string) => void
}
export interface PollListProps {
userid: string | undefined,
}
export interface PollData extends Record<string, SignedData<VoteData>[]> {
}
export interface SignedData<T> {
data: T,
signature: string
}
export interface VoteData {
userid: string,
timestamp: string
}
export interface OptionData {
userid: string,
timestamp: string,
optionName: string
}
export interface UserData {
userid: string,
private_key: CryptoKey | undefined,
public_key: CryptoKey | undefined
}

View File

@@ -0,0 +1,37 @@
"use client";
import { useRepo } from "@automerge/automerge-repo-react-hooks";
import { useEffect, useState, useCallback } from "react";
export default function ConnectionStatus() {
const repo = useRepo();
const [connected, setConnected] = useState(false);
const updateStatus = useCallback(() => {
setConnected(repo ? repo.peers.length > 0 : false);
}, [repo]);
useEffect(() => {
updateStatus();
const onChange = () => updateStatus();
repo.networkSubsystem.on("peer", onChange);
repo.networkSubsystem.on("peer-disconnected", onChange);
return () => {
repo.networkSubsystem.off("peer", onChange);
repo.networkSubsystem.off("peer-disconnected", onChange);
};
}, [repo, updateStatus]);
return (
<div className="flex items-center gap-2 text-xs text-gray-500">
<span
className={`inline-block h-2 w-2 rounded-full ${
connected ? "bg-green-500" : "bg-yellow-500"
}`}
/>
{connected ? "Connected" : "Connecting..."}
</div>
);
}

View File

@@ -0,0 +1,79 @@
"use client";
import { useState } from "react";
import { useRouter } from "waku";
import { useRepo } from "@automerge/automerge-repo-react-hooks";
import { createPoll } from "../lib/poll.js";
import type { Poll } from "../lib/types.js";
export default function HomeClient() {
const [title, setTitle] = useState("");
const [joinId, setJoinId] = useState("");
const repo = useRepo();
const router = useRouter();
const handleCreate = () => {
if (!title.trim()) return;
const handle = repo.create<Poll>();
handle.change((doc) => {
const poll = createPoll(title.trim());
doc.title = poll.title;
doc.options = poll.options;
});
router.push(`/poll/${handle.documentId}`);
};
const handleJoin = () => {
const id = joinId.trim();
if (!id) return;
router.push(`/poll/${id}`);
};
return (
<div className="space-y-8">
<title>P2P Poll</title>
{/* Create Poll */}
<section className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
<h2 className="mb-4 text-lg font-semibold">Create a New Poll</h2>
<div className="flex gap-2">
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleCreate()}
placeholder="Enter poll title..."
className="flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
<button
onClick={handleCreate}
className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
>
Create
</button>
</div>
</section>
{/* Join Poll */}
<section className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
<h2 className="mb-4 text-lg font-semibold">Join an Existing Poll</h2>
<div className="flex gap-2">
<input
type="text"
value={joinId}
onChange={(e) => setJoinId(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleJoin()}
placeholder="Paste poll ID or link..."
className="flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
<button
onClick={handleJoin}
className="rounded-md bg-gray-600 px-4 py-2 text-sm font-medium text-white hover:bg-gray-700"
>
Join
</button>
</div>
</section>
</div>
);
}

View File

@@ -0,0 +1,28 @@
"use client";
import { isValidAutomergeUrl } from "@automerge/automerge-repo";
import PollView from "./PollView.js";
export default function PollPageClient({ id }: { id: string }) {
const automergeUrl = `automerge:${id}` as const;
if (!isValidAutomergeUrl(automergeUrl)) {
return (
<div className="rounded-lg border border-red-200 bg-red-50 p-6 text-center">
<h2 className="text-lg font-semibold text-red-800">Invalid Poll ID</h2>
<p className="mt-2 text-sm text-red-600">
The poll ID in the URL is not valid.
</p>
<a href="/" className="mt-4 inline-block text-sm text-blue-600 hover:underline">
Go back home
</a>
</div>
);
}
return (
<div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
<PollView docUrl={automergeUrl} />
</div>
);
}

140
src/components/PollView.tsx Normal file
View File

@@ -0,0 +1,140 @@
"use client";
import { useState, useMemo } from "react";
import type { AutomergeUrl } from "@automerge/automerge-repo";
import { useDocument } from "@automerge/automerge-repo-react-hooks";
import { addOption, vote, unvote, hasVoted } from "../lib/poll.js";
import { getPeerId } from "../lib/peer.js";
import type { Poll } from "../lib/types.js";
import ConnectionStatus from "./ConnectionStatus.js";
export default function PollView({ docUrl }: { docUrl: AutomergeUrl }) {
const [doc, changeDoc] = useDocument<Poll>(docUrl);
const [newOption, setNewOption] = useState("");
const [copied, setCopied] = useState(false);
const peerId = useMemo(() => getPeerId(), []);
if (!doc) {
return (
<div className="flex items-center justify-center py-12">
<div className="text-gray-500">Loading poll...</div>
</div>
);
}
const totalVotes = doc.options.reduce((sum, o) => sum + o.votes.length, 0);
const handleAddOption = () => {
const text = newOption.trim();
if (!text) return;
changeDoc((d) => addOption(d, text));
setNewOption("");
};
const handleVote = (optionId: string) => {
if (hasVoted(doc, optionId, peerId)) {
changeDoc((d) => unvote(d, optionId, peerId));
} else {
changeDoc((d) => vote(d, optionId, peerId));
}
};
const handleCopy = () => {
const url = window.location.href;
navigator.clipboard.writeText(url).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}).catch(() => {});
};
return (
<div className="space-y-6">
<div className="flex items-start justify-between">
<div>
<h2 className="text-2xl font-bold">{doc.title}</h2>
<p className="mt-1 text-sm text-gray-500">
{totalVotes} vote{totalVotes !== 1 ? "s" : ""} total
</p>
</div>
<ConnectionStatus />
</div>
{/* Options list */}
<div className="space-y-3">
{doc.options.map((option) => {
const voted = hasVoted(doc, option.id, peerId);
const pct = totalVotes > 0 ? (option.votes.length / totalVotes) * 100 : 0;
return (
<div
key={option.id}
className="relative overflow-hidden rounded-lg border border-gray-200 bg-white"
>
{/* Vote bar */}
<div
className="absolute inset-y-0 left-0 bg-blue-50 transition-all duration-300"
style={{ width: `${pct}%` }}
/>
<div className="relative flex items-center justify-between px-4 py-3">
<button
onClick={() => handleVote(option.id)}
className={`flex items-center gap-2 text-left text-sm font-medium ${
voted ? "text-blue-600" : "text-gray-700 hover:text-blue-600"
}`}
>
<span
className={`flex h-5 w-5 items-center justify-center rounded border text-xs ${
voted
? "border-blue-600 bg-blue-600 text-white"
: "border-gray-300"
}`}
>
{voted ? "\u2713" : ""}
</span>
{option.text}
</button>
<span className="text-sm text-gray-500">
{option.votes.length} ({pct.toFixed(0)}%)
</span>
</div>
</div>
);
})}
{doc.options.length === 0 && (
<p className="py-4 text-center text-sm text-gray-400">
No options yet. Add one below!
</p>
)}
</div>
{/* Add option form */}
<div className="flex gap-2">
<input
type="text"
value={newOption}
onChange={(e) => setNewOption(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleAddOption()}
placeholder="Add an option..."
className="flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
<button
onClick={handleAddOption}
className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
>
Add
</button>
</div>
{/* Share */}
<div className="border-t border-gray-200 pt-4">
<button
onClick={handleCopy}
className="rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-50"
>
{copied ? "Copied!" : "Copy shareable link"}
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,45 @@
"use client";
import { type ReactNode, useState, useEffect } from "react";
import { RepoContext } from "@automerge/automerge-repo-react-hooks";
import type { Repo } from "@automerge/automerge-repo";
export default function Providers({ children }: { children: ReactNode }) {
const [repo, setRepo] = useState<Repo | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cleanup: (() => void) | undefined;
let handleBeforeUnload: (() => void) | undefined;
import("../lib/repo.js").then(({ getRepo, cleanupRepo }) => {
const r = getRepo();
setRepo(r);
cleanup = cleanupRepo;
handleBeforeUnload = () => {
r.networkSubsystem.adapters.forEach((adapter) => adapter.disconnect());
};
window.addEventListener("beforeunload", handleBeforeUnload);
}).catch(() => {
setError("Failed to initialize. Please refresh the page.");
});
return () => {
if (handleBeforeUnload) {
window.removeEventListener("beforeunload", handleBeforeUnload);
}
cleanup?.();
};
}, []);
if (error) {
return <div className="flex min-h-screen items-center justify-center text-red-500">{error}</div>;
}
if (!repo) {
return <div className="flex min-h-screen items-center justify-center text-gray-400">Loading...</div>;
}
return <RepoContext.Provider value={repo}>{children}</RepoContext.Provider>;
}

View File

@@ -0,0 +1,56 @@
import { describe, test, expect, beforeEach } from "bun:test";
// Mock localStorage
const storage = new Map<string, string>();
const localStorageMock = {
getItem: (key: string) => storage.get(key) ?? null,
setItem: (key: string, value: string) => storage.set(key, value),
removeItem: (key: string) => storage.delete(key),
clear: () => storage.clear(),
get length() {
return storage.size;
},
key: (_index: number) => null,
};
Object.defineProperty(globalThis, "localStorage", {
value: localStorageMock,
writable: true,
});
// Import after mock is set up
const { getPeerId } = await import("../peer.js");
describe("getPeerId", () => {
beforeEach(() => {
storage.clear();
});
test("persists and returns the same ID on subsequent calls", () => {
const id1 = getPeerId();
const id2 = getPeerId();
expect(id1).toBe(id2);
});
test("stores the ID in localStorage", () => {
const id = getPeerId();
expect(storage.get("p2p-poll-peer-id")).toBe(id);
});
test("returns a new UUID each time when localStorage is unavailable", () => {
const saved = globalThis.localStorage;
// @ts-expect-error — deliberately removing localStorage to test fallback
globalThis.localStorage = undefined;
const id1 = getPeerId();
const id2 = getPeerId();
expect(id1).not.toBe(id2);
expect(id1).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
);
// Restore
globalThis.localStorage = saved;
});
});

View File

@@ -0,0 +1,94 @@
import { describe, test, expect } from "bun:test";
import { createPoll, addOption, vote, unvote, hasVoted } from "../poll.js";
describe("vote", () => {
test("prevents double-vote (no duplicate entries)", () => {
const poll = createPoll("Test");
addOption(poll, "Red");
const optionId = poll.options[0]!.id;
vote(poll, optionId, "peer-1");
vote(poll, optionId, "peer-1");
expect(poll.options[0]!.votes).toEqual(["peer-1"]);
expect(poll.options[0]!.votes).toHaveLength(1);
});
test("allows different peers to vote", () => {
const poll = createPoll("Test");
addOption(poll, "Red");
const optionId = poll.options[0]!.id;
vote(poll, optionId, "peer-1");
vote(poll, optionId, "peer-2");
expect(poll.options[0]!.votes).toEqual(["peer-1", "peer-2"]);
});
test("is a no-op for non-existent option ID", () => {
const poll = createPoll("Test");
addOption(poll, "Red");
vote(poll, "non-existent-id", "peer-1");
expect(poll.options[0]!.votes).toEqual([]);
});
test("voting on one option does not affect another option", () => {
const poll = createPoll("Test");
addOption(poll, "Red");
addOption(poll, "Blue");
const redId = poll.options[0]!.id;
const blueId = poll.options[1]!.id;
vote(poll, redId, "peer-1");
expect(poll.options[0]!.votes).toEqual(["peer-1"]);
expect(poll.options[1]!.votes).toEqual([]);
vote(poll, blueId, "peer-2");
expect(poll.options[0]!.votes).toEqual(["peer-1"]);
expect(poll.options[1]!.votes).toEqual(["peer-2"]);
});
});
describe("unvote", () => {
test("removes peer ID from votes array", () => {
const poll = createPoll("Test");
addOption(poll, "Red");
const optionId = poll.options[0]!.id;
vote(poll, optionId, "peer-1");
unvote(poll, optionId, "peer-1");
expect(poll.options[0]!.votes).toEqual([]);
});
test("does nothing if peer has not voted", () => {
const poll = createPoll("Test");
addOption(poll, "Red");
const optionId = poll.options[0]!.id;
unvote(poll, optionId, "peer-1");
expect(poll.options[0]!.votes).toEqual([]);
});
});
describe("hasVoted", () => {
test("returns true when peer has voted", () => {
const poll = createPoll("Test");
addOption(poll, "Red");
const optionId = poll.options[0]!.id;
vote(poll, optionId, "peer-1");
expect(hasVoted(poll, optionId, "peer-1")).toBe(true);
});
test("returns false for non-existent option", () => {
const poll = createPoll("Test");
expect(hasVoted(poll, "non-existent", "peer-1")).toBe(false);
});
});

22
src/lib/peer.ts Normal file
View File

@@ -0,0 +1,22 @@
const PEER_ID_KEY = "p2p-poll-peer-id";
/** Generate a UUID v4 */
function generateUUID(): string {
return crypto.randomUUID();
}
/**
* Get or create a persistent peer ID.
* Stored in localStorage so each browser tab/device gets a stable identity.
*/
export function getPeerId(): string {
if (typeof globalThis.localStorage === "undefined") {
return generateUUID();
}
let id = localStorage.getItem(PEER_ID_KEY);
if (!id) {
id = generateUUID();
localStorage.setItem(PEER_ID_KEY, id);
}
return id;
}

43
src/lib/poll.ts Normal file
View File

@@ -0,0 +1,43 @@
import type { Poll } from "./types.js";
/** Create a new poll document with the given title and no options */
export function createPoll(title: string): Poll {
return {
title,
options: [],
};
}
/** Add a new option to the poll */
export function addOption(poll: Poll, text: string): void {
poll.options.push({
id: crypto.randomUUID(),
text,
votes: [],
});
}
/** Check if a peer has already voted on an option */
export function hasVoted(poll: Poll, optionId: string, peerId: string): boolean {
const option = poll.options.find((o) => o.id === optionId);
if (!option) return false;
return option.votes.includes(peerId);
}
/** Add a vote for the given option. Prevents double-voting. */
export function vote(poll: Poll, optionId: string, peerId: string): void {
const option = poll.options.find((o) => o.id === optionId);
if (!option) return;
if (option.votes.includes(peerId)) return;
option.votes.push(peerId);
}
/** Remove a vote from the given option */
export function unvote(poll: Poll, optionId: string, peerId: string): void {
const option = poll.options.find((o) => o.id === optionId);
if (!option) return;
const idx = option.votes.indexOf(peerId);
if (idx !== -1) {
option.votes.splice(idx, 1);
}
}

29
src/lib/repo.ts Normal file
View File

@@ -0,0 +1,29 @@
import { Repo } from "@automerge/automerge-repo";
import { BroadcastChannelNetworkAdapter } from "@automerge/automerge-repo-network-broadcastchannel";
import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb";
import { BrowserWebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket";
let repo: Repo | null = null;
/** Get or create the singleton Automerge Repo instance (browser-only) */
export function getRepo(): Repo {
if (repo) return repo;
repo = new Repo({
network: [
new BrowserWebSocketClientAdapter("wss://sync.automerge.org"),
new BroadcastChannelNetworkAdapter(),
],
storage: new IndexedDBStorageAdapter("p2p-poll-app"),
});
return repo;
}
/** Shut down the repo and reset the singleton */
export function cleanupRepo(): void {
if (repo) {
repo.shutdown();
repo = null;
}
}

12
src/lib/types.ts Normal file
View File

@@ -0,0 +1,12 @@
/** A single poll option that users can vote on */
export interface PollOption {
id: string;
text: string;
votes: string[]; // Array of peer IDs
}
/** The shared poll document stored in Automerge */
export interface Poll {
title: string;
options: PollOption[];
}

27
src/pages.gen.ts Normal file
View File

@@ -0,0 +1,27 @@
// deno-fmt-ignore-file
// biome-ignore format: generated types do not need formatting
// prettier-ignore
import type { PathsForPages, GetConfigResponse } from 'waku/router';
// prettier-ignore
import type { getConfig as File_Root_getConfig } from './pages/_root';
// prettier-ignore
import type { getConfig as File_Index_getConfig } from './pages/index';
// prettier-ignore
import type { getConfig as File_PollId_getConfig } from './pages/poll/[id]';
// prettier-ignore
type Page =
| ({ path: '/_root' } & GetConfigResponse<typeof File_Root_getConfig>)
| ({ path: '/' } & GetConfigResponse<typeof File_Index_getConfig>)
| ({ path: '/poll/[id]' } & GetConfigResponse<typeof File_PollId_getConfig>);
// prettier-ignore
declare module 'waku/router' {
interface RouteConfig {
paths: PathsForPages<Page>;
}
interface CreatePagesConfig {
pages: Page;
}
}

24
src/pages/_layout.tsx Normal file
View File

@@ -0,0 +1,24 @@
import type { ReactNode } from "react";
import "../styles/global.css";
import Providers from "../components/Providers.js";
const Layout = ({ children }: { children: ReactNode }) => (
<Providers>
<div className="min-h-screen bg-gray-50 text-gray-900">
<header className="border-b border-gray-200 bg-white px-4 py-3">
<h1 className="text-xl font-bold">
<a href="/" className="hover:text-blue-600">
P2P Poll
</a>
</h1>
</header>
<main className="mx-auto max-w-xl px-4 py-8">{children}</main>
</div>
</Providers>
);
export default Layout;
export const getConfig = async () => {
return { render: "static" } as const;
};

17
src/pages/_root.tsx Normal file
View File

@@ -0,0 +1,17 @@
import type { ReactNode } from "react";
export default function Root({ children }: { children: ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>{children}</body>
</html>
);
}
export const getConfig = () => {
return { render: "dynamic" } as const;
};

9
src/pages/index.tsx Normal file
View File

@@ -0,0 +1,9 @@
import HomeClient from "../components/HomeClient.js";
export default async function HomePage() {
return <HomeClient />;
}
export const getConfig = async () => {
return { render: "dynamic" } as const;
};

10
src/pages/poll/[id].tsx Normal file
View File

@@ -0,0 +1,10 @@
import type { PageProps } from "waku/router";
import PollPageClient from "../../components/PollPageClient.js";
export default async function PollPage({ id }: PageProps<"/poll/[id]">) {
return <PollPageClient id={id} />;
}
export const getConfig = async () => {
return { render: "dynamic" } as const;
};

1
src/styles/global.css Normal file
View File

@@ -0,0 +1 @@
@import "tailwindcss";

View File

@@ -1,18 +1,17 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"files": [],
"references": [
{
"path": "./.nuxt/tsconfig.app.json"
},
{
"path": "./.nuxt/tsconfig.server.json"
},
{
"path": "./.nuxt/tsconfig.shared.json"
},
{
"path": "./.nuxt/tsconfig.node.json"
"compilerOptions": {
"strict": true,
"target": "esnext",
"noEmit": true,
"isolatedModules": true,
"moduleDetection": "force",
"downlevelIteration": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"skipLibCheck": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"jsx": "react-jsx"
}
]
}

10
waku.config.ts Normal file
View File

@@ -0,0 +1,10 @@
import tailwindcss from "@tailwindcss/vite";
import wasm from "vite-plugin-wasm";
import topLevelAwait from "vite-plugin-top-level-await";
import { defineConfig } from "waku/config";
export default defineConfig({
vite: {
plugins: [tailwindcss(), wasm(), topLevelAwait()],
},
});