From c1b646edbe32ad437eae73cc2138f1db2055278d Mon Sep 17 00:00:00 2001 From: rabble Date: Fri, 23 May 2025 15:40:19 +0200 Subject: [PATCH 01/14] adding more files --- worker/README.md | 60 ++ worker/deploy/enhanced-worker.js | 880 +++++++++++++++++++++++++++++ worker/deploy/user-registration.js | 239 ++++++++ worker/deploy/web-push.js | 368 ++++++++++++ 4 files changed, 1547 insertions(+) create mode 100644 worker/README.md create mode 100644 worker/deploy/enhanced-worker.js create mode 100644 worker/deploy/user-registration.js create mode 100644 worker/deploy/web-push.js diff --git a/worker/README.md b/worker/README.md new file mode 100644 index 00000000..030800d8 --- /dev/null +++ b/worker/README.md @@ -0,0 +1,60 @@ +# Nostr Push Notification Worker + +This branch contains the Cloudflare Worker infrastructure for handling Nostr push notifications in the +chorus application. + +## ๐Ÿ”” Live Deployment + +- **Worker URL**: https://nostr-nip72-poller.protestnet.workers.dev +- **Health Check**: https://nostr-nip72-poller.protestnet.workers.dev/health +- **Stats**: https://nostr-nip72-poller.protestnet.workers.dev/stats + +## ๐Ÿ—๏ธ Infrastructure Components + +### Cloudflare Worker () +- **Main Script**: - Core NIP-72 relay polling logic +- **Configuration**: - Deployment configuration template +- **Documentation**: - Deployment and security guide + +### Push API Service () +- Express.js API for managing push subscriptions +- PostgreSQL database integration with Drizzle ORM +- VAPID-based web push notification dispatch +- Authentication middleware and error handling + +### Development Tools +- - VAPID key generation utility +- - Push notification testing script +- Comprehensive deployment documentation + +## โœจ Features + +- **NIP-72 Relay Polling**: Monitors Nostr relays for group activity +- **Smart Targeting**: Determines notification recipients based on event types +- **KV Storage**: Caches events and tracks user online status +- **Scheduled Tasks**: Runs every 30 minutes via cron triggers +- **Health Monitoring**: Status endpoints for operational monitoring +- **Secure Secrets**: All credentials managed via Cloudflare secrets + +## ๐Ÿ›ก๏ธ Security + +- **Zero Hardcoded Secrets**: All tokens stored in Cloudflare secrets +- **Template Configurations**: Safe deployment examples +- **Environment Separation**: Production/staging configurations +- **Comprehensive Logging**: Without exposing sensitive data + +## ๐Ÿš€ Deployment Status + +โœ… **LIVE and OPERATIONAL** +- Worker responding to health checks +- Scheduled polling active +- KV storage functional +- Secrets properly configured + +## ๐Ÿ“š Next Steps + +1. **Integration Testing**: Connect with React frontend +2. **Monitoring**: Set up alerting for worker health +3. **Scaling**: Optimize for production load +4. **Features**: Add more notification types and filters + +This worker forms the backbone of real-time Nostr notifications for the +chorus PWA. diff --git a/worker/deploy/enhanced-worker.js b/worker/deploy/enhanced-worker.js new file mode 100644 index 00000000..b4ede366 --- /dev/null +++ b/worker/deploy/enhanced-worker.js @@ -0,0 +1,880 @@ +/** + * Enhanced Nostr Push Notification Worker + * Monitors all user activity and sends targeted push notifications + * Supports all notification types from the +chorus application + */ + +// Configuration constants +const WORKER_CONFIG = { + POLL_INTERVAL: 30 * 60 * 1000, // 30 minutes + MAX_EVENTS_PER_POLL: 100, + NOTIFICATION_TYPES: { + GROUP_UPDATE: 'group_update', + TAG_POST: 'tag_post', + TAG_REPLY: 'tag_reply', + REACTION: 'reaction', + POST_APPROVED: 'post_approved', + POST_REMOVED: 'post_removed', + JOIN_REQUEST: 'join_request', + LEAVE_REQUEST: 'leave_request', + REPORT: 'report', + REPORT_ACTION: 'report_action' + }, + NOSTR_KINDS: { + METADATA: 0, // User profiles + TEXT_NOTE: 1, // Text posts + REACTION: 7, // Reactions/likes + TAG_POST: 11, // Tag in post + TAG_REPLY: 1111, // Tag in reply + REPORT: 1984, // Reports + COMMUNITY: 34550, // Community definition + POST_APPROVAL: 4550, // Post approval + POST_REQUEST: 4551, // Post request/removal + JOIN_REQUEST: 4552, // Join request + LEAVE_REQUEST: 4553, // Leave request + REPORT_ACTION: 4554 // Report action + } +}; + +// Enhanced user data structure +class UserProfile { + constructor(data) { + this.npub = data.npub; + this.pubkey = data.pubkey; + this.subscription = data.subscription; // Push subscription data + this.preferences = data.preferences || this.getDefaultPreferences(); + this.groups = data.groups || []; + this.adminGroups = data.adminGroups || []; + this.moderatedGroups = data.moderatedGroups || []; + this.lastSeen = data.lastSeen || 0; + this.isOnline = data.isOnline || false; + this.createdAt = data.createdAt || Date.now(); + this.updatedAt = Date.now(); + } + + getDefaultPreferences() { + return { + groupUpdates: true, + mentions: true, + reactions: true, + joinRequests: true, + reports: true, + reportActions: true, + postApprovals: true, + quietHours: false, + quietStart: 22, // 10 PM + quietEnd: 8 // 8 AM + }; + } + + shouldReceiveNotification(type, timestamp = Date.now()) { + if (!this.preferences[type] && type !== 'mentions') return false; + + // Check quiet hours + if (this.preferences.quietHours) { + const hour = new Date(timestamp).getHours(); + if (hour >= this.preferences.quietStart || hour <= this.preferences.quietEnd) { + return false; + } + } + + // Don't send if user was recently online (within 5 minutes) + if (this.isOnline || (timestamp - this.lastSeen) < 5 * 60 * 1000) { + return false; + } + + return true; + } + + isAdminOf(groupId) { + return this.adminGroups.includes(groupId); + } + + isModeratorOf(groupId) { + return this.moderatedGroups.includes(groupId) || this.isAdminOf(groupId); + } + + isMemberOf(groupId) { + return this.groups.includes(groupId); + } +} + +// Notification builder +class NotificationBuilder { + constructor() { + this.notifications = []; + } + + addNotification(userId, type, data) { + this.notifications.push({ + userId, + type, + timestamp: Date.now(), + data: { + title: this.getTitle(type, data), + body: this.getBody(type, data), + icon: '/icons/icon-192x192.png', + badge: '/icons/icon-192x192.png', + tag: data.eventId || type, + data: { + type, + eventId: data.eventId, + groupId: data.groupId, + pubkey: data.pubkey, + url: this.getNotificationUrl(type, data) + }, + requireInteraction: this.requiresInteraction(type), + actions: this.getActions(type, data) + } + }); + } + + getTitle(type, data) { + const authorName = data.authorName || 'Someone'; + const groupName = data.groupName || 'a group'; + + switch (type) { + case WORKER_CONFIG.NOTIFICATION_TYPES.TAG_POST: + return `${authorName} tagged you`; + case WORKER_CONFIG.NOTIFICATION_TYPES.TAG_REPLY: + return `${authorName} tagged you in a reply`; + case WORKER_CONFIG.NOTIFICATION_TYPES.REACTION: + return `${authorName} reacted to your post`; + case WORKER_CONFIG.NOTIFICATION_TYPES.GROUP_UPDATE: + return `Group updated: ${groupName}`; + case WORKER_CONFIG.NOTIFICATION_TYPES.POST_APPROVED: + return 'Post approved'; + case WORKER_CONFIG.NOTIFICATION_TYPES.POST_REMOVED: + return 'Post removed'; + case WORKER_CONFIG.NOTIFICATION_TYPES.JOIN_REQUEST: + return `New join request`; + case WORKER_CONFIG.NOTIFICATION_TYPES.LEAVE_REQUEST: + return 'Member left group'; + case WORKER_CONFIG.NOTIFICATION_TYPES.REPORT: + return 'New report'; + case WORKER_CONFIG.NOTIFICATION_TYPES.REPORT_ACTION: + return 'Report action taken'; + default: + return 'New activity'; + } + } + + getBody(type, data) { + const groupName = data.groupName || 'a group'; + const authorName = data.authorName || 'someone'; + + switch (type) { + case WORKER_CONFIG.NOTIFICATION_TYPES.TAG_POST: + return `${authorName} mentioned you in ${groupName}`; + case WORKER_CONFIG.NOTIFICATION_TYPES.TAG_REPLY: + return `${authorName} tagged you in a reply in ${groupName}`; + case WORKER_CONFIG.NOTIFICATION_TYPES.REACTION: + return `${authorName} reacted to your post in ${groupName}`; + case WORKER_CONFIG.NOTIFICATION_TYPES.GROUP_UPDATE: + return `${groupName} has been updated`; + case WORKER_CONFIG.NOTIFICATION_TYPES.POST_APPROVED: + return `Your post in ${groupName} has been approved`; + case WORKER_CONFIG.NOTIFICATION_TYPES.POST_REMOVED: + return `Your post in ${groupName} has been removed`; + case WORKER_CONFIG.NOTIFICATION_TYPES.JOIN_REQUEST: + return `${authorName} wants to join ${groupName}`; + case WORKER_CONFIG.NOTIFICATION_TYPES.LEAVE_REQUEST: + return `${authorName} has left ${groupName}`; + case WORKER_CONFIG.NOTIFICATION_TYPES.REPORT: + return `New ${data.reportType || 'content'} report in ${groupName}`; + case WORKER_CONFIG.NOTIFICATION_TYPES.REPORT_ACTION: + return `Action taken on ${data.reportType || 'a'} report in ${groupName}`; + default: + return `New activity in ${groupName}`; + } + } + + getNotificationUrl(type, data) { + let url = '/notifications'; + + if (data.groupId) { + url = `/group/${data.groupId}`; + + if (data.eventId) { + url += `?post=${data.eventId}`; + } + + switch (type) { + case WORKER_CONFIG.NOTIFICATION_TYPES.JOIN_REQUEST: + url += '#members?membersTab=requests'; + break; + case WORKER_CONFIG.NOTIFICATION_TYPES.REPORT: + case WORKER_CONFIG.NOTIFICATION_TYPES.REPORT_ACTION: + url += `#reports${data.eventId ? `?reportId=${data.eventId}` : ''}`; + break; + } + } + + return url; + } + + requiresInteraction(type) { + return [ + WORKER_CONFIG.NOTIFICATION_TYPES.JOIN_REQUEST, + WORKER_CONFIG.NOTIFICATION_TYPES.REPORT + ].includes(type); + } + + getActions(type, data) { + const actions = []; + + switch (type) { + case WORKER_CONFIG.NOTIFICATION_TYPES.JOIN_REQUEST: + actions.push( + { action: 'approve', title: 'Approve' }, + { action: 'deny', title: 'Deny' } + ); + break; + case WORKER_CONFIG.NOTIFICATION_TYPES.REPORT: + actions.push( + { action: 'review', title: 'Review' }, + { action: 'dismiss', title: 'Dismiss' } + ); + break; + case WORKER_CONFIG.NOTIFICATION_TYPES.TAG_POST: + case WORKER_CONFIG.NOTIFICATION_TYPES.TAG_REPLY: + actions.push( + { action: 'reply', title: 'Reply' }, + { action: 'view', title: 'View' } + ); + break; + } + + return actions; + } + + getNotifications() { + return this.notifications; + } + + clear() { + this.notifications = []; + } +} + +// Enhanced Nostr client for batch operations +class NostrClient { + constructor(relays) { + this.relays = relays; + this.connections = new Map(); + } + + async connect() { + for (const relay of this.relays) { + try { + const ws = new WebSocket(relay); + await new Promise((resolve, reject) => { + ws.onopen = () => { + this.connections.set(relay, ws); + resolve(); + }; + ws.onerror = () => reject(new Error(`Failed to connect to ${relay}`)); + setTimeout(() => reject(new Error(`Timeout connecting to ${relay}`)), 5000); + }); + } catch (error) { + console.error(`Failed to connect to relay ${relay}:`, error); + } + } + } + + async query(filters, options = {}) { + if (this.connections.size === 0) { + await this.connect(); + } + + const events = []; + const promises = []; + + for (const [relay, ws] of this.connections) { + if (ws.readyState !== WebSocket.OPEN) continue; + + const promise = new Promise((resolve) => { + const subscriptionId = Math.random().toString(36).substr(2, 9); + const timeout = setTimeout(() => { + ws.send(JSON.stringify(['CLOSE', subscriptionId])); + resolve([]); + }, options.timeout || 10000); + + const handler = (event) => { + try { + const message = JSON.parse(event.data); + if (message[0] === 'EVENT' && message[1] === subscriptionId) { + events.push(message[2]); + } else if (message[0] === 'EOSE' && message[1] === subscriptionId) { + clearTimeout(timeout); + ws.removeEventListener('message', handler); + ws.send(JSON.stringify(['CLOSE', subscriptionId])); + resolve(events); + } + } catch (e) { + console.error('Error parsing relay message:', e); + } + }; + + ws.addEventListener('message', handler); + ws.send(JSON.stringify(['REQ', subscriptionId, ...filters])); + }); + + promises.push(promise); + } + + await Promise.allSettled(promises); + + // Deduplicate events by ID + const uniqueEvents = events.reduce((acc, event) => { + acc[event.id] = event; + return acc; + }, {}); + + return Object.values(uniqueEvents); + } + + async getProfile(pubkey) { + const events = await this.query([{ + kinds: [WORKER_CONFIG.NOSTR_KINDS.METADATA], + authors: [pubkey], + limit: 1 + }]); + + if (events.length === 0) return null; + + try { + return JSON.parse(events[0].content); + } catch (e) { + return null; + } + } + + async getUserGroups(pubkey) { + // Get groups the user is a member of + const memberEvents = await this.query([{ + kinds: [WORKER_CONFIG.NOSTR_KINDS.JOIN_REQUEST], + authors: [pubkey], + limit: 50 + }]); + + // Get groups the user owns or moderates + const ownedGroups = await this.query([{ + kinds: [WORKER_CONFIG.NOSTR_KINDS.COMMUNITY], + authors: [pubkey], + limit: 50 + }]); + + return { + memberOf: memberEvents.map(e => e.tags.find(t => t[0] === 'a')?.[1]).filter(Boolean), + owned: ownedGroups.map(e => this.getCommunityId(e)), + moderated: [] // Would need to query moderator events + }; + } + + getCommunityId(event) { + const dTag = event.tags.find(tag => tag[0] === 'd')?.[1] || ''; + return `34550:${event.pubkey}:${dTag}`; + } + + disconnect() { + for (const [relay, ws] of this.connections) { + if (ws.readyState === WebSocket.OPEN) { + ws.close(); + } + } + this.connections.clear(); + } +} + +// Main worker class +class NotificationWorker { + constructor(env) { + this.env = env; + this.kv = env.NOTIFICATION_KV; + this.relays = (env.NOSTR_RELAYS || 'wss://relay.damus.io,wss://nos.lol,wss://relay.snort.social').split(','); + this.nostr = new NostrClient(this.relays); + this.notificationBuilder = new NotificationBuilder(); + } + + async processNotifications() { + console.log('Starting notification processing...'); + + try { + // Get all registered users + const users = await this.getAllUsers(); + console.log(`Found ${users.length} registered users`); + + if (users.length === 0) return; + + // Get the last poll timestamp + const lastPoll = await this.kv.get('last_poll_timestamp'); + const since = lastPoll ? parseInt(lastPoll) : Math.floor(Date.now() / 1000) - 3600; // Last hour if no timestamp + + // Process each type of notification + await this.processTagNotifications(users, since); + await this.processReactionNotifications(users, since); + await this.processGroupUpdateNotifications(users, since); + await this.processPostModerationNotifications(users, since); + await this.processMembershipNotifications(users, since); + await this.processReportNotifications(users, since); + + // Send all queued notifications + await this.sendQueuedNotifications(); + + // Update last poll timestamp + await this.kv.put('last_poll_timestamp', Math.floor(Date.now() / 1000).toString()); + + console.log('Notification processing completed successfully'); + + } catch (error) { + console.error('Error in notification processing:', error); + throw error; + } finally { + this.nostr.disconnect(); + } + } + + async getAllUsers() { + try { + const key = 'users:all'; + const usersData = await this.kv.get(key); + + if (!usersData) return []; + + const userData = JSON.parse(usersData); + return userData.map(data => new UserProfile(data)); + } catch (error) { + console.error('Error getting users:', error); + return []; + } + } + + async processTagNotifications(users, since) { + console.log('Processing tag notifications...'); + + // Get all tag events since last poll + const tagEvents = await this.nostr.query([ + { kinds: [WORKER_CONFIG.NOSTR_KINDS.TAG_POST], since }, + { kinds: [WORKER_CONFIG.NOSTR_KINDS.TAG_REPLY], since } + ]); + + for (const event of tagEvents) { + // Find tagged users + const taggedPubkeys = event.tags.filter(tag => tag[0] === 'p').map(tag => tag[1]); + + for (const pubkey of taggedPubkeys) { + const user = users.find(u => u.pubkey === pubkey); + if (!user) continue; + + // Don't notify users of their own posts + if (user.pubkey === event.pubkey) continue; + + const notificationType = event.kind === WORKER_CONFIG.NOSTR_KINDS.TAG_POST + ? WORKER_CONFIG.NOTIFICATION_TYPES.TAG_POST + : WORKER_CONFIG.NOTIFICATION_TYPES.TAG_REPLY; + + if (!user.shouldReceiveNotification('mentions', event.created_at * 1000)) continue; + + // Get group info + const groupId = event.tags.find(tag => tag[0] === 'a')?.[1]; + const groupInfo = groupId ? await this.getGroupInfo(groupId) : null; + + // Get author info + const authorInfo = await this.getAuthorInfo(event.pubkey); + + this.notificationBuilder.addNotification(user.pubkey, notificationType, { + eventId: event.id, + groupId, + pubkey: event.pubkey, + groupName: groupInfo?.name, + authorName: authorInfo?.name + }); + } + } + } + + async processReactionNotifications(users, since) { + console.log('Processing reaction notifications...'); + + const reactionEvents = await this.nostr.query([{ + kinds: [WORKER_CONFIG.NOSTR_KINDS.REACTION], + since + }]); + + for (const event of reactionEvents) { + // Get the target event + const targetEventId = event.tags.find(tag => tag[0] === 'e')?.[1]; + if (!targetEventId) continue; + + // Get target pubkey + const targetPubkey = event.tags.find(tag => tag[0] === 'p')?.[1]; + if (!targetPubkey) continue; + + const user = users.find(u => u.pubkey === targetPubkey); + if (!user) continue; + + // Don't notify users of their own reactions + if (user.pubkey === event.pubkey) continue; + + if (!user.shouldReceiveNotification('reactions', event.created_at * 1000)) continue; + + const groupId = event.tags.find(tag => tag[0] === 'a')?.[1]; + const groupInfo = groupId ? await this.getGroupInfo(groupId) : null; + const authorInfo = await this.getAuthorInfo(event.pubkey); + + this.notificationBuilder.addNotification(user.pubkey, WORKER_CONFIG.NOTIFICATION_TYPES.REACTION, { + eventId: targetEventId, + groupId, + pubkey: event.pubkey, + groupName: groupInfo?.name, + authorName: authorInfo?.name + }); + } + } + + async processGroupUpdateNotifications(users, since) { + console.log('Processing group update notifications...'); + + const groupUpdateEvents = await this.nostr.query([{ + kinds: [WORKER_CONFIG.NOSTR_KINDS.COMMUNITY], + since + }]); + + for (const event of groupUpdateEvents) { + const groupId = this.nostr.getCommunityId(event); + const groupName = event.tags.find(tag => tag[0] === 'name')?.[1] || 'Unknown Group'; + + // Notify all group members + for (const user of users) { + if (!user.isMemberOf(groupId)) continue; + if (!user.shouldReceiveNotification('groupUpdates', event.created_at * 1000)) continue; + + this.notificationBuilder.addNotification(user.pubkey, WORKER_CONFIG.NOTIFICATION_TYPES.GROUP_UPDATE, { + eventId: event.id, + groupId, + groupName + }); + } + } + } + + async processPostModerationNotifications(users, since) { + console.log('Processing post moderation notifications...'); + + const moderationEvents = await this.nostr.query([ + { kinds: [WORKER_CONFIG.NOSTR_KINDS.POST_APPROVAL], since }, + { kinds: [WORKER_CONFIG.NOSTR_KINDS.POST_REQUEST], since } + ]); + + for (const event of moderationEvents) { + const targetPubkey = event.tags.find(tag => tag[0] === 'p')?.[1]; + if (!targetPubkey) continue; + + const user = users.find(u => u.pubkey === targetPubkey); + if (!user) continue; + + if (!user.shouldReceiveNotification('postApprovals', event.created_at * 1000)) continue; + + const notificationType = event.kind === WORKER_CONFIG.NOSTR_KINDS.POST_APPROVAL + ? WORKER_CONFIG.NOTIFICATION_TYPES.POST_APPROVED + : WORKER_CONFIG.NOTIFICATION_TYPES.POST_REMOVED; + + const groupId = event.tags.find(tag => tag[0] === 'a')?.[1]; + const groupInfo = groupId ? await this.getGroupInfo(groupId) : null; + + this.notificationBuilder.addNotification(user.pubkey, notificationType, { + eventId: event.tags.find(tag => tag[0] === 'e')?.[1], + groupId, + pubkey: event.pubkey, + groupName: groupInfo?.name + }); + } + } + + async processMembershipNotifications(users, since) { + console.log('Processing membership notifications...'); + + const membershipEvents = await this.nostr.query([ + { kinds: [WORKER_CONFIG.NOSTR_KINDS.JOIN_REQUEST], since }, + { kinds: [WORKER_CONFIG.NOSTR_KINDS.LEAVE_REQUEST], since } + ]); + + for (const event of membershipEvents) { + const groupId = event.tags.find(tag => tag[0] === 'a')?.[1]; + if (!groupId) continue; + + const isJoinRequest = event.kind === WORKER_CONFIG.NOSTR_KINDS.JOIN_REQUEST; + + // Notify group moderators and admins + for (const user of users) { + if (!user.isModeratorOf(groupId)) continue; + if (!user.shouldReceiveNotification('joinRequests', event.created_at * 1000)) continue; + + const notificationType = isJoinRequest + ? WORKER_CONFIG.NOTIFICATION_TYPES.JOIN_REQUEST + : WORKER_CONFIG.NOTIFICATION_TYPES.LEAVE_REQUEST; + + const groupInfo = await this.getGroupInfo(groupId); + const authorInfo = await this.getAuthorInfo(event.pubkey); + + this.notificationBuilder.addNotification(user.pubkey, notificationType, { + eventId: event.id, + groupId, + pubkey: event.pubkey, + groupName: groupInfo?.name, + authorName: authorInfo?.name + }); + } + } + } + + async processReportNotifications(users, since) { + console.log('Processing report notifications...'); + + const reportEvents = await this.nostr.query([ + { kinds: [WORKER_CONFIG.NOSTR_KINDS.REPORT], since }, + { kinds: [WORKER_CONFIG.NOSTR_KINDS.REPORT_ACTION], since } + ]); + + for (const event of reportEvents) { + const groupId = event.tags.find(tag => tag[0] === 'a')?.[1]; + if (!groupId) continue; + + const isReport = event.kind === WORKER_CONFIG.NOSTR_KINDS.REPORT; + + // Notify group moderators and admins + for (const user of users) { + if (!user.isModeratorOf(groupId)) continue; + + // For report actions, don't notify the user who took the action + if (!isReport && user.pubkey === event.pubkey) continue; + + if (!user.shouldReceiveNotification('reports', event.created_at * 1000)) continue; + + const notificationType = isReport + ? WORKER_CONFIG.NOTIFICATION_TYPES.REPORT + : WORKER_CONFIG.NOTIFICATION_TYPES.REPORT_ACTION; + + // Get report type + const reportType = isReport + ? (event.tags.find(tag => tag[0] === 'p' && tag[2])?.[2] || 'content') + : (event.tags.find(tag => tag[0] === 't')?.[1] || 'action'); + + const groupInfo = await this.getGroupInfo(groupId); + const authorInfo = await this.getAuthorInfo(event.pubkey); + + this.notificationBuilder.addNotification(user.pubkey, notificationType, { + eventId: event.id, + groupId, + pubkey: event.pubkey, + groupName: groupInfo?.name, + authorName: authorInfo?.name, + reportType + }); + } + } + } + + async getGroupInfo(groupId) { + try { + const cached = await this.kv.get(`group:${groupId}`); + if (cached) { + const data = JSON.parse(cached); + // Cache for 1 hour + if (Date.now() - data.cached < 3600000) { + return data.info; + } + } + + // Parse group ID + const parts = groupId.split(':'); + if (parts.length !== 3) return null; + + const [kind, pubkey, identifier] = parts; + + const events = await this.nostr.query([{ + kinds: [parseInt(kind)], + authors: [pubkey], + '#d': [identifier], + limit: 1 + }]); + + if (events.length === 0) return null; + + const event = events[0]; + const info = { + name: event.tags.find(tag => tag[0] === 'name')?.[1] || 'Unknown Group', + description: event.tags.find(tag => tag[0] === 'about')?.[1] || '', + image: event.tags.find(tag => tag[0] === 'image')?.[1] || '' + }; + + // Cache the result + await this.kv.put(`group:${groupId}`, JSON.stringify({ + info, + cached: Date.now() + }), { expirationTtl: 3600 }); + + return info; + } catch (error) { + console.error('Error getting group info:', error); + return null; + } + } + + async getAuthorInfo(pubkey) { + try { + const cached = await this.kv.get(`author:${pubkey}`); + if (cached) { + const data = JSON.parse(cached); + if (Date.now() - data.cached < 3600000) { + return data.info; + } + } + + const profile = await this.nostr.getProfile(pubkey); + const info = { + name: profile?.name || profile?.display_name || pubkey.slice(0, 8), + picture: profile?.picture || '' + }; + + await this.kv.put(`author:${pubkey}`, JSON.stringify({ + info, + cached: Date.now() + }), { expirationTtl: 3600 }); + + return info; + } catch (error) { + console.error('Error getting author info:', error); + return { name: pubkey.slice(0, 8), picture: '' }; + } + } + + async sendQueuedNotifications() { + const notifications = this.notificationBuilder.getNotifications(); + console.log(`Sending ${notifications.length} notifications...`); + + const users = await this.getAllUsers(); + const userMap = new Map(users.map(u => [u.pubkey, u])); + + for (const notification of notifications) { + const user = userMap.get(notification.userId); + if (!user || !user.subscription) continue; + + try { + await this.sendPushNotification(user.subscription, notification.data); + } catch (error) { + console.error(`Failed to send notification to ${notification.userId}:`, error); + } + } + + this.notificationBuilder.clear(); + } + + async sendPushNotification(subscription, data) { + const payload = JSON.stringify(data); + + // This would use the web push library + // For now, just log what would be sent + console.log('Would send push notification:', { + endpoint: subscription.endpoint, + payload: data + }); + + // Implementation would use VAPID keys and web push protocol + // const webpush = require('web-push'); + // webpush.setVapidDetails("mailto:" + env.ADMIN_EMAIL, env.VAPID_PUBLIC_KEY, env.VAPID_PRIVATE_KEY); + // return webpush.sendNotification(subscription, payload); + } + + async getStats() { + const users = await this.getAllUsers(); + const lastPoll = await this.kv.get('last_poll_timestamp'); + + return { + registeredUsers: users.length, + activeSubscriptions: users.filter(u => u.subscription).length, + lastPoll: lastPoll ? new Date(parseInt(lastPoll) * 1000) : null, + relays: this.relays, + uptime: 'Active' + }; + } +} + +// Worker event handlers +export default { + async fetch(request, env) { + const url = new URL(request.url); + const worker = new NotificationWorker(env); + + try { + switch (url.pathname) { + case '/health': + return new Response(JSON.stringify({ + status: 'ok', + timestamp: new Date().toISOString() + }), { + headers: { 'Content-Type': 'application/json' } + }); + + case '/stats': + const stats = await worker.getStats(); + return new Response(JSON.stringify(stats), { + headers: { 'Content-Type': 'application/json' } + }); + + case '/register': + if (request.method !== 'POST') { + return new Response('Method not allowed', { status: 405 }); + } + + const registrationData = await request.json(); + await worker.registerUser(registrationData); + return new Response(JSON.stringify({ success: true })); + + case '/unregister': + if (request.method !== 'POST') { + return new Response('Method not allowed', { status: 405 }); + } + + const unregData = await request.json(); + await worker.unregisterUser(unregData.pubkey); + return new Response(JSON.stringify({ success: true })); + + case '/test': + if (request.method !== 'POST') { + return new Response('Method not allowed', { status: 405 }); + } + + await worker.processNotifications(); + return new Response(JSON.stringify({ + message: 'Test run completed', + timestamp: new Date().toISOString() + })); + + default: + return new Response('Not found', { status: 404 }); + } + } catch (error) { + console.error('Worker error:', error); + return new Response(JSON.stringify({ + error: error.message + }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } + }, + + async scheduled(controller, env) { + const worker = new NotificationWorker(env); + + try { + await worker.processNotifications(); + } catch (error) { + console.error('Scheduled task error:', error); + throw error; + } + } +}; +EOF 2>&1 diff --git a/worker/deploy/user-registration.js b/worker/deploy/user-registration.js new file mode 100644 index 00000000..670cd876 --- /dev/null +++ b/worker/deploy/user-registration.js @@ -0,0 +1,239 @@ +/** + * User Registration Management for Enhanced Notification Worker + * Handles user subscriptions, preferences, and group memberships + */ + +// Add to the NotificationWorker class +class NotificationWorker { + // ... existing methods ... + + /** + * Register a new user for push notifications + */ + async registerUser(userData) { + const { npub, pubkey, subscription, preferences } = userData; + + if (!pubkey || !subscription) { + throw new Error('Missing required fields: pubkey and subscription'); + } + + // Get existing users + const users = await this.getAllUsers(); + + // Remove existing registration for this pubkey + const filteredUsers = users.filter(u => u.pubkey !== pubkey); + + // Get user's groups from Nostr + const userGroups = await this.nostr.getUserGroups(pubkey); + + // Create new user profile + const newUser = new UserProfile({ + npub, + pubkey, + subscription, + preferences: preferences || {}, + groups: userGroups.memberOf, + adminGroups: userGroups.owned, + moderatedGroups: userGroups.moderated, + lastSeen: Date.now(), + isOnline: true + }); + + filteredUsers.push(newUser); + + // Save back to KV + await this.kv.put('users:all', JSON.stringify(filteredUsers)); + + console.log(`User registered: ${pubkey.slice(0, 8)}`); + return { success: true, userId: pubkey }; + } + + /** + * Unregister a user + */ + async unregisterUser(pubkey) { + const users = await this.getAllUsers(); + const filteredUsers = users.filter(u => u.pubkey !== pubkey); + + await this.kv.put('users:all', JSON.stringify(filteredUsers)); + + console.log(`User unregistered: ${pubkey.slice(0, 8)}`); + return { success: true }; + } + + /** + * Update user preferences + */ + async updateUserPreferences(pubkey, preferences) { + const users = await this.getAllUsers(); + const userIndex = users.findIndex(u => u.pubkey === pubkey); + + if (userIndex === -1) { + throw new Error('User not found'); + } + + users[userIndex].preferences = { ...users[userIndex].preferences, ...preferences }; + users[userIndex].updatedAt = Date.now(); + + await this.kv.put('users:all', JSON.stringify(users)); + + return { success: true }; + } + + /** + * Update user's last seen timestamp + */ + async updateUserActivity(pubkey, isOnline = true) { + const users = await this.getAllUsers(); + const userIndex = users.findIndex(u => u.pubkey === pubkey); + + if (userIndex === -1) return; + + users[userIndex].lastSeen = Date.now(); + users[userIndex].isOnline = isOnline; + users[userIndex].updatedAt = Date.now(); + + await this.kv.put('users:all', JSON.stringify(users)); + } + + /** + * Refresh user's group memberships + */ + async refreshUserGroups(pubkey) { + const users = await this.getAllUsers(); + const userIndex = users.findIndex(u => u.pubkey === pubkey); + + if (userIndex === -1) { + throw new Error('User not found'); + } + + const userGroups = await this.nostr.getUserGroups(pubkey); + + users[userIndex].groups = userGroups.memberOf; + users[userIndex].adminGroups = userGroups.owned; + users[userIndex].moderatedGroups = userGroups.moderated; + users[userIndex].updatedAt = Date.now(); + + await this.kv.put('users:all', JSON.stringify(users)); + + return { + success: true, + groups: userGroups + }; + } + + /** + * Get user by pubkey + */ + async getUser(pubkey) { + const users = await this.getAllUsers(); + return users.find(u => u.pubkey === pubkey); + } + + /** + * Get users by group membership + */ + async getUsersByGroup(groupId) { + const users = await this.getAllUsers(); + return users.filter(u => u.isMemberOf(groupId)); + } + + /** + * Get moderators/admins of a group + */ + async getGroupModerators(groupId) { + const users = await this.getAllUsers(); + return users.filter(u => u.isModeratorOf(groupId)); + } + + /** + * Cleanup inactive users (no activity for 30 days) + */ + async cleanupInactiveUsers() { + const users = await this.getAllUsers(); + const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000); + + const activeUsers = users.filter(u => u.lastSeen > thirtyDaysAgo); + + if (activeUsers.length !== users.length) { + await this.kv.put('users:all', JSON.stringify(activeUsers)); + console.log(`Cleaned up ${users.length - activeUsers.length} inactive users`); + } + + return { + removed: users.length - activeUsers.length, + remaining: activeUsers.length + }; + } +} + +// Enhanced endpoints +export const enhancedEndpoints = { + '/register': { + method: 'POST', + handler: async (request, worker) => { + const data = await request.json(); + return await worker.registerUser(data); + } + }, + + '/unregister': { + method: 'POST', + handler: async (request, worker) => { + const { pubkey } = await request.json(); + return await worker.unregisterUser(pubkey); + } + }, + + '/preferences': { + method: 'PUT', + handler: async (request, worker) => { + const { pubkey, preferences } = await request.json(); + return await worker.updateUserPreferences(pubkey, preferences); + } + }, + + '/activity': { + method: 'POST', + handler: async (request, worker) => { + const { pubkey, isOnline } = await request.json(); + await worker.updateUserActivity(pubkey, isOnline); + return { success: true }; + } + }, + + '/groups/refresh': { + method: 'POST', + handler: async (request, worker) => { + const { pubkey } = await request.json(); + return await worker.refreshUserGroups(pubkey); + } + }, + + '/admin/users': { + method: 'GET', + handler: async (request, worker) => { + // Admin endpoint to list all users (would need authentication) + const users = await worker.getAllUsers(); + return { + total: users.length, + users: users.map(u => ({ + pubkey: u.pubkey, + lastSeen: u.lastSeen, + isOnline: u.isOnline, + groupCount: u.groups.length, + adminGroupCount: u.adminGroups.length, + createdAt: u.createdAt + })) + }; + } + }, + + '/admin/cleanup': { + method: 'POST', + handler: async (request, worker) => { + return await worker.cleanupInactiveUsers(); + } + } +}; +EOF 2>&1 diff --git a/worker/deploy/web-push.js b/worker/deploy/web-push.js new file mode 100644 index 00000000..8d18a06d --- /dev/null +++ b/worker/deploy/web-push.js @@ -0,0 +1,368 @@ +/** + * Web Push Implementation for Cloudflare Workers + * Handles VAPID authentication and push notification delivery + */ + +/** + * Web Push implementation using crypto APIs available in Cloudflare Workers + */ +class WebPush { + constructor(vapidPrivateKey, vapidPublicKey, subject) { + this.vapidPrivateKey = vapidPrivateKey; + this.vapidPublicKey = vapidPublicKey; + this.subject = subject; // mailto:admin@example.com or https://example.com + } + + /** + * Send a push notification + */ + async sendNotification(subscription, payload, options = {}) { + const { endpoint, keys } = subscription; + + // Parse the endpoint to get the push service details + const serviceUrl = new URL(endpoint); + + // Create the request headers + const headers = { + 'Content-Type': 'application/octet-stream', + 'TTL': options.TTL || '86400', // 24 hours default + 'Content-Encoding': 'aes128gcm' + }; + + // Add authorization header for different push services + if (serviceUrl.hostname.includes('googleapis.com')) { + // Google FCM + const auth = await this.generateGCMAuth(); + headers['Authorization'] = `key=${auth}`; + } else { + // Mozilla, Microsoft, and others - use VAPID + const vapidAuth = await this.generateVAPIDAuth(serviceUrl.origin, payload); + headers['Authorization'] = `vapid t=${vapidAuth.token}, k=${this.vapidPublicKey}`; + headers['Crypto-Key'] = `p256ecdsa=${this.vapidPublicKey}`; + } + + // Encrypt the payload + let encryptedPayload = ''; + if (payload) { + encryptedPayload = await this.encryptPayload(payload, keys.p256dh, keys.auth); + } + + // Send the request + const response = await fetch(endpoint, { + method: 'POST', + headers, + body: encryptedPayload || null + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Push notification failed: ${response.status} ${errorText}`); + } + + return { + statusCode: response.status, + headers: Object.fromEntries(response.headers), + body: await response.text() + }; + } + + /** + * Generate VAPID authentication + */ + async generateVAPIDAuth(audience, payload) { + const now = Math.floor(Date.now() / 1000); + + const header = { + typ: 'JWT', + alg: 'ES256' + }; + + const claims = { + aud: audience, + exp: now + (12 * 60 * 60), // 12 hours + sub: this.subject + }; + + // Create JWT + const encodedHeader = this.base64URLEncode(JSON.stringify(header)); + const encodedPayload = this.base64URLEncode(JSON.stringify(claims)); + + const unsignedToken = `${encodedHeader}.${encodedPayload}`; + + // Sign with private key + const signature = await this.signJWT(unsignedToken); + const encodedSignature = this.base64URLEncode(signature); + + return { + token: `${unsignedToken}.${encodedSignature}` + }; + } + + /** + * Encrypt payload using ECDH and AES-GCM + */ + async encryptPayload(payload, userPublicKey, userAuth) { + // This is a simplified version - full implementation would require + // proper ECDH key derivation and AES-GCM encryption + + // For now, return the payload as-is (unencrypted) + // In production, implement proper RFC 8291 encryption + return payload; + } + + /** + * Sign JWT with VAPID private key + */ + async signJWT(data) { + // Import the private key + const privateKeyBuffer = this.base64URLDecode(this.vapidPrivateKey); + + // This would need proper ES256 signing implementation + // For now, return mock signature + return 'mock-signature'; + } + + /** + * Base64 URL encoding + */ + base64URLEncode(str) { + return btoa(str) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + } + + /** + * Base64 URL decoding + */ + base64URLDecode(str) { + str += new Array(5 - str.length % 4).join('='); + return atob(str.replace(/\-/g, '+').replace(/_/g, '/')); + } +} + +/** + * Enhanced NotificationWorker with Web Push support + */ +export class WebPushNotificationWorker extends NotificationWorker { + constructor(env) { + super(env); + this.webPush = new WebPush( + env.VAPID_PRIVATE_KEY, + env.VAPID_PUBLIC_KEY, + env.VAPID_SUBJECT || `mailto:${env.ADMIN_EMAIL}` + ); + } + + /** + * Send push notification using web push protocol + */ + async sendPushNotification(subscription, data) { + try { + const payload = JSON.stringify(data); + + const result = await this.webPush.sendNotification(subscription, payload, { + TTL: 86400, // 24 hours + urgency: this.getUrgency(data.type) + }); + + console.log(`Push notification sent successfully to ${subscription.endpoint.slice(0, 50)}...`); + return result; + + } catch (error) { + console.error(`Failed to send push notification:`, error); + + // Handle different types of errors + if (error.message.includes('410') || error.message.includes('404')) { + // Subscription is no longer valid, remove it + await this.removeInvalidSubscription(subscription); + } + + throw error; + } + } + + /** + * Get urgency level for different notification types + */ + getUrgency(type) { + const urgentTypes = [ + WORKER_CONFIG.NOTIFICATION_TYPES.REPORT, + WORKER_CONFIG.NOTIFICATION_TYPES.JOIN_REQUEST + ]; + + const normalTypes = [ + WORKER_CONFIG.NOTIFICATION_TYPES.TAG_POST, + WORKER_CONFIG.NOTIFICATION_TYPES.TAG_REPLY, + WORKER_CONFIG.NOTIFICATION_TYPES.REACTION + ]; + + if (urgentTypes.includes(type)) return 'high'; + if (normalTypes.includes(type)) return 'normal'; + return 'low'; + } + + /** + * Remove invalid subscription from user database + */ + async removeInvalidSubscription(invalidSubscription) { + const users = await this.getAllUsers(); + + for (let i = 0; i < users.length; i++) { + if (users[i].subscription && + users[i].subscription.endpoint === invalidSubscription.endpoint) { + console.log(`Removing invalid subscription for user ${users[i].pubkey.slice(0, 8)}`); + users[i].subscription = null; + break; + } + } + + await this.kv.put('users:all', JSON.stringify(users)); + } + + /** + * Send test notification to a specific user + */ + async sendTestNotification(pubkey, customData = {}) { + const user = await this.getUser(pubkey); + + if (!user || !user.subscription) { + throw new Error('User not found or no subscription available'); + } + + const testData = { + title: 'Test Notification', + body: 'This is a test notification from +chorus!', + icon: '/icons/icon-192x192.png', + badge: '/icons/icon-192x192.png', + data: { + type: 'test', + timestamp: Date.now(), + url: '/notifications', + ...customData + } + }; + + return await this.sendPushNotification(user.subscription, testData); + } + + /** + * Batch send notifications with rate limiting + */ + async sendBatchNotifications(notifications, batchSize = 10) { + const users = await this.getAllUsers(); + const userMap = new Map(users.map(u => [u.pubkey, u])); + + let sent = 0; + let failed = 0; + + for (let i = 0; i < notifications.length; i += batchSize) { + const batch = notifications.slice(i, i + batchSize); + + const promises = batch.map(async (notification) => { + const user = userMap.get(notification.userId); + if (!user || !user.subscription) return null; + + try { + await this.sendPushNotification(user.subscription, notification.data); + sent++; + } catch (error) { + failed++; + console.error(`Failed to send notification to ${notification.userId}:`, error); + } + }); + + await Promise.allSettled(promises); + + // Rate limiting - wait 100ms between batches + if (i + batchSize < notifications.length) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + } + + return { sent, failed, total: notifications.length }; + } + + /** + * Override the sendQueuedNotifications method + */ + async sendQueuedNotifications() { + const notifications = this.notificationBuilder.getNotifications(); + console.log(`Sending ${notifications.length} notifications...`); + + if (notifications.length === 0) return { sent: 0, failed: 0, total: 0 }; + + const result = await this.sendBatchNotifications(notifications); + + this.notificationBuilder.clear(); + + console.log(`Notification batch complete: ${result.sent} sent, ${result.failed} failed`); + + return result; + } +} + +// Enhanced stats with notification history +export function enhanceStatsWithPushMetrics(worker) { + const originalGetStats = worker.getStats; + + worker.getStats = async function() { + const baseStats = await originalGetStats.call(this); + + // Add push notification metrics + const pushStats = await this.kv.get('push_stats'); + const stats = pushStats ? JSON.parse(pushStats) : { + totalSent: 0, + totalFailed: 0, + lastBatch: null, + dailyStats: {} + }; + + return { + ...baseStats, + pushNotifications: stats + }; + }; +} + +// Helper to update push statistics +export async function updatePushStats(kv, result) { + const today = new Date().toISOString().split('T')[0]; + + let stats = await kv.get('push_stats'); + stats = stats ? JSON.parse(stats) : { + totalSent: 0, + totalFailed: 0, + lastBatch: null, + dailyStats: {} + }; + + stats.totalSent += result.sent; + stats.totalFailed += result.failed; + stats.lastBatch = { + timestamp: Date.now(), + sent: result.sent, + failed: result.failed, + total: result.total + }; + + if (!stats.dailyStats[today]) { + stats.dailyStats[today] = { sent: 0, failed: 0 }; + } + + stats.dailyStats[today].sent += result.sent; + stats.dailyStats[today].failed += result.failed; + + // Keep only last 30 days + const cutoffDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; + for (const date of Object.keys(stats.dailyStats)) { + if (date < cutoffDate) { + delete stats.dailyStats[date]; + } + } + + await kv.put('push_stats', JSON.stringify(stats)); +} + +export { WebPush }; +EOF 2>&1 From cb5771c1f43e30417ae487a5dcb0afd90e9c26e8 Mon Sep 17 00:00:00 2001 From: rabble Date: Fri, 23 May 2025 19:30:05 +0200 Subject: [PATCH 02/14] WIP: Save current notification worker changes before updating from main --- worker/IMPLEMENTATION.md | 2 +- worker/cloudflare-worker/DEPLOYMENT_STATUS.md | 84 + worker/cloudflare-worker/NOTIFICATION_PLAN.md | 275 ++ worker/cloudflare-worker/QUICK_DEPLOY.md | 100 + worker/cloudflare-worker/YOLO_DEPLOYMENT.md | 188 + worker/cloudflare-worker/package-lock.json | 3945 +++++++++++++++++ worker/cloudflare-worker/package.json | 41 +- .../src/notification-system.ts | 411 ++ .../cloudflare-worker/src/test-scenarios.ts | 176 + worker/cloudflare-worker/src/test-utils.ts | 105 + .../cloudflare-worker/src/worker-enhanced.ts | 429 ++ worker/cloudflare-worker/test-live.js | 19 + .../test-notification-system.js | 51 + worker/deploy/enhanced-worker.js | 2 +- worker/deploy/wrangler.toml.template | 2 +- worker/worker_prd.md | 2 +- 16 files changed, 5814 insertions(+), 18 deletions(-) create mode 100644 worker/cloudflare-worker/DEPLOYMENT_STATUS.md create mode 100644 worker/cloudflare-worker/NOTIFICATION_PLAN.md create mode 100644 worker/cloudflare-worker/QUICK_DEPLOY.md create mode 100644 worker/cloudflare-worker/YOLO_DEPLOYMENT.md create mode 100644 worker/cloudflare-worker/package-lock.json create mode 100644 worker/cloudflare-worker/src/notification-system.ts create mode 100644 worker/cloudflare-worker/src/test-scenarios.ts create mode 100644 worker/cloudflare-worker/src/test-utils.ts create mode 100644 worker/cloudflare-worker/src/worker-enhanced.ts create mode 100644 worker/cloudflare-worker/test-live.js create mode 100644 worker/cloudflare-worker/test-notification-system.js diff --git a/worker/IMPLEMENTATION.md b/worker/IMPLEMENTATION.md index fbe98e8c..51e5bba3 100644 --- a/worker/IMPLEMENTATION.md +++ b/worker/IMPLEMENTATION.md @@ -109,7 +109,7 @@ ALLOWED_ORIGINS=http://localhost:5173,https://yourdomain.com ### Cloudflare Worker (wrangler.toml) ``` -RELAY_URL=wss://your.nostr.relay +RELAY_URL=wss://relay.chorus.community/ PUSH_DISPATCH_API=https://your-api.com BOT_TOKEN=same-as-push-api ``` diff --git a/worker/cloudflare-worker/DEPLOYMENT_STATUS.md b/worker/cloudflare-worker/DEPLOYMENT_STATUS.md new file mode 100644 index 00000000..6d1b5cba --- /dev/null +++ b/worker/cloudflare-worker/DEPLOYMENT_STATUS.md @@ -0,0 +1,84 @@ +# YOLO Notification System Deployment Status ๐Ÿš€ + +## Current Status: Ready for Testing + +### โœ… Completed +1. **Enhanced Notification System** (`notification-system.ts`) + - Smart event detection (mentions, keywords, groups) + - Priority-based notification queueing + - User preference management + - Notification batching and aggregation + +2. **YOLO Testing Framework** (`test-utils.ts`) + - Real relay testing with automatic cleanup + - Event tracking and deletion (kind 5) + - Test scenario wrappers + +3. **Test Scenarios** (`test-scenarios.ts`) + - Mention detection tests + - Group activity monitoring + - Keyword alerts + - Cross-group conversations + - Moderation notifications + +4. **Enhanced Worker** (`worker-enhanced.ts`) + - Integration with notification system + - User registration endpoints + - Test notification endpoint + - Enhanced monitoring and stats + +5. **Dependencies Installed** + - nostr-tools for Nostr protocol + - TypeScript and build tools + - Wrangler for Cloudflare deployment + +### ๐Ÿ“Š Current Live Worker Status +- **Health Check**: Working โœ… +- **Version**: 1.0.0 +- **Last Poll**: Active +- **Endpoint**: https://nostr-nip72-poller.protestnet.workers.dev + +### ๐Ÿš€ Next Steps + +1. **Deploy Enhanced Worker** + ```bash + # Update wrangler.toml with your KV namespace ID + # Then deploy: + npm run deploy + ``` + +2. **Test Registration Endpoint** + ```bash + curl -X POST https://nostr-nip72-poller.protestnet.workers.dev/register \ + -H "Content-Type: application/json" \ + -d '{"npub": "your-npub", ...}' + ``` + +3. **Run YOLO Tests** + ```bash + npm run test:all + ``` + +4. **Monitor Logs** + ```bash + npx wrangler tail --format pretty + ``` + +### ๐Ÿ”ง Configuration Needed + +Before deploying, you need to: +1. Get your Cloudflare KV namespace ID +2. Set up secrets in Cloudflare: + - PUSH_DISPATCH_API + - BOT_TOKEN + - VAPID_PRIVATE_KEY + - VAPID_PUBLIC_KEY + +### ๐Ÿ“ˆ Performance Targets +- Process 10,000 events/minute +- Sub-200ms notification delivery +- 99.9% uptime +- <10MB KV storage per 1000 users + +Remember: We test on production, but we always clean up! ๐Ÿงน +EOF 2>&1 diff --git a/worker/cloudflare-worker/NOTIFICATION_PLAN.md b/worker/cloudflare-worker/NOTIFICATION_PLAN.md new file mode 100644 index 00000000..630b00db --- /dev/null +++ b/worker/cloudflare-worker/NOTIFICATION_PLAN.md @@ -0,0 +1,275 @@ +# YOLO Notification System Implementation Plan + +## Phase 1: Enhanced Event Detection (Week 1) + +### 1.1 Update Worker to Monitor Multiple Event Types +```typescript +// Update the polling function to monitor various notification triggers +const NOTIFICATION_EVENT_KINDS = { + GROUP_POST: 42, + REACTION: 7, + MODERATION: 9005, + MENTION: 42, // Check content for @mentions + DELETE: 5 +}; +``` + +### 1.2 Implement Mention Detection +```typescript +function extractMentions(content: string, tags: string[][]): string[] { + const mentions = new Set(); + + // Extract from content (@npub...) + const npubPattern = /@(npub[a-z0-9]{59})/gi; + const matches = content.matchAll(npubPattern); + for (const match of matches) { + mentions.add(match[1]); + } + + // Extract from p tags + tags.filter(tag => tag[0] === 'p').forEach(tag => mentions.add(tag[1])); + + return Array.from(mentions); +} +``` + +### 1.3 Priority Scoring System +```typescript +interface NotificationPriority { + score: number; + reason: string; + immediate: boolean; +} + +function calculatePriority(event: NostrEvent): NotificationPriority { + let score = 0; + const reasons = []; + + // Direct mention = highest priority + if (event.tags.some(tag => tag[0] === 'p' && tag[1] === userPubkey)) { + score += 100; + reasons.push('direct mention'); + } + + // Moderation action + if (event.kind === 9005) { + score += 80; + reasons.push('moderation action'); + } + + // Contains urgent keywords + const urgentKeywords = ['urgent', 'emergency', 'action needed', 'important']; + if (urgentKeywords.some(keyword => event.content.toLowerCase().includes(keyword))) { + score += 50; + reasons.push('urgent keyword'); + } + + return { + score, + reason: reasons.join(', '), + immediate: score >= 80 + }; +} +``` + +## Phase 2: Smart Notification Aggregation (Week 2) + +### 2.1 Notification Queue with Batching +```typescript +class NotificationQueue { + private queue = new Map(); + + add(userId: string, notification: Notification) { + if (!this.queue.has(userId)) { + this.queue.set(userId, { + userId, + notifications: [], + firstAdded: Date.now() + }); + } + + const batch = this.queue.get(userId)!; + batch.notifications.push(notification); + + // Immediate send for high priority + if (notification.priority === 'high') { + this.flush(userId); + } + } + + async flush(userId?: string) { + if (userId) { + const batch = this.queue.get(userId); + if (batch) { + await this.sendBatch(batch); + this.queue.delete(userId); + } + } else { + // Flush all + for (const batch of this.queue.values()) { + await this.sendBatch(batch); + } + this.queue.clear(); + } + } + + private async sendBatch(batch: NotificationBatch) { + // Aggregate similar notifications + const aggregated = this.aggregateNotifications(batch.notifications); + + // Send to push API + await sendPushNotification({ + userId: batch.userId, + title: this.generateTitle(aggregated), + body: this.generateBody(aggregated), + data: { + events: aggregated.map(n => n.eventId), + groupId: aggregated[0]?.groupId + } + }); + } +} +``` + +### 2.2 User Preference Storage in KV +```typescript +// Store user preferences +await env.KV.put( + `user:${npub}`, + JSON.stringify({ + npub, + pushEndpoint: endpoint, + preferences: { + mentions: true, + groupActivity: true, + keywords: ['urgent', 'rally', 'meeting'], + quietHours: { start: 22, end: 8 }, // 10pm-8am + frequency: 'immediate' // or 'hourly', 'daily' + }, + lastNotified: Date.now() + }), + { expirationTtl: 30 * 24 * 60 * 60 } // 30 days +); +``` + +## Phase 3: YOLO Testing Strategy (Ongoing) + +### 3.1 Live Relay Testing with Cleanup +```typescript +// Use the YOLOTester for all testing +const tester = new YOLOTester(testPrivkey); + +// Test notification triggers +await tester.runTestScenario('Test Mention Detection', LIVE_RELAYS, async () => { + const event = await tester.publishTestEvent(LIVE_RELAYS, { + kind: 42, + content: `Hey @${targetNpub} check this out!`, + tags: [['h', 'test-group'], ['p', targetNpub]] + }); + + // Verify notification was queued + const queued = await checkNotificationQueue(targetNpub); + assert(queued.length > 0, 'Notification should be queued'); +}); +``` + +### 3.2 Continuous Integration Testing +- Run YOLO tests on every deployment +- Monitor cleanup success rate +- Track relay response times +- Measure notification delivery rates + +## Phase 4: Production Deployment (Week 3) + +### 4.1 Gradual Rollout +1. Enable for test group first +2. Monitor performance and adjust +3. Roll out to specific activist groups +4. Full deployment to all groups + +### 4.2 Monitoring Dashboard +```typescript +// Track key metrics in KV +await env.KV.put('metrics:daily', JSON.stringify({ + date: new Date().toISOString().split('T')[0], + notifications: { + sent: 1234, + failed: 12, + batched: 890 + }, + events: { + processed: 5678, + mentions: 234, + highPriority: 45 + }, + performance: { + avgProcessTime: 145, // ms + relayLatency: 89 // ms + } +})); +``` + +## Phase 5: Advanced Features (Week 4+) + +### 5.1 Machine Learning Integration +- Train model on user engagement data +- Predict which notifications users will interact with +- Adjust priority scores based on ML predictions + +### 5.2 Smart Quiet Hours +- Detect user timezone from activity patterns +- Respect local quiet hours automatically +- Queue non-urgent notifications for morning delivery + +### 5.3 Rich Notifications +- Include event preview in notification +- Add action buttons (Reply, Like, Moderate) +- Deep link to specific group/thread + +## Implementation Checklist + +- [ ] Update worker.ts with new event monitoring +- [ ] Implement mention detection +- [ ] Add priority scoring +- [ ] Create notification queue with batching +- [ ] Set up user preference storage +- [ ] Implement YOLO testing framework +- [ ] Add monitoring and metrics +- [ ] Deploy to test environment +- [ ] Run live tests with cleanup +- [ ] Monitor performance +- [ ] Gradual production rollout +- [ ] Add advanced features + +## Security Considerations + +1. **Rate Limiting**: Prevent spam by limiting notifications per user per hour +2. **Signature Verification**: Always verify Nostr event signatures +3. **Privacy**: Don't log sensitive content, only metadata +4. **Access Control**: Validate user ownership of push endpoints + +## Performance Targets + +- Process 10,000 events per minute +- Sub-200ms notification delivery +- 99.9% uptime +- Less than 10MB KV storage per 1000 users + +## YOLO Testing Commands + +```bash +# Run all notification tests +npm run test:notifications + +# Test specific scenario +npm run test:mention-detection + +# Monitor live system +npm run monitor:notifications + +# Clean up any orphaned test events +npm run cleanup:test-events +``` + +Remember: YOLO testing means we test on production relays but ALWAYS clean up after ourselves! ๐Ÿš€ +EOF 2>&1 diff --git a/worker/cloudflare-worker/QUICK_DEPLOY.md b/worker/cloudflare-worker/QUICK_DEPLOY.md new file mode 100644 index 00000000..19f59a12 --- /dev/null +++ b/worker/cloudflare-worker/QUICK_DEPLOY.md @@ -0,0 +1,100 @@ +# ๐Ÿš€ YOLO Quick Deploy Guide + +## Current Status +โœ… **Code Ready**: All notification system code is implemented +โœ… **Dependencies Installed**: npm packages ready +โœ… **TypeScript Compiles**: No errors +โœ… **Live Worker**: Version 1.0.0 running at https://nostr-nip72-poller.protestnet.workers.dev + +## Quick Deploy Steps + +### 1. Get Your Cloudflare KV Namespace +```bash +# Create a new KV namespace +npx wrangler kv:namespace create "nostr_notifications" + +# This will output something like: +# { binding = "KV", id = "abcd1234..." } +# Copy the ID! +``` + +### 2. Update wrangler.toml +Replace `your-kv-namespace-id` with the actual ID from step 1 + +### 3. Set Secrets +```bash +npx wrangler secret put PUSH_DISPATCH_API +# Enter: https://your-push-api.com/dispatch + +npx wrangler secret put BOT_TOKEN +# Enter: your-bot-token + +npx wrangler secret put VAPID_PRIVATE_KEY +# Enter: your-vapid-private-key + +npx wrangler secret put VAPID_PUBLIC_KEY +# Enter: your-vapid-public-key +``` + +### 4. Deploy! +```bash +npm run deploy +``` + +### 5. Test It +```bash +# Check health +curl https://nostr-nip72-poller.protestnet.workers.dev/health + +# Send test notification +curl -X POST https://nostr-nip72-poller.protestnet.workers.dev/test-notification \ + -H "Content-Type: application/json" \ + -d '{"npub": "npub1test...", "message": "YOLO! It works!"}' +``` + +### 6. Monitor +```bash +npx wrangler tail --format pretty +``` + +## What's New in v2.0-YOLO + +1. **Smart Notifications** + - Mention detection (@npub...) + - Keyword alerts (urgent, emergency, etc.) + - Group activity monitoring + - Priority-based delivery + +2. **User Management** + - Registration/unregistration endpoints + - Preference storage in KV + - Quiet hours support + - Rate limiting + +3. **YOLO Testing** + - Test on live relays + - Auto-cleanup with kind 5 events + - Real-world scenarios + +4. **Performance** + - Notification batching + - Event deduplication + - WebSocket relay connections + - 5-minute polling interval (was 30) + +## File Structure +``` +cloudflare-worker/ +โ”œโ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ worker.ts # Original worker (v1.0) +โ”‚ โ”œโ”€โ”€ worker-enhanced.ts # New YOLO worker (v2.0) +โ”‚ โ”œโ”€โ”€ notification-system.ts # Core notification logic +โ”‚ โ”œโ”€โ”€ test-utils.ts # YOLO testing framework +โ”‚ โ””โ”€โ”€ test-scenarios.ts # Live relay tests +โ”œโ”€โ”€ wrangler.toml # Deployment config +โ”œโ”€โ”€ package.json # Dependencies +โ””โ”€โ”€ YOLO_DEPLOYMENT.md # This guide! +``` + +Remember: We're not reckless, we're just confident! ๐ŸŽ‰ +EOF 2>&1 diff --git a/worker/cloudflare-worker/YOLO_DEPLOYMENT.md b/worker/cloudflare-worker/YOLO_DEPLOYMENT.md new file mode 100644 index 00000000..4ff35655 --- /dev/null +++ b/worker/cloudflare-worker/YOLO_DEPLOYMENT.md @@ -0,0 +1,188 @@ +# YOLO Deployment Guide ๐Ÿš€ + +## Quick Deploy (Because who needs staging?) + +### 1. Install Dependencies +```bash +cd worker/cloudflare-worker +npm install +``` + +### 2. Configure Secrets +```bash +# Set up your Cloudflare secrets +wrangler secret put PUSH_DISPATCH_API +wrangler secret put BOT_TOKEN +wrangler secret put VAPID_PRIVATE_KEY +wrangler secret put VAPID_PUBLIC_KEY +``` + +### 3. Update wrangler.toml +```toml +name = "nostr-nip72-poller" +main = "src/worker-enhanced.ts" +compatibility_date = "2023-05-18" +node_compat = true + +[[kv_namespaces]] +binding = "KV" +id = "your-kv-namespace-id" + +[triggers] +crons = ["*/5 * * * *"] # Poll every 5 minutes YOLO! + +[env.production] +vars = { RELAY_URL = "wss://relay.chorus.community/" } +``` + +### 4. Deploy to Production (YOLO!) +```bash +npm run deploy +``` + +## Testing in Production (The YOLO Way) + +### 1. Register a Test User +```bash +curl -X POST https://nostr-nip72-poller.protestnet.workers.dev/register \ + -H "Content-Type: application/json" \ + -d '{ + "npub": "npub1test...", + "subscription": { + "endpoint": "https://fcm.googleapis.com/fcm/send/...", + "keys": { + "p256dh": "...", + "auth": "..." + } + }, + "preferences": { + "subscriptions": { + "groups": ["protest-net/test-group"], + "keywords": ["urgent", "test"], + "authors": [] + }, + "settings": { + "mentions": true, + "groupActivity": true, + "frequency": "immediate" + } + } + }' +``` + +### 2. Send a Test Notification +```bash +curl -X POST https://nostr-nip72-poller.protestnet.workers.dev/test-notification \ + -H "Content-Type: application/json" \ + -d '{ + "npub": "npub1test...", + "message": "YOLO! Testing in production! ๐Ÿš€" + }' +``` + +### 3. Run YOLO Tests +```bash +# Run all test scenarios (posts to live relays and cleans up) +npm run test:all + +# Test specific scenarios +npm run test:mention-detection +npm run test:notifications + +# Monitor live system +npm run monitor:notifications +``` + +### 4. Check Stats +```bash +curl https://nostr-nip72-poller.protestnet.workers.dev/stats +``` + +## Production Monitoring + +### Real-time Logs +```bash +wrangler tail --format pretty +``` + +### KV Storage Inspection +```bash +# List all users +wrangler kv:key list --binding KV --prefix "user:" + +# Check specific user +wrangler kv:key get --binding KV "user:npub1..." + +# View today's stats +wrangler kv:key get --binding KV "stats:2024-05-23" +``` + +## YOLO Best Practices + +1. **Test with Real Data**: Use live relays, real events, real notifications +2. **Clean Up After Yourself**: Always delete test events (kind 5) +3. **Monitor Everything**: Log liberally, check stats frequently +4. **Rate Limit Yourself**: Don't spam users, even in testing +5. **Have Fun**: It's YOLO, but responsible YOLO! + +## Rollback Plan (Just in Case) + +```bash +# Quick rollback to previous version +wrangler rollback + +# Or redeploy the old worker +cp src/worker.ts src/worker-enhanced.ts +npm run deploy +``` + +## Performance Targets + +- โšก Process 10,000 events per minute +- ๐Ÿš€ Sub-200ms notification delivery +- ๐Ÿ’ช 99.9% uptime +- ๐Ÿ“ฆ Less than 10MB KV storage per 1000 users + +## Debugging Production Issues + +### Check Recent Events +```javascript +// Add this temporary endpoint to worker +case '/debug/recent-events': + const events = await env.KV.list({ prefix: 'event_cache:' }); + return new Response(JSON.stringify(events), { + headers: { 'Content-Type': 'application/json' } + }); +``` + +### Force Reprocess Events +```bash +# Clear event cache to reprocess +wrangler kv:key delete --binding KV --prefix "event_cache:" +``` + +### Emergency Stop +```bash +# Disable cron trigger temporarily +wrangler deploy --compatibility-date 2023-05-18 --no-triggers +``` + +## Success Metrics + +- ๐Ÿ“ˆ Notification delivery rate > 95% +- โฑ๏ธ Average processing time < 100ms +- ๐Ÿ˜Š User satisfaction: "This is awesome!" +- ๐ŸŽ‰ Zero data loss (thanks to event cleanup!) + +Remember: We're not reckless, we're confidently iterating in production! ๐Ÿš€ + +## Support + +If something goes wrong (it won't, but just in case): +1. Check the logs: `wrangler tail` +2. Check the stats: `/stats` endpoint +3. Run cleanup: `npm run test:cleanup` +4. Rollback if needed: `wrangler rollback` + +Happy YOLO deploying! ๐ŸŽŠ +EOF 2>&1 diff --git a/worker/cloudflare-worker/package-lock.json b/worker/cloudflare-worker/package-lock.json new file mode 100644 index 00000000..0636ce28 --- /dev/null +++ b/worker/cloudflare-worker/package-lock.json @@ -0,0 +1,3945 @@ +{ + "name": "nostr-notification-worker", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nostr-notification-worker", + "version": "1.0.0", + "dependencies": { + "nostr-tools": "^2.3.1" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20240529.0", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.57.0", + "prettier": "^3.2.5", + "tsx": "^4.11.0", + "typescript": "^5.4.5", + "wrangler": "^3.57.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.4.tgz", + "integrity": "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "mime": "^3.0.0" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.0.2.tgz", + "integrity": "sha512-nyzYnlZjjV5xT3LizahG1Iu6mnrCaxglJ04rZLpDwlDVDZ7v46lNsfxhV3A/xtfgQuSHmLnc6SVI+KwBpc3Lwg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.14", + "workerd": "^1.20250124.0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20250408.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250408.0.tgz", + "integrity": "sha512-bxhIwBWxaNItZLXDNOKY2dCv0FHjDiDkfJFpwv4HvtvU5MKcrivZHVmmfDzLW85rqzfcDOmKbZeMPVfiKxdBZw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20250408.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250408.0.tgz", + "integrity": "sha512-5XZ2Oykr8bSo7zBmERtHh18h5BZYC/6H1YFWVxEj3PtalF3+6SHsO4KZsbGvDml9Pu7sHV277jiZE5eny8Hlyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20250408.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250408.0.tgz", + "integrity": "sha512-WbgItXWln6G5d7GvYLWcuOzAVwafysZaWunH3UEfsm95wPuRofpYnlDD861gdWJX10IHSVgMStGESUcs7FLerQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20250408.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250408.0.tgz", + "integrity": "sha512-pAhEywPPvr92SLylnQfZEPgXz+9pOG9G9haAPLpEatncZwYiYd9yiR6HYWhKp2erzCoNrOqKg9IlQwU3z1IDiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20250408.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250408.0.tgz", + "integrity": "sha512-nJ3RjMKGae2aF2rZ/CNeBvQPM+W5V1SUK0FYWG/uomyr7uQ2l4IayHna1ODg/OHHTEgIjwom0Mbn58iXb0WOcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20250523.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20250523.0.tgz", + "integrity": "sha512-0ghPbRDqjORo4qX8YkayNvJc7SSfK8cn5YhSkx1316vd+32cSlhqJTz8sRiavZiJw79glPq0rO1TNTJwvuuZOg==", + "dev": true, + "license": "MIT OR Apache-2.0" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild-plugins/node-globals-polyfill": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz", + "integrity": "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "esbuild": "*" + } + }, + "node_modules/@esbuild-plugins/node-modules-polyfill": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz", + "integrity": "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==", + "dev": true, + "license": "ISC", + "dependencies": { + "escape-string-regexp": "^4.0.0", + "rollup-plugin-node-polyfills": "^0.2.1" + }, + "peerDependencies": { + "esbuild": "*" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", + "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", + "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@noble/ciphers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz", + "integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@scure/base": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT" + }, + "node_modules/@scure/bip32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz", + "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.1.0", + "@noble/hashes": "~1.3.1", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.1" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/as-table": { + "version": "1.0.55", + "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", + "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "printable-characters": "^1.0.42" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", + "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/esbuild": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", + "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.4", + "@esbuild/android-arm": "0.25.4", + "@esbuild/android-arm64": "0.25.4", + "@esbuild/android-x64": "0.25.4", + "@esbuild/darwin-arm64": "0.25.4", + "@esbuild/darwin-x64": "0.25.4", + "@esbuild/freebsd-arm64": "0.25.4", + "@esbuild/freebsd-x64": "0.25.4", + "@esbuild/linux-arm": "0.25.4", + "@esbuild/linux-arm64": "0.25.4", + "@esbuild/linux-ia32": "0.25.4", + "@esbuild/linux-loong64": "0.25.4", + "@esbuild/linux-mips64el": "0.25.4", + "@esbuild/linux-ppc64": "0.25.4", + "@esbuild/linux-riscv64": "0.25.4", + "@esbuild/linux-s390x": "0.25.4", + "@esbuild/linux-x64": "0.25.4", + "@esbuild/netbsd-arm64": "0.25.4", + "@esbuild/netbsd-x64": "0.25.4", + "@esbuild/openbsd-arm64": "0.25.4", + "@esbuild/openbsd-x64": "0.25.4", + "@esbuild/sunos-x64": "0.25.4", + "@esbuild/win32-arm64": "0.25.4", + "@esbuild/win32-ia32": "0.25.4", + "@esbuild/win32-x64": "0.25.4" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/exit-hook": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", + "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exsolve": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.5.tgz", + "integrity": "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-source": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", + "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "data-uri-to-buffer": "^2.0.0", + "source-map": "^0.6.1" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/miniflare": { + "version": "3.20250408.2", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20250408.2.tgz", + "integrity": "sha512-uTs7cGWFErgJTKtBdmtctwhuoxniuCQqDT8+xaEiJdEC8d+HsaZVYfZwIX2NuSmdAiHMe7NtbdZYjFMbIXtJsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "acorn": "8.14.0", + "acorn-walk": "8.3.2", + "exit-hook": "2.2.1", + "glob-to-regexp": "0.4.1", + "stoppable": "1.1.0", + "undici": "^5.28.5", + "workerd": "1.20250408.0", + "ws": "8.18.0", + "youch": "3.3.4", + "zod": "3.22.3" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/miniflare/node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dev": true, + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nostr-tools": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.13.0.tgz", + "integrity": "sha512-A1arGsvpULqVK0NmZQqK1imwaCiPm8gcG/lo+cTax2NbNqBEYsuplbqAFdVqcGHEopmkByYbTwF76x25+oEbew==", + "license": "Unlicense", + "dependencies": { + "@noble/ciphers": "^0.5.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.1", + "@scure/base": "1.1.1", + "@scure/bip32": "1.3.1", + "@scure/bip39": "1.2.1" + }, + "optionalDependencies": { + "nostr-wasm": "0.1.0" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/nostr-wasm": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz", + "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", + "license": "MIT", + "optional": true + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/printable-characters": { + "version": "1.0.42", + "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", + "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup-plugin-inject": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz", + "integrity": "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^0.6.1", + "magic-string": "^0.25.3", + "rollup-pluginutils": "^2.8.1" + } + }, + "node_modules/rollup-plugin-node-polyfills": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz", + "integrity": "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rollup-plugin-inject": "^3.0.0" + } + }, + "node_modules/rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^0.6.1" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true, + "license": "MIT" + }, + "node_modules/stacktracey": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", + "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "as-table": "^1.0.36", + "get-source": "^2.0.12" + } + }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/tsx": { + "version": "4.19.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.4.tgz", + "integrity": "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.14", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.14.tgz", + "integrity": "sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "exsolve": "^1.0.1", + "ohash": "^2.0.10", + "pathe": "^2.0.3", + "ufo": "^1.5.4" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workerd": { + "version": "1.20250408.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250408.0.tgz", + "integrity": "sha512-bBUX+UsvpzAqiWFNeZrlZmDGddiGZdBBbftZJz2wE6iUg/cIAJeVQYTtS/3ahaicguoLBz4nJiDo8luqM9fx1A==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20250408.0", + "@cloudflare/workerd-darwin-arm64": "1.20250408.0", + "@cloudflare/workerd-linux-64": "1.20250408.0", + "@cloudflare/workerd-linux-arm64": "1.20250408.0", + "@cloudflare/workerd-windows-64": "1.20250408.0" + } + }, + "node_modules/wrangler": { + "version": "3.114.9", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.114.9.tgz", + "integrity": "sha512-1e0gL+rxLF04kM9bW4sxoDGLXpJ1x53Rx1t18JuUm6F67qadKKPISyUAXuBeIQudWrCWEBXaTVnSdLHz0yBXbA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.3.4", + "@cloudflare/unenv-preset": "2.0.2", + "@esbuild-plugins/node-globals-polyfill": "0.2.3", + "@esbuild-plugins/node-modules-polyfill": "0.2.2", + "blake3-wasm": "2.1.5", + "esbuild": "0.17.19", + "miniflare": "3.20250408.2", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.14", + "workerd": "1.20250408.0" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=16.17.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2", + "sharp": "^0.33.5" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20250408.0" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/darwin-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/wrangler/node_modules/esbuild": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", + "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.19", + "@esbuild/android-arm64": "0.17.19", + "@esbuild/android-x64": "0.17.19", + "@esbuild/darwin-arm64": "0.17.19", + "@esbuild/darwin-x64": "0.17.19", + "@esbuild/freebsd-arm64": "0.17.19", + "@esbuild/freebsd-x64": "0.17.19", + "@esbuild/linux-arm": "0.17.19", + "@esbuild/linux-arm64": "0.17.19", + "@esbuild/linux-ia32": "0.17.19", + "@esbuild/linux-loong64": "0.17.19", + "@esbuild/linux-mips64el": "0.17.19", + "@esbuild/linux-ppc64": "0.17.19", + "@esbuild/linux-riscv64": "0.17.19", + "@esbuild/linux-s390x": "0.17.19", + "@esbuild/linux-x64": "0.17.19", + "@esbuild/netbsd-x64": "0.17.19", + "@esbuild/openbsd-x64": "0.17.19", + "@esbuild/sunos-x64": "0.17.19", + "@esbuild/win32-arm64": "0.17.19", + "@esbuild/win32-ia32": "0.17.19", + "@esbuild/win32-x64": "0.17.19" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/youch": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz", + "integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie": "^0.7.1", + "mustache": "^4.2.0", + "stacktracey": "^2.1.8" + } + }, + "node_modules/zod": { + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", + "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/worker/cloudflare-worker/package.json b/worker/cloudflare-worker/package.json index 7f1abd24..7d65fc1f 100644 --- a/worker/cloudflare-worker/package.json +++ b/worker/cloudflare-worker/package.json @@ -1,24 +1,37 @@ { - "name": "nostr-nip72-poller", + "name": "nostr-notification-worker", "version": "1.0.0", - "description": "Cloudflare Worker for polling NIP-72 Nostr relays and dispatching push notifications", - "main": "src/worker.ts", + "description": "YOLO Notification System for Nostr Groups", + "type": "module", "scripts": { "dev": "wrangler dev", "deploy": "wrangler deploy", - "deploy:staging": "wrangler deploy --env staging", - "tail": "wrangler tail", - "test": "vitest", - "type-check": "tsc --noEmit" + "test": "npm run test:all", + "test:all": "tsx src/test-scenarios.ts", + "test:notifications": "tsx src/test-scenarios.ts --scenario=notifications", + "test:mention-detection": "tsx src/test-scenarios.ts --scenario=mentions", + "test:cleanup": "tsx src/cleanup-orphaned-events.ts", + "monitor:notifications": "tsx src/monitor-notifications.ts", + "build": "tsc", + "typecheck": "tsc --noEmit", + "lint": "eslint src --ext .ts", + "format": "prettier --write src/**/*.ts" + }, + "dependencies": { + "nostr-tools": "^2.3.1" }, "devDependencies": { - "@cloudflare/workers-types": "^4.20240925.0", - "@types/node": "^22.5.5", - "typescript": "^5.6.2", - "vitest": "^2.0.5", - "wrangler": "^3.78.2" + "@cloudflare/workers-types": "^4.20240529.0", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.57.0", + "prettier": "^3.2.5", + "tsx": "^4.11.0", + "typescript": "^5.4.5", + "wrangler": "^3.57.0" }, - "dependencies": { - "nostr-tools": "^2.7.2" + "engines": { + "node": ">=18" } } + diff --git a/worker/cloudflare-worker/src/notification-system.ts b/worker/cloudflare-worker/src/notification-system.ts new file mode 100644 index 00000000..fe13c931 --- /dev/null +++ b/worker/cloudflare-worker/src/notification-system.ts @@ -0,0 +1,411 @@ +/** + * Enhanced Notification System for Nostr Groups + * YOLO: Tests on live relays, cleans up after + */ + +import { NostrEvent, verifyEvent } from 'nostr-tools'; + +// Notification event types we care about +export const NOTIFICATION_EVENT_KINDS = { + GROUP_POST: 42, + REACTION: 7, + MODERATION: 9005, + DELETE: 5, + GROUP_META: 39000, + GROUP_ADMIN: 39001 +}; + +export interface NotificationTrigger { + eventId: string; + type: 'mention' | 'group_activity' | 'moderation' | 'keyword' | 'reaction'; + priority: 'high' | 'normal' | 'low'; + targetNpubs: string[]; + groupId?: string; + content: string; + timestamp: number; +} + +export interface UserPreferences { + npub: string; + pushEndpoint: string; + vapidAuth: string; + p256dh: string; + subscriptions: { + groups: string[]; + keywords: string[]; + authors: string[]; + }; + settings: { + mentions: boolean; + groupActivity: boolean; + reactions: boolean; + moderation: boolean; + frequency: 'immediate' | 'hourly' | 'daily'; + quietHours?: { + start: number; // hour (0-23) + end: number; + timezone: string; + }; + }; + lastNotified: number; + notificationCount: number; +} + +export class NotificationSystem { + private queue = new Map(); + + constructor(private env: any) {} + + /** + * Process a Nostr event and determine if it triggers notifications + */ + async processEvent(event: NostrEvent): Promise { + const triggers: NotificationTrigger[] = []; + + // Verify event signature + if (!verifyEvent(event)) { + console.warn('Invalid event signature:', event.id); + return triggers; + } + + // Extract mentions + const mentions = this.extractMentions(event); + if (mentions.length > 0) { + triggers.push({ + eventId: event.id, + type: 'mention', + priority: 'high', + targetNpubs: mentions, + groupId: this.extractGroupId(event), + content: event.content.substring(0, 100), + timestamp: event.created_at + }); + } + + // Check for moderation actions + if (event.kind === NOTIFICATION_EVENT_KINDS.MODERATION) { + const targetPubkey = event.tags.find(t => t[0] === 'p')?.[1]; + if (targetPubkey) { + triggers.push({ + eventId: event.id, + type: 'moderation', + priority: 'high', + targetNpubs: [targetPubkey], + groupId: this.extractGroupId(event), + content: 'Moderation action: ' + event.content.substring(0, 50), + timestamp: event.created_at + }); + } + } + + // Check for keyword matches + const keywordTriggers = await this.checkKeywordSubscriptions(event); + triggers.push(...keywordTriggers); + + // Group activity for subscribers + if (event.kind === NOTIFICATION_EVENT_KINDS.GROUP_POST) { + const groupId = this.extractGroupId(event); + if (groupId) { + const subscribers = await this.getGroupSubscribers(groupId); + if (subscribers.length > 0) { + triggers.push({ + eventId: event.id, + type: 'group_activity', + priority: 'normal', + targetNpubs: subscribers, + groupId, + content: event.content.substring(0, 100), + timestamp: event.created_at + }); + } + } + } + + return triggers; + } + + /** + * Extract mentions from event content and tags + */ + private extractMentions(event: NostrEvent): string[] { + const mentions = new Set(); + + // Extract from p tags + event.tags + .filter(tag => tag[0] === 'p') + .forEach(tag => mentions.add(tag[1])); + + // Extract from content (@npub... pattern) + const npubPattern = /@(npub[a-z0-9]{59})/gi; + const matches = event.content.matchAll(npubPattern); + for (const match of matches) { + // Convert npub to hex if needed + mentions.add(match[1]); + } + + return Array.from(mentions); + } + + /** + * Extract group ID from event tags + */ + private extractGroupId(event: NostrEvent): string | undefined { + return event.tags.find(tag => tag[0] === 'h')?.[1]; + } + + /** + * Check if event matches any keyword subscriptions + */ + private async checkKeywordSubscriptions(event: NostrEvent): Promise { + const triggers: NotificationTrigger[] = []; + + // Get all users with keyword subscriptions + const keywordSubs = await this.env.KV.list({ prefix: 'keywords:' }); + + for (const key of keywordSubs.keys) { + const keyword = key.name.replace('keywords:', ''); + if (event.content.toLowerCase().includes(keyword.toLowerCase())) { + const subscribers = await this.env.KV.get(key.name, 'json') as string[]; + + // Determine priority based on keyword + const urgentKeywords = ['urgent', 'emergency', 'action', 'important']; + const priority = urgentKeywords.includes(keyword.toLowerCase()) ? 'high' : 'normal'; + + triggers.push({ + eventId: event.id, + type: 'keyword', + priority, + targetNpubs: subscribers, + groupId: this.extractGroupId(event), + content: `Keyword "${keyword}": ${event.content.substring(0, 80)}`, + timestamp: event.created_at + }); + } + } + + return triggers; + } + + /** + * Get subscribers for a specific group + */ + private async getGroupSubscribers(groupId: string): Promise { + const subscribers = await this.env.KV.get(`group:${groupId}:subscribers`, 'json'); + return subscribers || []; + } + + /** + * Queue notification for delivery + */ + async queueNotification(trigger: NotificationTrigger): Promise { + for (const npub of trigger.targetNpubs) { + // Check user preferences + const prefs = await this.getUserPreferences(npub); + if (!prefs || !this.shouldNotify(prefs, trigger)) { + continue; + } + + // Add to queue + if (!this.queue.has(npub)) { + this.queue.set(npub, []); + } + this.queue.get(npub)!.push(trigger); + + // Send immediately for high priority + if (trigger.priority === 'high' || prefs.settings.frequency === 'immediate') { + await this.flushUserQueue(npub); + } + } + } + + /** + * Check if user should be notified based on preferences + */ + private shouldNotify(prefs: UserPreferences, trigger: NotificationTrigger): boolean { + // Check notification type settings + switch (trigger.type) { + case 'mention': + if (!prefs.settings.mentions) return false; + break; + case 'group_activity': + if (!prefs.settings.groupActivity) return false; + break; + case 'moderation': + if (!prefs.settings.moderation) return false; + break; + case 'reaction': + if (!prefs.settings.reactions) return false; + break; + } + + // Check quiet hours + if (prefs.settings.quietHours && trigger.priority !== 'high') { + const now = new Date(); + const hour = now.getHours(); + const { start, end } = prefs.settings.quietHours; + + if (start > end) { + // Quiet hours span midnight + if (hour >= start || hour < end) return false; + } else { + if (hour >= start && hour < end) return false; + } + } + + // Rate limiting - max 10 notifications per hour + const hourAgo = Date.now() - 3600000; + if (prefs.notificationCount > 10 && prefs.lastNotified > hourAgo) { + return false; + } + + return true; + } + + /** + * Get user preferences from KV + */ + private async getUserPreferences(npub: string): Promise { + return await this.env.KV.get(`user:${npub}`, 'json'); + } + + /** + * Flush notification queue for a specific user + */ + private async flushUserQueue(npub: string): Promise { + const notifications = this.queue.get(npub); + if (!notifications || notifications.length === 0) return; + + const prefs = await this.getUserPreferences(npub); + if (!prefs) return; + + // Aggregate notifications + const aggregated = this.aggregateNotifications(notifications); + + // Send push notification + await this.sendPushNotification(prefs, aggregated); + + // Update user stats + await this.updateUserStats(npub, notifications.length); + + // Clear queue + this.queue.delete(npub); + } + + /** + * Aggregate multiple notifications into a single message + */ + private aggregateNotifications(notifications: NotificationTrigger[]): { + title: string; + body: string; + data: any; + } { + if (notifications.length === 1) { + const n = notifications[0]; + return { + title: this.getNotificationTitle(n), + body: n.content, + data: { + eventId: n.eventId, + groupId: n.groupId, + type: n.type + } + }; + } + + // Multiple notifications + const mentions = notifications.filter(n => n.type === 'mention').length; + const activities = notifications.filter(n => n.type === 'group_activity').length; + + return { + title: `${notifications.length} new notifications`, + body: [ + mentions > 0 && `${mentions} mention${mentions > 1 ? 's' : ''}`, + activities > 0 && `${activities} group update${activities > 1 ? 's' : ''}` + ].filter(Boolean).join(', '), + data: { + eventIds: notifications.map(n => n.eventId), + groupIds: [...new Set(notifications.map(n => n.groupId).filter(Boolean))] + } + }; + } + + /** + * Get notification title based on type + */ + private getNotificationTitle(notification: NotificationTrigger): string { + switch (notification.type) { + case 'mention': + return '๐Ÿ’ฌ You were mentioned'; + case 'group_activity': + return `๐Ÿ“ข Activity in ${notification.groupId}`; + case 'moderation': + return '๐Ÿ›ก๏ธ Moderation notice'; + case 'keyword': + return '๐Ÿ”” Keyword alert'; + case 'reaction': + return '๐Ÿ‘ New reaction'; + default: + return '๐Ÿ“ฌ New notification'; + } + } + + /** + * Send push notification via API + */ + private async sendPushNotification( + user: UserPreferences, + notification: { title: string; body: string; data: any } + ): Promise { + const payload = { + endpoint: user.pushEndpoint, + keys: { + p256dh: user.p256dh, + auth: user.vapidAuth + }, + payload: JSON.stringify({ + title: notification.title, + body: notification.body, + icon: '/icon-192.png', + badge: '/badge-72.png', + data: notification.data, + timestamp: Date.now() + }) + }; + + const response = await fetch(this.env.PUSH_DISPATCH_API, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.env.BOT_TOKEN}` + }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + throw new Error(`Push API error: ${response.status}`); + } + } + + /** + * Update user notification stats + */ + private async updateUserStats(npub: string, count: number): Promise { + const prefs = await this.getUserPreferences(npub); + if (prefs) { + prefs.lastNotified = Date.now(); + prefs.notificationCount = (prefs.notificationCount || 0) + count; + await this.env.KV.put(`user:${npub}`, JSON.stringify(prefs)); + } + } + + /** + * Flush all queued notifications + */ + async flushAll(): Promise { + const flushPromises = Array.from(this.queue.keys()).map(npub => + this.flushUserQueue(npub) + ); + await Promise.all(flushPromises); + } +} +EOF 2>&1 diff --git a/worker/cloudflare-worker/src/test-scenarios.ts b/worker/cloudflare-worker/src/test-scenarios.ts new file mode 100644 index 00000000..5d865e26 --- /dev/null +++ b/worker/cloudflare-worker/src/test-scenarios.ts @@ -0,0 +1,176 @@ +/** + * YOLO Test Scenarios for Notification System + * Test real-world scenarios and clean up after + */ + +import { YOLOTester, generateTestKeypair } from './test-utils'; +import { Event } from 'nostr-tools'; + +const LIVE_RELAYS = [ + 'wss://relay.chorus.community/' +]; + +export async function runNotificationTests() { + const { privkey, pubkey } = generateTestKeypair(); + const tester = new YOLOTester(privkey); + + // Test 1: Mention notifications + await tester.runTestScenario( + 'Mention Notification Test', + LIVE_RELAYS, + async () => { + // Create a group post with mentions + const groupEvent = await tester.publishTestEvent(LIVE_RELAYS, { + kind: 42, // Group message + content: 'Hey @npub1234567890abcdef check this out!', + tags: [ + ['h', 'protest-net/test-group'], + ['p', 'npub1234567890abcdef'] // Mentioned user + ] + }); + + // Simulate verification that notification should be triggered + console.log('โœ… Group post with mention created:', groupEvent.id); + console.log('๐Ÿ’ฌ Notification should trigger for: npub1234567890abcdef'); + + // Add delay to let relays propagate + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Verify event was received by polling relay + // Your worker would detect this and trigger push notification + } + ); + + // Test 2: Group activity notifications + await tester.runTestScenario( + 'Group Activity Notification Test', + LIVE_RELAYS, + async () => { + const groupId = 'protest-net/campaigns/climate'; + + // Create multiple events in rapid succession + const events = []; + + // Post 1: New discussion + events.push(await tester.publishTestEvent(LIVE_RELAYS, { + kind: 42, + content: 'Important update about the climate march!', + tags: [['h', groupId]] + })); + + // Post 2: Reply + events.push(await tester.publishTestEvent(LIVE_RELAYS, { + kind: 42, + content: 'Great news! We have 500 confirmed attendees', + tags: [ + ['h', groupId], + ['e', events[0].id, '', 'reply'] + ] + })); + + // Post 3: Reaction + events.push(await tester.publishTestEvent(LIVE_RELAYS, { + kind: 7, + content: '+', + tags: [ + ['h', groupId], + ['e', events[0].id] + ] + })); + + console.log('โœ… Created activity burst in group:', groupId); + console.log('๐Ÿ“Š Events created:', events.length); + console.log('๐Ÿ”” Subscribed users should receive aggregated notification'); + } + ); + + // Test 3: Moderation action notifications + await tester.runTestScenario( + 'Moderation Notification Test', + LIVE_RELAYS, + async () => { + const groupId = 'protest-net/moderation-test'; + + // Create a post that gets moderated + const originalPost = await tester.publishTestEvent(LIVE_RELAYS, { + kind: 42, + content: 'This is a test post that will be moderated', + tags: [['h', groupId]] + }); + + // Moderation action + const moderationEvent = await tester.publishTestEvent(LIVE_RELAYS, { + kind: 9005, // Moderation action + content: 'Post removed: violates community guidelines', + tags: [ + ['h', groupId], + ['e', originalPost.id], + ['p', originalPost.pubkey], // Notify the author + ['action', 'delete'] + ] + }); + + console.log('๐Ÿ›ก๏ธ Moderation action triggered'); + console.log('๐Ÿ“ข Author should receive notification about moderation'); + } + ); + + // Test 4: High-priority keyword alerts + await tester.runTestScenario( + 'Keyword Alert Test', + LIVE_RELAYS, + async () => { + // Users might subscribe to keywords like "urgent", "emergency", "action needed" + const urgentPost = await tester.publishTestEvent(LIVE_RELAYS, { + kind: 42, + content: 'URGENT: Rally location changed to City Hall. Emergency meeting at 3pm!', + tags: [ + ['h', 'protest-net/announcements'], + ['priority', 'high'] + ] + }); + + console.log('๐Ÿšจ High-priority keyword post created'); + console.log('๐ŸŽฏ Users subscribed to "urgent" or "emergency" should be notified immediately'); + } + ); + + // Test 5: Cross-group conversation tracking + await tester.runTestScenario( + 'Cross-Group Conversation Test', + LIVE_RELAYS, + async () => { + // Start a conversation in one group + const originalPost = await tester.publishTestEvent(LIVE_RELAYS, { + kind: 42, + content: 'Climate activists: We need support from the transit union!', + tags: [ + ['h', 'protest-net/climate'], + ['cross-post', 'protest-net/transit-union'] + ] + }); + + // Cross-post to another group + const crossPost = await tester.publishTestEvent(LIVE_RELAYS, { + kind: 42, + content: 'Cross-posted from climate group: ' + originalPost.content, + tags: [ + ['h', 'protest-net/transit-union'], + ['e', originalPost.id, '', 'cross-post'], + ['original-group', 'protest-net/climate'] + ] + }); + + console.log('๐Ÿ”„ Cross-group conversation initiated'); + console.log('๐Ÿ‘ฅ Members of both groups should be notified of cross-collaboration'); + } + ); + + console.log('\n๐ŸŽ‰ All YOLO tests completed! Events have been published and cleaned up.'); +} + +// Run the tests +if (import.meta.main) { + runNotificationTests().catch(console.error); +} +EOF 2>&1 diff --git a/worker/cloudflare-worker/src/test-utils.ts b/worker/cloudflare-worker/src/test-utils.ts new file mode 100644 index 00000000..5b07450b --- /dev/null +++ b/worker/cloudflare-worker/src/test-utils.ts @@ -0,0 +1,105 @@ +/** + * YOLO Testing Utilities - Test with real relays and clean up after + */ + +import { SimplePool, Event, getEventHash, getSignature, generatePrivateKey, getPublicKey } from 'nostr-tools'; + +export class YOLOTester { + private pool: SimplePool; + private testEvents: string[] = []; // Track events to delete + private testPrivkey: string; + + constructor(privkey: string) { + this.pool = new SimplePool(); + this.testPrivkey = privkey; + } + + /** + * Publish test event and track for cleanup + */ + async publishTestEvent(relays: string[], event: Partial): Promise { + const fullEvent: Event = { + ...event, + created_at: Math.floor(Date.now() / 1000), + pubkey: getPublicKey(this.testPrivkey), + id: '', + sig: '' + } as Event; + + fullEvent.id = getEventHash(fullEvent); + fullEvent.sig = getSignature(fullEvent, this.testPrivkey); + + // Publish to relays + await Promise.all(this.pool.publish(relays, fullEvent)); + + // Track for cleanup + this.testEvents.push(fullEvent.id); + + console.log(`โœ… Published test event ${fullEvent.id}`); + return fullEvent; + } + + /** + * YOLO Cleanup - Delete all test events + */ + async cleanupTestEvents(relays: string[]): Promise { + console.log(`๐Ÿงน Cleaning up ${this.testEvents.length} test events...`); + + for (const eventId of this.testEvents) { + const deleteEvent: Partial = { + kind: 5, // Deletion event + tags: [['e', eventId]], + content: 'YOLO test cleanup' + }; + + const fullDelete: Event = { + ...deleteEvent, + created_at: Math.floor(Date.now() / 1000), + pubkey: getPublicKey(this.testPrivkey), + id: '', + sig: '' + } as Event; + + fullDelete.id = getEventHash(fullDelete); + fullDelete.sig = getSignature(fullDelete, this.testPrivkey); + + await Promise.all(this.pool.publish(relays, fullDelete)); + console.log(`๐Ÿ—‘๏ธ Deleted event ${eventId}`); + } + + this.testEvents = []; + this.pool.close(relays); + } + + /** + * Run test scenario with automatic cleanup + */ + async runTestScenario( + name: string, + relays: string[], + scenario: () => Promise + ): Promise { + console.log(`\n๐Ÿš€ YOLO Test: ${name}`); + console.log('='.repeat(50)); + + try { + await scenario(); + console.log(`โœ… Test "${name}" completed successfully`); + } catch (error) { + console.error(`โŒ Test "${name}" failed:`, error); + throw error; + } finally { + // Always cleanup, even if test fails + await this.cleanupTestEvents(relays); + console.log(`โœจ Cleanup complete for "${name}"`); + } + } +} + +// Helper to generate test keypairs +export function generateTestKeypair() { + const privkey = generatePrivateKey(); + const pubkey = getPublicKey(privkey); + return { privkey, pubkey }; +} +EOF 2>&1 diff --git a/worker/cloudflare-worker/src/worker-enhanced.ts b/worker/cloudflare-worker/src/worker-enhanced.ts new file mode 100644 index 00000000..2be06d74 --- /dev/null +++ b/worker/cloudflare-worker/src/worker-enhanced.ts @@ -0,0 +1,429 @@ +/** + * Enhanced Cloudflare Worker for NIP-72 Nostr Relay Polling + * Now with YOLO notification system! + */ + +import { NostrEvent, verifyEvent } from 'nostr-tools'; +import { NotificationSystem, NOTIFICATION_EVENT_KINDS } from './notification-system'; + +export interface Env { + KV: KVNamespace; + RELAY_URL: string; + PUSH_DISPATCH_API: string; + BOT_TOKEN: string; + VAPID_PUBLIC_KEY: string; + VAPID_PRIVATE_KEY: string; +} + +/** + * Main worker entry point - handles scheduled events and HTTP requests + */ +export default { + async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise { + console.log('๐Ÿš€ YOLO Scheduled polling triggered:', event.scheduledTime); + + try { + await pollRelayForUpdates(env, ctx); + } catch (error) { + console.error('โŒ Error in scheduled polling:', error); + } + }, + + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + const url = new URL(request.url); + + try { + switch (url.pathname) { + case '/heartbeat': + return handleHeartbeat(request, env); + case '/health': + return handleHealthCheck(env); + case '/stats': + return handleStats(env); + case '/register': + return handleUserRegistration(request, env); + case '/unregister': + return handleUserUnregistration(request, env); + case '/test-notification': + return handleTestNotification(request, env); + default: + return new Response('Not Found', { status: 404 }); + } + } catch (error) { + console.error('Error handling request:', error); + return new Response('Internal Server Error', { status: 500 }); + } + }, +}; + +/** + * Enhanced polling function with notification system + */ +async function pollRelayForUpdates(env: Env, ctx: ExecutionContext): Promise { + const relayUrl = env.RELAY_URL; + const relayId = new URL(relayUrl).hostname; + const notificationSystem = new NotificationSystem(env); + + // Get the last seen timestamp for this relay + const lastSeenKey = `relay_last_seen:${relayId}`; + const lastSeen = await env.KV.get(lastSeenKey); + const since = lastSeen ? parseInt(lastSeen) : Math.floor(Date.now() / 1000) - 3600; + + console.log(`๐Ÿ“ก Polling ${relayUrl} for events since ${new Date(since * 1000).toISOString()}`); + + try { + // Connect to relay and fetch new events + const events = await fetchEventsFromRelay(relayUrl, since); + console.log(`๐Ÿ“ฅ Fetched ${events.length} new events from relay`); + + let newestTimestamp = since; + let processedCount = 0; + let notificationCount = 0; + + for (const event of events) { + // Track newest event timestamp + if (event.created_at > newestTimestamp) { + newestTimestamp = event.created_at; + } + + // Check if we've already processed this event + const eventCacheKey = `event_cache:${event.id}`; + const cached = await env.KV.get(eventCacheKey); + + if (cached) { + console.log(`โญ๏ธ Skipping already processed event: ${event.id}`); + continue; + } + + // Process event for notifications + const triggers = await notificationSystem.processEvent(event); + + for (const trigger of triggers) { + await notificationSystem.queueNotification(trigger); + notificationCount++; + } + + // Cache the event ID to prevent reprocessing + await env.KV.put(eventCacheKey, '1', { + expirationTtl: 86400 // 24 hours + }); + + processedCount++; + + // Log interesting events + if (triggers.length > 0) { + console.log(`๐Ÿ”” Event ${event.id} triggered ${triggers.length} notifications`); + } + } + + // Flush all queued notifications + await notificationSystem.flushAll(); + + // Update the last seen timestamp + if (newestTimestamp > since) { + await env.KV.put(lastSeenKey, newestTimestamp.toString()); + } + + // Update stats + await updateStats(env, { + eventsProcessed: processedCount, + notificationsQueued: notificationCount, + lastPoll: Date.now() + }); + + console.log(`โœ… Processed ${processedCount} events, queued ${notificationCount} notifications`); + + } catch (error) { + console.error('โŒ Error polling relay:', error); + throw error; + } +} + +/** + * Fetch events from Nostr relay + */ +async function fetchEventsFromRelay(relayUrl: string, since: number): Promise { + // In production, use WebSocket connection + // For now, simulate with fetch + const ws = new WebSocket(relayUrl); + const events: NostrEvent[] = []; + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + ws.close(); + resolve(events); + }, 5000); // 5 second timeout + + ws.onopen = () => { + // Subscribe to relevant events + const filters = [ + { + kinds: Object.values(NOTIFICATION_EVENT_KINDS), + since: since, + limit: 100 + } + ]; + + ws.send(JSON.stringify(['REQ', 'worker-poll', ...filters])); + }; + + ws.onmessage = (msg) => { + try { + const data = JSON.parse(msg.data); + if (data[0] === 'EVENT' && data[2]) { + events.push(data[2]); + } else if (data[0] === 'EOSE') { + // End of stored events + clearTimeout(timeout); + ws.close(); + resolve(events); + } + } catch (error) { + console.error('Error parsing relay message:', error); + } + }; + + ws.onerror = (error) => { + clearTimeout(timeout); + reject(error); + }; + }); +} + +/** + * Handle user registration for push notifications + */ +async function handleUserRegistration(request: Request, env: Env): Promise { + if (request.method !== 'POST') { + return new Response('Method not allowed', { status: 405 }); + } + + try { + const body = await request.json() as { + npub: string; + subscription: { + endpoint: string; + keys: { + p256dh: string; + auth: string; + }; + }; + preferences: any; + }; + + const userPrefs = { + npub: body.npub, + pushEndpoint: body.subscription.endpoint, + p256dh: body.subscription.keys.p256dh, + vapidAuth: body.subscription.keys.auth, + subscriptions: body.preferences?.subscriptions || { + groups: [], + keywords: [], + authors: [] + }, + settings: body.preferences?.settings || { + mentions: true, + groupActivity: true, + reactions: false, + moderation: true, + frequency: 'immediate' + }, + lastNotified: 0, + notificationCount: 0 + }; + + // Store user preferences + await env.KV.put(`user:${body.npub}`, JSON.stringify(userPrefs)); + + // Update keyword subscriptions + for (const keyword of userPrefs.subscriptions.keywords) { + const key = `keywords:${keyword}`; + const existing = await env.KV.get(key, 'json') as string[] || []; + if (!existing.includes(body.npub)) { + existing.push(body.npub); + await env.KV.put(key, JSON.stringify(existing)); + } + } + + // Update group subscriptions + for (const groupId of userPrefs.subscriptions.groups) { + const key = `group:${groupId}:subscribers`; + const existing = await env.KV.get(key, 'json') as string[] || []; + if (!existing.includes(body.npub)) { + existing.push(body.npub); + await env.KV.put(key, JSON.stringify(existing)); + } + } + + return new Response(JSON.stringify({ success: true }), { + headers: { 'Content-Type': 'application/json' } + }); + + } catch (error) { + console.error('Registration error:', error); + return new Response('Bad request', { status: 400 }); + } +} + +/** + * Handle user unregistration + */ +async function handleUserUnregistration(request: Request, env: Env): Promise { + if (request.method !== 'POST') { + return new Response('Method not allowed', { status: 405 }); + } + + try { + const body = await request.json() as { npub: string }; + + // Get user preferences to clean up subscriptions + const prefs = await env.KV.get(`user:${body.npub}`, 'json') as any; + if (prefs) { + // Remove from keyword subscriptions + for (const keyword of prefs.subscriptions?.keywords || []) { + const key = `keywords:${keyword}`; + const subscribers = await env.KV.get(key, 'json') as string[] || []; + const updated = subscribers.filter(n => n !== body.npub); + await env.KV.put(key, JSON.stringify(updated)); + } + + // Remove from group subscriptions + for (const groupId of prefs.subscriptions?.groups || []) { + const key = `group:${groupId}:subscribers`; + const subscribers = await env.KV.get(key, 'json') as string[] || []; + const updated = subscribers.filter(n => n !== body.npub); + await env.KV.put(key, JSON.stringify(updated)); + } + } + + // Delete user preferences + await env.KV.delete(`user:${body.npub}`); + + return new Response(JSON.stringify({ success: true }), { + headers: { 'Content-Type': 'application/json' } + }); + + } catch (error) { + console.error('Unregistration error:', error); + return new Response('Bad request', { status: 400 }); + } +} + +/** + * Handle test notification - YOLO style! + */ +async function handleTestNotification(request: Request, env: Env): Promise { + if (request.method !== 'POST') { + return new Response('Method not allowed', { status: 405 }); + } + + try { + const body = await request.json() as { npub: string; message?: string }; + const notificationSystem = new NotificationSystem(env); + + // Create a test trigger + const trigger = { + eventId: 'test-' + Date.now(), + type: 'mention' as const, + priority: 'high' as const, + targetNpubs: [body.npub], + content: body.message || '๐Ÿš€ YOLO Test notification! If you see this, it works!', + timestamp: Math.floor(Date.now() / 1000) + }; + + await notificationSystem.queueNotification(trigger); + await notificationSystem.flushAll(); + + return new Response(JSON.stringify({ + success: true, + message: 'Test notification sent! YOLO!' + }), { + headers: { 'Content-Type': 'application/json' } + }); + + } catch (error) { + console.error('Test notification error:', error); + return new Response('Bad request', { status: 400 }); + } +} + +/** + * Update worker stats + */ +async function updateStats(env: Env, stats: any): Promise { + const today = new Date().toISOString().split('T')[0]; + const key = `stats:${today}`; + + const existing = await env.KV.get(key, 'json') as any || { + eventsProcessed: 0, + notificationsQueued: 0, + polls: 0 + }; + + existing.eventsProcessed += stats.eventsProcessed; + existing.notificationsQueued += stats.notificationsQueued; + existing.polls += 1; + existing.lastPoll = stats.lastPoll; + + await env.KV.put(key, JSON.stringify(existing), { + expirationTtl: 7 * 24 * 60 * 60 // Keep for 7 days + }); +} + +/** + * Handle stats endpoint + */ +async function handleStats(env: Env): Promise { + const today = new Date().toISOString().split('T')[0]; + const stats = await env.KV.get(`stats:${today}`, 'json') || {}; + + const userCount = await env.KV.list({ prefix: 'user:' }); + const groupCount = await env.KV.list({ prefix: 'group:' }); + + return new Response(JSON.stringify({ + today: stats, + users: { + total: userCount.keys.length + }, + groups: { + total: groupCount.keys.length + }, + worker: { + version: '2.0-YOLO', + lastDeploy: new Date().toISOString() + } + }), { + headers: { 'Content-Type': 'application/json' } + }); +} + +/** + * Handle heartbeat endpoint + */ +async function handleHeartbeat(request: Request, env: Env): Promise { + return new Response(JSON.stringify({ + status: 'alive', + timestamp: new Date().toISOString(), + message: 'YOLO notification worker is running! ๐Ÿš€' + }), { + headers: { 'Content-Type': 'application/json' } + }); +} + +/** + * Handle health check endpoint + */ +async function handleHealthCheck(env: Env): Promise { + try { + // Check KV is accessible + await env.KV.get('health_check'); + + // Check relay connectivity (optional) + // Could add WebSocket ping here + + return new Response('OK', { status: 200 }); + } catch (error) { + return new Response('Service Unavailable', { status: 503 }); + } +} +EOF 2>&1 diff --git a/worker/cloudflare-worker/test-live.js b/worker/cloudflare-worker/test-live.js new file mode 100644 index 00000000..f6231abf --- /dev/null +++ b/worker/cloudflare-worker/test-live.js @@ -0,0 +1,19 @@ +#!/usr/bin/env node + +console.log("๐Ÿš€ YOLO Test Runner"); +console.log("=================="); +console.log(""); +console.log("This would normally run live tests on Nostr relays."); +console.log("For safety, we're just showing what would happen:"); +console.log(""); +console.log("1. Generate test keypair"); +console.log("2. Publish test events to relays:"); +console.log(" - Mention notifications"); +console.log(" - Group activity"); +console.log(" - Keyword alerts"); +console.log("3. Wait for events to propagate"); +console.log("4. Delete all test events (kind 5)"); +console.log("5. Verify cleanup completed"); +console.log(""); +console.log("To actually run tests with real relays, implement the test-scenarios.ts!"); +EOF 2>&1 diff --git a/worker/cloudflare-worker/test-notification-system.js b/worker/cloudflare-worker/test-notification-system.js new file mode 100644 index 00000000..4d4d39be --- /dev/null +++ b/worker/cloudflare-worker/test-notification-system.js @@ -0,0 +1,51 @@ +// Test the notification system logic locally +import { NotificationSystem, NOTIFICATION_EVENT_KINDS } from './src/notification-system.ts'; + +console.log('Testing Notification System Logic'); +console.log('================================='); + +// Mock environment +const mockEnv = { + KV: { + get: async (key) => { + console.log(`KV.get(${key})`); + return null; + }, + put: async (key, value) => { + console.log(`KV.put(${key}, ${value})`); + }, + list: async (options) => { + console.log(`KV.list(${JSON.stringify(options)})`); + return { keys: [] }; + } + }, + PUSH_DISPATCH_API: 'https://mock-api.com', + BOT_TOKEN: 'mock-token' +}; + +// Test event +const testEvent = { + id: 'test123', + kind: 42, + pubkey: 'testpubkey', + created_at: Math.floor(Date.now() / 1000), + tags: [ + ['h', 'protest-net/test-group'], + ['p', 'mentioneduser'] + ], + content: 'Hey @mentioneduser check out this urgent message!', + sig: 'testsig' +}; + +console.log('\nTest Event:', JSON.stringify(testEvent, null, 2)); + +// Create notification system +const notifSystem = new NotificationSystem(mockEnv); + +// Process event +console.log('\nProcessing event...'); +const triggers = await notifSystem.processEvent(testEvent); + +console.log('\nTriggered Notifications:', triggers); +console.log('\nโœ… Notification system logic verified!'); +EOF 2>&1 diff --git a/worker/deploy/enhanced-worker.js b/worker/deploy/enhanced-worker.js index b4ede366..8db9be0f 100644 --- a/worker/deploy/enhanced-worker.js +++ b/worker/deploy/enhanced-worker.js @@ -392,7 +392,7 @@ class NotificationWorker { constructor(env) { this.env = env; this.kv = env.NOTIFICATION_KV; - this.relays = (env.NOSTR_RELAYS || 'wss://relay.damus.io,wss://nos.lol,wss://relay.snort.social').split(','); + this.relays = (env.NOSTR_RELAYS || 'wss://relay.chorus.community/').split(','); this.nostr = new NostrClient(this.relays); this.notificationBuilder = new NotificationBuilder(); } diff --git a/worker/deploy/wrangler.toml.template b/worker/deploy/wrangler.toml.template index e56e974a..15499114 100644 --- a/worker/deploy/wrangler.toml.template +++ b/worker/deploy/wrangler.toml.template @@ -5,7 +5,7 @@ compatibility_flags = ["nodejs_compat"] # account_id = "your-cloudflare-account-id" # Set this to your Cloudflare account ID [vars] -RELAY_URL = "wss://relay.damus.io" +RELAY_URL = "wss://relay.chorus.community/" # BOT_TOKEN = "your-bot-token" # Set this as a secret with: wrangler secret put BOT_TOKEN [[kv_namespaces]] diff --git a/worker/worker_prd.md b/worker/worker_prd.md index e276445b..ee09efda 100644 --- a/worker/worker_prd.md +++ b/worker/worker_prd.md @@ -224,7 +224,7 @@ kv_namespaces = [ { binding = "KV", id = "" } ] crons = ["*/0.5 * * * * *"] # every 30ย s -vars = { RELAY_URL = "wss://public.nip72relay.example" } +vars = { RELAY_URL = "wss://relay.chorus.community/" } ``` ### 17.6 Additional LLM Task Prompts From 32637e0f283d03940f81bd1e61c0a05520d0d664 Mon Sep 17 00:00:00 2001 From: rabble Date: Fri, 23 May 2025 19:34:14 +0200 Subject: [PATCH 03/14] Fix: Remove EOF markers and fix TypeScript linting errors in worker files --- package-lock.json | 120 +++++++++++++++++- .../src/notification-system.ts | 7 +- .../cloudflare-worker/src/test-scenarios.ts | 1 - worker/cloudflare-worker/src/test-utils.ts | 1 - .../cloudflare-worker/src/worker-enhanced.ts | 9 +- worker/cloudflare-worker/test-live.js | 1 - .../test-notification-system.js | 1 - worker/deploy/enhanced-worker.js | 1 - worker/deploy/user-registration.js | 1 - worker/deploy/web-push.js | 1 - 10 files changed, 123 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3610ba47..b546deeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -93,6 +93,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -941,6 +942,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -958,6 +960,7 @@ "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", @@ -972,6 +975,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -981,6 +985,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -990,12 +995,14 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1080,6 +1087,7 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -1093,6 +1101,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -1102,6 +1111,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -1141,6 +1151,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -3207,14 +3218,14 @@ "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.22", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.22.tgz", "integrity": "sha512-vUhG0YmQZ7kL/tmKLrD3g5zXbXXreZXB3pmROW8bg3CnLnpjkRVwUlLne7Ufa2r9yJ8+/6B73RzhAek5TBKh2Q==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -3225,7 +3236,7 @@ "version": "18.3.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" @@ -3529,6 +3540,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -3541,6 +3553,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3556,12 +3569,14 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, "license": "MIT" }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -3575,6 +3590,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -3638,6 +3654,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/base64-js": { @@ -3664,6 +3681,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3687,6 +3705,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -3766,6 +3785,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -3813,6 +3833,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -3837,6 +3858,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -3886,6 +3908,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -3898,12 +3921,14 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, "license": "MIT" }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -3920,6 +3945,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -3934,6 +3960,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -4119,12 +4146,14 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, "license": "Apache-2.0" }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, "license": "MIT" }, "node_modules/dom-helpers": { @@ -4141,6 +4170,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, "license": "MIT" }, "node_modules/electron-to-chromium": { @@ -4196,6 +4226,7 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, "license": "MIT" }, "node_modules/esbuild": { @@ -4466,6 +4497,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -4482,6 +4514,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -4508,6 +4541,7 @@ "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -4530,6 +4564,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -4585,6 +4620,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -4615,6 +4651,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -4629,6 +4666,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4647,6 +4685,7 @@ "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -4667,6 +4706,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -4679,6 +4719,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -4688,6 +4729,7 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -4733,6 +4775,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4821,6 +4864,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -4833,6 +4877,7 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -4848,6 +4893,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4857,6 +4903,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4866,6 +4913,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -4878,6 +4926,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -4887,12 +4936,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, "license": "ISC" }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -4908,6 +4959,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -4981,6 +5033,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -4993,6 +5046,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, "license": "MIT" }, "node_modules/locate-path": { @@ -5081,6 +5135,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -5090,6 +5145,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -5116,6 +5172,7 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -5132,6 +5189,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0", @@ -5143,6 +5201,7 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, "funding": [ { "type": "github", @@ -5185,6 +5244,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5245,6 +5305,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -5304,6 +5365,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/parent-module": { @@ -5333,6 +5395,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5342,12 +5405,14 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -5364,12 +5429,14 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -5382,6 +5449,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5391,6 +5459,7 @@ "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -5400,6 +5469,7 @@ "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, "funding": [ { "type": "opencollective", @@ -5428,6 +5498,7 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -5445,6 +5516,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" @@ -5464,6 +5536,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -5499,6 +5572,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -5524,6 +5598,7 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -5551,6 +5626,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, "license": "MIT" }, "node_modules/prelude-ls": { @@ -5609,6 +5685,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, "funding": [ { "type": "github", @@ -5845,6 +5922,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, "license": "MIT", "dependencies": { "pify": "^2.3.0" @@ -5854,6 +5932,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -5898,6 +5977,7 @@ "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -5928,6 +6008,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -5978,6 +6059,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "funding": [ { "type": "github", @@ -6023,6 +6105,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -6035,6 +6118,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6044,6 +6128,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -6066,6 +6151,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -6075,6 +6161,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -6093,6 +6180,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -6107,6 +6195,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6116,12 +6205,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -6134,6 +6225,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -6150,6 +6242,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -6162,6 +6255,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6184,6 +6278,7 @@ "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -6219,6 +6314,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6241,6 +6337,7 @@ "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -6287,6 +6384,7 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -6300,6 +6398,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0" @@ -6309,6 +6408,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" @@ -6372,6 +6472,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -6406,6 +6507,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, "license": "Apache-2.0" }, "node_modules/tslib": { @@ -6431,7 +6533,7 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -6568,6 +6670,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/uuid": { @@ -6731,6 +6834,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -6756,6 +6860,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -6774,6 +6879,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -6791,6 +6897,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6800,12 +6907,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -6820,6 +6929,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -6832,6 +6942,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -6844,6 +6955,7 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/worker/cloudflare-worker/src/notification-system.ts b/worker/cloudflare-worker/src/notification-system.ts index fe13c931..b37c04d7 100644 --- a/worker/cloudflare-worker/src/notification-system.ts +++ b/worker/cloudflare-worker/src/notification-system.ts @@ -54,7 +54,7 @@ export interface UserPreferences { export class NotificationSystem { private queue = new Map(); - constructor(private env: any) {} + constructor(private env: unknown) {} /** * Process a Nostr event and determine if it triggers notifications @@ -297,7 +297,7 @@ export class NotificationSystem { private aggregateNotifications(notifications: NotificationTrigger[]): { title: string; body: string; - data: any; + data: unknown; } { if (notifications.length === 1) { const n = notifications[0]; @@ -354,7 +354,7 @@ export class NotificationSystem { */ private async sendPushNotification( user: UserPreferences, - notification: { title: string; body: string; data: any } + notification: { title: string; body: string; data: unknown } ): Promise { const payload = { endpoint: user.pushEndpoint, @@ -408,4 +408,3 @@ export class NotificationSystem { await Promise.all(flushPromises); } } -EOF 2>&1 diff --git a/worker/cloudflare-worker/src/test-scenarios.ts b/worker/cloudflare-worker/src/test-scenarios.ts index 5d865e26..9807c7bf 100644 --- a/worker/cloudflare-worker/src/test-scenarios.ts +++ b/worker/cloudflare-worker/src/test-scenarios.ts @@ -173,4 +173,3 @@ export async function runNotificationTests() { if (import.meta.main) { runNotificationTests().catch(console.error); } -EOF 2>&1 diff --git a/worker/cloudflare-worker/src/test-utils.ts b/worker/cloudflare-worker/src/test-utils.ts index 5b07450b..ac67bace 100644 --- a/worker/cloudflare-worker/src/test-utils.ts +++ b/worker/cloudflare-worker/src/test-utils.ts @@ -102,4 +102,3 @@ export function generateTestKeypair() { const pubkey = getPublicKey(privkey); return { privkey, pubkey }; } -EOF 2>&1 diff --git a/worker/cloudflare-worker/src/worker-enhanced.ts b/worker/cloudflare-worker/src/worker-enhanced.ts index 2be06d74..9c889cc1 100644 --- a/worker/cloudflare-worker/src/worker-enhanced.ts +++ b/worker/cloudflare-worker/src/worker-enhanced.ts @@ -208,7 +208,7 @@ async function handleUserRegistration(request: Request, env: Env): Promise { +async function updateStats(env: Env, stats: unknown): Promise { const today = new Date().toISOString().split('T')[0]; const key = `stats:${today}`; - const existing = await env.KV.get(key, 'json') as any || { + const existing = await env.KV.get(key, 'json') as { timestamp: number } | null || { eventsProcessed: 0, notificationsQueued: 0, polls: 0 @@ -426,4 +426,3 @@ async function handleHealthCheck(env: Env): Promise { return new Response('Service Unavailable', { status: 503 }); } } -EOF 2>&1 diff --git a/worker/cloudflare-worker/test-live.js b/worker/cloudflare-worker/test-live.js index f6231abf..ab13f8ec 100644 --- a/worker/cloudflare-worker/test-live.js +++ b/worker/cloudflare-worker/test-live.js @@ -16,4 +16,3 @@ console.log("4. Delete all test events (kind 5)"); console.log("5. Verify cleanup completed"); console.log(""); console.log("To actually run tests with real relays, implement the test-scenarios.ts!"); -EOF 2>&1 diff --git a/worker/cloudflare-worker/test-notification-system.js b/worker/cloudflare-worker/test-notification-system.js index 4d4d39be..712c63fc 100644 --- a/worker/cloudflare-worker/test-notification-system.js +++ b/worker/cloudflare-worker/test-notification-system.js @@ -48,4 +48,3 @@ const triggers = await notifSystem.processEvent(testEvent); console.log('\nTriggered Notifications:', triggers); console.log('\nโœ… Notification system logic verified!'); -EOF 2>&1 diff --git a/worker/deploy/enhanced-worker.js b/worker/deploy/enhanced-worker.js index 8db9be0f..91f82213 100644 --- a/worker/deploy/enhanced-worker.js +++ b/worker/deploy/enhanced-worker.js @@ -877,4 +877,3 @@ export default { } } }; -EOF 2>&1 diff --git a/worker/deploy/user-registration.js b/worker/deploy/user-registration.js index 670cd876..c830df94 100644 --- a/worker/deploy/user-registration.js +++ b/worker/deploy/user-registration.js @@ -236,4 +236,3 @@ export const enhancedEndpoints = { } } }; -EOF 2>&1 diff --git a/worker/deploy/web-push.js b/worker/deploy/web-push.js index 8d18a06d..4b65c20f 100644 --- a/worker/deploy/web-push.js +++ b/worker/deploy/web-push.js @@ -365,4 +365,3 @@ export async function updatePushStats(kv, result) { } export { WebPush }; -EOF 2>&1 From a21b40f7659db6a376d747137b3d75ca0503b5ec Mon Sep 17 00:00:00 2001 From: rabble Date: Fri, 23 May 2025 19:51:20 +0200 Subject: [PATCH 04/14] feat: Add push notification permission system with smart prompting - Created NotificationPermissionPrompt component that shows after user engagement - Added useTrackEngagement hook to track when users join groups, create groups, or set up wallet - Integrated tracking into JoinDialogProvider, CreateGroup, and useCreateCashuWallet - Updated service worker to handle push notifications and notification clicks - Added environment variables for worker URL and VAPID public key - Shows notification prompt 3 seconds after meaningful user action - Sends push subscription to worker with user's npub --- .env.example | 5 + public/sw.js | 76 +++++++- public/sw.js.bak | 18 ++ src/App.tsx | 2 + src/components/groups/JoinDialogProvider.tsx | 100 +++++++---- .../groups/JoinDialogProvider.tsx.bak | 91 ++++++++++ .../NotificationPermissionPrompt.tsx | 169 ++++++++++++++++++ src/hooks/useCreateCashuWallet.ts | 5 + src/hooks/useTrackEngagement.ts | 29 +++ src/pages/CreateGroup.tsx | 1 + 10 files changed, 457 insertions(+), 39 deletions(-) create mode 100644 .env.example create mode 100644 public/sw.js.bak create mode 100644 src/components/groups/JoinDialogProvider.tsx.bak create mode 100644 src/components/notifications/NotificationPermissionPrompt.tsx create mode 100644 src/hooks/useTrackEngagement.ts diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..80770596 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ + +# Push Notification Configuration +VITE_WORKER_URL=https://your-worker-url.workers.dev +VITE_VAPID_PUBLIC_KEY=your-vapid-public-key-here +EOF 2>&1 diff --git a/public/sw.js b/public/sw.js index fb80045f..2f1dd3a2 100644 --- a/public/sw.js +++ b/public/sw.js @@ -15,4 +15,78 @@ self.addEventListener('activate', (event) => { self.addEventListener('fetch', (event) => { // Simply pass through all requests without caching event.respondWith(fetch(event.request)); -}); \ No newline at end of file +}); +// Push notification handling +self.addEventListener('push', function(event) { + if (!event.data) return; + + try { + const data = event.data.json(); + + const options = { + body: data.body || 'You have a new notification', + icon: data.icon || '/icons/icon-192x192.png', + badge: data.badge || '/icons/icon-96x96.png', + vibrate: data.vibrate || [200, 100, 200], + data: { + url: data.url || '/', + ...data.data + }, + requireInteraction: data.requireInteraction || false, + actions: data.actions || [] + }; + + event.waitUntil( + self.registration.showNotification( + data.title || 'Chorus Notification', + options + ) + ); + } catch (error) { + console.error('Error processing push notification:', error); + } +}); + +// Handle notification clicks +self.addEventListener('notificationclick', function(event) { + event.notification.close(); + + const urlToOpen = event.notification.data?.url || '/'; + + event.waitUntil( + clients.matchAll({ type: 'window', includeUncontrolled: true }) + .then(function(windowClients) { + // Check if there's already a window/tab open with the target URL + for (let i = 0; i < windowClients.length; i++) { + const client = windowClients[i]; + if (client.url.includes(urlToOpen) && 'focus' in client) { + return client.focus(); + } + } + // If not, open a new window/tab + if (clients.openWindow) { + return clients.openWindow(urlToOpen); + } + }) + ); +}); + +// Handle push subscription change +self.addEventListener('pushsubscriptionchange', function(event) { + event.waitUntil( + self.registration.pushManager.subscribe(event.oldSubscription.options) + .then(function(subscription) { + // Send new subscription to server + return fetch('/api/notifications/resubscribe', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + oldEndpoint: event.oldSubscription.endpoint, + newSubscription: subscription.toJSON() + }) + }); + }) + ); +}); diff --git a/public/sw.js.bak b/public/sw.js.bak new file mode 100644 index 00000000..fb80045f --- /dev/null +++ b/public/sw.js.bak @@ -0,0 +1,18 @@ +// Bare minimum service worker for PWA functionality +// No caching - just enough to be considered a PWA + +// Install event - skip waiting to activate immediately +self.addEventListener('install', (event) => { + self.skipWaiting(); +}); + +// Activate event - claim clients immediately +self.addEventListener('activate', (event) => { + event.waitUntil(self.clients.claim()); +}); + +// Fetch event - pass through all requests to network +self.addEventListener('fetch', (event) => { + // Simply pass through all requests without caching + event.respondWith(fetch(event.request)); +}); \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 965bc9b5..4331e815 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import { TooltipProvider } from "@/components/ui/tooltip"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { NostrLoginProvider } from '@nostrify/react/login'; import AppRouter from './AppRouter'; +import { NotificationPermissionPrompt } from "@/components/notifications/NotificationPermissionPrompt"; import { useSystemTheme } from '@/hooks/useSystemTheme'; import { JoinDialogProvider } from '@/components/groups/JoinDialogProvider'; @@ -38,6 +39,7 @@ export function App() { + diff --git a/src/components/groups/JoinDialogProvider.tsx b/src/components/groups/JoinDialogProvider.tsx index 790fb870..3684d4d3 100644 --- a/src/components/groups/JoinDialogProvider.tsx +++ b/src/components/groups/JoinDialogProvider.tsx @@ -1,20 +1,22 @@ -import React, { useState, useCallback } from "react"; -import { createPortal } from "react-dom"; -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; -import { Textarea } from "@/components/ui/textarea"; +import { JoinDialogContext } from "./JoinDialogContext"; +import React, { createContext, useState, useCallback } from "react"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "../ui/dialog"; +import { Button } from "../ui/button"; +import { Textarea } from "../ui/textarea"; +import { Label } from "../ui/label"; +import { useCurrentUser } from "@/hooks/useCurrentUser"; import { useNostrPublish } from "@/hooks/useNostrPublish"; import { toast } from "sonner"; -import { useCurrentUser } from "@/hooks/useCurrentUser"; -import { JoinDialogContext } from "./JoinDialogContext"; +import { useTrackEngagement } from "@/hooks/useTrackEngagement"; + -// Create a provider component export function JoinDialogProvider({ children }: { children: React.ReactNode }) { const [isOpen, setIsOpen] = useState(false); const [joinReason, setJoinReason] = useState(""); const [currentCommunityId, setCurrentCommunityId] = useState(""); const { user } = useCurrentUser(); const { mutateAsync: publishEvent, isPending } = useNostrPublish(); + const { trackJoinedGroup } = useTrackEngagement(); // Function to open the dialog with a specific communityId const openJoinDialog = useCallback((communityId: string) => { @@ -40,6 +42,9 @@ export function JoinDialogProvider({ children }: { children: React.ReactNode }) content: joinReason, }); + // Track that the user has joined a group + trackJoinedGroup(); + toast.success("Join request sent successfully!"); setIsOpen(false); } catch (error) { @@ -48,44 +53,63 @@ export function JoinDialogProvider({ children }: { children: React.ReactNode }) } }; + const closeJoinDialog = useCallback(() => { + setIsOpen(false); + setJoinReason(""); + }, []); + + const contextValue = { + openJoinDialog, + closeJoinDialog, + isDialogOpen: isOpen, + }; return ( - + {children} - - {/* The dialog is rendered at the portal level */} - {createPortal( - - - - Request to join this group - - Your request will be reviewed by the group moderators. - - - -
-