Compare changes

Choose any two refs to compare.

+62 -10
atproto-notifications/package-lock.json
··· 8 8 "name": "atproto-notifications", 9 9 "version": "0.0.0", 10 10 "dependencies": { 11 + "@atcute/client": "^4.0.3", 11 12 "@atcute/identity-resolver": "^1.1.3", 12 13 "@uidotdev/usehooks": "^2.4.1", 13 14 "lexicons": "file:../lexicons", 14 15 "psl": "^1.15.0", 15 16 "react": "^19.1.0", 16 17 "react-dom": "^19.1.0", 18 + "react-error-boundary": "^6.0.0", 17 19 "react-router": "^7.6.3", 18 - "react-time-ago": "^7.3.3" 20 + "react-time-ago": "^7.3.3", 21 + "reactjs-popup": "^2.0.6" 19 22 }, 20 23 "devDependencies": { 21 24 "@eslint/js": "^9.29.0", ··· 32 35 } 33 36 }, 34 37 "../lexicons": { 35 - "version": "0.0.1" 38 + "version": "0.0.1", 39 + "dependencies": { 40 + "@atcute/client": "^4.0.3", 41 + "@atcute/identity-resolver": "^1.1.3", 42 + "jsonpath-plus": "^10.3.0", 43 + "psl": "^1.15.0" 44 + } 36 45 }, 37 46 "node_modules/@ampproject/remapping": { 38 47 "version": "2.3.0", ··· 48 57 "node": ">=6.0.0" 49 58 } 50 59 }, 60 + "node_modules/@atcute/client": { 61 + "version": "4.0.3", 62 + "resolved": "https://registry.npmjs.org/@atcute/client/-/client-4.0.3.tgz", 63 + "integrity": "sha512-RIOZWFVLca/HiPAAUDqQPOdOreCxTbL5cb+WUf5yqQOKIu5yEAP3eksinmlLmgIrlr5qVOE7brazUUzaskFCfw==", 64 + "license": "MIT", 65 + "dependencies": { 66 + "@atcute/identity": "^1.0.2", 67 + "@atcute/lexicons": "^1.0.3" 68 + } 69 + }, 51 70 "node_modules/@atcute/identity": { 52 71 "version": "1.0.3", 53 72 "resolved": "https://registry.npmjs.org/@atcute/identity/-/identity-1.0.3.tgz", 54 73 "integrity": "sha512-mNMxbKHFGys03A8JXKk0KfMBzdd0vrYMzZZWjpw1nYTs0+ea6bo5S1hwqVUZxHdo1gFHSe/t63jxQIF4yL9aKw==", 55 74 "license": "0BSD", 56 - "peer": true, 57 75 "dependencies": { 58 76 "@atcute/lexicons": "^1.0.4", 59 77 "@badrap/valita": "^0.4.5" ··· 313 331 }, 314 332 "peerDependencies": { 315 333 "@babel/core": "^7.0.0-0" 334 + } 335 + }, 336 + "node_modules/@babel/runtime": { 337 + "version": "7.27.6", 338 + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", 339 + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", 340 + "license": "MIT", 341 + "engines": { 342 + "node": ">=6.9.0" 316 343 } 317 344 }, 318 345 "node_modules/@babel/template": { ··· 948 975 } 949 976 }, 950 977 "node_modules/@eslint/plugin-kit": { 951 - "version": "0.3.2", 952 - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.2.tgz", 953 - "integrity": "sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==", 978 + "version": "0.3.4", 979 + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", 980 + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", 954 981 "dev": true, 955 982 "license": "Apache-2.0", 956 983 "dependencies": { 957 - "@eslint/core": "^0.15.0", 984 + "@eslint/core": "^0.15.1", 958 985 "levn": "^0.4.1" 959 986 }, 960 987 "engines": { ··· 962 989 } 963 990 }, 964 991 "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { 965 - "version": "0.15.0", 966 - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.0.tgz", 967 - "integrity": "sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==", 992 + "version": "0.15.1", 993 + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", 994 + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", 968 995 "dev": true, 969 996 "license": "Apache-2.0", 970 997 "dependencies": { ··· 3063 3090 "react": "^19.1.0" 3064 3091 } 3065 3092 }, 3093 + "node_modules/react-error-boundary": { 3094 + "version": "6.0.0", 3095 + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.0.0.tgz", 3096 + "integrity": "sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==", 3097 + "license": "MIT", 3098 + "dependencies": { 3099 + "@babel/runtime": "^7.12.5" 3100 + }, 3101 + "peerDependencies": { 3102 + "react": ">=16.13.1" 3103 + } 3104 + }, 3066 3105 "node_modules/react-is": { 3067 3106 "version": "16.13.1", 3068 3107 "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", ··· 3115 3154 "javascript-time-ago": "^2.3.7", 3116 3155 "react": ">=0.16.8", 3117 3156 "react-dom": ">=0.16.8" 3157 + } 3158 + }, 3159 + "node_modules/reactjs-popup": { 3160 + "version": "2.0.6", 3161 + "resolved": "https://registry.npmjs.org/reactjs-popup/-/reactjs-popup-2.0.6.tgz", 3162 + "integrity": "sha512-A+tt+x9wdgZiZjv0e2WzYLD3IfFwJALaRaqwrCSXGjo0iQdsry/EtBEbQXRSmQs7cHmOi5eytCiSlOm8k4C+dg==", 3163 + "license": "MIT", 3164 + "engines": { 3165 + "node": ">=10" 3166 + }, 3167 + "peerDependencies": { 3168 + "react": ">=16", 3169 + "react-dom": ">=16" 3118 3170 } 3119 3171 }, 3120 3172 "node_modules/relative-time-format": {
+4 -1
atproto-notifications/package.json
··· 11 11 "preview": "vite preview" 12 12 }, 13 13 "dependencies": { 14 + "@atcute/client": "^4.0.3", 14 15 "@atcute/identity-resolver": "^1.1.3", 15 16 "@uidotdev/usehooks": "^2.4.1", 16 17 "lexicons": "file:../lexicons", 17 18 "psl": "^1.15.0", 18 19 "react": "^19.1.0", 19 20 "react-dom": "^19.1.0", 21 + "react-error-boundary": "^6.0.0", 20 22 "react-router": "^7.6.3", 21 - "react-time-ago": "^7.3.3" 23 + "react-time-ago": "^7.3.3", 24 + "reactjs-popup": "^2.0.6" 22 25 }, 23 26 "devDependencies": { 24 27 "@eslint/js": "^9.29.0",
atproto-notifications/public/icons/microcosm.png

This is a binary file and will not be displayed.

+4
atproto-notifications/src/App.css
··· 41 41 padding: 0.5rem 1rem; 42 42 } 43 43 44 + h3 { 45 + margin-top: 2rem; 46 + } 47 + 44 48 .detail { 45 49 font-style: italic; 46 50 font-size: 0.8rem;
+2 -3
atproto-notifications/src/App.tsx
··· 1 1 import { useCallback, useState, useEffect } from 'react'; 2 + import { Outlet } from 'react-router'; 2 3 import { useLocalStorage } from "@uidotdev/usehooks"; 3 4 import { GetJson } from './components/fetch'; 4 5 import { WhoAmI } from './components/WhoAmI'; 5 - import { Feed } from './components/Feed'; 6 6 import { WithFeatureChecks } from './components/setup/WithFeatureChecks'; 7 7 import { WithServiceWorker } from './components/setup/WithServiceWorker'; 8 8 import { WithServerHello } from './components/setup/WithServerHello'; 9 - import { UserContext } from './context'; 10 9 import { WithNotificationPermission } from './components/setup/WithNotificationPermission'; 11 10 import { WithPushSubscription } from './components/setup/WithPushSubscription'; 12 11 import { urlBase64ToUint8Array } from './utils'; ··· 22 21 <WithServiceWorker> 23 22 <WithNotificationPermission> 24 23 <WithPushSubscription> 25 - <Feed /> 24 + {children} 26 25 </WithPushSubscription> 27 26 </WithNotificationPermission> 28 27 </WithServiceWorker>
+8 -3
atproto-notifications/src/components/Buttons.css
··· 1 - button { 1 + button, 2 + a.button { 2 3 border-radius: 0.5rem; 3 4 border: 1px solid transparent; 5 + color: inherit; 4 6 padding: 0.6em 1.2em; 5 7 font-size: 1em; 6 8 font-weight: 500; ··· 12 14 border-right-color: hsla(0, 0%, 0%, 0.3); 13 15 box-shadow: 0 42px 42px -42px inset #221828; 14 16 } 15 - button:hover { 17 + button:hover, 18 + a.button:hover { 16 19 border-color: #646cff; 17 20 } 18 21 button:focus, 19 - button:focus-visible { 22 + button:focus-visible, 23 + a.button:focus, 24 + a.button:focus-visible { 20 25 outline: 4px auto -webkit-focus-ring-color; 21 26 } 22 27
-28
atproto-notifications/src/components/Feed.css
··· 1 - .feed-filter-type, 2 - .feed-filter-secondary { 3 - align-items: baseline; 4 - display: flex; 5 - gap: 0.5rem; 6 - justify-content: center; 7 - max-width: calc(100vw - 2rem); 8 - margin-top: 0.5rem; 9 - } 10 - 11 - .feed-filter-type h4, 12 - .feed-filter-secondary h4 { 13 - margin: 0; 14 - } 15 - 16 - .app-icon { 17 - height: 1rem; 18 - display: inline-block; 19 - vertical-align: baseline; 20 - position: relative; 21 - top: 0.1rem; 22 - margin-right: 0.25rem; 23 - } 24 - 25 - .feed-notifications { 26 - text-align: left; 27 - margin: 2rem auto; 28 - }
-149
atproto-notifications/src/components/Feed.tsx
··· 1 - import { useEffect, useState } from 'react'; 2 - import { getNotifications, getSecondary } from '../db'; 3 - import { ButtonGroup } from './Buttons'; 4 - import { Notification } from './Notification'; 5 - import psl from 'psl'; 6 - import lexicons from 'lexicons'; 7 - 8 - import './feed.css'; 9 - 10 - function SecondaryFilter({ inc, secondary, current, onUpdate }) { 11 - const [secondaries, setSecondaries] = useState([]); 12 - 13 - useEffect(() => { 14 - (async () => { 15 - const secondaries = await getSecondary(secondary); 16 - secondaries.sort((a, b) => b.unread - a.unread); 17 - setSecondaries(secondaries); 18 - // onUpdate(secondaries[0]?.k); // TODO 19 - })(); 20 - }, [inc, secondary]); 21 - 22 - // reset secondary filter only when leaving due to secondary change 23 - useEffect(() => () => onUpdate(null), [secondary]); 24 - 25 - return ( 26 - <ButtonGroup 27 - options={secondaries.map(({ k, unread, total }) => { 28 - 29 - // blehhhhhhhhhhhh 30 - 31 - let title = k; 32 - let icon; 33 - let app; 34 - let appName; 35 - if (secondary === 'source') { 36 - // TODO: clean up / move this to lexicons package? 37 - let appPrefix; 38 - try { 39 - const [nsid, ...rp] = k.split(':'); 40 - const parts = nsid.split('.'); 41 - const unreversed = parts.toReversed().join('.'); 42 - app = psl.parse(unreversed)?.domain ?? 'unknown'; 43 - appPrefix = app.split('.').toReversed().join('.'); 44 - } catch (e) { 45 - console.error('getting top app failed', e); 46 - } 47 - const lex = lexicons[appPrefix]; 48 - icon = lex?.clients[0]?.icon; 49 - appName = lex?.name; 50 - title = lex?.known_sources[k.slice(app.length + 1)] ?? k; 51 - 52 - } else if (secondary === 'group') { 53 - 54 - let appPrefix; 55 - try { 56 - const [nsid, ...rp] = k.split(':'); 57 - const parts = nsid.split('.'); 58 - const unreversed = parts.toReversed().join('.'); 59 - app = psl.parse(unreversed)?.domain ?? 'unknown'; 60 - appPrefix = app.split('.').toReversed().join('.'); 61 - } catch (e) { 62 - console.error('getting top app failed', e); 63 - } 64 - const lex = lexicons[appPrefix]; 65 - icon = lex?.clients[0]?.icon; 66 - appName = lex?.name; 67 - 68 - } else if (secondary === 'app') { 69 - const appReversed = k.split('.').toReversed().join('.'); 70 - const lex = lexicons[appReversed]; 71 - icon = lex?.clients[0]?.icon; 72 - title = appName = lex?.name; 73 - } 74 - 75 - return { 76 - val: k, 77 - label: ( 78 - <> 79 - {icon && ( 80 - <img className="app-icon" src={icon} title={appName ?? app} alt="" /> 81 - )} 82 - {title} ({total}) 83 - </> 84 - ), 85 - }; 86 - })} 87 - current={current} 88 - onChange={onUpdate} 89 - /> 90 - ); 91 - } 92 - 93 - export function Feed() { 94 - const [secondary, setSecondary] = useState('all'); 95 - const [secondaryFilter, setSecondaryFilter] = useState(null); 96 - 97 - // for now, we just increment a counter when a new notif comes in, which forces a re-render 98 - const [inc, setInc] = useState(0); 99 - useEffect(() => { 100 - const handleMessage = () => setInc(n => n + 1); 101 - const chan = new BroadcastChannel('notif'); 102 - chan.addEventListener('message', handleMessage); 103 - return () => chan.removeEventListener('message', handleMessage); 104 - }); 105 - 106 - // semi-gross way to just pull out all the events so we can see them 107 - // this could be combined with the broadcast thing above, but for now just chain deps 108 - const [feed, setFeed] = useState([]); 109 - useEffect(() => { 110 - (async () => setFeed(await getNotifications(secondary, secondaryFilter)))(); 111 - }, [inc, secondary, secondaryFilter]); 112 - 113 - if (feed.length === 0) { 114 - return 'no notifications loaded'; 115 - } 116 - return ( 117 - <div className="feed"> 118 - <div className="feed-filter-type"> 119 - <h4>Filter by:</h4> 120 - <ButtonGroup 121 - options={[ 122 - {val: 'all', label: 'All'}, 123 - {val: 'app', label: 'App'}, 124 - {val: 'group', label: 'Lexicon group'}, 125 - {val: 'source', label: 'Every source'}, 126 - ]} 127 - current={secondary} 128 - onChange={setSecondary} 129 - /> 130 - </div> 131 - {secondary !== 'all' && ( 132 - <div className="feed-filter-secondary"> 133 - <h4>Filter:</h4> 134 - <SecondaryFilter 135 - inc={inc} 136 - secondary={secondary} 137 - current={secondaryFilter} 138 - onUpdate={setSecondaryFilter} 139 - /> 140 - </div> 141 - )} 142 - <div className="feed-notifications"> 143 - {feed.map(([k, n]) => ( 144 - <Notification key={k} {...n} /> 145 - ))} 146 - </div> 147 - </div> 148 - ); 149 - }
+12 -2
atproto-notifications/src/components/Fetch.tsx
··· 67 67 ); 68 68 } 69 69 70 - async function postJson(url, body, credentials) { 70 + export async function postJson(url, body, credentials) { 71 71 const opts = { 72 72 method: 'POST', 73 73 headers: {'Content-Type': 'applicaiton/json'}, ··· 77 77 const res = await fetch(url, opts); 78 78 if (!res.ok) { 79 79 const m = await res.text(); 80 + let reason 81 + try { 82 + reason = JSON.parse(m)?.reason; 83 + } catch (err) {}; 84 + if (reason) throw reason; 80 85 throw new Error(`Failed to fetch: ${m}`); 81 86 } 82 - return await res.json(); 87 + try { 88 + return await res.json(); 89 + } catch (e) { 90 + if ([201, 204].includes(res.status)) return null; 91 + throw e; 92 + } 83 93 } 84 94 85 95 export function PostJson({ endpoint, data, credentials, ...forFetch }) {
+21
atproto-notifications/src/components/Notification.css
··· 7 7 box-sizing: border-box; 8 8 display: flex; 9 9 justify-content: space-between; 10 + gap: 0.5rem; 10 11 } 11 12 a.notification { 12 13 font: inherit; ··· 22 23 border-bottom-color: hsla(0, 0%, 0%, 0.3); 23 24 } 24 25 26 + .notification.error { 27 + background: hsla(347, 72%, 20%, 0.333); 28 + align-items: center; 29 + } 30 + .notification.error p { 31 + margin: 0; 32 + } 33 + 34 + .notification-info { 35 + display: flex; 36 + align-items: baseline; 37 + } 38 + 25 39 .handle { 26 40 color: skyblue; 41 + } 42 + 43 + .notification-context { 44 + font-size: 0.8rem; 45 + opacity: 0.667; 46 + margin: 0.25rem 0 0; 47 + max-width: 48em; 27 48 } 28 49 29 50 .notification-when {
+50 -16
atproto-notifications/src/components/Notification.tsx
··· 1 + import { useState, useEffect } from 'react'; 1 2 import ReactTimeAgo from 'react-time-ago'; 2 3 import psl from 'psl'; 3 - import lexicons from 'lexicons'; 4 + import { default as lexicons, getLink, getContext } from 'lexicons'; 4 5 import { resolveDid } from '../atproto/resolve'; 5 6 import { Fetch } from './Fetch'; 6 7 7 8 import './Notification.css'; 8 9 10 + export function fallbackRender({ error, resetErrorBoundary }) { 11 + console.error('rendering fallback for error', error); 12 + return ( 13 + <div className="notification error"> 14 + <p>sorry, something went wrong trying to show this notification</p> 15 + <p><button onClick={resetErrorBoundary}>retry</button></p> 16 + </div> 17 + ); 18 + } 19 + 9 20 export function Notification({ app, group, source, source_record, source_did, subject, timestamp }) { 21 + const [resolvedLink, setResolvedLink] = useState(null); 22 + const [resolvedContext, setResolvedContext] = useState([]); 23 + 24 + useEffect(() => { 25 + (async () => { 26 + const link = await getLink(source, source_record, subject); 27 + if (link) setResolvedLink(link); 28 + })(); 29 + (async() => { 30 + const context = await getContext(source, source_record, subject); 31 + setResolvedContext(context); 32 + })(); 33 + }, [source, source_record, subject]); 10 34 11 35 // TODO: clean up / move this to lexicons package? 12 36 let title = source; ··· 23 47 let link = lex?.clients[0]?.notifications; 24 48 appName = lex?.name; 25 49 const sourceRemainder = source.slice(app.length + 1); 26 - title = lex?.known_sources[sourceRemainder] ?? source; 50 + title = lex?.known_sources[sourceRemainder]?.name ?? source; 27 51 28 52 let directLink; 29 53 if (subject.startsWith('did:')) { ··· 33 57 34 58 directLink = lex 35 59 ?.clients[0] 36 - ?.direct_links[`did:${sourceRemainder}`] 60 + ?.direct_links?.[`did:${sourceRemainder}`] 37 61 ?.replace('{subject.did}', subject) 38 62 ?.replace('{source_record.did}', sDid) 39 63 ?.replace('{source_record.collection}', sCollection) ··· 48 72 49 73 directLink = lex 50 74 ?.clients[0] 51 - ?.direct_links[`at_uri:${sourceRemainder}`] 75 + ?.direct_links?.[`at_uri:${sourceRemainder}`] 52 76 ?.replace('{subject.did}', did) 53 77 ?.replace('{subject.collection}', collection) 54 78 ?.replace('{subject.rkey}', rest.join('/') || null) ··· 56 80 ?.replace('{source_record.collection}', sCollection) 57 81 ?.replace('{source_record.rkey}', sRest.join('/') || null); 58 82 } 59 - link = directLink ?? link; 83 + link = resolvedLink ?? directLink ?? link; 84 + 85 + let contextClipped = resolvedContext.join(' '); 86 + if (contextClipped.length > 240) { 87 + contextClipped = contextClipped.slice(0, 239) + 'โ€ฆ'; 88 + } 60 89 61 90 const contents = ( 62 91 <> ··· 64 93 {icon && ( 65 94 <img className="app-icon" src={icon} title={appName ?? app} alt="" /> 66 95 )} 67 - {title} from 68 - {' '} 69 - {source_did ? ( 70 - <Fetch 71 - using={resolveDid} 72 - args={[source_did]} 73 - ok={handle => <span className="handle">@{handle}</span>} 74 - /> 75 - ) : ( 76 - source_record 77 - )} 96 + <div> 97 + {title} from 98 + {' '} 99 + {source_did ? ( 100 + <Fetch 101 + using={resolveDid} 102 + args={[source_did]} 103 + ok={handle => <span className="handle">@{handle}</span>} 104 + /> 105 + ) : ( 106 + source_record 107 + )} 108 + {contextClipped.length > 0 && ( 109 + <p className="notification-context">{contextClipped}</p> 110 + )} 111 + </div> 78 112 </div> 79 113 {timestamp && ( 80 114 <div className="notification-when">
+43
atproto-notifications/src/components/NotificationSettings.tsx
··· 1 + import { useState, useCallback } from 'react'; 2 + import { Link } from 'react-router'; 3 + import { GetJson, postJson } from './Fetch'; 4 + import { ButtonGroup } from './Buttons'; 5 + 6 + export function NotificationSettings({ secondary, secondaryFilter }) { 7 + const [notifyToggleCounter, setNotifyToggleCounter] = useState(0); 8 + 9 + // TODO move up (to chrome?) so it syncs 10 + const setGlobalNotifications = useCallback(async enabled => { 11 + const host = import.meta.env.VITE_NOTIFICATIONS_HOST; 12 + const url = new URL('/global-notify', host); 13 + try { 14 + await postJson(url, JSON.stringify({ notify_enabled: enabled }), true) 15 + } catch (err) { 16 + console.error('failed to set self-notify setting', err); 17 + } 18 + setNotifyToggleCounter(n => n + 1); 19 + }); 20 + 21 + if (secondary !== 'all') return; 22 + 23 + return ( 24 + <div className="feed-filter-type"> 25 + <h4>All notifications:</h4> 26 + <GetJson 27 + key={notifyToggleCounter} 28 + endpoint="/global-notify" 29 + credentials 30 + ok={({ notify_enabled }) => ( 31 + <ButtonGroup 32 + options={[ 33 + {val: 'paused', label: <>โธ&nbsp;&nbsp;pause{!notify_enabled && 'd'}</>}, 34 + {val: 'active', label: <>โ–ถ&nbsp;&nbsp;{notify_enabled ? 'notifications active' : 'enable notifications'}</>}, 35 + ]} 36 + current={notify_enabled ? 'active' : 'paused'} 37 + onChange={val => setGlobalNotifications(val === 'active')} 38 + /> 39 + )} 40 + /> 41 + </div> 42 + ); 43 + }
+12 -25
atproto-notifications/src/components/SecretPassword.jsx
··· 2 2 import { PostJson } from './Fetch'; 3 3 4 4 export function SecretPassword({ did, role }) { 5 - const [begun, setBegun] = useState(false); 6 - const [pw, setPw] = useState(''); 5 + const [submission, setSubmission] = useState(0); 7 6 const [submitting, setSubmitting] = useState(false); 8 7 9 8 const handleSubmit = useCallback(e => { 10 9 e.preventDefault(); 10 + setSubmission(n => n + 1); 11 11 setSubmitting(true); 12 12 }) 13 13 14 14 return ( 15 15 <form method="post" onSubmit={handleSubmit}> 16 - <h2>Secret password required</h2> 17 - <p>This demo is not ready for public yet, but you can get early access as a <a href="https://github.com/sponsors/uniphil/" target="_blank">github sponsor</a> or <a href="https://ko-fi.com/bad_example" target="_blank">ko-fi supporter</a>.</p> 16 + <h2>Secret early access</h2> 17 + <p>This demo is still in development! Your support helps keep it going: <a href="https://github.com/sponsors/uniphil/" target="_blank">github sponsors</a>, <a href="https://ko-fi.com/bad_example" target="_blank">ko-fi</a>.</p> 18 18 19 19 {submitting ? ( 20 20 <PostJson 21 + key={submission} 21 22 endpoint="/super-top-secret-access" 22 - data={{ secret_password: pw }} 23 + data={{ secret_password: "letmein" }} 23 24 credentials 24 25 loading={() => (<>Checking&hellip;</>)} 25 26 error={e => { ··· 35 36 }} 36 37 ok={() => ( 37 38 <> 38 - <p>That will do.</p> 39 + <p style={{ color: "#9f0" }}>Secret password accepted.</p> 39 40 <p> 40 - <button onClick={() => window.location.reload()}> 41 - Enter 42 - </button> 41 + {/* an <a> tag, not a <Link>, on purpose so we relaod for our role */} 42 + <a className="button" href="/early?hello"> 43 + Continue 44 + </a> 43 45 </p> 44 46 </> 45 47 )} 46 48 /> 47 49 ) : ( 48 50 <p> 49 - <label> 50 - Password: 51 - {' '} 52 - <input 53 - type="text" 54 - value={pw} 55 - onFocus={() => setBegun(true)} 56 - onChange={e => setPw(e.target.value)} 57 - /> 58 - </label> 59 - {' '} 60 - {begun && ( 61 - <button type="submit" className="subtle"> 62 - open sesame 63 - </button> 64 - )} 51 + <button type="submit">Let me in</button> 65 52 </p> 66 53 )} 67 54 </form>
+2 -1
atproto-notifications/src/components/WhoAmI.tsx
··· 17 17 18 18 return ( 19 19 <iframe 20 - src={`${origin}/prompt`} 20 + src={`${origin}/prompt?app=notifications.microcosm.blue`} 21 + referrerPolicy="strict-origin" 21 22 ref={frameRef} 22 23 height="180" 23 24 width="360"
+13
atproto-notifications/src/components/setup/Chrome.css
··· 1 + .chrome-role-tag { 2 + font-size: 0.6rem; 3 + /*background: #555;*/ 4 + color: #8c0; 5 + border: 0.5px solid #690; 6 + display: inline-block; 7 + border-radius: 0.5em; 8 + padding: 0.1em 0.2em 0.15em; 9 + line-height: 1; 10 + vertical-align: top; 11 + margin-left: -0.333rem; 12 + transform: rotate(13deg); 13 + }
+75 -26
atproto-notifications/src/components/setup/Chrome.tsx
··· 1 + import { useCallback, useEffect, useState } from 'react'; 2 + import { Link } from 'react-router'; 1 3 import { Handle } from '../User'; 4 + import { GetJson, postJson } from '../Fetch'; 5 + import './Chrome.css'; 2 6 3 - export function Chrome({ user, children }) { 4 - const content = children; 5 - const logout = () => null; 7 + function Header({ user, onLogout }) { 8 + return ( 9 + <header id="app-header"> 10 + <h1> 11 + <Link to="/" className="inherit-font"> 12 + spacedust notifications&nbsp;<span className="demo">demo!</span> 13 + </Link> 14 + </h1> 15 + {user && ( 16 + <div className="current-user"> 17 + <p> 18 + <span className="handle"> 19 + <Handle did={user.did} /> 20 + {user.role !== 'public' && ( 21 + <span className="chrome-role-tag"> 22 + {user.role === 'admin' ? ( 23 + <Link to="/admin" className="inherit-font">{user.role}</Link> 24 + ) : user.role === 'early' ? ( 25 + <Link to="/early" className="inherit-font">{user.role}</Link> 26 + ) : ( 27 + <>{user.role}</> 28 + )} 29 + </span> 30 + )} 31 + </span> 32 + <button className="subtle bad" onClick={onLogout}>&times;</button> 33 + </p> 34 + </div> 35 + )} 36 + </header> 37 + ); 38 + } 39 + 40 + export function Chrome({ user, onLogout, children }) { 41 + const [secretDevCounter, setSecretDevCounter] = useState(0); 42 + const [secretDevStatus, setSecretDevStatus] = useState(null); 43 + 44 + // ~~is this the best way~~ does it work? yeh 45 + const setSelfNotify = useCallback(async enabled => { 46 + setSecretDevStatus('pending'); 47 + const host = import.meta.env.VITE_NOTIFICATIONS_HOST; 48 + const url = new URL('/global-notify', host); 49 + try { 50 + await postJson(url, JSON.stringify({ notify_self: enabled }), true) 51 + setSecretDevStatus(null); 52 + } catch (err) { 53 + console.error('failed to set self-notify setting', err); 54 + setSecretDevStatus('failed'); 55 + } 56 + setSecretDevCounter(n => n + 1); 57 + }); 58 + 6 59 return ( 7 60 <> 8 - <header id="app-header"> 9 - <h1>spacedust notifications&nbsp;<span className="demo">demo!</span></h1> 10 - {user && ( 11 - <div className="current-user"> 12 - <p> 13 - <span className="handle"> 14 - <Handle did={user.did} /> 15 - </span> 16 - {/* TODO: clear *all* info on logout */} 17 - <button className="subtle bad" onClick={logout}>&times;</button> 18 - </p> 19 - </div> 20 - )} 21 - </header> 61 + <Header user={user} onLogout={onLogout} /> 22 62 23 63 <div id="app-content"> 24 - {content} 64 + {children} 25 65 </div> 26 66 27 67 <div className="footer"> ··· 56 96 <p className="secret-dev"> 57 97 secret dev setting: 58 98 {' '} 59 - <label> 60 - <input 61 - type="checkbox" 62 - onChange={e => setDev(e.target.checked)} 63 - checked={true /*isDev(ufosHost)*/} 64 - /> 65 - localhost 66 - </label> 99 + <GetJson 100 + key={secretDevCounter} 101 + endpoint="/global-notify" 102 + credentials 103 + loading={() => <>&hellip;</>} 104 + ok={({ notify_self }) => ( 105 + <label> 106 + <input 107 + type="checkbox" 108 + onChange={e => setSelfNotify(e.target.checked)} 109 + checked={notify_self ^ (secretDevStatus === 'pending')} 110 + disabled={secretDevStatus === 'pending'} 111 + /> 112 + self-notify 113 + </label> 114 + )} 115 + /> 67 116 </p> 68 117 </div> 69 118 </>
+9 -2
atproto-notifications/src/components/setup/WithFeatureChecks.tsx
··· 1 + import { Chrome } from './Chrome'; 2 + 1 3 export function WithFeatureChecks({ children }) { 2 4 if (!('serviceWorker' in navigator)) { 3 5 return ( ··· 7 9 8 10 if (!('PushManager' in window)) { 9 11 return ( 10 - <p>sorry, your browser does not support WebPush for notifications</p> 12 + <Chrome> 13 + <p>Sorry, your browser does not support Web Push for notifications</p> 14 + </Chrome> 11 15 ); 12 16 } 13 17 14 18 if (!('Notification' in window)) { 15 19 return ( 16 - <p>sorry, your browser does not support creating system notifications</p> 20 + <Chrome> 21 + <p>Sorry, your browser does not support the Notifications API for creating system notifications</p> 22 + <p>If you're on iOS, you can try tapping <strong>add to home screen</strong> from the <strong>share</strong> menu, and then opening <strong>Spacedust</strong> from your home screen to unlock notifications support, but note that Web Push in iOS is unreliable.</p> 23 + </Chrome> 17 24 ); 18 25 } 19 26
+1 -1
atproto-notifications/src/components/setup/WithNotificationPermission.tsx
··· 15 15 if (currentPermission !== 'granted') { 16 16 return ( 17 17 <> 18 - <h3>Step 2: Allow notifications</h3> 18 + <h3>Final step: Allow notifications</h3> 19 19 <p>To show notifications we need permission:</p> 20 20 <p> 21 21 <button
+48 -19
atproto-notifications/src/components/setup/WithServerHello.tsx
··· 1 1 import { useCallback, useEffect, useState } from 'react'; 2 - import { UserContext, PushServerContext } from '../../context'; 2 + import { RoleContext, PushServerContext } from '../../context'; 3 3 import { WhoAmI } from '../WhoAmI'; 4 4 import { SecretPassword } from '../SecretPassword'; 5 5 import { GetJson, PostJson } from '../Fetch'; 6 6 import { Chrome } from './Chrome'; 7 7 8 - 9 - // const logout = useCallback(async () => { 10 - // setRole('anonymous'); 11 - // setUser(null); 12 - // // TODO: clear indexeddb 13 - // await fetch(`${host}/logout`, { 14 - // method: 'POST', 15 - // credentials: 'include', 16 - // }); 17 - // }); 18 - 19 8 export function WithServerHello({ children }) { 9 + const [loggingOut, setLoggingOut] = useState(null); 10 + const [helloKey, setHelloKey] = useState(0); 11 + const [whoamiKey, setWhoamiKey] = useState(0); 20 12 const [whoamiInfo, setWhoamiInfo] = useState(null); 21 13 22 - const childrenFor = useCallback((did, role) => { 14 + const childrenFor = useCallback((did, role, parentChildren) => { 23 15 if (role === 'public') { 24 16 return <SecretPassword did={did} role={role} />; 25 17 } 26 - return 'hiiiiiiii ' + role; 18 + return parentChildren; 27 19 }) 28 20 21 + const reloadConnect = useCallback(e => { 22 + e.preventDefault(); 23 + setWhoamiKey(n => n + 1); 24 + }); 25 + 26 + const handleLogout = useCallback(async () => { 27 + setLoggingOut(true); 28 + try { 29 + const host = import.meta.env.VITE_NOTIFICATIONS_HOST; 30 + await fetch(`${host}/logout`, { 31 + method: 'POST', 32 + credentials: 'include', 33 + }); 34 + // TODO: cancel subscription, clear storage, etc 35 + } catch (e) { 36 + console.error('logout fail', e); 37 + } 38 + setLoggingOut(null); 39 + setHelloKey(n => n + 1); 40 + }); 41 + 42 + if (loggingOut !== null) { 43 + return <Chrome><p>Logging out&hellip;</p></Chrome>; 44 + } 45 + 29 46 return ( 30 47 <GetJson 31 48 /* todo: key on login state */ 49 + key={helloKey} 32 50 endpoint='/hello' 33 51 credentials 34 52 ok={({ whoamiHost, webPushPublicKey, role, did }) => { ··· 36 54 return whoamiInfo === null 37 55 ? ( 38 56 <Chrome> 39 - <WhoAmI origin={whoamiHost} onIdentify={setWhoamiInfo} /> 57 + <WhoAmI 58 + key={whoamiKey} 59 + origin={whoamiHost} 60 + onIdentify={setWhoamiInfo} 61 + /> 62 + <p style={{fontSize: '0.8rem'}}> 63 + <a href="#" onClick={reloadConnect}>Reload connect prompt</a> 64 + </p> 40 65 </Chrome> 41 66 ) : ( 42 67 <PostJson ··· 44 69 data={{ token: whoamiInfo.token }} 45 70 credentials 46 71 ok={({ did, role, webPushPublicKey }) => ( 47 - <Chrome user={{ did, role }}> 72 + <Chrome user={{ did, role }} onLogout={handleLogout}> 48 73 <PushServerContext.Provider value={webPushPublicKey}> 49 - {childrenFor(did, role)} 74 + <RoleContext.Provider value={role}> 75 + {childrenFor(did, role, children)} 76 + </RoleContext.Provider> 50 77 </PushServerContext.Provider> 51 78 </Chrome> 52 79 )} ··· 54 81 ) 55 82 } else { 56 83 return ( 57 - <Chrome user={{ did, role }}> 84 + <Chrome user={{ did, role }} onLogout={handleLogout}> 58 85 <PushServerContext.Provider value={webPushPublicKey}> 59 - {childrenFor(did, role)} 86 + <RoleContext.Provider value={role}> 87 + {childrenFor(did, role, children)} 88 + </RoleContext.Provider> 60 89 </PushServerContext.Provider> 61 90 </Chrome> 62 91 );
+1 -1
atproto-notifications/src/context.ts
··· 1 1 import { createContext } from 'react'; 2 2 3 - export const UserContext = createContext('light'); 3 + export const RoleContext = createContext('public'); 4 4 export const PushServerContext = createContext(null);
+7 -2
atproto-notifications/src/index.css
··· 22 22 color: #535bf2; 23 23 } 24 24 25 + a.inherit-font { 26 + font: inherit; 27 + color: inherit; 28 + } 29 + 25 30 body { 26 31 margin: 0; 27 32 min-width: 320px; ··· 34 39 } 35 40 36 41 @media (prefers-color-scheme: light) { 37 - :root { 42 + /* :root { 38 43 color: #213547; 39 44 background-color: #ffffff; 40 45 } ··· 43 48 } 44 49 button { 45 50 background-color: #f9f9f9; 46 - } 51 + }*/ 47 52 }
+16 -12
atproto-notifications/src/main.tsx
··· 1 - import { StrictMode } from 'react' 2 - import { createRoot } from 'react-dom/client' 1 + import { StrictMode } from 'react'; 2 + import { createRoot } from 'react-dom/client'; 3 3 import { BrowserRouter, Routes, Route } from "react-router"; 4 - import './index.css' 5 - import { App } from './App.tsx' 4 + import './index.css'; 5 + import { App } from './App'; 6 + import { Feed } from './pages/Feed'; 7 + import { Admin } from './pages/Admin'; 8 + import { Early } from './pages/Early'; 6 9 7 10 createRoot(document.getElementById('root')!).render( 8 - <StrictMode> 9 - <App> 10 - <BrowserRouter> 11 + // <StrictMode> 12 + <BrowserRouter> 13 + <App> 11 14 <Routes> 12 - {/*<Route index element={<Home />} />*/} 13 - {/*<Route path="/status" element={<Status />} />*/} 15 + <Route index element={<Feed />} /> 16 + <Route path="/admin" element={<Admin />} /> 17 + <Route path="/early" element={<Early />} /> 14 18 </Routes> 15 - </BrowserRouter> 16 - </App> 17 - </StrictMode>, 19 + </App> 20 + </BrowserRouter> 21 + // </StrictMode>, 18 22 ); 19 23 20 24 import TimeAgo from 'javascript-time-ago'
+18
atproto-notifications/src/pages/Admin.css
··· 1 + .admin-new-pw-p { 2 + margin-bottom: 0.25rem; 3 + } 4 + .admin-error-message { 5 + font-size: 0.8rem; 6 + color: #f90; 7 + margin-top: 0.25rem; 8 + } 9 + 10 + .admin-secret { 11 + border-left: 2px solid #555; 12 + padding: 0.25rem 0.5rem; 13 + margin: 0.75rem 0; 14 + text-align: left; 15 + } 16 + .admin-secret-secret { 17 + margin: 0 0 0.5rem; 18 + }
+157
atproto-notifications/src/pages/Admin.tsx
··· 1 + import { useContext, useEffect, useState } from 'react'; 2 + import { RoleContext } from '../context'; 3 + import ReactTimeAgo from 'react-time-ago'; 4 + import { GetJson, PostJson } from '../components/Fetch'; 5 + import { Handle } from '../components/User'; 6 + 7 + import './Admin.css' 8 + 9 + // yeah this is horrible, i don't care 10 + function OnMount({ callback }) { 11 + useEffect(() => { 12 + callback(); 13 + }); 14 + } 15 + 16 + function AddSecretForm({ onAdded }) { 17 + const [active, setActive] = useState(false); 18 + const [value, setValue] = useState(''); 19 + const [submit, setSubmit] = useState(false); 20 + 21 + const handleFocus = () => { 22 + setSubmit(false); 23 + setActive(true); 24 + }; 25 + 26 + const handleChange = e => { 27 + setSubmit(false); 28 + setValue(e.target.value); 29 + }; 30 + 31 + const handleSubmit = e => { 32 + e.preventDefault(); 33 + setSubmit(true); 34 + }; 35 + 36 + return ( 37 + <form onSubmit={handleSubmit}> 38 + <p className="admin-new-pw-p"> 39 + <label> 40 + new secret password: 41 + {' '} 42 + <input 43 + onFocus={handleFocus} 44 + onChange={handleChange} 45 + value={value} 46 + size={12} 47 + /> 48 + </label> 49 + {active && ( 50 + <>{' '}<button type="submit" className="subtle">add</button></> 51 + )} 52 + </p> 53 + {submit && ( 54 + <PostJson 55 + endpoint="/top-secret" 56 + data={{ secret_password: value }} 57 + credentials 58 + ok={() => (<p>added.<OnMount callback={() => onAdded(value)}/></p>)} 59 + error={e => { 60 + if (e === 'conflict') { 61 + return <p className="admin-error-message">rejected (likely exists or constraint failed)</p> 62 + } 63 + return <p className="admin-error-message">adding secret failed: {e.toString()}</p>; 64 + }} 65 + /> 66 + )} 67 + </form> 68 + ); 69 + } 70 + 71 + export function Admin({}) { 72 + const [listKey, setListKey] = useState(''); 73 + if (useContext(RoleContext) !== 'admin') { 74 + return <p>sorry, this page is admin-only</p> 75 + } 76 + 77 + return ( 78 + <> 79 + <h2>Top secret(s)</h2> 80 + <AddSecretForm onAdded={setListKey} /> 81 + <GetJson 82 + key={listKey} 83 + endpoint="/top-secrets" 84 + credentials 85 + ok={secrets => secrets.map(s => <Secret key={s.password} {...s} />)} 86 + /> 87 + <Secret password={null} added={0} expired={null} /> 88 + </> 89 + ); 90 + } 91 + 92 + function Secret({ password, added, expired }) { 93 + const [expiring, setExpiring] = useState(false); 94 + const [reallyExpired, setReallyExpired] = useState(expired); 95 + return ( 96 + <div className="admin-secret"> 97 + <p className="admin-secret-secret"> 98 + {password !== null ? <>"{password}"</> : '[no password]'} 99 + {' '} 100 + (added <ReactTimeAgo date={new Date(added)} locale="en-US" /> 101 + {expired && ( 102 + <>, expired <ReactTimeAgo date={new Date(expired)} locale="en-US" /></> 103 + )}) 104 + {' '} 105 + {!reallyExpired && ( 106 + expiring ? ( 107 + <PostJson 108 + endpoint="/expire-top-secret" 109 + data={{ secret_password: password }} 110 + credentials 111 + loading={() => <>&hellip;</>} 112 + ok={() => <OnMount callback={() => setReallyExpired(true)} />} 113 + /> 114 + ) : ( 115 + <button 116 + className="subtle" 117 + disabled={expiring} 118 + onClick={() => setExpiring(true)} 119 + > 120 + expire 121 + </button> 122 + ) 123 + )} 124 + </p> 125 + <GetJson 126 + endpoint="/top-secret-accounts" 127 + params={{ secret_password: password ?? '' }} 128 + credentials 129 + ok={accounts => accounts.length > 0 ? ( 130 + <ul> 131 + {accounts.map(info => ( 132 + <li key={info.did}> 133 + <Account {...info} /> 134 + </li> 135 + ))} 136 + </ul> 137 + ) : ( 138 + <p><em>no accounts</em></p> 139 + )} 140 + /> 141 + </div> 142 + ); 143 + } 144 + 145 + function Account({ did, first_seen, role, active_subs, total_pushes, last_push }) { 146 + return ( 147 + <p> 148 + <Handle did={did} /> 149 + {' '} 150 + ({active_subs} subs, {total_pushes} pushes, latest <ReactTimeAgo date={new Date(last_push)} locale="en-US" />) 151 + <br/> 152 + joined <ReactTimeAgo date={new Date(first_seen)} locale="en-US" /> 153 + {', '} 154 + role: <code>{role}</code> 155 + </p> 156 + ); 157 + }
+5
atproto-notifications/src/pages/Early.css
··· 1 + .early { 2 + max-width: 36rem; 3 + text-align: left; 4 + margin: 0 auto; 5 + }
+118
atproto-notifications/src/pages/Early.tsx
··· 1 + import { useCallback, useState } from 'react'; 2 + import { Link, useSearchParams } from 'react-router'; 3 + import { GetJson, postJson } from '../components/Fetch'; 4 + import { ButtonGroup } from '../components/Buttons'; 5 + import './Early.css'; 6 + 7 + export function Early({ }) { 8 + const [searchParams, _] = useSearchParams(); 9 + const [notified, setNotified] = useState(false); 10 + const [pushStatus, setPushStatus] = useState(null); 11 + const [pushed, setPushed] = useState(false); 12 + const [notifyToggleCounter, setNotifyToggleCounter] = useState(0); 13 + 14 + const returning = !searchParams.has('hello'); 15 + 16 + const localTest = useCallback(() => { 17 + try { 18 + new Notification("Hello world!", { body: "This notification never left your browser" }); 19 + } catch (e) { 20 + console.error('failed to create local notification', e); 21 + alert('Failed to create local notification. If you\'re up for helping debug, get in touch.'); 22 + } 23 + setNotified(true); 24 + }); 25 + 26 + const pushTest = useCallback(async () => { 27 + setPushStatus('pending'); 28 + const host = import.meta.env.VITE_NOTIFICATIONS_HOST; 29 + const url = new URL('/push-test', host); 30 + try { 31 + await postJson(url, JSON.stringify(null), true); 32 + setPushStatus(null); 33 + } catch (e) { 34 + console.error('failed push test request', e); 35 + setPushStatus('failed'); 36 + } 37 + setPushed(true); 38 + }); 39 + 40 + // TODO move up (to chrome?) so it syncs 41 + const setGlobalNotifications = useCallback(async enabled => { 42 + const host = import.meta.env.VITE_NOTIFICATIONS_HOST; 43 + const url = new URL('/global-notify', host); 44 + try { 45 + await postJson(url, JSON.stringify({ notify_enabled: enabled }), true) 46 + } catch (err) { 47 + console.error('failed to set self-notify setting', err); 48 + } 49 + setNotifyToggleCounter(n => n + 1); 50 + }); 51 + 52 + return ( 53 + <div className="early"> 54 + <h2>Hello!</h2> 55 + <p>Welcome to the early preview for the spacedust notifications demo, and since you're here early: thanks so much for supporting microcosm!</p> 56 + <p>A few things to keep in mind:</p> 57 + <ol> 58 + <li>This is a <a href="https://spacedust.microcosm.blue" target="_blank">spacedust</a> demo, not a polished product</li> 59 + <li>Mobile browsers are unreliable at delivering Web Push notifications</li> 60 + <li>Many features are easy to add! Some are surprisingly hard! Make a request and let's see :)</li> 61 + </ol> 62 + <p>With that out of the way, let's cover some basics!</p> 63 + 64 + <h3>Testing 1, 2, 3&hellip;</h3> 65 + <p> 66 + To see a test notification, <button onClick={localTest}>click on this</button>. This is a local-only test. 67 + </p> 68 + {(returning || notified) && ( 69 + <> 70 + <p> 71 + Then 72 + {' '} 73 + <button 74 + disabled={pushStatus === 'pending'} 75 + onClick={pushTest} 76 + > 77 + click here {pushed > 0 && 'โœ…'} 78 + </button> 79 + {' '} 80 + for another. This one uses Web Push! 81 + </p> 82 + {pushStatus === 'failed' && <p>uh oh, something went wrong requesting a web push</p>} 83 + </> 84 + )} 85 + {(returning || (pushed && pushStatus !== 'failed')) && ( 86 + <> 87 + <h3>Great!</h3> 88 + <p>You're all set up to enable notifications:</p> 89 + 90 + <GetJson 91 + key={notifyToggleCounter} 92 + endpoint="/global-notify" 93 + credentials 94 + ok={({ notify_enabled }) => ( 95 + <ButtonGroup 96 + options={[ 97 + {val: 'paused', label: <>โธ&nbsp;&nbsp;pause{!notify_enabled && 'd'}</>}, 98 + {val: 'active', label: <>โ–ถ&nbsp;&nbsp;{notify_enabled ? 'notifications active' : 'enable notifications'}</>}, 99 + ]} 100 + current={notify_enabled ? 'active' : 'paused'} 101 + onChange={val => setGlobalNotifications(val === 'active')} 102 + /> 103 + )} 104 + /> 105 + 106 + <p> 107 + You can get back to this page by clicking the early 108 + <span className="chrome-role-tag">early</span> 109 + {' '} 110 + tag by your handle. 111 + </p> 112 + <p><Link to="/">Go to Notifications</Link></p> 113 + </> 114 + )} 115 + 116 + </div> 117 + ); 118 + }
+72
atproto-notifications/src/pages/Feed.css
··· 1 + .feed-filter-type, 2 + .feed-filter-secondary { 3 + align-items: baseline; 4 + display: flex; 5 + gap: 0.5rem; 6 + justify-content: center; 7 + max-width: calc(100vw - 2rem); 8 + margin-top: 0.5rem; 9 + } 10 + 11 + .feed-filter-type h4, 12 + .feed-filter-secondary h4 { 13 + margin: 0; 14 + } 15 + 16 + .app-icon { 17 + height: 1rem; 18 + display: inline-block; 19 + vertical-align: baseline; 20 + position: relative; 21 + top: 0.1rem; 22 + margin-right: 0.25rem; 23 + } 24 + 25 + .feed-notifications { 26 + text-align: left; 27 + margin: 2rem auto; 28 + } 29 + 30 + .filter-pref-wrapper { 31 + display: inline-block; 32 + } 33 + 34 + .filter-pref-trigger { 35 + display: inline-block; 36 + padding: 0 0.25rem; 37 + } 38 + .filter-pref-trigger:hover { 39 + background: hsla(0, 0%, 50%, 0.333); 40 + border-radius: 0.3333rem; 41 + } 42 + 43 + .popup-arrow { 44 + color: #2c343c; 45 + stroke-width: 1.5px; 46 + stroke: hsla(0, 0%, 50%, 0.333); 47 + stroke-dasharray: 30px; 48 + stroke-dashoffset: -54px; 49 + } 50 + 51 + .popup-overlay { 52 + background: hsla(0, 0%, 0%, 0.1); 53 + } 54 + 55 + .popup-content { 56 + background: #2c343c; 57 + padding: 0.25rem 0.333rem; 58 + font-size: 0.8rem; 59 + border: 0.5px solid hsla(0, 0%, 50%, 0.333); 60 + border-radius: 0.25rem; 61 + } 62 + .filter-pref-popup { 63 + text-align: center; 64 + } 65 + .filter-pref-popup h4 { 66 + margin: 0 0 0.25rem; 67 + font-size: 0.8rem; 68 + color: #bbb; 69 + } 70 + .filter-pref.option { 71 + display: block; 72 + }
+243
atproto-notifications/src/pages/Feed.tsx
··· 1 + import { useCallback, useEffect, useState } from 'react'; 2 + import { ErrorBoundary } from 'react-error-boundary'; 3 + import Popup from 'reactjs-popup'; 4 + import { getNotifications, getSecondary } from '../db'; 5 + import { ButtonGroup } from '../components/Buttons'; 6 + import { NotificationSettings } from '../components/NotificationSettings'; 7 + import { Notification, fallbackRender } from '../components/Notification'; 8 + import { GetJson, PostJson } from '../components/Fetch'; 9 + import psl from 'psl'; 10 + import lexicons from 'lexicons'; 11 + 12 + import './feed.css'; 13 + 14 + function FilterPref({ secondary, value }) { 15 + const [wanted, setWanted] = useState(null); 16 + const [updateCount, setUpdateCount] = useState(0); 17 + const v = `${updateCount}:${wanted}`; 18 + 19 + const setFilterBool = useCallback(val => { 20 + setUpdateCount(n => n + 1); 21 + setWanted(val === 'notify'); 22 + }); 23 + const resetFilter = useCallback(() => { 24 + setUpdateCount(n => n + 1); 25 + setWanted(null); 26 + }); 27 + 28 + const trigger = useCallback(notify => { 29 + let icon = 'โš™', title = 'Default (inherit)'; 30 + if (notify === true) { 31 + icon = '๐Ÿ”Š'; 32 + title = 'Always notify'; 33 + } else if (notify === false) { 34 + icon = '๐Ÿšซ'; 35 + title = 'Notifications muted'; 36 + } 37 + return ( 38 + <div className="filter-pref-trigger" title={title}> 39 + {icon} 40 + </div> 41 + ); 42 + }); 43 + 44 + const renderFilter = useCallback(({ notify }) => ( 45 + <Popup 46 + key="x" 47 + trigger={trigger(notify)} 48 + position={['bottom center']} 49 + closeOnDocumentClick 50 + > 51 + <div className="filter-pref-popup"> 52 + <h4>filter notifications</h4> 53 + <ButtonGroup 54 + options={[ 55 + { val: 'notify', label: 'notify' }, 56 + { val: 'mute' }, 57 + ]} 58 + current={notify === null ? null : notify ? 'notify' : 'mute'} 59 + onChange={setFilterBool} 60 + /> 61 + {notify !== null && ( 62 + <button className="subtle" onClick={resetFilter}>reset</button> 63 + )} 64 + </div> 65 + </Popup> 66 + )); 67 + 68 + const common = { 69 + endpoint: '/notification-filter', 70 + credentials: true, 71 + ok: renderFilter, 72 + loading: () => <>&hellip;</>, 73 + }; 74 + 75 + return updateCount === 0 76 + ? <GetJson key={v} 77 + params={{ selector: secondary, selection: value }} 78 + {...common} 79 + /> 80 + : <PostJson key={v} 81 + data={{ selector: secondary, selection: value, notify: wanted }} 82 + {...common} 83 + />; 84 + } 85 + 86 + function SecondaryFilter({ inc, secondary, current, onUpdate }) { 87 + const [secondaries, setSecondaries] = useState([]); 88 + 89 + useEffect(() => { 90 + (async () => { 91 + const secondaries = await getSecondary(secondary); 92 + secondaries.sort((a, b) => b.unread - a.unread); 93 + setSecondaries(secondaries); 94 + // onUpdate(secondaries[0]?.k); // TODO 95 + })(); 96 + }, [inc, secondary]); 97 + 98 + // reset secondary filter only when leaving due to secondary change 99 + useEffect(() => () => onUpdate(null), [secondary]); 100 + 101 + return ( 102 + <ButtonGroup 103 + options={secondaries.map(({ k, unread, total }) => { 104 + 105 + // blehhhhhhhhhhhh 106 + 107 + let title = k; 108 + let icon; 109 + let app; 110 + let appName; 111 + if (secondary === 'source') { 112 + // TODO: clean up / move this to lexicons package? 113 + let appPrefix; 114 + try { 115 + const [nsid, ...rp] = k.split(':'); 116 + const parts = nsid.split('.'); 117 + const unreversed = parts.toReversed().join('.'); 118 + app = psl.parse(unreversed)?.domain ?? 'unknown'; 119 + appPrefix = app.split('.').toReversed().join('.'); 120 + } catch (e) { 121 + console.error('getting top app failed', e); 122 + } 123 + const lex = lexicons[appPrefix]; 124 + icon = lex?.clients[0]?.icon; 125 + appName = lex?.name; 126 + title = lex?.known_sources[k.slice(app.length + 1)]?.name ?? k; 127 + 128 + } else if (secondary === 'group') { 129 + 130 + let appPrefix; 131 + try { 132 + const [nsid, ...rp] = k.split(':'); 133 + const parts = nsid.split('.'); 134 + const unreversed = parts.toReversed().join('.'); 135 + app = psl.parse(unreversed)?.domain ?? 'unknown'; 136 + appPrefix = app.split('.').toReversed().join('.'); 137 + } catch (e) { 138 + console.error('getting top app failed', e); 139 + } 140 + const lex = lexicons[appPrefix]; 141 + icon = lex?.clients[0]?.icon; 142 + appName = lex?.name; 143 + 144 + } else if (secondary === 'app') { 145 + const appReversed = k.split('.').toReversed().join('.'); 146 + const lex = lexicons[appReversed]; 147 + icon = lex?.clients[0]?.icon; 148 + title = appName = lex?.name; 149 + } 150 + 151 + return { 152 + val: k, 153 + label: ( 154 + <> 155 + {icon && ( 156 + <img className="app-icon" src={icon} title={appName ?? app} alt="" /> 157 + )} 158 + {title} 159 + <small style={{ 160 + display: 'inline-block', 161 + fontSize: '0.6rem', 162 + padding: '0 0.2rem', 163 + color: '#f90', 164 + fontFamily: 'monospace', 165 + verticalAlign: 'top', 166 + }}> 167 + {total >= 30 ? '30+' : total} 168 + </small> 169 + <FilterPref secondary={secondary} value={k} /> 170 + </> 171 + ), 172 + }; 173 + })} 174 + current={current} 175 + onChange={onUpdate} 176 + /> 177 + ); 178 + } 179 + 180 + export function Feed() { 181 + const [secondary, setSecondary] = useState('all'); 182 + const [secondaryFilter, setSecondaryFilter] = useState(null); 183 + 184 + // for now, we just increment a counter when a new notif comes in, which forces a re-render 185 + const [inc, setInc] = useState(0); 186 + useEffect(() => { 187 + const handleMessage = () => setInc(n => n + 1); 188 + const chan = new BroadcastChannel('notif'); 189 + chan.addEventListener('message', handleMessage); 190 + return () => chan.removeEventListener('message', handleMessage); 191 + }); 192 + 193 + // semi-gross way to just pull out all the events so we can see them 194 + // this could be combined with the broadcast thing above, but for now just chain deps 195 + const [feed, setFeed] = useState([]); 196 + useEffect(() => { 197 + (async () => setFeed(await getNotifications(secondary, secondaryFilter)))(); 198 + }, [inc, secondary, secondaryFilter]); 199 + 200 + if (feed.length === 0) { 201 + return 'no notifications loaded'; 202 + } 203 + return ( 204 + <div className="feed"> 205 + <div className="feed-filter-type"> 206 + <h4>Filter by:</h4> 207 + <ButtonGroup 208 + options={[ 209 + {val: 'all', label: 'All'}, 210 + {val: 'app', label: 'App'}, 211 + {val: 'group', label: 'Lexicon group'}, 212 + {val: 'source', label: 'Every source'}, 213 + ]} 214 + current={secondary} 215 + onChange={setSecondary} 216 + /> 217 + </div> 218 + {secondary !== 'all' && ( 219 + <div className="feed-filter-secondary"> 220 + <SecondaryFilter 221 + inc={inc} 222 + secondary={secondary} 223 + current={secondaryFilter} 224 + onUpdate={setSecondaryFilter} 225 + /> 226 + </div> 227 + )} 228 + 229 + <NotificationSettings 230 + secondary={secondary} 231 + secondaryFilter={secondaryFilter} 232 + /> 233 + 234 + <div className="feed-notifications"> 235 + {feed.map(([k, n]) => ( 236 + <ErrorBoundary key={k} fallbackRender={fallbackRender}> 237 + <Notification {...n} /> 238 + </ErrorBoundary> 239 + ))} 240 + </div> 241 + </div> 242 + ); 243 + }
+8 -4
atproto-notifications/src/service-worker.ts
··· 37 37 // TODO: user pref for alt client -> prefer that client's icon 38 38 const lex = lexicons[appPrefix]; 39 39 const icon = lex?.clients[0]?.icon; 40 - const title = lex?.known_sources[source.slice(app.length + 1)] ?? source; 40 + const title = lex?.known_sources[source.slice(app.length + 1)]?.name ?? source; 41 41 const body = `from @${handle} on ${lex?.name ?? app}`; 42 42 43 43 // const tag = 'simple-push-demo-notification-tag'; ··· 72 72 includeUncontrolled: true, 73 73 }); 74 74 75 - // focus the first available existing window/tab 76 - for (const client of clientList) 77 - return await client.focus(); 75 + // focus the first available existing window/tab open at / 76 + for (const client of clientList) { 77 + const pathname = new URL(client.url).pathname; 78 + if (pathname === '/') { 79 + return await client.focus(); 80 + } 81 + } 78 82 79 83 // otherwise open a new tab 80 84 await clients.openWindow('/');
+3
gh-pages.sh
··· 20 20 cp docs/index.html "docs${page}/index.html" 21 21 } 22 22 23 + mkpage /admin 24 + mkpage /early 25 + 23 26 git add docs 24 27 git commit -m 'update build' 25 28 git push
+67
lexicons/atproto.js
··· 1 + import { Client, CredentialManager, ok, simpleFetchHandler } from '@atcute/client'; 2 + import { CompositeDidDocumentResolver, PlcDidDocumentResolver, WebDidDocumentResolver } from '@atcute/identity-resolver'; 3 + 4 + // cleanup needed 5 + 6 + const docResolver = new CompositeDidDocumentResolver({ 7 + methods: { 8 + plc: new PlcDidDocumentResolver(), 9 + web: new WebDidDocumentResolver(), 10 + }, 11 + }); 12 + 13 + async function resolve_did(did) { 14 + return await docResolver.resolve(did); 15 + } 16 + 17 + function pds({ service }) { 18 + if (!service) { 19 + throw new Error('missing service from identity doc'); 20 + } 21 + const { serviceEndpoint } = service[0]; 22 + if (!serviceEndpoint) { 23 + throw new Error('missing serviceEndpoint from identity service array'); 24 + } 25 + return serviceEndpoint; 26 + } 27 + 28 + 29 + async function get_pds_record(endpoint, did, collection, rkey) { 30 + const handler = simpleFetchHandler({ service: endpoint }); 31 + const rpc = new Client({ handler }); 32 + const { ok, data } = await rpc.get('com.atproto.repo.getRecord', { 33 + params: { repo: did, collection, rkey }, 34 + }); 35 + if (!ok) throw new Error('fetching pds record failed'); 36 + return data; 37 + } 38 + 39 + function parse_at_uri(uri) { 40 + let collection, rkey; 41 + if (!uri.startsWith('at://')) { 42 + throw new Error('invalid at-uri: did not start with "at://"'); 43 + } 44 + let remaining = uri.slice('at://'.length); // remove the at:// prefix 45 + remaining = remaining.split('#')[0]; // hash is valid in at-uri but we don't handle them 46 + remaining = remaining.split('?')[0]; // query is valid in at-uri but we don't handle it 47 + const segments = remaining.split('/'); 48 + if (segments.length === 0) { 49 + throw new Error('invalid at-uri: could not find did after "at://"'); 50 + } 51 + const did = segments[0]; 52 + if (segments.length > 1) { 53 + collection = segments[1]; 54 + } 55 + if (segments.length > 2) { 56 + rkey = segments.slice(2).join('/'); // hmm are slashes actually valid in rkey? 57 + } 58 + return { did, collection, rkey }; 59 + } 60 + 61 + export async function getAtUri(atUri) { 62 + const { did, collection, rkey } = parse_at_uri(atUri); 63 + const doc = await resolve_did(did); 64 + const endpoint = pds(doc); 65 + const { value } = await get_pds_record(endpoint, did, collection, rkey); 66 + return value; 67 + }
+106
lexicons/bits.js
··· 1 + import psl from 'psl'; 2 + import { JSONPath } from 'jsonpath-plus'; 3 + import defs from './defs.js'; 4 + import { getAtUri } from './atproto.js'; 5 + 6 + export function getBits(source) { 7 + const [nsid, ...rp] = source.split(':'); 8 + const segments = nsid.split('.'); 9 + const group = segments.slice(0, segments.length - 1).join('.') ?? null; 10 + segments.reverse(); 11 + const app = psl.parse(segments.join('.'))?.domain ?? null; 12 + return { app, group }; 13 + } 14 + 15 + function getAppDefs(source) { 16 + const { app } = getBits(source); 17 + const appPrefix = source.slice(0, app.length); 18 + const appSource = source.slice(app.length + 1); 19 + return [appSource, defs[appPrefix]]; 20 + } 21 + 22 + const uriBits = async uri => { 23 + const bits = uri.slice('at://'.length).split('/'); 24 + // TODO: identifier might be a handle 25 + // TODO: rest might contain stuff after the rkey 26 + const [did, nsid, rkey] = [bits[0], bits[1], bits.slice(2)]; 27 + return [did, nsid, rkey.join('/') || null]; 28 + }; 29 + 30 + export async function getLink(source, source_record, subject) { 31 + // TODO: pass in preferred client 32 + const [appSource, appDefs] = getAppDefs(source); 33 + const appLinks = appDefs?.clients?.[0]?.direct_links; 34 + const linkType = subject.startsWith('did:') ? 'did' : 'at_uri'; 35 + const linkTemplate = appLinks?.[`${linkType}:${appSource}`]; 36 + if (!linkTemplate) return null; 37 + 38 + let link = linkTemplate; 39 + 40 + // 1. sync subs 41 + const [sourceDid, sourceNsid, sourceRkey] = await uriBits(source_record); 42 + link = link 43 + .replaceAll('{source_record.did}', sourceDid) 44 + .replaceAll('{source_record.collection}', sourceNsid) 45 + .replaceAll('{source_record.rkey}', sourceRkey); 46 + if (linkTemplate === 'did') { 47 + link = link.replaceAll('{subject.did}', subject); 48 + } else { 49 + const [subjectDid, subjectNsid, subjectRkey] = await uriBits(subject); 50 + link = link 51 + .replaceAll('{subject.did}', subjectDid) 52 + .replaceAll('{subject.collection}', subjectNsid) 53 + .replaceAll('{subject.rkey}', subjectRkey); 54 + } 55 + 56 + // 2. async lookups 57 + 58 + // do we need to fetch anything from the link subject record? 59 + if (linkType === 'at_uri') { 60 + const subjectMatches = [...link.matchAll(/(\{@subject:(?<path>[^\}]+)\})/g)]; 61 + if (subjectMatches.length > 0) { 62 + const subjectRecord = await getAtUri(subject); 63 + 64 + // do the actual replacements 65 + for (const match of subjectMatches) { 66 + // TODO: JSONPath won't actually cut it once we get $type in 67 + const sub = JSONPath({ 68 + path: `$.${match.groups.path}`, 69 + json: subjectRecord, 70 + })[0]; // TODO: array result? 71 + 72 + link = link.replaceAll(match[0], sub); 73 + } 74 + } 75 + } 76 + 77 + // 2.b TODO: source record lookups if needed 78 + return link; 79 + } 80 + 81 + export async function getContext(source, source_record, subject) { 82 + const [appSource, appDefs] = getAppDefs(source); 83 + const contexts = appDefs?.known_sources?.[appSource]?.context ?? []; 84 + const linkType = subject.startsWith('did:') ? 'did' : 'at_uri'; 85 + 86 + let loaded = []; 87 + for (const ctx of contexts) { 88 + const [o, ...pathstuff] = ctx.split(':'); 89 + if (o !== '@subject') { 90 + throw new Error('only @subject is implemented for context loading so far'); 91 + } 92 + if (linkType !== 'at_uri') { 93 + throw new Error('only at_uris can be used for @subject loading so far'); 94 + } 95 + const path = pathstuff.join(':'); 96 + const subjectRecord = await getAtUri(subject); 97 + // using json path is temporary -- need recordpath convention defined 98 + const found = JSONPath({ 99 + path, 100 + json: subjectRecord, 101 + }); 102 + loaded = loaded.concat(found); // TODO: think about array handling 103 + } 104 + 105 + return loaded; 106 + }
+271
lexicons/defs.js
··· 1 + export default { 2 + 'blue.microcosm': { 3 + name: 'microcosm', 4 + clients: [ 5 + { 6 + app_name: 'Spacedust notifications demo', 7 + canonical: true, 8 + main: 'https://notifications.microcosm.blue', 9 + icon: '/icons/microcosm.png', 10 + }, 11 + ], 12 + known_sources: { 13 + 'test.notification:hello': 'Hello spacedust!', 14 + }, 15 + }, 16 + 'app.bsky': { 17 + name: 'Bluesky', 18 + profile: { 19 + display_name: 'app.bsky.actor.profile:displayName', 20 + avatar: 'app.bsky.actor.profile:avatar', 21 + }, 22 + clients: [ 23 + { 24 + app_name: 'Bluesky Social', 25 + canonical: true, 26 + main: 'https://bsky.app', 27 + icon: '/icons/app.bsky.png', 28 + notifications: 'https://bsky.app/notifications', 29 + direct_links: { 30 + 'at_uri:feed.like:subject.uri': 'https://bsky.app/profile/{subject.did}/post/{subject.rkey}', 31 + 'at_uri:feed.post:reply.parent.uri': 'https://bsky.app/profile/{source_record.did}/post/{source_record.rkey}', 32 + 'at_uri:feed.post:reply.root.uri': 'https://bsky.app/profile/{source_record.did}/post/{source_record.rkey}', 33 + 'at_uri:feed.post:embed.record.uri': 'https://bsky.app/profile/{source_record.did}/post/{source_record.rkey}', 34 + 'at_uri:feed.post:embed.record.record.uri': 'https://bsky.app/profile/{source_record.did}/post/{source_record.rkey}', 35 + 'did:graph.follow:subject': 'https://bsky.app/profile/{source_record.did}', 36 + 'did:feed.post:facets[app.bsky.richtext.facet].features[app.bsky.richtext.facet#mention].did': 'https://bsky.app/profile/{source_record.did}/post/{source_record.rkey}', 37 + }, 38 + }, 39 + { 40 + app_name: 'Deer Social', 41 + main: 'https://deer.social', 42 + notifications: 'https://deer.social/notifications', 43 + direct_links: { 44 + 'at_uri:feed.post': 'https://deer.social/profile/{did}/post/{rkey}', 45 + 'did': 'https://deer.social/profile/{did}', 46 + }, 47 + }, 48 + ], 49 + known_sources: { 50 + 'graph.follow:subject': { 51 + name: 'Follow', 52 + }, 53 + 'graph.verification:subject': { 54 + name: 'Verification', 55 + }, 56 + 'feed.like:subject.uri': { 57 + name: 'Like', 58 + context: ['@subject:text'], 59 + }, 60 + 'feed.like:via.uri': { 61 + name: 'Repost like', 62 + }, 63 + 'feed.post:reply.parent.uri': { 64 + name: 'Reply', 65 + }, 66 + 'feed.post:reply.root.uri': { 67 + name: 'Reply in thread', 68 + }, 69 + 'feed.post:embed.record.uri': { 70 + name: 'Quote', 71 + }, 72 + 'feed.post:embed.record.record.uri': { 73 + name: 'Quote', // with media 74 + }, 75 + 'feed.post:facets[app.bsky.richtext.facet].features[app.bsky.richtext.facet#mention].did': { 76 + name: 'Mention', 77 + }, 78 + 'feed.repost:subject.uri': { 79 + name: 'Repost', 80 + }, 81 + 'feed.repost:via.uri': { 82 + name: 'Repost repost', 83 + }, 84 + }, 85 + torment_sources: { 86 + 'graph.block:subject': null, 87 + 'graph.listitem:subject': null, // we are never ever building listifications 88 + 'graph.listblock:subject': null, // "subscribed to your blocklist?" idk 89 + 'feed.threadgate:hiddenReplies[]': null, 90 + 'feed.postgate:detachedEmbeddingUris[]': null, 91 + }, 92 + }, 93 + 'pub.leaflet': { 94 + name: 'Leaflet', 95 + clients: [ 96 + { 97 + app_name: 'leaflet.pub', 98 + canonical: true, 99 + icon: '/icons/pub.leaflet.jpg', 100 + main: 'https://leaflet.pub/home', 101 + direct_links: { 102 + 'at_uri:graph.subscription:publication': 'https://leaflet.pub/lish/{did}/{rkey}/dashboard', 103 + }, 104 + } 105 + ], 106 + known_sources: { 107 + 'graph.subscription:publication': { 108 + name: 'Subscription', 109 + }, 110 + }, 111 + }, 112 + 'sh.tangled': { 113 + name: 'Tangled', 114 + clients: [ 115 + { 116 + app_name: 'Tangled', 117 + canonical: true, 118 + icon: '/icons/sh.tangled.jpg', 119 + main: 'https://tangled.sh', 120 + direct_links: { 121 + 'at_uri:feed.star:subject': 'https://tangled.sh/{subject.did}/{@subject:name}', 122 + 'did:graph.follow:subject': 'https://tangled.sh/{source_record.did}', 123 + }, 124 + } 125 + ], 126 + known_sources: { 127 + 'feed.star:subject': { 128 + name: 'Star', 129 + context: ['@subject:name'], 130 + }, 131 + 'feed.reaction:subject': { 132 + name: 'Reaction', 133 + }, 134 + 'graph.follow:subject': { 135 + name: 'Follow', 136 + }, 137 + 'actor.profile:pinnedRepositories[]': { 138 + name: 'Pinned repo', 139 + }, 140 + 'repo.issue.comment:issue': { 141 + name: 'Issue comment', 142 + }, 143 + 'repo.issue.comment:owner': { 144 + name: 'Issue comment', 145 + }, 146 + 'repo.issue.comment:repo': { 147 + name: 'Issue comment', 148 + }, 149 + 'repo.pull:targetRepo': { 150 + name: 'Pull', 151 + }, 152 + 'repo.pull.comment:owner': { 153 + name: 'Pull comment', 154 + }, 155 + 'repo.pull.comment:pull': { 156 + name: 'Pull comment', 157 + }, 158 + 'repo.pull.comment:repo': { 159 + name: 'Pull comment', 160 + }, 161 + 'knot.member:subject': { 162 + name: 'Knot member', 163 + }, 164 + 'spindle.member:subject': { 165 + name: 'Spindle member', 166 + }, 167 + }, 168 + }, 169 + 'com.shinolabs': { // TODO: this app isn't exactly tld+1 170 + name: 'Pinksea', 171 + clients: [ 172 + { 173 + app_name: 'Pinksea', 174 + canonical: true, 175 + icon: '/icons/com.shinolabs.jpg', 176 + main: 'https://pinksea.art', 177 + }, 178 + ], 179 + known_sources: { 180 + 'pinksea.oekaki:inResponseTo.uri': { 181 + name: 'Response', 182 + }, 183 + }, 184 + }, 185 + 'place.stream': { 186 + name: 'Streamplace', 187 + clients: [ 188 + { 189 + app_name: 'Streamplace', 190 + canonical: true, 191 + icon: '/icons/place.stream.png', 192 + main: 'https://stream.place', 193 + }, 194 + ], 195 + known_sources: { 196 + 'chat.message:streamer': { 197 + name: 'Message', 198 + }, 199 + 'key:signingKey': { 200 + name: 'Signing key', 201 + }, 202 + }, 203 + }, 204 + 'so.sprk': { 205 + name: 'Spark', 206 + clients: [ 207 + { 208 + app_name: 'Spark', 209 + canonical: true, 210 + icon: '/icons/so.sprk.png', 211 + main: 'https://spark.so', 212 + }, 213 + ], 214 + known_sources: { 215 + 'feed.like:subject.uri': { 216 + name: 'Like', 217 + }, 218 + // it's not actually clear to me if *all* the bsky sources were copied for sprk posts or not 219 + 'feed.post:reply.parent.uri': { 220 + name: 'Reply', 221 + }, 222 + 'feed.post:reply.root.uri': { 223 + name: 'Reply in thread', 224 + }, 225 + 'feed.post:embed.record.uri': { 226 + name: 'Quote', 227 + }, 228 + 'feed.post:embed.record.record.uri': { 229 + name: 'Quote', // with media 230 + }, 231 + 'feed.post:facets[app.bsky.richtext.facet].features[app.bsky.richtext.facet#mention].did': { 232 + name: 'Mention', 233 + }, 234 + }, 235 + }, 236 + 'events.smokesignal': { 237 + name: 'Smoke Signal', 238 + clients: [ 239 + { 240 + app_name: 'Smoke Signal', 241 + canonical: true, 242 + icon: '/icons/events.smokesignal.png', 243 + main: 'https://smokesignal.events', 244 + }, 245 + ], 246 + known_sources: { 247 + 'calendar.rsvp:subject.uri': { 248 + name: 'RSVP', 249 + }, 250 + }, 251 + }, 252 + 'app.popsky': { 253 + name: 'Popsky', 254 + clients: [ 255 + { 256 + app_name: 'Popsky', 257 + canonical: true, 258 + icon: '/icons/app.popsky.png', 259 + main: 'https://popsky.social', 260 + }, 261 + ], 262 + known_sources: { 263 + 'like:subjectUri': { 264 + name: 'Like', 265 + }, 266 + 'comment:subjectUri': { 267 + name: 'Comment', 268 + }, 269 + }, 270 + }, 271 + };
+2 -173
lexicons/index.js
··· 1 - export default { 2 - 'app.bsky': { 3 - name: 'Bluesky', 4 - clients: [ 5 - { 6 - app_name: 'Bluesky Social', 7 - canonical: true, 8 - main: 'https://bsky.app', 9 - icon: '/icons/app.bsky.png', 10 - notifications: 'https://bsky.app/notifications', 11 - direct_links: { 12 - 'at_uri:feed.like:subject.uri': 'https://bsky.app/profile/{subject.did}/post/{subject.rkey}', 13 - 'at_uri:feed.post:reply.parent.uri': 'https://bsky.app/profile/{source_record.did}/post/{source_record.rkey}', 14 - 'at_uri:feed.post:reply.root.uri': 'https://bsky.app/profile/{source_record.did}/post/{source_record.rkey}', 15 - 'at_uri:feed.post:embed.record.uri': 'https://bsky.app/profile/{source_record.did}/post/{source_record.rkey}', 16 - 'at_uri:feed.post:embed.record.record.uri': 'https://bsky.app/profile/{source_record.did}/post/{source_record.rkey}', 17 - 'did:graph.follow:subject': 'https://bsky.app/profile/{source_record.did}', 18 - 'did:feed.post:facets[app.bsky.richtext.facet].features[app.bsky.richtext.facet#mention].did': 'https://bsky.app/profile/{source_record.did}/post/{source_record.rkey}', 19 - }, 20 - }, 21 - { 22 - app_name: 'Deer Social', 23 - main: 'https://deer.social', 24 - notifications: 'https://deer.social/notifications', 25 - direct_links: { 26 - 'at_uri:feed.post': 'https://deer.social/profile/{did}/post/{rkey}', 27 - 'did': 'https://deer.social/profile/{did}', 28 - }, 29 - }, 30 - ], 31 - known_sources: { 32 - 'graph.follow:subject': 'Follow', 33 - 'graph.verification:subject': 'Verification โœ…', 34 - 'feed.like:subject.uri': 'Like ๐Ÿ’œ', 35 - 'feed.like:via.uri': 'Repost like', 36 - 'feed.post:reply.parent.uri': 'Reply', 37 - 'feed.post:reply.root.uri': 'Reply in thread', 38 - 'feed.post:embed.record.uri': 'Quote', 39 - 'feed.post:embed.record.record.uri': 'Quote', // with media 40 - 'feed.post:facets[app.bsky.richtext.facet].features[app.bsky.richtext.facet#mention].did': 'Mention', 41 - 'feed.repost:subject.uri': 'Repost', 42 - 'feed.repost:via.uri': 'Repost repost', 43 - }, 44 - torment_sources: { 45 - 'graph.block:subject': null, 46 - 'graph.listitem:subject': null, // we are never ever building listifications 47 - 'graph.listblock:subject': null, // "subscribed to your blocklist?" idk 48 - 'feed.threadgate:hiddenReplies[]': null, 49 - 'feed.postgate:detachedEmbeddingUris[]': null, 50 - }, 51 - }, 52 - 'pub.leaflet': { 53 - name: 'Leaflet', 54 - clients: [ 55 - { 56 - 'app_name': 'leaflet.pub', 57 - canonical: true, 58 - icon: '/icons/pub.leaflet.jpg', 59 - main: 'https://leaflet.pub/home', 60 - direct_links: { 61 - 'at_uri:graph.subscription:publication': 'https://leaflet.pub/lish/{did}/{rkey}/dashboard', 62 - }, 63 - } 64 - ], 65 - known_sources: { 66 - 'graph.subscription:publication': 'Subscription', 67 - }, 68 - }, 69 - 'sh.tangled': { 70 - name: 'Tangled', 71 - clients: [ 72 - { 73 - 'app_name': 'Tangled', 74 - canonical: true, 75 - icon: '/icons/sh.tangled.jpg', 76 - main: 'https://tangled.sh', 77 - } 78 - ], 79 - known_sources: { 80 - 'feed.star:subject': 'Star', 81 - 'feed.reaction:subject': 'Reaction', 82 - 'graph.follow:subject': 'Follow', 83 - 'actor.profile:pinnedRepositories[]': 'Pinned repo', 84 - 'repo.issue.comment:issue': 'Issue comment', 85 - 'repo.issue.comment:owner': 'Issue comment', 86 - 'repo.issue.comment:repo': 'Issue comment', 87 - 'repo.pull:targetRepo': 'Pull', 88 - 'repo.pull.comment:owner': 'Pull comment', 89 - 'repo.pull.comment:pull': 'Pull comment', 90 - 'repo.pull.comment:repo': 'Pull comment', 91 - 'knot.member:subject': 'Knot member', 92 - 'spindle.member:subject': 'Spindle member', 93 - }, 94 - }, 95 - 'com.shinolabs': { // TODO: this app isn't exactly tld+1 96 - name: 'Pinksea', 97 - clients: [ 98 - { 99 - 'app_name': 'Pinksea', 100 - canonical: true, 101 - icon: '/icons/com.shinolabs.jpg', 102 - main: 'https://pinksea.art', 103 - }, 104 - ], 105 - known_sources: { 106 - 'pinksea.oekaki:inResponseTo.uri': 'Response', 107 - }, 108 - }, 109 - 'place.stream': { 110 - name: 'Streamplace', 111 - clients: [ 112 - { 113 - app_name: 'Streamplace', 114 - canonical: true, 115 - icon: '/icons/place.stream.png', 116 - main: 'https://stream.place', 117 - }, 118 - ], 119 - known_sources: { 120 - 'chat.message:streamer': 'Message', 121 - 'key:signingKey': 'Signing key', 122 - }, 123 - }, 124 - 'so.sprk': { 125 - name: 'Spark', 126 - clients: [ 127 - { 128 - app_name: 'Spark', 129 - canonical: true, 130 - icon: '/icons/so.sprk.png', 131 - main: 'https://spark.so', 132 - }, 133 - ], 134 - known_sources: { 135 - 'feed.like:subject.uri': 'Like', 136 - // it's not actually clear to me if *all* the bsky sources were copied for sprk posts or not 137 - 'feed.post:reply.parent.uri': 'Reply', 138 - 'feed.post:reply.root.uri': 'Reply in thread', 139 - 'feed.post:embed.record.uri': 'Quote', 140 - 'feed.post:embed.record.record.uri': 'Quote', // with media 141 - 'feed.post:facets[app.bsky.richtext.facet].features[app.bsky.richtext.facet#mention].did': 'Mention', 142 - }, 143 - }, 144 - 'events.smokesignal': { 145 - name: 'Smoke Signal', 146 - clients: [ 147 - { 148 - app_name: 'Smoke Signal', 149 - canonical: true, 150 - icon: '/icons/events.smokesignal.png', 151 - main: 'https://smokesignal.events', 152 - }, 153 - ], 154 - known_sources: { 155 - 'calendar.rsvp:subject.uri': 'RSVP', 156 - }, 157 - }, 158 - 'app.popsky': { 159 - name: 'Popsky', 160 - clients: [ 161 - { 162 - app_name: 'Popsky', 163 - canonical: true, 164 - icon: '/icons/app.popsky.png', 165 - main: 'https://popsky.social', 166 - }, 167 - ], 168 - known_sources: { 169 - 'like:subjectUri': 'Like', 170 - 'comment:subjectUri': 'Comment', 171 - }, 172 - }, 173 - }; 1 + export { getBits, getLink, getContext } from './bits.js'; 2 + export { default } from './defs.js';
+157
lexicons/package-lock.json
··· 1 + { 2 + "name": "lexicons", 3 + "version": "0.0.1", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "lexicons", 9 + "version": "0.0.1", 10 + "dependencies": { 11 + "@atcute/client": "^4.0.3", 12 + "@atcute/identity-resolver": "^1.1.3", 13 + "jsonpath-plus": "^10.3.0", 14 + "psl": "^1.15.0" 15 + } 16 + }, 17 + "node_modules/@atcute/client": { 18 + "version": "4.0.3", 19 + "resolved": "https://registry.npmjs.org/@atcute/client/-/client-4.0.3.tgz", 20 + "integrity": "sha512-RIOZWFVLca/HiPAAUDqQPOdOreCxTbL5cb+WUf5yqQOKIu5yEAP3eksinmlLmgIrlr5qVOE7brazUUzaskFCfw==", 21 + "license": "MIT", 22 + "dependencies": { 23 + "@atcute/identity": "^1.0.2", 24 + "@atcute/lexicons": "^1.0.3" 25 + } 26 + }, 27 + "node_modules/@atcute/identity": { 28 + "version": "1.0.3", 29 + "resolved": "https://registry.npmjs.org/@atcute/identity/-/identity-1.0.3.tgz", 30 + "integrity": "sha512-mNMxbKHFGys03A8JXKk0KfMBzdd0vrYMzZZWjpw1nYTs0+ea6bo5S1hwqVUZxHdo1gFHSe/t63jxQIF4yL9aKw==", 31 + "license": "0BSD", 32 + "dependencies": { 33 + "@atcute/lexicons": "^1.0.4", 34 + "@badrap/valita": "^0.4.5" 35 + } 36 + }, 37 + "node_modules/@atcute/identity-resolver": { 38 + "version": "1.1.3", 39 + "resolved": "https://registry.npmjs.org/@atcute/identity-resolver/-/identity-resolver-1.1.3.tgz", 40 + "integrity": "sha512-KZgGgg99CWaV7Df3+h3X/WMrDzTPQVfsaoIVbTNLx2B56BvCL2EmaxPSVw/7BFUJMZHlVU4rtoEB4lyvNyMswA==", 41 + "license": "MIT", 42 + "dependencies": { 43 + "@atcute/lexicons": "^1.0.4", 44 + "@atcute/util-fetch": "^1.0.1", 45 + "@badrap/valita": "^0.4.4" 46 + }, 47 + "peerDependencies": { 48 + "@atcute/identity": "^1.0.0" 49 + } 50 + }, 51 + "node_modules/@atcute/lexicons": { 52 + "version": "1.1.0", 53 + "resolved": "https://registry.npmjs.org/@atcute/lexicons/-/lexicons-1.1.0.tgz", 54 + "integrity": "sha512-LFqwnria78xLYb62Ri/+WwQpUTgZp2DuyolNGIIOV1dpiKhFFFh//nscHMA6IExFLQRqWDs3tTjy7zv0h3sf1Q==", 55 + "license": "0BSD", 56 + "dependencies": { 57 + "esm-env": "^1.2.2" 58 + } 59 + }, 60 + "node_modules/@atcute/util-fetch": { 61 + "version": "1.0.1", 62 + "resolved": "https://registry.npmjs.org/@atcute/util-fetch/-/util-fetch-1.0.1.tgz", 63 + "integrity": "sha512-Clc0E/5ufyGBVfYBUwWNlHONlZCoblSr4Ho50l1LhmRPGB1Wu/AQ9Sz+rsBg7fdaW/auve8ulmwhRhnX2cGRow==", 64 + "license": "MIT", 65 + "dependencies": { 66 + "@badrap/valita": "^0.4.2" 67 + } 68 + }, 69 + "node_modules/@badrap/valita": { 70 + "version": "0.4.5", 71 + "resolved": "https://registry.npmjs.org/@badrap/valita/-/valita-0.4.5.tgz", 72 + "integrity": "sha512-4QwGbuhh/JesHRQj79mO/l37PvJj4l/tlAu7+S1n4h47qwaNpZ0WDvIwUGLYUsdi9uQ5UPpiG9wb1Wm3XUFBUQ==", 73 + "license": "MIT", 74 + "engines": { 75 + "node": ">= 18" 76 + } 77 + }, 78 + "node_modules/@jsep-plugin/assignment": { 79 + "version": "1.3.0", 80 + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", 81 + "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", 82 + "license": "MIT", 83 + "engines": { 84 + "node": ">= 10.16.0" 85 + }, 86 + "peerDependencies": { 87 + "jsep": "^0.4.0||^1.0.0" 88 + } 89 + }, 90 + "node_modules/@jsep-plugin/regex": { 91 + "version": "1.0.4", 92 + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", 93 + "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", 94 + "license": "MIT", 95 + "engines": { 96 + "node": ">= 10.16.0" 97 + }, 98 + "peerDependencies": { 99 + "jsep": "^0.4.0||^1.0.0" 100 + } 101 + }, 102 + "node_modules/esm-env": { 103 + "version": "1.2.2", 104 + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", 105 + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", 106 + "license": "MIT" 107 + }, 108 + "node_modules/jsep": { 109 + "version": "1.4.0", 110 + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", 111 + "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", 112 + "license": "MIT", 113 + "engines": { 114 + "node": ">= 10.16.0" 115 + } 116 + }, 117 + "node_modules/jsonpath-plus": { 118 + "version": "10.3.0", 119 + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz", 120 + "integrity": "sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==", 121 + "license": "MIT", 122 + "dependencies": { 123 + "@jsep-plugin/assignment": "^1.3.0", 124 + "@jsep-plugin/regex": "^1.0.4", 125 + "jsep": "^1.4.0" 126 + }, 127 + "bin": { 128 + "jsonpath": "bin/jsonpath-cli.js", 129 + "jsonpath-plus": "bin/jsonpath-cli.js" 130 + }, 131 + "engines": { 132 + "node": ">=18.0.0" 133 + } 134 + }, 135 + "node_modules/psl": { 136 + "version": "1.15.0", 137 + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", 138 + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", 139 + "license": "MIT", 140 + "dependencies": { 141 + "punycode": "^2.3.1" 142 + }, 143 + "funding": { 144 + "url": "https://github.com/sponsors/lupomontero" 145 + } 146 + }, 147 + "node_modules/punycode": { 148 + "version": "2.3.1", 149 + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", 150 + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", 151 + "license": "MIT", 152 + "engines": { 153 + "node": ">=6" 154 + } 155 + } 156 + } 157 + }
+7 -1
lexicons/package.json
··· 5 5 "main": "index.js", 6 6 "scripts": {}, 7 7 "author": "", 8 - "type": "module" 8 + "type": "module", 9 + "dependencies": { 10 + "@atcute/client": "^4.0.3", 11 + "@atcute/identity-resolver": "^1.1.3", 12 + "jsonpath-plus": "^10.3.0", 13 + "psl": "^1.15.0" 14 + } 9 15 }
+24
live-embed/.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?
+69
live-embed/README.md
··· 1 + # React + TypeScript + Vite 2 + 3 + This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 + 5 + Currently, two official plugins are available: 6 + 7 + - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh 8 + - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 + 10 + ## Expanding the ESLint configuration 11 + 12 + If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: 13 + 14 + ```js 15 + export default tseslint.config([ 16 + globalIgnores(['dist']), 17 + { 18 + files: ['**/*.{ts,tsx}'], 19 + extends: [ 20 + // Other configs... 21 + 22 + // Remove tseslint.configs.recommended and replace with this 23 + ...tseslint.configs.recommendedTypeChecked, 24 + // Alternatively, use this for stricter rules 25 + ...tseslint.configs.strictTypeChecked, 26 + // Optionally, add this for stylistic rules 27 + ...tseslint.configs.stylisticTypeChecked, 28 + 29 + // Other configs... 30 + ], 31 + languageOptions: { 32 + parserOptions: { 33 + project: ['./tsconfig.node.json', './tsconfig.app.json'], 34 + tsconfigRootDir: import.meta.dirname, 35 + }, 36 + // other options... 37 + }, 38 + }, 39 + ]) 40 + ``` 41 + 42 + You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: 43 + 44 + ```js 45 + // eslint.config.js 46 + import reactX from 'eslint-plugin-react-x' 47 + import reactDom from 'eslint-plugin-react-dom' 48 + 49 + export default tseslint.config([ 50 + globalIgnores(['dist']), 51 + { 52 + files: ['**/*.{ts,tsx}'], 53 + extends: [ 54 + // Other configs... 55 + // Enable lint rules for React 56 + reactX.configs['recommended-typescript'], 57 + // Enable lint rules for React DOM 58 + reactDom.configs.recommended, 59 + ], 60 + languageOptions: { 61 + parserOptions: { 62 + project: ['./tsconfig.node.json', './tsconfig.app.json'], 63 + tsconfigRootDir: import.meta.dirname, 64 + }, 65 + // other options... 66 + }, 67 + }, 68 + ]) 69 + ```
+23
live-embed/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 { globalIgnores } from 'eslint/config' 7 + 8 + export default tseslint.config([ 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 + ])
+12
live-embed/index.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <title>zero bluesky live-updating post rendering</title> 7 + </head> 8 + <body> 9 + <div id="root"></div> 10 + <script type="module" src="/src/main.tsx"></script> 11 + </body> 12 + </html>
+3444
live-embed/package-lock.json
··· 1 + { 2 + "name": "live-embed", 3 + "version": "0.0.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "live-embed", 9 + "version": "0.0.0", 10 + "dependencies": { 11 + "@atcute/client": "^4.0.3", 12 + "@atcute/identity-resolver": "^1.1.3", 13 + "react": "^19.1.0", 14 + "react-dom": "^19.1.0" 15 + }, 16 + "devDependencies": { 17 + "@eslint/js": "^9.30.1", 18 + "@types/react": "^19.1.8", 19 + "@types/react-dom": "^19.1.6", 20 + "@vitejs/plugin-react": "^4.6.0", 21 + "eslint": "^9.30.1", 22 + "eslint-plugin-react-hooks": "^5.2.0", 23 + "eslint-plugin-react-refresh": "^0.4.20", 24 + "globals": "^16.3.0", 25 + "typescript": "~5.8.3", 26 + "typescript-eslint": "^8.35.1", 27 + "vite": "^7.0.4" 28 + } 29 + }, 30 + "node_modules/@ampproject/remapping": { 31 + "version": "2.3.0", 32 + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", 33 + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", 34 + "dev": true, 35 + "license": "Apache-2.0", 36 + "dependencies": { 37 + "@jridgewell/gen-mapping": "^0.3.5", 38 + "@jridgewell/trace-mapping": "^0.3.24" 39 + }, 40 + "engines": { 41 + "node": ">=6.0.0" 42 + } 43 + }, 44 + "node_modules/@atcute/client": { 45 + "version": "4.0.3", 46 + "resolved": "https://registry.npmjs.org/@atcute/client/-/client-4.0.3.tgz", 47 + "integrity": "sha512-RIOZWFVLca/HiPAAUDqQPOdOreCxTbL5cb+WUf5yqQOKIu5yEAP3eksinmlLmgIrlr5qVOE7brazUUzaskFCfw==", 48 + "license": "MIT", 49 + "dependencies": { 50 + "@atcute/identity": "^1.0.2", 51 + "@atcute/lexicons": "^1.0.3" 52 + } 53 + }, 54 + "node_modules/@atcute/identity": { 55 + "version": "1.0.3", 56 + "resolved": "https://registry.npmjs.org/@atcute/identity/-/identity-1.0.3.tgz", 57 + "integrity": "sha512-mNMxbKHFGys03A8JXKk0KfMBzdd0vrYMzZZWjpw1nYTs0+ea6bo5S1hwqVUZxHdo1gFHSe/t63jxQIF4yL9aKw==", 58 + "license": "0BSD", 59 + "dependencies": { 60 + "@atcute/lexicons": "^1.0.4", 61 + "@badrap/valita": "^0.4.5" 62 + } 63 + }, 64 + "node_modules/@atcute/identity-resolver": { 65 + "version": "1.1.3", 66 + "resolved": "https://registry.npmjs.org/@atcute/identity-resolver/-/identity-resolver-1.1.3.tgz", 67 + "integrity": "sha512-KZgGgg99CWaV7Df3+h3X/WMrDzTPQVfsaoIVbTNLx2B56BvCL2EmaxPSVw/7BFUJMZHlVU4rtoEB4lyvNyMswA==", 68 + "license": "MIT", 69 + "dependencies": { 70 + "@atcute/lexicons": "^1.0.4", 71 + "@atcute/util-fetch": "^1.0.1", 72 + "@badrap/valita": "^0.4.4" 73 + }, 74 + "peerDependencies": { 75 + "@atcute/identity": "^1.0.0" 76 + } 77 + }, 78 + "node_modules/@atcute/lexicons": { 79 + "version": "1.1.0", 80 + "resolved": "https://registry.npmjs.org/@atcute/lexicons/-/lexicons-1.1.0.tgz", 81 + "integrity": "sha512-LFqwnria78xLYb62Ri/+WwQpUTgZp2DuyolNGIIOV1dpiKhFFFh//nscHMA6IExFLQRqWDs3tTjy7zv0h3sf1Q==", 82 + "license": "0BSD", 83 + "dependencies": { 84 + "esm-env": "^1.2.2" 85 + } 86 + }, 87 + "node_modules/@atcute/util-fetch": { 88 + "version": "1.0.1", 89 + "resolved": "https://registry.npmjs.org/@atcute/util-fetch/-/util-fetch-1.0.1.tgz", 90 + "integrity": "sha512-Clc0E/5ufyGBVfYBUwWNlHONlZCoblSr4Ho50l1LhmRPGB1Wu/AQ9Sz+rsBg7fdaW/auve8ulmwhRhnX2cGRow==", 91 + "license": "MIT", 92 + "dependencies": { 93 + "@badrap/valita": "^0.4.2" 94 + } 95 + }, 96 + "node_modules/@babel/code-frame": { 97 + "version": "7.27.1", 98 + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", 99 + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", 100 + "dev": true, 101 + "license": "MIT", 102 + "dependencies": { 103 + "@babel/helper-validator-identifier": "^7.27.1", 104 + "js-tokens": "^4.0.0", 105 + "picocolors": "^1.1.1" 106 + }, 107 + "engines": { 108 + "node": ">=6.9.0" 109 + } 110 + }, 111 + "node_modules/@babel/compat-data": { 112 + "version": "7.28.0", 113 + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", 114 + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", 115 + "dev": true, 116 + "license": "MIT", 117 + "engines": { 118 + "node": ">=6.9.0" 119 + } 120 + }, 121 + "node_modules/@babel/core": { 122 + "version": "7.28.0", 123 + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", 124 + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", 125 + "dev": true, 126 + "license": "MIT", 127 + "dependencies": { 128 + "@ampproject/remapping": "^2.2.0", 129 + "@babel/code-frame": "^7.27.1", 130 + "@babel/generator": "^7.28.0", 131 + "@babel/helper-compilation-targets": "^7.27.2", 132 + "@babel/helper-module-transforms": "^7.27.3", 133 + "@babel/helpers": "^7.27.6", 134 + "@babel/parser": "^7.28.0", 135 + "@babel/template": "^7.27.2", 136 + "@babel/traverse": "^7.28.0", 137 + "@babel/types": "^7.28.0", 138 + "convert-source-map": "^2.0.0", 139 + "debug": "^4.1.0", 140 + "gensync": "^1.0.0-beta.2", 141 + "json5": "^2.2.3", 142 + "semver": "^6.3.1" 143 + }, 144 + "engines": { 145 + "node": ">=6.9.0" 146 + }, 147 + "funding": { 148 + "type": "opencollective", 149 + "url": "https://opencollective.com/babel" 150 + } 151 + }, 152 + "node_modules/@babel/generator": { 153 + "version": "7.28.0", 154 + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", 155 + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", 156 + "dev": true, 157 + "license": "MIT", 158 + "dependencies": { 159 + "@babel/parser": "^7.28.0", 160 + "@babel/types": "^7.28.0", 161 + "@jridgewell/gen-mapping": "^0.3.12", 162 + "@jridgewell/trace-mapping": "^0.3.28", 163 + "jsesc": "^3.0.2" 164 + }, 165 + "engines": { 166 + "node": ">=6.9.0" 167 + } 168 + }, 169 + "node_modules/@babel/helper-compilation-targets": { 170 + "version": "7.27.2", 171 + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", 172 + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", 173 + "dev": true, 174 + "license": "MIT", 175 + "dependencies": { 176 + "@babel/compat-data": "^7.27.2", 177 + "@babel/helper-validator-option": "^7.27.1", 178 + "browserslist": "^4.24.0", 179 + "lru-cache": "^5.1.1", 180 + "semver": "^6.3.1" 181 + }, 182 + "engines": { 183 + "node": ">=6.9.0" 184 + } 185 + }, 186 + "node_modules/@babel/helper-globals": { 187 + "version": "7.28.0", 188 + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", 189 + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", 190 + "dev": true, 191 + "license": "MIT", 192 + "engines": { 193 + "node": ">=6.9.0" 194 + } 195 + }, 196 + "node_modules/@babel/helper-module-imports": { 197 + "version": "7.27.1", 198 + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", 199 + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", 200 + "dev": true, 201 + "license": "MIT", 202 + "dependencies": { 203 + "@babel/traverse": "^7.27.1", 204 + "@babel/types": "^7.27.1" 205 + }, 206 + "engines": { 207 + "node": ">=6.9.0" 208 + } 209 + }, 210 + "node_modules/@babel/helper-module-transforms": { 211 + "version": "7.27.3", 212 + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", 213 + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", 214 + "dev": true, 215 + "license": "MIT", 216 + "dependencies": { 217 + "@babel/helper-module-imports": "^7.27.1", 218 + "@babel/helper-validator-identifier": "^7.27.1", 219 + "@babel/traverse": "^7.27.3" 220 + }, 221 + "engines": { 222 + "node": ">=6.9.0" 223 + }, 224 + "peerDependencies": { 225 + "@babel/core": "^7.0.0" 226 + } 227 + }, 228 + "node_modules/@babel/helper-plugin-utils": { 229 + "version": "7.27.1", 230 + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", 231 + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", 232 + "dev": true, 233 + "license": "MIT", 234 + "engines": { 235 + "node": ">=6.9.0" 236 + } 237 + }, 238 + "node_modules/@babel/helper-string-parser": { 239 + "version": "7.27.1", 240 + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", 241 + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", 242 + "dev": true, 243 + "license": "MIT", 244 + "engines": { 245 + "node": ">=6.9.0" 246 + } 247 + }, 248 + "node_modules/@babel/helper-validator-identifier": { 249 + "version": "7.27.1", 250 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", 251 + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", 252 + "dev": true, 253 + "license": "MIT", 254 + "engines": { 255 + "node": ">=6.9.0" 256 + } 257 + }, 258 + "node_modules/@babel/helper-validator-option": { 259 + "version": "7.27.1", 260 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", 261 + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", 262 + "dev": true, 263 + "license": "MIT", 264 + "engines": { 265 + "node": ">=6.9.0" 266 + } 267 + }, 268 + "node_modules/@babel/helpers": { 269 + "version": "7.28.2", 270 + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", 271 + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", 272 + "dev": true, 273 + "license": "MIT", 274 + "dependencies": { 275 + "@babel/template": "^7.27.2", 276 + "@babel/types": "^7.28.2" 277 + }, 278 + "engines": { 279 + "node": ">=6.9.0" 280 + } 281 + }, 282 + "node_modules/@babel/parser": { 283 + "version": "7.28.0", 284 + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", 285 + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", 286 + "dev": true, 287 + "license": "MIT", 288 + "dependencies": { 289 + "@babel/types": "^7.28.0" 290 + }, 291 + "bin": { 292 + "parser": "bin/babel-parser.js" 293 + }, 294 + "engines": { 295 + "node": ">=6.0.0" 296 + } 297 + }, 298 + "node_modules/@babel/plugin-transform-react-jsx-self": { 299 + "version": "7.27.1", 300 + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", 301 + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", 302 + "dev": true, 303 + "license": "MIT", 304 + "dependencies": { 305 + "@babel/helper-plugin-utils": "^7.27.1" 306 + }, 307 + "engines": { 308 + "node": ">=6.9.0" 309 + }, 310 + "peerDependencies": { 311 + "@babel/core": "^7.0.0-0" 312 + } 313 + }, 314 + "node_modules/@babel/plugin-transform-react-jsx-source": { 315 + "version": "7.27.1", 316 + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", 317 + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", 318 + "dev": true, 319 + "license": "MIT", 320 + "dependencies": { 321 + "@babel/helper-plugin-utils": "^7.27.1" 322 + }, 323 + "engines": { 324 + "node": ">=6.9.0" 325 + }, 326 + "peerDependencies": { 327 + "@babel/core": "^7.0.0-0" 328 + } 329 + }, 330 + "node_modules/@babel/template": { 331 + "version": "7.27.2", 332 + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", 333 + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", 334 + "dev": true, 335 + "license": "MIT", 336 + "dependencies": { 337 + "@babel/code-frame": "^7.27.1", 338 + "@babel/parser": "^7.27.2", 339 + "@babel/types": "^7.27.1" 340 + }, 341 + "engines": { 342 + "node": ">=6.9.0" 343 + } 344 + }, 345 + "node_modules/@babel/traverse": { 346 + "version": "7.28.0", 347 + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", 348 + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", 349 + "dev": true, 350 + "license": "MIT", 351 + "dependencies": { 352 + "@babel/code-frame": "^7.27.1", 353 + "@babel/generator": "^7.28.0", 354 + "@babel/helper-globals": "^7.28.0", 355 + "@babel/parser": "^7.28.0", 356 + "@babel/template": "^7.27.2", 357 + "@babel/types": "^7.28.0", 358 + "debug": "^4.3.1" 359 + }, 360 + "engines": { 361 + "node": ">=6.9.0" 362 + } 363 + }, 364 + "node_modules/@babel/types": { 365 + "version": "7.28.2", 366 + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", 367 + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", 368 + "dev": true, 369 + "license": "MIT", 370 + "dependencies": { 371 + "@babel/helper-string-parser": "^7.27.1", 372 + "@babel/helper-validator-identifier": "^7.27.1" 373 + }, 374 + "engines": { 375 + "node": ">=6.9.0" 376 + } 377 + }, 378 + "node_modules/@badrap/valita": { 379 + "version": "0.4.5", 380 + "resolved": "https://registry.npmjs.org/@badrap/valita/-/valita-0.4.5.tgz", 381 + "integrity": "sha512-4QwGbuhh/JesHRQj79mO/l37PvJj4l/tlAu7+S1n4h47qwaNpZ0WDvIwUGLYUsdi9uQ5UPpiG9wb1Wm3XUFBUQ==", 382 + "license": "MIT", 383 + "engines": { 384 + "node": ">= 18" 385 + } 386 + }, 387 + "node_modules/@esbuild/aix-ppc64": { 388 + "version": "0.25.8", 389 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", 390 + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", 391 + "cpu": [ 392 + "ppc64" 393 + ], 394 + "dev": true, 395 + "license": "MIT", 396 + "optional": true, 397 + "os": [ 398 + "aix" 399 + ], 400 + "engines": { 401 + "node": ">=18" 402 + } 403 + }, 404 + "node_modules/@esbuild/android-arm": { 405 + "version": "0.25.8", 406 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", 407 + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", 408 + "cpu": [ 409 + "arm" 410 + ], 411 + "dev": true, 412 + "license": "MIT", 413 + "optional": true, 414 + "os": [ 415 + "android" 416 + ], 417 + "engines": { 418 + "node": ">=18" 419 + } 420 + }, 421 + "node_modules/@esbuild/android-arm64": { 422 + "version": "0.25.8", 423 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", 424 + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", 425 + "cpu": [ 426 + "arm64" 427 + ], 428 + "dev": true, 429 + "license": "MIT", 430 + "optional": true, 431 + "os": [ 432 + "android" 433 + ], 434 + "engines": { 435 + "node": ">=18" 436 + } 437 + }, 438 + "node_modules/@esbuild/android-x64": { 439 + "version": "0.25.8", 440 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", 441 + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", 442 + "cpu": [ 443 + "x64" 444 + ], 445 + "dev": true, 446 + "license": "MIT", 447 + "optional": true, 448 + "os": [ 449 + "android" 450 + ], 451 + "engines": { 452 + "node": ">=18" 453 + } 454 + }, 455 + "node_modules/@esbuild/darwin-arm64": { 456 + "version": "0.25.8", 457 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", 458 + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", 459 + "cpu": [ 460 + "arm64" 461 + ], 462 + "dev": true, 463 + "license": "MIT", 464 + "optional": true, 465 + "os": [ 466 + "darwin" 467 + ], 468 + "engines": { 469 + "node": ">=18" 470 + } 471 + }, 472 + "node_modules/@esbuild/darwin-x64": { 473 + "version": "0.25.8", 474 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", 475 + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", 476 + "cpu": [ 477 + "x64" 478 + ], 479 + "dev": true, 480 + "license": "MIT", 481 + "optional": true, 482 + "os": [ 483 + "darwin" 484 + ], 485 + "engines": { 486 + "node": ">=18" 487 + } 488 + }, 489 + "node_modules/@esbuild/freebsd-arm64": { 490 + "version": "0.25.8", 491 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", 492 + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", 493 + "cpu": [ 494 + "arm64" 495 + ], 496 + "dev": true, 497 + "license": "MIT", 498 + "optional": true, 499 + "os": [ 500 + "freebsd" 501 + ], 502 + "engines": { 503 + "node": ">=18" 504 + } 505 + }, 506 + "node_modules/@esbuild/freebsd-x64": { 507 + "version": "0.25.8", 508 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", 509 + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", 510 + "cpu": [ 511 + "x64" 512 + ], 513 + "dev": true, 514 + "license": "MIT", 515 + "optional": true, 516 + "os": [ 517 + "freebsd" 518 + ], 519 + "engines": { 520 + "node": ">=18" 521 + } 522 + }, 523 + "node_modules/@esbuild/linux-arm": { 524 + "version": "0.25.8", 525 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", 526 + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", 527 + "cpu": [ 528 + "arm" 529 + ], 530 + "dev": true, 531 + "license": "MIT", 532 + "optional": true, 533 + "os": [ 534 + "linux" 535 + ], 536 + "engines": { 537 + "node": ">=18" 538 + } 539 + }, 540 + "node_modules/@esbuild/linux-arm64": { 541 + "version": "0.25.8", 542 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", 543 + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", 544 + "cpu": [ 545 + "arm64" 546 + ], 547 + "dev": true, 548 + "license": "MIT", 549 + "optional": true, 550 + "os": [ 551 + "linux" 552 + ], 553 + "engines": { 554 + "node": ">=18" 555 + } 556 + }, 557 + "node_modules/@esbuild/linux-ia32": { 558 + "version": "0.25.8", 559 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", 560 + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", 561 + "cpu": [ 562 + "ia32" 563 + ], 564 + "dev": true, 565 + "license": "MIT", 566 + "optional": true, 567 + "os": [ 568 + "linux" 569 + ], 570 + "engines": { 571 + "node": ">=18" 572 + } 573 + }, 574 + "node_modules/@esbuild/linux-loong64": { 575 + "version": "0.25.8", 576 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", 577 + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", 578 + "cpu": [ 579 + "loong64" 580 + ], 581 + "dev": true, 582 + "license": "MIT", 583 + "optional": true, 584 + "os": [ 585 + "linux" 586 + ], 587 + "engines": { 588 + "node": ">=18" 589 + } 590 + }, 591 + "node_modules/@esbuild/linux-mips64el": { 592 + "version": "0.25.8", 593 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", 594 + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", 595 + "cpu": [ 596 + "mips64el" 597 + ], 598 + "dev": true, 599 + "license": "MIT", 600 + "optional": true, 601 + "os": [ 602 + "linux" 603 + ], 604 + "engines": { 605 + "node": ">=18" 606 + } 607 + }, 608 + "node_modules/@esbuild/linux-ppc64": { 609 + "version": "0.25.8", 610 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", 611 + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", 612 + "cpu": [ 613 + "ppc64" 614 + ], 615 + "dev": true, 616 + "license": "MIT", 617 + "optional": true, 618 + "os": [ 619 + "linux" 620 + ], 621 + "engines": { 622 + "node": ">=18" 623 + } 624 + }, 625 + "node_modules/@esbuild/linux-riscv64": { 626 + "version": "0.25.8", 627 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", 628 + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", 629 + "cpu": [ 630 + "riscv64" 631 + ], 632 + "dev": true, 633 + "license": "MIT", 634 + "optional": true, 635 + "os": [ 636 + "linux" 637 + ], 638 + "engines": { 639 + "node": ">=18" 640 + } 641 + }, 642 + "node_modules/@esbuild/linux-s390x": { 643 + "version": "0.25.8", 644 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", 645 + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", 646 + "cpu": [ 647 + "s390x" 648 + ], 649 + "dev": true, 650 + "license": "MIT", 651 + "optional": true, 652 + "os": [ 653 + "linux" 654 + ], 655 + "engines": { 656 + "node": ">=18" 657 + } 658 + }, 659 + "node_modules/@esbuild/linux-x64": { 660 + "version": "0.25.8", 661 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", 662 + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", 663 + "cpu": [ 664 + "x64" 665 + ], 666 + "dev": true, 667 + "license": "MIT", 668 + "optional": true, 669 + "os": [ 670 + "linux" 671 + ], 672 + "engines": { 673 + "node": ">=18" 674 + } 675 + }, 676 + "node_modules/@esbuild/netbsd-arm64": { 677 + "version": "0.25.8", 678 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", 679 + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", 680 + "cpu": [ 681 + "arm64" 682 + ], 683 + "dev": true, 684 + "license": "MIT", 685 + "optional": true, 686 + "os": [ 687 + "netbsd" 688 + ], 689 + "engines": { 690 + "node": ">=18" 691 + } 692 + }, 693 + "node_modules/@esbuild/netbsd-x64": { 694 + "version": "0.25.8", 695 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", 696 + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", 697 + "cpu": [ 698 + "x64" 699 + ], 700 + "dev": true, 701 + "license": "MIT", 702 + "optional": true, 703 + "os": [ 704 + "netbsd" 705 + ], 706 + "engines": { 707 + "node": ">=18" 708 + } 709 + }, 710 + "node_modules/@esbuild/openbsd-arm64": { 711 + "version": "0.25.8", 712 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", 713 + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", 714 + "cpu": [ 715 + "arm64" 716 + ], 717 + "dev": true, 718 + "license": "MIT", 719 + "optional": true, 720 + "os": [ 721 + "openbsd" 722 + ], 723 + "engines": { 724 + "node": ">=18" 725 + } 726 + }, 727 + "node_modules/@esbuild/openbsd-x64": { 728 + "version": "0.25.8", 729 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", 730 + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", 731 + "cpu": [ 732 + "x64" 733 + ], 734 + "dev": true, 735 + "license": "MIT", 736 + "optional": true, 737 + "os": [ 738 + "openbsd" 739 + ], 740 + "engines": { 741 + "node": ">=18" 742 + } 743 + }, 744 + "node_modules/@esbuild/openharmony-arm64": { 745 + "version": "0.25.8", 746 + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", 747 + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", 748 + "cpu": [ 749 + "arm64" 750 + ], 751 + "dev": true, 752 + "license": "MIT", 753 + "optional": true, 754 + "os": [ 755 + "openharmony" 756 + ], 757 + "engines": { 758 + "node": ">=18" 759 + } 760 + }, 761 + "node_modules/@esbuild/sunos-x64": { 762 + "version": "0.25.8", 763 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", 764 + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", 765 + "cpu": [ 766 + "x64" 767 + ], 768 + "dev": true, 769 + "license": "MIT", 770 + "optional": true, 771 + "os": [ 772 + "sunos" 773 + ], 774 + "engines": { 775 + "node": ">=18" 776 + } 777 + }, 778 + "node_modules/@esbuild/win32-arm64": { 779 + "version": "0.25.8", 780 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", 781 + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", 782 + "cpu": [ 783 + "arm64" 784 + ], 785 + "dev": true, 786 + "license": "MIT", 787 + "optional": true, 788 + "os": [ 789 + "win32" 790 + ], 791 + "engines": { 792 + "node": ">=18" 793 + } 794 + }, 795 + "node_modules/@esbuild/win32-ia32": { 796 + "version": "0.25.8", 797 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", 798 + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", 799 + "cpu": [ 800 + "ia32" 801 + ], 802 + "dev": true, 803 + "license": "MIT", 804 + "optional": true, 805 + "os": [ 806 + "win32" 807 + ], 808 + "engines": { 809 + "node": ">=18" 810 + } 811 + }, 812 + "node_modules/@esbuild/win32-x64": { 813 + "version": "0.25.8", 814 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", 815 + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", 816 + "cpu": [ 817 + "x64" 818 + ], 819 + "dev": true, 820 + "license": "MIT", 821 + "optional": true, 822 + "os": [ 823 + "win32" 824 + ], 825 + "engines": { 826 + "node": ">=18" 827 + } 828 + }, 829 + "node_modules/@eslint-community/eslint-utils": { 830 + "version": "4.7.0", 831 + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", 832 + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", 833 + "dev": true, 834 + "license": "MIT", 835 + "dependencies": { 836 + "eslint-visitor-keys": "^3.4.3" 837 + }, 838 + "engines": { 839 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 840 + }, 841 + "funding": { 842 + "url": "https://opencollective.com/eslint" 843 + }, 844 + "peerDependencies": { 845 + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" 846 + } 847 + }, 848 + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { 849 + "version": "3.4.3", 850 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", 851 + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", 852 + "dev": true, 853 + "license": "Apache-2.0", 854 + "engines": { 855 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 856 + }, 857 + "funding": { 858 + "url": "https://opencollective.com/eslint" 859 + } 860 + }, 861 + "node_modules/@eslint-community/regexpp": { 862 + "version": "4.12.1", 863 + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", 864 + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", 865 + "dev": true, 866 + "license": "MIT", 867 + "engines": { 868 + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" 869 + } 870 + }, 871 + "node_modules/@eslint/config-array": { 872 + "version": "0.21.0", 873 + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", 874 + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", 875 + "dev": true, 876 + "license": "Apache-2.0", 877 + "dependencies": { 878 + "@eslint/object-schema": "^2.1.6", 879 + "debug": "^4.3.1", 880 + "minimatch": "^3.1.2" 881 + }, 882 + "engines": { 883 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 884 + } 885 + }, 886 + "node_modules/@eslint/config-helpers": { 887 + "version": "0.3.0", 888 + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", 889 + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", 890 + "dev": true, 891 + "license": "Apache-2.0", 892 + "engines": { 893 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 894 + } 895 + }, 896 + "node_modules/@eslint/core": { 897 + "version": "0.15.1", 898 + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", 899 + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", 900 + "dev": true, 901 + "license": "Apache-2.0", 902 + "dependencies": { 903 + "@types/json-schema": "^7.0.15" 904 + }, 905 + "engines": { 906 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 907 + } 908 + }, 909 + "node_modules/@eslint/eslintrc": { 910 + "version": "3.3.1", 911 + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", 912 + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", 913 + "dev": true, 914 + "license": "MIT", 915 + "dependencies": { 916 + "ajv": "^6.12.4", 917 + "debug": "^4.3.2", 918 + "espree": "^10.0.1", 919 + "globals": "^14.0.0", 920 + "ignore": "^5.2.0", 921 + "import-fresh": "^3.2.1", 922 + "js-yaml": "^4.1.0", 923 + "minimatch": "^3.1.2", 924 + "strip-json-comments": "^3.1.1" 925 + }, 926 + "engines": { 927 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 928 + }, 929 + "funding": { 930 + "url": "https://opencollective.com/eslint" 931 + } 932 + }, 933 + "node_modules/@eslint/eslintrc/node_modules/globals": { 934 + "version": "14.0.0", 935 + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", 936 + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", 937 + "dev": true, 938 + "license": "MIT", 939 + "engines": { 940 + "node": ">=18" 941 + }, 942 + "funding": { 943 + "url": "https://github.com/sponsors/sindresorhus" 944 + } 945 + }, 946 + "node_modules/@eslint/js": { 947 + "version": "9.31.0", 948 + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", 949 + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", 950 + "dev": true, 951 + "license": "MIT", 952 + "engines": { 953 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 954 + }, 955 + "funding": { 956 + "url": "https://eslint.org/donate" 957 + } 958 + }, 959 + "node_modules/@eslint/object-schema": { 960 + "version": "2.1.6", 961 + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", 962 + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", 963 + "dev": true, 964 + "license": "Apache-2.0", 965 + "engines": { 966 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 967 + } 968 + }, 969 + "node_modules/@eslint/plugin-kit": { 970 + "version": "0.3.4", 971 + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", 972 + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", 973 + "dev": true, 974 + "license": "Apache-2.0", 975 + "dependencies": { 976 + "@eslint/core": "^0.15.1", 977 + "levn": "^0.4.1" 978 + }, 979 + "engines": { 980 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 981 + } 982 + }, 983 + "node_modules/@humanfs/core": { 984 + "version": "0.19.1", 985 + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", 986 + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", 987 + "dev": true, 988 + "license": "Apache-2.0", 989 + "engines": { 990 + "node": ">=18.18.0" 991 + } 992 + }, 993 + "node_modules/@humanfs/node": { 994 + "version": "0.16.6", 995 + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", 996 + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", 997 + "dev": true, 998 + "license": "Apache-2.0", 999 + "dependencies": { 1000 + "@humanfs/core": "^0.19.1", 1001 + "@humanwhocodes/retry": "^0.3.0" 1002 + }, 1003 + "engines": { 1004 + "node": ">=18.18.0" 1005 + } 1006 + }, 1007 + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { 1008 + "version": "0.3.1", 1009 + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", 1010 + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", 1011 + "dev": true, 1012 + "license": "Apache-2.0", 1013 + "engines": { 1014 + "node": ">=18.18" 1015 + }, 1016 + "funding": { 1017 + "type": "github", 1018 + "url": "https://github.com/sponsors/nzakas" 1019 + } 1020 + }, 1021 + "node_modules/@humanwhocodes/module-importer": { 1022 + "version": "1.0.1", 1023 + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", 1024 + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", 1025 + "dev": true, 1026 + "license": "Apache-2.0", 1027 + "engines": { 1028 + "node": ">=12.22" 1029 + }, 1030 + "funding": { 1031 + "type": "github", 1032 + "url": "https://github.com/sponsors/nzakas" 1033 + } 1034 + }, 1035 + "node_modules/@humanwhocodes/retry": { 1036 + "version": "0.4.3", 1037 + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", 1038 + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", 1039 + "dev": true, 1040 + "license": "Apache-2.0", 1041 + "engines": { 1042 + "node": ">=18.18" 1043 + }, 1044 + "funding": { 1045 + "type": "github", 1046 + "url": "https://github.com/sponsors/nzakas" 1047 + } 1048 + }, 1049 + "node_modules/@jridgewell/gen-mapping": { 1050 + "version": "0.3.12", 1051 + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", 1052 + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", 1053 + "dev": true, 1054 + "license": "MIT", 1055 + "dependencies": { 1056 + "@jridgewell/sourcemap-codec": "^1.5.0", 1057 + "@jridgewell/trace-mapping": "^0.3.24" 1058 + } 1059 + }, 1060 + "node_modules/@jridgewell/resolve-uri": { 1061 + "version": "3.1.2", 1062 + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 1063 + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 1064 + "dev": true, 1065 + "license": "MIT", 1066 + "engines": { 1067 + "node": ">=6.0.0" 1068 + } 1069 + }, 1070 + "node_modules/@jridgewell/sourcemap-codec": { 1071 + "version": "1.5.4", 1072 + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", 1073 + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", 1074 + "dev": true, 1075 + "license": "MIT" 1076 + }, 1077 + "node_modules/@jridgewell/trace-mapping": { 1078 + "version": "0.3.29", 1079 + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", 1080 + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", 1081 + "dev": true, 1082 + "license": "MIT", 1083 + "dependencies": { 1084 + "@jridgewell/resolve-uri": "^3.1.0", 1085 + "@jridgewell/sourcemap-codec": "^1.4.14" 1086 + } 1087 + }, 1088 + "node_modules/@nodelib/fs.scandir": { 1089 + "version": "2.1.5", 1090 + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", 1091 + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", 1092 + "dev": true, 1093 + "license": "MIT", 1094 + "dependencies": { 1095 + "@nodelib/fs.stat": "2.0.5", 1096 + "run-parallel": "^1.1.9" 1097 + }, 1098 + "engines": { 1099 + "node": ">= 8" 1100 + } 1101 + }, 1102 + "node_modules/@nodelib/fs.stat": { 1103 + "version": "2.0.5", 1104 + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", 1105 + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", 1106 + "dev": true, 1107 + "license": "MIT", 1108 + "engines": { 1109 + "node": ">= 8" 1110 + } 1111 + }, 1112 + "node_modules/@nodelib/fs.walk": { 1113 + "version": "1.2.8", 1114 + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", 1115 + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", 1116 + "dev": true, 1117 + "license": "MIT", 1118 + "dependencies": { 1119 + "@nodelib/fs.scandir": "2.1.5", 1120 + "fastq": "^1.6.0" 1121 + }, 1122 + "engines": { 1123 + "node": ">= 8" 1124 + } 1125 + }, 1126 + "node_modules/@rolldown/pluginutils": { 1127 + "version": "1.0.0-beta.27", 1128 + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", 1129 + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", 1130 + "dev": true, 1131 + "license": "MIT" 1132 + }, 1133 + "node_modules/@rollup/rollup-android-arm-eabi": { 1134 + "version": "4.45.1", 1135 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz", 1136 + "integrity": "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==", 1137 + "cpu": [ 1138 + "arm" 1139 + ], 1140 + "dev": true, 1141 + "license": "MIT", 1142 + "optional": true, 1143 + "os": [ 1144 + "android" 1145 + ] 1146 + }, 1147 + "node_modules/@rollup/rollup-android-arm64": { 1148 + "version": "4.45.1", 1149 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.1.tgz", 1150 + "integrity": "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==", 1151 + "cpu": [ 1152 + "arm64" 1153 + ], 1154 + "dev": true, 1155 + "license": "MIT", 1156 + "optional": true, 1157 + "os": [ 1158 + "android" 1159 + ] 1160 + }, 1161 + "node_modules/@rollup/rollup-darwin-arm64": { 1162 + "version": "4.45.1", 1163 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.1.tgz", 1164 + "integrity": "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==", 1165 + "cpu": [ 1166 + "arm64" 1167 + ], 1168 + "dev": true, 1169 + "license": "MIT", 1170 + "optional": true, 1171 + "os": [ 1172 + "darwin" 1173 + ] 1174 + }, 1175 + "node_modules/@rollup/rollup-darwin-x64": { 1176 + "version": "4.45.1", 1177 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.1.tgz", 1178 + "integrity": "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==", 1179 + "cpu": [ 1180 + "x64" 1181 + ], 1182 + "dev": true, 1183 + "license": "MIT", 1184 + "optional": true, 1185 + "os": [ 1186 + "darwin" 1187 + ] 1188 + }, 1189 + "node_modules/@rollup/rollup-freebsd-arm64": { 1190 + "version": "4.45.1", 1191 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.1.tgz", 1192 + "integrity": "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==", 1193 + "cpu": [ 1194 + "arm64" 1195 + ], 1196 + "dev": true, 1197 + "license": "MIT", 1198 + "optional": true, 1199 + "os": [ 1200 + "freebsd" 1201 + ] 1202 + }, 1203 + "node_modules/@rollup/rollup-freebsd-x64": { 1204 + "version": "4.45.1", 1205 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.1.tgz", 1206 + "integrity": "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==", 1207 + "cpu": [ 1208 + "x64" 1209 + ], 1210 + "dev": true, 1211 + "license": "MIT", 1212 + "optional": true, 1213 + "os": [ 1214 + "freebsd" 1215 + ] 1216 + }, 1217 + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { 1218 + "version": "4.45.1", 1219 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.1.tgz", 1220 + "integrity": "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==", 1221 + "cpu": [ 1222 + "arm" 1223 + ], 1224 + "dev": true, 1225 + "license": "MIT", 1226 + "optional": true, 1227 + "os": [ 1228 + "linux" 1229 + ] 1230 + }, 1231 + "node_modules/@rollup/rollup-linux-arm-musleabihf": { 1232 + "version": "4.45.1", 1233 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.1.tgz", 1234 + "integrity": "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==", 1235 + "cpu": [ 1236 + "arm" 1237 + ], 1238 + "dev": true, 1239 + "license": "MIT", 1240 + "optional": true, 1241 + "os": [ 1242 + "linux" 1243 + ] 1244 + }, 1245 + "node_modules/@rollup/rollup-linux-arm64-gnu": { 1246 + "version": "4.45.1", 1247 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.1.tgz", 1248 + "integrity": "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==", 1249 + "cpu": [ 1250 + "arm64" 1251 + ], 1252 + "dev": true, 1253 + "license": "MIT", 1254 + "optional": true, 1255 + "os": [ 1256 + "linux" 1257 + ] 1258 + }, 1259 + "node_modules/@rollup/rollup-linux-arm64-musl": { 1260 + "version": "4.45.1", 1261 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.1.tgz", 1262 + "integrity": "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==", 1263 + "cpu": [ 1264 + "arm64" 1265 + ], 1266 + "dev": true, 1267 + "license": "MIT", 1268 + "optional": true, 1269 + "os": [ 1270 + "linux" 1271 + ] 1272 + }, 1273 + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { 1274 + "version": "4.45.1", 1275 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.1.tgz", 1276 + "integrity": "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==", 1277 + "cpu": [ 1278 + "loong64" 1279 + ], 1280 + "dev": true, 1281 + "license": "MIT", 1282 + "optional": true, 1283 + "os": [ 1284 + "linux" 1285 + ] 1286 + }, 1287 + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { 1288 + "version": "4.45.1", 1289 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.1.tgz", 1290 + "integrity": "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==", 1291 + "cpu": [ 1292 + "ppc64" 1293 + ], 1294 + "dev": true, 1295 + "license": "MIT", 1296 + "optional": true, 1297 + "os": [ 1298 + "linux" 1299 + ] 1300 + }, 1301 + "node_modules/@rollup/rollup-linux-riscv64-gnu": { 1302 + "version": "4.45.1", 1303 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.1.tgz", 1304 + "integrity": "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==", 1305 + "cpu": [ 1306 + "riscv64" 1307 + ], 1308 + "dev": true, 1309 + "license": "MIT", 1310 + "optional": true, 1311 + "os": [ 1312 + "linux" 1313 + ] 1314 + }, 1315 + "node_modules/@rollup/rollup-linux-riscv64-musl": { 1316 + "version": "4.45.1", 1317 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.1.tgz", 1318 + "integrity": "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==", 1319 + "cpu": [ 1320 + "riscv64" 1321 + ], 1322 + "dev": true, 1323 + "license": "MIT", 1324 + "optional": true, 1325 + "os": [ 1326 + "linux" 1327 + ] 1328 + }, 1329 + "node_modules/@rollup/rollup-linux-s390x-gnu": { 1330 + "version": "4.45.1", 1331 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.1.tgz", 1332 + "integrity": "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==", 1333 + "cpu": [ 1334 + "s390x" 1335 + ], 1336 + "dev": true, 1337 + "license": "MIT", 1338 + "optional": true, 1339 + "os": [ 1340 + "linux" 1341 + ] 1342 + }, 1343 + "node_modules/@rollup/rollup-linux-x64-gnu": { 1344 + "version": "4.45.1", 1345 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.1.tgz", 1346 + "integrity": "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==", 1347 + "cpu": [ 1348 + "x64" 1349 + ], 1350 + "dev": true, 1351 + "license": "MIT", 1352 + "optional": true, 1353 + "os": [ 1354 + "linux" 1355 + ] 1356 + }, 1357 + "node_modules/@rollup/rollup-linux-x64-musl": { 1358 + "version": "4.45.1", 1359 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.1.tgz", 1360 + "integrity": "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==", 1361 + "cpu": [ 1362 + "x64" 1363 + ], 1364 + "dev": true, 1365 + "license": "MIT", 1366 + "optional": true, 1367 + "os": [ 1368 + "linux" 1369 + ] 1370 + }, 1371 + "node_modules/@rollup/rollup-win32-arm64-msvc": { 1372 + "version": "4.45.1", 1373 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.1.tgz", 1374 + "integrity": "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==", 1375 + "cpu": [ 1376 + "arm64" 1377 + ], 1378 + "dev": true, 1379 + "license": "MIT", 1380 + "optional": true, 1381 + "os": [ 1382 + "win32" 1383 + ] 1384 + }, 1385 + "node_modules/@rollup/rollup-win32-ia32-msvc": { 1386 + "version": "4.45.1", 1387 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.1.tgz", 1388 + "integrity": "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==", 1389 + "cpu": [ 1390 + "ia32" 1391 + ], 1392 + "dev": true, 1393 + "license": "MIT", 1394 + "optional": true, 1395 + "os": [ 1396 + "win32" 1397 + ] 1398 + }, 1399 + "node_modules/@rollup/rollup-win32-x64-msvc": { 1400 + "version": "4.45.1", 1401 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.1.tgz", 1402 + "integrity": "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==", 1403 + "cpu": [ 1404 + "x64" 1405 + ], 1406 + "dev": true, 1407 + "license": "MIT", 1408 + "optional": true, 1409 + "os": [ 1410 + "win32" 1411 + ] 1412 + }, 1413 + "node_modules/@types/babel__core": { 1414 + "version": "7.20.5", 1415 + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", 1416 + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", 1417 + "dev": true, 1418 + "license": "MIT", 1419 + "dependencies": { 1420 + "@babel/parser": "^7.20.7", 1421 + "@babel/types": "^7.20.7", 1422 + "@types/babel__generator": "*", 1423 + "@types/babel__template": "*", 1424 + "@types/babel__traverse": "*" 1425 + } 1426 + }, 1427 + "node_modules/@types/babel__generator": { 1428 + "version": "7.27.0", 1429 + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", 1430 + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", 1431 + "dev": true, 1432 + "license": "MIT", 1433 + "dependencies": { 1434 + "@babel/types": "^7.0.0" 1435 + } 1436 + }, 1437 + "node_modules/@types/babel__template": { 1438 + "version": "7.4.4", 1439 + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", 1440 + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", 1441 + "dev": true, 1442 + "license": "MIT", 1443 + "dependencies": { 1444 + "@babel/parser": "^7.1.0", 1445 + "@babel/types": "^7.0.0" 1446 + } 1447 + }, 1448 + "node_modules/@types/babel__traverse": { 1449 + "version": "7.20.7", 1450 + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", 1451 + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", 1452 + "dev": true, 1453 + "license": "MIT", 1454 + "dependencies": { 1455 + "@babel/types": "^7.20.7" 1456 + } 1457 + }, 1458 + "node_modules/@types/estree": { 1459 + "version": "1.0.8", 1460 + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", 1461 + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", 1462 + "dev": true, 1463 + "license": "MIT" 1464 + }, 1465 + "node_modules/@types/json-schema": { 1466 + "version": "7.0.15", 1467 + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", 1468 + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", 1469 + "dev": true, 1470 + "license": "MIT" 1471 + }, 1472 + "node_modules/@types/react": { 1473 + "version": "19.1.8", 1474 + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", 1475 + "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", 1476 + "dev": true, 1477 + "license": "MIT", 1478 + "dependencies": { 1479 + "csstype": "^3.0.2" 1480 + } 1481 + }, 1482 + "node_modules/@types/react-dom": { 1483 + "version": "19.1.6", 1484 + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", 1485 + "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", 1486 + "dev": true, 1487 + "license": "MIT", 1488 + "peerDependencies": { 1489 + "@types/react": "^19.0.0" 1490 + } 1491 + }, 1492 + "node_modules/@typescript-eslint/eslint-plugin": { 1493 + "version": "8.38.0", 1494 + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", 1495 + "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==", 1496 + "dev": true, 1497 + "license": "MIT", 1498 + "dependencies": { 1499 + "@eslint-community/regexpp": "^4.10.0", 1500 + "@typescript-eslint/scope-manager": "8.38.0", 1501 + "@typescript-eslint/type-utils": "8.38.0", 1502 + "@typescript-eslint/utils": "8.38.0", 1503 + "@typescript-eslint/visitor-keys": "8.38.0", 1504 + "graphemer": "^1.4.0", 1505 + "ignore": "^7.0.0", 1506 + "natural-compare": "^1.4.0", 1507 + "ts-api-utils": "^2.1.0" 1508 + }, 1509 + "engines": { 1510 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1511 + }, 1512 + "funding": { 1513 + "type": "opencollective", 1514 + "url": "https://opencollective.com/typescript-eslint" 1515 + }, 1516 + "peerDependencies": { 1517 + "@typescript-eslint/parser": "^8.38.0", 1518 + "eslint": "^8.57.0 || ^9.0.0", 1519 + "typescript": ">=4.8.4 <5.9.0" 1520 + } 1521 + }, 1522 + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { 1523 + "version": "7.0.5", 1524 + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", 1525 + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", 1526 + "dev": true, 1527 + "license": "MIT", 1528 + "engines": { 1529 + "node": ">= 4" 1530 + } 1531 + }, 1532 + "node_modules/@typescript-eslint/parser": { 1533 + "version": "8.38.0", 1534 + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz", 1535 + "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", 1536 + "dev": true, 1537 + "license": "MIT", 1538 + "dependencies": { 1539 + "@typescript-eslint/scope-manager": "8.38.0", 1540 + "@typescript-eslint/types": "8.38.0", 1541 + "@typescript-eslint/typescript-estree": "8.38.0", 1542 + "@typescript-eslint/visitor-keys": "8.38.0", 1543 + "debug": "^4.3.4" 1544 + }, 1545 + "engines": { 1546 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1547 + }, 1548 + "funding": { 1549 + "type": "opencollective", 1550 + "url": "https://opencollective.com/typescript-eslint" 1551 + }, 1552 + "peerDependencies": { 1553 + "eslint": "^8.57.0 || ^9.0.0", 1554 + "typescript": ">=4.8.4 <5.9.0" 1555 + } 1556 + }, 1557 + "node_modules/@typescript-eslint/project-service": { 1558 + "version": "8.38.0", 1559 + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz", 1560 + "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==", 1561 + "dev": true, 1562 + "license": "MIT", 1563 + "dependencies": { 1564 + "@typescript-eslint/tsconfig-utils": "^8.38.0", 1565 + "@typescript-eslint/types": "^8.38.0", 1566 + "debug": "^4.3.4" 1567 + }, 1568 + "engines": { 1569 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1570 + }, 1571 + "funding": { 1572 + "type": "opencollective", 1573 + "url": "https://opencollective.com/typescript-eslint" 1574 + }, 1575 + "peerDependencies": { 1576 + "typescript": ">=4.8.4 <5.9.0" 1577 + } 1578 + }, 1579 + "node_modules/@typescript-eslint/scope-manager": { 1580 + "version": "8.38.0", 1581 + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz", 1582 + "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==", 1583 + "dev": true, 1584 + "license": "MIT", 1585 + "dependencies": { 1586 + "@typescript-eslint/types": "8.38.0", 1587 + "@typescript-eslint/visitor-keys": "8.38.0" 1588 + }, 1589 + "engines": { 1590 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1591 + }, 1592 + "funding": { 1593 + "type": "opencollective", 1594 + "url": "https://opencollective.com/typescript-eslint" 1595 + } 1596 + }, 1597 + "node_modules/@typescript-eslint/tsconfig-utils": { 1598 + "version": "8.38.0", 1599 + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz", 1600 + "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==", 1601 + "dev": true, 1602 + "license": "MIT", 1603 + "engines": { 1604 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1605 + }, 1606 + "funding": { 1607 + "type": "opencollective", 1608 + "url": "https://opencollective.com/typescript-eslint" 1609 + }, 1610 + "peerDependencies": { 1611 + "typescript": ">=4.8.4 <5.9.0" 1612 + } 1613 + }, 1614 + "node_modules/@typescript-eslint/type-utils": { 1615 + "version": "8.38.0", 1616 + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz", 1617 + "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==", 1618 + "dev": true, 1619 + "license": "MIT", 1620 + "dependencies": { 1621 + "@typescript-eslint/types": "8.38.0", 1622 + "@typescript-eslint/typescript-estree": "8.38.0", 1623 + "@typescript-eslint/utils": "8.38.0", 1624 + "debug": "^4.3.4", 1625 + "ts-api-utils": "^2.1.0" 1626 + }, 1627 + "engines": { 1628 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1629 + }, 1630 + "funding": { 1631 + "type": "opencollective", 1632 + "url": "https://opencollective.com/typescript-eslint" 1633 + }, 1634 + "peerDependencies": { 1635 + "eslint": "^8.57.0 || ^9.0.0", 1636 + "typescript": ">=4.8.4 <5.9.0" 1637 + } 1638 + }, 1639 + "node_modules/@typescript-eslint/types": { 1640 + "version": "8.38.0", 1641 + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz", 1642 + "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==", 1643 + "dev": true, 1644 + "license": "MIT", 1645 + "engines": { 1646 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1647 + }, 1648 + "funding": { 1649 + "type": "opencollective", 1650 + "url": "https://opencollective.com/typescript-eslint" 1651 + } 1652 + }, 1653 + "node_modules/@typescript-eslint/typescript-estree": { 1654 + "version": "8.38.0", 1655 + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz", 1656 + "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==", 1657 + "dev": true, 1658 + "license": "MIT", 1659 + "dependencies": { 1660 + "@typescript-eslint/project-service": "8.38.0", 1661 + "@typescript-eslint/tsconfig-utils": "8.38.0", 1662 + "@typescript-eslint/types": "8.38.0", 1663 + "@typescript-eslint/visitor-keys": "8.38.0", 1664 + "debug": "^4.3.4", 1665 + "fast-glob": "^3.3.2", 1666 + "is-glob": "^4.0.3", 1667 + "minimatch": "^9.0.4", 1668 + "semver": "^7.6.0", 1669 + "ts-api-utils": "^2.1.0" 1670 + }, 1671 + "engines": { 1672 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1673 + }, 1674 + "funding": { 1675 + "type": "opencollective", 1676 + "url": "https://opencollective.com/typescript-eslint" 1677 + }, 1678 + "peerDependencies": { 1679 + "typescript": ">=4.8.4 <5.9.0" 1680 + } 1681 + }, 1682 + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { 1683 + "version": "2.0.2", 1684 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", 1685 + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", 1686 + "dev": true, 1687 + "license": "MIT", 1688 + "dependencies": { 1689 + "balanced-match": "^1.0.0" 1690 + } 1691 + }, 1692 + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { 1693 + "version": "9.0.5", 1694 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", 1695 + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", 1696 + "dev": true, 1697 + "license": "ISC", 1698 + "dependencies": { 1699 + "brace-expansion": "^2.0.1" 1700 + }, 1701 + "engines": { 1702 + "node": ">=16 || 14 >=14.17" 1703 + }, 1704 + "funding": { 1705 + "url": "https://github.com/sponsors/isaacs" 1706 + } 1707 + }, 1708 + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { 1709 + "version": "7.7.2", 1710 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", 1711 + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", 1712 + "dev": true, 1713 + "license": "ISC", 1714 + "bin": { 1715 + "semver": "bin/semver.js" 1716 + }, 1717 + "engines": { 1718 + "node": ">=10" 1719 + } 1720 + }, 1721 + "node_modules/@typescript-eslint/utils": { 1722 + "version": "8.38.0", 1723 + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz", 1724 + "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==", 1725 + "dev": true, 1726 + "license": "MIT", 1727 + "dependencies": { 1728 + "@eslint-community/eslint-utils": "^4.7.0", 1729 + "@typescript-eslint/scope-manager": "8.38.0", 1730 + "@typescript-eslint/types": "8.38.0", 1731 + "@typescript-eslint/typescript-estree": "8.38.0" 1732 + }, 1733 + "engines": { 1734 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1735 + }, 1736 + "funding": { 1737 + "type": "opencollective", 1738 + "url": "https://opencollective.com/typescript-eslint" 1739 + }, 1740 + "peerDependencies": { 1741 + "eslint": "^8.57.0 || ^9.0.0", 1742 + "typescript": ">=4.8.4 <5.9.0" 1743 + } 1744 + }, 1745 + "node_modules/@typescript-eslint/visitor-keys": { 1746 + "version": "8.38.0", 1747 + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz", 1748 + "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==", 1749 + "dev": true, 1750 + "license": "MIT", 1751 + "dependencies": { 1752 + "@typescript-eslint/types": "8.38.0", 1753 + "eslint-visitor-keys": "^4.2.1" 1754 + }, 1755 + "engines": { 1756 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1757 + }, 1758 + "funding": { 1759 + "type": "opencollective", 1760 + "url": "https://opencollective.com/typescript-eslint" 1761 + } 1762 + }, 1763 + "node_modules/@vitejs/plugin-react": { 1764 + "version": "4.7.0", 1765 + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", 1766 + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", 1767 + "dev": true, 1768 + "license": "MIT", 1769 + "dependencies": { 1770 + "@babel/core": "^7.28.0", 1771 + "@babel/plugin-transform-react-jsx-self": "^7.27.1", 1772 + "@babel/plugin-transform-react-jsx-source": "^7.27.1", 1773 + "@rolldown/pluginutils": "1.0.0-beta.27", 1774 + "@types/babel__core": "^7.20.5", 1775 + "react-refresh": "^0.17.0" 1776 + }, 1777 + "engines": { 1778 + "node": "^14.18.0 || >=16.0.0" 1779 + }, 1780 + "peerDependencies": { 1781 + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" 1782 + } 1783 + }, 1784 + "node_modules/acorn": { 1785 + "version": "8.15.0", 1786 + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", 1787 + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", 1788 + "dev": true, 1789 + "license": "MIT", 1790 + "bin": { 1791 + "acorn": "bin/acorn" 1792 + }, 1793 + "engines": { 1794 + "node": ">=0.4.0" 1795 + } 1796 + }, 1797 + "node_modules/acorn-jsx": { 1798 + "version": "5.3.2", 1799 + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", 1800 + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", 1801 + "dev": true, 1802 + "license": "MIT", 1803 + "peerDependencies": { 1804 + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" 1805 + } 1806 + }, 1807 + "node_modules/ajv": { 1808 + "version": "6.12.6", 1809 + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 1810 + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 1811 + "dev": true, 1812 + "license": "MIT", 1813 + "dependencies": { 1814 + "fast-deep-equal": "^3.1.1", 1815 + "fast-json-stable-stringify": "^2.0.0", 1816 + "json-schema-traverse": "^0.4.1", 1817 + "uri-js": "^4.2.2" 1818 + }, 1819 + "funding": { 1820 + "type": "github", 1821 + "url": "https://github.com/sponsors/epoberezkin" 1822 + } 1823 + }, 1824 + "node_modules/ansi-styles": { 1825 + "version": "4.3.0", 1826 + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 1827 + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 1828 + "dev": true, 1829 + "license": "MIT", 1830 + "dependencies": { 1831 + "color-convert": "^2.0.1" 1832 + }, 1833 + "engines": { 1834 + "node": ">=8" 1835 + }, 1836 + "funding": { 1837 + "url": "https://github.com/chalk/ansi-styles?sponsor=1" 1838 + } 1839 + }, 1840 + "node_modules/argparse": { 1841 + "version": "2.0.1", 1842 + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 1843 + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 1844 + "dev": true, 1845 + "license": "Python-2.0" 1846 + }, 1847 + "node_modules/balanced-match": { 1848 + "version": "1.0.2", 1849 + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 1850 + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 1851 + "dev": true, 1852 + "license": "MIT" 1853 + }, 1854 + "node_modules/brace-expansion": { 1855 + "version": "1.1.12", 1856 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", 1857 + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", 1858 + "dev": true, 1859 + "license": "MIT", 1860 + "dependencies": { 1861 + "balanced-match": "^1.0.0", 1862 + "concat-map": "0.0.1" 1863 + } 1864 + }, 1865 + "node_modules/braces": { 1866 + "version": "3.0.3", 1867 + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", 1868 + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", 1869 + "dev": true, 1870 + "license": "MIT", 1871 + "dependencies": { 1872 + "fill-range": "^7.1.1" 1873 + }, 1874 + "engines": { 1875 + "node": ">=8" 1876 + } 1877 + }, 1878 + "node_modules/browserslist": { 1879 + "version": "4.25.1", 1880 + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", 1881 + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", 1882 + "dev": true, 1883 + "funding": [ 1884 + { 1885 + "type": "opencollective", 1886 + "url": "https://opencollective.com/browserslist" 1887 + }, 1888 + { 1889 + "type": "tidelift", 1890 + "url": "https://tidelift.com/funding/github/npm/browserslist" 1891 + }, 1892 + { 1893 + "type": "github", 1894 + "url": "https://github.com/sponsors/ai" 1895 + } 1896 + ], 1897 + "license": "MIT", 1898 + "dependencies": { 1899 + "caniuse-lite": "^1.0.30001726", 1900 + "electron-to-chromium": "^1.5.173", 1901 + "node-releases": "^2.0.19", 1902 + "update-browserslist-db": "^1.1.3" 1903 + }, 1904 + "bin": { 1905 + "browserslist": "cli.js" 1906 + }, 1907 + "engines": { 1908 + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" 1909 + } 1910 + }, 1911 + "node_modules/callsites": { 1912 + "version": "3.1.0", 1913 + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 1914 + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 1915 + "dev": true, 1916 + "license": "MIT", 1917 + "engines": { 1918 + "node": ">=6" 1919 + } 1920 + }, 1921 + "node_modules/caniuse-lite": { 1922 + "version": "1.0.30001727", 1923 + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", 1924 + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", 1925 + "dev": true, 1926 + "funding": [ 1927 + { 1928 + "type": "opencollective", 1929 + "url": "https://opencollective.com/browserslist" 1930 + }, 1931 + { 1932 + "type": "tidelift", 1933 + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" 1934 + }, 1935 + { 1936 + "type": "github", 1937 + "url": "https://github.com/sponsors/ai" 1938 + } 1939 + ], 1940 + "license": "CC-BY-4.0" 1941 + }, 1942 + "node_modules/chalk": { 1943 + "version": "4.1.2", 1944 + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 1945 + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 1946 + "dev": true, 1947 + "license": "MIT", 1948 + "dependencies": { 1949 + "ansi-styles": "^4.1.0", 1950 + "supports-color": "^7.1.0" 1951 + }, 1952 + "engines": { 1953 + "node": ">=10" 1954 + }, 1955 + "funding": { 1956 + "url": "https://github.com/chalk/chalk?sponsor=1" 1957 + } 1958 + }, 1959 + "node_modules/color-convert": { 1960 + "version": "2.0.1", 1961 + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 1962 + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 1963 + "dev": true, 1964 + "license": "MIT", 1965 + "dependencies": { 1966 + "color-name": "~1.1.4" 1967 + }, 1968 + "engines": { 1969 + "node": ">=7.0.0" 1970 + } 1971 + }, 1972 + "node_modules/color-name": { 1973 + "version": "1.1.4", 1974 + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 1975 + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 1976 + "dev": true, 1977 + "license": "MIT" 1978 + }, 1979 + "node_modules/concat-map": { 1980 + "version": "0.0.1", 1981 + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 1982 + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 1983 + "dev": true, 1984 + "license": "MIT" 1985 + }, 1986 + "node_modules/convert-source-map": { 1987 + "version": "2.0.0", 1988 + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", 1989 + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", 1990 + "dev": true, 1991 + "license": "MIT" 1992 + }, 1993 + "node_modules/cross-spawn": { 1994 + "version": "7.0.6", 1995 + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 1996 + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 1997 + "dev": true, 1998 + "license": "MIT", 1999 + "dependencies": { 2000 + "path-key": "^3.1.0", 2001 + "shebang-command": "^2.0.0", 2002 + "which": "^2.0.1" 2003 + }, 2004 + "engines": { 2005 + "node": ">= 8" 2006 + } 2007 + }, 2008 + "node_modules/csstype": { 2009 + "version": "3.1.3", 2010 + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", 2011 + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", 2012 + "dev": true, 2013 + "license": "MIT" 2014 + }, 2015 + "node_modules/debug": { 2016 + "version": "4.4.1", 2017 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", 2018 + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", 2019 + "dev": true, 2020 + "license": "MIT", 2021 + "dependencies": { 2022 + "ms": "^2.1.3" 2023 + }, 2024 + "engines": { 2025 + "node": ">=6.0" 2026 + }, 2027 + "peerDependenciesMeta": { 2028 + "supports-color": { 2029 + "optional": true 2030 + } 2031 + } 2032 + }, 2033 + "node_modules/deep-is": { 2034 + "version": "0.1.4", 2035 + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 2036 + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", 2037 + "dev": true, 2038 + "license": "MIT" 2039 + }, 2040 + "node_modules/electron-to-chromium": { 2041 + "version": "1.5.190", 2042 + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.190.tgz", 2043 + "integrity": "sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==", 2044 + "dev": true, 2045 + "license": "ISC" 2046 + }, 2047 + "node_modules/esbuild": { 2048 + "version": "0.25.8", 2049 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", 2050 + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", 2051 + "dev": true, 2052 + "hasInstallScript": true, 2053 + "license": "MIT", 2054 + "bin": { 2055 + "esbuild": "bin/esbuild" 2056 + }, 2057 + "engines": { 2058 + "node": ">=18" 2059 + }, 2060 + "optionalDependencies": { 2061 + "@esbuild/aix-ppc64": "0.25.8", 2062 + "@esbuild/android-arm": "0.25.8", 2063 + "@esbuild/android-arm64": "0.25.8", 2064 + "@esbuild/android-x64": "0.25.8", 2065 + "@esbuild/darwin-arm64": "0.25.8", 2066 + "@esbuild/darwin-x64": "0.25.8", 2067 + "@esbuild/freebsd-arm64": "0.25.8", 2068 + "@esbuild/freebsd-x64": "0.25.8", 2069 + "@esbuild/linux-arm": "0.25.8", 2070 + "@esbuild/linux-arm64": "0.25.8", 2071 + "@esbuild/linux-ia32": "0.25.8", 2072 + "@esbuild/linux-loong64": "0.25.8", 2073 + "@esbuild/linux-mips64el": "0.25.8", 2074 + "@esbuild/linux-ppc64": "0.25.8", 2075 + "@esbuild/linux-riscv64": "0.25.8", 2076 + "@esbuild/linux-s390x": "0.25.8", 2077 + "@esbuild/linux-x64": "0.25.8", 2078 + "@esbuild/netbsd-arm64": "0.25.8", 2079 + "@esbuild/netbsd-x64": "0.25.8", 2080 + "@esbuild/openbsd-arm64": "0.25.8", 2081 + "@esbuild/openbsd-x64": "0.25.8", 2082 + "@esbuild/openharmony-arm64": "0.25.8", 2083 + "@esbuild/sunos-x64": "0.25.8", 2084 + "@esbuild/win32-arm64": "0.25.8", 2085 + "@esbuild/win32-ia32": "0.25.8", 2086 + "@esbuild/win32-x64": "0.25.8" 2087 + } 2088 + }, 2089 + "node_modules/escalade": { 2090 + "version": "3.2.0", 2091 + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", 2092 + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", 2093 + "dev": true, 2094 + "license": "MIT", 2095 + "engines": { 2096 + "node": ">=6" 2097 + } 2098 + }, 2099 + "node_modules/escape-string-regexp": { 2100 + "version": "4.0.0", 2101 + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 2102 + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 2103 + "dev": true, 2104 + "license": "MIT", 2105 + "engines": { 2106 + "node": ">=10" 2107 + }, 2108 + "funding": { 2109 + "url": "https://github.com/sponsors/sindresorhus" 2110 + } 2111 + }, 2112 + "node_modules/eslint": { 2113 + "version": "9.31.0", 2114 + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", 2115 + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", 2116 + "dev": true, 2117 + "license": "MIT", 2118 + "dependencies": { 2119 + "@eslint-community/eslint-utils": "^4.2.0", 2120 + "@eslint-community/regexpp": "^4.12.1", 2121 + "@eslint/config-array": "^0.21.0", 2122 + "@eslint/config-helpers": "^0.3.0", 2123 + "@eslint/core": "^0.15.0", 2124 + "@eslint/eslintrc": "^3.3.1", 2125 + "@eslint/js": "9.31.0", 2126 + "@eslint/plugin-kit": "^0.3.1", 2127 + "@humanfs/node": "^0.16.6", 2128 + "@humanwhocodes/module-importer": "^1.0.1", 2129 + "@humanwhocodes/retry": "^0.4.2", 2130 + "@types/estree": "^1.0.6", 2131 + "@types/json-schema": "^7.0.15", 2132 + "ajv": "^6.12.4", 2133 + "chalk": "^4.0.0", 2134 + "cross-spawn": "^7.0.6", 2135 + "debug": "^4.3.2", 2136 + "escape-string-regexp": "^4.0.0", 2137 + "eslint-scope": "^8.4.0", 2138 + "eslint-visitor-keys": "^4.2.1", 2139 + "espree": "^10.4.0", 2140 + "esquery": "^1.5.0", 2141 + "esutils": "^2.0.2", 2142 + "fast-deep-equal": "^3.1.3", 2143 + "file-entry-cache": "^8.0.0", 2144 + "find-up": "^5.0.0", 2145 + "glob-parent": "^6.0.2", 2146 + "ignore": "^5.2.0", 2147 + "imurmurhash": "^0.1.4", 2148 + "is-glob": "^4.0.0", 2149 + "json-stable-stringify-without-jsonify": "^1.0.1", 2150 + "lodash.merge": "^4.6.2", 2151 + "minimatch": "^3.1.2", 2152 + "natural-compare": "^1.4.0", 2153 + "optionator": "^0.9.3" 2154 + }, 2155 + "bin": { 2156 + "eslint": "bin/eslint.js" 2157 + }, 2158 + "engines": { 2159 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2160 + }, 2161 + "funding": { 2162 + "url": "https://eslint.org/donate" 2163 + }, 2164 + "peerDependencies": { 2165 + "jiti": "*" 2166 + }, 2167 + "peerDependenciesMeta": { 2168 + "jiti": { 2169 + "optional": true 2170 + } 2171 + } 2172 + }, 2173 + "node_modules/eslint-plugin-react-hooks": { 2174 + "version": "5.2.0", 2175 + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", 2176 + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", 2177 + "dev": true, 2178 + "license": "MIT", 2179 + "engines": { 2180 + "node": ">=10" 2181 + }, 2182 + "peerDependencies": { 2183 + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" 2184 + } 2185 + }, 2186 + "node_modules/eslint-plugin-react-refresh": { 2187 + "version": "0.4.20", 2188 + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", 2189 + "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", 2190 + "dev": true, 2191 + "license": "MIT", 2192 + "peerDependencies": { 2193 + "eslint": ">=8.40" 2194 + } 2195 + }, 2196 + "node_modules/eslint-scope": { 2197 + "version": "8.4.0", 2198 + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", 2199 + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", 2200 + "dev": true, 2201 + "license": "BSD-2-Clause", 2202 + "dependencies": { 2203 + "esrecurse": "^4.3.0", 2204 + "estraverse": "^5.2.0" 2205 + }, 2206 + "engines": { 2207 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2208 + }, 2209 + "funding": { 2210 + "url": "https://opencollective.com/eslint" 2211 + } 2212 + }, 2213 + "node_modules/eslint-visitor-keys": { 2214 + "version": "4.2.1", 2215 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", 2216 + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", 2217 + "dev": true, 2218 + "license": "Apache-2.0", 2219 + "engines": { 2220 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2221 + }, 2222 + "funding": { 2223 + "url": "https://opencollective.com/eslint" 2224 + } 2225 + }, 2226 + "node_modules/esm-env": { 2227 + "version": "1.2.2", 2228 + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", 2229 + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", 2230 + "license": "MIT" 2231 + }, 2232 + "node_modules/espree": { 2233 + "version": "10.4.0", 2234 + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", 2235 + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", 2236 + "dev": true, 2237 + "license": "BSD-2-Clause", 2238 + "dependencies": { 2239 + "acorn": "^8.15.0", 2240 + "acorn-jsx": "^5.3.2", 2241 + "eslint-visitor-keys": "^4.2.1" 2242 + }, 2243 + "engines": { 2244 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 2245 + }, 2246 + "funding": { 2247 + "url": "https://opencollective.com/eslint" 2248 + } 2249 + }, 2250 + "node_modules/esquery": { 2251 + "version": "1.6.0", 2252 + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", 2253 + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", 2254 + "dev": true, 2255 + "license": "BSD-3-Clause", 2256 + "dependencies": { 2257 + "estraverse": "^5.1.0" 2258 + }, 2259 + "engines": { 2260 + "node": ">=0.10" 2261 + } 2262 + }, 2263 + "node_modules/esrecurse": { 2264 + "version": "4.3.0", 2265 + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", 2266 + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", 2267 + "dev": true, 2268 + "license": "BSD-2-Clause", 2269 + "dependencies": { 2270 + "estraverse": "^5.2.0" 2271 + }, 2272 + "engines": { 2273 + "node": ">=4.0" 2274 + } 2275 + }, 2276 + "node_modules/estraverse": { 2277 + "version": "5.3.0", 2278 + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 2279 + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 2280 + "dev": true, 2281 + "license": "BSD-2-Clause", 2282 + "engines": { 2283 + "node": ">=4.0" 2284 + } 2285 + }, 2286 + "node_modules/esutils": { 2287 + "version": "2.0.3", 2288 + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 2289 + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 2290 + "dev": true, 2291 + "license": "BSD-2-Clause", 2292 + "engines": { 2293 + "node": ">=0.10.0" 2294 + } 2295 + }, 2296 + "node_modules/fast-deep-equal": { 2297 + "version": "3.1.3", 2298 + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 2299 + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 2300 + "dev": true, 2301 + "license": "MIT" 2302 + }, 2303 + "node_modules/fast-glob": { 2304 + "version": "3.3.3", 2305 + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", 2306 + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", 2307 + "dev": true, 2308 + "license": "MIT", 2309 + "dependencies": { 2310 + "@nodelib/fs.stat": "^2.0.2", 2311 + "@nodelib/fs.walk": "^1.2.3", 2312 + "glob-parent": "^5.1.2", 2313 + "merge2": "^1.3.0", 2314 + "micromatch": "^4.0.8" 2315 + }, 2316 + "engines": { 2317 + "node": ">=8.6.0" 2318 + } 2319 + }, 2320 + "node_modules/fast-glob/node_modules/glob-parent": { 2321 + "version": "5.1.2", 2322 + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 2323 + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 2324 + "dev": true, 2325 + "license": "ISC", 2326 + "dependencies": { 2327 + "is-glob": "^4.0.1" 2328 + }, 2329 + "engines": { 2330 + "node": ">= 6" 2331 + } 2332 + }, 2333 + "node_modules/fast-json-stable-stringify": { 2334 + "version": "2.1.0", 2335 + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 2336 + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 2337 + "dev": true, 2338 + "license": "MIT" 2339 + }, 2340 + "node_modules/fast-levenshtein": { 2341 + "version": "2.0.6", 2342 + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 2343 + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", 2344 + "dev": true, 2345 + "license": "MIT" 2346 + }, 2347 + "node_modules/fastq": { 2348 + "version": "1.19.1", 2349 + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", 2350 + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", 2351 + "dev": true, 2352 + "license": "ISC", 2353 + "dependencies": { 2354 + "reusify": "^1.0.4" 2355 + } 2356 + }, 2357 + "node_modules/file-entry-cache": { 2358 + "version": "8.0.0", 2359 + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", 2360 + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", 2361 + "dev": true, 2362 + "license": "MIT", 2363 + "dependencies": { 2364 + "flat-cache": "^4.0.0" 2365 + }, 2366 + "engines": { 2367 + "node": ">=16.0.0" 2368 + } 2369 + }, 2370 + "node_modules/fill-range": { 2371 + "version": "7.1.1", 2372 + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", 2373 + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", 2374 + "dev": true, 2375 + "license": "MIT", 2376 + "dependencies": { 2377 + "to-regex-range": "^5.0.1" 2378 + }, 2379 + "engines": { 2380 + "node": ">=8" 2381 + } 2382 + }, 2383 + "node_modules/find-up": { 2384 + "version": "5.0.0", 2385 + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 2386 + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 2387 + "dev": true, 2388 + "license": "MIT", 2389 + "dependencies": { 2390 + "locate-path": "^6.0.0", 2391 + "path-exists": "^4.0.0" 2392 + }, 2393 + "engines": { 2394 + "node": ">=10" 2395 + }, 2396 + "funding": { 2397 + "url": "https://github.com/sponsors/sindresorhus" 2398 + } 2399 + }, 2400 + "node_modules/flat-cache": { 2401 + "version": "4.0.1", 2402 + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", 2403 + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", 2404 + "dev": true, 2405 + "license": "MIT", 2406 + "dependencies": { 2407 + "flatted": "^3.2.9", 2408 + "keyv": "^4.5.4" 2409 + }, 2410 + "engines": { 2411 + "node": ">=16" 2412 + } 2413 + }, 2414 + "node_modules/flatted": { 2415 + "version": "3.3.3", 2416 + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", 2417 + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", 2418 + "dev": true, 2419 + "license": "ISC" 2420 + }, 2421 + "node_modules/fsevents": { 2422 + "version": "2.3.3", 2423 + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 2424 + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 2425 + "dev": true, 2426 + "hasInstallScript": true, 2427 + "license": "MIT", 2428 + "optional": true, 2429 + "os": [ 2430 + "darwin" 2431 + ], 2432 + "engines": { 2433 + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 2434 + } 2435 + }, 2436 + "node_modules/gensync": { 2437 + "version": "1.0.0-beta.2", 2438 + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", 2439 + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", 2440 + "dev": true, 2441 + "license": "MIT", 2442 + "engines": { 2443 + "node": ">=6.9.0" 2444 + } 2445 + }, 2446 + "node_modules/glob-parent": { 2447 + "version": "6.0.2", 2448 + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 2449 + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 2450 + "dev": true, 2451 + "license": "ISC", 2452 + "dependencies": { 2453 + "is-glob": "^4.0.3" 2454 + }, 2455 + "engines": { 2456 + "node": ">=10.13.0" 2457 + } 2458 + }, 2459 + "node_modules/globals": { 2460 + "version": "16.3.0", 2461 + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", 2462 + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", 2463 + "dev": true, 2464 + "license": "MIT", 2465 + "engines": { 2466 + "node": ">=18" 2467 + }, 2468 + "funding": { 2469 + "url": "https://github.com/sponsors/sindresorhus" 2470 + } 2471 + }, 2472 + "node_modules/graphemer": { 2473 + "version": "1.4.0", 2474 + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", 2475 + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", 2476 + "dev": true, 2477 + "license": "MIT" 2478 + }, 2479 + "node_modules/has-flag": { 2480 + "version": "4.0.0", 2481 + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 2482 + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 2483 + "dev": true, 2484 + "license": "MIT", 2485 + "engines": { 2486 + "node": ">=8" 2487 + } 2488 + }, 2489 + "node_modules/ignore": { 2490 + "version": "5.3.2", 2491 + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", 2492 + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", 2493 + "dev": true, 2494 + "license": "MIT", 2495 + "engines": { 2496 + "node": ">= 4" 2497 + } 2498 + }, 2499 + "node_modules/import-fresh": { 2500 + "version": "3.3.1", 2501 + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", 2502 + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", 2503 + "dev": true, 2504 + "license": "MIT", 2505 + "dependencies": { 2506 + "parent-module": "^1.0.0", 2507 + "resolve-from": "^4.0.0" 2508 + }, 2509 + "engines": { 2510 + "node": ">=6" 2511 + }, 2512 + "funding": { 2513 + "url": "https://github.com/sponsors/sindresorhus" 2514 + } 2515 + }, 2516 + "node_modules/imurmurhash": { 2517 + "version": "0.1.4", 2518 + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 2519 + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", 2520 + "dev": true, 2521 + "license": "MIT", 2522 + "engines": { 2523 + "node": ">=0.8.19" 2524 + } 2525 + }, 2526 + "node_modules/is-extglob": { 2527 + "version": "2.1.1", 2528 + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 2529 + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 2530 + "dev": true, 2531 + "license": "MIT", 2532 + "engines": { 2533 + "node": ">=0.10.0" 2534 + } 2535 + }, 2536 + "node_modules/is-glob": { 2537 + "version": "4.0.3", 2538 + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 2539 + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 2540 + "dev": true, 2541 + "license": "MIT", 2542 + "dependencies": { 2543 + "is-extglob": "^2.1.1" 2544 + }, 2545 + "engines": { 2546 + "node": ">=0.10.0" 2547 + } 2548 + }, 2549 + "node_modules/is-number": { 2550 + "version": "7.0.0", 2551 + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 2552 + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 2553 + "dev": true, 2554 + "license": "MIT", 2555 + "engines": { 2556 + "node": ">=0.12.0" 2557 + } 2558 + }, 2559 + "node_modules/isexe": { 2560 + "version": "2.0.0", 2561 + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 2562 + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 2563 + "dev": true, 2564 + "license": "ISC" 2565 + }, 2566 + "node_modules/js-tokens": { 2567 + "version": "4.0.0", 2568 + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 2569 + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 2570 + "dev": true, 2571 + "license": "MIT" 2572 + }, 2573 + "node_modules/js-yaml": { 2574 + "version": "4.1.0", 2575 + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 2576 + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 2577 + "dev": true, 2578 + "license": "MIT", 2579 + "dependencies": { 2580 + "argparse": "^2.0.1" 2581 + }, 2582 + "bin": { 2583 + "js-yaml": "bin/js-yaml.js" 2584 + } 2585 + }, 2586 + "node_modules/jsesc": { 2587 + "version": "3.1.0", 2588 + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", 2589 + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", 2590 + "dev": true, 2591 + "license": "MIT", 2592 + "bin": { 2593 + "jsesc": "bin/jsesc" 2594 + }, 2595 + "engines": { 2596 + "node": ">=6" 2597 + } 2598 + }, 2599 + "node_modules/json-buffer": { 2600 + "version": "3.0.1", 2601 + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", 2602 + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", 2603 + "dev": true, 2604 + "license": "MIT" 2605 + }, 2606 + "node_modules/json-schema-traverse": { 2607 + "version": "0.4.1", 2608 + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 2609 + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 2610 + "dev": true, 2611 + "license": "MIT" 2612 + }, 2613 + "node_modules/json-stable-stringify-without-jsonify": { 2614 + "version": "1.0.1", 2615 + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 2616 + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", 2617 + "dev": true, 2618 + "license": "MIT" 2619 + }, 2620 + "node_modules/json5": { 2621 + "version": "2.2.3", 2622 + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", 2623 + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", 2624 + "dev": true, 2625 + "license": "MIT", 2626 + "bin": { 2627 + "json5": "lib/cli.js" 2628 + }, 2629 + "engines": { 2630 + "node": ">=6" 2631 + } 2632 + }, 2633 + "node_modules/keyv": { 2634 + "version": "4.5.4", 2635 + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", 2636 + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", 2637 + "dev": true, 2638 + "license": "MIT", 2639 + "dependencies": { 2640 + "json-buffer": "3.0.1" 2641 + } 2642 + }, 2643 + "node_modules/levn": { 2644 + "version": "0.4.1", 2645 + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", 2646 + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", 2647 + "dev": true, 2648 + "license": "MIT", 2649 + "dependencies": { 2650 + "prelude-ls": "^1.2.1", 2651 + "type-check": "~0.4.0" 2652 + }, 2653 + "engines": { 2654 + "node": ">= 0.8.0" 2655 + } 2656 + }, 2657 + "node_modules/locate-path": { 2658 + "version": "6.0.0", 2659 + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 2660 + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 2661 + "dev": true, 2662 + "license": "MIT", 2663 + "dependencies": { 2664 + "p-locate": "^5.0.0" 2665 + }, 2666 + "engines": { 2667 + "node": ">=10" 2668 + }, 2669 + "funding": { 2670 + "url": "https://github.com/sponsors/sindresorhus" 2671 + } 2672 + }, 2673 + "node_modules/lodash.merge": { 2674 + "version": "4.6.2", 2675 + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 2676 + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 2677 + "dev": true, 2678 + "license": "MIT" 2679 + }, 2680 + "node_modules/lru-cache": { 2681 + "version": "5.1.1", 2682 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", 2683 + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", 2684 + "dev": true, 2685 + "license": "ISC", 2686 + "dependencies": { 2687 + "yallist": "^3.0.2" 2688 + } 2689 + }, 2690 + "node_modules/merge2": { 2691 + "version": "1.4.1", 2692 + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", 2693 + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", 2694 + "dev": true, 2695 + "license": "MIT", 2696 + "engines": { 2697 + "node": ">= 8" 2698 + } 2699 + }, 2700 + "node_modules/micromatch": { 2701 + "version": "4.0.8", 2702 + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", 2703 + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", 2704 + "dev": true, 2705 + "license": "MIT", 2706 + "dependencies": { 2707 + "braces": "^3.0.3", 2708 + "picomatch": "^2.3.1" 2709 + }, 2710 + "engines": { 2711 + "node": ">=8.6" 2712 + } 2713 + }, 2714 + "node_modules/minimatch": { 2715 + "version": "3.1.2", 2716 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 2717 + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 2718 + "dev": true, 2719 + "license": "ISC", 2720 + "dependencies": { 2721 + "brace-expansion": "^1.1.7" 2722 + }, 2723 + "engines": { 2724 + "node": "*" 2725 + } 2726 + }, 2727 + "node_modules/ms": { 2728 + "version": "2.1.3", 2729 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 2730 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 2731 + "dev": true, 2732 + "license": "MIT" 2733 + }, 2734 + "node_modules/nanoid": { 2735 + "version": "3.3.11", 2736 + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", 2737 + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", 2738 + "dev": true, 2739 + "funding": [ 2740 + { 2741 + "type": "github", 2742 + "url": "https://github.com/sponsors/ai" 2743 + } 2744 + ], 2745 + "license": "MIT", 2746 + "bin": { 2747 + "nanoid": "bin/nanoid.cjs" 2748 + }, 2749 + "engines": { 2750 + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 2751 + } 2752 + }, 2753 + "node_modules/natural-compare": { 2754 + "version": "1.4.0", 2755 + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 2756 + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", 2757 + "dev": true, 2758 + "license": "MIT" 2759 + }, 2760 + "node_modules/node-releases": { 2761 + "version": "2.0.19", 2762 + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", 2763 + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", 2764 + "dev": true, 2765 + "license": "MIT" 2766 + }, 2767 + "node_modules/optionator": { 2768 + "version": "0.9.4", 2769 + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", 2770 + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", 2771 + "dev": true, 2772 + "license": "MIT", 2773 + "dependencies": { 2774 + "deep-is": "^0.1.3", 2775 + "fast-levenshtein": "^2.0.6", 2776 + "levn": "^0.4.1", 2777 + "prelude-ls": "^1.2.1", 2778 + "type-check": "^0.4.0", 2779 + "word-wrap": "^1.2.5" 2780 + }, 2781 + "engines": { 2782 + "node": ">= 0.8.0" 2783 + } 2784 + }, 2785 + "node_modules/p-limit": { 2786 + "version": "3.1.0", 2787 + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 2788 + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 2789 + "dev": true, 2790 + "license": "MIT", 2791 + "dependencies": { 2792 + "yocto-queue": "^0.1.0" 2793 + }, 2794 + "engines": { 2795 + "node": ">=10" 2796 + }, 2797 + "funding": { 2798 + "url": "https://github.com/sponsors/sindresorhus" 2799 + } 2800 + }, 2801 + "node_modules/p-locate": { 2802 + "version": "5.0.0", 2803 + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 2804 + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 2805 + "dev": true, 2806 + "license": "MIT", 2807 + "dependencies": { 2808 + "p-limit": "^3.0.2" 2809 + }, 2810 + "engines": { 2811 + "node": ">=10" 2812 + }, 2813 + "funding": { 2814 + "url": "https://github.com/sponsors/sindresorhus" 2815 + } 2816 + }, 2817 + "node_modules/parent-module": { 2818 + "version": "1.0.1", 2819 + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 2820 + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 2821 + "dev": true, 2822 + "license": "MIT", 2823 + "dependencies": { 2824 + "callsites": "^3.0.0" 2825 + }, 2826 + "engines": { 2827 + "node": ">=6" 2828 + } 2829 + }, 2830 + "node_modules/path-exists": { 2831 + "version": "4.0.0", 2832 + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 2833 + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 2834 + "dev": true, 2835 + "license": "MIT", 2836 + "engines": { 2837 + "node": ">=8" 2838 + } 2839 + }, 2840 + "node_modules/path-key": { 2841 + "version": "3.1.1", 2842 + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 2843 + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 2844 + "dev": true, 2845 + "license": "MIT", 2846 + "engines": { 2847 + "node": ">=8" 2848 + } 2849 + }, 2850 + "node_modules/picocolors": { 2851 + "version": "1.1.1", 2852 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 2853 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 2854 + "dev": true, 2855 + "license": "ISC" 2856 + }, 2857 + "node_modules/picomatch": { 2858 + "version": "2.3.1", 2859 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 2860 + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 2861 + "dev": true, 2862 + "license": "MIT", 2863 + "engines": { 2864 + "node": ">=8.6" 2865 + }, 2866 + "funding": { 2867 + "url": "https://github.com/sponsors/jonschlinkert" 2868 + } 2869 + }, 2870 + "node_modules/postcss": { 2871 + "version": "8.5.6", 2872 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", 2873 + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", 2874 + "dev": true, 2875 + "funding": [ 2876 + { 2877 + "type": "opencollective", 2878 + "url": "https://opencollective.com/postcss/" 2879 + }, 2880 + { 2881 + "type": "tidelift", 2882 + "url": "https://tidelift.com/funding/github/npm/postcss" 2883 + }, 2884 + { 2885 + "type": "github", 2886 + "url": "https://github.com/sponsors/ai" 2887 + } 2888 + ], 2889 + "license": "MIT", 2890 + "dependencies": { 2891 + "nanoid": "^3.3.11", 2892 + "picocolors": "^1.1.1", 2893 + "source-map-js": "^1.2.1" 2894 + }, 2895 + "engines": { 2896 + "node": "^10 || ^12 || >=14" 2897 + } 2898 + }, 2899 + "node_modules/prelude-ls": { 2900 + "version": "1.2.1", 2901 + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", 2902 + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", 2903 + "dev": true, 2904 + "license": "MIT", 2905 + "engines": { 2906 + "node": ">= 0.8.0" 2907 + } 2908 + }, 2909 + "node_modules/punycode": { 2910 + "version": "2.3.1", 2911 + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", 2912 + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", 2913 + "dev": true, 2914 + "license": "MIT", 2915 + "engines": { 2916 + "node": ">=6" 2917 + } 2918 + }, 2919 + "node_modules/queue-microtask": { 2920 + "version": "1.2.3", 2921 + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", 2922 + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", 2923 + "dev": true, 2924 + "funding": [ 2925 + { 2926 + "type": "github", 2927 + "url": "https://github.com/sponsors/feross" 2928 + }, 2929 + { 2930 + "type": "patreon", 2931 + "url": "https://www.patreon.com/feross" 2932 + }, 2933 + { 2934 + "type": "consulting", 2935 + "url": "https://feross.org/support" 2936 + } 2937 + ], 2938 + "license": "MIT" 2939 + }, 2940 + "node_modules/react": { 2941 + "version": "19.1.0", 2942 + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", 2943 + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", 2944 + "license": "MIT", 2945 + "engines": { 2946 + "node": ">=0.10.0" 2947 + } 2948 + }, 2949 + "node_modules/react-dom": { 2950 + "version": "19.1.0", 2951 + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", 2952 + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", 2953 + "license": "MIT", 2954 + "dependencies": { 2955 + "scheduler": "^0.26.0" 2956 + }, 2957 + "peerDependencies": { 2958 + "react": "^19.1.0" 2959 + } 2960 + }, 2961 + "node_modules/react-refresh": { 2962 + "version": "0.17.0", 2963 + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", 2964 + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", 2965 + "dev": true, 2966 + "license": "MIT", 2967 + "engines": { 2968 + "node": ">=0.10.0" 2969 + } 2970 + }, 2971 + "node_modules/resolve-from": { 2972 + "version": "4.0.0", 2973 + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 2974 + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 2975 + "dev": true, 2976 + "license": "MIT", 2977 + "engines": { 2978 + "node": ">=4" 2979 + } 2980 + }, 2981 + "node_modules/reusify": { 2982 + "version": "1.1.0", 2983 + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", 2984 + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", 2985 + "dev": true, 2986 + "license": "MIT", 2987 + "engines": { 2988 + "iojs": ">=1.0.0", 2989 + "node": ">=0.10.0" 2990 + } 2991 + }, 2992 + "node_modules/rollup": { 2993 + "version": "4.45.1", 2994 + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz", 2995 + "integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==", 2996 + "dev": true, 2997 + "license": "MIT", 2998 + "dependencies": { 2999 + "@types/estree": "1.0.8" 3000 + }, 3001 + "bin": { 3002 + "rollup": "dist/bin/rollup" 3003 + }, 3004 + "engines": { 3005 + "node": ">=18.0.0", 3006 + "npm": ">=8.0.0" 3007 + }, 3008 + "optionalDependencies": { 3009 + "@rollup/rollup-android-arm-eabi": "4.45.1", 3010 + "@rollup/rollup-android-arm64": "4.45.1", 3011 + "@rollup/rollup-darwin-arm64": "4.45.1", 3012 + "@rollup/rollup-darwin-x64": "4.45.1", 3013 + "@rollup/rollup-freebsd-arm64": "4.45.1", 3014 + "@rollup/rollup-freebsd-x64": "4.45.1", 3015 + "@rollup/rollup-linux-arm-gnueabihf": "4.45.1", 3016 + "@rollup/rollup-linux-arm-musleabihf": "4.45.1", 3017 + "@rollup/rollup-linux-arm64-gnu": "4.45.1", 3018 + "@rollup/rollup-linux-arm64-musl": "4.45.1", 3019 + "@rollup/rollup-linux-loongarch64-gnu": "4.45.1", 3020 + "@rollup/rollup-linux-powerpc64le-gnu": "4.45.1", 3021 + "@rollup/rollup-linux-riscv64-gnu": "4.45.1", 3022 + "@rollup/rollup-linux-riscv64-musl": "4.45.1", 3023 + "@rollup/rollup-linux-s390x-gnu": "4.45.1", 3024 + "@rollup/rollup-linux-x64-gnu": "4.45.1", 3025 + "@rollup/rollup-linux-x64-musl": "4.45.1", 3026 + "@rollup/rollup-win32-arm64-msvc": "4.45.1", 3027 + "@rollup/rollup-win32-ia32-msvc": "4.45.1", 3028 + "@rollup/rollup-win32-x64-msvc": "4.45.1", 3029 + "fsevents": "~2.3.2" 3030 + } 3031 + }, 3032 + "node_modules/run-parallel": { 3033 + "version": "1.2.0", 3034 + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", 3035 + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 3036 + "dev": true, 3037 + "funding": [ 3038 + { 3039 + "type": "github", 3040 + "url": "https://github.com/sponsors/feross" 3041 + }, 3042 + { 3043 + "type": "patreon", 3044 + "url": "https://www.patreon.com/feross" 3045 + }, 3046 + { 3047 + "type": "consulting", 3048 + "url": "https://feross.org/support" 3049 + } 3050 + ], 3051 + "license": "MIT", 3052 + "dependencies": { 3053 + "queue-microtask": "^1.2.2" 3054 + } 3055 + }, 3056 + "node_modules/scheduler": { 3057 + "version": "0.26.0", 3058 + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", 3059 + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", 3060 + "license": "MIT" 3061 + }, 3062 + "node_modules/semver": { 3063 + "version": "6.3.1", 3064 + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", 3065 + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", 3066 + "dev": true, 3067 + "license": "ISC", 3068 + "bin": { 3069 + "semver": "bin/semver.js" 3070 + } 3071 + }, 3072 + "node_modules/shebang-command": { 3073 + "version": "2.0.0", 3074 + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 3075 + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 3076 + "dev": true, 3077 + "license": "MIT", 3078 + "dependencies": { 3079 + "shebang-regex": "^3.0.0" 3080 + }, 3081 + "engines": { 3082 + "node": ">=8" 3083 + } 3084 + }, 3085 + "node_modules/shebang-regex": { 3086 + "version": "3.0.0", 3087 + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 3088 + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 3089 + "dev": true, 3090 + "license": "MIT", 3091 + "engines": { 3092 + "node": ">=8" 3093 + } 3094 + }, 3095 + "node_modules/source-map-js": { 3096 + "version": "1.2.1", 3097 + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 3098 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 3099 + "dev": true, 3100 + "license": "BSD-3-Clause", 3101 + "engines": { 3102 + "node": ">=0.10.0" 3103 + } 3104 + }, 3105 + "node_modules/strip-json-comments": { 3106 + "version": "3.1.1", 3107 + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 3108 + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 3109 + "dev": true, 3110 + "license": "MIT", 3111 + "engines": { 3112 + "node": ">=8" 3113 + }, 3114 + "funding": { 3115 + "url": "https://github.com/sponsors/sindresorhus" 3116 + } 3117 + }, 3118 + "node_modules/supports-color": { 3119 + "version": "7.2.0", 3120 + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 3121 + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 3122 + "dev": true, 3123 + "license": "MIT", 3124 + "dependencies": { 3125 + "has-flag": "^4.0.0" 3126 + }, 3127 + "engines": { 3128 + "node": ">=8" 3129 + } 3130 + }, 3131 + "node_modules/tinyglobby": { 3132 + "version": "0.2.14", 3133 + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", 3134 + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", 3135 + "dev": true, 3136 + "license": "MIT", 3137 + "dependencies": { 3138 + "fdir": "^6.4.4", 3139 + "picomatch": "^4.0.2" 3140 + }, 3141 + "engines": { 3142 + "node": ">=12.0.0" 3143 + }, 3144 + "funding": { 3145 + "url": "https://github.com/sponsors/SuperchupuDev" 3146 + } 3147 + }, 3148 + "node_modules/tinyglobby/node_modules/fdir": { 3149 + "version": "6.4.6", 3150 + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", 3151 + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", 3152 + "dev": true, 3153 + "license": "MIT", 3154 + "peerDependencies": { 3155 + "picomatch": "^3 || ^4" 3156 + }, 3157 + "peerDependenciesMeta": { 3158 + "picomatch": { 3159 + "optional": true 3160 + } 3161 + } 3162 + }, 3163 + "node_modules/tinyglobby/node_modules/picomatch": { 3164 + "version": "4.0.3", 3165 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", 3166 + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 3167 + "dev": true, 3168 + "license": "MIT", 3169 + "engines": { 3170 + "node": ">=12" 3171 + }, 3172 + "funding": { 3173 + "url": "https://github.com/sponsors/jonschlinkert" 3174 + } 3175 + }, 3176 + "node_modules/to-regex-range": { 3177 + "version": "5.0.1", 3178 + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 3179 + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 3180 + "dev": true, 3181 + "license": "MIT", 3182 + "dependencies": { 3183 + "is-number": "^7.0.0" 3184 + }, 3185 + "engines": { 3186 + "node": ">=8.0" 3187 + } 3188 + }, 3189 + "node_modules/ts-api-utils": { 3190 + "version": "2.1.0", 3191 + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", 3192 + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", 3193 + "dev": true, 3194 + "license": "MIT", 3195 + "engines": { 3196 + "node": ">=18.12" 3197 + }, 3198 + "peerDependencies": { 3199 + "typescript": ">=4.8.4" 3200 + } 3201 + }, 3202 + "node_modules/type-check": { 3203 + "version": "0.4.0", 3204 + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", 3205 + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", 3206 + "dev": true, 3207 + "license": "MIT", 3208 + "dependencies": { 3209 + "prelude-ls": "^1.2.1" 3210 + }, 3211 + "engines": { 3212 + "node": ">= 0.8.0" 3213 + } 3214 + }, 3215 + "node_modules/typescript": { 3216 + "version": "5.8.3", 3217 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", 3218 + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", 3219 + "dev": true, 3220 + "license": "Apache-2.0", 3221 + "bin": { 3222 + "tsc": "bin/tsc", 3223 + "tsserver": "bin/tsserver" 3224 + }, 3225 + "engines": { 3226 + "node": ">=14.17" 3227 + } 3228 + }, 3229 + "node_modules/typescript-eslint": { 3230 + "version": "8.38.0", 3231 + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.38.0.tgz", 3232 + "integrity": "sha512-FsZlrYK6bPDGoLeZRuvx2v6qrM03I0U0SnfCLPs/XCCPCFD80xU9Pg09H/K+XFa68uJuZo7l/Xhs+eDRg2l3hg==", 3233 + "dev": true, 3234 + "license": "MIT", 3235 + "dependencies": { 3236 + "@typescript-eslint/eslint-plugin": "8.38.0", 3237 + "@typescript-eslint/parser": "8.38.0", 3238 + "@typescript-eslint/typescript-estree": "8.38.0", 3239 + "@typescript-eslint/utils": "8.38.0" 3240 + }, 3241 + "engines": { 3242 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 3243 + }, 3244 + "funding": { 3245 + "type": "opencollective", 3246 + "url": "https://opencollective.com/typescript-eslint" 3247 + }, 3248 + "peerDependencies": { 3249 + "eslint": "^8.57.0 || ^9.0.0", 3250 + "typescript": ">=4.8.4 <5.9.0" 3251 + } 3252 + }, 3253 + "node_modules/update-browserslist-db": { 3254 + "version": "1.1.3", 3255 + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", 3256 + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", 3257 + "dev": true, 3258 + "funding": [ 3259 + { 3260 + "type": "opencollective", 3261 + "url": "https://opencollective.com/browserslist" 3262 + }, 3263 + { 3264 + "type": "tidelift", 3265 + "url": "https://tidelift.com/funding/github/npm/browserslist" 3266 + }, 3267 + { 3268 + "type": "github", 3269 + "url": "https://github.com/sponsors/ai" 3270 + } 3271 + ], 3272 + "license": "MIT", 3273 + "dependencies": { 3274 + "escalade": "^3.2.0", 3275 + "picocolors": "^1.1.1" 3276 + }, 3277 + "bin": { 3278 + "update-browserslist-db": "cli.js" 3279 + }, 3280 + "peerDependencies": { 3281 + "browserslist": ">= 4.21.0" 3282 + } 3283 + }, 3284 + "node_modules/uri-js": { 3285 + "version": "4.4.1", 3286 + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 3287 + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 3288 + "dev": true, 3289 + "license": "BSD-2-Clause", 3290 + "dependencies": { 3291 + "punycode": "^2.1.0" 3292 + } 3293 + }, 3294 + "node_modules/vite": { 3295 + "version": "7.0.6", 3296 + "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz", 3297 + "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==", 3298 + "dev": true, 3299 + "license": "MIT", 3300 + "dependencies": { 3301 + "esbuild": "^0.25.0", 3302 + "fdir": "^6.4.6", 3303 + "picomatch": "^4.0.3", 3304 + "postcss": "^8.5.6", 3305 + "rollup": "^4.40.0", 3306 + "tinyglobby": "^0.2.14" 3307 + }, 3308 + "bin": { 3309 + "vite": "bin/vite.js" 3310 + }, 3311 + "engines": { 3312 + "node": "^20.19.0 || >=22.12.0" 3313 + }, 3314 + "funding": { 3315 + "url": "https://github.com/vitejs/vite?sponsor=1" 3316 + }, 3317 + "optionalDependencies": { 3318 + "fsevents": "~2.3.3" 3319 + }, 3320 + "peerDependencies": { 3321 + "@types/node": "^20.19.0 || >=22.12.0", 3322 + "jiti": ">=1.21.0", 3323 + "less": "^4.0.0", 3324 + "lightningcss": "^1.21.0", 3325 + "sass": "^1.70.0", 3326 + "sass-embedded": "^1.70.0", 3327 + "stylus": ">=0.54.8", 3328 + "sugarss": "^5.0.0", 3329 + "terser": "^5.16.0", 3330 + "tsx": "^4.8.1", 3331 + "yaml": "^2.4.2" 3332 + }, 3333 + "peerDependenciesMeta": { 3334 + "@types/node": { 3335 + "optional": true 3336 + }, 3337 + "jiti": { 3338 + "optional": true 3339 + }, 3340 + "less": { 3341 + "optional": true 3342 + }, 3343 + "lightningcss": { 3344 + "optional": true 3345 + }, 3346 + "sass": { 3347 + "optional": true 3348 + }, 3349 + "sass-embedded": { 3350 + "optional": true 3351 + }, 3352 + "stylus": { 3353 + "optional": true 3354 + }, 3355 + "sugarss": { 3356 + "optional": true 3357 + }, 3358 + "terser": { 3359 + "optional": true 3360 + }, 3361 + "tsx": { 3362 + "optional": true 3363 + }, 3364 + "yaml": { 3365 + "optional": true 3366 + } 3367 + } 3368 + }, 3369 + "node_modules/vite/node_modules/fdir": { 3370 + "version": "6.4.6", 3371 + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", 3372 + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", 3373 + "dev": true, 3374 + "license": "MIT", 3375 + "peerDependencies": { 3376 + "picomatch": "^3 || ^4" 3377 + }, 3378 + "peerDependenciesMeta": { 3379 + "picomatch": { 3380 + "optional": true 3381 + } 3382 + } 3383 + }, 3384 + "node_modules/vite/node_modules/picomatch": { 3385 + "version": "4.0.3", 3386 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", 3387 + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 3388 + "dev": true, 3389 + "license": "MIT", 3390 + "engines": { 3391 + "node": ">=12" 3392 + }, 3393 + "funding": { 3394 + "url": "https://github.com/sponsors/jonschlinkert" 3395 + } 3396 + }, 3397 + "node_modules/which": { 3398 + "version": "2.0.2", 3399 + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 3400 + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 3401 + "dev": true, 3402 + "license": "ISC", 3403 + "dependencies": { 3404 + "isexe": "^2.0.0" 3405 + }, 3406 + "bin": { 3407 + "node-which": "bin/node-which" 3408 + }, 3409 + "engines": { 3410 + "node": ">= 8" 3411 + } 3412 + }, 3413 + "node_modules/word-wrap": { 3414 + "version": "1.2.5", 3415 + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", 3416 + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", 3417 + "dev": true, 3418 + "license": "MIT", 3419 + "engines": { 3420 + "node": ">=0.10.0" 3421 + } 3422 + }, 3423 + "node_modules/yallist": { 3424 + "version": "3.1.1", 3425 + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", 3426 + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", 3427 + "dev": true, 3428 + "license": "ISC" 3429 + }, 3430 + "node_modules/yocto-queue": { 3431 + "version": "0.1.0", 3432 + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 3433 + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 3434 + "dev": true, 3435 + "license": "MIT", 3436 + "engines": { 3437 + "node": ">=10" 3438 + }, 3439 + "funding": { 3440 + "url": "https://github.com/sponsors/sindresorhus" 3441 + } 3442 + } 3443 + } 3444 + }
+32
live-embed/package.json
··· 1 + { 2 + "name": "live-embed", 3 + "private": true, 4 + "version": "0.0.0", 5 + "type": "module", 6 + "scripts": { 7 + "dev": "vite", 8 + "build": "tsc -b && vite build", 9 + "bsky": "vite build --base=/zero-bluesky-realtime-embed/", 10 + "lint": "eslint .", 11 + "preview": "vite preview" 12 + }, 13 + "dependencies": { 14 + "@atcute/client": "^4.0.3", 15 + "@atcute/identity-resolver": "^1.1.3", 16 + "react": "^19.1.0", 17 + "react-dom": "^19.1.0" 18 + }, 19 + "devDependencies": { 20 + "@eslint/js": "^9.30.1", 21 + "@types/react": "^19.1.8", 22 + "@types/react-dom": "^19.1.6", 23 + "@vitejs/plugin-react": "^4.6.0", 24 + "eslint": "^9.30.1", 25 + "eslint-plugin-react-hooks": "^5.2.0", 26 + "eslint-plugin-react-refresh": "^0.4.20", 27 + "globals": "^16.3.0", 28 + "typescript": "~5.8.3", 29 + "typescript-eslint": "^8.35.1", 30 + "vite": "^7.0.4" 31 + } 32 + }
live-embed/public/.gitkeep

This is a binary file and will not be displayed.

+22
live-embed/src/App.css
··· 1 + #root { 2 + max-width: 51em; 3 + margin: 0 auto; 4 + padding: 2rem; 5 + text-align: center; 6 + } 7 + 8 + .with { 9 + margin: 0.5em 0 1.5em; 10 + font-size: 1.2em; 11 + } 12 + 13 + .posts { 14 + display: flex; 15 + flex-wrap: wrap; 16 + gap: 1rem; 17 + justify-content: center; 18 + } 19 + 20 + .explain { 21 + text-align: left; 22 + }
+76
live-embed/src/App.tsx
··· 1 + import { useEffect, useState } from 'react'; 2 + import { Post } from './Post'; 3 + import { rotatingPair } from './samplePosts'; 4 + import { Spacedust } from './spacedust'; 5 + 6 + import './App.css' 7 + 8 + function App() { 9 + const [currentPair, setCurrentPair] = useState([]); 10 + const [updates, setUpdates] = useState({}); 11 + 12 + useEffect(() => { 13 + let iMightBeAZombie = false; 14 + 15 + const spacedust = new Spacedust(handleLink); 16 + const cancelRotation = rotatingPair(updatedPair => { 17 + if (iMightBeAZombie) return; 18 + 19 + setCurrentPair(updatedPair); 20 + spacedust.setSubjects(updatedPair); 21 + setUpdates(current => { // cleanup, probably should combine with pair directly 22 + const next = {}; 23 + updatedPair.forEach(uri => next[uri] = current[uri] ?? {}); 24 + return next; 25 + }); 26 + }); 27 + 28 + function handleLink({ subject, source }) { 29 + setUpdates(current => ({ 30 + ...current, 31 + [subject]: { 32 + ...current[subject], 33 + [source]: (current[subject][source] ?? 0) + 1 34 + }, 35 + })); 36 + } 37 + 38 + return () => { 39 + cancelRotation(); 40 + spacedust.close(); 41 + iMightBeAZombie = true; 42 + }; 43 + }, []); 44 + 45 + return ( 46 + <> 47 + <h1>zero bluesky infra post rendering (WIP)</h1> 48 + <p className="with">with real-time interaction count updates</p> 49 + <div className="posts"> 50 + {currentPair.map(p => ( 51 + <Post key={p} atUri={p} updatedLinks={updates[p]} /> 52 + ))} 53 + </div> 54 + 55 + <div className="explain"> 56 + <h2>How does it work?</h2> 57 + <ul> 58 + <li><strong>Post content</strong>: fetches direct from PDS with <a href="https://github.com/mary-ext/atcute" target="_blank">atcute</a>.</li> 59 + <li><strong>Interaction counts</strong>: queries <a href="https://constellation.microcosm.blue/" target="_blank">constellation</a>.</li> 60 + <li><strong>Interaction updates</strong>: subscribes to <a href="https://spacedust.microcosm.blue/" target="_blank">spacedust</a>.</li> 61 + <li>There is no backend.</li> 62 + </ul> 63 + <p>The post selection takes a couple top posts from the public bluesky Discover feed so I guess it's kind of cheating but hey. And media files load from Bluesky's CDN (or, will soon) so that's also cheating.</p> 64 + 65 + <p>Source code is on <a href="https://tangled.sh/@bad-example.com/spacedust-utils/tree/main/live-embed" target="_blank">tangled</a>. Still a work in progress.</p> 66 + 67 + <h2>PS</h2> 68 + <p>If you actually want to embed a post on a web page, check out <a href="https://mary-ext.codeberg.page/bluesky-embed/" target="_blank"><code>&lt;bluesky-embed&gt;</code></a> from <a href="https://mary.my.id" target="_blank">mary</a>. It's a very solid post renderer, unlike this demo.</p> 69 + 70 + </div> 71 + 72 + </> 73 + ) 74 + } 75 + 76 + export default App
+67
live-embed/src/Fetch.tsx
··· 1 + import { useContext, useEffect, useState } from 'react'; 2 + 3 + const loadingDefault = () => ( 4 + <em>Loading&hellip;</em> 5 + ); 6 + 7 + const errorDefault = err => ( 8 + <span className="error"> 9 + <strong>Error</strong>:<br/>{`${err}`} 10 + </span> 11 + ); 12 + 13 + export function Fetch({ using, args, ok, loading, error }) { 14 + const [asyncData, setAsyncData] = useState({ state: null }); 15 + 16 + useEffect(() => { 17 + let ignore = false; 18 + setAsyncData({ state: 'loading' }); 19 + (async () => { 20 + try { 21 + const data = await using(...args); 22 + !ignore && setAsyncData({ state: 'done', data }); 23 + } catch (err) { 24 + !ignore && setAsyncData({ state: 'error', err }); 25 + } 26 + })(); 27 + return () => { ignore = true; } 28 + }, args); 29 + 30 + if (asyncData.state === 'loading') { 31 + return (loading || loadingDefault)(...args); 32 + } else if (asyncData.state === 'error') { 33 + return (error || errorDefault)(asyncData.err); 34 + } else if (asyncData.state === null) { 35 + return <span>wat, request has not started (bug?)</span>; 36 + } else { 37 + if (asyncData.state !== 'done') { console.warn(`unexpected async data state: ${asyncData.state}`); } 38 + return ok(asyncData.data); 39 + } 40 + } 41 + 42 + ///// 43 + 44 + async function getJson(url, credentials) { 45 + const opts = {}; 46 + if (credentials) opts.credentials = 'include'; 47 + const res = await fetch(url, opts); 48 + if (!res.ok) { 49 + const m = await res.text(); 50 + throw new Error(`Failed to fetch: ${m}`); 51 + } 52 + return await res.json(); 53 + } 54 + 55 + export function GetJson({ url, params, credentials, ...forFetch }) { 56 + const u = new URL(url); 57 + for (let [key, val] of Object.entries(params ?? {})) { 58 + u.searchParams.append(key, val); 59 + } 60 + return ( 61 + <Fetch 62 + using={getJson} 63 + args={[u.toString(), credentials]} 64 + {...forFetch} 65 + /> 66 + ); 67 + }
+42
live-embed/src/Post.css
··· 1 + .post { 2 + background-color: rgb(32, 41, 53); 3 + border: 1px solid rgb(46, 64, 82); 4 + border-radius: 0.1em; 5 + display: flex; 6 + flex-basis: 20em; 7 + flex-direction: column; 8 + min-width: 12em; 9 + max-width: 24em; 10 + } 11 + 12 + .record-contents { 13 + flex-grow: 1; 14 + padding: 1em; 15 + text-align: left; 16 + align-content: center; 17 + } 18 + 19 + .stats { 20 + border-top: 0.5px solid rgb(46, 64, 82); 21 + display: flex; 22 + justify-content: space-around; 23 + margin: 0; 24 + gap: 1em; 25 + padding: 0 1em; 26 + opacity: 0.667; 27 + } 28 + .stat.reply:before { 29 + content: "๐Ÿซง "; 30 + } 31 + .stat.repost:before { 32 + content: "โ™ป "; 33 + } 34 + .stat.like:before { 35 + content: "โ™ก "; 36 + } 37 + 38 + @media (prefers-color-scheme: light) { 39 + .post { 40 + background-color: rgb(241, 243, 245); 41 + } 42 + }
+68
live-embed/src/Post.tsx
··· 1 + import { useEffect, useState } from 'react'; 2 + import { getPostStats } from './constellation'; 3 + import { getAtUri } from './getPost'; 4 + import linkSources from './linkSources'; 5 + 6 + import './Post.css'; 7 + 8 + export function Post({ atUri, updatedLinks }) { 9 + const [record, setRecord] = useState({ state: 'loading' }); 10 + const [baseStats, setBaseStats] = useState({}); 11 + const liveStats = { ...baseStats }; 12 + 13 + for (const [key, val] of Object.entries(updatedLinks)) { 14 + const name = linkSources[key]; 15 + if (!liveStats[name]) liveStats[name] = 0; 16 + liveStats[name] += val; 17 + } 18 + 19 + useEffect(() => { 20 + let alive = true; 21 + 22 + getAtUri(atUri).then( 23 + record => alive && setRecord({ state: 'loaded', record }), 24 + error => alive && setRecord({ state: 'failed', error })); 25 + 26 + getPostStats(atUri).then( 27 + stats => alive && setBaseStats(stats), 28 + e => console.warn('fetching base stats failed', e)); 29 + 30 + return () => alive = false; 31 + }, [atUri]); 32 + 33 + return ( 34 + <div className="post"> 35 + {record.state === 'loading' 36 + ? <p className="loading">loading post&hellip;</p> 37 + : record.state === 'failed' 38 + ? <p className="failed">failed to load post :/ {`${record.error}`}</p> 39 + : <RecordContents data={record.record} /> 40 + } 41 + <p className="stats"> 42 + {liveStats.reply && ( 43 + <span className="stat reply" title="total replies"> 44 + {liveStats.reply.toLocaleString()} 45 + </span> 46 + )} 47 + {liveStats.repost && ( 48 + <span className="stat repost" title="total reposts"> 49 + {liveStats.repost.toLocaleString()} 50 + </span> 51 + )} 52 + {liveStats.like && ( 53 + <span className="stat like" title="total likes"> 54 + {liveStats.like.toLocaleString()} 55 + </span> 56 + )} 57 + </p> 58 + </div> 59 + ); 60 + } 61 + 62 + function RecordContents({ data }) { 63 + return ( 64 + <div className="record-contents"> 65 + {data.text} 66 + </div> 67 + ); 68 + }
live-embed/src/assets/.gitkeep

This is a binary file and will not be displayed.

+39
live-embed/src/constellation.ts
··· 1 + import linkSources from './linkSources'; 2 + 3 + /** 4 + * get nice historical counts from constellation 5 + * 6 + * constellation's api still uses separated collection/path sources, and 7 + * likes should be distinct where everything else is record counts. 8 + * 9 + * constellation still can only specify one link source per request or /all 10 + * 11 + * handles stuff like that 12 + **/ 13 + export async function getPostStats( 14 + atUri: string, 15 + endpoint: string = 'https://constellation.microcosm.blue' 16 + ) { 17 + const url = new URL('/links/all', endpoint); 18 + url.searchParams.set('target', atUri); 19 + const res = await fetch(url, { signal: AbortSignal.timeout(4000) }); 20 + if (!res.ok) throw new Error(res); 21 + const { links } = await res.json(); 22 + 23 + const niceLinks = {}; 24 + 25 + for (const [collection, paths] of Object.entries(links)) { 26 + for (const [oldStylePath, counts] of Object.entries(paths)) { 27 + const newStylePath = `${collection}:${oldStylePath.slice(1)}`; 28 + const name = linkSources[newStylePath]; 29 + if (!name) continue; // perils of constellation's (soon-deprecated) /all 30 + if (name === 'like') { 31 + niceLinks[name] = counts.distinct_dids; 32 + } else { 33 + niceLinks[name] = counts.records; 34 + } 35 + } 36 + } 37 + 38 + return niceLinks; 39 + }
+73
live-embed/src/getPost.ts
··· 1 + // heyyyyyy if you're reading this: 2 + // 3 + // it's all copy-pasta from the atproto notifications demo, pasted over from 4 + // previous experimental stuff. 5 + // 6 + // all to say it probably needs some attention and cleanup, in particular with 7 + // handling failures. i'll get back to it hopefully soon. 8 + 9 + import { Client, CredentialManager, ok, simpleFetchHandler } from '@atcute/client'; 10 + import { CompositeDidDocumentResolver, PlcDidDocumentResolver, WebDidDocumentResolver } from '@atcute/identity-resolver'; 11 + 12 + const docResolver = new CompositeDidDocumentResolver({ 13 + methods: { 14 + plc: new PlcDidDocumentResolver(), 15 + web: new WebDidDocumentResolver(), 16 + }, 17 + }); 18 + 19 + async function resolve_did(did) { 20 + return await docResolver.resolve(did); 21 + } 22 + 23 + function pds({ service }) { 24 + if (!service) { 25 + throw new Error('missing service from identity doc'); 26 + } 27 + const { serviceEndpoint } = service[0]; 28 + if (!serviceEndpoint) { 29 + throw new Error('missing serviceEndpoint from identity service array'); 30 + } 31 + return serviceEndpoint; 32 + } 33 + 34 + 35 + async function get_pds_record(endpoint, did, collection, rkey) { 36 + const handler = simpleFetchHandler({ service: endpoint }); 37 + const rpc = new Client({ handler }); 38 + const { ok, data } = await rpc.get('com.atproto.repo.getRecord', { 39 + params: { repo: did, collection, rkey }, 40 + }); 41 + if (!ok) throw new Error('fetching pds record failed'); 42 + return data; 43 + } 44 + 45 + function parse_at_uri(uri) { 46 + let collection, rkey; 47 + if (!uri.startsWith('at://')) { 48 + throw new Error('invalid at-uri: did not start with "at://"'); 49 + } 50 + let remaining = uri.slice('at://'.length); // remove the at:// prefix 51 + remaining = remaining.split('#')[0]; // hash is valid in at-uri but we don't handle them 52 + remaining = remaining.split('?')[0]; // query is valid in at-uri but we don't handle it 53 + const segments = remaining.split('/'); 54 + if (segments.length === 0) { 55 + throw new Error('invalid at-uri: could not find did after "at://"'); 56 + } 57 + const did = segments[0]; 58 + if (segments.length > 1) { 59 + collection = segments[1]; 60 + } 61 + if (segments.length > 2) { 62 + rkey = segments.slice(2).join('/'); // hmm are slashes actually valid in rkey? 63 + } 64 + return { did, collection, rkey }; 65 + } 66 + 67 + export async function getAtUri(atUri) { 68 + const { did, collection, rkey } = parse_at_uri(atUri); 69 + const doc = await resolve_did(did); 70 + const endpoint = pds(doc); 71 + const { value } = await get_pds_record(endpoint, did, collection, rkey); 72 + return value; 73 + }
+73
live-embed/src/index.css
··· 1 + :root { 2 + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; 3 + line-height: 1.5; 4 + font-weight: 400; 5 + 6 + color-scheme: light dark; 7 + color: rgba(255, 255, 255, 0.87); 8 + background-color: rgb(24, 30, 38); 9 + 10 + font-synthesis: none; 11 + text-rendering: optimizeLegibility; 12 + -webkit-font-smoothing: antialiased; 13 + -moz-osx-font-smoothing: grayscale; 14 + } 15 + 16 + a { 17 + font-weight: 500; 18 + color: #646cff; 19 + text-decoration: inherit; 20 + } 21 + a:hover { 22 + color: #535bf2; 23 + } 24 + 25 + body { 26 + margin: 0; 27 + display: flex; 28 + place-items: center; 29 + min-width: 320px; 30 + min-height: 100vh; 31 + } 32 + 33 + h1 { 34 + font-size: 2em; 35 + line-height: 1.1; 36 + margin: 0; 37 + } 38 + 39 + h2 { 40 + margin-top: 2em; 41 + } 42 + 43 + button { 44 + border-radius: 8px; 45 + border: 1px solid transparent; 46 + padding: 0.6em 1.2em; 47 + font-size: 1em; 48 + font-weight: 500; 49 + font-family: inherit; 50 + background-color: #1a1a1a; 51 + cursor: pointer; 52 + transition: border-color 0.25s; 53 + } 54 + button:hover { 55 + border-color: #646cff; 56 + } 57 + button:focus, 58 + button:focus-visible { 59 + outline: 4px auto -webkit-focus-ring-color; 60 + } 61 + 62 + @media (prefers-color-scheme: light) { 63 + :root { 64 + color: #213547; 65 + background-color: #ffffff; 66 + } 67 + a:hover { 68 + color: #747bff; 69 + } 70 + button { 71 + background-color: #f9f9f9; 72 + } 73 + }
+9
live-embed/src/linkSources.ts
··· 1 + const linkSources = { 2 + 'app.bsky.feed.like:subject.uri': 'like', 3 + 'app.bsky.feed.repost:subject.uri': 'repost', // actual repost 4 + 'app.bsky.feed.post:embed.record.uri': 'repost', // normal quote (grouped for count) 5 + 'app.bsky.feed.post:embed.record.record.uri': 'repost', // RecordWithMedia quote (grouped for count) 6 + 'app.bsky.feed.post:reply.root.uri': 'reply', // root: count all descendent replies 7 + }; 8 + 9 + export { linkSources as default };
+10
live-embed/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 + )
+85
live-embed/src/samplePosts.ts
··· 1 + import { getPostStats } from './constellation'; 2 + 3 + const SEKELETON_API = 'https://discover.bsky.app/xrpc/app.bsky.feed.getFeedSkeleton'; 4 + const FEED = 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot'; 5 + const POLL_DELAY = 9000; 6 + const POST_LIMIT = 5; 7 + 8 + async function getFeed() { 9 + const url = new URL(SEKELETON_API); 10 + url.searchParams.append('feed', FEED); 11 + url.searchParams.append('limit', POST_LIMIT.toString()); 12 + const res = await fetch(url); 13 + const data = await res.json(); 14 + return data; 15 + } 16 + 17 + /** 18 + * fetch a pair of posts from discover and alternately replace the first/second 19 + * 20 + * TODO: check w constellation to prioritize popular posts 21 + **/ 22 + export function rotatingPair(onRotate: any) { 23 + let timer: number; 24 + let dying = false; 25 + const seen = new Set(); // TODO: mem leak, slowly 26 + let A: string | null = null; 27 + let B: string | null = null; 28 + let which: 'A' | 'B' = 'A'; 29 + 30 + async function next() { 31 + console.info('[sample posts: next]'); 32 + try { 33 + const { feed } = await getFeed(); 34 + if (dying) return; 35 + 36 + const withStats = await Promise.all(feed.map(async ({ post }) => { 37 + if (seen.has(post)) return { post, total: 0 }; 38 + let stats = {}; 39 + try { 40 + stats = await getPostStats(post); 41 + } catch (e) { 42 + console.warn('failed to get stats from constellation', e); 43 + } 44 + const total = Array.from(Object.values(stats)).reduce((a, b) => a + b, 0); 45 + return ({ post, total }) 46 + })) 47 + if (dying) return; 48 + 49 + // idk if sorting by most interactions yields more-interactive posts but eh 50 + withStats.sort(({ total: a }, { total: b }) => b - a); 51 + 52 + // special case: first load 53 + if (A === null && B === null) { 54 + if (withStats.length < 2) throw new Error('withStats returned fewer than two posts to start'); 55 + seen.add(A = withStats[0].post); 56 + seen.add(B = withStats[1].post); 57 + } else { 58 + for (const { post } of withStats) { 59 + if (seen.has(post)) { 60 + continue; 61 + } 62 + if (which === 'A') { 63 + seen.add(B = post); 64 + which = 'B'; 65 + } else { 66 + seen.add(A = post); 67 + which = 'A'; 68 + } 69 + break; 70 + } 71 + } 72 + onRotate([A, B]); 73 + } catch (e) { 74 + console.error('hmm, failed to get withStats', e); 75 + } 76 + timer = setTimeout(next, POLL_DELAY); 77 + } 78 + setTimeout(next); 79 + 80 + return () => { 81 + console.log('clearing'); 82 + clearTimeout(timer); 83 + dying = true; 84 + } 85 + }
+160
live-embed/src/spacedust.ts
··· 1 + import linkSources from './linkSources'; 2 + 3 + type SpacedustStatus = 'disconnected' | 'connecting' | 'connected'; 4 + 5 + type Linkrement = { // this name should send me to jail 6 + subject: String // at-uri 7 + source: String // link source 8 + }; 9 + type LinkHandler = (l: Linkrement) => void; 10 + 11 + /** 12 + * simple spacedust demo client 13 + * 14 + * only purpose for now is to serve this demo so it might contain hacks 15 + **/ 16 + export class Spacedust { 17 + #callback: LinkHandler; 18 + #endpoint: string; 19 + 20 + // #socket must be null when #status is 'disconnected' 21 + #socket: WebSocket | null = null; 22 + #status: SpacedustStatus = 'disconnected'; 23 + 24 + #subjects: string[]; 25 + #subjectsDirty: boolean = false; // in case we try to update while disconnected 26 + #sources: string[] = Object.keys(linkSources); // hard-coding for demo 27 + #eol: boolean = false; // flag: we should shut down 28 + 29 + constructor( 30 + onLink: LinkHandler, 31 + endpoint: string = 'https://spacedust.microcosm.blue', 32 + subjects: string[] = [], 33 + ) { 34 + this.#callback = onLink; 35 + this.#endpoint = endpoint; 36 + this.#subjects = subjects; 37 + this.#connect(); 38 + } 39 + 40 + async #connect(reconnecting: boolean = false) { 41 + this.#status = 'connecting'; 42 + 43 + if (reconnecting) { 44 + const wait = Math.round(1000 + (Math.random() * 1800)); 45 + console.info(`waiting ${(wait / 1000).toFixed(1)}s to reconnect...`); 46 + await new Promise(r => setTimeout(r, wait)); 47 + } 48 + if (this.#eol) return this.close(); 49 + 50 + // up to date as of this connection init 51 + this.#subjectsDirty = false; 52 + 53 + if (this.#subjects.length === 0) { 54 + console.info('no subjects, not connecting spacedust to avoid getting firehosed'); 55 + this.#status = 'disconnected'; 56 + return; 57 + } 58 + 59 + const url = new URL('/subscribe', this.#endpoint); 60 + url.searchParams.set('instant', 'true'); 61 + 62 + for (const source of this.#sources) { 63 + url.searchParams.append('wantedSources', source); 64 + } 65 + 66 + // note: here we put all subjects in the url 67 + // that's fine since we only have a few for this demo 68 + // but spacedust accepts up to 50,000 subjects! more than fit in a url-- you 69 + // have to send a subscriber sourced message in that case after reconnect. 70 + for (const subject of this.#subjects) { 71 + url.searchParams.append('wantedSubjects', subject); 72 + } 73 + 74 + this.#socket = new WebSocket(url); 75 + 76 + this.#socket.onopen = () => { 77 + console.info('spacedust connected.'); 78 + if (this.#eol) return this.close(); 79 + this.#status = 'connected'; 80 + // in case the subjects were changed while connecting 81 + if (this.#subjectsDirty) { 82 + this.setSubjects(this.#subjects); 83 + } 84 + }; 85 + 86 + this.#socket.onmessage = message => { 87 + if (this.#eol) return this.close(); 88 + this.#handleMessage(message); 89 + }; 90 + 91 + this.#socket.onerror = err => { 92 + console.warn('spacedust socket errored. reconnecting...', err); 93 + this.#status = 'disconnected'; 94 + this.#connect(true); 95 + }; 96 + 97 + this.#socket.onclose = () => { 98 + if (this.#eol) { 99 + console.info('spacedust socket closed and we\'re EOL, not restarting'); 100 + return; 101 + } 102 + console.info('spacedust socket closed. restarting...'); 103 + this.#status = 'disconnected'; 104 + this.#connect(true); 105 + }; 106 + } 107 + 108 + #handleMessage(m: MessageEvent) { 109 + if (this.#eol) return; 110 + const data = JSON.parse(m.data); 111 + if (data.kind !== "link" || data.link.operation !== "create") { 112 + console.info('ignoring non-link-create event', data); 113 + return; 114 + } 115 + const { link: { subject, source } } = data; 116 + this.#callback({ subject, source }); 117 + } 118 + 119 + setSubjects(newSubjects: string[]) { 120 + this.#subjects = newSubjects; 121 + 122 + if (this.#subjects.length === 0) { 123 + // no subjects specified: just disconnect (would get firehose from spacedust) 124 + this.#socket?.close(); 125 + // closing should trigger the .onclose handler to take it from here 126 + return; 127 + } 128 + 129 + if (this.#status === 'disconnected') { 130 + console.info('spacedust currently disconnected; connecting for updated subjects'); 131 + this.#subjectsDirty = true; 132 + this.#connect(); 133 + return; 134 + } else if (this.#status === 'connecting') { 135 + console.info('spacedust currently connecting; just flagging subjects as dirty'); 136 + this.#subjectsDirty = true; // on connect it should automatically update 137 + return; 138 + } 139 + 140 + if (!this.#socket) { 141 + throw new Error(`spacedust status is "${this.#status}" but the socket is null -- a bug?`); 142 + } 143 + 144 + this.#socket.send(JSON.stringify({ 145 + type: 'options_update', 146 + payload: { 147 + wantedSources: this.#sources, 148 + wantedSubjects: this.#subjects, 149 + }, 150 + })); 151 + 152 + this.#subjectsDirty = false; 153 + } 154 + 155 + close() { 156 + this.#eol = true; 157 + this.#socket?.close(); 158 + } 159 + 160 + }
+1
live-embed/src/vite-env.d.ts
··· 1 + /// <reference types="vite/client" />
+27
live-embed/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 + "skipLibCheck": true, 9 + 10 + /* Bundler mode */ 11 + "moduleResolution": "bundler", 12 + "allowImportingTsExtensions": true, 13 + "verbatimModuleSyntax": true, 14 + "moduleDetection": "force", 15 + "noEmit": true, 16 + "jsx": "react-jsx", 17 + 18 + /* Linting */ 19 + "strict": true, 20 + "noUnusedLocals": true, 21 + "noUnusedParameters": true, 22 + "erasableSyntaxOnly": true, 23 + "noFallthroughCasesInSwitch": true, 24 + "noUncheckedSideEffectImports": true 25 + }, 26 + "include": ["src"] 27 + }
+7
live-embed/tsconfig.json
··· 1 + { 2 + "files": [], 3 + "references": [ 4 + { "path": "./tsconfig.app.json" }, 5 + { "path": "./tsconfig.node.json" } 6 + ] 7 + }
+25
live-embed/tsconfig.node.json
··· 1 + { 2 + "compilerOptions": { 3 + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 + "target": "ES2023", 5 + "lib": ["ES2023"], 6 + "module": "ESNext", 7 + "skipLibCheck": true, 8 + 9 + /* Bundler mode */ 10 + "moduleResolution": "bundler", 11 + "allowImportingTsExtensions": true, 12 + "verbatimModuleSyntax": true, 13 + "moduleDetection": "force", 14 + "noEmit": true, 15 + 16 + /* Linting */ 17 + "strict": true, 18 + "noUnusedLocals": true, 19 + "noUnusedParameters": true, 20 + "erasableSyntaxOnly": true, 21 + "noFallthroughCasesInSwitch": true, 22 + "noUncheckedSideEffectImports": true 23 + }, 24 + "include": ["vite.config.ts"] 25 + }
+7
live-embed/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 + })
+351
server/api.js
··· 1 + import fs from 'node:fs'; 2 + import http from 'http'; 3 + import { jwtVerify } from 'jose'; 4 + import cookie from 'cookie'; 5 + import cookieSig from 'cookie-signature'; 6 + import { v4 as uuidv4 } from 'uuid'; 7 + 8 + const replyJson = (res, code) => res.setHeader('Content-Type', 'application/json').writeHead(code); 9 + const errJson = (code, reason) => res => replyJson(res, code).end(JSON.stringify({ reason })); 10 + 11 + const ok = (res, data) => replyJson(res, 200).end(JSON.stringify(data)); 12 + const gotIt = res => res.writeHead(201).end(); 13 + const okBye = res => res.writeHead(204).end(); 14 + const notModified = res => res.writeHead(304).end(); 15 + const badRequest = (res, reason) => errJson(400, reason)(res); 16 + const forbidden = errJson(401, 'forbidden'); 17 + const unauthorized = errJson(403, 'unauthorized'); 18 + const notFound = errJson(404, 'not found'); 19 + const conflict = errJson(409, 'conflict'); 20 + const serverError = errJson(500, 'internal server error'); 21 + 22 + const getRequesBody = async req => new Promise((resolve, reject) => { 23 + let body = ''; 24 + req.on('data', chunk => body += chunk); 25 + req.on('end', () => resolve(body)); 26 + req.on('error', err => reject(err)); 27 + }); 28 + 29 + const COOKIE_BASE = { httpOnly: true, secure: true, partitioned: true, sameSite: 'None' }; 30 + const setAccountCookie = (res, did, session, appSecret) => res.setHeader('Set-Cookie', cookie.serialize( 31 + 'verified-account', 32 + cookieSig.sign(JSON.stringify([did, session]), appSecret), 33 + { ...COOKIE_BASE, maxAge: 90 * 86_400 }, 34 + )); 35 + const clearAccountCookie = res => res.setHeader('Set-Cookie', cookie.serialize( 36 + 'verified-account', 37 + '', 38 + { ...COOKIE_BASE, expires: new Date(0) }, 39 + )); 40 + 41 + const getUser = (req, res, db, appSecret, adminDid) => { 42 + const cookies = cookie.parse(req.headers.cookie ?? ''); 43 + const untrusted = cookies['verified-account'] ?? ''; 44 + const json = cookieSig.unsign(untrusted, appSecret); 45 + if (!json) { 46 + clearAccountCookie(res); 47 + return null; 48 + } 49 + let did, session; 50 + try { 51 + [did, session] = JSON.parse(json); 52 + } catch (e) { 53 + console.warn('validated account cookie but failed to parse json', e); 54 + clearAccountCookie(res); 55 + return null; 56 + } 57 + let role; 58 + if (did === adminDid) { 59 + role = 'admin'; 60 + } else { 61 + const account = db.getAccount(did); 62 + if (!account) { 63 + console.warn('valid account cookie but could not find in db'); 64 + clearAccountCookie(res); 65 + return null; 66 + } 67 + role = account.role ?? 'public'; 68 + } 69 + return { did, session, role }; 70 + }; 71 + 72 + /////// handlers 73 + 74 + // never EVER allow user-controllable input into fname (or just fix the path joining) 75 + const handleFile = (fname, ftype) => async (req, res, replace = {}) => { 76 + let content 77 + try { 78 + content = await fs.promises.readFile(`./web-content/${fname}`); // DANGERDANGER 79 + content = content.toString(); 80 + } catch (err) { 81 + console.error(err); 82 + return serverError(res); 83 + } 84 + res.setHeader('Content-Type', ftype); 85 + res.writeHead(200); 86 + for (let k in replace) { 87 + content = content.replace(k, JSON.stringify(replace[k])); 88 + } 89 + res.end(content); 90 + } 91 + const handleIndex = handleFile('index.html', 'text/html'); 92 + 93 + const handleVerify = async (db, req, res, secrets, jwks, adminDid) => { 94 + const body = await getRequesBody(req); 95 + const { token } = JSON.parse(body); 96 + let did; 97 + try { 98 + const verified = await jwtVerify(token, jwks); 99 + did = verified.payload.sub; 100 + } catch (e) { 101 + console.warn('jwks verification failed', e); 102 + return badRequest(res, 'token verification failed'); 103 + } 104 + const isAdmin = did && did === adminDid; 105 + db.addAccount(did); 106 + const session = uuidv4(); 107 + setAccountCookie(res, did, session, secrets.appSecret); 108 + return ok(res, { 109 + webPushPublicKey: secrets.pushKeys.publicKey, 110 + role: isAdmin ? 'admin' : 'public', 111 + did, 112 + }); 113 + }; 114 + 115 + const handleHello = async (user, req, res, webPushPublicKey, whoamiHost) => 116 + ok(res, { 117 + whoamiHost, 118 + webPushPublicKey, 119 + role: user?.role ?? 'anonymous', 120 + did: user?.did, 121 + }); 122 + 123 + const handleSubscribe = async (db, user, req, res, updateSubs) => { 124 + const body = await getRequesBody(req); 125 + const { sub } = JSON.parse(body); 126 + try { 127 + db.addPushSub(user.did, user.session, JSON.stringify(sub)); 128 + } catch (e) { 129 + console.warn('failed to add sub', e); 130 + return serverError(res); 131 + } 132 + updateSubs(db); 133 + return gotIt(res); 134 + }; 135 + 136 + const handlePushTest = async (db, user, res, push) => { 137 + const subscription = db.getSubBySession(user.session); 138 + const payload = JSON.stringify({ 139 + subject: user.did, 140 + source: 'blue.microcosm.test.notification:hello', 141 + source_record: `at://${user.did}/blue.microcosm.test.notification/test`, 142 + timestamp: +new Date(), 143 + }); 144 + await push(db, subscription, payload); 145 + return okBye(res); 146 + }; 147 + 148 + const handleLogout = async (db, user, req, res, appSecret, updateSubs) => { 149 + try { 150 + db.deleteSub(user.session); 151 + } catch (e) { 152 + console.warn('failed to remove sub', e); 153 + return serverError(res); 154 + } 155 + updateSubs(db); 156 + clearAccountCookie(res); 157 + return okBye(res); 158 + }; 159 + 160 + const handleTopSecret = async (db, user, req, res) => { 161 + // TODO: succeed early if they're already in? 162 + const body = await getRequesBody(req); 163 + const { secret_password } = JSON.parse(body); 164 + const { did } = user; 165 + const role = 'early'; 166 + const updated = db.setRole({ did, role, secret_password }); 167 + if (updated) { 168 + return okBye(res); 169 + } else { 170 + return forbidden(res); 171 + } 172 + }; 173 + 174 + const handleGetGlobalNotifySettings = async (db, user, res) => { 175 + const settings = db.getNotifyAccountGlobals(user.did); 176 + return ok(res, settings); 177 + }; 178 + 179 + const handleSetGlobalNotifySettings = async (db, user, req, res) => { 180 + const body = await getRequesBody(req); 181 + const { notify_enabled, notify_self } = JSON.parse(body); 182 + db.setNotifyAccountGlobals(user.did, { notify_enabled, notify_self }); 183 + return gotIt(res); 184 + }; 185 + 186 + const handleGetNotificationFilter = async (db, user, searchParams, res) => { 187 + const selector = searchParams.get('selector'); 188 + if (!selector) return badRequest(res, '"selector" required in search query'); 189 + 190 + const selection = searchParams.get('selection'); 191 + if (!selection) return badRequest(res, '"selection" required in search query'); 192 + 193 + const { did } = user; 194 + 195 + const notify = db.getNotificationFilter(did, selector, selection) ?? null; 196 + return ok(res, { notify }); 197 + }; 198 + 199 + const handleSetNotificationFilter = async (db, user, req, res) => { 200 + const body = await getRequesBody(req); 201 + const { selector, selection, notify } = JSON.parse(body); 202 + const { did } = user; 203 + db.setNotificationFilter(did, selector, selection, notify); 204 + return ok(res, { notify }); 205 + }; 206 + 207 + /// admin stuff 208 + 209 + const handleListSecrets = async (db, res) => { 210 + const secrets = db.getSecrets(); 211 + return ok(res, secrets); 212 + }; 213 + 214 + const handleAddSecret = async (db, req, res) => { 215 + const body = await getRequesBody(req); 216 + const { secret_password } = JSON.parse(body); 217 + try { 218 + db.addTopSecret(secret_password); 219 + } catch (e) { 220 + if (['SQLITE_CONSTRAINT_PRIMARYKEY', 'SQLITE_CONSTRAINT_CHECK'].includes(e.code)) { 221 + return conflict(res); 222 + } 223 + throw e; 224 + } 225 + return gotIt(res); 226 + }; 227 + 228 + const handleExpireSecret = async (db, req, res) => { 229 + const body = await getRequesBody(req); 230 + const { secret_password } = JSON.parse(body); 231 + if (db.expireTopSecret(secret_password)) { 232 + return gotIt(res); 233 + } else { 234 + return notModified(res); 235 + } 236 + }; 237 + 238 + const handleTopSecretAccounts = async (db, req, res, searchParams) => { 239 + const secret = searchParams.get('secret_password'); 240 + const accounts = secret ? db.getSecretAccounts(secret) : db.getNonSecretAccounts(); 241 + return ok(res, accounts); 242 + }; 243 + 244 + 245 + /////// end handlers 246 + 247 + const attempt = listener => async (req, res) => { 248 + console.log(`-> ${req.method} ${req.url}`); 249 + try { 250 + await listener(req, res); 251 + console.log(` <-${req.method} ${req.url} (${res.statusCode})`); 252 + } catch (e) { 253 + console.error('listener errored:', e); 254 + return serverError(res); 255 + } 256 + }; 257 + 258 + const withCors = (allowedOrigin, listener) => { 259 + const corsHeaders = new Headers({ 260 + 'Access-Control-Allow-Origin': allowedOrigin, 261 + 'Access-Control-Allow-Methods': 'OPTIONS, GET, POST', 262 + 'Access-Control-Allow-Headers': 'Content-Type', 263 + 'Access-Control-Allow-Credentials': 'true', 264 + }); 265 + return (req, res) => { 266 + res.setHeaders(corsHeaders); 267 + if (req.method === 'OPTIONS') { 268 + return okBye(res); 269 + } 270 + return listener(req, res); 271 + } 272 + } 273 + 274 + export const server = (secrets, jwks, allowedOrigin, whoamiHost, db, updateSubs, push, adminDid) => { 275 + const handler = (req, res) => { 276 + // don't love this but whatever 277 + const { pathname, searchParams } = new URL(`http://localhost${req.url}`); 278 + const { method } = req; 279 + 280 + // public (we're doing fall-through auth, what could go wrong) 281 + if (method === 'GET' && pathname === '/') { 282 + return handleIndex(req, res, {}); 283 + } 284 + if (method === 'POST' && pathname === '/verify') { 285 + return handleVerify(db, req, res, secrets, jwks, adminDid); 286 + } 287 + 288 + // semi-public 289 + const user = getUser(req, res, db, secrets.appSecret, adminDid); 290 + if (method === 'GET' && pathname === '/hello') { 291 + return handleHello(user, req, res, secrets.pushKeys.publicKey, whoamiHost); 292 + } 293 + 294 + // login required 295 + if (method === 'POST' && pathname === '/logout') { 296 + if (!user) return unauthorized(res); 297 + return handleLogout(db, user, req, res, secrets.appSecret, updateSubs); 298 + } 299 + if (method === 'POST' && pathname === '/super-top-secret-access') { 300 + if (!user) return unauthorized(res); 301 + return handleTopSecret(db, user, req, res); 302 + } 303 + if (method === 'GET' && pathname === '/global-notify') { 304 + if (!user) return unauthorized(res); 305 + return handleGetGlobalNotifySettings(db, user, res); 306 + } 307 + if (method === 'POST' && pathname === '/global-notify') { 308 + if (!user) return unauthorized(res); 309 + return handleSetGlobalNotifySettings(db, user, req, res); 310 + } 311 + if (method === 'GET' && pathname === '/notification-filter') { 312 + if (!user) return unauthorized(res); 313 + return handleGetNotificationFilter(db, user, searchParams, res); 314 + } 315 + if (method === 'POST' && pathname === '/notification-filter') { 316 + if (!user) return unauthorized(res); 317 + return handleSetNotificationFilter(db, user, req, res); 318 + } 319 + 320 + // non-public access required 321 + if (method === 'POST' && pathname === '/subscribe') { 322 + if (!user || user.role === 'public') return forbidden(res); 323 + return handleSubscribe(db, user, req, res, updateSubs); 324 + } 325 + if (method === 'POST' && pathname === '/push-test') { 326 + if (!user || user.role === 'public') return forbidden(res); 327 + return handlePushTest(db, user, res, push); 328 + } 329 + 330 + // admin required (just 404 for non-admin) 331 + if (user?.role === 'admin') { 332 + if (method === 'GET' && pathname === '/top-secrets') { 333 + return handleListSecrets(db, res); 334 + } 335 + if (method === 'POST' && pathname === '/top-secret') { 336 + return handleAddSecret(db, req, res); 337 + } 338 + if (method === 'POST' && pathname === '/expire-top-secret') { 339 + return handleExpireSecret(db, req, res); 340 + } 341 + if (method === 'GET' && pathname === '/top-secret-accounts') { 342 + return handleTopSecretAccounts(db, req, res, searchParams); 343 + } 344 + } 345 + 346 + // sigh 347 + return notFound(res); 348 + }; 349 + 350 + return http.createServer(attempt(withCors(allowedOrigin, handler))); 351 + }
+177 -7
server/db.js
··· 2 2 import Database from 'better-sqlite3'; 3 3 4 4 const SUBS_PER_ACCOUNT_LIMIT = 5; 5 + const SECONDARY_FILTERS_LIMIT = 100; 6 + 5 7 const SCHEMA_FNAME = './schema.sql'; 6 8 7 9 export class DB { ··· 11 13 #stmt_insert_push_sub; 12 14 #stmt_get_all_sub_dids; 13 15 #stmt_get_push_subs; 16 + #stmt_get_push_sub; 14 17 #stmt_update_push_sub; 15 18 #stmt_delete_push_sub; 16 19 #stmt_get_push_info; 17 20 #stmt_set_role; 21 + #stmt_get_notify_account_globals; 22 + #stmt_set_notify_account_globals; 23 + #stmt_set_notification_filter; 24 + #stmt_get_notification_filter; 25 + #stmt_count_notification_filters; 26 + #stmt_rm_notification_filter; 27 + 28 + #stmt_admin_add_secret; 29 + #stmt_admin_expire_secret; 30 + #stmt_admin_get_secrets; 31 + #stmt_admin_secret_accounts; 32 + #stmt_admin_nonsecret_accounts; 33 + 18 34 #transactionally; 19 35 #db; 20 36 ··· 72 88 from push_subs 73 89 where account_did = ?`); 74 90 91 + this.#stmt_get_push_sub = db.prepare( 92 + `select session, 93 + subscription, 94 + (julianday(CURRENT_TIMESTAMP) - julianday(last_push)) * 24 * 60 * 60 95 + as 'since_last_push' 96 + from push_subs 97 + where session = ?`); 98 + 75 99 this.#stmt_update_push_sub = db.prepare( 76 100 `update push_subs 77 101 set last_push = CURRENT_TIMESTAMP, ··· 91 115 92 116 this.#stmt_set_role = db.prepare( 93 117 `update accounts 94 - set role = ?, 95 - secret_password = ? 96 - where did = ?`); 118 + set role = :role, 119 + secret_password = :secret_password 120 + where did = :did 121 + and :secret_password in (select password 122 + from top_secret_passwords)`); 123 + 124 + this.#stmt_get_notify_account_globals = db.prepare( 125 + `select notify_enabled, 126 + notify_self 127 + from accounts 128 + where did = :did`); 129 + 130 + this.#stmt_set_notify_account_globals = db.prepare( 131 + `update accounts 132 + set notify_enabled = :notify_enabled, 133 + notify_self = :notify_self 134 + where did = :did`); 135 + 136 + this.#stmt_set_notification_filter = db.prepare( 137 + `insert into notification_filters (account_did, selector, selection, notify) 138 + values (:did, :selector, :selection, :notify) 139 + on conflict do update 140 + set notify = excluded.notify`); 141 + 142 + this.#stmt_get_notification_filter = db.prepare( 143 + `select notify 144 + from notification_filters 145 + where account_did = :did 146 + and selector = :selector 147 + and selection = :selection`); 148 + 149 + this.#stmt_count_notification_filters = db.prepare( 150 + `select count(*) as n 151 + from notification_filters 152 + where account_did = :did`); 153 + 154 + this.#stmt_rm_notification_filter = db.prepare( 155 + `delete from notification_filters 156 + where account_did = :did 157 + and selector = :selector 158 + and selection = :selection`); 159 + 160 + 161 + this.#stmt_admin_add_secret = db.prepare( 162 + `insert into top_secret_passwords (password) 163 + values (?)`); 164 + 165 + this.#stmt_admin_expire_secret = db.prepare( 166 + `update top_secret_passwords 167 + set expired = CURRENT_TIMESTAMP 168 + where expired is null 169 + and password = ?`); 170 + 171 + this.#stmt_admin_get_secrets = db.prepare( 172 + `select password, 173 + unixepoch(added) * 1000 as 'added', 174 + unixepoch(expired) * 1000 as 'expired' 175 + from top_secret_passwords 176 + order by expired, added desc`); 177 + 178 + this.#stmt_admin_secret_accounts = db.prepare( 179 + `select did, 180 + unixepoch(first_seen) * 1000 as 'first_seen', 181 + role, 182 + count(*) as 'active_subs', 183 + sum(p.total_pushes) as 'total_pushes', 184 + unixepoch(max(p.last_push)) * 1000 as 'last_push' 185 + from accounts 186 + left outer join push_subs p on (p.account_did = did) 187 + where secret_password = :password 188 + group by did 189 + order by first_seen desc`); 190 + 191 + this.#stmt_admin_nonsecret_accounts = db.prepare( 192 + `select did, 193 + unixepoch(first_seen) * 1000 as 'first_seen', 194 + role, 195 + count(*) as 'active_subs', 196 + sum(p.total_pushes) as 'total_pushes', 197 + unixepoch(max(p.last_push)) * 1000 as 'last_push' 198 + from accounts 199 + left outer join push_subs p on (p.account_did = did) 200 + left outer join top_secret_passwords s on (s.password = secret_password) 201 + where s.password is null 202 + group by did 203 + order by first_seen desc`); 97 204 98 205 this.#transactionally = t => db.transaction(t).immediate(); 99 206 } ··· 127 234 return this.#stmt_get_push_subs.all(did); 128 235 } 129 236 237 + getSubBySession(session) { 238 + return this.#stmt_get_push_sub.get(session); 239 + } 240 + 130 241 updateLastPush(session) { 131 242 this.#stmt_update_push_sub.run(session); 132 243 } ··· 135 246 this.#stmt_delete_push_sub.run(session); 136 247 } 137 248 138 - setRole(did, role, secret_password) { 139 - let res = this.#stmt_set_role.run(role, secret_password, did); 140 - if (res.changes === 0) { 141 - throw new Error('no changes'); 249 + setRole(params) { 250 + let res = this.#stmt_set_role.run(params); 251 + return res.changes > 0; 252 + } 253 + 254 + getNotifyAccountGlobals(did) { 255 + return this.#stmt_get_notify_account_globals.get({ did }); 256 + } 257 + 258 + setNotifyAccountGlobals(did, globals) { 259 + this.#transactionally(() => { 260 + const update = this.getNotifyAccountGlobals(did); 261 + if (globals.notify_enabled !== undefined) update.notify_enabled = +globals.notify_enabled; 262 + if (globals.notify_self !== undefined) update.notify_self = +globals.notify_self; 263 + update.did = did; 264 + this.#stmt_set_notify_account_globals.run(update); 265 + }); 266 + } 267 + 268 + getNotificationFilter(did, selector, selection) { 269 + const res = this.#stmt_get_notification_filter.get({ did, selector, selection }); 270 + const dbNotify = res?.notify; 271 + if (dbNotify === 1) return true; 272 + else if (dbNotify === 0) return false; 273 + else return null; 274 + } 275 + 276 + setNotificationFilter(did, selector, selection, notify) { 277 + if (notify === null) { 278 + this.#stmt_rm_notification_filter.run({ did, selector, selection }); 279 + } else { 280 + this.#transactionally(() => { 281 + const { n } = this.#stmt_count_notification_filters.get({ did }); 282 + if (n >= SECONDARY_FILTERS_LIMIT) { 283 + throw new Error('max filters set for account'); 284 + } 285 + let dbNotify = null; 286 + if (notify === true) dbNotify = 1; 287 + else if (notify === false) dbNotify = 0; 288 + this.#stmt_set_notification_filter.run({ did, selector, selection, notify: dbNotify }); 289 + }); 142 290 } 291 + } 292 + 293 + 294 + addTopSecret(secretPassword) { 295 + this.#stmt_admin_add_secret.run(secretPassword); 296 + } 297 + 298 + expireTopSecret(secretPassword) { 299 + let res = this.#stmt_admin_expire_secret.run(secretPassword); 300 + return res.changes > 0; 301 + } 302 + 303 + getSecrets() { 304 + return this.#stmt_admin_get_secrets.all(); 305 + } 306 + 307 + getSecretAccounts(secretPassword) { 308 + return this.#stmt_admin_secret_accounts.all({ password: secretPassword }); 309 + } 310 + 311 + getNonSecretAccounts() { 312 + return this.#stmt_admin_nonsecret_accounts.all(); 143 313 } 144 314 }
+29 -399
server/index.js
··· 1 1 #!/usr/bin/env node 2 - "use strict"; 3 2 3 + import { createRemoteJWKSet } from 'jose'; 4 4 import fs from 'node:fs'; 5 5 import { randomBytes } from 'node:crypto'; 6 - import http from 'http'; 7 - import * as jose from 'jose'; 8 - import cookie from 'cookie'; 9 - import cookieSig from 'cookie-signature'; 10 - import lexicons from 'lexicons'; 11 - import psl from 'psl'; 6 + import https from 'node:https'; 12 7 import webpush from 'web-push'; 13 - import WebSocket from 'ws'; 14 - import { v4 as uuidv4 } from 'uuid'; 15 8 import { DB } from './db.js'; 16 - 17 - // kind of silly but right now there's no way to tell spacedust that we want an alive connection 18 - // but don't want the notification firehose (everything filtered out) 19 - // so... the final filter is an absolute on this fake did, effectively filtering all notifs. 20 - // (this is only used when there are no subscribers registered) 21 - const DUMMY_DID = 'did:plc:zzzzzzzzzzzzzzzzzzzzzzzz'; 22 - 23 - const CORS_PERMISSIVE = req => ({ 24 - 'Access-Control-Allow-Origin': req.headers.origin, // DANGERRRRR 25 - 'Access-Control-Allow-Methods': 'OPTIONS, GET, POST', 26 - 'Access-Control-Allow-Headers': 'Content-Type', 27 - 'Access-Control-Allow-Credentials': 'true', // TODO: *def* want to restrict allowed origin, probably 28 - }); 29 - 30 - let spacedust; 31 - let spacedustEverStarted = false; 32 - 33 - 34 - const updateSubs = db => { 35 - if (!spacedust) { 36 - console.warn('not updating subscription, no spacedust (reconnecting?)'); 37 - return; 38 - } 39 - const wantedSubjectDids = db.getSubscribedDids(); 40 - if (wantedSubjectDids.length === 0) { 41 - wantedSubjectDids.push(DUMMY_DID); 42 - } 43 - console.log('updating for wantedSubjectDids', wantedSubjectDids); 44 - spacedust.send(JSON.stringify({ 45 - type: 'options_update', 46 - payload: { 47 - wantedSubjectDids, 48 - }, 49 - })); 50 - }; 51 - 52 - async function push(db, pushSubscription, payload) { 53 - const { session, subscription, since_last_push } = pushSubscription; 54 - if (since_last_push !== null && since_last_push < 1.618) { 55 - console.warn(`rate limiter: dropping too-soon push (${since_last_push})`); 56 - return; 57 - } 58 - 59 - let sub; 60 - try { 61 - sub = JSON.parse(subscription); 62 - } catch (e) { 63 - console.error('failed to parse subscription json, dropping session', e); 64 - db.deleteSub(session); 65 - return; 66 - } 67 - 68 - try { 69 - await webpush.sendNotification(sub, payload); 70 - } catch (err) { 71 - if (400 <= err.statusCode && err.statusCode < 500) { 72 - console.info(`removing sub for ${err.statusCode}`); 73 - db.deleteSub(session); 74 - return; 75 - } else { 76 - console.warn('something went wrong for another reason', err); 77 - } 78 - } 79 - 80 - db.updateLastPush(session); 81 - } 82 - 83 - const isTorment = source => { 84 - try { 85 - const [nsid, ...rp] = source.split(':'); 86 - 87 - let parts = nsid.split('.'); 88 - parts.reverse(); 89 - parts = parts.join('.'); 90 - 91 - // const unreversed = parts.toReversed().join('.'); 92 - 93 - const app = psl.parse(parts)?.domain ?? 'unknown'; 94 - 95 - let appPrefix = app.split('.'); 96 - appPrefix.reverse(); 97 - appPrefix = appPrefix.join('.') 98 - 99 - return source.slice(app.length + 1) in lexicons[appPrefix]?.torment_sources; 100 - } catch (e) { 101 - console.error('checking tormentedness failed, allowing through', e); 102 - return false; 103 - } 104 - } 105 - 106 - const handleDust = db => async event => { 107 - console.log('got', event.data); 108 - let data; 109 - try { 110 - data = JSON.parse(event.data); 111 - } catch (err) { 112 - console.error(err); 113 - return; 114 - } 115 - const { link: { subject, source, source_record } } = data; 116 - if (isTorment(source)) { 117 - console.log('nope! not today,', source); 118 - return; 119 - } 120 - const timestamp = +new Date(); 121 - 122 - let did; 123 - if (subject.startsWith('did:')) did = subject; 124 - else if (subject.startsWith('at://')) { 125 - const [id, ..._] = subject.slice('at://'.length).split('/'); 126 - if (id.startsWith('did:')) did = id; 127 - } 128 - if (!did) { 129 - console.warn(`ignoring link with non-DID subject: ${subject}`) 130 - return; 131 - } 132 - 133 - const subs = db.getSubsByDid(did); 134 - const payload = JSON.stringify({ subject, source, source_record, timestamp }); 135 - await Promise.all(subs.map(pushSubscription => push(db, pushSubscription, payload))); 136 - }; 137 - 138 - const connectSpacedust = (db, host) => { 139 - spacedust = new WebSocket(`${host}/subscribe?instant=true&wantedSubjectDids=${DUMMY_DID}`); 140 - let restarting = false; 141 - 142 - const restart = () => { 143 - if (restarting) return; 144 - restarting = true; 145 - let wait = Math.round(500 + (Math.random() * 1000)); 146 - console.info(`restarting spacedust connection in ${wait}ms...`); 147 - setTimeout(() => connectSpacedust(db, host), wait); 148 - spacedust = null; 149 - } 150 - 151 - spacedust.onopen = () => updateSubs(db); 152 - spacedust.onmessage = handleDust(db); 153 - 154 - spacedust.onerror = e => { 155 - console.error('spacedust errored:', e); 156 - restart(); 157 - }; 158 - 159 - spacedust.onclose = () => { 160 - console.log('spacedust closed'); 161 - restart(); 162 - }; 163 - } 9 + import { connectSpacedust } from './notifications.js'; 10 + import { server } from './api.js'; 164 11 165 12 const getOrCreateSecrets = filename => { 166 13 let secrets; ··· 180 27 return secrets; 181 28 } 182 29 183 - const getRequesBody = async req => new Promise((resolve, reject) => { 184 - let body = ''; 185 - req.on('data', chunk => body += chunk); 186 - req.on('end', () => resolve(body)); 187 - req.on('error', err => reject(err)); 188 - }); 189 - 190 - const COOKIE_BASE = { httpOnly: true, secure: true, partitioned: true, sameSite: 'None' }; 191 - const setAccountCookie = (res, did, session, appSecret) => res.setHeader('Set-Cookie', cookie.serialize( 192 - 'verified-account', 193 - cookieSig.sign(JSON.stringify([did, session]), appSecret), 194 - { ...COOKIE_BASE, maxAge: 90 * 86_400 }, 195 - )); 196 - const clearAccountCookie = res => res.setHeader('Set-Cookie', cookie.serialize( 197 - 'verified-account', 198 - '', 199 - { ...COOKIE_BASE, expires: new Date(0) }, 200 - )); 201 - 202 - const getAccountCookie = (req, res, appSecret, adminDid, noDidCheck = false) => { 203 - const cookies = cookie.parse(req.headers.cookie ?? ''); 204 - const untrusted = cookies['verified-account'] ?? ''; 205 - const json = cookieSig.unsign(untrusted, appSecret); 206 - if (!json) { 207 - clearAccountCookie(res); 208 - return null; 209 - } 210 - let did, session; 211 - try { 212 - [did, session] = JSON.parse(json); 213 - } catch (e) { 214 - console.warn('validated account cookie but failed to parse json', e); 215 - clearAccountCookie(res); 216 - return null; 217 - } 218 - 219 - // not yet public!! 220 - if (!did || (did !== adminDid && !noDidCheck)) { 221 - console.log('no, clearing you', did, did === adminDid, noDidCheck); 222 - clearAccountCookie(res) 223 - .setHeader('Content-Type', 'application/json') 224 - .writeHead(403) 225 - .end(JSON.stringify({ 226 - reason: 'the spacedust notifications demo isn\'t public yet!', 227 - })); 228 - throw new Error('unauthorized'); 229 - } 230 - 231 - return [did, session, did && (did === adminDid)]; 232 - }; 233 - 234 - // never EVER allow user-controllable input into fname (or just fix the path joining) 235 - const handleFile = (fname, ftype) => async (req, res, replace = {}) => { 236 - let content 237 - try { 238 - content = await fs.promises.readFile(`./web-content/${fname}`); // DANGERDANGER 239 - content = content.toString(); 240 - } catch (err) { 241 - console.error(err); 242 - res.writeHead(500); 243 - res.end('Internal server error'); 244 - return; 245 - } 246 - res.setHeader('Content-Type', ftype); 247 - res.writeHead(200); 248 - for (let k in replace) { 249 - content = content.replace(k, JSON.stringify(replace[k])); 250 - } 251 - res.end(content); 252 - } 253 - const handleIndex = handleFile('index.html', 'text/html'); 254 - const handleServiceWorker = handleFile('service-worker.js', 'application/javascript'); 255 - 256 - const handleHello = async (db, req, res, secrets, whoamiHost, adminDid) => { 257 - const resBase = { webPushPublicKey: secrets.pushKeys.publicKey, whoamiHost }; 258 - res.setHeader('Content-Type', 'application/json'); 259 - let info = getAccountCookie(req, res, secrets.appSecret, adminDid, true); 260 - if (info) { 261 - const [did, _session, isAdmin] = info; 262 - let { role } = db.getAccount(did); 263 - role = isAdmin ? 'admin' : (role ?? 'public'); 264 - res 265 - .setHeader('Content-Type', 'application/json') 266 - .writeHead(200) 267 - .end(JSON.stringify({ ...resBase, role, did })); 268 - } else { 269 - res 270 - .setHeader('Content-Type', 'application/json') 271 - .writeHead(200) 272 - .end(JSON.stringify({ ...resBase, role: 'anonymous' })); 273 - } 274 - }; 275 - 276 - const handleVerify = async (db, req, res, secrets, whoamiHost, adminDid, jwks) => { 277 - const body = await getRequesBody(req); 278 - const { token } = JSON.parse(body); 279 - let did; 280 - try { 281 - const verified = await jose.jwtVerify(token, jwks); 282 - did = verified.payload.sub; 283 - } catch (e) { 284 - console.warn('jwks verification failed', e); 285 - return clearAccountCookie(res).writeHead(400).end(JSON.stringify({ reason: 'verification failed' })); 286 - } 287 - const isAdmin = did && did === adminDid; 288 - db.addAccount(did); 289 - const session = uuidv4(); 290 - setAccountCookie(res, did, session, secrets.appSecret); 291 - return res 292 - .setHeader('Content-Type', 'application/json') 293 - .writeHead(200) 294 - .end(JSON.stringify({ 295 - did, 296 - role: isAdmin ? 'admin' : 'public', 297 - webPushPublicKey: secrets.pushKeys.publicKey, 298 - })); 299 - }; 300 - 301 - const handleSubscribe = async (db, req, res, appSecret, adminDid) => { 302 - let info = getAccountCookie(req, res, appSecret, adminDid); 303 - if (!info) return res.writeHead(400).end(JSON.stringify({ reason: 'failed to verify cookie signature' })); 304 - const [did, session, _isAdmin] = info; 305 - const body = await getRequesBody(req); 306 - const { sub } = JSON.parse(body); 307 - // addSub('did:plc:z72i7hdynmk6r22z27h6tvur', sub); // DELETEME @bsky.app (DEBUG) 308 - try { 309 - db.addPushSub(did, session, JSON.stringify(sub)); 310 - } catch (e) { 311 - console.warn('failed to add sub', e); 312 - return res 313 - .setHeader('Content-Type', 'application/json') 314 - .writeHead(500) 315 - .end(JSON.stringify({ reason: 'failed to register subscription' })); 316 - } 317 - updateSubs(db); 318 - res.setHeader('Content-Type', 'application/json'); 319 - res.writeHead(201); 320 - res.end(JSON.stringify({ sup: 'hi' })); 321 - }; 322 - 323 - const handleLogout = async (db, req, res, appSecret) => { 324 - let info = getAccountCookie(req, res, appSecret, null, true); 325 - if (!info) return res.writeHead(400).end(JSON.stringify({ reason: 'failed to verify cookie signature' })); 326 - const [_did, session, _isAdmin] = info; 327 - try { 328 - db.deleteSub(session); 329 - } catch (e) { 330 - console.warn('failed to remove sub', e); 331 - return res 332 - .setHeader('Content-Type', 'application/json') 333 - .writeHead(500) 334 - .end(JSON.stringify({ reason: 'failed to register subscription' })); 335 - } 336 - updateSubs(db); 337 - res.setHeader('Content-Type', 'application/json'); 338 - res.writeHead(201); 339 - res.end(JSON.stringify({ sup: 'bye' })); 340 - } 30 + function startHealthcheckPing(endpoint) { 31 + const next = () => setTimeout(() => startHealthcheckPing(endpoint), 90 * 1000); 341 32 342 - const handleOpenSesame = async (db, req, res, appSecret) => { 343 - let info = getAccountCookie(req, res, appSecret, null, true); 344 - if (!info) return res.writeHead(400).end(JSON.stringify({ reason: 'failed to verify cookie signature' })); 345 - const [did, _session, _isAdmin] = info; 346 - const body = await getRequesBody(req); 347 - const { secret_password } = JSON.parse(body); 348 - console.log({ secret_password }); 349 - const role = 'early'; 350 - db.setRole(did, role, secret_password); 351 - res.setHeader('Content-Type', 'application/json') 352 - .writeHead(200) 353 - .end('"heyyy"'); 33 + https 34 + .get(endpoint, res => { 35 + if (res.statusCode !== 200) console.warn('non-200 health check response', res.statusCode); 36 + res 37 + .on('data', () => {}) 38 + .on('end', next); 39 + }) 40 + .on('error', err => { 41 + console.warn('healthcheck request errored', err); 42 + next(); 43 + }); 354 44 } 355 45 356 - const attempt = listener => async (req, res) => { 357 - console.log(`-> ${req.method} ${req.url}`); 358 - try { 359 - return await listener(req, res); 360 - } catch (e) { 361 - console.error('listener errored:', e); 362 - } 363 - }; 364 - 365 - const requestListener = (secrets, jwks, whoamiHost, db, adminDid) => attempt((req, res) => { 366 - if (req.method === 'GET' && req.url === '/') { 367 - return handleIndex(req, res, { PUBKEY: secrets.pushKeys.publicKey }); 368 - } 369 - if (req.method === 'GET' && req.url === '/service-worker.js') { 370 - return handleServiceWorker(req, res, { PUBKEY: secrets.pushKeys.publicKey }); 371 - } 372 - 373 - if (req.method === 'OPTIONS' && req.url === '/hello') { 374 - return res.writeHead(204, CORS_PERMISSIVE(req)).end(); 375 - } 376 - if (req.method === 'GET' && req.url === '/hello') { 377 - res.setHeaders(new Headers(CORS_PERMISSIVE(req))); 378 - return handleHello(db, req, res, secrets, whoamiHost, adminDid); 379 - } 380 - 381 - if (req.method === 'OPTIONS' && req.url === '/verify') { 382 - // TODO: probably restrict the origin 383 - return res.writeHead(204, CORS_PERMISSIVE(req)).end(); 384 - } 385 - if (req.method === 'POST' && req.url === '/verify') { 386 - res.setHeaders(new Headers(CORS_PERMISSIVE(req))); 387 - return handleVerify(db, req, res, secrets, whoamiHost, adminDid, jwks); 388 - } 389 - 390 - if (req.method === 'OPTIONS' && req.url === '/subscribe') { 391 - // TODO: probably restrict the origin 392 - return res.writeHead(204, CORS_PERMISSIVE(req)).end(); 393 - } 394 - if (req.method === 'POST' && req.url === '/subscribe') { 395 - res.setHeaders(new Headers(CORS_PERMISSIVE(req))); 396 - return handleSubscribe(db, req, res, secrets.appSecret, adminDid); 397 - } 398 - 399 - if (req.method === 'OPTIONS' && req.url === '/logout') { 400 - // TODO: probably restrict the origin 401 - return res.writeHead(204, CORS_PERMISSIVE(req)).end(); 402 - } 403 - if (req.method === 'POST' && req.url === '/logout') { 404 - res.setHeaders(new Headers(CORS_PERMISSIVE(req))); 405 - return handleLogout(db, req, res, secrets.appSecret); 406 - } 407 - 408 - if (req.method === 'OPTIONS' && req.url === '/super-top-secret-access') { 409 - // TODO: probably restrict the origin 410 - return res.writeHead(204, CORS_PERMISSIVE(req)).end(); 411 - } 412 - if (req.method === 'POST' && req.url === '/super-top-secret-access') { 413 - res.setHeaders(new Headers(CORS_PERMISSIVE(req))); 414 - return handleOpenSesame(db, req, res, secrets.appSecret); 415 - } 416 - 417 - res 418 - .setHeaders(new Headers(CORS_PERMISSIVE(req))) 419 - .writeHead(404) 420 - .end('not found (sorry)'); 421 - }); 422 - 423 46 const main = env => { 424 47 if (!env.ADMIN_DID) throw new Error('ADMIN_DID is required to run'); 425 48 const adminDid = env.ADMIN_DID; ··· 433 56 ); 434 57 435 58 const whoamiHost = env.WHOAMI_HOST ?? 'https://who-am-i.microcosm.blue'; 436 - const jwks = jose.createRemoteJWKSet(new URL(`${whoamiHost}/.well-known/jwks.json`)); 59 + const jwks = createRemoteJWKSet(new URL(`${whoamiHost}/.well-known/jwks.json`)); 437 60 438 61 const dbFilename = env.DB_FILE ?? './db.sqlite3'; 439 62 const initDb = process.argv.includes('--init-db'); ··· 441 64 const db = new DB(dbFilename, initDb); 442 65 443 66 const spacedustHost = env.SPACEDUST_HOST ?? 'wss://spacedust.microcosm.blue'; 444 - connectSpacedust(db, spacedustHost); 67 + const { updateSubs, push } = connectSpacedust(db, spacedustHost); 445 68 446 69 const host = env.HOST ?? 'localhost'; 447 70 const port = parseInt(env.PORT ?? 8000, 10); 448 71 449 - http 450 - .createServer(requestListener(secrets, jwks, whoamiHost, db, adminDid)) 451 - .listen(port, host, () => console.log(`listening at http://${host}:${port}`)); 72 + const allowedOrigin = env.ALLOWED_ORIGIN ?? 'http://127.0.0.1:5173'; 73 + 74 + if (env.HEALTHCHECK) startHealthcheckPing(env.HEALTHCHECK); 75 + else console.warn('no HEALTHCHECK in env, not sending healthcheck pings'); 76 + 77 + server(secrets, jwks, allowedOrigin, whoamiHost, db, updateSubs, push, adminDid).listen( 78 + port, 79 + host, 80 + () => console.log(`listening at http://${host}:${port} with allowed origin: ${allowedOrigin}`), 81 + ); 452 82 }; 453 83 454 84 main(process.env);
+201
server/notifications.js
··· 1 + import { default as lexicons, getBits } from 'lexicons'; 2 + import psl from 'psl'; 3 + import webpush from 'web-push'; 4 + import WebSocket from 'ws'; 5 + 6 + // kind of silly but right now there's no way to tell spacedust that we want an alive connection 7 + // but don't want the notification firehose (everything filtered out) 8 + // so... the final filter is an absolute on this fake did, effectively filtering all notifs. 9 + // (this is only used when there are no subscribers registered) 10 + const DUMMY_DID = 'did:plc:zzzzzzzzzzzzzzzzzzzzzzzz'; 11 + 12 + let spacedust; 13 + let spacedustEverStarted = false; 14 + 15 + const updateSubs = db => { 16 + if (!spacedust) { 17 + console.warn('not updating subscription, no spacedust (reconnecting?)'); 18 + return; 19 + } 20 + const wantedSubjectDids = db.getSubscribedDids(); 21 + if (wantedSubjectDids.length === 0) { 22 + wantedSubjectDids.push(DUMMY_DID); 23 + } 24 + console.log('updating for wantedSubjectDids', wantedSubjectDids); 25 + spacedust.send(JSON.stringify({ 26 + type: 'options_update', 27 + payload: { 28 + wantedSubjectDids, 29 + }, 30 + })); 31 + }; 32 + 33 + const push = async (db, pushSubscription, payload) => { 34 + const { session, subscription, since_last_push } = pushSubscription; 35 + if (since_last_push !== null && since_last_push < 1.618) { 36 + console.warn(`rate limiter: dropping too-soon push (${since_last_push})`); 37 + return; 38 + } 39 + 40 + let sub; 41 + try { 42 + sub = JSON.parse(subscription); 43 + } catch (e) { 44 + console.error('failed to parse subscription json, dropping session', e); 45 + db.deleteSub(session); 46 + return; 47 + } 48 + 49 + try { 50 + await webpush.sendNotification(sub, payload); 51 + } catch (err) { 52 + if (400 <= err.statusCode && err.statusCode < 500) { 53 + console.info(`removing sub for ${err.statusCode}`); 54 + db.deleteSub(session); 55 + return; 56 + } else { 57 + console.warn('something went wrong for another reason', err); 58 + } 59 + } 60 + 61 + db.updateLastPush(session); 62 + }; 63 + 64 + const isTorment = source => { 65 + try { 66 + const [nsid, ...rp] = source.split(':'); 67 + 68 + let parts = nsid.split('.'); 69 + parts.reverse(); 70 + parts = parts.join('.'); 71 + 72 + const app = psl.parse(parts)?.domain ?? 'unknown'; 73 + 74 + let appPrefix = app.split('.'); 75 + appPrefix.reverse(); 76 + appPrefix = appPrefix.join('.') 77 + 78 + return source.slice(app.length + 1) in lexicons[appPrefix]?.torment_sources; 79 + } catch (e) { 80 + console.error('checking tormentedness failed, allowing through', e); 81 + return false; 82 + } 83 + }; 84 + 85 + const extractUriDid = at_uri => { 86 + if (!at_uri.startsWith('at://')) { 87 + console.warn(`ignoring non-at-uri: ${at_uri}`); 88 + return null; 89 + } 90 + const [id, ..._] = at_uri.slice('at://'.length).split('/'); 91 + if (!id) { 92 + console.warn(`ignoring at-uri with missing id segment: ${at_uri}`); 93 + return null; 94 + } 95 + if (id.startsWith('@')) { 96 + console.warn(`ignoring @handle at-uri: ${at_uri}`); 97 + return null; 98 + } 99 + if (!id.startsWith('did:')) { 100 + console.warn(`ignoring non-did at-uri: ${at_uri}`); 101 + return null; 102 + } 103 + return id; 104 + }; 105 + 106 + const handleDust = db => async event => { 107 + console.log('got', event.data); 108 + let data; 109 + try { 110 + data = JSON.parse(event.data); 111 + } catch (err) { 112 + console.error(err); 113 + return; 114 + } 115 + const { link: { subject, source, source_record } } = data; 116 + if (isTorment(source)) { 117 + console.log('nope! not today,', source); 118 + return; 119 + } 120 + const timestamp = +new Date(); 121 + 122 + const did = subject.startsWith('did:') ? subject : extractUriDid(subject); 123 + if (!did) { 124 + console.warn(`ignoring link with non-DID subject: ${subject}`) 125 + return; 126 + } 127 + 128 + // this works for now since only the account owner is assumed to be a notification target 129 + // but for "replies on post" etc that won't hold 130 + const { notify_enabled, notify_self } = db.getNotifyAccountGlobals(did); 131 + if (!notify_enabled) { 132 + console.warn('dropping notification for global not-enabled setting'); 133 + return; 134 + } 135 + if (!notify_self) { 136 + const source_did = extractUriDid(source_record); 137 + if (!source_did) { 138 + console.warn(`ignoring link with non-DID source_record: ${source_record}`) 139 + return; 140 + } 141 + if (source_did === did) { 142 + console.warn(`ignoring self-notification`); 143 + return; 144 + } 145 + } 146 + 147 + // like above, this over-assumes that did is the only recipient we could care about for now 148 + const { app, group } = getBits(source); 149 + for (const [selector, selection] of [ 150 + ['source', source], 151 + ['group', group], 152 + ['app', app], 153 + ]) { 154 + const notify = db.getNotificationFilter(did, selector, selection); 155 + if (notify === true) { 156 + console.info(`explicitly allowing notification by filter for ${selector}=${selection}`); 157 + break; 158 + }; 159 + if (notify === false) { 160 + console.warn(`ignoring filtered notification for ${selector}=${selection}`); 161 + return; 162 + } 163 + } 164 + 165 + const subs = db.getSubsByDid(did); 166 + const payload = JSON.stringify({ subject, source, source_record, timestamp }); 167 + try { 168 + await Promise.all(subs.map(pushSubscription => push(db, pushSubscription, payload))); 169 + } catch (e) { 170 + console.warn('at least one notification send failed', e); 171 + } 172 + }; 173 + 174 + export const connectSpacedust = (db, host) => { 175 + spacedust = new WebSocket(`${host}/subscribe?instant=true&wantedSubjectDids=${DUMMY_DID}`); 176 + let restarting = false; 177 + 178 + const restart = () => { 179 + if (restarting) return; 180 + restarting = true; 181 + let wait = Math.round(500 + (Math.random() * 1000)); 182 + console.info(`restarting spacedust connection in ${wait}ms...`); 183 + setTimeout(() => connectSpacedust(db, host), wait); 184 + spacedust = null; 185 + } 186 + 187 + spacedust.onopen = () => updateSubs(db); 188 + spacedust.onmessage = handleDust(db); 189 + 190 + spacedust.onerror = e => { 191 + console.error('spacedust errored:', e); 192 + restart(); 193 + }; 194 + 195 + spacedust.onclose = () => { 196 + console.log('spacedust closed'); 197 + restart(); 198 + }; 199 + 200 + return { updateSubs, push }; 201 + };
+25 -2
server/schema.sql
··· 1 - create table accounts ( 1 + create table if not exists accounts ( 2 2 did text primary key, 3 3 first_seen text not null default CURRENT_TIMESTAMP, 4 4 role text null, 5 5 secret_password text null, 6 + notify_enabled integer not null default false, 7 + notify_self integer not null default false, 6 8 7 9 check(did like 'did:%') 8 10 ) strict; 9 11 10 - create table push_subs ( 12 + create table if not exists push_subs ( 11 13 session text primary key, -- uuidv4, bound to signed browser cookie 12 14 account_did text not null, 13 15 subscription text not null, -- from browser, treat as opaque blob ··· 20 22 foreign key(account_did) references accounts(did) 21 23 on delete cascade on update cascade 22 24 ) strict; 25 + 26 + create table if not exists top_secret_passwords ( 27 + password text primary key, 28 + added text not null default CURRENT_TIMESTAMP, 29 + expired text null, -- timestamp 30 + 31 + check(length(password) >= 3) 32 + ) strict; 33 + 34 + create table if not exists notification_filters ( 35 + account_did text not null, 36 + selector text not null, 37 + selection text not null, 38 + notify integer null, 39 + 40 + primary key(account_did, selector, selection), 41 + check(selector in ('all', 'app', 'group', 'source')), 42 + 43 + foreign key(account_did) references accounts(did) 44 + on delete cascade on update cascade 45 + ) strict;