diff --git a/.claude/commands/release.md b/.claude/commands/release.md new file mode 100644 index 0000000..5120229 --- /dev/null +++ b/.claude/commands/release.md @@ -0,0 +1,62 @@ +# Release Command + +Review all changes in the repository and create a release with proper commit message, version tag, and push to origin. + +## Argument: $ARGUMENTS + +The argument should be one of: +- `patch` - Bump the patch version (e.g., 0.0.4 -> 0.0.5) +- `minor` - Bump the minor version and reset patch to 0 (e.g., 0.0.4 -> 0.1.0) +- `major` - Bump the major version and reset minor/patch to 0 (e.g., 0.0.4 -> 1.0.0) + +If no argument provided, default to `patch`. + +## Steps to perform: + +1. **Read the current version** from `package.json` (the `version` field) + +2. **Calculate the new version** based on the argument: + - Parse the current version (format: MAJOR.MINOR.PATCH) + - If `patch`: increment PATCH by 1 + - If `minor`: increment MINOR by 1, set PATCH to 0 + - If `major`: increment MAJOR by 1, set MINOR and PATCH to 0 + +3. **Update package.json** with the new version in all three places: + - `version` + - `custom.chrome.version` + - `custom.firefox.version` + +4. **Review changes** using `git status` and `git diff --stat HEAD` + +5. **Verify the build** before committing: + ``` + npm run lint + npm run build:chrome + npm run build:firefox + ``` + If any step fails, fix issues before proceeding. + +6. **Compose a commit message** following this format: + - First line: 72 chars max, imperative mood summary (e.g., "Release v0.0.5") + - Blank line + - Bullet points describing each significant change + - "Files modified:" section listing affected files + - Footer with Claude Code attribution + +7. **Stage all changes** with `git add -A` + +8. **Create the commit** with the composed message + +9. **Create a git tag** with the new version prefixed with 'v' (e.g., `v0.0.5`) + +10. **Push to origin** with tags: + ``` + git push origin main --tags + ``` + +11. **Report completion** with the new version and commit hash + +## Important: +- This is a browser extension with separate Chrome and Firefox builds +- All three version fields in package.json must be updated together +- Always verify both Chrome and Firefox builds compile before committing diff --git a/.claude/skills/applesauce-core/SKILL.md b/.claude/skills/applesauce-core/SKILL.md new file mode 100644 index 0000000..7830711 --- /dev/null +++ b/.claude/skills/applesauce-core/SKILL.md @@ -0,0 +1,634 @@ +--- +name: applesauce-core +description: This skill should be used when working with applesauce-core library for Nostr client development, including event stores, queries, observables, and client utilities. Provides comprehensive knowledge of applesauce patterns for building reactive Nostr applications. +--- + +# applesauce-core Skill + +This skill provides comprehensive knowledge and patterns for working with applesauce-core, a library that provides reactive utilities and patterns for building Nostr clients. + +## When to Use This Skill + +Use this skill when: +- Building reactive Nostr applications +- Managing event stores and caches +- Working with observable patterns for Nostr +- Implementing real-time updates +- Building timeline and feed views +- Managing replaceable events +- Working with profiles and metadata +- Creating efficient Nostr queries + +## Core Concepts + +### applesauce-core Overview + +applesauce-core provides: +- **Event stores** - Reactive event caching and management +- **Queries** - Declarative event querying patterns +- **Observables** - RxJS-based reactive patterns +- **Profile helpers** - Profile metadata management +- **Timeline utilities** - Feed and timeline building +- **NIP helpers** - NIP-specific utilities + +### Installation + +```bash +npm install applesauce-core +``` + +### Basic Architecture + +applesauce-core is built on reactive principles: +- Events are stored in reactive stores +- Queries return observables that update when new events arrive +- Components subscribe to observables for real-time updates + +## Event Store + +### Creating an Event Store + +```javascript +import { EventStore } from 'applesauce-core'; + +// Create event store +const eventStore = new EventStore(); + +// Add events +eventStore.add(event1); +eventStore.add(event2); + +// Add multiple events +eventStore.addMany([event1, event2, event3]); + +// Check if event exists +const exists = eventStore.has(eventId); + +// Get event by ID +const event = eventStore.get(eventId); + +// Remove event +eventStore.remove(eventId); + +// Clear all events +eventStore.clear(); +``` + +### Event Store Queries + +```javascript +// Get all events +const allEvents = eventStore.getAll(); + +// Get events by filter +const filtered = eventStore.filter({ + kinds: [1], + authors: [pubkey] +}); + +// Get events by author +const authorEvents = eventStore.getByAuthor(pubkey); + +// Get events by kind +const textNotes = eventStore.getByKind(1); +``` + +### Replaceable Events + +applesauce-core handles replaceable events automatically: + +```javascript +// For kind 0 (profile), only latest is kept +eventStore.add(profileEvent1); // stored +eventStore.add(profileEvent2); // replaces if newer + +// For parameterized replaceable (30000-39999) +eventStore.add(articleEvent); // keyed by author + kind + d-tag + +// Get replaceable event +const profile = eventStore.getReplaceable(0, pubkey); +const article = eventStore.getReplaceable(30023, pubkey, 'article-slug'); +``` + +## Queries + +### Query Patterns + +```javascript +import { createQuery } from 'applesauce-core'; + +// Create a query +const query = createQuery(eventStore, { + kinds: [1], + limit: 50 +}); + +// Subscribe to query results +query.subscribe(events => { + console.log('Current events:', events); +}); + +// Query updates automatically when new events added +eventStore.add(newEvent); // Subscribers notified +``` + +### Timeline Query + +```javascript +import { TimelineQuery } from 'applesauce-core'; + +// Create timeline for user's notes +const timeline = new TimelineQuery(eventStore, { + kinds: [1], + authors: [userPubkey] +}); + +// Get observable of timeline +const timeline$ = timeline.events$; + +// Subscribe +timeline$.subscribe(events => { + // Events sorted by created_at, newest first + renderTimeline(events); +}); +``` + +### Profile Query + +```javascript +import { ProfileQuery } from 'applesauce-core'; + +// Query profile metadata +const profileQuery = new ProfileQuery(eventStore, pubkey); + +// Get observable +const profile$ = profileQuery.profile$; + +profile$.subscribe(profile => { + if (profile) { + console.log('Name:', profile.name); + console.log('Picture:', profile.picture); + } +}); +``` + +## Observables + +### Working with RxJS + +applesauce-core uses RxJS observables: + +```javascript +import { map, filter, distinctUntilChanged } from 'rxjs/operators'; + +// Transform query results +const names$ = profileQuery.profile$.pipe( + filter(profile => profile !== null), + map(profile => profile.name), + distinctUntilChanged() +); + +// Combine multiple observables +import { combineLatest } from 'rxjs'; + +const combined$ = combineLatest([ + timeline$, + profile$ +]).pipe( + map(([events, profile]) => ({ + events, + authorName: profile?.name + })) +); +``` + +### Creating Custom Observables + +```javascript +import { Observable } from 'rxjs'; + +function createEventObservable(store, filter) { + return new Observable(subscriber => { + // Initial emit + subscriber.next(store.filter(filter)); + + // Subscribe to store changes + const unsubscribe = store.onChange(() => { + subscriber.next(store.filter(filter)); + }); + + // Cleanup + return () => unsubscribe(); + }); +} +``` + +## Profile Helpers + +### Profile Metadata + +```javascript +import { parseProfile, ProfileContent } from 'applesauce-core'; + +// Parse kind 0 content +const profileEvent = await getProfileEvent(pubkey); +const profile = parseProfile(profileEvent); + +// Profile fields +console.log(profile.name); // Display name +console.log(profile.about); // Bio +console.log(profile.picture); // Avatar URL +console.log(profile.banner); // Banner image URL +console.log(profile.nip05); // NIP-05 identifier +console.log(profile.lud16); // Lightning address +console.log(profile.website); // Website URL +``` + +### Profile Store + +```javascript +import { ProfileStore } from 'applesauce-core'; + +const profileStore = new ProfileStore(eventStore); + +// Get profile observable +const profile$ = profileStore.getProfile(pubkey); + +// Get multiple profiles +const profiles$ = profileStore.getProfiles([pubkey1, pubkey2]); + +// Request profile load (triggers fetch if not cached) +profileStore.requestProfile(pubkey); +``` + +## Timeline Utilities + +### Building Feeds + +```javascript +import { Timeline } from 'applesauce-core'; + +// Create timeline +const timeline = new Timeline(eventStore); + +// Add filter +timeline.setFilter({ + kinds: [1, 6], + authors: followedPubkeys +}); + +// Get events observable +const events$ = timeline.events$; + +// Load more (pagination) +timeline.loadMore(50); + +// Refresh (get latest) +timeline.refresh(); +``` + +### Thread Building + +```javascript +import { ThreadBuilder } from 'applesauce-core'; + +// Build thread from root event +const thread = new ThreadBuilder(eventStore, rootEventId); + +// Get thread observable +const thread$ = thread.thread$; + +thread$.subscribe(threadData => { + console.log('Root:', threadData.root); + console.log('Replies:', threadData.replies); + console.log('Reply count:', threadData.replyCount); +}); +``` + +### Reactions and Zaps + +```javascript +import { ReactionStore, ZapStore } from 'applesauce-core'; + +// Reactions +const reactionStore = new ReactionStore(eventStore); +const reactions$ = reactionStore.getReactions(eventId); + +reactions$.subscribe(reactions => { + console.log('Likes:', reactions.likes); + console.log('Custom:', reactions.custom); +}); + +// Zaps +const zapStore = new ZapStore(eventStore); +const zaps$ = zapStore.getZaps(eventId); + +zaps$.subscribe(zaps => { + console.log('Total sats:', zaps.totalAmount); + console.log('Zap count:', zaps.count); +}); +``` + +## NIP Helpers + +### NIP-05 Verification + +```javascript +import { verifyNip05 } from 'applesauce-core'; + +// Verify NIP-05 +const result = await verifyNip05('alice@example.com', expectedPubkey); + +if (result.valid) { + console.log('NIP-05 verified'); +} else { + console.log('Verification failed:', result.error); +} +``` + +### NIP-10 Reply Parsing + +```javascript +import { parseReplyTags } from 'applesauce-core'; + +// Parse reply structure +const parsed = parseReplyTags(event); + +console.log('Root event:', parsed.root); +console.log('Reply to:', parsed.reply); +console.log('Mentions:', parsed.mentions); +``` + +### NIP-65 Relay Lists + +```javascript +import { parseRelayList } from 'applesauce-core'; + +// Parse relay list event (kind 10002) +const relays = parseRelayList(relayListEvent); + +console.log('Read relays:', relays.read); +console.log('Write relays:', relays.write); +``` + +## Integration with nostr-tools + +### Using with SimplePool + +```javascript +import { SimplePool } from 'nostr-tools'; +import { EventStore } from 'applesauce-core'; + +const pool = new SimplePool(); +const eventStore = new EventStore(); + +// Load events into store +pool.subscribeMany(relays, [filter], { + onevent(event) { + eventStore.add(event); + } +}); + +// Query store reactively +const timeline$ = createTimelineQuery(eventStore, filter); +``` + +### Publishing Events + +```javascript +import { finalizeEvent } from 'nostr-tools'; + +// Create event +const event = finalizeEvent({ + kind: 1, + content: 'Hello!', + created_at: Math.floor(Date.now() / 1000), + tags: [] +}, secretKey); + +// Add to local store immediately (optimistic update) +eventStore.add(event); + +// Publish to relays +await pool.publish(relays, event); +``` + +## Svelte Integration + +### Using in Svelte Components + +```svelte + + +{#each events as event} +
+ {event.content} +
+{/each} +``` + +### Svelte Store Adapter + +```javascript +import { readable } from 'svelte/store'; + +// Convert RxJS observable to Svelte store +function fromObservable(observable, initialValue) { + return readable(initialValue, set => { + const subscription = observable.subscribe(set); + return () => subscription.unsubscribe(); + }); +} + +// Usage +const events$ = timeline.events$; +const eventsStore = fromObservable(events$, []); +``` + +```svelte + + +{#each $eventsStore as event} +
{event.content}
+{/each} +``` + +## Best Practices + +### Store Management + +1. **Single store instance** - Use one EventStore per app +2. **Clear stale data** - Implement cache limits +3. **Handle replaceable events** - Let store manage deduplication +4. **Unsubscribe** - Clean up subscriptions on component destroy + +### Query Optimization + +1. **Use specific filters** - Narrow queries perform better +2. **Limit results** - Use limit for initial loads +3. **Cache queries** - Reuse query instances +4. **Debounce updates** - Throttle rapid changes + +### Memory Management + +1. **Limit store size** - Implement LRU or time-based eviction +2. **Clean up observables** - Unsubscribe when done +3. **Use weak references** - For profile caches +4. **Paginate large feeds** - Don't load everything at once + +### Reactive Patterns + +1. **Prefer observables** - Over imperative queries +2. **Use operators** - Transform data with RxJS +3. **Combine streams** - For complex views +4. **Handle loading states** - Show placeholders + +## Common Patterns + +### Event Deduplication + +```javascript +// EventStore handles deduplication automatically +eventStore.add(event1); +eventStore.add(event1); // No duplicate + +// For manual deduplication +const seen = new Set(); +events.filter(e => { + if (seen.has(e.id)) return false; + seen.add(e.id); + return true; +}); +``` + +### Optimistic Updates + +```javascript +async function publishNote(content) { + // Create event + const event = await createEvent(content); + + // Add to store immediately (optimistic) + eventStore.add(event); + + try { + // Publish to relays + await pool.publish(relays, event); + } catch (error) { + // Remove on failure + eventStore.remove(event.id); + throw error; + } +} +``` + +### Loading States + +```javascript +import { BehaviorSubject, combineLatest } from 'rxjs'; + +const loading$ = new BehaviorSubject(true); +const events$ = timeline.events$; + +const state$ = combineLatest([loading$, events$]).pipe( + map(([loading, events]) => ({ + loading, + events, + empty: !loading && events.length === 0 + })) +); + +// Start loading +loading$.next(true); +await loadEvents(); +loading$.next(false); +``` + +### Infinite Scroll + +```javascript +function createInfiniteScroll(timeline, pageSize = 50) { + let loading = false; + + async function loadMore() { + if (loading) return; + + loading = true; + await timeline.loadMore(pageSize); + loading = false; + } + + function onScroll(event) { + const { scrollTop, scrollHeight, clientHeight } = event.target; + if (scrollHeight - scrollTop <= clientHeight * 1.5) { + loadMore(); + } + } + + return { loadMore, onScroll }; +} +``` + +## Troubleshooting + +### Common Issues + +**Events not updating:** +- Check subscription is active +- Verify events are being added to store +- Ensure filter matches events + +**Memory growing:** +- Implement store size limits +- Clean up subscriptions +- Use weak references where appropriate + +**Slow queries:** +- Add indexes for common queries +- Use more specific filters +- Implement pagination + +**Stale data:** +- Implement refresh mechanisms +- Set up real-time subscriptions +- Handle replaceable event updates + +## References + +- **applesauce GitHub**: https://github.com/hzrd149/applesauce +- **RxJS Documentation**: https://rxjs.dev +- **nostr-tools**: https://github.com/nbd-wtf/nostr-tools +- **Nostr Protocol**: https://github.com/nostr-protocol/nostr + +## Related Skills + +- **nostr-tools** - Lower-level Nostr operations +- **applesauce-signers** - Event signing abstractions +- **svelte** - Building reactive UIs +- **nostr** - Nostr protocol fundamentals diff --git a/.claude/skills/applesauce-signers/SKILL.md b/.claude/skills/applesauce-signers/SKILL.md new file mode 100644 index 0000000..40d07e6 --- /dev/null +++ b/.claude/skills/applesauce-signers/SKILL.md @@ -0,0 +1,757 @@ +--- +name: applesauce-signers +description: This skill should be used when working with applesauce-signers library for Nostr event signing, including NIP-07 browser extensions, NIP-46 remote signing, and custom signer implementations. Provides comprehensive knowledge of signing patterns and signer abstractions. +--- + +# applesauce-signers Skill + +This skill provides comprehensive knowledge and patterns for working with applesauce-signers, a library that provides signing abstractions for Nostr applications. + +## When to Use This Skill + +Use this skill when: +- Implementing event signing in Nostr applications +- Integrating with NIP-07 browser extensions +- Working with NIP-46 remote signers +- Building custom signer implementations +- Managing signing sessions +- Handling signing requests and permissions +- Implementing multi-signer support + +## Core Concepts + +### applesauce-signers Overview + +applesauce-signers provides: +- **Signer abstraction** - Unified interface for different signers +- **NIP-07 integration** - Browser extension support +- **NIP-46 support** - Remote signing (Nostr Connect) +- **Simple signers** - Direct key signing +- **Permission handling** - Manage signing requests +- **Observable patterns** - Reactive signing states + +### Installation + +```bash +npm install applesauce-signers +``` + +### Signer Interface + +All signers implement a common interface: + +```typescript +interface Signer { + // Get public key + getPublicKey(): Promise; + + // Sign event + signEvent(event: UnsignedEvent): Promise; + + // Encrypt (NIP-04) + nip04Encrypt?(pubkey: string, plaintext: string): Promise; + nip04Decrypt?(pubkey: string, ciphertext: string): Promise; + + // Encrypt (NIP-44) + nip44Encrypt?(pubkey: string, plaintext: string): Promise; + nip44Decrypt?(pubkey: string, ciphertext: string): Promise; +} +``` + +## Simple Signer + +### Using Secret Key + +```javascript +import { SimpleSigner } from 'applesauce-signers'; +import { generateSecretKey } from 'nostr-tools'; + +// Create signer with existing key +const signer = new SimpleSigner(secretKey); + +// Or generate new key +const newSecretKey = generateSecretKey(); +const newSigner = new SimpleSigner(newSecretKey); + +// Get public key +const pubkey = await signer.getPublicKey(); + +// Sign event +const unsignedEvent = { + kind: 1, + content: 'Hello Nostr!', + created_at: Math.floor(Date.now() / 1000), + tags: [] +}; + +const signedEvent = await signer.signEvent(unsignedEvent); +``` + +### NIP-04 Encryption + +```javascript +// Encrypt message +const ciphertext = await signer.nip04Encrypt( + recipientPubkey, + 'Secret message' +); + +// Decrypt message +const plaintext = await signer.nip04Decrypt( + senderPubkey, + ciphertext +); +``` + +### NIP-44 Encryption + +```javascript +// Encrypt with NIP-44 (preferred) +const ciphertext = await signer.nip44Encrypt( + recipientPubkey, + 'Secret message' +); + +// Decrypt +const plaintext = await signer.nip44Decrypt( + senderPubkey, + ciphertext +); +``` + +## NIP-07 Signer + +### Browser Extension Integration + +```javascript +import { Nip07Signer } from 'applesauce-signers'; + +// Check if extension is available +if (window.nostr) { + const signer = new Nip07Signer(); + + // Get public key (may prompt user) + const pubkey = await signer.getPublicKey(); + + // Sign event (prompts user) + const signedEvent = await signer.signEvent(unsignedEvent); +} +``` + +### Handling Extension Availability + +```javascript +function getAvailableSigner() { + if (typeof window !== 'undefined' && window.nostr) { + return new Nip07Signer(); + } + return null; +} + +// Wait for extension to load +async function waitForExtension(timeout = 3000) { + const start = Date.now(); + + while (Date.now() - start < timeout) { + if (window.nostr) { + return new Nip07Signer(); + } + await new Promise(r => setTimeout(r, 100)); + } + + return null; +} +``` + +### Extension Permissions + +```javascript +// Some extensions support granular permissions +const signer = new Nip07Signer(); + +// Request specific permissions +try { + // This varies by extension + await window.nostr.enable(); +} catch (error) { + console.log('User denied permission'); +} +``` + +## NIP-46 Remote Signer + +### Nostr Connect + +```javascript +import { Nip46Signer } from 'applesauce-signers'; + +// Create remote signer +const signer = new Nip46Signer({ + // Remote signer's pubkey + remotePubkey: signerPubkey, + + // Relays for communication + relays: ['wss://relay.example.com'], + + // Local secret key for encryption + localSecretKey: localSecretKey, + + // Optional: custom client name + clientName: 'My Nostr App' +}); + +// Connect to remote signer +await signer.connect(); + +// Get public key +const pubkey = await signer.getPublicKey(); + +// Sign event +const signedEvent = await signer.signEvent(unsignedEvent); + +// Disconnect when done +signer.disconnect(); +``` + +### Connection URL + +```javascript +// Parse nostrconnect:// URL +function parseNostrConnectUrl(url) { + const parsed = new URL(url); + + return { + pubkey: parsed.pathname.replace('//', ''), + relay: parsed.searchParams.get('relay'), + secret: parsed.searchParams.get('secret') + }; +} + +// Create signer from URL +const { pubkey, relay, secret } = parseNostrConnectUrl(connectUrl); + +const signer = new Nip46Signer({ + remotePubkey: pubkey, + relays: [relay], + localSecretKey: generateSecretKey(), + secret: secret +}); +``` + +### Bunker URL + +```javascript +// Parse bunker:// URL (NIP-46) +function parseBunkerUrl(url) { + const parsed = new URL(url); + + return { + pubkey: parsed.pathname.replace('//', ''), + relays: parsed.searchParams.getAll('relay'), + secret: parsed.searchParams.get('secret') + }; +} + +const { pubkey, relays, secret } = parseBunkerUrl(bunkerUrl); +``` + +## Signer Management + +### Signer Store + +```javascript +import { SignerStore } from 'applesauce-signers'; + +const signerStore = new SignerStore(); + +// Set active signer +signerStore.setSigner(signer); + +// Get active signer +const activeSigner = signerStore.getSigner(); + +// Clear signer (logout) +signerStore.clearSigner(); + +// Observable for signer changes +signerStore.signer$.subscribe(signer => { + if (signer) { + console.log('Logged in'); + } else { + console.log('Logged out'); + } +}); +``` + +### Multi-Account Support + +```javascript +class AccountManager { + constructor() { + this.accounts = new Map(); + this.activeAccount = null; + } + + addAccount(pubkey, signer) { + this.accounts.set(pubkey, signer); + } + + removeAccount(pubkey) { + this.accounts.delete(pubkey); + if (this.activeAccount === pubkey) { + this.activeAccount = null; + } + } + + switchAccount(pubkey) { + if (this.accounts.has(pubkey)) { + this.activeAccount = pubkey; + return this.accounts.get(pubkey); + } + return null; + } + + getActiveSigner() { + return this.activeAccount + ? this.accounts.get(this.activeAccount) + : null; + } +} +``` + +## Custom Signers + +### Implementing a Custom Signer + +```javascript +class CustomSigner { + constructor(options) { + this.options = options; + } + + async getPublicKey() { + // Return public key + return this.options.pubkey; + } + + async signEvent(event) { + // Implement signing logic + // Could call external API, hardware wallet, etc. + + const signedEvent = await this.externalSign(event); + return signedEvent; + } + + async nip04Encrypt(pubkey, plaintext) { + // Implement NIP-04 encryption + throw new Error('NIP-04 not supported'); + } + + async nip04Decrypt(pubkey, ciphertext) { + throw new Error('NIP-04 not supported'); + } + + async nip44Encrypt(pubkey, plaintext) { + // Implement NIP-44 encryption + throw new Error('NIP-44 not supported'); + } + + async nip44Decrypt(pubkey, ciphertext) { + throw new Error('NIP-44 not supported'); + } +} +``` + +### Hardware Wallet Signer + +```javascript +class HardwareWalletSigner { + constructor(devicePath) { + this.devicePath = devicePath; + } + + async connect() { + // Connect to hardware device + this.device = await connectToDevice(this.devicePath); + } + + async getPublicKey() { + // Get public key from device + return await this.device.getNostrPubkey(); + } + + async signEvent(event) { + // Sign on device (user confirms on device) + const signature = await this.device.signNostrEvent(event); + + return { + ...event, + pubkey: await this.getPublicKey(), + id: getEventHash(event), + sig: signature + }; + } +} +``` + +### Read-Only Signer + +```javascript +class ReadOnlySigner { + constructor(pubkey) { + this.pubkey = pubkey; + } + + async getPublicKey() { + return this.pubkey; + } + + async signEvent(event) { + throw new Error('Read-only mode: cannot sign events'); + } + + async nip04Encrypt(pubkey, plaintext) { + throw new Error('Read-only mode: cannot encrypt'); + } + + async nip04Decrypt(pubkey, ciphertext) { + throw new Error('Read-only mode: cannot decrypt'); + } +} +``` + +## Signing Utilities + +### Event Creation Helper + +```javascript +async function createAndSignEvent(signer, template) { + const pubkey = await signer.getPublicKey(); + + const event = { + ...template, + pubkey, + created_at: template.created_at || Math.floor(Date.now() / 1000) + }; + + return await signer.signEvent(event); +} + +// Usage +const signedNote = await createAndSignEvent(signer, { + kind: 1, + content: 'Hello!', + tags: [] +}); +``` + +### Batch Signing + +```javascript +async function signEvents(signer, events) { + const signed = []; + + for (const event of events) { + const signedEvent = await signer.signEvent(event); + signed.push(signedEvent); + } + + return signed; +} + +// With parallelization (if signer supports) +async function signEventsParallel(signer, events) { + return Promise.all( + events.map(event => signer.signEvent(event)) + ); +} +``` + +## Svelte Integration + +### Signer Context + +```svelte + + + + +``` + +```svelte + + +``` + +### Login Component + +```svelte + + +{#if $signer} + +{:else} + + +
+ + +
+{/if} +``` + +## Best Practices + +### Security + +1. **Never store secret keys in plain text** - Use secure storage +2. **Prefer NIP-07** - Let extensions manage keys +3. **Clear keys on logout** - Don't leave in memory +4. **Validate before signing** - Check event content + +### User Experience + +1. **Show signing status** - Loading states +2. **Handle rejections gracefully** - User may cancel +3. **Provide fallbacks** - Multiple login options +4. **Remember preferences** - Store signer type + +### Error Handling + +```javascript +async function safeSign(signer, event) { + try { + return await signer.signEvent(event); + } catch (error) { + if (error.message.includes('rejected')) { + console.log('User rejected signing'); + return null; + } + if (error.message.includes('timeout')) { + console.log('Signing timed out'); + return null; + } + throw error; + } +} +``` + +### Permission Checking + +```javascript +function hasEncryptionSupport(signer) { + return typeof signer.nip04Encrypt === 'function' || + typeof signer.nip44Encrypt === 'function'; +} + +function getEncryptionMethod(signer) { + // Prefer NIP-44 + if (typeof signer.nip44Encrypt === 'function') { + return 'nip44'; + } + if (typeof signer.nip04Encrypt === 'function') { + return 'nip04'; + } + return null; +} +``` + +## Common Patterns + +### Signer Detection + +```javascript +async function detectSigners() { + const available = []; + + // Check NIP-07 + if (typeof window !== 'undefined' && window.nostr) { + available.push({ + type: 'nip07', + name: 'Browser Extension', + create: () => new Nip07Signer() + }); + } + + // Check stored credentials + const storedKey = localStorage.getItem('nsec'); + if (storedKey) { + available.push({ + type: 'stored', + name: 'Saved Key', + create: () => new SimpleSigner(storedKey) + }); + } + + return available; +} +``` + +### Auto-Reconnect for NIP-46 + +```javascript +class ReconnectingNip46Signer { + constructor(options) { + this.options = options; + this.signer = null; + } + + async connect() { + this.signer = new Nip46Signer(this.options); + await this.signer.connect(); + } + + async signEvent(event) { + try { + return await this.signer.signEvent(event); + } catch (error) { + if (error.message.includes('disconnected')) { + await this.connect(); + return await this.signer.signEvent(event); + } + throw error; + } + } +} +``` + +### Signer Type Persistence + +```javascript +const SIGNER_KEY = 'nostr_signer_type'; + +function saveSigner(type, data) { + localStorage.setItem(SIGNER_KEY, JSON.stringify({ type, data })); +} + +async function restoreSigner() { + const saved = localStorage.getItem(SIGNER_KEY); + if (!saved) return null; + + const { type, data } = JSON.parse(saved); + + switch (type) { + case 'nip07': + if (window.nostr) { + return new Nip07Signer(); + } + break; + case 'simple': + // Don't store secret keys! + break; + case 'nip46': + const signer = new Nip46Signer(data); + await signer.connect(); + return signer; + } + + return null; +} +``` + +## Troubleshooting + +### Common Issues + +**Extension not detected:** +- Wait for page load +- Check window.nostr exists +- Verify extension is enabled + +**Signing rejected:** +- User cancelled in extension +- Handle gracefully with error message + +**NIP-46 connection fails:** +- Check relay is accessible +- Verify remote signer is online +- Check secret matches + +**Encryption not supported:** +- Check signer has encrypt methods +- Fall back to alternative method +- Show user appropriate error + +## References + +- **applesauce GitHub**: https://github.com/hzrd149/applesauce +- **NIP-07 Specification**: https://github.com/nostr-protocol/nips/blob/master/07.md +- **NIP-46 Specification**: https://github.com/nostr-protocol/nips/blob/master/46.md +- **nostr-tools**: https://github.com/nbd-wtf/nostr-tools + +## Related Skills + +- **nostr-tools** - Event creation and signing utilities +- **applesauce-core** - Event stores and queries +- **nostr** - Nostr protocol fundamentals +- **svelte** - Building Nostr UIs diff --git a/.claude/skills/nostr-tools/SKILL.md b/.claude/skills/nostr-tools/SKILL.md new file mode 100644 index 0000000..905d41c --- /dev/null +++ b/.claude/skills/nostr-tools/SKILL.md @@ -0,0 +1,767 @@ +--- +name: nostr-tools +description: This skill should be used when working with nostr-tools library for Nostr protocol operations, including event creation, signing, filtering, relay communication, and NIP implementations. Provides comprehensive knowledge of nostr-tools APIs and patterns. +--- + +# nostr-tools Skill + +This skill provides comprehensive knowledge and patterns for working with nostr-tools, the most popular JavaScript/TypeScript library for Nostr protocol development. + +## When to Use This Skill + +Use this skill when: +- Building Nostr clients or applications +- Creating and signing Nostr events +- Connecting to Nostr relays +- Implementing NIP features +- Working with Nostr keys and cryptography +- Filtering and querying events +- Building relay pools or connections +- Implementing NIP-44/NIP-04 encryption + +## Core Concepts + +### nostr-tools Overview + +nostr-tools provides: +- **Event handling** - Create, sign, verify events +- **Key management** - Generate, convert, encode keys +- **Relay communication** - Connect, subscribe, publish +- **NIP implementations** - NIP-04, NIP-05, NIP-19, NIP-44, etc. +- **Cryptographic operations** - Schnorr signatures, encryption +- **Filter building** - Query events by various criteria + +### Installation + +```bash +npm install nostr-tools +``` + +### Basic Imports + +```javascript +// Core functionality +import { + SimplePool, + generateSecretKey, + getPublicKey, + finalizeEvent, + verifyEvent +} from 'nostr-tools'; + +// NIP-specific imports +import { nip04, nip05, nip19, nip44 } from 'nostr-tools'; + +// Relay operations +import { Relay } from 'nostr-tools/relay'; +``` + +## Key Management + +### Generating Keys + +```javascript +import { generateSecretKey, getPublicKey } from 'nostr-tools/pure'; + +// Generate new secret key (Uint8Array) +const secretKey = generateSecretKey(); + +// Derive public key +const publicKey = getPublicKey(secretKey); + +console.log('Secret key:', bytesToHex(secretKey)); +console.log('Public key:', publicKey); // hex string +``` + +### Key Encoding (NIP-19) + +```javascript +import { nip19 } from 'nostr-tools'; + +// Encode to bech32 +const nsec = nip19.nsecEncode(secretKey); +const npub = nip19.npubEncode(publicKey); +const note = nip19.noteEncode(eventId); + +console.log(nsec); // nsec1... +console.log(npub); // npub1... +console.log(note); // note1... + +// Decode from bech32 +const { type, data } = nip19.decode(npub); +// type: 'npub', data: publicKey (hex) + +// Encode profile reference (nprofile) +const nprofile = nip19.nprofileEncode({ + pubkey: publicKey, + relays: ['wss://relay.example.com'] +}); + +// Encode event reference (nevent) +const nevent = nip19.neventEncode({ + id: eventId, + relays: ['wss://relay.example.com'], + author: publicKey, + kind: 1 +}); + +// Encode address (naddr) for replaceable events +const naddr = nip19.naddrEncode({ + identifier: 'my-article', + pubkey: publicKey, + kind: 30023, + relays: ['wss://relay.example.com'] +}); +``` + +## Event Operations + +### Event Structure + +```javascript +// Unsigned event template +const eventTemplate = { + kind: 1, + created_at: Math.floor(Date.now() / 1000), + tags: [], + content: 'Hello Nostr!' +}; + +// Signed event (after finalizeEvent) +const signedEvent = { + id: '...', // 32-byte sha256 hash as hex + pubkey: '...', // 32-byte public key as hex + created_at: 1234567890, + kind: 1, + tags: [], + content: 'Hello Nostr!', + sig: '...' // 64-byte Schnorr signature as hex +}; +``` + +### Creating and Signing Events + +```javascript +import { finalizeEvent, verifyEvent } from 'nostr-tools/pure'; + +// Create event template +const eventTemplate = { + kind: 1, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ['p', publicKey], // Mention + ['e', eventId, '', 'reply'], // Reply + ['t', 'nostr'] // Hashtag + ], + content: 'Hello Nostr!' +}; + +// Sign event +const signedEvent = finalizeEvent(eventTemplate, secretKey); + +// Verify event +const isValid = verifyEvent(signedEvent); +console.log('Event valid:', isValid); +``` + +### Event Kinds + +```javascript +// Common event kinds +const KINDS = { + Metadata: 0, // Profile metadata (NIP-01) + Text: 1, // Short text note (NIP-01) + RecommendRelay: 2, // Relay recommendation + Contacts: 3, // Contact list (NIP-02) + EncryptedDM: 4, // Encrypted DM (NIP-04) + EventDeletion: 5, // Delete events (NIP-09) + Repost: 6, // Repost (NIP-18) + Reaction: 7, // Reaction (NIP-25) + ChannelCreation: 40, // Channel (NIP-28) + ChannelMessage: 42, // Channel message + Zap: 9735, // Zap receipt (NIP-57) + Report: 1984, // Report (NIP-56) + RelayList: 10002, // Relay list (NIP-65) + Article: 30023, // Long-form content (NIP-23) +}; +``` + +### Creating Specific Events + +```javascript +// Profile metadata (kind 0) +const profileEvent = finalizeEvent({ + kind: 0, + created_at: Math.floor(Date.now() / 1000), + tags: [], + content: JSON.stringify({ + name: 'Alice', + about: 'Nostr enthusiast', + picture: 'https://example.com/avatar.jpg', + nip05: 'alice@example.com', + lud16: 'alice@getalby.com' + }) +}, secretKey); + +// Contact list (kind 3) +const contactsEvent = finalizeEvent({ + kind: 3, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ['p', pubkey1, 'wss://relay1.com', 'alice'], + ['p', pubkey2, 'wss://relay2.com', 'bob'], + ['p', pubkey3, '', 'carol'] + ], + content: '' // Or JSON relay preferences +}, secretKey); + +// Reply to an event +const replyEvent = finalizeEvent({ + kind: 1, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ['e', rootEventId, '', 'root'], + ['e', parentEventId, '', 'reply'], + ['p', parentEventPubkey] + ], + content: 'This is a reply' +}, secretKey); + +// Reaction (kind 7) +const reactionEvent = finalizeEvent({ + kind: 7, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ['e', eventId], + ['p', eventPubkey] + ], + content: '+' // or '-' or emoji +}, secretKey); + +// Delete event (kind 5) +const deleteEvent = finalizeEvent({ + kind: 5, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ['e', eventIdToDelete], + ['e', anotherEventIdToDelete] + ], + content: 'Deletion reason' +}, secretKey); +``` + +## Relay Communication + +### Using SimplePool + +SimplePool is the recommended way to interact with multiple relays: + +```javascript +import { SimplePool } from 'nostr-tools/pool'; + +const pool = new SimplePool(); +const relays = [ + 'wss://relay.damus.io', + 'wss://nos.lol', + 'wss://relay.nostr.band' +]; + +// Subscribe to events +const subscription = pool.subscribeMany( + relays, + [ + { + kinds: [1], + authors: [publicKey], + limit: 10 + } + ], + { + onevent(event) { + console.log('Received event:', event); + }, + oneose() { + console.log('End of stored events'); + } + } +); + +// Close subscription when done +subscription.close(); + +// Publish event to all relays +const results = await Promise.allSettled( + pool.publish(relays, signedEvent) +); + +// Query events (returns Promise) +const events = await pool.querySync(relays, { + kinds: [0], + authors: [publicKey] +}); + +// Get single event +const event = await pool.get(relays, { + ids: [eventId] +}); + +// Close pool when done +pool.close(relays); +``` + +### Direct Relay Connection + +```javascript +import { Relay } from 'nostr-tools/relay'; + +const relay = await Relay.connect('wss://relay.damus.io'); + +console.log(`Connected to ${relay.url}`); + +// Subscribe +const sub = relay.subscribe([ + { + kinds: [1], + limit: 100 + } +], { + onevent(event) { + console.log('Event:', event); + }, + oneose() { + console.log('EOSE'); + sub.close(); + } +}); + +// Publish +await relay.publish(signedEvent); + +// Close +relay.close(); +``` + +### Handling Connection States + +```javascript +import { Relay } from 'nostr-tools/relay'; + +const relay = await Relay.connect('wss://relay.example.com'); + +// Listen for disconnect +relay.onclose = () => { + console.log('Relay disconnected'); +}; + +// Check connection status +console.log('Connected:', relay.connected); +``` + +## Filters + +### Filter Structure + +```javascript +const filter = { + // Event IDs + ids: ['abc123...'], + + // Authors (pubkeys) + authors: ['pubkey1', 'pubkey2'], + + // Event kinds + kinds: [1, 6, 7], + + // Tags (single-letter keys) + '#e': ['eventId1', 'eventId2'], + '#p': ['pubkey1'], + '#t': ['nostr', 'bitcoin'], + '#d': ['article-identifier'], + + // Time range + since: 1704067200, // Unix timestamp + until: 1704153600, + + // Limit results + limit: 100, + + // Search (NIP-50, if relay supports) + search: 'nostr protocol' +}; +``` + +### Common Filter Patterns + +```javascript +// User's recent posts +const userPosts = { + kinds: [1], + authors: [userPubkey], + limit: 50 +}; + +// User's profile +const userProfile = { + kinds: [0], + authors: [userPubkey] +}; + +// User's contacts +const userContacts = { + kinds: [3], + authors: [userPubkey] +}; + +// Replies to an event +const replies = { + kinds: [1], + '#e': [eventId] +}; + +// Reactions to an event +const reactions = { + kinds: [7], + '#e': [eventId] +}; + +// Feed from followed users +const feed = { + kinds: [1, 6], + authors: followedPubkeys, + limit: 100 +}; + +// Events mentioning user +const mentions = { + kinds: [1], + '#p': [userPubkey], + limit: 50 +}; + +// Hashtag search +const hashtagEvents = { + kinds: [1], + '#t': ['bitcoin'], + limit: 100 +}; + +// Replaceable event by d-tag +const replaceableEvent = { + kinds: [30023], + authors: [authorPubkey], + '#d': ['article-slug'] +}; +``` + +### Multiple Filters + +```javascript +// Subscribe with multiple filters (OR logic) +const filters = [ + { kinds: [1], authors: [userPubkey], limit: 20 }, + { kinds: [1], '#p': [userPubkey], limit: 20 } +]; + +pool.subscribeMany(relays, filters, { + onevent(event) { + // Receives events matching ANY filter + } +}); +``` + +## Encryption + +### NIP-04 (Legacy DMs) + +```javascript +import { nip04 } from 'nostr-tools'; + +// Encrypt message +const ciphertext = await nip04.encrypt( + secretKey, + recipientPubkey, + 'Hello, this is secret!' +); + +// Create encrypted DM event +const dmEvent = finalizeEvent({ + kind: 4, + created_at: Math.floor(Date.now() / 1000), + tags: [['p', recipientPubkey]], + content: ciphertext +}, secretKey); + +// Decrypt message +const plaintext = await nip04.decrypt( + secretKey, + senderPubkey, + ciphertext +); +``` + +### NIP-44 (Modern Encryption) + +```javascript +import { nip44 } from 'nostr-tools'; + +// Get conversation key (cache this for multiple messages) +const conversationKey = nip44.getConversationKey( + secretKey, + recipientPubkey +); + +// Encrypt +const ciphertext = nip44.encrypt( + 'Hello with NIP-44!', + conversationKey +); + +// Decrypt +const plaintext = nip44.decrypt( + ciphertext, + conversationKey +); +``` + +## NIP Implementations + +### NIP-05 (DNS Identifier) + +```javascript +import { nip05 } from 'nostr-tools'; + +// Query NIP-05 identifier +const profile = await nip05.queryProfile('alice@example.com'); + +if (profile) { + console.log('Pubkey:', profile.pubkey); + console.log('Relays:', profile.relays); +} + +// Verify NIP-05 for a pubkey +const isValid = await nip05.queryProfile('alice@example.com') + .then(p => p?.pubkey === expectedPubkey); +``` + +### NIP-10 (Reply Threading) + +```javascript +import { nip10 } from 'nostr-tools'; + +// Parse reply tags +const parsed = nip10.parse(event); + +console.log('Root:', parsed.root); // Original event +console.log('Reply:', parsed.reply); // Direct parent +console.log('Mentions:', parsed.mentions); // Other mentions +console.log('Profiles:', parsed.profiles); // Mentioned pubkeys +``` + +### NIP-21 (nostr: URIs) + +```javascript +// Parse nostr: URIs +const uri = 'nostr:npub1...'; +const { type, data } = nip19.decode(uri.replace('nostr:', '')); +``` + +### NIP-27 (Content References) + +```javascript +// Parse nostr:npub and nostr:note references in content +const content = 'Check out nostr:npub1abc... and nostr:note1xyz...'; + +const references = content.match(/nostr:(n[a-z]+1[a-z0-9]+)/g); +references?.forEach(ref => { + const decoded = nip19.decode(ref.replace('nostr:', '')); + console.log(decoded.type, decoded.data); +}); +``` + +### NIP-57 (Zaps) + +```javascript +import { nip57 } from 'nostr-tools'; + +// Validate zap receipt +const zapReceipt = await pool.get(relays, { + kinds: [9735], + '#e': [eventId] +}); + +const validatedZap = await nip57.validateZapRequest(zapReceipt); +``` + +## Utilities + +### Hex and Bytes Conversion + +```javascript +import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; + +// Convert secret key to hex +const secretKeyHex = bytesToHex(secretKey); + +// Convert hex back to bytes +const secretKeyBytes = hexToBytes(secretKeyHex); +``` + +### Event ID Calculation + +```javascript +import { getEventHash } from 'nostr-tools/pure'; + +// Calculate event ID without signing +const eventId = getEventHash(unsignedEvent); +``` + +### Signature Operations + +```javascript +import { + getSignature, + verifyEvent +} from 'nostr-tools/pure'; + +// Sign event data +const signature = getSignature(unsignedEvent, secretKey); + +// Verify complete event +const isValid = verifyEvent(signedEvent); +``` + +## Best Practices + +### Connection Management + +1. **Use SimplePool** - Manages connections efficiently +2. **Limit concurrent connections** - Don't connect to too many relays +3. **Handle disconnections** - Implement reconnection logic +4. **Close subscriptions** - Always close when done + +### Event Handling + +1. **Verify events** - Always verify signatures +2. **Deduplicate** - Events may come from multiple relays +3. **Handle replaceable events** - Latest by created_at wins +4. **Validate content** - Don't trust event content blindly + +### Key Security + +1. **Never expose secret keys** - Keep in secure storage +2. **Use NIP-07 in browsers** - Let extensions handle signing +3. **Validate input** - Check key formats before use + +### Performance + +1. **Cache events** - Avoid re-fetching +2. **Use filters wisely** - Be specific, use limits +3. **Batch operations** - Combine related queries +4. **Close idle connections** - Free up resources + +## Common Patterns + +### Building a Feed + +```javascript +const pool = new SimplePool(); +const relays = ['wss://relay.damus.io', 'wss://nos.lol']; + +async function loadFeed(followedPubkeys) { + const events = await pool.querySync(relays, { + kinds: [1, 6], + authors: followedPubkeys, + limit: 100 + }); + + // Sort by timestamp + return events.sort((a, b) => b.created_at - a.created_at); +} +``` + +### Real-time Updates + +```javascript +function subscribeToFeed(followedPubkeys, onEvent) { + return pool.subscribeMany( + relays, + [{ kinds: [1, 6], authors: followedPubkeys }], + { + onevent: onEvent, + oneose() { + console.log('Caught up with stored events'); + } + } + ); +} +``` + +### Profile Loading + +```javascript +async function loadProfile(pubkey) { + const [metadata] = await pool.querySync(relays, { + kinds: [0], + authors: [pubkey], + limit: 1 + }); + + if (metadata) { + return JSON.parse(metadata.content); + } + return null; +} +``` + +### Event Deduplication + +```javascript +const seenEvents = new Set(); + +function handleEvent(event) { + if (seenEvents.has(event.id)) { + return; // Skip duplicate + } + seenEvents.add(event.id); + + // Process event... +} +``` + +## Troubleshooting + +### Common Issues + +**Events not publishing:** +- Check relay is writable +- Verify event is properly signed +- Check relay's accepted kinds + +**Subscription not receiving events:** +- Verify filter syntax +- Check relay has matching events +- Ensure subscription isn't closed + +**Signature verification fails:** +- Check event structure is correct +- Verify keys are in correct format +- Ensure event hasn't been modified + +**NIP-05 lookup fails:** +- Check CORS headers on server +- Verify .well-known path is correct +- Handle network timeouts + +## References + +- **nostr-tools GitHub**: https://github.com/nbd-wtf/nostr-tools +- **Nostr Protocol**: https://github.com/nostr-protocol/nostr +- **NIPs Repository**: https://github.com/nostr-protocol/nips +- **NIP-01 (Basic Protocol)**: https://github.com/nostr-protocol/nips/blob/master/01.md + +## Related Skills + +- **nostr** - Nostr protocol fundamentals +- **svelte** - Building Nostr UIs with Svelte +- **applesauce-core** - Higher-level Nostr client utilities +- **applesauce-signers** - Nostr signing abstractions diff --git a/.claude/skills/nostr/README.md b/.claude/skills/nostr/README.md new file mode 100644 index 0000000..6806b77 --- /dev/null +++ b/.claude/skills/nostr/README.md @@ -0,0 +1,162 @@ +# Nostr Protocol Skill + +A comprehensive Claude skill for working with the Nostr protocol and implementing Nostr clients and relays. + +## Overview + +This skill provides expert-level knowledge of the Nostr protocol, including: +- Complete NIP (Nostr Implementation Possibilities) reference +- Event structure and cryptographic operations +- Client-relay WebSocket communication +- Event kinds and their behaviors +- Best practices and common pitfalls + +## Contents + +### SKILL.md +The main skill file containing: +- Core protocol concepts +- Event structure and signing +- WebSocket communication patterns +- Cryptographic operations +- Common implementation patterns +- Quick reference guides + +### Reference Files + +#### references/nips-overview.md +Comprehensive documentation of all standard NIPs including: +- Core protocol NIPs (NIP-01, NIP-02, etc.) +- Social features (reactions, reposts, channels) +- Identity and discovery (NIP-05, NIP-65) +- Security and privacy (NIP-44, NIP-42) +- Lightning integration (NIP-47, NIP-57) +- Advanced features + +#### references/event-kinds.md +Complete reference for all Nostr event kinds: +- Core events (0-999) +- Regular events (1000-9999) +- Replaceable events (10000-19999) +- Ephemeral events (20000-29999) +- Parameterized replaceable events (30000-39999) +- Event lifecycle behaviors +- Common patterns and examples + +#### references/common-mistakes.md +Detailed guide on implementation pitfalls: +- Event creation and signing errors +- WebSocket communication issues +- Filter query problems +- Threading mistakes +- Relay management errors +- Security vulnerabilities +- UX considerations +- Testing strategies + +## When to Use + +Use this skill when: +- Implementing Nostr clients or relays +- Working with Nostr events and messages +- Handling cryptographic signatures and keys +- Implementing any NIP +- Building social features on Nostr +- Debugging Nostr applications +- Discussing Nostr protocol architecture + +## Key Features + +### Complete NIP Coverage +All standard NIPs documented with: +- Purpose and status +- Implementation details +- Code examples +- Usage patterns +- Interoperability notes + +### Cryptographic Operations +Detailed guidance on: +- Event signing with Schnorr signatures +- Event ID calculation +- Signature verification +- Key management (BIP-39, NIP-06) +- Encryption (NIP-04, NIP-44) + +### WebSocket Protocol +Complete reference for: +- Message types (EVENT, REQ, CLOSE, OK, EOSE, etc.) +- Filter queries and optimization +- Subscription management +- Connection handling +- Error handling + +### Event Lifecycle +Understanding of: +- Regular events (immutable) +- Replaceable events (latest only) +- Ephemeral events (real-time only) +- Parameterized replaceable events (by identifier) + +### Best Practices +Comprehensive guidance on: +- Multi-relay architecture +- NIP-65 relay lists +- Event caching +- Optimistic UI +- Security considerations +- Performance optimization + +## Quick Start Examples + +### Publishing a Note +```javascript +const event = { + pubkey: userPublicKey, + created_at: Math.floor(Date.now() / 1000), + kind: 1, + tags: [], + content: "Hello Nostr!" +} +event.id = calculateId(event) +event.sig = signEvent(event, privateKey) +ws.send(JSON.stringify(["EVENT", event])) +``` + +### Subscribing to Events +```javascript +const filter = { + kinds: [1], + authors: [followedPubkey], + limit: 50 +} +ws.send(JSON.stringify(["REQ", "sub-id", filter])) +``` + +### Replying to a Note +```javascript +const reply = { + kind: 1, + tags: [ + ["e", originalEventId, "", "root"], + ["p", originalAuthorPubkey] + ], + content: "Great post!" +} +``` + +## Official Resources + +- **NIPs Repository**: https://github.com/nostr-protocol/nips +- **Nostr Website**: https://nostr.com +- **Nostr Documentation**: https://nostr.how +- **NIP Status**: https://nostr-nips.com + +## Skill Maintenance + +This skill is based on the official Nostr NIPs repository. As new NIPs are proposed and implemented, this skill should be updated to reflect the latest standards and best practices. + +## License + +Based on public Nostr protocol specifications (MIT License). + diff --git a/.claude/skills/nostr/SKILL.md b/.claude/skills/nostr/SKILL.md new file mode 100644 index 0000000..357d51a --- /dev/null +++ b/.claude/skills/nostr/SKILL.md @@ -0,0 +1,459 @@ +--- +name: nostr +description: This skill should be used when working with the Nostr protocol, implementing Nostr clients or relays, handling Nostr events, or discussing Nostr Implementation Possibilities (NIPs). Provides comprehensive knowledge of Nostr's decentralized protocol, event structure, cryptographic operations, and all standard NIPs. +--- + +# Nostr Protocol Expert + +## Purpose + +This skill provides expert-level assistance with the Nostr protocol, a simple, open protocol for global, decentralized, and censorship-resistant social networks. The protocol is built on relays and cryptographic keys, enabling direct peer-to-peer communication without central servers. + +## When to Use + +Activate this skill when: +- Implementing Nostr clients or relays +- Working with Nostr events and messages +- Handling cryptographic signatures and keys (schnorr signatures on secp256k1) +- Implementing any Nostr Implementation Possibility (NIP) +- Building social networking features on Nostr +- Querying or filtering Nostr events +- Discussing Nostr protocol architecture +- Implementing WebSocket communication with relays + +## Core Concepts + +### The Protocol Foundation + +Nostr operates on two main components: +1. **Clients** - Applications users run to read/write data +2. **Relays** - Servers that store and forward messages + +Key principles: +- Everyone runs a client +- Anyone can run a relay +- Users identified by public keys +- Messages signed with private keys +- No central authority or trusted servers + +### Events Structure + +All data in Nostr is represented as events. An event is a JSON object with this structure: + +```json +{ + "id": "<32-bytes lowercase hex-encoded sha256 of the serialized event data>", + "pubkey": "<32-bytes lowercase hex-encoded public key of the event creator>", + "created_at": "", + "kind": "", + "tags": [ + ["", "", "", "..."] + ], + "content": "", + "sig": "<64-bytes lowercase hex of the schnorr signature of the sha256 hash of the serialized event data>" +} +``` + +### Event Kinds + +Standard event kinds (from various NIPs): +- `0` - Metadata (user profile) +- `1` - Text note (short post) +- `2` - Recommend relay +- `3` - Contacts (following list) +- `4` - Encrypted direct messages +- `5` - Event deletion +- `6` - Repost +- `7` - Reaction (like, emoji reaction) +- `40` - Channel creation +- `41` - Channel metadata +- `42` - Channel message +- `43` - Channel hide message +- `44` - Channel mute user +- `1000-9999` - Regular events +- `10000-19999` - Replaceable events +- `20000-29999` - Ephemeral events +- `30000-39999` - Parameterized replaceable events + +### Tags + +Common tag types: +- `["e", "", "", ""]` - Reference to an event +- `["p", "", ""]` - Reference to a user +- `["a", "::", ""]` - Reference to a replaceable event +- `["d", ""]` - Identifier for parameterized replaceable events +- `["r", ""]` - Reference/link to a web resource +- `["t", ""]` - Hashtag +- `["g", ""]` - Geolocation +- `["nonce", "", ""]` - Proof of work +- `["subject", ""]` - Subject/title +- `["client", ""]` - Client application used + +## Key NIPs Reference + +For detailed specifications, refer to **references/nips-overview.md**. + +### Core Protocol NIPs + +#### NIP-01: Basic Protocol Flow +The foundation of Nostr. Defines: +- Event structure and validation +- Event ID calculation (SHA256 of serialized event) +- Signature verification (schnorr signatures) +- Client-relay communication via WebSocket +- Message types: EVENT, REQ, CLOSE, EOSE, OK, NOTICE + +#### NIP-02: Contact List and Petnames +Event kind `3` for following lists: +- Each `p` tag represents a followed user +- Optional relay URL and petname in tag +- Replaceable event (latest overwrites) + +#### NIP-04: Encrypted Direct Messages +Event kind `4` for private messages: +- Content encrypted with shared secret (ECDH) +- `p` tag for recipient pubkey +- Deprecated in favor of NIP-44 + +#### NIP-05: Mapping Nostr Keys to DNS +Internet identifier format: `name@domain.com` +- `.well-known/nostr.json` endpoint +- Maps names to pubkeys +- Optional relay list + +#### NIP-09: Event Deletion +Event kind `5` to request deletion: +- Contains `e` tags for events to delete +- Relays should delete referenced events +- Only works for own events + +#### NIP-10: Text Note References (Threads) +Conventions for `e` and `p` tags in replies: +- Root event reference +- Reply event reference +- Mentions +- Marker types: "root", "reply", "mention" + +#### NIP-11: Relay Information Document +HTTP endpoint for relay metadata: +- GET request to relay URL +- Returns JSON with relay information +- Supported NIPs, software, limitations + +### Social Features NIPs + +#### NIP-25: Reactions +Event kind `7` for reactions: +- Content usually "+" (like) or emoji +- `e` tag for reacted event +- `p` tag for event author + +#### NIP-42: Authentication +Client authentication to relays: +- AUTH message from relay (challenge) +- Client responds with event kind `22242` signed auth event +- Proves key ownership + +**CRITICAL: Clients MUST wait for OK response after AUTH** +- Relays MUST respond to AUTH with an OK message (same as EVENT) +- An OK with `true` confirms the relay has stored the authenticated pubkey +- An OK with `false` indicates authentication failed: + 1. **Alert the user** that authentication failed + 2. **Assume the relay will reject** subsequent events requiring auth + 3. Check the `reason` field for error details (e.g., "error: failed to parse auth event") +- Do NOT send events requiring authentication until OK `true` is received +- If no OK is received within timeout, assume connection issues and retry or alert user + +#### NIP-50: Search +Query filter extension for full-text search: +- `search` field in REQ filters +- Implementation-defined behavior + +### Advanced NIPs + +#### NIP-19: bech32-encoded Entities +Human-readable identifiers: +- `npub`: public key +- `nsec`: private key (sensitive!) +- `note`: note/event ID +- `nprofile`: profile with relay hints +- `nevent`: event with relay hints +- `naddr`: replaceable event coordinate + +#### NIP-44: Encrypted Payloads +Improved encryption for direct messages: +- Versioned encryption scheme +- Better security than NIP-04 +- ChaCha20-Poly1305 AEAD + +#### NIP-65: Relay List Metadata +Event kind `10002` for relay lists: +- Read/write relay preferences +- Optimizes relay discovery +- Replaceable event + +## Client-Relay Communication + +### WebSocket Messages + +#### From Client to Relay + +**EVENT** - Publish an event: +```json +["EVENT", ] +``` + +**REQ** - Request events (subscription): +```json +["REQ", , , , ...] +``` + +**CLOSE** - Stop a subscription: +```json +["CLOSE", ] +``` + +**AUTH** - Respond to auth challenge: +```json +["AUTH", ] +``` + +#### From Relay to Client + +**EVENT** - Send event to client: +```json +["EVENT", , ] +``` + +**OK** - Acceptance/rejection notice: +```json +["OK", , , ] +``` + +**EOSE** - End of stored events: +```json +["EOSE", ] +``` + +**CLOSED** - Subscription closed: +```json +["CLOSED", , ] +``` + +**NOTICE** - Human-readable message: +```json +["NOTICE", ] +``` + +**AUTH** - Authentication challenge: +```json +["AUTH", ] +``` + +### Filter Objects + +Filters select events in REQ messages: + +```json +{ + "ids": ["", ...], + "authors": ["", ...], + "kinds": [, ...], + "#e": ["", ...], + "#p": ["", ...], + "#a": ["", ...], + "#t": ["", ...], + "since": , + "until": , + "limit": +} +``` + +Filtering rules: +- Arrays are ORed together +- Different fields are ANDed +- Tag filters: `#` matches tag values +- Prefix matching allowed for `ids` and `authors` + +## Cryptographic Operations + +### Key Management + +- **Private Key**: 32-byte random value, keep secure +- **Public Key**: Derived via secp256k1 +- **Encoding**: Hex (lowercase) or bech32 + +### Event Signing (schnorr) + +Steps to create a signed event: +1. Set all fields except `id` and `sig` +2. Serialize event data to JSON (specific order) +3. Calculate SHA256 hash → `id` +4. Sign `id` with schnorr signature → `sig` + +Serialization format for ID calculation: +```json +[ + 0, + , + , + , + , + +] +``` + +### Event Verification + +Steps to verify an event: +1. Verify ID matches SHA256 of serialized data +2. Verify signature is valid schnorr signature +3. Check created_at is reasonable (not far future) +4. Validate event structure and required fields + +## Implementation Best Practices + +### For Clients + +1. **Connect to Multiple Relays**: Don't rely on single relay +2. **Cache Events**: Reduce redundant relay queries +3. **Verify Signatures**: Always verify event signatures +4. **Handle Replaceable Events**: Keep only latest version +5. **Respect User Privacy**: Careful with sensitive data +6. **Implement NIP-65**: Use user's preferred relays +7. **Proper Error Handling**: Handle relay disconnections +8. **Pagination**: Use `limit`, `since`, `until` for queries + +### For Relays + +1. **Validate Events**: Check signatures, IDs, structure +2. **Rate Limiting**: Prevent spam and abuse +3. **Storage Management**: Ephemeral events, retention policies +4. **Implement NIP-11**: Provide relay information +5. **WebSocket Optimization**: Handle many connections +6. **Filter Optimization**: Efficient event querying +7. **Consider NIP-42**: Authentication for write access +8. **Performance**: Index by pubkey, kind, tags, timestamp + +### Security Considerations + +1. **Never Expose Private Keys**: Handle nsec carefully +2. **Validate All Input**: Prevent injection attacks +3. **Use NIP-44**: For encrypted messages (not NIP-04) +4. **Check Event Timestamps**: Reject far-future events +5. **Implement Proof of Work**: NIP-13 for spam prevention +6. **Sanitize Content**: XSS prevention in displayed content +7. **Relay Trust**: Don't trust single relay for critical data + +## Common Patterns + +### Publishing a Note + +```javascript +const event = { + pubkey: userPublicKey, + created_at: Math.floor(Date.now() / 1000), + kind: 1, + tags: [], + content: "Hello Nostr!", +} +// Calculate ID and sign +event.id = calculateId(event) +event.sig = signEvent(event, privateKey) +// Publish to relay +ws.send(JSON.stringify(["EVENT", event])) +``` + +### Subscribing to Notes + +```javascript +const filter = { + kinds: [1], + authors: [followedPubkey1, followedPubkey2], + limit: 50 +} +ws.send(JSON.stringify(["REQ", "my-sub", filter])) +``` + +### Replying to a Note + +```javascript +const reply = { + kind: 1, + tags: [ + ["e", originalEventId, relayUrl, "root"], + ["p", originalAuthorPubkey] + ], + content: "Great post!", + // ... other fields +} +``` + +### Reacting to a Note + +```javascript +const reaction = { + kind: 7, + tags: [ + ["e", eventId], + ["p", eventAuthorPubkey] + ], + content: "+", // or emoji + // ... other fields +} +``` + +## Development Resources + +### Essential NIPs for Beginners + +Start with these NIPs in order: +1. **NIP-01** - Basic protocol (MUST read) +2. **NIP-19** - Bech32 identifiers +3. **NIP-02** - Following lists +4. **NIP-10** - Threaded conversations +5. **NIP-25** - Reactions +6. **NIP-65** - Relay lists + +### Testing and Development + +- **Relay Implementations**: nostream, strfry, relay.py +- **Test Relays**: wss://relay.damus.io, wss://nos.lol +- **Libraries**: nostr-tools (JS), rust-nostr (Rust), python-nostr (Python) +- **Development Tools**: NostrDebug, Nostr Army Knife, nostril +- **Reference Clients**: Damus (iOS), Amethyst (Android), Snort (Web) + +### Key Repositories + +- **NIPs Repository**: https://github.com/nostr-protocol/nips +- **Awesome Nostr**: https://github.com/aljazceru/awesome-nostr +- **Nostr Resources**: https://nostr.how + +## Reference Files + +For comprehensive NIP details, see: +- **references/nips-overview.md** - Detailed descriptions of all standard NIPs +- **references/event-kinds.md** - Complete event kinds reference +- **references/common-mistakes.md** - Pitfalls and how to avoid them + +## Quick Checklist + +When implementing Nostr: +- [ ] Events have all required fields (id, pubkey, created_at, kind, tags, content, sig) +- [ ] Event IDs calculated correctly (SHA256 of serialization) +- [ ] Signatures verified (schnorr on secp256k1) +- [ ] WebSocket messages properly formatted +- [ ] Filter queries optimized with appropriate limits +- [ ] Handling replaceable events correctly +- [ ] Connected to multiple relays for redundancy +- [ ] Following relevant NIPs for features implemented +- [ ] Private keys never exposed or transmitted +- [ ] Event timestamps validated + +## Official Resources + +- **NIPs Repository**: https://github.com/nostr-protocol/nips +- **Nostr Website**: https://nostr.com +- **Nostr Documentation**: https://nostr.how +- **NIP Status**: https://nostr-nips.com + diff --git a/.claude/skills/nostr/references/common-mistakes.md b/.claude/skills/nostr/references/common-mistakes.md new file mode 100644 index 0000000..569a144 --- /dev/null +++ b/.claude/skills/nostr/references/common-mistakes.md @@ -0,0 +1,657 @@ +# Common Nostr Implementation Mistakes and How to Avoid Them + +This document highlights frequent errors made when implementing Nostr clients and relays, along with solutions. + +## Event Creation and Signing + +### Mistake 1: Incorrect Event ID Calculation + +**Problem**: Wrong serialization order or missing fields when calculating SHA256. + +**Correct Serialization**: +```json +[ + 0, // Must be integer 0 + , // Lowercase hex string + , // Unix timestamp integer + , // Integer + , // Array of arrays + // String +] +``` + +**Common errors**: +- Using string "0" instead of integer 0 +- Including `id` or `sig` fields in serialization +- Wrong field order +- Not using compact JSON (no spaces) +- Using uppercase hex + +**Fix**: Serialize exactly as shown, compact JSON, SHA256 the UTF-8 bytes. + +### Mistake 2: Wrong Signature Algorithm + +**Problem**: Using ECDSA instead of Schnorr signatures. + +**Correct**: +- Use Schnorr signatures (BIP-340) +- Curve: secp256k1 +- Sign the 32-byte event ID + +**Libraries**: +- JavaScript: noble-secp256k1 +- Rust: secp256k1 +- Go: btcsuite/btcd/btcec/v2/schnorr +- Python: secp256k1-py + +### Mistake 3: Invalid created_at Timestamps + +**Problem**: Events with far-future timestamps or very old timestamps. + +**Best practices**: +- Use current Unix time: `Math.floor(Date.now() / 1000)` +- Relays often reject if `created_at > now + 15 minutes` +- Don't backdate events to manipulate ordering + +**Fix**: Always use current time when creating events. + +### Mistake 4: Malformed Tags + +**Problem**: Tags that aren't arrays or have wrong structure. + +**Correct format**: +```json +{ + "tags": [ + ["e", "event-id", "relay-url", "marker"], + ["p", "pubkey", "relay-url"], + ["t", "hashtag"] + ] +} +``` + +**Common errors**: +- Using objects instead of arrays: `{"e": "..."}` ❌ +- Missing inner arrays: `["e", "event-id"]` when nested in tags is wrong +- Wrong nesting depth +- Non-string values (except for specific NIPs) + +### Mistake 5: Not Handling Replaceable Events + +**Problem**: Showing multiple versions of replaceable events. + +**Event types**: +- **Replaceable (10000-19999)**: Same author + kind → replace +- **Parameterized Replaceable (30000-39999)**: Same author + kind + d-tag → replace + +**Fix**: +```javascript +// For replaceable events +const key = `${event.pubkey}:${event.kind}` +if (latestEvents[key]?.created_at < event.created_at) { + latestEvents[key] = event +} + +// For parameterized replaceable events +const dTag = event.tags.find(t => t[0] === 'd')?.[1] || '' +const key = `${event.pubkey}:${event.kind}:${dTag}` +if (latestEvents[key]?.created_at < event.created_at) { + latestEvents[key] = event +} +``` + +## WebSocket Communication + +### Mistake 6: Not Handling EOSE + +**Problem**: Loading indicators never finish or show wrong state. + +**Solution**: +```javascript +const receivedEvents = new Set() +let eoseReceived = false + +ws.onmessage = (msg) => { + const [type, ...rest] = JSON.parse(msg.data) + + if (type === 'EVENT') { + const [subId, event] = rest + receivedEvents.add(event.id) + displayEvent(event) + } + + if (type === 'EOSE') { + eoseReceived = true + hideLoadingSpinner() + } +} +``` + +### Mistake 7: Not Closing Subscriptions + +**Problem**: Memory leaks and wasted bandwidth from unclosed subscriptions. + +**Fix**: Always send CLOSE when done: +```javascript +ws.send(JSON.stringify(['CLOSE', subId])) +``` + +**Best practices**: +- Close when component unmounts +- Close before opening new subscription with same ID +- Use unique subscription IDs +- Track active subscriptions + +### Mistake 8: Ignoring OK Messages + +**Problem**: Not knowing if events were accepted or rejected. + +**Solution**: +```javascript +ws.onmessage = (msg) => { + const [type, eventId, accepted, message] = JSON.parse(msg.data) + + if (type === 'OK') { + if (!accepted) { + console.error(`Event ${eventId} rejected: ${message}`) + handleRejection(eventId, message) + } + } +} +``` + +**Common rejection reasons**: +- `pow:` - Insufficient proof of work +- `blocked:` - Pubkey or content blocked +- `rate-limited:` - Too many requests +- `invalid:` - Failed validation + +### Mistake 9: Sending Events Before WebSocket Ready + +**Problem**: Events lost because WebSocket not connected. + +**Fix**: +```javascript +const sendWhenReady = (ws, message) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(message) + } else { + ws.addEventListener('open', () => ws.send(message), { once: true }) + } +} +``` + +### Mistake 10: Not Handling WebSocket Disconnections + +**Problem**: App breaks when relay goes offline. + +**Solution**: Implement reconnection with exponential backoff: +```javascript +let reconnectDelay = 1000 +const maxDelay = 30000 + +const connect = () => { + const ws = new WebSocket(relayUrl) + + ws.onclose = () => { + setTimeout(() => { + reconnectDelay = Math.min(reconnectDelay * 2, maxDelay) + connect() + }, reconnectDelay) + } + + ws.onopen = () => { + reconnectDelay = 1000 // Reset on successful connection + resubscribe() // Re-establish subscriptions + } +} +``` + +## Filter Queries + +### Mistake 11: Overly Broad Filters + +**Problem**: Requesting too many events, overwhelming relay and client. + +**Bad**: +```json +{ + "kinds": [1], + "limit": 10000 +} +``` + +**Good**: +```json +{ + "kinds": [1], + "authors": [""], + "limit": 50, + "since": 1234567890 +} +``` + +**Best practices**: +- Always set reasonable `limit` (50-500) +- Filter by `authors` when possible +- Use `since`/`until` for time ranges +- Be specific with `kinds` +- Multiple smaller queries > one huge query + +### Mistake 12: Not Using Prefix Matching + +**Problem**: Full hex strings in filters unnecessarily. + +**Optimization**: +```json +{ + "ids": ["abc12345"], // 8 chars enough for uniqueness + "authors": ["def67890"] +} +``` + +Relays support prefix matching for `ids` and `authors`. + +### Mistake 13: Duplicate Filter Fields + +**Problem**: Redundant filter conditions. + +**Bad**: +```json +{ + "authors": ["pubkey1", "pubkey1"], + "kinds": [1, 1] +} +``` + +**Good**: +```json +{ + "authors": ["pubkey1"], + "kinds": [1] +} +``` + +Deduplicate filter arrays. + +## Threading and References + +### Mistake 14: Incorrect Thread Structure + +**Problem**: Missing root/reply markers or wrong tag order. + +**Correct reply structure** (NIP-10): +```json +{ + "kind": 1, + "tags": [ + ["e", "", "", "root"], + ["e", "", "", "reply"], + ["p", ""], + ["p", ""] + ] +} +``` + +**Key points**: +- Root event should have "root" marker +- Direct parent should have "reply" marker +- Include `p` tags for all mentioned users +- Relay hints are optional but helpful + +### Mistake 15: Missing p Tags in Replies + +**Problem**: Authors not notified of replies. + +**Fix**: Always add `p` tag for: +- Original author +- Authors mentioned in content +- Authors in the thread chain + +```json +{ + "tags": [ + ["e", "event-id", "", "reply"], + ["p", "original-author"], + ["p", "mentioned-user1"], + ["p", "mentioned-user2"] + ] +} +``` + +### Mistake 16: Not Using Markers + +**Problem**: Ambiguous thread structure. + +**Solution**: Always use markers in `e` tags: +- `root` - Root of thread +- `reply` - Direct parent +- `mention` - Referenced but not replied to + +Without markers, clients must guess thread structure. + +## Relay Management + +### Mistake 17: Relying on Single Relay + +**Problem**: Single point of failure, censorship vulnerability. + +**Solution**: Connect to multiple relays (5-15 common): +```javascript +const relays = [ + 'wss://relay1.com', + 'wss://relay2.com', + 'wss://relay3.com' +] + +const connections = relays.map(url => connect(url)) +``` + +**Best practices**: +- Publish to 3-5 write relays +- Read from 5-10 read relays +- Use NIP-65 for user's preferred relays +- Fall back to NIP-05 relays +- Implement relay rotation on failure + +### Mistake 18: Not Implementing NIP-65 + +**Problem**: Querying wrong relays, missing user's events. + +**Correct flow**: +1. Fetch user's kind `10002` event (relay list) +2. Connect to their read relays to fetch their content +3. Connect to their write relays to send them messages + +```javascript +async function getUserRelays(pubkey) { + // Fetch kind 10002 + const relayList = await fetchEvent({ + kinds: [10002], + authors: [pubkey] + }) + + const readRelays = [] + const writeRelays = [] + + relayList.tags.forEach(([tag, url, mode]) => { + if (tag === 'r') { + if (!mode || mode === 'read') readRelays.push(url) + if (!mode || mode === 'write') writeRelays.push(url) + } + }) + + return { readRelays, writeRelays } +} +``` + +### Mistake 19: Not Respecting Relay Limitations + +**Problem**: Violating relay policies, getting rate limited or banned. + +**Solution**: Fetch and respect NIP-11 relay info: +```javascript +const getRelayInfo = async (relayUrl) => { + const url = relayUrl.replace('wss://', 'https://').replace('ws://', 'http://') + const response = await fetch(url, { + headers: { 'Accept': 'application/nostr+json' } + }) + return response.json() +} + +// Respect limitations +const info = await getRelayInfo(relayUrl) +const maxLimit = info.limitation?.max_limit || 500 +const maxFilters = info.limitation?.max_filters || 10 +``` + +## Security + +### Mistake 20: Exposing Private Keys + +**Problem**: Including nsec in client code, logs, or network requests. + +**Never**: +- Store nsec in localStorage without encryption +- Log private keys +- Send nsec over network +- Display nsec to user unless explicitly requested +- Hard-code private keys + +**Best practices**: +- Use NIP-07 (browser extension) when possible +- Encrypt keys at rest +- Use NIP-46 (remote signing) for web apps +- Warn users when showing nsec + +### Mistake 21: Not Verifying Signatures + +**Problem**: Accepting invalid events, vulnerability to attacks. + +**Always verify**: +```javascript +const verifyEvent = (event) => { + // 1. Verify ID + const calculatedId = sha256(serializeEvent(event)) + if (calculatedId !== event.id) return false + + // 2. Verify signature + const signatureValid = schnorr.verify( + event.sig, + event.id, + event.pubkey + ) + if (!signatureValid) return false + + // 3. Check timestamp + const now = Math.floor(Date.now() / 1000) + if (event.created_at > now + 900) return false // 15 min future + + return true +} +``` + +**Verify before**: +- Displaying to user +- Storing in database +- Using event data for logic + +### Mistake 22: Using NIP-04 Encryption + +**Problem**: Weak encryption, vulnerable to attacks. + +**Solution**: Use NIP-44 instead: +- Modern authenticated encryption +- ChaCha20-Poly1305 AEAD +- Proper key derivation +- Version byte for upgradability + +**Migration**: Update to NIP-44 for all new encrypted messages. + +### Mistake 23: Not Sanitizing Content + +**Problem**: XSS vulnerabilities in displayed content. + +**Solution**: Sanitize before rendering: +```javascript +import DOMPurify from 'dompurify' + +const safeContent = DOMPurify.sanitize(event.content, { + ALLOWED_TAGS: ['b', 'i', 'u', 'a', 'code', 'pre'], + ALLOWED_ATTR: ['href', 'target', 'rel'] +}) +``` + +**Especially critical for**: +- Markdown rendering +- Link parsing +- Image URLs +- User-provided HTML + +## User Experience + +### Mistake 24: Not Caching Events + +**Problem**: Re-fetching same events repeatedly, poor performance. + +**Solution**: Implement event cache: +```javascript +const eventCache = new Map() + +const cacheEvent = (event) => { + eventCache.set(event.id, event) +} + +const getCachedEvent = (eventId) => { + return eventCache.get(eventId) +} +``` + +**Cache strategies**: +- LRU eviction for memory management +- IndexedDB for persistence +- Invalidate replaceable events on update +- Cache metadata (kind 0) aggressively + +### Mistake 25: Not Implementing Optimistic UI + +**Problem**: Slow feeling app, waiting for relay confirmation. + +**Solution**: Show user's events immediately: +```javascript +const publishEvent = async (event) => { + // Immediately show to user + displayEvent(event, { pending: true }) + + // Publish to relays + const results = await Promise.all( + relays.map(relay => relay.publish(event)) + ) + + // Update status based on results + const success = results.some(r => r.accepted) + displayEvent(event, { pending: false, success }) +} +``` + +### Mistake 26: Poor Loading States + +**Problem**: User doesn't know if app is working. + +**Solution**: Clear loading indicators: +- Show spinner until EOSE +- Display "Loading..." placeholder +- Show how many relays responded +- Indicate connection status per relay + +### Mistake 27: Not Handling Large Threads + +**Problem**: Loading entire thread at once, performance issues. + +**Solution**: Implement pagination: +```javascript +const loadThread = async (eventId, cursor = null) => { + const filter = { + "#e": [eventId], + kinds: [1], + limit: 20, + until: cursor + } + + const replies = await fetchEvents(filter) + return { replies, nextCursor: replies[replies.length - 1]?.created_at } +} +``` + +## Testing + +### Mistake 28: Not Testing with Multiple Relays + +**Problem**: App works with one relay but fails with others. + +**Solution**: Test with: +- Fast relays +- Slow relays +- Unreliable relays +- Paid relays (auth required) +- Relays with different NIP support + +### Mistake 29: Not Testing Edge Cases + +**Critical tests**: +- Empty filter results +- WebSocket disconnections +- Malformed events +- Very long content +- Invalid signatures +- Relay errors +- Rate limiting +- Concurrent operations + +### Mistake 30: Not Monitoring Performance + +**Metrics to track**: +- Event verification time +- WebSocket latency per relay +- Events per second processed +- Memory usage (event cache) +- Subscription count +- Failed publishes + +## Best Practices Checklist + +**Event Creation**: +- [ ] Correct serialization for ID +- [ ] Schnorr signatures +- [ ] Current timestamp +- [ ] Valid tag structure +- [ ] Handle replaceable events + +**WebSocket**: +- [ ] Handle EOSE +- [ ] Close subscriptions +- [ ] Process OK messages +- [ ] Check WebSocket state +- [ ] Reconnection logic + +**Filters**: +- [ ] Set reasonable limits +- [ ] Specific queries +- [ ] Deduplicate arrays +- [ ] Use prefix matching + +**Threading**: +- [ ] Use root/reply markers +- [ ] Include all p tags +- [ ] Proper thread structure + +**Relays**: +- [ ] Multiple relays +- [ ] Implement NIP-65 +- [ ] Respect limitations +- [ ] Handle failures + +**Security**: +- [ ] Never expose nsec +- [ ] Verify all signatures +- [ ] Use NIP-44 encryption +- [ ] Sanitize content + +**UX**: +- [ ] Cache events +- [ ] Optimistic UI +- [ ] Loading states +- [ ] Pagination + +**Testing**: +- [ ] Multiple relays +- [ ] Edge cases +- [ ] Monitor performance + +## Resources + +- **nostr-tools**: JavaScript library with best practices +- **rust-nostr**: Rust implementation with strong typing +- **NIPs Repository**: Official specifications +- **Nostr Dev**: Community resources and help + diff --git a/.claude/skills/nostr/references/event-kinds.md b/.claude/skills/nostr/references/event-kinds.md new file mode 100644 index 0000000..8b587da --- /dev/null +++ b/.claude/skills/nostr/references/event-kinds.md @@ -0,0 +1,361 @@ +# Nostr Event Kinds - Complete Reference + +This document provides a comprehensive list of all standard and commonly-used Nostr event kinds. + +## Standard Event Kinds + +### Core Events (0-999) + +#### Metadata and Profile +- **0**: `Metadata` - User profile information (name, about, picture, etc.) + - Replaceable + - Content: JSON with profile fields + +#### Text Content +- **1**: `Text Note` - Short-form post (like a tweet) + - Regular event (not replaceable) + - Most common event type + +#### Relay Recommendations +- **2**: `Recommend Relay` - Deprecated, use NIP-65 instead + +#### Contact Lists +- **3**: `Contacts` - Following list with optional relay hints + - Replaceable + - Tags: `p` tags for each followed user + +#### Encrypted Messages +- **4**: `Encrypted Direct Message` - Private message (NIP-04, deprecated) + - Regular event + - Use NIP-44 instead for better security + +#### Content Management +- **5**: `Event Deletion` - Request to delete events + - Tags: `e` tags for events to delete + - Only works for own events + +#### Sharing +- **6**: `Repost` - Share another event + - Tags: `e` for reposted event, `p` for original author + - May include original event in content + +#### Reactions +- **7**: `Reaction` - Like, emoji reaction to event + - Content: "+" or emoji + - Tags: `e` for reacted event, `p` for author + +### Channel Events (40-49) + +- **40**: `Channel Creation` - Create a public chat channel +- **41**: `Channel Metadata` - Set channel name, about, picture +- **42**: `Channel Message` - Post message in channel +- **43**: `Channel Hide Message` - Hide a message in channel +- **44**: `Channel Mute User` - Mute a user in channel + +### Regular Events (1000-9999) + +Regular events are never deleted or replaced. All versions are kept. + +- **1000**: `Example regular event` +- **1063**: `File Metadata` (NIP-94) - Metadata for shared files + - Tags: url, MIME type, hash, size, dimensions + +### Replaceable Events (10000-19999) + +Only the latest event of each kind is kept per pubkey. + +- **10000**: `Mute List` - List of muted users/content +- **10001**: `Pin List` - Pinned events +- **10002**: `Relay List Metadata` (NIP-65) - User's preferred relays + - Critical for routing + - Tags: `r` with relay URLs and read/write markers + +### Ephemeral Events (20000-29999) + +Not stored by relays, only forwarded once. + +- **20000**: `Example ephemeral event` +- **21000**: `Typing Indicator` - User is typing +- **22242**: `Client Authentication` (NIP-42) - Auth response to relay + +### Parameterized Replaceable Events (30000-39999) + +Replaced based on `d` tag value. + +#### Lists (30000-30009) +- **30000**: `Categorized People List` - Custom people lists + - `d` tag: list identifier + - `p` tags: people in list + +- **30001**: `Categorized Bookmark List` - Bookmark collections + - `d` tag: list identifier + - `e` or `a` tags: bookmarked items + +- **30008**: `Badge Definition` (NIP-58) - Define a badge/achievement + - `d` tag: badge ID + - Tags: name, description, image + +- **30009**: `Profile Badges` (NIP-58) - Badges displayed on profile + - `d` tag: badge ID + - `e` or `a` tags: badge awards + +#### Long-form Content (30023) +- **30023**: `Long-form Article` (NIP-23) - Blog post, article + - `d` tag: article identifier (slug) + - Tags: title, summary, published_at, image + - Content: Markdown + +#### Application Data (30078) +- **30078**: `Application-specific Data` (NIP-78) + - `d` tag: app-name:data-key + - Content: app-specific data (may be encrypted) + +#### Other Parameterized Replaceables +- **31989**: `Application Handler Information` (NIP-89) + - Declares app can handle certain event kinds + +- **31990**: `Handler Recommendation` (NIP-89) + - User's preferred apps for event kinds + +## Special Event Kinds + +### Authentication & Signing +- **22242**: `Client Authentication` - Prove key ownership to relay +- **24133**: `Nostr Connect` - Remote signer protocol (NIP-46) + +### Lightning & Payments +- **9734**: `Zap Request` (NIP-57) - Request Lightning payment + - Not published to regular relays + - Sent to LNURL provider + +- **9735**: `Zap Receipt` (NIP-57) - Proof of Lightning payment + - Published by LNURL provider + - Proves zap was paid + +- **23194**: `Wallet Request` (NIP-47) - Request wallet operation +- **23195**: `Wallet Response` (NIP-47) - Response to wallet request + +### Content & Annotations +- **1984**: `Reporting` (NIP-56) - Report content/users + - Tags: reason (spam, illegal, etc.) + +- **9802**: `Highlights` (NIP-84) - Highlight text + - Content: highlighted text + - Tags: context, source event + +### Badges & Reputation +- **8**: `Badge Award` (NIP-58) - Award a badge to someone + - Tags: `a` for badge definition, `p` for recipient + +### Generic Events +- **16**: `Generic Repost` (NIP-18) - Repost any event kind + - More flexible than kind 6 + +- **27235**: `HTTP Auth` (NIP-98) - Authenticate HTTP requests + - Tags: URL, method + +## Event Kind Ranges Summary + +| Range | Type | Behavior | Examples | +|-------|------|----------|----------| +| 0-999 | Core | Varies | Metadata, notes, reactions | +| 1000-9999 | Regular | Immutable, all kept | File metadata | +| 10000-19999 | Replaceable | Only latest kept | Mute list, relay list | +| 20000-29999 | Ephemeral | Not stored | Typing, presence | +| 30000-39999 | Parameterized Replaceable | Replaced by `d` tag | Articles, lists, badges | + +## Event Lifecycle + +### Regular Events (1000-9999) +``` +Event A published → Stored +Event A' published → Both A and A' stored +``` + +### Replaceable Events (10000-19999) +``` +Event A published → Stored +Event A' published (same kind, same pubkey) → A deleted, A' stored +``` + +### Parameterized Replaceable Events (30000-39999) +``` +Event A (d="foo") published → Stored +Event B (d="bar") published → Both stored (different d) +Event A' (d="foo") published → A deleted, A' stored (same d) +``` + +### Ephemeral Events (20000-29999) +``` +Event A published → Forwarded to subscribers, NOT stored +``` + +## Common Patterns + +### Metadata (Kind 0) +```json +{ + "kind": 0, + "content": "{\"name\":\"Alice\",\"about\":\"Nostr user\",\"picture\":\"https://...\",\"nip05\":\"alice@example.com\"}", + "tags": [] +} +``` + +### Text Note (Kind 1) +```json +{ + "kind": 1, + "content": "Hello Nostr!", + "tags": [ + ["t", "nostr"], + ["t", "hello"] + ] +} +``` + +### Reply (Kind 1 with thread tags) +```json +{ + "kind": 1, + "content": "Great post!", + "tags": [ + ["e", "", "", "root"], + ["e", "", "", "reply"], + ["p", ""] + ] +} +``` + +### Reaction (Kind 7) +```json +{ + "kind": 7, + "content": "+", + "tags": [ + ["e", ""], + ["p", ""], + ["k", "1"] + ] +} +``` + +### Long-form Article (Kind 30023) +```json +{ + "kind": 30023, + "content": "# My Article\n\nContent here...", + "tags": [ + ["d", "my-article-slug"], + ["title", "My Article"], + ["summary", "This is about..."], + ["published_at", "1234567890"], + ["t", "nostr"], + ["image", "https://..."] + ] +} +``` + +### Relay List (Kind 10002) +```json +{ + "kind": 10002, + "content": "", + "tags": [ + ["r", "wss://relay1.com"], + ["r", "wss://relay2.com", "write"], + ["r", "wss://relay3.com", "read"] + ] +} +``` + +### Zap Request (Kind 9734) +```json +{ + "kind": 9734, + "content": "", + "tags": [ + ["relays", "wss://relay1.com", "wss://relay2.com"], + ["amount", "21000"], + ["lnurl", "lnurl..."], + ["p", ""], + ["e", ""] + ] +} +``` + +### File Metadata (Kind 1063) +```json +{ + "kind": 1063, + "content": "My photo from the trip", + "tags": [ + ["url", "https://cdn.example.com/image.jpg"], + ["m", "image/jpeg"], + ["x", "abc123..."], + ["size", "524288"], + ["dim", "1920x1080"], + ["blurhash", "LEHV6n..."] + ] +} +``` + +### Report (Kind 1984) +```json +{ + "kind": 1984, + "content": "This is spam", + "tags": [ + ["e", "", ""], + ["p", ""], + ["report", "spam"] + ] +} +``` + +## Future Event Kinds + +The event kind space is open-ended. New NIPs may define new event kinds. + +**Guidelines for new event kinds**: +1. Use appropriate range for desired behavior +2. Document in a NIP +3. Implement in at least 2 clients and 1 relay +4. Ensure backwards compatibility +5. Don't overlap with existing kinds + +**Custom event kinds**: +- Applications can use undefined event kinds +- Document behavior for interoperability +- Consider proposing as a NIP if useful broadly + +## Event Kind Selection Guide + +**Choose based on lifecycle needs**: + +- **Regular (1000-9999)**: When you need history + - User posts, comments, reactions + - Payment records, receipts + - Immutable records + +- **Replaceable (10000-19999)**: When you need latest state + - User settings, preferences + - Mute/block lists + - Current status + +- **Ephemeral (20000-29999)**: When you need real-time only + - Typing indicators + - Online presence + - Temporary notifications + +- **Parameterized Replaceable (30000-39999)**: When you need multiple latest states + - Articles (one per slug) + - Product listings (one per product ID) + - Configuration sets (one per setting name) + +## References + +- NIPs Repository: https://github.com/nostr-protocol/nips +- NIP-16: Event Treatment +- NIP-01: Event structure +- Various feature NIPs for specific kinds + diff --git a/.claude/skills/nostr/references/nips-overview.md b/.claude/skills/nostr/references/nips-overview.md new file mode 100644 index 0000000..bcf3e96 --- /dev/null +++ b/.claude/skills/nostr/references/nips-overview.md @@ -0,0 +1,1170 @@ +# Nostr Implementation Possibilities (NIPs) - Complete Overview + +This document provides detailed descriptions of all standard NIPs from the nostr-protocol/nips repository. + +## Core Protocol NIPs + +### NIP-01: Basic Protocol Flow Description + +**Status**: Mandatory for all implementations + +The foundational NIP that defines the entire Nostr protocol. + +#### Events + +Events are the only object type in Nostr. Structure: + +```json +{ + "id": "<32-bytes lowercase hex>", + "pubkey": "<32-bytes lowercase hex>", + "created_at": "", + "kind": "", + "tags": [["", "", ...]], + "content": "", + "sig": "<64-bytes hex>" +} +``` + +**Event ID Calculation**: +1. Serialize to JSON array: `[0, pubkey, created_at, kind, tags, content]` +2. UTF-8 encode +3. Calculate SHA256 hash +4. Result is the event ID + +**Signature**: +- Schnorr signature of the event ID +- Uses secp256k1 curve +- 64-byte hex-encoded + +#### Communication Protocol + +All communication happens over WebSocket. + +**Client Messages**: + +1. `["EVENT", ]` - Publish event +2. `["REQ", , , ...]` - Subscribe +3. `["CLOSE", ]` - Unsubscribe + +**Relay Messages**: + +1. `["EVENT", , ]` - Send event +2. `["OK", , , ]` - Command result +3. `["EOSE", ]` - End of stored events +4. `["CLOSED", , ]` - Forced close +5. `["NOTICE", ]` - Human-readable notice + +#### Filters + +Filter object fields (all optional): +- `ids`: List of event IDs (prefix match) +- `authors`: List of pubkeys (prefix match) +- `kinds`: List of event kinds +- `#`: Tag queries +- `since`: Unix timestamp (events after) +- `until`: Unix timestamp (events before) +- `limit`: Maximum events to return + +A filter matches if ALL conditions are met. Within arrays, conditions are ORed. + +#### Basic Event Kinds + +- `0`: Metadata (user profile) +- `1`: Text note +- `2`: Recommend relay (deprecated) + +### NIP-02: Contact List and Petnames + +**Status**: Widely implemented + +Defines event kind `3` for user contact lists (following lists). + +**Format**: +```json +{ + "kind": 3, + "tags": [ + ["p", "", "", ""] + ], + "content": "" +} +``` + +**Characteristics**: +- Replaceable event (latest version is authoritative) +- Each `p` tag is a followed user +- Relay URL (optional): where to find this user +- Petname (optional): user's chosen name for contact +- Content may contain JSON relay list (deprecated, use NIP-65) + +**Usage**: +- Clients fetch kind 3 to build following list +- Always replace old version with new +- Use for social graph discovery + +### NIP-03: OpenTimestamps Attestations + +**Status**: Optional + +Allows embedding OpenTimestamps proofs in events. + +**Format**: +```json +{ + "tags": [ + ["ots", ""] + ] +} +``` + +Used to prove an event existed at a specific time via Bitcoin blockchain timestamps. + +### NIP-04: Encrypted Direct Messages + +**Status**: Deprecated (use NIP-44) + +Event kind `4` for encrypted private messages. + +**Encryption**: +- ECDH shared secret between sender/receiver +- AES-256-CBC encryption +- Base64 encoded result + +**Format**: +```json +{ + "kind": 4, + "tags": [ + ["p", ""] + ], + "content": "" +} +``` + +**Security Issues**: +- Vulnerable to certain attacks +- No forward secrecy +- Use NIP-44 instead + +### NIP-05: Mapping Nostr Keys to DNS-based Internet Identifiers + +**Status**: Widely implemented + +Allows verification of identity via domain names (like email addresses). + +**Format**: `name@domain.com` + +**Implementation**: + +1. User adds `"nip05": "alice@example.com"` to metadata (kind 0) +2. Domain serves `/.well-known/nostr.json`: + +```json +{ + "names": { + "alice": "" + }, + "relays": { + "": ["wss://relay1.com", "wss://relay2.com"] + } +} +``` + +3. Clients verify by fetching and checking pubkey match + +**Benefits**: +- Human-readable identifiers +- Domain-based verification +- Optional relay hints +- Spam prevention (verified users) + +### NIP-06: Basic Key Derivation from Mnemonic Seed Phrase + +**Status**: Optional + +Derives Nostr keys from BIP39 mnemonic phrases. + +**Derivation Path**: `m/44'/1237'/0'/0/0` +- 1237 is the coin type for Nostr +- Allows HD wallet-style key management + +**Benefits**: +- Backup with 12/24 words +- Multiple accounts from one seed +- Compatible with BIP39 tools + +### NIP-07: window.nostr Capability for Web Browsers + +**Status**: Browser extension standard + +Defines browser API for Nostr key management. + +**API Methods**: + +```javascript +window.nostr.getPublicKey(): Promise +window.nostr.signEvent(event): Promise +window.nostr.getRelays(): Promise<{[url]: {read: boolean, write: boolean}}> +window.nostr.nip04.encrypt(pubkey, plaintext): Promise +window.nostr.nip04.decrypt(pubkey, ciphertext): Promise +``` + +**Usage**: +- Web apps request signatures from extension +- Private keys never leave extension +- User approves each action +- Popular extensions: nos2x, Alby, Flamingo + +### NIP-08: Handling Mentions + +**Status**: Core convention + +Defines how to mention users and events in notes. + +**Format**: +- Add `p` or `e` tags for mentions +- Reference in content with `#[index]` + +```json +{ + "kind": 1, + "tags": [ + ["p", "<pubkey>", "<relay>"], + ["e", "<event-id>", "<relay>"] + ], + "content": "Hello #[0], check out #[1]" +} +``` + +Clients replace `#[0]`, `#[1]` with user-friendly displays. + +### NIP-09: Event Deletion + +**Status**: Widely implemented + +Event kind `5` requests deletion of events. + +**Format**: +```json +{ + "kind": 5, + "tags": [ + ["e", "<event-id-to-delete>"], + ["e", "<another-event-id>"] + ], + "content": "Reason for deletion (optional)" +} +``` + +**Behavior**: +- Only author can delete their events +- Relays SHOULD delete referenced events +- Not guaranteed (relays may ignore) +- Some clients show deletion notice + +### NIP-10: Text Note References (Reply, Threads) + +**Status**: Core threading standard + +Conventions for `e` and `p` tags in threaded conversations. + +**Markers**: +- `root`: The root event of the thread +- `reply`: Direct parent being replied to +- `mention`: Mentioned but not replied to + +**Format**: +```json +{ + "kind": 1, + "tags": [ + ["e", "<root-event-id>", "<relay>", "root"], + ["e", "<parent-event-id>", "<relay>", "reply"], + ["e", "<mentioned-event-id>", "<relay>", "mention"], + ["p", "<author1-pubkey>"], + ["p", "<author2-pubkey>"] + ] +} +``` + +**Best Practices**: +- Always include root marker for thread context +- Include reply marker for direct parent +- Add p tags for all mentioned users +- Maintains thread integrity + +### NIP-11: Relay Information Document + +**Status**: Standard + +HTTP endpoint for relay metadata. + +**Implementation**: +- HTTP GET to relay URL (not WebSocket) +- Accept header: `application/nostr+json` + +**Response Example**: +```json +{ + "name": "Example Relay", + "description": "A Nostr relay", + "pubkey": "<admin-pubkey>", + "contact": "admin@example.com", + "supported_nips": [1, 2, 9, 11, 12, 15, 16, 20, 22], + "software": "git+https://github.com/...", + "version": "1.0.0", + "limitation": { + "max_message_length": 16384, + "max_subscriptions": 20, + "max_filters": 100, + "max_limit": 5000, + "max_subid_length": 100, + "min_prefix": 4, + "max_event_tags": 100, + "max_content_length": 8196, + "min_pow_difficulty": 30, + "auth_required": false, + "payment_required": false + }, + "relay_countries": ["US", "CA"], + "language_tags": ["en", "es"], + "tags": ["adult-content", "no-spam"], + "posting_policy": "https://example.com/policy", + "payments_url": "https://example.com/pay", + "fees": { + "admission": [{"amount": 5000000, "unit": "msats"}], + "subscription": [{"amount": 1000000, "unit": "msats", "period": 2592000}], + "publication": [] + }, + "icon": "https://example.com/icon.png" +} +``` + +**Usage**: +- Clients discover relay capabilities +- Check NIP support before using features +- Display relay info to users +- Respect limitations + +### NIP-12: Generic Tag Queries + +**Status**: Core functionality + +Extends filtering to support any single-letter tag. + +**Syntax**: `#<letter>: [<value>, ...]` + +**Examples**: +```json +{ + "#t": ["bitcoin", "nostr"], + "#p": ["pubkey1", "pubkey2"], + "#e": ["eventid1"] +} +``` + +Matches events with specified tag values. + +### NIP-13: Proof of Work + +**Status**: Spam prevention + +Requires computational work for event publication. + +**Implementation**: +- Add `nonce` tag: `["nonce", "<number>", "<target-difficulty>"]` +- Hash event ID until leading zero bits >= difficulty +- Increment nonce until condition met + +**Example**: +```json +{ + "tags": [ + ["nonce", "12345", "20"] + ], + "id": "00000abcd..." // 20+ leading zero bits +} +``` + +**Difficulty Levels**: +- 0-10: Very easy +- 20: Moderate +- 30+: Difficult +- 40+: Very difficult + +Relays can require minimum PoW for acceptance. + +### NIP-14: Subject Tag + +**Status**: Convenience + +Adds `subject` tag for event titles/subjects. + +**Format**: +```json +{ + "tags": [ + ["subject", "My Post Title"] + ] +} +``` + +Used for long-form content, discussions, emails-style messages. + +### NIP-15: End of Stored Events (EOSE) + +**Status**: Core protocol + +Relay sends `EOSE` after sending all stored events matching a subscription. + +**Format**: `["EOSE", <subscription_id>]` + +**Usage**: +- Clients know when historical events are complete +- Can show "loading" state until EOSE +- New events after EOSE are real-time + +### NIP-16: Event Treatment + +**Status**: Event lifecycle + +Defines three event categories: + +1. **Regular Events** (1000-9999): + - Immutable + - All versions kept + - Examples: notes, reactions + +2. **Replaceable Events** (10000-19999): + - Only latest kept + - Same author + kind → replace + - Examples: metadata, contacts + +3. **Ephemeral Events** (20000-29999): + - Not stored + - Forwarded once + - Examples: typing indicators, presence + +4. **Parameterized Replaceable Events** (30000-39999): + - Replaced based on `d` tag + - Same author + kind + d-tag → replace + - Examples: long-form posts, product listings + +### NIP-18: Reposts + +**Status**: Social feature + +Event kind `6` for reposting/sharing events. + +**Format**: +```json +{ + "kind": 6, + "tags": [ + ["e", "<reposted-event-id>", "<relay>"], + ["p", "<original-author-pubkey>"] + ], + "content": "" // or reposted event JSON +} +``` + +**Generic Repost** (kind 16): +- Can repost any event kind +- Preserves original context + +### NIP-19: bech32-encoded Entities + +**Status**: Widely implemented + +Human-readable encodings for Nostr entities. + +**Formats**: + +1. **npub**: Public key + - `npub1xyz...` + - Safer to share than hex + +2. **nsec**: Private key (SENSITIVE!) + - `nsec1xyz...` + - Never share publicly + +3. **note**: Event ID + - `note1xyz...` + - Links to specific events + +4. **nprofile**: Profile with hints + - Includes pubkey + relay URLs + - Better discovery + +5. **nevent**: Event with hints + - Includes event ID + relay URLs + author + - Reliable event fetching + +6. **naddr**: Replaceable event coordinate + - Includes kind + pubkey + d-tag + relays + - For parameterized replaceable events + +**Usage**: +- Use for sharing/displaying identifiers +- Clients should support all formats +- Always use npub/nsec instead of hex when possible + +### NIP-20: Command Results + +**Status**: Core protocol + +Defines `OK` message format from relays. + +**Format**: `["OK", <event_id>, <accepted>, <message>]` + +**Examples**: +```json +["OK", "abc123...", true, ""] +["OK", "def456...", false, "invalid: signature verification failed"] +["OK", "ghi789...", false, "pow: difficulty too low"] +["OK", "jkl012...", false, "rate-limited: slow down"] +``` + +**Common Rejection Prefixes**: +- `duplicate:` - Event already received +- `pow:` - Insufficient proof of work +- `blocked:` - Pubkey or content blocked +- `rate-limited:` - Too many requests +- `invalid:` - Event validation failed +- `error:` - Server error + +### NIP-21: nostr: URI Scheme + +**Status**: Standard linking + +Defines `nostr:` URI scheme for deep linking. + +**Format**: +- `nostr:npub1...` +- `nostr:note1...` +- `nostr:nevent1...` +- `nostr:nprofile1...` +- `nostr:naddr1...` + +**Usage**: +- Clickable links in web/mobile +- Cross-app navigation +- QR codes + +### NIP-22: Event created_at Limits + +**Status**: Relay policy + +Relays may reject events with timestamps too far in past/future. + +**Recommendations**: +- Reject events created_at > 15 minutes in future +- Reject very old events (relay-specific) +- Prevents timestamp manipulation + +### NIP-23: Long-form Content + +**Status**: Blog/article support + +Event kind `30023` for long-form content (articles, blogs). + +**Format**: +```json +{ + "kind": 30023, + "tags": [ + ["d", "<unique-identifier>"], + ["title", "Article Title"], + ["summary", "Brief description"], + ["published_at", "<unix-timestamp>"], + ["t", "tag1"], ["t", "tag2"], + ["image", "https://..."] + ], + "content": "Markdown content..." +} +``` + +**Characteristics**: +- Parameterized replaceable (by `d` tag) +- Content in Markdown +- Rich metadata +- Can be edited (updates replace) + +### NIP-25: Reactions + +**Status**: Widely implemented + +Event kind `7` for reactions to events (likes, emoji reactions). + +**Format**: +```json +{ + "kind": 7, + "tags": [ + ["e", "<reacted-event-id>"], + ["p", "<event-author-pubkey>"], + ["k", "<reacted-event-kind>"] + ], + "content": "+" // or emoji +} +``` + +**Content Values**: +- `+`: Like/upvote +- `-`: Dislike (discouraged) +- Emoji: 👍, ❤️, 😂, etc. +- Custom reactions + +**Client Display**: +- Count reactions per event +- Group by emoji +- Show who reacted + +### NIP-26: Delegated Event Signing + +**Status**: Advanced delegation + +Allows delegating event signing to another key. + +**Use Cases**: +- Bot accounts posting for user +- Temporary keys for devices +- Service providers posting on behalf + +**Implementation**: +- Delegation token in tags +- Limits by kind, time range +- Original author still verifiable + +### NIP-27: Text Note References + +**Status**: Convenience + +Shortcuts for mentioning entities inline. + +**Format**: +- `nostr:npub1...` → user mention +- `nostr:note1...` → event reference +- `nostr:nevent1...` → event with context + +Clients render as clickable links. + +### NIP-28: Public Chat (Channels) + +**Status**: Channel support + +Event kinds for public chat channels. + +**Event Kinds**: +- `40`: Create channel +- `41`: Set channel metadata +- `42`: Create message +- `43`: Hide message +- `44`: Mute user + +**Channel Creation (kind 40)**: +```json +{ + "kind": 40, + "content": "{\"name\": \"Bitcoin\", \"about\": \"Discussion\", \"picture\": \"url\"}" +} +``` + +**Channel Message (kind 42)**: +```json +{ + "kind": 42, + "tags": [ + ["e", "<channel-id>", "<relay>", "root"] + ], + "content": "Hello channel!" +} +``` + +### NIP-33: Parameterized Replaceable Events + +**Status**: Core feature + +Event kinds 30000-39999 are replaceable by `d` tag. + +**Format**: +```json +{ + "kind": 30000, + "tags": [ + ["d", "<identifier>"] + ] +} +``` + +**Replacement Rule**: +- Same author + kind + d-tag → replace old event +- Different d-tag → separate events +- No d-tag → treated as `d` = "" + +**Coordinate Reference**: +`<kind>:<pubkey>:<d-value>` + +**Use Cases**: +- Product catalogs (each product = d-tag) +- Article revisions (article slug = d-tag) +- Configuration settings (setting name = d-tag) + +### NIP-36: Sensitive Content Warning + +**Status**: Content moderation + +Tags for marking sensitive/NSFW content. + +**Format**: +```json +{ + "tags": [ + ["content-warning", "nudity"], + ["content-warning", "violence"] + ] +} +``` + +Clients can hide/blur until user confirms. + +### NIP-39: External Identities + +**Status**: Identity verification + +Links Nostr identity to external platforms. + +**Format (in kind 0 metadata)**: +```json +{ + "kind": 0, + "content": "{\"identities\": [{\"platform\": \"github\", \"username\": \"alice\", \"proof\": \"url\"}]}" +} +``` + +**Supported Platforms**: +- GitHub +- Twitter +- Mastodon +- Matrix +- Telegram + +### NIP-40: Expiration Timestamp + +**Status**: Ephemeral content + +Tag for auto-expiring events. + +**Format**: +```json +{ + "tags": [ + ["expiration", "<unix-timestamp>"] + ] +} +``` + +Relays should delete event after expiration time. + +### NIP-42: Authentication of Clients to Relays + +**Status**: Access control + +Relays can require client authentication. + +**Flow**: +1. Relay sends: `["AUTH", "<challenge>"]` +2. Client creates kind `22242` event: +```json +{ + "kind": 22242, + "tags": [ + ["relay", "<relay-url>"], + ["challenge", "<challenge-string>"] + ], + "created_at": <now> +} +``` +3. Client sends: `["AUTH", <signed-event>]` +4. Relay verifies signature and challenge + +**Benefits**: +- Spam prevention +- Access control +- Rate limiting per user +- Paid relays + +### NIP-44: Encrypted Payloads (Versioned) + +**Status**: Modern encryption + +Improved encryption replacing NIP-04. + +**Algorithm**: +- ECDH shared secret +- ChaCha20-Poly1305 AEAD +- Version byte for upgradability +- Salt for key derivation + +**Security Improvements**: +- Authenticated encryption +- Better key derivation +- Version support +- Resistance to padding oracle attacks + +**Format**: +``` +<version-byte><encrypted-payload> +``` + +Base64 encode for `content` field. + +### NIP-45: Event Counts + +**Status**: Statistics + +Request for event counts matching filters. + +**Client Request**: +```json +["COUNT", <subscription_id>, <filters>] +``` + +**Relay Response**: +```json +["COUNT", <subscription_id>, {"count": 123, "approximate": false}] +``` + +**Usage**: +- Display follower counts +- Show engagement metrics +- Statistics dashboards + +### NIP-46: Nostr Connect (Remote Signing) + +**Status**: Remote signer protocol + +Protocol for remote key management and signing. + +**Architecture**: +- Signer: Holds private key +- Client: Requests signatures +- Communication via Nostr events + +**Use Cases**: +- Mobile app delegates to desktop signer +- Browser extension as signer +- Hardware wallet integration +- Multi-device key sharing + +### NIP-47: Wallet Connect + +**Status**: Lightning integration + +Protocol for connecting Lightning wallets to Nostr apps. + +**Commands**: +- `pay_invoice` +- `get_balance` +- `get_info` +- `make_invoice` +- `lookup_invoice` + +Enables in-app Lightning payments. + +### NIP-50: Search Capability + +**Status**: Optional + +Full-text search in filter queries. + +**Format**: +```json +{ + "search": "bitcoin nostr" +} +``` + +**Implementation**: +- Relay-specific behavior +- May search content, tags, etc. +- Not standardized ranking + +### NIP-51: Lists + +**Status**: Curation + +Event kinds for various list types. + +**List Kinds**: +- `30000`: Categorized people list +- `30001`: Categorized bookmarks +- `10000`: Mute list +- `10001`: Pin list + +**Format**: +```json +{ + "kind": 30000, + "tags": [ + ["d", "my-list"], + ["p", "<pubkey>", "<relay>", "<petname>"], + ["t", "<category>"] + ] +} +``` + +### NIP-56: Reporting + +**Status**: Moderation + +Event kind `1984` for reporting content. + +**Format**: +```json +{ + "kind": 1984, + "tags": [ + ["e", "<event-id>", "<relay>"], + ["p", "<pubkey>"], + ["report", "spam"] // or "nudity", "profanity", "illegal", "impersonation" + ], + "content": "Additional details" +} +``` + +Used by relays and clients for moderation. + +### NIP-57: Lightning Zaps + +**Status**: Widely implemented + +Protocol for Lightning tips with proof. + +**Flow**: +1. Get user's Lightning address (from metadata) +2. Fetch LNURL data +3. Create zap request (kind `9734`) +4. Pay invoice +5. Relay publishes zap receipt (kind `9735`) + +**Zap Request (kind 9734)**: +```json +{ + "kind": 9734, + "tags": [ + ["p", "<recipient-pubkey>"], + ["amount", "<millisats>"], + ["relays", "relay1", "relay2"], + ["e", "<event-id>"] // if zapping event + ] +} +``` + +**Zap Receipt (kind 9735)**: +Published by LNURL provider, proves payment. + +### NIP-58: Badges + +**Status**: Reputation system + +Award and display badges (achievements, credentials). + +**Event Kinds**: +- `30008`: Badge definition +- `30009`: Profile badges +- `8`: Badge award + +**Badge Definition**: +```json +{ + "kind": 30008, + "tags": [ + ["d", "badge-id"], + ["name", "Badge Name"], + ["description", "What this means"], + ["image", "url"], + ["thumb", "thumbnail-url"] + ] +} +``` + +### NIP-65: Relay List Metadata + +**Status**: Critical for routing + +Event kind `10002` for user's relay preferences. + +**Format**: +```json +{ + "kind": 10002, + "tags": [ + ["r", "wss://relay1.com"], + ["r", "wss://relay2.com", "write"], + ["r", "wss://relay3.com", "read"] + ] +} +``` + +**Usage**: +- Clients discover where to fetch user's events (read) +- Clients know where to send events for user (write) +- Optimizes relay connections +- Reduces bandwidth + +**Best Practice**: +- Always check NIP-65 before querying +- Fall back to NIP-05 relays if no NIP-65 +- Cache relay lists + +### NIP-78: App-Specific Data + +**Status**: Application storage + +Event kind `30078` for arbitrary app data. + +**Format**: +```json +{ + "kind": 30078, + "tags": [ + ["d", "<app-name>:<data-key>"] + ], + "content": "<encrypted-or-public-data>" +} +``` + +**Use Cases**: +- App settings +- Client-specific cache +- User preferences +- Draft posts + +### NIP-84: Highlights + +**Status**: Annotation + +Event kind `9802` for highlighting content. + +**Format**: +```json +{ + "kind": 9802, + "tags": [ + ["e", "<event-id>"], + ["context", "surrounding text..."], + ["a", "<article-coordinate>"] + ], + "content": "highlighted portion" +} +``` + +Like a highlighter pen for web content. + +### NIP-89: Application Handlers + +**Status**: App discovery + +Advertise and discover apps that handle specific event kinds. + +**Format (kind 31989)**: +```json +{ + "kind": 31989, + "tags": [ + ["k", "1"], // handles kind 1 + ["web", "https://app.com/<bech32>"], + ["ios", "app-scheme://<bech32>"], + ["android", "app-package://<bech32>"] + ] +} +``` + +**Kind 31990**: User's preferred handlers + +### NIP-94: File Metadata + +**Status**: File sharing + +Event kind `1063` for file metadata. + +**Format**: +```json +{ + "kind": 1063, + "tags": [ + ["url", "https://..."], + ["m", "image/jpeg"], // MIME type + ["x", "<sha256-hash>"], + ["size", "123456"], + ["dim", "1920x1080"], + ["magnet", "magnet:..."], + ["blurhash", "..."] + ], + "content": "Description" +} +``` + +**Use Cases**: +- Images, videos, audio +- Documents +- Torrents +- IPFS files + +### NIP-96: HTTP File Storage Integration + +**Status**: File hosting + +HTTP API for file uploads/downloads. + +**Endpoints**: +- `GET /.well-known/nostr/nip96.json` - Server info +- `POST /upload` - Upload file +- `DELETE /delete` - Delete file + +**Upload Response**: +Returns kind `1063` event data for the file. + +### NIP-98: HTTP Auth + +**Status**: API authentication + +Use Nostr events for HTTP API auth. + +**Flow**: +1. Create kind `27235` event with: + - `u` tag: API URL + - `method` tag: HTTP method +2. Add `Authorization: Nostr <base64-event>` header +3. Server verifies signature + +**Benefits**: +- No passwords +- Cryptographic authentication +- Works with Nostr keys + +## Summary of Key NIPs by Category + +### Essential (All implementations) +- NIP-01, NIP-02, NIP-10, NIP-19 + +### Social Features +- NIP-25 (reactions), NIP-18 (reposts), NIP-23 (long-form), NIP-28 (channels) + +### Identity & Discovery +- NIP-05 (verification), NIP-39 (external identities), NIP-65 (relay lists) + +### Security & Privacy +- NIP-04 (deprecated encryption), NIP-44 (modern encryption), NIP-42 (auth), NIP-13 (PoW) + +### Lightning Integration +- NIP-47 (wallet connect), NIP-57 (zaps) + +### Content & Moderation +- NIP-56 (reporting), NIP-36 (content warnings), NIP-09 (deletion) + +### Advanced Features +- NIP-33 (parameterized replaceable), NIP-46 (remote signing), NIP-50 (search) + diff --git a/.claude/skills/react/README.md b/.claude/skills/react/README.md new file mode 100644 index 0000000..9144da8 --- /dev/null +++ b/.claude/skills/react/README.md @@ -0,0 +1,119 @@ +# React 19 Skill + +A comprehensive Claude skill for working with React 19, including hooks, components, server components, and modern React architecture. + +## Contents + +### Main Skill File +- **SKILL.md** - Main skill document with React 19 fundamentals, hooks, components, and best practices + +### References +- **hooks-quick-reference.md** - Quick reference for all React hooks with examples +- **server-components.md** - Complete guide to React Server Components and Server Functions +- **performance.md** - Performance optimization strategies and techniques + +### Examples +- **practical-patterns.tsx** - Real-world React patterns and solutions + +## What This Skill Covers + +### Core Topics +- React 19 features and improvements +- All built-in hooks (useState, useEffect, useTransition, useOptimistic, etc.) +- Component patterns and composition +- Server Components and Server Functions +- React Compiler and automatic optimization +- Performance optimization techniques +- Form handling and validation +- Error boundaries and error handling +- Context and global state management +- Code splitting and lazy loading + +### Best Practices +- Component design principles +- State management strategies +- Performance optimization +- Error handling patterns +- TypeScript integration +- Testing considerations +- Accessibility guidelines + +## When to Use This Skill + +Use this skill when: +- Building React 19 applications +- Working with React hooks +- Implementing server components +- Optimizing React performance +- Troubleshooting React-specific issues +- Understanding concurrent features +- Working with forms and user input +- Implementing complex UI patterns + +## Quick Start Examples + +### Basic Component +```typescript +interface ButtonProps { + label: string + onClick: () => void +} + +const Button = ({ label, onClick }: ButtonProps) => { + return <button onClick={onClick}>{label}</button> +} +``` + +### Using Hooks +```typescript +const Counter = () => { + const [count, setCount] = useState(0) + + useEffect(() => { + console.log(`Count is: ${count}`) + }, [count]) + + return ( + <button onClick={() => setCount(c => c + 1)}> + Count: {count} + </button> + ) +} +``` + +### Server Component +```typescript +const Page = async () => { + const data = await fetchData() + return <div>{data}</div> +} +``` + +### Server Function +```typescript +'use server' + +export async function createUser(formData: FormData) { + const name = formData.get('name') + return await db.user.create({ data: { name } }) +} +``` + +## Related Skills + +- **typescript** - TypeScript patterns for React +- **ndk** - Nostr integration with React +- **skill-creator** - Creating reusable component libraries + +## Resources + +- [React Documentation](https://react.dev) +- [React API Reference](https://react.dev/reference/react) +- [React Hooks Reference](https://react.dev/reference/react/hooks) +- [React Server Components](https://react.dev/reference/rsc) +- [React Compiler](https://react.dev/reference/react-compiler) + +## Version + +This skill is based on React 19.2 and includes the latest features and APIs. + diff --git a/.claude/skills/react/SKILL.md b/.claude/skills/react/SKILL.md new file mode 100644 index 0000000..abe826f --- /dev/null +++ b/.claude/skills/react/SKILL.md @@ -0,0 +1,1026 @@ +--- +name: react +description: This skill should be used when working with React 19, including hooks, components, server components, concurrent features, and React DOM APIs. Provides comprehensive knowledge of React patterns, best practices, and modern React architecture. +--- + +# React 19 Skill + +This skill provides comprehensive knowledge and patterns for working with React 19 effectively in modern applications. + +## When to Use This Skill + +Use this skill when: +- Building React applications with React 19 features +- Working with React hooks and component patterns +- Implementing server components and server functions +- Using concurrent features and transitions +- Optimizing React application performance +- Troubleshooting React-specific issues +- Working with React DOM APIs and client/server rendering +- Using React Compiler features + +## Core Concepts + +### React 19 Overview + +React 19 introduces significant improvements: +- **Server Components** - Components that render on the server +- **Server Functions** - Functions that run on the server from client code +- **Concurrent Features** - Better performance with concurrent rendering +- **React Compiler** - Automatic memoization and optimization +- **Form Actions** - Built-in form handling with useActionState +- **Improved Hooks** - New hooks like useOptimistic, useActionState +- **Better Hydration** - Improved SSR and hydration performance + +### Component Fundamentals + +Use functional components with hooks: + +```typescript +// Functional component with props interface +interface ButtonProps { + label: string + onClick: () => void + variant?: 'primary' | 'secondary' +} + +const Button = ({ label, onClick, variant = 'primary' }: ButtonProps) => { + return ( + <button + onClick={onClick} + className={`btn btn-${variant}`} + > + {label} + </button> + ) +} +``` + +**Key Principles:** +- Use functional components over class components +- Define prop interfaces in TypeScript +- Use destructuring for props +- Provide default values for optional props +- Keep components focused and composable + +## React Hooks Reference + +### State Hooks + +#### useState +Manage local component state: + +```typescript +const [count, setCount] = useState<number>(0) +const [user, setUser] = useState<User | null>(null) + +// Named return variables pattern +const handleIncrement = () => { + setCount(prev => prev + 1) // Functional update +} + +// Update object state immutably +setUser(prev => prev ? { ...prev, name: 'New Name' } : null) +``` + +#### useReducer +Manage complex state with reducer pattern: + +```typescript +type State = { count: number; status: 'idle' | 'loading' } +type Action = + | { type: 'increment' } + | { type: 'decrement' } + | { type: 'setStatus'; status: State['status'] } + +const reducer = (state: State, action: Action): State => { + switch (action.type) { + case 'increment': + return { ...state, count: state.count + 1 } + case 'decrement': + return { ...state, count: state.count - 1 } + case 'setStatus': + return { ...state, status: action.status } + default: + return state + } +} + +const [state, dispatch] = useReducer(reducer, { count: 0, status: 'idle' }) +``` + +#### useActionState +Handle form actions with pending states (React 19): + +```typescript +const [state, formAction, isPending] = useActionState( + async (previousState: FormState, formData: FormData) => { + const name = formData.get('name') as string + + // Server action or async operation + const result = await saveUser({ name }) + + return { success: true, data: result } + }, + { success: false, data: null } +) + +return ( + <form action={formAction}> + <input name="name" /> + <button disabled={isPending}> + {isPending ? 'Saving...' : 'Save'} + </button> + </form> +) +``` + +### Effect Hooks + +#### useEffect +Run side effects after render: + +```typescript +// Named return variables preferred +useEffect(() => { + const controller = new AbortController() + + const fetchData = async () => { + const response = await fetch('/api/data', { + signal: controller.signal + }) + const data = await response.json() + setData(data) + } + + fetchData() + + // Cleanup function + return () => { + controller.abort() + } +}, [dependencies]) // Dependencies array +``` + +**Key Points:** +- Always return cleanup function for subscriptions +- Use dependency array correctly to avoid infinite loops +- Don't forget to handle race conditions with AbortController +- Effects run after paint, not during render + +#### useLayoutEffect +Run effects synchronously after DOM mutations but before paint: + +```typescript +useLayoutEffect(() => { + // Measure DOM nodes + const height = ref.current?.getBoundingClientRect().height + setHeight(height) +}, []) +``` + +Use when you need to: +- Measure DOM layout +- Synchronously re-render before browser paints +- Prevent visual flicker + +#### useInsertionEffect +Insert styles before any DOM reads (for CSS-in-JS libraries): + +```typescript +useInsertionEffect(() => { + const style = document.createElement('style') + style.textContent = '.my-class { color: red; }' + document.head.appendChild(style) + + return () => { + document.head.removeChild(style) + } +}, []) +``` + +### Performance Hooks + +#### useMemo +Memoize expensive calculations: + +```typescript +const expensiveValue = useMemo(() => { + return computeExpensiveValue(a, b) +}, [a, b]) +``` + +**When to use:** +- Expensive calculations that would slow down renders +- Creating stable object references for dependency arrays +- Optimizing child component re-renders + +**When NOT to use:** +- Simple calculations (overhead not worth it) +- Values that change frequently + +#### useCallback +Memoize callback functions: + +```typescript +const handleClick = useCallback(() => { + console.log('Clicked', value) +}, [value]) + +// Pass to child that uses memo +<ChildComponent onClick={handleClick} /> +``` + +**Use when:** +- Passing callbacks to optimized child components +- Function is a dependency in another hook +- Function is used in effect cleanup + +### Ref Hooks + +#### useRef +Store mutable values that don't trigger re-renders: + +```typescript +// DOM reference +const inputRef = useRef<HTMLInputElement>(null) + +useEffect(() => { + inputRef.current?.focus() +}, []) + +// Mutable value storage +const countRef = useRef<number>(0) +countRef.current += 1 // Doesn't trigger re-render +``` + +#### useImperativeHandle +Customize ref handle for parent components: + +```typescript +interface InputHandle { + focus: () => void + clear: () => void +} + +const CustomInput = forwardRef<InputHandle, InputProps>((props, ref) => { + const inputRef = useRef<HTMLInputElement>(null) + + useImperativeHandle(ref, () => ({ + focus: () => { + inputRef.current?.focus() + }, + clear: () => { + if (inputRef.current) { + inputRef.current.value = '' + } + } + })) + + return <input ref={inputRef} {...props} /> +}) +``` + +### Context Hooks + +#### useContext +Access context values: + +```typescript +// Create context +interface ThemeContext { + theme: 'light' | 'dark' + toggleTheme: () => void +} + +const ThemeContext = createContext<ThemeContext | null>(null) + +// Provider +const ThemeProvider = ({ children }: { children: React.ReactNode }) => { + const [theme, setTheme] = useState<'light' | 'dark'>('light') + + const toggleTheme = useCallback(() => { + setTheme(prev => prev === 'light' ? 'dark' : 'light') + }, []) + + return ( + <ThemeContext.Provider value={{ theme, toggleTheme }}> + {children} + </ThemeContext.Provider> + ) +} + +// Consumer +const ThemedButton = () => { + const context = useContext(ThemeContext) + if (!context) throw new Error('useTheme must be used within ThemeProvider') + + const { theme, toggleTheme } = context + + return ( + <button onClick={toggleTheme}> + Current theme: {theme} + </button> + ) +} +``` + +### Transition Hooks + +#### useTransition +Mark state updates as non-urgent: + +```typescript +const [isPending, startTransition] = useTransition() + +const handleTabChange = (newTab: string) => { + startTransition(() => { + setTab(newTab) // Non-urgent update + }) +} + +return ( + <> + <button onClick={() => handleTabChange('profile')}> + Profile + </button> + {isPending && <Spinner />} + <TabContent tab={tab} /> + </> +) +``` + +**Use for:** +- Marking expensive updates as non-urgent +- Keeping UI responsive during state transitions +- Preventing loading states for quick updates + +#### useDeferredValue +Defer re-rendering for non-urgent updates: + +```typescript +const [query, setQuery] = useState('') +const deferredQuery = useDeferredValue(query) + +// Use deferred value for expensive rendering +const results = useMemo(() => { + return searchResults(deferredQuery) +}, [deferredQuery]) + +return ( + <> + <input value={query} onChange={e => setQuery(e.target.value)} /> + <Results data={results} /> + </> +) +``` + +### Optimistic Updates + +#### useOptimistic +Show optimistic state while async operation completes (React 19): + +```typescript +const [optimisticMessages, addOptimisticMessage] = useOptimistic( + messages, + (state, newMessage: string) => [ + ...state, + { id: 'temp', text: newMessage, pending: true } + ] +) + +const handleSend = async (formData: FormData) => { + const message = formData.get('message') as string + + // Show optimistic update immediately + addOptimisticMessage(message) + + // Send to server + await sendMessage(message) +} + +return ( + <> + {optimisticMessages.map(msg => ( + <div key={msg.id} className={msg.pending ? 'opacity-50' : ''}> + {msg.text} + </div> + ))} + <form action={handleSend}> + <input name="message" /> + <button>Send</button> + </form> + </> +) +``` + +### Other Hooks + +#### useId +Generate unique IDs for accessibility: + +```typescript +const id = useId() + +return ( + <> + <label htmlFor={id}>Name:</label> + <input id={id} type="text" /> + </> +) +``` + +#### useSyncExternalStore +Subscribe to external stores: + +```typescript +const subscribe = (callback: () => void) => { + store.subscribe(callback) + return () => store.unsubscribe(callback) +} + +const getSnapshot = () => store.getState() +const getServerSnapshot = () => store.getInitialState() + +const state = useSyncExternalStore( + subscribe, + getSnapshot, + getServerSnapshot +) +``` + +#### useDebugValue +Display custom label in React DevTools: + +```typescript +const useCustomHook = (value: string) => { + useDebugValue(value ? `Active: ${value}` : 'Inactive') + return value +} +``` + +## React Components + +### Fragment +Group elements without extra DOM nodes: + +```typescript +// Short syntax +<> + <ChildA /> + <ChildB /> +</> + +// Full syntax (when you need key prop) +<Fragment key={item.id}> + <dt>{item.term}</dt> + <dd>{item.description}</dd> +</Fragment> +``` + +### Suspense +Show fallback while loading: + +```typescript +<Suspense fallback={<Loading />}> + <AsyncComponent /> +</Suspense> + +// With error boundary +<ErrorBoundary fallback={<Error />}> + <Suspense fallback={<Loading />}> + <AsyncComponent /> + </Suspense> +</ErrorBoundary> +``` + +### StrictMode +Enable additional checks in development: + +```typescript +<StrictMode> + <App /> +</StrictMode> +``` + +**StrictMode checks:** +- Warns about deprecated APIs +- Detects unexpected side effects +- Highlights potential problems +- Double-invokes functions to catch bugs + +### Profiler +Measure rendering performance: + +```typescript +<Profiler id="App" onRender={onRender}> + <App /> +</Profiler> + +const onRender = ( + id: string, + phase: 'mount' | 'update', + actualDuration: number, + baseDuration: number, + startTime: number, + commitTime: number +) => { + console.log(`${id} took ${actualDuration}ms`) +} +``` + +## React APIs + +### memo +Prevent unnecessary re-renders: + +```typescript +const ExpensiveComponent = memo(({ data }: Props) => { + return <div>{data}</div> +}, (prevProps, nextProps) => { + // Return true if props are equal (skip render) + return prevProps.data === nextProps.data +}) +``` + +### lazy +Code-split components: + +```typescript +const Dashboard = lazy(() => import('./Dashboard')) + +<Suspense fallback={<Loading />}> + <Dashboard /> +</Suspense> +``` + +### startTransition +Mark updates as transitions imperatively: + +```typescript +startTransition(() => { + setTab('profile') +}) +``` + +### cache (React Server Components) +Cache function results per request: + +```typescript +const getUser = cache(async (id: string) => { + return await db.user.findUnique({ where: { id } }) +}) +``` + +### use (React 19) +Read context or promises in render: + +```typescript +// Read context +const theme = use(ThemeContext) + +// Read promise (must be wrapped in Suspense) +const data = use(fetchDataPromise) +``` + +## Server Components & Server Functions + +### Server Components + +Components that run only on the server: + +```typescript +// app/page.tsx (Server Component by default) +const Page = async () => { + // Can fetch data directly + const posts = await db.post.findMany() + + return ( + <div> + {posts.map(post => ( + <PostCard key={post.id} post={post} /> + ))} + </div> + ) +} + +export default Page +``` + +**Benefits:** +- Direct database access +- Zero bundle size for server-only code +- Automatic code splitting +- Better performance + +### Server Functions + +Functions that run on server, callable from client: + +```typescript +'use server' + +export async function createPost(formData: FormData) { + const title = formData.get('title') as string + const content = formData.get('content') as string + + const post = await db.post.create({ + data: { title, content } + }) + + revalidatePath('/posts') + return post +} +``` + +**Usage from client:** + +```typescript +'use client' + +import { createPost } from './actions' + +const PostForm = () => { + const [state, formAction] = useActionState(createPost, null) + + return ( + <form action={formAction}> + <input name="title" /> + <textarea name="content" /> + <button>Create</button> + </form> + ) +} +``` + +### Directives + +#### 'use client' +Mark file as client component: + +```typescript +'use client' + +import { useState } from 'react' + +// This component runs on client +export const Counter = () => { + const [count, setCount] = useState(0) + return <button onClick={() => setCount(c => c + 1)}>{count}</button> +} +``` + +#### 'use server' +Mark functions as server functions: + +```typescript +'use server' + +export async function updateUser(userId: string, data: UserData) { + return await db.user.update({ where: { id: userId }, data }) +} +``` + +## React DOM + +### Client APIs + +#### createRoot +Create root for client rendering (React 19): + +```typescript +import { createRoot } from 'react-dom/client' + +const root = createRoot(document.getElementById('root')!) +root.render(<App />) + +// Update root +root.render(<App newProp="value" />) + +// Unmount +root.unmount() +``` + +#### hydrateRoot +Hydrate server-rendered HTML: + +```typescript +import { hydrateRoot } from 'react-dom/client' + +hydrateRoot(document.getElementById('root')!, <App />) +``` + +### Component APIs + +#### createPortal +Render children outside parent DOM hierarchy: + +```typescript +import { createPortal } from 'react-dom' + +const Modal = ({ children }: { children: React.ReactNode }) => { + return createPortal( + <div className="modal">{children}</div>, + document.body + ) +} +``` + +#### flushSync +Force synchronous update: + +```typescript +import { flushSync } from 'react-dom' + +flushSync(() => { + setCount(1) +}) +// DOM is updated synchronously +``` + +### Form Components + +#### <form> with actions + +```typescript +const handleSubmit = async (formData: FormData) => { + 'use server' + const email = formData.get('email') + await saveEmail(email) +} + +<form action={handleSubmit}> + <input name="email" type="email" /> + <button>Subscribe</button> +</form> +``` + +#### useFormStatus + +```typescript +import { useFormStatus } from 'react-dom' + +const SubmitButton = () => { + const { pending } = useFormStatus() + + return ( + <button disabled={pending}> + {pending ? 'Submitting...' : 'Submit'} + </button> + ) +} +``` + +## React Compiler + +### Configuration + +Configure React Compiler in babel or bundler config: + +```javascript +// babel.config.js +module.exports = { + plugins: [ + ['react-compiler', { + compilationMode: 'annotation', // or 'all' + panicThreshold: 'all_errors', + }] + ] +} +``` + +### Directives + +#### "use memo" +Force memoization of component: + +```typescript +'use memo' + +const ExpensiveComponent = ({ data }: Props) => { + const processed = expensiveComputation(data) + return <div>{processed}</div> +} +``` + +#### "use no memo" +Prevent automatic memoization: + +```typescript +'use no memo' + +const SimpleComponent = ({ text }: Props) => { + return <div>{text}</div> +} +``` + +## Best Practices + +### Component Design + +1. **Keep components focused** - Single responsibility principle +2. **Prefer composition** - Build complex UIs from simple components +3. **Extract custom hooks** - Reusable logic in hooks +4. **Named return variables** - Use named returns in functions +5. **Type everything** - Proper TypeScript interfaces for all props + +### Performance + +1. **Use React.memo sparingly** - Only for expensive components +2. **Optimize context** - Split contexts to avoid unnecessary re-renders +3. **Lazy load routes** - Code-split at route boundaries +4. **Use transitions** - Mark non-urgent updates with useTransition +5. **Virtualize lists** - Use libraries like react-window for long lists + +### State Management + +1. **Local state first** - useState for component-specific state +2. **Lift state up** - Only when multiple components need it +3. **Use reducers for complex state** - useReducer for complex logic +4. **Context for global state** - Theme, auth, etc. +5. **External stores** - TanStack Query, Zustand for complex apps + +### Error Handling + +1. **Error boundaries** - Catch rendering errors +2. **Guard clauses** - Early returns for invalid states +3. **Null checks** - Always check for null/undefined +4. **Try-catch in effects** - Handle async errors +5. **User-friendly errors** - Show helpful error messages + +### Testing Considerations + +1. **Testable components** - Pure, predictable components +2. **Test user behavior** - Not implementation details +3. **Mock external dependencies** - APIs, context, etc. +4. **Test error states** - Verify error handling works +5. **Accessibility tests** - Test keyboard navigation, screen readers + +## Common Patterns + +### Compound Components + +```typescript +interface TabsProps { + children: React.ReactNode + defaultValue: string +} + +const TabsContext = createContext<{ + value: string + setValue: (v: string) => void +} | null>(null) + +const Tabs = ({ children, defaultValue }: TabsProps) => { + const [value, setValue] = useState(defaultValue) + + return ( + <TabsContext.Provider value={{ value, setValue }}> + {children} + </TabsContext.Provider> + ) +} + +const TabsList = ({ children }: { children: React.ReactNode }) => ( + <div role="tablist">{children}</div> +) + +const TabsTrigger = ({ value, children }: { value: string, children: React.ReactNode }) => { + const context = useContext(TabsContext) + if (!context) throw new Error('TabsTrigger must be used within Tabs') + + return ( + <button + role="tab" + aria-selected={context.value === value} + onClick={() => context.setValue(value)} + > + {children} + </button> + ) +} + +const TabsContent = ({ value, children }: { value: string, children: React.ReactNode }) => { + const context = useContext(TabsContext) + if (!context) throw new Error('TabsContent must be used within Tabs') + + if (context.value !== value) return null + + return <div role="tabpanel">{children}</div> +} + +// Usage +<Tabs defaultValue="profile"> + <TabsList> + <TabsTrigger value="profile">Profile</TabsTrigger> + <TabsTrigger value="settings">Settings</TabsTrigger> + </TabsList> + <TabsContent value="profile">Profile content</TabsContent> + <TabsContent value="settings">Settings content</TabsContent> +</Tabs> +``` + +### Render Props + +```typescript +interface DataFetcherProps<T> { + url: string + children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode +} + +const DataFetcher = <T,>({ url, children }: DataFetcherProps<T>) => { + const [data, setData] = useState<T | null>(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState<Error | null>(null) + + useEffect(() => { + fetch(url) + .then(res => res.json()) + .then(setData) + .catch(setError) + .finally(() => setLoading(false)) + }, [url]) + + return <>{children(data, loading, error)}</> +} + +// Usage +<DataFetcher<User> url="/api/user"> + {(user, loading, error) => { + if (loading) return <Spinner /> + if (error) return <Error error={error} /> + if (!user) return null + return <UserProfile user={user} /> + }} +</DataFetcher> +``` + +### Custom Hooks Pattern + +```typescript +const useLocalStorage = <T,>(key: string, initialValue: T) => { + const [storedValue, setStoredValue] = useState<T>(() => { + try { + const item = window.localStorage.getItem(key) + return item ? JSON.parse(item) : initialValue + } catch (error) { + console.error(error) + return initialValue + } + }) + + const setValue = useCallback((value: T | ((val: T) => T)) => { + try { + const valueToStore = value instanceof Function ? value(storedValue) : value + setStoredValue(valueToStore) + window.localStorage.setItem(key, JSON.stringify(valueToStore)) + } catch (error) { + console.error(error) + } + }, [key, storedValue]) + + return [storedValue, setValue] as const +} +``` + +## Troubleshooting + +### Common Issues + +#### Infinite Loops +- Check useEffect dependencies +- Ensure state updates don't trigger themselves +- Use functional setState updates + +#### Stale Closures +- Add all used variables to dependency arrays +- Use useCallback for functions in dependencies +- Consider using refs for values that shouldn't trigger re-renders + +#### Performance Issues +- Use React DevTools Profiler +- Check for unnecessary re-renders +- Optimize with memo, useMemo, useCallback +- Consider code splitting + +#### Hydration Mismatches +- Ensure server and client render same HTML +- Avoid using Date.now() or random values during render +- Use useEffect for browser-only code +- Check for conditional rendering based on browser APIs + +## References + +- **React Documentation**: https://react.dev +- **React API Reference**: https://react.dev/reference/react +- **React DOM Reference**: https://react.dev/reference/react-dom +- **React Compiler**: https://react.dev/reference/react-compiler +- **Rules of React**: https://react.dev/reference/rules +- **GitHub**: https://github.com/facebook/react + +## Related Skills + +- **typescript** - TypeScript patterns and types for React +- **ndk** - Nostr integration with React hooks +- **skill-creator** - Creating reusable component libraries + diff --git a/.claude/skills/react/examples/practical-patterns.tsx b/.claude/skills/react/examples/practical-patterns.tsx new file mode 100644 index 0000000..2883726 --- /dev/null +++ b/.claude/skills/react/examples/practical-patterns.tsx @@ -0,0 +1,878 @@ +# React Practical Examples + +This file contains real-world examples of React patterns and solutions. + +## Example 1: Custom Hook for Data Fetching + +```typescript +import { useState, useEffect } from 'react' + +interface FetchState<T> { + data: T | null + loading: boolean + error: Error | null +} + +const useFetch = <T,>(url: string) => { + const [state, setState] = useState<FetchState<T>>({ + data: null, + loading: true, + error: null + }) + + useEffect(() => { + let cancelled = false + const controller = new AbortController() + + const fetchData = async () => { + try { + setState(prev => ({ ...prev, loading: true, error: null })) + + const response = await fetch(url, { + signal: controller.signal + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const data = await response.json() + + if (!cancelled) { + setState({ data, loading: false, error: null }) + } + } catch (error) { + if (!cancelled && error.name !== 'AbortError') { + setState({ + data: null, + loading: false, + error: error as Error + }) + } + } + } + + fetchData() + + return () => { + cancelled = true + controller.abort() + } + }, [url]) + + return state +} + +// Usage +const UserProfile = ({ userId }: { userId: string }) => { + const { data, loading, error } = useFetch<User>(`/api/users/${userId}`) + + if (loading) return <Spinner /> + if (error) return <ErrorMessage error={error} /> + if (!data) return null + + return <UserCard user={data} /> +} +``` + +## Example 2: Form with Validation + +```typescript +import { useState, useCallback } from 'react' +import { z } from 'zod' + +const userSchema = z.object({ + name: z.string().min(2, 'Name must be at least 2 characters'), + email: z.string().email('Invalid email address'), + age: z.number().min(18, 'Must be 18 or older') +}) + +type UserForm = z.infer<typeof userSchema> +type FormErrors = Partial<Record<keyof UserForm, string>> + +const UserForm = () => { + const [formData, setFormData] = useState<UserForm>({ + name: '', + email: '', + age: 0 + }) + const [errors, setErrors] = useState<FormErrors>({}) + const [isSubmitting, setIsSubmitting] = useState(false) + + const handleChange = useCallback(( + field: keyof UserForm, + value: string | number + ) => { + setFormData(prev => ({ ...prev, [field]: value })) + // Clear error when user starts typing + setErrors(prev => ({ ...prev, [field]: undefined })) + }, []) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + // Validate + const result = userSchema.safeParse(formData) + if (!result.success) { + const fieldErrors: FormErrors = {} + result.error.errors.forEach(err => { + const field = err.path[0] as keyof UserForm + fieldErrors[field] = err.message + }) + setErrors(fieldErrors) + return + } + + // Submit + setIsSubmitting(true) + try { + await submitUser(result.data) + // Success handling + } catch (error) { + console.error(error) + } finally { + setIsSubmitting(false) + } + } + + return ( + <form onSubmit={handleSubmit}> + <div> + <label htmlFor="name">Name</label> + <input + id="name" + value={formData.name} + onChange={e => handleChange('name', e.target.value)} + /> + {errors.name && <span className="error">{errors.name}</span>} + </div> + + <div> + <label htmlFor="email">Email</label> + <input + id="email" + type="email" + value={formData.email} + onChange={e => handleChange('email', e.target.value)} + /> + {errors.email && <span className="error">{errors.email}</span>} + </div> + + <div> + <label htmlFor="age">Age</label> + <input + id="age" + type="number" + value={formData.age || ''} + onChange={e => handleChange('age', Number(e.target.value))} + /> + {errors.age && <span className="error">{errors.age}</span>} + </div> + + <button type="submit" disabled={isSubmitting}> + {isSubmitting ? 'Submitting...' : 'Submit'} + </button> + </form> + ) +} +``` + +## Example 3: Modal with Portal + +```typescript +import { createPortal } from 'react-dom' +import { useEffect, useRef, useState } from 'react' + +interface ModalProps { + isOpen: boolean + onClose: () => void + children: React.ReactNode + title?: string +} + +const Modal = ({ isOpen, onClose, children, title }: ModalProps) => { + const modalRef = useRef<HTMLDivElement>(null) + + // Close on Escape key + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose() + } + + if (isOpen) { + document.addEventListener('keydown', handleEscape) + // Prevent body scroll + document.body.style.overflow = 'hidden' + } + + return () => { + document.removeEventListener('keydown', handleEscape) + document.body.style.overflow = 'unset' + } + }, [isOpen, onClose]) + + // Close on backdrop click + const handleBackdropClick = (e: React.MouseEvent) => { + if (e.target === modalRef.current) { + onClose() + } + } + + if (!isOpen) return null + + return createPortal( + <div + ref={modalRef} + className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" + onClick={handleBackdropClick} + > + <div className="bg-white rounded-lg p-6 max-w-md w-full mx-4"> + <div className="flex justify-between items-center mb-4"> + {title && <h2 className="text-xl font-bold">{title}</h2>} + <button + onClick={onClose} + className="text-gray-500 hover:text-gray-700" + aria-label="Close modal" + > + ✕ + </button> + </div> + {children} + </div> + </div>, + document.body + ) +} + +// Usage +const App = () => { + const [isOpen, setIsOpen] = useState(false) + + return ( + <> + <button onClick={() => setIsOpen(true)}>Open Modal</button> + <Modal isOpen={isOpen} onClose={() => setIsOpen(false)} title="My Modal"> + <p>Modal content goes here</p> + <button onClick={() => setIsOpen(false)}>Close</button> + </Modal> + </> + ) +} +``` + +## Example 4: Infinite Scroll + +```typescript +import { useState, useEffect, useRef, useCallback } from 'react' + +interface InfiniteScrollProps<T> { + fetchData: (page: number) => Promise<T[]> + renderItem: (item: T, index: number) => React.ReactNode + loader?: React.ReactNode + endMessage?: React.ReactNode +} + +const InfiniteScroll = <T extends { id: string | number },>({ + fetchData, + renderItem, + loader = <div>Loading...</div>, + endMessage = <div>No more items</div> +}: InfiniteScrollProps<T>) => { + const [items, setItems] = useState<T[]>([]) + const [page, setPage] = useState(1) + const [loading, setLoading] = useState(false) + const [hasMore, setHasMore] = useState(true) + const observerRef = useRef<IntersectionObserver | null>(null) + const loadMoreRef = useRef<HTMLDivElement>(null) + + const loadMore = useCallback(async () => { + if (loading || !hasMore) return + + setLoading(true) + try { + const newItems = await fetchData(page) + + if (newItems.length === 0) { + setHasMore(false) + } else { + setItems(prev => [...prev, ...newItems]) + setPage(prev => prev + 1) + } + } catch (error) { + console.error('Failed to load items:', error) + } finally { + setLoading(false) + } + }, [page, loading, hasMore, fetchData]) + + // Set up intersection observer + useEffect(() => { + observerRef.current = new IntersectionObserver( + entries => { + if (entries[0].isIntersecting) { + loadMore() + } + }, + { threshold: 0.1 } + ) + + const currentRef = loadMoreRef.current + if (currentRef) { + observerRef.current.observe(currentRef) + } + + return () => { + if (observerRef.current && currentRef) { + observerRef.current.unobserve(currentRef) + } + } + }, [loadMore]) + + // Initial load + useEffect(() => { + loadMore() + }, []) + + return ( + <div> + {items.map((item, index) => ( + <div key={item.id}> + {renderItem(item, index)} + </div> + ))} + + <div ref={loadMoreRef}> + {loading && loader} + {!loading && !hasMore && endMessage} + </div> + </div> + ) +} + +// Usage +const PostsList = () => { + const fetchPosts = async (page: number) => { + const response = await fetch(`/api/posts?page=${page}`) + return response.json() + } + + return ( + <InfiniteScroll<Post> + fetchData={fetchPosts} + renderItem={(post) => <PostCard post={post} />} + /> + ) +} +``` + +## Example 5: Dark Mode Toggle + +```typescript +import { createContext, useContext, useState, useEffect } from 'react' + +type Theme = 'light' | 'dark' + +interface ThemeContextType { + theme: Theme + toggleTheme: () => void +} + +const ThemeContext = createContext<ThemeContextType | null>(null) + +export const useTheme = () => { + const context = useContext(ThemeContext) + if (!context) { + throw new Error('useTheme must be used within ThemeProvider') + } + return context +} + +export const ThemeProvider = ({ children }: { children: React.ReactNode }) => { + const [theme, setTheme] = useState<Theme>(() => { + // Check localStorage and system preference + const saved = localStorage.getItem('theme') as Theme | null + if (saved) return saved + + if (window.matchMedia('(prefers-color-scheme: dark)').matches) { + return 'dark' + } + + return 'light' + }) + + useEffect(() => { + // Update DOM and localStorage + const root = document.documentElement + root.classList.remove('light', 'dark') + root.classList.add(theme) + localStorage.setItem('theme', theme) + }, [theme]) + + const toggleTheme = () => { + setTheme(prev => prev === 'light' ? 'dark' : 'light') + } + + return ( + <ThemeContext.Provider value={{ theme, toggleTheme }}> + {children} + </ThemeContext.Provider> + ) +} + +// Usage +const ThemeToggle = () => { + const { theme, toggleTheme } = useTheme() + + return ( + <button onClick={toggleTheme} aria-label="Toggle theme"> + {theme === 'light' ? '🌙' : '☀️'} + </button> + ) +} +``` + +## Example 6: Debounced Search + +```typescript +import { useState, useEffect, useMemo } from 'react' + +const useDebounce = <T,>(value: T, delay: number): T => { + const [debouncedValue, setDebouncedValue] = useState(value) + + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedValue(value) + }, delay) + + return () => { + clearTimeout(timer) + } + }, [value, delay]) + + return debouncedValue +} + +const SearchPage = () => { + const [query, setQuery] = useState('') + const [results, setResults] = useState<Product[]>([]) + const [loading, setLoading] = useState(false) + + const debouncedQuery = useDebounce(query, 500) + + useEffect(() => { + if (!debouncedQuery) { + setResults([]) + return + } + + const searchProducts = async () => { + setLoading(true) + try { + const response = await fetch(`/api/search?q=${debouncedQuery}`) + const data = await response.json() + setResults(data) + } catch (error) { + console.error('Search failed:', error) + } finally { + setLoading(false) + } + } + + searchProducts() + }, [debouncedQuery]) + + return ( + <div> + <input + type="search" + value={query} + onChange={e => setQuery(e.target.value)} + placeholder="Search products..." + /> + + {loading && <Spinner />} + + {!loading && results.length > 0 && ( + <div> + {results.map(product => ( + <ProductCard key={product.id} product={product} /> + ))} + </div> + )} + + {!loading && query && results.length === 0 && ( + <p>No results found for "{query}"</p> + )} + </div> + ) +} +``` + +## Example 7: Tabs Component + +```typescript +import { createContext, useContext, useState, useId } from 'react' + +interface TabsContextType { + activeTab: string + setActiveTab: (id: string) => void + tabsId: string +} + +const TabsContext = createContext<TabsContextType | null>(null) + +const useTabs = () => { + const context = useContext(TabsContext) + if (!context) throw new Error('Tabs compound components must be used within Tabs') + return context +} + +interface TabsProps { + children: React.ReactNode + defaultValue: string + className?: string +} + +const Tabs = ({ children, defaultValue, className }: TabsProps) => { + const [activeTab, setActiveTab] = useState(defaultValue) + const tabsId = useId() + + return ( + <TabsContext.Provider value={{ activeTab, setActiveTab, tabsId }}> + <div className={className}> + {children} + </div> + </TabsContext.Provider> + ) +} + +const TabsList = ({ children, className }: { + children: React.ReactNode + className?: string +}) => ( + <div role="tablist" className={className}> + {children} + </div> +) + +interface TabsTriggerProps { + value: string + children: React.ReactNode + className?: string +} + +const TabsTrigger = ({ value, children, className }: TabsTriggerProps) => { + const { activeTab, setActiveTab, tabsId } = useTabs() + const isActive = activeTab === value + + return ( + <button + role="tab" + id={`${tabsId}-tab-${value}`} + aria-controls={`${tabsId}-panel-${value}`} + aria-selected={isActive} + onClick={() => setActiveTab(value)} + className={`${className} ${isActive ? 'active' : ''}`} + > + {children} + </button> + ) +} + +interface TabsContentProps { + value: string + children: React.ReactNode + className?: string +} + +const TabsContent = ({ value, children, className }: TabsContentProps) => { + const { activeTab, tabsId } = useTabs() + + if (activeTab !== value) return null + + return ( + <div + role="tabpanel" + id={`${tabsId}-panel-${value}`} + aria-labelledby={`${tabsId}-tab-${value}`} + className={className} + > + {children} + </div> + ) +} + +// Export compound component +export { Tabs, TabsList, TabsTrigger, TabsContent } + +// Usage +const App = () => ( + <Tabs defaultValue="profile"> + <TabsList> + <TabsTrigger value="profile">Profile</TabsTrigger> + <TabsTrigger value="settings">Settings</TabsTrigger> + <TabsTrigger value="notifications">Notifications</TabsTrigger> + </TabsList> + + <TabsContent value="profile"> + <h2>Profile Content</h2> + </TabsContent> + + <TabsContent value="settings"> + <h2>Settings Content</h2> + </TabsContent> + + <TabsContent value="notifications"> + <h2>Notifications Content</h2> + </TabsContent> + </Tabs> +) +``` + +## Example 8: Error Boundary + +```typescript +import { Component, ErrorInfo, ReactNode } from 'react' + +interface Props { + children: ReactNode + fallback?: (error: Error, reset: () => void) => ReactNode + onError?: (error: Error, errorInfo: ErrorInfo) => void +} + +interface State { + hasError: boolean + error: Error | null +} + +class ErrorBoundary extends Component<Props, State> { + constructor(props: Props) { + super(props) + this.state = { hasError: false, error: null } + } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error } + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('ErrorBoundary caught:', error, errorInfo) + this.props.onError?.(error, errorInfo) + } + + reset = () => { + this.setState({ hasError: false, error: null }) + } + + render() { + if (this.state.hasError && this.state.error) { + if (this.props.fallback) { + return this.props.fallback(this.state.error, this.reset) + } + + return ( + <div className="error-boundary"> + <h2>Something went wrong</h2> + <details> + <summary>Error details</summary> + <pre>{this.state.error.message}</pre> + </details> + <button onClick={this.reset}>Try again</button> + </div> + ) + } + + return this.props.children + } +} + +// Usage +const App = () => ( + <ErrorBoundary + fallback={(error, reset) => ( + <div> + <h1>Oops! Something went wrong</h1> + <p>{error.message}</p> + <button onClick={reset}>Retry</button> + </div> + )} + onError={(error, errorInfo) => { + // Send to error tracking service + console.error('Error logged:', error, errorInfo) + }} + > + <YourApp /> + </ErrorBoundary> +) +``` + +## Example 9: Custom Hook for Local Storage + +```typescript +import { useState, useEffect, useCallback } from 'react' + +const useLocalStorage = <T,>( + key: string, + initialValue: T +): [T, (value: T | ((val: T) => T)) => void, () => void] => { + // Get initial value from localStorage + const [storedValue, setStoredValue] = useState<T>(() => { + try { + const item = window.localStorage.getItem(key) + return item ? JSON.parse(item) : initialValue + } catch (error) { + console.error(`Error loading ${key} from localStorage:`, error) + return initialValue + } + }) + + // Update localStorage when value changes + const setValue = useCallback((value: T | ((val: T) => T)) => { + try { + const valueToStore = value instanceof Function ? value(storedValue) : value + setStoredValue(valueToStore) + window.localStorage.setItem(key, JSON.stringify(valueToStore)) + + // Dispatch storage event for other tabs + window.dispatchEvent(new Event('storage')) + } catch (error) { + console.error(`Error saving ${key} to localStorage:`, error) + } + }, [key, storedValue]) + + // Remove from localStorage + const removeValue = useCallback(() => { + try { + window.localStorage.removeItem(key) + setStoredValue(initialValue) + } catch (error) { + console.error(`Error removing ${key} from localStorage:`, error) + } + }, [key, initialValue]) + + // Listen for changes in other tabs + useEffect(() => { + const handleStorageChange = (e: StorageEvent) => { + if (e.key === key && e.newValue) { + setStoredValue(JSON.parse(e.newValue)) + } + } + + window.addEventListener('storage', handleStorageChange) + return () => window.removeEventListener('storage', handleStorageChange) + }, [key]) + + return [storedValue, setValue, removeValue] +} + +// Usage +const UserPreferences = () => { + const [preferences, setPreferences, clearPreferences] = useLocalStorage('user-prefs', { + theme: 'light', + language: 'en', + notifications: true + }) + + return ( + <div> + <label> + <input + type="checkbox" + checked={preferences.notifications} + onChange={e => setPreferences({ + ...preferences, + notifications: e.target.checked + })} + /> + Enable notifications + </label> + + <button onClick={clearPreferences}> + Reset to defaults + </button> + </div> + ) +} +``` + +## Example 10: Optimistic Updates with useOptimistic + +```typescript +'use client' + +import { useOptimistic } from 'react' +import { likePost, unlikePost } from './actions' + +interface Post { + id: string + content: string + likes: number + isLiked: boolean +} + +const PostCard = ({ post }: { post: Post }) => { + const [optimisticPost, addOptimistic] = useOptimistic( + post, + (currentPost, update: Partial<Post>) => ({ + ...currentPost, + ...update + }) + ) + + const handleLike = async () => { + // Optimistically update UI + addOptimistic({ + likes: optimisticPost.likes + 1, + isLiked: true + }) + + try { + // Send server request + await likePost(post.id) + } catch (error) { + // Server will send correct state via revalidation + console.error('Failed to like post:', error) + } + } + + const handleUnlike = async () => { + addOptimistic({ + likes: optimisticPost.likes - 1, + isLiked: false + }) + + try { + await unlikePost(post.id) + } catch (error) { + console.error('Failed to unlike post:', error) + } + } + + return ( + <div className="post-card"> + <p>{optimisticPost.content}</p> + <button + onClick={optimisticPost.isLiked ? handleUnlike : handleLike} + className={optimisticPost.isLiked ? 'liked' : ''} + > + ❤️ {optimisticPost.likes} + </button> + </div> + ) +} +``` + +## References + +These examples demonstrate: +- Custom hooks for reusable logic +- Form handling with validation +- Portal usage for modals +- Infinite scroll with Intersection Observer +- Context for global state +- Debouncing for performance +- Compound components pattern +- Error boundaries +- LocalStorage integration +- Optimistic updates (React 19) + diff --git a/.claude/skills/react/references/hooks-quick-reference.md b/.claude/skills/react/references/hooks-quick-reference.md new file mode 100644 index 0000000..26e8f59 --- /dev/null +++ b/.claude/skills/react/references/hooks-quick-reference.md @@ -0,0 +1,291 @@ +# React Hooks Quick Reference + +## State Hooks + +### useState +```typescript +const [state, setState] = useState<Type>(initialValue) +const [count, setCount] = useState(0) + +// Functional update +setCount(prev => prev + 1) + +// Lazy initialization +const [state, setState] = useState(() => expensiveComputation()) +``` + +### useReducer +```typescript +type State = { count: number } +type Action = { type: 'increment' } | { type: 'decrement' } + +const reducer = (state: State, action: Action): State => { + switch (action.type) { + case 'increment': return { count: state.count + 1 } + case 'decrement': return { count: state.count - 1 } + } +} + +const [state, dispatch] = useReducer(reducer, { count: 0 }) +dispatch({ type: 'increment' }) +``` + +### useActionState (React 19) +```typescript +const [state, formAction, isPending] = useActionState( + async (previousState, formData: FormData) => { + // Server action + return await processForm(formData) + }, + initialState +) + +<form action={formAction}> + <button disabled={isPending}>Submit</button> +</form> +``` + +## Effect Hooks + +### useEffect +```typescript +useEffect(() => { + // Side effect + const subscription = api.subscribe() + + // Cleanup + return () => subscription.unsubscribe() +}, [dependencies]) +``` + +**Timing**: After render & paint +**Use for**: Data fetching, subscriptions, DOM mutations + +### useLayoutEffect +```typescript +useLayoutEffect(() => { + // Runs before paint + const height = ref.current.offsetHeight + setHeight(height) +}, []) +``` + +**Timing**: After render, before paint +**Use for**: DOM measurements, preventing flicker + +### useInsertionEffect +```typescript +useInsertionEffect(() => { + // Insert styles before any DOM reads + const style = document.createElement('style') + style.textContent = css + document.head.appendChild(style) + return () => document.head.removeChild(style) +}, [css]) +``` + +**Timing**: Before any DOM mutations +**Use for**: CSS-in-JS libraries + +## Performance Hooks + +### useMemo +```typescript +const memoizedValue = useMemo(() => { + return expensiveComputation(a, b) +}, [a, b]) +``` + +**Use for**: Expensive calculations, stable object references + +### useCallback +```typescript +const memoizedCallback = useCallback(() => { + doSomething(a, b) +}, [a, b]) +``` + +**Use for**: Passing callbacks to optimized components + +## Ref Hooks + +### useRef +```typescript +// DOM reference +const ref = useRef<HTMLDivElement>(null) +ref.current?.focus() + +// Mutable value (doesn't trigger re-render) +const countRef = useRef(0) +countRef.current += 1 +``` + +### useImperativeHandle +```typescript +useImperativeHandle(ref, () => ({ + focus: () => inputRef.current?.focus(), + clear: () => inputRef.current && (inputRef.current.value = '') +}), []) +``` + +## Context Hook + +### useContext +```typescript +const value = useContext(MyContext) +``` + +Must be used within a Provider. + +## Transition Hooks + +### useTransition +```typescript +const [isPending, startTransition] = useTransition() + +startTransition(() => { + setState(newValue) // Non-urgent update +}) +``` + +### useDeferredValue +```typescript +const [input, setInput] = useState('') +const deferredInput = useDeferredValue(input) + +// Use deferredInput for expensive operations +const results = useMemo(() => search(deferredInput), [deferredInput]) +``` + +## Optimistic Updates (React 19) + +### useOptimistic +```typescript +const [optimisticState, addOptimistic] = useOptimistic( + actualState, + (currentState, optimisticValue) => { + return [...currentState, optimisticValue] + } +) +``` + +## Other Hooks + +### useId +```typescript +const id = useId() +<label htmlFor={id}>Name</label> +<input id={id} /> +``` + +### useSyncExternalStore +```typescript +const state = useSyncExternalStore( + subscribe, + getSnapshot, + getServerSnapshot +) +``` + +### useDebugValue +```typescript +useDebugValue(isOnline ? 'Online' : 'Offline') +``` + +### use (React 19) +```typescript +// Read context or promise +const value = use(MyContext) +const data = use(fetchPromise) // Must be in Suspense +``` + +## Form Hooks (React DOM) + +### useFormStatus +```typescript +import { useFormStatus } from 'react-dom' + +const { pending, data, method, action } = useFormStatus() +``` + +## Hook Rules + +1. **Only call at top level** - Not in loops, conditions, or nested functions +2. **Only call from React functions** - Components or custom hooks +3. **Custom hooks start with "use"** - Naming convention +4. **Same hooks in same order** - Every render must call same hooks + +## Dependencies Best Practices + +1. **Include all used values** - Variables, props, state from component scope +2. **Use ESLint plugin** - `eslint-plugin-react-hooks` enforces rules +3. **Functions as dependencies** - Wrap with useCallback or define outside component +4. **Object/array dependencies** - Use useMemo for stable references + +## Common Patterns + +### Fetching Data +```typescript +const [data, setData] = useState(null) +const [loading, setLoading] = useState(true) +const [error, setError] = useState(null) + +useEffect(() => { + const controller = new AbortController() + + fetch('/api/data', { signal: controller.signal }) + .then(res => res.json()) + .then(setData) + .catch(setError) + .finally(() => setLoading(false)) + + return () => controller.abort() +}, []) +``` + +### Debouncing +```typescript +const [value, setValue] = useState('') +const [debouncedValue, setDebouncedValue] = useState(value) + +useEffect(() => { + const timer = setTimeout(() => { + setDebouncedValue(value) + }, 500) + + return () => clearTimeout(timer) +}, [value]) +``` + +### Previous Value +```typescript +const usePrevious = <T,>(value: T): T | undefined => { + const ref = useRef<T>() + useEffect(() => { + ref.current = value + }) + return ref.current +} +``` + +### Interval +```typescript +useEffect(() => { + const id = setInterval(() => { + setCount(c => c + 1) + }, 1000) + + return () => clearInterval(id) +}, []) +``` + +### Event Listeners +```typescript +useEffect(() => { + const handleResize = () => setWidth(window.innerWidth) + + window.addEventListener('resize', handleResize) + return () => window.removeEventListener('resize', handleResize) +}, []) +``` + diff --git a/.claude/skills/react/references/performance.md b/.claude/skills/react/references/performance.md new file mode 100644 index 0000000..87c3ba9 --- /dev/null +++ b/.claude/skills/react/references/performance.md @@ -0,0 +1,658 @@ +# React Performance Optimization Guide + +## Overview + +This guide covers performance optimization strategies for React 19 applications. + +## Measurement & Profiling + +### React DevTools Profiler + +Record performance data: +1. Open React DevTools +2. Go to Profiler tab +3. Click record button +4. Interact with app +5. Stop recording +6. Analyze flame graph and ranked chart + +### Profiler Component + +```typescript +import { Profiler } from 'react' + +const App = () => { + const onRender = ( + id: string, + phase: 'mount' | 'update', + actualDuration: number, + baseDuration: number, + startTime: number, + commitTime: number + ) => { + console.log({ + component: id, + phase, + actualDuration, // Time spent rendering this update + baseDuration // Estimated time without memoization + }) + } + + return ( + <Profiler id="App" onRender={onRender}> + <YourApp /> + </Profiler> + ) +} +``` + +### Performance Metrics + +```typescript +// Custom performance tracking +const startTime = performance.now() +// ... do work +const endTime = performance.now() +console.log(`Operation took ${endTime - startTime}ms`) + +// React rendering metrics +import { unstable_trace as trace } from 'react' + +trace('expensive-operation', async () => { + await performExpensiveOperation() +}) +``` + +## Memoization Strategies + +### React.memo + +Prevent unnecessary re-renders: + +```typescript +// Basic memoization +const ExpensiveComponent = memo(({ data }: Props) => { + return <div>{processData(data)}</div> +}) + +// Custom comparison +const MemoizedComponent = memo( + ({ user }: Props) => <UserCard user={user} />, + (prevProps, nextProps) => { + // Return true if props are equal (skip render) + return prevProps.user.id === nextProps.user.id + } +) +``` + +**When to use:** +- Component renders often with same props +- Rendering is expensive +- Component receives complex prop objects + +**When NOT to use:** +- Props change frequently +- Component is already fast +- Premature optimization + +### useMemo + +Memoize computed values: + +```typescript +const SortedList = ({ items, filter }: Props) => { + // Without memoization - runs every render + const filteredItems = items.filter(item => item.type === filter) + const sortedItems = filteredItems.sort((a, b) => a.name.localeCompare(b.name)) + + // With memoization - only runs when dependencies change + const sortedFilteredItems = useMemo(() => { + const filtered = items.filter(item => item.type === filter) + return filtered.sort((a, b) => a.name.localeCompare(b.name)) + }, [items, filter]) + + return ( + <ul> + {sortedFilteredItems.map(item => ( + <li key={item.id}>{item.name}</li> + ))} + </ul> + ) +} +``` + +**When to use:** +- Expensive calculations (sorting, filtering large arrays) +- Creating stable object references +- Computed values used as dependencies + +### useCallback + +Memoize callback functions: + +```typescript +const Parent = () => { + const [count, setCount] = useState(0) + + // Without useCallback - new function every render + const handleClick = () => { + setCount(c => c + 1) + } + + // With useCallback - stable function reference + const handleClickMemo = useCallback(() => { + setCount(c => c + 1) + }, []) + + return <MemoizedChild onClick={handleClickMemo} /> +} + +const MemoizedChild = memo(({ onClick }: Props) => { + return <button onClick={onClick}>Click</button> +}) +``` + +**When to use:** +- Passing callbacks to memoized components +- Callback is used in dependency array +- Callback is expensive to create + +## React Compiler (Automatic Optimization) + +### Enable React Compiler + +React 19 can automatically optimize without manual memoization: + +```javascript +// babel.config.js +module.exports = { + plugins: [ + ['react-compiler', { + compilationMode: 'all', // Optimize all components + }] + ] +} +``` + +### Compilation Modes + +```javascript +{ + compilationMode: 'annotation', // Only components with "use memo" + compilationMode: 'all', // All components (recommended) + compilationMode: 'infer' // Based on component complexity +} +``` + +### Directives + +```typescript +// Force memoization +'use memo' +const Component = ({ data }: Props) => { + return <div>{data}</div> +} + +// Prevent memoization +'use no memo' +const SimpleComponent = ({ text }: Props) => { + return <span>{text}</span> +} +``` + +## State Management Optimization + +### State Colocation + +Keep state as close as possible to where it's used: + +```typescript +// Bad - state too high +const App = () => { + const [showModal, setShowModal] = useState(false) + + return ( + <> + <Header /> + <Content /> + <Modal show={showModal} onClose={() => setShowModal(false)} /> + </> + ) +} + +// Good - state colocated +const App = () => { + return ( + <> + <Header /> + <Content /> + <ModalContainer /> + </> + ) +} + +const ModalContainer = () => { + const [showModal, setShowModal] = useState(false) + + return <Modal show={showModal} onClose={() => setShowModal(false)} /> +} +``` + +### Split Context + +Avoid unnecessary re-renders by splitting context: + +```typescript +// Bad - single context causes all consumers to re-render +const AppContext = createContext({ user, theme, settings }) + +// Good - split into separate contexts +const UserContext = createContext(user) +const ThemeContext = createContext(theme) +const SettingsContext = createContext(settings) +``` + +### Context with useMemo + +```typescript +const ThemeProvider = ({ children }: Props) => { + const [theme, setTheme] = useState('light') + + // Memoize context value to prevent unnecessary re-renders + const value = useMemo(() => ({ + theme, + setTheme + }), [theme]) + + return ( + <ThemeContext.Provider value={value}> + {children} + </ThemeContext.Provider> + ) +} +``` + +## Code Splitting & Lazy Loading + +### React.lazy + +Split components into separate bundles: + +```typescript +import { lazy, Suspense } from 'react' + +// Lazy load components +const Dashboard = lazy(() => import('./Dashboard')) +const Settings = lazy(() => import('./Settings')) +const Profile = lazy(() => import('./Profile')) + +const App = () => { + return ( + <Suspense fallback={<Loading />}> + <Routes> + <Route path="/dashboard" element={<Dashboard />} /> + <Route path="/settings" element={<Settings />} /> + <Route path="/profile" element={<Profile />} /> + </Routes> + </Suspense> + ) +} +``` + +### Route-based Splitting + +```typescript +// App.tsx +const routes = [ + { path: '/', component: lazy(() => import('./pages/Home')) }, + { path: '/about', component: lazy(() => import('./pages/About')) }, + { path: '/products', component: lazy(() => import('./pages/Products')) }, +] + +const App = () => ( + <Suspense fallback={<PageLoader />}> + <Routes> + {routes.map(({ path, component: Component }) => ( + <Route key={path} path={path} element={<Component />} /> + ))} + </Routes> + </Suspense> +) +``` + +### Component-based Splitting + +```typescript +// Split expensive components +const HeavyChart = lazy(() => import('./HeavyChart')) + +const Dashboard = () => { + const [showChart, setShowChart] = useState(false) + + return ( + <> + <button onClick={() => setShowChart(true)}> + Load Chart + </button> + {showChart && ( + <Suspense fallback={<ChartSkeleton />}> + <HeavyChart /> + </Suspense> + )} + </> + ) +} +``` + +## List Rendering Optimization + +### Keys + +Always use stable, unique keys: + +```typescript +// Bad - index as key (causes issues on reorder/insert) +{items.map((item, index) => ( + <Item key={index} data={item} /> +))} + +// Good - unique ID as key +{items.map(item => ( + <Item key={item.id} data={item} /> +))} + +// For static lists without IDs +{items.map(item => ( + <Item key={`${item.name}-${item.category}`} data={item} /> +))} +``` + +### Virtualization + +For long lists, render only visible items: + +```typescript +import { useVirtualizer } from '@tanstack/react-virtual' + +const VirtualList = ({ items }: { items: Item[] }) => { + const parentRef = useRef<HTMLDivElement>(null) + + const virtualizer = useVirtualizer({ + count: items.length, + getScrollElement: () => parentRef.current, + estimateSize: () => 50, // Estimated item height + overscan: 5 // Render 5 extra items above/below viewport + }) + + return ( + <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}> + <div + style={{ + height: `${virtualizer.getTotalSize()}px`, + position: 'relative' + }} + > + {virtualizer.getVirtualItems().map(virtualItem => ( + <div + key={virtualItem.key} + style={{ + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: `${virtualItem.size}px`, + transform: `translateY(${virtualItem.start}px)` + }} + > + <Item data={items[virtualItem.index]} /> + </div> + ))} + </div> + </div> + ) +} +``` + +### Pagination + +```typescript +const PaginatedList = ({ items }: Props) => { + const [page, setPage] = useState(1) + const itemsPerPage = 20 + + const paginatedItems = useMemo(() => { + const start = (page - 1) * itemsPerPage + const end = start + itemsPerPage + return items.slice(start, end) + }, [items, page, itemsPerPage]) + + return ( + <> + {paginatedItems.map(item => ( + <Item key={item.id} data={item} /> + ))} + <Pagination + page={page} + total={Math.ceil(items.length / itemsPerPage)} + onChange={setPage} + /> + </> + ) +} +``` + +## Transitions & Concurrent Features + +### useTransition + +Keep UI responsive during expensive updates: + +```typescript +const SearchPage = () => { + const [query, setQuery] = useState('') + const [results, setResults] = useState([]) + const [isPending, startTransition] = useTransition() + + const handleSearch = (value: string) => { + setQuery(value) // Urgent - update input immediately + + // Non-urgent - can be interrupted + startTransition(() => { + const filtered = expensiveFilter(items, value) + setResults(filtered) + }) + } + + return ( + <> + <input value={query} onChange={e => handleSearch(e.target.value)} /> + {isPending && <Spinner />} + <ResultsList results={results} /> + </> + ) +} +``` + +### useDeferredValue + +Defer non-urgent renders: + +```typescript +const SearchPage = () => { + const [query, setQuery] = useState('') + const deferredQuery = useDeferredValue(query) + + // Input updates immediately + // Results update with deferred value (can be interrupted) + const results = useMemo(() => { + return expensiveFilter(items, deferredQuery) + }, [deferredQuery]) + + return ( + <> + <input value={query} onChange={e => setQuery(e.target.value)} /> + <ResultsList results={results} /> + </> + ) +} +``` + +## Image & Asset Optimization + +### Lazy Load Images + +```typescript +const LazyImage = ({ src, alt }: Props) => { + const [isLoaded, setIsLoaded] = useState(false) + + return ( + <div className="relative"> + {!isLoaded && <ImageSkeleton />} + <img + src={src} + alt={alt} + loading="lazy" // Native lazy loading + onLoad={() => setIsLoaded(true)} + className={isLoaded ? 'opacity-100' : 'opacity-0'} + /> + </div> + ) +} +``` + +### Next.js Image Component + +```typescript +import Image from 'next/image' + +const OptimizedImage = () => ( + <Image + src="/hero.jpg" + alt="Hero" + width={800} + height={600} + priority // Load immediately for above-fold images + placeholder="blur" + blurDataURL="data:image/jpeg;base64,..." + /> +) +``` + +## Bundle Size Optimization + +### Tree Shaking + +Import only what you need: + +```typescript +// Bad - imports entire library +import _ from 'lodash' + +// Good - import only needed functions +import debounce from 'lodash/debounce' +import throttle from 'lodash/throttle' + +// Even better - use native methods when possible +const debounce = (fn, delay) => { + let timeoutId + return (...args) => { + clearTimeout(timeoutId) + timeoutId = setTimeout(() => fn(...args), delay) + } +} +``` + +### Analyze Bundle + +```bash +# Next.js +ANALYZE=true npm run build + +# Create React App +npm install --save-dev webpack-bundle-analyzer +``` + +### Dynamic Imports + +```typescript +// Load library only when needed +const handleExport = async () => { + const { jsPDF } = await import('jspdf') + const doc = new jsPDF() + doc.save('report.pdf') +} +``` + +## Common Performance Pitfalls + +### 1. Inline Object Creation + +```typescript +// Bad - new object every render +<Component style={{ margin: 10 }} /> + +// Good - stable reference +const style = { margin: 10 } +<Component style={style} /> + +// Or use useMemo +const style = useMemo(() => ({ margin: 10 }), []) +``` + +### 2. Inline Functions + +```typescript +// Bad - new function every render (if child is memoized) +<MemoizedChild onClick={() => handleClick(id)} /> + +// Good +const handleClickMemo = useCallback(() => handleClick(id), [id]) +<MemoizedChild onClick={handleClickMemo} /> +``` + +### 3. Spreading Props + +```typescript +// Bad - causes re-renders even when props unchanged +<Component {...props} /> + +// Good - pass only needed props +<Component value={props.value} onChange={props.onChange} /> +``` + +### 4. Large Context + +```typescript +// Bad - everything re-renders on any state change +const AppContext = createContext({ user, theme, cart, settings, ... }) + +// Good - split into focused contexts +const UserContext = createContext(user) +const ThemeContext = createContext(theme) +const CartContext = createContext(cart) +``` + +## Performance Checklist + +- [ ] Measure before optimizing (use Profiler) +- [ ] Use React DevTools to identify slow components +- [ ] Implement code splitting for large routes +- [ ] Lazy load below-the-fold content +- [ ] Virtualize long lists +- [ ] Memoize expensive calculations +- [ ] Split large contexts +- [ ] Colocate state close to usage +- [ ] Use transitions for non-urgent updates +- [ ] Optimize images and assets +- [ ] Analyze and minimize bundle size +- [ ] Remove console.logs in production +- [ ] Use production build for testing +- [ ] Monitor real-world performance metrics + +## References + +- React Performance: https://react.dev/learn/render-and-commit +- React Profiler: https://react.dev/reference/react/Profiler +- React Compiler: https://react.dev/reference/react-compiler +- Web Vitals: https://web.dev/vitals/ + diff --git a/.claude/skills/react/references/server-components.md b/.claude/skills/react/references/server-components.md new file mode 100644 index 0000000..3db4980 --- /dev/null +++ b/.claude/skills/react/references/server-components.md @@ -0,0 +1,656 @@ +# React Server Components & Server Functions + +## Overview + +React Server Components (RSC) allow components to render on the server, improving performance and enabling direct data access. Server Functions allow client components to call server-side functions. + +## Server Components + +### What are Server Components? + +Components that run **only on the server**: +- Can access databases directly +- Zero bundle size (code stays on server) +- Better performance (less JavaScript to client) +- Automatic code splitting + +### Creating Server Components + +```typescript +// app/products/page.tsx +// Server Component by default in App Router + +import { db } from '@/lib/db' + +const ProductsPage = async () => { + // Direct database access + const products = await db.product.findMany({ + where: { active: true }, + include: { category: true } + }) + + return ( + <div> + <h1>Products</h1> + {products.map(product => ( + <ProductCard key={product.id} product={product} /> + ))} + </div> + ) +} + +export default ProductsPage +``` + +### Server Component Rules + +**Can do:** +- Access databases and APIs directly +- Use server-only modules (fs, path, etc.) +- Keep secrets secure (API keys, tokens) +- Reduce client bundle size +- Use async/await at top level + +**Cannot do:** +- Use hooks (useState, useEffect, etc.) +- Use browser APIs (window, document) +- Attach event handlers (onClick, etc.) +- Use Context + +### Mixing Server and Client Components + +```typescript +// Server Component (default) +const Page = async () => { + const data = await fetchData() + + return ( + <div> + <ServerComponent data={data} /> + {/* Client component for interactivity */} + <ClientComponent initialData={data} /> + </div> + ) +} + +// Client Component +'use client' + +import { useState } from 'react' + +const ClientComponent = ({ initialData }) => { + const [count, setCount] = useState(0) + + return ( + <button onClick={() => setCount(c => c + 1)}> + {count} + </button> + ) +} +``` + +### Server Component Patterns + +#### Data Fetching +```typescript +// app/user/[id]/page.tsx +interface PageProps { + params: { id: string } +} + +const UserPage = async ({ params }: PageProps) => { + const user = await db.user.findUnique({ + where: { id: params.id } + }) + + if (!user) { + notFound() // Next.js 404 + } + + return <UserProfile user={user} /> +} +``` + +#### Parallel Data Fetching +```typescript +const DashboardPage = async () => { + // Fetch in parallel + const [user, orders, stats] = await Promise.all([ + fetchUser(), + fetchOrders(), + fetchStats() + ]) + + return ( + <> + <UserHeader user={user} /> + <OrdersList orders={orders} /> + <StatsWidget stats={stats} /> + </> + ) +} +``` + +#### Streaming with Suspense +```typescript +const Page = () => { + return ( + <> + <Header /> + <Suspense fallback={<ProductsSkeleton />}> + <Products /> + </Suspense> + <Suspense fallback={<ReviewsSkeleton />}> + <Reviews /> + </Suspense> + </> + ) +} + +const Products = async () => { + const products = await fetchProducts() // Slow query + return <ProductsList products={products} /> +} +``` + +## Server Functions (Server Actions) + +### What are Server Functions? + +Functions that run on the server but can be called from client components: +- Marked with `'use server'` directive +- Can mutate data +- Integrated with forms +- Type-safe with TypeScript + +### Creating Server Functions + +#### File-level directive +```typescript +// app/actions.ts +'use server' + +import { db } from '@/lib/db' +import { revalidatePath } from 'next/cache' + +export async function createProduct(formData: FormData) { + const name = formData.get('name') as string + const price = Number(formData.get('price')) + + const product = await db.product.create({ + data: { name, price } + }) + + revalidatePath('/products') + return product +} + +export async function deleteProduct(id: string) { + await db.product.delete({ where: { id } }) + revalidatePath('/products') +} +``` + +#### Function-level directive +```typescript +// Inside a Server Component +const MyComponent = async () => { + async function handleSubmit(formData: FormData) { + 'use server' + const email = formData.get('email') as string + await saveEmail(email) + } + + return <form action={handleSubmit}>...</form> +} +``` + +### Using Server Functions + +#### With Forms +```typescript +'use client' + +import { createProduct } from './actions' + +const ProductForm = () => { + return ( + <form action={createProduct}> + <input name="name" required /> + <input name="price" type="number" required /> + <button type="submit">Create</button> + </form> + ) +} +``` + +#### With useActionState +```typescript +'use client' + +import { useActionState } from 'react' +import { createProduct } from './actions' + +type FormState = { + message: string + success: boolean +} | null + +const ProductForm = () => { + const [state, formAction, isPending] = useActionState<FormState>( + async (previousState, formData: FormData) => { + try { + await createProduct(formData) + return { message: 'Product created!', success: true } + } catch (error) { + return { message: 'Failed to create product', success: false } + } + }, + null + ) + + return ( + <form action={formAction}> + <input name="name" required /> + <input name="price" type="number" required /> + <button disabled={isPending}> + {isPending ? 'Creating...' : 'Create'} + </button> + {state?.message && ( + <p className={state.success ? 'text-green-600' : 'text-red-600'}> + {state.message} + </p> + )} + </form> + ) +} +``` + +#### Programmatic Invocation +```typescript +'use client' + +import { deleteProduct } from './actions' + +const DeleteButton = ({ productId }: { productId: string }) => { + const [isPending, setIsPending] = useState(false) + + const handleDelete = async () => { + setIsPending(true) + try { + await deleteProduct(productId) + } catch (error) { + console.error(error) + } finally { + setIsPending(false) + } + } + + return ( + <button onClick={handleDelete} disabled={isPending}> + {isPending ? 'Deleting...' : 'Delete'} + </button> + ) +} +``` + +### Server Function Patterns + +#### Validation with Zod +```typescript +'use server' + +import { z } from 'zod' + +const ProductSchema = z.object({ + name: z.string().min(3), + price: z.number().positive(), + description: z.string().optional() +}) + +export async function createProduct(formData: FormData) { + const rawData = { + name: formData.get('name'), + price: Number(formData.get('price')), + description: formData.get('description') + } + + // Validate + const result = ProductSchema.safeParse(rawData) + if (!result.success) { + return { + success: false, + errors: result.error.flatten().fieldErrors + } + } + + // Create product + const product = await db.product.create({ + data: result.data + }) + + revalidatePath('/products') + return { success: true, product } +} +``` + +#### Authentication Check +```typescript +'use server' + +import { auth } from '@/lib/auth' +import { redirect } from 'next/navigation' + +export async function createOrder(formData: FormData) { + const session = await auth() + + if (!session?.user) { + redirect('/login') + } + + const order = await db.order.create({ + data: { + userId: session.user.id, + // ... other fields + } + }) + + return order +} +``` + +#### Error Handling +```typescript +'use server' + +export async function updateProfile(formData: FormData) { + try { + const userId = await getCurrentUserId() + + const profile = await db.user.update({ + where: { id: userId }, + data: { + name: formData.get('name') as string, + bio: formData.get('bio') as string + } + }) + + revalidatePath('/profile') + return { success: true, profile } + } catch (error) { + console.error('Failed to update profile:', error) + return { + success: false, + error: 'Failed to update profile. Please try again.' + } + } +} +``` + +#### Optimistic Updates +```typescript +'use client' + +import { useOptimistic } from 'react' +import { likePost } from './actions' + +const Post = ({ post }: { post: Post }) => { + const [optimisticLikes, addOptimisticLike] = useOptimistic( + post.likes, + (currentLikes) => currentLikes + 1 + ) + + const handleLike = async () => { + addOptimisticLike(null) + await likePost(post.id) + } + + return ( + <div> + <p>{post.content}</p> + <button onClick={handleLike}> + ❤️ {optimisticLikes} + </button> + </div> + ) +} +``` + +## Data Mutations & Revalidation + +### revalidatePath +Invalidate cached data for a path: + +```typescript +'use server' + +import { revalidatePath } from 'next/cache' + +export async function createPost(formData: FormData) { + await db.post.create({ data: {...} }) + + // Revalidate the posts page + revalidatePath('/posts') + + // Revalidate with layout + revalidatePath('/posts', 'layout') +} +``` + +### revalidateTag +Invalidate cached data by tag: + +```typescript +'use server' + +import { revalidateTag } from 'next/cache' + +export async function updateProduct(id: string, data: ProductData) { + await db.product.update({ where: { id }, data }) + + // Revalidate all queries tagged with 'products' + revalidateTag('products') +} +``` + +### redirect +Redirect after mutation: + +```typescript +'use server' + +import { redirect } from 'next/navigation' + +export async function createPost(formData: FormData) { + const post = await db.post.create({ data: {...} }) + + // Redirect to the new post + redirect(`/posts/${post.id}`) +} +``` + +## Caching with Server Components + +### cache Function +Deduplicate requests within a render: + +```typescript +import { cache } from 'react' + +export const getUser = cache(async (id: string) => { + return await db.user.findUnique({ where: { id } }) +}) + +// Called multiple times but only fetches once per render +const Page = async () => { + const user1 = await getUser('123') + const user2 = await getUser('123') // Uses cached result + + return <div>...</div> +} +``` + +### Next.js fetch Caching +```typescript +// Cached by default +const data = await fetch('https://api.example.com/data') + +// Revalidate every 60 seconds +const data = await fetch('https://api.example.com/data', { + next: { revalidate: 60 } +}) + +// Never cache +const data = await fetch('https://api.example.com/data', { + cache: 'no-store' +}) + +// Tag for revalidation +const data = await fetch('https://api.example.com/data', { + next: { tags: ['products'] } +}) +``` + +## Best Practices + +### 1. Component Placement +- Keep interactive components client-side +- Use server components for data fetching +- Place 'use client' as deep as possible in tree + +### 2. Data Fetching +- Fetch in parallel when possible +- Use Suspense for streaming +- Cache expensive operations + +### 3. Server Functions +- Validate all inputs +- Check authentication/authorization +- Handle errors gracefully +- Return serializable data only + +### 4. Performance +- Minimize client JavaScript +- Use streaming for slow queries +- Implement proper caching +- Optimize database queries + +### 5. Security +- Never expose secrets to client +- Validate server function inputs +- Use environment variables +- Implement rate limiting + +## Common Patterns + +### Layout with Dynamic Data +```typescript +// app/layout.tsx +const RootLayout = async ({ children }: { children: React.ReactNode }) => { + const user = await getCurrentUser() + + return ( + <html> + <body> + <Header user={user} /> + {children} + <Footer /> + </body> + </html> + ) +} +``` + +### Loading States +```typescript +// app/products/loading.tsx +export default function Loading() { + return <ProductsSkeleton /> +} + +// app/products/page.tsx +const ProductsPage = async () => { + const products = await fetchProducts() + return <ProductsList products={products} /> +} +``` + +### Error Boundaries +```typescript +// app/products/error.tsx +'use client' + +export default function Error({ + error, + reset +}: { + error: Error + reset: () => void +}) { + return ( + <div> + <h2>Something went wrong!</h2> + <p>{error.message}</p> + <button onClick={reset}>Try again</button> + </div> + ) +} +``` + +### Search with Server Functions +```typescript +'use client' + +import { searchProducts } from './actions' +import { useDeferredValue, useState, useEffect } from 'react' + +const SearchPage = () => { + const [query, setQuery] = useState('') + const [results, setResults] = useState([]) + const deferredQuery = useDeferredValue(query) + + useEffect(() => { + if (deferredQuery) { + searchProducts(deferredQuery).then(setResults) + } + }, [deferredQuery]) + + return ( + <> + <input + value={query} + onChange={e => setQuery(e.target.value)} + /> + <ResultsList results={results} /> + </> + ) +} +``` + +## Troubleshooting + +### Common Issues + +1. **"Cannot use hooks in Server Component"** + - Add 'use client' directive + - Move state logic to client component + +2. **"Functions cannot be passed to Client Components"** + - Use Server Functions instead + - Pass data, not functions + +3. **Hydration mismatches** + - Ensure server and client render same HTML + - Use useEffect for browser-only code + +4. **Slow initial load** + - Implement Suspense boundaries + - Use streaming rendering + - Optimize database queries + +## References + +- React Server Components: https://react.dev/reference/rsc/server-components +- Server Functions: https://react.dev/reference/rsc/server-functions +- Next.js App Router: https://nextjs.org/docs/app + diff --git a/.claude/skills/skill-creator/LICENSE.txt b/.claude/skills/skill-creator/LICENSE.txt new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/.claude/skills/skill-creator/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/.claude/skills/skill-creator/SKILL.md b/.claude/skills/skill-creator/SKILL.md new file mode 100644 index 0000000..4069935 --- /dev/null +++ b/.claude/skills/skill-creator/SKILL.md @@ -0,0 +1,209 @@ +--- +name: skill-creator +description: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations. +license: Complete terms in LICENSE.txt +--- + +# Skill Creator + +This skill provides guidance for creating effective skills. + +## About Skills + +Skills are modular, self-contained packages that extend Claude's capabilities by providing +specialized knowledge, workflows, and tools. Think of them as "onboarding guides" for specific +domains or tasks—they transform Claude from a general-purpose agent into a specialized agent +equipped with procedural knowledge that no model can fully possess. + +### What Skills Provide + +1. Specialized workflows - Multi-step procedures for specific domains +2. Tool integrations - Instructions for working with specific file formats or APIs +3. Domain expertise - Company-specific knowledge, schemas, business logic +4. Bundled resources - Scripts, references, and assets for complex and repetitive tasks + +### Anatomy of a Skill + +Every skill consists of a required SKILL.md file and optional bundled resources: + +``` +skill-name/ +├── SKILL.md (required) +│ ├── YAML frontmatter metadata (required) +│ │ ├── name: (required) +│ │ └── description: (required) +│ └── Markdown instructions (required) +└── Bundled Resources (optional) + ├── scripts/ - Executable code (Python/Bash/etc.) + ├── references/ - Documentation intended to be loaded into context as needed + └── assets/ - Files used in output (templates, icons, fonts, etc.) +``` + +#### SKILL.md (required) + +**Metadata Quality:** The `name` and `description` in YAML frontmatter determine when Claude will use the skill. Be specific about what the skill does and when to use it. Use the third-person (e.g. "This skill should be used when..." instead of "Use this skill when..."). + +#### Bundled Resources (optional) + +##### Scripts (`scripts/`) + +Executable code (Python/Bash/etc.) for tasks that require deterministic reliability or are repeatedly rewritten. + +- **When to include**: When the same code is being rewritten repeatedly or deterministic reliability is needed +- **Example**: `scripts/rotate_pdf.py` for PDF rotation tasks +- **Benefits**: Token efficient, deterministic, may be executed without loading into context +- **Note**: Scripts may still need to be read by Claude for patching or environment-specific adjustments + +##### References (`references/`) + +Documentation and reference material intended to be loaded as needed into context to inform Claude's process and thinking. + +- **When to include**: For documentation that Claude should reference while working +- **Examples**: `references/finance.md` for financial schemas, `references/mnda.md` for company NDA template, `references/policies.md` for company policies, `references/api_docs.md` for API specifications +- **Use cases**: Database schemas, API documentation, domain knowledge, company policies, detailed workflow guides +- **Benefits**: Keeps SKILL.md lean, loaded only when Claude determines it's needed +- **Best practice**: If files are large (>10k words), include grep search patterns in SKILL.md +- **Avoid duplication**: Information should live in either SKILL.md or references files, not both. Prefer references files for detailed information unless it's truly core to the skill—this keeps SKILL.md lean while making information discoverable without hogging the context window. Keep only essential procedural instructions and workflow guidance in SKILL.md; move detailed reference material, schemas, and examples to references files. + +##### Assets (`assets/`) + +Files not intended to be loaded into context, but rather used within the output Claude produces. + +- **When to include**: When the skill needs files that will be used in the final output +- **Examples**: `assets/logo.png` for brand assets, `assets/slides.pptx` for PowerPoint templates, `assets/frontend-template/` for HTML/React boilerplate, `assets/font.ttf` for typography +- **Use cases**: Templates, images, icons, boilerplate code, fonts, sample documents that get copied or modified +- **Benefits**: Separates output resources from documentation, enables Claude to use files without loading them into context + +### Progressive Disclosure Design Principle + +Skills use a three-level loading system to manage context efficiently: + +1. **Metadata (name + description)** - Always in context (~100 words) +2. **SKILL.md body** - When skill triggers (<5k words) +3. **Bundled resources** - As needed by Claude (Unlimited*) + +*Unlimited because scripts can be executed without reading into context window. + +## Skill Creation Process + +To create a skill, follow the "Skill Creation Process" in order, skipping steps only if there is a clear reason why they are not applicable. + +### Step 1: Understanding the Skill with Concrete Examples + +Skip this step only when the skill's usage patterns are already clearly understood. It remains valuable even when working with an existing skill. + +To create an effective skill, clearly understand concrete examples of how the skill will be used. This understanding can come from either direct user examples or generated examples that are validated with user feedback. + +For example, when building an image-editor skill, relevant questions include: + +- "What functionality should the image-editor skill support? Editing, rotating, anything else?" +- "Can you give some examples of how this skill would be used?" +- "I can imagine users asking for things like 'Remove the red-eye from this image' or 'Rotate this image'. Are there other ways you imagine this skill being used?" +- "What would a user say that should trigger this skill?" + +To avoid overwhelming users, avoid asking too many questions in a single message. Start with the most important questions and follow up as needed for better effectiveness. + +Conclude this step when there is a clear sense of the functionality the skill should support. + +### Step 2: Planning the Reusable Skill Contents + +To turn concrete examples into an effective skill, analyze each example by: + +1. Considering how to execute on the example from scratch +2. Identifying what scripts, references, and assets would be helpful when executing these workflows repeatedly + +Example: When building a `pdf-editor` skill to handle queries like "Help me rotate this PDF," the analysis shows: + +1. Rotating a PDF requires re-writing the same code each time +2. A `scripts/rotate_pdf.py` script would be helpful to store in the skill + +Example: When designing a `frontend-webapp-builder` skill for queries like "Build me a todo app" or "Build me a dashboard to track my steps," the analysis shows: + +1. Writing a frontend webapp requires the same boilerplate HTML/React each time +2. An `assets/hello-world/` template containing the boilerplate HTML/React project files would be helpful to store in the skill + +Example: When building a `big-query` skill to handle queries like "How many users have logged in today?" the analysis shows: + +1. Querying BigQuery requires re-discovering the table schemas and relationships each time +2. A `references/schema.md` file documenting the table schemas would be helpful to store in the skill + +To establish the skill's contents, analyze each concrete example to create a list of the reusable resources to include: scripts, references, and assets. + +### Step 3: Initializing the Skill + +At this point, it is time to actually create the skill. + +Skip this step only if the skill being developed already exists, and iteration or packaging is needed. In this case, continue to the next step. + +When creating a new skill from scratch, always run the `init_skill.py` script. The script conveniently generates a new template skill directory that automatically includes everything a skill requires, making the skill creation process much more efficient and reliable. + +Usage: + +```bash +scripts/init_skill.py <skill-name> --path <output-directory> +``` + +The script: + +- Creates the skill directory at the specified path +- Generates a SKILL.md template with proper frontmatter and TODO placeholders +- Creates example resource directories: `scripts/`, `references/`, and `assets/` +- Adds example files in each directory that can be customized or deleted + +After initialization, customize or remove the generated SKILL.md and example files as needed. + +### Step 4: Edit the Skill + +When editing the (newly-generated or existing) skill, remember that the skill is being created for another instance of Claude to use. Focus on including information that would be beneficial and non-obvious to Claude. Consider what procedural knowledge, domain-specific details, or reusable assets would help another Claude instance execute these tasks more effectively. + +#### Start with Reusable Skill Contents + +To begin implementation, start with the reusable resources identified above: `scripts/`, `references/`, and `assets/` files. Note that this step may require user input. For example, when implementing a `brand-guidelines` skill, the user may need to provide brand assets or templates to store in `assets/`, or documentation to store in `references/`. + +Also, delete any example files and directories not needed for the skill. The initialization script creates example files in `scripts/`, `references/`, and `assets/` to demonstrate structure, but most skills won't need all of them. + +#### Update SKILL.md + +**Writing Style:** Write the entire skill using **imperative/infinitive form** (verb-first instructions), not second person. Use objective, instructional language (e.g., "To accomplish X, do Y" rather than "You should do X" or "If you need to do X"). This maintains consistency and clarity for AI consumption. + +To complete SKILL.md, answer the following questions: + +1. What is the purpose of the skill, in a few sentences? +2. When should the skill be used? +3. In practice, how should Claude use the skill? All reusable skill contents developed above should be referenced so that Claude knows how to use them. + +### Step 5: Packaging a Skill + +Once the skill is ready, it should be packaged into a distributable zip file that gets shared with the user. The packaging process automatically validates the skill first to ensure it meets all requirements: + +```bash +scripts/package_skill.py <path/to/skill-folder> +``` + +Optional output directory specification: + +```bash +scripts/package_skill.py <path/to/skill-folder> ./dist +``` + +The packaging script will: + +1. **Validate** the skill automatically, checking: + - YAML frontmatter format and required fields + - Skill naming conventions and directory structure + - Description completeness and quality + - File organization and resource references + +2. **Package** the skill if validation passes, creating a zip file named after the skill (e.g., `my-skill.zip`) that includes all files and maintains the proper directory structure for distribution. + +If validation fails, the script will report the errors and exit without creating a package. Fix any validation errors and run the packaging command again. + +### Step 6: Iterate + +After testing the skill, users may request improvements. Often this happens right after using the skill, with fresh context of how the skill performed. + +**Iteration workflow:** +1. Use the skill on real tasks +2. Notice struggles or inefficiencies +3. Identify how SKILL.md or bundled resources should be updated +4. Implement changes and test again diff --git a/.claude/skills/skill-creator/scripts/init_skill.py b/.claude/skills/skill-creator/scripts/init_skill.py new file mode 100644 index 0000000..329ad4e --- /dev/null +++ b/.claude/skills/skill-creator/scripts/init_skill.py @@ -0,0 +1,303 @@ +#!/usr/bin/env python3 +""" +Skill Initializer - Creates a new skill from template + +Usage: + init_skill.py <skill-name> --path <path> + +Examples: + init_skill.py my-new-skill --path skills/public + init_skill.py my-api-helper --path skills/private + init_skill.py custom-skill --path /custom/location +""" + +import sys +from pathlib import Path + + +SKILL_TEMPLATE = """--- +name: {skill_name} +description: [TODO: Complete and informative explanation of what the skill does and when to use it. Include WHEN to use this skill - specific scenarios, file types, or tasks that trigger it.] +--- + +# {skill_title} + +## Overview + +[TODO: 1-2 sentences explaining what this skill enables] + +## Structuring This Skill + +[TODO: Choose the structure that best fits this skill's purpose. Common patterns: + +**1. Workflow-Based** (best for sequential processes) +- Works well when there are clear step-by-step procedures +- Example: DOCX skill with "Workflow Decision Tree" → "Reading" → "Creating" → "Editing" +- Structure: ## Overview → ## Workflow Decision Tree → ## Step 1 → ## Step 2... + +**2. Task-Based** (best for tool collections) +- Works well when the skill offers different operations/capabilities +- Example: PDF skill with "Quick Start" → "Merge PDFs" → "Split PDFs" → "Extract Text" +- Structure: ## Overview → ## Quick Start → ## Task Category 1 → ## Task Category 2... + +**3. Reference/Guidelines** (best for standards or specifications) +- Works well for brand guidelines, coding standards, or requirements +- Example: Brand styling with "Brand Guidelines" → "Colors" → "Typography" → "Features" +- Structure: ## Overview → ## Guidelines → ## Specifications → ## Usage... + +**4. Capabilities-Based** (best for integrated systems) +- Works well when the skill provides multiple interrelated features +- Example: Product Management with "Core Capabilities" → numbered capability list +- Structure: ## Overview → ## Core Capabilities → ### 1. Feature → ### 2. Feature... + +Patterns can be mixed and matched as needed. Most skills combine patterns (e.g., start with task-based, add workflow for complex operations). + +Delete this entire "Structuring This Skill" section when done - it's just guidance.] + +## [TODO: Replace with the first main section based on chosen structure] + +[TODO: Add content here. See examples in existing skills: +- Code samples for technical skills +- Decision trees for complex workflows +- Concrete examples with realistic user requests +- References to scripts/templates/references as needed] + +## Resources + +This skill includes example resource directories that demonstrate how to organize different types of bundled resources: + +### scripts/ +Executable code (Python/Bash/etc.) that can be run directly to perform specific operations. + +**Examples from other skills:** +- PDF skill: `fill_fillable_fields.py`, `extract_form_field_info.py` - utilities for PDF manipulation +- DOCX skill: `document.py`, `utilities.py` - Python modules for document processing + +**Appropriate for:** Python scripts, shell scripts, or any executable code that performs automation, data processing, or specific operations. + +**Note:** Scripts may be executed without loading into context, but can still be read by Claude for patching or environment adjustments. + +### references/ +Documentation and reference material intended to be loaded into context to inform Claude's process and thinking. + +**Examples from other skills:** +- Product management: `communication.md`, `context_building.md` - detailed workflow guides +- BigQuery: API reference documentation and query examples +- Finance: Schema documentation, company policies + +**Appropriate for:** In-depth documentation, API references, database schemas, comprehensive guides, or any detailed information that Claude should reference while working. + +### assets/ +Files not intended to be loaded into context, but rather used within the output Claude produces. + +**Examples from other skills:** +- Brand styling: PowerPoint template files (.pptx), logo files +- Frontend builder: HTML/React boilerplate project directories +- Typography: Font files (.ttf, .woff2) + +**Appropriate for:** Templates, boilerplate code, document templates, images, icons, fonts, or any files meant to be copied or used in the final output. + +--- + +**Any unneeded directories can be deleted.** Not every skill requires all three types of resources. +""" + +EXAMPLE_SCRIPT = '''#!/usr/bin/env python3 +""" +Example helper script for {skill_name} + +This is a placeholder script that can be executed directly. +Replace with actual implementation or delete if not needed. + +Example real scripts from other skills: +- pdf/scripts/fill_fillable_fields.py - Fills PDF form fields +- pdf/scripts/convert_pdf_to_images.py - Converts PDF pages to images +""" + +def main(): + print("This is an example script for {skill_name}") + # TODO: Add actual script logic here + # This could be data processing, file conversion, API calls, etc. + +if __name__ == "__main__": + main() +''' + +EXAMPLE_REFERENCE = """# Reference Documentation for {skill_title} + +This is a placeholder for detailed reference documentation. +Replace with actual reference content or delete if not needed. + +Example real reference docs from other skills: +- product-management/references/communication.md - Comprehensive guide for status updates +- product-management/references/context_building.md - Deep-dive on gathering context +- bigquery/references/ - API references and query examples + +## When Reference Docs Are Useful + +Reference docs are ideal for: +- Comprehensive API documentation +- Detailed workflow guides +- Complex multi-step processes +- Information too lengthy for main SKILL.md +- Content that's only needed for specific use cases + +## Structure Suggestions + +### API Reference Example +- Overview +- Authentication +- Endpoints with examples +- Error codes +- Rate limits + +### Workflow Guide Example +- Prerequisites +- Step-by-step instructions +- Common patterns +- Troubleshooting +- Best practices +""" + +EXAMPLE_ASSET = """# Example Asset File + +This placeholder represents where asset files would be stored. +Replace with actual asset files (templates, images, fonts, etc.) or delete if not needed. + +Asset files are NOT intended to be loaded into context, but rather used within +the output Claude produces. + +Example asset files from other skills: +- Brand guidelines: logo.png, slides_template.pptx +- Frontend builder: hello-world/ directory with HTML/React boilerplate +- Typography: custom-font.ttf, font-family.woff2 +- Data: sample_data.csv, test_dataset.json + +## Common Asset Types + +- Templates: .pptx, .docx, boilerplate directories +- Images: .png, .jpg, .svg, .gif +- Fonts: .ttf, .otf, .woff, .woff2 +- Boilerplate code: Project directories, starter files +- Icons: .ico, .svg +- Data files: .csv, .json, .xml, .yaml + +Note: This is a text placeholder. Actual assets can be any file type. +""" + + +def title_case_skill_name(skill_name): + """Convert hyphenated skill name to Title Case for display.""" + return ' '.join(word.capitalize() for word in skill_name.split('-')) + + +def init_skill(skill_name, path): + """ + Initialize a new skill directory with template SKILL.md. + + Args: + skill_name: Name of the skill + path: Path where the skill directory should be created + + Returns: + Path to created skill directory, or None if error + """ + # Determine skill directory path + skill_dir = Path(path).resolve() / skill_name + + # Check if directory already exists + if skill_dir.exists(): + print(f"❌ Error: Skill directory already exists: {skill_dir}") + return None + + # Create skill directory + try: + skill_dir.mkdir(parents=True, exist_ok=False) + print(f"✅ Created skill directory: {skill_dir}") + except Exception as e: + print(f"❌ Error creating directory: {e}") + return None + + # Create SKILL.md from template + skill_title = title_case_skill_name(skill_name) + skill_content = SKILL_TEMPLATE.format( + skill_name=skill_name, + skill_title=skill_title + ) + + skill_md_path = skill_dir / 'SKILL.md' + try: + skill_md_path.write_text(skill_content) + print("✅ Created SKILL.md") + except Exception as e: + print(f"❌ Error creating SKILL.md: {e}") + return None + + # Create resource directories with example files + try: + # Create scripts/ directory with example script + scripts_dir = skill_dir / 'scripts' + scripts_dir.mkdir(exist_ok=True) + example_script = scripts_dir / 'example.py' + example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name)) + example_script.chmod(0o755) + print("✅ Created scripts/example.py") + + # Create references/ directory with example reference doc + references_dir = skill_dir / 'references' + references_dir.mkdir(exist_ok=True) + example_reference = references_dir / 'api_reference.md' + example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title)) + print("✅ Created references/api_reference.md") + + # Create assets/ directory with example asset placeholder + assets_dir = skill_dir / 'assets' + assets_dir.mkdir(exist_ok=True) + example_asset = assets_dir / 'example_asset.txt' + example_asset.write_text(EXAMPLE_ASSET) + print("✅ Created assets/example_asset.txt") + except Exception as e: + print(f"❌ Error creating resource directories: {e}") + return None + + # Print next steps + print(f"\n✅ Skill '{skill_name}' initialized successfully at {skill_dir}") + print("\nNext steps:") + print("1. Edit SKILL.md to complete the TODO items and update the description") + print("2. Customize or delete the example files in scripts/, references/, and assets/") + print("3. Run the validator when ready to check the skill structure") + + return skill_dir + + +def main(): + if len(sys.argv) < 4 or sys.argv[2] != '--path': + print("Usage: init_skill.py <skill-name> --path <path>") + print("\nSkill name requirements:") + print(" - Hyphen-case identifier (e.g., 'data-analyzer')") + print(" - Lowercase letters, digits, and hyphens only") + print(" - Max 40 characters") + print(" - Must match directory name exactly") + print("\nExamples:") + print(" init_skill.py my-new-skill --path skills/public") + print(" init_skill.py my-api-helper --path skills/private") + print(" init_skill.py custom-skill --path /custom/location") + sys.exit(1) + + skill_name = sys.argv[1] + path = sys.argv[3] + + print(f"🚀 Initializing skill: {skill_name}") + print(f" Location: {path}") + print() + + result = init_skill(skill_name, path) + + if result: + sys.exit(0) + else: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/skill-creator/scripts/package_skill.py b/.claude/skills/skill-creator/scripts/package_skill.py new file mode 100644 index 0000000..3ee8e8e --- /dev/null +++ b/.claude/skills/skill-creator/scripts/package_skill.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +""" +Skill Packager - Creates a distributable zip file of a skill folder + +Usage: + python utils/package_skill.py <path/to/skill-folder> [output-directory] + +Example: + python utils/package_skill.py skills/public/my-skill + python utils/package_skill.py skills/public/my-skill ./dist +""" + +import sys +import zipfile +from pathlib import Path +from quick_validate import validate_skill + + +def package_skill(skill_path, output_dir=None): + """ + Package a skill folder into a zip file. + + Args: + skill_path: Path to the skill folder + output_dir: Optional output directory for the zip file (defaults to current directory) + + Returns: + Path to the created zip file, or None if error + """ + skill_path = Path(skill_path).resolve() + + # Validate skill folder exists + if not skill_path.exists(): + print(f"❌ Error: Skill folder not found: {skill_path}") + return None + + if not skill_path.is_dir(): + print(f"❌ Error: Path is not a directory: {skill_path}") + return None + + # Validate SKILL.md exists + skill_md = skill_path / "SKILL.md" + if not skill_md.exists(): + print(f"❌ Error: SKILL.md not found in {skill_path}") + return None + + # Run validation before packaging + print("🔍 Validating skill...") + valid, message = validate_skill(skill_path) + if not valid: + print(f"❌ Validation failed: {message}") + print(" Please fix the validation errors before packaging.") + return None + print(f"✅ {message}\n") + + # Determine output location + skill_name = skill_path.name + if output_dir: + output_path = Path(output_dir).resolve() + output_path.mkdir(parents=True, exist_ok=True) + else: + output_path = Path.cwd() + + zip_filename = output_path / f"{skill_name}.zip" + + # Create the zip file + try: + with zipfile.ZipFile(zip_filename, 'w', zipfile.ZIP_DEFLATED) as zipf: + # Walk through the skill directory + for file_path in skill_path.rglob('*'): + if file_path.is_file(): + # Calculate the relative path within the zip + arcname = file_path.relative_to(skill_path.parent) + zipf.write(file_path, arcname) + print(f" Added: {arcname}") + + print(f"\n✅ Successfully packaged skill to: {zip_filename}") + return zip_filename + + except Exception as e: + print(f"❌ Error creating zip file: {e}") + return None + + +def main(): + if len(sys.argv) < 2: + print("Usage: python utils/package_skill.py <path/to/skill-folder> [output-directory]") + print("\nExample:") + print(" python utils/package_skill.py skills/public/my-skill") + print(" python utils/package_skill.py skills/public/my-skill ./dist") + sys.exit(1) + + skill_path = sys.argv[1] + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + print(f"📦 Packaging skill: {skill_path}") + if output_dir: + print(f" Output directory: {output_dir}") + print() + + result = package_skill(skill_path, output_dir) + + if result: + sys.exit(0) + else: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/skill-creator/scripts/quick_validate.py b/.claude/skills/skill-creator/scripts/quick_validate.py new file mode 100644 index 0000000..6fa6c63 --- /dev/null +++ b/.claude/skills/skill-creator/scripts/quick_validate.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +""" +Quick validation script for skills - minimal version +""" + +import sys +import os +import re +from pathlib import Path + +def validate_skill(skill_path): + """Basic validation of a skill""" + skill_path = Path(skill_path) + + # Check SKILL.md exists + skill_md = skill_path / 'SKILL.md' + if not skill_md.exists(): + return False, "SKILL.md not found" + + # Read and validate frontmatter + content = skill_md.read_text() + if not content.startswith('---'): + return False, "No YAML frontmatter found" + + # Extract frontmatter + match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL) + if not match: + return False, "Invalid frontmatter format" + + frontmatter = match.group(1) + + # Check required fields + if 'name:' not in frontmatter: + return False, "Missing 'name' in frontmatter" + if 'description:' not in frontmatter: + return False, "Missing 'description' in frontmatter" + + # Extract name for validation + name_match = re.search(r'name:\s*(.+)', frontmatter) + if name_match: + name = name_match.group(1).strip() + # Check naming convention (hyphen-case: lowercase with hyphens) + if not re.match(r'^[a-z0-9-]+$', name): + return False, f"Name '{name}' should be hyphen-case (lowercase letters, digits, and hyphens only)" + if name.startswith('-') or name.endswith('-') or '--' in name: + return False, f"Name '{name}' cannot start/end with hyphen or contain consecutive hyphens" + + # Extract and validate description + desc_match = re.search(r'description:\s*(.+)', frontmatter) + if desc_match: + description = desc_match.group(1).strip() + # Check for angle brackets + if '<' in description or '>' in description: + return False, "Description cannot contain angle brackets (< or >)" + + return True, "Skill is valid!" + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python quick_validate.py <skill_directory>") + sys.exit(1) + + valid, message = validate_skill(sys.argv[1]) + print(message) + sys.exit(0 if valid else 1) \ No newline at end of file diff --git a/.claude/skills/svelte/SKILL.md b/.claude/skills/svelte/SKILL.md new file mode 100644 index 0000000..0d87233 --- /dev/null +++ b/.claude/skills/svelte/SKILL.md @@ -0,0 +1,1004 @@ +--- +name: svelte +description: This skill should be used when working with Svelte 3/4, including components, reactivity, stores, lifecycle, and component communication. Provides comprehensive knowledge of Svelte patterns, best practices, and reactive programming concepts. +--- + +# Svelte 3/4 Skill + +This skill provides comprehensive knowledge and patterns for working with Svelte effectively in modern web applications. + +## When to Use This Skill + +Use this skill when: +- Building Svelte applications and components +- Working with Svelte reactivity and stores +- Implementing component communication patterns +- Managing component lifecycle +- Optimizing Svelte application performance +- Troubleshooting Svelte-specific issues +- Working with Svelte transitions and animations +- Integrating with external libraries + +## Core Concepts + +### Svelte Overview + +Svelte is a compiler-based frontend framework that: +- **Compiles to vanilla JavaScript** - No runtime library shipped to browser +- **Reactive by default** - Variables are reactive, assignments trigger updates +- **Component-based** - Single-file components with `.svelte` extension +- **CSS scoping** - Styles are scoped to components by default +- **Built-in transitions** - Animation primitives included +- **Two-way binding** - Simple data binding with `bind:` + +### Component Structure + +Svelte components have three sections: + +```svelte +<script> + // JavaScript logic + let count = 0; + + function increment() { + count += 1; + } +</script> + +<style> + /* Scoped CSS */ + button { + background: #ff3e00; + color: white; + } +</style> + +<!-- HTML template --> +<button on:click={increment}> + Clicked {count} times +</button> +``` + +## Reactivity + +### Reactive Declarations + +Use `$:` for reactive statements and computed values: + +```svelte +<script> + let count = 0; + + // Reactive declaration - recomputes when count changes + $: doubled = count * 2; + + // Reactive statement - runs when dependencies change + $: console.log(`count is ${count}`); + + // Reactive block + $: { + console.log(`count is ${count}`); + console.log(`doubled is ${doubled}`); + } + + // Reactive if statement + $: if (count >= 10) { + alert('count is high!'); + count = 0; + } +</script> +``` + +### Reactive Assignments + +Reactivity is triggered by assignments: + +```svelte +<script> + let numbers = [1, 2, 3]; + + function addNumber() { + // This triggers reactivity + numbers = [...numbers, numbers.length + 1]; + + // This also works + numbers.push(numbers.length + 1); + numbers = numbers; + } + + let obj = { foo: 'bar' }; + + function updateObject() { + // Reassignment triggers update + obj.foo = 'baz'; + obj = obj; + + // Or use spread + obj = { ...obj, foo: 'baz' }; + } +</script> +``` + +**Key Points:** +- Array methods like `push`, `pop` need reassignment to trigger updates +- Object property changes need reassignment +- Use spread operator for immutable updates + +## Props + +### Declaring Props + +```svelte +<script> + // Basic prop + export let name; + + // Prop with default value + export let greeting = 'Hello'; + + // Readonly prop (convention) + export let readonly count = 0; +</script> + +<p>{greeting}, {name}!</p> +``` + +### Spread Props + +```svelte +<script> + // Forward all props to child + export let info = {}; +</script> + +<Child {...info} /> + +<!-- Or forward unknown props --> +<Child {...$$restProps} /> +``` + +### Prop Types with JSDoc + +```svelte +<script> + /** + * @type {string} + */ + export let name; + + /** + * @type {'primary' | 'secondary'} + */ + export let variant = 'primary'; + + /** + * @type {(event: CustomEvent) => void} + */ + export let onSelect; +</script> +``` + +## Events + +### DOM Events + +```svelte +<script> + function handleClick(event) { + console.log('clicked', event.target); + } +</script> + +<!-- Basic event --> +<button on:click={handleClick}>Click me</button> + +<!-- Inline handler --> +<button on:click={() => console.log('clicked')}>Click</button> + +<!-- Event modifiers --> +<button on:click|preventDefault={handleClick}>Submit</button> +<button on:click|stopPropagation|once={handleClick}>Once</button> + +<!-- Available modifiers --> +<!-- preventDefault, stopPropagation, passive, nonpassive, capture, once, self, trusted --> +``` + +### Component Events + +Dispatch custom events from components: + +```svelte +<!-- Child.svelte --> +<script> + import { createEventDispatcher } from 'svelte'; + + const dispatch = createEventDispatcher(); + + function handleSelect(item) { + dispatch('select', { item }); + } +</script> + +<button on:click={() => handleSelect('foo')}> + Select +</button> +``` + +```svelte +<!-- Parent.svelte --> +<script> + function handleSelect(event) { + console.log('selected:', event.detail.item); + } +</script> + +<Child on:select={handleSelect} /> +``` + +### Event Forwarding + +```svelte +<!-- Forward all events of a type --> +<button on:click>Click me</button> + +<!-- The parent can now listen --> +<Child on:click={handleClick} /> +``` + +## Bindings + +### Two-Way Binding + +```svelte +<script> + let name = ''; + let agreed = false; + let selected = 'a'; + let quantity = 1; +</script> + +<!-- Text input --> +<input bind:value={name} /> + +<!-- Checkbox --> +<input type="checkbox" bind:checked={agreed} /> + +<!-- Radio buttons --> +<input type="radio" bind:group={selected} value="a" /> A +<input type="radio" bind:group={selected} value="b" /> B + +<!-- Number input --> +<input type="number" bind:value={quantity} /> + +<!-- Select --> +<select bind:value={selected}> + <option value="a">A</option> + <option value="b">B</option> +</select> + +<!-- Textarea --> +<textarea bind:value={content}></textarea> +``` + +### Component Bindings + +```svelte +<!-- Bind to component props --> +<Child bind:value={parentValue} /> + +<!-- Bind to component instance --> +<Child bind:this={childComponent} /> +``` + +### Element Bindings + +```svelte +<script> + let inputElement; + let divWidth; + let divHeight; +</script> + +<!-- DOM element reference --> +<input bind:this={inputElement} /> + +<!-- Dimension bindings (read-only) --> +<div bind:clientWidth={divWidth} bind:clientHeight={divHeight}> + {divWidth} x {divHeight} +</div> +``` + +## Stores + +### Writable Stores + +```javascript +// stores.js +import { writable } from 'svelte/store'; + +export const count = writable(0); + +// With custom methods +function createCounter() { + const { subscribe, set, update } = writable(0); + + return { + subscribe, + increment: () => update(n => n + 1), + decrement: () => update(n => n - 1), + reset: () => set(0) + }; +} + +export const counter = createCounter(); +``` + +```svelte +<script> + import { count, counter } from './stores.js'; + + // Manual subscription + let countValue; + const unsubscribe = count.subscribe(value => { + countValue = value; + }); + + // Auto-subscription with $ prefix (recommended) + // Automatically subscribes and unsubscribes +</script> + +<p>Count: {$count}</p> +<button on:click={() => $count += 1}>Increment</button> + +<p>Counter: {$counter}</p> +<button on:click={counter.increment}>Increment</button> +``` + +### Readable Stores + +```javascript +import { readable } from 'svelte/store'; + +// Time store that updates every second +export const time = readable(new Date(), function start(set) { + const interval = setInterval(() => { + set(new Date()); + }, 1000); + + return function stop() { + clearInterval(interval); + }; +}); +``` + +### Derived Stores + +```javascript +import { derived } from 'svelte/store'; +import { time } from './stores.js'; + +export const elapsed = derived( + time, + $time => Math.round(($time - start) / 1000) +); + +// Derived from multiple stores +export const combined = derived( + [storeA, storeB], + ([$a, $b]) => $a + $b +); + +// Async derived +export const asyncDerived = derived( + source, + ($source, set) => { + fetch(`/api/${$source}`) + .then(r => r.json()) + .then(set); + }, + 'loading...' // initial value +); +``` + +### Store Contract + +Any object with a `subscribe` method is a store: + +```javascript +// Custom store implementation +function createCustomStore(initial) { + let value = initial; + const subscribers = new Set(); + + return { + subscribe(fn) { + subscribers.add(fn); + fn(value); + return () => subscribers.delete(fn); + }, + set(newValue) { + value = newValue; + subscribers.forEach(fn => fn(value)); + } + }; +} +``` + +## Lifecycle + +### Lifecycle Functions + +```svelte +<script> + import { onMount, onDestroy, beforeUpdate, afterUpdate, tick } from 'svelte'; + + // Called when component is mounted to DOM + onMount(() => { + console.log('mounted'); + + // Return cleanup function (like onDestroy) + return () => { + console.log('cleanup on unmount'); + }; + }); + + // Called before component is destroyed + onDestroy(() => { + console.log('destroying'); + }); + + // Called before DOM updates + beforeUpdate(() => { + console.log('about to update'); + }); + + // Called after DOM updates + afterUpdate(() => { + console.log('updated'); + }); + + // Wait for next DOM update + async function handleClick() { + count += 1; + await tick(); + // DOM is now updated + } +</script> +``` + +**Key Points:** +- `onMount` runs only in browser, not during SSR +- `onMount` callbacks must be called during component initialization +- Use `tick()` to wait for pending state changes to apply to DOM + +## Logic Blocks + +### If Blocks + +```svelte +{#if condition} + <p>Condition is true</p> +{:else if otherCondition} + <p>Other condition is true</p> +{:else} + <p>Neither condition is true</p> +{/if} +``` + +### Each Blocks + +```svelte +{#each items as item} + <li>{item.name}</li> +{/each} + +<!-- With index --> +{#each items as item, index} + <li>{index}: {item.name}</li> +{/each} + +<!-- With key for animations/reordering --> +{#each items as item (item.id)} + <li>{item.name}</li> +{/each} + +<!-- Destructuring --> +{#each items as { id, name }} + <li>{id}: {name}</li> +{/each} + +<!-- Empty state --> +{#each items as item} + <li>{item.name}</li> +{:else} + <p>No items</p> +{/each} +``` + +### Await Blocks + +```svelte +{#await promise} + <p>Loading...</p> +{:then value} + <p>The value is {value}</p> +{:catch error} + <p>Error: {error.message}</p> +{/await} + +<!-- Short form (no loading state) --> +{#await promise then value} + <p>The value is {value}</p> +{/await} +``` + +### Key Blocks + +Force component recreation when value changes: + +```svelte +{#key value} + <Component /> +{/key} +``` + +## Slots + +### Basic Slots + +```svelte +<!-- Card.svelte --> +<div class="card"> + <slot> + <!-- Fallback content --> + <p>No content provided</p> + </slot> +</div> +``` + +```svelte +<Card> + <p>Card content</p> +</Card> +``` + +### Named Slots + +```svelte +<!-- Layout.svelte --> +<div class="layout"> + <header> + <slot name="header"></slot> + </header> + <main> + <slot></slot> + </main> + <footer> + <slot name="footer"></slot> + </footer> +</div> +``` + +```svelte +<Layout> + <h1 slot="header">Page Title</h1> + <p>Main content</p> + <p slot="footer">Footer content</p> +</Layout> +``` + +### Slot Props + +```svelte +<!-- List.svelte --> +<ul> + {#each items as item} + <li> + <slot {item} index={items.indexOf(item)}> + {item.name} + </slot> + </li> + {/each} +</ul> +``` + +```svelte +<List {items} let:item let:index> + <span>{index}: {item.name}</span> +</List> +``` + +## Transitions and Animations + +### Transitions + +```svelte +<script> + import { fade, fly, slide, scale, blur, draw } from 'svelte/transition'; + import { quintOut } from 'svelte/easing'; + + let visible = true; +</script> + +<!-- Basic transition --> +{#if visible} + <div transition:fade>Fades in and out</div> +{/if} + +<!-- With parameters --> +{#if visible} + <div transition:fly={{ y: 200, duration: 300 }}> + Flies in + </div> +{/if} + +<!-- Separate in/out transitions --> +{#if visible} + <div in:fly={{ y: 200 }} out:fade> + Different transitions + </div> +{/if} + +<!-- With easing --> +{#if visible} + <div transition:slide={{ duration: 300, easing: quintOut }}> + Slides with easing + </div> +{/if} +``` + +### Custom Transitions + +```javascript +function typewriter(node, { speed = 1 }) { + const valid = node.childNodes.length === 1 + && node.childNodes[0].nodeType === Node.TEXT_NODE; + + if (!valid) { + throw new Error('This transition only works on text nodes'); + } + + const text = node.textContent; + const duration = text.length / (speed * 0.01); + + return { + duration, + tick: t => { + const i = Math.trunc(text.length * t); + node.textContent = text.slice(0, i); + } + }; +} +``` + +### Animations + +Animate elements when they move within an each block: + +```svelte +<script> + import { flip } from 'svelte/animate'; +</script> + +{#each items as item (item.id)} + <li animate:flip={{ duration: 300 }}> + {item.name} + </li> +{/each} +``` + +## Actions + +Reusable element-level logic: + +```svelte +<script> + function clickOutside(node, callback) { + const handleClick = event => { + if (!node.contains(event.target)) { + callback(); + } + }; + + document.addEventListener('click', handleClick, true); + + return { + destroy() { + document.removeEventListener('click', handleClick, true); + } + }; + } + + function tooltip(node, text) { + // Setup tooltip + + return { + update(newText) { + // Update when text changes + }, + destroy() { + // Cleanup + } + }; + } +</script> + +<div use:clickOutside={() => visible = false}> + Click outside to close +</div> + +<button use:tooltip={'Click me!'}> + Hover for tooltip +</button> +``` + +## Special Elements + +### svelte:component + +Dynamic component rendering: + +```svelte +<script> + import Red from './Red.svelte'; + import Blue from './Blue.svelte'; + + let selected = Red; +</script> + +<svelte:component this={selected} /> +``` + +### svelte:element + +Dynamic HTML elements: + +```svelte +<script> + let tag = 'h1'; +</script> + +<svelte:element this={tag}>Dynamic heading</svelte:element> +``` + +### svelte:window + +```svelte +<script> + let innerWidth; + let innerHeight; + + function handleKeydown(event) { + console.log(event.key); + } +</script> + +<svelte:window + bind:innerWidth + bind:innerHeight + on:keydown={handleKeydown} +/> +``` + +### svelte:body and svelte:head + +```svelte +<svelte:body on:mouseenter={handleMouseenter} /> + +<svelte:head> + <title>Page Title</title> + <meta name="description" content="..." /> +</svelte:head> +``` + +### svelte:options + +```svelte +<svelte:options + immutable={true} + accessors={true} + namespace="svg" +/> +``` + +## Context API + +Share data between components without prop drilling: + +```svelte +<!-- Parent.svelte --> +<script> + import { setContext } from 'svelte'; + + setContext('theme', { + color: 'dark', + toggle: () => { /* ... */ } + }); +</script> +``` + +```svelte +<!-- Deeply nested child --> +<script> + import { getContext } from 'svelte'; + + const theme = getContext('theme'); +</script> + +<p>Current theme: {theme.color}</p> +``` + +**Key Points:** +- Context is not reactive by default +- Use stores in context for reactive values +- Context is available only during component initialization + +## Best Practices + +### Component Design + +1. **Keep components focused** - Single responsibility +2. **Use composition** - Prefer slots over complex props +3. **Extract logic to stores** - Shared state in stores +4. **Use actions for DOM logic** - Reusable element behaviors +5. **Type with JSDoc** - Document prop types + +### Reactivity + +1. **Understand triggers** - Assignments trigger updates +2. **Use immutable patterns** - Spread for arrays/objects +3. **Avoid side effects in reactive statements** - Keep them pure +4. **Use derived stores** - For computed values from stores + +### Performance + +1. **Key each blocks** - Use unique keys for list items +2. **Use immutable option** - When data is immutable +3. **Lazy load components** - Dynamic imports +4. **Minimize store subscriptions** - Unsubscribe when done + +### State Management + +1. **Local state first** - Component variables for local state +2. **Stores for shared state** - Cross-component communication +3. **Context for configuration** - Theme, i18n, etc. +4. **Custom stores for logic** - Encapsulate complex state + +## Common Patterns + +### Async Data Loading + +```svelte +<script> + import { onMount } from 'svelte'; + + let data = null; + let loading = true; + let error = null; + + onMount(async () => { + try { + const response = await fetch('/api/data'); + data = await response.json(); + } catch (e) { + error = e; + } finally { + loading = false; + } + }); +</script> + +{#if loading} + <p>Loading...</p> +{:else if error} + <p>Error: {error.message}</p> +{:else} + <p>{data}</p> +{/if} +``` + +### Form Handling + +```svelte +<script> + let formData = { + name: '', + email: '' + }; + let errors = {}; + let submitting = false; + + function validate() { + errors = {}; + if (!formData.name) errors.name = 'Name is required'; + if (!formData.email) errors.email = 'Email is required'; + return Object.keys(errors).length === 0; + } + + async function handleSubmit() { + if (!validate()) return; + + submitting = true; + try { + await fetch('/api/submit', { + method: 'POST', + body: JSON.stringify(formData) + }); + } finally { + submitting = false; + } + } +</script> + +<form on:submit|preventDefault={handleSubmit}> + <input bind:value={formData.name} /> + {#if errors.name}<span class="error">{errors.name}</span>{/if} + + <input bind:value={formData.email} type="email" /> + {#if errors.email}<span class="error">{errors.email}</span>{/if} + + <button disabled={submitting}> + {submitting ? 'Submitting...' : 'Submit'} + </button> +</form> +``` + +### Modal Pattern + +```svelte +<!-- Modal.svelte --> +<script> + export let open = false; + + function close() { + open = false; + } +</script> + +{#if open} + <div class="backdrop" on:click={close}> + <div class="modal" on:click|stopPropagation> + <slot /> + <button on:click={close}>Close</button> + </div> + </div> +{/if} +``` + +## Troubleshooting + +### Common Issues + +**Reactivity not working:** +- Check for proper assignment (reassign arrays/objects) +- Use `$:` for derived values +- Store subscriptions need `$` prefix + +**Component not updating:** +- Verify prop changes trigger parent re-render +- Check key blocks for forced recreation +- Use `{#key}` to force component recreation + +**Memory leaks:** +- Clean up subscriptions in `onDestroy` +- Return cleanup functions from `onMount` +- Unsubscribe from stores manually if not using `$` + +**Styles not applying:** +- Check for `:global()` if targeting child components +- Verify CSS specificity +- Use `class:` directive properly + +## References + +- **Svelte Documentation**: https://svelte.dev/docs +- **Svelte Tutorial**: https://svelte.dev/tutorial +- **Svelte REPL**: https://svelte.dev/repl +- **Svelte Society**: https://sveltesociety.dev +- **GitHub**: https://github.com/sveltejs/svelte + +## Related Skills + +- **rollup** - Bundling Svelte applications +- **nostr-tools** - Nostr integration in Svelte apps +- **typescript** - TypeScript with Svelte diff --git a/.claude/skills/typescript/README.md b/.claude/skills/typescript/README.md new file mode 100644 index 0000000..84093f7 --- /dev/null +++ b/.claude/skills/typescript/README.md @@ -0,0 +1,133 @@ +# TypeScript Claude Skill + +Comprehensive TypeScript skill for type-safe development with modern JavaScript/TypeScript applications. + +## Overview + +This skill provides in-depth knowledge about TypeScript's type system, patterns, best practices, and integration with popular frameworks like React. It covers everything from basic types to advanced type manipulation techniques. + +## Files + +### Core Documentation +- **SKILL.md** - Main skill file with workflows and when to use this skill +- **quick-reference.md** - Quick lookup guide for common TypeScript syntax and patterns + +### Reference Materials +- **references/type-system.md** - Comprehensive guide to TypeScript's type system +- **references/utility-types.md** - Complete reference for built-in and custom utility types +- **references/common-patterns.md** - Real-world TypeScript patterns and idioms + +### Examples +- **examples/type-system-basics.ts** - Fundamental TypeScript concepts +- **examples/advanced-types.ts** - Generics, conditional types, mapped types +- **examples/react-patterns.ts** - Type-safe React components and hooks +- **examples/README.md** - Guide to using the examples + +## Usage + +### When to Use This Skill + +Reference this skill when: +- Writing or refactoring TypeScript code +- Designing type-safe APIs and interfaces +- Working with advanced type system features +- Configuring TypeScript projects +- Troubleshooting type errors +- Implementing type-safe patterns with libraries +- Converting JavaScript to TypeScript + +### Quick Start + +For quick lookups, start with `quick-reference.md` which provides concise syntax and patterns. + +For learning or deep dives: +1. **Fundamentals**: Start with `references/type-system.md` +2. **Utilities**: Learn about transformations in `references/utility-types.md` +3. **Patterns**: Study real-world patterns in `references/common-patterns.md` +4. **Practice**: Explore code examples in `examples/` + +## Key Topics Covered + +### Type System +- Primitive types and special types +- Object types (interfaces, type aliases) +- Union and intersection types +- Literal types and template literal types +- Type inference and narrowing +- Generic types with constraints +- Conditional types and mapped types +- Recursive types + +### Advanced Features +- Type guards and type predicates +- Assertion functions +- Branded types for nominal typing +- Key remapping and filtering +- Distributive conditional types +- Type-level programming + +### Utility Types +- Built-in utilities (Partial, Pick, Omit, etc.) +- Custom utility type patterns +- Deep transformations +- Type composition + +### React Integration +- Component props typing +- Generic components +- Hooks with TypeScript +- Context with type safety +- Event handlers +- Ref typing + +### Best Practices +- Type safety patterns +- Error handling +- Code organization +- Integration with Zod for runtime validation +- Named return variables (Go-style) +- Discriminated unions for state management + +## Integration with Project Stack + +This skill is designed to work seamlessly with: +- **React 19**: Type-safe component development +- **TanStack Ecosystem**: Typed queries, routing, forms, and stores +- **Zod**: Runtime validation with type inference +- **Radix UI**: Component prop typing +- **Tailwind CSS**: Type-safe className composition + +## Examples + +All examples are self-contained and demonstrate practical patterns: +- Based on real-world usage +- Follow project best practices +- Include comprehensive comments +- Can be run with `ts-node` +- Ready to adapt to your needs + +## Configuration + +The skill includes guidance on TypeScript configuration with recommended settings for: +- Strict type checking +- Module resolution +- JSX support +- Path aliases +- Declaration files + +## Contributing + +When adding new patterns or examples: +1. Follow existing file structure +2. Include comprehensive comments +3. Demonstrate real-world usage +4. Add to appropriate reference file +5. Update this README if needed + +## Resources + +- [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/) +- [TypeScript Deep Dive](https://basarat.gitbook.io/typescript/) +- [Type Challenges](https://github.com/type-challenges/type-challenges) +- [TSConfig Reference](https://www.typescriptlang.org/tsconfig) + diff --git a/.claude/skills/typescript/SKILL.md b/.claude/skills/typescript/SKILL.md new file mode 100644 index 0000000..ebe5937 --- /dev/null +++ b/.claude/skills/typescript/SKILL.md @@ -0,0 +1,359 @@ +--- +name: typescript +description: This skill should be used when working with TypeScript code, including type definitions, type inference, generics, utility types, and TypeScript configuration. Provides comprehensive knowledge of TypeScript patterns, best practices, and advanced type system features. +--- + +# TypeScript Skill + +This skill provides comprehensive knowledge and patterns for working with TypeScript effectively in modern applications. + +## When to Use This Skill + +Use this skill when: +- Writing or refactoring TypeScript code +- Designing type-safe APIs and interfaces +- Working with advanced type system features (generics, conditional types, mapped types) +- Configuring TypeScript projects (tsconfig.json) +- Troubleshooting type errors +- Implementing type-safe patterns with libraries (React, TanStack, etc.) +- Converting JavaScript code to TypeScript + +## Core Concepts + +### Type System Fundamentals + +TypeScript provides static typing for JavaScript with a powerful type system that includes: +- Primitive types (string, number, boolean, null, undefined, symbol, bigint) +- Object types (interfaces, type aliases, classes) +- Array and tuple types +- Union and intersection types +- Literal types and template literal types +- Type inference and type narrowing +- Generic types with constraints +- Conditional types and mapped types + +### Type Inference + +Leverage TypeScript's type inference to write less verbose code: +- Let TypeScript infer return types when obvious +- Use type inference for variable declarations +- Rely on generic type inference in function calls +- Use `as const` for immutable literal types + +### Type Safety Patterns + +Implement type-safe patterns: +- Use discriminated unions for state management +- Implement type guards for runtime type checking +- Use branded types for nominal typing +- Leverage conditional types for API design +- Use template literal types for string manipulation + +## Key Workflows + +### 1. Designing Type-Safe APIs + +When designing APIs, follow these patterns: + +**Interface vs Type Alias:** +- Use `interface` for object shapes that may be extended +- Use `type` for unions, intersections, and complex type operations +- Use `type` with mapped types and conditional types + +**Generic Constraints:** +```typescript +// Use extends for generic constraints +function getValue<T extends { id: string }>(item: T): string { + return item.id +} +``` + +**Discriminated Unions:** +```typescript +// Use for type-safe state machines +type State = + | { status: 'idle' } + | { status: 'loading' } + | { status: 'success'; data: Data } + | { status: 'error'; error: Error } +``` + +### 2. Working with Utility Types + +Use built-in utility types for common transformations: +- `Partial<T>` - Make all properties optional +- `Required<T>` - Make all properties required +- `Readonly<T>` - Make all properties readonly +- `Pick<T, K>` - Select specific properties +- `Omit<T, K>` - Exclude specific properties +- `Record<K, T>` - Create object type with specific keys +- `Exclude<T, U>` - Exclude types from union +- `Extract<T, U>` - Extract types from union +- `NonNullable<T>` - Remove null/undefined +- `ReturnType<T>` - Get function return type +- `Parameters<T>` - Get function parameter types +- `Awaited<T>` - Unwrap Promise type + +### 3. Advanced Type Patterns + +**Mapped Types:** +```typescript +// Transform object types +type Nullable<T> = { + [K in keyof T]: T[K] | null +} + +type ReadonlyDeep<T> = { + readonly [K in keyof T]: T[K] extends object + ? ReadonlyDeep<T[K]> + : T[K] +} +``` + +**Conditional Types:** +```typescript +// Type-level logic +type IsArray<T> = T extends Array<any> ? true : false + +type Flatten<T> = T extends Array<infer U> ? U : T +``` + +**Template Literal Types:** +```typescript +// String manipulation at type level +type EventName<T extends string> = `on${Capitalize<T>}` +type Route = `/api/${'users' | 'posts'}/${string}` +``` + +### 4. Type Narrowing + +Use type guards and narrowing techniques: + +**typeof guards:** +```typescript +if (typeof value === 'string') { + // value is string here +} +``` + +**instanceof guards:** +```typescript +if (error instanceof Error) { + // error is Error here +} +``` + +**Custom type guards:** +```typescript +function isUser(value: unknown): value is User { + return typeof value === 'object' && value !== null && 'id' in value +} +``` + +**Discriminated unions:** +```typescript +function handle(state: State) { + switch (state.status) { + case 'idle': + // state is { status: 'idle' } + break + case 'success': + // state is { status: 'success'; data: Data } + console.log(state.data) + break + } +} +``` + +### 5. Working with External Libraries + +**Typing Third-Party Libraries:** +- Install type definitions: `npm install --save-dev @types/package-name` +- Create custom declarations in `.d.ts` files when types unavailable +- Use module augmentation to extend existing type definitions + +**Declaration Files:** +```typescript +// globals.d.ts +declare global { + interface Window { + myCustomProperty: string + } +} + +export {} +``` + +### 6. TypeScript Configuration + +Configure `tsconfig.json` for strict type checking: + +**Essential Strict Options:** +```json +{ + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true + } +} +``` + +## Best Practices + +### 1. Prefer Type Inference Over Explicit Types +Let TypeScript infer types when they're obvious from context. + +### 2. Use Strict Mode +Enable strict type checking to catch more errors at compile time. + +### 3. Avoid `any` Type +Use `unknown` for truly unknown types, then narrow with type guards. + +### 4. Use Const Assertions +Use `as const` for immutable values and narrow literal types. + +### 5. Leverage Discriminated Unions +Use for state machines and variant types for better type safety. + +### 6. Create Reusable Generic Types +Extract common type patterns into reusable generics. + +### 7. Use Branded Types for Nominal Typing +Create distinct types for values with same structure but different meaning. + +### 8. Document Complex Types +Add JSDoc comments to explain non-obvious type decisions. + +### 9. Use Type-Only Imports +Use `import type` for type-only imports to aid tree-shaking. + +### 10. Handle Errors with Type Guards +Use type guards to safely work with error objects. + +## Common Patterns + +### React Component Props +```typescript +// Use interface for component props +interface ButtonProps { + variant?: 'primary' | 'secondary' + size?: 'sm' | 'md' | 'lg' + onClick?: () => void + children: React.ReactNode +} + +export function Button({ variant = 'primary', size = 'md', onClick, children }: ButtonProps) { + // implementation +} +``` + +### API Response Types +```typescript +// Use discriminated unions for API responses +type ApiResponse<T> = + | { success: true; data: T } + | { success: false; error: string } + +// Helper for safe API calls +async function fetchData<T>(url: string): Promise<ApiResponse<T>> { + try { + const response = await fetch(url) + const data = await response.json() + return { success: true, data } + } catch (error) { + return { success: false, error: String(error) } + } +} +``` + +### Store/State Types +```typescript +// Use interfaces for state objects +interface AppState { + user: User | null + isAuthenticated: boolean + theme: 'light' | 'dark' +} + +// Use type for actions (discriminated union) +type AppAction = + | { type: 'LOGIN'; payload: User } + | { type: 'LOGOUT' } + | { type: 'SET_THEME'; payload: 'light' | 'dark' } +``` + +## References + +For detailed information on specific topics, refer to: +- `references/type-system.md` - Deep dive into TypeScript's type system +- `references/utility-types.md` - Complete guide to built-in utility types +- `references/advanced-types.md` - Advanced type patterns and techniques +- `references/tsconfig-reference.md` - Comprehensive tsconfig.json reference +- `references/common-patterns.md` - Common TypeScript patterns and idioms +- `examples/` - Practical code examples + +## Troubleshooting + +### Common Type Errors + +**Type 'X' is not assignable to type 'Y':** +- Check if types are compatible +- Use type assertions when you know better than the compiler +- Consider using union types or widening the target type + +**Object is possibly 'null' or 'undefined':** +- Use optional chaining: `object?.property` +- Use nullish coalescing: `value ?? defaultValue` +- Add type guards or null checks + +**Type 'any' implicitly has...** +- Enable strict mode and fix type definitions +- Add explicit type annotations +- Use `unknown` instead of `any` when appropriate + +**Cannot find module or its type declarations:** +- Install type definitions: `@types/package-name` +- Create custom `.d.ts` declaration file +- Add to `types` array in tsconfig.json + +## Integration with Project Stack + +### React 19 +Use TypeScript with React 19 features: +- Type component props with interfaces +- Use generic types for hooks +- Type context providers properly +- Use `React.FC` sparingly (prefer explicit typing) + +### TanStack Ecosystem +Type TanStack libraries properly: +- TanStack Query: Type query keys and data +- TanStack Router: Use typed route definitions +- TanStack Form: Type form values and validation +- TanStack Store: Type state and actions + +### Zod Integration +Combine Zod with TypeScript: +- Use `z.infer<typeof schema>` to extract types from schemas +- Let Zod handle runtime validation +- Use TypeScript for compile-time type checking + +## Resources + +The TypeScript documentation provides comprehensive information: +- Handbook: https://www.typescriptlang.org/docs/handbook/ +- Type manipulation: https://www.typescriptlang.org/docs/handbook/2/types-from-types.html +- Utility types: https://www.typescriptlang.org/docs/handbook/utility-types.html +- TSConfig reference: https://www.typescriptlang.org/tsconfig + diff --git a/.claude/skills/typescript/examples/README.md b/.claude/skills/typescript/examples/README.md new file mode 100644 index 0000000..4a19a0a --- /dev/null +++ b/.claude/skills/typescript/examples/README.md @@ -0,0 +1,45 @@ +# TypeScript Examples + +This directory contains practical TypeScript examples demonstrating various patterns and features. + +## Examples + +1. **type-system-basics.ts** - Fundamental TypeScript types and features +2. **advanced-types.ts** - Generics, conditional types, and mapped types +3. **react-patterns.ts** - Type-safe React components and hooks +4. **api-patterns.ts** - API response handling with type safety +5. **validation.ts** - Runtime validation with Zod and TypeScript + +## How to Use + +Each example file is self-contained and demonstrates specific TypeScript concepts. They're based on real-world patterns used in the Plebeian Market application and follow best practices for: + +- Type safety +- Error handling +- Code organization +- Reusability +- Maintainability + +## Running Examples + +These examples are TypeScript files that can be: +- Copied into your project +- Used as reference for patterns +- Modified for your specific needs +- Run with `ts-node` for testing + +```bash +# Run an example +npx ts-node examples/type-system-basics.ts +``` + +## Learning Path + +1. Start with `type-system-basics.ts` to understand fundamentals +2. Move to `advanced-types.ts` for complex type patterns +3. Explore `react-patterns.ts` for component typing +4. Study `api-patterns.ts` for type-safe API handling +5. Review `validation.ts` for runtime safety + +Each example builds on previous concepts, so following this order is recommended for learners. + diff --git a/.claude/skills/typescript/examples/advanced-types.ts b/.claude/skills/typescript/examples/advanced-types.ts new file mode 100644 index 0000000..0a00ac6 --- /dev/null +++ b/.claude/skills/typescript/examples/advanced-types.ts @@ -0,0 +1,478 @@ +/** + * Advanced TypeScript Types + * + * This file demonstrates advanced TypeScript features including: + * - Generics with constraints + * - Conditional types + * - Mapped types + * - Template literal types + * - Recursive types + * - Utility type implementations + */ + +// ============================================================================ +// Generics Basics +// ============================================================================ + +// Generic function +function identity<T>(value: T): T { + return value +} + +const stringValue = identity('hello') // Type: string +const numberValue = identity(42) // Type: number + +// Generic interface +interface Box<T> { + value: T +} + +const stringBox: Box<string> = { value: 'hello' } +const numberBox: Box<number> = { value: 42 } + +// Generic class +class Stack<T> { + private items: T[] = [] + + push(item: T): void { + this.items.push(item) + } + + pop(): T | undefined { + return this.items.pop() + } + + peek(): T | undefined { + return this.items[this.items.length - 1] + } + + isEmpty(): boolean { + return this.items.length === 0 + } +} + +const numberStack = new Stack<number>() +numberStack.push(1) +numberStack.push(2) +numberStack.pop() // Type: number | undefined + +// ============================================================================ +// Generic Constraints +// ============================================================================ + +// Constrain to specific type +interface HasLength { + length: number +} + +function logLength<T extends HasLength>(item: T): void { + console.log(item.length) +} + +logLength('string') // OK +logLength([1, 2, 3]) // OK +logLength({ length: 10 }) // OK +// logLength(42) // Error: number doesn't have length + +// Constrain to object keys +function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { + return obj[key] +} + +interface User { + id: string + name: string + age: number +} + +const user: User = { id: '1', name: 'Alice', age: 30 } +const userName = getProperty(user, 'name') // Type: string +// const invalid = getProperty(user, 'invalid') // Error + +// Multiple type parameters with constraints +function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U { + return { ...obj1, ...obj2 } +} + +const merged = merge({ a: 1 }, { b: 2 }) // Type: { a: number } & { b: number } + +// ============================================================================ +// Conditional Types +// ============================================================================ + +// Basic conditional type +type IsString<T> = T extends string ? true : false + +type A = IsString<string> // true +type B = IsString<number> // false + +// Nested conditional types +type TypeName<T> = T extends string + ? 'string' + : T extends number + ? 'number' + : T extends boolean + ? 'boolean' + : T extends undefined + ? 'undefined' + : T extends Function + ? 'function' + : 'object' + +type T1 = TypeName<string> // "string" +type T2 = TypeName<number> // "number" +type T3 = TypeName<() => void> // "function" + +// Distributive conditional types +type ToArray<T> = T extends any ? T[] : never + +type StrArrOrNumArr = ToArray<string | number> // string[] | number[] + +// infer keyword +type Flatten<T> = T extends Array<infer U> ? U : T + +type Str = Flatten<string[]> // string +type Num = Flatten<number> // number + +// Return type extraction +type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never + +function exampleFn(): string { + return 'hello' +} + +type ExampleReturn = MyReturnType<typeof exampleFn> // string + +// Parameters extraction +type MyParameters<T> = T extends (...args: infer P) => any ? P : never + +function createUser(name: string, age: number): User { + return { id: '1', name, age } +} + +type CreateUserParams = MyParameters<typeof createUser> // [string, number] + +// ============================================================================ +// Mapped Types +// ============================================================================ + +// Make all properties optional +type MyPartial<T> = { + [K in keyof T]?: T[K] +} + +interface Person { + name: string + age: number + email: string +} + +type PartialPerson = MyPartial<Person> +// { +// name?: string +// age?: number +// email?: string +// } + +// Make all properties required +type MyRequired<T> = { + [K in keyof T]-?: T[K] +} + +// Make all properties readonly +type MyReadonly<T> = { + readonly [K in keyof T]: T[K] +} + +// Pick specific properties +type MyPick<T, K extends keyof T> = { + [P in K]: T[P] +} + +type UserProfile = MyPick<User, 'id' | 'name'> +// { id: string; name: string } + +// Omit specific properties +type MyOmit<T, K extends keyof T> = { + [P in keyof T as P extends K ? never : P]: T[P] +} + +type UserWithoutAge = MyOmit<User, 'age'> +// { id: string; name: string } + +// Transform property types +type Nullable<T> = { + [K in keyof T]: T[K] | null +} + +type NullablePerson = Nullable<Person> +// { +// name: string | null +// age: number | null +// email: string | null +// } + +// ============================================================================ +// Key Remapping +// ============================================================================ + +// Add prefix to keys +type Getters<T> = { + [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] +} + +type PersonGetters = Getters<Person> +// { +// getName: () => string +// getAge: () => number +// getEmail: () => string +// } + +// Filter keys by type +type PickByType<T, U> = { + [K in keyof T as T[K] extends U ? K : never]: T[K] +} + +interface Model { + id: number + name: string + description: string + price: number +} + +type StringFields = PickByType<Model, string> +// { name: string; description: string } + +// Remove specific key +type RemoveKindField<T> = { + [K in keyof T as Exclude<K, 'kind'>]: T[K] +} + +// ============================================================================ +// Template Literal Types +// ============================================================================ + +// Event name generation +type EventName<T extends string> = `on${Capitalize<T>}` + +type ClickEvent = EventName<'click'> // "onClick" +type SubmitEvent = EventName<'submit'> // "onSubmit" + +// Combining literals +type Color = 'red' | 'green' | 'blue' +type Shade = 'light' | 'dark' +type ColorShade = `${Shade}-${Color}` +// "light-red" | "light-green" | "light-blue" | "dark-red" | "dark-green" | "dark-blue" + +// CSS properties +type CSSProperty = 'margin' | 'padding' +type Side = 'top' | 'right' | 'bottom' | 'left' +type CSSPropertyWithSide = `${CSSProperty}-${Side}` +// "margin-top" | "margin-right" | ... | "padding-left" + +// Route generation +type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' +type Endpoint = '/users' | '/products' | '/orders' +type ApiRoute = `${HttpMethod} ${Endpoint}` +// "GET /users" | "POST /users" | ... | "DELETE /orders" + +// ============================================================================ +// Recursive Types +// ============================================================================ + +// JSON value type +type JSONValue = string | number | boolean | null | JSONObject | JSONArray + +interface JSONObject { + [key: string]: JSONValue +} + +interface JSONArray extends Array<JSONValue> {} + +// Tree structure +interface TreeNode<T> { + value: T + children?: TreeNode<T>[] +} + +const tree: TreeNode<number> = { + value: 1, + children: [ + { value: 2, children: [{ value: 4 }, { value: 5 }] }, + { value: 3, children: [{ value: 6 }] }, + ], +} + +// Deep readonly +type DeepReadonly<T> = { + readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K] +} + +interface NestedConfig { + api: { + url: string + timeout: number + } + features: { + darkMode: boolean + } +} + +type ImmutableConfig = DeepReadonly<NestedConfig> +// All properties at all levels are readonly + +// Deep partial +type DeepPartial<T> = { + [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K] +} + +// ============================================================================ +// Advanced Utility Types +// ============================================================================ + +// Exclude types from union +type MyExclude<T, U> = T extends U ? never : T + +type T4 = MyExclude<'a' | 'b' | 'c', 'a'> // "b" | "c" + +// Extract types from union +type MyExtract<T, U> = T extends U ? T : never + +type T5 = MyExtract<'a' | 'b' | 'c', 'a' | 'f'> // "a" + +// NonNullable +type MyNonNullable<T> = T extends null | undefined ? never : T + +type T6 = MyNonNullable<string | null | undefined> // string + +// Record +type MyRecord<K extends keyof any, T> = { + [P in K]: T +} + +type PageInfo = MyRecord<string, number> + +// Awaited +type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T + +type T7 = MyAwaited<Promise<string>> // string +type T8 = MyAwaited<Promise<Promise<number>>> // number + +// ============================================================================ +// Branded Types +// ============================================================================ + +type Brand<K, T> = K & { __brand: T } + +type USD = Brand<number, 'USD'> +type EUR = Brand<number, 'EUR'> +type UserId = Brand<string, 'UserId'> +type ProductId = Brand<string, 'ProductId'> + +function makeUSD(amount: number): USD { + return amount as USD +} + +function makeUserId(id: string): UserId { + return id as UserId +} + +const usd = makeUSD(100) +const userId = makeUserId('user-123') + +// Type-safe operations +function addMoney(a: USD, b: USD): USD { + return (a + b) as USD +} + +// Prevents mixing different branded types +// const total = addMoney(usd, eur) // Error + +// ============================================================================ +// Union to Intersection +// ============================================================================ + +type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ( + k: infer I, +) => void + ? I + : never + +type Union = { a: string } | { b: number } +type Intersection = UnionToIntersection<Union> +// { a: string } & { b: number } + +// ============================================================================ +// Advanced Generic Patterns +// ============================================================================ + +// Constraining multiple related types +function merge< + T extends Record<string, any>, + U extends Record<string, any>, + K extends keyof T & keyof U, +>(obj1: T, obj2: U, conflictKeys: K[]): T & U { + const result = { ...obj1, ...obj2 } + conflictKeys.forEach((key) => { + // Handle conflicts + }) + return result as T & U +} + +// Builder pattern with fluent API +class QueryBuilder<T, Selected extends keyof T = never> { + private selectFields: Set<keyof T> = new Set() + + select<K extends keyof T>( + ...fields: K[] + ): QueryBuilder<T, Selected | K> { + fields.forEach((field) => this.selectFields.add(field)) + return this as any + } + + execute(): Pick<T, Selected> { + // Execute query + return {} as Pick<T, Selected> + } +} + +// Usage +interface Product { + id: string + name: string + price: number + description: string +} + +const result = new QueryBuilder<Product>() + .select('id', 'name') + .select('price') + .execute() +// Type: { id: string; name: string; price: number } + +// ============================================================================ +// Exports +// ============================================================================ + +export type { + Box, + HasLength, + IsString, + Flatten, + MyPartial, + MyRequired, + MyReadonly, + Nullable, + DeepReadonly, + DeepPartial, + Brand, + USD, + EUR, + UserId, + ProductId, + JSONValue, + TreeNode, +} + +export { Stack, identity, getProperty, merge, makeUSD, makeUserId } + diff --git a/.claude/skills/typescript/examples/react-patterns.ts b/.claude/skills/typescript/examples/react-patterns.ts new file mode 100644 index 0000000..a50b689 --- /dev/null +++ b/.claude/skills/typescript/examples/react-patterns.ts @@ -0,0 +1,555 @@ +/** + * TypeScript React Patterns + * + * This file demonstrates type-safe React patterns including: + * - Component props typing + * - Hooks with TypeScript + * - Context with type safety + * - Generic components + * - Event handlers + * - Ref types + */ + +import { createContext, useContext, useEffect, useReducer, useRef, useState } from 'react' +import type { ReactNode, InputHTMLAttributes, FormEvent, ChangeEvent } from 'react' + +// ============================================================================ +// Component Props Patterns +// ============================================================================ + +// Basic component with props +interface ButtonProps { + variant?: 'primary' | 'secondary' | 'tertiary' + size?: 'sm' | 'md' | 'lg' + disabled?: boolean + onClick?: () => void + children: ReactNode +} + +export function Button({ + variant = 'primary', + size = 'md', + disabled = false, + onClick, + children, +}: ButtonProps) { + return ( + <button + className={`btn-${variant} btn-${size}`} + disabled={disabled} + onClick={onClick} + > + {children} + </button> + ) +} + +// Props extending HTML attributes +interface InputProps extends InputHTMLAttributes<HTMLInputElement> { + label?: string + error?: string + helperText?: string +} + +export function Input({ label, error, helperText, ...inputProps }: InputProps) { + return ( + <div className="input-wrapper"> + {label && <label>{label}</label>} + <input className={error ? 'input-error' : ''} {...inputProps} /> + {error && <span className="error">{error}</span>} + {helperText && <span className="helper">{helperText}</span>} + </div> + ) +} + +// Generic component +interface ListProps<T> { + items: T[] + renderItem: (item: T, index: number) => ReactNode + keyExtractor: (item: T, index: number) => string + emptyMessage?: string +} + +export function List<T>({ + items, + renderItem, + keyExtractor, + emptyMessage = 'No items', +}: ListProps<T>) { + if (items.length === 0) { + return <div>{emptyMessage}</div> + } + + return ( + <ul> + {items.map((item, index) => ( + <li key={keyExtractor(item, index)}>{renderItem(item, index)}</li> + ))} + </ul> + ) +} + +// Component with children render prop +interface ContainerProps { + isLoading: boolean + error: Error | null + children: (props: { retry: () => void }) => ReactNode +} + +export function Container({ isLoading, error, children }: ContainerProps) { + const retry = () => { + // Retry logic + } + + if (isLoading) return <div>Loading...</div> + if (error) return <div>Error: {error.message}</div> + + return <>{children({ retry })}</> +} + +// ============================================================================ +// Hooks Patterns +// ============================================================================ + +// useState with explicit type +function useCounter(initialValue: number = 0) { + const [count, setCount] = useState<number>(initialValue) + + const increment = () => setCount((c) => c + 1) + const decrement = () => setCount((c) => c - 1) + const reset = () => setCount(initialValue) + + return { count, increment, decrement, reset } +} + +// useState with union type +type LoadingState = 'idle' | 'loading' | 'success' | 'error' + +function useLoadingState() { + const [state, setState] = useState<LoadingState>('idle') + + const startLoading = () => setState('loading') + const setSuccess = () => setState('success') + const setError = () => setState('error') + const reset = () => setState('idle') + + return { state, startLoading, setSuccess, setError, reset } +} + +// Custom hook with options +interface UseFetchOptions<T> { + initialData?: T + onSuccess?: (data: T) => void + onError?: (error: Error) => void +} + +interface UseFetchReturn<T> { + data: T | undefined + loading: boolean + error: Error | null + refetch: () => Promise<void> +} + +function useFetch<T>(url: string, options?: UseFetchOptions<T>): UseFetchReturn<T> { + const [data, setData] = useState<T | undefined>(options?.initialData) + const [loading, setLoading] = useState(false) + const [error, setError] = useState<Error | null>(null) + + const fetchData = async () => { + setLoading(true) + setError(null) + + try { + const response = await fetch(url) + if (!response.ok) { + throw new Error(`HTTP ${response.status}`) + } + const json = await response.json() + setData(json) + options?.onSuccess?.(json) + } catch (err) { + const error = err instanceof Error ? err : new Error(String(err)) + setError(error) + options?.onError?.(error) + } finally { + setLoading(false) + } + } + + useEffect(() => { + fetchData() + }, [url]) + + return { data, loading, error, refetch: fetchData } +} + +// useReducer with discriminated unions +interface User { + id: string + name: string + email: string +} + +type FetchState<T> = + | { status: 'idle' } + | { status: 'loading' } + | { status: 'success'; data: T } + | { status: 'error'; error: Error } + +type FetchAction<T> = + | { type: 'FETCH_START' } + | { type: 'FETCH_SUCCESS'; payload: T } + | { type: 'FETCH_ERROR'; error: Error } + | { type: 'RESET' } + +function fetchReducer<T>(state: FetchState<T>, action: FetchAction<T>): FetchState<T> { + switch (action.type) { + case 'FETCH_START': + return { status: 'loading' } + case 'FETCH_SUCCESS': + return { status: 'success', data: action.payload } + case 'FETCH_ERROR': + return { status: 'error', error: action.error } + case 'RESET': + return { status: 'idle' } + } +} + +function useFetchWithReducer<T>(url: string) { + const [state, dispatch] = useReducer(fetchReducer<T>, { status: 'idle' }) + + useEffect(() => { + let isCancelled = false + + const fetchData = async () => { + dispatch({ type: 'FETCH_START' }) + + try { + const response = await fetch(url) + const data = await response.json() + + if (!isCancelled) { + dispatch({ type: 'FETCH_SUCCESS', payload: data }) + } + } catch (error) { + if (!isCancelled) { + dispatch({ + type: 'FETCH_ERROR', + error: error instanceof Error ? error : new Error(String(error)), + }) + } + } + } + + fetchData() + + return () => { + isCancelled = true + } + }, [url]) + + return state +} + +// ============================================================================ +// Context Patterns +// ============================================================================ + +// Type-safe context +interface AuthContextType { + user: User | null + isAuthenticated: boolean + login: (email: string, password: string) => Promise<void> + logout: () => void +} + +const AuthContext = createContext<AuthContextType | undefined>(undefined) + +export function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState<User | null>(null) + + const login = async (email: string, password: string) => { + // Login logic + const userData = await fetch('/api/login', { + method: 'POST', + body: JSON.stringify({ email, password }), + }).then((r) => r.json()) + + setUser(userData) + } + + const logout = () => { + setUser(null) + } + + const value: AuthContextType = { + user, + isAuthenticated: user !== null, + login, + logout, + } + + return <AuthContext.Provider value={value}>{children}</AuthContext.Provider> +} + +// Custom hook with error handling +export function useAuth(): AuthContextType { + const context = useContext(AuthContext) + + if (context === undefined) { + throw new Error('useAuth must be used within AuthProvider') + } + + return context +} + +// ============================================================================ +// Event Handler Patterns +// ============================================================================ + +interface FormData { + name: string + email: string + message: string +} + +function ContactForm() { + const [formData, setFormData] = useState<FormData>({ + name: '', + email: '', + message: '', + }) + + // Type-safe change handler + const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { + const { name, value } = e.target + setFormData((prev) => ({ + ...prev, + [name]: value, + })) + } + + // Type-safe submit handler + const handleSubmit = (e: FormEvent<HTMLFormElement>) => { + e.preventDefault() + console.log('Submitting:', formData) + } + + // Specific field handler + const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => { + setFormData((prev) => ({ ...prev, name: e.target.value })) + } + + return ( + <form onSubmit={handleSubmit}> + <input + name="name" + value={formData.name} + onChange={handleChange} + placeholder="Name" + /> + <input + name="email" + value={formData.email} + onChange={handleChange} + placeholder="Email" + /> + <textarea + name="message" + value={formData.message} + onChange={handleChange} + placeholder="Message" + /> + <button type="submit">Submit</button> + </form> + ) +} + +// ============================================================================ +// Ref Patterns +// ============================================================================ + +function FocusInput() { + // useRef with DOM element + const inputRef = useRef<HTMLInputElement>(null) + + const focusInput = () => { + inputRef.current?.focus() + } + + return ( + <div> + <input ref={inputRef} /> + <button onClick={focusInput}>Focus Input</button> + </div> + ) +} + +function Timer() { + // useRef for mutable value + const countRef = useRef<number>(0) + const intervalRef = useRef<NodeJS.Timeout | null>(null) + + const startTimer = () => { + intervalRef.current = setInterval(() => { + countRef.current += 1 + console.log(countRef.current) + }, 1000) + } + + const stopTimer = () => { + if (intervalRef.current) { + clearInterval(intervalRef.current) + intervalRef.current = null + } + } + + return ( + <div> + <button onClick={startTimer}>Start</button> + <button onClick={stopTimer}>Stop</button> + </div> + ) +} + +// ============================================================================ +// Generic Component Patterns +// ============================================================================ + +// Select component with generic options +interface SelectProps<T> { + options: T[] + value: T + onChange: (value: T) => void + getLabel: (option: T) => string + getValue: (option: T) => string +} + +export function Select<T>({ + options, + value, + onChange, + getLabel, + getValue, +}: SelectProps<T>) { + return ( + <select + value={getValue(value)} + onChange={(e) => { + const selectedValue = e.target.value + const option = options.find((opt) => getValue(opt) === selectedValue) + if (option) { + onChange(option) + } + }} + > + {options.map((option) => ( + <option key={getValue(option)} value={getValue(option)}> + {getLabel(option)} + </option> + ))} + </select> + ) +} + +// Data table component +interface Column<T> { + key: keyof T + header: string + render?: (value: T[keyof T], row: T) => ReactNode +} + +interface TableProps<T> { + data: T[] + columns: Column<T>[] + keyExtractor: (row: T) => string +} + +export function Table<T>({ data, columns, keyExtractor }: TableProps<T>) { + return ( + <table> + <thead> + <tr> + {columns.map((col) => ( + <th key={String(col.key)}>{col.header}</th> + ))} + </tr> + </thead> + <tbody> + {data.map((row) => ( + <tr key={keyExtractor(row)}> + {columns.map((col) => ( + <td key={String(col.key)}> + {col.render ? col.render(row[col.key], row) : String(row[col.key])} + </td> + ))} + </tr> + ))} + </tbody> + </table> + ) +} + +// ============================================================================ +// Higher-Order Component Pattern +// ============================================================================ + +interface WithLoadingProps { + isLoading: boolean +} + +function withLoading<P extends object>( + Component: React.ComponentType<P>, +): React.FC<P & WithLoadingProps> { + return ({ isLoading, ...props }: WithLoadingProps & P) => { + if (isLoading) { + return <div>Loading...</div> + } + + return <Component {...(props as P)} /> + } +} + +// Usage +interface UserListProps { + users: User[] +} + +const UserList: React.FC<UserListProps> = ({ users }) => ( + <ul> + {users.map((user) => ( + <li key={user.id}>{user.name}</li> + ))} + </ul> +) + +const UserListWithLoading = withLoading(UserList) + +// ============================================================================ +// Exports +// ============================================================================ + +export { + useCounter, + useLoadingState, + useFetch, + useFetchWithReducer, + ContactForm, + FocusInput, + Timer, +} + +export type { + ButtonProps, + InputProps, + ListProps, + UseFetchOptions, + UseFetchReturn, + FetchState, + FetchAction, + AuthContextType, + SelectProps, + Column, + TableProps, +} + diff --git a/.claude/skills/typescript/examples/type-system-basics.ts b/.claude/skills/typescript/examples/type-system-basics.ts new file mode 100644 index 0000000..bc9742d --- /dev/null +++ b/.claude/skills/typescript/examples/type-system-basics.ts @@ -0,0 +1,361 @@ +/** + * TypeScript Type System Basics + * + * This file demonstrates fundamental TypeScript concepts including: + * - Primitive types + * - Object types (interfaces, type aliases) + * - Union and intersection types + * - Type inference and narrowing + * - Function types + */ + +// ============================================================================ +// Primitive Types +// ============================================================================ + +const message: string = 'Hello, TypeScript!' +const count: number = 42 +const isActive: boolean = true +const nothing: null = null +const notDefined: undefined = undefined + +// ============================================================================ +// Object Types +// ============================================================================ + +// Interface definition +interface User { + id: string + name: string + email: string + age?: number // Optional property + readonly createdAt: Date // Readonly property +} + +// Type alias definition +type Product = { + id: string + name: string + price: number + category: string +} + +// Creating objects +const user: User = { + id: '1', + name: 'Alice', + email: 'alice@example.com', + createdAt: new Date(), +} + +const product: Product = { + id: 'p1', + name: 'Laptop', + price: 999, + category: 'electronics', +} + +// ============================================================================ +// Union Types +// ============================================================================ + +type Status = 'idle' | 'loading' | 'success' | 'error' +type ID = string | number + +function formatId(id: ID): string { + if (typeof id === 'string') { + return id.toUpperCase() + } + return id.toString() +} + +// Discriminated unions +type ApiResponse = + | { success: true; data: User } + | { success: false; error: string } + +function handleResponse(response: ApiResponse) { + if (response.success) { + // TypeScript knows response.data exists here + console.log(response.data.name) + } else { + // TypeScript knows response.error exists here + console.error(response.error) + } +} + +// ============================================================================ +// Intersection Types +// ============================================================================ + +type Timestamped = { + createdAt: Date + updatedAt: Date +} + +type TimestampedUser = User & Timestamped + +const timestampedUser: TimestampedUser = { + id: '1', + name: 'Bob', + email: 'bob@example.com', + createdAt: new Date(), + updatedAt: new Date(), +} + +// ============================================================================ +// Array Types +// ============================================================================ + +const numbers: number[] = [1, 2, 3, 4, 5] +const strings: Array<string> = ['a', 'b', 'c'] +const users: User[] = [user, timestampedUser] + +// Readonly arrays +const immutableNumbers: readonly number[] = [1, 2, 3] +// immutableNumbers.push(4) // Error: push does not exist on readonly array + +// ============================================================================ +// Tuple Types +// ============================================================================ + +type Point = [number, number] +type NamedPoint = [x: number, y: number, z?: number] + +const point: Point = [10, 20] +const namedPoint: NamedPoint = [10, 20, 30] + +// ============================================================================ +// Function Types +// ============================================================================ + +// Function declaration +function add(a: number, b: number): number { + return a + b +} + +// Arrow function +const subtract = (a: number, b: number): number => a - b + +// Function type alias +type MathOperation = (a: number, b: number) => number + +const multiply: MathOperation = (a, b) => a * b + +// Optional parameters +function greet(name: string, greeting?: string): string { + return `${greeting ?? 'Hello'}, ${name}!` +} + +// Default parameters +function createUser(name: string, role: string = 'user'): User { + return { + id: Math.random().toString(), + name, + email: `${name.toLowerCase()}@example.com`, + createdAt: new Date(), + } +} + +// Rest parameters +function sum(...numbers: number[]): number { + return numbers.reduce((acc, n) => acc + n, 0) +} + +// ============================================================================ +// Type Inference +// ============================================================================ + +// Type is inferred as string +let inferredString = 'hello' + +// Type is inferred as number +let inferredNumber = 42 + +// Type is inferred as { name: string; age: number } +let inferredObject = { + name: 'Alice', + age: 30, +} + +// Return type is inferred as number +function inferredReturn(a: number, b: number) { + return a + b +} + +// ============================================================================ +// Type Narrowing +// ============================================================================ + +// typeof guard +function processValue(value: string | number) { + if (typeof value === 'string') { + // value is string here + return value.toUpperCase() + } + // value is number here + return value.toFixed(2) +} + +// Truthiness narrowing +function printName(name: string | null | undefined) { + if (name) { + // name is string here + console.log(name.toUpperCase()) + } +} + +// Equality narrowing +function example(x: string | number, y: string | boolean) { + if (x === y) { + // x and y are both string here + console.log(x.toUpperCase(), y.toLowerCase()) + } +} + +// in operator narrowing +type Fish = { swim: () => void } +type Bird = { fly: () => void } + +function move(animal: Fish | Bird) { + if ('swim' in animal) { + // animal is Fish here + animal.swim() + } else { + // animal is Bird here + animal.fly() + } +} + +// instanceof narrowing +function processError(error: Error | string) { + if (error instanceof Error) { + // error is Error here + console.error(error.message) + } else { + // error is string here + console.error(error) + } +} + +// ============================================================================ +// Type Predicates (Custom Type Guards) +// ============================================================================ + +function isUser(value: unknown): value is User { + return ( + typeof value === 'object' && + value !== null && + 'id' in value && + 'name' in value && + 'email' in value + ) +} + +function processData(data: unknown) { + if (isUser(data)) { + // data is User here + console.log(data.name) + } +} + +// ============================================================================ +// Const Assertions +// ============================================================================ + +// Without const assertion +const mutableConfig = { + host: 'localhost', + port: 8080, +} +// mutableConfig.host = 'example.com' // OK + +// With const assertion +const immutableConfig = { + host: 'localhost', + port: 8080, +} as const +// immutableConfig.host = 'example.com' // Error: cannot assign to readonly property + +// Array with const assertion +const directions = ['north', 'south', 'east', 'west'] as const +// Type: readonly ["north", "south", "east", "west"] + +// ============================================================================ +// Literal Types +// ============================================================================ + +type Direction = 'north' | 'south' | 'east' | 'west' +type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' +type DiceValue = 1 | 2 | 3 | 4 | 5 | 6 + +function move(direction: Direction, steps: number) { + console.log(`Moving ${direction} by ${steps} steps`) +} + +move('north', 10) // OK +// move('up', 10) // Error: "up" is not assignable to Direction + +// ============================================================================ +// Index Signatures +// ============================================================================ + +interface StringMap { + [key: string]: string +} + +const translations: StringMap = { + hello: 'Hola', + goodbye: 'Adiós', + thanks: 'Gracias', +} + +// ============================================================================ +// Utility Functions +// ============================================================================ + +// Type-safe object keys +function getObjectKeys<T extends object>(obj: T): Array<keyof T> { + return Object.keys(obj) as Array<keyof T> +} + +// Type-safe property access +function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { + return obj[key] +} + +const userName = getProperty(user, 'name') // Type: string +const userAge = getProperty(user, 'age') // Type: number | undefined + +// ============================================================================ +// Named Return Values (Go-style) +// ============================================================================ + +function parseJSON(json: string): { data: unknown | null; err: Error | null } { + let data: unknown | null = null + let err: Error | null = null + + try { + data = JSON.parse(json) + } catch (error) { + err = error instanceof Error ? error : new Error(String(error)) + } + + return { data, err } +} + +// Usage +const { data, err } = parseJSON('{"name": "Alice"}') +if (err) { + console.error('Failed to parse JSON:', err.message) +} else { + console.log('Parsed data:', data) +} + +// ============================================================================ +// Exports +// ============================================================================ + +export type { User, Product, Status, ID, ApiResponse, TimestampedUser } +export { formatId, handleResponse, processValue, isUser, getProperty, parseJSON } + diff --git a/.claude/skills/typescript/quick-reference.md b/.claude/skills/typescript/quick-reference.md new file mode 100644 index 0000000..260d1c6 --- /dev/null +++ b/.claude/skills/typescript/quick-reference.md @@ -0,0 +1,395 @@ +# TypeScript Quick Reference + +Quick lookup guide for common TypeScript patterns and syntax. + +## Basic Types + +```typescript +// Primitives +string, number, boolean, null, undefined, symbol, bigint + +// Special types +any // Avoid - disables type checking +unknown // Type-safe alternative to any +void // No return value +never // Never returns + +// Arrays +number[] +Array<string> +readonly number[] + +// Tuples +[string, number] +[x: number, y: number] + +// Objects +{ name: string; age: number } +Record<string, number> +``` + +## Type Declarations + +```typescript +// Interface +interface User { + id: string + name: string + age?: number // Optional + readonly createdAt: Date // Readonly +} + +// Type alias +type Status = 'idle' | 'loading' | 'success' | 'error' +type ID = string | number +type Point = { x: number; y: number } + +// Function type +type Callback = (data: string) => void +type MathOp = (a: number, b: number) => number +``` + +## Union & Intersection + +```typescript +// Union (OR) +string | number +type Result = Success | Error + +// Intersection (AND) +A & B +type Combined = User & Timestamped + +// Discriminated union +type State = + | { status: 'idle' } + | { status: 'loading' } + | { status: 'success'; data: Data } + | { status: 'error'; error: Error } +``` + +## Generics + +```typescript +// Generic function +function identity<T>(value: T): T + +// Generic interface +interface Box<T> { value: T } + +// Generic with constraint +function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] + +// Multiple type parameters +function merge<T, U>(a: T, b: U): T & U + +// Default type parameter +interface Response<T = unknown> { data: T } +``` + +## Utility Types + +```typescript +Partial<T> // Make all optional +Required<T> // Make all required +Readonly<T> // Make all readonly +Pick<T, K> // Select properties +Omit<T, K> // Exclude properties +Record<K, T> // Object with specific keys +Exclude<T, U> // Remove from union +Extract<T, U> // Extract from union +NonNullable<T> // Remove null/undefined +ReturnType<T> // Get function return type +Parameters<T> // Get function parameters +Awaited<T> // Unwrap Promise +``` + +## Type Guards + +```typescript +// typeof +if (typeof value === 'string') { } + +// instanceof +if (error instanceof Error) { } + +// in operator +if ('property' in object) { } + +// Custom type guard +function isUser(value: unknown): value is User { + return typeof value === 'object' && value !== null && 'id' in value +} + +// Assertion function +function assertIsString(value: unknown): asserts value is string { + if (typeof value !== 'string') throw new Error() +} +``` + +## Advanced Types + +```typescript +// Conditional types +type IsString<T> = T extends string ? true : false + +// Mapped types +type Nullable<T> = { [K in keyof T]: T[K] | null } + +// Template literal types +type EventName<T extends string> = `on${Capitalize<T>}` + +// Key remapping +type Getters<T> = { + [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] +} + +// infer keyword +type Flatten<T> = T extends Array<infer U> ? U : T +``` + +## Functions + +```typescript +// Function declaration +function add(a: number, b: number): number { return a + b } + +// Arrow function +const subtract = (a: number, b: number): number => a - b + +// Optional parameters +function greet(name: string, greeting?: string): string { } + +// Default parameters +function create(name: string, role = 'user'): User { } + +// Rest parameters +function sum(...numbers: number[]): number { } + +// Overloads +function format(value: string): string +function format(value: number): string +function format(value: string | number): string { } +``` + +## Classes + +```typescript +class User { + // Properties + private id: string + public name: string + protected age: number + readonly createdAt: Date + + // Constructor + constructor(name: string) { + this.name = name + this.createdAt = new Date() + } + + // Methods + greet(): string { + return `Hello, ${this.name}` + } + + // Static + static create(name: string): User { + return new User(name) + } + + // Getters/Setters + get displayName(): string { + return this.name.toUpperCase() + } +} + +// Inheritance +class Admin extends User { + constructor(name: string, public permissions: string[]) { + super(name) + } +} + +// Abstract class +abstract class Animal { + abstract makeSound(): void +} +``` + +## React Patterns + +```typescript +// Component props +interface ButtonProps { + variant?: 'primary' | 'secondary' + onClick?: () => void + children: React.ReactNode +} + +export function Button({ variant = 'primary', onClick, children }: ButtonProps) { } + +// Generic component +interface ListProps<T> { + items: T[] + renderItem: (item: T) => React.ReactNode +} + +export function List<T>({ items, renderItem }: ListProps<T>) { } + +// Hooks +const [state, setState] = useState<string>('') +const [data, setData] = useState<User | null>(null) + +// Context +interface AuthContextType { + user: User | null + login: () => Promise<void> +} + +const AuthContext = createContext<AuthContextType | undefined>(undefined) + +export function useAuth(): AuthContextType { + const context = useContext(AuthContext) + if (!context) throw new Error('useAuth must be used within AuthProvider') + return context +} +``` + +## Common Patterns + +### Result Type +```typescript +type Result<T, E = Error> = + | { success: true; data: T } + | { success: false; error: E } +``` + +### Option Type +```typescript +type Option<T> = Some<T> | None +interface Some<T> { _tag: 'Some'; value: T } +interface None { _tag: 'None' } +``` + +### Branded Types +```typescript +type Brand<K, T> = K & { __brand: T } +type UserId = Brand<string, 'UserId'> +``` + +### Named Returns (Go-style) +```typescript +function parseJSON(json: string): { data: unknown | null; err: Error | null } { + let data: unknown | null = null + let err: Error | null = null + + try { + data = JSON.parse(json) + } catch (error) { + err = error instanceof Error ? error : new Error(String(error)) + } + + return { data, err } +} +``` + +## Type Assertions + +```typescript +// as syntax (preferred) +const value = input as string + +// Angle bracket syntax (not in JSX) +const value = <string>input + +// as const +const config = { host: 'localhost' } as const + +// Non-null assertion (use sparingly) +const element = document.getElementById('app')! +``` + +## Type Narrowing + +```typescript +// Control flow +if (value !== null) { + // value is non-null here +} + +// Switch with discriminated unions +switch (state.status) { + case 'success': + console.log(state.data) // TypeScript knows data exists + break + case 'error': + console.log(state.error) // TypeScript knows error exists + break +} + +// Optional chaining +user?.profile?.name + +// Nullish coalescing +const name = user?.name ?? 'Anonymous' +``` + +## Module Syntax + +```typescript +// Named exports +export function helper() { } +export const CONFIG = { } + +// Default export +export default class App { } + +// Type-only imports/exports +import type { User } from './types' +export type { User } + +// Namespace imports +import * as utils from './utils' +``` + +## TSConfig Essentials + +```json +{ + "compilerOptions": { + "strict": true, + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true + } +} +``` + +## Common Errors & Fixes + +| Error | Fix | +|-------|-----| +| Type 'X' is not assignable to type 'Y' | Check type compatibility, use type assertion if needed | +| Object is possibly 'null' | Use optional chaining `?.` or null check | +| Cannot find module | Install `@types/package-name` | +| Implicit any | Add type annotation or enable strict mode | +| Property does not exist | Check object shape, use type guard | + +## Best Practices + +1. Enable `strict` mode in tsconfig.json +2. Avoid `any`, use `unknown` instead +3. Use discriminated unions for state +4. Leverage type inference +5. Use `const` assertions for immutable data +6. Create custom type guards for runtime safety +7. Use utility types instead of recreating +8. Document complex types with JSDoc +9. Prefer interfaces for objects, types for unions +10. Use branded types for domain-specific primitives + diff --git a/.claude/skills/typescript/references/common-patterns.md b/.claude/skills/typescript/references/common-patterns.md new file mode 100644 index 0000000..b73d42b --- /dev/null +++ b/.claude/skills/typescript/references/common-patterns.md @@ -0,0 +1,756 @@ +# TypeScript Common Patterns Reference + +This document contains commonly used TypeScript patterns and idioms from real-world applications. + +## React Patterns + +### Component Props + +```typescript +// Basic props with children +interface ButtonProps { + variant?: 'primary' | 'secondary' | 'tertiary' + size?: 'sm' | 'md' | 'lg' + disabled?: boolean + onClick?: () => void + children: React.ReactNode +} + +export function Button({ + variant = 'primary', + size = 'md', + disabled = false, + onClick, + children, +}: ButtonProps) { + return ( + <button className={`btn-${variant} btn-${size}`} disabled={disabled} onClick={onClick}> + {children} + </button> + ) +} + +// Props extending HTML attributes +interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> { + label?: string + error?: string +} + +export function Input({ label, error, ...inputProps }: InputProps) { + return ( + <div> + {label && <label>{label}</label>} + <input {...inputProps} /> + {error && <span>{error}</span>} + </div> + ) +} + +// Generic component props +interface ListProps<T> { + items: T[] + renderItem: (item: T) => React.ReactNode + keyExtractor: (item: T) => string +} + +export function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) { + return ( + <ul> + {items.map((item) => ( + <li key={keyExtractor(item)}>{renderItem(item)}</li> + ))} + </ul> + ) +} +``` + +### Hooks + +```typescript +// Custom hook with return type +function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] { + const [storedValue, setStoredValue] = useState<T>(() => { + try { + const item = window.localStorage.getItem(key) + return item ? JSON.parse(item) : initialValue + } catch (error) { + return initialValue + } + }) + + const setValue = (value: T) => { + setStoredValue(value) + window.localStorage.setItem(key, JSON.stringify(value)) + } + + return [storedValue, setValue] +} + +// Hook with options object +interface UseFetchOptions<T> { + initialData?: T + onSuccess?: (data: T) => void + onError?: (error: Error) => void +} + +function useFetch<T>(url: string, options?: UseFetchOptions<T>) { + const [data, setData] = useState<T | undefined>(options?.initialData) + const [loading, setLoading] = useState(false) + const [error, setError] = useState<Error | null>(null) + + useEffect(() => { + let isCancelled = false + + const fetchData = async () => { + setLoading(true) + try { + const response = await fetch(url) + const json = await response.json() + if (!isCancelled) { + setData(json) + options?.onSuccess?.(json) + } + } catch (err) { + if (!isCancelled) { + const error = err instanceof Error ? err : new Error(String(err)) + setError(error) + options?.onError?.(error) + } + } finally { + if (!isCancelled) { + setLoading(false) + } + } + } + + fetchData() + + return () => { + isCancelled = true + } + }, [url]) + + return { data, loading, error } +} +``` + +### Context + +```typescript +// Type-safe context +interface AuthContextType { + user: User | null + login: (email: string, password: string) => Promise<void> + logout: () => void + isAuthenticated: boolean +} + +const AuthContext = createContext<AuthContextType | undefined>(undefined) + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState<User | null>(null) + + const login = async (email: string, password: string) => { + // Login logic + const user = await api.login(email, password) + setUser(user) + } + + const logout = () => { + setUser(null) + } + + const value: AuthContextType = { + user, + login, + logout, + isAuthenticated: user !== null, + } + + return <AuthContext.Provider value={value}>{children}</AuthContext.Provider> +} + +// Custom hook with proper error handling +export function useAuth(): AuthContextType { + const context = useContext(AuthContext) + if (context === undefined) { + throw new Error('useAuth must be used within AuthProvider') + } + return context +} +``` + +## API Response Patterns + +### Result Type Pattern + +```typescript +// Discriminated union for API responses +type Result<T, E = Error> = + | { success: true; data: T } + | { success: false; error: E } + +// Helper functions +function success<T>(data: T): Result<T> { + return { success: true, data } +} + +function failure<E = Error>(error: E): Result<never, E> { + return { success: false, error } +} + +// Usage +async function fetchUser(id: string): Promise<Result<User>> { + try { + const response = await fetch(`/api/users/${id}`) + if (!response.ok) { + return failure(new Error(`HTTP ${response.status}`)) + } + const data = await response.json() + return success(data) + } catch (error) { + return failure(error instanceof Error ? error : new Error(String(error))) + } +} + +// Consuming the result +const result = await fetchUser('123') +if (result.success) { + console.log(result.data.name) // Type-safe access +} else { + console.error(result.error.message) // Type-safe error handling +} +``` + +### Option Type Pattern + +```typescript +// Option/Maybe type for nullable values +type Option<T> = Some<T> | None + +interface Some<T> { + readonly _tag: 'Some' + readonly value: T +} + +interface None { + readonly _tag: 'None' +} + +// Constructors +function some<T>(value: T): Option<T> { + return { _tag: 'Some', value } +} + +function none(): Option<never> { + return { _tag: 'None' } +} + +// Helper functions +function isSome<T>(option: Option<T>): option is Some<T> { + return option._tag === 'Some' +} + +function isNone<T>(option: Option<T>): option is None { + return option._tag === 'None' +} + +function map<T, U>(option: Option<T>, fn: (value: T) => U): Option<U> { + return isSome(option) ? some(fn(option.value)) : none() +} + +function getOrElse<T>(option: Option<T>, defaultValue: T): T { + return isSome(option) ? option.value : defaultValue +} + +// Usage +function findUser(id: string): Option<User> { + const user = users.find((u) => u.id === id) + return user ? some(user) : none() +} + +const user = findUser('123') +const userName = getOrElse(map(user, (u) => u.name), 'Unknown') +``` + +## State Management Patterns + +### Discriminated Union for State + +```typescript +// State machine using discriminated unions +type FetchState<T> = + | { status: 'idle' } + | { status: 'loading' } + | { status: 'success'; data: T } + | { status: 'error'; error: Error } + +// Reducer pattern +type FetchAction<T> = + | { type: 'FETCH_START' } + | { type: 'FETCH_SUCCESS'; payload: T } + | { type: 'FETCH_ERROR'; error: Error } + | { type: 'RESET' } + +function fetchReducer<T>(state: FetchState<T>, action: FetchAction<T>): FetchState<T> { + switch (action.type) { + case 'FETCH_START': + return { status: 'loading' } + case 'FETCH_SUCCESS': + return { status: 'success', data: action.payload } + case 'FETCH_ERROR': + return { status: 'error', error: action.error } + case 'RESET': + return { status: 'idle' } + } +} + +// Usage in component +function UserProfile({ userId }: { userId: string }) { + const [state, dispatch] = useReducer(fetchReducer<User>, { status: 'idle' }) + + useEffect(() => { + dispatch({ type: 'FETCH_START' }) + fetchUser(userId) + .then((user) => dispatch({ type: 'FETCH_SUCCESS', payload: user })) + .catch((error) => dispatch({ type: 'FETCH_ERROR', error })) + }, [userId]) + + switch (state.status) { + case 'idle': + return <div>Ready to load</div> + case 'loading': + return <div>Loading...</div> + case 'success': + return <div>{state.data.name}</div> + case 'error': + return <div>Error: {state.error.message}</div> + } +} +``` + +### Store Pattern + +```typescript +// Type-safe store implementation +interface Store<T> { + getState: () => T + setState: (partial: Partial<T>) => void + subscribe: (listener: (state: T) => void) => () => void +} + +function createStore<T>(initialState: T): Store<T> { + let state = initialState + const listeners = new Set<(state: T) => void>() + + return { + getState: () => state, + setState: (partial) => { + state = { ...state, ...partial } + listeners.forEach((listener) => listener(state)) + }, + subscribe: (listener) => { + listeners.add(listener) + return () => listeners.delete(listener) + }, + } +} + +// Usage +interface AppState { + user: User | null + theme: 'light' | 'dark' +} + +const store = createStore<AppState>({ + user: null, + theme: 'light', +}) + +// React hook integration +function useStore<T, U>(store: Store<T>, selector: (state: T) => U): U { + const [value, setValue] = useState(() => selector(store.getState())) + + useEffect(() => { + const unsubscribe = store.subscribe((state) => { + setValue(selector(state)) + }) + return unsubscribe + }, [store, selector]) + + return value +} + +// Usage in component +function ThemeToggle() { + const theme = useStore(store, (state) => state.theme) + + return ( + <button + onClick={() => store.setState({ theme: theme === 'light' ? 'dark' : 'light' })} + > + Toggle Theme + </button> + ) +} +``` + +## Form Patterns + +### Form State Management + +```typescript +// Generic form state +interface FormState<T> { + values: T + errors: Partial<Record<keyof T, string>> + touched: Partial<Record<keyof T, boolean>> + isSubmitting: boolean +} + +// Form hook +function useForm<T extends Record<string, any>>( + initialValues: T, + validate: (values: T) => Partial<Record<keyof T, string>>, +) { + const [state, setState] = useState<FormState<T>>({ + values: initialValues, + errors: {}, + touched: {}, + isSubmitting: false, + }) + + const handleChange = <K extends keyof T>(field: K, value: T[K]) => { + setState((prev) => ({ + ...prev, + values: { ...prev.values, [field]: value }, + errors: { ...prev.errors, [field]: undefined }, + })) + } + + const handleBlur = <K extends keyof T>(field: K) => { + setState((prev) => ({ + ...prev, + touched: { ...prev.touched, [field]: true }, + })) + } + + const handleSubmit = async (onSubmit: (values: T) => Promise<void>) => { + const errors = validate(state.values) + + if (Object.keys(errors).length > 0) { + setState((prev) => ({ + ...prev, + errors, + touched: Object.keys(state.values).reduce( + (acc, key) => ({ ...acc, [key]: true }), + {}, + ), + })) + return + } + + setState((prev) => ({ ...prev, isSubmitting: true })) + try { + await onSubmit(state.values) + } finally { + setState((prev) => ({ ...prev, isSubmitting: false })) + } + } + + return { + values: state.values, + errors: state.errors, + touched: state.touched, + isSubmitting: state.isSubmitting, + handleChange, + handleBlur, + handleSubmit, + } +} + +// Usage +interface LoginFormValues { + email: string + password: string +} + +function LoginForm() { + const form = useForm<LoginFormValues>( + { email: '', password: '' }, + (values) => { + const errors: Partial<Record<keyof LoginFormValues, string>> = {} + if (!values.email) { + errors.email = 'Email is required' + } + if (!values.password) { + errors.password = 'Password is required' + } + return errors + }, + ) + + return ( + <form + onSubmit={(e) => { + e.preventDefault() + form.handleSubmit(async (values) => { + await login(values.email, values.password) + }) + }} + > + <input + value={form.values.email} + onChange={(e) => form.handleChange('email', e.target.value)} + onBlur={() => form.handleBlur('email')} + /> + {form.touched.email && form.errors.email && <span>{form.errors.email}</span>} + + <input + type="password" + value={form.values.password} + onChange={(e) => form.handleChange('password', e.target.value)} + onBlur={() => form.handleBlur('password')} + /> + {form.touched.password && form.errors.password && ( + <span>{form.errors.password}</span> + )} + + <button type="submit" disabled={form.isSubmitting}> + Login + </button> + </form> + ) +} +``` + +## Validation Patterns + +### Zod Integration + +```typescript +import { z } from 'zod' + +// Schema definition +const userSchema = z.object({ + id: z.string().uuid(), + name: z.string().min(1).max(100), + email: z.string().email(), + age: z.number().int().min(0).max(120), + role: z.enum(['admin', 'user', 'guest']), +}) + +// Extract type from schema +type User = z.infer<typeof userSchema> + +// Validation function +function validateUser(data: unknown): Result<User> { + const result = userSchema.safeParse(data) + if (result.success) { + return { success: true, data: result.data } + } + return { + success: false, + error: new Error(result.error.errors.map((e) => e.message).join(', ')), + } +} + +// API integration +async function createUser(data: unknown): Promise<Result<User>> { + const validation = validateUser(data) + if (!validation.success) { + return validation + } + + try { + const response = await fetch('/api/users', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(validation.data), + }) + + if (!response.ok) { + return failure(new Error(`HTTP ${response.status}`)) + } + + const user = await response.json() + return success(user) + } catch (error) { + return failure(error instanceof Error ? error : new Error(String(error))) + } +} +``` + +## Builder Pattern + +```typescript +// Fluent builder pattern +class QueryBuilder<T> { + private filters: Array<(item: T) => boolean> = [] + private sortFn?: (a: T, b: T) => number + private limitValue?: number + + where(predicate: (item: T) => boolean): this { + this.filters.push(predicate) + return this + } + + sortBy(compareFn: (a: T, b: T) => number): this { + this.sortFn = compareFn + return this + } + + limit(count: number): this { + this.limitValue = count + return this + } + + execute(data: T[]): T[] { + let result = data + + // Apply filters + this.filters.forEach((filter) => { + result = result.filter(filter) + }) + + // Apply sorting + if (this.sortFn) { + result = result.sort(this.sortFn) + } + + // Apply limit + if (this.limitValue !== undefined) { + result = result.slice(0, this.limitValue) + } + + return result + } +} + +// Usage +interface Product { + id: string + name: string + price: number + category: string +} + +const products: Product[] = [ + /* ... */ +] + +const query = new QueryBuilder<Product>() + .where((p) => p.category === 'electronics') + .where((p) => p.price < 1000) + .sortBy((a, b) => a.price - b.price) + .limit(10) + .execute(products) +``` + +## Factory Pattern + +```typescript +// Abstract factory pattern with TypeScript +interface Button { + render: () => string + onClick: () => void +} + +interface ButtonFactory { + createButton: (label: string, onClick: () => void) => Button +} + +class PrimaryButton implements Button { + constructor(private label: string, private clickHandler: () => void) {} + + render() { + return `<button class="primary">${this.label}</button>` + } + + onClick() { + this.clickHandler() + } +} + +class SecondaryButton implements Button { + constructor(private label: string, private clickHandler: () => void) {} + + render() { + return `<button class="secondary">${this.label}</button>` + } + + onClick() { + this.clickHandler() + } +} + +class PrimaryButtonFactory implements ButtonFactory { + createButton(label: string, onClick: () => void): Button { + return new PrimaryButton(label, onClick) + } +} + +class SecondaryButtonFactory implements ButtonFactory { + createButton(label: string, onClick: () => void): Button { + return new SecondaryButton(label, onClick) + } +} + +// Usage +function createUI(factory: ButtonFactory) { + const button = factory.createButton('Click me', () => console.log('Clicked!')) + return button.render() +} +``` + +## Named Return Variables Pattern + +```typescript +// Following Go-style named returns +function parseUser(data: unknown): { user: User | null; err: Error | null } { + let user: User | null = null + let err: Error | null = null + + try { + user = userSchema.parse(data) + } catch (error) { + err = error instanceof Error ? error : new Error(String(error)) + } + + return { user, err } +} + +// With explicit naming +function fetchData(url: string): { + data: unknown | null + status: number + err: Error | null +} { + let data: unknown | null = null + let status = 0 + let err: Error | null = null + + try { + const response = fetch(url) + // Process response + } catch (error) { + err = error instanceof Error ? error : new Error(String(error)) + } + + return { data, status, err } +} +``` + +## Best Practices + +1. **Use discriminated unions** for type-safe state management +2. **Leverage generic types** for reusable components and hooks +3. **Extract types from Zod schemas** for runtime + compile-time safety +4. **Use Result/Option types** for explicit error handling +5. **Create builder patterns** for complex object construction +6. **Use factory patterns** for flexible object creation +7. **Type context properly** to catch usage errors at compile time +8. **Prefer const assertions** for immutable configurations +9. **Use branded types** for domain-specific primitives +10. **Document patterns** with JSDoc for team knowledge sharing + diff --git a/.claude/skills/typescript/references/type-system.md b/.claude/skills/typescript/references/type-system.md new file mode 100644 index 0000000..c7cac7b --- /dev/null +++ b/.claude/skills/typescript/references/type-system.md @@ -0,0 +1,804 @@ +# TypeScript Type System Reference + +## Overview + +TypeScript's type system is structural (duck-typed) rather than nominal. Two types are compatible if their structure matches, regardless of their names. + +## Primitive Types + +### Basic Primitives + +```typescript +let str: string = 'hello' +let num: number = 42 +let bool: boolean = true +let nul: null = null +let undef: undefined = undefined +let sym: symbol = Symbol('key') +let big: bigint = 100n +``` + +### Special Types + +**any** - Disables type checking (avoid when possible): +```typescript +let anything: any = 'string' +anything = 42 // OK +anything.nonExistent() // OK at compile time, error at runtime +``` + +**unknown** - Type-safe alternative to any (requires type checking): +```typescript +let value: unknown = 'string' +// value.toUpperCase() // Error: must narrow type first + +if (typeof value === 'string') { + value.toUpperCase() // OK after narrowing +} +``` + +**void** - Absence of a value (function return type): +```typescript +function log(message: string): void { + console.log(message) +} +``` + +**never** - Value that never occurs (exhaustive checks, infinite loops): +```typescript +function throwError(message: string): never { + throw new Error(message) +} + +function exhaustiveCheck(value: never): never { + throw new Error(`Unhandled case: ${value}`) +} +``` + +## Object Types + +### Interfaces + +```typescript +// Basic interface +interface User { + id: string + name: string + email: string +} + +// Optional properties +interface Product { + id: string + name: string + description?: string // Optional +} + +// Readonly properties +interface Config { + readonly apiUrl: string + readonly timeout: number +} + +// Index signatures +interface Dictionary { + [key: string]: string +} + +// Method signatures +interface Calculator { + add(a: number, b: number): number + subtract(a: number, b: number): number +} + +// Extending interfaces +interface Employee extends User { + role: string + department: string +} + +// Multiple inheritance +interface Admin extends User, Employee { + permissions: string[] +} +``` + +### Type Aliases + +```typescript +// Basic type alias +type ID = string | number + +// Object type +type Point = { + x: number + y: number +} + +// Union type +type Status = 'idle' | 'loading' | 'success' | 'error' + +// Intersection type +type Timestamped = { + createdAt: Date + updatedAt: Date +} + +type TimestampedUser = User & Timestamped + +// Function type +type Callback = (data: string) => void + +// Generic type alias +type Result<T> = { success: true; data: T } | { success: false; error: string } +``` + +### Interface vs Type Alias + +**Use interface when:** +- Defining object shapes +- Need declaration merging +- Building public API types that others might extend + +**Use type when:** +- Creating unions or intersections +- Working with mapped types +- Need conditional types +- Defining primitive aliases + +## Array and Tuple Types + +### Arrays + +```typescript +// Array syntax +let numbers: number[] = [1, 2, 3] +let strings: Array<string> = ['a', 'b', 'c'] + +// Readonly arrays +let immutable: readonly number[] = [1, 2, 3] +let alsoImmutable: ReadonlyArray<string> = ['a', 'b'] +``` + +### Tuples + +```typescript +// Fixed-length, mixed-type arrays +type Point = [number, number] +type NamedPoint = [x: number, y: number] + +// Optional elements +type OptionalTuple = [string, number?] + +// Rest elements +type StringNumberBooleans = [string, number, ...boolean[]] + +// Readonly tuples +type ReadonlyPair = readonly [string, number] +``` + +## Union and Intersection Types + +### Union Types + +```typescript +// Value can be one of several types +type StringOrNumber = string | number + +function format(value: StringOrNumber): string { + if (typeof value === 'string') { + return value + } + return value.toString() +} + +// Discriminated unions +type Shape = + | { kind: 'circle'; radius: number } + | { kind: 'square'; size: number } + | { kind: 'rectangle'; width: number; height: number } + +function area(shape: Shape): number { + switch (shape.kind) { + case 'circle': + return Math.PI * shape.radius ** 2 + case 'square': + return shape.size ** 2 + case 'rectangle': + return shape.width * shape.height + } +} +``` + +### Intersection Types + +```typescript +// Combine multiple types +type Draggable = { + drag: () => void +} + +type Resizable = { + resize: () => void +} + +type UIWidget = Draggable & Resizable + +const widget: UIWidget = { + drag: () => console.log('dragging'), + resize: () => console.log('resizing'), +} +``` + +## Literal Types + +### String Literal Types + +```typescript +type Direction = 'north' | 'south' | 'east' | 'west' +type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' + +function move(direction: Direction) { + // direction can only be one of the four values +} +``` + +### Number Literal Types + +```typescript +type DiceValue = 1 | 2 | 3 | 4 | 5 | 6 +type PowerOfTwo = 1 | 2 | 4 | 8 | 16 | 32 +``` + +### Boolean Literal Types + +```typescript +type Yes = true +type No = false +``` + +### Template Literal Types + +```typescript +// String manipulation at type level +type EventName<T extends string> = `on${Capitalize<T>}` +type ClickEvent = EventName<'click'> // "onClick" + +// Combining literals +type Color = 'red' | 'blue' | 'green' +type Shade = 'light' | 'dark' +type ColorShade = `${Shade}-${Color}` // "light-red" | "light-blue" | ... + +// Extract patterns +type EmailLocaleIDs = 'welcome_email' | 'email_heading' +type FooterLocaleIDs = 'footer_title' | 'footer_sendoff' +type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id` +``` + +## Type Inference + +### Automatic Inference + +```typescript +// Type inferred as string +let message = 'hello' + +// Type inferred as number[] +let numbers = [1, 2, 3] + +// Type inferred as { name: string; age: number } +let person = { + name: 'Alice', + age: 30, +} + +// Return type inferred +function add(a: number, b: number) { + return a + b // Returns number +} +``` + +### Const Assertions + +```typescript +// Without const assertion +let colors1 = ['red', 'green', 'blue'] // Type: string[] + +// With const assertion +let colors2 = ['red', 'green', 'blue'] as const // Type: readonly ["red", "green", "blue"] + +// Object with const assertion +const config = { + host: 'localhost', + port: 8080, +} as const // All properties become readonly with literal types +``` + +### Type Inference in Generics + +```typescript +// Generic type inference from usage +function identity<T>(value: T): T { + return value +} + +let str = identity('hello') // T inferred as string +let num = identity(42) // T inferred as number + +// Multiple type parameters +function pair<T, U>(first: T, second: U): [T, U] { + return [first, second] +} + +let p = pair('hello', 42) // [string, number] +``` + +## Type Narrowing + +### typeof Guards + +```typescript +function padLeft(value: string, padding: string | number) { + if (typeof padding === 'number') { + // padding is number here + return ' '.repeat(padding) + value + } + // padding is string here + return padding + value +} +``` + +### instanceof Guards + +```typescript +class Dog { + bark() { + console.log('Woof!') + } +} + +class Cat { + meow() { + console.log('Meow!') + } +} + +function makeSound(animal: Dog | Cat) { + if (animal instanceof Dog) { + animal.bark() + } else { + animal.meow() + } +} +``` + +### in Operator + +```typescript +type Fish = { swim: () => void } +type Bird = { fly: () => void } + +function move(animal: Fish | Bird) { + if ('swim' in animal) { + animal.swim() + } else { + animal.fly() + } +} +``` + +### Equality Narrowing + +```typescript +function example(x: string | number, y: string | boolean) { + if (x === y) { + // x and y are both string here + x.toUpperCase() + y.toLowerCase() + } +} +``` + +### Control Flow Analysis + +```typescript +function example(value: string | null) { + if (value === null) { + return + } + // value is string here (null eliminated) + console.log(value.toUpperCase()) +} +``` + +### Type Predicates (Custom Type Guards) + +```typescript +function isString(value: unknown): value is string { + return typeof value === 'string' +} + +function example(value: unknown) { + if (isString(value)) { + // value is string here + console.log(value.toUpperCase()) + } +} + +// More complex example +interface User { + id: string + name: string +} + +function isUser(value: unknown): value is User { + return ( + typeof value === 'object' && + value !== null && + 'id' in value && + 'name' in value && + typeof (value as User).id === 'string' && + typeof (value as User).name === 'string' + ) +} +``` + +### Assertion Functions + +```typescript +function assert(condition: unknown, message?: string): asserts condition { + if (!condition) { + throw new Error(message || 'Assertion failed') + } +} + +function assertIsString(value: unknown): asserts value is string { + if (typeof value !== 'string') { + throw new Error('Value must be a string') + } +} + +function example(value: unknown) { + assertIsString(value) + // value is string here + console.log(value.toUpperCase()) +} +``` + +## Generic Types + +### Basic Generics + +```typescript +// Generic function +function first<T>(items: T[]): T | undefined { + return items[0] +} + +// Generic interface +interface Box<T> { + value: T +} + +// Generic type alias +type Result<T> = { success: true; data: T } | { success: false; error: string } + +// Generic class +class Stack<T> { + private items: T[] = [] + + push(item: T) { + this.items.push(item) + } + + pop(): T | undefined { + return this.items.pop() + } +} +``` + +### Generic Constraints + +```typescript +// Constrain to specific type +function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { + return obj[key] +} + +// Constrain to interface +interface HasLength { + length: number +} + +function logLength<T extends HasLength>(item: T): void { + console.log(item.length) +} + +logLength('string') // OK +logLength([1, 2, 3]) // OK +logLength({ length: 10 }) // OK +// logLength(42) // Error: number doesn't have length +``` + +### Default Generic Parameters + +```typescript +interface Response<T = unknown> { + data: T + status: number +} + +// Uses default +let response1: Response = { data: 'anything', status: 200 } + +// Explicitly typed +let response2: Response<User> = { data: user, status: 200 } +``` + +### Generic Utility Functions + +```typescript +// Pick specific properties +function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> { + const result = {} as Pick<T, K> + keys.forEach((key) => { + result[key] = obj[key] + }) + return result +} + +// Map array +function map<T, U>(items: T[], fn: (item: T) => U): U[] { + return items.map(fn) +} +``` + +## Advanced Type Features + +### Conditional Types + +```typescript +// Basic conditional type +type IsString<T> = T extends string ? true : false + +type A = IsString<string> // true +type B = IsString<number> // false + +// Distributive conditional types +type ToArray<T> = T extends any ? T[] : never + +type StrArrOrNumArr = ToArray<string | number> // string[] | number[] + +// Infer keyword +type Flatten<T> = T extends Array<infer U> ? U : T + +type Str = Flatten<string[]> // string +type Num = Flatten<number> // number + +// ReturnType implementation +type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never +``` + +### Mapped Types + +```typescript +// Make all properties optional +type Partial<T> = { + [K in keyof T]?: T[K] +} + +// Make all properties required +type Required<T> = { + [K in keyof T]-?: T[K] +} + +// Make all properties readonly +type Readonly<T> = { + readonly [K in keyof T]: T[K] +} + +// Transform keys +type Getters<T> = { + [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] +} + +interface Person { + name: string + age: number +} + +type PersonGetters = Getters<Person> +// { +// getName: () => string +// getAge: () => number +// } +``` + +### Key Remapping + +```typescript +// Filter keys +type RemoveKindField<T> = { + [K in keyof T as Exclude<K, 'kind'>]: T[K] +} + +// Conditional key inclusion +type PickByType<T, U> = { + [K in keyof T as T[K] extends U ? K : never]: T[K] +} + +interface Model { + id: number + name: string + age: number + email: string +} + +type StringFields = PickByType<Model, string> // { name: string, email: string } +``` + +### Recursive Types + +```typescript +// JSON value type +type JSONValue = string | number | boolean | null | JSONObject | JSONArray + +interface JSONObject { + [key: string]: JSONValue +} + +interface JSONArray extends Array<JSONValue> {} + +// Tree structure +interface TreeNode<T> { + value: T + children?: TreeNode<T>[] +} + +// Deep readonly +type DeepReadonly<T> = { + readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K] +} +``` + +## Type Compatibility + +### Structural Typing + +```typescript +interface Point { + x: number + y: number +} + +interface Named { + name: string +} + +// Compatible if structure matches +let point: Point = { x: 0, y: 0 } +let namedPoint = { x: 0, y: 0, name: 'origin' } + +point = namedPoint // OK: namedPoint has x and y +``` + +### Variance + +**Covariance** (return types): +```typescript +interface Animal { + name: string +} + +interface Dog extends Animal { + breed: string +} + +let getDog: () => Dog +let getAnimal: () => Animal + +getAnimal = getDog // OK: Dog is assignable to Animal +``` + +**Contravariance** (parameter types): +```typescript +let handleAnimal: (animal: Animal) => void +let handleDog: (dog: Dog) => void + +handleDog = handleAnimal // OK: can pass Dog to function expecting Animal +``` + +## Index Types + +### Index Signatures + +```typescript +// String index +interface StringMap { + [key: string]: string +} + +// Number index +interface NumberArray { + [index: number]: number +} + +// Combine with named properties +interface MixedInterface { + length: number + [index: number]: string +} +``` + +### keyof Operator + +```typescript +interface Person { + name: string + age: number +} + +type PersonKeys = keyof Person // "name" | "age" + +function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { + return obj[key] +} +``` + +### Indexed Access Types + +```typescript +interface Person { + name: string + age: number + address: { + street: string + city: string + } +} + +type Name = Person['name'] // string +type Age = Person['age'] // number +type Address = Person['address'] // { street: string; city: string } +type AddressCity = Person['address']['city'] // string + +// Access multiple keys +type NameOrAge = Person['name' | 'age'] // string | number +``` + +## Branded Types + +```typescript +// Create nominal types from structural types +type Brand<K, T> = K & { __brand: T } + +type USD = Brand<number, 'USD'> +type EUR = Brand<number, 'EUR'> + +function makeUSD(amount: number): USD { + return amount as USD +} + +function makeEUR(amount: number): EUR { + return amount as EUR +} + +let usd = makeUSD(100) +let eur = makeEUR(100) + +// usd = eur // Error: different brands +``` + +## Best Practices + +1. **Prefer type inference** - Let TypeScript infer types when obvious +2. **Use strict null checks** - Enable strictNullChecks for better safety +3. **Avoid `any`** - Use `unknown` and narrow with type guards +4. **Use discriminated unions** - Better than loose unions for state +5. **Leverage const assertions** - Get narrow literal types +6. **Use branded types** - When structural typing isn't enough +7. **Document complex types** - Add JSDoc comments +8. **Extract reusable types** - DRY principle applies to types too +9. **Use utility types** - Leverage built-in transformation types +10. **Test your types** - Use type assertions to verify type correctness + diff --git a/.claude/skills/typescript/references/utility-types.md b/.claude/skills/typescript/references/utility-types.md new file mode 100644 index 0000000..6783be3 --- /dev/null +++ b/.claude/skills/typescript/references/utility-types.md @@ -0,0 +1,666 @@ +# TypeScript Utility Types Reference + +TypeScript provides several built-in utility types that help transform and manipulate types. These are implemented using advanced type features like mapped types and conditional types. + +## Property Modifiers + +### Partial\<T\> + +Makes all properties in `T` optional. + +```typescript +interface User { + id: string + name: string + email: string + age: number +} + +type PartialUser = Partial<User> +// { +// id?: string +// name?: string +// email?: string +// age?: number +// } + +// Useful for update operations +function updateUser(id: string, updates: Partial<User>) { + // Only update provided fields +} + +updateUser('123', { name: 'Alice' }) // OK +updateUser('123', { name: 'Alice', age: 30 }) // OK +``` + +### Required\<T\> + +Makes all properties in `T` required (removes optionality). + +```typescript +interface Config { + host?: string + port?: number + timeout?: number +} + +type RequiredConfig = Required<Config> +// { +// host: string +// port: number +// timeout: number +// } + +function initServer(config: RequiredConfig) { + // All properties are guaranteed to exist + console.log(config.host, config.port, config.timeout) +} +``` + +### Readonly\<T\> + +Makes all properties in `T` readonly. + +```typescript +interface MutablePoint { + x: number + y: number +} + +type ImmutablePoint = Readonly<MutablePoint> +// { +// readonly x: number +// readonly y: number +// } + +const point: ImmutablePoint = { x: 0, y: 0 } +// point.x = 10 // Error: Cannot assign to 'x' because it is a read-only property +``` + +### Mutable\<T\> (Custom) + +Removes readonly modifiers (not built-in, but useful pattern). + +```typescript +type Mutable<T> = { + -readonly [K in keyof T]: T[K] +} + +interface ReadonlyPerson { + readonly name: string + readonly age: number +} + +type MutablePerson = Mutable<ReadonlyPerson> +// { +// name: string +// age: number +// } +``` + +## Property Selection + +### Pick\<T, K\> + +Creates a type by picking specific properties from `T`. + +```typescript +interface User { + id: string + name: string + email: string + password: string + createdAt: Date +} + +type UserProfile = Pick<User, 'id' | 'name' | 'email'> +// { +// id: string +// name: string +// email: string +// } + +// Useful for API responses +function getUserProfile(id: string): UserProfile { + // Return only safe properties +} +``` + +### Omit\<T, K\> + +Creates a type by omitting specific properties from `T`. + +```typescript +interface User { + id: string + name: string + email: string + password: string +} + +type UserWithoutPassword = Omit<User, 'password'> +// { +// id: string +// name: string +// email: string +// } + +// Useful for public user data +function publishUser(user: User): UserWithoutPassword { + const { password, ...publicData } = user + return publicData +} +``` + +## Union Type Utilities + +### Exclude\<T, U\> + +Excludes types from `T` that are assignable to `U`. + +```typescript +type T1 = Exclude<'a' | 'b' | 'c', 'a'> // "b" | "c" +type T2 = Exclude<string | number | boolean, boolean> // string | number + +type EventType = 'click' | 'scroll' | 'mousemove' | 'keypress' +type UIEvent = Exclude<EventType, 'scroll'> // "click" | "mousemove" | "keypress" +``` + +### Extract\<T, U\> + +Extracts types from `T` that are assignable to `U`. + +```typescript +type T1 = Extract<'a' | 'b' | 'c', 'a' | 'f'> // "a" +type T2 = Extract<string | number | boolean, boolean> // boolean + +type Shape = 'circle' | 'square' | 'triangle' | 'rectangle' +type RoundedShape = Extract<Shape, 'circle'> // "circle" +``` + +### NonNullable\<T\> + +Excludes `null` and `undefined` from `T`. + +```typescript +type T1 = NonNullable<string | null | undefined> // string +type T2 = NonNullable<string | number | null> // string | number + +function processValue(value: string | null | undefined) { + if (value !== null && value !== undefined) { + const nonNull: NonNullable<typeof value> = value + // nonNull is guaranteed to be string + } +} +``` + +## Object Construction + +### Record\<K, T\> + +Constructs an object type with keys of type `K` and values of type `T`. + +```typescript +type PageInfo = Record<string, number> +// { [key: string]: number } + +const pages: PageInfo = { + home: 1, + about: 2, + contact: 3, +} + +// Useful for mapped objects +type UserRole = 'admin' | 'user' | 'guest' +type RolePermissions = Record<UserRole, string[]> + +const permissions: RolePermissions = { + admin: ['read', 'write', 'delete'], + user: ['read', 'write'], + guest: ['read'], +} + +// With specific keys +type ThemeColors = Record<'primary' | 'secondary' | 'accent', string> + +const colors: ThemeColors = { + primary: '#007bff', + secondary: '#6c757d', + accent: '#28a745', +} +``` + +## Function Utilities + +### Parameters\<T\> + +Extracts the parameter types of a function type as a tuple. + +```typescript +function createUser(name: string, age: number, email: string) { + // ... +} + +type CreateUserParams = Parameters<typeof createUser> +// [name: string, age: number, email: string] + +// Useful for higher-order functions +function withLogging<T extends (...args: any[]) => any>( + fn: T, + ...args: Parameters<T> +): ReturnType<T> { + console.log('Calling with:', args) + return fn(...args) +} +``` + +### ConstructorParameters\<T\> + +Extracts the parameter types of a constructor function type. + +```typescript +class User { + constructor(public name: string, public age: number) {} +} + +type UserConstructorParams = ConstructorParameters<typeof User> +// [name: string, age: number] + +function createUser(...args: UserConstructorParams): User { + return new User(...args) +} +``` + +### ReturnType\<T\> + +Extracts the return type of a function type. + +```typescript +function createUser() { + return { + id: '123', + name: 'Alice', + email: 'alice@example.com', + } +} + +type User = ReturnType<typeof createUser> +// { +// id: string +// name: string +// email: string +// } + +// Useful with async functions +async function fetchData() { + return { success: true, data: [1, 2, 3] } +} + +type FetchResult = ReturnType<typeof fetchData> +// Promise<{ success: boolean; data: number[] }> + +type UnwrappedResult = Awaited<FetchResult> +// { success: boolean; data: number[] } +``` + +### InstanceType\<T\> + +Extracts the instance type of a constructor function type. + +```typescript +class User { + name: string + constructor(name: string) { + this.name = name + } +} + +type UserInstance = InstanceType<typeof User> +// User + +function processUser(user: UserInstance) { + console.log(user.name) +} +``` + +### ThisParameterType\<T\> + +Extracts the type of the `this` parameter for a function type. + +```typescript +function toHex(this: Number) { + return this.toString(16) +} + +type ThisType = ThisParameterType<typeof toHex> // Number +``` + +### OmitThisParameter\<T\> + +Removes the `this` parameter from a function type. + +```typescript +function toHex(this: Number) { + return this.toString(16) +} + +type PlainFunction = OmitThisParameter<typeof toHex> +// () => string +``` + +## String Manipulation + +### Uppercase\<S\> + +Converts string literal type to uppercase. + +```typescript +type Greeting = 'hello' +type LoudGreeting = Uppercase<Greeting> // "HELLO" + +// Useful for constants +type HttpMethod = 'get' | 'post' | 'put' | 'delete' +type HttpMethodUppercase = Uppercase<HttpMethod> +// "GET" | "POST" | "PUT" | "DELETE" +``` + +### Lowercase\<S\> + +Converts string literal type to lowercase. + +```typescript +type Greeting = 'HELLO' +type QuietGreeting = Lowercase<Greeting> // "hello" +``` + +### Capitalize\<S\> + +Capitalizes the first letter of a string literal type. + +```typescript +type Event = 'click' | 'scroll' | 'mousemove' +type EventHandler = `on${Capitalize<Event>}` +// "onClick" | "onScroll" | "onMousemove" +``` + +### Uncapitalize\<S\> + +Uncapitalizes the first letter of a string literal type. + +```typescript +type Greeting = 'Hello' +type LowerGreeting = Uncapitalize<Greeting> // "hello" +``` + +## Async Utilities + +### Awaited\<T\> + +Unwraps the type of a Promise (recursively). + +```typescript +type T1 = Awaited<Promise<string>> // string +type T2 = Awaited<Promise<Promise<number>>> // number +type T3 = Awaited<boolean | Promise<string>> // boolean | string + +// Useful with async functions +async function fetchUser() { + return { id: '123', name: 'Alice' } +} + +type User = Awaited<ReturnType<typeof fetchUser>> +// { id: string; name: string } +``` + +## Custom Utility Types + +### DeepPartial\<T\> + +Makes all properties and nested properties optional. + +```typescript +type DeepPartial<T> = { + [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K] +} + +interface User { + id: string + profile: { + name: string + address: { + street: string + city: string + } + } +} + +type PartialUser = DeepPartial<User> +// All properties at all levels are optional +``` + +### DeepReadonly\<T\> + +Makes all properties and nested properties readonly. + +```typescript +type DeepReadonly<T> = { + readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K] +} + +interface User { + id: string + profile: { + name: string + address: { + street: string + city: string + } + } +} + +type ImmutableUser = DeepReadonly<User> +// All properties at all levels are readonly +``` + +### PartialBy\<T, K\> + +Makes specific properties optional. + +```typescript +type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>> + +interface User { + id: string + name: string + email: string + age: number +} + +type UserWithOptionalEmail = PartialBy<User, 'email' | 'age'> +// { +// id: string +// name: string +// email?: string +// age?: number +// } +``` + +### RequiredBy\<T, K\> + +Makes specific properties required. + +```typescript +type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>> + +interface User { + id?: string + name?: string + email?: string +} + +type UserWithRequiredId = RequiredBy<User, 'id'> +// { +// id: string +// name?: string +// email?: string +// } +``` + +### PickByType\<T, U\> + +Picks properties by their value type. + +```typescript +type PickByType<T, U> = { + [K in keyof T as T[K] extends U ? K : never]: T[K] +} + +interface User { + id: string + name: string + age: number + active: boolean +} + +type StringProperties = PickByType<User, string> +// { id: string; name: string } + +type NumberProperties = PickByType<User, number> +// { age: number } +``` + +### OmitByType\<T, U\> + +Omits properties by their value type. + +```typescript +type OmitByType<T, U> = { + [K in keyof T as T[K] extends U ? never : K]: T[K] +} + +interface User { + id: string + name: string + age: number + active: boolean +} + +type NonStringProperties = OmitByType<User, string> +// { age: number; active: boolean } +``` + +### Prettify\<T\> + +Flattens intersections for better IDE tooltips. + +```typescript +type Prettify<T> = { + [K in keyof T]: T[K] +} & {} + +type A = { a: string } +type B = { b: number } +type C = A & B + +type PrettyC = Prettify<C> +// Displays as: { a: string; b: number } +// Instead of: A & B +``` + +### ValueOf\<T\> + +Gets the union of all value types. + +```typescript +type ValueOf<T> = T[keyof T] + +interface Colors { + red: '#ff0000' + green: '#00ff00' + blue: '#0000ff' +} + +type ColorValue = ValueOf<Colors> +// "#ff0000" | "#00ff00" | "#0000ff" +``` + +### Nullable\<T\> + +Makes type nullable. + +```typescript +type Nullable<T> = T | null + +type NullableString = Nullable<string> // string | null +``` + +### Maybe\<T\> + +Makes type nullable or undefined. + +```typescript +type Maybe<T> = T | null | undefined + +type MaybeString = Maybe<string> // string | null | undefined +``` + +### UnionToIntersection\<U\> + +Converts union to intersection (advanced). + +```typescript +type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ( + k: infer I, +) => void + ? I + : never + +type Union = { a: string } | { b: number } +type Intersection = UnionToIntersection<Union> +// { a: string } & { b: number } +``` + +## Combining Utility Types + +Utility types can be composed for powerful transformations: + +```typescript +// Make specific properties optional and readonly +type PartialReadonly<T, K extends keyof T> = Readonly<Pick<T, K>> & + Partial<Omit<T, K>> + +interface User { + id: string + name: string + email: string + password: string +} + +type SafeUser = PartialReadonly<User, 'id' | 'name'> +// { +// readonly id: string +// readonly name: string +// email?: string +// password?: string +// } + +// Pick and make readonly +type ReadonlyPick<T, K extends keyof T> = Readonly<Pick<T, K>> + +// Omit and make required +type RequiredOmit<T, K extends keyof T> = Required<Omit<T, K>> +``` + +## Best Practices + +1. **Use built-in utilities first** - They're well-tested and optimized +2. **Compose utilities** - Combine utilities for complex transformations +3. **Create custom utilities** - For patterns you use frequently +4. **Name utilities clearly** - Make intent obvious from the name +5. **Document complex utilities** - Add JSDoc for non-obvious transformations +6. **Test utility types** - Use type assertions to verify behavior +7. **Avoid over-engineering** - Don't create utilities for one-off uses +8. **Consider readability** - Sometimes explicit types are clearer +9. **Use Prettify** - For better IDE tooltips with intersections +10. **Leverage keyof** - For type-safe property selection + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4f2bac0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,87 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Plebian Signer is a browser extension for managing multiple Nostr identities and signing events without exposing private keys to web applications. It implements NIP-07 (window.nostr interface) with support for NIP-04 and NIP-44 encryption. + +## Build Commands + +```bash +npm ci # Install dependencies +npm run build:chrome # Build Chrome extension (outputs to dist/chrome) +npm run build:firefox # Build Firefox extension (outputs to dist/firefox) +npm run watch:chrome # Development build with watch mode for Chrome +npm run watch:firefox # Development build with watch mode for Firefox +npm test # Run unit tests with Karma +npm run lint # Run ESLint +``` + +## Architecture + +### Monorepo Structure + +This is an Angular CLI monorepo with three projects: + +- **projects/chrome**: Chrome extension (MV3) +- **projects/firefox**: Firefox extension +- **projects/common**: Shared Angular library used by both extensions + +### Extension Architecture + +The extension follows a three-layer communication model: + +1. **Content Script** (`plebian-signer-content-script.ts`): Injected into web pages, bridges messages between page scripts and the background service worker + +2. **Injected Script** (`plebian-signer-extension.ts`): Injected into page context, exposes `window.nostr` API to web applications + +3. **Background Service Worker** (`background.ts`): Handles NIP-07 requests, manages permissions, performs cryptographic operations + +Message flow: Web App → `window.nostr` → Content Script → Background → Content Script → Web App + +### Storage Layers + +- **BrowserSyncHandler**: Encrypted vault data synced across browser instances (or local-only based on user preference) +- **BrowserSessionHandler**: Session-scoped decrypted data (unlocked vault state) +- **SignerMetaHandler**: Extension metadata (sync flow preference) + +Each browser (Chrome/Firefox) has its own handler implementations in `projects/{browser}/src/app/common/data/`. + +### Custom Webpack Build + +Both extensions use `@angular-builders/custom-webpack` to bundle additional entry points beyond the main Angular app: +- `background.ts` - Service worker +- `plebian-signer-extension.ts` - Page-injected script +- `plebian-signer-content-script.ts` - Content script +- `prompt.ts` - Permission prompt popup +- `options.ts` - Extension options page + +### Common Library + +The `@common` import alias resolves to `projects/common/src/public-api.ts`. Key exports: +- `StorageService`: Central data management with encryption/decryption +- `CryptoHelper`, `NostrHelper`: Cryptographic utilities +- Shared Angular components and pipes + +## Testing Extensions Locally + +**Chrome:** +1. Navigate to `chrome://extensions` +2. Enable "Developer mode" +3. Click "Load unpacked" +4. Select `dist/chrome` + +**Firefox:** +1. Navigate to `about:debugging` +2. Click "This Firefox" +3. Click "Load Temporary Add-on..." +4. Select a file in `dist/firefox` + +## NIP-07 Methods Implemented + +- `getPublicKey()` - Return public key +- `signEvent(event)` - Sign Nostr event +- `getRelays()` - Get configured relays +- `nip04.encrypt/decrypt` - NIP-04 encryption +- `nip44.encrypt/decrypt` - NIP-44 encryption diff --git a/NWC-IMPLEMENTATION.md b/NWC-IMPLEMENTATION.md new file mode 100644 index 0000000..e559dec --- /dev/null +++ b/NWC-IMPLEMENTATION.md @@ -0,0 +1,722 @@ +# Nostr Wallet Connect (NWC) Implementation Guide + +This document provides guidance for implementing NIP-47 (Nostr Wallet Connect) support in the Plebeian Signer browser extension. + +## What is Nostr Wallet Connect? + +Nostr Wallet Connect (NWC), defined in [NIP-47](https://nips.nostr.com/47), is a protocol that enables Nostr applications to interact with Lightning wallets through encrypted messages over Nostr relays. It allows apps to: + +- Request payments (pay invoices, keysend) +- Create invoices +- Check wallet balance +- List transactions +- Receive payment notifications + +The key benefit is that users can connect their Lightning wallet once and authorize apps to make payments without requiring manual approval for each transaction (within configured limits). + +## Wallets Supporting NWC + +### Self-Custodial Wallets + +| Wallet | Description | Link | +|--------|-------------|------| +| **Alby Hub** | Self-custodial Lightning node with seamless NWC service | [getalby.com](https://getalby.com) | +| **Phoenix** | Popular self-custodial Lightning wallet (via Phoenixd) | [phoenix.acinq.co](https://phoenix.acinq.co) | +| **Electrum** | Legendary on-chain and Lightning wallet | [electrum.org](https://electrum.org) | +| **Flash Wallet** | Self-custodial wallet built on Breez SDK | [paywithflash.com](https://paywithflash.com) | +| **Blitz** | Self-custodial wallet supporting Spark and Lightning | - | +| **LNbits** | Powerful suite of Bitcoin tools with NWC plugin | [lnbits.com](https://lnbits.com) | +| **Minibits** | Ecash wallet with focus on performance | [minibits.cash](https://minibits.cash) | +| **Cashu.me** | Ecash-based Cashu PWA wallet | [cashu.me](https://cashu.me) | + +### Custodial Wallets + +| Wallet | Description | +|--------|-------------| +| **Primal Wallet** | Integrated with Primal Nostr clients | +| **Coinos** | Free custodial web wallet and payment page | +| **Bitvora** | Custodial wallet and Bitcoin Lightning API platform | +| **Orange Pill App** | Social app with integrated custodial wallet | + +### Node Software with NWC Support + +- **Umbrel** - NWC app available in official marketplace +- **Start9** - Embassy OS with NWC support + +## NIP-47 Protocol Specification + +### Event Kinds + +| Kind | Purpose | Encrypted | +|------|---------|-----------| +| 13194 | Wallet Info (capabilities) | No | +| 23194 | Client Request | Yes | +| 23195 | Wallet Response | Yes | +| 23197 | Notifications (NIP-44) | Yes | +| 23196 | Notifications (NIP-04 legacy) | Yes | + +### Connection URI Format + +``` +nostr+walletconnect://<wallet_pubkey>?relay=<relay_url>&secret=<client_secret>&lud16=<optional_lightning_address> +``` + +**Example:** +``` +nostr+walletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&secret=71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100e4f3c +``` + +Components: +- **wallet_pubkey**: The wallet service's public key (used for encryption) +- **relay**: Nostr relay URL for communication (URL-encoded) +- **secret**: 32-byte hex client secret key for signing requests +- **lud16**: Optional Lightning address for the wallet + +### Supported Methods + +| Method | Description | +|--------|-------------| +| `get_info` | Get wallet info and supported methods | +| `get_balance` | Get wallet balance in millisatoshis | +| `pay_invoice` | Pay a BOLT11 Lightning invoice | +| `multi_pay_invoice` | Pay multiple invoices in batch | +| `pay_keysend` | Send spontaneous payment to a pubkey | +| `multi_pay_keysend` | Batch keysend payments | +| `make_invoice` | Create a new Lightning invoice | +| `lookup_invoice` | Look up invoice status | +| `list_transactions` | Get transaction history | + +### Error Codes + +| Code | Description | +|------|-------------| +| `RATE_LIMITED` | Too many requests | +| `NOT_IMPLEMENTED` | Method not supported | +| `INSUFFICIENT_BALANCE` | Not enough funds | +| `QUOTA_EXCEEDED` | Spending limit reached | +| `RESTRICTED` | Operation not allowed for this connection | +| `UNAUTHORIZED` | No wallet connected for this pubkey | +| `INTERNAL` | Internal wallet error | +| `UNSUPPORTED_ENCRYPTION` | Encryption method not supported | +| `OTHER` | Generic error | + +### Encryption + +NWC supports two encryption methods: +- **NIP-44 v2** (preferred): Modern, secure encryption +- **NIP-04** (deprecated): Legacy support only + +The wallet advertises supported encryption in the info event (kind 13194). Clients should prefer NIP-44 when available. + +## Request/Response Formats + +### Request Structure + +```json +{ + "method": "method_name", + "params": { + // method-specific parameters + } +} +``` + +### Response Structure + +```json +{ + "result_type": "method_name", + "error": { + "code": "ERROR_CODE", + "message": "Human readable message" + }, + "result": { + // method-specific response + } +} +``` + +### Method Examples + +#### get_info + +**Request:** +```json +{ + "method": "get_info", + "params": {} +} +``` + +**Response:** +```json +{ + "result_type": "get_info", + "result": { + "alias": "My Wallet", + "color": "#ff9900", + "pubkey": "03abcdef...", + "network": "mainnet", + "block_height": 800000, + "methods": ["pay_invoice", "get_balance", "make_invoice"], + "notifications": ["payment_received", "payment_sent"] + } +} +``` + +#### get_balance + +**Request:** +```json +{ + "method": "get_balance", + "params": {} +} +``` + +**Response:** +```json +{ + "result_type": "get_balance", + "result": { + "balance": 100000000 + } +} +``` + +Balance is in **millisatoshis** (1 sat = 1000 msats). + +#### pay_invoice + +**Request:** +```json +{ + "method": "pay_invoice", + "params": { + "invoice": "lnbc50n1p3...", + "amount": 5000, + "metadata": { + "comment": "Payment for coffee" + } + } +} +``` + +**Response:** +```json +{ + "result_type": "pay_invoice", + "result": { + "preimage": "0123456789abcdef...", + "fees_paid": 10 + } +} +``` + +#### make_invoice + +**Request:** +```json +{ + "method": "make_invoice", + "params": { + "amount": 21000, + "description": "Donation", + "expiry": 3600 + } +} +``` + +**Response:** +```json +{ + "result_type": "make_invoice", + "result": { + "type": "incoming", + "state": "pending", + "invoice": "lnbc210n1p3...", + "payment_hash": "abc123...", + "amount": 21000, + "created_at": 1700000000, + "expires_at": 1700003600 + } +} +``` + +## Implementation for Browser Extension + +### Architecture Overview + +For Plebeian Signer, NWC support would add a new capability alongside NIP-07: + +``` +Web App + ↓ +window.nostr.nwc.* (new NWC methods) + ↓ +Content Script + ↓ +Background Service Worker + ↓ +Nostr Relay (WebSocket) + ↓ +Lightning Wallet Service +``` + +### Recommended SDK + +Use the **Alby JS SDK** for NWC client implementation: + +```bash +npm install @getalby/sdk +``` + +### Code Examples + +#### Basic NWC Client Setup + +```typescript +import { nwc } from '@getalby/sdk'; + +// Parse and validate NWC connection URL +function parseNwcUrl(url: string): { + walletPubkey: string; + relayUrl: string; + secret: string; + lud16?: string; +} { + const parsed = new URL(url); + if (parsed.protocol !== 'nostr+walletconnect:') { + throw new Error('Invalid NWC URL protocol'); + } + + return { + walletPubkey: parsed.hostname || parsed.pathname.replace('//', ''), + relayUrl: decodeURIComponent(parsed.searchParams.get('relay') || ''), + secret: parsed.searchParams.get('secret') || '', + lud16: parsed.searchParams.get('lud16') || undefined, + }; +} + +// Create NWC client from connection URL +async function createNwcClient(connectionUrl: string) { + const client = new nwc.NWCClient({ + nostrWalletConnectUrl: connectionUrl, + }); + + return client; +} +``` + +#### Implementing NWC Methods + +```typescript +import { nwc } from '@getalby/sdk'; + +class NwcService { + private client: nwc.NWCClient | null = null; + + async connect(connectionUrl: string): Promise<void> { + this.client = new nwc.NWCClient({ + nostrWalletConnectUrl: connectionUrl, + }); + } + + async getInfo(): Promise<any> { + if (!this.client) throw new Error('Not connected'); + return await this.client.getInfo(); + } + + async getBalance(): Promise<{ balance: number }> { + if (!this.client) throw new Error('Not connected'); + return await this.client.getBalance(); + } + + async payInvoice(invoice: string, amount?: number): Promise<{ + preimage: string; + fees_paid?: number; + }> { + if (!this.client) throw new Error('Not connected'); + return await this.client.payInvoice({ + invoice, + amount, + }); + } + + async makeInvoice(params: { + amount: number; + description?: string; + expiry?: number; + }): Promise<{ + invoice: string; + payment_hash: string; + }> { + if (!this.client) throw new Error('Not connected'); + return await this.client.makeInvoice(params); + } + + async lookupInvoice(params: { + payment_hash?: string; + invoice?: string; + }): Promise<any> { + if (!this.client) throw new Error('Not connected'); + return await this.client.lookupInvoice(params); + } + + async listTransactions(params?: { + from?: number; + until?: number; + limit?: number; + offset?: number; + type?: 'incoming' | 'outgoing'; + }): Promise<any> { + if (!this.client) throw new Error('Not connected'); + return await this.client.listTransactions(params); + } + + disconnect(): void { + // Clean up WebSocket connection + this.client = null; + } +} +``` + +#### Manual Implementation (Without SDK) + +If you prefer not to use the SDK, here's how to implement NWC manually using your existing Nostr helpers: + +```typescript +import { NostrHelper, CryptoHelper } from '@common'; + +interface NwcConnection { + walletPubkey: string; + relayUrl: string; + secret: string; // Client's private key for this connection +} + +class ManualNwcClient { + private connection: NwcConnection; + private ws: WebSocket | null = null; + private pendingRequests: Map<string, { + resolve: (value: any) => void; + reject: (error: any) => void; + }> = new Map(); + + constructor(connectionUrl: string) { + this.connection = this.parseConnectionUrl(connectionUrl); + } + + private parseConnectionUrl(url: string): NwcConnection { + const parsed = new URL(url); + return { + walletPubkey: parsed.hostname || parsed.pathname.replace('//', ''), + relayUrl: decodeURIComponent(parsed.searchParams.get('relay') || ''), + secret: parsed.searchParams.get('secret') || '', + }; + } + + async connect(): Promise<void> { + return new Promise((resolve, reject) => { + this.ws = new WebSocket(this.connection.relayUrl); + + this.ws.onopen = () => { + // Subscribe to responses from the wallet + const clientPubkey = NostrHelper.getPublicKey(this.connection.secret); + const subId = CryptoHelper.randomHex(16); + + this.ws!.send(JSON.stringify([ + 'REQ', + subId, + { + kinds: [23195], // Response events + '#p': [clientPubkey], + since: Math.floor(Date.now() / 1000) - 60, + } + ])); + + resolve(); + }; + + this.ws.onerror = (error) => reject(error); + + this.ws.onmessage = (event) => { + this.handleMessage(JSON.parse(event.data)); + }; + }); + } + + private handleMessage(message: any[]): void { + if (message[0] === 'EVENT') { + const event = message[2]; + this.handleResponseEvent(event); + } + } + + private async handleResponseEvent(event: any): Promise<void> { + // Decrypt the response using NIP-44 (preferred) or NIP-04 + const decrypted = await CryptoHelper.nip44Decrypt( + this.connection.secret, + this.connection.walletPubkey, + event.content + ); + + const response = JSON.parse(decrypted); + + // Find the original request by event ID from 'e' tag + const requestId = event.tags.find((t: string[]) => t[0] === 'e')?.[1]; + + if (requestId && this.pendingRequests.has(requestId)) { + const { resolve, reject } = this.pendingRequests.get(requestId)!; + this.pendingRequests.delete(requestId); + + if (response.error) { + reject(new Error(`${response.error.code}: ${response.error.message}`)); + } else { + resolve(response.result); + } + } + } + + async sendRequest(method: string, params: any = {}): Promise<any> { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + throw new Error('Not connected'); + } + + const clientPubkey = NostrHelper.getPublicKey(this.connection.secret); + + // Create the request payload + const payload = JSON.stringify({ method, params }); + + // Encrypt with NIP-44 + const encrypted = await CryptoHelper.nip44Encrypt( + this.connection.secret, + this.connection.walletPubkey, + payload + ); + + // Create the request event + const event = { + kind: 23194, + pubkey: clientPubkey, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ['p', this.connection.walletPubkey], + ['encryption', 'nip44_v2'], + ], + content: encrypted, + }; + + // Sign the event + const signedEvent = await NostrHelper.signEvent(event, this.connection.secret); + + // Send to relay + this.ws.send(JSON.stringify(['EVENT', signedEvent])); + + // Return promise that resolves when we get response + return new Promise((resolve, reject) => { + this.pendingRequests.set(signedEvent.id, { resolve, reject }); + + // Timeout after 30 seconds + setTimeout(() => { + if (this.pendingRequests.has(signedEvent.id)) { + this.pendingRequests.delete(signedEvent.id); + reject(new Error('Request timeout')); + } + }, 30000); + }); + } + + // Convenience methods + async getInfo() { + return this.sendRequest('get_info'); + } + + async getBalance() { + return this.sendRequest('get_balance'); + } + + async payInvoice(invoice: string, amount?: number) { + return this.sendRequest('pay_invoice', { invoice, amount }); + } + + async makeInvoice(amount: number, description?: string, expiry?: number) { + return this.sendRequest('make_invoice', { amount, description, expiry }); + } + + disconnect(): void { + if (this.ws) { + this.ws.close(); + this.ws = null; + } + this.pendingRequests.clear(); + } +} +``` + +### Exposing NWC via window.nostr + +Extend the existing `window.nostr` interface: + +```typescript +// In plebian-signer-extension.ts + +interface WindowNostr { + // Existing NIP-07 methods + getPublicKey(): Promise<string>; + signEvent(event: UnsignedEvent): Promise<SignedEvent>; + getRelays(): Promise<Record<string, RelayPolicy>>; + nip04: { + encrypt(pubkey: string, plaintext: string): Promise<string>; + decrypt(pubkey: string, ciphertext: string): Promise<string>; + }; + nip44: { + encrypt(pubkey: string, plaintext: string): Promise<string>; + decrypt(pubkey: string, ciphertext: string): Promise<string>; + }; + + // New NWC methods + nwc?: { + isEnabled(): Promise<boolean>; + getInfo(): Promise<NwcInfo>; + getBalance(): Promise<{ balance: number }>; + payInvoice(invoice: string, amount?: number): Promise<{ preimage: string }>; + makeInvoice(params: MakeInvoiceParams): Promise<InvoiceResult>; + lookupInvoice(params: LookupParams): Promise<InvoiceResult>; + listTransactions(params?: ListParams): Promise<Transaction[]>; + }; +} +``` + +### Storage Considerations + +Store NWC connection details securely: + +```typescript +interface NwcConnectionData { + id: string; + name: string; // User-friendly name + connectionUrl: string; // Full nostr+walletconnect:// URL + walletPubkey: string; // Extracted for quick access + relayUrl: string; // Extracted for quick access + createdAt: number; + lastUsed?: number; + + // Optional spending limits (enforced client-side as additional protection) + maxSinglePayment?: number; // Max per-payment in sats + dailyLimit?: number; // Max daily spending in sats + dailySpent?: number; // Track daily spending + dailyResetAt?: number; // When to reset daily counter +} + +// Store in encrypted vault alongside identities +interface VaultData { + identities: Identity[]; + nwcConnections: NwcConnectionData[]; +} +``` + +### Permission Flow + +When a web app requests NWC operations: + +1. **Check if NWC is configured**: Show setup prompt if not +2. **Validate the request**: Check method, params, and spending limits +3. **Prompt for approval**: Show payment details for `pay_invoice` +4. **Execute and respond**: Send to wallet and return result + +```typescript +// Permission levels +enum NwcPermission { + READ_ONLY = 'read_only', // get_info, get_balance, lookup, list + RECEIVE = 'receive', // + make_invoice + SEND = 'send', // + pay_invoice, pay_keysend (requires approval) + AUTO_PAY = 'auto_pay', // + automatic payments within limits +} + +interface NwcSitePermission { + origin: string; + permission: NwcPermission; + autoPayLimit?: number; // Auto-approve payments under this amount +} +``` + +## UI Components Needed + +### Connection Setup Page + +- Input field for NWC connection URL (or QR scanner) +- Parse and display connection details +- Test connection button +- Save connection + +### Wallet Dashboard + +- Display connected wallet info +- Show current balance +- Transaction history +- Spending statistics + +### Payment Approval Prompt + +- Invoice amount and description +- Recipient info if available +- Fee estimate +- Approve/Reject buttons +- "Remember for this site" option + +## Security Considerations + +1. **Store secrets securely**: NWC secrets should be encrypted in the vault like private keys +2. **Validate all inputs**: Sanitize invoice strings, validate amounts +3. **Implement spending limits**: Add client-side limits as defense in depth +4. **Audit trail**: Log all NWC operations for user review +5. **Clear error handling**: Never expose raw errors to web pages +6. **Connection isolation**: Each site should not see other sites' NWC activity + +## Testing + +### Test Wallets + +1. **Alby Hub** - Full NWC support, easy setup: [getalby.com](https://getalby.com) +2. **LNbits** - Self-hosted, great for testing: [lnbits.com](https://lnbits.com) +3. **Coinos** - Custodial, quick signup: [coinos.io](https://coinos.io) + +### Test Scenarios + +- [ ] Parse valid NWC URLs +- [ ] Reject invalid NWC URLs +- [ ] Connect to relay successfully +- [ ] Get wallet info +- [ ] Get balance +- [ ] Create invoice +- [ ] Pay invoice (testnet/small amount) +- [ ] Handle connection errors gracefully +- [ ] Handle wallet errors gracefully +- [ ] Enforce spending limits +- [ ] Permission prompts work correctly + +## Resources + +### Official Documentation + +- [NIP-47 Specification](https://nips.nostr.com/47) +- [NIP-47 on GitHub](https://github.com/nostr-protocol/nips/blob/master/47.md) +- [NWC Developer Portal](https://nwc.dev) +- [NWC Documentation](https://docs.nwc.dev) + +### SDKs and Libraries + +- [Alby JS SDK](https://github.com/getAlby/js-sdk) - Recommended for browser extensions +- [Alby SDK on npm](https://www.npmjs.com/package/@getalby/sdk) +- [Alby Developer Guide](https://guides.getalby.com/developer-guide/nostr-wallet-connect-api) + +### Example Implementations + +- [Alby NWC Examples](https://github.com/getAlby/js-sdk/tree/master/examples/nwc) +- [Alby Browser Extension](https://github.com/getAlby/lightning-browser-extension) +- [Awesome NWC List](https://github.com/getAlby/awesome-nwc) + +### Community + +- [NWC Discord](https://discord.gg/PRhQPZCmeF) +- [Nostr Protocol GitHub](https://github.com/nostr-protocol) diff --git a/README.md b/README.md index 1ed3212..a251094 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Gooti +# Plebian Signer ## Nostr Identity Manager & Signer -Gooti is a browser extension for managing multiple [Nostr](https://github.com/nostr-protocol/nostr) identities and for signing events on web apps without having to give them your keys. +Plebian Signer is a browser extension for managing multiple [Nostr](https://github.com/nostr-protocol/nostr) identities and for signing events on web apps without having to give them your keys. It implements these mandatory [NIP-07](https://github.com/nostr-protocol/nips/blob/master/07.md) methods: @@ -21,19 +21,15 @@ async window.nostr.nip44.encrypt(pubkey, plaintext): string async window.nostr.nip44.decrypt(pubkey, ciphertext): string ``` -The repository is configured as monorepo to hold the extensions for Chrome and Firefox. - -[Get the Firefox extension here!](https://addons.mozilla.org/en-US/firefox/addon/gooti/) - -[Get the Chrome extension here!](https://chromewebstore.google.com/detail/gooti/cpcnmacmpalecmijkbcajanpdlcgjpgj) +The repository is configured as monorepo to hold the extensions for Chrome and Firefox. ## Develop Chrome Extension To build and run the Chrome extension from this code: ``` -git clone https://github.com/sam-hayes-org/gooti-extension -cd gooti-extension +git clone https://git.mleku.dev/mleku/plebeian-signer +cd plebeian-signer npm ci npm run build:chrome ``` @@ -50,8 +46,8 @@ then To build and run the Firefox extension from this code: ``` -git clone https://github.com/sam-hayes-org/gooti-extension -cd gooti-extension +git clone https://git.mleku.dev/mleku/plebeian-signer +cd plebeian-signer npm ci npm run build:firefox ``` @@ -65,4 +61,4 @@ then --- -LICENSE: Public Domain \ No newline at end of file +LICENSE: Public Domain diff --git a/angular.json b/angular.json index 088d2a2..08e7013 100644 --- a/angular.json +++ b/angular.json @@ -3,7 +3,10 @@ "version": 1, "newProjectRoot": "projects", "cli": { - "schematicCollections": ["angular-eslint"] + "schematicCollections": [ + "angular-eslint" + ], + "analytics": false }, "projects": { "chrome": { diff --git a/chrome_prepare_manifest.sh b/chrome_prepare_manifest.sh old mode 100755 new mode 100644 diff --git a/firefox_prepare_manifest.sh b/firefox_prepare_manifest.sh old mode 100755 new mode 100644 diff --git a/package-lock.json b/package-lock.json index 8c46684..d541443 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "gooti-extension", + "name": "plebian-signer", "version": "0.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "gooti-extension", + "name": "plebian-signer", "version": "0.0.4", "dependencies": { "@angular/animations": "^19.0.0", @@ -101,6 +101,2391 @@ "@angular/compiler-cli": "^19.0.0" } }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/architect": { + "version": "0.1902.19", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1902.19.tgz", + "integrity": "sha512-iexYDIYpGAeAU7T60bGcfrGwtq1bxpZixYxWuHYiaD1b5baQgNSfd1isGEOh37GgDNsf4In9i2LOLPm0wBdtgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.19", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-angular": { + "version": "19.2.19", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-19.2.19.tgz", + "integrity": "sha512-uIxi6Vzss6+ycljVhkyPUPWa20w8qxJL9lEn0h6+sX/fhM8Djt0FHIuTQjoX58EoMaQ/1jrXaRaGimkbaFcG9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1902.19", + "@angular-devkit/build-webpack": "0.1902.19", + "@angular-devkit/core": "19.2.19", + "@angular/build": "19.2.19", + "@babel/core": "7.26.10", + "@babel/generator": "7.26.10", + "@babel/helper-annotate-as-pure": "7.25.9", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-transform-async-generator-functions": "7.26.8", + "@babel/plugin-transform-async-to-generator": "7.25.9", + "@babel/plugin-transform-runtime": "7.26.10", + "@babel/preset-env": "7.26.9", + "@babel/runtime": "7.26.10", + "@discoveryjs/json-ext": "0.6.3", + "@ngtools/webpack": "19.2.19", + "@vitejs/plugin-basic-ssl": "1.2.0", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.20", + "babel-loader": "9.2.1", + "browserslist": "^4.21.5", + "copy-webpack-plugin": "12.0.2", + "css-loader": "7.1.2", + "esbuild-wasm": "0.25.4", + "fast-glob": "3.3.3", + "http-proxy-middleware": "3.0.5", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", + "karma-source-map-support": "1.4.0", + "less": "4.2.2", + "less-loader": "12.2.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.3.1", + "mini-css-extract-plugin": "2.9.2", + "open": "10.1.0", + "ora": "5.4.1", + "picomatch": "4.0.2", + "piscina": "4.8.0", + "postcss": "8.5.2", + "postcss-loader": "8.1.1", + "resolve-url-loader": "5.0.0", + "rxjs": "7.8.1", + "sass": "1.85.0", + "sass-loader": "16.0.5", + "semver": "7.7.1", + "source-map-loader": "5.0.0", + "source-map-support": "0.5.21", + "terser": "5.39.0", + "tree-kill": "1.2.2", + "tslib": "2.8.1", + "webpack": "5.98.0", + "webpack-dev-middleware": "7.4.2", + "webpack-dev-server": "5.2.2", + "webpack-merge": "6.0.1", + "webpack-subresource-integrity": "5.1.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.25.4" + }, + "peerDependencies": { + "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", + "@angular/localize": "^19.0.0 || ^19.2.0-next.0", + "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0", + "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0", + "@angular/ssr": "^19.2.19", + "@web/test-runner": "^0.20.0", + "browser-sync": "^3.0.2", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "karma": "^6.3.0", + "ng-packagr": "^19.0.0 || ^19.2.0-next.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "typescript": ">=5.5 <5.9" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "@web/test-runner": { + "optional": true + }, + "browser-sync": { + "optional": true + }, + "jest": { + "optional": true + }, + "jest-environment-jsdom": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-angular/node_modules/@angular/build": { + "version": "19.2.19", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.2.19.tgz", + "integrity": "sha512-SFzQ1bRkNFiOVu+aaz+9INmts7tDUrsHLEr9HmARXr9qk5UmR8prlw39p2u+Bvi6/lCiJ18TZMQQl9mGyr63lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1902.19", + "@babel/core": "7.26.10", + "@babel/helper-annotate-as-pure": "7.25.9", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-syntax-import-attributes": "7.26.0", + "@inquirer/confirm": "5.1.6", + "@vitejs/plugin-basic-ssl": "1.2.0", + "beasties": "0.3.2", + "browserslist": "^4.23.0", + "esbuild": "0.25.4", + "fast-glob": "3.3.3", + "https-proxy-agent": "7.0.6", + "istanbul-lib-instrument": "6.0.3", + "listr2": "8.2.5", + "magic-string": "0.30.17", + "mrmime": "2.0.1", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.2", + "piscina": "4.8.0", + "rollup": "4.34.8", + "sass": "1.85.0", + "semver": "7.7.1", + "source-map-support": "0.5.21", + "vite": "6.4.1", + "watchpack": "2.4.2" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "lmdb": "3.2.6" + }, + "peerDependencies": { + "@angular/compiler": "^19.0.0 || ^19.2.0-next.0", + "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", + "@angular/localize": "^19.0.0 || ^19.2.0-next.0", + "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0", + "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0", + "@angular/ssr": "^19.2.19", + "karma": "^6.4.0", + "less": "^4.2.0", + "ng-packagr": "^19.0.0 || ^19.2.0-next.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "typescript": ">=5.5 <5.9" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "karma": { + "optional": true + }, + "less": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-angular/node_modules/@angular/build/node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-angular/node_modules/@angular/build/node_modules/vite/node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-angular/node_modules/@angular/build/node_modules/vite/node_modules/rollup": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.5.tgz", + "integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.5", + "@rollup/rollup-android-arm64": "4.53.5", + "@rollup/rollup-darwin-arm64": "4.53.5", + "@rollup/rollup-darwin-x64": "4.53.5", + "@rollup/rollup-freebsd-arm64": "4.53.5", + "@rollup/rollup-freebsd-x64": "4.53.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.5", + "@rollup/rollup-linux-arm-musleabihf": "4.53.5", + "@rollup/rollup-linux-arm64-gnu": "4.53.5", + "@rollup/rollup-linux-arm64-musl": "4.53.5", + "@rollup/rollup-linux-loong64-gnu": "4.53.5", + "@rollup/rollup-linux-ppc64-gnu": "4.53.5", + "@rollup/rollup-linux-riscv64-gnu": "4.53.5", + "@rollup/rollup-linux-riscv64-musl": "4.53.5", + "@rollup/rollup-linux-s390x-gnu": "4.53.5", + "@rollup/rollup-linux-x64-gnu": "4.53.5", + "@rollup/rollup-linux-x64-musl": "4.53.5", + "@rollup/rollup-openharmony-arm64": "4.53.5", + "@rollup/rollup-win32-arm64-msvc": "4.53.5", + "@rollup/rollup-win32-ia32-msvc": "4.53.5", + "@rollup/rollup-win32-x64-gnu": "4.53.5", + "@rollup/rollup-win32-x64-msvc": "4.53.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz", + "integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz", + "integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz", + "integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz", + "integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz", + "integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz", + "integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz", + "integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz", + "integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz", + "integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.5.tgz", + "integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz", + "integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz", + "integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz", + "integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz", + "integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz", + "integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz", + "integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz", + "integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-angular/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/build-webpack": { + "version": "0.1902.19", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1902.19.tgz", + "integrity": "sha512-x2tlGg5CsUveFzuRuqeHknSbGirSAoRynEh+KqPRGK0G3WpMViW/M8SuVurecasegfIrDWtYZ4FnVxKqNbKwXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1902.19", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^5.0.2" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@angular-devkit/core": { + "version": "19.2.19", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.19.tgz", + "integrity": "sha512-JbLL+4IMLMBgjLZlnPG4lYDfz4zGrJ/s6Aoon321NJKuw1Kb1k5KpFu9dUY0BqLIe8xPQ2UJBpI+xXdK5MXMHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@babel/core": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@babel/generator": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", + "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.10", + "@babel/types": "^7.26.10", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz", + "integrity": "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-remap-async-to-generator": "^7.25.9", + "@babel/traverse": "^7.26.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@babel/plugin-transform-runtime": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.26.10.tgz", + "integrity": "sha512-NWaL2qG6HRpONTnj4JvDU6th4jYeZOJgu3QhmFTCihib0ermtOJqktA5BduGm3suhhVe9EMP9c9+mfJ/I9slqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@babel/preset-env": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz", + "integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.8", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.26.0", + "@babel/plugin-syntax-import-attributes": "^7.26.0", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.25.9", + "@babel/plugin-transform-async-generator-functions": "^7.26.8", + "@babel/plugin-transform-async-to-generator": "^7.25.9", + "@babel/plugin-transform-block-scoped-functions": "^7.26.5", + "@babel/plugin-transform-block-scoping": "^7.25.9", + "@babel/plugin-transform-class-properties": "^7.25.9", + "@babel/plugin-transform-class-static-block": "^7.26.0", + "@babel/plugin-transform-classes": "^7.25.9", + "@babel/plugin-transform-computed-properties": "^7.25.9", + "@babel/plugin-transform-destructuring": "^7.25.9", + "@babel/plugin-transform-dotall-regex": "^7.25.9", + "@babel/plugin-transform-duplicate-keys": "^7.25.9", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-dynamic-import": "^7.25.9", + "@babel/plugin-transform-exponentiation-operator": "^7.26.3", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-for-of": "^7.26.9", + "@babel/plugin-transform-function-name": "^7.25.9", + "@babel/plugin-transform-json-strings": "^7.25.9", + "@babel/plugin-transform-literals": "^7.25.9", + "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", + "@babel/plugin-transform-member-expression-literals": "^7.25.9", + "@babel/plugin-transform-modules-amd": "^7.25.9", + "@babel/plugin-transform-modules-commonjs": "^7.26.3", + "@babel/plugin-transform-modules-systemjs": "^7.25.9", + "@babel/plugin-transform-modules-umd": "^7.25.9", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-new-target": "^7.25.9", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.26.6", + "@babel/plugin-transform-numeric-separator": "^7.25.9", + "@babel/plugin-transform-object-rest-spread": "^7.25.9", + "@babel/plugin-transform-object-super": "^7.25.9", + "@babel/plugin-transform-optional-catch-binding": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9", + "@babel/plugin-transform-private-methods": "^7.25.9", + "@babel/plugin-transform-private-property-in-object": "^7.25.9", + "@babel/plugin-transform-property-literals": "^7.25.9", + "@babel/plugin-transform-regenerator": "^7.25.9", + "@babel/plugin-transform-regexp-modifiers": "^7.26.0", + "@babel/plugin-transform-reserved-words": "^7.25.9", + "@babel/plugin-transform-shorthand-properties": "^7.25.9", + "@babel/plugin-transform-spread": "^7.25.9", + "@babel/plugin-transform-sticky-regex": "^7.25.9", + "@babel/plugin-transform-template-literals": "^7.26.8", + "@babel/plugin-transform-typeof-symbol": "^7.26.7", + "@babel/plugin-transform-unicode-escapes": "^7.25.9", + "@babel/plugin-transform-unicode-property-regex": "^7.25.9", + "@babel/plugin-transform-unicode-regex": "^7.25.9", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.40.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@babel/runtime": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/node_modules/@inquirer/confirm": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.6.tgz", + "integrity": "sha512-6ZXYK3M1XmaVBZX6FCfChgtponnL0R6I7k8Nu+kaoNkT828FVZTcca1MqmWQipaW2oNREQl5AaPCUOOCVNdRMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.7", + "@inquirer/type": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.2.6.tgz", + "integrity": "sha512-yF/ih9EJJZc72psFQbwnn8mExIWfTnzWJg+N02hnpXtDPETYLmQswIMBn7+V88lfCaFrMozJsUvcEQIkEPU0Gg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.2.6.tgz", + "integrity": "sha512-5BbCumsFLbCi586Bb1lTWQFkekdQUw8/t8cy++Uq251cl3hbDIGEwD9HAwh8H6IS2F6QA9KdKmO136LmipRNkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.2.6.tgz", + "integrity": "sha512-+6XgLpMb7HBoWxXj+bLbiiB4s0mRRcDPElnRS3LpWRzdYSe+gFk5MT/4RrVNqd2MESUDmb53NUXw1+BP69bjiQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.2.6.tgz", + "integrity": "sha512-l5VmJamJ3nyMmeD1ANBQCQqy7do1ESaJQfKPSm2IG9/ADZryptTyCj8N6QaYgIWewqNUrcbdMkJajRQAt5Qjfg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.2.6.tgz", + "integrity": "sha512-nDYT8qN9si5+onHYYaI4DiauDMx24OAiuZAUsEqrDy+ja/3EbpXPX/VAkMV8AEaQhy3xc4dRC+KcYIvOFefJ4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.2.6.tgz", + "integrity": "sha512-XlqVtILonQnG+9fH2N3Aytria7P/1fwDgDhl29rde96uH2sLB8CHORIf2PfuLVzFQJ7Uqp8py9AYwr3ZUCFfWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@ngtools/webpack": { + "version": "19.2.19", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-19.2.19.tgz", + "integrity": "sha512-R9aeTrOBiRVl8I698JWPniUAAEpSvzc8SUGWSM5UXWMcHnWqd92cOnJJ1aXDGJZKXrbhMhCBx9Dglmcks5IDpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", + "typescript": ">=5.5 <5.9", + "webpack": "^5.54.0" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz", + "integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@rollup/rollup-android-arm64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz", + "integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz", + "integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz", + "integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz", + "integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz", + "integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz", + "integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz", + "integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz", + "integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz", + "integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz", + "integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz", + "integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz", + "integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz", + "integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz", + "integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz", + "integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz", + "integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz", + "integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz", + "integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@angular-builders/custom-webpack/node_modules/@types/express-serve-static-core": { + "version": "4.19.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", + "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.3", + "core-js-compat": "^3.40.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/beasties": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.3.2.tgz", + "integrity": "sha512-p4AF8uYzm9Fwu8m/hSVTCPXrRBPmB34hQpHsec2KOaR9CZmgoU8IOv4Cvwq4hgz2p4hLMNbsdNl5XeA6XbAQwA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "htmlparser2": "^10.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.49", + "postcss-media-query-parser": "^0.2.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular-builders/custom-webpack/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular-builders/custom-webpack/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/node_modules/esbuild-wasm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.25.4.tgz", + "integrity": "sha512-2HlCS6rNvKWaSKhWaG/YIyRsTsL3gUrMP2ToZMBIjw9LM7vVcIs+rz8kE2vExvTJgvM8OKPqNpcHawY/BQc/qQ==", + "dev": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@angular-builders/custom-webpack/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/@angular-builders/custom-webpack/node_modules/htmlparser2": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/http-proxy-middleware": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.5.tgz", + "integrity": "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.15", + "debug": "^4.3.6", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.3", + "is-plain-object": "^5.0.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular-builders/custom-webpack/node_modules/less": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/less/-/less-4.2.2.tgz", + "integrity": "sha512-tkuLHQlvWUTeQ3doAqnHbNn8T6WX1KA8yvbKG9x4VtKtIjHsVKQZCH11zRgAfbDAXC2UNIg/K9BYAAcEzUIrNg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/less/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", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/lmdb": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.2.6.tgz", + "integrity": "sha512-SuHqzPl7mYStna8WRotY8XX/EUZBjjv3QyKIByeCLFfC9uXT/OIHByEcA07PzbMfQAM0KYJtLgtpMRlIe5dErQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "msgpackr": "^1.11.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.5.3", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.2.6", + "@lmdb/lmdb-darwin-x64": "3.2.6", + "@lmdb/lmdb-linux-arm": "3.2.6", + "@lmdb/lmdb-linux-arm64": "3.2.6", + "@lmdb/lmdb-linux-x64": "3.2.6", + "@lmdb/lmdb-win32-x64": "3.2.6" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/postcss": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz", + "integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/readdirp": { + "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" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/readdirp/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/@angular-builders/custom-webpack/node_modules/rollup": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz", + "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.34.8", + "@rollup/rollup-android-arm64": "4.34.8", + "@rollup/rollup-darwin-arm64": "4.34.8", + "@rollup/rollup-darwin-x64": "4.34.8", + "@rollup/rollup-freebsd-arm64": "4.34.8", + "@rollup/rollup-freebsd-x64": "4.34.8", + "@rollup/rollup-linux-arm-gnueabihf": "4.34.8", + "@rollup/rollup-linux-arm-musleabihf": "4.34.8", + "@rollup/rollup-linux-arm64-gnu": "4.34.8", + "@rollup/rollup-linux-arm64-musl": "4.34.8", + "@rollup/rollup-linux-loongarch64-gnu": "4.34.8", + "@rollup/rollup-linux-powerpc64le-gnu": "4.34.8", + "@rollup/rollup-linux-riscv64-gnu": "4.34.8", + "@rollup/rollup-linux-s390x-gnu": "4.34.8", + "@rollup/rollup-linux-x64-gnu": "4.34.8", + "@rollup/rollup-linux-x64-musl": "4.34.8", + "@rollup/rollup-win32-arm64-msvc": "4.34.8", + "@rollup/rollup-win32-ia32-msvc": "4.34.8", + "@rollup/rollup-win32-x64-msvc": "4.34.8", + "fsevents": "~2.3.2" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/sass": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz", + "integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/sass-loader": { + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz", + "integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/terser": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", + "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/webpack": { + "version": "5.98.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", + "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/webpack-dev-server": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz", + "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/express-serve-static-core": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "express": "^4.21.2", + "graceful-fs": "^4.2.6", + "http-proxy-middleware": "^2.0.9", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/webpack-dev-server/node_modules/chokidar": { + "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", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/@angular-builders/custom-webpack/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "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/@angular-devkit/architect": { "version": "0.1901.5", "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1901.5.tgz", @@ -702,24 +3087,24 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", - "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", "dev": true, "license": "MIT", "engines": { @@ -908,6 +3293,16 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", @@ -968,9 +3363,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", - "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "dev": true, "license": "MIT", "engines": { @@ -1014,14 +3409,14 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", - "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1041,9 +3436,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { @@ -1051,9 +3446,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -1086,27 +3481,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz", - "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.7" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz", - "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.26.7" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -1532,14 +3927,14 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", - "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -2031,13 +4426,13 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", - "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -2252,50 +4647,50 @@ } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz", - "integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.5", - "@babel/parser": "^7.26.7", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.7", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", - "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.5", - "@babel/types": "^7.26.5", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -2303,14 +4698,14 @@ } }, "node_modules/@babel/types": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz", - "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -2717,6 +5112,23 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.24.2", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", @@ -2786,9 +5198,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { @@ -2815,13 +5227,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -2830,9 +5242,9 @@ } }, "node_modules/@eslint/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==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -2853,10 +5265,23 @@ "node": "*" } }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/core": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", - "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2867,9 +5292,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2879,7 +5304,7 @@ "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", + "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, @@ -2908,9 +5333,9 @@ } }, "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==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -2962,19 +5387,22 @@ } }, "node_modules/@eslint/js": { - "version": "9.19.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz", - "integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2982,13 +5410,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", - "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.10.0", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { @@ -3048,9 +5476,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3061,6 +5489,16 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@inquirer/checkbox": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.0.7.tgz", @@ -3099,41 +5537,54 @@ } }, "node_modules/@inquirer/core": { - "version": "10.1.5", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.5.tgz", - "integrity": "sha512-/vyCWhET0ktav/mUeBqJRYTwmjFPIKPRYb3COAw7qORULgipGSUO2vL32lQKki3UxDKJ8BvuEbokaoyCA6YlWw==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/figures": "^1.0.10", - "@inquirer/type": "^3.0.3", - "ansi-escapes": "^4.3.2", + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/editor": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.4.tgz", - "integrity": "sha512-S8b6+K9PLzxiFGGc02m4syhEu5JsH0BukzRsuZ+tpjJ5aDsDX1WfNfOil2fmsO36Y1RMcpJGxlfQ1yh4WfU28Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.1.5", - "@inquirer/type": "^3.0.3", - "external-editor": "^3.1.0" + "yoctocolors-cjs": "^2.1.3" }, "engines": { "node": ">=18" }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@inquirer/expand": { @@ -3154,10 +5605,49 @@ "@types/node": ">=18" } }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", + "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/@inquirer/figures": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.10.tgz", - "integrity": "sha512-Ey6176gZmeqZuY/W/nZiUyvmb1/qInjcpiZjXWi6nON+nxJpD1bxtSoBxNliGISae32n6OwbY+TSXPZ1CfS4bw==", + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", "dev": true, "license": "MIT", "engines": { @@ -3299,9 +5789,9 @@ } }, "node_modules/@inquirer/type": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.3.tgz", - "integrity": "sha512-I4VIHFxUuY1bshGbXZTxCmhwaaEst9s/lll3ekok+o1Z26/ZUKdx8y1b7lsoG6rtsBDwEGfiBJ2SfirjoISLpg==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", "dev": true, "license": "MIT", "engines": { @@ -3309,6 +5799,34 @@ }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" } }, "node_modules/@isaacs/cliui": { @@ -3409,18 +5927,14 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -3433,16 +5947,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "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" - } - }, "node_modules/@jridgewell/source-map": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", @@ -3462,9 +5966,9 @@ "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==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -4308,9 +6812,9 @@ } }, "node_modules/@npmcli/package-json/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -4958,6 +7462,20 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz", + "integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { "version": "4.30.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.30.1.tgz", @@ -4986,6 +7504,20 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz", + "integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { "version": "4.30.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.30.1.tgz", @@ -5000,6 +7532,20 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz", + "integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-s390x-gnu": { "version": "4.30.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.30.1.tgz", @@ -5042,6 +7588,20 @@ "linux" ] }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz", + "integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { "version": "4.30.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.30.1.tgz", @@ -5070,6 +7630,20 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz", + "integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.30.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.30.1.tgz", @@ -6469,9 +9043,9 @@ } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -7078,9 +9652,9 @@ "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==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7238,9 +9812,9 @@ } }, "node_modules/cacache/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -7389,9 +9963,9 @@ } }, "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", "dev": true, "license": "MIT" }, @@ -7678,9 +10252,9 @@ } }, "node_modules/compression": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz", - "integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "dev": true, "license": "MIT", "dependencies": { @@ -7688,7 +10262,7 @@ "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", - "on-headers": "~1.0.2", + "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" }, @@ -8681,32 +11255,32 @@ } }, "node_modules/eslint": { - "version": "9.19.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz", - "integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.10.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.19.0", - "@eslint/plugin-kit": "^0.2.5", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", + "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -8741,9 +11315,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -8788,9 +11362,9 @@ } }, "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==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -8799,9 +11373,9 @@ } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -8857,15 +11431,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -8875,9 +11449,9 @@ } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -9113,21 +11687,6 @@ "dev": true, "license": "MIT" }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "license": "MIT", - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -9219,6 +11778,24 @@ "node": ">=0.8.0" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -9381,13 +11958,13 @@ } }, "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "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.0", + "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" }, "engines": { @@ -9606,9 +12183,9 @@ "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==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -10578,9 +13155,9 @@ "license": "MIT" }, "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==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -10762,9 +13339,9 @@ } }, "node_modules/karma-coverage/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==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -10868,9 +13445,9 @@ } }, "node_modules/karma/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==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -11039,16 +13616,6 @@ "node": ">=8" } }, - "node_modules/karma/node_modules/tmp": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", - "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, "node_modules/karma/node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -12004,9 +14571,9 @@ } }, "node_modules/minizlib/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -12128,9 +14695,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -12209,9 +14776,9 @@ "license": "ISC" }, "node_modules/ng-packagr": { - "version": "19.1.2", - "resolved": "https://registry.npmjs.org/ng-packagr/-/ng-packagr-19.1.2.tgz", - "integrity": "sha512-h8YDp6YdPwAwbl7rs0lJE7vVugobY6m+JogS0hQ7P+52RmslPT8kRCgdvGLIS1JySwPrDFQkPh2PLBaSjwcRqQ==", + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/ng-packagr/-/ng-packagr-19.2.2.tgz", + "integrity": "sha512-dFuwFsDJMBSd1YtmLLcX5bNNUCQUlRqgf34aXA+79PmkOP+0eF8GP2949wq3+jMjmFTNm80Oo8IUYiSLwklKCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12224,7 +14791,7 @@ "commander": "^13.0.0", "convert-source-map": "^2.0.0", "dependency-graph": "^1.0.0", - "esbuild": "^0.24.0", + "esbuild": "^0.25.0", "fast-glob": "^3.3.2", "find-cache-dir": "^3.3.2", "injection-js": "^2.4.0", @@ -12249,7 +14816,7 @@ "@angular/compiler-cli": "^19.0.0 || ^19.1.0-next.0 || ^19.2.0-next.0", "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", "tslib": "^2.3.0", - "typescript": ">=5.5 <5.8" + "typescript": ">=5.5 <5.9" }, "peerDependenciesMeta": { "tailwindcss": { @@ -12257,6 +14824,431 @@ } } }, + "node_modules/ng-packagr/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/ng-packagr/node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -12264,6 +15256,48 @@ "dev": true, "license": "MIT" }, + "node_modules/ng-packagr/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/ng-packagr/node_modules/find-cache-dir": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", @@ -12386,9 +15420,9 @@ "optional": true }, "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", + "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", "dev": true, "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { @@ -12458,9 +15492,9 @@ } }, "node_modules/node-gyp/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -12829,9 +15863,9 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "dev": true, "license": "MIT", "engines": { @@ -13006,16 +16040,6 @@ "license": "MIT", "optional": true }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -14083,15 +17107,15 @@ } }, "node_modules/rimraf/node_modules/glob": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", - "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" @@ -14107,9 +17131,9 @@ } }, "node_modules/rimraf/node_modules/jackspeak": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", - "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -14133,13 +17157,13 @@ } }, "node_modules/rimraf/node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { "node": "20 || >=22" @@ -15535,17 +18559,44 @@ "dev": true, "license": "MIT" }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { - "os-tmpdir": "~1.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { - "node": ">=0.6.0" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" } }, "node_modules/to-regex-range": { @@ -16513,9 +19564,9 @@ } }, "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", - "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", "dev": true, "license": "MIT", "dependencies": { @@ -17122,9 +20173,9 @@ } }, "node_modules/yoctocolors-cjs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", - "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index e69ab48..3556a8e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "gooti-extension", + "name": "plebian-signer", "version": "0.0.4", "custom": { "chrome": { diff --git a/projects/chrome/custom-webpack.config.ts b/projects/chrome/custom-webpack.config.ts index d5586e0..98eabb7 100644 --- a/projects/chrome/custom-webpack.config.ts +++ b/projects/chrome/custom-webpack.config.ts @@ -6,12 +6,12 @@ module.exports = { import: 'src/background.ts', runtime: false, }, - 'gooti-extension': { - import: 'src/gooti-extension.ts', + 'plebian-signer-extension': { + import: 'src/plebian-signer-extension.ts', runtime: false, }, - 'gooti-content-script': { - import: 'src/gooti-content-script.ts', + 'plebian-signer-content-script': { + import: 'src/plebian-signer-content-script.ts', runtime: false, }, prompt: { diff --git a/projects/chrome/public/manifest.json b/projects/chrome/public/manifest.json index 6bb6513..ea57e33 100644 --- a/projects/chrome/public/manifest.json +++ b/projects/chrome/public/manifest.json @@ -1,9 +1,9 @@ { "manifest_version": 3, - "name": "Gooti - Nostr Identity Manager & Signer", + "name": "Plebian Signer - Nostr Identity Manager & Signer", "description": "Manage and switch between multiple identities while interacting with Nostr apps", "version": "0.0.4", - "homepage_url": "https://getgooti.com", + "homepage_url": "https://git.mleku.dev/mleku/plebeian-signer", "options_page": "options.html", "permissions": [ "windows", @@ -23,7 +23,7 @@ "<all_urls>" ], "js": [ - "gooti-content-script.js" + "plebian-signer-content-script.js" ], "all_frames": true } @@ -31,7 +31,7 @@ "web_accessible_resources": [ { "resources": [ - "gooti-extension.js" + "plebian-signer-extension.js" ], "matches": [ "https://*/*", diff --git a/projects/chrome/public/options.html b/projects/chrome/public/options.html index 587c212..9ad60d1 100644 --- a/projects/chrome/public/options.html +++ b/projects/chrome/public/options.html @@ -2,7 +2,7 @@ <html data-bs-theme="dark"> <head> - <title>Gooti - Options</title> + <title>Plebian Signer - Options</title> <link rel="stylesheet" type="text/css" href="styles.css" /> <script src="scripts.js"></script> <style> @@ -104,7 +104,7 @@ <div class="logo"> <img src="gooti.svg" alt="" /> </div> - <span class="brand-name">Gooti</span> + <span class="brand-name">Plebian Signer</span> <span>OPTIONS</span> </div> diff --git a/projects/chrome/public/prompt.html b/projects/chrome/public/prompt.html index 8d7b4d7..bb1011c 100644 --- a/projects/chrome/public/prompt.html +++ b/projects/chrome/public/prompt.html @@ -2,7 +2,7 @@ <html data-bs-theme="dark"> <head> - <title>Gooti</title> + <title>Plebian Signer</title> <link rel="stylesheet" type="text/css" href="styles.css" /> <script src="scripts.js"></script> <style> diff --git a/projects/chrome/src/app/app.component.ts b/projects/chrome/src/app/app.component.ts index c94765b..8837c7c 100644 --- a/projects/chrome/src/app/app.component.ts +++ b/projects/chrome/src/app/app.component.ts @@ -14,7 +14,7 @@ export class AppComponent implements OnInit { readonly #logger = inject(LoggerService); ngOnInit(): void { - this.#logger.initialize('Gooti Chrome Extension'); + this.#logger.initialize('Plebian Signer Chrome Extension'); this.#startup.startOver(getNewStorageServiceConfig()); } diff --git a/projects/chrome/src/app/common/data/chrome-meta-handler.ts b/projects/chrome/src/app/common/data/chrome-meta-handler.ts index a1d5bb7..20d1e07 100644 --- a/projects/chrome/src/app/common/data/chrome-meta-handler.ts +++ b/projects/chrome/src/app/common/data/chrome-meta-handler.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { GootiMetaData, GootiMetaHandler } from '@common'; +import { SignerMetaData, SignerMetaHandler } from '@common'; -export class ChromeMetaHandler extends GootiMetaHandler { +export class ChromeMetaHandler extends SignerMetaHandler { async loadFullData(): Promise<Partial<Record<string, any>>> { const dataWithPossibleAlienProperties = await chrome.storage.local.get( null @@ -19,7 +19,7 @@ export class ChromeMetaHandler extends GootiMetaHandler { return data; } - async saveFullData(data: GootiMetaData): Promise<void> { + async saveFullData(data: SignerMetaData): Promise<void> { await chrome.storage.local.set(data); } diff --git a/projects/chrome/src/app/common/data/chrome-sync-no-handler.ts b/projects/chrome/src/app/common/data/chrome-sync-no-handler.ts index 6ef754b..65cd42b 100644 --- a/projects/chrome/src/app/common/data/chrome-sync-no-handler.ts +++ b/projects/chrome/src/app/common/data/chrome-sync-no-handler.ts @@ -10,7 +10,7 @@ import { /** * Handles the browser "sync data" when the user does not want to sync anything. * It uses the chrome.storage.local API to store the data. Since we also use this API - * to store local Gooti system data (like the user's decision to not sync), we + * to store local Signer system data (like the user's decision to not sync), we * have to exclude these properties from the sync data. */ export class ChromeSyncNoHandler extends BrowserSyncHandler { diff --git a/projects/chrome/src/app/common/data/get-new-storage-service-config.ts b/projects/chrome/src/app/common/data/get-new-storage-service-config.ts index 121091f..51d36cc 100644 --- a/projects/chrome/src/app/common/data/get-new-storage-service-config.ts +++ b/projects/chrome/src/app/common/data/get-new-storage-service-config.ts @@ -9,7 +9,7 @@ export const getNewStorageServiceConfig = () => { browserSessionHandler: new ChromeSessionHandler(), browserSyncYesHandler: new ChromeSyncYesHandler(), browserSyncNoHandler: new ChromeSyncNoHandler(), - gootiMetaHandler: new ChromeMetaHandler(), + signerMetaHandler: new ChromeMetaHandler(), }; return storageConfig; diff --git a/projects/chrome/src/app/components/home/info/info.component.html b/projects/chrome/src/app/components/home/info/info.component.html index db10f8f..d434505 100644 --- a/projects/chrome/src/app/components/home/info/info.component.html +++ b/projects/chrome/src/app/components/home/info/info.component.html @@ -1,22 +1,17 @@ <div class="sam-text-header"> - <span> Gooti </span> + <span> Plebian Signer </span> </div> <span>Version {{ version }}</span> <span>&nbsp;</span> -<span> Website </span> -<a href="https://getgooti.com" target="_blank">www.getgooti.com</a> - -<span>&nbsp;</span> - <span> Source code</span> <a - href="https://github.com/sam-hayes-org/gooti-extension" + href="https://git.mleku.dev/mleku/plebeian-signer" target="_blank" > - github.com/sam-hayes-org/gooti-extension + git.mleku.dev/mleku/plebeian-signer </a> <div class="sam-flex-grow"></div> diff --git a/projects/chrome/src/app/components/home/settings/settings.component.ts b/projects/chrome/src/app/components/home/settings/settings.component.ts index ec9c2e7..15f0792 100644 --- a/projects/chrome/src/app/components/home/settings/settings.component.ts +++ b/projects/chrome/src/app/components/home/settings/settings.component.ts @@ -28,7 +28,7 @@ export class SettingsComponent extends NavComponent implements OnInit { ); console.log(vault.length / 1024 + ' KB'); - switch (this.#storage.getGootiMetaHandler().gootiMetaData?.syncFlow) { + switch (this.#storage.getSignerMetaHandler().signerMetaData?.syncFlow) { case BrowserSyncFlow.NO_SYNC: this.syncFlow = 'Off'; break; @@ -81,7 +81,7 @@ export class SettingsComponent extends NavComponent implements OnInit { const jsonVault = this.#storage.exportVault(); const dateTimeString = DateHelper.dateToISOLikeButLocal(new Date()); - const fileName = `Gooti Chrome - Vault Export - ${dateTimeString}.json`; + const fileName = `Plebian Signer Chrome - Vault Export - ${dateTimeString}.json`; this.#downloadJson(jsonVault, fileName); } diff --git a/projects/chrome/src/app/components/vault-create/home/home.component.html b/projects/chrome/src/app/components/vault-create/home/home.component.html index d7be47b..65f3047 100644 --- a/projects/chrome/src/app/components/vault-create/home/home.component.html +++ b/projects/chrome/src/app/components/vault-create/home/home.component.html @@ -1,5 +1,5 @@ <div class="sam-text-header"> - <span>Gooti</span> + <span>Plebian Signer</span> </div> <div class="vertically-centered"> diff --git a/projects/chrome/src/app/components/vault-create/new/new.component.html b/projects/chrome/src/app/components/vault-create/new/new.component.html index b146b3d..6a5d9c8 100644 --- a/projects/chrome/src/app/components/vault-create/new/new.component.html +++ b/projects/chrome/src/app/components/vault-create/new/new.component.html @@ -1,5 +1,5 @@ <div class="sam-text-header"> - <span>Gooti</span> + <span>Plebian Signer</span> </div> <div class="content"> diff --git a/projects/chrome/src/app/components/vault-import/vault-import.component.ts b/projects/chrome/src/app/components/vault-import/vault-import.component.ts index 2290dc3..cdb070c 100644 --- a/projects/chrome/src/app/components/vault-import/vault-import.component.ts +++ b/projects/chrome/src/app/components/vault-import/vault-import.component.ts @@ -2,7 +2,7 @@ import { Component, inject, OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { BrowserSyncFlow, - GootiMetaData_VaultSnapshot, + SignerMetaData_VaultSnapshot, IconButtonComponent, NavComponent, StartupService, @@ -18,8 +18,8 @@ import { getNewStorageServiceConfig } from '../../common/data/get-new-storage-se styleUrl: './vault-import.component.scss', }) export class VaultImportComponent extends NavComponent implements OnInit { - snapshots: GootiMetaData_VaultSnapshot[] = []; - selectedSnapshot: GootiMetaData_VaultSnapshot | undefined; + snapshots: SignerMetaData_VaultSnapshot[] = []; + selectedSnapshot: SignerMetaData_VaultSnapshot | undefined; syncText: string | undefined; readonly #storage = inject(StorageService); @@ -51,11 +51,11 @@ export class VaultImportComponent extends NavComponent implements OnInit { async #loadData() { this.snapshots = ( - this.#storage.getGootiMetaHandler().gootiMetaData?.vaultSnapshots ?? [] + this.#storage.getSignerMetaHandler().signerMetaData?.vaultSnapshots ?? [] ).sortBy((x) => x.fileName, 'desc'); const syncFlow = - this.#storage.getGootiMetaHandler().gootiMetaData?.syncFlow; + this.#storage.getSignerMetaHandler().signerMetaData?.syncFlow; switch (syncFlow) { case BrowserSyncFlow.BROWSER_SYNC: diff --git a/projects/chrome/src/app/components/vault-login/vault-login.component.html b/projects/chrome/src/app/components/vault-login/vault-login.component.html index b01b7b8..2eab33d 100644 --- a/projects/chrome/src/app/components/vault-login/vault-login.component.html +++ b/projects/chrome/src/app/components/vault-login/vault-login.component.html @@ -1,5 +1,5 @@ <div class="sam-text-header"> - <span class="brand">Gooti</span> + <span class="brand">Plebian Signer</span> </div> <div class="content-login-vault"> diff --git a/projects/chrome/src/app/components/welcome/welcome.component.html b/projects/chrome/src/app/components/welcome/welcome.component.html index abd7a78..cf28fee 100644 --- a/projects/chrome/src/app/components/welcome/welcome.component.html +++ b/projects/chrome/src/app/components/welcome/welcome.component.html @@ -1,9 +1,9 @@ <div class="sam-text-header sam-mb-2"> - <span>Gooti Setup - Sync Preference</span> + <span>Plebian Signer Setup - Sync Preference</span> </div> <span class="sam-text-muted sam-text-md sam-text-align-center2"> - Gooti always encrypts sensitive data like private keys and site permissions + Plebian Signer always encrypts sensitive data like private keys and site permissions independent of the chosen sync mode. </span> diff --git a/projects/chrome/src/background-common.ts b/projects/chrome/src/background-common.ts index b89db27..4a3b49e 100644 --- a/projects/chrome/src/background-common.ts +++ b/projects/chrome/src/background-common.ts @@ -4,7 +4,7 @@ import { BrowserSyncData, BrowserSyncFlow, CryptoHelper, - GootiMetaData, + SignerMetaData, Identity_DECRYPTED, Nip07Method, Nip07MethodPolicy, @@ -17,7 +17,7 @@ import { Event, EventTemplate, finalizeEvent, nip04, nip44 } from 'nostr-tools'; export const debug = function (message: any) { const dateString = new Date().toISOString(); - console.log(`[Gooti - ${dateString}]: ${JSON.stringify(message)}`); + console.log(`[Plebian Signer - ${dateString}]: ${JSON.stringify(message)}`); }; export type PromptResponse = @@ -51,15 +51,15 @@ export const getBrowserSessionData = async function (): Promise< export const getBrowserSyncData = async function (): Promise< BrowserSyncData | undefined > { - const gootiMetaHandler = new ChromeMetaHandler(); - const gootiMetaData = - (await gootiMetaHandler.loadFullData()) as GootiMetaData; + const signerMetaHandler = new ChromeMetaHandler(); + const signerMetaData = + (await signerMetaHandler.loadFullData()) as SignerMetaData; let browserSyncData: BrowserSyncData | undefined; - if (gootiMetaData.syncFlow === BrowserSyncFlow.NO_SYNC) { + if (signerMetaData.syncFlow === BrowserSyncFlow.NO_SYNC) { browserSyncData = (await chrome.storage.local.get(null)) as BrowserSyncData; - } else if (gootiMetaData.syncFlow === BrowserSyncFlow.BROWSER_SYNC) { + } else if (signerMetaData.syncFlow === BrowserSyncFlow.BROWSER_SYNC) { browserSyncData = (await chrome.storage.sync.get(null)) as BrowserSyncData; } @@ -69,13 +69,13 @@ export const getBrowserSyncData = async function (): Promise< export const savePermissionsToBrowserSyncStorage = async function ( permissions: Permission_ENCRYPTED[] ): Promise<void> { - const gootiMetaHandler = new ChromeMetaHandler(); - const gootiMetaData = - (await gootiMetaHandler.loadFullData()) as GootiMetaData; + const signerMetaHandler = new ChromeMetaHandler(); + const signerMetaData = + (await signerMetaHandler.loadFullData()) as SignerMetaData; - if (gootiMetaData.syncFlow === BrowserSyncFlow.NO_SYNC) { + if (signerMetaData.syncFlow === BrowserSyncFlow.NO_SYNC) { await chrome.storage.local.set({ permissions }); - } else if (gootiMetaData.syncFlow === BrowserSyncFlow.BROWSER_SYNC) { + } else if (signerMetaData.syncFlow === BrowserSyncFlow.BROWSER_SYNC) { await chrome.storage.sync.set({ permissions }); } }; diff --git a/projects/chrome/src/background.ts b/projects/chrome/src/background.ts index 6c33232..db830bb 100644 --- a/projects/chrome/src/background.ts +++ b/projects/chrome/src/background.ts @@ -51,7 +51,7 @@ browser.runtime.onMessage.addListener(async (message /*, sender*/) => { const browserSessionData = await getBrowserSessionData(); if (!browserSessionData) { - throw new Error('Gooti vault not unlocked by the user.'); + throw new Error('Plebian Signer vault not unlocked by the user.'); } const currentIdentity = browserSessionData.identities.find( diff --git a/projects/chrome/src/index.html b/projects/chrome/src/index.html index ca78917..777bb0e 100644 --- a/projects/chrome/src/index.html +++ b/projects/chrome/src/index.html @@ -2,7 +2,7 @@ <html lang="en" data-bs-theme="dark"> <head> <meta charset="utf-8"> - <title>Gooti</title> + <title>Plebian Signer</title> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- <link rel="icon" type="image/x-icon" href="favicon.ico"> --> diff --git a/projects/chrome/src/options.ts b/projects/chrome/src/options.ts index 02bf5df..3cb22e1 100644 --- a/projects/chrome/src/options.ts +++ b/projects/chrome/src/options.ts @@ -1,7 +1,7 @@ import { BrowserSyncData, - GOOTI_META_DATA_KEY, - GootiMetaData_VaultSnapshot, + SIGNER_META_DATA_KEY, + SignerMetaData_VaultSnapshot, } from '@common'; import './app/common/extensions/array'; import browser from 'webextension-polyfill'; @@ -10,13 +10,13 @@ import browser from 'webextension-polyfill'; // Functions // -async function getGootiMetaDataVaultSnapshots(): Promise< - GootiMetaData_VaultSnapshot[] +async function getSignerMetaDataVaultSnapshots(): Promise< + SignerMetaData_VaultSnapshot[] > { const data = (await browser.storage.local.get( - GOOTI_META_DATA_KEY.vaultSnapshots + SIGNER_META_DATA_KEY.vaultSnapshots )) as { - vaultSnapshots?: GootiMetaData_VaultSnapshot[]; + vaultSnapshots?: SignerMetaData_VaultSnapshot[]; }; return typeof data.vaultSnapshots === 'undefined' @@ -24,15 +24,15 @@ async function getGootiMetaDataVaultSnapshots(): Promise< : data.vaultSnapshots.sortBy((x) => x.fileName, 'desc'); } -async function setGootiMetaDataVaultSnapshots( - vaultSnapshots: GootiMetaData_VaultSnapshot[] +async function setSignerMetaDataVaultSnapshots( + vaultSnapshots: SignerMetaData_VaultSnapshot[] ): Promise<void> { await browser.storage.local.set({ vaultSnapshots, }); } -function rebuildSnapshotsList(snapshots: GootiMetaData_VaultSnapshot[]) { +function rebuildSnapshotsList(snapshots: SignerMetaData_VaultSnapshot[]) { const ul = document.getElementById('snapshotsList'); if (!ul) { return; @@ -77,7 +77,7 @@ document.addEventListener('DOMContentLoaded', async () => { ) as HTMLInputElement; deleteSnapshotsButton?.addEventListener('click', async () => { - await setGootiMetaDataVaultSnapshots([]); + await setSignerMetaDataVaultSnapshots([]); rebuildSnapshotsList([]); }); @@ -92,9 +92,9 @@ document.addEventListener('DOMContentLoaded', async () => { } try { - const existingSnapshots = await getGootiMetaDataVaultSnapshots(); + const existingSnapshots = await getSignerMetaDataVaultSnapshots(); - const newSnapshots: GootiMetaData_VaultSnapshot[] = []; + const newSnapshots: SignerMetaData_VaultSnapshot[] = []; for (const file of files) { const text = await file.text(); const vault = JSON.parse(text) as BrowserSyncData; @@ -116,7 +116,7 @@ document.addEventListener('DOMContentLoaded', async () => { ); // Persist the new snapshots to the local storage - await setGootiMetaDataVaultSnapshots(snapshots); + await setSignerMetaDataVaultSnapshots(snapshots); // rebuildSnapshotsList(snapshots); @@ -125,6 +125,6 @@ document.addEventListener('DOMContentLoaded', async () => { } }); - const snapshots = await getGootiMetaDataVaultSnapshots(); + const snapshots = await getSignerMetaDataVaultSnapshots(); rebuildSnapshotsList(snapshots); }); diff --git a/projects/chrome/src/gooti-content-script.ts b/projects/chrome/src/plebian-signer-content-script.ts similarity index 85% rename from projects/chrome/src/gooti-content-script.ts rename to projects/chrome/src/plebian-signer-content-script.ts index 6044d1a..bbc1496 100644 --- a/projects/chrome/src/gooti-content-script.ts +++ b/projects/chrome/src/plebian-signer-content-script.ts @@ -7,7 +7,7 @@ import { BackgroundRequestMessage } from './background-common'; const script = document.createElement('script'); script.setAttribute('async', 'false'); script.setAttribute('type', 'text/javascript'); -script.setAttribute('src', browser.runtime.getURL('gooti-extension.js')); +script.setAttribute('src', browser.runtime.getURL('plebian-signer-extension.js')); (document.head || document.documentElement).appendChild(script); // listen for messages from that script @@ -18,7 +18,7 @@ window.addEventListener('message', async (message) => { if (message.source !== window) return; if (!message.data) return; if (!message.data.params) return; - if (message.data.ext !== 'gooti') return; + if (message.data.ext !== 'plebian-signer') return; // pass on to background let response; @@ -36,7 +36,7 @@ window.addEventListener('message', async (message) => { // return response window.postMessage( - { id: message.data.id, ext: 'gooti', response }, + { id: message.data.id, ext: 'plebian-signer', response }, message.origin ); }); diff --git a/projects/firefox/src/gooti-extension.ts b/projects/chrome/src/plebian-signer-extension.ts similarity index 97% rename from projects/firefox/src/gooti-extension.ts rename to projects/chrome/src/plebian-signer-extension.ts index 35f90a5..9dacc9c 100644 --- a/projects/firefox/src/gooti-extension.ts +++ b/projects/chrome/src/plebian-signer-extension.ts @@ -25,7 +25,7 @@ class Messenger { window.postMessage( { id, - ext: 'gooti', + ext: 'plebian-signer', method, params, }, @@ -41,7 +41,7 @@ class Messenger { !message.data || message.data.response === null || message.data.response === undefined || - message.data.ext !== 'gooti' || + message.data.ext !== 'plebian-signer' || !this.#requests.has(message.data.id) ) { return; diff --git a/projects/chrome/tsconfig.app.json b/projects/chrome/tsconfig.app.json index 5885059..8401005 100644 --- a/projects/chrome/tsconfig.app.json +++ b/projects/chrome/tsconfig.app.json @@ -9,8 +9,8 @@ "files": [ "src/main.ts", "src/background.ts", - "src/gooti-extension.ts", - "src/gooti-content-script.ts", + "src/plebian-signer-extension.ts", + "src/plebian-signer-content-script.ts", "src/prompt.ts", "src/options.ts" ], diff --git a/projects/common/src/lib/services/startup/startup.service.ts b/projects/common/src/lib/services/startup/startup.service.ts index 9b6dec9..c01245d 100644 --- a/projects/common/src/lib/services/startup/startup.service.ts +++ b/projects/common/src/lib/services/startup/startup.service.ts @@ -19,17 +19,17 @@ export class StartupService { // Step 0: storageConfig.browserSyncNoHandler.setIgnoreProperties( - storageConfig.gootiMetaHandler.metaProperties + storageConfig.signerMetaHandler.metaProperties ); - // Step 1: Load the gooti's user settings - const gootiMetaData = await this.#storage.loadGootiMetaData(); - if (typeof gootiMetaData?.syncFlow === 'undefined') { - // Very first run. The user has not set up Gooti yet. + // Step 1: Load the user settings + const signerMetaData = await this.#storage.loadSignerMetaData(); + if (typeof signerMetaData?.syncFlow === 'undefined') { + // Very first run. The user has not set up Plebian Signer yet. this.#router.navigateByUrl('/welcome'); return; } - this.#storage.enableBrowserSyncFlow(gootiMetaData.syncFlow); + this.#storage.enableBrowserSyncFlow(signerMetaData.syncFlow); // Load the browser session data. const browserSessionData = await this.#storage.loadBrowserSessionData(); diff --git a/projects/common/src/lib/services/storage/related/vault.ts b/projects/common/src/lib/services/storage/related/vault.ts index d8a205a..43306dc 100644 --- a/projects/common/src/lib/services/storage/related/vault.ts +++ b/projects/common/src/lib/services/storage/related/vault.ts @@ -113,7 +113,7 @@ export const deleteVault = async function ( doNotSetIsInitializedToFalse: boolean ): Promise<void> { this.assureIsInitialized(); - const syncFlow = this.getGootiMetaHandler().gootiMetaData?.syncFlow; + const syncFlow = this.getSignerMetaHandler().signerMetaData?.syncFlow; if (typeof syncFlow === 'undefined') { throw new Error('Sync flow is not set.'); } diff --git a/projects/common/src/lib/services/storage/gooti-meta-handler.ts b/projects/common/src/lib/services/storage/signer-meta-handler.ts similarity index 60% rename from projects/common/src/lib/services/storage/gooti-meta-handler.ts rename to projects/common/src/lib/services/storage/signer-meta-handler.ts index 88a0898..f4b919c 100644 --- a/projects/common/src/lib/services/storage/gooti-meta-handler.ts +++ b/projects/common/src/lib/services/storage/signer-meta-handler.ts @@ -1,12 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { BrowserSyncFlow, GootiMetaData } from './types'; +import { BrowserSyncFlow, SignerMetaData } from './types'; -export abstract class GootiMetaHandler { - get gootiMetaData(): GootiMetaData | undefined { - return this.#gootiMetaData; +export abstract class SignerMetaHandler { + get signerMetaData(): SignerMetaData | undefined { + return this.#signerMetaData; } - #gootiMetaData?: GootiMetaData; + #signerMetaData?: SignerMetaData; readonly metaProperties = ['syncFlow', 'vaultSnapshots']; /** @@ -18,25 +18,25 @@ export abstract class GootiMetaHandler { */ abstract loadFullData(): Promise<Partial<Record<string, any>>>; - setFullData(data: GootiMetaData) { - this.#gootiMetaData = data; + setFullData(data: SignerMetaData) { + this.#signerMetaData = data; } - abstract saveFullData(data: GootiMetaData): Promise<void>; + abstract saveFullData(data: SignerMetaData): Promise<void>; /** * Sets the browser sync flow for the user and immediately saves it. */ async setBrowserSyncFlow(flow: BrowserSyncFlow): Promise<void> { - if (!this.#gootiMetaData) { - this.#gootiMetaData = { + if (!this.#signerMetaData) { + this.#signerMetaData = { syncFlow: flow, }; } else { - this.#gootiMetaData.syncFlow = flow; + this.#signerMetaData.syncFlow = flow; } - await this.saveFullData(this.#gootiMetaData); + await this.saveFullData(this.#signerMetaData); } abstract clearData(keep: string[]): Promise<void>; diff --git a/projects/common/src/lib/services/storage/storage.service.ts b/projects/common/src/lib/services/storage/storage.service.ts index 801de6e..42d8d99 100644 --- a/projects/common/src/lib/services/storage/storage.service.ts +++ b/projects/common/src/lib/services/storage/storage.service.ts @@ -6,10 +6,10 @@ import { BrowserSessionData, BrowserSyncData, BrowserSyncFlow, - GootiMetaData, + SignerMetaData, Relay_DECRYPTED, } from './types'; -import { GootiMetaHandler } from './gooti-meta-handler'; +import { SignerMetaHandler } from './signer-meta-handler'; import { CryptoHelper } from '@common'; import { addIdentity, @@ -24,7 +24,7 @@ export interface StorageServiceConfig { browserSessionHandler: BrowserSessionHandler; browserSyncYesHandler: BrowserSyncHandler; browserSyncNoHandler: BrowserSyncHandler; - gootiMetaHandler: GootiMetaHandler; + signerMetaHandler: SignerMetaHandler; } @Injectable({ @@ -37,7 +37,7 @@ export class StorageService { #browserSessionHandler!: BrowserSessionHandler; #browserSyncYesHandler!: BrowserSyncHandler; #browserSyncNoHandler!: BrowserSyncHandler; - #gootiMetaHandler!: GootiMetaHandler; + #signerMetaHandler!: SignerMetaHandler; initialize(config: StorageServiceConfig): void { if (this.isInitialized) { @@ -46,27 +46,27 @@ export class StorageService { this.#browserSessionHandler = config.browserSessionHandler; this.#browserSyncYesHandler = config.browserSyncYesHandler; this.#browserSyncNoHandler = config.browserSyncNoHandler; - this.#gootiMetaHandler = config.gootiMetaHandler; + this.#signerMetaHandler = config.signerMetaHandler; this.isInitialized = true; } async enableBrowserSyncFlow(flow: BrowserSyncFlow): Promise<void> { this.assureIsInitialized(); - this.#gootiMetaHandler.setBrowserSyncFlow(flow); + this.#signerMetaHandler.setBrowserSyncFlow(flow); } - async loadGootiMetaData(): Promise<GootiMetaData | undefined> { + async loadSignerMetaData(): Promise<SignerMetaData | undefined> { this.assureIsInitialized(); - const data = await this.#gootiMetaHandler.loadFullData(); + const data = await this.#signerMetaHandler.loadFullData(); if (Object.keys(data).length === 0) { // No data available yet. return undefined; } - this.#gootiMetaHandler.setFullData(data as GootiMetaData); - return data as GootiMetaData; + this.#signerMetaHandler.setFullData(data as SignerMetaData); + return data as SignerMetaData; } async loadBrowserSessionData(): Promise<BrowserSessionData | undefined> { @@ -119,7 +119,7 @@ export class StorageService { this.assureIsInitialized(); await this.getBrowserSyncHandler().clearData(); await this.getBrowserSessionHandler().clearData(); - await this.getGootiMetaHandler().clearData([]); + await this.getSignerMetaHandler().clearData([]); this.isInitialized = false; } @@ -195,7 +195,7 @@ export class StorageService { getBrowserSyncHandler(): BrowserSyncHandler { this.assureIsInitialized(); - switch (this.#gootiMetaHandler.gootiMetaData?.syncFlow) { + switch (this.#signerMetaHandler.signerMetaData?.syncFlow) { case BrowserSyncFlow.NO_SYNC: return this.#browserSyncNoHandler; @@ -211,10 +211,10 @@ export class StorageService { return this.#browserSessionHandler; } - getGootiMetaHandler(): GootiMetaHandler { + getSignerMetaHandler(): SignerMetaHandler { this.assureIsInitialized(); - return this.#gootiMetaHandler; + return this.#signerMetaHandler; } /** diff --git a/projects/common/src/lib/services/storage/types.ts b/projects/common/src/lib/services/storage/types.ts index b73d4e8..f2705fe 100644 --- a/projects/common/src/lib/services/storage/types.ts +++ b/projects/common/src/lib/services/storage/types.ts @@ -62,7 +62,7 @@ export type BrowserSyncData = BrowserSyncData_PART_Unencrypted & export enum BrowserSyncFlow { NO_SYNC = 0, BROWSER_SYNC = 1, - GOOTI_SYNC = 2, + SIGNER_SYNC = 2, CUSTOM_SYNC = 3, } @@ -79,17 +79,17 @@ export interface BrowserSessionData { relays: Relay_DECRYPTED[]; } -export interface GootiMetaData_VaultSnapshot { +export interface SignerMetaData_VaultSnapshot { fileName: string; data: BrowserSyncData; } -export const GOOTI_META_DATA_KEY = { +export const SIGNER_META_DATA_KEY = { vaultSnapshots: 'vaultSnapshots', }; -export interface GootiMetaData { - syncFlow?: number; // 0 = no sync, 1 = browser sync, (future: 2 = Gooti sync, 3 = Custom sync (bring your own sync)) +export interface SignerMetaData { + syncFlow?: number; // 0 = no sync, 1 = browser sync, (future: 2 = Signer sync, 3 = Custom sync (bring your own sync)) - vaultSnapshots?: GootiMetaData_VaultSnapshot[]; + vaultSnapshots?: SignerMetaData_VaultSnapshot[]; } diff --git a/projects/common/src/public-api.ts b/projects/common/src/public-api.ts index 67eec8a..050d29f 100644 --- a/projects/common/src/public-api.ts +++ b/projects/common/src/public-api.ts @@ -19,7 +19,7 @@ export * from './lib/services/storage/storage.service'; export * from './lib/services/storage/types'; export * from './lib/services/storage/browser-sync-handler'; export * from './lib/services/storage/browser-session-handler'; -export * from './lib/services/storage/gooti-meta-handler'; +export * from './lib/services/storage/signer-meta-handler'; export * from './lib/services/logger/logger.service'; export * from './lib/services/startup/startup.service'; diff --git a/projects/firefox/custom-webpack.config.ts b/projects/firefox/custom-webpack.config.ts index d5586e0..98eabb7 100644 --- a/projects/firefox/custom-webpack.config.ts +++ b/projects/firefox/custom-webpack.config.ts @@ -6,12 +6,12 @@ module.exports = { import: 'src/background.ts', runtime: false, }, - 'gooti-extension': { - import: 'src/gooti-extension.ts', + 'plebian-signer-extension': { + import: 'src/plebian-signer-extension.ts', runtime: false, }, - 'gooti-content-script': { - import: 'src/gooti-content-script.ts', + 'plebian-signer-content-script': { + import: 'src/plebian-signer-content-script.ts', runtime: false, }, prompt: { diff --git a/projects/firefox/public/manifest.json b/projects/firefox/public/manifest.json index 1b4faa1..bbe3dcf 100644 --- a/projects/firefox/public/manifest.json +++ b/projects/firefox/public/manifest.json @@ -1,9 +1,9 @@ { "manifest_version": 3, - "name": "Gooti", + "name": "Plebian Signer", "description": "Nostr Identity Manager & Signer", "version": "0.0.4", - "homepage_url": "https://getgooti.com", + "homepage_url": "https://git.mleku.dev/mleku/plebeian-signer", "options_page": "options.html", "permissions": [ "storage" @@ -24,7 +24,7 @@ "<all_urls>" ], "js": [ - "gooti-content-script.js" + "plebian-signer-content-script.js" ], "all_frames": true } @@ -32,7 +32,7 @@ "web_accessible_resources": [ { "resources": [ - "gooti-extension.js" + "plebian-signer-extension.js" ], "matches": [ "<all_urls>" @@ -41,7 +41,7 @@ ], "browser_specific_settings": { "gecko": { - "id": "fox@getgooti.com" + "id": "plebian-signer@mleku.dev" } } } diff --git a/projects/firefox/public/options.html b/projects/firefox/public/options.html index a259a58..5206071 100644 --- a/projects/firefox/public/options.html +++ b/projects/firefox/public/options.html @@ -2,7 +2,7 @@ <html data-bs-theme="dark"> <head> - <title>Gooti - Options</title> + <title>Plebian Signer - Options</title> <link rel="stylesheet" type="text/css" href="styles.css" /> <script src="scripts.js"></script> <style> @@ -104,7 +104,7 @@ <div class="logo"> <img src="gooti.svg" alt="" /> </div> - <span class="brand-name">Gooti</span> + <span class="brand-name">Plebian Signer</span> <span>OPTIONS</span> </div> diff --git a/projects/firefox/public/prompt.html b/projects/firefox/public/prompt.html index 8d7b4d7..bb1011c 100644 --- a/projects/firefox/public/prompt.html +++ b/projects/firefox/public/prompt.html @@ -2,7 +2,7 @@ <html data-bs-theme="dark"> <head> - <title>Gooti</title> + <title>Plebian Signer</title> <link rel="stylesheet" type="text/css" href="styles.css" /> <script src="scripts.js"></script> <style> diff --git a/projects/firefox/src/app/app.component.ts b/projects/firefox/src/app/app.component.ts index 510563a..63abcb0 100644 --- a/projects/firefox/src/app/app.component.ts +++ b/projects/firefox/src/app/app.component.ts @@ -14,7 +14,7 @@ export class AppComponent implements OnInit { readonly #logger = inject(LoggerService); ngOnInit(): void { - this.#logger.initialize('Gooti Firefox Extension'); + this.#logger.initialize('Plebian Signer Firefox Extension'); this.#startup.startOver(getNewStorageServiceConfig()); } diff --git a/projects/firefox/src/app/common/data/firefox-meta-handler.ts b/projects/firefox/src/app/common/data/firefox-meta-handler.ts index 3000787..5fa8ad9 100644 --- a/projects/firefox/src/app/common/data/firefox-meta-handler.ts +++ b/projects/firefox/src/app/common/data/firefox-meta-handler.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { GootiMetaData, GootiMetaHandler } from '@common'; +import { SignerMetaData, SignerMetaHandler } from '@common'; import browser from 'webextension-polyfill'; -export class FirefoxMetaHandler extends GootiMetaHandler { +export class FirefoxMetaHandler extends SignerMetaHandler { async loadFullData(): Promise<Partial<Record<string, any>>> { const dataWithPossibleAlienProperties = await browser.storage.local.get( null @@ -20,7 +20,7 @@ export class FirefoxMetaHandler extends GootiMetaHandler { return data; } - async saveFullData(data: GootiMetaData): Promise<void> { + async saveFullData(data: SignerMetaData): Promise<void> { await browser.storage.local.set(data as Record<string, any>); console.log(data); } diff --git a/projects/firefox/src/app/common/data/get-new-storage-service-config.ts b/projects/firefox/src/app/common/data/get-new-storage-service-config.ts index ddf7ff1..29b3ee2 100644 --- a/projects/firefox/src/app/common/data/get-new-storage-service-config.ts +++ b/projects/firefox/src/app/common/data/get-new-storage-service-config.ts @@ -8,7 +8,7 @@ export const getNewStorageServiceConfig = () => { browserSessionHandler: new FirefoxSessionHandler(), browserSyncYesHandler: new FirefoxSyncYesHandler(), browserSyncNoHandler: new FirefoxSyncNoHandler(), - gootiMetaHandler: new FirefoxMetaHandler(), + signerMetaHandler: new FirefoxMetaHandler(), }; return storageConfig; diff --git a/projects/firefox/src/app/components/home/info/info.component.html b/projects/firefox/src/app/components/home/info/info.component.html index d88d1f2..e400fa0 100644 --- a/projects/firefox/src/app/components/home/info/info.component.html +++ b/projects/firefox/src/app/components/home/info/info.component.html @@ -1,19 +1,14 @@ <div class="sam-text-header"> - <span> Gooti </span> + <span> Plebian Signer </span> </div> <span>Version {{ version }}</span> <span>&nbsp;</span> -<span> Website </span> -<a href="https://getgooti.com" target="_blank">www.getgooti.com</a> - -<span>&nbsp;</span> - <span> Source code</span> -<a href="https://github.com/sam-hayes-org/gooti-extension" target="_blank"> - github.com/sam-hayes-org/gooti-extension +<a href="https://git.mleku.dev/mleku/plebeian-signer" target="_blank"> + git.mleku.dev/mleku/plebeian-signer </a> <div class="sam-flex-grow"></div> diff --git a/projects/firefox/src/app/components/home/settings/settings.component.ts b/projects/firefox/src/app/components/home/settings/settings.component.ts index 0351cc8..20def8a 100644 --- a/projects/firefox/src/app/components/home/settings/settings.component.ts +++ b/projects/firefox/src/app/components/home/settings/settings.component.ts @@ -27,7 +27,7 @@ export class SettingsComponent extends NavComponent implements OnInit { ); console.log(vault.length / 1024 + ' KB'); - switch (this.#storage.getGootiMetaHandler().gootiMetaData?.syncFlow) { + switch (this.#storage.getSignerMetaHandler().signerMetaData?.syncFlow) { case BrowserSyncFlow.NO_SYNC: this.syncFlow = 'Off'; break; @@ -55,7 +55,7 @@ export class SettingsComponent extends NavComponent implements OnInit { const jsonVault = this.#storage.exportVault(); const dateTimeString = DateHelper.dateToISOLikeButLocal(new Date()); - const fileName = `Gooti Chrome - Vault Export - ${dateTimeString}.json`; + const fileName = `Plebian Signer Firefox - Vault Export - ${dateTimeString}.json`; this.#downloadJson(jsonVault, fileName); } diff --git a/projects/firefox/src/app/components/vault-create/home/home.component.html b/projects/firefox/src/app/components/vault-create/home/home.component.html index c0ceae1..7efd0e4 100644 --- a/projects/firefox/src/app/components/vault-create/home/home.component.html +++ b/projects/firefox/src/app/components/vault-create/home/home.component.html @@ -1,5 +1,5 @@ <div class="sam-text-header"> - <span>Gooti</span> + <span>Plebian Signer</span> </div> <div class="vertically-centered"> diff --git a/projects/firefox/src/app/components/vault-create/new/new.component.html b/projects/firefox/src/app/components/vault-create/new/new.component.html index c46c2b2..d2f14b8 100644 --- a/projects/firefox/src/app/components/vault-create/new/new.component.html +++ b/projects/firefox/src/app/components/vault-create/new/new.component.html @@ -1,5 +1,5 @@ <div class="sam-text-header"> - <span>Gooti</span> + <span>Plebian Signer</span> </div> <div class="content"> diff --git a/projects/firefox/src/app/components/vault-import/vault-import.component.ts b/projects/firefox/src/app/components/vault-import/vault-import.component.ts index f35bf4d..34f467a 100644 --- a/projects/firefox/src/app/components/vault-import/vault-import.component.ts +++ b/projects/firefox/src/app/components/vault-import/vault-import.component.ts @@ -2,7 +2,7 @@ import { Component, inject, OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { BrowserSyncFlow, - GootiMetaData_VaultSnapshot, + SignerMetaData_VaultSnapshot, IconButtonComponent, NavComponent, StartupService, @@ -18,8 +18,8 @@ import { getNewStorageServiceConfig } from '../../common/data/get-new-storage-se styleUrl: './vault-import.component.scss', }) export class VaultImportComponent extends NavComponent implements OnInit { - snapshots: GootiMetaData_VaultSnapshot[] = []; - selectedSnapshot: GootiMetaData_VaultSnapshot | undefined; + snapshots: SignerMetaData_VaultSnapshot[] = []; + selectedSnapshot: SignerMetaData_VaultSnapshot | undefined; syncText: string | undefined; readonly #storage = inject(StorageService); @@ -51,11 +51,11 @@ export class VaultImportComponent extends NavComponent implements OnInit { async #loadData() { this.snapshots = ( - this.#storage.getGootiMetaHandler().gootiMetaData?.vaultSnapshots ?? [] + this.#storage.getSignerMetaHandler().signerMetaData?.vaultSnapshots ?? [] ).sortBy((x) => x.fileName, 'desc'); const syncFlow = - this.#storage.getGootiMetaHandler().gootiMetaData?.syncFlow; + this.#storage.getSignerMetaHandler().signerMetaData?.syncFlow; switch (syncFlow) { case BrowserSyncFlow.BROWSER_SYNC: diff --git a/projects/firefox/src/app/components/vault-login/vault-login.component.html b/projects/firefox/src/app/components/vault-login/vault-login.component.html index b01b7b8..2eab33d 100644 --- a/projects/firefox/src/app/components/vault-login/vault-login.component.html +++ b/projects/firefox/src/app/components/vault-login/vault-login.component.html @@ -1,5 +1,5 @@ <div class="sam-text-header"> - <span class="brand">Gooti</span> + <span class="brand">Plebian Signer</span> </div> <div class="content-login-vault"> diff --git a/projects/firefox/src/app/components/welcome/welcome.component.html b/projects/firefox/src/app/components/welcome/welcome.component.html index c44cd67..d1b4030 100644 --- a/projects/firefox/src/app/components/welcome/welcome.component.html +++ b/projects/firefox/src/app/components/welcome/welcome.component.html @@ -1,9 +1,9 @@ <div class="sam-text-header sam-mb-2"> - <span>Gooti Setup - Sync Preference</span> + <span>Plebian Signer Setup - Sync Preference</span> </div> <span class="sam-text-muted sam-text-md sam-text-align-center2"> - Gooti always encrypts sensitive data like private keys and site permissions + Plebian Signer always encrypts sensitive data like private keys and site permissions independent of the chosen sync mode. </span> diff --git a/projects/firefox/src/background-common.ts b/projects/firefox/src/background-common.ts index bad95e6..5064aaa 100644 --- a/projects/firefox/src/background-common.ts +++ b/projects/firefox/src/background-common.ts @@ -4,7 +4,7 @@ import { BrowserSyncData, BrowserSyncFlow, CryptoHelper, - GootiMetaData, + SignerMetaData, Identity_DECRYPTED, Nip07Method, Nip07MethodPolicy, @@ -18,7 +18,7 @@ import browser from 'webextension-polyfill'; export const debug = function (message: any) { const dateString = new Date().toISOString(); - console.log(`[Gooti - ${dateString}]: ${JSON.stringify(message)}`); + console.log(`[Plebian Signer - ${dateString}]: ${JSON.stringify(message)}`); }; export type PromptResponse = @@ -52,17 +52,17 @@ export const getBrowserSessionData = async function (): Promise< export const getBrowserSyncData = async function (): Promise< BrowserSyncData | undefined > { - const gootiMetaHandler = new FirefoxMetaHandler(); - const gootiMetaData = - (await gootiMetaHandler.loadFullData()) as GootiMetaData; + const signerMetaHandler = new FirefoxMetaHandler(); + const signerMetaData = + (await signerMetaHandler.loadFullData()) as SignerMetaData; let browserSyncData: BrowserSyncData | undefined; - if (gootiMetaData.syncFlow === BrowserSyncFlow.NO_SYNC) { + if (signerMetaData.syncFlow === BrowserSyncFlow.NO_SYNC) { browserSyncData = (await browser.storage.local.get( null )) as unknown as BrowserSyncData; - } else if (gootiMetaData.syncFlow === BrowserSyncFlow.BROWSER_SYNC) { + } else if (signerMetaData.syncFlow === BrowserSyncFlow.BROWSER_SYNC) { browserSyncData = (await browser.storage.sync.get( null )) as unknown as BrowserSyncData; @@ -74,13 +74,13 @@ export const getBrowserSyncData = async function (): Promise< export const savePermissionsToBrowserSyncStorage = async function ( permissions: Permission_ENCRYPTED[] ): Promise<void> { - const gootiMetaHandler = new FirefoxMetaHandler(); - const gootiMetaData = - (await gootiMetaHandler.loadFullData()) as GootiMetaData; + const signerMetaHandler = new FirefoxMetaHandler(); + const signerMetaData = + (await signerMetaHandler.loadFullData()) as SignerMetaData; - if (gootiMetaData.syncFlow === BrowserSyncFlow.NO_SYNC) { + if (signerMetaData.syncFlow === BrowserSyncFlow.NO_SYNC) { await browser.storage.local.set({ permissions }); - } else if (gootiMetaData.syncFlow === BrowserSyncFlow.BROWSER_SYNC) { + } else if (signerMetaData.syncFlow === BrowserSyncFlow.BROWSER_SYNC) { await browser.storage.sync.set({ permissions }); } }; diff --git a/projects/firefox/src/background.ts b/projects/firefox/src/background.ts index 6c33232..db830bb 100644 --- a/projects/firefox/src/background.ts +++ b/projects/firefox/src/background.ts @@ -51,7 +51,7 @@ browser.runtime.onMessage.addListener(async (message /*, sender*/) => { const browserSessionData = await getBrowserSessionData(); if (!browserSessionData) { - throw new Error('Gooti vault not unlocked by the user.'); + throw new Error('Plebian Signer vault not unlocked by the user.'); } const currentIdentity = browserSessionData.identities.find( diff --git a/projects/firefox/src/index.html b/projects/firefox/src/index.html index 090680b..d45b94d 100644 --- a/projects/firefox/src/index.html +++ b/projects/firefox/src/index.html @@ -2,7 +2,7 @@ <html lang="en" data-bs-theme="dark"> <head> <meta charset="utf-8"> - <title>Gooti</title> + <title>Plebian Signer</title> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1"> </head> diff --git a/projects/firefox/src/options.ts b/projects/firefox/src/options.ts index 02bf5df..3cb22e1 100644 --- a/projects/firefox/src/options.ts +++ b/projects/firefox/src/options.ts @@ -1,7 +1,7 @@ import { BrowserSyncData, - GOOTI_META_DATA_KEY, - GootiMetaData_VaultSnapshot, + SIGNER_META_DATA_KEY, + SignerMetaData_VaultSnapshot, } from '@common'; import './app/common/extensions/array'; import browser from 'webextension-polyfill'; @@ -10,13 +10,13 @@ import browser from 'webextension-polyfill'; // Functions // -async function getGootiMetaDataVaultSnapshots(): Promise< - GootiMetaData_VaultSnapshot[] +async function getSignerMetaDataVaultSnapshots(): Promise< + SignerMetaData_VaultSnapshot[] > { const data = (await browser.storage.local.get( - GOOTI_META_DATA_KEY.vaultSnapshots + SIGNER_META_DATA_KEY.vaultSnapshots )) as { - vaultSnapshots?: GootiMetaData_VaultSnapshot[]; + vaultSnapshots?: SignerMetaData_VaultSnapshot[]; }; return typeof data.vaultSnapshots === 'undefined' @@ -24,15 +24,15 @@ async function getGootiMetaDataVaultSnapshots(): Promise< : data.vaultSnapshots.sortBy((x) => x.fileName, 'desc'); } -async function setGootiMetaDataVaultSnapshots( - vaultSnapshots: GootiMetaData_VaultSnapshot[] +async function setSignerMetaDataVaultSnapshots( + vaultSnapshots: SignerMetaData_VaultSnapshot[] ): Promise<void> { await browser.storage.local.set({ vaultSnapshots, }); } -function rebuildSnapshotsList(snapshots: GootiMetaData_VaultSnapshot[]) { +function rebuildSnapshotsList(snapshots: SignerMetaData_VaultSnapshot[]) { const ul = document.getElementById('snapshotsList'); if (!ul) { return; @@ -77,7 +77,7 @@ document.addEventListener('DOMContentLoaded', async () => { ) as HTMLInputElement; deleteSnapshotsButton?.addEventListener('click', async () => { - await setGootiMetaDataVaultSnapshots([]); + await setSignerMetaDataVaultSnapshots([]); rebuildSnapshotsList([]); }); @@ -92,9 +92,9 @@ document.addEventListener('DOMContentLoaded', async () => { } try { - const existingSnapshots = await getGootiMetaDataVaultSnapshots(); + const existingSnapshots = await getSignerMetaDataVaultSnapshots(); - const newSnapshots: GootiMetaData_VaultSnapshot[] = []; + const newSnapshots: SignerMetaData_VaultSnapshot[] = []; for (const file of files) { const text = await file.text(); const vault = JSON.parse(text) as BrowserSyncData; @@ -116,7 +116,7 @@ document.addEventListener('DOMContentLoaded', async () => { ); // Persist the new snapshots to the local storage - await setGootiMetaDataVaultSnapshots(snapshots); + await setSignerMetaDataVaultSnapshots(snapshots); // rebuildSnapshotsList(snapshots); @@ -125,6 +125,6 @@ document.addEventListener('DOMContentLoaded', async () => { } }); - const snapshots = await getGootiMetaDataVaultSnapshots(); + const snapshots = await getSignerMetaDataVaultSnapshots(); rebuildSnapshotsList(snapshots); }); diff --git a/projects/firefox/src/gooti-content-script.ts b/projects/firefox/src/plebian-signer-content-script.ts similarity index 85% rename from projects/firefox/src/gooti-content-script.ts rename to projects/firefox/src/plebian-signer-content-script.ts index 6044d1a..bbc1496 100644 --- a/projects/firefox/src/gooti-content-script.ts +++ b/projects/firefox/src/plebian-signer-content-script.ts @@ -7,7 +7,7 @@ import { BackgroundRequestMessage } from './background-common'; const script = document.createElement('script'); script.setAttribute('async', 'false'); script.setAttribute('type', 'text/javascript'); -script.setAttribute('src', browser.runtime.getURL('gooti-extension.js')); +script.setAttribute('src', browser.runtime.getURL('plebian-signer-extension.js')); (document.head || document.documentElement).appendChild(script); // listen for messages from that script @@ -18,7 +18,7 @@ window.addEventListener('message', async (message) => { if (message.source !== window) return; if (!message.data) return; if (!message.data.params) return; - if (message.data.ext !== 'gooti') return; + if (message.data.ext !== 'plebian-signer') return; // pass on to background let response; @@ -36,7 +36,7 @@ window.addEventListener('message', async (message) => { // return response window.postMessage( - { id: message.data.id, ext: 'gooti', response }, + { id: message.data.id, ext: 'plebian-signer', response }, message.origin ); }); diff --git a/projects/chrome/src/gooti-extension.ts b/projects/firefox/src/plebian-signer-extension.ts similarity index 97% rename from projects/chrome/src/gooti-extension.ts rename to projects/firefox/src/plebian-signer-extension.ts index 35f90a5..9dacc9c 100644 --- a/projects/chrome/src/gooti-extension.ts +++ b/projects/firefox/src/plebian-signer-extension.ts @@ -25,7 +25,7 @@ class Messenger { window.postMessage( { id, - ext: 'gooti', + ext: 'plebian-signer', method, params, }, @@ -41,7 +41,7 @@ class Messenger { !message.data || message.data.response === null || message.data.response === undefined || - message.data.ext !== 'gooti' || + message.data.ext !== 'plebian-signer' || !this.#requests.has(message.data.id) ) { return; diff --git a/projects/firefox/tsconfig.app.json b/projects/firefox/tsconfig.app.json index 5e747f7..c6b574a 100644 --- a/projects/firefox/tsconfig.app.json +++ b/projects/firefox/tsconfig.app.json @@ -9,8 +9,8 @@ "files": [ "src/main.ts", "src/background.ts", - "src/gooti-extension.ts", - "src/gooti-content-script.ts", + "src/plebian-signer-extension.ts", + "src/plebian-signer-content-script.ts", "src/prompt.ts", "src/options.ts" ],