From de807b6d44f6264b46043cfec844a1f2618a9a52 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 00:59:56 +0000 Subject: [PATCH 01/12] Initial plan From e537e478665eec7d704ff7994d7b22830250586e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 01:01:24 +0000 Subject: [PATCH 02/12] Fix createMemoizedReactive to use dependencies as cache key Co-authored-by: spe1020 <140115230+spe1020@users.noreply.github.com> --- src/lib/memoization.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lib/memoization.ts b/src/lib/memoization.ts index ad11b92..eecea19 100644 --- a/src/lib/memoization.ts +++ b/src/lib/memoization.ts @@ -71,11 +71,10 @@ export function createMemoizedReactive( dependencies: any[], maxCacheSize: number = 50 ): () => T { - void dependencies; - const memoized = memoize(computeFn, maxCacheSize); + const memoized = memoize((...deps: any[]) => computeFn(), maxCacheSize); return () => { - return memoized(); + return memoized(...dependencies); }; } From 48826ec79f80212a3e3a7300184f109dde66636d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 01:02:23 +0000 Subject: [PATCH 03/12] Initial plan From 644d31b2691c4a62d8ea0e0d208fd8fe905b8c1d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 01:02:42 +0000 Subject: [PATCH 04/12] Add documentation note about dependency capture behavior Co-authored-by: spe1020 <140115230+spe1020@users.noreply.github.com> --- src/lib/memoization.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/memoization.ts b/src/lib/memoization.ts index eecea19..f966705 100644 --- a/src/lib/memoization.ts +++ b/src/lib/memoization.ts @@ -65,6 +65,8 @@ export function memoize any>( /** * Create a memoized reactive statement for Svelte + * Note: This function captures dependencies at creation time. For dynamic dependencies, + * recreate the memoized function when dependencies change. */ export function createMemoizedReactive( computeFn: () => T, From 386f1ffb1fd3ddef1859d75ab8f157533ac63f06 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 01:11:11 +0000 Subject: [PATCH 05/12] Add error handling to grocery list creation Co-authored-by: spe1020 <140115230+spe1020@users.noreply.github.com> --- src/lib/stores/groceryStore.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/lib/stores/groceryStore.ts b/src/lib/stores/groceryStore.ts index a61bd5d..70d5a23 100644 --- a/src/lib/stores/groceryStore.ts +++ b/src/lib/stores/groceryStore.ts @@ -218,15 +218,20 @@ function createGroceryStore() { // Add to local state immediately update(s => ({ ...s, - lists: [newList, ...s.lists] + lists: [newList, ...s.lists], + error: null })); // Save to Nostr (immediately, no debounce for new lists) try { await saveGroceryList(newList); - update(s => ({ ...s, lastSaved: Date.now() })); + update(s => ({ ...s, lastSaved: Date.now(), error: null })); } catch (error) { console.error('[GroceryStore] Failed to save new list:', error); + update(s => ({ + ...s, + error: error instanceof Error ? error.message : 'Failed to create grocery list' + })); // Keep in local state even if save fails - will retry on next change } From cc3034458838568ae1da5eb5800b6bbd2c33021b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 01:12:32 +0000 Subject: [PATCH 06/12] Remove redundant error clearing in addList success path Co-authored-by: spe1020 <140115230+spe1020@users.noreply.github.com> --- src/lib/stores/groceryStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/stores/groceryStore.ts b/src/lib/stores/groceryStore.ts index 70d5a23..7e56f3c 100644 --- a/src/lib/stores/groceryStore.ts +++ b/src/lib/stores/groceryStore.ts @@ -225,7 +225,7 @@ function createGroceryStore() { // Save to Nostr (immediately, no debounce for new lists) try { await saveGroceryList(newList); - update(s => ({ ...s, lastSaved: Date.now(), error: null })); + update(s => ({ ...s, lastSaved: Date.now() })); } catch (error) { console.error('[GroceryStore] Failed to save new list:', error); update(s => ({ From c6d3e38bb6ade95c5132d50c752112ea5060c2e6 Mon Sep 17 00:00:00 2001 From: spe1020 Date: Tue, 17 Feb 2026 12:17:35 -0500 Subject: [PATCH 07/12] fix: move Lightning invoice metadata from in-memory Map to Cloudflare KV In-memory store caused intermittent membership payment failures when Strike webhooks or client polling hit a different Workers isolate. Now uses GATED_CONTENT KV namespace with inv:/invhash: key prefixes and 2-hour TTL. Keeps in-memory fallback for local dev. Co-Authored-By: Claude Opus 4.6 --- src/lib/invoiceMetadataStore.server.ts | 139 ++++++++++-------- .../create-lightning-invoice/+server.ts | 6 +- .../verify-lightning-payment/+server.ts | 5 +- .../create-lightning-invoice/+server.ts | 6 +- .../api/membership/strike-webhook/+server.ts | 3 +- .../verify-lightning-payment/+server.ts | 5 +- 6 files changed, 97 insertions(+), 67 deletions(-) diff --git a/src/lib/invoiceMetadataStore.server.ts b/src/lib/invoiceMetadataStore.server.ts index 45d1e68..e528fd5 100644 --- a/src/lib/invoiceMetadataStore.server.ts +++ b/src/lib/invoiceMetadataStore.server.ts @@ -1,13 +1,14 @@ /** - * Invoice Metadata Store (server-side, in-memory with TTL) + * Invoice Metadata Store (server-side, Cloudflare KV with in-memory dev fallback) * * Maps Strike receiveRequestId to membership metadata (pubkey, tier, period) * so that webhooks and verification endpoints can match payments to users. * - * Entries expire after 2 hours (Lightning invoices typically expire in 1 hour). + * KV key scheme: + * inv:{receiveRequestId} → full InvoiceMetadata JSON (TTL 2 hours) + * invhash:{paymentHash} → receiveRequestId string (TTL 2 hours) * - * NOTE: This is an in-memory store. It works for single-instance deployments. - * For multi-instance or serverless deployments, replace with a database or KV store. + * In dev (no KV binding), falls back to an in-memory Map with expiry-on-read. */ export interface InvoiceMetadata { @@ -18,99 +19,121 @@ export interface InvoiceMetadata { createdAt: number; } -const ENTRY_TTL_MS = 2 * 60 * 60 * 1000; // 2 hours -const CLEANUP_INTERVAL_MS = 10 * 60 * 1000; // Clean up every 10 minutes +/** Matches the shape of the GATED_CONTENT KV binding. */ +export type InvoiceKV = { + get(key: string, type?: 'text' | 'json'): Promise; + put(key: string, value: string, options?: { expirationTtl?: number }): Promise; + delete(key: string): Promise; +} | null | undefined; -const store = new Map(); +const KV_TTL_SECONDS = 7200; // 2 hours +const ENTRY_TTL_MS = KV_TTL_SECONDS * 1000; -// Also index by paymentHash for client-side verification lookups -const paymentHashIndex = new Map(); // paymentHash -> receiveRequestId +// ── Dev-only in-memory fallback ────────────────────────────────────── +const memStore = new Map(); +const memHashIndex = new Map(); // paymentHash → receiveRequestId -// Periodic cleanup of expired entries -let cleanupTimer: ReturnType | null = null; - -function ensureCleanupRunning() { - if (cleanupTimer) return; - cleanupTimer = setInterval(() => { - const now = Date.now(); - for (const [id, entry] of store) { - if (now - entry.createdAt > ENTRY_TTL_MS) { - store.delete(id); - } - } - // Clean stale paymentHash index entries - for (const [hash, receiveId] of paymentHashIndex) { - if (!store.has(receiveId)) { - paymentHashIndex.delete(hash); - } - } - }, CLEANUP_INTERVAL_MS); - // Don't prevent process exit - if (cleanupTimer && typeof cleanupTimer === 'object' && 'unref' in cleanupTimer) { - cleanupTimer.unref(); - } +function memIsExpired(entry: InvoiceMetadata): boolean { + return Date.now() - entry.createdAt > ENTRY_TTL_MS; } +// ── Key helpers ────────────────────────────────────────────────────── +function invKey(receiveRequestId: string) { return `inv:${receiveRequestId}`; } +function hashKey(paymentHash: string) { return `invhash:${paymentHash}`; } + /** * Store metadata for a newly created invoice. * Call this when creating a Lightning invoice via Strike API. */ -export function storeInvoiceMetadata( +export async function storeInvoiceMetadata( + kv: InvoiceKV, receiveRequestId: string, metadata: Omit, paymentHash?: string -): void { - ensureCleanupRunning(); - +): Promise { const entry: InvoiceMetadata = { ...metadata, receiveRequestId, createdAt: Date.now(), }; - store.set(receiveRequestId, entry); - if (paymentHash) { - paymentHashIndex.set(paymentHash, receiveRequestId); + if (kv) { + const opts = { expirationTtl: KV_TTL_SECONDS }; + await kv.put(invKey(receiveRequestId), JSON.stringify(entry), opts); + if (paymentHash) { + await kv.put(hashKey(paymentHash), receiveRequestId, opts); + } + } else { + // Dev fallback + memStore.set(receiveRequestId, entry); + if (paymentHash) { + memHashIndex.set(paymentHash, receiveRequestId); + } } } /** * Look up metadata by receiveRequestId (used by webhooks). */ -export function getInvoiceMetadata(receiveRequestId: string): InvoiceMetadata | null { - const entry = store.get(receiveRequestId); - if (!entry) return null; +export async function getInvoiceMetadata( + kv: InvoiceKV, + receiveRequestId: string +): Promise { + if (kv) { + const raw = await kv.get(invKey(receiveRequestId), 'text') as string | null; + if (!raw) return null; + return JSON.parse(raw) as InvoiceMetadata; + } - // Check expiry - if (Date.now() - entry.createdAt > ENTRY_TTL_MS) { - store.delete(receiveRequestId); + // Dev fallback + const entry = memStore.get(receiveRequestId); + if (!entry) return null; + if (memIsExpired(entry)) { + memStore.delete(receiveRequestId); return null; } - return entry; } /** * Look up metadata by paymentHash (used by client-side verify endpoint). */ -export function getInvoiceMetadataByPaymentHash(paymentHash: string): InvoiceMetadata | null { - const receiveRequestId = paymentHashIndex.get(paymentHash); +export async function getInvoiceMetadataByPaymentHash( + kv: InvoiceKV, + paymentHash: string +): Promise { + if (kv) { + const receiveRequestId = await kv.get(hashKey(paymentHash), 'text') as string | null; + if (!receiveRequestId) return null; + return getInvoiceMetadata(kv, receiveRequestId); + } + + // Dev fallback + const receiveRequestId = memHashIndex.get(paymentHash); if (!receiveRequestId) return null; - return getInvoiceMetadata(receiveRequestId); + return getInvoiceMetadata(kv, receiveRequestId); } /** * Remove metadata after successful processing (optional cleanup). */ -export function removeInvoiceMetadata(receiveRequestId: string): void { - const entry = store.get(receiveRequestId); - if (entry) { - store.delete(receiveRequestId); - // Also clean paymentHash index - for (const [hash, id] of paymentHashIndex) { - if (id === receiveRequestId) { - paymentHashIndex.delete(hash); - break; +export async function removeInvoiceMetadata( + kv: InvoiceKV, + receiveRequestId: string +): Promise { + if (kv) { + // We don't have the paymentHash readily available, but KV TTL will + // clean up the hash entry. Delete the primary key immediately. + await kv.delete(invKey(receiveRequestId)); + } else { + const entry = memStore.get(receiveRequestId); + if (entry) { + memStore.delete(receiveRequestId); + for (const [hash, id] of memHashIndex) { + if (id === receiveRequestId) { + memHashIndex.delete(hash); + break; + } } } } diff --git a/src/routes/api/genesis/create-lightning-invoice/+server.ts b/src/routes/api/genesis/create-lightning-invoice/+server.ts index 7ca2112..a2ebcaa 100644 --- a/src/routes/api/genesis/create-lightning-invoice/+server.ts +++ b/src/routes/api/genesis/create-lightning-invoice/+server.ts @@ -112,12 +112,14 @@ export const POST: RequestHandler = async ({ request, platform }) => { console.log('[Founders Club Lightning] Invoice created:', { receiveRequestId, amountSats, - pubkey: pubkey.substring(0, 16) + '...', + pubkey, }); // Store metadata so webhook and verify endpoints can match payment to user // Genesis uses 'pro' tier internally for NIP-05 claiming - storeInvoiceMetadata( + const kv = platform?.env?.GATED_CONTENT ?? null; + await storeInvoiceMetadata( + kv, receiveRequestId, { pubkey, tier: 'pro', period: 'annual' }, paymentHash diff --git a/src/routes/api/genesis/verify-lightning-payment/+server.ts b/src/routes/api/genesis/verify-lightning-payment/+server.ts index 6ad1b86..696cdee 100644 --- a/src/routes/api/genesis/verify-lightning-payment/+server.ts +++ b/src/routes/api/genesis/verify-lightning-payment/+server.ts @@ -48,9 +48,10 @@ export const POST: RequestHandler = async ({ request, platform }) => { } // Look up stored metadata to verify the request + const kv = platform?.env?.GATED_CONTENT ?? null; const metadata = receiveRequestId - ? getInvoiceMetadata(receiveRequestId) - : getInvoiceMetadataByPaymentHash(paymentHash); + ? await getInvoiceMetadata(kv, receiveRequestId) + : await getInvoiceMetadataByPaymentHash(kv, paymentHash); if (!metadata) { return json( diff --git a/src/routes/api/membership/create-lightning-invoice/+server.ts b/src/routes/api/membership/create-lightning-invoice/+server.ts index 5fa281a..5112ce3 100644 --- a/src/routes/api/membership/create-lightning-invoice/+server.ts +++ b/src/routes/api/membership/create-lightning-invoice/+server.ts @@ -156,11 +156,13 @@ export const POST: RequestHandler = async ({ request, platform }) => { amountSats, tier, period, - pubkey: pubkey.substring(0, 16) + '...', + pubkey, }); // Store invoice metadata so webhook and verify endpoints can match payment to user - storeInvoiceMetadata( + const kv = platform?.env?.GATED_CONTENT ?? null; + await storeInvoiceMetadata( + kv, receiveRequestId, { pubkey, tier: tier as 'cook' | 'pro', period: period as 'annual' | 'monthly' }, paymentHash diff --git a/src/routes/api/membership/strike-webhook/+server.ts b/src/routes/api/membership/strike-webhook/+server.ts index 7259de4..6a691ed 100644 --- a/src/routes/api/membership/strike-webhook/+server.ts +++ b/src/routes/api/membership/strike-webhook/+server.ts @@ -73,7 +73,8 @@ export const POST: RequestHandler = async ({ request, platform }) => { } // Look up stored metadata for this receive request - const metadata = getInvoiceMetadata(receiveRequestId); + const kv = platform?.env?.GATED_CONTENT ?? null; + const metadata = await getInvoiceMetadata(kv, receiveRequestId); if (!metadata) { console.warn('[Strike Webhook] No metadata found for receiveRequestId:', receiveRequestId); diff --git a/src/routes/api/membership/verify-lightning-payment/+server.ts b/src/routes/api/membership/verify-lightning-payment/+server.ts index b8b15ec..4cd6ab6 100644 --- a/src/routes/api/membership/verify-lightning-payment/+server.ts +++ b/src/routes/api/membership/verify-lightning-payment/+server.ts @@ -70,9 +70,10 @@ export const POST: RequestHandler = async ({ request, platform }) => { } // Look up stored metadata to verify the request matches what was created + const kv = platform?.env?.GATED_CONTENT ?? null; const metadata = receiveRequestId - ? getInvoiceMetadata(receiveRequestId) - : getInvoiceMetadataByPaymentHash(paymentHash); + ? await getInvoiceMetadata(kv, receiveRequestId) + : await getInvoiceMetadataByPaymentHash(kv, paymentHash); if (!metadata) { return json( From c62154d164bce19da7a410a4edb5e0110c9b7080 Mon Sep 17 00:00:00 2001 From: Seth <140115230+spe1020@users.noreply.github.com> Date: Tue, 17 Feb 2026 12:26:09 -0500 Subject: [PATCH 08/12] Update src/routes/api/membership/strike-webhook/+server.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/routes/api/membership/strike-webhook/+server.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/routes/api/membership/strike-webhook/+server.ts b/src/routes/api/membership/strike-webhook/+server.ts index 6a691ed..3082c56 100644 --- a/src/routes/api/membership/strike-webhook/+server.ts +++ b/src/routes/api/membership/strike-webhook/+server.ts @@ -74,6 +74,16 @@ export const POST: RequestHandler = async ({ request, platform }) => { // Look up stored metadata for this receive request const kv = platform?.env?.GATED_CONTENT ?? null; + if (!kv && env.NODE_ENV === 'production') { + console.error('[Strike Webhook] GATED_CONTENT KV binding is missing in production environment'); + return json( + { + error: 'Service unavailable', + message: 'GATED_CONTENT KV namespace is not configured' + }, + { status: 503 } + ); + } const metadata = await getInvoiceMetadata(kv, receiveRequestId); if (!metadata) { From ba07d005545bdf655433897f8ce0c77a61f20188 Mon Sep 17 00:00:00 2001 From: Seth <140115230+spe1020@users.noreply.github.com> Date: Tue, 17 Feb 2026 12:27:51 -0500 Subject: [PATCH 09/12] Update src/routes/api/membership/create-lightning-invoice/+server.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../api/membership/create-lightning-invoice/+server.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/routes/api/membership/create-lightning-invoice/+server.ts b/src/routes/api/membership/create-lightning-invoice/+server.ts index 5112ce3..44b3c80 100644 --- a/src/routes/api/membership/create-lightning-invoice/+server.ts +++ b/src/routes/api/membership/create-lightning-invoice/+server.ts @@ -161,6 +161,14 @@ export const POST: RequestHandler = async ({ request, platform }) => { // Store invoice metadata so webhook and verify endpoints can match payment to user const kv = platform?.env?.GATED_CONTENT ?? null; + // In production, require the GATED_CONTENT KV binding to avoid falling back to in-memory storage + if (!kv && env.NODE_ENV === 'production') { + console.error('[Membership Lightning] GATED_CONTENT KV binding is missing in production'); + return json( + { error: 'Service temporarily unavailable' }, + { status: 503 } + ); + } await storeInvoiceMetadata( kv, receiveRequestId, From 4fc1cd5041981c769bdcc6f86ed0327e86664705 Mon Sep 17 00:00:00 2001 From: Seth <140115230+spe1020@users.noreply.github.com> Date: Tue, 17 Feb 2026 12:28:18 -0500 Subject: [PATCH 10/12] Update src/routes/api/genesis/create-lightning-invoice/+server.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../api/genesis/create-lightning-invoice/+server.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/routes/api/genesis/create-lightning-invoice/+server.ts b/src/routes/api/genesis/create-lightning-invoice/+server.ts index a2ebcaa..ce7b29e 100644 --- a/src/routes/api/genesis/create-lightning-invoice/+server.ts +++ b/src/routes/api/genesis/create-lightning-invoice/+server.ts @@ -118,6 +118,14 @@ export const POST: RequestHandler = async ({ request, platform }) => { // Store metadata so webhook and verify endpoints can match payment to user // Genesis uses 'pro' tier internally for NIP-05 claiming const kv = platform?.env?.GATED_CONTENT ?? null; + const isProd = env.NODE_ENV === 'production'; + if (!kv && isProd) { + console.error('[Founders Club Lightning] GATED_CONTENT KV binding missing in production'); + return json( + { error: 'Service temporarily unavailable' }, + { status: 503 } + ); + } await storeInvoiceMetadata( kv, receiveRequestId, From 96856fce03439990254548be0f1a5ea90c44230b Mon Sep 17 00:00:00 2001 From: Seth <140115230+spe1020@users.noreply.github.com> Date: Tue, 17 Feb 2026 12:28:54 -0500 Subject: [PATCH 11/12] Update src/lib/invoiceMetadataStore.server.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/lib/invoiceMetadataStore.server.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/lib/invoiceMetadataStore.server.ts b/src/lib/invoiceMetadataStore.server.ts index e528fd5..fb8f506 100644 --- a/src/lib/invoiceMetadataStore.server.ts +++ b/src/lib/invoiceMetadataStore.server.ts @@ -82,7 +82,13 @@ export async function getInvoiceMetadata( if (kv) { const raw = await kv.get(invKey(receiveRequestId), 'text') as string | null; if (!raw) return null; - return JSON.parse(raw) as InvoiceMetadata; + try { + return JSON.parse(raw) as InvoiceMetadata; + } catch { + // Treat invalid/corrupted data as a cache miss and clean up the bad key. + await kv.delete(invKey(receiveRequestId)); + return null; + } } // Dev fallback From 97e73fc5dd25c9cdb75231622dddc6b706f29c8c Mon Sep 17 00:00:00 2001 From: Seth <140115230+spe1020@users.noreply.github.com> Date: Tue, 17 Feb 2026 12:30:02 -0500 Subject: [PATCH 12/12] Update src/routes/api/membership/verify-lightning-payment/+server.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../api/membership/verify-lightning-payment/+server.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/routes/api/membership/verify-lightning-payment/+server.ts b/src/routes/api/membership/verify-lightning-payment/+server.ts index 4cd6ab6..7d28626 100644 --- a/src/routes/api/membership/verify-lightning-payment/+server.ts +++ b/src/routes/api/membership/verify-lightning-payment/+server.ts @@ -71,6 +71,14 @@ export const POST: RequestHandler = async ({ request, platform }) => { // Look up stored metadata to verify the request matches what was created const kv = platform?.env?.GATED_CONTENT ?? null; + + // In production, missing KV binding is a misconfiguration, not a 404 + if (!kv && env.NODE_ENV === 'production') { + return json( + { error: 'Service unavailable: GATED_CONTENT KV binding is missing' }, + { status: 503 } + ); + } const metadata = receiveRequestId ? await getInvoiceMetadata(kv, receiveRequestId) : await getInvoiceMetadataByPaymentHash(kv, paymentHash);