this repo has no description
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

initial commit

Nick Gerakines 75f512f0

+8651
+3
.env
··· 1 + VITE_CLIENT_ID=https://oauth-spa.smokesignal.tools/client-metadata.json 2 + VITE_REDIRECT_URI=https://oauth-spa.smokesignal.tools/oauth/callback 3 + VITE_DEFAULT_PDS=https://bsky.social
+24
.gitignore
··· 1 + # Logs 2 + logs 3 + *.log 4 + npm-debug.log* 5 + yarn-debug.log* 6 + yarn-error.log* 7 + pnpm-debug.log* 8 + lerna-debug.log* 9 + 10 + node_modules 11 + dist 12 + dist-ssr 13 + *.local 14 + 15 + # Editor directories and files 16 + .vscode/* 17 + !.vscode/extensions.json 18 + .idea 19 + .DS_Store 20 + *.suo 21 + *.ntvs* 22 + *.njsproj 23 + *.sln 24 + *.sw?
+185
CLAUDE.md
··· 1 + # ATProto OAuth React Demo 2 + 3 + ## Tech Stack 4 + - **Framework**: React 18 with Vite 5 + - **Language**: TypeScript 5.3 (strict mode) 6 + - **Styling**: Tailwind CSS 3.4 7 + - **State Management**: React Context API for auth 8 + - **Routing**: React Router v6 9 + - **HTTP Client**: Fetch API with custom hooks 10 + - **Authentication**: ATProtocol OAuth 2.1 with PKCE, DPoP, and PAR 11 + - **ATProto Libraries**: 12 + - @atproto/oauth-client-browser for OAuth 13 + - @atproto/api for XRPC and RichText 14 + - @atproto/identity for handle/DID resolution 15 + 16 + ## Project Structure 17 + ``` 18 + src/ 19 + ├── components/ 20 + │ ├── auth/ # Authentication components 21 + │ ├── post/ # Post creation components 22 + │ └── layout/ # Layout and navigation components 23 + ├── contexts/ # React contexts (AuthContext) 24 + ├── hooks/ # Custom hooks (useAuth, useATProto) 25 + ├── lib/ # ATProtocol client setup 26 + ├── utils/ # Utility functions (crypto, DPoP) 27 + └── types/ # TypeScript type definitions 28 + ``` 29 + 30 + ## Authentication Architecture 31 + 32 + ### OAuth 2.1 Flow 33 + 1. **Discovery**: Handle → DID → PDS → Authorization Server 34 + 2. **Session Secrets**: PKCE verifier, DPoP keypair, state token 35 + 3. **PAR**: Pushed Authorization Request with nonce discovery 36 + 4. **Authorization**: User consent at PDS 37 + 5. **Callback**: Verify state and issuer 38 + 6. **Token Exchange**: Code for tokens with DPoP proof 39 + 7. **Identity Verification**: Verify sub (DID) matches expected 40 + 41 + ### Token Management 42 + - **Access Tokens**: 15-30 min lifetime, DPoP-bound 43 + - **Storage**: sessionStorage (never localStorage) 44 + - **DPoP Keys**: IndexedDB, non-exportable CryptoKeyPair 45 + - **Refresh Tokens**: Single-use with mandatory rotation 46 + - **Auto-refresh**: Before access token expiry 47 + 48 + ### Security Requirements 49 + - PKCE with S256 (mandatory) 50 + - DPoP with ES256 (mandatory) 51 + - PAR (Pushed Authorization Requests) 52 + - State parameter for CSRF protection 53 + - Issuer verification in callback 54 + - Bidirectional handle/DID verification 55 + 56 + ## Coding Standards 57 + 58 + ### TypeScript 59 + ```typescript 60 + // ✅ Good 61 + interface AuthState { 62 + user: ProfileView | null; 63 + loading: boolean; 64 + error: Error | null; 65 + } 66 + 67 + // ❌ Bad 68 + interface AuthState { 69 + user: any; 70 + loading: any; 71 + error: any; 72 + } 73 + ``` 74 + 75 + ### React Patterns 76 + - Functional components with hooks only 77 + - Custom hooks for reusable logic 78 + - Context for auth state management 79 + - Error boundaries for graceful failures 80 + - Suspense for code splitting 81 + 82 + ### Error Handling 83 + ```typescript 84 + try { 85 + const result = await oauthClient.authorize(handle); 86 + // Handle success 87 + } catch (error) { 88 + console.error('OAuth authorization failed:', error); 89 + // User-friendly error message 90 + setError('Failed to authenticate. Please try again.'); 91 + } 92 + ``` 93 + 94 + ### Security Best Practices 95 + - **Never** store tokens in localStorage 96 + - **Always** verify DID matches expected identity 97 + - **Always** generate fresh DPoP proof per request 98 + - **Always** validate state parameter in callback 99 + - **Never** export DPoP private keys 100 + 101 + ## API Patterns 102 + 103 + ### XRPC Requests 104 + ```typescript 105 + // Every request needs Authorization and DPoP headers 106 + const response = await fetch(`${pdsUrl}/xrpc/${nsid}`, { 107 + method: 'POST', 108 + headers: { 109 + 'Authorization': `DPoP ${accessToken}`, 110 + 'DPoP': dpopProof, 111 + 'Content-Type': 'application/json' 112 + }, 113 + body: JSON.stringify(data) 114 + }); 115 + ``` 116 + 117 + ### Record Creation 118 + ```typescript 119 + const record = { 120 + repo: userDid, 121 + collection: 'app.bsky.feed.post', 122 + record: { 123 + $type: 'app.bsky.feed.post', 124 + text: postText, 125 + createdAt: new Date().toISOString() 126 + } 127 + }; 128 + ``` 129 + 130 + ## Testing Guidelines 131 + 132 + ### Unit Tests 133 + - Crypto utilities (PKCE, DPoP generation) 134 + - DID resolution logic 135 + - Token refresh logic 136 + - Input validation 137 + 138 + ### Integration Tests 139 + - Complete OAuth flow (mocked) 140 + - XRPC requests with DPoP 141 + - Protected route navigation 142 + 143 + ## Environment Variables 144 + ```env 145 + VITE_CLIENT_ID=https://oauth-spa.smokesignal.tools/client-metadata.json 146 + VITE_REDIRECT_URI=https://oauth-spa.smokesignal.tools/oauth/callback 147 + VITE_DEFAULT_PDS=https://bsky.social 148 + ``` 149 + 150 + ## Do's and Don'ts 151 + 152 + ### Do's 153 + ✅ Use sessionStorage for tokens 154 + ✅ Generate fresh DPoP proof for each request 155 + ✅ Verify DID matches expected identity 156 + ✅ Handle token refresh proactively 157 + ✅ Clear all storage on logout 158 + ✅ Use RichText library for text processing 159 + ✅ Implement proper error boundaries 160 + ✅ Follow TypeScript strict mode 161 + 162 + ### Don'ts 163 + ❌ Use localStorage for sensitive data 164 + ❌ Skip DPoP implementation 165 + ❌ Forget bidirectional handle/DID verification 166 + ❌ Use the same DPoP proof twice 167 + ❌ Export DPoP private keys 168 + ❌ Trust user input without validation 169 + ❌ Use console.log in production 170 + ❌ Create commits unless asked 171 + 172 + ## Development Commands 173 + ```bash 174 + npm run dev # Start dev server 175 + npm run build # Build for production 176 + npm run preview # Preview production build 177 + npm run test # Run tests 178 + ``` 179 + 180 + ## Key Files 181 + - `public/client-metadata.json` - OAuth client metadata 182 + - `src/lib/atproto-client.ts` - ATProto client setup 183 + - `src/utils/crypto-utils.ts` - PKCE and DPoP utilities 184 + - `src/contexts/AuthContext.tsx` - Authentication state 185 + - `src/hooks/useAuth.ts` - Auth hook for components
+177
README.md
··· 1 + # ATProtocol OAuth React Demo 2 + 3 + A complete implementation of ATProtocol OAuth 2.1 authentication in a React Single Page Application. This demo showcases secure authentication with Bluesky and other ATProtocol-compatible Personal Data Servers using modern security features including PKCE, DPoP, and PAR. 4 + 5 + ## Features 6 + 7 + - **OAuth 2.1 Authorization Code Flow** with mandatory PKCE (S256) 8 + - **DPoP (Demonstrating Proof of Possession)** with ES256 token binding 9 + - **PAR (Pushed Authorization Requests)** for enhanced security 10 + - **Automatic token refresh** before expiry 11 + - **XRPC API integration** for creating posts 12 + - **Secure token storage** (sessionStorage + IndexedDB for DPoP keys) 13 + - **Identity verification** with bidirectional handle/DID validation 14 + 15 + ## Prerequisites 16 + 17 + - Node.js 18+ and npm 18 + - A Bluesky account (or account on any ATProtocol PDS) 19 + 20 + ## Installation 21 + 22 + ```bash 23 + # Install dependencies 24 + npm install 25 + ``` 26 + 27 + ## Configuration 28 + 29 + The app is pre-configured for local development. The OAuth client metadata is served from `/public/client-metadata.json`: 30 + 31 + ```json 32 + { 33 + "client_id": "https://oauth-spa.smokesignal.tools/client-metadata.json", 34 + "application_type": "web", 35 + "client_name": "ATProto Demo App", 36 + "redirect_uris": ["https://oauth-spa.smokesignal.tools/oauth/callback"], 37 + "scope": "atproto repo:garden.lexicon.oauth-masterclass.now", 38 + "grant_types": ["authorization_code", "refresh_token"], 39 + "response_types": ["code"], 40 + "token_endpoint_auth_method": "none", 41 + "dpop_bound_access_tokens": true 42 + } 43 + ``` 44 + 45 + Environment variables (`.env`): 46 + 47 + ```env 48 + VITE_CLIENT_ID=https://oauth-spa.smokesignal.tools/client-metadata.json 49 + VITE_REDIRECT_URI=https://oauth-spa.smokesignal.tools/oauth/callback 50 + VITE_DEFAULT_PDS=https://bsky.social 51 + ``` 52 + 53 + ## Running the Application 54 + 55 + ```bash 56 + # Start the development server 57 + npm run dev 58 + 59 + # The app will be available at https://oauth-spa.smokesignal.tools 60 + ``` 61 + 62 + ## Usage 63 + 64 + 1. **Home Page**: Visit `https://oauth-spa.smokesignal.tools` to see the landing page 65 + 2. **Login**: Click "Get Started" and enter your Bluesky handle (e.g., `alice.bsky.social`) 66 + 3. **Authorization**: You'll be redirected to your PDS to authorize the app 67 + 4. **Dashboard**: After authorization, view your profile information 68 + 5. **Create Post**: Navigate to "Create Post" to publish content to your PDS 69 + 70 + ## Authentication Flow 71 + 72 + 1. **Handle Resolution**: Converts handle → DID → PDS URL 73 + 2. **PAR Request**: Registers authorization request with server 74 + 3. **User Authorization**: User approves scopes at their PDS 75 + 4. **Token Exchange**: Authorization code → Access + Refresh tokens 76 + 5. **Identity Verification**: Validates DID matches expected identity 77 + 6. **API Access**: Make authenticated XRPC calls with DPoP proofs 78 + 79 + ## Project Structure 80 + 81 + ``` 82 + src/ 83 + ├── components/ 84 + │ ├── auth/ # Authentication components 85 + │ │ ├── LoginForm.tsx 86 + │ │ └── OAuthCallback.tsx 87 + │ ├── post/ # Post creation 88 + │ │ └── CreatePost.tsx 89 + │ └── layout/ # Layout components 90 + │ ├── Header.tsx 91 + │ └── ProtectedRoute.tsx 92 + ├── contexts/ 93 + │ └── AuthContext.tsx # Auth state management 94 + ├── lib/ 95 + │ ├── atproto-oauth-client.ts # OAuth client 96 + │ └── xrpc-client.ts # XRPC API client 97 + ├── utils/ 98 + │ ├── crypto-utils.ts # PKCE & DPoP generation 99 + │ └── did-resolver.ts # Handle/DID resolution 100 + ├── types/ 101 + │ └── auth.ts # TypeScript definitions 102 + └── App.tsx # Main app with routing 103 + ``` 104 + 105 + ## Security Features 106 + 107 + - **PKCE S256**: Protects against authorization code interception 108 + - **DPoP Token Binding**: Prevents token theft and replay attacks 109 + - **PAR**: Pre-registers authorization requests for validation 110 + - **Secure Storage**: Tokens in sessionStorage, DPoP keys in IndexedDB 111 + - **Identity Verification**: Bidirectional handle ↔ DID validation 112 + - **HTTPS Client Metadata**: Client configuration served over HTTPS 113 + 114 + ## Building for Production 115 + 116 + ```bash 117 + # Build the application 118 + npm run build 119 + 120 + # Preview the production build 121 + npm run preview 122 + ``` 123 + 124 + For production deployment: 125 + 126 + 1. Update `client-metadata.json` with your production URLs 127 + 2. Host the metadata file at a publicly accessible HTTPS URL 128 + 3. Update environment variables with production values 129 + 4. Deploy to a secure HTTPS host 130 + 131 + ## Technologies Used 132 + 133 + - **React 18** with TypeScript 134 + - **Vite** for build tooling 135 + - **React Router v6** for navigation 136 + - **Tailwind CSS** for styling 137 + - **Web Crypto API** for cryptographic operations 138 + - **IndexedDB** for secure key storage 139 + - **@atproto/api** for ATProtocol types and utilities 140 + 141 + ## Important Security Notes 142 + 143 + - Never store tokens in localStorage (XSS vulnerability) 144 + - Always verify the DID matches the expected identity 145 + - Generate fresh DPoP proofs for every request 146 + - Implement proper CSRF protection with state parameter 147 + - Use HTTPS in production for all endpoints 148 + - Keep DPoP private keys non-exportable 149 + 150 + ## Troubleshooting 151 + 152 + ### "No OAuth state found" 153 + Clear browser storage and try logging in again. 154 + 155 + ### "DPoP key not found" 156 + IndexedDB may be blocked. Check browser settings. 157 + 158 + ### "Failed to resolve handle" 159 + Ensure the handle is valid and the PDS is accessible. 160 + 161 + ### "Token refresh failed" 162 + The session may have expired. Log in again. 163 + 164 + ## License 165 + 166 + MIT 167 + 168 + ## Contributing 169 + 170 + Contributions are welcome! Please ensure all OAuth security requirements are maintained. 171 + 172 + ## Resources 173 + 174 + - [ATProtocol OAuth Specification](https://atproto.com/specs/oauth) 175 + - [Bluesky Documentation](https://docs.bsky.app) 176 + - [OAuth 2.1 RFC](https://datatracker.ietf.org/doc/draft-ietf-oauth-v2-1/) 177 + - [DPoP RFC 9449](https://datatracker.ietf.org/doc/rfc9449/)
+23
eslint.config.js
··· 1 + import js from '@eslint/js' 2 + import globals from 'globals' 3 + import reactHooks from 'eslint-plugin-react-hooks' 4 + import reactRefresh from 'eslint-plugin-react-refresh' 5 + import tseslint from 'typescript-eslint' 6 + import { defineConfig, globalIgnores } from 'eslint/config' 7 + 8 + export default defineConfig([ 9 + globalIgnores(['dist']), 10 + { 11 + files: ['**/*.{ts,tsx}'], 12 + extends: [ 13 + js.configs.recommended, 14 + tseslint.configs.recommended, 15 + reactHooks.configs['recommended-latest'], 16 + reactRefresh.configs.vite, 17 + ], 18 + languageOptions: { 19 + ecmaVersion: 2020, 20 + globals: globals.browser, 21 + }, 22 + }, 23 + ])
+13
index.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <link rel="icon" type="image/svg+xml" href="/vite.svg" /> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 + <title>atproto-oauth-react</title> 8 + </head> 9 + <body> 10 + <div id="root"></div> 11 + <script type="module" src="/src/main.tsx"></script> 12 + </body> 13 + </html>
+4549
package-lock.json
··· 1 + { 2 + "name": "atproto-oauth-react", 3 + "version": "0.0.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "atproto-oauth-react", 9 + "version": "0.0.0", 10 + "dependencies": { 11 + "@atproto/api": "^0.17.0", 12 + "@atproto/identity": "^0.4.9", 13 + "@atproto/oauth-client-browser": "^0.3.32", 14 + "react": "^19.1.1", 15 + "react-dom": "^19.1.1", 16 + "react-router-dom": "^7.9.3" 17 + }, 18 + "devDependencies": { 19 + "@eslint/js": "^9.36.0", 20 + "@tailwindcss/postcss": "^4.1.14", 21 + "@types/node": "^24.6.0", 22 + "@types/react": "^19.1.16", 23 + "@types/react-dom": "^19.1.9", 24 + "@vitejs/plugin-react": "^5.0.4", 25 + "autoprefixer": "^10.4.21", 26 + "eslint": "^9.36.0", 27 + "eslint-plugin-react-hooks": "^5.2.0", 28 + "eslint-plugin-react-refresh": "^0.4.22", 29 + "globals": "^16.4.0", 30 + "postcss": "^8.5.6", 31 + "tailwindcss": "^4.1.14", 32 + "typescript": "~5.9.3", 33 + "typescript-eslint": "^8.45.0", 34 + "vite": "^7.1.7" 35 + } 36 + }, 37 + "node_modules/@alloc/quick-lru": { 38 + "version": "5.2.0", 39 + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", 40 + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", 41 + "dev": true, 42 + "license": "MIT", 43 + "engines": { 44 + "node": ">=10" 45 + }, 46 + "funding": { 47 + "url": "https://github.com/sponsors/sindresorhus" 48 + } 49 + }, 50 + "node_modules/@atproto-labs/did-resolver": { 51 + "version": "0.2.1", 52 + "resolved": "https://registry.npmjs.org/@atproto-labs/did-resolver/-/did-resolver-0.2.1.tgz", 53 + "integrity": "sha512-zSoHyqwwRYUtMNLW+RrWsImt1U5S47nJv5FfmAXTmon6wVKjxKD/PFrD1pg/4G6THqJmQHTs1Hj+54XVupYnvQ==", 54 + "license": "MIT", 55 + "dependencies": { 56 + "@atproto-labs/fetch": "0.2.3", 57 + "@atproto-labs/pipe": "0.1.1", 58 + "@atproto-labs/simple-store": "0.3.0", 59 + "@atproto-labs/simple-store-memory": "0.1.4", 60 + "@atproto/did": "0.2.0", 61 + "zod": "^3.23.8" 62 + } 63 + }, 64 + "node_modules/@atproto-labs/fetch": { 65 + "version": "0.2.3", 66 + "resolved": "https://registry.npmjs.org/@atproto-labs/fetch/-/fetch-0.2.3.tgz", 67 + "integrity": "sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw==", 68 + "license": "MIT", 69 + "dependencies": { 70 + "@atproto-labs/pipe": "0.1.1" 71 + } 72 + }, 73 + "node_modules/@atproto-labs/handle-resolver": { 74 + "version": "0.3.1", 75 + "resolved": "https://registry.npmjs.org/@atproto-labs/handle-resolver/-/handle-resolver-0.3.1.tgz", 76 + "integrity": "sha512-mLZdMNvwomgnn9sffKO1/xr02ctgeiT0FUVw7JekbchTckub2RM7qMu8Rw1mC4bpCpW+i7DXDiOxpoajkppwYQ==", 77 + "license": "MIT", 78 + "dependencies": { 79 + "@atproto-labs/simple-store": "0.3.0", 80 + "@atproto-labs/simple-store-memory": "0.1.4", 81 + "@atproto/did": "0.2.0", 82 + "zod": "^3.23.8" 83 + } 84 + }, 85 + "node_modules/@atproto-labs/identity-resolver": { 86 + "version": "0.3.1", 87 + "resolved": "https://registry.npmjs.org/@atproto-labs/identity-resolver/-/identity-resolver-0.3.1.tgz", 88 + "integrity": "sha512-jCgotRRqPykPwh4gh0FBLOqeofv1G8OH/DZ5s88HWm7biUZeksZwDrEvL5TnqEFUpXT3O9Hcyp/XEpfCAplRoQ==", 89 + "license": "MIT", 90 + "dependencies": { 91 + "@atproto-labs/did-resolver": "0.2.1", 92 + "@atproto-labs/handle-resolver": "0.3.1" 93 + } 94 + }, 95 + "node_modules/@atproto-labs/pipe": { 96 + "version": "0.1.1", 97 + "resolved": "https://registry.npmjs.org/@atproto-labs/pipe/-/pipe-0.1.1.tgz", 98 + "integrity": "sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg==", 99 + "license": "MIT" 100 + }, 101 + "node_modules/@atproto-labs/simple-store": { 102 + "version": "0.3.0", 103 + "resolved": "https://registry.npmjs.org/@atproto-labs/simple-store/-/simple-store-0.3.0.tgz", 104 + "integrity": "sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ==", 105 + "license": "MIT" 106 + }, 107 + "node_modules/@atproto-labs/simple-store-memory": { 108 + "version": "0.1.4", 109 + "resolved": "https://registry.npmjs.org/@atproto-labs/simple-store-memory/-/simple-store-memory-0.1.4.tgz", 110 + "integrity": "sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw==", 111 + "license": "MIT", 112 + "dependencies": { 113 + "@atproto-labs/simple-store": "0.3.0", 114 + "lru-cache": "^10.2.0" 115 + } 116 + }, 117 + "node_modules/@atproto-labs/simple-store-memory/node_modules/lru-cache": { 118 + "version": "10.4.3", 119 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", 120 + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", 121 + "license": "ISC" 122 + }, 123 + "node_modules/@atproto/api": { 124 + "version": "0.17.0", 125 + "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.17.0.tgz", 126 + "integrity": "sha512-FNS9SW7/3kslAnJH7F4fO9/jPjXzC0NMD6u9NjJ/h4EnaIEpWHZQPkmD9Q2hvAwD6+Uo2boYZEPKkOa55Lr5Dg==", 127 + "license": "MIT", 128 + "dependencies": { 129 + "@atproto/common-web": "^0.4.3", 130 + "@atproto/lexicon": "^0.5.1", 131 + "@atproto/syntax": "^0.4.1", 132 + "@atproto/xrpc": "^0.7.5", 133 + "await-lock": "^2.2.2", 134 + "multiformats": "^9.9.0", 135 + "tlds": "^1.234.0", 136 + "zod": "^3.23.8" 137 + } 138 + }, 139 + "node_modules/@atproto/common-web": { 140 + "version": "0.4.3", 141 + "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.3.tgz", 142 + "integrity": "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg==", 143 + "license": "MIT", 144 + "dependencies": { 145 + "graphemer": "^1.4.0", 146 + "multiformats": "^9.9.0", 147 + "uint8arrays": "3.0.0", 148 + "zod": "^3.23.8" 149 + } 150 + }, 151 + "node_modules/@atproto/crypto": { 152 + "version": "0.4.4", 153 + "resolved": "https://registry.npmjs.org/@atproto/crypto/-/crypto-0.4.4.tgz", 154 + "integrity": "sha512-Yq9+crJ7WQl7sxStVpHgie5Z51R05etaK9DLWYG/7bR5T4bhdcIgF6IfklLShtZwLYdVVj+K15s0BqW9a8PSDA==", 155 + "license": "MIT", 156 + "dependencies": { 157 + "@noble/curves": "^1.7.0", 158 + "@noble/hashes": "^1.6.1", 159 + "uint8arrays": "3.0.0" 160 + }, 161 + "engines": { 162 + "node": ">=18.7.0" 163 + } 164 + }, 165 + "node_modules/@atproto/did": { 166 + "version": "0.2.0", 167 + "resolved": "https://registry.npmjs.org/@atproto/did/-/did-0.2.0.tgz", 168 + "integrity": "sha512-BskT39KYbwY1DUsWekkHh47xS+wvJpFq5F9acsicNfYniinyAMnNTzGKQEhnjQuG7K0qQItg/SnmC+y0tJXV7Q==", 169 + "license": "MIT", 170 + "dependencies": { 171 + "zod": "^3.23.8" 172 + } 173 + }, 174 + "node_modules/@atproto/identity": { 175 + "version": "0.4.9", 176 + "resolved": "https://registry.npmjs.org/@atproto/identity/-/identity-0.4.9.tgz", 177 + "integrity": "sha512-pRYCaeaEJMZ4vQlRQYYTrF3cMiRp21n/k/pUT1o7dgKby56zuLErDmFXkbKfKWPf7SgWRgamSaNmsGLqAOD7lQ==", 178 + "license": "MIT", 179 + "dependencies": { 180 + "@atproto/common-web": "^0.4.3", 181 + "@atproto/crypto": "^0.4.4" 182 + }, 183 + "engines": { 184 + "node": ">=18.7.0" 185 + } 186 + }, 187 + "node_modules/@atproto/jwk": { 188 + "version": "0.5.0", 189 + "resolved": "https://registry.npmjs.org/@atproto/jwk/-/jwk-0.5.0.tgz", 190 + "integrity": "sha512-Qi2NtEqhkG+uz3CKia4+H05WMV/z//dz3ESo5+cyBKrOnxVTJ5ZubMyltWjoYvy6v/jLhorXdDWcjn07yky7MQ==", 191 + "license": "MIT", 192 + "dependencies": { 193 + "multiformats": "^9.9.0", 194 + "zod": "^3.23.8" 195 + } 196 + }, 197 + "node_modules/@atproto/jwk-jose": { 198 + "version": "0.1.10", 199 + "resolved": "https://registry.npmjs.org/@atproto/jwk-jose/-/jwk-jose-0.1.10.tgz", 200 + "integrity": "sha512-Eiu/u4tZHz3IIhHZt0zneYEffSAO3Oqk/ToKwlu1TqKte6sjtPs/4uquSiAAGFYozqgo92JC/AQclWzzkHI5QQ==", 201 + "license": "MIT", 202 + "dependencies": { 203 + "@atproto/jwk": "0.5.0", 204 + "jose": "^5.2.0" 205 + } 206 + }, 207 + "node_modules/@atproto/jwk-webcrypto": { 208 + "version": "0.1.10", 209 + "resolved": "https://registry.npmjs.org/@atproto/jwk-webcrypto/-/jwk-webcrypto-0.1.10.tgz", 210 + "integrity": "sha512-JZsavs6JiSmw5rgcjkGDwzr1aCJGdybZOjVfYH+m9sXRU1BrUCA30uwNfZY7eFyWXyRAnCFiYiGVZgypXyKotw==", 211 + "license": "MIT", 212 + "dependencies": { 213 + "@atproto/jwk": "0.5.0", 214 + "@atproto/jwk-jose": "0.1.10", 215 + "zod": "^3.23.8" 216 + } 217 + }, 218 + "node_modules/@atproto/lexicon": { 219 + "version": "0.5.1", 220 + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.5.1.tgz", 221 + "integrity": "sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A==", 222 + "license": "MIT", 223 + "dependencies": { 224 + "@atproto/common-web": "^0.4.3", 225 + "@atproto/syntax": "^0.4.1", 226 + "iso-datestring-validator": "^2.2.2", 227 + "multiformats": "^9.9.0", 228 + "zod": "^3.23.8" 229 + } 230 + }, 231 + "node_modules/@atproto/oauth-client": { 232 + "version": "0.5.6", 233 + "resolved": "https://registry.npmjs.org/@atproto/oauth-client/-/oauth-client-0.5.6.tgz", 234 + "integrity": "sha512-O1S9lPptJxWPcNd2kODaLgWntz+A7PzskU2hP4IFa7hVLs4aEnEt9dKq5wJE97tDli8mgyh/ndPQhxUaCVQ5iQ==", 235 + "license": "MIT", 236 + "dependencies": { 237 + "@atproto-labs/did-resolver": "0.2.1", 238 + "@atproto-labs/fetch": "0.2.3", 239 + "@atproto-labs/handle-resolver": "0.3.1", 240 + "@atproto-labs/identity-resolver": "0.3.1", 241 + "@atproto-labs/simple-store": "0.3.0", 242 + "@atproto-labs/simple-store-memory": "0.1.4", 243 + "@atproto/did": "0.2.0", 244 + "@atproto/jwk": "0.5.0", 245 + "@atproto/oauth-types": "0.4.1", 246 + "@atproto/xrpc": "0.7.5", 247 + "multiformats": "^9.9.0", 248 + "zod": "^3.23.8" 249 + } 250 + }, 251 + "node_modules/@atproto/oauth-client-browser": { 252 + "version": "0.3.32", 253 + "resolved": "https://registry.npmjs.org/@atproto/oauth-client-browser/-/oauth-client-browser-0.3.32.tgz", 254 + "integrity": "sha512-h6Rsa/LgMnugaVKkMtHbQ1DSlhhIhL4HbjW1egg7z0BQdWJzczJ8nX3guHN5r/YioL6vOigxbMim+p/Z1LeG9g==", 255 + "license": "MIT", 256 + "dependencies": { 257 + "@atproto-labs/did-resolver": "0.2.1", 258 + "@atproto-labs/handle-resolver": "0.3.1", 259 + "@atproto-labs/simple-store": "0.3.0", 260 + "@atproto/did": "0.2.0", 261 + "@atproto/jwk": "0.5.0", 262 + "@atproto/jwk-webcrypto": "0.1.10", 263 + "@atproto/oauth-client": "0.5.6", 264 + "@atproto/oauth-types": "0.4.1" 265 + } 266 + }, 267 + "node_modules/@atproto/oauth-types": { 268 + "version": "0.4.1", 269 + "resolved": "https://registry.npmjs.org/@atproto/oauth-types/-/oauth-types-0.4.1.tgz", 270 + "integrity": "sha512-c5ixf2ZOzcltOu1fDBnO/tok6Wj7JDDK66+Z0q/+bAr8LXgOnxP7zQfJ+DD4gTkB+saTqsqWtVv8qvx/IEtm1g==", 271 + "license": "MIT", 272 + "dependencies": { 273 + "@atproto/jwk": "0.5.0", 274 + "zod": "^3.23.8" 275 + } 276 + }, 277 + "node_modules/@atproto/syntax": { 278 + "version": "0.4.1", 279 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.1.tgz", 280 + "integrity": "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==", 281 + "license": "MIT" 282 + }, 283 + "node_modules/@atproto/xrpc": { 284 + "version": "0.7.5", 285 + "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.7.5.tgz", 286 + "integrity": "sha512-MUYNn5d2hv8yVegRL0ccHvTHAVj5JSnW07bkbiaz96UH45lvYNRVwt44z+yYVnb0/mvBzyD3/ZQ55TRGt7fHkA==", 287 + "license": "MIT", 288 + "dependencies": { 289 + "@atproto/lexicon": "^0.5.1", 290 + "zod": "^3.23.8" 291 + } 292 + }, 293 + "node_modules/@babel/code-frame": { 294 + "version": "7.27.1", 295 + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", 296 + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", 297 + "dev": true, 298 + "license": "MIT", 299 + "dependencies": { 300 + "@babel/helper-validator-identifier": "^7.27.1", 301 + "js-tokens": "^4.0.0", 302 + "picocolors": "^1.1.1" 303 + }, 304 + "engines": { 305 + "node": ">=6.9.0" 306 + } 307 + }, 308 + "node_modules/@babel/compat-data": { 309 + "version": "7.28.4", 310 + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", 311 + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", 312 + "dev": true, 313 + "license": "MIT", 314 + "engines": { 315 + "node": ">=6.9.0" 316 + } 317 + }, 318 + "node_modules/@babel/core": { 319 + "version": "7.28.4", 320 + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", 321 + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", 322 + "dev": true, 323 + "license": "MIT", 324 + "dependencies": { 325 + "@babel/code-frame": "^7.27.1", 326 + "@babel/generator": "^7.28.3", 327 + "@babel/helper-compilation-targets": "^7.27.2", 328 + "@babel/helper-module-transforms": "^7.28.3", 329 + "@babel/helpers": "^7.28.4", 330 + "@babel/parser": "^7.28.4", 331 + "@babel/template": "^7.27.2", 332 + "@babel/traverse": "^7.28.4", 333 + "@babel/types": "^7.28.4", 334 + "@jridgewell/remapping": "^2.3.5", 335 + "convert-source-map": "^2.0.0", 336 + "debug": "^4.1.0", 337 + "gensync": "^1.0.0-beta.2", 338 + "json5": "^2.2.3", 339 + "semver": "^6.3.1" 340 + }, 341 + "engines": { 342 + "node": ">=6.9.0" 343 + }, 344 + "funding": { 345 + "type": "opencollective", 346 + "url": "https://opencollective.com/babel" 347 + } 348 + }, 349 + "node_modules/@babel/generator": { 350 + "version": "7.28.3", 351 + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", 352 + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", 353 + "dev": true, 354 + "license": "MIT", 355 + "dependencies": { 356 + "@babel/parser": "^7.28.3", 357 + "@babel/types": "^7.28.2", 358 + "@jridgewell/gen-mapping": "^0.3.12", 359 + "@jridgewell/trace-mapping": "^0.3.28", 360 + "jsesc": "^3.0.2" 361 + }, 362 + "engines": { 363 + "node": ">=6.9.0" 364 + } 365 + }, 366 + "node_modules/@babel/helper-compilation-targets": { 367 + "version": "7.27.2", 368 + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", 369 + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", 370 + "dev": true, 371 + "license": "MIT", 372 + "dependencies": { 373 + "@babel/compat-data": "^7.27.2", 374 + "@babel/helper-validator-option": "^7.27.1", 375 + "browserslist": "^4.24.0", 376 + "lru-cache": "^5.1.1", 377 + "semver": "^6.3.1" 378 + }, 379 + "engines": { 380 + "node": ">=6.9.0" 381 + } 382 + }, 383 + "node_modules/@babel/helper-globals": { 384 + "version": "7.28.0", 385 + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", 386 + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", 387 + "dev": true, 388 + "license": "MIT", 389 + "engines": { 390 + "node": ">=6.9.0" 391 + } 392 + }, 393 + "node_modules/@babel/helper-module-imports": { 394 + "version": "7.27.1", 395 + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", 396 + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", 397 + "dev": true, 398 + "license": "MIT", 399 + "dependencies": { 400 + "@babel/traverse": "^7.27.1", 401 + "@babel/types": "^7.27.1" 402 + }, 403 + "engines": { 404 + "node": ">=6.9.0" 405 + } 406 + }, 407 + "node_modules/@babel/helper-module-transforms": { 408 + "version": "7.28.3", 409 + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", 410 + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", 411 + "dev": true, 412 + "license": "MIT", 413 + "dependencies": { 414 + "@babel/helper-module-imports": "^7.27.1", 415 + "@babel/helper-validator-identifier": "^7.27.1", 416 + "@babel/traverse": "^7.28.3" 417 + }, 418 + "engines": { 419 + "node": ">=6.9.0" 420 + }, 421 + "peerDependencies": { 422 + "@babel/core": "^7.0.0" 423 + } 424 + }, 425 + "node_modules/@babel/helper-plugin-utils": { 426 + "version": "7.27.1", 427 + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", 428 + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", 429 + "dev": true, 430 + "license": "MIT", 431 + "engines": { 432 + "node": ">=6.9.0" 433 + } 434 + }, 435 + "node_modules/@babel/helper-string-parser": { 436 + "version": "7.27.1", 437 + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", 438 + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", 439 + "dev": true, 440 + "license": "MIT", 441 + "engines": { 442 + "node": ">=6.9.0" 443 + } 444 + }, 445 + "node_modules/@babel/helper-validator-identifier": { 446 + "version": "7.27.1", 447 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", 448 + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", 449 + "dev": true, 450 + "license": "MIT", 451 + "engines": { 452 + "node": ">=6.9.0" 453 + } 454 + }, 455 + "node_modules/@babel/helper-validator-option": { 456 + "version": "7.27.1", 457 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", 458 + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", 459 + "dev": true, 460 + "license": "MIT", 461 + "engines": { 462 + "node": ">=6.9.0" 463 + } 464 + }, 465 + "node_modules/@babel/helpers": { 466 + "version": "7.28.4", 467 + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", 468 + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", 469 + "dev": true, 470 + "license": "MIT", 471 + "dependencies": { 472 + "@babel/template": "^7.27.2", 473 + "@babel/types": "^7.28.4" 474 + }, 475 + "engines": { 476 + "node": ">=6.9.0" 477 + } 478 + }, 479 + "node_modules/@babel/parser": { 480 + "version": "7.28.4", 481 + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", 482 + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", 483 + "dev": true, 484 + "license": "MIT", 485 + "dependencies": { 486 + "@babel/types": "^7.28.4" 487 + }, 488 + "bin": { 489 + "parser": "bin/babel-parser.js" 490 + }, 491 + "engines": { 492 + "node": ">=6.0.0" 493 + } 494 + }, 495 + "node_modules/@babel/plugin-transform-react-jsx-self": { 496 + "version": "7.27.1", 497 + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", 498 + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", 499 + "dev": true, 500 + "license": "MIT", 501 + "dependencies": { 502 + "@babel/helper-plugin-utils": "^7.27.1" 503 + }, 504 + "engines": { 505 + "node": ">=6.9.0" 506 + }, 507 + "peerDependencies": { 508 + "@babel/core": "^7.0.0-0" 509 + } 510 + }, 511 + "node_modules/@babel/plugin-transform-react-jsx-source": { 512 + "version": "7.27.1", 513 + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", 514 + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", 515 + "dev": true, 516 + "license": "MIT", 517 + "dependencies": { 518 + "@babel/helper-plugin-utils": "^7.27.1" 519 + }, 520 + "engines": { 521 + "node": ">=6.9.0" 522 + }, 523 + "peerDependencies": { 524 + "@babel/core": "^7.0.0-0" 525 + } 526 + }, 527 + "node_modules/@babel/template": { 528 + "version": "7.27.2", 529 + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", 530 + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", 531 + "dev": true, 532 + "license": "MIT", 533 + "dependencies": { 534 + "@babel/code-frame": "^7.27.1", 535 + "@babel/parser": "^7.27.2", 536 + "@babel/types": "^7.27.1" 537 + }, 538 + "engines": { 539 + "node": ">=6.9.0" 540 + } 541 + }, 542 + "node_modules/@babel/traverse": { 543 + "version": "7.28.4", 544 + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", 545 + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", 546 + "dev": true, 547 + "license": "MIT", 548 + "dependencies": { 549 + "@babel/code-frame": "^7.27.1", 550 + "@babel/generator": "^7.28.3", 551 + "@babel/helper-globals": "^7.28.0", 552 + "@babel/parser": "^7.28.4", 553 + "@babel/template": "^7.27.2", 554 + "@babel/types": "^7.28.4", 555 + "debug": "^4.3.1" 556 + }, 557 + "engines": { 558 + "node": ">=6.9.0" 559 + } 560 + }, 561 + "node_modules/@babel/types": { 562 + "version": "7.28.4", 563 + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", 564 + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", 565 + "dev": true, 566 + "license": "MIT", 567 + "dependencies": { 568 + "@babel/helper-string-parser": "^7.27.1", 569 + "@babel/helper-validator-identifier": "^7.27.1" 570 + }, 571 + "engines": { 572 + "node": ">=6.9.0" 573 + } 574 + }, 575 + "node_modules/@esbuild/aix-ppc64": { 576 + "version": "0.25.10", 577 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", 578 + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", 579 + "cpu": [ 580 + "ppc64" 581 + ], 582 + "dev": true, 583 + "license": "MIT", 584 + "optional": true, 585 + "os": [ 586 + "aix" 587 + ], 588 + "engines": { 589 + "node": ">=18" 590 + } 591 + }, 592 + "node_modules/@esbuild/android-arm": { 593 + "version": "0.25.10", 594 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", 595 + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", 596 + "cpu": [ 597 + "arm" 598 + ], 599 + "dev": true, 600 + "license": "MIT", 601 + "optional": true, 602 + "os": [ 603 + "android" 604 + ], 605 + "engines": { 606 + "node": ">=18" 607 + } 608 + }, 609 + "node_modules/@esbuild/android-arm64": { 610 + "version": "0.25.10", 611 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", 612 + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", 613 + "cpu": [ 614 + "arm64" 615 + ], 616 + "dev": true, 617 + "license": "MIT", 618 + "optional": true, 619 + "os": [ 620 + "android" 621 + ], 622 + "engines": { 623 + "node": ">=18" 624 + } 625 + }, 626 + "node_modules/@esbuild/android-x64": { 627 + "version": "0.25.10", 628 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", 629 + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", 630 + "cpu": [ 631 + "x64" 632 + ], 633 + "dev": true, 634 + "license": "MIT", 635 + "optional": true, 636 + "os": [ 637 + "android" 638 + ], 639 + "engines": { 640 + "node": ">=18" 641 + } 642 + }, 643 + "node_modules/@esbuild/darwin-arm64": { 644 + "version": "0.25.10", 645 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", 646 + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", 647 + "cpu": [ 648 + "arm64" 649 + ], 650 + "dev": true, 651 + "license": "MIT", 652 + "optional": true, 653 + "os": [ 654 + "darwin" 655 + ], 656 + "engines": { 657 + "node": ">=18" 658 + } 659 + }, 660 + "node_modules/@esbuild/darwin-x64": { 661 + "version": "0.25.10", 662 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", 663 + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", 664 + "cpu": [ 665 + "x64" 666 + ], 667 + "dev": true, 668 + "license": "MIT", 669 + "optional": true, 670 + "os": [ 671 + "darwin" 672 + ], 673 + "engines": { 674 + "node": ">=18" 675 + } 676 + }, 677 + "node_modules/@esbuild/freebsd-arm64": { 678 + "version": "0.25.10", 679 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", 680 + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", 681 + "cpu": [ 682 + "arm64" 683 + ], 684 + "dev": true, 685 + "license": "MIT", 686 + "optional": true, 687 + "os": [ 688 + "freebsd" 689 + ], 690 + "engines": { 691 + "node": ">=18" 692 + } 693 + }, 694 + "node_modules/@esbuild/freebsd-x64": { 695 + "version": "0.25.10", 696 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", 697 + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", 698 + "cpu": [ 699 + "x64" 700 + ], 701 + "dev": true, 702 + "license": "MIT", 703 + "optional": true, 704 + "os": [ 705 + "freebsd" 706 + ], 707 + "engines": { 708 + "node": ">=18" 709 + } 710 + }, 711 + "node_modules/@esbuild/linux-arm": { 712 + "version": "0.25.10", 713 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", 714 + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", 715 + "cpu": [ 716 + "arm" 717 + ], 718 + "dev": true, 719 + "license": "MIT", 720 + "optional": true, 721 + "os": [ 722 + "linux" 723 + ], 724 + "engines": { 725 + "node": ">=18" 726 + } 727 + }, 728 + "node_modules/@esbuild/linux-arm64": { 729 + "version": "0.25.10", 730 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", 731 + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", 732 + "cpu": [ 733 + "arm64" 734 + ], 735 + "dev": true, 736 + "license": "MIT", 737 + "optional": true, 738 + "os": [ 739 + "linux" 740 + ], 741 + "engines": { 742 + "node": ">=18" 743 + } 744 + }, 745 + "node_modules/@esbuild/linux-ia32": { 746 + "version": "0.25.10", 747 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", 748 + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", 749 + "cpu": [ 750 + "ia32" 751 + ], 752 + "dev": true, 753 + "license": "MIT", 754 + "optional": true, 755 + "os": [ 756 + "linux" 757 + ], 758 + "engines": { 759 + "node": ">=18" 760 + } 761 + }, 762 + "node_modules/@esbuild/linux-loong64": { 763 + "version": "0.25.10", 764 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", 765 + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", 766 + "cpu": [ 767 + "loong64" 768 + ], 769 + "dev": true, 770 + "license": "MIT", 771 + "optional": true, 772 + "os": [ 773 + "linux" 774 + ], 775 + "engines": { 776 + "node": ">=18" 777 + } 778 + }, 779 + "node_modules/@esbuild/linux-mips64el": { 780 + "version": "0.25.10", 781 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", 782 + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", 783 + "cpu": [ 784 + "mips64el" 785 + ], 786 + "dev": true, 787 + "license": "MIT", 788 + "optional": true, 789 + "os": [ 790 + "linux" 791 + ], 792 + "engines": { 793 + "node": ">=18" 794 + } 795 + }, 796 + "node_modules/@esbuild/linux-ppc64": { 797 + "version": "0.25.10", 798 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", 799 + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", 800 + "cpu": [ 801 + "ppc64" 802 + ], 803 + "dev": true, 804 + "license": "MIT", 805 + "optional": true, 806 + "os": [ 807 + "linux" 808 + ], 809 + "engines": { 810 + "node": ">=18" 811 + } 812 + }, 813 + "node_modules/@esbuild/linux-riscv64": { 814 + "version": "0.25.10", 815 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", 816 + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", 817 + "cpu": [ 818 + "riscv64" 819 + ], 820 + "dev": true, 821 + "license": "MIT", 822 + "optional": true, 823 + "os": [ 824 + "linux" 825 + ], 826 + "engines": { 827 + "node": ">=18" 828 + } 829 + }, 830 + "node_modules/@esbuild/linux-s390x": { 831 + "version": "0.25.10", 832 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", 833 + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", 834 + "cpu": [ 835 + "s390x" 836 + ], 837 + "dev": true, 838 + "license": "MIT", 839 + "optional": true, 840 + "os": [ 841 + "linux" 842 + ], 843 + "engines": { 844 + "node": ">=18" 845 + } 846 + }, 847 + "node_modules/@esbuild/linux-x64": { 848 + "version": "0.25.10", 849 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", 850 + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", 851 + "cpu": [ 852 + "x64" 853 + ], 854 + "dev": true, 855 + "license": "MIT", 856 + "optional": true, 857 + "os": [ 858 + "linux" 859 + ], 860 + "engines": { 861 + "node": ">=18" 862 + } 863 + }, 864 + "node_modules/@esbuild/netbsd-arm64": { 865 + "version": "0.25.10", 866 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", 867 + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", 868 + "cpu": [ 869 + "arm64" 870 + ], 871 + "dev": true, 872 + "license": "MIT", 873 + "optional": true, 874 + "os": [ 875 + "netbsd" 876 + ], 877 + "engines": { 878 + "node": ">=18" 879 + } 880 + }, 881 + "node_modules/@esbuild/netbsd-x64": { 882 + "version": "0.25.10", 883 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", 884 + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", 885 + "cpu": [ 886 + "x64" 887 + ], 888 + "dev": true, 889 + "license": "MIT", 890 + "optional": true, 891 + "os": [ 892 + "netbsd" 893 + ], 894 + "engines": { 895 + "node": ">=18" 896 + } 897 + }, 898 + "node_modules/@esbuild/openbsd-arm64": { 899 + "version": "0.25.10", 900 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", 901 + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", 902 + "cpu": [ 903 + "arm64" 904 + ], 905 + "dev": true, 906 + "license": "MIT", 907 + "optional": true, 908 + "os": [ 909 + "openbsd" 910 + ], 911 + "engines": { 912 + "node": ">=18" 913 + } 914 + }, 915 + "node_modules/@esbuild/openbsd-x64": { 916 + "version": "0.25.10", 917 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", 918 + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", 919 + "cpu": [ 920 + "x64" 921 + ], 922 + "dev": true, 923 + "license": "MIT", 924 + "optional": true, 925 + "os": [ 926 + "openbsd" 927 + ], 928 + "engines": { 929 + "node": ">=18" 930 + } 931 + }, 932 + "node_modules/@esbuild/openharmony-arm64": { 933 + "version": "0.25.10", 934 + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", 935 + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", 936 + "cpu": [ 937 + "arm64" 938 + ], 939 + "dev": true, 940 + "license": "MIT", 941 + "optional": true, 942 + "os": [ 943 + "openharmony" 944 + ], 945 + "engines": { 946 + "node": ">=18" 947 + } 948 + }, 949 + "node_modules/@esbuild/sunos-x64": { 950 + "version": "0.25.10", 951 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", 952 + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", 953 + "cpu": [ 954 + "x64" 955 + ], 956 + "dev": true, 957 + "license": "MIT", 958 + "optional": true, 959 + "os": [ 960 + "sunos" 961 + ], 962 + "engines": { 963 + "node": ">=18" 964 + } 965 + }, 966 + "node_modules/@esbuild/win32-arm64": { 967 + "version": "0.25.10", 968 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", 969 + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", 970 + "cpu": [ 971 + "arm64" 972 + ], 973 + "dev": true, 974 + "license": "MIT", 975 + "optional": true, 976 + "os": [ 977 + "win32" 978 + ], 979 + "engines": { 980 + "node": ">=18" 981 + } 982 + }, 983 + "node_modules/@esbuild/win32-ia32": { 984 + "version": "0.25.10", 985 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", 986 + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", 987 + "cpu": [ 988 + "ia32" 989 + ], 990 + "dev": true, 991 + "license": "MIT", 992 + "optional": true, 993 + "os": [ 994 + "win32" 995 + ], 996 + "engines": { 997 + "node": ">=18" 998 + } 999 + }, 1000 + "node_modules/@esbuild/win32-x64": { 1001 + "version": "0.25.10", 1002 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", 1003 + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", 1004 + "cpu": [ 1005 + "x64" 1006 + ], 1007 + "dev": true, 1008 + "license": "MIT", 1009 + "optional": true, 1010 + "os": [ 1011 + "win32" 1012 + ], 1013 + "engines": { 1014 + "node": ">=18" 1015 + } 1016 + }, 1017 + "node_modules/@eslint-community/eslint-utils": { 1018 + "version": "4.9.0", 1019 + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", 1020 + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", 1021 + "dev": true, 1022 + "license": "MIT", 1023 + "dependencies": { 1024 + "eslint-visitor-keys": "^3.4.3" 1025 + }, 1026 + "engines": { 1027 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 1028 + }, 1029 + "funding": { 1030 + "url": "https://opencollective.com/eslint" 1031 + }, 1032 + "peerDependencies": { 1033 + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" 1034 + } 1035 + }, 1036 + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { 1037 + "version": "3.4.3", 1038 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", 1039 + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", 1040 + "dev": true, 1041 + "license": "Apache-2.0", 1042 + "engines": { 1043 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 1044 + }, 1045 + "funding": { 1046 + "url": "https://opencollective.com/eslint" 1047 + } 1048 + }, 1049 + "node_modules/@eslint-community/regexpp": { 1050 + "version": "4.12.1", 1051 + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", 1052 + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", 1053 + "dev": true, 1054 + "license": "MIT", 1055 + "engines": { 1056 + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" 1057 + } 1058 + }, 1059 + "node_modules/@eslint/config-array": { 1060 + "version": "0.21.0", 1061 + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", 1062 + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", 1063 + "dev": true, 1064 + "license": "Apache-2.0", 1065 + "dependencies": { 1066 + "@eslint/object-schema": "^2.1.6", 1067 + "debug": "^4.3.1", 1068 + "minimatch": "^3.1.2" 1069 + }, 1070 + "engines": { 1071 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1072 + } 1073 + }, 1074 + "node_modules/@eslint/config-helpers": { 1075 + "version": "0.3.1", 1076 + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", 1077 + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", 1078 + "dev": true, 1079 + "license": "Apache-2.0", 1080 + "engines": { 1081 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1082 + } 1083 + }, 1084 + "node_modules/@eslint/core": { 1085 + "version": "0.15.2", 1086 + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", 1087 + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", 1088 + "dev": true, 1089 + "license": "Apache-2.0", 1090 + "dependencies": { 1091 + "@types/json-schema": "^7.0.15" 1092 + }, 1093 + "engines": { 1094 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1095 + } 1096 + }, 1097 + "node_modules/@eslint/eslintrc": { 1098 + "version": "3.3.1", 1099 + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", 1100 + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", 1101 + "dev": true, 1102 + "license": "MIT", 1103 + "dependencies": { 1104 + "ajv": "^6.12.4", 1105 + "debug": "^4.3.2", 1106 + "espree": "^10.0.1", 1107 + "globals": "^14.0.0", 1108 + "ignore": "^5.2.0", 1109 + "import-fresh": "^3.2.1", 1110 + "js-yaml": "^4.1.0", 1111 + "minimatch": "^3.1.2", 1112 + "strip-json-comments": "^3.1.1" 1113 + }, 1114 + "engines": { 1115 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1116 + }, 1117 + "funding": { 1118 + "url": "https://opencollective.com/eslint" 1119 + } 1120 + }, 1121 + "node_modules/@eslint/eslintrc/node_modules/globals": { 1122 + "version": "14.0.0", 1123 + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", 1124 + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", 1125 + "dev": true, 1126 + "license": "MIT", 1127 + "engines": { 1128 + "node": ">=18" 1129 + }, 1130 + "funding": { 1131 + "url": "https://github.com/sponsors/sindresorhus" 1132 + } 1133 + }, 1134 + "node_modules/@eslint/js": { 1135 + "version": "9.36.0", 1136 + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.36.0.tgz", 1137 + "integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==", 1138 + "dev": true, 1139 + "license": "MIT", 1140 + "engines": { 1141 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1142 + }, 1143 + "funding": { 1144 + "url": "https://eslint.org/donate" 1145 + } 1146 + }, 1147 + "node_modules/@eslint/object-schema": { 1148 + "version": "2.1.6", 1149 + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", 1150 + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", 1151 + "dev": true, 1152 + "license": "Apache-2.0", 1153 + "engines": { 1154 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1155 + } 1156 + }, 1157 + "node_modules/@eslint/plugin-kit": { 1158 + "version": "0.3.5", 1159 + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", 1160 + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", 1161 + "dev": true, 1162 + "license": "Apache-2.0", 1163 + "dependencies": { 1164 + "@eslint/core": "^0.15.2", 1165 + "levn": "^0.4.1" 1166 + }, 1167 + "engines": { 1168 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1169 + } 1170 + }, 1171 + "node_modules/@humanfs/core": { 1172 + "version": "0.19.1", 1173 + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", 1174 + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", 1175 + "dev": true, 1176 + "license": "Apache-2.0", 1177 + "engines": { 1178 + "node": ">=18.18.0" 1179 + } 1180 + }, 1181 + "node_modules/@humanfs/node": { 1182 + "version": "0.16.7", 1183 + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", 1184 + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", 1185 + "dev": true, 1186 + "license": "Apache-2.0", 1187 + "dependencies": { 1188 + "@humanfs/core": "^0.19.1", 1189 + "@humanwhocodes/retry": "^0.4.0" 1190 + }, 1191 + "engines": { 1192 + "node": ">=18.18.0" 1193 + } 1194 + }, 1195 + "node_modules/@humanwhocodes/module-importer": { 1196 + "version": "1.0.1", 1197 + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", 1198 + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", 1199 + "dev": true, 1200 + "license": "Apache-2.0", 1201 + "engines": { 1202 + "node": ">=12.22" 1203 + }, 1204 + "funding": { 1205 + "type": "github", 1206 + "url": "https://github.com/sponsors/nzakas" 1207 + } 1208 + }, 1209 + "node_modules/@humanwhocodes/retry": { 1210 + "version": "0.4.3", 1211 + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", 1212 + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", 1213 + "dev": true, 1214 + "license": "Apache-2.0", 1215 + "engines": { 1216 + "node": ">=18.18" 1217 + }, 1218 + "funding": { 1219 + "type": "github", 1220 + "url": "https://github.com/sponsors/nzakas" 1221 + } 1222 + }, 1223 + "node_modules/@isaacs/fs-minipass": { 1224 + "version": "4.0.1", 1225 + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", 1226 + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", 1227 + "dev": true, 1228 + "license": "ISC", 1229 + "dependencies": { 1230 + "minipass": "^7.0.4" 1231 + }, 1232 + "engines": { 1233 + "node": ">=18.0.0" 1234 + } 1235 + }, 1236 + "node_modules/@jridgewell/gen-mapping": { 1237 + "version": "0.3.13", 1238 + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", 1239 + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", 1240 + "dev": true, 1241 + "license": "MIT", 1242 + "dependencies": { 1243 + "@jridgewell/sourcemap-codec": "^1.5.0", 1244 + "@jridgewell/trace-mapping": "^0.3.24" 1245 + } 1246 + }, 1247 + "node_modules/@jridgewell/remapping": { 1248 + "version": "2.3.5", 1249 + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", 1250 + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", 1251 + "dev": true, 1252 + "license": "MIT", 1253 + "dependencies": { 1254 + "@jridgewell/gen-mapping": "^0.3.5", 1255 + "@jridgewell/trace-mapping": "^0.3.24" 1256 + } 1257 + }, 1258 + "node_modules/@jridgewell/resolve-uri": { 1259 + "version": "3.1.2", 1260 + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 1261 + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 1262 + "dev": true, 1263 + "license": "MIT", 1264 + "engines": { 1265 + "node": ">=6.0.0" 1266 + } 1267 + }, 1268 + "node_modules/@jridgewell/sourcemap-codec": { 1269 + "version": "1.5.5", 1270 + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", 1271 + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", 1272 + "dev": true, 1273 + "license": "MIT" 1274 + }, 1275 + "node_modules/@jridgewell/trace-mapping": { 1276 + "version": "0.3.31", 1277 + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", 1278 + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", 1279 + "dev": true, 1280 + "license": "MIT", 1281 + "dependencies": { 1282 + "@jridgewell/resolve-uri": "^3.1.0", 1283 + "@jridgewell/sourcemap-codec": "^1.4.14" 1284 + } 1285 + }, 1286 + "node_modules/@noble/curves": { 1287 + "version": "1.9.7", 1288 + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", 1289 + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", 1290 + "license": "MIT", 1291 + "dependencies": { 1292 + "@noble/hashes": "1.8.0" 1293 + }, 1294 + "engines": { 1295 + "node": "^14.21.3 || >=16" 1296 + }, 1297 + "funding": { 1298 + "url": "https://paulmillr.com/funding/" 1299 + } 1300 + }, 1301 + "node_modules/@noble/hashes": { 1302 + "version": "1.8.0", 1303 + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", 1304 + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", 1305 + "license": "MIT", 1306 + "engines": { 1307 + "node": "^14.21.3 || >=16" 1308 + }, 1309 + "funding": { 1310 + "url": "https://paulmillr.com/funding/" 1311 + } 1312 + }, 1313 + "node_modules/@nodelib/fs.scandir": { 1314 + "version": "2.1.5", 1315 + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", 1316 + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", 1317 + "dev": true, 1318 + "license": "MIT", 1319 + "dependencies": { 1320 + "@nodelib/fs.stat": "2.0.5", 1321 + "run-parallel": "^1.1.9" 1322 + }, 1323 + "engines": { 1324 + "node": ">= 8" 1325 + } 1326 + }, 1327 + "node_modules/@nodelib/fs.stat": { 1328 + "version": "2.0.5", 1329 + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", 1330 + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", 1331 + "dev": true, 1332 + "license": "MIT", 1333 + "engines": { 1334 + "node": ">= 8" 1335 + } 1336 + }, 1337 + "node_modules/@nodelib/fs.walk": { 1338 + "version": "1.2.8", 1339 + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", 1340 + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", 1341 + "dev": true, 1342 + "license": "MIT", 1343 + "dependencies": { 1344 + "@nodelib/fs.scandir": "2.1.5", 1345 + "fastq": "^1.6.0" 1346 + }, 1347 + "engines": { 1348 + "node": ">= 8" 1349 + } 1350 + }, 1351 + "node_modules/@rolldown/pluginutils": { 1352 + "version": "1.0.0-beta.38", 1353 + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz", 1354 + "integrity": "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==", 1355 + "dev": true, 1356 + "license": "MIT" 1357 + }, 1358 + "node_modules/@rollup/rollup-android-arm-eabi": { 1359 + "version": "4.52.4", 1360 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", 1361 + "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", 1362 + "cpu": [ 1363 + "arm" 1364 + ], 1365 + "dev": true, 1366 + "license": "MIT", 1367 + "optional": true, 1368 + "os": [ 1369 + "android" 1370 + ] 1371 + }, 1372 + "node_modules/@rollup/rollup-android-arm64": { 1373 + "version": "4.52.4", 1374 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", 1375 + "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", 1376 + "cpu": [ 1377 + "arm64" 1378 + ], 1379 + "dev": true, 1380 + "license": "MIT", 1381 + "optional": true, 1382 + "os": [ 1383 + "android" 1384 + ] 1385 + }, 1386 + "node_modules/@rollup/rollup-darwin-arm64": { 1387 + "version": "4.52.4", 1388 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", 1389 + "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", 1390 + "cpu": [ 1391 + "arm64" 1392 + ], 1393 + "dev": true, 1394 + "license": "MIT", 1395 + "optional": true, 1396 + "os": [ 1397 + "darwin" 1398 + ] 1399 + }, 1400 + "node_modules/@rollup/rollup-darwin-x64": { 1401 + "version": "4.52.4", 1402 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", 1403 + "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", 1404 + "cpu": [ 1405 + "x64" 1406 + ], 1407 + "dev": true, 1408 + "license": "MIT", 1409 + "optional": true, 1410 + "os": [ 1411 + "darwin" 1412 + ] 1413 + }, 1414 + "node_modules/@rollup/rollup-freebsd-arm64": { 1415 + "version": "4.52.4", 1416 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", 1417 + "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", 1418 + "cpu": [ 1419 + "arm64" 1420 + ], 1421 + "dev": true, 1422 + "license": "MIT", 1423 + "optional": true, 1424 + "os": [ 1425 + "freebsd" 1426 + ] 1427 + }, 1428 + "node_modules/@rollup/rollup-freebsd-x64": { 1429 + "version": "4.52.4", 1430 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", 1431 + "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", 1432 + "cpu": [ 1433 + "x64" 1434 + ], 1435 + "dev": true, 1436 + "license": "MIT", 1437 + "optional": true, 1438 + "os": [ 1439 + "freebsd" 1440 + ] 1441 + }, 1442 + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { 1443 + "version": "4.52.4", 1444 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", 1445 + "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", 1446 + "cpu": [ 1447 + "arm" 1448 + ], 1449 + "dev": true, 1450 + "license": "MIT", 1451 + "optional": true, 1452 + "os": [ 1453 + "linux" 1454 + ] 1455 + }, 1456 + "node_modules/@rollup/rollup-linux-arm-musleabihf": { 1457 + "version": "4.52.4", 1458 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", 1459 + "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", 1460 + "cpu": [ 1461 + "arm" 1462 + ], 1463 + "dev": true, 1464 + "license": "MIT", 1465 + "optional": true, 1466 + "os": [ 1467 + "linux" 1468 + ] 1469 + }, 1470 + "node_modules/@rollup/rollup-linux-arm64-gnu": { 1471 + "version": "4.52.4", 1472 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", 1473 + "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", 1474 + "cpu": [ 1475 + "arm64" 1476 + ], 1477 + "dev": true, 1478 + "license": "MIT", 1479 + "optional": true, 1480 + "os": [ 1481 + "linux" 1482 + ] 1483 + }, 1484 + "node_modules/@rollup/rollup-linux-arm64-musl": { 1485 + "version": "4.52.4", 1486 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", 1487 + "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", 1488 + "cpu": [ 1489 + "arm64" 1490 + ], 1491 + "dev": true, 1492 + "license": "MIT", 1493 + "optional": true, 1494 + "os": [ 1495 + "linux" 1496 + ] 1497 + }, 1498 + "node_modules/@rollup/rollup-linux-loong64-gnu": { 1499 + "version": "4.52.4", 1500 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", 1501 + "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", 1502 + "cpu": [ 1503 + "loong64" 1504 + ], 1505 + "dev": true, 1506 + "license": "MIT", 1507 + "optional": true, 1508 + "os": [ 1509 + "linux" 1510 + ] 1511 + }, 1512 + "node_modules/@rollup/rollup-linux-ppc64-gnu": { 1513 + "version": "4.52.4", 1514 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", 1515 + "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", 1516 + "cpu": [ 1517 + "ppc64" 1518 + ], 1519 + "dev": true, 1520 + "license": "MIT", 1521 + "optional": true, 1522 + "os": [ 1523 + "linux" 1524 + ] 1525 + }, 1526 + "node_modules/@rollup/rollup-linux-riscv64-gnu": { 1527 + "version": "4.52.4", 1528 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", 1529 + "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", 1530 + "cpu": [ 1531 + "riscv64" 1532 + ], 1533 + "dev": true, 1534 + "license": "MIT", 1535 + "optional": true, 1536 + "os": [ 1537 + "linux" 1538 + ] 1539 + }, 1540 + "node_modules/@rollup/rollup-linux-riscv64-musl": { 1541 + "version": "4.52.4", 1542 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", 1543 + "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", 1544 + "cpu": [ 1545 + "riscv64" 1546 + ], 1547 + "dev": true, 1548 + "license": "MIT", 1549 + "optional": true, 1550 + "os": [ 1551 + "linux" 1552 + ] 1553 + }, 1554 + "node_modules/@rollup/rollup-linux-s390x-gnu": { 1555 + "version": "4.52.4", 1556 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", 1557 + "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", 1558 + "cpu": [ 1559 + "s390x" 1560 + ], 1561 + "dev": true, 1562 + "license": "MIT", 1563 + "optional": true, 1564 + "os": [ 1565 + "linux" 1566 + ] 1567 + }, 1568 + "node_modules/@rollup/rollup-linux-x64-gnu": { 1569 + "version": "4.52.4", 1570 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", 1571 + "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", 1572 + "cpu": [ 1573 + "x64" 1574 + ], 1575 + "dev": true, 1576 + "license": "MIT", 1577 + "optional": true, 1578 + "os": [ 1579 + "linux" 1580 + ] 1581 + }, 1582 + "node_modules/@rollup/rollup-linux-x64-musl": { 1583 + "version": "4.52.4", 1584 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz", 1585 + "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==", 1586 + "cpu": [ 1587 + "x64" 1588 + ], 1589 + "dev": true, 1590 + "license": "MIT", 1591 + "optional": true, 1592 + "os": [ 1593 + "linux" 1594 + ] 1595 + }, 1596 + "node_modules/@rollup/rollup-openharmony-arm64": { 1597 + "version": "4.52.4", 1598 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz", 1599 + "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==", 1600 + "cpu": [ 1601 + "arm64" 1602 + ], 1603 + "dev": true, 1604 + "license": "MIT", 1605 + "optional": true, 1606 + "os": [ 1607 + "openharmony" 1608 + ] 1609 + }, 1610 + "node_modules/@rollup/rollup-win32-arm64-msvc": { 1611 + "version": "4.52.4", 1612 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz", 1613 + "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==", 1614 + "cpu": [ 1615 + "arm64" 1616 + ], 1617 + "dev": true, 1618 + "license": "MIT", 1619 + "optional": true, 1620 + "os": [ 1621 + "win32" 1622 + ] 1623 + }, 1624 + "node_modules/@rollup/rollup-win32-ia32-msvc": { 1625 + "version": "4.52.4", 1626 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz", 1627 + "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==", 1628 + "cpu": [ 1629 + "ia32" 1630 + ], 1631 + "dev": true, 1632 + "license": "MIT", 1633 + "optional": true, 1634 + "os": [ 1635 + "win32" 1636 + ] 1637 + }, 1638 + "node_modules/@rollup/rollup-win32-x64-gnu": { 1639 + "version": "4.52.4", 1640 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz", 1641 + "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==", 1642 + "cpu": [ 1643 + "x64" 1644 + ], 1645 + "dev": true, 1646 + "license": "MIT", 1647 + "optional": true, 1648 + "os": [ 1649 + "win32" 1650 + ] 1651 + }, 1652 + "node_modules/@rollup/rollup-win32-x64-msvc": { 1653 + "version": "4.52.4", 1654 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz", 1655 + "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==", 1656 + "cpu": [ 1657 + "x64" 1658 + ], 1659 + "dev": true, 1660 + "license": "MIT", 1661 + "optional": true, 1662 + "os": [ 1663 + "win32" 1664 + ] 1665 + }, 1666 + "node_modules/@tailwindcss/node": { 1667 + "version": "4.1.14", 1668 + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.14.tgz", 1669 + "integrity": "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==", 1670 + "dev": true, 1671 + "license": "MIT", 1672 + "dependencies": { 1673 + "@jridgewell/remapping": "^2.3.4", 1674 + "enhanced-resolve": "^5.18.3", 1675 + "jiti": "^2.6.0", 1676 + "lightningcss": "1.30.1", 1677 + "magic-string": "^0.30.19", 1678 + "source-map-js": "^1.2.1", 1679 + "tailwindcss": "4.1.14" 1680 + } 1681 + }, 1682 + "node_modules/@tailwindcss/oxide": { 1683 + "version": "4.1.14", 1684 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.14.tgz", 1685 + "integrity": "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==", 1686 + "dev": true, 1687 + "hasInstallScript": true, 1688 + "license": "MIT", 1689 + "dependencies": { 1690 + "detect-libc": "^2.0.4", 1691 + "tar": "^7.5.1" 1692 + }, 1693 + "engines": { 1694 + "node": ">= 10" 1695 + }, 1696 + "optionalDependencies": { 1697 + "@tailwindcss/oxide-android-arm64": "4.1.14", 1698 + "@tailwindcss/oxide-darwin-arm64": "4.1.14", 1699 + "@tailwindcss/oxide-darwin-x64": "4.1.14", 1700 + "@tailwindcss/oxide-freebsd-x64": "4.1.14", 1701 + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14", 1702 + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.14", 1703 + "@tailwindcss/oxide-linux-arm64-musl": "4.1.14", 1704 + "@tailwindcss/oxide-linux-x64-gnu": "4.1.14", 1705 + "@tailwindcss/oxide-linux-x64-musl": "4.1.14", 1706 + "@tailwindcss/oxide-wasm32-wasi": "4.1.14", 1707 + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.14", 1708 + "@tailwindcss/oxide-win32-x64-msvc": "4.1.14" 1709 + } 1710 + }, 1711 + "node_modules/@tailwindcss/oxide-android-arm64": { 1712 + "version": "4.1.14", 1713 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.14.tgz", 1714 + "integrity": "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==", 1715 + "cpu": [ 1716 + "arm64" 1717 + ], 1718 + "dev": true, 1719 + "license": "MIT", 1720 + "optional": true, 1721 + "os": [ 1722 + "android" 1723 + ], 1724 + "engines": { 1725 + "node": ">= 10" 1726 + } 1727 + }, 1728 + "node_modules/@tailwindcss/oxide-darwin-arm64": { 1729 + "version": "4.1.14", 1730 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.14.tgz", 1731 + "integrity": "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==", 1732 + "cpu": [ 1733 + "arm64" 1734 + ], 1735 + "dev": true, 1736 + "license": "MIT", 1737 + "optional": true, 1738 + "os": [ 1739 + "darwin" 1740 + ], 1741 + "engines": { 1742 + "node": ">= 10" 1743 + } 1744 + }, 1745 + "node_modules/@tailwindcss/oxide-darwin-x64": { 1746 + "version": "4.1.14", 1747 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.14.tgz", 1748 + "integrity": "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==", 1749 + "cpu": [ 1750 + "x64" 1751 + ], 1752 + "dev": true, 1753 + "license": "MIT", 1754 + "optional": true, 1755 + "os": [ 1756 + "darwin" 1757 + ], 1758 + "engines": { 1759 + "node": ">= 10" 1760 + } 1761 + }, 1762 + "node_modules/@tailwindcss/oxide-freebsd-x64": { 1763 + "version": "4.1.14", 1764 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.14.tgz", 1765 + "integrity": "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==", 1766 + "cpu": [ 1767 + "x64" 1768 + ], 1769 + "dev": true, 1770 + "license": "MIT", 1771 + "optional": true, 1772 + "os": [ 1773 + "freebsd" 1774 + ], 1775 + "engines": { 1776 + "node": ">= 10" 1777 + } 1778 + }, 1779 + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { 1780 + "version": "4.1.14", 1781 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.14.tgz", 1782 + "integrity": "sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw==", 1783 + "cpu": [ 1784 + "arm" 1785 + ], 1786 + "dev": true, 1787 + "license": "MIT", 1788 + "optional": true, 1789 + "os": [ 1790 + "linux" 1791 + ], 1792 + "engines": { 1793 + "node": ">= 10" 1794 + } 1795 + }, 1796 + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { 1797 + "version": "4.1.14", 1798 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.14.tgz", 1799 + "integrity": "sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==", 1800 + "cpu": [ 1801 + "arm64" 1802 + ], 1803 + "dev": true, 1804 + "license": "MIT", 1805 + "optional": true, 1806 + "os": [ 1807 + "linux" 1808 + ], 1809 + "engines": { 1810 + "node": ">= 10" 1811 + } 1812 + }, 1813 + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { 1814 + "version": "4.1.14", 1815 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.14.tgz", 1816 + "integrity": "sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==", 1817 + "cpu": [ 1818 + "arm64" 1819 + ], 1820 + "dev": true, 1821 + "license": "MIT", 1822 + "optional": true, 1823 + "os": [ 1824 + "linux" 1825 + ], 1826 + "engines": { 1827 + "node": ">= 10" 1828 + } 1829 + }, 1830 + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { 1831 + "version": "4.1.14", 1832 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.14.tgz", 1833 + "integrity": "sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==", 1834 + "cpu": [ 1835 + "x64" 1836 + ], 1837 + "dev": true, 1838 + "license": "MIT", 1839 + "optional": true, 1840 + "os": [ 1841 + "linux" 1842 + ], 1843 + "engines": { 1844 + "node": ">= 10" 1845 + } 1846 + }, 1847 + "node_modules/@tailwindcss/oxide-linux-x64-musl": { 1848 + "version": "4.1.14", 1849 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.14.tgz", 1850 + "integrity": "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==", 1851 + "cpu": [ 1852 + "x64" 1853 + ], 1854 + "dev": true, 1855 + "license": "MIT", 1856 + "optional": true, 1857 + "os": [ 1858 + "linux" 1859 + ], 1860 + "engines": { 1861 + "node": ">= 10" 1862 + } 1863 + }, 1864 + "node_modules/@tailwindcss/oxide-wasm32-wasi": { 1865 + "version": "4.1.14", 1866 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.14.tgz", 1867 + "integrity": "sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==", 1868 + "bundleDependencies": [ 1869 + "@napi-rs/wasm-runtime", 1870 + "@emnapi/core", 1871 + "@emnapi/runtime", 1872 + "@tybys/wasm-util", 1873 + "@emnapi/wasi-threads", 1874 + "tslib" 1875 + ], 1876 + "cpu": [ 1877 + "wasm32" 1878 + ], 1879 + "dev": true, 1880 + "license": "MIT", 1881 + "optional": true, 1882 + "dependencies": { 1883 + "@emnapi/core": "^1.5.0", 1884 + "@emnapi/runtime": "^1.5.0", 1885 + "@emnapi/wasi-threads": "^1.1.0", 1886 + "@napi-rs/wasm-runtime": "^1.0.5", 1887 + "@tybys/wasm-util": "^0.10.1", 1888 + "tslib": "^2.4.0" 1889 + }, 1890 + "engines": { 1891 + "node": ">=14.0.0" 1892 + } 1893 + }, 1894 + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { 1895 + "version": "4.1.14", 1896 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.14.tgz", 1897 + "integrity": "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==", 1898 + "cpu": [ 1899 + "arm64" 1900 + ], 1901 + "dev": true, 1902 + "license": "MIT", 1903 + "optional": true, 1904 + "os": [ 1905 + "win32" 1906 + ], 1907 + "engines": { 1908 + "node": ">= 10" 1909 + } 1910 + }, 1911 + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { 1912 + "version": "4.1.14", 1913 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.14.tgz", 1914 + "integrity": "sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA==", 1915 + "cpu": [ 1916 + "x64" 1917 + ], 1918 + "dev": true, 1919 + "license": "MIT", 1920 + "optional": true, 1921 + "os": [ 1922 + "win32" 1923 + ], 1924 + "engines": { 1925 + "node": ">= 10" 1926 + } 1927 + }, 1928 + "node_modules/@tailwindcss/postcss": { 1929 + "version": "4.1.14", 1930 + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.14.tgz", 1931 + "integrity": "sha512-BdMjIxy7HUNThK87C7BC8I1rE8BVUsfNQSI5siQ4JK3iIa3w0XyVvVL9SXLWO//CtYTcp1v7zci0fYwJOjB+Zg==", 1932 + "dev": true, 1933 + "license": "MIT", 1934 + "dependencies": { 1935 + "@alloc/quick-lru": "^5.2.0", 1936 + "@tailwindcss/node": "4.1.14", 1937 + "@tailwindcss/oxide": "4.1.14", 1938 + "postcss": "^8.4.41", 1939 + "tailwindcss": "4.1.14" 1940 + } 1941 + }, 1942 + "node_modules/@types/babel__core": { 1943 + "version": "7.20.5", 1944 + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", 1945 + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", 1946 + "dev": true, 1947 + "license": "MIT", 1948 + "dependencies": { 1949 + "@babel/parser": "^7.20.7", 1950 + "@babel/types": "^7.20.7", 1951 + "@types/babel__generator": "*", 1952 + "@types/babel__template": "*", 1953 + "@types/babel__traverse": "*" 1954 + } 1955 + }, 1956 + "node_modules/@types/babel__generator": { 1957 + "version": "7.27.0", 1958 + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", 1959 + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", 1960 + "dev": true, 1961 + "license": "MIT", 1962 + "dependencies": { 1963 + "@babel/types": "^7.0.0" 1964 + } 1965 + }, 1966 + "node_modules/@types/babel__template": { 1967 + "version": "7.4.4", 1968 + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", 1969 + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", 1970 + "dev": true, 1971 + "license": "MIT", 1972 + "dependencies": { 1973 + "@babel/parser": "^7.1.0", 1974 + "@babel/types": "^7.0.0" 1975 + } 1976 + }, 1977 + "node_modules/@types/babel__traverse": { 1978 + "version": "7.28.0", 1979 + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", 1980 + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", 1981 + "dev": true, 1982 + "license": "MIT", 1983 + "dependencies": { 1984 + "@babel/types": "^7.28.2" 1985 + } 1986 + }, 1987 + "node_modules/@types/estree": { 1988 + "version": "1.0.8", 1989 + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", 1990 + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", 1991 + "dev": true, 1992 + "license": "MIT" 1993 + }, 1994 + "node_modules/@types/json-schema": { 1995 + "version": "7.0.15", 1996 + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", 1997 + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", 1998 + "dev": true, 1999 + "license": "MIT" 2000 + }, 2001 + "node_modules/@types/node": { 2002 + "version": "24.6.2", 2003 + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.2.tgz", 2004 + "integrity": "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==", 2005 + "dev": true, 2006 + "license": "MIT", 2007 + "dependencies": { 2008 + "undici-types": "~7.13.0" 2009 + } 2010 + }, 2011 + "node_modules/@types/react": { 2012 + "version": "19.2.0", 2013 + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz", 2014 + "integrity": "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==", 2015 + "dev": true, 2016 + "license": "MIT", 2017 + "dependencies": { 2018 + "csstype": "^3.0.2" 2019 + } 2020 + }, 2021 + "node_modules/@types/react-dom": { 2022 + "version": "19.2.0", 2023 + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.0.tgz", 2024 + "integrity": "sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg==", 2025 + "dev": true, 2026 + "license": "MIT", 2027 + "peerDependencies": { 2028 + "@types/react": "^19.2.0" 2029 + } 2030 + }, 2031 + "node_modules/@typescript-eslint/eslint-plugin": { 2032 + "version": "8.45.0", 2033 + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.45.0.tgz", 2034 + "integrity": "sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==", 2035 + "dev": true, 2036 + "license": "MIT", 2037 + "dependencies": { 2038 + "@eslint-community/regexpp": "^4.10.0", 2039 + "@typescript-eslint/scope-manager": "8.45.0", 2040 + "@typescript-eslint/type-utils": "8.45.0", 2041 + "@typescript-eslint/utils": "8.45.0", 2042 + "@typescript-eslint/visitor-keys": "8.45.0", 2043 + "graphemer": "^1.4.0", 2044 + "ignore": "^7.0.0", 2045 + "natural-compare": "^1.4.0", 2046 + "ts-api-utils": "^2.1.0" 2047 + }, 2048 + "engines": { 2049 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2050 + }, 2051 + "funding": { 2052 + "type": "opencollective", 2053 + "url": "https://opencollective.com/typescript-eslint" 2054 + }, 2055 + "peerDependencies": { 2056 + "@typescript-eslint/parser": "^8.45.0", 2057 + "eslint": "^8.57.0 || ^9.0.0", 2058 + "typescript": ">=4.8.4 <6.0.0" 2059 + } 2060 + }, 2061 + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { 2062 + "version": "7.0.5", 2063 + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", 2064 + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", 2065 + "dev": true, 2066 + "license": "MIT", 2067 + "engines": { 2068 + "node": ">= 4" 2069 + } 2070 + }, 2071 + "node_modules/@typescript-eslint/parser": { 2072 + "version": "8.45.0", 2073 + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.45.0.tgz", 2074 + "integrity": "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==", 2075 + "dev": true, 2076 + "license": "MIT", 2077 + "dependencies": { 2078 + "@typescript-eslint/scope-manager": "8.45.0", 2079 + "@typescript-eslint/types": "8.45.0", 2080 + "@typescript-eslint/typescript-estree": "8.45.0", 2081 + "@typescript-eslint/visitor-keys": "8.45.0", 2082 + "debug": "^4.3.4" 2083 + }, 2084 + "engines": { 2085 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2086 + }, 2087 + "funding": { 2088 + "type": "opencollective", 2089 + "url": "https://opencollective.com/typescript-eslint" 2090 + }, 2091 + "peerDependencies": { 2092 + "eslint": "^8.57.0 || ^9.0.0", 2093 + "typescript": ">=4.8.4 <6.0.0" 2094 + } 2095 + }, 2096 + "node_modules/@typescript-eslint/project-service": { 2097 + "version": "8.45.0", 2098 + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.45.0.tgz", 2099 + "integrity": "sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg==", 2100 + "dev": true, 2101 + "license": "MIT", 2102 + "dependencies": { 2103 + "@typescript-eslint/tsconfig-utils": "^8.45.0", 2104 + "@typescript-eslint/types": "^8.45.0", 2105 + "debug": "^4.3.4" 2106 + }, 2107 + "engines": { 2108 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2109 + }, 2110 + "funding": { 2111 + "type": "opencollective", 2112 + "url": "https://opencollective.com/typescript-eslint" 2113 + }, 2114 + "peerDependencies": { 2115 + "typescript": ">=4.8.4 <6.0.0" 2116 + } 2117 + }, 2118 + "node_modules/@typescript-eslint/scope-manager": { 2119 + "version": "8.45.0", 2120 + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.45.0.tgz", 2121 + "integrity": "sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA==", 2122 + "dev": true, 2123 + "license": "MIT", 2124 + "dependencies": { 2125 + "@typescript-eslint/types": "8.45.0", 2126 + "@typescript-eslint/visitor-keys": "8.45.0" 2127 + }, 2128 + "engines": { 2129 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2130 + }, 2131 + "funding": { 2132 + "type": "opencollective", 2133 + "url": "https://opencollective.com/typescript-eslint" 2134 + } 2135 + }, 2136 + "node_modules/@typescript-eslint/tsconfig-utils": { 2137 + "version": "8.45.0", 2138 + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.45.0.tgz", 2139 + "integrity": "sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w==", 2140 + "dev": true, 2141 + "license": "MIT", 2142 + "engines": { 2143 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2144 + }, 2145 + "funding": { 2146 + "type": "opencollective", 2147 + "url": "https://opencollective.com/typescript-eslint" 2148 + }, 2149 + "peerDependencies": { 2150 + "typescript": ">=4.8.4 <6.0.0" 2151 + } 2152 + }, 2153 + "node_modules/@typescript-eslint/type-utils": { 2154 + "version": "8.45.0", 2155 + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.45.0.tgz", 2156 + "integrity": "sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A==", 2157 + "dev": true, 2158 + "license": "MIT", 2159 + "dependencies": { 2160 + "@typescript-eslint/types": "8.45.0", 2161 + "@typescript-eslint/typescript-estree": "8.45.0", 2162 + "@typescript-eslint/utils": "8.45.0", 2163 + "debug": "^4.3.4", 2164 + "ts-api-utils": "^2.1.0" 2165 + }, 2166 + "engines": { 2167 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2168 + }, 2169 + "funding": { 2170 + "type": "opencollective", 2171 + "url": "https://opencollective.com/typescript-eslint" 2172 + }, 2173 + "peerDependencies": { 2174 + "eslint": "^8.57.0 || ^9.0.0", 2175 + "typescript": ">=4.8.4 <6.0.0" 2176 + } 2177 + }, 2178 + "node_modules/@typescript-eslint/types": { 2179 + "version": "8.45.0", 2180 + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.45.0.tgz", 2181 + "integrity": "sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA==", 2182 + "dev": true, 2183 + "license": "MIT", 2184 + "engines": { 2185 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2186 + }, 2187 + "funding": { 2188 + "type": "opencollective", 2189 + "url": "https://opencollective.com/typescript-eslint" 2190 + } 2191 + }, 2192 + "node_modules/@typescript-eslint/typescript-estree": { 2193 + "version": "8.45.0", 2194 + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.45.0.tgz", 2195 + "integrity": "sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA==", 2196 + "dev": true, 2197 + "license": "MIT", 2198 + "dependencies": { 2199 + "@typescript-eslint/project-service": "8.45.0", 2200 + "@typescript-eslint/tsconfig-utils": "8.45.0", 2201 + "@typescript-eslint/types": "8.45.0", 2202 + "@typescript-eslint/visitor-keys": "8.45.0", 2203 + "debug": "^4.3.4", 2204 + "fast-glob": "^3.3.2", 2205 + "is-glob": "^4.0.3", 2206 + "minimatch": "^9.0.4", 2207 + "semver": "^7.6.0", 2208 + "ts-api-utils": "^2.1.0" 2209 + }, 2210 + "engines": { 2211 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2212 + }, 2213 + "funding": { 2214 + "type": "opencollective", 2215 + "url": "https://opencollective.com/typescript-eslint" 2216 + }, 2217 + "peerDependencies": { 2218 + "typescript": ">=4.8.4 <6.0.0" 2219 + } 2220 + }, 2221 + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { 2222 + "version": "2.0.2", 2223 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", 2224 + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", 2225 + "dev": true, 2226 + "license": "MIT", 2227 + "dependencies": { 2228 + "balanced-match": "^1.0.0" 2229 + } 2230 + }, 2231 + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { 2232 + "version": "9.0.5", 2233 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", 2234 + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", 2235 + "dev": true, 2236 + "license": "ISC", 2237 + "dependencies": { 2238 + "brace-expansion": "^2.0.1" 2239 + }, 2240 + "engines": { 2241 + "node": ">=16 || 14 >=14.17" 2242 + }, 2243 + "funding": { 2244 + "url": "https://github.com/sponsors/isaacs" 2245 + } 2246 + }, 2247 + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { 2248 + "version": "7.7.2", 2249 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", 2250 + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", 2251 + "dev": true, 2252 + "license": "ISC", 2253 + "bin": { 2254 + "semver": "bin/semver.js" 2255 + }, 2256 + "engines": { 2257 + "node": ">=10" 2258 + } 2259 + }, 2260 + "node_modules/@typescript-eslint/utils": { 2261 + "version": "8.45.0", 2262 + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.45.0.tgz", 2263 + "integrity": "sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg==", 2264 + "dev": true, 2265 + "license": "MIT", 2266 + "dependencies": { 2267 + "@eslint-community/eslint-utils": "^4.7.0", 2268 + "@typescript-eslint/scope-manager": "8.45.0", 2269 + "@typescript-eslint/types": "8.45.0", 2270 + "@typescript-eslint/typescript-estree": "8.45.0" 2271 + }, 2272 + "engines": { 2273 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2274 + }, 2275 + "funding": { 2276 + "type": "opencollective", 2277 + "url": "https://opencollective.com/typescript-eslint" 2278 + }, 2279 + "peerDependencies": { 2280 + "eslint": "^8.57.0 || ^9.0.0", 2281 + "typescript": ">=4.8.4 <6.0.0" 2282 + } 2283 + }, 2284 + "node_modules/@typescript-eslint/visitor-keys": { 2285 + "version": "8.45.0", 2286 + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.45.0.tgz", 2287 + "integrity": "sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==", 2288 + "dev": true, 2289 + "license": "MIT", 2290 + "dependencies": { 2291 + "@typescript-eslint/types": "8.45.0", 2292 + "eslint-visitor-keys": "^4.2.1" 2293 + }, 2294 + "engines": { 2295 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2296 + }, 2297 + "funding": { 2298 + "type": "opencollective", 2299 + "url": "https://opencollective.com/typescript-eslint" 2300 + } 2301 + }, 2302 + "node_modules/@vitejs/plugin-react": { 2303 + "version": "5.0.4", 2304 + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.4.tgz", 2305 + "integrity": "sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA==", 2306 + "dev": true, 2307 + "license": "MIT", 2308 + "dependencies": { 2309 + "@babel/core": "^7.28.4", 2310 + "@babel/plugin-transform-react-jsx-self": "^7.27.1", 2311 + "@babel/plugin-transform-react-jsx-source": "^7.27.1", 2312 + "@rolldown/pluginutils": "1.0.0-beta.38", 2313 + "@types/babel__core": "^7.20.5", 2314 + "react-refresh": "^0.17.0" 2315 + }, 2316 + "engines": { 2317 + "node": "^20.19.0 || >=22.12.0" 2318 + }, 2319 + "peerDependencies": { 2320 + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" 2321 + } 2322 + }, 2323 + "node_modules/acorn": { 2324 + "version": "8.15.0", 2325 + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", 2326 + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", 2327 + "dev": true, 2328 + "license": "MIT", 2329 + "bin": { 2330 + "acorn": "bin/acorn" 2331 + }, 2332 + "engines": { 2333 + "node": ">=0.4.0" 2334 + } 2335 + }, 2336 + "node_modules/acorn-jsx": { 2337 + "version": "5.3.2", 2338 + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", 2339 + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", 2340 + "dev": true, 2341 + "license": "MIT", 2342 + "peerDependencies": { 2343 + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" 2344 + } 2345 + }, 2346 + "node_modules/ajv": { 2347 + "version": "6.12.6", 2348 + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 2349 + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 2350 + "dev": true, 2351 + "license": "MIT", 2352 + "dependencies": { 2353 + "fast-deep-equal": "^3.1.1", 2354 + "fast-json-stable-stringify": "^2.0.0", 2355 + "json-schema-traverse": "^0.4.1", 2356 + "uri-js": "^4.2.2" 2357 + }, 2358 + "funding": { 2359 + "type": "github", 2360 + "url": "https://github.com/sponsors/epoberezkin" 2361 + } 2362 + }, 2363 + "node_modules/ansi-styles": { 2364 + "version": "4.3.0", 2365 + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 2366 + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 2367 + "dev": true, 2368 + "license": "MIT", 2369 + "dependencies": { 2370 + "color-convert": "^2.0.1" 2371 + }, 2372 + "engines": { 2373 + "node": ">=8" 2374 + }, 2375 + "funding": { 2376 + "url": "https://github.com/chalk/ansi-styles?sponsor=1" 2377 + } 2378 + }, 2379 + "node_modules/argparse": { 2380 + "version": "2.0.1", 2381 + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 2382 + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 2383 + "dev": true, 2384 + "license": "Python-2.0" 2385 + }, 2386 + "node_modules/autoprefixer": { 2387 + "version": "10.4.21", 2388 + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", 2389 + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", 2390 + "dev": true, 2391 + "funding": [ 2392 + { 2393 + "type": "opencollective", 2394 + "url": "https://opencollective.com/postcss/" 2395 + }, 2396 + { 2397 + "type": "tidelift", 2398 + "url": "https://tidelift.com/funding/github/npm/autoprefixer" 2399 + }, 2400 + { 2401 + "type": "github", 2402 + "url": "https://github.com/sponsors/ai" 2403 + } 2404 + ], 2405 + "license": "MIT", 2406 + "dependencies": { 2407 + "browserslist": "^4.24.4", 2408 + "caniuse-lite": "^1.0.30001702", 2409 + "fraction.js": "^4.3.7", 2410 + "normalize-range": "^0.1.2", 2411 + "picocolors": "^1.1.1", 2412 + "postcss-value-parser": "^4.2.0" 2413 + }, 2414 + "bin": { 2415 + "autoprefixer": "bin/autoprefixer" 2416 + }, 2417 + "engines": { 2418 + "node": "^10 || ^12 || >=14" 2419 + }, 2420 + "peerDependencies": { 2421 + "postcss": "^8.1.0" 2422 + } 2423 + }, 2424 + "node_modules/await-lock": { 2425 + "version": "2.2.2", 2426 + "resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz", 2427 + "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==", 2428 + "license": "MIT" 2429 + }, 2430 + "node_modules/balanced-match": { 2431 + "version": "1.0.2", 2432 + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 2433 + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 2434 + "dev": true, 2435 + "license": "MIT" 2436 + }, 2437 + "node_modules/baseline-browser-mapping": { 2438 + "version": "2.8.10", 2439 + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.10.tgz", 2440 + "integrity": "sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA==", 2441 + "dev": true, 2442 + "license": "Apache-2.0", 2443 + "bin": { 2444 + "baseline-browser-mapping": "dist/cli.js" 2445 + } 2446 + }, 2447 + "node_modules/brace-expansion": { 2448 + "version": "1.1.12", 2449 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", 2450 + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", 2451 + "dev": true, 2452 + "license": "MIT", 2453 + "dependencies": { 2454 + "balanced-match": "^1.0.0", 2455 + "concat-map": "0.0.1" 2456 + } 2457 + }, 2458 + "node_modules/braces": { 2459 + "version": "3.0.3", 2460 + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", 2461 + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", 2462 + "dev": true, 2463 + "license": "MIT", 2464 + "dependencies": { 2465 + "fill-range": "^7.1.1" 2466 + }, 2467 + "engines": { 2468 + "node": ">=8" 2469 + } 2470 + }, 2471 + "node_modules/browserslist": { 2472 + "version": "4.26.3", 2473 + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", 2474 + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", 2475 + "dev": true, 2476 + "funding": [ 2477 + { 2478 + "type": "opencollective", 2479 + "url": "https://opencollective.com/browserslist" 2480 + }, 2481 + { 2482 + "type": "tidelift", 2483 + "url": "https://tidelift.com/funding/github/npm/browserslist" 2484 + }, 2485 + { 2486 + "type": "github", 2487 + "url": "https://github.com/sponsors/ai" 2488 + } 2489 + ], 2490 + "license": "MIT", 2491 + "dependencies": { 2492 + "baseline-browser-mapping": "^2.8.9", 2493 + "caniuse-lite": "^1.0.30001746", 2494 + "electron-to-chromium": "^1.5.227", 2495 + "node-releases": "^2.0.21", 2496 + "update-browserslist-db": "^1.1.3" 2497 + }, 2498 + "bin": { 2499 + "browserslist": "cli.js" 2500 + }, 2501 + "engines": { 2502 + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" 2503 + } 2504 + }, 2505 + "node_modules/callsites": { 2506 + "version": "3.1.0", 2507 + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 2508 + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 2509 + "dev": true, 2510 + "license": "MIT", 2511 + "engines": { 2512 + "node": ">=6" 2513 + } 2514 + }, 2515 + "node_modules/caniuse-lite": { 2516 + "version": "1.0.30001747", 2517 + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001747.tgz", 2518 + "integrity": "sha512-mzFa2DGIhuc5490Nd/G31xN1pnBnYMadtkyTjefPI7wzypqgCEpeWu9bJr0OnDsyKrW75zA9ZAt7pbQFmwLsQg==", 2519 + "dev": true, 2520 + "funding": [ 2521 + { 2522 + "type": "opencollective", 2523 + "url": "https://opencollective.com/browserslist" 2524 + }, 2525 + { 2526 + "type": "tidelift", 2527 + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" 2528 + }, 2529 + { 2530 + "type": "github", 2531 + "url": "https://github.com/sponsors/ai" 2532 + } 2533 + ], 2534 + "license": "CC-BY-4.0" 2535 + }, 2536 + "node_modules/chalk": { 2537 + "version": "4.1.2", 2538 + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 2539 + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 2540 + "dev": true, 2541 + "license": "MIT", 2542 + "dependencies": { 2543 + "ansi-styles": "^4.1.0", 2544 + "supports-color": "^7.1.0" 2545 + }, 2546 + "engines": { 2547 + "node": ">=10" 2548 + }, 2549 + "funding": { 2550 + "url": "https://github.com/chalk/chalk?sponsor=1" 2551 + } 2552 + }, 2553 + "node_modules/chownr": { 2554 + "version": "3.0.0", 2555 + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", 2556 + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", 2557 + "dev": true, 2558 + "license": "BlueOak-1.0.0", 2559 + "engines": { 2560 + "node": ">=18" 2561 + } 2562 + }, 2563 + "node_modules/color-convert": { 2564 + "version": "2.0.1", 2565 + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 2566 + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 2567 + "dev": true, 2568 + "license": "MIT", 2569 + "dependencies": { 2570 + "color-name": "~1.1.4" 2571 + }, 2572 + "engines": { 2573 + "node": ">=7.0.0" 2574 + } 2575 + }, 2576 + "node_modules/color-name": { 2577 + "version": "1.1.4", 2578 + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 2579 + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 2580 + "dev": true, 2581 + "license": "MIT" 2582 + }, 2583 + "node_modules/concat-map": { 2584 + "version": "0.0.1", 2585 + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 2586 + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 2587 + "dev": true, 2588 + "license": "MIT" 2589 + }, 2590 + "node_modules/convert-source-map": { 2591 + "version": "2.0.0", 2592 + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", 2593 + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", 2594 + "dev": true, 2595 + "license": "MIT" 2596 + }, 2597 + "node_modules/cookie": { 2598 + "version": "1.0.2", 2599 + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", 2600 + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", 2601 + "license": "MIT", 2602 + "engines": { 2603 + "node": ">=18" 2604 + } 2605 + }, 2606 + "node_modules/cross-spawn": { 2607 + "version": "7.0.6", 2608 + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 2609 + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 2610 + "dev": true, 2611 + "license": "MIT", 2612 + "dependencies": { 2613 + "path-key": "^3.1.0", 2614 + "shebang-command": "^2.0.0", 2615 + "which": "^2.0.1" 2616 + }, 2617 + "engines": { 2618 + "node": ">= 8" 2619 + } 2620 + }, 2621 + "node_modules/csstype": { 2622 + "version": "3.1.3", 2623 + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", 2624 + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", 2625 + "dev": true, 2626 + "license": "MIT" 2627 + }, 2628 + "node_modules/debug": { 2629 + "version": "4.4.3", 2630 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", 2631 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", 2632 + "dev": true, 2633 + "license": "MIT", 2634 + "dependencies": { 2635 + "ms": "^2.1.3" 2636 + }, 2637 + "engines": { 2638 + "node": ">=6.0" 2639 + }, 2640 + "peerDependenciesMeta": { 2641 + "supports-color": { 2642 + "optional": true 2643 + } 2644 + } 2645 + }, 2646 + "node_modules/deep-is": { 2647 + "version": "0.1.4", 2648 + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 2649 + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", 2650 + "dev": true, 2651 + "license": "MIT" 2652 + }, 2653 + "node_modules/detect-libc": { 2654 + "version": "2.1.1", 2655 + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz", 2656 + "integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==", 2657 + "dev": true, 2658 + "license": "Apache-2.0", 2659 + "engines": { 2660 + "node": ">=8" 2661 + } 2662 + }, 2663 + "node_modules/electron-to-chromium": { 2664 + "version": "1.5.230", 2665 + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.230.tgz", 2666 + "integrity": "sha512-A6A6Fd3+gMdaed9wX83CvHYJb4UuapPD5X5SLq72VZJzxHSY0/LUweGXRWmQlh2ln7KV7iw7jnwXK7dlPoOnHQ==", 2667 + "dev": true, 2668 + "license": "ISC" 2669 + }, 2670 + "node_modules/enhanced-resolve": { 2671 + "version": "5.18.3", 2672 + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", 2673 + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", 2674 + "dev": true, 2675 + "license": "MIT", 2676 + "dependencies": { 2677 + "graceful-fs": "^4.2.4", 2678 + "tapable": "^2.2.0" 2679 + }, 2680 + "engines": { 2681 + "node": ">=10.13.0" 2682 + } 2683 + }, 2684 + "node_modules/esbuild": { 2685 + "version": "0.25.10", 2686 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", 2687 + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", 2688 + "dev": true, 2689 + "hasInstallScript": true, 2690 + "license": "MIT", 2691 + "bin": { 2692 + "esbuild": "bin/esbuild" 2693 + }, 2694 + "engines": { 2695 + "node": ">=18" 2696 + }, 2697 + "optionalDependencies": { 2698 + "@esbuild/aix-ppc64": "0.25.10", 2699 + "@esbuild/android-arm": "0.25.10", 2700 + "@esbuild/android-arm64": "0.25.10", 2701 + "@esbuild/android-x64": "0.25.10", 2702 + "@esbuild/darwin-arm64": "0.25.10", 2703 + "@esbuild/darwin-x64": "0.25.10", 2704 + "@esbuild/freebsd-arm64": "0.25.10", 2705 + "@esbuild/freebsd-x64": "0.25.10", 2706 + "@esbuild/linux-arm": "0.25.10", 2707 + "@esbuild/linux-arm64": "0.25.10", 2708 + "@esbuild/linux-ia32": "0.25.10", 2709 + "@esbuild/linux-loong64": "0.25.10", 2710 + "@esbuild/linux-mips64el": "0.25.10", 2711 + "@esbuild/linux-ppc64": "0.25.10", 2712 + "@esbuild/linux-riscv64": "0.25.10", 2713 + "@esbuild/linux-s390x": "0.25.10", 2714 + "@esbuild/linux-x64": "0.25.10", 2715 + "@esbuild/netbsd-arm64": "0.25.10", 2716 + "@esbuild/netbsd-x64": "0.25.10", 2717 + "@esbuild/openbsd-arm64": "0.25.10", 2718 + "@esbuild/openbsd-x64": "0.25.10", 2719 + "@esbuild/openharmony-arm64": "0.25.10", 2720 + "@esbuild/sunos-x64": "0.25.10", 2721 + "@esbuild/win32-arm64": "0.25.10", 2722 + "@esbuild/win32-ia32": "0.25.10", 2723 + "@esbuild/win32-x64": "0.25.10" 2724 + } 2725 + }, 2726 + "node_modules/escalade": { 2727 + "version": "3.2.0", 2728 + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", 2729 + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", 2730 + "dev": true, 2731 + "license": "MIT", 2732 + "engines": { 2733 + "node": ">=6" 2734 + } 2735 + }, 2736 + "node_modules/escape-string-regexp": { 2737 + "version": "4.0.0", 2738 + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 2739 + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 2740 + "dev": true, 2741 + "license": "MIT", 2742 + "engines": { 2743 + "node": ">=10" 2744 + }, 2745 + "funding": { 2746 + "url": "https://github.com/sponsors/sindresorhus" 2747 + } 2748 + }, 2749 + "node_modules/eslint": { 2750 + "version": "9.36.0", 2751 + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz", 2752 + "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", 2753 + "dev": true, 2754 + "license": "MIT", 2755 + "dependencies": { 2756 + "@eslint-community/eslint-utils": "^4.8.0", 2757 + "@eslint-community/regexpp": "^4.12.1", 2758 + "@eslint/config-array": "^0.21.0", 2759 + "@eslint/config-helpers": "^0.3.1", 2760 + "@eslint/core": "^0.15.2", 2761 + "@eslint/eslintrc": "^3.3.1", 2762 + "@eslint/js": "9.36.0", 2763 + "@eslint/plugin-kit": "^0.3.5", 2764 + "@humanfs/node": "^0.16.6", 2765 + "@humanwhocodes/module-importer": "^1.0.1", 2766 + "@humanwhocodes/retry": "^0.4.2", 2767 + "@types/estree": "^1.0.6", 2768 + "@types/json-schema": "^7.0.15", 2769 + "ajv": "^6.12.4", 2770 + "chalk": "^4.0.0", 2771 + "cross-spawn": "^7.0.6", 2772 + "debug": "^4.3.2", 2773 + "escape-string-regexp": "^4.0.0", 2774 + "eslint-scope": "^8.4.0", 2775 + "eslint-visitor-keys": "^4.2.1", 2776 + "espree": "^10.4.0", 2777 + "esquery": "^1.5.0", 2778 + "esutils": "^2.0.2", 2779 + "fast-deep-equal": "^3.1.3", 2780 + "file-entry-cache": "^8.0.0", 2781 + "find-up": "^5.0.0", 2782 + "glob-parent": "^6.0.2", 2783 + "ignore": "^5.2.0", 2784 + "imurmurhash": "^0.1.4", 2785 + "is-glob": "^4.0.0", 2786 + "json-stable-stringify-without-jsonify": "^1.0.1", 2787 + "lodash.merge": "^4.6.2", 2788 + "minimatch": "^3.1.2", 2789 + "natural-compare": "^1.4.0", 2790 + "optionator": "^0.9.3" 2791 + }, 2792 + "bin": { 2793 + "eslint": "bin/eslint.js" 2794 + }, 2795 + "engines": { 2796 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2797 + }, 2798 + "funding": { 2799 + "url": "https://eslint.org/donate" 2800 + }, 2801 + "peerDependencies": { 2802 + "jiti": "*" 2803 + }, 2804 + "peerDependenciesMeta": { 2805 + "jiti": { 2806 + "optional": true 2807 + } 2808 + } 2809 + }, 2810 + "node_modules/eslint-plugin-react-hooks": { 2811 + "version": "5.2.0", 2812 + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", 2813 + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", 2814 + "dev": true, 2815 + "license": "MIT", 2816 + "engines": { 2817 + "node": ">=10" 2818 + }, 2819 + "peerDependencies": { 2820 + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" 2821 + } 2822 + }, 2823 + "node_modules/eslint-plugin-react-refresh": { 2824 + "version": "0.4.23", 2825 + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.23.tgz", 2826 + "integrity": "sha512-G4j+rv0NmbIR45kni5xJOrYvCtyD3/7LjpVH8MPPcudXDcNu8gv+4ATTDXTtbRR8rTCM5HxECvCSsRmxKnWDsA==", 2827 + "dev": true, 2828 + "license": "MIT", 2829 + "peerDependencies": { 2830 + "eslint": ">=8.40" 2831 + } 2832 + }, 2833 + "node_modules/eslint-scope": { 2834 + "version": "8.4.0", 2835 + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", 2836 + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", 2837 + "dev": true, 2838 + "license": "BSD-2-Clause", 2839 + "dependencies": { 2840 + "esrecurse": "^4.3.0", 2841 + "estraverse": "^5.2.0" 2842 + }, 2843 + "engines": { 2844 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2845 + }, 2846 + "funding": { 2847 + "url": "https://opencollective.com/eslint" 2848 + } 2849 + }, 2850 + "node_modules/eslint-visitor-keys": { 2851 + "version": "4.2.1", 2852 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", 2853 + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", 2854 + "dev": true, 2855 + "license": "Apache-2.0", 2856 + "engines": { 2857 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2858 + }, 2859 + "funding": { 2860 + "url": "https://opencollective.com/eslint" 2861 + } 2862 + }, 2863 + "node_modules/espree": { 2864 + "version": "10.4.0", 2865 + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", 2866 + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", 2867 + "dev": true, 2868 + "license": "BSD-2-Clause", 2869 + "dependencies": { 2870 + "acorn": "^8.15.0", 2871 + "acorn-jsx": "^5.3.2", 2872 + "eslint-visitor-keys": "^4.2.1" 2873 + }, 2874 + "engines": { 2875 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2876 + }, 2877 + "funding": { 2878 + "url": "https://opencollective.com/eslint" 2879 + } 2880 + }, 2881 + "node_modules/esquery": { 2882 + "version": "1.6.0", 2883 + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", 2884 + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", 2885 + "dev": true, 2886 + "license": "BSD-3-Clause", 2887 + "dependencies": { 2888 + "estraverse": "^5.1.0" 2889 + }, 2890 + "engines": { 2891 + "node": ">=0.10" 2892 + } 2893 + }, 2894 + "node_modules/esrecurse": { 2895 + "version": "4.3.0", 2896 + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", 2897 + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", 2898 + "dev": true, 2899 + "license": "BSD-2-Clause", 2900 + "dependencies": { 2901 + "estraverse": "^5.2.0" 2902 + }, 2903 + "engines": { 2904 + "node": ">=4.0" 2905 + } 2906 + }, 2907 + "node_modules/estraverse": { 2908 + "version": "5.3.0", 2909 + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 2910 + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 2911 + "dev": true, 2912 + "license": "BSD-2-Clause", 2913 + "engines": { 2914 + "node": ">=4.0" 2915 + } 2916 + }, 2917 + "node_modules/esutils": { 2918 + "version": "2.0.3", 2919 + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 2920 + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 2921 + "dev": true, 2922 + "license": "BSD-2-Clause", 2923 + "engines": { 2924 + "node": ">=0.10.0" 2925 + } 2926 + }, 2927 + "node_modules/fast-deep-equal": { 2928 + "version": "3.1.3", 2929 + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 2930 + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 2931 + "dev": true, 2932 + "license": "MIT" 2933 + }, 2934 + "node_modules/fast-glob": { 2935 + "version": "3.3.3", 2936 + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", 2937 + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", 2938 + "dev": true, 2939 + "license": "MIT", 2940 + "dependencies": { 2941 + "@nodelib/fs.stat": "^2.0.2", 2942 + "@nodelib/fs.walk": "^1.2.3", 2943 + "glob-parent": "^5.1.2", 2944 + "merge2": "^1.3.0", 2945 + "micromatch": "^4.0.8" 2946 + }, 2947 + "engines": { 2948 + "node": ">=8.6.0" 2949 + } 2950 + }, 2951 + "node_modules/fast-glob/node_modules/glob-parent": { 2952 + "version": "5.1.2", 2953 + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 2954 + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 2955 + "dev": true, 2956 + "license": "ISC", 2957 + "dependencies": { 2958 + "is-glob": "^4.0.1" 2959 + }, 2960 + "engines": { 2961 + "node": ">= 6" 2962 + } 2963 + }, 2964 + "node_modules/fast-json-stable-stringify": { 2965 + "version": "2.1.0", 2966 + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 2967 + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 2968 + "dev": true, 2969 + "license": "MIT" 2970 + }, 2971 + "node_modules/fast-levenshtein": { 2972 + "version": "2.0.6", 2973 + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 2974 + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", 2975 + "dev": true, 2976 + "license": "MIT" 2977 + }, 2978 + "node_modules/fastq": { 2979 + "version": "1.19.1", 2980 + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", 2981 + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", 2982 + "dev": true, 2983 + "license": "ISC", 2984 + "dependencies": { 2985 + "reusify": "^1.0.4" 2986 + } 2987 + }, 2988 + "node_modules/file-entry-cache": { 2989 + "version": "8.0.0", 2990 + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", 2991 + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", 2992 + "dev": true, 2993 + "license": "MIT", 2994 + "dependencies": { 2995 + "flat-cache": "^4.0.0" 2996 + }, 2997 + "engines": { 2998 + "node": ">=16.0.0" 2999 + } 3000 + }, 3001 + "node_modules/fill-range": { 3002 + "version": "7.1.1", 3003 + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", 3004 + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", 3005 + "dev": true, 3006 + "license": "MIT", 3007 + "dependencies": { 3008 + "to-regex-range": "^5.0.1" 3009 + }, 3010 + "engines": { 3011 + "node": ">=8" 3012 + } 3013 + }, 3014 + "node_modules/find-up": { 3015 + "version": "5.0.0", 3016 + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 3017 + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 3018 + "dev": true, 3019 + "license": "MIT", 3020 + "dependencies": { 3021 + "locate-path": "^6.0.0", 3022 + "path-exists": "^4.0.0" 3023 + }, 3024 + "engines": { 3025 + "node": ">=10" 3026 + }, 3027 + "funding": { 3028 + "url": "https://github.com/sponsors/sindresorhus" 3029 + } 3030 + }, 3031 + "node_modules/flat-cache": { 3032 + "version": "4.0.1", 3033 + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", 3034 + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", 3035 + "dev": true, 3036 + "license": "MIT", 3037 + "dependencies": { 3038 + "flatted": "^3.2.9", 3039 + "keyv": "^4.5.4" 3040 + }, 3041 + "engines": { 3042 + "node": ">=16" 3043 + } 3044 + }, 3045 + "node_modules/flatted": { 3046 + "version": "3.3.3", 3047 + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", 3048 + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", 3049 + "dev": true, 3050 + "license": "ISC" 3051 + }, 3052 + "node_modules/fraction.js": { 3053 + "version": "4.3.7", 3054 + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", 3055 + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", 3056 + "dev": true, 3057 + "license": "MIT", 3058 + "engines": { 3059 + "node": "*" 3060 + }, 3061 + "funding": { 3062 + "type": "patreon", 3063 + "url": "https://github.com/sponsors/rawify" 3064 + } 3065 + }, 3066 + "node_modules/fsevents": { 3067 + "version": "2.3.3", 3068 + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 3069 + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 3070 + "dev": true, 3071 + "hasInstallScript": true, 3072 + "license": "MIT", 3073 + "optional": true, 3074 + "os": [ 3075 + "darwin" 3076 + ], 3077 + "engines": { 3078 + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 3079 + } 3080 + }, 3081 + "node_modules/gensync": { 3082 + "version": "1.0.0-beta.2", 3083 + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", 3084 + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", 3085 + "dev": true, 3086 + "license": "MIT", 3087 + "engines": { 3088 + "node": ">=6.9.0" 3089 + } 3090 + }, 3091 + "node_modules/glob-parent": { 3092 + "version": "6.0.2", 3093 + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 3094 + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 3095 + "dev": true, 3096 + "license": "ISC", 3097 + "dependencies": { 3098 + "is-glob": "^4.0.3" 3099 + }, 3100 + "engines": { 3101 + "node": ">=10.13.0" 3102 + } 3103 + }, 3104 + "node_modules/globals": { 3105 + "version": "16.4.0", 3106 + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", 3107 + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", 3108 + "dev": true, 3109 + "license": "MIT", 3110 + "engines": { 3111 + "node": ">=18" 3112 + }, 3113 + "funding": { 3114 + "url": "https://github.com/sponsors/sindresorhus" 3115 + } 3116 + }, 3117 + "node_modules/graceful-fs": { 3118 + "version": "4.2.11", 3119 + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 3120 + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", 3121 + "dev": true, 3122 + "license": "ISC" 3123 + }, 3124 + "node_modules/graphemer": { 3125 + "version": "1.4.0", 3126 + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", 3127 + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", 3128 + "license": "MIT" 3129 + }, 3130 + "node_modules/has-flag": { 3131 + "version": "4.0.0", 3132 + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 3133 + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 3134 + "dev": true, 3135 + "license": "MIT", 3136 + "engines": { 3137 + "node": ">=8" 3138 + } 3139 + }, 3140 + "node_modules/ignore": { 3141 + "version": "5.3.2", 3142 + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", 3143 + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", 3144 + "dev": true, 3145 + "license": "MIT", 3146 + "engines": { 3147 + "node": ">= 4" 3148 + } 3149 + }, 3150 + "node_modules/import-fresh": { 3151 + "version": "3.3.1", 3152 + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", 3153 + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", 3154 + "dev": true, 3155 + "license": "MIT", 3156 + "dependencies": { 3157 + "parent-module": "^1.0.0", 3158 + "resolve-from": "^4.0.0" 3159 + }, 3160 + "engines": { 3161 + "node": ">=6" 3162 + }, 3163 + "funding": { 3164 + "url": "https://github.com/sponsors/sindresorhus" 3165 + } 3166 + }, 3167 + "node_modules/imurmurhash": { 3168 + "version": "0.1.4", 3169 + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 3170 + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", 3171 + "dev": true, 3172 + "license": "MIT", 3173 + "engines": { 3174 + "node": ">=0.8.19" 3175 + } 3176 + }, 3177 + "node_modules/is-extglob": { 3178 + "version": "2.1.1", 3179 + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 3180 + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 3181 + "dev": true, 3182 + "license": "MIT", 3183 + "engines": { 3184 + "node": ">=0.10.0" 3185 + } 3186 + }, 3187 + "node_modules/is-glob": { 3188 + "version": "4.0.3", 3189 + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 3190 + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 3191 + "dev": true, 3192 + "license": "MIT", 3193 + "dependencies": { 3194 + "is-extglob": "^2.1.1" 3195 + }, 3196 + "engines": { 3197 + "node": ">=0.10.0" 3198 + } 3199 + }, 3200 + "node_modules/is-number": { 3201 + "version": "7.0.0", 3202 + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 3203 + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 3204 + "dev": true, 3205 + "license": "MIT", 3206 + "engines": { 3207 + "node": ">=0.12.0" 3208 + } 3209 + }, 3210 + "node_modules/isexe": { 3211 + "version": "2.0.0", 3212 + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 3213 + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 3214 + "dev": true, 3215 + "license": "ISC" 3216 + }, 3217 + "node_modules/iso-datestring-validator": { 3218 + "version": "2.2.2", 3219 + "resolved": "https://registry.npmjs.org/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz", 3220 + "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==", 3221 + "license": "MIT" 3222 + }, 3223 + "node_modules/jiti": { 3224 + "version": "2.6.1", 3225 + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", 3226 + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", 3227 + "dev": true, 3228 + "license": "MIT", 3229 + "bin": { 3230 + "jiti": "lib/jiti-cli.mjs" 3231 + } 3232 + }, 3233 + "node_modules/jose": { 3234 + "version": "5.10.0", 3235 + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", 3236 + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", 3237 + "license": "MIT", 3238 + "funding": { 3239 + "url": "https://github.com/sponsors/panva" 3240 + } 3241 + }, 3242 + "node_modules/js-tokens": { 3243 + "version": "4.0.0", 3244 + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 3245 + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 3246 + "dev": true, 3247 + "license": "MIT" 3248 + }, 3249 + "node_modules/js-yaml": { 3250 + "version": "4.1.0", 3251 + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 3252 + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 3253 + "dev": true, 3254 + "license": "MIT", 3255 + "dependencies": { 3256 + "argparse": "^2.0.1" 3257 + }, 3258 + "bin": { 3259 + "js-yaml": "bin/js-yaml.js" 3260 + } 3261 + }, 3262 + "node_modules/jsesc": { 3263 + "version": "3.1.0", 3264 + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", 3265 + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", 3266 + "dev": true, 3267 + "license": "MIT", 3268 + "bin": { 3269 + "jsesc": "bin/jsesc" 3270 + }, 3271 + "engines": { 3272 + "node": ">=6" 3273 + } 3274 + }, 3275 + "node_modules/json-buffer": { 3276 + "version": "3.0.1", 3277 + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", 3278 + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", 3279 + "dev": true, 3280 + "license": "MIT" 3281 + }, 3282 + "node_modules/json-schema-traverse": { 3283 + "version": "0.4.1", 3284 + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 3285 + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 3286 + "dev": true, 3287 + "license": "MIT" 3288 + }, 3289 + "node_modules/json-stable-stringify-without-jsonify": { 3290 + "version": "1.0.1", 3291 + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 3292 + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", 3293 + "dev": true, 3294 + "license": "MIT" 3295 + }, 3296 + "node_modules/json5": { 3297 + "version": "2.2.3", 3298 + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", 3299 + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", 3300 + "dev": true, 3301 + "license": "MIT", 3302 + "bin": { 3303 + "json5": "lib/cli.js" 3304 + }, 3305 + "engines": { 3306 + "node": ">=6" 3307 + } 3308 + }, 3309 + "node_modules/keyv": { 3310 + "version": "4.5.4", 3311 + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", 3312 + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", 3313 + "dev": true, 3314 + "license": "MIT", 3315 + "dependencies": { 3316 + "json-buffer": "3.0.1" 3317 + } 3318 + }, 3319 + "node_modules/levn": { 3320 + "version": "0.4.1", 3321 + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", 3322 + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", 3323 + "dev": true, 3324 + "license": "MIT", 3325 + "dependencies": { 3326 + "prelude-ls": "^1.2.1", 3327 + "type-check": "~0.4.0" 3328 + }, 3329 + "engines": { 3330 + "node": ">= 0.8.0" 3331 + } 3332 + }, 3333 + "node_modules/lightningcss": { 3334 + "version": "1.30.1", 3335 + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", 3336 + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", 3337 + "dev": true, 3338 + "license": "MPL-2.0", 3339 + "dependencies": { 3340 + "detect-libc": "^2.0.3" 3341 + }, 3342 + "engines": { 3343 + "node": ">= 12.0.0" 3344 + }, 3345 + "funding": { 3346 + "type": "opencollective", 3347 + "url": "https://opencollective.com/parcel" 3348 + }, 3349 + "optionalDependencies": { 3350 + "lightningcss-darwin-arm64": "1.30.1", 3351 + "lightningcss-darwin-x64": "1.30.1", 3352 + "lightningcss-freebsd-x64": "1.30.1", 3353 + "lightningcss-linux-arm-gnueabihf": "1.30.1", 3354 + "lightningcss-linux-arm64-gnu": "1.30.1", 3355 + "lightningcss-linux-arm64-musl": "1.30.1", 3356 + "lightningcss-linux-x64-gnu": "1.30.1", 3357 + "lightningcss-linux-x64-musl": "1.30.1", 3358 + "lightningcss-win32-arm64-msvc": "1.30.1", 3359 + "lightningcss-win32-x64-msvc": "1.30.1" 3360 + } 3361 + }, 3362 + "node_modules/lightningcss-darwin-arm64": { 3363 + "version": "1.30.1", 3364 + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", 3365 + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", 3366 + "cpu": [ 3367 + "arm64" 3368 + ], 3369 + "dev": true, 3370 + "license": "MPL-2.0", 3371 + "optional": true, 3372 + "os": [ 3373 + "darwin" 3374 + ], 3375 + "engines": { 3376 + "node": ">= 12.0.0" 3377 + }, 3378 + "funding": { 3379 + "type": "opencollective", 3380 + "url": "https://opencollective.com/parcel" 3381 + } 3382 + }, 3383 + "node_modules/lightningcss-darwin-x64": { 3384 + "version": "1.30.1", 3385 + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", 3386 + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", 3387 + "cpu": [ 3388 + "x64" 3389 + ], 3390 + "dev": true, 3391 + "license": "MPL-2.0", 3392 + "optional": true, 3393 + "os": [ 3394 + "darwin" 3395 + ], 3396 + "engines": { 3397 + "node": ">= 12.0.0" 3398 + }, 3399 + "funding": { 3400 + "type": "opencollective", 3401 + "url": "https://opencollective.com/parcel" 3402 + } 3403 + }, 3404 + "node_modules/lightningcss-freebsd-x64": { 3405 + "version": "1.30.1", 3406 + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", 3407 + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", 3408 + "cpu": [ 3409 + "x64" 3410 + ], 3411 + "dev": true, 3412 + "license": "MPL-2.0", 3413 + "optional": true, 3414 + "os": [ 3415 + "freebsd" 3416 + ], 3417 + "engines": { 3418 + "node": ">= 12.0.0" 3419 + }, 3420 + "funding": { 3421 + "type": "opencollective", 3422 + "url": "https://opencollective.com/parcel" 3423 + } 3424 + }, 3425 + "node_modules/lightningcss-linux-arm-gnueabihf": { 3426 + "version": "1.30.1", 3427 + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", 3428 + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", 3429 + "cpu": [ 3430 + "arm" 3431 + ], 3432 + "dev": true, 3433 + "license": "MPL-2.0", 3434 + "optional": true, 3435 + "os": [ 3436 + "linux" 3437 + ], 3438 + "engines": { 3439 + "node": ">= 12.0.0" 3440 + }, 3441 + "funding": { 3442 + "type": "opencollective", 3443 + "url": "https://opencollective.com/parcel" 3444 + } 3445 + }, 3446 + "node_modules/lightningcss-linux-arm64-gnu": { 3447 + "version": "1.30.1", 3448 + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", 3449 + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", 3450 + "cpu": [ 3451 + "arm64" 3452 + ], 3453 + "dev": true, 3454 + "license": "MPL-2.0", 3455 + "optional": true, 3456 + "os": [ 3457 + "linux" 3458 + ], 3459 + "engines": { 3460 + "node": ">= 12.0.0" 3461 + }, 3462 + "funding": { 3463 + "type": "opencollective", 3464 + "url": "https://opencollective.com/parcel" 3465 + } 3466 + }, 3467 + "node_modules/lightningcss-linux-arm64-musl": { 3468 + "version": "1.30.1", 3469 + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", 3470 + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", 3471 + "cpu": [ 3472 + "arm64" 3473 + ], 3474 + "dev": true, 3475 + "license": "MPL-2.0", 3476 + "optional": true, 3477 + "os": [ 3478 + "linux" 3479 + ], 3480 + "engines": { 3481 + "node": ">= 12.0.0" 3482 + }, 3483 + "funding": { 3484 + "type": "opencollective", 3485 + "url": "https://opencollective.com/parcel" 3486 + } 3487 + }, 3488 + "node_modules/lightningcss-linux-x64-gnu": { 3489 + "version": "1.30.1", 3490 + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", 3491 + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", 3492 + "cpu": [ 3493 + "x64" 3494 + ], 3495 + "dev": true, 3496 + "license": "MPL-2.0", 3497 + "optional": true, 3498 + "os": [ 3499 + "linux" 3500 + ], 3501 + "engines": { 3502 + "node": ">= 12.0.0" 3503 + }, 3504 + "funding": { 3505 + "type": "opencollective", 3506 + "url": "https://opencollective.com/parcel" 3507 + } 3508 + }, 3509 + "node_modules/lightningcss-linux-x64-musl": { 3510 + "version": "1.30.1", 3511 + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", 3512 + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", 3513 + "cpu": [ 3514 + "x64" 3515 + ], 3516 + "dev": true, 3517 + "license": "MPL-2.0", 3518 + "optional": true, 3519 + "os": [ 3520 + "linux" 3521 + ], 3522 + "engines": { 3523 + "node": ">= 12.0.0" 3524 + }, 3525 + "funding": { 3526 + "type": "opencollective", 3527 + "url": "https://opencollective.com/parcel" 3528 + } 3529 + }, 3530 + "node_modules/lightningcss-win32-arm64-msvc": { 3531 + "version": "1.30.1", 3532 + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", 3533 + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", 3534 + "cpu": [ 3535 + "arm64" 3536 + ], 3537 + "dev": true, 3538 + "license": "MPL-2.0", 3539 + "optional": true, 3540 + "os": [ 3541 + "win32" 3542 + ], 3543 + "engines": { 3544 + "node": ">= 12.0.0" 3545 + }, 3546 + "funding": { 3547 + "type": "opencollective", 3548 + "url": "https://opencollective.com/parcel" 3549 + } 3550 + }, 3551 + "node_modules/lightningcss-win32-x64-msvc": { 3552 + "version": "1.30.1", 3553 + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", 3554 + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", 3555 + "cpu": [ 3556 + "x64" 3557 + ], 3558 + "dev": true, 3559 + "license": "MPL-2.0", 3560 + "optional": true, 3561 + "os": [ 3562 + "win32" 3563 + ], 3564 + "engines": { 3565 + "node": ">= 12.0.0" 3566 + }, 3567 + "funding": { 3568 + "type": "opencollective", 3569 + "url": "https://opencollective.com/parcel" 3570 + } 3571 + }, 3572 + "node_modules/locate-path": { 3573 + "version": "6.0.0", 3574 + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 3575 + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 3576 + "dev": true, 3577 + "license": "MIT", 3578 + "dependencies": { 3579 + "p-locate": "^5.0.0" 3580 + }, 3581 + "engines": { 3582 + "node": ">=10" 3583 + }, 3584 + "funding": { 3585 + "url": "https://github.com/sponsors/sindresorhus" 3586 + } 3587 + }, 3588 + "node_modules/lodash.merge": { 3589 + "version": "4.6.2", 3590 + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 3591 + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 3592 + "dev": true, 3593 + "license": "MIT" 3594 + }, 3595 + "node_modules/lru-cache": { 3596 + "version": "5.1.1", 3597 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", 3598 + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", 3599 + "dev": true, 3600 + "license": "ISC", 3601 + "dependencies": { 3602 + "yallist": "^3.0.2" 3603 + } 3604 + }, 3605 + "node_modules/magic-string": { 3606 + "version": "0.30.19", 3607 + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", 3608 + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", 3609 + "dev": true, 3610 + "license": "MIT", 3611 + "dependencies": { 3612 + "@jridgewell/sourcemap-codec": "^1.5.5" 3613 + } 3614 + }, 3615 + "node_modules/merge2": { 3616 + "version": "1.4.1", 3617 + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", 3618 + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", 3619 + "dev": true, 3620 + "license": "MIT", 3621 + "engines": { 3622 + "node": ">= 8" 3623 + } 3624 + }, 3625 + "node_modules/micromatch": { 3626 + "version": "4.0.8", 3627 + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", 3628 + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", 3629 + "dev": true, 3630 + "license": "MIT", 3631 + "dependencies": { 3632 + "braces": "^3.0.3", 3633 + "picomatch": "^2.3.1" 3634 + }, 3635 + "engines": { 3636 + "node": ">=8.6" 3637 + } 3638 + }, 3639 + "node_modules/minimatch": { 3640 + "version": "3.1.2", 3641 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 3642 + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 3643 + "dev": true, 3644 + "license": "ISC", 3645 + "dependencies": { 3646 + "brace-expansion": "^1.1.7" 3647 + }, 3648 + "engines": { 3649 + "node": "*" 3650 + } 3651 + }, 3652 + "node_modules/minipass": { 3653 + "version": "7.1.2", 3654 + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", 3655 + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", 3656 + "dev": true, 3657 + "license": "ISC", 3658 + "engines": { 3659 + "node": ">=16 || 14 >=14.17" 3660 + } 3661 + }, 3662 + "node_modules/minizlib": { 3663 + "version": "3.1.0", 3664 + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", 3665 + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", 3666 + "dev": true, 3667 + "license": "MIT", 3668 + "dependencies": { 3669 + "minipass": "^7.1.2" 3670 + }, 3671 + "engines": { 3672 + "node": ">= 18" 3673 + } 3674 + }, 3675 + "node_modules/ms": { 3676 + "version": "2.1.3", 3677 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 3678 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 3679 + "dev": true, 3680 + "license": "MIT" 3681 + }, 3682 + "node_modules/multiformats": { 3683 + "version": "9.9.0", 3684 + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", 3685 + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 3686 + "license": "(Apache-2.0 AND MIT)" 3687 + }, 3688 + "node_modules/nanoid": { 3689 + "version": "3.3.11", 3690 + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", 3691 + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", 3692 + "dev": true, 3693 + "funding": [ 3694 + { 3695 + "type": "github", 3696 + "url": "https://github.com/sponsors/ai" 3697 + } 3698 + ], 3699 + "license": "MIT", 3700 + "bin": { 3701 + "nanoid": "bin/nanoid.cjs" 3702 + }, 3703 + "engines": { 3704 + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 3705 + } 3706 + }, 3707 + "node_modules/natural-compare": { 3708 + "version": "1.4.0", 3709 + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 3710 + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", 3711 + "dev": true, 3712 + "license": "MIT" 3713 + }, 3714 + "node_modules/node-releases": { 3715 + "version": "2.0.21", 3716 + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", 3717 + "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", 3718 + "dev": true, 3719 + "license": "MIT" 3720 + }, 3721 + "node_modules/normalize-range": { 3722 + "version": "0.1.2", 3723 + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", 3724 + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", 3725 + "dev": true, 3726 + "license": "MIT", 3727 + "engines": { 3728 + "node": ">=0.10.0" 3729 + } 3730 + }, 3731 + "node_modules/optionator": { 3732 + "version": "0.9.4", 3733 + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", 3734 + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", 3735 + "dev": true, 3736 + "license": "MIT", 3737 + "dependencies": { 3738 + "deep-is": "^0.1.3", 3739 + "fast-levenshtein": "^2.0.6", 3740 + "levn": "^0.4.1", 3741 + "prelude-ls": "^1.2.1", 3742 + "type-check": "^0.4.0", 3743 + "word-wrap": "^1.2.5" 3744 + }, 3745 + "engines": { 3746 + "node": ">= 0.8.0" 3747 + } 3748 + }, 3749 + "node_modules/p-limit": { 3750 + "version": "3.1.0", 3751 + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 3752 + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 3753 + "dev": true, 3754 + "license": "MIT", 3755 + "dependencies": { 3756 + "yocto-queue": "^0.1.0" 3757 + }, 3758 + "engines": { 3759 + "node": ">=10" 3760 + }, 3761 + "funding": { 3762 + "url": "https://github.com/sponsors/sindresorhus" 3763 + } 3764 + }, 3765 + "node_modules/p-locate": { 3766 + "version": "5.0.0", 3767 + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 3768 + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 3769 + "dev": true, 3770 + "license": "MIT", 3771 + "dependencies": { 3772 + "p-limit": "^3.0.2" 3773 + }, 3774 + "engines": { 3775 + "node": ">=10" 3776 + }, 3777 + "funding": { 3778 + "url": "https://github.com/sponsors/sindresorhus" 3779 + } 3780 + }, 3781 + "node_modules/parent-module": { 3782 + "version": "1.0.1", 3783 + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 3784 + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 3785 + "dev": true, 3786 + "license": "MIT", 3787 + "dependencies": { 3788 + "callsites": "^3.0.0" 3789 + }, 3790 + "engines": { 3791 + "node": ">=6" 3792 + } 3793 + }, 3794 + "node_modules/path-exists": { 3795 + "version": "4.0.0", 3796 + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 3797 + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 3798 + "dev": true, 3799 + "license": "MIT", 3800 + "engines": { 3801 + "node": ">=8" 3802 + } 3803 + }, 3804 + "node_modules/path-key": { 3805 + "version": "3.1.1", 3806 + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 3807 + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 3808 + "dev": true, 3809 + "license": "MIT", 3810 + "engines": { 3811 + "node": ">=8" 3812 + } 3813 + }, 3814 + "node_modules/picocolors": { 3815 + "version": "1.1.1", 3816 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 3817 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 3818 + "dev": true, 3819 + "license": "ISC" 3820 + }, 3821 + "node_modules/picomatch": { 3822 + "version": "2.3.1", 3823 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 3824 + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 3825 + "dev": true, 3826 + "license": "MIT", 3827 + "engines": { 3828 + "node": ">=8.6" 3829 + }, 3830 + "funding": { 3831 + "url": "https://github.com/sponsors/jonschlinkert" 3832 + } 3833 + }, 3834 + "node_modules/postcss": { 3835 + "version": "8.5.6", 3836 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", 3837 + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", 3838 + "dev": true, 3839 + "funding": [ 3840 + { 3841 + "type": "opencollective", 3842 + "url": "https://opencollective.com/postcss/" 3843 + }, 3844 + { 3845 + "type": "tidelift", 3846 + "url": "https://tidelift.com/funding/github/npm/postcss" 3847 + }, 3848 + { 3849 + "type": "github", 3850 + "url": "https://github.com/sponsors/ai" 3851 + } 3852 + ], 3853 + "license": "MIT", 3854 + "dependencies": { 3855 + "nanoid": "^3.3.11", 3856 + "picocolors": "^1.1.1", 3857 + "source-map-js": "^1.2.1" 3858 + }, 3859 + "engines": { 3860 + "node": "^10 || ^12 || >=14" 3861 + } 3862 + }, 3863 + "node_modules/postcss-value-parser": { 3864 + "version": "4.2.0", 3865 + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", 3866 + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", 3867 + "dev": true, 3868 + "license": "MIT" 3869 + }, 3870 + "node_modules/prelude-ls": { 3871 + "version": "1.2.1", 3872 + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", 3873 + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", 3874 + "dev": true, 3875 + "license": "MIT", 3876 + "engines": { 3877 + "node": ">= 0.8.0" 3878 + } 3879 + }, 3880 + "node_modules/punycode": { 3881 + "version": "2.3.1", 3882 + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", 3883 + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", 3884 + "dev": true, 3885 + "license": "MIT", 3886 + "engines": { 3887 + "node": ">=6" 3888 + } 3889 + }, 3890 + "node_modules/queue-microtask": { 3891 + "version": "1.2.3", 3892 + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", 3893 + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", 3894 + "dev": true, 3895 + "funding": [ 3896 + { 3897 + "type": "github", 3898 + "url": "https://github.com/sponsors/feross" 3899 + }, 3900 + { 3901 + "type": "patreon", 3902 + "url": "https://www.patreon.com/feross" 3903 + }, 3904 + { 3905 + "type": "consulting", 3906 + "url": "https://feross.org/support" 3907 + } 3908 + ], 3909 + "license": "MIT" 3910 + }, 3911 + "node_modules/react": { 3912 + "version": "19.2.0", 3913 + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", 3914 + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", 3915 + "license": "MIT", 3916 + "engines": { 3917 + "node": ">=0.10.0" 3918 + } 3919 + }, 3920 + "node_modules/react-dom": { 3921 + "version": "19.2.0", 3922 + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", 3923 + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", 3924 + "license": "MIT", 3925 + "dependencies": { 3926 + "scheduler": "^0.27.0" 3927 + }, 3928 + "peerDependencies": { 3929 + "react": "^19.2.0" 3930 + } 3931 + }, 3932 + "node_modules/react-refresh": { 3933 + "version": "0.17.0", 3934 + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", 3935 + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", 3936 + "dev": true, 3937 + "license": "MIT", 3938 + "engines": { 3939 + "node": ">=0.10.0" 3940 + } 3941 + }, 3942 + "node_modules/react-router": { 3943 + "version": "7.9.3", 3944 + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.3.tgz", 3945 + "integrity": "sha512-4o2iWCFIwhI/eYAIL43+cjORXYn/aRQPgtFRRZb3VzoyQ5Uej0Bmqj7437L97N9NJW4wnicSwLOLS+yCXfAPgg==", 3946 + "license": "MIT", 3947 + "dependencies": { 3948 + "cookie": "^1.0.1", 3949 + "set-cookie-parser": "^2.6.0" 3950 + }, 3951 + "engines": { 3952 + "node": ">=20.0.0" 3953 + }, 3954 + "peerDependencies": { 3955 + "react": ">=18", 3956 + "react-dom": ">=18" 3957 + }, 3958 + "peerDependenciesMeta": { 3959 + "react-dom": { 3960 + "optional": true 3961 + } 3962 + } 3963 + }, 3964 + "node_modules/react-router-dom": { 3965 + "version": "7.9.3", 3966 + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.3.tgz", 3967 + "integrity": "sha512-1QSbA0TGGFKTAc/aWjpfW/zoEukYfU4dc1dLkT/vvf54JoGMkW+fNA+3oyo2gWVW1GM7BxjJVHz5GnPJv40rvg==", 3968 + "license": "MIT", 3969 + "dependencies": { 3970 + "react-router": "7.9.3" 3971 + }, 3972 + "engines": { 3973 + "node": ">=20.0.0" 3974 + }, 3975 + "peerDependencies": { 3976 + "react": ">=18", 3977 + "react-dom": ">=18" 3978 + } 3979 + }, 3980 + "node_modules/resolve-from": { 3981 + "version": "4.0.0", 3982 + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 3983 + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 3984 + "dev": true, 3985 + "license": "MIT", 3986 + "engines": { 3987 + "node": ">=4" 3988 + } 3989 + }, 3990 + "node_modules/reusify": { 3991 + "version": "1.1.0", 3992 + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", 3993 + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", 3994 + "dev": true, 3995 + "license": "MIT", 3996 + "engines": { 3997 + "iojs": ">=1.0.0", 3998 + "node": ">=0.10.0" 3999 + } 4000 + }, 4001 + "node_modules/rollup": { 4002 + "version": "4.52.4", 4003 + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", 4004 + "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", 4005 + "dev": true, 4006 + "license": "MIT", 4007 + "dependencies": { 4008 + "@types/estree": "1.0.8" 4009 + }, 4010 + "bin": { 4011 + "rollup": "dist/bin/rollup" 4012 + }, 4013 + "engines": { 4014 + "node": ">=18.0.0", 4015 + "npm": ">=8.0.0" 4016 + }, 4017 + "optionalDependencies": { 4018 + "@rollup/rollup-android-arm-eabi": "4.52.4", 4019 + "@rollup/rollup-android-arm64": "4.52.4", 4020 + "@rollup/rollup-darwin-arm64": "4.52.4", 4021 + "@rollup/rollup-darwin-x64": "4.52.4", 4022 + "@rollup/rollup-freebsd-arm64": "4.52.4", 4023 + "@rollup/rollup-freebsd-x64": "4.52.4", 4024 + "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", 4025 + "@rollup/rollup-linux-arm-musleabihf": "4.52.4", 4026 + "@rollup/rollup-linux-arm64-gnu": "4.52.4", 4027 + "@rollup/rollup-linux-arm64-musl": "4.52.4", 4028 + "@rollup/rollup-linux-loong64-gnu": "4.52.4", 4029 + "@rollup/rollup-linux-ppc64-gnu": "4.52.4", 4030 + "@rollup/rollup-linux-riscv64-gnu": "4.52.4", 4031 + "@rollup/rollup-linux-riscv64-musl": "4.52.4", 4032 + "@rollup/rollup-linux-s390x-gnu": "4.52.4", 4033 + "@rollup/rollup-linux-x64-gnu": "4.52.4", 4034 + "@rollup/rollup-linux-x64-musl": "4.52.4", 4035 + "@rollup/rollup-openharmony-arm64": "4.52.4", 4036 + "@rollup/rollup-win32-arm64-msvc": "4.52.4", 4037 + "@rollup/rollup-win32-ia32-msvc": "4.52.4", 4038 + "@rollup/rollup-win32-x64-gnu": "4.52.4", 4039 + "@rollup/rollup-win32-x64-msvc": "4.52.4", 4040 + "fsevents": "~2.3.2" 4041 + } 4042 + }, 4043 + "node_modules/run-parallel": { 4044 + "version": "1.2.0", 4045 + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", 4046 + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 4047 + "dev": true, 4048 + "funding": [ 4049 + { 4050 + "type": "github", 4051 + "url": "https://github.com/sponsors/feross" 4052 + }, 4053 + { 4054 + "type": "patreon", 4055 + "url": "https://www.patreon.com/feross" 4056 + }, 4057 + { 4058 + "type": "consulting", 4059 + "url": "https://feross.org/support" 4060 + } 4061 + ], 4062 + "license": "MIT", 4063 + "dependencies": { 4064 + "queue-microtask": "^1.2.2" 4065 + } 4066 + }, 4067 + "node_modules/scheduler": { 4068 + "version": "0.27.0", 4069 + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", 4070 + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", 4071 + "license": "MIT" 4072 + }, 4073 + "node_modules/semver": { 4074 + "version": "6.3.1", 4075 + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", 4076 + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", 4077 + "dev": true, 4078 + "license": "ISC", 4079 + "bin": { 4080 + "semver": "bin/semver.js" 4081 + } 4082 + }, 4083 + "node_modules/set-cookie-parser": { 4084 + "version": "2.7.1", 4085 + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", 4086 + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", 4087 + "license": "MIT" 4088 + }, 4089 + "node_modules/shebang-command": { 4090 + "version": "2.0.0", 4091 + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 4092 + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 4093 + "dev": true, 4094 + "license": "MIT", 4095 + "dependencies": { 4096 + "shebang-regex": "^3.0.0" 4097 + }, 4098 + "engines": { 4099 + "node": ">=8" 4100 + } 4101 + }, 4102 + "node_modules/shebang-regex": { 4103 + "version": "3.0.0", 4104 + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 4105 + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 4106 + "dev": true, 4107 + "license": "MIT", 4108 + "engines": { 4109 + "node": ">=8" 4110 + } 4111 + }, 4112 + "node_modules/source-map-js": { 4113 + "version": "1.2.1", 4114 + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 4115 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 4116 + "dev": true, 4117 + "license": "BSD-3-Clause", 4118 + "engines": { 4119 + "node": ">=0.10.0" 4120 + } 4121 + }, 4122 + "node_modules/strip-json-comments": { 4123 + "version": "3.1.1", 4124 + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 4125 + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 4126 + "dev": true, 4127 + "license": "MIT", 4128 + "engines": { 4129 + "node": ">=8" 4130 + }, 4131 + "funding": { 4132 + "url": "https://github.com/sponsors/sindresorhus" 4133 + } 4134 + }, 4135 + "node_modules/supports-color": { 4136 + "version": "7.2.0", 4137 + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 4138 + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 4139 + "dev": true, 4140 + "license": "MIT", 4141 + "dependencies": { 4142 + "has-flag": "^4.0.0" 4143 + }, 4144 + "engines": { 4145 + "node": ">=8" 4146 + } 4147 + }, 4148 + "node_modules/tailwindcss": { 4149 + "version": "4.1.14", 4150 + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz", 4151 + "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==", 4152 + "dev": true, 4153 + "license": "MIT" 4154 + }, 4155 + "node_modules/tapable": { 4156 + "version": "2.3.0", 4157 + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", 4158 + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", 4159 + "dev": true, 4160 + "license": "MIT", 4161 + "engines": { 4162 + "node": ">=6" 4163 + }, 4164 + "funding": { 4165 + "type": "opencollective", 4166 + "url": "https://opencollective.com/webpack" 4167 + } 4168 + }, 4169 + "node_modules/tar": { 4170 + "version": "7.5.1", 4171 + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", 4172 + "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", 4173 + "dev": true, 4174 + "license": "ISC", 4175 + "dependencies": { 4176 + "@isaacs/fs-minipass": "^4.0.0", 4177 + "chownr": "^3.0.0", 4178 + "minipass": "^7.1.2", 4179 + "minizlib": "^3.1.0", 4180 + "yallist": "^5.0.0" 4181 + }, 4182 + "engines": { 4183 + "node": ">=18" 4184 + } 4185 + }, 4186 + "node_modules/tar/node_modules/yallist": { 4187 + "version": "5.0.0", 4188 + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", 4189 + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", 4190 + "dev": true, 4191 + "license": "BlueOak-1.0.0", 4192 + "engines": { 4193 + "node": ">=18" 4194 + } 4195 + }, 4196 + "node_modules/tinyglobby": { 4197 + "version": "0.2.15", 4198 + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", 4199 + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", 4200 + "dev": true, 4201 + "license": "MIT", 4202 + "dependencies": { 4203 + "fdir": "^6.5.0", 4204 + "picomatch": "^4.0.3" 4205 + }, 4206 + "engines": { 4207 + "node": ">=12.0.0" 4208 + }, 4209 + "funding": { 4210 + "url": "https://github.com/sponsors/SuperchupuDev" 4211 + } 4212 + }, 4213 + "node_modules/tinyglobby/node_modules/fdir": { 4214 + "version": "6.5.0", 4215 + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", 4216 + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", 4217 + "dev": true, 4218 + "license": "MIT", 4219 + "engines": { 4220 + "node": ">=12.0.0" 4221 + }, 4222 + "peerDependencies": { 4223 + "picomatch": "^3 || ^4" 4224 + }, 4225 + "peerDependenciesMeta": { 4226 + "picomatch": { 4227 + "optional": true 4228 + } 4229 + } 4230 + }, 4231 + "node_modules/tinyglobby/node_modules/picomatch": { 4232 + "version": "4.0.3", 4233 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", 4234 + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 4235 + "dev": true, 4236 + "license": "MIT", 4237 + "engines": { 4238 + "node": ">=12" 4239 + }, 4240 + "funding": { 4241 + "url": "https://github.com/sponsors/jonschlinkert" 4242 + } 4243 + }, 4244 + "node_modules/tlds": { 4245 + "version": "1.260.0", 4246 + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.260.0.tgz", 4247 + "integrity": "sha512-78+28EWBhCEE7qlyaHA9OR3IPvbCLiDh3Ckla593TksfFc9vfTsgvH7eS+dr3o9qr31gwGbogcI16yN91PoRjQ==", 4248 + "license": "MIT", 4249 + "bin": { 4250 + "tlds": "bin.js" 4251 + } 4252 + }, 4253 + "node_modules/to-regex-range": { 4254 + "version": "5.0.1", 4255 + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 4256 + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 4257 + "dev": true, 4258 + "license": "MIT", 4259 + "dependencies": { 4260 + "is-number": "^7.0.0" 4261 + }, 4262 + "engines": { 4263 + "node": ">=8.0" 4264 + } 4265 + }, 4266 + "node_modules/ts-api-utils": { 4267 + "version": "2.1.0", 4268 + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", 4269 + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", 4270 + "dev": true, 4271 + "license": "MIT", 4272 + "engines": { 4273 + "node": ">=18.12" 4274 + }, 4275 + "peerDependencies": { 4276 + "typescript": ">=4.8.4" 4277 + } 4278 + }, 4279 + "node_modules/type-check": { 4280 + "version": "0.4.0", 4281 + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", 4282 + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", 4283 + "dev": true, 4284 + "license": "MIT", 4285 + "dependencies": { 4286 + "prelude-ls": "^1.2.1" 4287 + }, 4288 + "engines": { 4289 + "node": ">= 0.8.0" 4290 + } 4291 + }, 4292 + "node_modules/typescript": { 4293 + "version": "5.9.3", 4294 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", 4295 + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", 4296 + "dev": true, 4297 + "license": "Apache-2.0", 4298 + "bin": { 4299 + "tsc": "bin/tsc", 4300 + "tsserver": "bin/tsserver" 4301 + }, 4302 + "engines": { 4303 + "node": ">=14.17" 4304 + } 4305 + }, 4306 + "node_modules/typescript-eslint": { 4307 + "version": "8.45.0", 4308 + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.45.0.tgz", 4309 + "integrity": "sha512-qzDmZw/Z5beNLUrXfd0HIW6MzIaAV5WNDxmMs9/3ojGOpYavofgNAAD/nC6tGV2PczIi0iw8vot2eAe/sBn7zg==", 4310 + "dev": true, 4311 + "license": "MIT", 4312 + "dependencies": { 4313 + "@typescript-eslint/eslint-plugin": "8.45.0", 4314 + "@typescript-eslint/parser": "8.45.0", 4315 + "@typescript-eslint/typescript-estree": "8.45.0", 4316 + "@typescript-eslint/utils": "8.45.0" 4317 + }, 4318 + "engines": { 4319 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 4320 + }, 4321 + "funding": { 4322 + "type": "opencollective", 4323 + "url": "https://opencollective.com/typescript-eslint" 4324 + }, 4325 + "peerDependencies": { 4326 + "eslint": "^8.57.0 || ^9.0.0", 4327 + "typescript": ">=4.8.4 <6.0.0" 4328 + } 4329 + }, 4330 + "node_modules/uint8arrays": { 4331 + "version": "3.0.0", 4332 + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz", 4333 + "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==", 4334 + "license": "MIT", 4335 + "dependencies": { 4336 + "multiformats": "^9.4.2" 4337 + } 4338 + }, 4339 + "node_modules/undici-types": { 4340 + "version": "7.13.0", 4341 + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz", 4342 + "integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==", 4343 + "dev": true, 4344 + "license": "MIT" 4345 + }, 4346 + "node_modules/update-browserslist-db": { 4347 + "version": "1.1.3", 4348 + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", 4349 + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", 4350 + "dev": true, 4351 + "funding": [ 4352 + { 4353 + "type": "opencollective", 4354 + "url": "https://opencollective.com/browserslist" 4355 + }, 4356 + { 4357 + "type": "tidelift", 4358 + "url": "https://tidelift.com/funding/github/npm/browserslist" 4359 + }, 4360 + { 4361 + "type": "github", 4362 + "url": "https://github.com/sponsors/ai" 4363 + } 4364 + ], 4365 + "license": "MIT", 4366 + "dependencies": { 4367 + "escalade": "^3.2.0", 4368 + "picocolors": "^1.1.1" 4369 + }, 4370 + "bin": { 4371 + "update-browserslist-db": "cli.js" 4372 + }, 4373 + "peerDependencies": { 4374 + "browserslist": ">= 4.21.0" 4375 + } 4376 + }, 4377 + "node_modules/uri-js": { 4378 + "version": "4.4.1", 4379 + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 4380 + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 4381 + "dev": true, 4382 + "license": "BSD-2-Clause", 4383 + "dependencies": { 4384 + "punycode": "^2.1.0" 4385 + } 4386 + }, 4387 + "node_modules/vite": { 4388 + "version": "7.1.9", 4389 + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz", 4390 + "integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==", 4391 + "dev": true, 4392 + "license": "MIT", 4393 + "dependencies": { 4394 + "esbuild": "^0.25.0", 4395 + "fdir": "^6.5.0", 4396 + "picomatch": "^4.0.3", 4397 + "postcss": "^8.5.6", 4398 + "rollup": "^4.43.0", 4399 + "tinyglobby": "^0.2.15" 4400 + }, 4401 + "bin": { 4402 + "vite": "bin/vite.js" 4403 + }, 4404 + "engines": { 4405 + "node": "^20.19.0 || >=22.12.0" 4406 + }, 4407 + "funding": { 4408 + "url": "https://github.com/vitejs/vite?sponsor=1" 4409 + }, 4410 + "optionalDependencies": { 4411 + "fsevents": "~2.3.3" 4412 + }, 4413 + "peerDependencies": { 4414 + "@types/node": "^20.19.0 || >=22.12.0", 4415 + "jiti": ">=1.21.0", 4416 + "less": "^4.0.0", 4417 + "lightningcss": "^1.21.0", 4418 + "sass": "^1.70.0", 4419 + "sass-embedded": "^1.70.0", 4420 + "stylus": ">=0.54.8", 4421 + "sugarss": "^5.0.0", 4422 + "terser": "^5.16.0", 4423 + "tsx": "^4.8.1", 4424 + "yaml": "^2.4.2" 4425 + }, 4426 + "peerDependenciesMeta": { 4427 + "@types/node": { 4428 + "optional": true 4429 + }, 4430 + "jiti": { 4431 + "optional": true 4432 + }, 4433 + "less": { 4434 + "optional": true 4435 + }, 4436 + "lightningcss": { 4437 + "optional": true 4438 + }, 4439 + "sass": { 4440 + "optional": true 4441 + }, 4442 + "sass-embedded": { 4443 + "optional": true 4444 + }, 4445 + "stylus": { 4446 + "optional": true 4447 + }, 4448 + "sugarss": { 4449 + "optional": true 4450 + }, 4451 + "terser": { 4452 + "optional": true 4453 + }, 4454 + "tsx": { 4455 + "optional": true 4456 + }, 4457 + "yaml": { 4458 + "optional": true 4459 + } 4460 + } 4461 + }, 4462 + "node_modules/vite/node_modules/fdir": { 4463 + "version": "6.5.0", 4464 + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", 4465 + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", 4466 + "dev": true, 4467 + "license": "MIT", 4468 + "engines": { 4469 + "node": ">=12.0.0" 4470 + }, 4471 + "peerDependencies": { 4472 + "picomatch": "^3 || ^4" 4473 + }, 4474 + "peerDependenciesMeta": { 4475 + "picomatch": { 4476 + "optional": true 4477 + } 4478 + } 4479 + }, 4480 + "node_modules/vite/node_modules/picomatch": { 4481 + "version": "4.0.3", 4482 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", 4483 + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 4484 + "dev": true, 4485 + "license": "MIT", 4486 + "engines": { 4487 + "node": ">=12" 4488 + }, 4489 + "funding": { 4490 + "url": "https://github.com/sponsors/jonschlinkert" 4491 + } 4492 + }, 4493 + "node_modules/which": { 4494 + "version": "2.0.2", 4495 + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 4496 + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 4497 + "dev": true, 4498 + "license": "ISC", 4499 + "dependencies": { 4500 + "isexe": "^2.0.0" 4501 + }, 4502 + "bin": { 4503 + "node-which": "bin/node-which" 4504 + }, 4505 + "engines": { 4506 + "node": ">= 8" 4507 + } 4508 + }, 4509 + "node_modules/word-wrap": { 4510 + "version": "1.2.5", 4511 + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", 4512 + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", 4513 + "dev": true, 4514 + "license": "MIT", 4515 + "engines": { 4516 + "node": ">=0.10.0" 4517 + } 4518 + }, 4519 + "node_modules/yallist": { 4520 + "version": "3.1.1", 4521 + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", 4522 + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", 4523 + "dev": true, 4524 + "license": "ISC" 4525 + }, 4526 + "node_modules/yocto-queue": { 4527 + "version": "0.1.0", 4528 + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 4529 + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 4530 + "dev": true, 4531 + "license": "MIT", 4532 + "engines": { 4533 + "node": ">=10" 4534 + }, 4535 + "funding": { 4536 + "url": "https://github.com/sponsors/sindresorhus" 4537 + } 4538 + }, 4539 + "node_modules/zod": { 4540 + "version": "3.25.76", 4541 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 4542 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 4543 + "license": "MIT", 4544 + "funding": { 4545 + "url": "https://github.com/sponsors/colinhacks" 4546 + } 4547 + } 4548 + } 4549 + }
+38
package.json
··· 1 + { 2 + "name": "atproto-oauth-react", 3 + "private": true, 4 + "version": "0.0.0", 5 + "type": "module", 6 + "scripts": { 7 + "dev": "vite", 8 + "build": "tsc -b && vite build", 9 + "lint": "eslint .", 10 + "preview": "vite preview" 11 + }, 12 + "dependencies": { 13 + "@atproto/api": "^0.17.0", 14 + "@atproto/identity": "^0.4.9", 15 + "@atproto/oauth-client-browser": "^0.3.32", 16 + "react": "^19.1.1", 17 + "react-dom": "^19.1.1", 18 + "react-router-dom": "^7.9.3" 19 + }, 20 + "devDependencies": { 21 + "@eslint/js": "^9.36.0", 22 + "@tailwindcss/postcss": "^4.1.14", 23 + "@types/node": "^24.6.0", 24 + "@types/react": "^19.1.16", 25 + "@types/react-dom": "^19.1.9", 26 + "@vitejs/plugin-react": "^5.0.4", 27 + "autoprefixer": "^10.4.21", 28 + "eslint": "^9.36.0", 29 + "eslint-plugin-react-hooks": "^5.2.0", 30 + "eslint-plugin-react-refresh": "^0.4.22", 31 + "globals": "^16.4.0", 32 + "postcss": "^8.5.6", 33 + "tailwindcss": "^4.1.14", 34 + "typescript": "~5.9.3", 35 + "typescript-eslint": "^8.45.0", 36 + "vite": "^7.1.7" 37 + } 38 + }
+6
postcss.config.js
··· 1 + export default { 2 + plugins: { 3 + '@tailwindcss/postcss': {}, 4 + autoprefixer: {}, 5 + }, 6 + }
+11
public/client-metadata.json
··· 1 + { 2 + "client_id": "https://oauth-spa.smokesignal.tools/client-metadata.json", 3 + "application_type": "web", 4 + "client_name": "ATProto Demo App", 5 + "redirect_uris": ["https://oauth-spa.smokesignal.tools/oauth/callback"], 6 + "scope": "atproto repo:garden.lexicon.oauth-masterclass.now", 7 + "grant_types": ["authorization_code", "refresh_token"], 8 + "response_types": ["code"], 9 + "token_endpoint_auth_method": "none", 10 + "dpop_bound_access_tokens": true 11 + }
+1
public/vite.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
+41
src/App.tsx
··· 1 + /** 2 + * Main App Component 3 + * Sets up routing and authentication context 4 + */ 5 + 6 + import React from 'react'; 7 + import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; 8 + import { AuthProvider } from './contexts/AuthContext'; 9 + import { ProtectedRoute } from './components/layout/ProtectedRoute'; 10 + import { Header } from './components/layout/Header'; 11 + import { Home } from './components/Home'; 12 + import { LoginForm } from './components/auth/LoginForm'; 13 + import { OAuthCallback } from './components/auth/OAuthCallback'; 14 + import { Dashboard } from './components/Dashboard'; 15 + import { CreatePost } from './components/post/CreatePost'; 16 + 17 + function App() { 18 + return ( 19 + <Router> 20 + <AuthProvider> 21 + <div className="min-h-screen bg-gray-50"> 22 + <Header /> 23 + <Routes> 24 + {/* Public routes */} 25 + <Route path="/" element={<Home />} /> 26 + <Route path="/login" element={<LoginForm />} /> 27 + <Route path="/oauth/callback" element={<OAuthCallback />} /> 28 + 29 + {/* Protected routes */} 30 + <Route element={<ProtectedRoute />}> 31 + <Route path="/dashboard" element={<Dashboard />} /> 32 + <Route path="/create-post" element={<CreatePost />} /> 33 + </Route> 34 + </Routes> 35 + </div> 36 + </AuthProvider> 37 + </Router> 38 + ); 39 + } 40 + 41 + export default App;
+1
src/assets/react.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
+243
src/components/Dashboard.tsx
··· 1 + /** 2 + * Dashboard Component 3 + * Main authenticated view showing user profile and recent posts 4 + */ 5 + 6 + import React from 'react'; 7 + import { Link } from 'react-router-dom'; 8 + import { useAuth } from '../contexts/AuthContext'; 9 + 10 + export const Dashboard: React.FC = () => { 11 + const { user } = useAuth(); 12 + 13 + if (!user) { 14 + return null; 15 + } 16 + 17 + return ( 18 + <div className="min-h-screen bg-gray-50"> 19 + <div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8"> 20 + {/* Welcome section */} 21 + <div className="px-4 py-6 sm:px-0"> 22 + <div className="bg-white overflow-hidden shadow rounded-lg"> 23 + <div className="px-4 py-5 sm:p-6"> 24 + <div className="flex items-center"> 25 + {user.avatar && ( 26 + <img 27 + className="h-16 w-16 rounded-full mr-4" 28 + src={user.avatar} 29 + alt={user.handle} 30 + /> 31 + )} 32 + <div> 33 + <h1 className="text-2xl font-bold text-gray-900"> 34 + Welcome, {user.displayName || user.handle}! 35 + </h1> 36 + <p className="mt-1 text-sm text-gray-500"> 37 + @{user.handle} • DID: {user.did.substring(0, 20)}... 38 + </p> 39 + </div> 40 + </div> 41 + 42 + {user.description && ( 43 + <div className="mt-4"> 44 + <p className="text-gray-700">{user.description}</p> 45 + </div> 46 + )} 47 + 48 + <div className="mt-6 grid grid-cols-3 gap-4 sm:gap-6"> 49 + <div className="col-span-1 text-center"> 50 + <p className="text-2xl font-semibold text-gray-900"> 51 + {user.postsCount || 0} 52 + </p> 53 + <p className="text-sm text-gray-500">Posts</p> 54 + </div> 55 + <div className="col-span-1 text-center"> 56 + <p className="text-2xl font-semibold text-gray-900"> 57 + {user.followersCount || 0} 58 + </p> 59 + <p className="text-sm text-gray-500">Followers</p> 60 + </div> 61 + <div className="col-span-1 text-center"> 62 + <p className="text-2xl font-semibold text-gray-900"> 63 + {user.followsCount || 0} 64 + </p> 65 + <p className="text-sm text-gray-500">Following</p> 66 + </div> 67 + </div> 68 + 69 + <div className="mt-6"> 70 + <Link 71 + to="/create-post" 72 + className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" 73 + > 74 + <svg 75 + className="-ml-1 mr-2 h-5 w-5" 76 + xmlns="http://www.w3.org/2000/svg" 77 + viewBox="0 0 20 20" 78 + fill="currentColor" 79 + aria-hidden="true" 80 + > 81 + <path 82 + fillRule="evenodd" 83 + d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" 84 + clipRule="evenodd" 85 + /> 86 + </svg> 87 + Create Post 88 + </Link> 89 + </div> 90 + </div> 91 + </div> 92 + </div> 93 + 94 + {/* Info cards */} 95 + <div className="px-4 py-4 sm:px-0"> 96 + <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"> 97 + {/* OAuth Status */} 98 + <div className="bg-white overflow-hidden shadow rounded-lg"> 99 + <div className="p-5"> 100 + <div className="flex items-center"> 101 + <div className="flex-shrink-0"> 102 + <svg 103 + className="h-6 w-6 text-green-400" 104 + xmlns="http://www.w3.org/2000/svg" 105 + fill="none" 106 + viewBox="0 0 24 24" 107 + stroke="currentColor" 108 + > 109 + <path 110 + strokeLinecap="round" 111 + strokeLinejoin="round" 112 + strokeWidth={2} 113 + d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" 114 + /> 115 + </svg> 116 + </div> 117 + <div className="ml-5 w-0 flex-1"> 118 + <dl> 119 + <dt className="text-sm font-medium text-gray-500 truncate"> 120 + OAuth Status 121 + </dt> 122 + <dd className="mt-1 text-sm text-gray-900"> 123 + Authenticated via ATProtocol 124 + </dd> 125 + </dl> 126 + </div> 127 + </div> 128 + </div> 129 + </div> 130 + 131 + {/* Token Status */} 132 + <div className="bg-white overflow-hidden shadow rounded-lg"> 133 + <div className="p-5"> 134 + <div className="flex items-center"> 135 + <div className="flex-shrink-0"> 136 + <svg 137 + className="h-6 w-6 text-blue-400" 138 + xmlns="http://www.w3.org/2000/svg" 139 + fill="none" 140 + viewBox="0 0 24 24" 141 + stroke="currentColor" 142 + > 143 + <path 144 + strokeLinecap="round" 145 + strokeLinejoin="round" 146 + strokeWidth={2} 147 + d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" 148 + /> 149 + </svg> 150 + </div> 151 + <div className="ml-5 w-0 flex-1"> 152 + <dl> 153 + <dt className="text-sm font-medium text-gray-500 truncate"> 154 + Security 155 + </dt> 156 + <dd className="mt-1 text-sm text-gray-900"> 157 + DPoP-bound tokens active 158 + </dd> 159 + </dl> 160 + </div> 161 + </div> 162 + </div> 163 + </div> 164 + 165 + {/* API Access */} 166 + <div className="bg-white overflow-hidden shadow rounded-lg"> 167 + <div className="p-5"> 168 + <div className="flex items-center"> 169 + <div className="flex-shrink-0"> 170 + <svg 171 + className="h-6 w-6 text-indigo-400" 172 + xmlns="http://www.w3.org/2000/svg" 173 + fill="none" 174 + viewBox="0 0 24 24" 175 + stroke="currentColor" 176 + > 177 + <path 178 + strokeLinecap="round" 179 + strokeLinejoin="round" 180 + strokeWidth={2} 181 + d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" 182 + /> 183 + </svg> 184 + </div> 185 + <div className="ml-5 w-0 flex-1"> 186 + <dl> 187 + <dt className="text-sm font-medium text-gray-500 truncate"> 188 + API Access 189 + </dt> 190 + <dd className="mt-1 text-sm text-gray-900"> 191 + Full XRPC access granted 192 + </dd> 193 + </dl> 194 + </div> 195 + </div> 196 + </div> 197 + </div> 198 + </div> 199 + </div> 200 + 201 + {/* Demo information */} 202 + <div className="px-4 py-4 sm:px-0"> 203 + <div className="bg-blue-50 border-l-4 border-blue-400 p-4"> 204 + <div className="flex"> 205 + <div className="flex-shrink-0"> 206 + <svg 207 + className="h-5 w-5 text-blue-400" 208 + xmlns="http://www.w3.org/2000/svg" 209 + viewBox="0 0 20 20" 210 + fill="currentColor" 211 + aria-hidden="true" 212 + > 213 + <path 214 + fillRule="evenodd" 215 + d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" 216 + clipRule="evenodd" 217 + /> 218 + </svg> 219 + </div> 220 + <div className="ml-3"> 221 + <h3 className="text-sm font-medium text-blue-800"> 222 + ATProtocol OAuth Demo 223 + </h3> 224 + <div className="mt-2 text-sm text-blue-700"> 225 + <p> 226 + This demo app demonstrates secure authentication with ATProtocol OAuth 2.1 including: 227 + </p> 228 + <ul className="list-disc list-inside mt-1 space-y-1"> 229 + <li>PKCE (Proof Key for Code Exchange) with S256</li> 230 + <li>DPoP (Demonstrating Proof of Possession) token binding</li> 231 + <li>PAR (Pushed Authorization Requests)</li> 232 + <li>Automatic token refresh</li> 233 + <li>XRPC API calls to your PDS</li> 234 + </ul> 235 + </div> 236 + </div> 237 + </div> 238 + </div> 239 + </div> 240 + </div> 241 + </div> 242 + ); 243 + };
+199
src/components/Home.tsx
··· 1 + /** 2 + * Home Component 3 + * Landing page for unauthenticated users 4 + */ 5 + 6 + import React from 'react'; 7 + import { Link } from 'react-router-dom'; 8 + import { useAuth } from '../contexts/AuthContext'; 9 + import { Navigate } from 'react-router-dom'; 10 + 11 + export const Home: React.FC = () => { 12 + const { isAuthenticated } = useAuth(); 13 + 14 + // If already authenticated, redirect to dashboard 15 + if (isAuthenticated) { 16 + return <Navigate to="/dashboard" replace />; 17 + } 18 + 19 + return ( 20 + <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100"> 21 + <div className="max-w-7xl mx-auto py-16 px-4 sm:py-24 sm:px-6 lg:px-8"> 22 + <div className="text-center"> 23 + <h1 className="text-4xl font-extrabold text-gray-900 sm:text-5xl sm:tracking-tight lg:text-6xl"> 24 + ATProtocol OAuth Demo 25 + </h1> 26 + <p className="mt-5 max-w-2xl mx-auto text-xl text-gray-500"> 27 + Experience secure, decentralized authentication with ATProtocol OAuth 2.1 28 + </p> 29 + </div> 30 + 31 + <div className="mt-10"> 32 + <div className="max-w-3xl mx-auto"> 33 + <div className="bg-white shadow-xl rounded-lg overflow-hidden"> 34 + <div className="px-6 py-8"> 35 + <h2 className="text-2xl font-bold text-gray-900 mb-4"> 36 + Features of this Demo 37 + </h2> 38 + <div className="space-y-4"> 39 + <div className="flex items-start"> 40 + <div className="flex-shrink-0"> 41 + <svg 42 + className="h-6 w-6 text-green-500" 43 + xmlns="http://www.w3.org/2000/svg" 44 + fill="none" 45 + viewBox="0 0 24 24" 46 + stroke="currentColor" 47 + > 48 + <path 49 + strokeLinecap="round" 50 + strokeLinejoin="round" 51 + strokeWidth={2} 52 + d="M5 13l4 4L19 7" 53 + /> 54 + </svg> 55 + </div> 56 + <div className="ml-3"> 57 + <h3 className="text-lg font-medium text-gray-900"> 58 + OAuth 2.1 with PKCE 59 + </h3> 60 + <p className="text-gray-500"> 61 + Proof Key for Code Exchange (PKCE) with S256 challenge method for secure authorization 62 + </p> 63 + </div> 64 + </div> 65 + 66 + <div className="flex items-start"> 67 + <div className="flex-shrink-0"> 68 + <svg 69 + className="h-6 w-6 text-green-500" 70 + xmlns="http://www.w3.org/2000/svg" 71 + fill="none" 72 + viewBox="0 0 24 24" 73 + stroke="currentColor" 74 + > 75 + <path 76 + strokeLinecap="round" 77 + strokeLinejoin="round" 78 + strokeWidth={2} 79 + d="M5 13l4 4L19 7" 80 + /> 81 + </svg> 82 + </div> 83 + <div className="ml-3"> 84 + <h3 className="text-lg font-medium text-gray-900"> 85 + DPoP Token Binding 86 + </h3> 87 + <p className="text-gray-500"> 88 + Demonstrating Proof of Possession with ES256 cryptographic binding 89 + </p> 90 + </div> 91 + </div> 92 + 93 + <div className="flex items-start"> 94 + <div className="flex-shrink-0"> 95 + <svg 96 + className="h-6 w-6 text-green-500" 97 + xmlns="http://www.w3.org/2000/svg" 98 + fill="none" 99 + viewBox="0 0 24 24" 100 + stroke="currentColor" 101 + > 102 + <path 103 + strokeLinecap="round" 104 + strokeLinejoin="round" 105 + strokeWidth={2} 106 + d="M5 13l4 4L19 7" 107 + /> 108 + </svg> 109 + </div> 110 + <div className="ml-3"> 111 + <h3 className="text-lg font-medium text-gray-900"> 112 + Pushed Authorization Requests 113 + </h3> 114 + <p className="text-gray-500"> 115 + PAR for enhanced security by pre-registering authorization requests 116 + </p> 117 + </div> 118 + </div> 119 + 120 + <div className="flex items-start"> 121 + <div className="flex-shrink-0"> 122 + <svg 123 + className="h-6 w-6 text-green-500" 124 + xmlns="http://www.w3.org/2000/svg" 125 + fill="none" 126 + viewBox="0 0 24 24" 127 + stroke="currentColor" 128 + > 129 + <path 130 + strokeLinecap="round" 131 + strokeLinejoin="round" 132 + strokeWidth={2} 133 + d="M5 13l4 4L19 7" 134 + /> 135 + </svg> 136 + </div> 137 + <div className="ml-3"> 138 + <h3 className="text-lg font-medium text-gray-900"> 139 + XRPC API Integration 140 + </h3> 141 + <p className="text-gray-500"> 142 + Create posts directly in your Personal Data Server using authenticated XRPC calls 143 + </p> 144 + </div> 145 + </div> 146 + 147 + <div className="flex items-start"> 148 + <div className="flex-shrink-0"> 149 + <svg 150 + className="h-6 w-6 text-green-500" 151 + xmlns="http://www.w3.org/2000/svg" 152 + fill="none" 153 + viewBox="0 0 24 24" 154 + stroke="currentColor" 155 + > 156 + <path 157 + strokeLinecap="round" 158 + strokeLinejoin="round" 159 + strokeWidth={2} 160 + d="M5 13l4 4L19 7" 161 + /> 162 + </svg> 163 + </div> 164 + <div className="ml-3"> 165 + <h3 className="text-lg font-medium text-gray-900"> 166 + Automatic Token Refresh 167 + </h3> 168 + <p className="text-gray-500"> 169 + Seamless token rotation with automatic refresh before expiry 170 + </p> 171 + </div> 172 + </div> 173 + </div> 174 + 175 + <div className="mt-8"> 176 + <Link 177 + to="/login" 178 + className="w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" 179 + > 180 + Get Started with Bluesky Authentication 181 + </Link> 182 + </div> 183 + </div> 184 + </div> 185 + 186 + <div className="mt-8 text-center"> 187 + <p className="text-sm text-gray-600"> 188 + This is a demonstration of ATProtocol OAuth 2.1 implementation. 189 + </p> 190 + <p className="text-sm text-gray-600 mt-1"> 191 + Your data remains in your Personal Data Server at all times. 192 + </p> 193 + </div> 194 + </div> 195 + </div> 196 + </div> 197 + </div> 198 + ); 199 + };
+190
src/components/auth/LoginForm.tsx
··· 1 + /** 2 + * Login Form Component 3 + * Handles user handle input and OAuth initiation 4 + */ 5 + 6 + import React, { useState } from 'react'; 7 + import { useAuth } from '../../contexts/AuthContext'; 8 + import { isValidHandle } from '../../utils/did-resolver'; 9 + 10 + export const LoginForm: React.FC = () => { 11 + const [handle, setHandle] = useState(''); 12 + const [isLoading, setIsLoading] = useState(false); 13 + const [error, setError] = useState<string | null>(null); 14 + const { login } = useAuth(); 15 + 16 + const handleSubmit = async (e: React.FormEvent) => { 17 + e.preventDefault(); 18 + setError(null); 19 + 20 + // Validate handle format 21 + if (!handle) { 22 + setError('Please enter your Bluesky handle'); 23 + return; 24 + } 25 + 26 + if (!isValidHandle(handle)) { 27 + setError('Invalid handle format. Example: alice.bsky.social'); 28 + return; 29 + } 30 + 31 + try { 32 + setIsLoading(true); 33 + await login(handle); 34 + // login will redirect to auth server, so this won't be reached 35 + } catch (err) { 36 + console.error('Login failed:', err); 37 + setError( 38 + err instanceof Error 39 + ? err.message 40 + : 'Failed to start authentication. Please try again.' 41 + ); 42 + setIsLoading(false); 43 + } 44 + }; 45 + 46 + const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { 47 + setHandle(e.target.value); 48 + // Clear error when user starts typing 49 + if (error) { 50 + setError(null); 51 + } 52 + }; 53 + 54 + return ( 55 + <div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> 56 + <div className="max-w-md w-full space-y-8"> 57 + <div> 58 + <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900"> 59 + Sign in to ATProto Demo 60 + </h2> 61 + <p className="mt-2 text-center text-sm text-gray-600"> 62 + Enter your Bluesky handle to get started 63 + </p> 64 + </div> 65 + 66 + <form className="mt-8 space-y-6" onSubmit={handleSubmit}> 67 + <div className="rounded-md shadow-sm -space-y-px"> 68 + <div> 69 + <label htmlFor="handle" className="sr-only"> 70 + Bluesky Handle 71 + </label> 72 + <input 73 + id="handle" 74 + name="handle" 75 + type="text" 76 + autoComplete="username" 77 + required 78 + className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm" 79 + placeholder="alice.bsky.social" 80 + value={handle} 81 + onChange={handleInputChange} 82 + disabled={isLoading} 83 + aria-label="Bluesky handle" 84 + aria-describedby={error ? 'handle-error' : undefined} 85 + /> 86 + </div> 87 + </div> 88 + 89 + {error && ( 90 + <div 91 + id="handle-error" 92 + className="rounded-md bg-red-50 p-4" 93 + role="alert" 94 + > 95 + <div className="flex"> 96 + <div className="flex-shrink-0"> 97 + <svg 98 + className="h-5 w-5 text-red-400" 99 + xmlns="http://www.w3.org/2000/svg" 100 + viewBox="0 0 20 20" 101 + fill="currentColor" 102 + aria-hidden="true" 103 + > 104 + <path 105 + fillRule="evenodd" 106 + d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" 107 + clipRule="evenodd" 108 + /> 109 + </svg> 110 + </div> 111 + <div className="ml-3"> 112 + <p className="text-sm text-red-800">{error}</p> 113 + </div> 114 + </div> 115 + </div> 116 + )} 117 + 118 + <div> 119 + <button 120 + type="submit" 121 + disabled={isLoading || !handle} 122 + className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" 123 + > 124 + {isLoading ? ( 125 + <> 126 + <span className="absolute left-0 inset-y-0 flex items-center pl-3"> 127 + <svg 128 + className="animate-spin h-5 w-5 text-white" 129 + xmlns="http://www.w3.org/2000/svg" 130 + fill="none" 131 + viewBox="0 0 24 24" 132 + > 133 + <circle 134 + className="opacity-25" 135 + cx="12" 136 + cy="12" 137 + r="10" 138 + stroke="currentColor" 139 + strokeWidth="4" 140 + /> 141 + <path 142 + className="opacity-75" 143 + fill="currentColor" 144 + d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" 145 + /> 146 + </svg> 147 + </span> 148 + Connecting... 149 + </> 150 + ) : ( 151 + 'Sign in with Bluesky' 152 + )} 153 + </button> 154 + </div> 155 + 156 + <div className="text-center"> 157 + <p className="text-xs text-gray-500"> 158 + By signing in, you'll be redirected to your Bluesky PDS to 159 + authorize this app. 160 + </p> 161 + </div> 162 + </form> 163 + 164 + <div className="mt-6"> 165 + <div className="relative"> 166 + <div className="absolute inset-0 flex items-center"> 167 + <div className="w-full border-t border-gray-300" /> 168 + </div> 169 + <div className="relative flex justify-center text-sm"> 170 + <span className="px-2 bg-gray-50 text-gray-500"> 171 + Don't have a Bluesky account? 172 + </span> 173 + </div> 174 + </div> 175 + 176 + <div className="mt-6"> 177 + <a 178 + href="https://bsky.app" 179 + target="_blank" 180 + rel="noopener noreferrer" 181 + className="w-full inline-flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-500 hover:bg-gray-50" 182 + > 183 + Sign up for Bluesky 184 + </a> 185 + </div> 186 + </div> 187 + </div> 188 + </div> 189 + ); 190 + };
+157
src/components/auth/OAuthCallback.tsx
··· 1 + /** 2 + * OAuth Callback Component 3 + * Handles the OAuth callback and exchanges code for tokens 4 + */ 5 + 6 + import React, { useEffect, useState, useRef } from 'react'; 7 + import { useNavigate, useSearchParams } from 'react-router-dom'; 8 + import { useAuth } from '../../contexts/AuthContext'; 9 + 10 + export const OAuthCallback: React.FC = () => { 11 + const [searchParams] = useSearchParams(); 12 + const navigate = useNavigate(); 13 + const { handleCallback } = useAuth(); 14 + const [error, setError] = useState<string | null>(null); 15 + const [isProcessing, setIsProcessing] = useState(false); 16 + const hasProcessed = useRef(false); 17 + 18 + useEffect(() => { 19 + // Prevent duplicate processing (React StrictMode or re-renders) 20 + if (hasProcessed.current || isProcessing) { 21 + console.log('[Callback] Already processed or processing, skipping'); 22 + return; 23 + } 24 + 25 + const processCallback = async () => { 26 + // Mark as processing to prevent concurrent calls 27 + setIsProcessing(true); 28 + hasProcessed.current = true; 29 + 30 + console.log('[Callback] Starting OAuth callback processing'); 31 + 32 + // Extract parameters from URL 33 + const code = searchParams.get('code'); 34 + const state = searchParams.get('state'); 35 + const iss = searchParams.get('iss'); 36 + const errorParam = searchParams.get('error'); 37 + const errorDescription = searchParams.get('error_description'); 38 + 39 + // Handle OAuth errors 40 + if (errorParam) { 41 + console.error('[Callback] OAuth error from server:', errorParam, errorDescription); 42 + setError(errorDescription || errorParam); 43 + setIsProcessing(false); 44 + return; 45 + } 46 + 47 + // Validate required parameters 48 + if (!code || !state) { 49 + console.error('[Callback] Missing required parameters'); 50 + setError('Missing required OAuth parameters'); 51 + setIsProcessing(false); 52 + return; 53 + } 54 + 55 + try { 56 + console.log('[Callback] Processing callback with code and state'); 57 + // Process the callback 58 + await handleCallback(code, state, iss || undefined); 59 + 60 + console.log('[Callback] Success! Redirecting to dashboard'); 61 + // Success - redirect to dashboard 62 + navigate('/dashboard', { replace: true }); 63 + } catch (err) { 64 + console.error('[Callback] Processing failed:', err); 65 + setError( 66 + err instanceof Error 67 + ? err.message 68 + : 'Failed to complete authentication' 69 + ); 70 + setIsProcessing(false); 71 + } 72 + }; 73 + 74 + processCallback(); 75 + }, [searchParams, handleCallback, navigate, isProcessing]); 76 + 77 + if (error) { 78 + return ( 79 + <div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> 80 + <div className="max-w-md w-full space-y-8"> 81 + <div className="rounded-md bg-red-50 p-4"> 82 + <div className="flex"> 83 + <div className="flex-shrink-0"> 84 + <svg 85 + className="h-5 w-5 text-red-400" 86 + xmlns="http://www.w3.org/2000/svg" 87 + viewBox="0 0 20 20" 88 + fill="currentColor" 89 + aria-hidden="true" 90 + > 91 + <path 92 + fillRule="evenodd" 93 + d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" 94 + clipRule="evenodd" 95 + /> 96 + </svg> 97 + </div> 98 + <div className="ml-3"> 99 + <h3 className="text-sm font-medium text-red-800"> 100 + Authentication Failed 101 + </h3> 102 + <div className="mt-2 text-sm text-red-700"> 103 + <p>{error}</p> 104 + </div> 105 + <div className="mt-4"> 106 + <button 107 + onClick={() => navigate('/login', { replace: true })} 108 + className="text-sm font-medium text-red-800 hover:text-red-700" 109 + > 110 + Back to login 111 + </button> 112 + </div> 113 + </div> 114 + </div> 115 + </div> 116 + </div> 117 + </div> 118 + ); 119 + } 120 + 121 + return ( 122 + <div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> 123 + <div className="max-w-md w-full space-y-8 text-center"> 124 + <div> 125 + <div className="flex justify-center"> 126 + <svg 127 + className="animate-spin h-12 w-12 text-blue-600" 128 + xmlns="http://www.w3.org/2000/svg" 129 + fill="none" 130 + viewBox="0 0 24 24" 131 + > 132 + <circle 133 + className="opacity-25" 134 + cx="12" 135 + cy="12" 136 + r="10" 137 + stroke="currentColor" 138 + strokeWidth="4" 139 + /> 140 + <path 141 + className="opacity-75" 142 + fill="currentColor" 143 + d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" 144 + /> 145 + </svg> 146 + </div> 147 + <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900"> 148 + Completing authentication... 149 + </h2> 150 + <p className="mt-2 text-center text-sm text-gray-600"> 151 + Please wait while we verify your credentials 152 + </p> 153 + </div> 154 + </div> 155 + </div> 156 + ); 157 + };
+113
src/components/layout/Header.tsx
··· 1 + /** 2 + * Header Component 3 + * Displays user information and navigation 4 + */ 5 + 6 + import React from 'react'; 7 + import { Link, useNavigate } from 'react-router-dom'; 8 + import { useAuth } from '../../contexts/AuthContext'; 9 + 10 + export const Header: React.FC = () => { 11 + const { user, isAuthenticated, logout } = useAuth(); 12 + const navigate = useNavigate(); 13 + 14 + const handleLogout = async () => { 15 + await logout(); 16 + navigate('/login', { replace: true }); 17 + }; 18 + 19 + if (!isAuthenticated || !user) { 20 + return null; 21 + } 22 + 23 + return ( 24 + <header className="bg-white shadow"> 25 + <nav className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> 26 + <div className="flex justify-between h-16"> 27 + <div className="flex"> 28 + <div className="flex-shrink-0 flex items-center"> 29 + <Link to="/dashboard" className="text-xl font-bold text-gray-800"> 30 + ATProto Demo 31 + </Link> 32 + </div> 33 + <div className="hidden sm:ml-6 sm:flex sm:space-x-8"> 34 + <Link 35 + to="/dashboard" 36 + className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium" 37 + > 38 + Dashboard 39 + </Link> 40 + <Link 41 + to="/create-post" 42 + className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium" 43 + > 44 + Create Post 45 + </Link> 46 + </div> 47 + </div> 48 + 49 + <div className="flex items-center"> 50 + <div className="flex-shrink-0 flex items-center space-x-4"> 51 + <div className="flex items-center space-x-2"> 52 + {user.avatar && ( 53 + <img 54 + className="h-8 w-8 rounded-full" 55 + src={user.avatar} 56 + alt={user.handle} 57 + /> 58 + )} 59 + <div className="hidden md:block"> 60 + <div className="text-sm font-medium text-gray-700"> 61 + {user.displayName || user.handle} 62 + </div> 63 + <div className="text-xs text-gray-500">@{user.handle}</div> 64 + </div> 65 + </div> 66 + 67 + <button 68 + onClick={handleLogout} 69 + className="bg-white p-1 rounded-full text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" 70 + title="Sign out" 71 + > 72 + <span className="sr-only">Sign out</span> 73 + <svg 74 + className="h-6 w-6" 75 + xmlns="http://www.w3.org/2000/svg" 76 + fill="none" 77 + viewBox="0 0 24 24" 78 + stroke="currentColor" 79 + aria-hidden="true" 80 + > 81 + <path 82 + strokeLinecap="round" 83 + strokeLinejoin="round" 84 + strokeWidth={2} 85 + d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" 86 + /> 87 + </svg> 88 + </button> 89 + </div> 90 + </div> 91 + </div> 92 + 93 + {/* Mobile menu */} 94 + <div className="sm:hidden"> 95 + <div className="pt-2 pb-3 space-y-1"> 96 + <Link 97 + to="/dashboard" 98 + className="bg-blue-50 border-blue-500 text-blue-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium" 99 + > 100 + Dashboard 101 + </Link> 102 + <Link 103 + to="/create-post" 104 + className="border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium" 105 + > 106 + Create Post 107 + </Link> 108 + </div> 109 + </div> 110 + </nav> 111 + </header> 112 + ); 113 + };
+56
src/components/layout/ProtectedRoute.tsx
··· 1 + /** 2 + * Protected Route Component 3 + * Wraps routes that require authentication 4 + */ 5 + 6 + import React from 'react'; 7 + import { Navigate, Outlet, useLocation } from 'react-router-dom'; 8 + import { useAuth } from '../../contexts/AuthContext'; 9 + 10 + interface ProtectedRouteProps { 11 + children?: React.ReactNode; 12 + } 13 + 14 + export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => { 15 + const { isAuthenticated, loading } = useAuth(); 16 + const location = useLocation(); 17 + 18 + // Show loading spinner while checking auth status 19 + if (loading) { 20 + return ( 21 + <div className="min-h-screen flex items-center justify-center bg-gray-50"> 22 + <div className="text-center"> 23 + <svg 24 + className="animate-spin h-8 w-8 text-blue-600 mx-auto" 25 + xmlns="http://www.w3.org/2000/svg" 26 + fill="none" 27 + viewBox="0 0 24 24" 28 + > 29 + <circle 30 + className="opacity-25" 31 + cx="12" 32 + cy="12" 33 + r="10" 34 + stroke="currentColor" 35 + strokeWidth="4" 36 + /> 37 + <path 38 + className="opacity-75" 39 + fill="currentColor" 40 + d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" 41 + /> 42 + </svg> 43 + <p className="mt-2 text-sm text-gray-600">Loading...</p> 44 + </div> 45 + </div> 46 + ); 47 + } 48 + 49 + // Redirect to login if not authenticated 50 + if (!isAuthenticated) { 51 + return <Navigate to="/login" state={{ from: location }} replace />; 52 + } 53 + 54 + // Render children or outlet for nested routes 55 + return children || <Outlet />; 56 + };
+262
src/components/post/CreatePost.tsx
··· 1 + /** 2 + * Create Now Component 3 + * Allows users to create "now" records in their PDS 4 + */ 5 + 6 + import React, { useState } from 'react'; 7 + import { useNavigate } from 'react-router-dom'; 8 + import { useXRPC } from '../../contexts/AuthContext'; 9 + 10 + export const CreatePost: React.FC = () => { 11 + const [nowText, setNowText] = useState(''); 12 + const [isSubmitting, setIsSubmitting] = useState(false); 13 + const [error, setError] = useState<string | null>(null); 14 + const [success, setSuccess] = useState(false); 15 + const [recordUri, setRecordUri] = useState<string | null>(null); 16 + 17 + const xrpcClient = useXRPC(); 18 + const navigate = useNavigate(); 19 + 20 + const characterCount = nowText.length; 21 + const maxCharacters = 300; 22 + 23 + const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { 24 + const text = e.target.value; 25 + 26 + // Enforce character limit 27 + if (text.length <= maxCharacters) { 28 + setNowText(text); 29 + setError(null); 30 + setSuccess(false); 31 + } 32 + }; 33 + 34 + const handleSubmit = async (e: React.FormEvent) => { 35 + e.preventDefault(); 36 + 37 + if (!xrpcClient) { 38 + setError('Not authenticated'); 39 + return; 40 + } 41 + 42 + if (!nowText.trim()) { 43 + setError('Please enter what you\'re doing now'); 44 + return; 45 + } 46 + 47 + if (characterCount > maxCharacters) { 48 + setError(`Text must be ${maxCharacters} characters or less`); 49 + return; 50 + } 51 + 52 + try { 53 + setIsSubmitting(true); 54 + setError(null); 55 + 56 + // Create the "now" record 57 + const result = await xrpcClient.createNow(nowText.trim()); 58 + 59 + console.log('Now record created:', result); 60 + 61 + // Show success message 62 + setSuccess(true); 63 + setRecordUri(result.uri); 64 + 65 + // Clear the form 66 + setNowText(''); 67 + 68 + // After 3 seconds, redirect to dashboard 69 + setTimeout(() => { 70 + navigate('/dashboard'); 71 + }, 3000); 72 + } catch (err) { 73 + console.error('Failed to create now record:', err); 74 + setError( 75 + err instanceof Error 76 + ? err.message 77 + : 'Failed to create record. Please try again.' 78 + ); 79 + } finally { 80 + setIsSubmitting(false); 81 + } 82 + }; 83 + 84 + const getCharacterCountColor = () => { 85 + if (characterCount > maxCharacters) return 'text-red-500'; 86 + if (characterCount > maxCharacters * 0.9) return 'text-yellow-500'; 87 + return 'text-gray-500'; 88 + }; 89 + 90 + return ( 91 + <div className="min-h-screen bg-gray-50"> 92 + <div className="max-w-3xl mx-auto py-8 px-4 sm:px-6 lg:px-8"> 93 + <div className="bg-white shadow rounded-lg"> 94 + <div className="px-4 py-5 sm:p-6"> 95 + <h3 className="text-lg leading-6 font-medium text-gray-900"> 96 + Create Now 97 + </h3> 98 + <div className="mt-2 max-w-xl text-sm text-gray-500"> 99 + <p>Share what you're currently working on.</p> 100 + </div> 101 + 102 + <form className="mt-5 space-y-4" onSubmit={handleSubmit}> 103 + <div> 104 + <label 105 + htmlFor="now-text" 106 + className="block text-sm font-medium text-gray-700" 107 + > 108 + What are you doing now? 109 + </label> 110 + <div className="mt-1"> 111 + <textarea 112 + id="now-text" 113 + name="now-text" 114 + rows={4} 115 + className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" 116 + placeholder="Working on my React app" 117 + value={nowText} 118 + onChange={handleTextChange} 119 + disabled={isSubmitting} 120 + aria-describedby="character-count" 121 + /> 122 + </div> 123 + <p 124 + id="character-count" 125 + className={`mt-2 text-sm ${getCharacterCountColor()}`} 126 + > 127 + {characterCount} / {maxCharacters} characters 128 + </p> 129 + </div> 130 + 131 + {error && ( 132 + <div className="rounded-md bg-red-50 p-4"> 133 + <div className="flex"> 134 + <div className="flex-shrink-0"> 135 + <svg 136 + className="h-5 w-5 text-red-400" 137 + xmlns="http://www.w3.org/2000/svg" 138 + viewBox="0 0 20 20" 139 + fill="currentColor" 140 + aria-hidden="true" 141 + > 142 + <path 143 + fillRule="evenodd" 144 + d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" 145 + clipRule="evenodd" 146 + /> 147 + </svg> 148 + </div> 149 + <div className="ml-3"> 150 + <p className="text-sm text-red-800">{error}</p> 151 + </div> 152 + </div> 153 + </div> 154 + )} 155 + 156 + {success && ( 157 + <div className="rounded-md bg-green-50 p-4"> 158 + <div className="flex"> 159 + <div className="flex-shrink-0"> 160 + <svg 161 + className="h-5 w-5 text-green-400" 162 + xmlns="http://www.w3.org/2000/svg" 163 + viewBox="0 0 20 20" 164 + fill="currentColor" 165 + aria-hidden="true" 166 + > 167 + <path 168 + fillRule="evenodd" 169 + d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" 170 + clipRule="evenodd" 171 + /> 172 + </svg> 173 + </div> 174 + <div className="ml-3"> 175 + <p className="text-sm text-green-800"> 176 + Now record created successfully! 177 + </p> 178 + {recordUri && ( 179 + <p className="text-xs text-green-600 mt-1"> 180 + URI: {recordUri} 181 + </p> 182 + )} 183 + </div> 184 + </div> 185 + </div> 186 + )} 187 + 188 + <div className="flex justify-end space-x-3"> 189 + <button 190 + type="button" 191 + onClick={() => navigate('/dashboard')} 192 + className="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" 193 + disabled={isSubmitting} 194 + > 195 + Cancel 196 + </button> 197 + <button 198 + type="submit" 199 + className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" 200 + disabled={isSubmitting || !nowText.trim() || characterCount > maxCharacters} 201 + > 202 + {isSubmitting ? ( 203 + <> 204 + <svg 205 + className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" 206 + xmlns="http://www.w3.org/2000/svg" 207 + fill="none" 208 + viewBox="0 0 24 24" 209 + > 210 + <circle 211 + className="opacity-25" 212 + cx="12" 213 + cy="12" 214 + r="10" 215 + stroke="currentColor" 216 + strokeWidth="4" 217 + /> 218 + <path 219 + className="opacity-75" 220 + fill="currentColor" 221 + d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" 222 + /> 223 + </svg> 224 + Creating... 225 + </> 226 + ) : ( 227 + 'Create Now' 228 + )} 229 + </button> 230 + </div> 231 + </form> 232 + </div> 233 + </div> 234 + 235 + <div className="mt-6 bg-blue-50 rounded-lg p-4"> 236 + <div className="flex"> 237 + <div className="flex-shrink-0"> 238 + <svg 239 + className="h-5 w-5 text-blue-400" 240 + xmlns="http://www.w3.org/2000/svg" 241 + viewBox="0 0 20 20" 242 + fill="currentColor" 243 + aria-hidden="true" 244 + > 245 + <path 246 + fillRule="evenodd" 247 + d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" 248 + clipRule="evenodd" 249 + /> 250 + </svg> 251 + </div> 252 + <div className="ml-3 flex-1 md:flex md:justify-between"> 253 + <p className="text-sm text-blue-700"> 254 + Now records are stored in your Personal Data Server (PDS) using the collection garden.lexicon.oauth-masterclass.now. 255 + </p> 256 + </div> 257 + </div> 258 + </div> 259 + </div> 260 + </div> 261 + ); 262 + };
+235
src/contexts/AuthContext.tsx
··· 1 + /** 2 + * Authentication Context for ATProtocol OAuth 3 + * Manages auth state, login/logout, and token refresh 4 + */ 5 + 6 + import React, { 7 + createContext, 8 + useContext, 9 + useState, 10 + useEffect, 11 + useCallback, 12 + ReactNode, 13 + } from 'react'; 14 + 15 + import { ATProtoOAuthClient } from '../lib/atproto-oauth-client'; 16 + import { XRPCClient } from '../lib/xrpc-client'; 17 + 18 + import type { 19 + OAuthSession, 20 + UserProfile, 21 + AuthContextType, 22 + } from '../types/auth'; 23 + 24 + const AuthContext = createContext<AuthContextType | null>(null); 25 + 26 + interface AuthProviderProps { 27 + children: ReactNode; 28 + } 29 + 30 + export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => { 31 + const [user, setUser] = useState<UserProfile | null>(null); 32 + const [session, setSession] = useState<OAuthSession | null>(null); 33 + const [loading, setLoading] = useState(true); 34 + const [error, setError] = useState<Error | null>(null); 35 + 36 + // Create stable client instances (only once) 37 + const [oauthClient] = useState(() => new ATProtoOAuthClient()); 38 + const [xrpcClient] = useState(() => new XRPCClient()); 39 + 40 + /** 41 + * Initialize auth state on mount 42 + */ 43 + useEffect(() => { 44 + const initAuth = async () => { 45 + try { 46 + const storedSession = await oauthClient.getSession(); 47 + 48 + if (storedSession) { 49 + // Session exists, restore user from session data 50 + setSession(storedSession); 51 + setUser({ 52 + did: storedSession.did, 53 + handle: storedSession.handle, 54 + }); 55 + } 56 + } catch (err) { 57 + console.error('Auth initialization error:', err); 58 + setError(err as Error); 59 + } finally { 60 + setLoading(false); 61 + } 62 + }; 63 + 64 + initAuth(); 65 + }, [oauthClient, xrpcClient]); 66 + 67 + /** 68 + * Start OAuth login flow 69 + * @param handle - User's handle 70 + */ 71 + const login = useCallback(async (handle: string) => { 72 + try { 73 + setLoading(true); 74 + setError(null); 75 + 76 + const authUrl = await oauthClient.authorize(handle); 77 + 78 + // Redirect to authorization server 79 + window.location.href = authUrl; 80 + } catch (err) { 81 + console.error('Login error:', err); 82 + setError(err as Error); 83 + setLoading(false); 84 + throw err; 85 + } 86 + }, [oauthClient]); 87 + 88 + /** 89 + * Handle OAuth callback 90 + * @param code - Authorization code 91 + * @param state - State parameter 92 + * @param iss - Optional issuer parameter 93 + */ 94 + const handleCallback = useCallback(async ( 95 + code: string, 96 + state: string, 97 + iss?: string 98 + ) => { 99 + try { 100 + setLoading(true); 101 + setError(null); 102 + 103 + const newSession = await oauthClient.callback(code, state, iss); 104 + 105 + // Set session and user from the session data 106 + setSession(newSession); 107 + setUser({ 108 + did: newSession.did, 109 + handle: newSession.handle, 110 + }); 111 + } catch (err) { 112 + console.error('Callback error:', err); 113 + setError(err as Error); 114 + throw err; 115 + } finally { 116 + setLoading(false); 117 + } 118 + }, [oauthClient, xrpcClient]); 119 + 120 + /** 121 + * Logout and clear session 122 + */ 123 + const logout = useCallback(async () => { 124 + try { 125 + setLoading(true); 126 + 127 + await oauthClient.clearSession(); 128 + 129 + setUser(null); 130 + setSession(null); 131 + setError(null); 132 + } catch (err) { 133 + console.error('Logout error:', err); 134 + setError(err as Error); 135 + } finally { 136 + setLoading(false); 137 + } 138 + }, [oauthClient]); 139 + 140 + /** 141 + * Manually refresh access token 142 + */ 143 + const refreshToken = useCallback(async () => { 144 + if (!session) { 145 + throw new Error('No active session'); 146 + } 147 + 148 + try { 149 + const newSession = await oauthClient.refreshAccessToken(session); 150 + setSession(newSession); 151 + } catch (err) { 152 + console.error('Token refresh error:', err); 153 + setError(err as Error); 154 + throw err; 155 + } 156 + }, [session, oauthClient]); 157 + 158 + /** 159 + * Auto-refresh token before expiry 160 + */ 161 + useEffect(() => { 162 + if (!session) { 163 + return; 164 + } 165 + 166 + // Calculate time until token expiry 167 + const timeUntilExpiry = session.expiresAt - Date.now(); 168 + 169 + // If token is already expired or expiring in less than 1 minute 170 + if (timeUntilExpiry <= 60000) { 171 + refreshToken().catch((err) => { 172 + console.error('Auto-refresh failed:', err); 173 + // If refresh fails, clear session 174 + logout(); 175 + }); 176 + return; 177 + } 178 + 179 + // Set timeout to refresh 1 minute before expiry 180 + const refreshTimeout = setTimeout(() => { 181 + refreshToken().catch((err) => { 182 + console.error('Auto-refresh failed:', err); 183 + logout(); 184 + }); 185 + }, timeUntilExpiry - 60000); 186 + 187 + return () => clearTimeout(refreshTimeout); 188 + }, [session, refreshToken, logout]); 189 + 190 + const contextValue: AuthContextType = { 191 + user, 192 + session, 193 + loading, 194 + error, 195 + isAuthenticated: !!user && !!session, 196 + login, 197 + handleCallback, 198 + logout, 199 + refreshToken, 200 + }; 201 + 202 + return ( 203 + <AuthContext.Provider value={contextValue}> 204 + {children} 205 + </AuthContext.Provider> 206 + ); 207 + }; 208 + 209 + /** 210 + * Hook to use auth context 211 + * @returns Auth context value 212 + */ 213 + export const useAuth = (): AuthContextType => { 214 + const context = useContext(AuthContext); 215 + if (!context) { 216 + throw new Error('useAuth must be used within AuthProvider'); 217 + } 218 + return context; 219 + }; 220 + 221 + /** 222 + * Get XRPC client with current session 223 + * @returns Configured XRPC client 224 + */ 225 + export const useXRPC = (): XRPCClient | null => { 226 + const { session } = useAuth(); 227 + 228 + if (!session) { 229 + return null; 230 + } 231 + 232 + const client = new XRPCClient(); 233 + client.setSession(session); 234 + return client; 235 + };
+3
src/index.css
··· 1 + @tailwind base; 2 + @tailwind components; 3 + @tailwind utilities;
+618
src/lib/atproto-oauth-client.ts
··· 1 + /** 2 + * ATProtocol OAuth Client Wrapper 3 + * Implements OAuth 2.1 with PKCE, DPoP, and PAR 4 + */ 5 + 6 + import { 7 + generateCodeVerifier, 8 + generateCodeChallenge, 9 + generateDpopKeyPair, 10 + generateDpopProof, 11 + generateRandomString, 12 + storeDpopKeypair, 13 + retrieveDpopKeypair, 14 + clearDpopKeypairs, 15 + } from '../utils/crypto-utils'; 16 + 17 + import { 18 + discoverFromHandle, 19 + verifyHandleDidBinding, 20 + } from '../utils/did-resolver'; 21 + 22 + import type { 23 + OAuthSession, 24 + OAuthState, 25 + TokenResponse, 26 + PARResponse, 27 + AuthorizationServerMetadata, 28 + ClientMetadata, 29 + DPoPNonce, 30 + OAuthError, 31 + } from '../types/auth'; 32 + 33 + export class ATProtoOAuthClient { 34 + private clientMetadata: ClientMetadata; 35 + private dpopNonces: Map<string, DPoPNonce> = new Map(); 36 + 37 + constructor() { 38 + this.clientMetadata = { 39 + client_id: import.meta.env.VITE_CLIENT_ID || 'https://oauth-spa.smokesignal.tools/client-metadata.json', 40 + application_type: 'web', 41 + client_name: 'ATProto Demo App', 42 + redirect_uris: [import.meta.env.VITE_REDIRECT_URI || 'https://oauth-spa.smokesignal.tools/oauth/callback'], 43 + scope: 'atproto repo:garden.lexicon.oauth-masterclass.now', 44 + grant_types: ['authorization_code', 'refresh_token'], 45 + response_types: ['code'], 46 + token_endpoint_auth_method: 'none', 47 + dpop_bound_access_tokens: true, 48 + }; 49 + } 50 + 51 + /** 52 + * Start OAuth flow for a given handle 53 + * @param handle - The user's handle 54 + * @returns Authorization URL to redirect user to 55 + */ 56 + async authorize(handle: string): Promise<string> { 57 + // Discover PDS and auth server from handle 58 + const { did, pdsUrl, authServer } = await discoverFromHandle(handle); 59 + 60 + // Generate session secrets 61 + const codeVerifier = generateCodeVerifier(); 62 + const codeChallenge = await generateCodeChallenge(codeVerifier); 63 + const state = generateRandomString(16); 64 + const dpopKey = await generateDpopKeyPair(); 65 + 66 + // Store state for callback 67 + const oauthState: OAuthState = { 68 + codeVerifier, 69 + state, 70 + dpopKey, 71 + handle, 72 + pdsUrl, 73 + authServerUrl: authServer.issuer, 74 + }; 75 + 76 + // Store in sessionStorage 77 + sessionStorage.setItem('oauth_state', JSON.stringify({ 78 + ...oauthState, 79 + dpopKey: undefined, // Don't store key in sessionStorage 80 + })); 81 + 82 + // Store DPoP key in IndexedDB 83 + await storeDpopKeypair(dpopKey, `oauth_${state}`); 84 + 85 + // Make PAR request 86 + const requestUri = await this.pushedAuthorizationRequest( 87 + authServer, 88 + codeChallenge, 89 + state, 90 + dpopKey 91 + ); 92 + 93 + // Build authorization URL 94 + const authUrl = new URL(authServer.authorization_endpoint); 95 + authUrl.searchParams.set('client_id', this.clientMetadata.client_id); 96 + authUrl.searchParams.set('request_uri', requestUri); 97 + 98 + return authUrl.toString(); 99 + } 100 + 101 + /** 102 + * Discover DPoP nonce for an endpoint by making a preflight request 103 + * Note: Disabled for now as some servers don't support HEAD requests 104 + * We rely on error-based nonce discovery instead 105 + */ 106 + private async discoverDpopNonce( 107 + endpoint: string, 108 + dpopKey: CryptoKeyPair 109 + ): Promise<string | undefined> { 110 + // Disabled - rely on error-based discovery 111 + return undefined; 112 + } 113 + 114 + /** 115 + * Make Pushed Authorization Request (PAR) 116 + * @param authServer - Authorization server metadata 117 + * @param codeChallenge - PKCE code challenge 118 + * @param state - State parameter 119 + * @param dpopKey - DPoP keypair 120 + * @returns Request URI for authorization 121 + */ 122 + private async pushedAuthorizationRequest( 123 + authServer: AuthorizationServerMetadata, 124 + codeChallenge: string, 125 + state: string, 126 + dpopKey: CryptoKeyPair 127 + ): Promise<string> { 128 + const parEndpoint = authServer.pushed_authorization_request_endpoint; 129 + 130 + // Try to discover nonce proactively 131 + await this.discoverDpopNonce(parEndpoint, dpopKey); 132 + 133 + // Build PAR parameters 134 + const params = new URLSearchParams({ 135 + client_id: this.clientMetadata.client_id, 136 + response_type: 'code', 137 + redirect_uri: this.clientMetadata.redirect_uris[0], 138 + scope: this.clientMetadata.scope, 139 + state: state, 140 + code_challenge: codeChallenge, 141 + code_challenge_method: 'S256', 142 + }); 143 + 144 + // Get any previously stored nonce for this endpoint 145 + let nonce = this.getDpopNonce(parEndpoint); 146 + 147 + // Try PAR with nonce if available 148 + let dpopProof = await generateDpopProof( 149 + dpopKey, 150 + 'POST', 151 + parEndpoint, 152 + undefined, 153 + nonce 154 + ); 155 + 156 + let response = await fetch(parEndpoint, { 157 + method: 'POST', 158 + headers: { 159 + 'Content-Type': 'application/x-www-form-urlencoded', 160 + 'DPoP': dpopProof, 161 + }, 162 + body: params.toString(), 163 + }); 164 + 165 + // Always check for new nonce in response headers 166 + let responseNonce = response.headers.get('DPoP-Nonce'); 167 + if (responseNonce) { 168 + this.storeDpopNonce(parEndpoint, responseNonce); 169 + nonce = responseNonce; // Update our local nonce variable 170 + } 171 + 172 + // If we get a 401/400, retry exactly once (with or without nonce) 173 + if (!response.ok && (response.status === 401 || response.status === 400)) { 174 + console.log('[PAR] First attempt failed with status:', response.status); 175 + console.log('[PAR] Nonce from error response:', responseNonce || 'none'); 176 + 177 + // Always retry on 401/400, even if no nonce was provided 178 + // Use whatever nonce we have (from response or from previous storage) 179 + const retryNonce = responseNonce || this.getDpopNonce(parEndpoint); 180 + console.log('[PAR] Retrying with nonce:', retryNonce || 'none'); 181 + 182 + dpopProof = await generateDpopProof( 183 + dpopKey, 184 + 'POST', 185 + parEndpoint, 186 + undefined, 187 + retryNonce 188 + ); 189 + 190 + response = await fetch(parEndpoint, { 191 + method: 'POST', 192 + headers: { 193 + 'Content-Type': 'application/x-www-form-urlencoded', 194 + 'DPoP': dpopProof, 195 + }, 196 + body: params.toString(), 197 + }); 198 + 199 + // Store any new nonce from retry 200 + const newRetryNonce = response.headers.get('DPoP-Nonce'); 201 + if (newRetryNonce) { 202 + this.storeDpopNonce(parEndpoint, newRetryNonce); 203 + console.log('[PAR] Retry succeeded, stored new nonce'); 204 + } 205 + 206 + if (response.ok) { 207 + console.log('[PAR] Retry succeeded!'); 208 + } else { 209 + console.log('[PAR] Retry failed with status:', response.status); 210 + } 211 + } 212 + 213 + if (!response.ok) { 214 + const error = await response.text(); 215 + console.error('[PAR] Final error:', error); 216 + throw new Error(`PAR request failed: ${error}`); 217 + } 218 + 219 + const parResponse: PARResponse = await response.json(); 220 + return parResponse.request_uri; 221 + } 222 + 223 + /** 224 + * Handle OAuth callback 225 + * @param code - Authorization code 226 + * @param state - State parameter 227 + * @param iss - Issuer parameter 228 + * @returns OAuth session 229 + */ 230 + async callback( 231 + code: string, 232 + state: string, 233 + iss?: string 234 + ): Promise<OAuthSession> { 235 + // Retrieve stored state 236 + const storedStateStr = sessionStorage.getItem('oauth_state'); 237 + if (!storedStateStr) { 238 + throw new Error('No OAuth state found'); 239 + } 240 + 241 + const storedState = JSON.parse(storedStateStr) as Omit<OAuthState, 'dpopKey'>; 242 + 243 + // Verify state parameter 244 + if (state !== storedState.state) { 245 + throw new Error('State mismatch - possible CSRF attack'); 246 + } 247 + 248 + // Verify issuer if provided 249 + if (iss && iss !== storedState.authServerUrl) { 250 + throw new Error('Issuer mismatch'); 251 + } 252 + 253 + // Retrieve DPoP key from IndexedDB 254 + const dpopKey = await retrieveDpopKeypair(`oauth_${state}`); 255 + if (!dpopKey) { 256 + throw new Error('DPoP key not found'); 257 + } 258 + 259 + // Exchange code for tokens 260 + const authServer = { issuer: storedState.authServerUrl } as AuthorizationServerMetadata; 261 + const { did, pdsUrl, authServer: fullAuthServer } = await discoverFromHandle(storedState.handle); 262 + 263 + const tokens = await this.exchangeCodeForTokens( 264 + fullAuthServer, 265 + code, 266 + storedState.codeVerifier, 267 + dpopKey 268 + ); 269 + 270 + // CRITICAL: Verify DID matches 271 + if (tokens.sub !== did) { 272 + throw new Error('DID mismatch - identity verification failed'); 273 + } 274 + 275 + // Verify bidirectional handle/DID binding 276 + const bindingValid = await verifyHandleDidBinding(storedState.handle, did); 277 + if (!bindingValid) { 278 + throw new Error('Handle/DID binding verification failed'); 279 + } 280 + 281 + // Create session 282 + const session: OAuthSession = { 283 + did: tokens.sub, 284 + handle: storedState.handle, 285 + accessToken: tokens.access_token, 286 + refreshToken: tokens.refresh_token || '', 287 + dpopKey, 288 + pdsUrl, 289 + expiresAt: Date.now() + (tokens.expires_in * 1000), 290 + }; 291 + 292 + // Store session 293 + await this.storeSession(session); 294 + 295 + // Clean up OAuth state 296 + sessionStorage.removeItem('oauth_state'); 297 + 298 + return session; 299 + } 300 + 301 + /** 302 + * Exchange authorization code for tokens 303 + * @param authServer - Authorization server metadata 304 + * @param code - Authorization code 305 + * @param codeVerifier - PKCE code verifier 306 + * @param dpopKey - DPoP keypair 307 + * @returns Token response 308 + */ 309 + private async exchangeCodeForTokens( 310 + authServer: AuthorizationServerMetadata, 311 + code: string, 312 + codeVerifier: string, 313 + dpopKey: CryptoKeyPair 314 + ): Promise<TokenResponse> { 315 + const tokenEndpoint = authServer.token_endpoint; 316 + 317 + // Try to discover nonce proactively 318 + await this.discoverDpopNonce(tokenEndpoint, dpopKey); 319 + 320 + // Build token request parameters 321 + const params = new URLSearchParams({ 322 + grant_type: 'authorization_code', 323 + code: code, 324 + redirect_uri: this.clientMetadata.redirect_uris[0], 325 + client_id: this.clientMetadata.client_id, 326 + code_verifier: codeVerifier, 327 + }); 328 + 329 + // Get nonce for token endpoint 330 + let nonce = this.getDpopNonce(tokenEndpoint); 331 + 332 + // Generate DPoP proof 333 + let dpopProof = await generateDpopProof( 334 + dpopKey, 335 + 'POST', 336 + tokenEndpoint, 337 + undefined, 338 + nonce 339 + ); 340 + 341 + let response = await fetch(tokenEndpoint, { 342 + method: 'POST', 343 + headers: { 344 + 'Content-Type': 'application/x-www-form-urlencoded', 345 + 'DPoP': dpopProof, 346 + }, 347 + body: params.toString(), 348 + }); 349 + 350 + // Always check for nonce in response 351 + let newNonce = response.headers.get('DPoP-Nonce'); 352 + if (newNonce) { 353 + this.storeDpopNonce(tokenEndpoint, newNonce); 354 + } 355 + 356 + // Handle nonce errors - retry exactly once (with or without nonce) 357 + if (!response.ok && (response.status === 401 || response.status === 400)) { 358 + console.log('[Token] First attempt failed with status:', response.status); 359 + console.log('[Token] Nonce from error response:', newNonce || 'none'); 360 + 361 + // Always retry on 401/400, use whatever nonce we have 362 + const retryNonce = newNonce || this.getDpopNonce(tokenEndpoint); 363 + console.log('[Token] Retrying with nonce:', retryNonce || 'none'); 364 + 365 + // Retry with nonce 366 + dpopProof = await generateDpopProof( 367 + dpopKey, 368 + 'POST', 369 + tokenEndpoint, 370 + undefined, 371 + retryNonce 372 + ); 373 + 374 + response = await fetch(tokenEndpoint, { 375 + method: 'POST', 376 + headers: { 377 + 'Content-Type': 'application/x-www-form-urlencoded', 378 + 'DPoP': dpopProof, 379 + }, 380 + body: params.toString(), 381 + }); 382 + 383 + // Store any new nonce from retry 384 + const newRetryNonce = response.headers.get('DPoP-Nonce'); 385 + if (newRetryNonce) { 386 + this.storeDpopNonce(tokenEndpoint, newRetryNonce); 387 + console.log('[Token] Stored new nonce from retry'); 388 + } 389 + 390 + if (response.ok) { 391 + console.log('[Token] Retry succeeded!'); 392 + } else { 393 + console.log('[Token] Retry failed with status:', response.status); 394 + } 395 + } 396 + 397 + if (!response.ok) { 398 + const error = await response.text(); 399 + console.error('[Token] Final error:', error); 400 + throw new Error(`Token exchange failed: ${error}`); 401 + } 402 + 403 + const tokens: TokenResponse = await response.json(); 404 + return tokens; 405 + } 406 + 407 + /** 408 + * Refresh access token 409 + * @param session - Current session 410 + * @returns Updated session 411 + */ 412 + async refreshAccessToken(session: OAuthSession): Promise<OAuthSession> { 413 + if (!session.refreshToken) { 414 + throw new Error('No refresh token available'); 415 + } 416 + 417 + // Discover auth server 418 + const { authServer } = await discoverFromHandle(session.handle); 419 + const tokenEndpoint = authServer.token_endpoint; 420 + 421 + // Try to discover nonce proactively 422 + await this.discoverDpopNonce(tokenEndpoint, session.dpopKey); 423 + 424 + // Build refresh request parameters 425 + const params = new URLSearchParams({ 426 + grant_type: 'refresh_token', 427 + refresh_token: session.refreshToken, 428 + client_id: this.clientMetadata.client_id, 429 + }); 430 + 431 + // Get nonce for token endpoint 432 + let nonce = this.getDpopNonce(tokenEndpoint); 433 + 434 + // Generate DPoP proof 435 + let dpopProof = await generateDpopProof( 436 + session.dpopKey, 437 + 'POST', 438 + tokenEndpoint, 439 + undefined, 440 + nonce 441 + ); 442 + 443 + let response = await fetch(tokenEndpoint, { 444 + method: 'POST', 445 + headers: { 446 + 'Content-Type': 'application/x-www-form-urlencoded', 447 + 'DPoP': dpopProof, 448 + }, 449 + body: params.toString(), 450 + }); 451 + 452 + // Always check for nonce in response 453 + let newNonce = response.headers.get('DPoP-Nonce'); 454 + if (newNonce) { 455 + this.storeDpopNonce(tokenEndpoint, newNonce); 456 + } 457 + 458 + // Handle nonce errors - retry exactly once (with or without nonce) 459 + if (!response.ok && (response.status === 401 || response.status === 400)) { 460 + console.log('[Refresh] First attempt failed with status:', response.status); 461 + console.log('[Refresh] Nonce from error response:', newNonce || 'none'); 462 + 463 + // Always retry on 401/400, use whatever nonce we have 464 + const retryNonce = newNonce || this.getDpopNonce(tokenEndpoint); 465 + console.log('[Refresh] Retrying with nonce:', retryNonce || 'none'); 466 + 467 + // Retry with new nonce 468 + dpopProof = await generateDpopProof( 469 + session.dpopKey, 470 + 'POST', 471 + tokenEndpoint, 472 + undefined, 473 + retryNonce 474 + ); 475 + 476 + response = await fetch(tokenEndpoint, { 477 + method: 'POST', 478 + headers: { 479 + 'Content-Type': 'application/x-www-form-urlencoded', 480 + 'DPoP': dpopProof, 481 + }, 482 + body: params.toString(), 483 + }); 484 + 485 + // Store any new nonce from retry 486 + const newRetryNonce = response.headers.get('DPoP-Nonce'); 487 + if (newRetryNonce) { 488 + this.storeDpopNonce(tokenEndpoint, newRetryNonce); 489 + console.log('[Refresh] Stored new nonce from retry'); 490 + } 491 + 492 + if (response.ok) { 493 + console.log('[Refresh] Retry succeeded!'); 494 + } else { 495 + console.log('[Refresh] Retry failed with status:', response.status); 496 + } 497 + } 498 + 499 + if (!response.ok) { 500 + const error = await response.text(); 501 + console.error('[Refresh] Final error:', error); 502 + throw new Error(`Token refresh failed: ${error}`); 503 + } 504 + 505 + const tokens: TokenResponse = await response.json(); 506 + 507 + // Update session with new tokens 508 + const updatedSession: OAuthSession = { 509 + ...session, 510 + accessToken: tokens.access_token, 511 + refreshToken: tokens.refresh_token || session.refreshToken, // Keep old if not rotated 512 + expiresAt: Date.now() + (tokens.expires_in * 1000), 513 + }; 514 + 515 + // Store updated session 516 + await this.storeSession(updatedSession); 517 + 518 + return updatedSession; 519 + } 520 + 521 + /** 522 + * Store OAuth session 523 + * @param session - Session to store 524 + */ 525 + private async storeSession(session: OAuthSession): Promise<void> { 526 + // Store tokens in sessionStorage (excluding DPoP key) 527 + const sessionData = { 528 + did: session.did, 529 + handle: session.handle, 530 + accessToken: session.accessToken, 531 + refreshToken: session.refreshToken, 532 + pdsUrl: session.pdsUrl, 533 + expiresAt: session.expiresAt, 534 + }; 535 + sessionStorage.setItem('oauth_session', JSON.stringify(sessionData)); 536 + 537 + // Store DPoP key in IndexedDB 538 + await storeDpopKeypair(session.dpopKey, 'session_dpop'); 539 + } 540 + 541 + /** 542 + * Retrieve stored OAuth session 543 + * @returns Session or null 544 + */ 545 + async getSession(): Promise<OAuthSession | null> { 546 + const sessionStr = sessionStorage.getItem('oauth_session'); 547 + if (!sessionStr) { 548 + return null; 549 + } 550 + 551 + const sessionData = JSON.parse(sessionStr); 552 + 553 + // Retrieve DPoP key from IndexedDB 554 + const dpopKey = await retrieveDpopKeypair('session_dpop'); 555 + if (!dpopKey) { 556 + return null; 557 + } 558 + 559 + const session: OAuthSession = { 560 + ...sessionData, 561 + dpopKey, 562 + }; 563 + 564 + // Check if token is expired 565 + if (Date.now() >= session.expiresAt - 60000) { // Refresh 1 minute before expiry 566 + try { 567 + return await this.refreshAccessToken(session); 568 + } catch (error) { 569 + console.error('Token refresh failed:', error); 570 + await this.clearSession(); 571 + return null; 572 + } 573 + } 574 + 575 + return session; 576 + } 577 + 578 + /** 579 + * Clear OAuth session 580 + */ 581 + async clearSession(): Promise<void> { 582 + sessionStorage.removeItem('oauth_session'); 583 + sessionStorage.removeItem('oauth_state'); 584 + await clearDpopKeypairs(); 585 + this.dpopNonces.clear(); 586 + } 587 + 588 + /** 589 + * Store DPoP nonce for an endpoint 590 + * @param endpoint - The endpoint URL 591 + * @param nonce - The nonce value 592 + */ 593 + private storeDpopNonce(endpoint: string, nonce: string): void { 594 + this.dpopNonces.set(endpoint, { 595 + value: nonce, 596 + expiresAt: Date.now() + 600000, // 10 minutes 597 + }); 598 + } 599 + 600 + /** 601 + * Get stored DPoP nonce for an endpoint 602 + * @param endpoint - The endpoint URL 603 + * @returns Nonce value or undefined 604 + */ 605 + getDpopNonce(endpoint: string): string | undefined { 606 + const nonce = this.dpopNonces.get(endpoint); 607 + if (!nonce) { 608 + return undefined; 609 + } 610 + 611 + if (Date.now() >= nonce.expiresAt) { 612 + this.dpopNonces.delete(endpoint); 613 + return undefined; 614 + } 615 + 616 + return nonce.value; 617 + } 618 + }
+515
src/lib/xrpc-client.ts
··· 1 + /** 2 + * XRPC Client for ATProtocol 3 + * Handles authenticated requests to PDS with DPoP proofs 4 + */ 5 + 6 + import { generateDpopProof } from '../utils/crypto-utils'; 7 + import { ATProtoOAuthClient } from './atproto-oauth-client'; 8 + import type { 9 + OAuthSession, 10 + CreatePostRequest, 11 + CreateRecordResponse, 12 + XRPCError, 13 + PostRecord, 14 + NowRecord, 15 + UserProfile, 16 + } from '../types/auth'; 17 + 18 + export class XRPCClient { 19 + private oauthClient: ATProtoOAuthClient; 20 + private session: OAuthSession | null = null; 21 + 22 + constructor() { 23 + this.oauthClient = new ATProtoOAuthClient(); 24 + } 25 + 26 + /** 27 + * Set the current session 28 + * @param session - OAuth session 29 + */ 30 + setSession(session: OAuthSession): void { 31 + this.session = session; 32 + } 33 + 34 + /** 35 + * Make an authenticated XRPC request 36 + * @param method - HTTP method 37 + * @param nsid - XRPC namespace ID 38 + * @param params - Query parameters (for GET) 39 + * @param data - Request body (for POST) 40 + * @returns Response data 41 + */ 42 + private async xrpcRequest<T>( 43 + method: 'GET' | 'POST', 44 + nsid: string, 45 + params?: Record<string, any>, 46 + data?: any 47 + ): Promise<T> { 48 + if (!this.session) { 49 + throw new Error('No active session'); 50 + } 51 + 52 + // Check if token needs refresh 53 + if (Date.now() >= this.session.expiresAt - 60000) { 54 + this.session = await this.oauthClient.refreshAccessToken(this.session); 55 + } 56 + 57 + const url = new URL(`${this.session.pdsUrl}/xrpc/${nsid}`); 58 + 59 + // Add query parameters for GET requests 60 + if (method === 'GET' && params) { 61 + Object.entries(params).forEach(([key, value]) => { 62 + if (value !== undefined) { 63 + url.searchParams.set(key, String(value)); 64 + } 65 + }); 66 + } 67 + 68 + // Get stored nonce for this endpoint 69 + const nonce = this.oauthClient.getDpopNonce(url.toString()); 70 + 71 + // Generate DPoP proof with access token binding 72 + const dpopProof = await generateDpopProof( 73 + this.session.dpopKey, 74 + method, 75 + url.toString(), 76 + this.session.accessToken, 77 + nonce 78 + ); 79 + 80 + // Build request headers 81 + const headers: HeadersInit = { 82 + 'Authorization': `DPoP ${this.session.accessToken}`, 83 + 'DPoP': dpopProof, 84 + }; 85 + 86 + if (method === 'POST' && data) { 87 + headers['Content-Type'] = 'application/json'; 88 + } 89 + 90 + // Make the request 91 + let response = await fetch(url.toString(), { 92 + method, 93 + headers, 94 + body: method === 'POST' ? JSON.stringify(data) : undefined, 95 + }); 96 + 97 + // Always check for nonce in response 98 + let newNonce = response.headers.get('DPoP-Nonce'); 99 + if (newNonce) { 100 + this.oauthClient.storeDpopNonce(url.toString(), newNonce); 101 + } 102 + 103 + // Handle nonce errors - retry exactly once (with or without nonce) 104 + if (!response.ok && (response.status === 401 || response.status === 400)) { 105 + console.log('[XRPC] First attempt failed with status:', response.status); 106 + console.log('[XRPC] Nonce from error response:', newNonce || 'none'); 107 + 108 + // Check if it's a DPoP nonce error 109 + let errorData: XRPCError | null = null; 110 + try { 111 + errorData = await response.json(); 112 + } catch { 113 + // Ignore parse errors 114 + } 115 + 116 + // Retry if we have a nonce or if it's a use_dpop_nonce error 117 + if (newNonce || (errorData && errorData.error === 'use_dpop_nonce')) { 118 + const retryNonce = newNonce || this.oauthClient.getDpopNonce(url.toString()); 119 + console.log('[XRPC] Retrying with nonce:', retryNonce || 'none'); 120 + 121 + // Regenerate DPoP proof with nonce 122 + const retryDpopProof = await generateDpopProof( 123 + this.session!.dpopKey, 124 + method, 125 + url.toString(), 126 + this.session!.accessToken, 127 + retryNonce 128 + ); 129 + 130 + // Update headers with new DPoP proof 131 + const retryHeaders: HeadersInit = { 132 + 'Authorization': `DPoP ${this.session!.accessToken}`, 133 + 'DPoP': retryDpopProof, 134 + }; 135 + 136 + if (method === 'POST' && data) { 137 + retryHeaders['Content-Type'] = 'application/json'; 138 + } 139 + 140 + // Retry the request 141 + response = await fetch(url.toString(), { 142 + method, 143 + headers: retryHeaders, 144 + body: method === 'POST' ? JSON.stringify(data) : undefined, 145 + }); 146 + 147 + // Store any new nonce from retry 148 + const retryResponseNonce = response.headers.get('DPoP-Nonce'); 149 + if (retryResponseNonce) { 150 + this.oauthClient.storeDpopNonce(url.toString(), retryResponseNonce); 151 + console.log('[XRPC] Stored new nonce from retry'); 152 + } 153 + 154 + if (response.ok) { 155 + console.log('[XRPC] Retry succeeded!'); 156 + } else { 157 + console.log('[XRPC] Retry failed with status:', response.status); 158 + } 159 + } 160 + } 161 + 162 + // Handle errors 163 + if (!response.ok) { 164 + let errorData: XRPCError; 165 + try { 166 + errorData = await response.json(); 167 + } catch { 168 + errorData = { 169 + error: 'UnknownError', 170 + message: `Request failed with status ${response.status}`, 171 + }; 172 + } 173 + console.error('[XRPC] Final error:', errorData); 174 + throw new Error(errorData.message || errorData.error); 175 + } 176 + 177 + // Parse and return response 178 + if (response.headers.get('content-type')?.includes('application/json')) { 179 + return await response.json(); 180 + } else { 181 + return await response.text() as any; 182 + } 183 + } 184 + 185 + /** 186 + * Create a "now" record 187 + * @param text - What you're doing now 188 + * @returns Created record response 189 + */ 190 + async createNow(text: string): Promise<CreateRecordResponse> { 191 + if (!this.session) { 192 + throw new Error('No active session'); 193 + } 194 + 195 + // Validate text length 196 + if (text.length > 300) { 197 + throw new Error('Text exceeds 300 character limit'); 198 + } 199 + 200 + const record: NowRecord = { 201 + $type: 'garden.lexicon.oauth-masterclass.now', 202 + now: text, 203 + createdAt: new Date().toISOString(), 204 + }; 205 + 206 + const request: CreatePostRequest = { 207 + repo: this.session.did, 208 + collection: 'garden.lexicon.oauth-masterclass.now', 209 + record, 210 + }; 211 + 212 + return await this.xrpcRequest<CreateRecordResponse>( 213 + 'POST', 214 + 'com.atproto.repo.createRecord', 215 + undefined, 216 + request 217 + ); 218 + } 219 + 220 + /** 221 + * Create a post in the user's repository 222 + * @param text - Post text 223 + * @param langs - Language codes 224 + * @returns Created record response 225 + */ 226 + async createPost( 227 + text: string, 228 + langs: string[] = ['en-US'] 229 + ): Promise<CreateRecordResponse> { 230 + if (!this.session) { 231 + throw new Error('No active session'); 232 + } 233 + 234 + // Validate text length (300 grapheme limit for Bluesky) 235 + if (text.length > 300) { 236 + throw new Error('Post text exceeds 300 character limit'); 237 + } 238 + 239 + const record: PostRecord = { 240 + $type: 'app.bsky.feed.post', 241 + text, 242 + createdAt: new Date().toISOString(), 243 + langs, 244 + }; 245 + 246 + const request: CreatePostRequest = { 247 + repo: this.session.did, 248 + collection: 'app.bsky.feed.post', 249 + record, 250 + }; 251 + 252 + return await this.xrpcRequest<CreateRecordResponse>( 253 + 'POST', 254 + 'com.atproto.repo.createRecord', 255 + undefined, 256 + request 257 + ); 258 + } 259 + 260 + /** 261 + * Get user profile 262 + * @param actor - DID or handle (defaults to current user) 263 + * @returns User profile 264 + */ 265 + async getProfile(actor?: string): Promise<UserProfile> { 266 + if (!this.session) { 267 + throw new Error('No active session'); 268 + } 269 + 270 + const params = { 271 + actor: actor || this.session.did, 272 + }; 273 + 274 + const response = await this.xrpcRequest<any>( 275 + 'GET', 276 + 'app.bsky.actor.getProfile', 277 + params 278 + ); 279 + 280 + // Map response to UserProfile 281 + const profile: UserProfile = { 282 + did: response.did, 283 + handle: response.handle, 284 + displayName: response.displayName, 285 + description: response.description, 286 + avatar: response.avatar, 287 + banner: response.banner, 288 + followsCount: response.followsCount, 289 + followersCount: response.followersCount, 290 + postsCount: response.postsCount, 291 + indexedAt: response.indexedAt, 292 + }; 293 + 294 + return profile; 295 + } 296 + 297 + /** 298 + * Delete a record 299 + * @param uri - The AT URI of the record to delete 300 + * @returns Success response 301 + */ 302 + async deleteRecord(uri: string): Promise<void> { 303 + if (!this.session) { 304 + throw new Error('No active session'); 305 + } 306 + 307 + // Parse AT URI: at://did/collection/rkey 308 + const match = uri.match(/^at:\/\/([^\/]+)\/([^\/]+)\/([^\/]+)$/); 309 + if (!match) { 310 + throw new Error('Invalid AT URI format'); 311 + } 312 + 313 + const [, repo, collection, rkey] = match; 314 + 315 + await this.xrpcRequest<void>( 316 + 'POST', 317 + 'com.atproto.repo.deleteRecord', 318 + undefined, 319 + { 320 + repo, 321 + collection, 322 + rkey, 323 + } 324 + ); 325 + } 326 + 327 + /** 328 + * Get a specific record 329 + * @param uri - The AT URI of the record 330 + * @returns The record data 331 + */ 332 + async getRecord(uri: string): Promise<any> { 333 + // Parse AT URI: at://did/collection/rkey 334 + const match = uri.match(/^at:\/\/([^\/]+)\/([^\/]+)\/([^\/]+)$/); 335 + if (!match) { 336 + throw new Error('Invalid AT URI format'); 337 + } 338 + 339 + const [, repo, collection, rkey] = match; 340 + 341 + const response = await this.xrpcRequest<any>( 342 + 'GET', 343 + 'com.atproto.repo.getRecord', 344 + { 345 + repo, 346 + collection, 347 + rkey, 348 + } 349 + ); 350 + 351 + return response.value; 352 + } 353 + 354 + /** 355 + * List records from a repository 356 + * @param collection - Collection NSID 357 + * @param limit - Maximum number of records 358 + * @param cursor - Pagination cursor 359 + * @returns Records and cursor 360 + */ 361 + async listRecords( 362 + collection: string, 363 + limit: number = 50, 364 + cursor?: string 365 + ): Promise<{ 366 + records: any[]; 367 + cursor?: string; 368 + }> { 369 + if (!this.session) { 370 + throw new Error('No active session'); 371 + } 372 + 373 + const response = await this.xrpcRequest<any>( 374 + 'GET', 375 + 'com.atproto.repo.listRecords', 376 + { 377 + repo: this.session.did, 378 + collection, 379 + limit, 380 + cursor, 381 + } 382 + ); 383 + 384 + return { 385 + records: response.records, 386 + cursor: response.cursor, 387 + }; 388 + } 389 + 390 + /** 391 + * Get the user's timeline 392 + * @param limit - Number of posts 393 + * @param cursor - Pagination cursor 394 + * @returns Timeline posts 395 + */ 396 + async getTimeline( 397 + limit: number = 30, 398 + cursor?: string 399 + ): Promise<{ 400 + feed: any[]; 401 + cursor?: string; 402 + }> { 403 + const response = await this.xrpcRequest<any>( 404 + 'GET', 405 + 'app.bsky.feed.getTimeline', 406 + { 407 + limit, 408 + cursor, 409 + } 410 + ); 411 + 412 + return { 413 + feed: response.feed, 414 + cursor: response.cursor, 415 + }; 416 + } 417 + 418 + /** 419 + * Like a post 420 + * @param uri - Post URI 421 + * @param cid - Post CID 422 + * @returns Created like record 423 + */ 424 + async likePost(uri: string, cid: string): Promise<CreateRecordResponse> { 425 + if (!this.session) { 426 + throw new Error('No active session'); 427 + } 428 + 429 + const record = { 430 + $type: 'app.bsky.feed.like', 431 + subject: { 432 + uri, 433 + cid, 434 + }, 435 + createdAt: new Date().toISOString(), 436 + }; 437 + 438 + const request = { 439 + repo: this.session.did, 440 + collection: 'app.bsky.feed.like', 441 + record, 442 + }; 443 + 444 + return await this.xrpcRequest<CreateRecordResponse>( 445 + 'POST', 446 + 'com.atproto.repo.createRecord', 447 + undefined, 448 + request 449 + ); 450 + } 451 + 452 + /** 453 + * Repost a post 454 + * @param uri - Post URI 455 + * @param cid - Post CID 456 + * @returns Created repost record 457 + */ 458 + async repostPost(uri: string, cid: string): Promise<CreateRecordResponse> { 459 + if (!this.session) { 460 + throw new Error('No active session'); 461 + } 462 + 463 + const record = { 464 + $type: 'app.bsky.feed.repost', 465 + subject: { 466 + uri, 467 + cid, 468 + }, 469 + createdAt: new Date().toISOString(), 470 + }; 471 + 472 + const request = { 473 + repo: this.session.did, 474 + collection: 'app.bsky.feed.repost', 475 + record, 476 + }; 477 + 478 + return await this.xrpcRequest<CreateRecordResponse>( 479 + 'POST', 480 + 'com.atproto.repo.createRecord', 481 + undefined, 482 + request 483 + ); 484 + } 485 + 486 + /** 487 + * Follow a user 488 + * @param did - User DID to follow 489 + * @returns Created follow record 490 + */ 491 + async followUser(did: string): Promise<CreateRecordResponse> { 492 + if (!this.session) { 493 + throw new Error('No active session'); 494 + } 495 + 496 + const record = { 497 + $type: 'app.bsky.graph.follow', 498 + subject: did, 499 + createdAt: new Date().toISOString(), 500 + }; 501 + 502 + const request = { 503 + repo: this.session.did, 504 + collection: 'app.bsky.graph.follow', 505 + record, 506 + }; 507 + 508 + return await this.xrpcRequest<CreateRecordResponse>( 509 + 'POST', 510 + 'com.atproto.repo.createRecord', 511 + undefined, 512 + request 513 + ); 514 + } 515 + }
+10
src/main.tsx
··· 1 + import { StrictMode } from 'react' 2 + import { createRoot } from 'react-dom/client' 3 + import './index.css' 4 + import App from './App.tsx' 5 + 6 + createRoot(document.getElementById('root')!).render( 7 + <StrictMode> 8 + <App /> 9 + </StrictMode>, 10 + )
+187
src/types/auth.ts
··· 1 + /** 2 + * TypeScript type definitions for ATProtocol OAuth 3 + */ 4 + 5 + export interface OAuthSession { 6 + did: string; 7 + handle: string; 8 + accessToken: string; 9 + refreshToken: string; 10 + dpopKey: CryptoKeyPair; 11 + pdsUrl: string; 12 + expiresAt: number; 13 + } 14 + 15 + export interface OAuthState { 16 + codeVerifier: string; 17 + state: string; 18 + dpopKey: CryptoKeyPair; 19 + handle: string; 20 + pdsUrl: string; 21 + authServerUrl: string; 22 + } 23 + 24 + export interface TokenResponse { 25 + access_token: string; 26 + token_type: string; 27 + expires_in: number; 28 + refresh_token?: string; 29 + scope?: string; 30 + sub: string; 31 + } 32 + 33 + export interface PARResponse { 34 + request_uri: string; 35 + expires_in: number; 36 + } 37 + 38 + export interface AuthorizationServerMetadata { 39 + issuer: string; 40 + authorization_endpoint: string; 41 + token_endpoint: string; 42 + pushed_authorization_request_endpoint: string; 43 + response_types_supported: string[]; 44 + grant_types_supported: string[]; 45 + code_challenge_methods_supported: string[]; 46 + scopes_supported: string[]; 47 + require_pushed_authorization_requests: boolean; 48 + dpop_signing_alg_values_supported: string[]; 49 + } 50 + 51 + export interface ClientMetadata { 52 + client_id: string; 53 + application_type: 'web'; 54 + client_name: string; 55 + redirect_uris: string[]; 56 + scope: string; 57 + grant_types: string[]; 58 + response_types: string[]; 59 + token_endpoint_auth_method: 'none'; 60 + dpop_bound_access_tokens: boolean; 61 + } 62 + 63 + export interface AuthState { 64 + user: UserProfile | null; 65 + session: OAuthSession | null; 66 + loading: boolean; 67 + error: Error | null; 68 + } 69 + 70 + export interface UserProfile { 71 + did: string; 72 + handle: string; 73 + displayName?: string; 74 + description?: string; 75 + avatar?: string; 76 + banner?: string; 77 + followsCount?: number; 78 + followersCount?: number; 79 + postsCount?: number; 80 + indexedAt?: string; 81 + } 82 + 83 + export interface CreatePostRequest { 84 + repo: string; 85 + collection: string; 86 + record: PostRecord | NowRecord; 87 + rkey?: string; 88 + validate?: boolean; 89 + swapCommit?: string; 90 + } 91 + 92 + export interface PostRecord { 93 + $type: 'app.bsky.feed.post'; 94 + text: string; 95 + createdAt: string; 96 + langs?: string[]; 97 + reply?: ReplyRef; 98 + facets?: Facet[]; 99 + } 100 + 101 + export interface NowRecord { 102 + $type: 'garden.lexicon.oauth-masterclass.now'; 103 + now: string; 104 + createdAt: string; 105 + } 106 + 107 + export interface ReplyRef { 108 + root: StrongRef; 109 + parent: StrongRef; 110 + } 111 + 112 + export interface StrongRef { 113 + uri: string; 114 + cid: string; 115 + } 116 + 117 + export interface Facet { 118 + index: ByteSlice; 119 + features: FacetFeature[]; 120 + } 121 + 122 + export interface ByteSlice { 123 + byteStart: number; 124 + byteEnd: number; 125 + } 126 + 127 + export type FacetFeature = MentionFeature | LinkFeature | TagFeature; 128 + 129 + export interface MentionFeature { 130 + $type: 'app.bsky.richtext.facet#mention'; 131 + did: string; 132 + } 133 + 134 + export interface LinkFeature { 135 + $type: 'app.bsky.richtext.facet#link'; 136 + uri: string; 137 + } 138 + 139 + export interface TagFeature { 140 + $type: 'app.bsky.richtext.facet#tag'; 141 + tag: string; 142 + } 143 + 144 + export interface CreateRecordResponse { 145 + uri: string; 146 + cid: string; 147 + commit?: { 148 + cid: string; 149 + rev: string; 150 + }; 151 + validationStatus?: 'valid' | 'unknown'; 152 + } 153 + 154 + export interface OAuthError { 155 + error: string; 156 + error_description?: string; 157 + error_uri?: string; 158 + state?: string; 159 + } 160 + 161 + export interface DPoPNonce { 162 + value: string; 163 + expiresAt: number; 164 + } 165 + 166 + export interface AuthContextType { 167 + user: UserProfile | null; 168 + session: OAuthSession | null; 169 + loading: boolean; 170 + error: Error | null; 171 + isAuthenticated: boolean; 172 + login: (handle: string) => Promise<void>; 173 + handleCallback: (code: string, state: string) => Promise<void>; 174 + logout: () => Promise<void>; 175 + refreshToken: () => Promise<void>; 176 + } 177 + 178 + export interface XRPCHeaders { 179 + 'Authorization': string; 180 + 'DPoP': string; 181 + 'Content-Type'?: string; 182 + } 183 + 184 + export interface XRPCError { 185 + error: string; 186 + message?: string; 187 + }
+315
src/utils/crypto-utils.ts
··· 1 + /** 2 + * Cryptographic utilities for ATProtocol OAuth 3 + * Implements PKCE (S256) and DPoP (ES256) generation 4 + */ 5 + 6 + /** 7 + * Generate a PKCE code verifier 8 + * Must be 43-128 characters from [A-Za-z0-9-._~] 9 + * @returns Base64url encoded verifier 10 + */ 11 + export function generateCodeVerifier(): string { 12 + const array = new Uint8Array(32); 13 + crypto.getRandomValues(array); 14 + return base64urlEncode(array); 15 + } 16 + 17 + /** 18 + * Generate PKCE code challenge from verifier 19 + * Uses S256 method (SHA-256 hash) 20 + * @param verifier - The code verifier 21 + * @returns Base64url encoded challenge 22 + */ 23 + export async function generateCodeChallenge(verifier: string): Promise<string> { 24 + const encoder = new TextEncoder(); 25 + const data = encoder.encode(verifier); 26 + const hash = await crypto.subtle.digest('SHA-256', data); 27 + return base64urlEncode(new Uint8Array(hash)); 28 + } 29 + 30 + /** 31 + * Generate ES256 DPoP keypair for token binding 32 + * @returns CryptoKeyPair for DPoP proofs 33 + */ 34 + export async function generateDpopKeyPair(): Promise<CryptoKeyPair> { 35 + return await crypto.subtle.generateKey( 36 + { 37 + name: 'ECDSA', 38 + namedCurve: 'P-256', 39 + }, 40 + false, // non-exportable for security 41 + ['sign', 'verify'] 42 + ); 43 + } 44 + 45 + /** 46 + * Export public key as JWK for DPoP header 47 + * @param publicKey - The public key to export 48 + * @returns JWK representation 49 + */ 50 + export async function exportPublicKeyAsJwk(publicKey: CryptoKey): Promise<JsonWebKey> { 51 + const jwk = await crypto.subtle.exportKey('jwk', publicKey); 52 + // Remove private key material if present 53 + delete jwk.d; 54 + delete jwk.dp; 55 + delete jwk.dq; 56 + delete jwk.p; 57 + delete jwk.q; 58 + delete jwk.qi; 59 + jwk.use = 'sig'; 60 + jwk.alg = 'ES256'; 61 + return jwk; 62 + } 63 + 64 + /** 65 + * Generate a DPoP proof JWT 66 + * @param keypair - The DPoP keypair 67 + * @param method - HTTP method (GET, POST, etc.) 68 + * @param url - Full URL of the request 69 + * @param accessToken - Optional access token for binding 70 + * @param nonce - Optional server-provided nonce 71 + * @returns Signed DPoP JWT 72 + */ 73 + export async function generateDpopProof( 74 + keypair: CryptoKeyPair, 75 + method: string, 76 + url: string, 77 + accessToken?: string, 78 + nonce?: string 79 + ): Promise<string> { 80 + const jwk = await exportPublicKeyAsJwk(keypair.publicKey); 81 + 82 + const header = { 83 + typ: 'dpop+jwt', 84 + alg: 'ES256', 85 + jwk, 86 + }; 87 + 88 + const payload: any = { 89 + jti: generateRandomString(16), 90 + htm: method.toUpperCase(), 91 + htu: url, 92 + iat: Math.floor(Date.now() / 1000), 93 + }; 94 + 95 + if (nonce) { 96 + payload.nonce = nonce; 97 + } 98 + 99 + if (accessToken) { 100 + // Include access token hash for token binding 101 + payload.ath = await sha256Hash(accessToken); 102 + } 103 + 104 + return await signJwt(header, payload, keypair.privateKey); 105 + } 106 + 107 + /** 108 + * Sign a JWT with ES256 109 + * @param header - JWT header 110 + * @param payload - JWT payload 111 + * @param privateKey - Private key for signing 112 + * @returns Signed JWT 113 + */ 114 + async function signJwt( 115 + header: any, 116 + payload: any, 117 + privateKey: CryptoKey 118 + ): Promise<string> { 119 + const encodedHeader = base64urlEncode( 120 + new TextEncoder().encode(JSON.stringify(header)) 121 + ); 122 + const encodedPayload = base64urlEncode( 123 + new TextEncoder().encode(JSON.stringify(payload)) 124 + ); 125 + 126 + const message = `${encodedHeader}.${encodedPayload}`; 127 + const signature = await crypto.subtle.sign( 128 + { 129 + name: 'ECDSA', 130 + hash: 'SHA-256', 131 + }, 132 + privateKey, 133 + new TextEncoder().encode(message) 134 + ); 135 + 136 + const encodedSignature = base64urlEncode(new Uint8Array(signature)); 137 + return `${message}.${encodedSignature}`; 138 + } 139 + 140 + /** 141 + * Calculate SHA-256 hash and encode as base64url 142 + * @param data - Data to hash 143 + * @returns Base64url encoded hash 144 + */ 145 + export async function sha256Hash(data: string): Promise<string> { 146 + const encoder = new TextEncoder(); 147 + const hash = await crypto.subtle.digest('SHA-256', encoder.encode(data)); 148 + return base64urlEncode(new Uint8Array(hash)); 149 + } 150 + 151 + /** 152 + * Generate a random string for state tokens and JTI 153 + * @param length - Length of the string 154 + * @returns Random string 155 + */ 156 + export function generateRandomString(length: number): string { 157 + const array = new Uint8Array(length); 158 + crypto.getRandomValues(array); 159 + return base64urlEncode(array); 160 + } 161 + 162 + /** 163 + * Base64url encode (no padding, URL-safe) 164 + * @param data - Data to encode 165 + * @returns Base64url encoded string 166 + */ 167 + export function base64urlEncode(data: Uint8Array): string { 168 + const base64 = btoa(String.fromCharCode(...data)); 169 + return base64 170 + .replace(/\+/g, '-') 171 + .replace(/\//g, '_') 172 + .replace(/=/g, ''); 173 + } 174 + 175 + /** 176 + * Base64url decode 177 + * @param str - Base64url string 178 + * @returns Decoded bytes 179 + */ 180 + export function base64urlDecode(str: string): Uint8Array { 181 + const base64 = str 182 + .replace(/-/g, '+') 183 + .replace(/_/g, '/') 184 + .padEnd(str.length + ((4 - (str.length % 4)) % 4), '='); 185 + 186 + const binary = atob(base64); 187 + const bytes = new Uint8Array(binary.length); 188 + for (let i = 0; i < binary.length; i++) { 189 + bytes[i] = binary.charCodeAt(i); 190 + } 191 + return bytes; 192 + } 193 + 194 + /** 195 + * Store DPoP keypair in IndexedDB 196 + * @param keypair - The keypair to store 197 + * @param identifier - Unique identifier for the keypair 198 + */ 199 + export async function storeDpopKeypair( 200 + keypair: CryptoKeyPair, 201 + identifier: string 202 + ): Promise<void> { 203 + return new Promise((resolve, reject) => { 204 + const request = indexedDB.open('atproto-oauth', 1); 205 + 206 + request.onupgradeneeded = (event) => { 207 + const db = (event.target as IDBOpenDBRequest).result; 208 + if (!db.objectStoreNames.contains('dpop-keys')) { 209 + db.createObjectStore('dpop-keys'); 210 + } 211 + }; 212 + 213 + request.onsuccess = (event) => { 214 + const db = (event.target as IDBOpenDBRequest).result; 215 + const transaction = db.transaction(['dpop-keys'], 'readwrite'); 216 + const store = transaction.objectStore('dpop-keys'); 217 + 218 + store.put(keypair, identifier); 219 + 220 + transaction.oncomplete = () => { 221 + db.close(); 222 + resolve(); 223 + }; 224 + 225 + transaction.onerror = () => { 226 + db.close(); 227 + reject(new Error('Failed to store DPoP keypair')); 228 + }; 229 + }; 230 + 231 + request.onerror = () => { 232 + reject(new Error('Failed to open IndexedDB')); 233 + }; 234 + }); 235 + } 236 + 237 + /** 238 + * Retrieve DPoP keypair from IndexedDB 239 + * @param identifier - Unique identifier for the keypair 240 + * @returns The stored keypair or null 241 + */ 242 + export async function retrieveDpopKeypair( 243 + identifier: string 244 + ): Promise<CryptoKeyPair | null> { 245 + return new Promise((resolve, reject) => { 246 + const request = indexedDB.open('atproto-oauth', 1); 247 + 248 + request.onupgradeneeded = (event) => { 249 + const db = (event.target as IDBOpenDBRequest).result; 250 + if (!db.objectStoreNames.contains('dpop-keys')) { 251 + db.createObjectStore('dpop-keys'); 252 + } 253 + }; 254 + 255 + request.onsuccess = (event) => { 256 + const db = (event.target as IDBOpenDBRequest).result; 257 + const transaction = db.transaction(['dpop-keys'], 'readonly'); 258 + const store = transaction.objectStore('dpop-keys'); 259 + 260 + const getRequest = store.get(identifier); 261 + 262 + getRequest.onsuccess = () => { 263 + db.close(); 264 + resolve(getRequest.result || null); 265 + }; 266 + 267 + getRequest.onerror = () => { 268 + db.close(); 269 + reject(new Error('Failed to retrieve DPoP keypair')); 270 + }; 271 + }; 272 + 273 + request.onerror = () => { 274 + reject(new Error('Failed to open IndexedDB')); 275 + }; 276 + }); 277 + } 278 + 279 + /** 280 + * Clear all stored DPoP keypairs 281 + */ 282 + export async function clearDpopKeypairs(): Promise<void> { 283 + return new Promise((resolve, reject) => { 284 + const request = indexedDB.open('atproto-oauth', 1); 285 + 286 + request.onupgradeneeded = (event) => { 287 + const db = (event.target as IDBOpenDBRequest).result; 288 + if (!db.objectStoreNames.contains('dpop-keys')) { 289 + db.createObjectStore('dpop-keys'); 290 + } 291 + }; 292 + 293 + request.onsuccess = (event) => { 294 + const db = (event.target as IDBOpenDBRequest).result; 295 + const transaction = db.transaction(['dpop-keys'], 'readwrite'); 296 + const store = transaction.objectStore('dpop-keys'); 297 + 298 + store.clear(); 299 + 300 + transaction.oncomplete = () => { 301 + db.close(); 302 + resolve(); 303 + }; 304 + 305 + transaction.onerror = () => { 306 + db.close(); 307 + reject(new Error('Failed to clear DPoP keypairs')); 308 + }; 309 + }; 310 + 311 + request.onerror = () => { 312 + reject(new Error('Failed to open IndexedDB')); 313 + }; 314 + }); 315 + }
+394
src/utils/did-resolver.ts
··· 1 + /** 2 + * DID and Handle Resolution utilities for ATProtocol 3 + * Implements handle→DID and DID→Document resolution 4 + */ 5 + 6 + interface DidDocument { 7 + id: string; 8 + alsoKnownAs?: string[]; 9 + verificationMethod?: Array<{ 10 + id: string; 11 + type: string; 12 + controller?: string; 13 + publicKeyMultibase?: string; 14 + }>; 15 + service?: Array<{ 16 + id: string; 17 + type: string; 18 + serviceEndpoint: string; 19 + }>; 20 + } 21 + 22 + interface AuthorizationServerMetadata { 23 + issuer: string; 24 + authorization_endpoint: string; 25 + token_endpoint: string; 26 + pushed_authorization_request_endpoint: string; 27 + response_types_supported: string[]; 28 + grant_types_supported: string[]; 29 + code_challenge_methods_supported: string[]; 30 + scopes_supported: string[]; 31 + require_pushed_authorization_requests: boolean; 32 + dpop_signing_alg_values_supported: string[]; 33 + } 34 + 35 + interface ResourceServerMetadata { 36 + resource: string; 37 + authorization_servers: string[]; 38 + } 39 + 40 + /** 41 + * Resolve a handle to a DID 42 + * Tries DNS TXT record first, falls back to HTTPS well-known 43 + * @param handle - The handle to resolve (e.g., "alice.bsky.social") 44 + * @returns The DID or throws an error 45 + */ 46 + export async function resolveHandleToDid(handle: string): Promise<string> { 47 + // Validate handle format 48 + if (!isValidHandle(handle)) { 49 + throw new Error(`Invalid handle format: ${handle}`); 50 + } 51 + 52 + // Try HTTPS well-known first (more reliable in browser) 53 + try { 54 + const httpsDid = await resolveViaHttps(handle); 55 + if (httpsDid) { 56 + return httpsDid; 57 + } 58 + } catch (error) { 59 + console.warn('HTTPS resolution failed, trying DNS:', error); 60 + } 61 + 62 + // Try DNS TXT record (may not work in all browsers due to CORS) 63 + try { 64 + const dnsDid = await resolveViaDns(handle); 65 + if (dnsDid) { 66 + return dnsDid; 67 + } 68 + } catch (error) { 69 + console.warn('DNS resolution failed:', error); 70 + } 71 + 72 + throw new Error(`Failed to resolve handle ${handle} to DID`); 73 + } 74 + 75 + /** 76 + * Resolve handle via HTTPS well-known 77 + * @param handle - The handle to resolve 78 + * @returns The DID or null 79 + */ 80 + async function resolveViaHttps(handle: string): Promise<string | null> { 81 + try { 82 + const response = await fetch(`https://${handle}/.well-known/atproto-did`, { 83 + method: 'GET', 84 + headers: { 85 + 'Accept': 'text/plain', 86 + }, 87 + cache: 'no-cache', 88 + }); 89 + 90 + if (!response.ok) { 91 + return null; 92 + } 93 + 94 + const did = (await response.text()).trim(); 95 + 96 + if (!isValidDid(did)) { 97 + throw new Error(`Invalid DID format: ${did}`); 98 + } 99 + 100 + return did; 101 + } catch (error) { 102 + console.error('HTTPS resolution error:', error); 103 + return null; 104 + } 105 + } 106 + 107 + /** 108 + * Resolve handle via DNS TXT record 109 + * Note: This may not work in browsers due to CORS restrictions 110 + * @param handle - The handle to resolve 111 + * @returns The DID or null 112 + */ 113 + async function resolveViaDns(handle: string): Promise<string | null> { 114 + try { 115 + // Use a public DNS-over-HTTPS service 116 + const response = await fetch( 117 + `https://cloudflare-dns.com/dns-query?name=_atproto.${handle}&type=TXT`, 118 + { 119 + headers: { 120 + 'Accept': 'application/dns-json', 121 + }, 122 + } 123 + ); 124 + 125 + if (!response.ok) { 126 + return null; 127 + } 128 + 129 + const data = await response.json(); 130 + 131 + if (!data.Answer || data.Answer.length === 0) { 132 + return null; 133 + } 134 + 135 + for (const answer of data.Answer) { 136 + if (answer.type === 16 && answer.data) { 137 + // TXT records are quoted, remove quotes 138 + const txtData = answer.data.replace(/"/g, ''); 139 + if (txtData.startsWith('did=')) { 140 + const did = txtData.substring(4); 141 + if (isValidDid(did)) { 142 + return did; 143 + } 144 + } 145 + } 146 + } 147 + 148 + return null; 149 + } catch (error) { 150 + console.error('DNS resolution error:', error); 151 + return null; 152 + } 153 + } 154 + 155 + /** 156 + * Resolve a DID to a DID document 157 + * Supports did:plc and did:web 158 + * @param did - The DID to resolve 159 + * @returns The DID document 160 + */ 161 + export async function resolveDidDocument(did: string): Promise<DidDocument> { 162 + if (!isValidDid(did)) { 163 + throw new Error(`Invalid DID format: ${did}`); 164 + } 165 + 166 + if (did.startsWith('did:plc:')) { 167 + return await resolvePlcDid(did); 168 + } else if (did.startsWith('did:web:')) { 169 + return await resolveWebDid(did); 170 + } else { 171 + throw new Error(`Unsupported DID method: ${did}`); 172 + } 173 + } 174 + 175 + /** 176 + * Resolve a did:plc DID 177 + * @param did - The did:plc DID 178 + * @returns The DID document 179 + */ 180 + async function resolvePlcDid(did: string): Promise<DidDocument> { 181 + const plcId = did.substring('did:plc:'.length); 182 + 183 + // Try multiple PLC directories for resilience 184 + const plcDirectories = [ 185 + 'https://plc.directory', 186 + 'https://plc.bsky-sandbox.dev', 187 + ]; 188 + 189 + for (const directory of plcDirectories) { 190 + try { 191 + const response = await fetch(`${directory}/${did}`, { 192 + headers: { 193 + 'Accept': 'application/json', 194 + }, 195 + }); 196 + 197 + if (response.ok) { 198 + const document = await response.json(); 199 + return document; 200 + } 201 + } catch (error) { 202 + console.warn(`Failed to resolve from ${directory}:`, error); 203 + } 204 + } 205 + 206 + throw new Error(`Failed to resolve did:plc: ${did}`); 207 + } 208 + 209 + /** 210 + * Resolve a did:web DID 211 + * @param did - The did:web DID 212 + * @returns The DID document 213 + */ 214 + async function resolveWebDid(did: string): Promise<DidDocument> { 215 + const domain = did.substring('did:web:'.length).replace(/:/g, '/'); 216 + 217 + const response = await fetch(`https://${domain}/.well-known/did.json`, { 218 + headers: { 219 + 'Accept': 'application/json', 220 + }, 221 + }); 222 + 223 + if (!response.ok) { 224 + throw new Error(`Failed to resolve did:web: ${did}`); 225 + } 226 + 227 + const document = await response.json(); 228 + return document; 229 + } 230 + 231 + /** 232 + * Extract PDS URL from DID document 233 + * @param document - The DID document 234 + * @returns The PDS URL or null 235 + */ 236 + export function extractPdsUrl(document: DidDocument): string | null { 237 + if (!document.service || !Array.isArray(document.service)) { 238 + return null; 239 + } 240 + 241 + for (const service of document.service) { 242 + if ( 243 + service.type === 'AtprotoPersonalDataServer' && 244 + service.id.endsWith('#atproto_pds') 245 + ) { 246 + return service.serviceEndpoint; 247 + } 248 + } 249 + 250 + return null; 251 + } 252 + 253 + /** 254 + * Verify bidirectional handle/DID binding 255 + * @param handle - The handle 256 + * @param did - The DID 257 + * @returns True if the binding is valid 258 + */ 259 + export async function verifyHandleDidBinding( 260 + handle: string, 261 + did: string 262 + ): Promise<boolean> { 263 + try { 264 + // Verify handle resolves to DID 265 + const resolvedDid = await resolveHandleToDid(handle); 266 + if (resolvedDid !== did) { 267 + return false; 268 + } 269 + 270 + // Verify DID document includes handle in alsoKnownAs 271 + const didDocument = await resolveDidDocument(did); 272 + if (!didDocument.alsoKnownAs || !Array.isArray(didDocument.alsoKnownAs)) { 273 + return false; 274 + } 275 + 276 + const expectedAlias = `at://${handle}`; 277 + return didDocument.alsoKnownAs.includes(expectedAlias); 278 + } catch (error) { 279 + console.error('Failed to verify handle/DID binding:', error); 280 + return false; 281 + } 282 + } 283 + 284 + /** 285 + * Discover Authorization Server metadata for a PDS 286 + * @param pdsUrl - The PDS URL 287 + * @returns The authorization server metadata 288 + */ 289 + export async function discoverAuthorizationServer( 290 + pdsUrl: string 291 + ): Promise<AuthorizationServerMetadata> { 292 + // First, get the resource server metadata 293 + const resourceResponse = await fetch( 294 + `${pdsUrl}/.well-known/oauth-protected-resource`, 295 + { 296 + headers: { 297 + 'Accept': 'application/json', 298 + }, 299 + } 300 + ); 301 + 302 + if (!resourceResponse.ok) { 303 + throw new Error('Failed to fetch resource server metadata'); 304 + } 305 + 306 + const resourceMetadata: ResourceServerMetadata = await resourceResponse.json(); 307 + 308 + if (!resourceMetadata.authorization_servers || 309 + resourceMetadata.authorization_servers.length === 0) { 310 + throw new Error('No authorization servers found'); 311 + } 312 + 313 + // Use the first authorization server 314 + const authServerUrl = resourceMetadata.authorization_servers[0]; 315 + 316 + // Fetch the authorization server metadata 317 + const authResponse = await fetch( 318 + `${authServerUrl}/.well-known/oauth-authorization-server`, 319 + { 320 + headers: { 321 + 'Accept': 'application/json', 322 + }, 323 + } 324 + ); 325 + 326 + if (!authResponse.ok) { 327 + throw new Error('Failed to fetch authorization server metadata'); 328 + } 329 + 330 + const authMetadata: AuthorizationServerMetadata = await authResponse.json(); 331 + 332 + // Validate required fields 333 + if (!authMetadata.authorization_endpoint || 334 + !authMetadata.token_endpoint || 335 + !authMetadata.pushed_authorization_request_endpoint) { 336 + throw new Error('Invalid authorization server metadata'); 337 + } 338 + 339 + return authMetadata; 340 + } 341 + 342 + /** 343 + * Validate handle format 344 + * @param handle - The handle to validate 345 + * @returns True if valid 346 + */ 347 + export function isValidHandle(handle: string): boolean { 348 + // Handle must be a valid domain name 349 + const handleRegex = /^([a-z0-9][a-z0-9-]*[a-z0-9]|[a-z0-9])(\.([a-z0-9][a-z0-9-]*[a-z0-9]|[a-z0-9]))+$/i; 350 + return handleRegex.test(handle); 351 + } 352 + 353 + /** 354 + * Validate DID format 355 + * @param did - The DID to validate 356 + * @returns True if valid 357 + */ 358 + export function isValidDid(did: string): boolean { 359 + // Basic DID validation 360 + const didRegex = /^did:[a-z]+:[a-zA-Z0-9._%-]+$/; 361 + return didRegex.test(did); 362 + } 363 + 364 + /** 365 + * Complete handle to PDS discovery flow 366 + * @param handle - The handle to resolve 367 + * @returns Object with DID, PDS URL, and auth server metadata 368 + */ 369 + export async function discoverFromHandle(handle: string): Promise<{ 370 + did: string; 371 + pdsUrl: string; 372 + authServer: AuthorizationServerMetadata; 373 + }> { 374 + // Resolve handle to DID 375 + const did = await resolveHandleToDid(handle); 376 + 377 + // Resolve DID to document 378 + const didDocument = await resolveDidDocument(did); 379 + 380 + // Extract PDS URL 381 + const pdsUrl = extractPdsUrl(didDocument); 382 + if (!pdsUrl) { 383 + throw new Error('No PDS URL found in DID document'); 384 + } 385 + 386 + // Discover authorization server 387 + const authServer = await discoverAuthorizationServer(pdsUrl); 388 + 389 + return { 390 + did, 391 + pdsUrl, 392 + authServer, 393 + }; 394 + }
+11
tailwind.config.js
··· 1 + /** @type {import('tailwindcss').Config} */ 2 + export default { 3 + content: [ 4 + "./index.html", 5 + "./src/**/*.{js,ts,jsx,tsx}", 6 + ], 7 + theme: { 8 + extend: {}, 9 + }, 10 + plugins: [], 11 + }
+28
tsconfig.app.json
··· 1 + { 2 + "compilerOptions": { 3 + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 + "target": "ES2022", 5 + "useDefineForClassFields": true, 6 + "lib": ["ES2022", "DOM", "DOM.Iterable"], 7 + "module": "ESNext", 8 + "types": ["vite/client"], 9 + "skipLibCheck": true, 10 + 11 + /* Bundler mode */ 12 + "moduleResolution": "bundler", 13 + "allowImportingTsExtensions": true, 14 + "verbatimModuleSyntax": true, 15 + "moduleDetection": "force", 16 + "noEmit": true, 17 + "jsx": "react-jsx", 18 + 19 + /* Linting */ 20 + "strict": true, 21 + "noUnusedLocals": true, 22 + "noUnusedParameters": true, 23 + "erasableSyntaxOnly": true, 24 + "noFallthroughCasesInSwitch": true, 25 + "noUncheckedSideEffectImports": true 26 + }, 27 + "include": ["src"] 28 + }
+7
tsconfig.json
··· 1 + { 2 + "files": [], 3 + "references": [ 4 + { "path": "./tsconfig.app.json" }, 5 + { "path": "./tsconfig.node.json" } 6 + ] 7 + }
+26
tsconfig.node.json
··· 1 + { 2 + "compilerOptions": { 3 + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 + "target": "ES2023", 5 + "lib": ["ES2023"], 6 + "module": "ESNext", 7 + "types": ["node"], 8 + "skipLibCheck": true, 9 + 10 + /* Bundler mode */ 11 + "moduleResolution": "bundler", 12 + "allowImportingTsExtensions": true, 13 + "verbatimModuleSyntax": true, 14 + "moduleDetection": "force", 15 + "noEmit": true, 16 + 17 + /* Linting */ 18 + "strict": true, 19 + "noUnusedLocals": true, 20 + "noUnusedParameters": true, 21 + "erasableSyntaxOnly": true, 22 + "noFallthroughCasesInSwitch": true, 23 + "noUncheckedSideEffectImports": true 24 + }, 25 + "include": ["vite.config.ts"] 26 + }
+10
vite.config.ts
··· 1 + import { defineConfig } from 'vite' 2 + import react from '@vitejs/plugin-react' 3 + 4 + // https://vite.dev/config/ 5 + export default defineConfig({ 6 + plugins: [react()], 7 + server: { 8 + allowedHosts: ['oauth-spa.smokesignal.tools'], 9 + }, 10 + })